From e0f01b159e3e1fa1aa7f65106ec9cff86a3443fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Thu, 24 Apr 2025 20:23:11 +0800 Subject: [PATCH 01/79] fix: Ruff --- src/plugins/heartFC_chat/heartFC_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index 464e94e91..da43c334f 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -78,7 +78,7 @@ class HeartFCGenerator: ) -> str: info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - with Timer() as t_build_prompt: + with Timer() as _build_prompt: prompt = await prompt_builder.build_prompt( build_mode="focus", reason=reason, From 996276ad1e127d3a5a4a72b8404434855bd6a0a1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Apr 2025 12:23:27 +0000 Subject: [PATCH 02/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_chat.py | 10 +++++----- .../heartFC_chat/heartflow_prompt_builder.py | 13 +++++++------ src/plugins/heartFC_chat/normal_chat.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 8735ff7df..b87ad6529 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -595,12 +595,12 @@ class HeartFChatting: self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any] ) -> str: """构建 Planner LLM 的提示词""" - + # 准备结构化信息块 structured_info_block = "" if structured_info: structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" - + # 准备聊天内容块 chat_content_block = "" if observed_messages_str: @@ -609,14 +609,14 @@ class HeartFChatting: chat_content_block += "\n---" else: chat_content_block = "当前没有观察到新的聊天内容。\n" - + # 准备当前思维块 current_mind_block = "" if current_mind: current_mind_block = f"\n---\n{current_mind}\n---\n\n" else: current_mind_block = " [没有特别的想法] \n\n" - + # 获取提示词模板并填充数据 prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( bot_name=global_config.BOT_NICKNAME, @@ -624,7 +624,7 @@ class HeartFChatting: chat_content_block=chat_content_block, current_mind_block=current_mind_block, ) - + return prompt # --- 回复器 (Replier) 的定义 --- # diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index e9148c4fc..73ad91293 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -36,7 +36,7 @@ def init_prompt(): {moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) - + # Planner提示词 Prompt( """你的名字是 {bot_name}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。 @@ -56,7 +56,7 @@ def init_prompt(): 必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。""", "planner_prompt", ) - + Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") @@ -116,13 +116,14 @@ class PromptBuilder: elif build_mode == "focus": return await self._build_prompt_focus( - reason, current_mind_info, structured_info, chat_stream, + reason, + current_mind_info, + structured_info, + chat_stream, ) return None - async def _build_prompt_focus( - self, reason, current_mind_info, structured_info, chat_stream - ) -> tuple[str, str]: + async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1) prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1) diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index a3aaf3a0b..fc0c750b3 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -402,7 +402,7 @@ class NormalChat: # 确保任务状态更新,即使等待出错 (回调函数也会尝试更新) if self._chat_task is task: self._chat_task = None - + # 清理所有未处理的思考消息 try: container = await message_manager.get_container(self.stream_id) From 4f6ef7b0a754220aae8873b800a87d8b14a6d341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Thu, 24 Apr 2025 21:12:32 +0800 Subject: [PATCH 03/79] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=BA=A6?= =?UTF-8?q?=E9=BA=A6logo=E6=98=BE=E7=A4=BA=E6=AF=94=E4=BE=8B=E4=B8=8D?= =?UTF-8?q?=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7eca22601..df5c1c94e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

- Logo + Logo
From af08ef9b04058b9750d6c15f238e57182ca7f7ad Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 23:45:13 +0800 Subject: [PATCH 04/79] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E7=9A=84=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E5=8C=85=E7=B3=BB=E7=BB=9F=EF=BC=8C=E8=A1=A8=E6=83=85?= =?UTF-8?q?=E5=8C=85=E6=88=90=E4=B8=BA=E7=B1=BB=EF=BC=8C=E4=B8=94=E5=90=AB?= =?UTF-8?q?=E4=B9=89=E6=9B=B4=E4=B8=B0=E5=AF=8C=EF=BC=8C=E5=8F=91=E9=80=81?= =?UTF-8?q?=E6=9B=B4=E5=BF=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llm_tool_benchmark_results.json | 71 -- .../tool_call_benchmark.py | 0 src/common/logger.py | 2 +- src/config/config.py | 14 +- src/heart_flow/sub_mind.py | 9 +- src/main.py | 3 +- src/plugins/__init__.py | 2 +- src/plugins/chat/__init__.py | 2 +- src/plugins/chat/emoji_manager.py | 595 ------------- src/plugins/chat/message.py | 14 +- src/plugins/chat/utils_image.py | 6 +- src/plugins/emoji_system/emoji_manager.py | 794 ++++++++++++++++++ src/plugins/heartFC_chat/heartFC_chat.py | 2 +- .../heartFC_chat/heartflow_prompt_builder.py | 18 +- src/plugins/heartFC_chat/normal_chat.py | 2 +- template/bot_config_template.toml | 7 +- 16 files changed, 839 insertions(+), 702 deletions(-) delete mode 100644 llm_tool_benchmark_results.json rename tool_call_benchmark.py => scripts/tool_call_benchmark.py (100%) delete mode 100644 src/plugins/chat/emoji_manager.py create mode 100644 src/plugins/emoji_system/emoji_manager.py diff --git a/llm_tool_benchmark_results.json b/llm_tool_benchmark_results.json deleted file mode 100644 index e6be2a7dc..000000000 --- a/llm_tool_benchmark_results.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "测试时间": "2025-04-24 13:22:36", - "测试迭代次数": 3, - "不使用工具调用": { - "平均耗时": 3.1020479996999106, - "最短耗时": 2.980656862258911, - "最长耗时": 3.2487313747406006, - "标准差": 0.13581516492157006, - "所有耗时": [ - 2.98, - 3.08, - 3.25 - ] - }, - "不使用工具调用_详细响应": [ - { - "内容摘要": "那个猫猫头表情包真的太可爱了,墨墨发的表情包也好萌,感觉可以分享一下我收藏的猫猫头系列", - "推理内容摘要": "" - }, - { - "内容摘要": "那个猫猫头表情包确实很魔性,我存了好多张,每次看到都觉得特别治愈。墨墨好像也喜欢这种可爱的表情包,可以分享一下我收藏的。", - "推理内容摘要": "" - }, - { - "内容摘要": "那个猫猫头表情包真的超可爱,我存了好多张,每次看到都会忍不住笑出来。墨墨发的表情包也好萌,感觉可以和大家分享一下我收藏的猫猫头。\n\n工具:无", - "推理内容摘要": "" - } - ], - "使用工具调用": { - "平均耗时": 7.927528937657674, - "最短耗时": 5.714647531509399, - "最长耗时": 11.046205997467041, - "标准差": 2.778799784731646, - "所有耗时": [ - 7.02, - 11.05, - 5.71 - ] - }, - "使用工具调用_详细响应": [ - { - "内容摘要": "这个猫猫头表情包确实挺有意思的,不过他们好像还在讨论版本问题。小千石在问3.8和3.11谁大,这挺简单的。", - "推理内容摘要": "", - "工具调用数量": 1, - "工具调用详情": [ - { - "工具名称": "compare_numbers", - "参数": "{\"num1\":3.8,\"num2\":3.11}" - } - ] - }, - { - "内容摘要": "3.8和3.11谁大这个问题有点突然,不过可以简单比较一下。可能小千石在测试我或者真的想知道答案。现在群里的话题有点分散,既有技术讨论又有表情包的话题,我还是先回答数字比较的问题好了,毕竟比较直接。", - "推理内容摘要": "", - "工具调用数量": 1, - "工具调用详情": [ - { - "工具名称": "compare_numbers", - "参数": "{\"num1\":3.8,\"num2\":3.11}" - } - ] - }, - { - "内容摘要": "他们还在纠结调试消息的事儿,不过好像讨论得差不多了。猫猫头表情包确实挺有意思的,但感觉聊得有点散了哦。小千石问3.8和3.11谁大,这个问题可以回答一下。", - "推理内容摘要": "", - "工具调用数量": 0, - "工具调用详情": [] - } - ], - "差异百分比": 155.56 -} \ No newline at end of file diff --git a/tool_call_benchmark.py b/scripts/tool_call_benchmark.py similarity index 100% rename from tool_call_benchmark.py rename to scripts/tool_call_benchmark.py diff --git a/src/common/logger.py b/src/common/logger.py index 8a5b7ffc7..2fc1cbb17 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -464,7 +464,7 @@ EMOJI_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}", }, "simple": { - "console_format": "{time:MM-DD HH:mm} | 表情 | {message} ", # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 表情 | {message} ", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}", }, } diff --git a/src/config/config.py b/src/config/config.py index 7c16aaa59..0390b056c 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -222,7 +222,11 @@ class BotConfig: max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包 EMOJI_CHECK_INTERVAL: int = 120 # 表情包检查间隔(分钟) EMOJI_REGISTER_INTERVAL: int = 10 # 表情包注册间隔(分钟) - EMOJI_SAVE: bool = True # 偷表情包 + + save_pic: bool = False # 是否保存图片 + save_emoji: bool = False # 是否保存表情包 + steal_emoji: bool = True # 是否偷取表情包,让麦麦可以发送她保存的这些表情包 + EMOJI_CHECK: bool = False # 是否开启过滤 EMOJI_CHECK_PROMPT: str = "符合公序良俗" # 表情包过滤要求 @@ -392,12 +396,16 @@ class BotConfig: config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL) config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL) config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT) - config.EMOJI_SAVE = emoji_config.get("auto_save", config.EMOJI_SAVE) config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK) if config.INNER_VERSION in SpecifierSet(">=1.1.1"): config.max_emoji_num = emoji_config.get("max_emoji_num", config.max_emoji_num) config.max_reach_deletion = emoji_config.get("max_reach_deletion", config.max_reach_deletion) - + if config.INNER_VERSION in SpecifierSet(">=1.4.2"): + config.save_pic = emoji_config.get("save_pic", config.save_pic) + config.save_emoji = emoji_config.get("save_emoji", config.save_emoji) + config.steal_emoji = emoji_config.get("steal_emoji", config.steal_emoji) + + def bot(parent: dict): # 机器人基础配置 bot_config = parent["bot"] diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index c7baa91ed..92f0a9606 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -21,18 +21,15 @@ logger = get_module_logger("subheartflow", config=subheartflow_config) def init_prompt(): prompt = "" - # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" prompt += "{extra_info}\n" - # prompt += "{prompt_schedule}\n" - # prompt += "{relation_prompt_all}\n" prompt += "{prompt_personality}\n" - prompt += "刚刚你的想法是:\n我是{bot_name},我想,{current_thinking_info}\n" + prompt += "刚刚你的内心想法是:{current_thinking_info}\n" prompt += "-----------------------------------\n" - prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:\n{chat_observe_info}\n" + prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" prompt += "现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。" prompt += "请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" - prompt += "回复的要求是:平淡一些,简短一些,如果你要回复,最好只回复一个人的一个话题\n" + prompt += "回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题\n" prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言\n" prompt += "现在请你先输出想法,{hf_do_next},不要分点输出,文字不要浮夸" prompt += "在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。" diff --git a/src/main.py b/src/main.py index 62fa70a6e..3ef1ed229 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,7 @@ import time from .plugins.utils.statistic import LLMStatistics from .plugins.moods.moods import MoodManager from .plugins.schedule.schedule_generator import bot_schedule -from .plugins.chat.emoji_manager import emoji_manager +from .plugins.emoji_system.emoji_manager import emoji_manager from .plugins.person_info.person_info import person_info_manager from .plugins.willing.willing_manager import willing_manager from .plugins.chat.chat_stream import chat_manager @@ -128,7 +128,6 @@ class MainSystem: self.print_mood_task(), self.remove_recalled_message_task(), emoji_manager.start_periodic_check_register(), - # emoji_manager.start_periodic_register(), self.app.run(), self.server.run(), ] diff --git a/src/plugins/__init__.py b/src/plugins/__init__.py index 85de966e0..2e057e6fe 100644 --- a/src/plugins/__init__.py +++ b/src/plugins/__init__.py @@ -4,7 +4,7 @@ MaiMBot插件系统 """ from .chat.chat_stream import chat_manager -from .chat.emoji_manager import emoji_manager +from .emoji_system.emoji_manager import emoji_manager from .person_info.relationship_manager import relationship_manager from .moods.moods import MoodManager from .willing.willing_manager import willing_manager diff --git a/src/plugins/chat/__init__.py b/src/plugins/chat/__init__.py index 8d9aa1f8e..e5b0b942b 100644 --- a/src/plugins/chat/__init__.py +++ b/src/plugins/chat/__init__.py @@ -1,4 +1,4 @@ -from .emoji_manager import emoji_manager +from ..emoji_system.emoji_manager import emoji_manager from ..person_info.relationship_manager import relationship_manager from .chat_stream import chat_manager from .message_sender import message_manager diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py deleted file mode 100644 index cbc8e600a..000000000 --- a/src/plugins/chat/emoji_manager.py +++ /dev/null @@ -1,595 +0,0 @@ -import asyncio -import base64 -import hashlib -import os -import random -import time -import traceback -from typing import Optional, Tuple -from PIL import Image -import io - -from ...common.database import db -from ...config.config import global_config -from ..chat.utils import get_embedding -from ..chat.utils_image import ImageManager, image_path_to_base64 -from ..models.utils_model import LLMRequest -from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG - -emoji_log_config = LogConfig( - console_format=EMOJI_STYLE_CONFIG["console_format"], - file_format=EMOJI_STYLE_CONFIG["file_format"], -) - -logger = get_module_logger("emoji", config=emoji_log_config) - - -image_manager = ImageManager() - - -class EmojiManager: - _instance = None - EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录 - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance._initialized = False - return cls._instance - - def __init__(self): - self._scan_task = None - self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji") - self.llm_emotion_judge = LLMRequest( - model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji" - ) # 更高的温度,更少的token(后续可以根据情绪来调整温度) - - self.emoji_num = 0 - self.emoji_num_max = global_config.max_emoji_num - self.emoji_num_max_reach_deletion = global_config.max_reach_deletion - - logger.info("启动表情包管理器") - - def _ensure_emoji_dir(self): - """确保表情存储目录存在""" - os.makedirs(self.EMOJI_DIR, exist_ok=True) - - def _update_emoji_count(self): - """更新表情包数量统计 - - 检查数据库中的表情包数量并更新到 self.emoji_num - """ - try: - self._ensure_db() - self.emoji_num = db.emoji.count_documents({}) - logger.info(f"[统计] 当前表情包数量: {self.emoji_num}") - except Exception as e: - logger.error(f"[错误] 更新表情包数量失败: {str(e)}") - - def initialize(self): - """初始化数据库连接和表情目录""" - if not self._initialized: - try: - self._ensure_emoji_collection() - self._ensure_emoji_dir() - self._initialized = True - # 更新表情包数量 - self._update_emoji_count() - # 启动时执行一次完整性检查 - self.check_emoji_file_integrity() - except Exception: - logger.exception("初始化表情管理器失败") - - def _ensure_db(self): - """确保数据库已初始化""" - if not self._initialized: - self.initialize() - if not self._initialized: - raise RuntimeError("EmojiManager not initialized") - - @staticmethod - def _ensure_emoji_collection(): - """确保emoji集合存在并创建索引 - - 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。 - - 索引的作用是加快数据库查询速度: - - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包 - - tags字段的普通索引: 加快按标签搜索表情包的速度 - - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度 - - 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。 - """ - if "emoji" not in db.list_collection_names(): - db.create_collection("emoji") - db.emoji.create_index([("embedding", "2dsphere")]) - db.emoji.create_index([("filename", 1)], unique=True) - - def record_usage(self, emoji_id: str): - """记录表情使用次数""" - try: - self._ensure_db() - db.emoji.update_one({"_id": emoji_id}, {"$inc": {"usage_count": 1}}) - except Exception as e: - logger.error(f"记录表情使用失败: {str(e)}") - - async def get_emoji_for_text(self, text: str) -> Optional[Tuple[str, str]]: - """根据文本内容获取相关表情包 - Args: - text: 输入文本 - Returns: - Optional[str]: 表情包文件路径,如果没有找到则返回None - - - 可不可以通过 配置文件中的指令 来自定义使用表情包的逻辑? - 我觉得可行 - - """ - try: - self._ensure_db() - - # 获取文本的embedding - text_for_search = await self._get_kimoji_for_text(text) - if not text_for_search: - logger.error("无法获取文本的情绪") - return None - text_embedding = await get_embedding(text_for_search, request_type="emoji") - if not text_embedding: - logger.error("无法获取文本的embedding") - return None - - try: - # 获取所有表情包 - all_emojis = [ - e - for e in db.emoji.find({}, {"_id": 1, "path": 1, "embedding": 1, "description": 1, "blacklist": 1}) - if "blacklist" not in e - ] - - if not all_emojis: - logger.warning("数据库中没有任何表情包") - return None - - # 计算余弦相似度并排序 - def cosine_similarity(v1, v2): - if not v1 or not v2: - return 0 - dot_product = sum(a * b for a, b in zip(v1, v2)) - norm_v1 = sum(a * a for a in v1) ** 0.5 - norm_v2 = sum(b * b for b in v2) ** 0.5 - if norm_v1 == 0 or norm_v2 == 0: - return 0 - return dot_product / (norm_v1 * norm_v2) - - # 计算所有表情包与输入文本的相似度 - emoji_similarities = [ - (emoji, cosine_similarity(text_embedding, emoji.get("embedding", []))) for emoji in all_emojis - ] - - # 按相似度降序排序 - emoji_similarities.sort(key=lambda x: x[1], reverse=True) - - # 获取前3个最相似的表情包 - top_10_emojis = emoji_similarities[: 10 if len(emoji_similarities) > 10 else len(emoji_similarities)] - - if not top_10_emojis: - logger.warning("未找到匹配的表情包") - return None - - # 从前3个中随机选择一个 - selected_emoji, similarity = random.choice(top_10_emojis) - - if selected_emoji and "path" in selected_emoji: - # 更新使用次数 - db.emoji.update_one({"_id": selected_emoji["_id"]}, {"$inc": {"usage_count": 1}}) - - logger.info( - f"[匹配] 找到表情包: {selected_emoji.get('description', '无描述')} (相似度: {similarity:.4f})" - ) - # 稍微改一下文本描述,不然容易产生幻觉,描述已经包含 表情包 了 - return selected_emoji["path"], "[ %s ]" % selected_emoji.get("description", "无描述") - - except Exception as search_error: - logger.error(f"[错误] 搜索表情包失败: {str(search_error)}") - return None - - return None - - except Exception as e: - logger.error(f"[错误] 获取表情包失败: {str(e)}") - return None - - @staticmethod - async def _get_emoji_description(image_base64: str) -> str: - """获取表情包的标签,使用image_manager的描述生成功能""" - - try: - # 使用image_manager获取描述,去掉前后的方括号和"表情包:"前缀 - description = await image_manager.get_emoji_description(image_base64) - # 去掉[表情包:xxx]的格式,只保留描述内容 - description = description.strip("[]").replace("表情包:", "") - return description - - except Exception as e: - logger.error(f"[错误] 获取表情包描述失败: {str(e)}") - return None - - async def _check_emoji(self, image_base64: str, image_format: str) -> str: - try: - prompt = ( - f'这是一个表情包,请回答这个表情包是否满足"{global_config.EMOJI_CHECK_PROMPT}"的要求,是则回答是,' - f"否则回答否,不要出现任何其他内容" - ) - - content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) - logger.debug(f"[检查] 表情包检查结果: {content}") - return content - - except Exception as e: - logger.error(f"[错误] 表情包检查失败: {str(e)}") - return None - - async def _get_kimoji_for_text(self, text: str): - try: - prompt = ( - f"这是{global_config.BOT_NICKNAME}将要发送的消息内容:\n{text}\n若要为其配上表情包," - f"请你输出这个表情包应该表达怎样的情感,应该给人什么样的感觉,不要太简洁也不要太长," - f'注意不要输出任何对消息内容的分析内容,只输出"一种什么样的感觉"中间的形容词部分。' - ) - - content, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=1.5) - logger.info(f"[情感] 表情包情感描述: {content}") - return content - - except Exception as e: - logger.error(f"[错误] 获取表情包情感失败: {str(e)}") - return None - - async def scan_new_emojis(self): - """扫描新的表情包""" - try: - emoji_dir = self.EMOJI_DIR - os.makedirs(emoji_dir, exist_ok=True) - - # 获取所有支持的图片文件 - files_to_process = [ - f for f in os.listdir(emoji_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif")) - ] - - # 检查当前表情包数量 - self._update_emoji_count() - if self.emoji_num >= self.emoji_num_max: - logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),跳过注册") - return - - # 计算还可以注册的数量 - remaining_slots = self.emoji_num_max - self.emoji_num - logger.info(f"[注册] 还可以注册 {remaining_slots} 个表情包") - - for filename in files_to_process: - # 如果已经达到上限,停止注册 - if self.emoji_num >= self.emoji_num_max: - logger.warning(f"[警告] 表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max}),停止注册") - break - - image_path = os.path.join(emoji_dir, filename) - - # 获取图片的base64编码和哈希值 - image_base64 = image_path_to_base64(image_path) - if image_base64 is None: - os.remove(image_path) - continue - - image_bytes = base64.b64decode(image_base64) - image_hash = hashlib.md5(image_bytes).hexdigest() - image_format = Image.open(io.BytesIO(image_bytes)).format.lower() - # 检查是否已经注册过 - existing_emoji_by_path = db["emoji"].find_one({"filename": filename}) - existing_emoji_by_hash = db["emoji"].find_one({"hash": image_hash}) - if existing_emoji_by_path and existing_emoji_by_hash: - if existing_emoji_by_path["_id"] != existing_emoji_by_hash["_id"]: - logger.error(f"[错误] 表情包已存在但记录不一致: {filename}") - db.emoji.delete_one({"_id": existing_emoji_by_path["_id"]}) - db.emoji.delete_one({"_id": existing_emoji_by_hash["_id"]}) - existing_emoji = None - else: - existing_emoji = existing_emoji_by_hash - elif existing_emoji_by_hash: - logger.error(f"[错误] 表情包hash已存在但path不存在: {filename}") - db.emoji.delete_one({"_id": existing_emoji_by_hash["_id"]}) - existing_emoji = None - elif existing_emoji_by_path: - logger.error(f"[错误] 表情包path已存在但hash不存在: {filename}") - db.emoji.delete_one({"_id": existing_emoji_by_path["_id"]}) - existing_emoji = None - else: - existing_emoji = None - - description = None - - if existing_emoji: - # 即使表情包已存在,也检查是否需要同步到images集合 - description = existing_emoji.get("description") - # 检查是否在images集合中存在 - existing_image = db.images.find_one({"hash": image_hash}) - if not existing_image: - # 同步到images集合 - image_doc = { - "hash": image_hash, - "path": image_path, - "type": "emoji", - "description": description, - "timestamp": int(time.time()), - } - db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) - # 保存描述到image_descriptions集合 - image_manager._save_description_to_db(image_hash, description, "emoji") - logger.success(f"[同步] 已同步表情包到images集合: {filename}") - continue - - # 检查是否在images集合中已有描述 - existing_description = image_manager._get_description_from_db(image_hash, "emoji") - - if existing_description: - description = existing_description - else: - # 获取表情包的描述 - description = await self._get_emoji_description(image_base64) - - if global_config.EMOJI_CHECK: - check = await self._check_emoji(image_base64, image_format) - if "是" not in check: - os.remove(image_path) - logger.info(f"[过滤] 表情包描述: {description}") - logger.info(f"[过滤] 表情包不满足规则,已移除: {check}") - continue - logger.info(f"[检查] 表情包检查通过: {check}") - - if description is not None: - embedding = await get_embedding(description, request_type="emoji") - if not embedding: - logger.error("获取消息嵌入向量失败") - raise ValueError("获取消息嵌入向量失败") - # 准备数据库记录 - emoji_record = { - "filename": filename, - "path": image_path, - "embedding": embedding, - "description": description, - "hash": image_hash, - "timestamp": int(time.time()), - } - - # 保存到emoji数据库 - db["emoji"].insert_one(emoji_record) - logger.success(f"[注册] 新表情包: {filename}") - logger.info(f"[描述] {description}") - - # 更新当前表情包数量 - self.emoji_num += 1 - logger.info(f"[统计] 当前表情包数量: {self.emoji_num}/{self.emoji_num_max}") - - # 保存到images数据库 - image_doc = { - "hash": image_hash, - "path": image_path, - "type": "emoji", - "description": description, - "timestamp": int(time.time()), - } - db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) - # 保存描述到image_descriptions集合 - image_manager._save_description_to_db(image_hash, description, "emoji") - logger.success(f"[同步] 已保存到images集合: {filename}") - else: - logger.warning(f"[跳过] 表情包: {filename}") - - except Exception: - logger.exception("[错误] 扫描表情包失败") - - def check_emoji_file_integrity(self): - """检查表情包文件完整性 - 如果文件已被删除,则从数据库中移除对应记录 - """ - try: - self._ensure_db() - # 获取所有表情包记录 - all_emojis = list(db.emoji.find()) - removed_count = 0 - total_count = len(all_emojis) - - for emoji in all_emojis: - try: - if "path" not in emoji: - logger.warning(f"[检查] 发现无效记录(缺少path字段),ID: {emoji.get('_id', 'unknown')}") - db.emoji.delete_one({"_id": emoji["_id"]}) - removed_count += 1 - continue - - if "embedding" not in emoji: - logger.warning(f"[检查] 发现过时记录(缺少embedding字段),ID: {emoji.get('_id', 'unknown')}") - db.emoji.delete_one({"_id": emoji["_id"]}) - removed_count += 1 - continue - - # 检查文件是否存在 - if not os.path.exists(emoji["path"]): - logger.warning(f"[检查] 表情包文件已被删除: {emoji['path']}") - # 从数据库中删除记录 - result = db.emoji.delete_one({"_id": emoji["_id"]}) - if result.deleted_count > 0: - logger.debug(f"[清理] 成功删除数据库记录: {emoji['_id']}") - removed_count += 1 - else: - logger.error(f"[错误] 删除数据库记录失败: {emoji['_id']}") - continue - - if "hash" not in emoji: - logger.warning(f"[检查] 发现缺失记录(缺少hash字段),ID: {emoji.get('_id', 'unknown')}") - hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() - db.emoji.update_one({"_id": emoji["_id"]}, {"$set": {"hash": hash}}) - else: - file_hash = hashlib.md5(open(emoji["path"], "rb").read()).hexdigest() - if emoji["hash"] != file_hash: - logger.warning(f"[检查] 表情包文件hash不匹配,ID: {emoji.get('_id', 'unknown')}") - db.emoji.delete_one({"_id": emoji["_id"]}) - removed_count += 1 - - # 修复拼写错误 - if "discription" in emoji: - desc = emoji["discription"] - db.emoji.update_one( - {"_id": emoji["_id"]}, {"$unset": {"discription": ""}, "$set": {"description": desc}} - ) - - except Exception as item_error: - logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}") - continue - - # 验证清理结果 - remaining_count = db.emoji.count_documents({}) - if removed_count > 0: - logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录") - logger.info(f"[统计] 清理前: {total_count} | 清理后: {remaining_count}") - else: - logger.info(f"[检查] 已检查 {total_count} 个表情包记录") - - except Exception as e: - logger.error(f"[错误] 检查表情包完整性失败: {str(e)}") - logger.error(traceback.format_exc()) - - def check_emoji_file_full(self): - """检查表情包文件是否完整,如果数量超出限制且允许删除,则删除多余的表情包 - - 删除规则: - 1. 优先删除创建时间更早的表情包 - 2. 优先删除使用次数少的表情包,但使用次数多的也有小概率被删除 - """ - try: - self._ensure_db() - # 更新表情包数量 - self._update_emoji_count() - - # 检查是否超出限制 - if self.emoji_num <= self.emoji_num_max: - return - - # 如果超出限制但不允许删除,则只记录警告 - if not global_config.max_reach_deletion: - logger.warning(f"[警告] 表情包数量({self.emoji_num})超出限制({self.emoji_num_max}),但未开启自动删除") - return - - # 计算需要删除的数量 - delete_count = self.emoji_num - self.emoji_num_max - logger.info(f"[清理] 需要删除 {delete_count} 个表情包") - - # 获取所有表情包,按时间戳升序(旧的在前)排序 - all_emojis = list(db.emoji.find().sort([("timestamp", 1)])) - - # 计算权重:使用次数越多,被删除的概率越小 - weights = [] - max_usage = max((emoji.get("usage_count", 0) for emoji in all_emojis), default=1) - for emoji in all_emojis: - usage_count = emoji.get("usage_count", 0) - # 使用指数衰减函数计算权重,使用次数越多权重越小 - weight = 1.0 / (1.0 + usage_count / max(1, max_usage)) - weights.append(weight) - - # 根据权重随机选择要删除的表情包 - to_delete = [] - remaining_indices = list(range(len(all_emojis))) - - while len(to_delete) < delete_count and remaining_indices: - # 计算当前剩余表情包的权重 - current_weights = [weights[i] for i in remaining_indices] - # 归一化权重 - total_weight = sum(current_weights) - if total_weight == 0: - break - normalized_weights = [w / total_weight for w in current_weights] - - # 随机选择一个表情包 - selected_idx = random.choices(remaining_indices, weights=normalized_weights, k=1)[0] - to_delete.append(all_emojis[selected_idx]) - remaining_indices.remove(selected_idx) - - # 删除选中的表情包 - deleted_count = 0 - for emoji in to_delete: - try: - # 删除文件 - if "path" in emoji and os.path.exists(emoji["path"]): - os.remove(emoji["path"]) - logger.info(f"[删除] 文件: {emoji['path']} (使用次数: {emoji.get('usage_count', 0)})") - - # 删除数据库记录 - db.emoji.delete_one({"_id": emoji["_id"]}) - deleted_count += 1 - - # 同时从images集合中删除 - if "hash" in emoji: - db.images.delete_one({"hash": emoji["hash"]}) - - except Exception as e: - logger.error(f"[错误] 删除表情包失败: {str(e)}") - continue - - # 更新表情包数量 - self._update_emoji_count() - logger.success(f"[清理] 已删除 {deleted_count} 个表情包,当前数量: {self.emoji_num}") - - except Exception as e: - logger.error(f"[错误] 检查表情包数量失败: {str(e)}") - - async def start_periodic_check_register(self): - """定期检查表情包完整性和数量""" - while True: - logger.info("[扫描] 开始检查表情包完整性...") - self.check_emoji_file_integrity() - logger.info("[扫描] 开始删除所有图片缓存...") - await self.delete_all_images() - logger.info("[扫描] 开始扫描新表情包...") - if self.emoji_num < self.emoji_num_max: - await self.scan_new_emojis() - if self.emoji_num > self.emoji_num_max: - logger.warning(f"[警告] 表情包数量超过最大限制: {self.emoji_num} > {self.emoji_num_max},跳过注册") - if not global_config.max_reach_deletion: - logger.warning("表情包数量超过最大限制,终止注册") - break - else: - logger.warning("表情包数量超过最大限制,开始删除表情包") - self.check_emoji_file_full() - await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) - - @staticmethod - async def delete_all_images(): - """删除 data/image 目录下的所有文件""" - try: - image_dir = os.path.join("data", "image") - if not os.path.exists(image_dir): - logger.warning(f"[警告] 目录不存在: {image_dir}") - return - - deleted_count = 0 - failed_count = 0 - - # 遍历目录下的所有文件 - for filename in os.listdir(image_dir): - file_path = os.path.join(image_dir, filename) - try: - if os.path.isfile(file_path): - os.remove(file_path) - deleted_count += 1 - logger.debug(f"[删除] 文件: {file_path}") - except Exception as e: - failed_count += 1 - logger.error(f"[错误] 删除文件失败 {file_path}: {str(e)}") - - logger.success(f"[清理] 已删除 {deleted_count} 个文件,失败 {failed_count} 个") - - except Exception as e: - logger.error(f"[错误] 删除图片目录失败: {str(e)}") - - -# 创建全局单例 -emoji_manager = EmojiManager() diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 2ba645f95..093ccc30d 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -127,12 +127,12 @@ class MessageRecv(Message): # 如果是base64图片数据 if isinstance(seg.data, str): return await image_manager.get_image_description(seg.data) - return "[图片]" + return "[发了一张图片,网卡了加载不出来]" elif seg.type == "emoji": self.is_emoji = True if isinstance(seg.data, str): return await image_manager.get_emoji_description(seg.data) - return "[表情]" + return "[发了一个表情包,网卡了加载不出来]" else: return f"[{seg.type}:{str(seg.data)}]" except Exception as e: @@ -141,14 +141,8 @@ class MessageRecv(Message): def _generate_detailed_text(self) -> str: """生成详细文本,包含时间和用户信息""" - # time_str = time.strftime("%m-%d %H:%M:%S", time.localtime(self.message_info.time)) timestamp = self.message_info.time user_info = self.message_info.user_info - # name = ( - # f"{user_info.user_nickname}(ta的昵称:{user_info.user_cardname},ta的id:{user_info.user_id})" - # if user_info.user_cardname != None - # else f"{user_info.user_nickname}(ta的id:{user_info.user_id})" - # ) name = f"<{self.message_info.platform}:{user_info.user_id}:{user_info.user_nickname}:{user_info.user_cardname}>" return f"[{timestamp}] {name}: {self.processed_plain_text}\n" @@ -222,11 +216,11 @@ class MessageProcessBase(Message): # 如果是base64图片数据 if isinstance(seg.data, str): return await image_manager.get_image_description(seg.data) - return "[图片]" + return "[图片,网卡了加载不出来]" elif seg.type == "emoji": if isinstance(seg.data, str): return await image_manager.get_emoji_description(seg.data) - return "[表情]" + return "[表情,网卡了加载不出来]" elif seg.type == "at": return f"[@{seg.data}]" elif seg.type == "reply": diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 9c7a03b06..bf549b97e 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -121,7 +121,7 @@ class ImageManager: prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,使用1-2个词描述一下表情包表达的情感和内容,简短一些" description, _ = await self._llm.generate_response_for_image(prompt, image_base64, "jpg") else: - prompt = "这是一个表情包,请用使用1-2个词描述一下表情包所表达的情感和内容,简短一些" + prompt = "这是一个表情包,请用使用几个词描述一下表情包所表达的情感和内容,简短一些" description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) cached_description = self._get_description_from_db(image_hash, "emoji") @@ -130,7 +130,7 @@ class ImageManager: return f"[表达了:{cached_description}]" # 根据配置决定是否保存图片 - if global_config.EMOJI_SAVE: + if global_config.save_emoji: # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" @@ -196,7 +196,7 @@ class ImageManager: return "[图片]" # 根据配置决定是否保存图片 - if global_config.EMOJI_SAVE: + if global_config.save_pic: # 生成文件名和路径 timestamp = int(time.time()) filename = f"{timestamp}_{image_hash[:8]}.{image_format}" diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py new file mode 100644 index 000000000..db5a31322 --- /dev/null +++ b/src/plugins/emoji_system/emoji_manager.py @@ -0,0 +1,794 @@ +import asyncio +import base64 +import hashlib +import os +import random +import time +import traceback +from typing import Optional, Tuple +from PIL import Image +import io +import re + +from ...common.database import db +from ...config.config import global_config +from ..chat.utils import get_embedding +from ..chat.utils_image import image_path_to_base64, image_manager +from ..models.utils_model import LLMRequest +from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG + + +emoji_log_config = LogConfig( + console_format=EMOJI_STYLE_CONFIG["console_format"], + file_format=EMOJI_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("emoji", config=emoji_log_config) + +EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录 +EMOJI_REGISTED_DIR = os.path.join("data", "emoji_registed") # 已注册的表情包注册目录 + + +class MaiEmoji: + """定义一个表情包""" + def __init__(self, filename: str, path: str): + self.path = path # 存储目录路径 + self.filename = filename + self.embedding = [] + self.hash = "" # 初始为空,在创建实例时会计算 + self.description = "" + self.emotion = [] + self.usage_count = 0 + self.last_used_time = time.time() + self.register_time = time.time() + self.is_deleted = False # 标记是否已被删除 + self.format = "" + + async def initialize_hash_format(self): + """从文件创建表情包实例 + + 参数: + file_path: 文件的完整路径 + + 返回: + MaiEmoji: 创建的表情包实例,如果失败则返回None + """ + try: + file_path = os.path.join(self.path, self.filename) + if not os.path.exists(file_path): + logger.error(f"[错误] 表情包文件不存在: {file_path}") + return None + + image_base64 = image_path_to_base64(file_path) + if image_base64 is None: + logger.error(f"[错误] 无法读取图片: {file_path}") + return None + + + # 计算哈希值 + image_bytes = base64.b64decode(image_base64) + self.hash = hashlib.md5(image_bytes).hexdigest() + + # 获取图片格式 + self.format = Image.open(io.BytesIO(image_bytes)).format.lower() + + + except Exception as e: + logger.error(f"[错误] 初始化表情包失败: {str(e)}") + logger.error(traceback.format_exc()) + return None + + async def register_to_db(self): + """ + 注册表情包 + 将表情包对应的文件,从当前路径移动到EMOJI_REGISTED_DIR目录下 + 并修改对应的实例属性,然后将表情包信息保存到数据库中 + """ + try: + # 确保目标目录存在 + os.makedirs(EMOJI_REGISTED_DIR, exist_ok=True) + + # 源路径是当前实例的完整路径 + source_path = os.path.join(self.path, self.filename) + # 目标路径 + destination_path = os.path.join(EMOJI_REGISTED_DIR, self.filename) + + # 检查源文件是否存在 + if not os.path.exists(source_path): + logger.error(f"[错误] 源文件不存在: {source_path}") + return False + + # --- 文件移动 --- + try: + # 如果目标文件已存在,先删除 (确保移动成功) + if os.path.exists(destination_path): + os.remove(destination_path) + + os.rename(source_path, destination_path) + logger.info(f"[移动] 文件从 {source_path} 移动到 {destination_path}") + # 更新实例的路径属性为新目录 + self.path = EMOJI_REGISTED_DIR + except Exception as move_error: + logger.error(f"[错误] 移动文件失败: {str(move_error)}") + return False # 文件移动失败,不继续 + + # --- 数据库操作 --- + try: + # 准备数据库记录 for emoji collection + emoji_record = { + "filename": self.filename, + "path": os.path.join(self.path, self.filename), # 使用更新后的路径 + "embedding": self.embedding, + "description": self.description, + "emotion": self.emotion, # 添加情感标签字段 + "hash": self.hash, + "format": self.format, + "timestamp": int(self.register_time), # 使用实例的注册时间 + "usage_count": self.usage_count, + "last_used_time": self.last_used_time + } + + # 使用upsert确保记录存在或被更新 + db["emoji"].update_one( + {"hash": self.hash}, + {"$set": emoji_record}, + upsert=True + ) + logger.success(f"[注册] 表情包信息保存到数据库: {self.description}") + + return True + + except Exception as db_error: + logger.error(f"[错误] 保存数据库失败: {str(db_error)}") + # 考虑是否需要将文件移回?为了简化,暂时只记录错误 + return False + + except Exception as e: + logger.error(f"[错误] 注册表情包失败: {str(e)}") + logger.error(traceback.format_exc()) + return False + + async def delete(self): + """删除表情包 + + 删除表情包的文件和数据库记录 + + 返回: + bool: 是否成功删除 + """ + try: + # 1. 删除文件 + if os.path.exists(os.path.join(self.path, self.filename)): + try: + os.remove(os.path.join(self.path, self.filename)) + logger.info(f"[删除] 文件: {os.path.join(self.path, self.filename)}") + except Exception as e: + logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}") + # 继续执行,即使文件删除失败也尝试删除数据库记录 + + # 2. 删除数据库记录 + result = db.emoji.delete_one({"hash": self.hash}) + deleted_in_db = result.deleted_count > 0 + + if deleted_in_db: + logger.success(f"[删除] 成功删除表情包记录: {self.description}") + + # 3. 标记对象已被删除 + self.is_deleted = True + return True + else: + logger.error(f"[错误] 删除表情包记录失败: {self.hash}") + return False + + except Exception as e: + logger.error(f"[错误] 删除表情包失败: {str(e)}") + return False + + +class EmojiManager: + _instance = None + + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + self._scan_task = None + self.vlm = LLMRequest(model=global_config.vlm, temperature=0.3, max_tokens=1000, request_type="emoji") + self.llm_emotion_judge = LLMRequest( + model=global_config.llm_emotion_judge, max_tokens=600, temperature=0.8, request_type="emoji" + ) # 更高的温度,更少的token(后续可以根据情绪来调整温度) + + self.emoji_num = 0 + self.emoji_num_max = global_config.max_emoji_num + self.emoji_num_max_reach_deletion = global_config.max_reach_deletion + self.emoji_objects: list[MaiEmoji] = [] # 存储MaiEmoji对象的列表,使用类型注解明确列表元素类型 + + logger.info("启动表情包管理器") + + def _ensure_emoji_dir(self): + """确保表情存储目录存在""" + os.makedirs(EMOJI_DIR, exist_ok=True) + + + def initialize(self): + """初始化数据库连接和表情目录""" + if not self._initialized: + try: + self._ensure_emoji_collection() + self._ensure_emoji_dir() + self._initialized = True + # 更新表情包数量 + # 启动时执行一次完整性检查 + self.check_emoji_file_integrity() + except Exception: + logger.exception("初始化表情管理器失败") + + def _ensure_db(self): + """确保数据库已初始化""" + if not self._initialized: + self.initialize() + if not self._initialized: + raise RuntimeError("EmojiManager not initialized") + + @staticmethod + def _ensure_emoji_collection(): + """确保emoji集合存在并创建索引 + + 这个函数用于确保MongoDB数据库中存在emoji集合,并创建必要的索引。 + + 索引的作用是加快数据库查询速度: + - embedding字段的2dsphere索引: 用于加速向量相似度搜索,帮助快速找到相似的表情包 + - tags字段的普通索引: 加快按标签搜索表情包的速度 + - filename字段的唯一索引: 确保文件名不重复,同时加快按文件名查找的速度 + + 没有索引的话,数据库每次查询都需要扫描全部数据,建立索引后可以大大提高查询效率。 + """ + if "emoji" not in db.list_collection_names(): + db.create_collection("emoji") + db.emoji.create_index([("embedding", "2dsphere")]) + db.emoji.create_index([("filename", 1)], unique=True) + + def record_usage(self, hash: str): + """记录表情使用次数""" + try: + for emoji in self.emoji_objects: + if emoji.hash == hash: + emoji.usage_count += 1 + break + except Exception as e: + logger.error(f"记录表情使用失败: {str(e)}") + + async def get_emoji_for_text(self, text_emotion: str) -> Optional[Tuple[str, str]]: + """根据文本内容获取相关表情包 + Args: + text_emotion: 输入的情感描述文本 + Returns: + Optional[Tuple[str, str]]: (表情包文件路径, 表情包描述),如果没有找到则返回None + """ + try: + self._ensure_db() + time_start = time.time() + + # 获取所有表情包 + all_emojis = self.emoji_objects + + if not all_emojis: + logger.warning("数据库中没有任何表情包") + return None + + # 计算每个表情包与输入文本的最大情感相似度 + emoji_similarities = [] + for emoji in all_emojis: + emotions = emoji.emotion + if not emotions: + continue + + # 计算与每个emotion标签的相似度,取最大值 + max_similarity = 0 + for emotion in emotions: + # 使用编辑距离计算相似度 + distance = self._levenshtein_distance(text_emotion, emotion) + max_len = max(len(text_emotion), len(emotion)) + similarity = 1 - (distance / max_len if max_len > 0 else 0) + max_similarity = max(max_similarity, similarity) + + emoji_similarities.append((emoji, max_similarity)) + + # 按相似度降序排序 + emoji_similarities.sort(key=lambda x: x[1], reverse=True) + + # 获取前5个最相似的表情包 + top_5_emojis = emoji_similarities[:5] if len(emoji_similarities) > 5 else emoji_similarities + + if not top_5_emojis: + logger.warning("未找到匹配的表情包") + return None + + # 从前5个中随机选择一个 + selected_emoji, similarity = random.choice(top_5_emojis) + + # 更新使用次数 + db.emoji.update_one({"hash": selected_emoji.hash}, {"$inc": {"usage_count": 1}}) + + logger.info( + f"[匹配] 找到表情包: {selected_emoji.description} (相似度: {similarity:.4f})" + ) + + time_end = time.time() + logger.info(f"[匹配] 搜索表情包用时: {time_end - time_start:.2f} 秒") + return os.path.join(selected_emoji.path, selected_emoji.filename), f"[ {selected_emoji.description} ]" + + except Exception as e: + logger.error(f"[错误] 获取表情包失败: {str(e)}") + return None + + def _levenshtein_distance(self, s1: str, s2: str) -> int: + """计算两个字符串的编辑距离 + + Args: + s1: 第一个字符串 + s2: 第二个字符串 + + Returns: + int: 编辑距离 + """ + if len(s1) < len(s2): + return self._levenshtein_distance(s2, s1) + + if len(s2) == 0: + return len(s1) + + previous_row = range(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] + + async def check_emoji_file_integrity(self): + """检查表情包文件完整性 + 遍历self.emoji_objects中的所有对象,检查文件是否存在 + 如果文件已被删除,则执行对象的删除方法并从列表中移除 + """ + try: + if not self.emoji_objects: + logger.warning("[检查] emoji_objects为空,跳过完整性检查") + return + + total_count = len(self.emoji_objects) + removed_count = 0 + # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表 + for emoji in self.emoji_objects[:]: + try: + # 检查文件是否存在 + if not os.path.exists(emoji.path): + logger.warning(f"[检查] 表情包文件已被删除: {emoji.path}") + # 执行表情包对象的删除方法 + await emoji.delete() + # 从列表中移除该对象 + self.emoji_objects.remove(emoji) + # 更新计数 + self.emoji_num -= 1 + removed_count += 1 + continue + + except Exception as item_error: + logger.error(f"[错误] 处理表情包记录时出错: {str(item_error)}") + continue + + # 输出清理结果 + if removed_count > 0: + logger.success(f"[清理] 已清理 {removed_count} 个失效的表情包记录") + logger.info(f"[统计] 清理前: {total_count} | 清理后: {len(self.emoji_objects)}") + else: + logger.info(f"[检查] 已检查 {total_count} 个表情包记录,全部完好") + + except Exception as e: + logger.error(f"[错误] 检查表情包完整性失败: {str(e)}") + logger.error(traceback.format_exc()) + + async def start_periodic_check_register(self): + """定期检查表情包完整性和数量""" + await self.get_all_emoji_from_db() + while True: + logger.info("[扫描] 开始检查表情包完整性...") + self.check_emoji_file_integrity() + logger.info("[扫描] 开始扫描新表情包...") + + # 检查表情包目录是否存在 + if not os.path.exists(EMOJI_DIR): + logger.warning(f"[警告] 表情包目录不存在: {EMOJI_DIR}") + os.makedirs(EMOJI_DIR, exist_ok=True) + logger.info(f"[创建] 已创建表情包目录: {EMOJI_DIR}") + await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) + continue + + # 检查目录是否为空 + files = os.listdir(EMOJI_DIR) + if not files: + logger.warning(f"[警告] 表情包目录为空: {EMOJI_DIR}") + await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) + continue + + # 检查是否需要处理表情包(数量超过最大值或不足) + if (self.emoji_num > self.emoji_num_max and global_config.max_reach_deletion) or (self.emoji_num < self.emoji_num_max): + try: + # 获取目录下所有图片文件 + files_to_process = [ + f for f in files + if os.path.isfile(os.path.join(EMOJI_DIR, f)) + and f.lower().endswith((".jpg", ".jpeg", ".png", ".gif")) + ] + + # 处理每个符合条件的文件 + for filename in files_to_process: + # 尝试注册表情包 + success = await self.register_emoji_by_filename(filename) + if success: + # 注册成功则跳出循环 + break + else: + # 注册失败则删除对应文件 + file_path = os.path.join(EMOJI_DIR, filename) + os.remove(file_path) + logger.warning(f"[清理] 删除注册失败的表情包文件: {filename}") + except Exception as e: + logger.error(f"[错误] 扫描表情包目录失败: {str(e)}") + + await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) + + async def get_all_emoji_from_db(self): + """获取所有表情包并初始化为MaiEmoji类对象 + + 参数: + hash: 可选,如果提供则只返回指定哈希值的表情包 + + 返回: + list[MaiEmoji]: 表情包对象列表 + """ + try: + self._ensure_db() + + # 获取所有表情包 + all_emoji_data = list(db.emoji.find()) + + # 将数据库记录转换为MaiEmoji对象 + emoji_objects = [] + for emoji_data in all_emoji_data: + emoji = MaiEmoji( + filename=emoji_data.get("filename", ""), + path=emoji_data.get("path", ""), + ) + + # 设置额外属性 + emoji.usage_count = emoji_data.get("usage_count", 0) + emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) + emoji.register_time = emoji_data.get("timestamp", time.time()) + emoji.description = emoji_data.get("description", "") + emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载 + emoji_objects.append(emoji) + + # 存储到EmojiManager中 + self.emoji_objects = emoji_objects + + except Exception as e: + logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") + + async def get_emoji_from_db(self, hash=None): + """获取所有表情包并初始化为MaiEmoji类对象 + + 参数: + hash: 可选,如果提供则只返回指定哈希值的表情包 + + 返回: + list[MaiEmoji]: 表情包对象列表 + """ + try: + self._ensure_db() + + # 准备查询条件 + query = {} + if hash: + query = {"hash": hash} + + # 获取所有表情包 + all_emoji_data = list(db.emoji.find(query)) + + # 将数据库记录转换为MaiEmoji对象 + emoji_objects = [] + for emoji_data in all_emoji_data: + emoji = MaiEmoji( + filename=emoji_data.get("filename", ""), + path=emoji_data.get("path", ""), + ) + + # 设置额外属性 + emoji.usage_count = emoji_data.get("usage_count", 0) + emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) + emoji.register_time = emoji_data.get("timestamp", time.time()) + emoji.description = emoji_data.get("description", "") + emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载 + + emoji_objects.append(emoji) + + # 存储到EmojiManager中 + self.emoji_objects = emoji_objects + + return emoji_objects + + except Exception as e: + logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") + return [] + + async def get_emoji_from_manager(self, hash) -> MaiEmoji: + """从EmojiManager中获取表情包 + + 参数: + hash:如果提供则只返回指定哈希值的表情包 + """ + for emoji in self.emoji_objects: + if emoji.hash == hash: + return emoji + return None + + + + async def delete_emoji(self, emoji_hash: str) -> bool: + """根据哈希值删除表情包 + + Args: + emoji_hash: 表情包的哈希值 + + Returns: + bool: 是否成功删除 + """ + try: + self._ensure_db() + + # 从emoji_objects中查找表情包对象 + emoji = await self.get_emoji_from_manager(emoji_hash) + + if not emoji: + logger.warning(f"[警告] 未找到哈希值为 {emoji_hash} 的表情包") + return False + + # 使用MaiEmoji对象的delete方法删除表情包 + success = await emoji.delete() + + if success: + # 从emoji_objects列表中移除该对象 + self.emoji_objects = [e for e in self.emoji_objects if e.hash != emoji_hash] + # 更新计数 + self.emoji_num -= 1 + logger.info(f"[统计] 当前表情包数量: {self.emoji_num}") + + return True + else: + logger.error(f"[错误] 删除表情包失败: {emoji_hash}") + return False + + except Exception as e: + logger.error(f"[错误] 删除表情包失败: {str(e)}") + logger.error(traceback.format_exc()) + return False + + def _emoji_objects_to_readable_list(self, emoji_objects): + """将表情包对象列表转换为可读的字符串列表 + + 参数: + emoji_objects: MaiEmoji对象列表 + + 返回: + list[str]: 可读的表情包信息字符串列表 + """ + emoji_info_list = [] + for i, emoji in enumerate(emoji_objects): + # 转换时间戳为可读时间 + time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time)) + # 构建每个表情包的信息字符串 + emoji_info = ( + f"编号: {i+1}\n" + f"描述: {emoji.description}\n" + f"使用次数: {emoji.usage_count}\n" + f"添加时间: {time_str}\n" + ) + emoji_info_list.append(emoji_info) + return emoji_info_list + + async def replace_a_emoji(self, new_emoji: MaiEmoji): + """替换一个表情包 + + Args: + new_emoji: 新表情包对象 + + Returns: + bool: 是否成功替换表情包 + """ + try: + self._ensure_db() + + # 获取所有表情包对象 + all_emojis = self.emoji_objects + + # 将表情包信息转换为可读的字符串 + emoji_info_list = self._emoji_objects_to_readable_list(all_emojis) + + # 构建提示词 + prompt = ( + f"{global_config.BOT_NICKNAME}的表情包存储已满({self.emoji_num}/{self.emoji_num_max})," + f"需要决定是否删除一个旧表情包来为新表情包腾出空间。\n\n" + f"新表情包信息:\n" + f"描述: {new_emoji.description}\n\n" + f"现有表情包列表:\n" + "\n".join(emoji_info_list) + "\n\n" + f"请决定:\n" + f"1. 是否要删除某个现有表情包来为新表情包腾出空间?\n" + f"2. 如果要删除,应该删除哪一个(给出编号)?\n" + f"请只回答:'不删除'或'删除编号X'(X为表情包编号)。" + ) + + # 调用大模型进行决策 + decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8) + logger.info(f"[决策] 大模型决策结果: {decision}") + + # 解析决策结果 + if "不删除" in decision: + logger.info("[决策] 决定不删除任何表情包") + return False + + # 尝试从决策中提取表情包编号 + match = re.search(r'删除编号(\d+)', decision) + if match: + emoji_index = int(match.group(1)) - 1 # 转换为0-based索引 + + # 检查索引是否有效 + if 0 <= emoji_index < len(all_emojis): + emoji_to_delete = all_emojis[emoji_index] + + # 删除选定的表情包 + logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}") + delete_success = await self.delete_emoji(emoji_to_delete.hash) + + if delete_success: + # 修复:等待异步注册完成 + register_success = await new_emoji.register_to_db() + if register_success: + self.emoji_objects.append(new_emoji) + self.emoji_num += 1 + logger.success(f"[成功] 注册表情包: {new_emoji.description}") + return True + else: + logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}") + return False + else: + logger.error(f"[错误] 删除表情包失败,无法完成替换") + return False + else: + logger.error(f"[错误] 无效的表情包编号: {emoji_index+1}") + else: + logger.error(f"[错误] 无法从决策中提取表情包编号: {decision}") + + return False + + except Exception as e: + logger.error(f"[错误] 替换表情包失败: {str(e)}") + logger.error(traceback.format_exc()) + return False + + async def build_emoji_description(self, image_base64: str) -> Tuple[str, list]: + """获取表情包描述和情感列表 + + Args: + image_base64: 图片的base64编码 + + Returns: + Tuple[str, list]: 返回表情包描述和情感列表 + """ + try: + # 解码图片并获取格式 + image_bytes = base64.b64decode(image_base64) + image_format = Image.open(io.BytesIO(image_bytes)).format.lower() + + # 调用AI获取描述 + if image_format == "gif" or image_format == "GIF": + image_base64 = image_manager.transform_gif(image_base64) + prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,详细描述一下表情包表达的情感和内容,请关注其幽默和讽刺意味" + description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, "jpg") + else: + prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味" + description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) + + # 审核表情包 + if global_config.EMOJI_CHECK: + prompt = f''' + 这是一个表情包,请对这个表情包进行审核,标准如下: + 1. 必须符合"{global_config.EMOJI_CHECK_PROMPT}"的要求 + 2. 不能是色情、暴力、等违法违规内容,必须符合公序良俗 + 3. 不能是任何形式的截图,聊天记录或视频截图 + 4. 不要出现5个以上文字 + 请回答这个表情包是否满足上述要求,是则回答是,否则回答否,不要出现任何其他内容 + ''' + content, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) + if content == "否": + return None, [] + + # 分析情感含义 + emotion_prompt = f''' + 基于这个表情包的描述:'{description}',请列出1-3个可能的情感标签,每个标签用一个词组表示,格式如下: + 幽默的讽刺 + 悲伤的无奈 + 愤怒的抗议 + 愤怒的讽刺 + 直接输出词组,词组检用逗号分隔。''' + emotions_text, _ = await self.llm_emotion_judge.generate_response_async(emotion_prompt, temperature=0.7) + + # 处理情感列表 + emotions = [e.strip() for e in emotions_text.split(',') if e.strip()] + + return f"[表情包:{description}]", emotions + + except Exception as e: + logger.error(f"获取表情包描述失败: {str(e)}") + return "", [] + + + async def register_emoji_by_filename(self, filename: str) -> bool: + """读取指定文件名的表情包图片,分析并注册到数据库 + + Args: + filename: 表情包文件名,必须位于EMOJI_DIR目录下 + + Returns: + bool: 注册是否成功 + """ + try: + # 使用MaiEmoji类创建表情包实例 + new_emoji = MaiEmoji(filename, EMOJI_DIR) + await new_emoji.initialize_hash_format() + emoji_base64 = image_path_to_base64(os.path.join(EMOJI_DIR, filename)) + description, emotions = await self.build_emoji_description(emoji_base64) + if description == "": + return False + new_emoji.description = description + new_emoji.emotion = emotions + + # 检查是否已经注册过 + # 对比数据库中是否存在相同哈希值的表情包 + if await self.get_emoji_from_manager(new_emoji.hash): + logger.warning(f"[警告] 表情包已存在: {filename}") + return False + + if self.emoji_num >= self.emoji_num_max: + logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})") + replaced = await self.replace_a_emoji(new_emoji) + if not replaced: + logger.error("[错误] 替换表情包失败,无法完成注册") + return False + else: + # 修复:等待异步注册完成 + register_success = await new_emoji.register_to_db() + if register_success: + self.emoji_objects.append(new_emoji) + self.emoji_num += 1 + logger.success(f"[成功] 注册表情包: {filename}") + return True + else: + logger.error(f"[错误] 注册表情包到数据库失败: {filename}") + return False + + except Exception as e: + logger.error(f"[错误] 注册表情包失败: {str(e)}") + logger.error(traceback.format_exc()) + return False + + +# 创建全局单例 +emoji_manager = EmojiManager() diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 8735ff7df..cf2081fca 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -15,7 +15,7 @@ from src.plugins.utils.timer_calculater import Timer # <--- Import Timer from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator from src.do_tool.tool_use import ToolUser from ..chat.message_sender import message_manager # <-- Import the global manager -from src.plugins.chat.emoji_manager import emoji_manager +from src.plugins.emoji_system.emoji_manager import emoji_manager from src.plugins.utils.json_utils import process_llm_tool_response # 导入新的JSON工具 from src.heart_flow.sub_mind import SubMind from src.heart_flow.observation import Observation diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 2c2a961ef..490618b7b 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -27,13 +27,15 @@ def init_prompt(): {chat_talking_prompt} 现在你想要在群里发言或者回复。\n 你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality} {prompt_identity}"。 -你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,小红书或者微博的回复风格。 -你刚刚脑子里在想: +你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,知乎或者微博的回复风格。 +看到以上聊天记录,你刚刚在想: + {current_mind_info} -{reason} +因为上述想法,你决定发言,原因是:{reason} + 回复尽量简短一些。请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人,不用指出你回复的是谁。{prompt_ger} 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要说你说过的话题 ,注意只输出回复内容。 -{moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", +{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) @@ -47,8 +49,12 @@ def init_prompt(): 请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。 决策依据: 1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。 -2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (例如:'开心的'、'惊讶的')。 -3. 如果聊天内容或你的内心想法适合用一个表情来回应(例如表示赞同、惊讶、无语等),选择 'emoji_reply' 并提供表情主题 'emoji_query'。 +2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (每个标签用一个词组表示,格式如下: + 幽默的讽刺 + 悲伤的无奈 + 愤怒的抗议 + 愤怒的讽刺)。 +3. 如果聊天内容或你的内心想法适合用一个表情来回应,选择 'emoji_reply' 并提供表情主题 'emoji_query'。 4. 如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。 5. 如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;。 6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。 diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index a3aaf3a0b..9bbdfaead 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -6,7 +6,7 @@ from typing import List, Optional # 导入 Optional from ..moods.moods import MoodManager from ...config.config import global_config -from ..chat.emoji_manager import emoji_manager +from ..emoji_system.emoji_manager import emoji_manager from .normal_chat_generator import NormalChatGenerator from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet from ..chat.message_sender import message_manager diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 5f342406e..9db3f1932 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.4.1" +version = "1.4.2" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -125,8 +125,13 @@ at_bot_inevitable_reply = false # @bot 必然回复 max_emoji_num = 90 # 表情包最大数量 max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包 check_interval = 30 # 检查表情包(注册,破损,删除)的时间间隔(分钟) + auto_save = true # 是否保存表情包和图片 +save_pic = false # 是否保存图片 +save_emoji = false # 是否保存表情包 +steal_emoji = true # 是否偷取表情包,让麦麦可以发送她保存的这些表情包 + enable_check = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 From 3ab39790474f48334a1058de35f5bb83842ce868 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 23:45:49 +0800 Subject: [PATCH 05/79] fix:ra --- src/config/config.py | 3 +- src/plugins/emoji_system/emoji_manager.py | 217 ++++++++---------- src/plugins/heartFC_chat/heartFC_chat.py | 10 +- src/plugins/heartFC_chat/heartFC_generator.py | 2 +- .../heartFC_chat/heartflow_prompt_builder.py | 13 +- src/plugins/heartFC_chat/normal_chat.py | 2 +- 6 files changed, 117 insertions(+), 130 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index 0390b056c..996b27385 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -404,8 +404,7 @@ class BotConfig: config.save_pic = emoji_config.get("save_pic", config.save_pic) config.save_emoji = emoji_config.get("save_emoji", config.save_emoji) config.steal_emoji = emoji_config.get("steal_emoji", config.steal_emoji) - - + def bot(parent: dict): # 机器人基础配置 bot_config = parent["bot"] diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index db5a31322..dea9a609d 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -12,7 +12,6 @@ import re from ...common.database import db from ...config.config import global_config -from ..chat.utils import get_embedding from ..chat.utils_image import image_path_to_base64, image_manager from ..models.utils_model import LLMRequest from src.common.logger import get_module_logger, LogConfig, EMOJI_STYLE_CONFIG @@ -31,6 +30,7 @@ EMOJI_REGISTED_DIR = os.path.join("data", "emoji_registed") # 已注册的表 class MaiEmoji: """定义一个表情包""" + def __init__(self, filename: str, path: str): self.path = path # 存储目录路径 self.filename = filename @@ -43,13 +43,13 @@ class MaiEmoji: self.register_time = time.time() self.is_deleted = False # 标记是否已被删除 self.format = "" - + async def initialize_hash_format(self): """从文件创建表情包实例 - + 参数: file_path: 文件的完整路径 - + 返回: MaiEmoji: 创建的表情包实例,如果失败则返回None """ @@ -58,26 +58,24 @@ class MaiEmoji: if not os.path.exists(file_path): logger.error(f"[错误] 表情包文件不存在: {file_path}") return None - + image_base64 = image_path_to_base64(file_path) if image_base64 is None: logger.error(f"[错误] 无法读取图片: {file_path}") return None - # 计算哈希值 image_bytes = base64.b64decode(image_base64) self.hash = hashlib.md5(image_bytes).hexdigest() - + # 获取图片格式 self.format = Image.open(io.BytesIO(image_bytes)).format.lower() - - + except Exception as e: logger.error(f"[错误] 初始化表情包失败: {str(e)}") logger.error(traceback.format_exc()) return None - + async def register_to_db(self): """ 注册表情包 @@ -110,30 +108,26 @@ class MaiEmoji: self.path = EMOJI_REGISTED_DIR except Exception as move_error: logger.error(f"[错误] 移动文件失败: {str(move_error)}") - return False # 文件移动失败,不继续 + return False # 文件移动失败,不继续 # --- 数据库操作 --- try: # 准备数据库记录 for emoji collection emoji_record = { "filename": self.filename, - "path": os.path.join(self.path, self.filename), # 使用更新后的路径 + "path": os.path.join(self.path, self.filename), # 使用更新后的路径 "embedding": self.embedding, "description": self.description, "emotion": self.emotion, # 添加情感标签字段 "hash": self.hash, "format": self.format, - "timestamp": int(self.register_time), # 使用实例的注册时间 + "timestamp": int(self.register_time), # 使用实例的注册时间 "usage_count": self.usage_count, - "last_used_time": self.last_used_time + "last_used_time": self.last_used_time, } # 使用upsert确保记录存在或被更新 - db["emoji"].update_one( - {"hash": self.hash}, - {"$set": emoji_record}, - upsert=True - ) + db["emoji"].update_one({"hash": self.hash}, {"$set": emoji_record}, upsert=True) logger.success(f"[注册] 表情包信息保存到数据库: {self.description}") return True @@ -147,12 +141,12 @@ class MaiEmoji: logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(traceback.format_exc()) return False - + async def delete(self): """删除表情包 - + 删除表情包的文件和数据库记录 - + 返回: bool: 是否成功删除 """ @@ -165,21 +159,21 @@ class MaiEmoji: except Exception as e: logger.error(f"[错误] 删除文件失败 {os.path.join(self.path, self.filename)}: {str(e)}") # 继续执行,即使文件删除失败也尝试删除数据库记录 - + # 2. 删除数据库记录 result = db.emoji.delete_one({"hash": self.hash}) deleted_in_db = result.deleted_count > 0 - + if deleted_in_db: logger.success(f"[删除] 成功删除表情包记录: {self.description}") - + # 3. 标记对象已被删除 self.is_deleted = True return True else: logger.error(f"[错误] 删除表情包记录失败: {self.hash}") return False - + except Exception as e: logger.error(f"[错误] 删除表情包失败: {str(e)}") return False @@ -188,7 +182,6 @@ class MaiEmoji: class EmojiManager: _instance = None - def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) @@ -213,7 +206,6 @@ class EmojiManager: """确保表情存储目录存在""" os.makedirs(EMOJI_DIR, exist_ok=True) - def initialize(self): """初始化数据库连接和表情目录""" if not self._initialized: @@ -286,7 +278,7 @@ class EmojiManager: emotions = emoji.emotion if not emotions: continue - + # 计算与每个emotion标签的相似度,取最大值 max_similarity = 0 for emotion in emotions: @@ -295,7 +287,7 @@ class EmojiManager: max_len = max(len(text_emotion), len(emotion)) similarity = 1 - (distance / max_len if max_len > 0 else 0) max_similarity = max(max_similarity, similarity) - + emoji_similarities.append((emoji, max_similarity)) # 按相似度降序排序 @@ -314,10 +306,8 @@ class EmojiManager: # 更新使用次数 db.emoji.update_one({"hash": selected_emoji.hash}, {"$inc": {"usage_count": 1}}) - logger.info( - f"[匹配] 找到表情包: {selected_emoji.description} (相似度: {similarity:.4f})" - ) - + logger.info(f"[匹配] 找到表情包: {selected_emoji.description} (相似度: {similarity:.4f})") + time_end = time.time() logger.info(f"[匹配] 搜索表情包用时: {time_end - time_start:.2f} 秒") return os.path.join(selected_emoji.path, selected_emoji.filename), f"[ {selected_emoji.description} ]" @@ -328,11 +318,11 @@ class EmojiManager: def _levenshtein_distance(self, s1: str, s2: str) -> int: """计算两个字符串的编辑距离 - + Args: s1: 第一个字符串 s2: 第二个字符串 - + Returns: int: 编辑距离 """ @@ -363,7 +353,7 @@ class EmojiManager: if not self.emoji_objects: logger.warning("[检查] emoji_objects为空,跳过完整性检查") return - + total_count = len(self.emoji_objects) removed_count = 0 # 使用列表复制进行遍历,因为我们会在遍历过程中修改列表 @@ -403,7 +393,7 @@ class EmojiManager: logger.info("[扫描] 开始检查表情包完整性...") self.check_emoji_file_integrity() logger.info("[扫描] 开始扫描新表情包...") - + # 检查表情包目录是否存在 if not os.path.exists(EMOJI_DIR): logger.warning(f"[警告] 表情包目录不存在: {EMOJI_DIR}") @@ -411,24 +401,27 @@ class EmojiManager: logger.info(f"[创建] 已创建表情包目录: {EMOJI_DIR}") await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) continue - + # 检查目录是否为空 files = os.listdir(EMOJI_DIR) if not files: logger.warning(f"[警告] 表情包目录为空: {EMOJI_DIR}") await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) continue - + # 检查是否需要处理表情包(数量超过最大值或不足) - if (self.emoji_num > self.emoji_num_max and global_config.max_reach_deletion) or (self.emoji_num < self.emoji_num_max): + if (self.emoji_num > self.emoji_num_max and global_config.max_reach_deletion) or ( + self.emoji_num < self.emoji_num_max + ): try: # 获取目录下所有图片文件 files_to_process = [ - f for f in files - if os.path.isfile(os.path.join(EMOJI_DIR, f)) + f + for f in files + if os.path.isfile(os.path.join(EMOJI_DIR, f)) and f.lower().endswith((".jpg", ".jpeg", ".png", ".gif")) ] - + # 处理每个符合条件的文件 for filename in files_to_process: # 尝试注册表情包 @@ -443,24 +436,24 @@ class EmojiManager: logger.warning(f"[清理] 删除注册失败的表情包文件: {filename}") except Exception as e: logger.error(f"[错误] 扫描表情包目录失败: {str(e)}") - + await asyncio.sleep(global_config.EMOJI_CHECK_INTERVAL * 60) async def get_all_emoji_from_db(self): """获取所有表情包并初始化为MaiEmoji类对象 - + 参数: hash: 可选,如果提供则只返回指定哈希值的表情包 - + 返回: list[MaiEmoji]: 表情包对象列表 """ try: self._ensure_db() - + # 获取所有表情包 all_emoji_data = list(db.emoji.find()) - + # 将数据库记录转换为MaiEmoji对象 emoji_objects = [] for emoji_data in all_emoji_data: @@ -468,7 +461,7 @@ class EmojiManager: filename=emoji_data.get("filename", ""), path=emoji_data.get("path", ""), ) - + # 设置额外属性 emoji.usage_count = emoji_data.get("usage_count", 0) emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) @@ -476,33 +469,33 @@ class EmojiManager: emoji.description = emoji_data.get("description", "") emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载 emoji_objects.append(emoji) - + # 存储到EmojiManager中 self.emoji_objects = emoji_objects - + except Exception as e: logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") - + async def get_emoji_from_db(self, hash=None): """获取所有表情包并初始化为MaiEmoji类对象 - + 参数: hash: 可选,如果提供则只返回指定哈希值的表情包 - + 返回: list[MaiEmoji]: 表情包对象列表 """ try: self._ensure_db() - + # 准备查询条件 query = {} if hash: query = {"hash": hash} - + # 获取所有表情包 all_emoji_data = list(db.emoji.find(query)) - + # 将数据库记录转换为MaiEmoji对象 emoji_objects = [] for emoji_data in all_emoji_data: @@ -510,28 +503,28 @@ class EmojiManager: filename=emoji_data.get("filename", ""), path=emoji_data.get("path", ""), ) - + # 设置额外属性 emoji.usage_count = emoji_data.get("usage_count", 0) emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) emoji.register_time = emoji_data.get("timestamp", time.time()) emoji.description = emoji_data.get("description", "") emoji.emotion = emoji_data.get("emotion", []) # 添加情感标签的加载 - + emoji_objects.append(emoji) - + # 存储到EmojiManager中 self.emoji_objects = emoji_objects - + return emoji_objects - + except Exception as e: logger.error(f"[错误] 获取所有表情包对象失败: {str(e)}") return [] async def get_emoji_from_manager(self, hash) -> MaiEmoji: """从EmojiManager中获取表情包 - + 参数: hash:如果提供则只返回指定哈希值的表情包 """ @@ -539,43 +532,41 @@ class EmojiManager: if emoji.hash == hash: return emoji return None - - - + async def delete_emoji(self, emoji_hash: str) -> bool: """根据哈希值删除表情包 - + Args: emoji_hash: 表情包的哈希值 - + Returns: bool: 是否成功删除 """ try: self._ensure_db() - + # 从emoji_objects中查找表情包对象 emoji = await self.get_emoji_from_manager(emoji_hash) - + if not emoji: logger.warning(f"[警告] 未找到哈希值为 {emoji_hash} 的表情包") return False - + # 使用MaiEmoji对象的delete方法删除表情包 success = await emoji.delete() - + if success: # 从emoji_objects列表中移除该对象 self.emoji_objects = [e for e in self.emoji_objects if e.hash != emoji_hash] # 更新计数 self.emoji_num -= 1 logger.info(f"[统计] 当前表情包数量: {self.emoji_num}") - + return True else: logger.error(f"[错误] 删除表情包失败: {emoji_hash}") return False - + except Exception as e: logger.error(f"[错误] 删除表情包失败: {str(e)}") logger.error(traceback.format_exc()) @@ -583,10 +574,10 @@ class EmojiManager: def _emoji_objects_to_readable_list(self, emoji_objects): """将表情包对象列表转换为可读的字符串列表 - + 参数: emoji_objects: MaiEmoji对象列表 - + 返回: list[str]: 可读的表情包信息字符串列表 """ @@ -596,32 +587,29 @@ class EmojiManager: time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(emoji.register_time)) # 构建每个表情包的信息字符串 emoji_info = ( - f"编号: {i+1}\n" - f"描述: {emoji.description}\n" - f"使用次数: {emoji.usage_count}\n" - f"添加时间: {time_str}\n" + f"编号: {i + 1}\n描述: {emoji.description}\n使用次数: {emoji.usage_count}\n添加时间: {time_str}\n" ) emoji_info_list.append(emoji_info) return emoji_info_list async def replace_a_emoji(self, new_emoji: MaiEmoji): """替换一个表情包 - + Args: new_emoji: 新表情包对象 - + Returns: bool: 是否成功替换表情包 """ try: self._ensure_db() - + # 获取所有表情包对象 all_emojis = self.emoji_objects - + # 将表情包信息转换为可读的字符串 emoji_info_list = self._emoji_objects_to_readable_list(all_emojis) - + # 构建提示词 prompt = ( f"{global_config.BOT_NICKNAME}的表情包存储已满({self.emoji_num}/{self.emoji_num_max})," @@ -629,34 +617,34 @@ class EmojiManager: f"新表情包信息:\n" f"描述: {new_emoji.description}\n\n" f"现有表情包列表:\n" + "\n".join(emoji_info_list) + "\n\n" - f"请决定:\n" - f"1. 是否要删除某个现有表情包来为新表情包腾出空间?\n" - f"2. 如果要删除,应该删除哪一个(给出编号)?\n" - f"请只回答:'不删除'或'删除编号X'(X为表情包编号)。" + "请决定:\n" + "1. 是否要删除某个现有表情包来为新表情包腾出空间?\n" + "2. 如果要删除,应该删除哪一个(给出编号)?\n" + "请只回答:'不删除'或'删除编号X'(X为表情包编号)。" ) - + # 调用大模型进行决策 decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.8) logger.info(f"[决策] 大模型决策结果: {decision}") - + # 解析决策结果 if "不删除" in decision: logger.info("[决策] 决定不删除任何表情包") return False - + # 尝试从决策中提取表情包编号 - match = re.search(r'删除编号(\d+)', decision) + match = re.search(r"删除编号(\d+)", decision) if match: emoji_index = int(match.group(1)) - 1 # 转换为0-based索引 - + # 检查索引是否有效 if 0 <= emoji_index < len(all_emojis): emoji_to_delete = all_emojis[emoji_index] - + # 删除选定的表情包 logger.info(f"[决策] 决定删除表情包: {emoji_to_delete.description}") delete_success = await self.delete_emoji(emoji_to_delete.hash) - + if delete_success: # 修复:等待异步注册完成 register_success = await new_emoji.register_to_db() @@ -669,26 +657,26 @@ class EmojiManager: logger.error(f"[错误] 注册表情包到数据库失败: {new_emoji.filename}") return False else: - logger.error(f"[错误] 删除表情包失败,无法完成替换") + logger.error("[错误] 删除表情包失败,无法完成替换") return False else: - logger.error(f"[错误] 无效的表情包编号: {emoji_index+1}") + logger.error(f"[错误] 无效的表情包编号: {emoji_index + 1}") else: logger.error(f"[错误] 无法从决策中提取表情包编号: {decision}") - + return False - + except Exception as e: logger.error(f"[错误] 替换表情包失败: {str(e)}") logger.error(traceback.format_exc()) return False - + async def build_emoji_description(self, image_base64: str) -> Tuple[str, list]: """获取表情包描述和情感列表 - + Args: image_base64: 图片的base64编码 - + Returns: Tuple[str, list]: 返回表情包描述和情感列表 """ @@ -705,7 +693,7 @@ class EmojiManager: else: prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,请关注其幽默和讽刺意味" description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) - + # 审核表情包 if global_config.EMOJI_CHECK: prompt = f''' @@ -721,31 +709,30 @@ class EmojiManager: return None, [] # 分析情感含义 - emotion_prompt = f''' + emotion_prompt = f""" 基于这个表情包的描述:'{description}',请列出1-3个可能的情感标签,每个标签用一个词组表示,格式如下: 幽默的讽刺 悲伤的无奈 愤怒的抗议 愤怒的讽刺 - 直接输出词组,词组检用逗号分隔。''' + 直接输出词组,词组检用逗号分隔。""" emotions_text, _ = await self.llm_emotion_judge.generate_response_async(emotion_prompt, temperature=0.7) - + # 处理情感列表 - emotions = [e.strip() for e in emotions_text.split(',') if e.strip()] + emotions = [e.strip() for e in emotions_text.split(",") if e.strip()] return f"[表情包:{description}]", emotions - + except Exception as e: logger.error(f"获取表情包描述失败: {str(e)}") return "", [] - async def register_emoji_by_filename(self, filename: str) -> bool: """读取指定文件名的表情包图片,分析并注册到数据库 - + Args: filename: 表情包文件名,必须位于EMOJI_DIR目录下 - + Returns: bool: 注册是否成功 """ @@ -765,7 +752,7 @@ class EmojiManager: if await self.get_emoji_from_manager(new_emoji.hash): logger.warning(f"[警告] 表情包已存在: {filename}") return False - + if self.emoji_num >= self.emoji_num_max: logger.warning(f"表情包数量已达到上限({self.emoji_num}/{self.emoji_num_max})") replaced = await self.replace_a_emoji(new_emoji) @@ -783,7 +770,7 @@ class EmojiManager: else: logger.error(f"[错误] 注册表情包到数据库失败: {filename}") return False - + except Exception as e: logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(traceback.format_exc()) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index cf2081fca..ab80beaab 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -595,12 +595,12 @@ class HeartFChatting: self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any] ) -> str: """构建 Planner LLM 的提示词""" - + # 准备结构化信息块 structured_info_block = "" if structured_info: structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" - + # 准备聊天内容块 chat_content_block = "" if observed_messages_str: @@ -609,14 +609,14 @@ class HeartFChatting: chat_content_block += "\n---" else: chat_content_block = "当前没有观察到新的聊天内容。\n" - + # 准备当前思维块 current_mind_block = "" if current_mind: current_mind_block = f"\n---\n{current_mind}\n---\n\n" else: current_mind_block = " [没有特别的想法] \n\n" - + # 获取提示词模板并填充数据 prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( bot_name=global_config.BOT_NICKNAME, @@ -624,7 +624,7 @@ class HeartFChatting: chat_content_block=chat_content_block, current_mind_block=current_mind_block, ) - + return prompt # --- 回复器 (Replier) 的定义 --- # diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index 464e94e91..6b5aaaa37 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -78,7 +78,7 @@ class HeartFCGenerator: ) -> str: info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - with Timer() as t_build_prompt: + with Timer() as _t_build_prompt: prompt = await prompt_builder.build_prompt( build_mode="focus", reason=reason, diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 490618b7b..aaeade54d 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -38,7 +38,7 @@ def init_prompt(): {moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) - + # Planner提示词 Prompt( """你的名字是 {bot_name}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。 @@ -62,7 +62,7 @@ def init_prompt(): 必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。""", "planner_prompt", ) - + Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") @@ -122,13 +122,14 @@ class PromptBuilder: elif build_mode == "focus": return await self._build_prompt_focus( - reason, current_mind_info, structured_info, chat_stream, + reason, + current_mind_info, + structured_info, + chat_stream, ) return None - async def _build_prompt_focus( - self, reason, current_mind_info, structured_info, chat_stream - ) -> tuple[str, str]: + async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1) prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1) diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 9bbdfaead..2ba5d79d4 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -402,7 +402,7 @@ class NormalChat: # 确保任务状态更新,即使等待出错 (回调函数也会尝试更新) if self._chat_task is task: self._chat_task = None - + # 清理所有未处理的思考消息 try: container = await message_manager.get_container(self.stream_id) From e24b7cedcbaef01623fb732ba9bfe74b335790d2 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 00:02:24 +0800 Subject: [PATCH 06/79] =?UTF-8?q?fix=EF=BC=9A=E8=A1=A8=E6=83=85=E5=8C=85?= =?UTF-8?q?=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils_image.py | 2 ++ src/plugins/emoji_system/emoji_manager.py | 2 +- src/plugins/heartFC_chat/heartflow_prompt_builder.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index bf549b97e..f8ff15aa0 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -12,6 +12,7 @@ from ...config.config import global_config from ..models.utils_model import LLMRequest from src.common.logger import get_module_logger +import traceback logger = get_module_logger("chat_image") @@ -316,4 +317,5 @@ def image_path_to_base64(image_path: str) -> str: return base64.b64encode(image_data).decode("utf-8") except Exception as e: logger.error(f"读取图片失败: {image_path}, 错误: {str(e)}") + traceback.print_exc() return None diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index dea9a609d..aa3d7506c 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -310,7 +310,7 @@ class EmojiManager: time_end = time.time() logger.info(f"[匹配] 搜索表情包用时: {time_end - time_start:.2f} 秒") - return os.path.join(selected_emoji.path, selected_emoji.filename), f"[ {selected_emoji.description} ]" + return selected_emoji.path, f"[ {selected_emoji.description} ]" except Exception as e: logger.error(f"[错误] 获取表情包失败: {str(e)}") diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 2478cd678..102aef52b 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -87,7 +87,7 @@ def init_prompt(): 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 -{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", +{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。,只输出回复内容""", "reasoning_prompt_main", ) Prompt( From 630c334c4aac0a0014e4c07e408cc9c25fb49bb9 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 00:39:34 +0800 Subject: [PATCH 07/79] =?UTF-8?q?fix=EF=BC=9A=E5=93=88=E5=B8=8C=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 22 +++++++++++++++++++++- src/plugins/chat/utils_image.py | 19 +++++++++++-------- src/plugins/emoji_system/emoji_manager.py | 9 +++++---- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 2fc1cbb17..f69a95222 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -480,7 +480,7 @@ MAI_STATE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}", }, "simple": { - "console_format": "{time:MM-DD HH:mm} | 麦麦状态 | {message} ", # noqa: E501 + "console_format": "{time:MM-DD HH:mm} | 麦麦状态 | {message} ", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦状态 | {message}", }, } @@ -528,6 +528,25 @@ CONFIRM_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}", } +# 天依蓝配置 +TIANYI_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "天依 | " + "{message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}", + }, + "simple": { + "console_format": ( + "{time:MM-DD HH:mm} | 天依 | {message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 天依 | {message}", + }, +} + # 根据SIMPLE_OUTPUT选择配置 MAIN_STYLE_CONFIG = MAIN_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MAIN_STYLE_CONFIG["advanced"] EMOJI_STYLE_CONFIG = EMOJI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else EMOJI_STYLE_CONFIG["advanced"] @@ -563,6 +582,7 @@ TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TO PFC_STYLE_CONFIG = PFC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_STYLE_CONFIG["advanced"] LPMM_STYLE_CONFIG = LPMM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LPMM_STYLE_CONFIG["advanced"] INTEREST_STYLE_CONFIG = INTEREST_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else INTEREST_STYLE_CONFIG["advanced"] +TIANYI_STYLE_CONFIG = TIANYI_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TIANYI_STYLE_CONFIG["advanced"] def is_registered_module(record: dict) -> bool: diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index f8ff15aa0..24572ed29 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -310,12 +310,15 @@ def image_path_to_base64(image_path: str) -> str: image_path: 图片文件路径 Returns: str: base64编码的图片数据 + Raises: + FileNotFoundError: 当图片文件不存在时 + IOError: 当读取图片文件失败时 """ - try: - with open(image_path, "rb") as f: - image_data = f.read() - return base64.b64encode(image_data).decode("utf-8") - except Exception as e: - logger.error(f"读取图片失败: {image_path}, 错误: {str(e)}") - traceback.print_exc() - return None + if not os.path.exists(image_path): + raise FileNotFoundError(f"图片文件不存在: {image_path}") + + with open(image_path, "rb") as f: + image_data = f.read() + if not image_data: + raise IOError(f"读取图片文件失败: {image_path}") + return base64.b64encode(image_data).decode("utf-8") diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index aa3d7506c..7222fd3f2 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -215,7 +215,7 @@ class EmojiManager: self._initialized = True # 更新表情包数量 # 启动时执行一次完整性检查 - self.check_emoji_file_integrity() + # await self.check_emoji_file_integrity() except Exception: logger.exception("初始化表情管理器失败") @@ -391,7 +391,7 @@ class EmojiManager: await self.get_all_emoji_from_db() while True: logger.info("[扫描] 开始检查表情包完整性...") - self.check_emoji_file_integrity() + await self.check_emoji_file_integrity() logger.info("[扫描] 开始扫描新表情包...") # 检查表情包目录是否存在 @@ -463,6 +463,7 @@ class EmojiManager: ) # 设置额外属性 + emoji.hash = emoji_data.get("hash", "") emoji.usage_count = emoji_data.get("usage_count", 0) emoji.last_used_time = emoji_data.get("last_used_time", emoji_data.get("timestamp", time.time())) emoji.register_time = emoji_data.get("timestamp", time.time()) @@ -710,7 +711,7 @@ class EmojiManager: # 分析情感含义 emotion_prompt = f""" - 基于这个表情包的描述:'{description}',请列出1-3个可能的情感标签,每个标签用一个词组表示,格式如下: + 基于这个表情包的描述:'{description}',请列出1-2个可能的情感标签,每个标签用一个词组表示,格式如下: 幽默的讽刺 悲伤的无奈 愤怒的抗议 @@ -748,7 +749,7 @@ class EmojiManager: new_emoji.emotion = emotions # 检查是否已经注册过 - # 对比数据库中是否存在相同哈希值的表情包 + # 对比内存中是否存在相同哈希值的表情包 if await self.get_emoji_from_manager(new_emoji.hash): logger.warning(f"[警告] 表情包已存在: {filename}") return False From 5ba36b6267de23a9107a8ba4de341fdd84183331 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 01:44:20 +0800 Subject: [PATCH 08/79] =?UTF-8?q?fix=EF=BC=9A=E5=82=BB=E9=80=BC=E6=8B=AC?= =?UTF-8?q?=E5=8F=B7=E5=92=8C=E6=8D=A2=E8=A1=8C=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat/utils.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index aed0025b8..60bb4d8c5 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -234,6 +234,13 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: Returns: List[str]: 分割和合并后的句子列表 """ + # 预处理:处理多余的换行符 + # 1. 将连续的换行符替换为单个换行符 + text = re.sub(r'\n\s*\n+', '\n', text) + # 2. 处理换行符和其他分隔符的组合 + text = re.sub(r'\n\s*([,,。;\s])', r'\1', text) + text = re.sub(r'([,,。;\s])\s*\n', r'\1', text) + # 处理两个汉字中间的换行符 text = re.sub(r"([\u4e00-\u9fff])\n([\u4e00-\u9fff])", r"\1。\2", text) @@ -370,7 +377,7 @@ def process_llm_response(text: str) -> List[str]: # 提取被 () 或 [] 包裹且包含中文的内容 pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]") # _extracted_contents = pattern.findall(text) - extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找 + _extracted_contents = pattern.findall(protected_text) # 在保护后的文本上查找 # 去除 () 和 [] 及其包裹的内容 cleaned_text = pattern.sub("", protected_text) @@ -413,13 +420,16 @@ def process_llm_response(text: str) -> List[str]: if len(sentences) > max_sentence_num: logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") return [f"{global_config.BOT_NICKNAME}不知道哦"] - if extracted_contents: - for content in extracted_contents: - sentences.append(content) + + # if extracted_contents: + # for content in extracted_contents: + # sentences.append(content) + + # 在所有句子处理完毕后,对包含占位符的列表进行恢复 sentences = recover_kaomoji(sentences, kaomoji_mapping) - print(sentences) + # print(sentences) return sentences From 60b3c1a7cb2616a1293180e91d4e959bcbccea31 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 02:10:05 +0800 Subject: [PATCH 09/79] =?UTF-8?q?feat=EF=BC=9A=E4=BA=94=E9=A2=9C=E5=85=AD?= =?UTF-8?q?=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 163 ++++++++++++++++++++++-------- src/config/config.py | 2 +- src/heart_flow/interest_logger.py | 4 +- src/heart_flow/mind.py | 9 +- src/heart_flow/sub_heartflow.py | 35 +++---- 5 files changed, 143 insertions(+), 70 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index f69a95222..30a97e927 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -5,7 +5,58 @@ import os from types import ModuleType from pathlib import Path from dotenv import load_dotenv -# from ..plugins.chat.config import global_config +''' +日志颜色说明: + +1. 主程序(Main) +浅黄色标题 | 浅黄色消息 + +2. 海马体(Memory) +浅黄色标题 | 浅黄色消息 + +3. PFC(前额叶皮质) +浅绿色标题 | 浅绿色消息 + +4. 心情(Mood) +品红色标题 | 品红色消息 + +5. 工具使用(Tool) +品红色标题 | 品红色消息 + +6. 关系(Relation) +浅品红色标题 | 浅品红色消息 + +7. 配置(Config) +浅青色标题 | 浅青色消息 + +8. 麦麦大脑袋 +浅绿色标题 | 浅绿色消息 + +9. 在干嘛 +青色标题 | 青色消息 + +10. 麦麦组织语言 +浅绿色标题 | 浅绿色消息 + +11. 见闻(Chat) +浅蓝色标题 | 绿色消息 + +12. 表情包(Emoji) +橙色标题 | 橙色消息 fg #FFD700 + +13. 子心流 + +13. 其他模块 +模块名标题 | 对应颜色消息 + + +注意: +1. 级别颜色遵循loguru默认配置 +2. 可通过环境变量修改日志级别 +''' + + + # 加载 .env 文件 env_path = Path(__file__).resolve().parent.parent.parent / ".env" @@ -88,25 +139,6 @@ MAIN_STYLE_CONFIG = { }, } -# 海马体日志样式配置 -MEMORY_STYLE_CONFIG = { - "advanced": { - "console_format": ( - "{time:YYYY-MM-DD HH:mm:ss} | " - "{level: <8} | " - "海马体 | " - "{message}" - ), - "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}", - }, - "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 海马体 | {message}" - ), - "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}", - }, -} - # pfc配置 PFC_STYLE_CONFIG = { "advanced": { @@ -314,6 +346,24 @@ REMOTE_STYLE_CONFIG = { } SUB_HEARTFLOW_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "麦麦水群 | " + "{message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", + }, + "simple": { + "console_format": ( + "{time:MM-DD HH:mm} | 麦麦水群 | {message}" + ), # noqa: E501 + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}", + }, +} + +SUB_HEARTFLOW_MIND_STYLE_CONFIG = { "advanced": { "console_format": ( "{time:YYYY-MM-DD HH:mm:ss} | " @@ -325,12 +375,30 @@ SUB_HEARTFLOW_STYLE_CONFIG = { }, "simple": { "console_format": ( - "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}" + "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}" ), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, } +SUBHEARTFLOW_MANAGER_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "麦麦水群[管理] | " + "{message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", + }, + "simple": { + "console_format": ( + "{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}" + ), # noqa: E501 + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", + }, +} + BASE_TOOL_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -403,24 +471,6 @@ BACKGROUND_TASKS_STYLE_CONFIG = { }, } -SUBHEARTFLOW_MANAGER_STYLE_CONFIG = { - "advanced": { - "console_format": ( - "{time:YYYY-MM-DD HH:mm:ss} | " - "{level: <8} | " - "小脑袋管理 | " - "{message}" - ), - "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}", - }, - "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 小脑袋管理 | {message}" - ), # noqa: E501 - "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 小脑袋管理 | {message}", - }, -} - WILLING_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -453,19 +503,20 @@ PFC_ACTION_PLANNER_STYLE_CONFIG = { }, } +# EMOJI,橙色,全着色 EMOJI_STYLE_CONFIG = { "advanced": { "console_format": ( "{time:YYYY-MM-DD HH:mm:ss} | " "{level: <8} | " - "表情 | " + "表情包 | " "{message}" ), - "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}", + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情包 | {message}", }, "simple": { - "console_format": "{time:MM-DD HH:mm} | 表情 | {message} ", # noqa: E501 - "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情 | {message}", + "console_format": "{time:MM-DD HH:mm} | 表情包 | {message} ", # noqa: E501 + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 表情包 | {message}", }, } @@ -485,6 +536,27 @@ MAI_STATE_CONFIG = { }, } + +# 海马体日志样式配置 +MEMORY_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "海马体 | " + "{message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}", + }, + "simple": { + "console_format": ( + "{time:MM-DD HH:mm} | 海马体 | {message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 海马体 | {message}", + }, +} + + # LPMM配置 LPMM_STYLE_CONFIG = { "advanced": { @@ -498,7 +570,7 @@ LPMM_STYLE_CONFIG = { }, "simple": { "console_format": ( - "{time:MM-DD HH:mm} | LPMM | {message}" + "{time:MM-DD HH:mm} | LPMM | {message}" ), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | LPMM | {message}", }, @@ -575,6 +647,9 @@ HEARTFLOW_STYLE_CONFIG = HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_STYLE_CONFIG = ( SUB_HEARTFLOW_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_STYLE_CONFIG["advanced"] ) # noqa: E501 +SUB_HEARTFLOW_MIND_STYLE_CONFIG = ( + SUB_HEARTFLOW_MIND_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SUB_HEARTFLOW_MIND_STYLE_CONFIG["advanced"] +) WILLING_STYLE_CONFIG = WILLING_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else WILLING_STYLE_CONFIG["advanced"] MAI_STATE_CONFIG = MAI_STATE_CONFIG["simple"] if SIMPLE_OUTPUT else MAI_STATE_CONFIG["advanced"] CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG_STYLE_CONFIG["advanced"] diff --git a/src/config/config.py b/src/config/config.py index 996b27385..1cc58f71b 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -28,7 +28,7 @@ logger = get_module_logger("config", config=config_config) # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 is_test = True mai_version_main = "0.6.3" -mai_version_fix = "snapshot-4" +mai_version_fix = "snapshot-5" if mai_version_fix: if is_test: diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 05a7da39d..62063f073 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -54,7 +54,7 @@ class InterestLogger: results = {} if not all_flows: - logger.debug("未找到任何子心流状态") + # logger.debug("未找到任何子心流状态") return results for subheartflow in all_flows: @@ -109,7 +109,7 @@ class InterestLogger: } if not all_subflow_states: - logger.debug("没有获取到任何子心流状态,仅记录主心流状态") + # logger.debug("没有获取到任何子心流状态,仅记录主心流状态") with open(self._history_log_file_path, "a", encoding="utf-8") as f: f.write(json.dumps(log_entry_base, ensure_ascii=False) + "\n") return diff --git a/src/heart_flow/mind.py b/src/heart_flow/mind.py index e806d18ae..a40ee6ef7 100644 --- a/src/heart_flow/mind.py +++ b/src/heart_flow/mind.py @@ -1,7 +1,7 @@ import traceback from typing import TYPE_CHECKING -from src.common.logger import get_module_logger +from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_MIND_STYLE_CONFIG from src.plugins.models.utils_model import LLMRequest from src.individuality.individuality import Individuality from src.plugins.utils.prompt_builder import global_prompt_manager @@ -12,7 +12,12 @@ if TYPE_CHECKING: from src.heart_flow.subheartflow_manager import SubHeartflowManager from src.heart_flow.mai_state_manager import MaiStateInfo -logger = get_module_logger("mind") +mind_log_config = LogConfig( + console_format=SUB_HEARTFLOW_MIND_STYLE_CONFIG["console_format"], + file_format=SUB_HEARTFLOW_MIND_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("mind", config=mind_log_config) class Mind: diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 7a6e009c7..7397a37f3 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -26,13 +26,6 @@ subheartflow_config = LogConfig( ) logger = get_module_logger("subheartflow", config=subheartflow_config) -interest_log_config = LogConfig( - console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"], - file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"], -) -interest_logger = get_module_logger("InterestChatting", config=interest_log_config) - - base_reply_probability = 0.05 probability_increase_rate_per_second = 0.08 max_reply_probability = 1 @@ -97,7 +90,7 @@ class InterestChatting: # 异常情况处理 if self.decay_rate_per_second <= 0: - interest_logger.warning(f"衰减率({self.decay_rate_per_second})无效,重置兴趣值为0") + logger.warning(f"衰减率({self.decay_rate_per_second})无效,重置兴趣值为0") self.interest_level = 0.0 return @@ -106,7 +99,7 @@ class InterestChatting: decay_factor = math.pow(self.decay_rate_per_second, self.update_interval) self.interest_level *= decay_factor except ValueError as e: - interest_logger.error( + logger.error( f"衰减计算错误: {e} 参数: 衰减率={self.decay_rate_per_second} 时间差={self.update_interval} 当前兴趣={self.interest_level}" ) self.interest_level = 0.0 @@ -161,46 +154,46 @@ class InterestChatting: # 正常超时,继续循环 continue except asyncio.CancelledError: - interest_logger.info("InterestChatting 更新循环被取消。") + logger.info("InterestChatting 更新循环被取消。") break except Exception as e: - interest_logger.error(f"InterestChatting 更新循环出错: {e}") - interest_logger.error(traceback.format_exc()) + logger.error(f"InterestChatting 更新循环出错: {e}") + logger.error(traceback.format_exc()) # 防止错误导致CPU飙升,稍作等待 await asyncio.sleep(5) - interest_logger.info("InterestChatting 更新循环已停止。") + logger.info("InterestChatting 更新循环已停止。") def start_updates(self, update_interval: float = 1.0): """启动后台更新任务""" if self.update_task is None or self.update_task.done(): self._stop_event.clear() self.update_task = asyncio.create_task(self._run_update_loop(update_interval)) - interest_logger.debug("后台兴趣更新任务已创建并启动。") + logger.debug("后台兴趣更新任务已创建并启动。") else: - interest_logger.debug("后台兴趣更新任务已在运行中。") + logger.debug("后台兴趣更新任务已在运行中。") async def stop_updates(self): """停止后台更新任务""" if self.update_task and not self.update_task.done(): - interest_logger.info("正在停止 InterestChatting 后台更新任务...") + logger.info("正在停止 InterestChatting 后台更新任务...") self._stop_event.set() # 发送停止信号 try: # 等待任务结束,设置超时 await asyncio.wait_for(self.update_task, timeout=5.0) - interest_logger.info("InterestChatting 后台更新任务已成功停止。") + logger.info("InterestChatting 后台更新任务已成功停止。") except asyncio.TimeoutError: - interest_logger.warning("停止 InterestChatting 后台任务超时,尝试取消...") + logger.warning("停止 InterestChatting 后台任务超时,尝试取消...") self.update_task.cancel() try: await self.update_task # 等待取消完成 except asyncio.CancelledError: - interest_logger.info("InterestChatting 后台更新任务已被取消。") + logger.info("InterestChatting 后台更新任务已被取消。") except Exception as e: - interest_logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}") + logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}") finally: self.update_task = None else: - interest_logger.debug("InterestChatting 后台更新任务未运行或已完成。") + logger.debug("InterestChatting 后台更新任务未运行或已完成。") # --- 结束 新增方法 --- From 1e7508214118d365d3b7688eab69476192928dff Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 02:11:21 +0800 Subject: [PATCH 10/79] fix:ruff --- src/common/logger.py | 19 ++++++------------- src/plugins/chat/utils.py | 13 ++++++------- src/plugins/chat/utils_image.py | 3 +-- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 30a97e927..19463c0fc 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -5,7 +5,8 @@ import os from types import ModuleType from pathlib import Path from dotenv import load_dotenv -''' + +""" 日志颜色说明: 1. 主程序(Main) @@ -53,9 +54,7 @@ from dotenv import load_dotenv 注意: 1. 级别颜色遵循loguru默认配置 2. 可通过环境变量修改日志级别 -''' - - +""" # 加载 .env 文件 @@ -356,9 +355,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 麦麦水群 | {message}" - ), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 麦麦水群 | {message}"), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群 | {message}", }, } @@ -374,9 +371,7 @@ SUB_HEARTFLOW_MIND_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}" - ), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 麦麦小脑袋 | {message}"), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦小脑袋 | {message}", }, } @@ -392,9 +387,7 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}" - ), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, } diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 60bb4d8c5..ab5efa9db 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -236,11 +236,11 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: """ # 预处理:处理多余的换行符 # 1. 将连续的换行符替换为单个换行符 - text = re.sub(r'\n\s*\n+', '\n', text) + text = re.sub(r"\n\s*\n+", "\n", text) # 2. 处理换行符和其他分隔符的组合 - text = re.sub(r'\n\s*([,,。;\s])', r'\1', text) - text = re.sub(r'([,,。;\s])\s*\n', r'\1', text) - + text = re.sub(r"\n\s*([,,。;\s])", r"\1", text) + text = re.sub(r"([,,。;\s])\s*\n", r"\1", text) + # 处理两个汉字中间的换行符 text = re.sub(r"([\u4e00-\u9fff])\n([\u4e00-\u9fff])", r"\1。\2", text) @@ -420,12 +420,11 @@ def process_llm_response(text: str) -> List[str]: if len(sentences) > max_sentence_num: logger.warning(f"分割后消息数量过多 ({len(sentences)} 条),返回默认回复") return [f"{global_config.BOT_NICKNAME}不知道哦"] - + # if extracted_contents: # for content in extracted_contents: # sentences.append(content) - - + # 在所有句子处理完毕后,对包含占位符的列表进行恢复 sentences = recover_kaomoji(sentences, kaomoji_mapping) diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index 24572ed29..fb8522b94 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -12,7 +12,6 @@ from ...config.config import global_config from ..models.utils_model import LLMRequest from src.common.logger import get_module_logger -import traceback logger = get_module_logger("chat_image") @@ -316,7 +315,7 @@ def image_path_to_base64(image_path: str) -> str: """ if not os.path.exists(image_path): raise FileNotFoundError(f"图片文件不存在: {image_path}") - + with open(image_path, "rb") as f: image_data = f.read() if not image_data: From b24358cc29d3678524ac89bdcd6e3f01bbdcf268 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 03:39:14 +0800 Subject: [PATCH 11/79] Update Dockerfile and workflows to add MaiMBot-LPMM support Added MaiMBot-LPMM directory in Dockerfile and its repository clone step in the GitHub workflow. Upgraded compiler setup to use build-essential and included a CPU info check in the Dockerfile. --- .github/workflows/docker-image.yml | 3 +++ Dockerfile | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 76636d746..605d838ce 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -24,6 +24,9 @@ jobs: - name: Clone maim_message run: git clone https://github.com/MaiM-with-u/maim_message maim_message + - name: Clone lpmm + run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/Dockerfile b/Dockerfile index 074711523..10b6b77fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,13 @@ WORKDIR /MaiMBot COPY requirements.txt . # 同级目录下需要有 maim_message COPY maim_message /maim_message +COPY MaiMBot-LPMM /MaiMBot-LPMM # 编译器 -RUN apt-get update && apt-get install -y g++ +RUN apt-get update && apt-get install -y build-essential + +# test +RUN cat /proc/cpuinfo | grep avx2 # 安装依赖 RUN uv pip install --system --upgrade pip From db7543dd8dcaa5aec721cc826c128205e9485f0c Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 03:40:03 +0800 Subject: [PATCH 12/79] =?UTF-8?q?test:=20test=E6=9F=A5=E7=9C=8Bcpu?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 10b6b77fb..6ef7070da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ COPY MaiMBot-LPMM /MaiMBot-LPMM RUN apt-get update && apt-get install -y build-essential # test +RUN cat /proc/cpuinfo RUN cat /proc/cpuinfo | grep avx2 # 安装依赖 From c7aff644acde491e1428929d6980f1f8f8fec18b Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 04:01:57 +0800 Subject: [PATCH 13/79] =?UTF-8?q?test:=20test=E6=9F=A5=E7=9C=8Bcpu?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 ++- test_cpu.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 test_cpu.py diff --git a/Dockerfile b/Dockerfile index 6ef7070da..39df23381 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,8 @@ RUN apt-get update && apt-get install -y build-essential # test RUN cat /proc/cpuinfo -RUN cat /proc/cpuinfo | grep avx2 +RUN uv pip install --system py-cpuinfo +RUN python test_cpu.py # 安装依赖 RUN uv pip install --system --upgrade pip diff --git a/test_cpu.py b/test_cpu.py new file mode 100644 index 000000000..befb40ec5 --- /dev/null +++ b/test_cpu.py @@ -0,0 +1,5 @@ +import cpuinfo + +cpu_info = cpuinfo.get_cpu_info() +print(f"当前cpu信息:{cpu_info}") +print(f"当前cpu指令集支持:{cpu_info["flags"]}") \ No newline at end of file From 380888a81a2cae11c0036db0b3c02713c94f7091 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Apr 2025 20:02:19 +0000 Subject: [PATCH 14/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test_cpu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_cpu.py b/test_cpu.py index befb40ec5..84b3d2f5d 100644 --- a/test_cpu.py +++ b/test_cpu.py @@ -2,4 +2,4 @@ import cpuinfo cpu_info = cpuinfo.get_cpu_info() print(f"当前cpu信息:{cpu_info}") -print(f"当前cpu指令集支持:{cpu_info["flags"]}") \ No newline at end of file +print(f"当前cpu指令集支持:{cpu_info['flags']}") From 72212ebfe21f424a6eaf32ab7b05add3f51bea83 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 04:04:34 +0800 Subject: [PATCH 15/79] =?UTF-8?q?test:=20test=E6=9F=A5=E7=9C=8Bcpu?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 39df23381..a9f84ccdc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ COPY requirements.txt . # 同级目录下需要有 maim_message COPY maim_message /maim_message COPY MaiMBot-LPMM /MaiMBot-LPMM +COPY test_cpu.py /test_cpu.py # 编译器 RUN apt-get update && apt-get install -y build-essential @@ -16,7 +17,7 @@ RUN apt-get update && apt-get install -y build-essential # test RUN cat /proc/cpuinfo RUN uv pip install --system py-cpuinfo -RUN python test_cpu.py +RUN python /test_cpu.py # 安装依赖 RUN uv pip install --system --upgrade pip From 4e222afacc321306243226fe97fdd92022f71e56 Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 04:31:50 +0800 Subject: [PATCH 16/79] =?UTF-8?q?test:=20test=E6=9F=A5=E7=9C=8Bcpu?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 2 +- Dockerfile | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 605d838ce..3bd4a21bc 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -25,7 +25,7 @@ jobs: run: git clone https://github.com/MaiM-with-u/maim_message maim_message - name: Clone lpmm - run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM + run: git clone https://github.com/infinitycat233/MaiMBot-LPMM.git MaiMBot-LPMM - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/Dockerfile b/Dockerfile index a9f84ccdc..24294125a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,9 @@ RUN apt-get update && apt-get install -y build-essential RUN cat /proc/cpuinfo RUN uv pip install --system py-cpuinfo RUN python /test_cpu.py +RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt +RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install + # 安装依赖 RUN uv pip install --system --upgrade pip From 5e423a092eb782ed411d08f93970499fb887204f Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 04:34:54 +0800 Subject: [PATCH 17/79] =?UTF-8?q?test:=20test=E6=9F=A5=E7=9C=8Bcpu?= =?UTF-8?q?=E6=8C=87=E4=BB=A4=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 24294125a..e055bf68f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN apt-get update && apt-get install -y build-essential RUN cat /proc/cpuinfo RUN uv pip install --system py-cpuinfo RUN python /test_cpu.py -RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt +RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt && uv pip install --system Cython py-cpuinfo setuptools RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install From f1414175f5b00745429b11d452b1f40c19b183da Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 06:33:40 +0800 Subject: [PATCH 18/79] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96dockerfile?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4test=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 15 ++++++--------- test_cpu.py | 5 ----- 2 files changed, 6 insertions(+), 14 deletions(-) delete mode 100644 test_cpu.py diff --git a/Dockerfile b/Dockerfile index e055bf68f..23165a23e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,25 +6,22 @@ WORKDIR /MaiMBot # 复制依赖列表 COPY requirements.txt . -# 同级目录下需要有 maim_message -COPY maim_message /maim_message +# 同级目录下需要有 maim_message MaiMBot-LPMM +#COPY maim_message /maim_message COPY MaiMBot-LPMM /MaiMBot-LPMM -COPY test_cpu.py /test_cpu.py # 编译器 RUN apt-get update && apt-get install -y build-essential -# test -RUN cat /proc/cpuinfo -RUN uv pip install --system py-cpuinfo -RUN python /test_cpu.py -RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt && uv pip install --system Cython py-cpuinfo setuptools +# lpmm编译安装 +RUN cd /MaiMBot-LPMM && uv pip install --system -r requirements.txt +RUN uv pip install --system Cython py-cpuinfo setuptools RUN cd /MaiMBot-LPMM/lib/quick_algo && python build_lib.py --cleanup --cythonize --install # 安装依赖 RUN uv pip install --system --upgrade pip -RUN uv pip install --system -e /maim_message +#RUN uv pip install --system -e /maim_message RUN uv pip install --system -r requirements.txt # 复制项目代码 diff --git a/test_cpu.py b/test_cpu.py deleted file mode 100644 index 84b3d2f5d..000000000 --- a/test_cpu.py +++ /dev/null @@ -1,5 +0,0 @@ -import cpuinfo - -cpu_info = cpuinfo.get_cpu_info() -print(f"当前cpu信息:{cpu_info}") -print(f"当前cpu指令集支持:{cpu_info['flags']}") From a2c94d3e8ef8f3b087360c43443fdc21ea72b69c Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 13:27:35 +0800 Subject: [PATCH 19/79] =?UTF-8?q?perf:=20=E6=9B=B4=E6=96=B0clone=E5=9C=B0?= =?UTF-8?q?=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 3bd4a21bc..605d838ce 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -25,7 +25,7 @@ jobs: run: git clone https://github.com/MaiM-with-u/maim_message maim_message - name: Clone lpmm - run: git clone https://github.com/infinitycat233/MaiMBot-LPMM.git MaiMBot-LPMM + run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 56c918d60ebb23c2dff879a189d6daf9c59d808e Mon Sep 17 00:00:00 2001 From: tcmofashi Date: Fri, 25 Apr 2025 13:35:51 +0800 Subject: [PATCH 20/79] =?UTF-8?q?feat:=20=E5=85=A8=E9=9D=A2=E6=94=B9?= =?UTF-8?q?=E7=94=A8maim=5Fmessage=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=AF=B9rest?= =?UTF-8?q?=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/chat_observer.py | 2 +- src/plugins/PFC/conversation.py | 2 +- src/plugins/PFC/message_sender.py | 2 +- src/plugins/PFC/observation_info.py | 2 +- src/plugins/PFC/pfc.py | 15 +- src/plugins/PFC/reply_checker.py | 2 +- src/plugins/chat/chat_stream.py | 2 +- src/plugins/chat/message.py | 2 +- src/plugins/chat/message_buffer.py | 2 +- src/plugins/chat/message_sender.py | 12 +- src/plugins/chat/utils.py | 2 +- .../heartFC_chat/heartflow_processor.py | 2 +- src/plugins/heartFC_chat/normal_chat.py | 2 +- src/plugins/message/__init__.py | 17 +- src/plugins/message/api.py | 246 +---------------- src/plugins/message/message_base.py | 247 ------------------ 16 files changed, 16 insertions(+), 543 deletions(-) delete mode 100644 src/plugins/message/message_base.py diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 60acb5f53..697833c84 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -3,7 +3,7 @@ import asyncio import traceback from typing import Optional, Dict, Any, List from src.common.logger import get_module_logger -from ..message.message_base import UserInfo +from maim_message import UserInfo from ...config.config import global_config from .chat_states import NotificationManager, create_new_message_notification, create_cold_chat_notification from .message_storage import MongoDBMessageStorage diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index d4888ff79..4cc894bda 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -13,7 +13,7 @@ from .observation_info import ObservationInfo from .conversation_info import ConversationInfo from .reply_generator import ReplyGenerator from ..chat.chat_stream import ChatStream -from ..message.message_base import UserInfo +from maim_message import UserInfo from src.plugins.chat.chat_stream import chat_manager from .pfc_KnowledgeFetcher import KnowledgeFetcher from .waiter import Waiter diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index bc4499ed9..8a0f41762 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -2,7 +2,7 @@ from typing import Optional from src.common.logger import get_module_logger from ..chat.chat_stream import ChatStream from ..chat.message import Message -from ..message.message_base import Seg +from maim_message import Seg from src.plugins.chat.message import MessageSending, MessageSet from src.plugins.chat.message_sender import message_manager diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 08ff3c046..4cb6aaaa8 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -1,7 +1,7 @@ # Programmable Friendly Conversationalist # Prefrontal cortex from typing import List, Optional, Dict, Any, Set -from ..message.message_base import UserInfo +from maim_message import UserInfo import time from dataclasses import dataclass, field from src.common.logger import get_module_logger diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 873d14674..19549825a 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -6,7 +6,7 @@ import datetime from typing import List, Optional, Tuple, TYPE_CHECKING from src.common.logger import get_module_logger from ..chat.chat_stream import ChatStream -from ..message.message_base import UserInfo, Seg +from maim_message import UserInfo, Seg from ..chat.message import Message from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -375,18 +375,7 @@ class DirectMessageSender: # 发送消息 try: - end_point = global_config.api_urls.get(message.message_info.platform, None) - if end_point: - # logger.info(f"发送消息到{end_point}") - # logger.info(message_json) - try: - await global_api.send_message_REST(end_point, message_json) - except Exception as e: - logger.error(f"REST方式发送失败,出现错误: {str(e)}") - logger.info("尝试使用ws发送") - await self.send_via_ws(message) - else: - await self.send_via_ws(message) + await self.send_via_ws(message) logger.success(f"PFC消息已发送: {content}") except Exception as e: logger.error(f"PFC消息发送失败: {str(e)}") diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 7e43715bf..e1a2a6fd7 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -5,7 +5,7 @@ from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest from ...config.config import global_config from .chat_observer import ChatObserver -from ..message.message_base import UserInfo +from maim_message import UserInfo logger = get_module_logger("reply_checker") diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index e50dc3ec2..9416ebadf 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -6,7 +6,7 @@ from typing import Dict, Optional from ...common.database import db -from ..message.message_base import GroupInfo, UserInfo +from maim_message import GroupInfo, UserInfo from src.common.logger import get_module_logger, LogConfig, CHAT_STREAM_STYLE_CONFIG diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index 093ccc30d..c7f7ac83e 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -7,7 +7,7 @@ import urllib3 from src.common.logger import get_module_logger from .chat_stream import ChatStream from .utils_image import image_manager -from ..message.message_base import Seg, UserInfo, BaseMessageInfo, MessageBase +from maim_message import Seg, UserInfo, BaseMessageInfo, MessageBase logger = get_module_logger("chat_message") diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py index d0ab56042..38d82b528 100644 --- a/src/plugins/chat/message_buffer.py +++ b/src/plugins/chat/message_buffer.py @@ -3,7 +3,7 @@ from src.common.logger import get_module_logger import asyncio from dataclasses import dataclass, field from .message import MessageRecv -from ..message.message_base import BaseMessageInfo, GroupInfo, Seg +from maim_message import BaseMessageInfo, GroupInfo, Seg import hashlib from typing import Dict from collections import OrderedDict diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index a737d99cf..d51492f70 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -62,20 +62,10 @@ class MessageSender: # logger.trace(f"{message.processed_plain_text},{typing_time},等待输入时间结束") # 减少日志 # --- 结束打字延迟 --- - message_json = message.to_dict() message_preview = truncate_message(message.processed_plain_text) try: - end_point = global_config.api_urls.get(message.message_info.platform, None) - if end_point: - try: - await global_api.send_message_rest(end_point, message_json) - except Exception as e: - logger.error(f"REST发送失败: {str(e)}") - logger.info(f"[{message.chat_stream.stream_id}] 尝试使用WS发送") - await self.send_via_ws(message) - else: - await self.send_via_ws(message) + await self.send_via_ws(message) logger.success(f"发送消息 '{message_preview}' 成功") # 调整日志格式 except Exception as e: logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index ab5efa9db..91e08e444 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -12,7 +12,7 @@ from ..models.utils_model import LLMRequest from ..utils.typo_generator import ChineseTypoGenerator from ...config.config import global_config from .message import MessageRecv, Message -from ..message.message_base import UserInfo +from maim_message import UserInfo from .chat_stream import ChatStream from ..moods.moods import MoodManager from ...common.database import db diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index f7c3a64fd..de8caf2da 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -5,7 +5,7 @@ from ...config.config import global_config from ..chat.message import MessageRecv from ..storage.storage import MessageStorage from ..chat.utils import is_mentioned_bot_in_message -from ..message import Seg +from maim_message import Seg from src.heart_flow.heartflow import heartflow from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from ..chat.chat_stream import chat_manager diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 2ba5d79d4..56fcfc346 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -12,7 +12,7 @@ from ..chat.message import MessageSending, MessageRecv, MessageThinking, Message from ..chat.message_sender import message_manager from ..chat.utils_image import image_path_to_base64 from ..willing.willing_manager import willing_manager -from ..message import UserInfo, Seg +from maim_message import UserInfo, Seg from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.person_info.relationship_manager import relationship_manager diff --git a/src/plugins/message/__init__.py b/src/plugins/message/__init__.py index 286ef2310..b5eed4d45 100644 --- a/src/plugins/message/__init__.py +++ b/src/plugins/message/__init__.py @@ -3,23 +3,8 @@ __version__ = "0.1.0" from .api import global_api -from .message_base import ( - Seg, - GroupInfo, - UserInfo, - FormatInfo, - TemplateInfo, - BaseMessageInfo, - MessageBase, -) + __all__ = [ - "Seg", "global_api", - "GroupInfo", - "UserInfo", - "FormatInfo", - "TemplateInfo", - "BaseMessageInfo", - "MessageBase", ] diff --git a/src/plugins/message/api.py b/src/plugins/message/api.py index fb51539e2..e82ab98fe 100644 --- a/src/plugins/message/api.py +++ b/src/plugins/message/api.py @@ -1,250 +1,6 @@ -from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect -from typing import Dict, Any, Callable, List, Set, Optional -from src.common.logger import get_module_logger -from src.plugins.message.message_base import MessageBase from src.common.server import global_server -import aiohttp -import asyncio -import uvicorn import os -import traceback - -logger = get_module_logger("api") - - -class BaseMessageHandler: - """消息处理基类""" - - def __init__(self): - self.message_handlers: List[Callable] = [] - self.background_tasks = set() - - def register_message_handler(self, handler: Callable): - """注册消息处理函数""" - self.message_handlers.append(handler) - - async def process_message(self, message: Dict[str, Any]): - """处理单条消息""" - tasks = [] - for handler in self.message_handlers: - try: - tasks.append(handler(message)) - except Exception as e: - logger.error(f"消息处理出错: {str(e)}") - logger.error(traceback.format_exc()) - # 不抛出异常,而是记录错误并继续处理其他消息 - continue - if tasks: - await asyncio.gather(*tasks, return_exceptions=True) - - async def _handle_message(self, message: Dict[str, Any]): - """后台处理单个消息""" - try: - await self.process_message(message) - except Exception as e: - raise RuntimeError(str(e)) from e - - -class MessageServer(BaseMessageHandler): - """WebSocket服务端""" - - _class_handlers: List[Callable] = [] # 类级别的消息处理器 - - def __init__( - self, - host: str = "0.0.0.0", - port: int = 18000, - enable_token=False, - app: Optional[FastAPI] = None, - path: str = "/ws", - ): - super().__init__() - # 将类级别的处理器添加到实例处理器中 - self.message_handlers.extend(self._class_handlers) - self.host = host - self.port = port - self.path = path - self.app = app or FastAPI() - self.own_app = app is None # 标记是否使用自己创建的app - self.active_websockets: Set[WebSocket] = set() - self.platform_websockets: Dict[str, WebSocket] = {} # 平台到websocket的映射 - self.valid_tokens: Set[str] = set() - self.enable_token = enable_token - self._setup_routes() - self._running = False - - def _setup_routes(self): - @self.app.post("/api/message") - async def handle_message(message: Dict[str, Any]): - try: - # 创建后台任务处理消息 - asyncio.create_task(self._handle_message(message)) - return {"status": "success"} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) from e - - @self.app.websocket("/ws") - async def websocket_endpoint(websocket: WebSocket): - headers = dict(websocket.headers) - token = headers.get("authorization") - platform = headers.get("platform", "default") # 获取platform标识 - if self.enable_token: - if not token or not await self.verify_token(token): - await websocket.close(code=1008, reason="Invalid or missing token") - return - - await websocket.accept() - self.active_websockets.add(websocket) - - # 添加到platform映射 - if platform not in self.platform_websockets: - self.platform_websockets[platform] = websocket - - try: - while True: - message = await websocket.receive_json() - # print(f"Received message: {message}") - asyncio.create_task(self._handle_message(message)) - except WebSocketDisconnect: - self._remove_websocket(websocket, platform) - except Exception as e: - self._remove_websocket(websocket, platform) - raise RuntimeError(str(e)) from e - finally: - self._remove_websocket(websocket, platform) - - @classmethod - def register_class_handler(cls, handler: Callable): - """注册类级别的消息处理器""" - if handler not in cls._class_handlers: - cls._class_handlers.append(handler) - - def register_message_handler(self, handler: Callable): - """注册实例级别的消息处理器""" - if handler not in self.message_handlers: - self.message_handlers.append(handler) - - async def verify_token(self, token: str) -> bool: - if not self.enable_token: - return True - return token in self.valid_tokens - - def add_valid_token(self, token: str): - self.valid_tokens.add(token) - - def remove_valid_token(self, token: str): - self.valid_tokens.discard(token) - - def run_sync(self): - """同步方式运行服务器""" - if not self.own_app: - raise RuntimeError("当使用外部FastAPI实例时,请使用该实例的运行方法") - uvicorn.run(self.app, host=self.host, port=self.port) - - async def run(self): - """异步方式运行服务器""" - self._running = True - try: - if self.own_app: - # 如果使用自己的 FastAPI 实例,运行 uvicorn 服务器 - # 禁用 uvicorn 默认日志和访问日志 - config = uvicorn.Config( - self.app, host=self.host, port=self.port, loop="asyncio", log_config=None, access_log=False - ) - self.server = uvicorn.Server(config) - await self.server.serve() - else: - # 如果使用外部 FastAPI 实例,保持运行状态以处理消息 - while self._running: - await asyncio.sleep(1) - except KeyboardInterrupt: - await self.stop() - raise - except Exception as e: - await self.stop() - raise RuntimeError(f"服务器运行错误: {str(e)}") from e - finally: - await self.stop() - - async def start_server(self): - """启动服务器的异步方法""" - if not self._running: - self._running = True - await self.run() - - async def stop(self): - """停止服务器""" - # 清理platform映射 - self.platform_websockets.clear() - - # 取消所有后台任务 - for task in self.background_tasks: - task.cancel() - # 等待所有任务完成 - await asyncio.gather(*self.background_tasks, return_exceptions=True) - self.background_tasks.clear() - - # 关闭所有WebSocket连接 - for websocket in self.active_websockets: - await websocket.close() - self.active_websockets.clear() - - if hasattr(self, "server") and self.own_app: - self._running = False - # 正确关闭 uvicorn 服务器 - self.server.should_exit = True - await self.server.shutdown() - # 等待服务器完全停止 - if hasattr(self.server, "started") and self.server.started: - await self.server.main_loop() - # 清理处理程序 - self.message_handlers.clear() - - def _remove_websocket(self, websocket: WebSocket, platform: str): - """从所有集合中移除websocket""" - if websocket in self.active_websockets: - self.active_websockets.remove(websocket) - if platform in self.platform_websockets: - if self.platform_websockets[platform] == websocket: - del self.platform_websockets[platform] - - async def broadcast_message(self, message: Dict[str, Any]): - disconnected = set() - for websocket in self.active_websockets: - try: - await websocket.send_json(message) - except Exception: - disconnected.add(websocket) - for websocket in disconnected: - self.active_websockets.remove(websocket) - - async def broadcast_to_platform(self, platform: str, message: Dict[str, Any]): - """向指定平台的所有WebSocket客户端广播消息""" - if platform not in self.platform_websockets: - raise ValueError(f"平台:{platform} 未连接") - - disconnected = set() - try: - await self.platform_websockets[platform].send_json(message) - except Exception: - disconnected.add(self.platform_websockets[platform]) - - # 清理断开的连接 - for websocket in disconnected: - self._remove_websocket(websocket, platform) - - async def send_message(self, message: MessageBase): - await self.broadcast_to_platform(message.message_info.platform, message.to_dict()) - - @staticmethod - async def send_message_rest(url: str, data: Dict[str, Any]) -> Dict[str, Any]: - """发送消息到指定端点""" - async with aiohttp.ClientSession() as session: - try: - async with session.post(url, json=data, headers={"Content-Type": "application/json"}) as response: - return await response.json() - except Exception as e: - raise e +from maim_message import MessageServer global_api = MessageServer(host=os.environ["HOST"], port=int(os.environ["PORT"]), app=global_server.get_app()) diff --git a/src/plugins/message/message_base.py b/src/plugins/message/message_base.py deleted file mode 100644 index b853d469a..000000000 --- a/src/plugins/message/message_base.py +++ /dev/null @@ -1,247 +0,0 @@ -from dataclasses import dataclass, asdict -from typing import List, Optional, Union, Dict - - -@dataclass -class Seg: - """消息片段类,用于表示消息的不同部分 - - Attributes: - type: 片段类型,可以是 'text'、'image'、'seglist' 等 - data: 片段的具体内容 - - 对于 text 类型,data 是字符串 - - 对于 image 类型,data 是 base64 字符串 - - 对于 seglist 类型,data 是 Seg 列表 - """ - - type: str - data: Union[str, List["Seg"]] - - # def __init__(self, type: str, data: Union[str, List['Seg']],): - # """初始化实例,确保字典和属性同步""" - # # 先初始化字典 - # self.type = type - # self.data = data - - @classmethod - def from_dict(cls, data: Dict) -> "Seg": - """从字典创建Seg实例""" - type = data.get("type") - data = data.get("data") - if type == "seglist": - data = [Seg.from_dict(seg) for seg in data] - return cls(type=type, data=data) - - def to_dict(self) -> Dict: - """转换为字典格式""" - result = {"type": self.type} - if self.type == "seglist": - result["data"] = [seg.to_dict() for seg in self.data] - else: - result["data"] = self.data - return result - - -@dataclass -class GroupInfo: - """群组信息类""" - - platform: Optional[str] = None - group_id: Optional[int] = None - group_name: Optional[str] = None # 群名称 - - def to_dict(self) -> Dict: - """转换为字典格式""" - return {k: v for k, v in asdict(self).items() if v is not None} - - @classmethod - def from_dict(cls, data: Dict) -> "GroupInfo": - """从字典创建GroupInfo实例 - - Args: - data: 包含必要字段的字典 - - Returns: - GroupInfo: 新的实例 - """ - if data.get("group_id") is None: - return None - return cls( - platform=data.get("platform"), group_id=data.get("group_id"), group_name=data.get("group_name", None) - ) - - -@dataclass -class UserInfo: - """用户信息类""" - - platform: Optional[str] = None - user_id: Optional[int] = None - user_nickname: Optional[str] = None # 用户昵称 - user_cardname: Optional[str] = None # 用户群昵称 - - def to_dict(self) -> Dict: - """转换为字典格式""" - return {k: v for k, v in asdict(self).items() if v is not None} - - @classmethod - def from_dict(cls, data: Dict) -> "UserInfo": - """从字典创建UserInfo实例 - - Args: - data: 包含必要字段的字典 - - Returns: - UserInfo: 新的实例 - """ - return cls( - platform=data.get("platform"), - user_id=data.get("user_id"), - user_nickname=data.get("user_nickname", None), - user_cardname=data.get("user_cardname", None), - ) - - -@dataclass -class FormatInfo: - """格式信息类""" - - """ - 目前maimcore可接受的格式为text,image,emoji - 可发送的格式为text,emoji,reply - """ - - content_format: Optional[str] = None - accept_format: Optional[str] = None - - def to_dict(self) -> Dict: - """转换为字典格式""" - return {k: v for k, v in asdict(self).items() if v is not None} - - @classmethod - def from_dict(cls, data: Dict) -> "FormatInfo": - """从字典创建FormatInfo实例 - Args: - data: 包含必要字段的字典 - Returns: - FormatInfo: 新的实例 - """ - return cls( - content_format=data.get("content_format"), - accept_format=data.get("accept_format"), - ) - - -@dataclass -class TemplateInfo: - """模板信息类""" - - template_items: Optional[Dict] = None - template_name: Optional[str] = None - template_default: bool = True - - def to_dict(self) -> Dict: - """转换为字典格式""" - return {k: v for k, v in asdict(self).items() if v is not None} - - @classmethod - def from_dict(cls, data: Dict) -> "TemplateInfo": - """从字典创建TemplateInfo实例 - Args: - data: 包含必要字段的字典 - Returns: - TemplateInfo: 新的实例 - """ - return cls( - template_items=data.get("template_items"), - template_name=data.get("template_name"), - template_default=data.get("template_default", True), - ) - - -@dataclass -class BaseMessageInfo: - """消息信息类""" - - platform: Optional[str] = None - message_id: Union[str, int, None] = None - time: Optional[float] = None - group_info: Optional[GroupInfo] = None - user_info: Optional[UserInfo] = None - format_info: Optional[FormatInfo] = None - template_info: Optional[TemplateInfo] = None - additional_config: Optional[dict] = None - - def to_dict(self) -> Dict: - """转换为字典格式""" - result = {} - for field, value in asdict(self).items(): - if value is not None: - if isinstance(value, (GroupInfo, UserInfo, FormatInfo, TemplateInfo)): - result[field] = value.to_dict() - else: - result[field] = value - return result - - @classmethod - def from_dict(cls, data: Dict) -> "BaseMessageInfo": - """从字典创建BaseMessageInfo实例 - - Args: - data: 包含必要字段的字典 - - Returns: - BaseMessageInfo: 新的实例 - """ - group_info = GroupInfo.from_dict(data.get("group_info", {})) - user_info = UserInfo.from_dict(data.get("user_info", {})) - format_info = FormatInfo.from_dict(data.get("format_info", {})) - template_info = TemplateInfo.from_dict(data.get("template_info", {})) - return cls( - platform=data.get("platform"), - message_id=data.get("message_id"), - time=data.get("time"), - additional_config=data.get("additional_config", None), - group_info=group_info, - user_info=user_info, - format_info=format_info, - template_info=template_info, - ) - - -@dataclass -class MessageBase: - """消息类""" - - message_info: BaseMessageInfo - message_segment: Seg - raw_message: Optional[str] = None # 原始消息,包含未解析的cq码 - - def to_dict(self) -> Dict: - """转换为字典格式 - - Returns: - Dict: 包含所有非None字段的字典,其中: - - message_info: 转换为字典格式 - - message_segment: 转换为字典格式 - - raw_message: 如果存在则包含 - """ - result = {"message_info": self.message_info.to_dict(), "message_segment": self.message_segment.to_dict()} - if self.raw_message is not None: - result["raw_message"] = self.raw_message - return result - - @classmethod - def from_dict(cls, data: Dict) -> "MessageBase": - """从字典创建MessageBase实例 - - Args: - data: 包含必要字段的字典 - - Returns: - MessageBase: 新的实例 - """ - message_info = BaseMessageInfo.from_dict(data.get("message_info", {})) - message_segment = Seg.from_dict(data.get("message_segment", {})) - raw_message = data.get("raw_message", None) - return cls(message_info=message_info, message_segment=message_segment, raw_message=raw_message) From b6b5150f6b1d0f006fe47e5d5898ae75eefaefff Mon Sep 17 00:00:00 2001 From: Cookie987 Date: Fri, 25 Apr 2025 17:30:58 +0800 Subject: [PATCH 21/79] =?UTF-8?q?feat:=20LPMM=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/run.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/run.sh b/scripts/run.sh index b7ecbc849..9fd3127f6 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -4,7 +4,7 @@ # 适用于Arch/Ubuntu 24.10/Debian 12/CentOS 9 # 请小心使用任何一键脚本! -INSTALLER_VERSION="0.0.3-refactor" +INSTALLER_VERSION="0.0.4-refactor" LANG=C.UTF-8 # 如无法访问GitHub请修改此处镜像地址 @@ -19,10 +19,10 @@ RESET="\e[0m" declare -A REQUIRED_PACKAGES=( ["common"]="git sudo python3 curl gnupg" - ["debian"]="python3-venv python3-pip" - ["ubuntu"]="python3-venv python3-pip" - ["centos"]="python3-pip" - ["arch"]="python-virtualenv python-pip" + ["debian"]="python3-venv python3-pip build-essential" + ["ubuntu"]="python3-venv python3-pip build-essential" + ["centos"]="epel-release python3-pip python3-devel gcc gcc-c++ make" + ["arch"]="python-virtualenv python-pip base-devel" ) # 默认项目目录 From a45b35e74ce899bb2905dd6ecb3fd0cfce8414ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Apr 2025 09:40:54 +0000 Subject: [PATCH 22/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 22 ++++++++++++---------- src/plugins/PFC/reply_checker.py | 3 ++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 39ebccc1d..5687e4205 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -248,7 +248,7 @@ class Conversation: # --- 根据不同的 action 执行 --- if action == "direct_reply": - max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大) + max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大) reply_attempt_count = 0 is_suitable = False need_replan = False @@ -273,17 +273,19 @@ class Conversation: reply=self.generated_reply, goal=current_goal_str, chat_history=observation_info.chat_history, - retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数) + retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数) + ) + logger.info( + f"第 {reply_attempt_count} 次检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}" ) - logger.info(f"第 {reply_attempt_count} 次检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}") if is_suitable: - final_reply_to_send = self.generated_reply # 保存合适的回复 - break # 回复合适,跳出循环 + final_reply_to_send = self.generated_reply # 保存合适的回复 + break # 回复合适,跳出循环 elif need_replan: - logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}") - break # 如果检查器建议重新规划,也停止尝试 + logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}") + break # 如果检查器建议重新规划,也停止尝试 # 如果不合适但不需要重新规划,循环会继续进行下一次尝试 except Exception as check_err: @@ -310,7 +312,7 @@ class Conversation: return # 发送合适的回复 - self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容 + self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容 await self._send_reply() # 更新 action 历史状态为 done @@ -326,7 +328,7 @@ class Conversation: logger.warning(f"经过 {reply_attempt_count} 次尝试,未能生成合适的回复。最终原因: {check_reason}") conversation_info.done_action[action_index].update( { - "status": "recall", # 标记为 recall 因为没有成功发送 + "status": "recall", # 标记为 recall 因为没有成功发送 "final_reason": f"尝试{reply_attempt_count}次后失败: {check_reason}", "time": datetime.datetime.now().strftime("%H:%M:%S"), } @@ -341,7 +343,7 @@ class Conversation: wait_action_record = { "action": "wait", "plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待", - "status": "done", # wait 完成后可以认为是 done + "status": "done", # wait 完成后可以认为是 done "time": datetime.datetime.now().strftime("%H:%M:%S"), "final_reason": None, } diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 312387f31..949b49a34 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -78,8 +78,9 @@ class ReplyChecker: except Exception as e: import traceback + logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}") - logger.error(traceback.format_exc()) # 打印详细的回溯信息 + logger.error(traceback.format_exc()) # 打印详细的回溯信息 for msg in chat_history[-20:]: time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S") From d7ca0255febba87868242d13ca0ac34ce3b01a3f Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 18:12:11 +0800 Subject: [PATCH 23/79] =?UTF-8?q?fix=EF=BC=9A=E8=BF=9B=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8C=96=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=A7=82?= =?UTF-8?q?=E5=AF=9F=E9=94=99=E4=BD=8D=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 10 +- src/heart_flow/mai_state_manager.py | 18 +- src/heart_flow/observation.py | 3 + src/heart_flow/sub_heartflow.py | 129 +- src/heart_flow/sub_mind.py | 9 +- src/heart_flow/subheartflow_manager.py | 144 +-- src/main.py | 2 +- src/plugins/chat/utils_image.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 14 +- src/plugins/heartFC_chat/heartFC_chat.py | 1078 +++++++++++------ src/plugins/heartFC_chat/heartFC_generator.py | 3 - src/plugins/heartFC_chat/heartFC_readme.md | 159 +++ .../heartFC_chat/heartflow_processor.py | 282 ++--- .../heartFC_chat/heartflow_prompt_builder.py | 28 +- src/plugins/moods/moods.py | 2 +- src/plugins/utils/chat_message_builder.py | 2 +- 16 files changed, 1217 insertions(+), 668 deletions(-) create mode 100644 src/plugins/heartFC_chat/heartFC_readme.md diff --git a/src/common/logger.py b/src/common/logger.py index 19463c0fc..4ed69f320 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -163,13 +163,13 @@ MOOD_STYLE_CONFIG = { "console_format": ( "{time:YYYY-MM-DD HH:mm:ss} | " "{level: <8} | " - "心情 | " + "心情 | " "{message}" ), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}", }, "simple": { - "console_format": "{time:MM-DD HH:mm} | 心情 | {message}", + "console_format": "{time:MM-DD HH:mm} | 心情 | {message} ", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}", }, } @@ -315,14 +315,14 @@ CHAT_STYLE_CONFIG = { "console_format": ( "{time:YYYY-MM-DD HH:mm:ss} | " "{level: <8} | " - "见闻 | " + "见闻 | " "{message}" ), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, "simple": { "console_format": ( - "{time:MM-DD HH:mm} | 见闻 | {message}" + "{time:MM-DD HH:mm} | 见闻 | {message}" ), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, @@ -387,7 +387,7 @@ SUBHEARTFLOW_MANAGER_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 麦麦水群[管理] | {message}"), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 麦麦水群[管理] | {message}", }, } diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 6f645f670..f8d4341eb 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -13,8 +13,8 @@ mai_state_config = LogConfig( logger = get_module_logger("mai_state_manager", config=mai_state_config) -# enable_unlimited_hfc_chat = True -enable_unlimited_hfc_chat = False +enable_unlimited_hfc_chat = True +# enable_unlimited_hfc_chat = False class MaiState(enum.Enum): @@ -22,14 +22,14 @@ class MaiState(enum.Enum): 聊天状态: OFFLINE: 不在线:回复概率极低,不会进行任何聊天 PEEKING: 看一眼手机:回复概率较低,会进行一些普通聊天 - NORMAL_CHAT: 正常聊天:回复概率较高,会进行一些普通聊天和少量的专注聊天 + NORMAL_CHAT: 正常看手机:回复概率较高,会进行一些普通聊天和少量的专注聊天 FOCUSED_CHAT: 专注聊天:回复概率极高,会进行专注聊天和少量的普通聊天 """ OFFLINE = "不在线" - PEEKING = "看一眼" - NORMAL_CHAT = "正常聊天" - FOCUSED_CHAT = "专心聊天" + PEEKING = "看一眼手机" + NORMAL_CHAT = "正常看手机" + FOCUSED_CHAT = "专心看手机" def get_normal_chat_max_num(self): # 调试用 @@ -137,11 +137,11 @@ class MaiStateManager: if current_status == MaiState.OFFLINE: logger.info("当前[离线],没看手机,思考要不要上线看看......") elif current_status == MaiState.PEEKING: - logger.info("当前[看一眼],思考要不要继续聊下去......") + logger.info("当前[看一眼手机],思考要不要继续聊下去......") elif current_status == MaiState.NORMAL_CHAT: - logger.info("当前在[正常聊天]思考要不要继续聊下去......") + logger.info("当前在[正常看手机]思考要不要继续聊下去......") elif current_status == MaiState.FOCUSED_CHAT: - logger.info("当前在[专心聊天]思考要不要继续聊下去......") + logger.info("当前在[专心看手机]思考要不要继续聊下去......") # 1. 麦麦每分钟都有概率离线 if time_since_last_min_check >= 60: diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 9391a660a..790c21805 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -22,6 +22,9 @@ class Observation: self.observe_type = observe_type self.observe_id = observe_id self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间 + + async def observe(self): + pass # 聊天观察 diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 7397a37f3..91ddc2cd7 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -43,6 +43,7 @@ class InterestChatting: max_probability=max_reply_probability, state_change_callback: Optional[Callable[[ChatState], None]] = None, ): + # 基础属性初始化 self.interest_level: float = 0.0 self.last_update_time: float = time.time() self.decay_rate_per_second: float = decay_rate @@ -56,16 +57,26 @@ class InterestChatting: self.max_reply_probability: float = max_probability self.current_reply_probability: float = 0.0 self.is_above_threshold: bool = False + + # 任务相关属性初始化 self.update_task: Optional[asyncio.Task] = None self._stop_event = asyncio.Event() + self._task_lock = asyncio.Lock() + self._is_running = False self.interest_dict: Dict[str, tuple[MessageRecv, float, bool]] = {} self.update_interval = 1.0 - self.start_updates(self.update_interval) # 初始化时启动后台更新任务 self.above_threshold = False self.start_hfc_probability = 0.0 + @classmethod + async def create(cls, *args, **kwargs): + """异步工厂方法,用于创建并初始化 InterestChatting 实例""" + instance = cls(*args, **kwargs) + await instance.start_updates(instance.update_interval) + return instance + def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool): self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) self.last_interaction_time = time.time() @@ -141,59 +152,74 @@ class InterestChatting: # --- 新增后台更新任务相关方法 --- async def _run_update_loop(self, update_interval: float = 1.0): """后台循环,定期更新兴趣和回复概率。""" - while not self._stop_event.is_set(): - try: - if self.interest_level != 0: - await self._calculate_decay() + try: + while not self._stop_event.is_set(): + try: + if self.interest_level != 0: + await self._calculate_decay() - await self._update_reply_probability() + await self._update_reply_probability() - # 等待下一个周期或停止事件 - await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval) - except asyncio.TimeoutError: - # 正常超时,继续循环 - continue - except asyncio.CancelledError: - logger.info("InterestChatting 更新循环被取消。") - break - except Exception as e: - logger.error(f"InterestChatting 更新循环出错: {e}") - logger.error(traceback.format_exc()) - # 防止错误导致CPU飙升,稍作等待 - await asyncio.sleep(5) - logger.info("InterestChatting 更新循环已停止。") + # 等待下一个周期或停止事件 + await asyncio.wait_for(self._stop_event.wait(), timeout=update_interval) + except asyncio.TimeoutError: + # 正常超时,继续循环 + continue + except Exception as e: + logger.error(f"InterestChatting 更新循环出错: {e}") + logger.error(traceback.format_exc()) + # 防止错误导致CPU飙升,稍作等待 + await asyncio.sleep(5) + except asyncio.CancelledError: + logger.info("InterestChatting 更新循环被取消。") + finally: + self._is_running = False + logger.info("InterestChatting 更新循环已停止。") - def start_updates(self, update_interval: float = 1.0): - """启动后台更新任务""" - if self.update_task is None or self.update_task.done(): - self._stop_event.clear() - self.update_task = asyncio.create_task(self._run_update_loop(update_interval)) - logger.debug("后台兴趣更新任务已创建并启动。") - else: - logger.debug("后台兴趣更新任务已在运行中。") + async def start_updates(self, update_interval: float = 1.0): + """启动后台更新任务,使用锁确保并发安全""" + async with self._task_lock: + if self._is_running: + logger.debug("后台兴趣更新任务已在运行中。") + return + + # 清理已完成或已取消的任务 + if self.update_task and (self.update_task.done() or self.update_task.cancelled()): + self.update_task = None + + if not self.update_task: + self._stop_event.clear() + self._is_running = True + self.update_task = asyncio.create_task(self._run_update_loop(update_interval)) + logger.debug("后台兴趣更新任务已创建并启动。") async def stop_updates(self): - """停止后台更新任务""" - if self.update_task and not self.update_task.done(): + """停止后台更新任务,使用锁确保并发安全""" + async with self._task_lock: + if not self._is_running: + logger.debug("后台兴趣更新任务未运行。") + return + logger.info("正在停止 InterestChatting 后台更新任务...") - self._stop_event.set() # 发送停止信号 - try: - # 等待任务结束,设置超时 - await asyncio.wait_for(self.update_task, timeout=5.0) - logger.info("InterestChatting 后台更新任务已成功停止。") - except asyncio.TimeoutError: - logger.warning("停止 InterestChatting 后台任务超时,尝试取消...") - self.update_task.cancel() + self._stop_event.set() + + if self.update_task and not self.update_task.done(): try: - await self.update_task # 等待取消完成 - except asyncio.CancelledError: - logger.info("InterestChatting 后台更新任务已被取消。") - except Exception as e: - logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}") - finally: - self.update_task = None - else: - logger.debug("InterestChatting 后台更新任务未运行或已完成。") + # 等待任务结束,设置超时 + await asyncio.wait_for(self.update_task, timeout=5.0) + logger.info("InterestChatting 后台更新任务已成功停止。") + except asyncio.TimeoutError: + logger.warning("停止 InterestChatting 后台任务超时,尝试取消...") + self.update_task.cancel() + try: + await self.update_task # 等待取消完成 + except asyncio.CancelledError: + logger.info("InterestChatting 后台更新任务已被取消。") + except Exception as e: + logger.error(f"停止 InterestChatting 后台任务时发生异常: {e}") + finally: + self.update_task = None + self._is_running = False # --- 结束 新增方法 --- @@ -214,7 +240,7 @@ class SubHeartflow: # 聊天状态管理 self.chat_state: ChatStateInfo = ChatStateInfo() # 该sub_heartflow的聊天状态信息 - self.interest_chatting = InterestChatting(state_change_callback=self.set_chat_state) + self.interest_chatting = None # 将在 initialize 中创建 # 活动状态管理 self.last_active_time = time.time() # 最后活跃时间 @@ -234,6 +260,11 @@ class SubHeartflow: self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id + async def initialize(self): + """异步初始化方法""" + self.interest_chatting = await InterestChatting.create(state_change_callback=self.set_chat_state) + logger.debug(f"{self.log_prefix} InterestChatting 实例已创建并初始化。") + async def add_time_current_state(self, add_time: float): self.current_state_time += add_time @@ -412,7 +443,7 @@ class SubHeartflow: - 负责子心流的主要后台循环 - 每30秒检查一次停止标志 """ - logger.info(f"{self.log_prefix} 子心流开始工作...") + logger.trace(f"{self.log_prefix} 子心流开始工作...") while not self.should_stop: await asyncio.sleep(30) # 30秒检查一次停止标志 diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 92f0a9606..111c2cf5c 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -10,7 +10,7 @@ from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.do_tool.tool_use import ToolUser from src.plugins.utils.json_utils import safe_json_dumps, normalize_llm_response, process_llm_tool_calls from src.heart_flow.chat_state_info import ChatStateInfo - +from src.plugins.chat.chat_stream import chat_manager subheartflow_config = LogConfig( console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"], @@ -30,6 +30,8 @@ def init_prompt(): prompt += "现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。" prompt += "请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" prompt += "回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题\n" + prompt += "如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。" + prompt += "如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。" prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言\n" prompt += "现在请你先输出想法,{hf_do_next},不要分点输出,文字不要浮夸" prompt += "在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。" @@ -138,7 +140,7 @@ class SubMind: hf_do_next=hf_do_next, ) - logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") + # logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 @@ -190,7 +192,8 @@ class SubMind: content = "思考过程中出现错误" # 记录最终思考结果 - logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{content}\n") + name = chat_manager.get_stream_name(self.subheartflow_id) + logger.debug(f"[{name}] \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n") # 处理空响应情况 if not content: diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index b9703e53b..dcc455917 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -75,18 +75,22 @@ class SubHeartflowManager: return subflow # 创建新的子心流实例 - logger.info(f"子心流 {subheartflow_id} 不存在,正在创建...") + # logger.info(f"子心流 {subheartflow_id} 不存在,正在创建...") try: # 初始化子心流 new_subflow = SubHeartflow(subheartflow_id, mai_states) + # 异步初始化 + await new_subflow.initialize() + # 添加聊天观察者 observation = ChattingObservation(chat_id=subheartflow_id) new_subflow.add_observation(observation) # 注册子心流 self.subheartflows[subheartflow_id] = new_subflow - logger.info(f"子心流 {subheartflow_id} 创建成功") + heartflow_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id + logger.info(f"[{heartflow_name}] 开始看消息") # 启动后台任务 asyncio.create_task(new_subflow.subheartflow_start_working()) @@ -264,104 +268,70 @@ class SubHeartflowManager: async def evaluate_interest_and_promote(self, current_mai_state: MaiStateInfo): """评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)""" - log_prefix_manager = "[子心流管理器-兴趣评估]" - logger.debug(f"{log_prefix_manager} 开始周期... 当前状态: {current_mai_state.get_current_state().value}") - - # 获取 FOCUSED 状态的数量上限 - current_state_enum = current_mai_state.get_current_state() - focused_limit = current_state_enum.get_focused_chat_max_num() + log_prefix = "[兴趣评估]" + current_state = current_mai_state.get_current_state() + focused_limit = current_state.get_focused_chat_max_num() + + + if int(time.time()) % 20 == 0: # 每20秒输出一次 + logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天") + if focused_limit <= 0: - logger.debug( - f"{log_prefix_manager} 当前状态 ({current_state_enum.value}) 不允许 FOCUSED 子心流, 跳过提升检查。" - ) + # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流") return - # 获取当前 FOCUSED 状态的数量 (初始值) current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED) - logger.debug(f"{log_prefix_manager} 专注上限: {focused_limit}, 当前专注数: {current_focused_count}") + if current_focused_count >= focused_limit: + logger.debug(f"{log_prefix} 已达专注上限 ({current_focused_count}/{focused_limit})") + return - # 使用快照安全遍历 - subflows_snapshot = list(self.subheartflows.values()) - promoted_count = 0 # 记录本次提升的数量 - try: - for sub_hf in subflows_snapshot: - flow_id = sub_hf.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id - log_prefix_flow = f"[{stream_name}]" + states_num = ( + self.count_subflows_by_state(ChatState.ABSENT), + self.count_subflows_by_state(ChatState.CHAT), + current_focused_count + ) - # 只处理 CHAT 状态的子心流 - # The code snippet is checking if the `chat_status` attribute of `sub_hf.chat_state` is not equal to - # `ChatState.CHAT`. If the condition is met, the code will continue to the next iteration of the loop - # or block of code where this snippet is located. - # if sub_hf.chat_state.chat_status != ChatState.CHAT: - # continue - - # 检查是否满足提升概率 - should_hfc = random.random() < sub_hf.interest_chatting.start_hfc_probability - if not should_hfc: + for sub_hf in list(self.subheartflows.values()): + flow_id = sub_hf.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 + if sub_hf.chat_state.chat_status == ChatState.FOCUSED: + continue + + from .mai_state_manager import enable_unlimited_hfc_chat + if not enable_unlimited_hfc_chat: + if sub_hf.chat_state.chat_status != ChatState.CHAT: continue + + # 检查是否满足提升概率 + if random.random() >= sub_hf.interest_chatting.start_hfc_probability: + continue - # --- 关键检查:检查 FOCUSED 数量是否已达上限 --- - # 注意:在循环内部再次获取当前数量,因为之前的提升可能已经改变了计数 - # 使用已经记录并在循环中更新的 current_focused_count - if current_focused_count >= focused_limit: - logger.debug( - f"{log_prefix_manager} {log_prefix_flow} 达到专注上限 ({current_focused_count}/{focused_limit}), 无法提升。概率={sub_hf.interest_chatting.start_hfc_probability:.2f}" - ) - continue # 跳过这个子心流,继续检查下一个 + # 再次检查是否达到上限 + if current_focused_count >= focused_limit: + logger.debug(f"{log_prefix} [{stream_name}] 已达专注上限") + break - # --- 执行提升 --- - # 获取当前实例以检查最新状态 (防御性编程) - current_subflow = self.subheartflows.get(flow_id) - if not current_subflow: - logger.warning(f"{log_prefix_manager} {log_prefix_flow} 尝试提升时状态已改变或实例消失,跳过。") - continue + # 获取最新状态并执行提升 + current_subflow = self.subheartflows.get(flow_id) + if not current_subflow: + continue - logger.info( - f"{log_prefix_manager} {log_prefix_flow} 兴趣评估触发升级 (prob={sub_hf.interest_chatting.start_hfc_probability:.2f}, 上限:{focused_limit}, 当前:{current_focused_count}) -> FOCUSED" - ) + logger.info( + f"{log_prefix} [{stream_name}] 触发 激情水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" + ) - states_num = ( - self.count_subflows_by_state(ChatState.ABSENT), - self.count_subflows_by_state(ChatState.CHAT), # 这个值在提升前计算 - current_focused_count, # 这个值在提升前计算 - ) + # 执行状态提升 + await current_subflow.set_chat_state(ChatState.FOCUSED, states_num) + + # 验证提升结果 + if (final_subflow := self.subheartflows.get(flow_id)) and \ + final_subflow.chat_state.chat_status == ChatState.FOCUSED: + current_focused_count += 1 - # --- 状态设置 --- - original_state = current_subflow.chat_state.chat_status # 记录原始状态 - await current_subflow.set_chat_state(ChatState.FOCUSED, states_num) - # --- 状态验证 --- - final_subflow = self.subheartflows.get(flow_id) - if final_subflow: - final_state = final_subflow.chat_state.chat_status - if final_state == ChatState.FOCUSED: - logger.debug( - f"{log_prefix_manager} {log_prefix_flow} 成功从 {original_state.value} 升级到 FOCUSED 状态" - ) - promoted_count += 1 - # 提升成功后,更新当前专注计数,以便后续检查能使用最新值 - current_focused_count += 1 - elif final_state == original_state: # 状态未变 - logger.warning( - f"{log_prefix_manager} {log_prefix_flow} 尝试从 {original_state.value} 升级 FOCUSED 失败,状态仍为: {final_state.value} (可能被内部逻辑阻止)" - ) - else: # 状态变成其他了? - logger.warning( - f"{log_prefix_manager} {log_prefix_flow} 尝试从 {original_state.value} 升级 FOCUSED 后状态变为 {final_state.value}" - ) - else: # 子心流消失了? - logger.warning(f"{log_prefix_manager} {log_prefix_flow} 升级后验证时子心流 {flow_id} 消失") - - except Exception as e: - logger.error(f"{log_prefix_manager} 兴趣评估周期出错: {e}", exc_info=True) - - if promoted_count > 0: - logger.info(f"{log_prefix_manager} 评估周期结束, 成功提升 {promoted_count} 个子心流到 FOCUSED。") - else: - logger.debug(f"{log_prefix_manager} 评估周期结束, 未提升任何子心流。") - - async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.3): + async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.1): """以一定概率将 FOCUSED 或 CHAT 状态的子心流回退到 ABSENT 状态。""" log_prefix_manager = "[子心流管理器-随机停用]" logger.debug(f"{log_prefix_manager} 开始随机停用检查... (概率: {deactivation_probability:.0%})") diff --git a/src/main.py b/src/main.py index 3ef1ed229..a2d8fc512 100644 --- a/src/main.py +++ b/src/main.py @@ -154,7 +154,7 @@ class MainSystem: """打印情绪状态""" while True: self.mood_manager.print_mood_status() - await asyncio.sleep(30) + await asyncio.sleep(60) @staticmethod async def remove_recalled_message_task(): diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py index fb8522b94..f6b9231ad 100644 --- a/src/plugins/chat/utils_image.py +++ b/src/plugins/chat/utils_image.py @@ -152,7 +152,7 @@ class ImageManager: "timestamp": timestamp, } db.images.update_one({"hash": image_hash}, {"$set": image_doc}, upsert=True) - logger.success(f"保存表情包: {file_path}") + logger.trace(f"保存表情包: {file_path}") except Exception as e: logger.error(f"保存表情包文件失败: {str(e)}") diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 7222fd3f2..1c73ec780 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -28,6 +28,11 @@ EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录 EMOJI_REGISTED_DIR = os.path.join("data", "emoji_registed") # 已注册的表情包注册目录 +''' +还没经过测试,有些地方数据库和内存数据同步可能不完全 + +''' + class MaiEmoji: """定义一个表情包""" @@ -247,10 +252,12 @@ class EmojiManager: def record_usage(self, hash: str): """记录表情使用次数""" try: + db.emoji.update_one({"hash": hash}, {"$inc": {"usage_count": 1}}) for emoji in self.emoji_objects: if emoji.hash == hash: emoji.usage_count += 1 break + except Exception as e: logger.error(f"记录表情使用失败: {str(e)}") @@ -304,12 +311,11 @@ class EmojiManager: selected_emoji, similarity = random.choice(top_5_emojis) # 更新使用次数 - db.emoji.update_one({"hash": selected_emoji.hash}, {"$inc": {"usage_count": 1}}) - - logger.info(f"[匹配] 找到表情包: {selected_emoji.description} (相似度: {similarity:.4f})") + self.record_usage(selected_emoji.hash) time_end = time.time() - logger.info(f"[匹配] 搜索表情包用时: {time_end - time_start:.2f} 秒") + + logger.info(f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})") return selected_emoji.path, f"[ {selected_emoji.description} ]" except Exception as e: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index ab80beaab..47cb52eb1 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1,7 +1,8 @@ import asyncio import time import traceback -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Set, Deque +from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageSet, Seg # Local import needed after move from src.plugins.chat.chat_stream import ChatStream @@ -20,6 +21,8 @@ from src.plugins.utils.json_utils import process_llm_tool_response # 导入新 from src.heart_flow.sub_mind import SubMind from src.heart_flow.observation import Observation from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager +import contextlib +from src.plugins.utils.chat_message_builder import num_new_messages_since # --- End import --- @@ -34,31 +37,175 @@ interest_log_config = LogConfig( logger = get_module_logger("HeartFCLoop", config=interest_log_config) # Logger Name Changed -PLANNER_TOOL_DEFINITION = [ - { - "type": "function", - "function": { - "name": "decide_reply_action", - "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。", - "parameters": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["no_reply", "text_reply", "emoji_reply"], - "description": "决定采取的行动:'no_reply'(不回复), 'text_reply'(文本回复, 可选附带表情) 或 'emoji_reply'(仅表情回复)。", - }, - "reasoning": {"type": "string", "description": "做出此决定的简要理由。"}, - "emoji_query": { - "type": "string", - "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。", +# 默认动作定义 +DEFAULT_ACTIONS = { + "no_reply": "不回复", + "text_reply": "文本回复, 可选附带表情", + "emoji_reply": "仅表情回复" +} + +class ActionManager: + """动作管理器:控制每次决策可以使用的动作""" + + def __init__(self): + # 初始化为默认动作集 + self._available_actions: Dict[str, str] = DEFAULT_ACTIONS.copy() + + def get_available_actions(self) -> Dict[str, str]: + """获取当前可用的动作集""" + return self._available_actions + + def add_action(self, action_name: str, description: str) -> bool: + """ + 添加新的动作 + + 参数: + action_name: 动作名称 + description: 动作描述 + + 返回: + bool: 是否添加成功 + """ + if action_name in self._available_actions: + return False + self._available_actions[action_name] = description + return True + + def remove_action(self, action_name: str) -> bool: + """ + 移除指定动作 + + 参数: + action_name: 动作名称 + + 返回: + bool: 是否移除成功 + """ + if action_name not in self._available_actions: + return False + del self._available_actions[action_name] + return True + + def clear_actions(self): + """清空所有动作""" + self._available_actions.clear() + + def reset_to_default(self): + """重置为默认动作集""" + self._available_actions = DEFAULT_ACTIONS.copy() + + def get_planner_tool_definition(self) -> List[Dict[str, Any]]: + """获取当前动作集对应的规划器工具定义""" + return [{ + "type": "function", + "function": { + "name": "decide_reply_action", + "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。", + "parameters": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": list(self._available_actions.keys()), + "description": "决定采取的行动:" + + ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]), + }, + "reasoning": {"type": "string", "description": "做出此决定的简要理由。"}, + "emoji_query": { + "type": "string", + "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。", + }, }, + "required": ["action", "reasoning"], }, - "required": ["action", "reasoning"], }, - }, - } -] + }] + + +# 在文件开头添加自定义异常类 +class HeartFCError(Exception): + """麦麦聊天系统基础异常类""" + pass + +class PlannerError(HeartFCError): + """规划器异常""" + pass + +class ReplierError(HeartFCError): + """回复器异常""" + pass + +class SenderError(HeartFCError): + """发送器异常""" + pass + + +class CycleInfo: + """循环信息记录类""" + def __init__(self, cycle_id: int): + self.cycle_id = cycle_id + self.start_time = time.time() + self.end_time: Optional[float] = None + self.action_taken = False + self.action_type = "unknown" + self.reasoning = "" + self.timers: Dict[str, float] = {} + self.thinking_id = "" + + # 添加响应信息相关字段 + self.response_info: Dict[str, Any] = { + "response_text": [], # 回复的文本列表 + "emoji_info": "", # 表情信息 + "anchor_message_id": "", # 锚点消息ID + "reply_message_ids": [], # 回复消息ID列表 + "sub_mind_thinking": "", # 子思维思考内容 + } + + def to_dict(self) -> Dict[str, Any]: + """将循环信息转换为字典格式""" + return { + "cycle_id": self.cycle_id, + "start_time": self.start_time, + "end_time": self.end_time, + "action_taken": self.action_taken, + "action_type": self.action_type, + "reasoning": self.reasoning, + "timers": self.timers, + "thinking_id": self.thinking_id, + "response_info": self.response_info + } + + def complete_cycle(self): + """完成循环,记录结束时间""" + self.end_time = time.time() + + def set_action_info(self, action_type: str, reasoning: str, action_taken: bool): + """设置动作信息""" + self.action_type = action_type + self.reasoning = reasoning + self.action_taken = action_taken + + def set_thinking_id(self, thinking_id: str): + """设置思考消息ID""" + self.thinking_id = thinking_id + + def set_response_info(self, + response_text: Optional[List[str]] = None, + emoji_info: Optional[str] = None, + anchor_message_id: Optional[str] = None, + reply_message_ids: Optional[List[str]] = None, + sub_mind_thinking: Optional[str] = None): + """设置响应信息""" + if response_text is not None: + self.response_info["response_text"] = response_text + if emoji_info is not None: + self.response_info["emoji_info"] = emoji_info + if anchor_message_id is not None: + self.response_info["anchor_message_id"] = anchor_message_id + if reply_message_ids is not None: + self.response_info["reply_message_ids"] = reply_message_ids + if sub_mind_thinking is not None: + self.response_info["sub_mind_thinking"] = sub_mind_thinking class HeartFChatting: @@ -79,7 +226,13 @@ class HeartFChatting: self.stream_id: str = chat_id # 聊天流ID self.chat_stream: Optional[ChatStream] = None # 关联的聊天流 self.sub_mind: SubMind = sub_mind # 关联的子思维 - self.observations: Observation = observations # 关联的观察 + self.observations: List[Observation] = observations # 关联的观察列表,用于监控聊天流状态 + + # 日志前缀 + self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]" + + # 动作管理器 + self.action_manager = ActionManager() # 初始化状态控制 self._initialized = False # 是否已初始化标志 @@ -101,331 +254,487 @@ class HeartFChatting: self._loop_active: bool = False # 循环是否正在运行 self._loop_task: Optional[asyncio.Task] = None # 主循环任务 - def _get_log_prefix(self) -> str: - """获取日志前缀,包含可读的流名称""" - stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id - return f"[{stream_name}]" + # 添加循环信息管理相关的属性 + self._cycle_counter = 0 + self._cycle_history: Deque[CycleInfo] = deque(maxlen=10) # 保留最近10个循环的信息 + self._current_cycle: Optional[CycleInfo] = None async def _initialize(self) -> bool: """ - 懒初始化以使用提供的标识符解析chat_stream和sub_hf。 + 懒初始化以使用提供的标识符解析chat_stream。 确保实例已准备好处理触发器。 """ if self._initialized: return True - log_prefix = self._get_log_prefix() # 获取前缀 - try: - self.chat_stream = chat_manager.get_stream(self.stream_id) - if not self.chat_stream: - logger.error(f"{log_prefix} 获取ChatStream失败。") - return False - self._initialized = True - logger.info(f"麦麦感觉到了,激发了HeartFChatting{log_prefix} 初始化成功。") - return True - except Exception as e: - logger.error(f"{log_prefix} 初始化失败: {e}") - logger.error(traceback.format_exc()) + self.chat_stream = chat_manager.get_stream(self.stream_id) + if not self.chat_stream: + logger.error(f"{self.log_prefix} 获取ChatStream失败。") return False + # 更新日志前缀(以防流名称发生变化) + self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + + self._initialized = True + logger.info(f"麦麦感觉到了,可以开始激情水群{self.log_prefix} ") + return True + async def start(self): """ - 显式尝试启动 HeartFChatting 的主循环。 - 如果循环未激活,则启动循环。 + 启动 HeartFChatting 的主循环。 + 注意:调用此方法前必须确保已经成功初始化。 """ - log_prefix = self._get_log_prefix() - if not self._initialized: - if not await self._initialize(): - logger.error(f"{log_prefix} 无法启动循环: 初始化失败。") - return - logger.info(f"{log_prefix} 尝试显式启动循环...") + logger.info(f"{self.log_prefix} 开始激情水群(HFC)...") await self._start_loop_if_needed() async def _start_loop_if_needed(self): """检查是否需要启动主循环,如果未激活则启动。""" - log_prefix = self._get_log_prefix() - should_start_loop = False - # 直接检查是否激活,无需检查计时器 - if not self._loop_active: - should_start_loop = True - self._loop_active = True # 标记为活动,防止重复启动 + # 如果循环已经激活,直接返回 + if self._loop_active: + return - if should_start_loop: - # 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False) - if self._loop_task and not self._loop_task.done(): - logger.warning(f"{log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。") - self._loop_task.cancel() - try: - # 等待旧任务确实被取消 - await asyncio.wait_for(self._loop_task, timeout=0.5) - except (asyncio.CancelledError, asyncio.TimeoutError): - pass # 忽略取消或超时错误 - self._loop_task = None # 清理旧任务引用 + # 标记为活动状态,防止重复启动 + self._loop_active = True - logger.info(f"{log_prefix} 循环未激活,启动主循环...") - # 创建新的循环任务 - self._loop_task = asyncio.create_task(self._run_pf_loop()) - # 添加完成回调 - self._loop_task.add_done_callback(self._handle_loop_completion) - # else: - # logger.trace(f"{log_prefix} 不需要启动循环(已激活)") # 可以取消注释以进行调试 + # 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False) + if self._loop_task and not self._loop_task.done(): + logger.warning(f"{self.log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。") + self._loop_task.cancel() + try: + # 等待旧任务确实被取消 + await asyncio.wait_for(self._loop_task, timeout=0.5) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass # 忽略取消或超时错误 + self._loop_task = None # 清理旧任务引用 + + logger.info(f"{self.log_prefix} 启动激情水群(HFC)主循环...") + # 创建新的循环任务 + self._loop_task = asyncio.create_task(self._hfc_loop()) + # 添加完成回调 + self._loop_task.add_done_callback(self._handle_loop_completion) def _handle_loop_completion(self, task: asyncio.Task): - """当 _run_pf_loop 任务完成时执行的回调。""" - log_prefix = self._get_log_prefix() + """当 _hfc_loop 任务完成时执行的回调。""" try: exception = task.exception() if exception: - logger.error(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}") + logger.error(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}") logger.error(traceback.format_exc()) # Log full traceback for exceptions else: # Loop completing normally now means it was cancelled/shutdown externally - logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)") + logger.info(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)") except asyncio.CancelledError: - logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)") + logger.info(f"{self.log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)") finally: self._loop_active = False self._loop_task = None if self._processing_lock.locked(): - logger.warning(f"{log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。") + logger.warning(f"{self.log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。") self._processing_lock.release() - async def _run_pf_loop(self): - """ - 主循环,持续进行计划并可能回复消息,直到被外部取消。 - 管理每个循环周期的处理锁。 - """ - log_prefix = self._get_log_prefix() - logger.info(f"{log_prefix} HeartFChatting: 麦麦打算好好聊聊 (进入专注模式)") + async def _hfc_loop(self): + """主循环,持续进行计划并可能回复消息,直到被外部取消。""" try: - thinking_id = "" - while True: # Loop indefinitely until cancelled - cycle_timers = {} # <--- Initialize timers dict for this cycle - - # Access MessageManager directly - if message_manager.check_if_sending_message_exist(self.stream_id, thinking_id): - # logger.info(f"{log_prefix} HeartFChatting: 麦麦还在发消息,等会再规划") - await asyncio.sleep(1) - continue - else: - # logger.info(f"{log_prefix} HeartFChatting: 麦麦不发消息了,开始规划") - pass - - # 记录循环周期开始时间,用于计时和休眠计算 + while True: # 主循环 + # 创建新的循环信息 + self._cycle_counter += 1 + self._current_cycle = CycleInfo(self._cycle_counter) + + # 初始化周期状态 + cycle_timers = {} loop_cycle_start_time = time.monotonic() - action_taken_this_cycle = False - acquired_lock = False - planner_start_db_time = 0.0 # 初始化 - - try: - with Timer("Total Cycle", cycle_timers) as _total_timer: # <--- Start total cycle timer - # Use try_acquire pattern or timeout? - await self._processing_lock.acquire() - acquired_lock = True - # logger.debug(f"{log_prefix} HeartFChatting: 循环获取到处理锁") - - # 在规划前记录数据库时间戳 + + with Timer("Total Cycle", cycle_timers): + # 执行规划和处理阶段 + async with self._get_cycle_context() as acquired_lock: + if not acquired_lock: + continue + + # 记录规划开始时间点 planner_start_db_time = time.time() - - # --- Planner --- # - planner_result = {} - with Timer("Planner", cycle_timers): # <--- Start Planner timer - planner_result = await self._planner() - action = planner_result.get("action", "error") - reasoning = planner_result.get("reasoning", "Planner did not provide reasoning.") - emoji_query = planner_result.get("emoji_query", "") - llm_error = planner_result.get("llm_error", False) - - if llm_error: - logger.error(f"{log_prefix} Planner LLM 失败,跳过本周期回复尝试。理由: {reasoning}") - # Optionally add a longer sleep? - action_taken_this_cycle = False # Ensure no action is counted - # Continue to sleep logic - - elif action == "text_reply": - logger.debug(f"{log_prefix} HeartFChatting: 麦麦决定回复文本. 理由: {reasoning}") - action_taken_this_cycle = True - anchor_message = await self._get_anchor_message() - if not anchor_message: - logger.error(f"{log_prefix} 循环: 无法获取锚点消息用于回复. 跳过周期.") - else: - # --- Create Thinking Message (Moved) --- - thinking_id = await self._create_thinking_message(anchor_message) - if not thinking_id: - logger.error(f"{log_prefix} 循环: 无法创建思考ID. 跳过周期.") - else: - replier_result = None - try: - # --- Replier Work --- # - with Timer("Replier", cycle_timers): # <--- Start Replier timer - replier_result = await self._replier_work( - anchor_message=anchor_message, - thinking_id=thinking_id, - reason=reasoning, - ) - except Exception as e_replier: - logger.error(f"{log_prefix} 循环: 回复器工作失败: {e_replier}") - # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call - - if replier_result: - # --- Sender Work --- # - try: - with Timer("Sender", cycle_timers): # <--- Start Sender timer - await self._sender( - thinking_id=thinking_id, - anchor_message=anchor_message, - response_set=replier_result, - send_emoji=emoji_query, - ) - # logger.info(f"{log_prefix} 循环: 发送器完成成功.") - except Exception as e_sender: - logger.error(f"{log_prefix} 循环: 发送器失败: {e_sender}") - # _sender should handle cleanup, but double check - # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call - else: - logger.warning(f"{log_prefix} 循环: 回复器未产生结果. 跳过发送.") - # self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call - elif action == "emoji_reply": - logger.info( - f"{log_prefix} HeartFChatting: 麦麦决定回复表情 ('{emoji_query}'). 理由: {reasoning}" + + # 执行规划阶段 + with Timer("Planning Phase", cycle_timers): + action_taken, thinking_id = await self._think_plan_execute( + cycle_timers, planner_start_db_time ) - action_taken_this_cycle = True - anchor = await self._get_anchor_message() - if anchor: - try: - # --- Handle Emoji (Moved) --- # - with Timer("Emoji Handler", cycle_timers): # <--- Start Emoji timer - await self._handle_emoji(anchor, [], emoji_query) - except Exception as e_emoji: - logger.error(f"{log_prefix} 循环: 发送表情失败: {e_emoji}") - else: - logger.warning(f"{log_prefix} 循环: 无法发送表情, 无法获取锚点.") - action_taken_this_cycle = True # 即使发送失败,Planner 也决策了动作 + + # 更新循环信息 + self._current_cycle.set_thinking_id(thinking_id) + self._current_cycle.timers = cycle_timers - elif action == "no_reply": - logger.info(f"{log_prefix} HeartFChatting: 麦麦决定不回复. 原因: {reasoning}") - action_taken_this_cycle = False # 标记为未执行动作 - # --- 新增:等待新消息 --- - logger.debug(f"{log_prefix} HeartFChatting: 开始等待新消息 (自 {planner_start_db_time})...") - observation = None - - observation = self.observations[0] - - if observation: - with Timer("Wait New Msg", cycle_timers): # <--- Start Wait timer - wait_start_time = time.monotonic() - while True: - # 检查是否有新消息 - has_new = await observation.has_new_messages_since(planner_start_db_time) - if has_new: - logger.info(f"{log_prefix} HeartFChatting: 检测到新消息,结束等待。") - break # 收到新消息,退出等待 - - # 检查等待是否超时(例如,防止无限等待) - if time.monotonic() - wait_start_time > 60: # 等待60秒示例 - logger.warning(f"{log_prefix} HeartFChatting: 等待新消息超时(60秒)。") - break # 超时退出 - - # 等待一段时间再检查 - try: - await asyncio.sleep(1.5) # 检查间隔 - except asyncio.CancelledError: - logger.info(f"{log_prefix} 等待新消息的 sleep 被中断。") - raise # 重新抛出取消错误,以便外层循环处理 - else: - logger.warning( - f"{log_prefix} HeartFChatting: 无法获取 Observation 实例,无法等待新消息。" - ) - # --- 等待结束 --- - - elif action == "error": # Action specifically set to error by planner - logger.error(f"{log_prefix} HeartFChatting: Planner返回错误状态. 原因: {reasoning}") - action_taken_this_cycle = False - - else: # Unknown action from planner - logger.warning( - f"{log_prefix} HeartFChatting: Planner返回未知动作 '{action}'. 原因: {reasoning}" - ) - action_taken_this_cycle = False - - # --- Print Timer Results --- # - if cycle_timers: # 先检查cycle_timers是否非空 - timer_strings = [] - for name, elapsed in cycle_timers.items(): - # 直接格式化存储在字典中的浮点数 elapsed - formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" - timer_strings.append(f"{name}: {formatted_time}") - - if timer_strings: # 如果有有效计时器数据才打印 - logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") - - # --- Timer Decrement Removed --- # - cycle_duration = time.monotonic() - loop_cycle_start_time - - except Exception as e_cycle: - logger.error(f"{log_prefix} 循环周期执行时发生错误: {e_cycle}") - logger.error(traceback.format_exc()) - if acquired_lock and self._processing_lock.locked(): - self._processing_lock.release() - acquired_lock = False - logger.warning(f"{log_prefix} 由于循环周期中的错误释放了处理锁.") - - finally: - if acquired_lock: - self._processing_lock.release() - # logger.trace(f"{log_prefix} 循环释放了处理锁.") # Reduce noise - - if cycle_duration > 0.1: - logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") - - # --- Delay --- # - try: - sleep_duration = 0.0 - if not action_taken_this_cycle and cycle_duration < 1.5: - sleep_duration = 1.5 - cycle_duration - elif cycle_duration < 0.2: # Keep minimal sleep even after action - sleep_duration = 0.2 - - if sleep_duration > 0: - # logger.debug(f"{log_prefix} Sleeping for {sleep_duration:.2f}s") - await asyncio.sleep(sleep_duration) - - except asyncio.CancelledError: - logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") - break # Exit loop immediately on cancellation + # 防止循环过快消耗资源 + with Timer("Cycle Delay", cycle_timers): + await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) + + # 等待直到所有消息都发送完成 + with Timer("Wait Messages Complete", cycle_timers): + while await self._should_skip_cycle(thinking_id): + await asyncio.sleep(0.2) + + # 完成当前循环并保存历史 + self._current_cycle.complete_cycle() + self._cycle_history.append(self._current_cycle) + + # 记录循环信息和计时器结果 + timer_strings = [] + for name, elapsed in cycle_timers.items(): + formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" + timer_strings.append(f"{name}: {formatted_time}") + + logger.debug( + f"{self.log_prefix} 循环 #{self._current_cycle.cycle_id} 完成, " + f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, " + f"动作: {self._current_cycle.action_type}" + + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "") + ) except asyncio.CancelledError: - logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环被取消了") - except Exception as e_loop_outer: - logger.error(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环意外出错: {e_loop_outer}") + logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的激情水群(HFC)被取消了") + except Exception as e: + logger.error(f"{self.log_prefix} HeartFChatting: 意外错误: {e}") logger.error(traceback.format_exc()) + + @contextlib.asynccontextmanager + async def _get_cycle_context(self): + """ + 循环周期的上下文管理器 + + 用于确保资源的正确获取和释放: + 1. 获取处理锁 + 2. 执行操作 + 3. 释放锁 + """ + acquired = False + try: + await self._processing_lock.acquire() + acquired = True + yield acquired finally: - # State reset is primarily handled by _handle_loop_completion callback - logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环结束。") + if acquired and self._processing_lock.locked(): + self._processing_lock.release() - async def _planner(self) -> Dict[str, Any]: + async def _check_new_messages(self, start_time: float) -> bool: """ - 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 + 检查从指定时间点后是否有新消息 + + 参数: + start_time: 开始检查的时间点 + + 返回: + bool: 是否有新消息 """ - log_prefix = self._get_log_prefix() - observed_messages: List[dict] = [] + try: + new_msg_count = num_new_messages_since(self.stream_id, start_time) + if new_msg_count > 0: + logger.info(f"{self.log_prefix} 检测到{new_msg_count}条新消息") + return True + return False + except Exception as e: + logger.error(f"{self.log_prefix} 检查新消息时出错: {e}") + return False - current_mind: Optional[str] = None - llm_error = False + async def _think_plan_execute( + self, cycle_timers: dict, planner_start_db_time: float + ) -> tuple[bool, str]: + """执行规划阶段""" + try: + # 获取子思维思考结果 + current_mind = "" + with Timer("SubMind Thinking", cycle_timers): + current_mind = await self._get_submind_thinking() + # 记录子思维思考内容 + if self._current_cycle: + self._current_cycle.set_response_info(sub_mind_thinking=current_mind) + + # 执行规划 + with Timer("Planner", cycle_timers): + planner_result = await self._planner(current_mind) + + # 在获取规划结果后检查新消息 + if await self._check_new_messages(planner_start_db_time): + # 更新循环信息 + logger.info(f"{self.log_prefix} 思考到一半,检测到新消息,重新思考") + self._current_cycle.set_action_info("new_messages", "检测到新消息", False) + return False, "new_messages" + + # 解析规划结果 + action = planner_result.get("action", "error") + reasoning = planner_result.get("reasoning", "未提供理由") + + # 更新循环信息 + self._current_cycle.set_action_info(action, reasoning, True) + + # 处理LLM错误 + if planner_result.get("llm_error"): + logger.error(f"{self.log_prefix} LLM失败: {reasoning}") + return False, "" + + # 根据动作类型执行对应处理 + return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) + + except PlannerError as e: + logger.error(f"{self.log_prefix} 规划错误: {e}") + # 更新循环信息 + self._current_cycle.set_action_info("error", str(e), False) + return False, "" + async def _handle_action( + self, + action: str, + reasoning: str, + emoji_query: str, + cycle_timers: dict, + planner_start_db_time: float + ) -> tuple[bool, str]: + """ + 处理规划动作 + + 参数: + action: 动作类型 + reasoning: 决策理由 + emoji_query: 表情查询 + cycle_timers: 计时器字典 + planner_start_db_time: 规划开始时间 + + 返回: + tuple[bool, str]: (是否执行了动作, 思考消息ID) + """ + action_handlers = { + "text_reply": self._handle_text_reply, + "emoji_reply": self._handle_emoji_reply, + "no_reply": self._handle_no_reply + } + + handler = action_handlers.get(action) + if not handler: + logger.warning(f"{self.log_prefix} 未知动作: {action}, 原因: {reasoning}") + return False, "" + + try: + if action == "text_reply": + return await handler(reasoning, emoji_query, cycle_timers) + elif action == "emoji_reply": + return await handler(reasoning, emoji_query), "" + else: # no_reply + return await handler(reasoning, planner_start_db_time, cycle_timers), "" + except HeartFCError as e: + logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") + return False, "" + + async def _handle_text_reply( + self, reasoning: str, emoji_query: str, cycle_timers: dict + ) -> tuple[bool, str]: + """ + 处理文本回复 + + 工作流程: + 1. 获取锚点消息 + 2. 创建思考消息 + 3. 生成回复 + 4. 发送消息 + + 参数: + reasoning: 回复原因 + emoji_query: 表情查询 + cycle_timers: 计时器字典 + + 返回: + tuple[bool, str]: (是否回复成功, 思考消息ID) + """ + + # 获取锚点消息 + anchor_message = await self._get_anchor_message() + if not anchor_message: + raise PlannerError("无法获取锚点消息") + + # 创建思考消息 + thinking_id = await self._create_thinking_message(anchor_message) + if not thinking_id: + raise PlannerError("无法创建思考消息") + + try: + # 生成回复 + with Timer("Replier", cycle_timers): + reply = await self._replier_work( + anchor_message=anchor_message, + thinking_id=thinking_id, + reason=reasoning, + ) + + if not reply: + raise ReplierError("回复生成失败") + + # 发送消息 + with Timer("Sender", cycle_timers): + await self._sender( + thinking_id=thinking_id, + anchor_message=anchor_message, + response_set=reply, + send_emoji=emoji_query, + ) + + return True, thinking_id + + except (ReplierError, SenderError) as e: + logger.error(f"{self.log_prefix} 回复失败: {e}") + return True, thinking_id # 仍然返回thinking_id以便跟踪 + + async def _handle_emoji_reply(self, reasoning: str, emoji_query: str) -> bool: + """ + 处理表情回复 + + 工作流程: + 1. 获取锚点消息 + 2. 发送表情 + + 参数: + reasoning: 回复原因 + emoji_query: 表情查询 + + 返回: + bool: 是否发送成功 + """ + logger.info(f"{self.log_prefix} 决定回复表情({emoji_query}): {reasoning}") + + try: + anchor = await self._get_anchor_message() + if not anchor: + raise PlannerError("无法获取锚点消息") + + await self._handle_emoji(anchor, [], emoji_query) + return True + + except Exception as e: + logger.error(f"{self.log_prefix} 表情发送失败: {e}") + return False + + async def _handle_no_reply( + self, reasoning: str, planner_start_db_time: float, cycle_timers: dict + ) -> bool: + """ + 处理不回复的情况 + + 工作流程: + 1. 等待新消息 + 2. 超时或收到新消息时返回 + + 参数: + reasoning: 不回复的原因 + planner_start_db_time: 规划开始时间 + cycle_timers: 计时器字典 + + 返回: + bool: 是否成功处理 + """ + logger.info(f"{self.log_prefix} 决定不回复: {reasoning}") + + observation = self.observations[0] if self.observations else None + + try: + with Timer("Wait New Msg", cycle_timers): + return await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) + except asyncio.CancelledError: + logger.info(f"{self.log_prefix} 等待被中断") + raise + + async def _wait_for_new_message( + self, observation, planner_start_db_time: float, log_prefix: str + ) -> bool: + """ + 等待新消息 + + 参数: + observation: 观察实例 + planner_start_db_time: 开始等待的时间 + log_prefix: 日志前缀 + + 返回: + bool: 是否检测到新消息 + """ + wait_start_time = time.monotonic() + while True: + if await observation.has_new_messages_since(planner_start_db_time): + logger.info(f"{log_prefix} 检测到新消息") + return True + + if time.monotonic() - wait_start_time > 60: + logger.warning(f"{log_prefix} 等待超时(60秒)") + return False + + await asyncio.sleep(1.5) + + async def _should_skip_cycle(self, thinking_id: str) -> bool: + """检查是否应该跳过当前循环周期""" + return message_manager.check_if_sending_message_exist(self.stream_id, thinking_id) + + async def _log_cycle_timers(self, cycle_timers: dict, log_prefix: str): + """记录循环周期的计时器结果""" + if cycle_timers: + timer_strings = [] + for name, elapsed in cycle_timers.items(): + formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" + timer_strings.append(f"{name}: {formatted_time}") + + if timer_strings: + logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") + + async def _handle_cycle_delay( + self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str + ): + """处理循环延迟""" + cycle_duration = time.monotonic() - cycle_start_time + if cycle_duration > 0.1: + logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") + + try: + sleep_duration = 0.0 + if not action_taken_this_cycle and cycle_duration < 1: + sleep_duration = 1 - cycle_duration + elif cycle_duration < 0.2: + sleep_duration = 0.2 + + if sleep_duration > 0: + await asyncio.sleep(sleep_duration) + + except asyncio.CancelledError: + logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") + raise + + async def _get_submind_thinking(self) -> str: + """ + 获取子思维的思考结果 + + 返回: + str: 思考结果,如果思考失败则返回错误信息 + """ try: observation = self.observations[0] await observation.observe() + current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply() + return current_mind + except Exception as e: + logger.error(f"{self.log_prefix}[SubMind] 思考失败: {e}") + logger.error(traceback.format_exc()) + return "[思考时出错]" + + async def _planner(self, current_mind: str) -> Dict[str, Any]: + """ + 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 + + 参数: + current_mind: 子思维的当前思考结果 + """ + logger.info(f"{self.log_prefix}[Planner] 开始执行规划器") + + planner_timers = {} # 用于存储各阶段计时结果 + + # 获取观察信息 + with Timer("获取观察信息", planner_timers): + observation = self.observations[0] + # await observation.observe() observed_messages = observation.talking_message observed_messages_str = observation.talking_message_str - except Exception as e: - logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}") - - try: - current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply() - except Exception as e_subhf: - logger.error(f"{log_prefix}[Planner] SubHeartflow 思考失败: {e_subhf}") - current_mind = "[思考时出错]" # --- 使用 LLM 进行决策 --- # action = "no_reply" # 默认动作 @@ -434,54 +743,65 @@ class HeartFChatting: llm_error = False # LLM错误标志 try: - prompt = await self._build_planner_prompt( - observed_messages_str, current_mind, self.sub_mind.structured_info - ) - payload = { - "model": self.planner_llm.model_name, - "messages": [{"role": "user", "content": prompt}], - "tools": PLANNER_TOOL_DEFINITION, - "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, - } - - # 执行LLM请求 - try: - response = await self.planner_llm._execute_request( - endpoint="/chat/completions", payload=payload, prompt=prompt + # 构建提示词 + with Timer("构建提示词", planner_timers): + prompt = await self._build_planner_prompt( + observed_messages_str, current_mind, self.sub_mind.structured_info ) - except Exception as req_e: - logger.error(f"{log_prefix}[Planner] LLM请求执行失败: {req_e}") - return { - "action": "error", - "reasoning": f"LLM请求执行失败: {req_e}", - "emoji_query": "", - "current_mind": current_mind, - "observed_messages": observed_messages, - "llm_error": True, + payload = { + "model": self.planner_llm.model_name, + "messages": [{"role": "user", "content": prompt}], + "tools": self.action_manager.get_planner_tool_definition(), + "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, } - # 使用辅助函数处理工具调用响应 - success, arguments, error_msg = process_llm_tool_response( - response, expected_tool_name="decide_reply_action", log_prefix=f"{log_prefix}[Planner] " - ) + # 执行LLM请求 + with Timer("LLM请求", planner_timers): + try: + response = await self.planner_llm._execute_request( + endpoint="/chat/completions", payload=payload, prompt=prompt + ) + except Exception as req_e: + logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") + return { + "action": "error", + "reasoning": f"LLM请求执行失败: {req_e}", + "emoji_query": "", + "current_mind": current_mind, + "observed_messages": observed_messages, + "llm_error": True, + } - if success: - # 提取决策参数 - action = arguments.get("action", "no_reply") - reasoning = arguments.get("reasoning", "未提供理由") - emoji_query = arguments.get("emoji_query", "") + # 处理LLM响应 + with Timer("处理LLM响应", planner_timers): + # 使用辅助函数处理工具调用响应 + success, arguments, error_msg = process_llm_tool_response( + response, expected_tool_name="decide_reply_action", log_prefix=f"{self.log_prefix}[Planner] " + ) - # 记录决策结果 - logger.debug(f"{log_prefix}[Planner] 决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'") - else: - # 处理工具调用失败 - logger.warning(f"{log_prefix}[Planner] {error_msg}") - action = "error" - reasoning = error_msg - llm_error = True + if success: + # 提取决策参数 + action = arguments.get("action", "no_reply") + # 验证动作是否在可用动作集中 + if action not in self.action_manager.get_available_actions(): + logger.warning(f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action},使用默认动作no_reply") + action = "no_reply" + reasoning = f"LLM返回了未授权的动作: {action}" + else: + reasoning = arguments.get("reasoning", "未提供理由") + emoji_query = arguments.get("emoji_query", "") + + # 记录决策结果 + logger.debug(f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'") + else: + # 处理工具调用失败 + logger.warning(f"{self.log_prefix}[Planner] {error_msg}") + action = "error" + reasoning = error_msg + llm_error = True except Exception as llm_e: - logger.error(f"{log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}") + logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}") logger.error(traceback.format_exc()) # 记录完整堆栈以便调试 action = "error" reasoning = f"LLM处理失败: {llm_e}" @@ -524,12 +844,12 @@ class HeartFChatting: anchor_message = MessageRecv(placeholder_msg_dict) anchor_message.update_chat_stream(self.chat_stream) logger.info( - f"{self._get_log_prefix()} Created placeholder anchor message: ID={anchor_message.message_info.message_id}" + f"{self.log_prefix} Created placeholder anchor message: ID={anchor_message.message_info.message_id}" ) return anchor_message except Exception as e: - logger.error(f"{self._get_log_prefix()} Error getting/creating anchor message: {e}") + logger.error(f"{self.log_prefix} Error getting/creating anchor message: {e}") logger.error(traceback.format_exc()) return None @@ -545,7 +865,7 @@ class HeartFChatting: 发送器 (Sender): 使用本类的方法发送生成的回复。 处理相关的操作,如发送表情和更新关系。 """ - log_prefix = self._get_log_prefix() + logger.info(f"{self.log_prefix}开始发送回复") first_bot_msg: Optional[MessageSending] = None # 尝试发送回复消息 @@ -553,43 +873,42 @@ class HeartFChatting: if first_bot_msg: # --- 处理关联表情(如果指定) --- # if send_emoji: - logger.info(f"{log_prefix}[Sender-{thinking_id}] 正在发送关联表情: '{send_emoji}'") + logger.info(f"{self.log_prefix}正在发送关联表情: '{send_emoji}'") # 优先使用first_bot_msg作为锚点,否则回退到原始锚点 emoji_anchor = first_bot_msg if first_bot_msg else anchor_message await self._handle_emoji(emoji_anchor, response_set, send_emoji) else: - # logger.warning(f"{log_prefix}[Sender-{thinking_id}] 发送回复失败(_send_response_messages返回None)。思考消息{thinking_id}可能已被移除。") + # logger.warning(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复失败(_send_response_messages返回None)。思考消息{thinking_id}可能已被移除。") # 无需清理,因为_send_response_messages返回None意味着已处理/已删除 raise RuntimeError("发送回复失败,_send_response_messages返回None") async def shutdown(self): """优雅关闭HeartFChatting实例,取消活动循环任务""" - log_prefix = self._get_log_prefix() - logger.info(f"{log_prefix} 正在关闭HeartFChatting...") + logger.info(f"{self.log_prefix} 正在关闭HeartFChatting...") # 取消循环任务 if self._loop_task and not self._loop_task.done(): - logger.info(f"{log_prefix} 正在取消HeartFChatting循环任务") + logger.info(f"{self.log_prefix} 正在取消HeartFChatting循环任务") self._loop_task.cancel() try: await asyncio.wait_for(self._loop_task, timeout=1.0) - logger.info(f"{log_prefix} HeartFChatting循环任务已取消") + logger.info(f"{self.log_prefix} HeartFChatting循环任务已取消") except (asyncio.CancelledError, asyncio.TimeoutError): pass except Exception as e: - logger.error(f"{log_prefix} 取消循环任务出错: {e}") + logger.error(f"{self.log_prefix} 取消循环任务出错: {e}") else: - logger.info(f"{log_prefix} 没有活动的HeartFChatting循环任务") + logger.info(f"{self.log_prefix} 没有活动的HeartFChatting循环任务") # 清理状态 self._loop_active = False self._loop_task = None if self._processing_lock.locked(): self._processing_lock.release() - logger.warning(f"{log_prefix} 已释放处理锁") + logger.warning(f"{self.log_prefix} 已释放处理锁") - logger.info(f"{log_prefix} HeartFChatting关闭完成") + logger.info(f"{self.log_prefix} HeartFChatting关闭完成") async def _build_planner_prompt( self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any] @@ -637,7 +956,6 @@ class HeartFChatting: """ 回复器 (Replier): 核心逻辑用于生成回复。 """ - log_prefix = self._get_log_prefix() response_set: Optional[List[str]] = None try: response_set = await self.gpt_instance.generate_response( @@ -647,15 +965,18 @@ class HeartFChatting: message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter) thinking_id=thinking_id, # Pass thinking_id positionally ) + + + if not response_set: - logger.warning(f"{log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。") + logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。") return None return response_set except Exception as e: - logger.error(f"{log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}") + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}") logger.error(traceback.format_exc()) return None @@ -663,7 +984,7 @@ class HeartFChatting: async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]: """创建思考消息 (尝试锚定到 anchor_message)""" if not anchor_message or not anchor_message.chat_stream: - logger.error(f"{self._get_log_prefix()} 无法创建思考消息,缺少有效的锚点消息或聊天流。") + logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") return None chat = anchor_message.chat_stream @@ -692,9 +1013,16 @@ class HeartFChatting: ) -> Optional[MessageSending]: """发送回复消息 (尝试锚定到 anchor_message)""" if not anchor_message or not anchor_message.chat_stream: - logger.error(f"{self._get_log_prefix()} 无法发送回复,缺少有效的锚点消息或聊天流。") + logger.error(f"{self.log_prefix} 无法发送回复,缺少有效的锚点消息或聊天流。") return None + # 记录锚点消息ID + if self._current_cycle and anchor_message: + self._current_cycle.set_response_info( + response_text=response_set, + anchor_message_id=anchor_message.message_info.message_id + ) + chat = anchor_message.chat_stream container = await message_manager.get_container(chat.stream_id) thinking_message = None @@ -704,7 +1032,7 @@ class HeartFChatting: if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: thinking_message = msg container.messages.remove(msg) # Remove the message directly here - logger.debug(f"{self._get_log_prefix()} Removed thinking message {thinking_id} via iteration.") + # logger.debug(f"{self.log_prefix} Removed thinking message {thinking_id} via iteration.") break if not thinking_message: @@ -716,6 +1044,7 @@ class HeartFChatting: message_set = MessageSet(chat, thinking_id) mark_head = False first_bot_msg = None + reply_message_ids = [] # 用于记录所有回复消息的ID bot_user_info = UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, @@ -738,6 +1067,11 @@ class HeartFChatting: mark_head = True first_bot_msg = bot_message message_set.add_message(bot_message) + reply_message_ids.append(bot_message.message_info.message_id) + + # 记录回复消息ID列表 + if self._current_cycle: + self._current_cycle.set_response_info(reply_message_ids=reply_message_ids) # Access MessageManager directly await message_manager.add_message(message_set) @@ -745,9 +1079,8 @@ class HeartFChatting: async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""): """处理表情包 (尝试锚定到 anchor_message)""" - if not anchor_message or not anchor_message.chat_stream: - logger.error(f"{self._get_log_prefix()} 无法处理表情包,缺少有效的锚点消息或聊天流。") + logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。") return chat = anchor_message.chat_stream @@ -759,7 +1092,13 @@ class HeartFChatting: emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source) if emoji_raw: - emoji_path, _description = emoji_raw + emoji_path, description = emoji_raw + # 记录表情信息 + if self._current_cycle: + self._current_cycle.set_response_info( + emoji_info=f"表情: {description}, 路径: {emoji_path}" + ) + emoji_cq = image_path_to_base64(emoji_path) thinking_time_point = round(time.time(), 2) message_segment = Seg(type="emoji", data=emoji_cq) @@ -780,3 +1119,24 @@ class HeartFChatting: ) # Access MessageManager directly await message_manager.add_message(bot_message) + + def get_cycle_history(self, last_n: Optional[int] = None) -> List[Dict[str, Any]]: + """获取循环历史记录 + + 参数: + last_n: 获取最近n个循环的信息,如果为None则获取所有历史记录 + + 返回: + List[Dict[str, Any]]: 循环历史记录列表 + """ + history = list(self._cycle_history) + if last_n is not None: + history = history[-last_n:] + return [cycle.to_dict() for cycle in history] + + def get_last_cycle_info(self) -> Optional[Dict[str, Any]]: + """获取最近一个循环的信息""" + if self._cycle_history: + return self._cycle_history[-1].to_dict() + return None + diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index da43c334f..c489e012c 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -57,9 +57,6 @@ class HeartFCGenerator: ) if model_response: - logger.info( - f"{global_config.BOT_NICKNAME}的回复是:{model_response},生成回复时间: {t_generate_response.human_readable}" - ) model_processed_response = await self._process_response(model_response) return model_processed_response diff --git a/src/plugins/heartFC_chat/heartFC_readme.md b/src/plugins/heartFC_chat/heartFC_readme.md new file mode 100644 index 000000000..07bc4c63c --- /dev/null +++ b/src/plugins/heartFC_chat/heartFC_readme.md @@ -0,0 +1,159 @@ +# HeartFC_chat 工作原理文档 + +HeartFC_chat 是一个基于心流理论的聊天系统,通过模拟人类的思维过程和情感变化来实现自然的对话交互。系统采用Plan-Replier-Sender循环机制,实现了智能化的对话决策和生成。 + +## 核心工作流程 + +### 1. 消息处理与存储 (HeartFCProcessor) +[代码位置: src/plugins/heartFC_chat/heartflow_processor.py] + +消息处理器负责接收和预处理消息,主要完成以下工作: +```mermaid +graph TD + A[接收原始消息] --> B[解析为MessageRecv对象] + B --> C[消息缓冲处理] + C --> D[过滤检查] + D --> E[存储到数据库] +``` + +核心实现: +- 消息处理入口:`process_message()` [行号: 38-215] + - 消息解析和缓冲:`message_buffer.start_caching_messages()` [行号: 63] + - 过滤检查:`_check_ban_words()`, `_check_ban_regex()` [行号: 196-215] + - 消息存储:`storage.store_message()` [行号: 108] + +### 2. 对话管理循环 (HeartFChatting) +[代码位置: src/plugins/heartFC_chat/heartFC_chat.py] + +HeartFChatting是系统的核心组件,实现了完整的对话管理循环: + +```mermaid +graph TD + A[Plan阶段] -->|决策是否回复| B[Replier阶段] + B -->|生成回复内容| C[Sender阶段] + C -->|发送消息| D[等待新消息] + D --> A +``` + +#### Plan阶段 [行号: 282-386] +- 主要函数:`_planner()` +- 功能实现: + * 获取观察信息:`observation.observe()` [行号: 297] + * 思维处理:`sub_mind.do_thinking_before_reply()` [行号: 301] + * LLM决策:使用`PLANNER_TOOL_DEFINITION`进行动作规划 [行号: 13-42] + +#### Replier阶段 [行号: 388-416] +- 主要函数:`_replier_work()` +- 调用生成器:`gpt_instance.generate_response()` [行号: 394] +- 处理生成结果和错误情况 + +#### Sender阶段 [行号: 418-450] +- 主要函数:`_sender()` +- 发送实现: + * 创建消息:`_create_thinking_message()` [行号: 452-477] + * 发送回复:`_send_response_messages()` [行号: 479-525] + * 处理表情:`_handle_emoji()` [行号: 527-567] + +### 3. 回复生成机制 (HeartFCGenerator) +[代码位置: src/plugins/heartFC_chat/heartFC_generator.py] + +回复生成器负责产生高质量的回复内容: + +```mermaid +graph TD + A[获取上下文信息] --> B[构建提示词] + B --> C[调用LLM生成] + C --> D[后处理优化] + D --> E[返回回复集] +``` + +核心实现: +- 生成入口:`generate_response()` [行号: 39-67] + * 情感调节:`arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()` [行号: 47] + * 模型生成:`_generate_response_with_model()` [行号: 69-95] + * 响应处理:`_process_response()` [行号: 97-106] + +### 4. 提示词构建系统 (HeartFlowPromptBuilder) +[代码位置: src/plugins/heartFC_chat/heartflow_prompt_builder.py] + +提示词构建器支持两种工作模式,HeartFC_chat专门使用Focus模式,而Normal模式是为normal_chat设计的: + +#### 专注模式 (Focus Mode) - HeartFC_chat专用 +- 实现函数:`_build_prompt_focus()` [行号: 116-141] +- 特点: + * 专注于当前对话状态和思维 + * 更强的目标导向性 + * 用于HeartFC_chat的Plan-Replier-Sender循环 + * 简化的上下文处理,专注于决策 + +#### 普通模式 (Normal Mode) - Normal_chat专用 +- 实现函数:`_build_prompt_normal()` [行号: 143-215] +- 特点: + * 用于normal_chat的常规对话 + * 完整的个性化处理 + * 关系系统集成 + * 知识库检索:`get_prompt_info()` [行号: 217-591] + +HeartFC_chat的Focus模式工作流程: +```mermaid +graph TD + A[获取结构化信息] --> B[获取当前思维状态] + B --> C[构建专注模式提示词] + C --> D[用于Plan阶段决策] + D --> E[用于Replier阶段生成] +``` + +## 智能特性 + +### 1. 对话决策机制 +- LLM决策工具定义:`PLANNER_TOOL_DEFINITION` [heartFC_chat.py 行号: 13-42] +- 决策执行:`_planner()` [heartFC_chat.py 行号: 282-386] +- 考虑因素: + * 上下文相关性 + * 情感状态 + * 兴趣程度 + * 对话时机 + +### 2. 状态管理 +[代码位置: src/plugins/heartFC_chat/heartFC_chat.py] +- 状态机实现:`HeartFChatting`类 [行号: 44-567] +- 核心功能: + * 初始化:`_initialize()` [行号: 89-112] + * 循环控制:`_run_pf_loop()` [行号: 192-281] + * 状态转换:`_handle_loop_completion()` [行号: 166-190] + +### 3. 回复生成策略 +[代码位置: src/plugins/heartFC_chat/heartFC_generator.py] +- 温度调节:`current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier` [行号: 48] +- 生成控制:`_generate_response_with_model()` [行号: 69-95] +- 响应处理:`_process_response()` [行号: 97-106] + +## 系统配置 + +### 关键参数 +- LLM配置:`model_normal` [heartFC_generator.py 行号: 32-37] +- 过滤规则:`_check_ban_words()`, `_check_ban_regex()` [heartflow_processor.py 行号: 196-215] +- 状态控制:`INITIAL_DURATION = 60.0` [heartFC_chat.py 行号: 11] + +### 优化建议 +1. 调整LLM参数:`temperature`和`max_tokens` +2. 优化提示词模板:`init_prompt()` [heartflow_prompt_builder.py 行号: 8-115] +3. 配置状态转换条件 +4. 维护过滤规则 + +## 注意事项 + +1. 系统稳定性 +- 异常处理:各主要函数都包含try-except块 +- 状态检查:`_processing_lock`确保并发安全 +- 循环控制:`_loop_active`和`_loop_task`管理 + +2. 性能优化 +- 缓存使用:`message_buffer`系统 +- LLM调用优化:批量处理和复用 +- 异步处理:使用`asyncio` + +3. 质量控制 +- 日志记录:使用`get_module_logger()` +- 错误追踪:详细的异常记录 +- 响应监控:完整的状态跟踪 diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index f7c3a64fd..27c88a983 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -12,6 +12,7 @@ from ..chat.chat_stream import chat_manager from ..chat.message_buffer import message_buffer from ..utils.timer_calculater import Timer from src.plugins.person_info.relationship_manager import relationship_manager +from typing import Optional, Tuple # 定义日志配置 processor_config = LogConfig( @@ -22,193 +23,204 @@ logger = get_module_logger("heartflow_processor", config=processor_config) class HeartFCProcessor: + """心流处理器,负责处理接收到的消息并计算兴趣度""" + def __init__(self): + """初始化心流处理器,创建消息存储实例""" self.storage = MessageStorage() - async def process_message(self, message_data: str) -> None: - """处理接收到的原始消息数据,完成消息解析、缓冲、过滤、存储、兴趣度计算与更新等核心流程。 - - 此函数是消息处理的核心入口,负责接收原始字符串格式的消息数据,并将其转化为结构化的 `MessageRecv` 对象。 - 主要执行步骤包括: - 1. 解析 `message_data` 为 `MessageRecv` 对象,提取用户信息、群组信息等。 - 2. 将消息加入 `message_buffer` 进行缓冲处理,以应对消息轰炸或者某些人一条消息分几次发等情况。 - 3. 获取或创建对应的 `chat_stream` 和 `subheartflow` 实例,用于管理会话状态和心流。 - 4. 对消息内容进行初步处理(如提取纯文本)。 - 5. 应用全局配置中的过滤词和正则表达式,过滤不符合规则的消息。 - 6. 查询消息缓冲结果,如果消息被缓冲器拦截(例如,判断为消息轰炸的一部分),则中止后续处理。 - 7. 对于通过缓冲的消息,将其存储到 `MessageStorage` 中。 - - 8. 调用海马体(`HippocampusManager`)计算消息内容的记忆激活率。(这部分算法后续会进行优化) - 9. 根据是否被提及(@)和记忆激活率,计算最终的兴趣度增量。(提及的额外兴趣增幅) - 10. 使用计算出的增量更新 `InterestManager` 中对应会话的兴趣度。 - 11. 记录处理后的消息信息及当前的兴趣度到日志。 - - 注意:此函数本身不负责生成和发送回复。回复的决策和生成逻辑被移至 `HeartFC_Chat` 类中的监控任务, - 该任务会根据 `InterestManager` 中的兴趣度变化来决定何时触发回复。 - + async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None: + """统一的错误处理函数 + Args: - message_data: str: 从消息源接收到的原始消息字符串。 + error: 捕获到的异常 + context: 错误发生的上下文描述 + message: 可选的消息对象,用于记录相关消息内容 + """ + logger.error(f"{context}: {error}") + logger.error(traceback.format_exc()) + if message and hasattr(message, 'raw_message'): + logger.error(f"相关消息原始内容: {message.raw_message}") + + async def _process_relationship(self, message: MessageRecv) -> None: + """处理用户关系逻辑 + + Args: + message: 消息对象,包含用户信息 + """ + platform = message.message_info.platform + user_id = message.message_info.user_info.user_id + nickname = message.message_info.user_info.user_nickname + cardname = message.message_info.user_info.user_cardname or nickname + + is_known = await relationship_manager.is_known_some_one(platform, user_id) + + if not is_known: + logger.info(f"首次认识用户: {nickname}") + await relationship_manager.first_knowing_some_one( + platform, user_id, nickname, cardname, "" + ) + elif not await relationship_manager.is_qved_name(platform, user_id): + logger.info(f"给用户({nickname},{cardname})取名: {nickname}") + await relationship_manager.first_knowing_some_one( + platform, user_id, nickname, cardname, "" + ) + + async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]: + """计算消息的兴趣度 + + Args: + message: 待处理的消息对象 + + Returns: + Tuple[float, bool]: (兴趣度, 是否被提及) + """ + is_mentioned, _ = is_mentioned_bot_in_message(message) + interested_rate = 0.0 + + with Timer("记忆激活"): + interested_rate = await HippocampusManager.get_instance().get_activate_from_text( + message.processed_plain_text, + fast_retrieval=True, + ) + logger.trace(f"记忆激活率: {interested_rate:.2f}") + + if is_mentioned: + interest_increase_on_mention = 1 + interested_rate += interest_increase_on_mention + + return interested_rate, is_mentioned + + def _get_message_type(self, message: MessageRecv) -> str: + """获取消息类型 + + Args: + message: 消息对象 + + Returns: + str: 消息类型 + """ + if message.message_segment.type != "seglist": + return message.message_segment.type + + if (isinstance(message.message_segment.data, list) + and all(isinstance(x, Seg) for x in message.message_segment.data) + and len(message.message_segment.data) == 1): + return message.message_segment.data[0].type + + return "seglist" + + async def process_message(self, message_data: str) -> None: + """处理接收到的原始消息数据 + + 主要流程: + 1. 消息解析与初始化 + 2. 消息缓冲处理 + 3. 过滤检查 + 4. 兴趣度计算 + 5. 关系处理 + + Args: + message_data: 原始消息字符串 """ - timing_results = {} # 初始化 timing_results message = None try: + # 1. 消息解析与初始化 message = MessageRecv(message_data) groupinfo = message.message_info.group_info userinfo = message.message_info.user_info messageinfo = message.message_info - # 消息加入缓冲池 + # 2. 消息缓冲与流程序化 await message_buffer.start_caching_messages(message) - - # 创建聊天流 + chat = await chat_manager.get_or_create_stream( platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo, ) - + subheartflow = await heartflow.create_subheartflow(chat.stream_id) - message.update_chat_stream(chat) - - await heartflow.create_subheartflow(chat.stream_id) - await message.process() - logger.trace(f"消息处理成功: {message.processed_plain_text}") - - # 过滤词/正则表达式过滤 - if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( - message.raw_message, chat, userinfo - ): + + # 3. 过滤检查 + if self._check_ban_words(message.processed_plain_text, chat, userinfo) or \ + self._check_ban_regex(message.raw_message, chat, userinfo): return - # 查询缓冲器结果 + # 4. 缓冲检查 buffer_result = await message_buffer.query_buffer_result(message) - - # 处理缓冲器结果 (Bombing logic) if not buffer_result: - f_type = "seglist" - if message.message_segment.type != "seglist": - f_type = message.message_segment.type - else: - if ( - isinstance(message.message_segment.data, list) - and all(isinstance(x, Seg) for x in message.message_segment.data) - and len(message.message_segment.data) == 1 - ): - f_type = message.message_segment.data[0].type - if f_type == "text": - logger.debug(f"触发缓冲,消息:{message.processed_plain_text}") - elif f_type == "image": - logger.debug("触发缓冲,表情包/图片等待中") - elif f_type == "seglist": - logger.debug("触发缓冲,消息列表等待中") - return # 被缓冲器拦截,不生成回复 - - # ---- 只有通过缓冲的消息才进行存储和后续处理 ---- - - # 存储消息 (使用可能被缓冲器更新过的 message) - try: - await self.storage.store_message(message, chat) - logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}") - except Exception as e: - logger.error(f"存储消息失败: {e}") - logger.error(traceback.format_exc()) - # 存储失败可能仍需考虑是否继续,暂时返回 + msg_type = self._get_message_type(message) + type_messages = { + "text": f"触发缓冲,消息:{message.processed_plain_text}", + "image": "触发缓冲,表情包/图片等待中", + "seglist": "触发缓冲,消息列表等待中" + } + logger.debug(type_messages.get(msg_type, "触发未知类型缓冲")) return - # 激活度计算 (使用可能被缓冲器更新过的 message.processed_plain_text) - is_mentioned, _ = is_mentioned_bot_in_message(message) - interested_rate = 0.0 # 默认值 - try: - with Timer("记忆激活", timing_results): - interested_rate = await HippocampusManager.get_instance().get_activate_from_text( - message.processed_plain_text, - fast_retrieval=True, # 使用更新后的文本 - ) - logger.trace(f"记忆激活率 (通过缓冲后): {interested_rate:.2f}") - except Exception as e: - logger.error(f"计算记忆激活率失败: {e}") - logger.error(traceback.format_exc()) + # 5. 消息存储 + await self.storage.store_message(message, chat) + logger.trace(f"存储成功: {message.processed_plain_text}") - # --- 修改:兴趣度更新逻辑 --- # - if is_mentioned: - interest_increase_on_mention = 1 - mentioned_boost = interest_increase_on_mention # 从配置获取提及增加值 - interested_rate += mentioned_boost - - # 更新兴趣度 (调用 SubHeartflow 的方法) + # 6. 兴趣度计算与更新 + interested_rate, is_mentioned = await self._calculate_interest(message) current_time = time.time() await subheartflow.interest_chatting.increase_interest(current_time, value=interested_rate) - - # 添加到 SubHeartflow 的 interest_dict,给normal_chat处理 await subheartflow.add_interest_dict_entry(message, interested_rate, is_mentioned) - # 打印消息接收和处理信息 + # 7. 日志记录 mes_name = chat.group_info.group_name if chat.group_info else "私聊" - current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time)) + current_time = time.strftime("%H点%M分%S秒", time.localtime(message.message_info.time)) logger.info( f"[{current_time}][{mes_name}]" - f"{message.message_info.user_info.user_nickname}:" + f"{userinfo.user_nickname}:" f"{message.processed_plain_text}" f"[兴趣度: {interested_rate:.2f}]" ) - try: - is_known = await relationship_manager.is_known_some_one( - message.message_info.platform, message.message_info.user_info.user_id - ) - if not is_known: - logger.info(f"首次认识用户: {message.message_info.user_info.user_nickname}") - await relationship_manager.first_knowing_some_one( - message.message_info.platform, - message.message_info.user_info.user_id, - message.message_info.user_info.user_nickname, - message.message_info.user_info.user_cardname or message.message_info.user_info.user_nickname, - "", - ) - else: - # logger.debug(f"已认识用户: {message.message_info.user_info.user_nickname}") - if not await relationship_manager.is_qved_name( - message.message_info.platform, message.message_info.user_info.user_id - ): - logger.info(f"更新已认识但未取名的用户: {message.message_info.user_info.user_nickname}") - await relationship_manager.first_knowing_some_one( - message.message_info.platform, - message.message_info.user_info.user_id, - message.message_info.user_info.user_nickname, - message.message_info.user_info.user_cardname - or message.message_info.user_info.user_nickname, - "", - ) - except Exception as e: - logger.error(f"处理认识关系失败: {e}") - logger.error(traceback.format_exc()) + # 8. 关系处理 + await self._process_relationship(message) except Exception as e: - logger.error(f"消息处理失败 (process_message V3): {e}") - logger.error(traceback.format_exc()) - if message: # 记录失败的消息内容 - logger.error(f"失败消息原始内容: {message.raw_message}") + await self._handle_error(e, "消息处理失败", message) def _check_ban_words(self, text: str, chat, userinfo) -> bool: - """检查消息中是否包含过滤词""" + """检查消息是否包含过滤词 + + Args: + text: 待检查的文本 + chat: 聊天对象 + userinfo: 用户信息 + + Returns: + bool: 是否包含过滤词 + """ for word in global_config.ban_words: if word in text: - logger.info( - f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" - ) + chat_name = chat.group_info.group_name if chat.group_info else "私聊" + logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") logger.info(f"[过滤词识别]消息中含有{word},filtered") return True return False def _check_ban_regex(self, text: str, chat, userinfo) -> bool: - """检查消息是否匹配过滤正则表达式""" + """检查消息是否匹配过滤正则表达式 + + Args: + text: 待检查的文本 + chat: 聊天对象 + userinfo: 用户信息 + + Returns: + bool: 是否匹配过滤正则 + """ for pattern in global_config.ban_msgs_regex: if pattern.search(text): - logger.info( - f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}" - ) + chat_name = chat.group_info.group_name if chat.group_info else "私聊" + logger.info(f"[{chat_name}]{userinfo.user_nickname}:{text}") logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered") return True return False diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 102aef52b..146a5307f 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -21,8 +21,7 @@ logger = get_module_logger("prompt") def init_prompt(): Prompt( """ -你有以下信息可供参考: -{structured_info} +{info_from_tools} {chat_target} {chat_talking_prompt} 现在你想要在群里发言或者回复。\n @@ -38,6 +37,12 @@ def init_prompt(): {moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) + + Prompt(""" +你有以下信息可供参考: +{structured_info} +以上的消息是你获取到的消息,或许可以帮助你更好地回复。 +""", "info_from_tools") # Planner提示词 Prompt( @@ -47,13 +52,9 @@ def init_prompt(): 看了以上内容,你产生的内心想法是: {current_mind_block} 请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。 -决策依据: +注意你必须参考以下决策依据来选择工具: 1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。 -2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (每个标签用一个词组表示,格式如下: - 幽默的讽刺 - 悲伤的无奈 - 愤怒的抗议 - 愤怒的讽刺)。 +2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (每个标签用一个词组表示,格式例如:幽默的讽刺,单纯的开心,愤怒的抗议)。 3. 如果聊天内容或你的内心想法适合用一个表情来回应,选择 'emoji_reply' 并提供表情主题 'emoji_query'。 4. 如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。 5. 如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;。 @@ -152,7 +153,7 @@ class PromptBuilder: message_list_before_now, replace_bot_name=True, merge_messages=False, - timestamp_mode="relative", + timestamp_mode="normal", read_mark=0.0, ) @@ -162,12 +163,19 @@ class PromptBuilder: prompt_ger += "你喜欢用倒装句" if random.random() < 0.02: prompt_ger += "你喜欢用反问句" + + if structured_info: + structured_info_prompt = await global_prompt_manager.format_prompt( + "info_from_tools", + structured_info = structured_info) + else: + structured_info_prompt = "" logger.debug("开始构建prompt") prompt = await global_prompt_manager.format_prompt( "heart_flow_prompt", - structured_info=structured_info, + info_from_tools=structured_info_prompt, chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1") if chat_in_group else await global_prompt_manager.get_prompt_async("chat_target_private1"), diff --git a/src/plugins/moods/moods.py b/src/plugins/moods/moods.py index e3fb377c6..eea2177ff 100644 --- a/src/plugins/moods/moods.py +++ b/src/plugins/moods/moods.py @@ -256,7 +256,7 @@ class MoodManager: def print_mood_status(self) -> None: """打印当前情绪状态""" logger.info( - f"[情绪状态]愉悦度: {self.current_mood.valence:.2f}, " + f"愉悦度: {self.current_mood.valence:.2f}, " f"唤醒度: {self.current_mood.arousal:.2f}, " f"心情: {self.current_mood.text}" ) diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index 6ae6ccc32..edd60c05a 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -304,7 +304,7 @@ async def build_readable_messages( readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode) read_mark_line = ( - f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 请关注你上次思考之后以下的新消息---\n" + f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" ) # 组合结果,确保空部分不引入多余的标记或换行 From fd052cd43b376039d853dfd209a2e34f5366fcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 25 Apr 2025 18:32:11 +0800 Subject: [PATCH 24/79] =?UTF-8?q?feat(KnowledgeFetcher):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0LPMM=E7=9F=A5=E8=AF=86=E5=BA=93=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为KnowledgeFetcher类新增_lpmm_get_knowledge方法,用于从LPMM知识库中获取相关知识。同时,在fetch方法中整合了LPMM知识库查询结果,以提供更全面的知识参考。 --- src/plugins/PFC/pfc_KnowledgeFetcher.py | 29 ++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py index 1a0d495c3..63f71aad8 100644 --- a/src/plugins/PFC/pfc_KnowledgeFetcher.py +++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py @@ -4,6 +4,7 @@ from src.plugins.memory_system.Hippocampus import HippocampusManager from ..models.utils_model import LLMRequest from ...config.config import global_config from ..chat.message import Message +from ..knowledge.knowledge_lib import qa_manager logger = get_module_logger("knowledge_fetcher") @@ -18,6 +19,25 @@ class KnowledgeFetcher: max_tokens=1000, request_type="knowledge_fetch", ) + + def _lpmm_get_knowledge(self, query: str) -> str: + """获取相关知识 + + Args: + query: 查询内容 + + Returns: + str: 构造好的,带相关度的知识 + """ + + logger.debug("正在从LPMM知识库中获取知识") + try: + knowledge_info = qa_manager.get_knowledge(query) + logger.debug(f"LPMM知识库查询结果: {knowledge_info:150}") + return knowledge_info + except Exception as e: + logger.error(f"LPMM知识库搜索工具执行失败: {str(e)}") + return "未找到匹配的知识" async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]: """获取相关知识 @@ -43,13 +63,16 @@ class KnowledgeFetcher: max_depth=3, fast_retrieval=False, ) - + knowledge = "" if related_memory: - knowledge = "" + sources = [] for memory in related_memory: knowledge += memory[1] + "\n" sources.append(f"记忆片段{memory[0]}") - return knowledge.strip(), ",".join(sources) + knowledge = knowledge.strip(), ",".join(sources) + + knowledge +="现在有以下**知识**可供参考:\n 请记住这些**知识**,并根据**知识**回答问题。\n" + knowledge += self._lpmm_get_knowledge(query) return "未找到相关知识", "无记忆匹配" From 33253cb2c9e6320b674d1304a75675c08d086a23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Apr 2025 10:32:23 +0000 Subject: [PATCH 25/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_KnowledgeFetcher.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py index 63f71aad8..54990de70 100644 --- a/src/plugins/PFC/pfc_KnowledgeFetcher.py +++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py @@ -19,7 +19,7 @@ class KnowledgeFetcher: max_tokens=1000, request_type="knowledge_fetch", ) - + def _lpmm_get_knowledge(self, query: str) -> str: """获取相关知识 @@ -29,7 +29,7 @@ class KnowledgeFetcher: Returns: str: 构造好的,带相关度的知识 """ - + logger.debug("正在从LPMM知识库中获取知识") try: knowledge_info = qa_manager.get_knowledge(query) @@ -65,14 +65,13 @@ class KnowledgeFetcher: ) knowledge = "" if related_memory: - sources = [] for memory in related_memory: knowledge += memory[1] + "\n" sources.append(f"记忆片段{memory[0]}") knowledge = knowledge.strip(), ",".join(sources) - - knowledge +="现在有以下**知识**可供参考:\n 请记住这些**知识**,并根据**知识**回答问题。\n" + + knowledge += "现在有以下**知识**可供参考:\n 请记住这些**知识**,并根据**知识**回答问题。\n" knowledge += self._lpmm_get_knowledge(query) return "未找到相关知识", "无记忆匹配" From 274366f86d91ca27548cf0543b1de67f4b7f59f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 25 Apr 2025 18:34:26 +0800 Subject: [PATCH 26/79] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=80=E4=B8=8Bpromp?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/pfc_KnowledgeFetcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py index 54990de70..95e66c8cd 100644 --- a/src/plugins/PFC/pfc_KnowledgeFetcher.py +++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py @@ -71,7 +71,8 @@ class KnowledgeFetcher: sources.append(f"记忆片段{memory[0]}") knowledge = knowledge.strip(), ",".join(sources) - knowledge += "现在有以下**知识**可供参考:\n 请记住这些**知识**,并根据**知识**回答问题。\n" + knowledge += "现在有以下**知识**可供参考:\n " knowledge += self._lpmm_get_knowledge(query) + knowledge += "请记住这些**知识**,并根据**知识**回答问题。\n" return "未找到相关知识", "无记忆匹配" From 21d1f102e4d791d5effe2221e10ac34b830e4d43 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 18:37:14 +0800 Subject: [PATCH 27/79] =?UTF-8?q?fix=EF=BC=9A=E6=98=AF=E5=90=A6=E4=BF=9D?= =?UTF-8?q?=E6=8A=A4=E9=A2=9C=E6=96=87=E5=AD=97=E8=BF=9B=E5=85=A5config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 19 +++++-------------- src/plugins/chat/utils.py | 13 ++++++++----- template/bot_config_template.toml | 9 +++------ 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index 1cc58f71b..187bb6cde 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -221,7 +221,6 @@ class BotConfig: max_emoji_num: int = 200 # 表情包最大数量 max_reach_deletion: bool = True # 开启则在达到最大数量时删除表情包,关闭则不会继续收集表情包 EMOJI_CHECK_INTERVAL: int = 120 # 表情包检查间隔(分钟) - EMOJI_REGISTER_INTERVAL: int = 10 # 表情包注册间隔(分钟) save_pic: bool = False # 是否保存图片 save_emoji: bool = False # 是否保存表情包 @@ -263,6 +262,7 @@ class BotConfig: chinese_typo_word_replace_rate = 0.02 # 整词替换概率 # response_splitter + enable_kaomoji_protection = False # 是否启用颜文字保护 enable_response_splitter = True # 是否启用回复分割器 response_max_length = 100 # 回复允许的最大长度 response_max_sentence_num = 3 # 回复允许的最大句子数 @@ -394,7 +394,6 @@ class BotConfig: def emoji(parent: dict): emoji_config = parent["emoji"] config.EMOJI_CHECK_INTERVAL = emoji_config.get("check_interval", config.EMOJI_CHECK_INTERVAL) - config.EMOJI_REGISTER_INTERVAL = emoji_config.get("register_interval", config.EMOJI_REGISTER_INTERVAL) config.EMOJI_CHECK_PROMPT = emoji_config.get("check_prompt", config.EMOJI_CHECK_PROMPT) config.EMOJI_CHECK = emoji_config.get("enable_check", config.EMOJI_CHECK) if config.INNER_VERSION in SpecifierSet(">=1.1.1"): @@ -428,21 +427,9 @@ class BotConfig: def heartflow(parent: dict): heartflow_config = parent["heartflow"] - # 加载新增的 heartflowC 参数 - - # 加载原有的 heartflow 参数 - # config.sub_heart_flow_update_interval = heartflow_config.get( - # "sub_heart_flow_update_interval", config.sub_heart_flow_update_interval - # ) - # config.sub_heart_flow_freeze_time = heartflow_config.get( - # "sub_heart_flow_freeze_time", config.sub_heart_flow_freeze_time - # ) config.sub_heart_flow_stop_time = heartflow_config.get( "sub_heart_flow_stop_time", config.sub_heart_flow_stop_time ) - # config.heart_flow_update_interval = heartflow_config.get( - # "heart_flow_update_interval", config.heart_flow_update_interval - # ) if config.INNER_VERSION in SpecifierSet(">=1.3.0"): config.observation_context_size = heartflow_config.get( "observation_context_size", config.observation_context_size @@ -654,6 +641,10 @@ class BotConfig: config.response_max_sentence_num = response_splitter_config.get( "response_max_sentence_num", config.response_max_sentence_num ) + if config.INNER_VERSION in SpecifierSet(">=1.4.2"): + config.enable_kaomoji_protection = response_splitter_config.get( + "enable_kaomoji_protection", config.enable_kaomoji_protection + ) def groups(parent: dict): groups_config = parent["groups"] diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index ab5efa9db..6d8962844 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -372,8 +372,12 @@ def random_remove_punctuation(text: str) -> str: def process_llm_response(text: str) -> List[str]: # 先保护颜文字 - protected_text, kaomoji_mapping = protect_kaomoji(text) - logger.trace(f"保护颜文字后的文本: {protected_text}") + if global_config.enable_kaomoji_protection: + protected_text, kaomoji_mapping = protect_kaomoji(text) + logger.trace(f"保护颜文字后的文本: {protected_text}") + else: + protected_text = text + kaomoji_mapping = {} # 提取被 () 或 [] 包裹且包含中文的内容 pattern = re.compile(r"[\(\[\(](?=.*[\u4e00-\u9fff]).*?[\)\]\)]") # _extracted_contents = pattern.findall(text) @@ -426,9 +430,8 @@ def process_llm_response(text: str) -> List[str]: # sentences.append(content) # 在所有句子处理完毕后,对包含占位符的列表进行恢复 - sentences = recover_kaomoji(sentences, kaomoji_mapping) - - # print(sentences) + if global_config.enable_kaomoji_protection: + sentences = recover_kaomoji(sentences, kaomoji_mapping) return sentences diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 9db3f1932..d55fca3f0 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -122,16 +122,12 @@ mentioned_bot_inevitable_reply = false # 提及 bot 必然回复 at_bot_inevitable_reply = false # @bot 必然回复 [emoji] -max_emoji_num = 90 # 表情包最大数量 +max_emoji_num = 40 # 表情包最大数量 max_reach_deletion = true # 开启则在达到最大数量时删除表情包,关闭则达到最大数量时不删除,只是不会继续收集表情包 -check_interval = 30 # 检查表情包(注册,破损,删除)的时间间隔(分钟) - -auto_save = true # 是否保存表情包和图片 - +check_interval = 10 # 检查表情包(注册,破损,删除)的时间间隔(分钟) save_pic = false # 是否保存图片 save_emoji = false # 是否保存表情包 steal_emoji = true # 是否偷取表情包,让麦麦可以发送她保存的这些表情包 - enable_check = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 @@ -185,6 +181,7 @@ word_replace_rate=0.006 # 整词替换概率 enable_response_splitter = true # 是否启用回复分割器 response_max_length = 256 # 回复允许的最大长度 response_max_sentence_num = 4 # 回复允许的最大句子数 +enable_kaomoji_protection = false # 是否启用颜文字保护 [remote] #发送统计信息,主要是看全球有多少只麦麦 enable = true From 75924bf499465db2ae9ffe09481be249658b39a2 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 19:15:15 +0800 Subject: [PATCH 28/79] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=A5=9E?= =?UTF-8?q?=E7=A7=98=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 2 +- src/plugins/emoji_system/emoji_manager.py | 45 ++++++- src/plugins/heartFC_chat/heartFC_chat.py | 115 +++++++++--------- ...all_benchmark.py => tool_call_benchmark.py | 66 +++++----- 4 files changed, 132 insertions(+), 96 deletions(-) rename scripts/tool_call_benchmark.py => tool_call_benchmark.py (74%) diff --git a/src/common/logger.py b/src/common/logger.py index 4ed69f320..526020824 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -163,7 +163,7 @@ MOOD_STYLE_CONFIG = { "console_format": ( "{time:YYYY-MM-DD HH:mm:ss} | " "{level: <8} | " - "心情 | " + "心情 | " "{message}" ), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}", diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 1c73ec780..754792c24 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -24,8 +24,9 @@ emoji_log_config = LogConfig( logger = get_module_logger("emoji", config=emoji_log_config) -EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录 -EMOJI_REGISTED_DIR = os.path.join("data", "emoji_registed") # 已注册的表情包注册目录 +BASE_DIR = os.path.join("data") +EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录 +EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录 ''' @@ -301,7 +302,7 @@ class EmojiManager: emoji_similarities.sort(key=lambda x: x[1], reverse=True) # 获取前5个最相似的表情包 - top_5_emojis = emoji_similarities[:5] if len(emoji_similarities) > 5 else emoji_similarities + top_5_emojis = emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities if not top_5_emojis: logger.warning("未找到匹配的表情包") @@ -398,6 +399,7 @@ class EmojiManager: while True: logger.info("[扫描] 开始检查表情包完整性...") await self.check_emoji_file_integrity() + await self.clear_temp_emoji() logger.info("[扫描] 开始扫描新表情包...") # 检查表情包目录是否存在 @@ -782,6 +784,43 @@ class EmojiManager: logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(traceback.format_exc()) return False + + + async def clear_temp_emoji(self): + """每天清理临时表情包 + 清理/data/emoji和/data/image目录下的所有文件 + 当目录中文件数超过50时,会全部删除 + """ + + logger.info("[清理] 开始清理临时表情包...") + + # 清理emoji目录 + emoji_dir = os.path.join(BASE_DIR, "emoji") + if os.path.exists(emoji_dir): + files = os.listdir(emoji_dir) + # 如果文件数超过50就全部删除 + if len(files) > 50: + for filename in files: + file_path = os.path.join(emoji_dir, filename) + if os.path.isfile(file_path): + os.remove(file_path) + logger.debug(f"[清理] 删除表情包文件: {filename}") + + # 清理image目录 + image_dir = os.path.join(BASE_DIR, "image") + if os.path.exists(image_dir): + files = os.listdir(image_dir) + # 如果文件数超过50就全部删除 + if len(files) > 50: + for filename in files: + file_path = os.path.join(image_dir, filename) + if os.path.isfile(file_path): + os.remove(file_path) + logger.debug(f"[清理] 删除图片文件: {filename}") + + logger.success("[清理] 临时文件清理完成") + + # 创建全局单例 diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 47cb52eb1..b8338c4b3 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -343,51 +343,48 @@ class HeartFChatting: # 初始化周期状态 cycle_timers = {} loop_cycle_start_time = time.monotonic() - - with Timer("Total Cycle", cycle_timers): - # 执行规划和处理阶段 - async with self._get_cycle_context() as acquired_lock: - if not acquired_lock: - continue - - # 记录规划开始时间点 - planner_start_db_time = time.time() + + # 执行规划和处理阶段 + async with self._get_cycle_context() as acquired_lock: + if not acquired_lock: + continue - # 执行规划阶段 - with Timer("Planning Phase", cycle_timers): - action_taken, thinking_id = await self._think_plan_execute( - cycle_timers, planner_start_db_time - ) - - # 更新循环信息 - self._current_cycle.set_thinking_id(thinking_id) - self._current_cycle.timers = cycle_timers - - # 防止循环过快消耗资源 - with Timer("Cycle Delay", cycle_timers): - await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) + # 记录规划开始时间点 + planner_start_db_time = time.time() - # 等待直到所有消息都发送完成 - with Timer("Wait Messages Complete", cycle_timers): - while await self._should_skip_cycle(thinking_id): - await asyncio.sleep(0.2) - - # 完成当前循环并保存历史 - self._current_cycle.complete_cycle() - self._cycle_history.append(self._current_cycle) - - # 记录循环信息和计时器结果 - timer_strings = [] - for name, elapsed in cycle_timers.items(): - formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" - timer_strings.append(f"{name}: {formatted_time}") - - logger.debug( - f"{self.log_prefix} 循环 #{self._current_cycle.cycle_id} 完成, " - f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, " - f"动作: {self._current_cycle.action_type}" - + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "") + # 执行规划阶段 + action_taken, thinking_id = await self._think_plan_execute_loop( + cycle_timers, planner_start_db_time ) + + # 更新循环信息 + self._current_cycle.set_thinking_id(thinking_id) + self._current_cycle.timers = cycle_timers + + # 防止循环过快消耗资源 + await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) + + # 等待直到所有消息都发送完成 + with Timer("发送消息", cycle_timers): + while await self._should_skip_cycle(thinking_id): + await asyncio.sleep(0.2) + + # 完成当前循环并保存历史 + self._current_cycle.complete_cycle() + self._cycle_history.append(self._current_cycle) + + # 记录循环信息和计时器结果 + timer_strings = [] + for name, elapsed in cycle_timers.items(): + formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" + timer_strings.append(f"{name}: {formatted_time}") + + logger.debug( + f"{self.log_prefix} 第 #{self._current_cycle.cycle_id}次思考完成," + f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, " + f"动作: {self._current_cycle.action_type}" + + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "") + ) except asyncio.CancelledError: logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的激情水群(HFC)被取消了") @@ -434,22 +431,22 @@ class HeartFChatting: logger.error(f"{self.log_prefix} 检查新消息时出错: {e}") return False - async def _think_plan_execute( + async def _think_plan_execute_loop( self, cycle_timers: dict, planner_start_db_time: float ) -> tuple[bool, str]: """执行规划阶段""" try: # 获取子思维思考结果 current_mind = "" - with Timer("SubMind Thinking", cycle_timers): + with Timer("思考", cycle_timers): current_mind = await self._get_submind_thinking() # 记录子思维思考内容 if self._current_cycle: self._current_cycle.set_response_info(sub_mind_thinking=current_mind) # 执行规划 - with Timer("Planner", cycle_timers): - planner_result = await self._planner(current_mind) + with Timer("决策", cycle_timers): + planner_result = await self._planner(current_mind, cycle_timers) # 在获取规划结果后检查新消息 if await self._check_new_messages(planner_start_db_time): @@ -471,7 +468,8 @@ class HeartFChatting: return False, "" # 根据动作类型执行对应处理 - return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) + with Timer("执行", cycle_timers): + return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) except PlannerError as e: logger.error(f"{self.log_prefix} 规划错误: {e}") @@ -684,8 +682,8 @@ class HeartFChatting: ): """处理循环延迟""" cycle_duration = time.monotonic() - cycle_start_time - if cycle_duration > 0.1: - logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") + # if cycle_duration > 0.1: + # logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") try: sleep_duration = 0.0 @@ -718,7 +716,7 @@ class HeartFChatting: logger.error(traceback.format_exc()) return "[思考时出错]" - async def _planner(self, current_mind: str) -> Dict[str, Any]: + async def _planner(self, current_mind: str, cycle_timers: dict) -> Dict[str, Any]: """ 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 @@ -726,15 +724,12 @@ class HeartFChatting: current_mind: 子思维的当前思考结果 """ logger.info(f"{self.log_prefix}[Planner] 开始执行规划器") - - planner_timers = {} # 用于存储各阶段计时结果 # 获取观察信息 - with Timer("获取观察信息", planner_timers): - observation = self.observations[0] - # await observation.observe() - observed_messages = observation.talking_message - observed_messages_str = observation.talking_message_str + observation = self.observations[0] + # await observation.observe() + observed_messages = observation.talking_message + observed_messages_str = observation.talking_message_str # --- 使用 LLM 进行决策 --- # action = "no_reply" # 默认动作 @@ -744,7 +739,7 @@ class HeartFChatting: try: # 构建提示词 - with Timer("构建提示词", planner_timers): + with Timer("构建提示词", cycle_timers): prompt = await self._build_planner_prompt( observed_messages_str, current_mind, self.sub_mind.structured_info ) @@ -756,7 +751,7 @@ class HeartFChatting: } # 执行LLM请求 - with Timer("LLM请求", planner_timers): + with Timer("LLM回复", cycle_timers): try: response = await self.planner_llm._execute_request( endpoint="/chat/completions", payload=payload, prompt=prompt @@ -773,7 +768,7 @@ class HeartFChatting: } # 处理LLM响应 - with Timer("处理LLM响应", planner_timers): + with Timer("使用工具", cycle_timers): # 使用辅助函数处理工具调用响应 success, arguments, error_msg = process_llm_tool_response( response, expected_tool_name="decide_reply_action", log_prefix=f"{self.log_prefix}[Planner] " diff --git a/scripts/tool_call_benchmark.py b/tool_call_benchmark.py similarity index 74% rename from scripts/tool_call_benchmark.py rename to tool_call_benchmark.py index e756d1da3..60f5459bb 100644 --- a/scripts/tool_call_benchmark.py +++ b/tool_call_benchmark.py @@ -144,41 +144,43 @@ async def test_without_tool_calls(): # 简单的测试提示词(与工具调用相同,以便公平比较) prompt = """ - 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会想瑟瑟,喜欢刷小红书 -刚刚你的想法是: -我是麦麦,我想,('小千石问3.8和3.11谁大,已经简单回答了3.11大,现在可以继续聊猫猫头表情包,毕竟大家好像对版本问题兴趣不大,而且猫猫头的话题更轻松有趣。', '') ------------------------------------ -现在是2025-04-24 12:37:00,你正在上网,和qq群里的网友们聊天,群里正在聊的话题是: -2025-04-24 12:33:00既文横 说:这条调试消息是napcat控制台输出的,还是麦麦log输出的; -2025-04-24 12:33:23麦麦(你) 说:应该是napcat吧; -2025-04-24 12:33:24麦麦(你) 说:[表达了:害羞、害羞。]; -2025-04-24 12:33:25兔伽兔伽 说:就打开麦麦的那个终端发的呀; -2025-04-24 12:33:45既文横 说:那应该不是napcat输出的,是麦麦输出的消息,怀疑版本问题; -2025-04-24 12:34:02兔伽兔伽 说:版本05.15; -2025-04-24 12:34:07麦麦(你) 说:话说你们最近刷贴吧看到那个猫猫头表情包了吗; -2025-04-24 12:34:07麦麦(你) 说:笑死; -2025-04-24 12:34:08麦麦(你) 说:[表达了:惊讶、搞笑。]; -2025-04-24 12:34:14兔伽兔伽 说:只开一个终端; -2025-04-24 12:35:45兔伽兔伽 说:回复既文横的消息(怀疑版本问题),说:因为之前你连模型的那个我用的了; -2025-04-24 12:35:56麦麦(你) 说:那个猫猫头真的魔性; -2025-04-24 12:35:56麦麦(你) 说:我存了一堆; -2025-04-24 12:35:56麦麦(你) 说:[表达了:温馨、宠爱]; -2025-04-24 12:36:03小千石 说:麦麦3.8和3.11谁大; -2025-04-24 12:36:22麦麦(你) 说:真的魔性那个猫猫头; -2025-04-24 12:36:22麦麦(你) 说:[表达了:害羞、可爱]; -2025-04-24 12:36:43麦麦(你) 说:3.11大啦; -2025-04-24 12:36:43麦麦(你) 说:[表达了:害羞、可爱]; + 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书 +刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈“那今晚的加班费是不是也要被吃掉了”或者“猫娘罢工警告”。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似“代码写完但奖励被吃掉”的搞笑职场经历,换个轻松的话题方向。 ---- 以上消息已读 (标记时间: 2025-04-24 12:36:43) --- ---- 请关注你上次思考之后以下的新消息--- -2025-04-24 12:36:53墨墨 说:[表情包:开心、满足。]; +暂时不需要使用工具。 +----------------------------------- +现在是2025-04-25 17:38:37,你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +2025-04-25 17:34:08麦麦(你) 说:[表达了:顽皮、嬉戏。]; +2025-04-25 17:34:39漂移菌 说:@麦麦。(id:3936257206) 你是一只猫娘; +2025-04-25 17:34:42薯宝 说:🤣; +2025-04-25 17:34:43麦麦(你) 说:行啊 工资分我一半; +2025-04-25 17:34:43麦麦(你) 说:我帮你写bug; +2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力]; +2025-04-25 17:34:53薯薯 说:?; +2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资; +2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有“死了”两个字。 + +图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经“死”了一样。] hfc这周,真能出来吗...; +2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默]; +2025-04-25 17:36:25麦麦(你) 说:喵喵; +2025-04-25 17:36:25麦麦(你) 说:代码写完了; +2025-04-25 17:36:25麦麦(你) 说:罐罐拿来; +2025-04-25 17:36:25麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力]; +2025-04-25 17:36:41薯薯 说:好可爱; +2025-04-25 17:37:05薯薯 说:脑补出来认真营业了一天等待主人发放奖励的小猫咪; +2025-04-25 17:37:25薯宝 说:敷衍营业(bushi); +2025-04-25 17:37:54漂移菌 说:回复麦麦。的消息(罐罐拿来),说:猫娘我昨晚上太饿吃完了; + +--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) --- +--- 以下新消息未读--- +2025-04-25 17:38:29麦麦(你) 说:那今晚的猫条是不是也要被克扣了(盯——); +2025-04-25 17:38:29麦麦(你) 说:[表达了:幽默,自嘲,无奈,父子关系,编程笑话]; 你现在当前心情:平静。 -现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。 -回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题 -请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。 -现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸 -在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" +现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复 +回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题 +如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言 +现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" # 发送不带工具调用的请求 response, reasoning_content = await llm_model.generate_response_async(prompt) From 91ad729b0c279a22ad2bf6a04d4055e50dab9851 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 21:38:16 +0800 Subject: [PATCH 29/79] =?UTF-8?q?better=EF=BC=9A=E6=9B=B4=E5=A5=BD?= =?UTF-8?q?=E7=9A=84=E9=87=8D=E6=96=B0=E6=80=9D=E8=80=83=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=8D=E8=AF=BB=EF=BC=8C=E8=AE=B0=E5=BD=95=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=8C=E6=8B=86=E5=88=86=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 5 +- src/heart_flow/background_tasks.py | 4 +- src/heart_flow/sub_mind.py | 31 +++- src/plugins/heartFC_chat/heartFC_Cycleinfo.py | 70 +++++++ src/plugins/heartFC_chat/heartFC_chat.py | 172 +++++++----------- src/plugins/heartFC_chat/heartFC_generator.py | 11 +- .../heartFC_chat/heartflow_prompt_builder.py | 5 +- template/bot_config_template.toml | 62 ++++--- tool_call_benchmark.py | 141 ++++++++++---- 9 files changed, 317 insertions(+), 184 deletions(-) create mode 100644 src/plugins/heartFC_chat/heartFC_Cycleinfo.py diff --git a/src/config/config.py b/src/config/config.py index 187bb6cde..2ade83f11 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -192,7 +192,6 @@ class BotConfig: reply_trigger_threshold: float = 3.0 # 心流聊天触发阈值,越低越容易触发 probability_decay_factor_per_second: float = 0.2 # 概率衰减因子,越大衰减越快 default_decay_rate_per_second: float = 0.98 # 默认衰减率,越大衰减越慢 - initial_duration: int = 60 # 初始持续时间,越大心流聊天持续的时间越长 # sub_heart_flow_update_interval: int = 60 # 子心流更新频率,间隔 单位秒 # sub_heart_flow_freeze_time: int = 120 # 子心流冻结时间,超过这个时间没有回复,子心流会冻结,间隔 单位秒 @@ -286,11 +285,11 @@ class BotConfig: vlm: Dict[str, str] = field(default_factory=lambda: {}) moderation: Dict[str, str] = field(default_factory=lambda: {}) - # 实验性 llm_observation: Dict[str, str] = field(default_factory=lambda: {}) llm_sub_heartflow: Dict[str, str] = field(default_factory=lambda: {}) llm_heartflow: Dict[str, str] = field(default_factory=lambda: {}) llm_tool_use: Dict[str, str] = field(default_factory=lambda: {}) + llm_plan: Dict[str, str] = field(default_factory=lambda: {}) api_urls: Dict[str, str] = field(default_factory=lambda: {}) @@ -448,7 +447,6 @@ class BotConfig: config.default_decay_rate_per_second = heartflow_config.get( "default_decay_rate_per_second", config.default_decay_rate_per_second ) - config.initial_duration = heartflow_config.get("initial_duration", config.initial_duration) def willing(parent: dict): willing_config = parent["willing"] @@ -489,6 +487,7 @@ class BotConfig: "llm_tool_use", "llm_observation", "llm_sub_heartflow", + "llm_plan", "llm_heartflow", "llm_PFC_action_planner", "llm_PFC_chat", diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 85fb6c502..21254ce78 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -230,8 +230,8 @@ class BackgroundTaskManager: if await self.subheartflow_manager.stop_subheartflow(flow_id, f"定期清理: {reason}"): stopped_count += 1 logger.info(f"[Background Task Cleanup] Cleanup cycle finished. Stopped {stopped_count} inactive flows.") - else: - logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.") + # else: + # logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.") async def _perform_logging_work(self): """执行一轮状态日志记录。""" diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 111c2cf5c..b213f6f11 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -11,6 +11,7 @@ from src.do_tool.tool_use import ToolUser from src.plugins.utils.json_utils import safe_json_dumps, normalize_llm_response, process_llm_tool_calls from src.heart_flow.chat_state_info import ChatStateInfo from src.plugins.chat.chat_stream import chat_manager +from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo subheartflow_config = LogConfig( console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"], @@ -23,12 +24,12 @@ def init_prompt(): prompt = "" prompt += "{extra_info}\n" prompt += "{prompt_personality}\n" - prompt += "刚刚你的内心想法是:{current_thinking_info}\n" + prompt += "{last_loop_prompt}\n" prompt += "-----------------------------------\n" prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" - prompt += "现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。" - prompt += "请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" + prompt += "现在请你,阅读群里正在进行的聊天内容,思考群里的正在进行的话题,分析群里成员与你的关系。" + prompt += "请你思考,生成你的内心想法,包括你的思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" prompt += "回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题\n" prompt += "如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。" prompt += "如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。" @@ -38,6 +39,12 @@ def init_prompt(): prompt += "如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" Prompt(prompt, "sub_heartflow_prompt_before") + + prompt = "" + prompt += "刚刚你的内心想法是:{current_thinking_info}\n" + prompt += "{if_replan_prompt}\n" + + Prompt(prompt, "last_loop") class SubMind: @@ -58,7 +65,7 @@ class SubMind: self.past_mind = [] self.structured_info = {} - async def do_thinking_before_reply(self): + async def do_thinking_before_reply(self, last_cycle: CycleInfo): """ 在回复前进行思考,生成内心想法并收集工具调用结果 @@ -122,6 +129,20 @@ class SubMind: ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), ] + #上一次决策信息 + last_action = last_cycle.action_type + last_reasoning = last_cycle.reasoning + is_replan = last_cycle.replanned + if is_replan: + if_replan_prompt = f"但是你有了上述想法之后,有了新消息,你决定重新思考后,你做了:{last_action}\n因为:{last_reasoning}\n" + else: + if_replan_prompt = f"出于这个想法,你刚才做了:{last_action}\n因为:{last_reasoning}\n" + + last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( + current_thinking_info=current_thinking_info, + if_replan_prompt=if_replan_prompt + ) + # 加权随机选择思考指导 hf_do_next = local_random.choices( [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 @@ -133,11 +154,11 @@ class SubMind: extra_info="", # 可以在这里添加额外信息 prompt_personality=prompt_personality, bot_name=individuality.personality.bot_nickname, - current_thinking_info=current_thinking_info, time_now=time_now, chat_observe_info=chat_observe_info, mood_info=mood_info, hf_do_next=hf_do_next, + last_loop_prompt=last_loop_prompt ) # logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") diff --git a/src/plugins/heartFC_chat/heartFC_Cycleinfo.py b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py new file mode 100644 index 000000000..030018ddf --- /dev/null +++ b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py @@ -0,0 +1,70 @@ +import time +from typing import List, Optional, Dict, Any + +class CycleInfo: + """循环信息记录类""" + def __init__(self, cycle_id: int): + self.cycle_id = cycle_id + self.start_time = time.time() + self.end_time: Optional[float] = None + self.action_taken = False + self.action_type = "unknown" + self.reasoning = "" + self.timers: Dict[str, float] = {} + self.thinking_id = "" + self.replanned = False + + # 添加响应信息相关字段 + self.response_info: Dict[str, Any] = { + "response_text": [], # 回复的文本列表 + "emoji_info": "", # 表情信息 + "anchor_message_id": "", # 锚点消息ID + "reply_message_ids": [], # 回复消息ID列表 + "sub_mind_thinking": "", # 子思维思考内容 + } + + def to_dict(self) -> Dict[str, Any]: + """将循环信息转换为字典格式""" + return { + "cycle_id": self.cycle_id, + "start_time": self.start_time, + "end_time": self.end_time, + "action_taken": self.action_taken, + "action_type": self.action_type, + "reasoning": self.reasoning, + "timers": self.timers, + "thinking_id": self.thinking_id, + "response_info": self.response_info + } + + def complete_cycle(self): + """完成循环,记录结束时间""" + self.end_time = time.time() + + def set_action_info(self, action_type: str, reasoning: str, action_taken: bool): + """设置动作信息""" + self.action_type = action_type + self.reasoning = reasoning + self.action_taken = action_taken + + def set_thinking_id(self, thinking_id: str): + """设置思考消息ID""" + self.thinking_id = thinking_id + + def set_response_info(self, + response_text: Optional[List[str]] = None, + emoji_info: Optional[str] = None, + anchor_message_id: Optional[str] = None, + reply_message_ids: Optional[List[str]] = None, + sub_mind_thinking: Optional[str] = None): + """设置响应信息""" + if response_text is not None: + self.response_info["response_text"] = response_text + if emoji_info is not None: + self.response_info["emoji_info"] = emoji_info + if anchor_message_id is not None: + self.response_info["anchor_message_id"] = anchor_message_id + if reply_message_ids is not None: + self.response_info["reply_message_ids"] = reply_message_ids + if sub_mind_thinking is not None: + self.response_info["sub_mind_thinking"] = sub_mind_thinking \ No newline at end of file diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index b8338c4b3..c11674fe2 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1,7 +1,8 @@ import asyncio import time import traceback -from typing import List, Optional, Dict, Any, Set, Deque +import random # <-- 添加导入 +from typing import List, Optional, Dict, Any, Deque from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageSet, Seg # Local import needed after move @@ -23,6 +24,7 @@ from src.heart_flow.observation import Observation from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager import contextlib from src.plugins.utils.chat_message_builder import num_new_messages_since +from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo # --- End import --- @@ -139,75 +141,6 @@ class SenderError(HeartFCError): """发送器异常""" pass - -class CycleInfo: - """循环信息记录类""" - def __init__(self, cycle_id: int): - self.cycle_id = cycle_id - self.start_time = time.time() - self.end_time: Optional[float] = None - self.action_taken = False - self.action_type = "unknown" - self.reasoning = "" - self.timers: Dict[str, float] = {} - self.thinking_id = "" - - # 添加响应信息相关字段 - self.response_info: Dict[str, Any] = { - "response_text": [], # 回复的文本列表 - "emoji_info": "", # 表情信息 - "anchor_message_id": "", # 锚点消息ID - "reply_message_ids": [], # 回复消息ID列表 - "sub_mind_thinking": "", # 子思维思考内容 - } - - def to_dict(self) -> Dict[str, Any]: - """将循环信息转换为字典格式""" - return { - "cycle_id": self.cycle_id, - "start_time": self.start_time, - "end_time": self.end_time, - "action_taken": self.action_taken, - "action_type": self.action_type, - "reasoning": self.reasoning, - "timers": self.timers, - "thinking_id": self.thinking_id, - "response_info": self.response_info - } - - def complete_cycle(self): - """完成循环,记录结束时间""" - self.end_time = time.time() - - def set_action_info(self, action_type: str, reasoning: str, action_taken: bool): - """设置动作信息""" - self.action_type = action_type - self.reasoning = reasoning - self.action_taken = action_taken - - def set_thinking_id(self, thinking_id: str): - """设置思考消息ID""" - self.thinking_id = thinking_id - - def set_response_info(self, - response_text: Optional[List[str]] = None, - emoji_info: Optional[str] = None, - anchor_message_id: Optional[str] = None, - reply_message_ids: Optional[List[str]] = None, - sub_mind_thinking: Optional[str] = None): - """设置响应信息""" - if response_text is not None: - self.response_info["response_text"] = response_text - if emoji_info is not None: - self.response_info["emoji_info"] = emoji_info - if anchor_message_id is not None: - self.response_info["anchor_message_id"] = anchor_message_id - if reply_message_ids is not None: - self.response_info["reply_message_ids"] = reply_message_ids - if sub_mind_thinking is not None: - self.response_info["sub_mind_thinking"] = sub_mind_thinking - - class HeartFChatting: """ 管理一个连续的Plan-Replier-Sender循环 @@ -244,8 +177,7 @@ class HeartFChatting: # LLM规划器配置 self.planner_llm = LLMRequest( - model=global_config.llm_normal, - temperature=global_config.llm_normal["temp"], + model=global_config.llm_plan, max_tokens=1000, request_type="action_planning", # 用于动作规划 ) @@ -352,7 +284,7 @@ class HeartFChatting: # 记录规划开始时间点 planner_start_db_time = time.time() - # 执行规划阶段 + # 主循环:思考->决策->执行 action_taken, thinking_id = await self._think_plan_execute_loop( cycle_timers, planner_start_db_time ) @@ -436,29 +368,34 @@ class HeartFChatting: ) -> tuple[bool, str]: """执行规划阶段""" try: - # 获取子思维思考结果 - current_mind = "" - with Timer("思考", cycle_timers): - current_mind = await self._get_submind_thinking() - # 记录子思维思考内容 - if self._current_cycle: - self._current_cycle.set_response_info(sub_mind_thinking=current_mind) + # think:思考 + current_mind = await self._get_submind_thinking(cycle_timers) + # 记录子思维思考内容 + if self._current_cycle: + self._current_cycle.set_response_info(sub_mind_thinking=current_mind) - # 执行规划 + # plan:决策 with Timer("决策", cycle_timers): planner_result = await self._planner(current_mind, cycle_timers) - - # 在获取规划结果后检查新消息 - if await self._check_new_messages(planner_start_db_time): - # 更新循环信息 - logger.info(f"{self.log_prefix} 思考到一半,检测到新消息,重新思考") - self._current_cycle.set_action_info("new_messages", "检测到新消息", False) - return False, "new_messages" - - # 解析规划结果 + action = planner_result.get("action", "error") reasoning = planner_result.get("reasoning", "未提供理由") + self._current_cycle.set_action_info(action, reasoning, False) + + # 在获取规划结果后检查新消息 + if await self._check_new_messages(planner_start_db_time): + if random.random() < 0.3: + logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...") + # 重新规划 + with Timer("重新决策", cycle_timers): + self._current_cycle.replanned = True + planner_result = await self._planner(current_mind, cycle_timers, is_re_planned=True) + logger.info(f"{self.log_prefix} 重新规划完成.") + + # 解析规划结果 + action = planner_result.get("action", "error") + reasoning = planner_result.get("reasoning", "未提供理由") # 更新循环信息 self._current_cycle.set_action_info(action, reasoning, True) @@ -467,7 +404,7 @@ class HeartFChatting: logger.error(f"{self.log_prefix} LLM失败: {reasoning}") return False, "" - # 根据动作类型执行对应处理 + # execute:执行 with Timer("执行", cycle_timers): return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) @@ -699,7 +636,7 @@ class HeartFChatting: logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.") raise - async def _get_submind_thinking(self) -> str: + async def _get_submind_thinking(self, cycle_timers: dict) -> str: """ 获取子思维的思考结果 @@ -707,27 +644,38 @@ class HeartFChatting: str: 思考结果,如果思考失败则返回错误信息 """ try: - observation = self.observations[0] - await observation.observe() - current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply() - return current_mind + with Timer("观察", cycle_timers): + observation = self.observations[0] + await observation.observe() + + # 获取上一个循环的信息 + last_cycle = self._cycle_history[-1] if self._cycle_history else None + + with Timer("思考", cycle_timers): + # 获取上一个循环的动作 + # 传递上一个循环的信息给 do_thinking_before_reply + current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply( + last_cycle=last_cycle + ) + return current_mind except Exception as e: logger.error(f"{self.log_prefix}[SubMind] 思考失败: {e}") logger.error(traceback.format_exc()) return "[思考时出错]" - async def _planner(self, current_mind: str, cycle_timers: dict) -> Dict[str, Any]: + async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]: """ 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 参数: current_mind: 子思维的当前思考结果 """ - logger.info(f"{self.log_prefix}[Planner] 开始执行规划器") + logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器") # 获取观察信息 observation = self.observations[0] - # await observation.observe() + if is_re_planned: + observation.observe() observed_messages = observation.talking_message observed_messages_str = observation.talking_message_str @@ -740,11 +688,18 @@ class HeartFChatting: try: # 构建提示词 with Timer("构建提示词", cycle_timers): + if is_re_planned: + replan_prompt = await self._build_replan_prompt( + self._current_cycle.action, self._current_cycle.reasoning + ) + prompt = replan_prompt + else: + replan_prompt = "" prompt = await self._build_planner_prompt( - observed_messages_str, current_mind, self.sub_mind.structured_info + observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt ) payload = { - "model": self.planner_llm.model_name, + "model": global_config.llm_plan["name"], "messages": [{"role": "user", "content": prompt}], "tools": self.action_manager.get_planner_tool_definition(), "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, @@ -904,9 +859,19 @@ class HeartFChatting: logger.warning(f"{self.log_prefix} 已释放处理锁") logger.info(f"{self.log_prefix} HeartFChatting关闭完成") - + + async def _build_replan_prompt( + self, action: str, reasoning: str + ) -> str: + """构建 Replanner LLM 的提示词""" + prompt = (await global_prompt_manager.get_prompt_async("replan_prompt")).format( + action=action, + reasoning=reasoning, + ) + return prompt + async def _build_planner_prompt( - self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any] + self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any], replan_prompt: str ) -> str: """构建 Planner LLM 的提示词""" @@ -937,6 +902,7 @@ class HeartFChatting: structured_info_block=structured_info_block, chat_content_block=chat_content_block, current_mind_block=current_mind_block, + replan=replan_prompt, ) return prompt diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index c489e012c..95ee0a754 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -49,12 +49,11 @@ class HeartFCGenerator: arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() - with Timer() as t_generate_response: - current_model = self.model_normal - current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高 - model_response = await self._generate_response_with_model( - structured_info, current_mind_info, reason, message, current_model, thinking_id - ) + current_model = self.model_normal + current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高 + model_response = await self._generate_response_with_model( + structured_info, current_mind_info, reason, message, current_model, thinking_id + ) if model_response: model_processed_response = await self._process_response(model_response) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 146a5307f..ec12e2adf 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -51,6 +51,7 @@ def init_prompt(): {chat_content_block} 看了以上内容,你产生的内心想法是: {current_mind_block} +{replan} 请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。 注意你必须参考以下决策依据来选择工具: 1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。 @@ -64,6 +65,8 @@ def init_prompt(): "planner_prompt", ) + Prompt("你原本打算{action},因为:{reasoning},但是你看到了新的消息,你决定重新决定行动。", "replan_prompt") + Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") @@ -86,7 +89,7 @@ def init_prompt(): 你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些, 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} -请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 +请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要重复自己说过的话。 请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 {moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。,只输出回复内容""", "reasoning_prompt_main", diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index d55fca3f0..a85d9f17a 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.4.2" +version = "1.5.0" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -81,12 +81,8 @@ model_normal_probability = 0.3 # 麦麦回答时选择一般模型 模型的概 reply_trigger_threshold = 3.0 # 心流聊天触发阈值,越低越容易进入心流聊天 probability_decay_factor_per_second = 0.2 # 概率衰减因子,越大衰减越快,越高越容易退出心流聊天 default_decay_rate_per_second = 0.98 # 默认衰减率,越大衰减越快,越高越难进入心流聊天 -initial_duration = 60 # 初始持续时间,越大心流聊天持续的时间越长 sub_heart_flow_stop_time = 500 # 子心流停止时间,超过这个时间没有回复,子心流会停止,间隔 单位秒 -# sub_heart_flow_update_interval = 60 -# sub_heart_flow_freeze_time = 100 -# heart_flow_update_interval = 600 observation_context_size = 20 # 心流观察到的最长上下文大小,超过这个值的上下文会被压缩 compressed_length = 5 # 不能大于observation_context_size,心流上下文压缩的最短压缩长度,超过心流观察到的上下文长度,会压缩,最短压缩长度为5 @@ -247,6 +243,29 @@ provider = "SILICONFLOW" pri_in = 0.35 pri_out = 0.35 + + +[model.llm_observation] #观察模型,压缩聊天内容,建议用免费的 +# name = "Pro/Qwen/Qwen2.5-7B-Instruct" +name = "Qwen/Qwen2.5-7B-Instruct" +provider = "SILICONFLOW" +pri_in = 0 +pri_out = 0 + +[model.llm_sub_heartflow] #子心流:激情水群时,生成麦麦的内心想法 +name = "Qwen/Qwen2.5-72B-Instruct" +provider = "SILICONFLOW" +pri_in = 4.13 +pri_out = 4.13 +temp = 0.7 #模型的温度,新V3建议0.1-0.3 + + +[model.llm_plan] #决策模型:激情水群时,负责决定麦麦该做什么 +name = "Qwen/Qwen2.5-32B-Instruct" +provider = "SILICONFLOW" +pri_in = 1.26 +pri_out = 1.26 + #嵌入模型 [model.embedding] #嵌入 @@ -255,26 +274,6 @@ provider = "SILICONFLOW" pri_in = 0 pri_out = 0 -[model.llm_observation] #观察模型,建议用免费的:建议使用qwen2.5 7b -# name = "Pro/Qwen/Qwen2.5-7B-Instruct" -name = "Qwen/Qwen2.5-7B-Instruct" -provider = "SILICONFLOW" -pri_in = 0 -pri_out = 0 - -[model.llm_sub_heartflow] #子心流:建议使用V3级别 -name = "Pro/deepseek-ai/DeepSeek-V3" -provider = "SILICONFLOW" -pri_in = 2 -pri_out = 8 -temp = 0.2 #模型的温度,新V3建议0.1-0.3 - -[model.llm_heartflow] #心流:建议使用qwen2.5 32b -# name = "Pro/Qwen/Qwen2.5-7B-Instruct" -name = "Qwen/Qwen2.5-32B-Instruct" -provider = "SILICONFLOW" -pri_in = 1.26 -pri_out = 1.26 #私聊PFC:需要开启PFC功能,默认三个模型均为硅基流动v3,如果需要支持多人同时私聊或频繁调用,建议把其中的一个或两个换成官方v3或其它模型,以免撞到429 @@ -299,4 +298,15 @@ pri_out = 8 name = "Pro/deepseek-ai/DeepSeek-V3" provider = "SILICONFLOW" pri_in = 2 -pri_out = 8 \ No newline at end of file +pri_out = 8 + + +#此模型暂时没有使用!! +#此模型暂时没有使用!! +#此模型暂时没有使用!! +[model.llm_heartflow] #心流 +# name = "Pro/Qwen/Qwen2.5-7B-Instruct" +name = "Qwen/Qwen2.5-32B-Instruct" +provider = "SILICONFLOW" +pri_in = 1.26 +pri_out = 1.26 \ No newline at end of file diff --git a/tool_call_benchmark.py b/tool_call_benchmark.py index 60f5459bb..7ef00c7c3 100644 --- a/tool_call_benchmark.py +++ b/tool_call_benchmark.py @@ -63,35 +63,43 @@ async def test_with_tool_calls(): # 简单的测试提示词 prompt = "请分析当前天气情况,并查询今日历史上的重要事件。并且3.9和3.11谁比较大?请使用适当的工具来获取这些信息。" prompt = """ - 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会想瑟瑟,喜欢刷小红书 ------------------------------------ -现在是2025-04-24 12:37:00,你正在上网,和qq群里的网友们聊天,群里正在聊的话题是: -2025-04-24 12:33:00既文横 说:这条调试消息是napcat控制台输出的,还是麦麦log输出的; -2025-04-24 12:33:23麦麦(你) 说:应该是napcat吧; -2025-04-24 12:33:24麦麦(你) 说:[表达了:害羞、害羞。]; -2025-04-24 12:33:25兔伽兔伽 说:就打开麦麦的那个终端发的呀; -2025-04-24 12:33:45既文横 说:那应该不是napcat输出的,是麦麦输出的消息,怀疑版本问题; -2025-04-24 12:34:02兔伽兔伽 说:版本05.15; -2025-04-24 12:34:07麦麦(你) 说:话说你们最近刷贴吧看到那个猫猫头表情包了吗; -2025-04-24 12:34:07麦麦(你) 说:笑死; -2025-04-24 12:34:08麦麦(你) 说:[表达了:惊讶、搞笑。]; -2025-04-24 12:34:14兔伽兔伽 说:只开一个终端; -2025-04-24 12:35:45兔伽兔伽 说:回复既文横的消息(怀疑版本问题),说:因为之前你连模型的那个我用的了; -2025-04-24 12:35:56麦麦(你) 说:那个猫猫头真的魔性; -2025-04-24 12:35:56麦麦(你) 说:我存了一堆; -2025-04-24 12:35:56麦麦(你) 说:[表达了:温馨、宠爱]; -2025-04-24 12:36:03小千石 说:麦麦3.8和3.11谁大; + 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书 +刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历,换个轻松的话题方向。 ---- 以上消息已读 (标记时间: 2025-04-24 12:36:43) --- ---- 请关注你上次思考之后以下的新消息--- -2025-04-24 12:36:53墨墨 说:[表情包:开心、满足。]; +暂时不需要使用工具。 +----------------------------------- +现在是2025-04-25 17:38:37,你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +2025-04-25 17:34:08麦麦(你) 说:[表达了:顽皮、嬉戏。]; +2025-04-25 17:34:39漂移菌 说:@麦麦。(id:3936257206) 你是一只猫娘; +2025-04-25 17:34:42薯宝 说:🤣; +2025-04-25 17:34:43麦麦(你) 说:行啊 工资分我一半; +2025-04-25 17:34:43麦麦(你) 说:我帮你写bug; +2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力]; +2025-04-25 17:34:53薯薯 说:?; +2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资; +2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有"死了"两个字。 + +图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经"死"了一样。] hfc这周,真能出来吗...; +2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默]; +2025-04-25 17:36:25麦麦(你) 说:喵喵; +2025-04-25 17:36:25麦麦(你) 说:代码写完了; +2025-04-25 17:36:25麦麦(你) 说:罐罐拿来; +2025-04-25 17:36:25麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力]; +2025-04-25 17:36:41薯薯 说:好可爱; +2025-04-25 17:37:05薯薯 说:脑补出来认真营业了一天等待主人发放奖励的小猫咪; +2025-04-25 17:37:25薯宝 说:敷衍营业(bushi); +2025-04-25 17:37:54漂移菌 说:回复麦麦。的消息(罐罐拿来),说:猫娘我昨晚上太饿吃完了; + +--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) --- +--- 以下新消息未读--- +2025-04-25 17:38:29麦麦(你) 说:那今晚的猫条是不是也要被克扣了(盯——); +2025-04-25 17:38:29麦麦(你) 说:[表达了:幽默,自嘲,无奈,父子关系,编程笑话]; 你现在当前心情:平静。 -现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。 -回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题 -请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。 -现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸 -在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" +现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复 +回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题 +如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言 +现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" # 发送带有工具调用的请求 response = await llm_model.generate_response_tool_async(prompt=prompt, tools=tools) @@ -145,7 +153,7 @@ async def test_without_tool_calls(): # 简单的测试提示词(与工具调用相同,以便公平比较) prompt = """ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会喜欢说一些奇怪的话,喜欢刷小红书 -刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈“那今晚的加班费是不是也要被吃掉了”或者“猫娘罢工警告”。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似“代码写完但奖励被吃掉”的搞笑职场经历,换个轻松的话题方向。 +刚刚你的内心想法是:漂移菌提到罐罐被吃完了,可以顺着这个梗继续玩一下,比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"。不过薯薯和薯宝之前已经接了不少梗,漂移菌刚刚也参与了,可能话题热度还在,可以再互动一下。如果没人接话,或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历,换个轻松的话题方向。 暂时不需要使用工具。 ----------------------------------- @@ -158,9 +166,9 @@ async def test_without_tool_calls(): 2025-04-25 17:34:43麦麦(你) 说:[表达了:悲伤、绝望、无奈、无力]; 2025-04-25 17:34:53薯薯 说:?; 2025-04-25 17:35:03既文横 说:麦麦,你是一只猫娘程序员,猫娘是不需要工资; -2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有“死了”两个字。 +2025-04-25 17:35:20薯宝 说:[图片:图片内容:一只卡通风格的灰色猫咪,眼睛闭着,表情显得很平静。图片下方有"死了"两个字。 -图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经“死”了一样。] hfc这周,真能出来吗...; +图片含义猜测:这可能是一个幽默的表达,用来形容某人或某事处于非常平静的状态,仿佛已经"死"了一样。] hfc这周,真能出来吗...; 2025-04-25 17:35:34薯宝 说:[表情包:搞笑、滑稽、讽刺、幽默]; 2025-04-25 17:36:25麦麦(你) 说:喵喵; 2025-04-25 17:36:25麦麦(你) 说:代码写完了; @@ -181,7 +189,6 @@ async def test_without_tool_calls(): 回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题 如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言 现在请你先输出想法,生成你在这个聊天中的想法,在原来的想法上尝试新的话题,不要分点输出,文字不要浮夸在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" - # 发送不带工具调用的请求 response, reasoning_content = await llm_model.generate_response_async(prompt) @@ -194,6 +201,69 @@ async def test_without_tool_calls(): return result_info +async def run_alternating_tests(iterations=5): + """ + 交替运行两种测试方法,每种方法运行指定次数 + + 参数: + iterations: 每种测试方法运行的次数 + + 返回: + 包含两种测试方法结果的元组 + """ + print(f"开始交替测试(每种方法{iterations}次)...") + + # 初始化结果列表 + times_without_tools = [] + times_with_tools = [] + responses_without_tools = [] + responses_with_tools = [] + + for i in range(iterations): + print(f"\n第 {i + 1}/{iterations} 轮交替测试") + + # 不使用工具的测试 + print("\n 执行不使用工具调用的测试...") + start_time = time.time() + response = await test_without_tool_calls() + end_time = time.time() + elapsed = end_time - start_time + times_without_tools.append(elapsed) + responses_without_tools.append(response) + print(f" - 耗时: {elapsed:.2f}秒") + + # 使用工具的测试 + print("\n 执行使用工具调用的测试...") + start_time = time.time() + response = await test_with_tool_calls() + end_time = time.time() + elapsed = end_time - start_time + times_with_tools.append(elapsed) + responses_with_tools.append(response) + print(f" - 耗时: {elapsed:.2f}秒") + + # 计算统计数据 + results_without_tools = { + "平均耗时": statistics.mean(times_without_tools), + "最短耗时": min(times_without_tools), + "最长耗时": max(times_without_tools), + "标准差": statistics.stdev(times_without_tools) if len(times_without_tools) > 1 else 0, + "所有耗时": times_without_tools, + "响应结果": responses_without_tools, + } + + results_with_tools = { + "平均耗时": statistics.mean(times_with_tools), + "最短耗时": min(times_with_tools), + "最长耗时": max(times_with_tools), + "标准差": statistics.stdev(times_with_tools) if len(times_with_tools) > 1 else 0, + "所有耗时": times_with_tools, + "响应结果": responses_with_tools, + } + + return results_without_tools, results_with_tools + + async def main(): """主测试函数""" print("=" * 50) @@ -201,15 +271,10 @@ async def main(): print("=" * 50) # 设置测试迭代次数 - iterations = 3 + iterations = 10 - # 测试不使用工具调用 - results_without_tools = await run_test("不使用工具调用", test_without_tool_calls, iterations) - - print("\n" + "-" * 50 + "\n") - - # 测试使用工具调用 - results_with_tools = await run_test("使用工具调用", test_with_tool_calls, iterations) + # 执行交替测试 + results_without_tools, results_with_tools = await run_alternating_tests(iterations) # 显示结果比较 print("\n" + "=" * 50) From 5ed676e404a4eb1ee2fd6a9a3e280bfbe1603b3c Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 21:40:36 +0800 Subject: [PATCH 30/79] Update sub_mind.py --- src/heart_flow/sub_mind.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index b213f6f11..8470c24e9 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -130,13 +130,19 @@ class SubMind: ] #上一次决策信息 - last_action = last_cycle.action_type - last_reasoning = last_cycle.reasoning - is_replan = last_cycle.replanned - if is_replan: - if_replan_prompt = f"但是你有了上述想法之后,有了新消息,你决定重新思考后,你做了:{last_action}\n因为:{last_reasoning}\n" + if last_cycle.action_type: + last_action = last_cycle.action_type + last_reasoning = last_cycle.reasoning + is_replan = last_cycle.replanned + if is_replan: + if_replan_prompt = f"但是你有了上述想法之后,有了新消息,你决定重新思考后,你做了:{last_action}\n因为:{last_reasoning}\n" + else: + if_replan_prompt = f"出于这个想法,你刚才做了:{last_action}\n因为:{last_reasoning}\n" else: - if_replan_prompt = f"出于这个想法,你刚才做了:{last_action}\n因为:{last_reasoning}\n" + last_action = "" + last_reasoning = "" + is_replan = False + if_replan_prompt = "" last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( current_thinking_info=current_thinking_info, From be1ba833190f7572a028ef3340cdd828c27b2b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 25 Apr 2025 22:59:07 +0800 Subject: [PATCH 31/79] fix: Ruff --- src/plugins/PFC/pfc.py | 2 +- src/plugins/heartFC_chat/heartFC_chat.py | 2 +- src/plugins/heartFC_chat/heartFC_generator.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 19549825a..033cf8226 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -371,7 +371,7 @@ class DirectMessageSender: # 处理消息 await message.process() - message_json = message.to_dict() + _message_json = message.to_dict() # 发送消息 try: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index b8338c4b3..8be4d42fe 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1,7 +1,7 @@ import asyncio import time import traceback -from typing import List, Optional, Dict, Any, Set, Deque +from typing import List, Optional, Dict, Any, Deque from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageSet, Seg # Local import needed after move diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index c489e012c..611888ff1 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -49,7 +49,7 @@ class HeartFCGenerator: arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() - with Timer() as t_generate_response: + with Timer() as _generate_response: current_model = self.model_normal current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高 model_response = await self._generate_response_with_model( From 8bfff8efe208d9e65383d660ed79909bc54e5c3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Apr 2025 14:59:23 +0000 Subject: [PATCH 32/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 4 +- src/heart_flow/observation.py | 2 +- src/heart_flow/sub_heartflow.py | 2 +- src/heart_flow/subheartflow_manager.py | 24 +- src/plugins/emoji_system/emoji_manager.py | 24 +- src/plugins/heartFC_chat/heartFC_chat.py | 281 +++++++++--------- .../heartFC_chat/heartflow_processor.py | 61 ++-- .../heartFC_chat/heartflow_prompt_builder.py | 15 +- src/plugins/utils/chat_message_builder.py | 4 +- 9 files changed, 203 insertions(+), 214 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 526020824..6ab3505df 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -321,9 +321,7 @@ CHAT_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 见闻 | {message}" - ), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 见闻 | {message}"), # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 见闻 | {message}", }, } diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 790c21805..b960154ca 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -22,7 +22,7 @@ class Observation: self.observe_type = observe_type self.observe_id = observe_id self.last_observe_time = datetime.now().timestamp() # 初始化为当前时间 - + async def observe(self): pass diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 91ddc2cd7..fb1a81c3b 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -57,7 +57,7 @@ class InterestChatting: self.max_reply_probability: float = max_probability self.current_reply_probability: float = 0.0 self.is_above_threshold: bool = False - + # 任务相关属性初始化 self.update_task: Optional[asyncio.Task] = None self._stop_event = asyncio.Event() diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index dcc455917..79f2a0ecf 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -271,11 +271,10 @@ class SubHeartflowManager: log_prefix = "[兴趣评估]" current_state = current_mai_state.get_current_state() focused_limit = current_state.get_focused_chat_max_num() - - + if int(time.time()) % 20 == 0: # 每20秒输出一次 logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天") - + if focused_limit <= 0: # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流") return @@ -288,22 +287,23 @@ class SubHeartflowManager: states_num = ( self.count_subflows_by_state(ChatState.ABSENT), self.count_subflows_by_state(ChatState.CHAT), - current_focused_count + current_focused_count, ) for sub_hf in list(self.subheartflows.values()): flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id - + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue - + from .mai_state_manager import enable_unlimited_hfc_chat + if not enable_unlimited_hfc_chat: if sub_hf.chat_state.chat_status != ChatState.CHAT: continue - + # 检查是否满足提升概率 if random.random() >= sub_hf.interest_chatting.start_hfc_probability: continue @@ -324,12 +324,12 @@ class SubHeartflowManager: # 执行状态提升 await current_subflow.set_chat_state(ChatState.FOCUSED, states_num) - - # 验证提升结果 - if (final_subflow := self.subheartflows.get(flow_id)) and \ - final_subflow.chat_state.chat_status == ChatState.FOCUSED: - current_focused_count += 1 + # 验证提升结果 + if ( + final_subflow := self.subheartflows.get(flow_id) + ) and final_subflow.chat_state.chat_status == ChatState.FOCUSED: + current_focused_count += 1 async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.1): """以一定概率将 FOCUSED 或 CHAT 状态的子心流回退到 ABSENT 状态。""" diff --git a/src/plugins/emoji_system/emoji_manager.py b/src/plugins/emoji_system/emoji_manager.py index 754792c24..cf3ebadb8 100644 --- a/src/plugins/emoji_system/emoji_manager.py +++ b/src/plugins/emoji_system/emoji_manager.py @@ -29,10 +29,11 @@ EMOJI_DIR = os.path.join(BASE_DIR, "emoji") # 表情包存储目录 EMOJI_REGISTED_DIR = os.path.join(BASE_DIR, "emoji_registed") # 已注册的表情包注册目录 -''' +""" 还没经过测试,有些地方数据库和内存数据同步可能不完全 -''' +""" + class MaiEmoji: """定义一个表情包""" @@ -258,7 +259,7 @@ class EmojiManager: if emoji.hash == hash: emoji.usage_count += 1 break - + except Exception as e: logger.error(f"记录表情使用失败: {str(e)}") @@ -316,7 +317,9 @@ class EmojiManager: time_end = time.time() - logger.info(f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})") + logger.info( + f"找到[{text_emotion}]表情包,用时:{time_end - time_start:.2f}秒: {selected_emoji.description} (相似度: {similarity:.4f})" + ) return selected_emoji.path, f"[ {selected_emoji.description} ]" except Exception as e: @@ -784,16 +787,15 @@ class EmojiManager: logger.error(f"[错误] 注册表情包失败: {str(e)}") logger.error(traceback.format_exc()) return False - - + async def clear_temp_emoji(self): """每天清理临时表情包 清理/data/emoji和/data/image目录下的所有文件 当目录中文件数超过50时,会全部删除 """ - + logger.info("[清理] 开始清理临时表情包...") - + # 清理emoji目录 emoji_dir = os.path.join(BASE_DIR, "emoji") if os.path.exists(emoji_dir): @@ -805,7 +807,7 @@ class EmojiManager: if os.path.isfile(file_path): os.remove(file_path) logger.debug(f"[清理] 删除表情包文件: {filename}") - + # 清理image目录 image_dir = os.path.join(BASE_DIR, "image") if os.path.exists(image_dir): @@ -817,10 +819,8 @@ class EmojiManager: if os.path.isfile(file_path): os.remove(file_path) logger.debug(f"[清理] 删除图片文件: {filename}") - + logger.success("[清理] 临时文件清理完成") - - # 创建全局单例 diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 8be4d42fe..b8e9781dc 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -38,31 +38,28 @@ logger = get_module_logger("HeartFCLoop", config=interest_log_config) # Logger # 默认动作定义 -DEFAULT_ACTIONS = { - "no_reply": "不回复", - "text_reply": "文本回复, 可选附带表情", - "emoji_reply": "仅表情回复" -} +DEFAULT_ACTIONS = {"no_reply": "不回复", "text_reply": "文本回复, 可选附带表情", "emoji_reply": "仅表情回复"} + class ActionManager: """动作管理器:控制每次决策可以使用的动作""" - + def __init__(self): # 初始化为默认动作集 self._available_actions: Dict[str, str] = DEFAULT_ACTIONS.copy() - + def get_available_actions(self) -> Dict[str, str]: """获取当前可用的动作集""" return self._available_actions - + def add_action(self, action_name: str, description: str) -> bool: """ 添加新的动作 - + 参数: action_name: 动作名称 description: 动作描述 - + 返回: bool: 是否添加成功 """ @@ -70,14 +67,14 @@ class ActionManager: return False self._available_actions[action_name] = description return True - + def remove_action(self, action_name: str) -> bool: """ 移除指定动作 - + 参数: action_name: 动作名称 - + 返回: bool: 是否移除成功 """ @@ -85,63 +82,73 @@ class ActionManager: return False del self._available_actions[action_name] return True - + def clear_actions(self): """清空所有动作""" self._available_actions.clear() - + def reset_to_default(self): """重置为默认动作集""" self._available_actions = DEFAULT_ACTIONS.copy() - + def get_planner_tool_definition(self) -> List[Dict[str, Any]]: """获取当前动作集对应的规划器工具定义""" - return [{ - "type": "function", - "function": { - "name": "decide_reply_action", - "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。", - "parameters": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": list(self._available_actions.keys()), - "description": "决定采取的行动:" + - ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]), - }, - "reasoning": {"type": "string", "description": "做出此决定的简要理由。"}, - "emoji_query": { - "type": "string", - "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。", + return [ + { + "type": "function", + "function": { + "name": "decide_reply_action", + "description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。", + "parameters": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": list(self._available_actions.keys()), + "description": "决定采取的行动:" + + ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]), + }, + "reasoning": {"type": "string", "description": "做出此决定的简要理由。"}, + "emoji_query": { + "type": "string", + "description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。", + }, }, + "required": ["action", "reasoning"], }, - "required": ["action", "reasoning"], }, - }, - }] + } + ] # 在文件开头添加自定义异常类 class HeartFCError(Exception): """麦麦聊天系统基础异常类""" + pass + class PlannerError(HeartFCError): """规划器异常""" + pass + class ReplierError(HeartFCError): """回复器异常""" + pass + class SenderError(HeartFCError): """发送器异常""" + pass class CycleInfo: """循环信息记录类""" + def __init__(self, cycle_id: int): self.cycle_id = cycle_id self.start_time = time.time() @@ -151,16 +158,16 @@ class CycleInfo: self.reasoning = "" self.timers: Dict[str, float] = {} self.thinking_id = "" - + # 添加响应信息相关字段 self.response_info: Dict[str, Any] = { "response_text": [], # 回复的文本列表 - "emoji_info": "", # 表情信息 + "emoji_info": "", # 表情信息 "anchor_message_id": "", # 锚点消息ID "reply_message_ids": [], # 回复消息ID列表 "sub_mind_thinking": "", # 子思维思考内容 } - + def to_dict(self) -> Dict[str, Any]: """将循环信息转换为字典格式""" return { @@ -172,29 +179,31 @@ class CycleInfo: "reasoning": self.reasoning, "timers": self.timers, "thinking_id": self.thinking_id, - "response_info": self.response_info + "response_info": self.response_info, } - + def complete_cycle(self): """完成循环,记录结束时间""" self.end_time = time.time() - + def set_action_info(self, action_type: str, reasoning: str, action_taken: bool): """设置动作信息""" self.action_type = action_type self.reasoning = reasoning self.action_taken = action_taken - + def set_thinking_id(self, thinking_id: str): """设置思考消息ID""" self.thinking_id = thinking_id - def set_response_info(self, - response_text: Optional[List[str]] = None, - emoji_info: Optional[str] = None, - anchor_message_id: Optional[str] = None, - reply_message_ids: Optional[List[str]] = None, - sub_mind_thinking: Optional[str] = None): + def set_response_info( + self, + response_text: Optional[List[str]] = None, + emoji_info: Optional[str] = None, + anchor_message_id: Optional[str] = None, + reply_message_ids: Optional[List[str]] = None, + sub_mind_thinking: Optional[str] = None, + ): """设置响应信息""" if response_text is not None: self.response_info["response_text"] = response_text @@ -227,7 +236,7 @@ class HeartFChatting: self.chat_stream: Optional[ChatStream] = None # 关联的聊天流 self.sub_mind: SubMind = sub_mind # 关联的子思维 self.observations: List[Observation] = observations # 关联的观察列表,用于监控聊天流状态 - + # 日志前缀 self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]" @@ -274,7 +283,7 @@ class HeartFChatting: # 更新日志前缀(以防流名称发生变化) self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" - + self._initialized = True logger.info(f"麦麦感觉到了,可以开始激情水群{self.log_prefix} ") return True @@ -333,52 +342,50 @@ class HeartFChatting: self._processing_lock.release() async def _hfc_loop(self): - """主循环,持续进行计划并可能回复消息,直到被外部取消。""" + """主循环,持续进行计划并可能回复消息,直到被外部取消。""" try: while True: # 主循环 # 创建新的循环信息 self._cycle_counter += 1 self._current_cycle = CycleInfo(self._cycle_counter) - + # 初始化周期状态 cycle_timers = {} loop_cycle_start_time = time.monotonic() - + # 执行规划和处理阶段 async with self._get_cycle_context() as acquired_lock: if not acquired_lock: continue - + # 记录规划开始时间点 planner_start_db_time = time.time() - - # 执行规划阶段 - action_taken, thinking_id = await self._think_plan_execute_loop( - cycle_timers, planner_start_db_time - ) - + + # 执行规划阶段 + action_taken, thinking_id = await self._think_plan_execute_loop(cycle_timers, planner_start_db_time) + # 更新循环信息 self._current_cycle.set_thinking_id(thinking_id) self._current_cycle.timers = cycle_timers # 防止循环过快消耗资源 await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) - + # 等待直到所有消息都发送完成 with Timer("发送消息", cycle_timers): while await self._should_skip_cycle(thinking_id): await asyncio.sleep(0.2) - + # 完成当前循环并保存历史 self._current_cycle.complete_cycle() self._cycle_history.append(self._current_cycle) - + # 记录循环信息和计时器结果 timer_strings = [] for name, elapsed in cycle_timers.items(): formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" timer_strings.append(f"{name}: {formatted_time}") - + logger.debug( f"{self.log_prefix} 第 #{self._current_cycle.cycle_id}次思考完成," f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, " @@ -396,7 +403,7 @@ class HeartFChatting: async def _get_cycle_context(self): """ 循环周期的上下文管理器 - + 用于确保资源的正确获取和释放: 1. 获取处理锁 2. 执行操作 @@ -414,10 +421,10 @@ class HeartFChatting: async def _check_new_messages(self, start_time: float) -> bool: """ 检查从指定时间点后是否有新消息 - + 参数: start_time: 开始检查的时间点 - + 返回: bool: 是否有新消息 """ @@ -431,9 +438,7 @@ class HeartFChatting: logger.error(f"{self.log_prefix} 检查新消息时出错: {e}") return False - async def _think_plan_execute_loop( - self, cycle_timers: dict, planner_start_db_time: float - ) -> tuple[bool, str]: + async def _think_plan_execute_loop(self, cycle_timers: dict, planner_start_db_time: float) -> tuple[bool, str]: """执行规划阶段""" try: # 获取子思维思考结果 @@ -443,34 +448,36 @@ class HeartFChatting: # 记录子思维思考内容 if self._current_cycle: self._current_cycle.set_response_info(sub_mind_thinking=current_mind) - + # 执行规划 with Timer("决策", cycle_timers): planner_result = await self._planner(current_mind, cycle_timers) - + # 在获取规划结果后检查新消息 if await self._check_new_messages(planner_start_db_time): # 更新循环信息 logger.info(f"{self.log_prefix} 思考到一半,检测到新消息,重新思考") self._current_cycle.set_action_info("new_messages", "检测到新消息", False) return False, "new_messages" - + # 解析规划结果 action = planner_result.get("action", "error") reasoning = planner_result.get("reasoning", "未提供理由") - + # 更新循环信息 self._current_cycle.set_action_info(action, reasoning, True) - + # 处理LLM错误 if planner_result.get("llm_error"): logger.error(f"{self.log_prefix} LLM失败: {reasoning}") return False, "" - + # 根据动作类型执行对应处理 with Timer("执行", cycle_timers): - return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) - + return await self._handle_action( + action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time + ) + except PlannerError as e: logger.error(f"{self.log_prefix} 规划错误: {e}") # 更新循环信息 @@ -478,37 +485,32 @@ class HeartFChatting: return False, "" async def _handle_action( - self, - action: str, - reasoning: str, - emoji_query: str, - cycle_timers: dict, - planner_start_db_time: float + self, action: str, reasoning: str, emoji_query: str, cycle_timers: dict, planner_start_db_time: float ) -> tuple[bool, str]: """ 处理规划动作 - + 参数: action: 动作类型 reasoning: 决策理由 emoji_query: 表情查询 cycle_timers: 计时器字典 planner_start_db_time: 规划开始时间 - + 返回: tuple[bool, str]: (是否执行了动作, 思考消息ID) """ action_handlers = { "text_reply": self._handle_text_reply, "emoji_reply": self._handle_emoji_reply, - "no_reply": self._handle_no_reply + "no_reply": self._handle_no_reply, } - + handler = action_handlers.get(action) if not handler: logger.warning(f"{self.log_prefix} 未知动作: {action}, 原因: {reasoning}") return False, "" - + try: if action == "text_reply": return await handler(reasoning, emoji_query, cycle_timers) @@ -520,37 +522,35 @@ class HeartFChatting: logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") return False, "" - async def _handle_text_reply( - self, reasoning: str, emoji_query: str, cycle_timers: dict - ) -> tuple[bool, str]: + async def _handle_text_reply(self, reasoning: str, emoji_query: str, cycle_timers: dict) -> tuple[bool, str]: """ 处理文本回复 - + 工作流程: 1. 获取锚点消息 2. 创建思考消息 3. 生成回复 4. 发送消息 - + 参数: reasoning: 回复原因 emoji_query: 表情查询 cycle_timers: 计时器字典 - + 返回: tuple[bool, str]: (是否回复成功, 思考消息ID) """ - + # 获取锚点消息 anchor_message = await self._get_anchor_message() if not anchor_message: raise PlannerError("无法获取锚点消息") - + # 创建思考消息 thinking_id = await self._create_thinking_message(anchor_message) if not thinking_id: raise PlannerError("无法创建思考消息") - + try: # 生成回复 with Timer("Replier", cycle_timers): @@ -559,10 +559,10 @@ class HeartFChatting: thinking_id=thinking_id, reason=reasoning, ) - + if not reply: raise ReplierError("回复生成失败") - + # 发送消息 with Timer("Sender", cycle_timers): await self._sender( @@ -571,9 +571,9 @@ class HeartFChatting: response_set=reply, send_emoji=emoji_query, ) - + return True, thinking_id - + except (ReplierError, SenderError) as e: logger.error(f"{self.log_prefix} 回复失败: {e}") return True, thinking_id # 仍然返回thinking_id以便跟踪 @@ -581,72 +581,68 @@ class HeartFChatting: async def _handle_emoji_reply(self, reasoning: str, emoji_query: str) -> bool: """ 处理表情回复 - + 工作流程: 1. 获取锚点消息 2. 发送表情 - + 参数: reasoning: 回复原因 emoji_query: 表情查询 - + 返回: bool: 是否发送成功 """ logger.info(f"{self.log_prefix} 决定回复表情({emoji_query}): {reasoning}") - + try: anchor = await self._get_anchor_message() if not anchor: raise PlannerError("无法获取锚点消息") - + await self._handle_emoji(anchor, [], emoji_query) return True - + except Exception as e: logger.error(f"{self.log_prefix} 表情发送失败: {e}") return False - async def _handle_no_reply( - self, reasoning: str, planner_start_db_time: float, cycle_timers: dict - ) -> bool: + async def _handle_no_reply(self, reasoning: str, planner_start_db_time: float, cycle_timers: dict) -> bool: """ 处理不回复的情况 - + 工作流程: 1. 等待新消息 2. 超时或收到新消息时返回 - + 参数: reasoning: 不回复的原因 planner_start_db_time: 规划开始时间 cycle_timers: 计时器字典 - + 返回: bool: 是否成功处理 """ logger.info(f"{self.log_prefix} 决定不回复: {reasoning}") - + observation = self.observations[0] if self.observations else None - + try: with Timer("Wait New Msg", cycle_timers): return await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) except asyncio.CancelledError: logger.info(f"{self.log_prefix} 等待被中断") raise - - async def _wait_for_new_message( - self, observation, planner_start_db_time: float, log_prefix: str - ) -> bool: + + async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool: """ 等待新消息 - + 参数: observation: 观察实例 planner_start_db_time: 开始等待的时间 log_prefix: 日志前缀 - + 返回: bool: 是否检测到新消息 """ @@ -655,11 +651,11 @@ class HeartFChatting: if await observation.has_new_messages_since(planner_start_db_time): logger.info(f"{log_prefix} 检测到新消息") return True - + if time.monotonic() - wait_start_time > 60: logger.warning(f"{log_prefix} 等待超时(60秒)") return False - + await asyncio.sleep(1.5) async def _should_skip_cycle(self, thinking_id: str) -> bool: @@ -677,13 +673,11 @@ class HeartFChatting: if timer_strings: logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") - async def _handle_cycle_delay( - self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str - ): + async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): """处理循环延迟""" cycle_duration = time.monotonic() - cycle_start_time # if cycle_duration > 0.1: - # logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") + # logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") try: sleep_duration = 0.0 @@ -702,7 +696,7 @@ class HeartFChatting: async def _get_submind_thinking(self) -> str: """ 获取子思维的思考结果 - + 返回: str: 思考结果,如果思考失败则返回错误信息 """ @@ -719,7 +713,7 @@ class HeartFChatting: async def _planner(self, current_mind: str, cycle_timers: dict) -> Dict[str, Any]: """ 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 - + 参数: current_mind: 子思维的当前思考结果 """ @@ -779,7 +773,9 @@ class HeartFChatting: action = arguments.get("action", "no_reply") # 验证动作是否在可用动作集中 if action not in self.action_manager.get_available_actions(): - logger.warning(f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action},使用默认动作no_reply") + logger.warning( + f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action},使用默认动作no_reply" + ) action = "no_reply" reasoning = f"LLM返回了未授权的动作: {action}" else: @@ -787,7 +783,9 @@ class HeartFChatting: emoji_query = arguments.get("emoji_query", "") # 记录决策结果 - logger.debug(f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'") + logger.debug( + f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" + ) else: # 处理工具调用失败 logger.warning(f"{self.log_prefix}[Planner] {error_msg}") @@ -960,9 +958,6 @@ class HeartFChatting: message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter) thinking_id=thinking_id, # Pass thinking_id positionally ) - - - if not response_set: logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。") @@ -1014,8 +1009,7 @@ class HeartFChatting: # 记录锚点消息ID if self._current_cycle and anchor_message: self._current_cycle.set_response_info( - response_text=response_set, - anchor_message_id=anchor_message.message_info.message_id + response_text=response_set, anchor_message_id=anchor_message.message_info.message_id ) chat = anchor_message.chat_stream @@ -1090,9 +1084,7 @@ class HeartFChatting: emoji_path, description = emoji_raw # 记录表情信息 if self._current_cycle: - self._current_cycle.set_response_info( - emoji_info=f"表情: {description}, 路径: {emoji_path}" - ) + self._current_cycle.set_response_info(emoji_info=f"表情: {description}, 路径: {emoji_path}") emoji_cq = image_path_to_base64(emoji_path) thinking_time_point = round(time.time(), 2) @@ -1117,10 +1109,10 @@ class HeartFChatting: def get_cycle_history(self, last_n: Optional[int] = None) -> List[Dict[str, Any]]: """获取循环历史记录 - + 参数: last_n: 获取最近n个循环的信息,如果为None则获取所有历史记录 - + 返回: List[Dict[str, Any]]: 循环历史记录列表 """ @@ -1134,4 +1126,3 @@ class HeartFChatting: if self._cycle_history: return self._cycle_history[-1].to_dict() return None - diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index e43659ee5..204ca703a 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -24,14 +24,14 @@ logger = get_module_logger("heartflow_processor", config=processor_config) class HeartFCProcessor: """心流处理器,负责处理接收到的消息并计算兴趣度""" - + def __init__(self): """初始化心流处理器,创建消息存储实例""" self.storage = MessageStorage() async def _handle_error(self, error: Exception, context: str, message: Optional[MessageRecv] = None) -> None: """统一的错误处理函数 - + Args: error: 捕获到的异常 context: 错误发生的上下文描述 @@ -39,12 +39,12 @@ class HeartFCProcessor: """ logger.error(f"{context}: {error}") logger.error(traceback.format_exc()) - if message and hasattr(message, 'raw_message'): + if message and hasattr(message, "raw_message"): logger.error(f"相关消息原始内容: {message.raw_message}") async def _process_relationship(self, message: MessageRecv) -> None: """处理用户关系逻辑 - + Args: message: 消息对象,包含用户信息 """ @@ -54,24 +54,20 @@ class HeartFCProcessor: cardname = message.message_info.user_info.user_cardname or nickname is_known = await relationship_manager.is_known_some_one(platform, user_id) - + if not is_known: logger.info(f"首次认识用户: {nickname}") - await relationship_manager.first_knowing_some_one( - platform, user_id, nickname, cardname, "" - ) + await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "") elif not await relationship_manager.is_qved_name(platform, user_id): logger.info(f"给用户({nickname},{cardname})取名: {nickname}") - await relationship_manager.first_knowing_some_one( - platform, user_id, nickname, cardname, "" - ) + await relationship_manager.first_knowing_some_one(platform, user_id, nickname, cardname, "") async def _calculate_interest(self, message: MessageRecv) -> Tuple[float, bool]: """计算消息的兴趣度 - + Args: message: 待处理的消息对象 - + Returns: Tuple[float, bool]: (兴趣度, 是否被提及) """ @@ -93,33 +89,35 @@ class HeartFCProcessor: def _get_message_type(self, message: MessageRecv) -> str: """获取消息类型 - + Args: message: 消息对象 - + Returns: str: 消息类型 """ if message.message_segment.type != "seglist": return message.message_segment.type - - if (isinstance(message.message_segment.data, list) + + if ( + isinstance(message.message_segment.data, list) and all(isinstance(x, Seg) for x in message.message_segment.data) - and len(message.message_segment.data) == 1): + and len(message.message_segment.data) == 1 + ): return message.message_segment.data[0].type - + return "seglist" async def process_message(self, message_data: str) -> None: """处理接收到的原始消息数据 - + 主要流程: 1. 消息解析与初始化 2. 消息缓冲处理 3. 过滤检查 4. 兴趣度计算 5. 关系处理 - + Args: message_data: 原始消息字符串 """ @@ -133,20 +131,21 @@ class HeartFCProcessor: # 2. 消息缓冲与流程序化 await message_buffer.start_caching_messages(message) - + chat = await chat_manager.get_or_create_stream( platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo, ) - + subheartflow = await heartflow.create_subheartflow(chat.stream_id) message.update_chat_stream(chat) await message.process() - + # 3. 过滤检查 - if self._check_ban_words(message.processed_plain_text, chat, userinfo) or \ - self._check_ban_regex(message.raw_message, chat, userinfo): + if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex( + message.raw_message, chat, userinfo + ): return # 4. 缓冲检查 @@ -156,7 +155,7 @@ class HeartFCProcessor: type_messages = { "text": f"触发缓冲,消息:{message.processed_plain_text}", "image": "触发缓冲,表情包/图片等待中", - "seglist": "触发缓冲,消息列表等待中" + "seglist": "触发缓冲,消息列表等待中", } logger.debug(type_messages.get(msg_type, "触发未知类型缓冲")) return @@ -189,12 +188,12 @@ class HeartFCProcessor: def _check_ban_words(self, text: str, chat, userinfo) -> bool: """检查消息是否包含过滤词 - + Args: text: 待检查的文本 chat: 聊天对象 userinfo: 用户信息 - + Returns: bool: 是否包含过滤词 """ @@ -208,12 +207,12 @@ class HeartFCProcessor: def _check_ban_regex(self, text: str, chat, userinfo) -> bool: """检查消息是否匹配过滤正则表达式 - + Args: text: 待检查的文本 chat: 聊天对象 userinfo: 用户信息 - + Returns: bool: 是否匹配过滤正则 """ diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 146a5307f..80587f2fe 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -37,12 +37,15 @@ def init_prompt(): {moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) - - Prompt(""" + + Prompt( + """ 你有以下信息可供参考: {structured_info} 以上的消息是你获取到的消息,或许可以帮助你更好地回复。 -""", "info_from_tools") +""", + "info_from_tools", + ) # Planner提示词 Prompt( @@ -163,11 +166,11 @@ class PromptBuilder: prompt_ger += "你喜欢用倒装句" if random.random() < 0.02: prompt_ger += "你喜欢用反问句" - + if structured_info: structured_info_prompt = await global_prompt_manager.format_prompt( - "info_from_tools", - structured_info = structured_info) + "info_from_tools", structured_info=structured_info + ) else: structured_info_prompt = "" diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index edd60c05a..630ff989a 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -303,9 +303,7 @@ async def build_readable_messages( ) readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode) - read_mark_line = ( - f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" - ) + read_mark_line = f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" # 组合结果,确保空部分不引入多余的标记或换行 if formatted_before and formatted_after: From 325bd567948fdd23ea428aa4802618cdb28314d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Fri, 25 Apr 2025 23:03:51 +0800 Subject: [PATCH 33/79] fix: typo --- src/plugins/heartFC_chat/heartFC_chat.py | 2 +- src/plugins/heartFC_chat/heartFC_generator.py | 2 +- src/plugins/heartFC_chat/heartflow_processor.py | 2 +- src/plugins/heartFC_chat/normal_chat.py | 2 +- src/plugins/heartFC_chat/normal_chat_generator.py | 2 +- src/plugins/utils/{timer_calculater.py => timer_calculator.py} | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename src/plugins/utils/{timer_calculater.py => timer_calculator.py} (100%) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index b8e9781dc..d77911808 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -12,7 +12,7 @@ from src.common.logger import get_module_logger, LogConfig, PFC_STYLE_CONFIG # from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move -from src.plugins.utils.timer_calculater import Timer # <--- Import Timer +from src.plugins.utils.timer_calculator import Timer # <--- Import Timer from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator from src.do_tool.tool_use import ToolUser from ..chat.message_sender import message_manager # <-- Import the global manager diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index 611888ff1..b750c13c1 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -8,7 +8,7 @@ from .heartflow_prompt_builder import prompt_builder from ..chat.utils import process_llm_response from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager -from ..utils.timer_calculater import Timer +from ..utils.timer_calculator import Timer from src.plugins.moods.moods import MoodManager diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index 204ca703a..da7b479ba 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -10,7 +10,7 @@ from src.heart_flow.heartflow import heartflow from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from ..chat.chat_stream import chat_manager from ..chat.message_buffer import message_buffer -from ..utils.timer_calculater import Timer +from ..utils.timer_calculator import Timer from src.plugins.person_info.relationship_manager import relationship_manager from typing import Optional, Tuple diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 56fcfc346..76cba5979 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -17,7 +17,7 @@ from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager -from src.plugins.utils.timer_calculater import Timer +from src.plugins.utils.timer_calculator import Timer # 定义日志配置 chat_config = LogConfig( diff --git a/src/plugins/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py index cd9208b3a..52d0f446f 100644 --- a/src/plugins/heartFC_chat/normal_chat_generator.py +++ b/src/plugins/heartFC_chat/normal_chat_generator.py @@ -5,7 +5,7 @@ from ...config.config import global_config from ..chat.message import MessageThinking from .heartflow_prompt_builder import prompt_builder from ..chat.utils import process_llm_response -from ..utils.timer_calculater import Timer +from ..utils.timer_calculator import Timer from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager diff --git a/src/plugins/utils/timer_calculater.py b/src/plugins/utils/timer_calculator.py similarity index 100% rename from src/plugins/utils/timer_calculater.py rename to src/plugins/utils/timer_calculator.py From e17e47bcaf211137edd38c1a21bfe7a5ba033b3b Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 23:05:46 +0800 Subject: [PATCH 34/79] =?UTF-8?q?better=EF=BC=9A=E4=BC=98=E5=8C=96prompt?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=94=B9buffer=E8=A1=8C=E4=B8=BA=EF=BC=88?= =?UTF-8?q?=E6=9B=B4=E4=B8=A5=E6=A0=BC=E5=88=A4=E5=AE=9A=E9=99=8D=E4=BD=8E?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=EF=BC=8C=E4=B8=8D=E4=B8=A2=E5=BC=83=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=89=8D=E6=96=87=E6=9C=AC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_mind.py | 46 +++++---- src/plugins/chat/message_buffer.py | 95 +++++++++---------- src/plugins/heartFC_chat/heartFC_chat.py | 88 ++++++++--------- .../heartFC_chat/heartflow_prompt_builder.py | 49 +++++++--- src/plugins/person_info/person_info.py | 38 +++++--- src/plugins/utils/chat_message_builder.py | 14 ++- 6 files changed, 185 insertions(+), 145 deletions(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 8470c24e9..6b2bdaac3 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -25,18 +25,20 @@ def init_prompt(): prompt += "{extra_info}\n" prompt += "{prompt_personality}\n" prompt += "{last_loop_prompt}\n" - prompt += "-----------------------------------\n" prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" - prompt += "现在请你,阅读群里正在进行的聊天内容,思考群里的正在进行的话题,分析群里成员与你的关系。" - prompt += "请你思考,生成你的内心想法,包括你的思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" - prompt += "回复的要求是:不要总是重复自己提到过的话题,如果你要回复,最好只回复一个人的一个话题\n" - prompt += "如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,不要回复。" - prompt += "如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等。" - prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言\n" - prompt += "现在请你先输出想法,{hf_do_next},不要分点输出,文字不要浮夸" - prompt += "在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。" - prompt += "如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" + prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,思考你要不要回复。" + prompt += "思考并输出你的内心想法\n" + prompt += "输出要求:\n" + prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n" + prompt += "2. 不要分点、不要使用表情符号\n" + prompt += "3. 避免多余符号(冒号、引号、括号等)\n" + prompt += "4. 语言简洁自然,不要浮夸\n" + prompt += "5. 如果你刚发言,并且没有人回复你,不要回复\n" + prompt += "工具使用说明:\n" + prompt += "1. 输出想法后考虑是否需要使用工具\n" + prompt += "2. 工具可获取信息或执行操作\n" + prompt += "3. 如需处理消息或回复,请使用工具\n" Prompt(prompt, "sub_heartflow_prompt_before") @@ -65,7 +67,7 @@ class SubMind: self.past_mind = [] self.structured_info = {} - async def do_thinking_before_reply(self, last_cycle: CycleInfo): + async def do_thinking_before_reply(self, last_cycle: CycleInfo = None): """ 在回复前进行思考,生成内心想法并收集工具调用结果 @@ -123,14 +125,14 @@ class SubMind: # 思考指导选项和权重 hf_options = [ - ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,但是不要纠结于同一个话题", 0.6), - ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1), - ("生成你在这个聊天中的想法,不要太深入", 0.2), - ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), + ("可以参考之前的想法,在原来想法的基础上继续思考", 0.2), + ("可以参考之前的想法,在原来的想法上尝试新的话题", 0.4), + ("不要太深入", 0.2), + ("进行深入思考", 0.2), ] #上一次决策信息 - if last_cycle.action_type: + if last_cycle != None: last_action = last_cycle.action_type last_reasoning = last_cycle.reasoning is_replan = last_cycle.replanned @@ -143,11 +145,13 @@ class SubMind: last_reasoning = "" is_replan = False if_replan_prompt = "" - - last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( - current_thinking_info=current_thinking_info, - if_replan_prompt=if_replan_prompt - ) + if current_thinking_info: + last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( + current_thinking_info=current_thinking_info, + if_replan_prompt=if_replan_prompt + ) + else: + last_loop_prompt = "" # 加权随机选择思考指导 hf_do_next = local_random.choices( diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py index d0ab56042..b3166f300 100644 --- a/src/plugins/chat/message_buffer.py +++ b/src/plugins/chat/message_buffer.py @@ -128,58 +128,55 @@ class MessageBuffer: if result: async with self.lock: # 再次加锁 # 清理所有早于当前消息的已处理消息, 收集所有早于当前消息的F消息的processed_plain_text - keep_msgs = OrderedDict() - combined_text = [] - found = False - type = "seglist" - is_update = True - for msg_id, msg in self.buffer_pool[person_id_].items(): + keep_msgs = OrderedDict() # 用于存放 T 消息之后的消息 + collected_texts = [] # 用于收集 T 消息及之前 F 消息的文本 + process_target_found = False + + # 遍历当前用户的所有缓冲消息 + for msg_id, cache_msg in self.buffer_pool[person_id_].items(): + # 如果找到了目标处理消息 (T 状态) if msg_id == message.message_info.message_id: - found = True - if msg.message.message_segment.type != "seglist": - type = msg.message.message_segment.type - else: - if ( - isinstance(msg.message.message_segment.data, list) - and all(isinstance(x, Seg) for x in msg.message.message_segment.data) - and len(msg.message.message_segment.data) == 1 - ): - type = msg.message.message_segment.data[0].type - combined_text.append(msg.message.processed_plain_text) - continue - if found: - keep_msgs[msg_id] = msg - elif msg.result == "F": - # 收集F消息的文本内容 - f_type = "seglist" - if msg.message.message_segment.type != "seglist": - f_type = msg.message.message_segment.type - else: - if ( - isinstance(msg.message.message_segment.data, list) - and all(isinstance(x, Seg) for x in msg.message.message_segment.data) - and len(msg.message.message_segment.data) == 1 - ): - f_type = msg.message.message_segment.data[0].type - if hasattr(msg.message, "processed_plain_text") and msg.message.processed_plain_text: - if f_type == "text": - combined_text.append(msg.message.processed_plain_text) - elif f_type != "text": - is_update = False - elif msg.result == "U": - logger.debug(f"异常未处理信息id: {msg.message.message_info.message_id}") + process_target_found = True + # 收集这条 T 消息的文本 (如果有) + if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + collected_texts.append(cache_msg.message.processed_plain_text) + # 不立即放入 keep_msgs,因为它之前的 F 消息也处理完了 - # 更新当前消息的processed_plain_text - if combined_text and combined_text[0] != message.processed_plain_text and is_update: - if type == "text": - message.processed_plain_text = ",".join(combined_text) - logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容到当前消息") - elif type == "emoji": - combined_text.pop() - message.processed_plain_text = ",".join(combined_text) - message.is_emoji = False - logger.debug(f"整合了{len(combined_text) - 1}条F消息的内容,覆盖当前emoji消息") + # 如果已经找到了目标 T 消息,之后的消息需要保留 + elif process_target_found: + keep_msgs[msg_id] = cache_msg + # 如果还没找到目标 T 消息,说明是之前的消息 (F 或 U) + else: + if cache_msg.result == "F": + # 收集这条 F 消息的文本 (如果有) + if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + collected_texts.append(cache_msg.message.processed_plain_text) + elif cache_msg.result == "U": + # 理论上不应该在 T 消息之前还有 U 消息,记录日志 + logger.warning(f"异常状态:在目标 T 消息 {message.message_info.message_id} 之前发现未处理的 U 消息 {cache_msg.message.message_info.message_id}") + # 也可以选择收集其文本 + if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + collected_texts.append(cache_msg.message.processed_plain_text) + + + # 更新当前消息 (message) 的 processed_plain_text + # 只有在收集到的文本多于一条,或者只有一条但与原始文本不同时才合并 + if collected_texts: + # 使用 OrderedDict 去重,同时保留原始顺序 + unique_texts = list(OrderedDict.fromkeys(collected_texts)) + merged_text = ",".join(unique_texts) + + # 只有在合并后的文本与原始文本不同时才更新 + # 并且确保不是空合并 + if merged_text and merged_text != message.processed_plain_text: + message.processed_plain_text = merged_text + # 如果合并了文本,原消息不再视为纯 emoji + if hasattr(message, 'is_emoji'): + message.is_emoji = False + logger.debug(f"合并了 {len(unique_texts)} 条消息的文本内容到当前消息 {message.message_info.message_id}") + + # 更新缓冲池,只保留 T 消息之后的消息 self.buffer_pool[person_id_] = keep_msgs return result except asyncio.TimeoutError: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index c11674fe2..0135cfb75 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -405,7 +405,7 @@ class HeartFChatting: return False, "" # execute:执行 - with Timer("执行", cycle_timers): + with Timer("执行动作", cycle_timers): return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) except PlannerError as e: @@ -490,7 +490,7 @@ class HeartFChatting: try: # 生成回复 - with Timer("Replier", cycle_timers): + with Timer("生成回复", cycle_timers): reply = await self._replier_work( anchor_message=anchor_message, thinking_id=thinking_id, @@ -501,13 +501,13 @@ class HeartFChatting: raise ReplierError("回复生成失败") # 发送消息 - with Timer("Sender", cycle_timers): - await self._sender( - thinking_id=thinking_id, - anchor_message=anchor_message, - response_set=reply, - send_emoji=emoji_query, - ) + + await self._sender( + thinking_id=thinking_id, + anchor_message=anchor_message, + response_set=reply, + send_emoji=emoji_query, + ) return True, thinking_id @@ -675,7 +675,7 @@ class HeartFChatting: # 获取观察信息 observation = self.observations[0] if is_re_planned: - observation.observe() + await observation.observe() observed_messages = observation.talking_message observed_messages_str = observation.talking_message_str @@ -687,40 +687,40 @@ class HeartFChatting: try: # 构建提示词 - with Timer("构建提示词", cycle_timers): - if is_re_planned: - replan_prompt = await self._build_replan_prompt( - self._current_cycle.action, self._current_cycle.reasoning - ) - prompt = replan_prompt - else: - replan_prompt = "" - prompt = await self._build_planner_prompt( - observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt + + if is_re_planned: + replan_prompt = await self._build_replan_prompt( + self._current_cycle.action_type, self._current_cycle.reasoning ) - payload = { - "model": global_config.llm_plan["name"], - "messages": [{"role": "user", "content": prompt}], - "tools": self.action_manager.get_planner_tool_definition(), - "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, - } + prompt = replan_prompt + else: + replan_prompt = "" + prompt = await self._build_planner_prompt( + observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt + ) + payload = { + "model": global_config.llm_plan["name"], + "messages": [{"role": "user", "content": prompt}], + "tools": self.action_manager.get_planner_tool_definition(), + "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, + } # 执行LLM请求 - with Timer("LLM回复", cycle_timers): - try: - response = await self.planner_llm._execute_request( - endpoint="/chat/completions", payload=payload, prompt=prompt - ) - except Exception as req_e: - logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") - return { - "action": "error", - "reasoning": f"LLM请求执行失败: {req_e}", - "emoji_query": "", - "current_mind": current_mind, - "observed_messages": observed_messages, - "llm_error": True, - } + + try: + response = await self.planner_llm._execute_request( + endpoint="/chat/completions", payload=payload, prompt=prompt + ) + except Exception as req_e: + logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}") + return { + "action": "error", + "reasoning": f"LLM请求执行失败: {req_e}", + "emoji_query": "", + "current_mind": current_mind, + "observed_messages": observed_messages, + "llm_error": True, + } # 处理LLM响应 with Timer("使用工具", cycle_timers): @@ -883,7 +883,7 @@ class HeartFChatting: # 准备聊天内容块 chat_content_block = "" if observed_messages_str: - chat_content_block = "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n" + chat_content_block = "观察到的最新聊天内容如下:\n---\n" chat_content_block += observed_messages_str chat_content_block += "\n---" else: @@ -892,9 +892,9 @@ class HeartFChatting: # 准备当前思维块 current_mind_block = "" if current_mind: - current_mind_block = f"\n---\n{current_mind}\n---\n\n" + current_mind_block = f"{current_mind}" else: - current_mind_block = " [没有特别的想法] \n\n" + current_mind_block = "[没有特别的想法]" # 获取提示词模板并填充数据 prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index ec12e2adf..9dd6e4d3a 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -44,28 +44,49 @@ def init_prompt(): 以上的消息是你获取到的消息,或许可以帮助你更好地回复。 """, "info_from_tools") - # Planner提示词 + # Planner提示词 - 优化版 Prompt( - """你的名字是 {bot_name}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。 + """你的名字是{bot_name},一名大二女生,正在QQ群聊中。需要基于以下信息决定如何参与对话: {structured_info_block} {chat_content_block} -看了以上内容,你产生的内心想法是: +你的内心想法: {current_mind_block} {replan} -请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。 -注意你必须参考以下决策依据来选择工具: -1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。 -2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (每个标签用一个词组表示,格式例如:幽默的讽刺,单纯的开心,愤怒的抗议)。 -3. 如果聊天内容或你的内心想法适合用一个表情来回应,选择 'emoji_reply' 并提供表情主题 'emoji_query'。 -4. 如果最后一条消息是你自己发的,观察到的内容只有你自己的发言,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。 -5. 如果聊天记录中最新的消息是你自己发送的,并且你还想继续回复,你应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;。 -6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。 -7. 不要回复你自己的话,不要把自己的话当做别人说的。 -必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。""", + +请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意: + +【回复原则】 +1. 不回复(no_reply)适用: +- 话题无关/无聊/不感兴趣 +- 最后一条消息是你自己发的且无人回应你 +- 讨论你不懂的专业话题 +- 讨论你不想参与的话题 +- 你发送了太多消息 + +2. 文字回复(text_reply)适用: +- 有实质性内容需要表达 +- 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃") +- 不要追加太多表情 + +3. 纯表情回复(emoji_reply)适用: +- 适合用表情回应的场景 +- 需提供明确的emoji_query + +4. 自我对话处理: +- 如果是自己发的消息想继续,需自然衔接 +- 避免重复或评价自己的发言 +- 不要和自己聊天 + +【必须遵守】 +- 必须调用工具并包含action和reasoning +- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) +- 选择text_reply或emoji_reply时必须提供emoji_query +- 保持回复自然,符合日常聊天习惯""", "planner_prompt", ) - Prompt("你原本打算{action},因为:{reasoning},但是你看到了新的消息,你决定重新决定行动。", "replan_prompt") + Prompt('''你原本打算{action},因为:{reasoning} +但是你看到了新的消息,你决定重新决定行动。''', "replan_prompt") Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index e4f4004e8..f5ec6d8f3 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -53,7 +53,7 @@ person_info_default = { # "impression" : None, # "gender" : Unkown, "konw_time": 0, - "msg_interval": 3000, + "msg_interval": 2000, "msg_interval_list": [], } # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项 @@ -384,18 +384,21 @@ class PersonInfoManager: if delta > 0: time_interval.append(delta) - time_interval = [t for t in time_interval if 500 <= t <= 8000] - if len(time_interval) >= 30: + time_interval = [t for t in time_interval if 200 <= t <= 8000] + # --- 修改后的逻辑 --- + # 数据量检查 (至少需要 30 条有效间隔,并且足够进行头尾截断) + if len(time_interval) >= 30 + 10: # 至少30条有效+头尾各5条 time_interval.sort() - # 画图(log) + # 画图(log) - 这部分保留 msg_interval_map = True log_dir = Path("logs/person_info") log_dir.mkdir(parents=True, exist_ok=True) plt.figure(figsize=(10, 6)) - time_series = pd.Series(time_interval) - plt.hist(time_series, bins=50, density=True, alpha=0.4, color="pink", label="Histogram") - time_series.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density") + # 使用截断前的数据画图,更能反映原始分布 + time_series_original = pd.Series(time_interval) + plt.hist(time_series_original, bins=50, density=True, alpha=0.4, color="pink", label="Histogram (Original Filtered)") + time_series_original.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density (Original Filtered)") plt.grid(True, alpha=0.2) plt.xlim(0, 8000) plt.title(f"Message Interval Distribution (User: {person_id[:8]}...)") @@ -405,15 +408,22 @@ class PersonInfoManager: img_path = log_dir / f"interval_distribution_{person_id[:8]}.png" plt.savefig(img_path) plt.close() - # 画图 + # 画图结束 - q25, q75 = np.percentile(time_interval, [25, 75]) - iqr = q75 - q25 - filtered = [x for x in time_interval if (q25 - 1.5 * iqr) <= x <= (q75 + 1.5 * iqr)] + # 去掉头尾各 5 个数据点 + trimmed_interval = time_interval[5:-5] - msg_interval = int(round(np.percentile(filtered, 80))) - await self.update_one_field(person_id, "msg_interval", msg_interval) - logger.trace(f"用户{person_id}的msg_interval已经被更新为{msg_interval}") + # 计算截断后数据的 37% 分位数 + if trimmed_interval: # 确保截断后列表不为空 + msg_interval = int(round(np.percentile(trimmed_interval, 37))) + # 更新数据库 + await self.update_one_field(person_id, "msg_interval", msg_interval) + logger.trace(f"用户{person_id}的msg_interval通过头尾截断和37分位数更新为{msg_interval}") + else: + logger.trace(f"用户{person_id}截断后数据为空,无法计算msg_interval") + else: + logger.trace(f"用户{person_id}有效消息间隔数量 ({len(time_interval)}) 不足进行推断 (需要至少 {30+10} 条)") + # --- 修改结束 --- except Exception as e: logger.trace(f"用户{person_id}消息间隔计算失败: {type(e).__name__}: {str(e)}") continue diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index edd60c05a..8ba49d9cd 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -168,7 +168,10 @@ async def _build_readable_messages_internal( user_info = msg.get("user_info", {}) platform = user_info.get("platform") user_id = user_info.get("user_id") - user_nickname = user_info.get("nickname") + + user_nickname = user_info.get("user_nickname") + user_cardname = user_info.get("user_cardname") + timestamp = msg.get("time") content = msg.get("processed_plain_text", "") # 默认空字符串 @@ -186,7 +189,12 @@ async def _build_readable_messages_internal( # 如果 person_name 未设置,则使用消息中的 nickname 或默认名称 if not person_name: - person_name = user_nickname + if user_cardname: + person_name = f"昵称:{user_cardname}" + elif user_nickname: + person_name = f"{user_nickname}" + else: + person_name = "某人" message_details.append((timestamp, person_name, content)) @@ -304,7 +312,7 @@ async def build_readable_messages( readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode) read_mark_line = ( - f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" + f"\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" ) # 组合结果,确保空部分不引入多余的标记或换行 From 6471f5e227b5caaf3d6fc1147b6382a857187094 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 23:09:10 +0800 Subject: [PATCH 35/79] fix|ruff --- src/heart_flow/background_tasks.py | 2 +- src/heart_flow/sub_mind.py | 11 +++-- src/plugins/chat/message_buffer.py | 32 ++++++++----- src/plugins/heartFC_chat/heartFC_Cycleinfo.py | 32 +++++++------ src/plugins/heartFC_chat/heartFC_chat.py | 45 ++++++++++--------- .../heartFC_chat/heartflow_prompt_builder.py | 9 ++-- src/plugins/person_info/person_info.py | 21 ++++++--- src/plugins/utils/chat_message_builder.py | 8 ++-- tool_call_benchmark.py | 4 +- 9 files changed, 96 insertions(+), 68 deletions(-) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 21254ce78..f5131a59a 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -231,7 +231,7 @@ class BackgroundTaskManager: stopped_count += 1 logger.info(f"[Background Task Cleanup] Cleanup cycle finished. Stopped {stopped_count} inactive flows.") # else: - # logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.") + # logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.") async def _perform_logging_work(self): """执行一轮状态日志记录。""" diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 6b2bdaac3..d8b1f75be 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -41,7 +41,7 @@ def init_prompt(): prompt += "3. 如需处理消息或回复,请使用工具\n" Prompt(prompt, "sub_heartflow_prompt_before") - + prompt = "" prompt += "刚刚你的内心想法是:{current_thinking_info}\n" prompt += "{if_replan_prompt}\n" @@ -131,7 +131,7 @@ class SubMind: ("进行深入思考", 0.2), ] - #上一次决策信息 + # 上一次决策信息 if last_cycle != None: last_action = last_cycle.action_type last_reasoning = last_cycle.reasoning @@ -147,12 +147,11 @@ class SubMind: if_replan_prompt = "" if current_thinking_info: last_loop_prompt = (await global_prompt_manager.get_prompt_async("last_loop")).format( - current_thinking_info=current_thinking_info, - if_replan_prompt=if_replan_prompt + current_thinking_info=current_thinking_info, if_replan_prompt=if_replan_prompt ) else: last_loop_prompt = "" - + # 加权随机选择思考指导 hf_do_next = local_random.choices( [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 @@ -168,7 +167,7 @@ class SubMind: chat_observe_info=chat_observe_info, mood_info=mood_info, hf_do_next=hf_do_next, - last_loop_prompt=last_loop_prompt + last_loop_prompt=last_loop_prompt, ) # logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") diff --git a/src/plugins/chat/message_buffer.py b/src/plugins/chat/message_buffer.py index 2c04fd50e..d76d23289 100644 --- a/src/plugins/chat/message_buffer.py +++ b/src/plugins/chat/message_buffer.py @@ -3,7 +3,7 @@ from src.common.logger import get_module_logger import asyncio from dataclasses import dataclass, field from .message import MessageRecv -from maim_message import BaseMessageInfo, GroupInfo, Seg +from maim_message import BaseMessageInfo, GroupInfo import hashlib from typing import Dict from collections import OrderedDict @@ -128,8 +128,8 @@ class MessageBuffer: if result: async with self.lock: # 再次加锁 # 清理所有早于当前消息的已处理消息, 收集所有早于当前消息的F消息的processed_plain_text - keep_msgs = OrderedDict() # 用于存放 T 消息之后的消息 - collected_texts = [] # 用于收集 T 消息及之前 F 消息的文本 + keep_msgs = OrderedDict() # 用于存放 T 消息之后的消息 + collected_texts = [] # 用于收集 T 消息及之前 F 消息的文本 process_target_found = False # 遍历当前用户的所有缓冲消息 @@ -138,7 +138,10 @@ class MessageBuffer: if msg_id == message.message_info.message_id: process_target_found = True # 收集这条 T 消息的文本 (如果有) - if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + if ( + hasattr(cache_msg.message, "processed_plain_text") + and cache_msg.message.processed_plain_text + ): collected_texts.append(cache_msg.message.processed_plain_text) # 不立即放入 keep_msgs,因为它之前的 F 消息也处理完了 @@ -150,16 +153,23 @@ class MessageBuffer: else: if cache_msg.result == "F": # 收集这条 F 消息的文本 (如果有) - if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + if ( + hasattr(cache_msg.message, "processed_plain_text") + and cache_msg.message.processed_plain_text + ): collected_texts.append(cache_msg.message.processed_plain_text) elif cache_msg.result == "U": # 理论上不应该在 T 消息之前还有 U 消息,记录日志 - logger.warning(f"异常状态:在目标 T 消息 {message.message_info.message_id} 之前发现未处理的 U 消息 {cache_msg.message.message_info.message_id}") + logger.warning( + f"异常状态:在目标 T 消息 {message.message_info.message_id} 之前发现未处理的 U 消息 {cache_msg.message.message_info.message_id}" + ) # 也可以选择收集其文本 - if hasattr(cache_msg.message, "processed_plain_text") and cache_msg.message.processed_plain_text: + if ( + hasattr(cache_msg.message, "processed_plain_text") + and cache_msg.message.processed_plain_text + ): collected_texts.append(cache_msg.message.processed_plain_text) - # 更新当前消息 (message) 的 processed_plain_text # 只有在收集到的文本多于一条,或者只有一条但与原始文本不同时才合并 if collected_texts: @@ -172,9 +182,11 @@ class MessageBuffer: if merged_text and merged_text != message.processed_plain_text: message.processed_plain_text = merged_text # 如果合并了文本,原消息不再视为纯 emoji - if hasattr(message, 'is_emoji'): + if hasattr(message, "is_emoji"): message.is_emoji = False - logger.debug(f"合并了 {len(unique_texts)} 条消息的文本内容到当前消息 {message.message_info.message_id}") + logger.debug( + f"合并了 {len(unique_texts)} 条消息的文本内容到当前消息 {message.message_info.message_id}" + ) # 更新缓冲池,只保留 T 消息之后的消息 self.buffer_pool[person_id_] = keep_msgs diff --git a/src/plugins/heartFC_chat/heartFC_Cycleinfo.py b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py index 030018ddf..966773841 100644 --- a/src/plugins/heartFC_chat/heartFC_Cycleinfo.py +++ b/src/plugins/heartFC_chat/heartFC_Cycleinfo.py @@ -1,8 +1,10 @@ import time from typing import List, Optional, Dict, Any + class CycleInfo: """循环信息记录类""" + def __init__(self, cycle_id: int): self.cycle_id = cycle_id self.start_time = time.time() @@ -13,16 +15,16 @@ class CycleInfo: self.timers: Dict[str, float] = {} self.thinking_id = "" self.replanned = False - + # 添加响应信息相关字段 self.response_info: Dict[str, Any] = { "response_text": [], # 回复的文本列表 - "emoji_info": "", # 表情信息 + "emoji_info": "", # 表情信息 "anchor_message_id": "", # 锚点消息ID "reply_message_ids": [], # 回复消息ID列表 "sub_mind_thinking": "", # 子思维思考内容 } - + def to_dict(self) -> Dict[str, Any]: """将循环信息转换为字典格式""" return { @@ -34,29 +36,31 @@ class CycleInfo: "reasoning": self.reasoning, "timers": self.timers, "thinking_id": self.thinking_id, - "response_info": self.response_info + "response_info": self.response_info, } - + def complete_cycle(self): """完成循环,记录结束时间""" self.end_time = time.time() - + def set_action_info(self, action_type: str, reasoning: str, action_taken: bool): """设置动作信息""" self.action_type = action_type self.reasoning = reasoning self.action_taken = action_taken - + def set_thinking_id(self, thinking_id: str): """设置思考消息ID""" self.thinking_id = thinking_id - def set_response_info(self, - response_text: Optional[List[str]] = None, - emoji_info: Optional[str] = None, - anchor_message_id: Optional[str] = None, - reply_message_ids: Optional[List[str]] = None, - sub_mind_thinking: Optional[str] = None): + def set_response_info( + self, + response_text: Optional[List[str]] = None, + emoji_info: Optional[str] = None, + anchor_message_id: Optional[str] = None, + reply_message_ids: Optional[List[str]] = None, + sub_mind_thinking: Optional[str] = None, + ): """设置响应信息""" if response_text is not None: self.response_info["response_text"] = response_text @@ -67,4 +71,4 @@ class CycleInfo: if reply_message_ids is not None: self.response_info["reply_message_ids"] = reply_message_ids if sub_mind_thinking is not None: - self.response_info["sub_mind_thinking"] = sub_mind_thinking \ No newline at end of file + self.response_info["sub_mind_thinking"] = sub_mind_thinking diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index d57885b9a..ba6be7eb1 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -147,6 +147,7 @@ class SenderError(HeartFCError): pass + class HeartFChatting: """ 管理一个连续的Plan-Replier-Sender循环 @@ -289,12 +290,10 @@ class HeartFChatting: # 记录规划开始时间点 planner_start_db_time = time.time() - + # 主循环:思考->决策->执行 - action_taken, thinking_id = await self._think_plan_execute_loop( - cycle_timers, planner_start_db_time - ) - + action_taken, thinking_id = await self._think_plan_execute_loop(cycle_timers, planner_start_db_time) + # 更新循环信息 self._current_cycle.set_thinking_id(thinking_id) self._current_cycle.timers = cycle_timers @@ -377,16 +376,16 @@ class HeartFChatting: # 记录子思维思考内容 if self._current_cycle: self._current_cycle.set_response_info(sub_mind_thinking=current_mind) - + # plan:决策 with Timer("决策", cycle_timers): planner_result = await self._planner(current_mind, cycle_timers) - + action = planner_result.get("action", "error") reasoning = planner_result.get("reasoning", "未提供理由") - + self._current_cycle.set_action_info(action, reasoning, False) - + # 在获取规划结果后检查新消息 if await self._check_new_messages(planner_start_db_time): if random.random() < 0.3: @@ -407,11 +406,13 @@ class HeartFChatting: if planner_result.get("llm_error"): logger.error(f"{self.log_prefix} LLM失败: {reasoning}") return False, "" - + # execute:执行 with Timer("执行动作", cycle_timers): - return await self._handle_action(action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time) - + return await self._handle_action( + action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time + ) + except PlannerError as e: logger.error(f"{self.log_prefix} 规划错误: {e}") # 更新循环信息 @@ -505,7 +506,7 @@ class HeartFChatting: response_set=reply, send_emoji=emoji_query, ) - + return True, thinking_id except (ReplierError, SenderError) as e: @@ -645,9 +646,7 @@ class HeartFChatting: with Timer("思考", cycle_timers): # 获取上一个循环的动作 # 传递上一个循环的信息给 do_thinking_before_reply - current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply( - last_cycle=last_cycle - ) + current_mind, _past_mind = await self.sub_mind.do_thinking_before_reply(last_cycle=last_cycle) return current_mind except Exception as e: logger.error(f"{self.log_prefix}[SubMind] 思考失败: {e}") @@ -854,19 +853,21 @@ class HeartFChatting: logger.warning(f"{self.log_prefix} 已释放处理锁") logger.info(f"{self.log_prefix} HeartFChatting关闭完成") - - async def _build_replan_prompt( - self, action: str, reasoning: str - ) -> str: + + async def _build_replan_prompt(self, action: str, reasoning: str) -> str: """构建 Replanner LLM 的提示词""" prompt = (await global_prompt_manager.get_prompt_async("replan_prompt")).format( action=action, reasoning=reasoning, ) return prompt - + async def _build_planner_prompt( - self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any], replan_prompt: str + self, + observed_messages_str: str, + current_mind: Optional[str], + structured_info: Dict[str, Any], + replan_prompt: str, ) -> str: """构建 Planner LLM 的提示词""" diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index eddc5c5ac..661c4e8af 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -88,9 +88,12 @@ def init_prompt(): "planner_prompt", ) - Prompt('''你原本打算{action},因为:{reasoning} -但是你看到了新的消息,你决定重新决定行动。''', "replan_prompt") - + Prompt( + """你原本打算{action},因为:{reasoning} +但是你看到了新的消息,你决定重新决定行动。""", + "replan_prompt", + ) + Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index f5ec6d8f3..1ec9f6d06 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -387,7 +387,7 @@ class PersonInfoManager: time_interval = [t for t in time_interval if 200 <= t <= 8000] # --- 修改后的逻辑 --- # 数据量检查 (至少需要 30 条有效间隔,并且足够进行头尾截断) - if len(time_interval) >= 30 + 10: # 至少30条有效+头尾各5条 + if len(time_interval) >= 30 + 10: # 至少30条有效+头尾各5条 time_interval.sort() # 画图(log) - 这部分保留 @@ -397,8 +397,17 @@ class PersonInfoManager: plt.figure(figsize=(10, 6)) # 使用截断前的数据画图,更能反映原始分布 time_series_original = pd.Series(time_interval) - plt.hist(time_series_original, bins=50, density=True, alpha=0.4, color="pink", label="Histogram (Original Filtered)") - time_series_original.plot(kind="kde", color="mediumpurple", linewidth=1, label="Density (Original Filtered)") + plt.hist( + time_series_original, + bins=50, + density=True, + alpha=0.4, + color="pink", + label="Histogram (Original Filtered)", + ) + time_series_original.plot( + kind="kde", color="mediumpurple", linewidth=1, label="Density (Original Filtered)" + ) plt.grid(True, alpha=0.2) plt.xlim(0, 8000) plt.title(f"Message Interval Distribution (User: {person_id[:8]}...)") @@ -414,7 +423,7 @@ class PersonInfoManager: trimmed_interval = time_interval[5:-5] # 计算截断后数据的 37% 分位数 - if trimmed_interval: # 确保截断后列表不为空 + if trimmed_interval: # 确保截断后列表不为空 msg_interval = int(round(np.percentile(trimmed_interval, 37))) # 更新数据库 await self.update_one_field(person_id, "msg_interval", msg_interval) @@ -422,7 +431,9 @@ class PersonInfoManager: else: logger.trace(f"用户{person_id}截断后数据为空,无法计算msg_interval") else: - logger.trace(f"用户{person_id}有效消息间隔数量 ({len(time_interval)}) 不足进行推断 (需要至少 {30+10} 条)") + logger.trace( + f"用户{person_id}有效消息间隔数量 ({len(time_interval)}) 不足进行推断 (需要至少 {30 + 10} 条)" + ) # --- 修改结束 --- except Exception as e: logger.trace(f"用户{person_id}消息间隔计算失败: {type(e).__name__}: {str(e)}") diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index 8ba49d9cd..5d9494488 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -168,10 +168,10 @@ async def _build_readable_messages_internal( user_info = msg.get("user_info", {}) platform = user_info.get("platform") user_id = user_info.get("user_id") - + user_nickname = user_info.get("user_nickname") user_cardname = user_info.get("user_cardname") - + timestamp = msg.get("time") content = msg.get("processed_plain_text", "") # 默认空字符串 @@ -311,9 +311,7 @@ async def build_readable_messages( ) readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode) - read_mark_line = ( - f"\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" - ) + read_mark_line = f"\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" # 组合结果,确保空部分不引入多余的标记或换行 if formatted_before and formatted_after: diff --git a/tool_call_benchmark.py b/tool_call_benchmark.py index 7ef00c7c3..a3e282734 100644 --- a/tool_call_benchmark.py +++ b/tool_call_benchmark.py @@ -212,7 +212,7 @@ async def run_alternating_tests(iterations=5): 包含两种测试方法结果的元组 """ print(f"开始交替测试(每种方法{iterations}次)...") - + # 初始化结果列表 times_without_tools = [] times_with_tools = [] @@ -221,7 +221,7 @@ async def run_alternating_tests(iterations=5): for i in range(iterations): print(f"\n第 {i + 1}/{iterations} 轮交替测试") - + # 不使用工具的测试 print("\n 执行不使用工具调用的测试...") start_time = time.time() From 2c8343b23a411227a0c763027567aa292e357a09 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 23:45:59 +0800 Subject: [PATCH 36/79] =?UTF-8?q?better=EF=BC=9A=E6=9B=B4=E7=AE=80?= =?UTF-8?q?=E6=B4=81=E7=9A=84=E5=8F=91=E9=80=81=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_chat.py | 175 +++++++++++---------- src/plugins/heartFC_chat/heartFC_sender.py | 161 +++++++++++++++++++ 2 files changed, 256 insertions(+), 80 deletions(-) create mode 100644 src/plugins/heartFC_chat/heartFC_sender.py diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index ba6be7eb1..b9c6209ca 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -16,7 +16,6 @@ from src.plugins.chat.utils_image import image_path_to_base64 # Local import ne from src.plugins.utils.timer_calculator import Timer # <--- Import Timer from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator from src.do_tool.tool_use import ToolUser -from ..chat.message_sender import message_manager # <-- Import the global manager from src.plugins.emoji_system.emoji_manager import emoji_manager from src.plugins.utils.json_utils import process_llm_tool_response # 导入新的JSON工具 from src.heart_flow.sub_mind import SubMind @@ -25,6 +24,7 @@ from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_mana import contextlib from src.plugins.utils.chat_message_builder import num_new_messages_since from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo +from .heartFC_sender import HeartFCSender # --- End import --- @@ -181,6 +181,7 @@ class HeartFChatting: # 依赖注入存储 self.gpt_instance = HeartFCGenerator() # 文本回复生成器 self.tool_user = ToolUser() # 工具使用实例 + self.heart_fc_sender = HeartFCSender() # LLM规划器配置 self.planner_llm = LLMRequest( @@ -301,11 +302,6 @@ class HeartFChatting: # 防止循环过快消耗资源 await self._handle_cycle_delay(action_taken, loop_cycle_start_time, self.log_prefix) - # 等待直到所有消息都发送完成 - with Timer("发送消息", cycle_timers): - while await self._should_skip_cycle(thinking_id): - await asyncio.sleep(0.2) - # 完成当前循环并保存历史 self._current_cycle.complete_cycle() self._cycle_history.append(self._current_cycle) @@ -593,10 +589,6 @@ class HeartFChatting: await asyncio.sleep(1.5) - async def _should_skip_cycle(self, thinking_id: str) -> bool: - """检查是否应该跳过当前循环周期""" - return message_manager.check_if_sending_message_exist(self.stream_id, thinking_id) - async def _log_cycle_timers(self, cycle_timers: dict, log_prefix: str): """记录循环周期的计时器结果""" if cycle_timers: @@ -806,26 +798,40 @@ class HeartFChatting: send_emoji: str, # Emoji query decided by planner or tools ): """ - 发送器 (Sender): 使用本类的方法发送生成的回复。 + 发送器 (Sender): 使用 HeartFCSender 实例发送生成的回复。 处理相关的操作,如发送表情和更新关系。 """ - logger.info(f"{self.log_prefix}开始发送回复") + logger.info(f"{self.log_prefix}开始发送回复 (使用 HeartFCSender)") first_bot_msg: Optional[MessageSending] = None - # 尝试发送回复消息 - first_bot_msg = await self._send_response_messages(anchor_message, response_set, thinking_id) - if first_bot_msg: - # --- 处理关联表情(如果指定) --- # - if send_emoji: - logger.info(f"{self.log_prefix}正在发送关联表情: '{send_emoji}'") - # 优先使用first_bot_msg作为锚点,否则回退到原始锚点 - emoji_anchor = first_bot_msg if first_bot_msg else anchor_message - await self._handle_emoji(emoji_anchor, response_set, send_emoji) + try: + # _send_response_messages 现在将使用 self.sender 内部处理注册和发送 + # 它需要负责创建 MessageThinking 和 MessageSending 对象 + # 并调用 self.sender.register_thinking 和 self.sender.type_and_send_message + first_bot_msg = await self._send_response_messages( + anchor_message=anchor_message, + response_set=response_set, + thinking_id=thinking_id + ) - else: - # logger.warning(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复失败(_send_response_messages返回None)。思考消息{thinking_id}可能已被移除。") - # 无需清理,因为_send_response_messages返回None意味着已处理/已删除 - raise RuntimeError("发送回复失败,_send_response_messages返回None") + if first_bot_msg: + # --- 处理关联表情(如果指定) --- # + if send_emoji: + logger.info(f"{self.log_prefix}正在发送关联表情: '{send_emoji}'") + # 优先使用 first_bot_msg 作为锚点,否则回退到原始锚点 + emoji_anchor = first_bot_msg + await self._handle_emoji(emoji_anchor, response_set, send_emoji) + else: + # 如果 _send_response_messages 返回 None,表示在发送前就失败或没有消息可发送 + logger.warning(f"{self.log_prefix}[Sender-{thinking_id}] 未能发送任何回复消息 (_send_response_messages 返回 None)。") + # 这里可能不需要抛出异常,取决于 _send_response_messages 的具体实现 + + except Exception as e: + # 异常现在由 type_and_send_message 内部处理日志,这里只记录发送流程失败 + logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复过程中遇到错误: {e}") + # 思考状态应已在 type_and_send_message 的 finally 块中清理 + # 可以选择重新抛出或根据业务逻辑处理 + # raise RuntimeError(f"发送回复失败: {e}") from e async def shutdown(self): """优雅关闭HeartFChatting实例,取消活动循环任务""" @@ -959,99 +965,103 @@ class HeartFChatting: thinking_start_time=thinking_time_point, ) # Access MessageManager directly - await message_manager.add_message(thinking_message) + await self.heart_fc_sender.register_thinking(thinking_message) return thinking_id async def _send_response_messages( self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str ) -> Optional[MessageSending]: - """发送回复消息 (尝试锚定到 anchor_message)""" + """发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender""" if not anchor_message or not anchor_message.chat_stream: logger.error(f"{self.log_prefix} 无法发送回复,缺少有效的锚点消息或聊天流。") return None - # 记录锚点消息ID - if self._current_cycle and anchor_message: - self._current_cycle.set_response_info( - response_text=response_set, anchor_message_id=anchor_message.message_info.message_id - ) - chat = anchor_message.chat_stream - container = await message_manager.get_container(chat.stream_id) - thinking_message = None + chat_id = chat.stream_id + stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志 - # 移除思考消息 - for msg in container.messages[:]: # Iterate over a copy - if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id: - thinking_message = msg - container.messages.remove(msg) # Remove the message directly here - # logger.debug(f"{self.log_prefix} Removed thinking message {thinking_id} via iteration.") - break + # 检查思考过程是否仍在进行,并获取开始时间 + thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id) - if not thinking_message: - stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id # 获取流名称 - logger.warning(f"[{stream_name}] {thinking_id},思考太久了,超时被移除") + if thinking_start_time is None: + logger.warning(f"[{stream_name}] {thinking_id} 思考过程未找到或已结束,无法发送回复。") return None - thinking_start_time = thinking_message.thinking_start_time - message_set = MessageSet(chat, thinking_id) + # 记录锚点消息ID和回复文本(在发送前记录) + self._current_cycle.set_response_info( + response_text=response_set, anchor_message_id=anchor_message.message_info.message_id + ) + mark_head = False - first_bot_msg = None - reply_message_ids = [] # 用于记录所有回复消息的ID + first_bot_msg: Optional[MessageSending] = None + reply_message_ids = [] # 记录实际发送的消息ID bot_user_info = UserInfo( user_id=global_config.BOT_QQ, user_nickname=global_config.BOT_NICKNAME, platform=anchor_message.message_info.platform, ) - for msg_text in response_set: + + for i, msg_text in enumerate(response_set): + # 为每个消息片段生成唯一ID + part_message_id = f"{thinking_id}_{i}" message_segment = Seg(type="text", data=msg_text) bot_message = MessageSending( - message_id=thinking_id, # 使用 thinking_id 作为批次标识 + message_id=part_message_id, # 使用片段的唯一ID chat_stream=chat, bot_user_info=bot_user_info, - sender_info=anchor_message.message_info.user_info, # 发送给锚点消息的用户 + sender_info=anchor_message.message_info.user_info, message_segment=message_segment, - reply=anchor_message, # 回复锚点消息 + reply=anchor_message, # 回复原始锚点 is_head=not mark_head, is_emoji=False, - thinking_start_time=thinking_start_time, + thinking_start_time=thinking_start_time, # 传递原始思考开始时间 ) - if not mark_head: - mark_head = True - first_bot_msg = bot_message - message_set.add_message(bot_message) - reply_message_ids.append(bot_message.message_info.message_id) + try: - # 记录回复消息ID列表 - if self._current_cycle: - self._current_cycle.set_response_info(reply_message_ids=reply_message_ids) + if not mark_head: + mark_head = True + first_bot_msg = bot_message # 保存第一个成功发送的消息对象 + await self.heart_fc_sender.type_and_send_message(bot_message, type = False) + else: + await self.heart_fc_sender.type_and_send_message(bot_message, type = True) - # Access MessageManager directly - await message_manager.add_message(message_set) - return first_bot_msg + reply_message_ids.append(part_message_id) # 记录我们生成的ID + + except Exception as e: + logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}") + # 这里可以选择是继续发送下一个片段还是中止 + + # 在尝试发送完所有片段后,完成原始的 thinking_id 状态 + try: + await self.heart_fc_sender.complete_thinking(chat_id, thinking_id) + except Exception as e: + logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}") + + self._current_cycle.set_response_info( + response_text=response_set, # 保留原始文本 + anchor_message_id=anchor_message.message_info.message_id, # 保留锚点ID + reply_message_ids=reply_message_ids # 添加实际发送的ID列表 + ) + + + return first_bot_msg # 返回第一个成功发送的消息对象 async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""): - """处理表情包 (尝试锚定到 anchor_message)""" + """处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender""" if not anchor_message or not anchor_message.chat_stream: logger.error(f"{self.log_prefix} 无法处理表情包,缺少有效的锚点消息或聊天流。") return chat = anchor_message.chat_stream - if send_emoji: - emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) - else: - emoji_text_source = "".join(response_set) if response_set else "" - emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source) + emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji) if emoji_raw: emoji_path, description = emoji_raw - # 记录表情信息 - if self._current_cycle: - self._current_cycle.set_response_info(emoji_info=f"表情: {description}, 路径: {emoji_path}") + emoji_cq = image_path_to_base64(emoji_path) - thinking_time_point = round(time.time(), 2) + thinking_time_point = round(time.time(), 2) # 用于唯一ID message_segment = Seg(type="emoji", data=emoji_cq) bot_user_info = UserInfo( user_id=global_config.BOT_QQ, @@ -1059,17 +1069,22 @@ class HeartFChatting: platform=anchor_message.message_info.platform, ) bot_message = MessageSending( - message_id="me" + str(thinking_time_point), # 使用不同的 ID 前缀? + message_id="me" + str(thinking_time_point), # 表情消息的唯一ID chat_stream=chat, bot_user_info=bot_user_info, sender_info=anchor_message.message_info.user_info, message_segment=message_segment, - reply=anchor_message, # 回复锚点消息 - is_head=False, + reply=anchor_message, # 回复原始锚点 + is_head=False, # 表情通常不是头部消息 is_emoji=True, + # 不需要 thinking_start_time ) - # Access MessageManager directly - await message_manager.add_message(bot_message) + + try: + await self.heart_fc_sender.send_and_store(bot_message) + except Exception as e: + logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}") + def get_cycle_history(self, last_n: Optional[int] = None) -> List[Dict[str, Any]]: """获取循环历史记录 diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py new file mode 100644 index 000000000..000496dd9 --- /dev/null +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -0,0 +1,161 @@ +# src/plugins/heartFC_chat/heartFC_sender.py +import asyncio # 重新导入 asyncio +import time +from typing import Dict, List, Optional, Union # 重新导入类型 + +from src.common.logger import get_module_logger +from ..message.api import global_api +from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking +from ..storage.storage import MessageStorage +from ..chat.utils import truncate_message +from src.common.logger import LogConfig, SENDER_STYLE_CONFIG +from src.plugins.chat.utils import calculate_typing_time + +# 定义日志配置 +sender_config = LogConfig( + # 使用消息发送专用样式 + console_format=SENDER_STYLE_CONFIG["console_format"], + file_format=SENDER_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("msg_sender", config=sender_config) + + +class HeartFCSender: + """管理消息的注册、即时处理、发送和存储,并跟踪思考状态。""" + + def __init__(self): + self.storage = MessageStorage() + # 用于存储活跃的思考消息 + self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {} + self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁 + + async def send_message(self, message: MessageSending) -> None: + """合并后的消息发送函数,包含WS发送和日志记录""" + message_preview = truncate_message(message.processed_plain_text) + + try: + # 直接调用API发送消息 + await global_api.send_message(message) + logger.success(f"发送消息 '{message_preview}' 成功") + + except Exception as e: + logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") + if not message.message_info.platform: + raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e + raise e # 重新抛出其他异常 + + async def register_thinking(self, thinking_message: MessageThinking): + """注册一个思考中的消息。""" + if not thinking_message.chat_stream or not thinking_message.message_info.message_id: + logger.error("无法注册缺少 chat_stream 或 message_id 的思考消息") + return + + chat_id = thinking_message.chat_stream.stream_id + message_id = thinking_message.message_info.message_id + + async with self._thinking_lock: + if chat_id not in self.thinking_messages: + self.thinking_messages[chat_id] = {} + if message_id in self.thinking_messages[chat_id]: + logger.warning(f"[{chat_id}] 尝试注册已存在的思考消息 ID: {message_id}") + self.thinking_messages[chat_id][message_id] = thinking_message + logger.debug(f"[{chat_id}] Registered thinking message: {message_id}") + + async def complete_thinking(self, chat_id: str, message_id: str): + """完成并移除一个思考中的消息记录。""" + async with self._thinking_lock: + if chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id]: + del self.thinking_messages[chat_id][message_id] + logger.debug(f"[{chat_id}] Completed thinking message: {message_id}") + if not self.thinking_messages[chat_id]: + del self.thinking_messages[chat_id] + logger.debug(f"[{chat_id}] Removed empty thinking message container.") + + def is_thinking(self, chat_id: str, message_id: str) -> bool: + """检查指定的消息 ID 是否当前正处于思考状态。""" + return chat_id in self.thinking_messages and message_id in self.thinking_messages[chat_id] + + async def get_thinking_start_time(self, chat_id: str, message_id: str) -> Optional[float]: + """获取已注册思考消息的开始时间。""" + async with self._thinking_lock: + thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id) + return thinking_message.thinking_start_time if thinking_message else None + + async def type_and_send_message(self, message: MessageSending, type = False): + """ + 立即处理、发送并存储单个 MessageSending 消息。 + 调用此方法前,应先调用 register_thinking 注册对应的思考消息。 + 此方法执行后会调用 complete_thinking 清理思考状态。 + """ + if not message.chat_stream: + logger.error("消息缺少 chat_stream,无法发送") + return + if not message.message_info or not message.message_info.message_id: + logger.error("消息缺少 message_info 或 message_id,无法发送") + return + + chat_id = message.chat_stream.stream_id + message_id = message.message_info.message_id + + try: + _ = message.update_thinking_time() + + # --- 条件应用 set_reply 逻辑 --- + if ( + message.apply_set_reply_logic + and message.is_head + and not message.is_private_message() + ): + logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...") + message.set_reply() + # --- 结束条件 set_reply --- + + await message.process() + + if type: + typing_time = calculate_typing_time( + input_string=message.processed_plain_text, + thinking_start_time=message.thinking_start_time, + is_emoji=message.is_emoji, + ) + await asyncio.sleep(typing_time) + + + await self.send_message(message) + await self.storage.store_message(message, message.chat_stream) + + except Exception as e: + logger.error( + f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}" + ) + raise e + finally: + await self.complete_thinking(chat_id, message_id) + + async def send_and_store(self, message: MessageSending): + """处理、发送并存储单个消息,不涉及思考状态管理。""" + if not message.chat_stream: + logger.error(f"[{message.message_info.platform or 'UnknownPlatform'}] 消息缺少 chat_stream,无法发送") + return + if not message.message_info or not message.message_info.message_id: + logger.error(f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id,无法发送") + return + + chat_id = message.chat_stream.stream_id + message_id = message.message_info.message_id # 获取消息ID用于日志 + + try: + await message.process() + + await asyncio.sleep(0.5) + + await self.send_message(message) # 使用现有的发送方法 + await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法 + + except Exception as e: + logger.error( + f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}" + ) + # 重新抛出异常,让调用者知道失败了 + raise e From 7b197ed0a7db437ed89ab0f05942929c92976dbc Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 23:46:49 +0800 Subject: [PATCH 37/79] Update heartFC_chat.py --- src/plugins/heartFC_chat/heartFC_chat.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index b9c6209ca..772941c4c 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -496,12 +496,13 @@ class HeartFChatting: # 发送消息 - await self._sender( - thinking_id=thinking_id, - anchor_message=anchor_message, - response_set=reply, - send_emoji=emoji_query, - ) + with Timer("发送消息", cycle_timers): + await self._sender( + thinking_id=thinking_id, + anchor_message=anchor_message, + response_set=reply, + send_emoji=emoji_query, + ) return True, thinking_id From 9a81979f67065ec4aa3e61c21fd0a41eedc9bb70 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Fri, 25 Apr 2025 23:47:19 +0800 Subject: [PATCH 38/79] rafafawfa --- src/plugins/heartFC_chat/heartFC_chat.py | 42 +++++++++--------- src/plugins/heartFC_chat/heartFC_sender.py | 50 +++++++++------------- 2 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 772941c4c..7a5298997 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -5,7 +5,7 @@ import random # <-- 添加导入 from typing import List, Optional, Dict, Any, Deque from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending -from src.plugins.chat.message import MessageSet, Seg # Local import needed after move +from src.plugins.chat.message import Seg # Local import needed after move from src.plugins.chat.chat_stream import ChatStream from src.plugins.chat.message import UserInfo from src.plugins.chat.chat_stream import chat_manager @@ -810,9 +810,7 @@ class HeartFChatting: # 它需要负责创建 MessageThinking 和 MessageSending 对象 # 并调用 self.sender.register_thinking 和 self.sender.type_and_send_message first_bot_msg = await self._send_response_messages( - anchor_message=anchor_message, - response_set=response_set, - thinking_id=thinking_id + anchor_message=anchor_message, response_set=response_set, thinking_id=thinking_id ) if first_bot_msg: @@ -824,7 +822,9 @@ class HeartFChatting: await self._handle_emoji(emoji_anchor, response_set, send_emoji) else: # 如果 _send_response_messages 返回 None,表示在发送前就失败或没有消息可发送 - logger.warning(f"{self.log_prefix}[Sender-{thinking_id}] 未能发送任何回复消息 (_send_response_messages 返回 None)。") + logger.warning( + f"{self.log_prefix}[Sender-{thinking_id}] 未能发送任何回复消息 (_send_response_messages 返回 None)。" + ) # 这里可能不需要抛出异常,取决于 _send_response_messages 的具体实现 except Exception as e: @@ -979,7 +979,7 @@ class HeartFChatting: chat = anchor_message.chat_stream chat_id = chat.stream_id - stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志 + stream_name = chat_manager.get_stream_name(chat_id) or chat_id # 获取流名称用于日志 # 检查思考过程是否仍在进行,并获取开始时间 thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id) @@ -1015,21 +1015,22 @@ class HeartFChatting: reply=anchor_message, # 回复原始锚点 is_head=not mark_head, is_emoji=False, - thinking_start_time=thinking_start_time, # 传递原始思考开始时间 + thinking_start_time=thinking_start_time, # 传递原始思考开始时间 ) try: - if not mark_head: mark_head = True - first_bot_msg = bot_message # 保存第一个成功发送的消息对象 - await self.heart_fc_sender.type_and_send_message(bot_message, type = False) + first_bot_msg = bot_message # 保存第一个成功发送的消息对象 + await self.heart_fc_sender.type_and_send_message(bot_message, type=False) else: - await self.heart_fc_sender.type_and_send_message(bot_message, type = True) + await self.heart_fc_sender.type_and_send_message(bot_message, type=True) - reply_message_ids.append(part_message_id) # 记录我们生成的ID + reply_message_ids.append(part_message_id) # 记录我们生成的ID except Exception as e: - logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}") + logger.error( + f"{self.log_prefix}[Sender-{thinking_id}] 发送回复片段 {i} ({part_message_id}) 时失败: {e}" + ) # 这里可以选择是继续发送下一个片段还是中止 # 在尝试发送完所有片段后,完成原始的 thinking_id 状态 @@ -1039,13 +1040,12 @@ class HeartFChatting: logger.error(f"{self.log_prefix}[Sender-{thinking_id}] 完成思考状态 {thinking_id} 时出错: {e}") self._current_cycle.set_response_info( - response_text=response_set, # 保留原始文本 - anchor_message_id=anchor_message.message_info.message_id, # 保留锚点ID - reply_message_ids=reply_message_ids # 添加实际发送的ID列表 + response_text=response_set, # 保留原始文本 + anchor_message_id=anchor_message.message_info.message_id, # 保留锚点ID + reply_message_ids=reply_message_ids, # 添加实际发送的ID列表 ) - - return first_bot_msg # 返回第一个成功发送的消息对象 + return first_bot_msg # 返回第一个成功发送的消息对象 async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""): """处理表情包 (尝试锚定到 anchor_message),使用 HeartFCSender""" @@ -1060,9 +1060,8 @@ class HeartFChatting: if emoji_raw: emoji_path, description = emoji_raw - emoji_cq = image_path_to_base64(emoji_path) - thinking_time_point = round(time.time(), 2) # 用于唯一ID + thinking_time_point = round(time.time(), 2) # 用于唯一ID message_segment = Seg(type="emoji", data=emoji_cq) bot_user_info = UserInfo( user_id=global_config.BOT_QQ, @@ -1076,7 +1075,7 @@ class HeartFChatting: sender_info=anchor_message.message_info.user_info, message_segment=message_segment, reply=anchor_message, # 回复原始锚点 - is_head=False, # 表情通常不是头部消息 + is_head=False, # 表情通常不是头部消息 is_emoji=True, # 不需要 thinking_start_time ) @@ -1086,7 +1085,6 @@ class HeartFChatting: except Exception as e: logger.error(f"{self.log_prefix} 发送表情包 {bot_message.message_info.message_id} 时失败: {e}") - def get_cycle_history(self, last_n: Optional[int] = None) -> List[Dict[str, Any]]: """获取循环历史记录 diff --git a/src/plugins/heartFC_chat/heartFC_sender.py b/src/plugins/heartFC_chat/heartFC_sender.py index 000496dd9..d436c668a 100644 --- a/src/plugins/heartFC_chat/heartFC_sender.py +++ b/src/plugins/heartFC_chat/heartFC_sender.py @@ -1,13 +1,12 @@ # src/plugins/heartFC_chat/heartFC_sender.py -import asyncio # 重新导入 asyncio -import time -from typing import Dict, List, Optional, Union # 重新导入类型 +import asyncio # 重新导入 asyncio +from typing import Dict, Optional # 重新导入类型 from src.common.logger import get_module_logger from ..message.api import global_api -from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking +from ..chat.message import MessageSending, MessageThinking # 只保留 MessageSending 和 MessageThinking from ..storage.storage import MessageStorage -from ..chat.utils import truncate_message +from ..chat.utils import truncate_message from src.common.logger import LogConfig, SENDER_STYLE_CONFIG from src.plugins.chat.utils import calculate_typing_time @@ -28,17 +27,17 @@ class HeartFCSender: self.storage = MessageStorage() # 用于存储活跃的思考消息 self.thinking_messages: Dict[str, Dict[str, MessageThinking]] = {} - self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁 + self._thinking_lock = asyncio.Lock() # 保护 thinking_messages 的锁 async def send_message(self, message: MessageSending) -> None: """合并后的消息发送函数,包含WS发送和日志记录""" message_preview = truncate_message(message.processed_plain_text) - + try: # 直接调用API发送消息 await global_api.send_message(message) logger.success(f"发送消息 '{message_preview}' 成功") - + except Exception as e: logger.error(f"发送消息 '{message_preview}' 失败: {str(e)}") if not message.message_info.platform: @@ -82,7 +81,7 @@ class HeartFCSender: thinking_message = self.thinking_messages.get(chat_id, {}).get(message_id) return thinking_message.thinking_start_time if thinking_message else None - async def type_and_send_message(self, message: MessageSending, type = False): + async def type_and_send_message(self, message: MessageSending, type=False): """ 立即处理、发送并存储单个 MessageSending 消息。 调用此方法前,应先调用 register_thinking 注册对应的思考消息。 @@ -102,17 +101,13 @@ class HeartFCSender: _ = message.update_thinking_time() # --- 条件应用 set_reply 逻辑 --- - if ( - message.apply_set_reply_logic - and message.is_head - and not message.is_private_message() - ): + if message.apply_set_reply_logic and message.is_head and not message.is_private_message(): logger.debug(f"[{chat_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}...") message.set_reply() # --- 结束条件 set_reply --- await message.process() - + if type: typing_time = calculate_typing_time( input_string=message.processed_plain_text, @@ -120,15 +115,12 @@ class HeartFCSender: is_emoji=message.is_emoji, ) await asyncio.sleep(typing_time) - - + await self.send_message(message) await self.storage.store_message(message, message.chat_stream) except Exception as e: - logger.error( - f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}" - ) + logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") raise e finally: await self.complete_thinking(chat_id, message_id) @@ -139,23 +131,23 @@ class HeartFCSender: logger.error(f"[{message.message_info.platform or 'UnknownPlatform'}] 消息缺少 chat_stream,无法发送") return if not message.message_info or not message.message_info.message_id: - logger.error(f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id,无法发送") + logger.error( + f"[{message.chat_stream.stream_id if message.chat_stream else 'UnknownStream'}] 消息缺少 message_info 或 message_id,无法发送" + ) return chat_id = message.chat_stream.stream_id - message_id = message.message_info.message_id # 获取消息ID用于日志 + message_id = message.message_info.message_id # 获取消息ID用于日志 try: await message.process() - + await asyncio.sleep(0.5) - - await self.send_message(message) # 使用现有的发送方法 - await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法 + + await self.send_message(message) # 使用现有的发送方法 + await self.storage.store_message(message, message.chat_stream) # 使用现有的存储方法 except Exception as e: - logger.error( - f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}" - ) + logger.error(f"[{chat_id}] 处理或存储消息 {message_id} 时出错: {e}") # 重新抛出异常,让调用者知道失败了 raise e From 8652ceb13e455ad414798bb55f013c27f2169b40 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 00:46:50 +0800 Subject: [PATCH 39/79] =?UTF-8?q?better=EF=BC=9A=E6=94=B9=E8=BF=9Bprompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_mind.py | 2 +- src/plugins/heartFC_chat/heartFC_chat.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index d8b1f75be..b176943a2 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -27,7 +27,7 @@ def init_prompt(): prompt += "{last_loop_prompt}\n" prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" - prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,思考你要不要回复。" + prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。" prompt += "思考并输出你的内心想法\n" prompt += "输出要求:\n" prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n" diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 7a5298997..bd4da95a7 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -584,8 +584,8 @@ class HeartFChatting: logger.info(f"{log_prefix} 检测到新消息") return True - if time.monotonic() - wait_start_time > 60: - logger.warning(f"{log_prefix} 等待超时(60秒)") + if time.monotonic() - wait_start_time > 300: + logger.warning(f"{log_prefix} 等待超时(300秒)") return False await asyncio.sleep(1.5) From 5f5e7224979ec5d9d19cb4e204dc7b87f7045cbe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 25 Apr 2025 16:47:03 +0000 Subject: [PATCH 40/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_mind.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index b176943a2..be995b843 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -27,7 +27,9 @@ def init_prompt(): prompt += "{last_loop_prompt}\n" prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" - prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。" + prompt += ( + "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。" + ) prompt += "思考并输出你的内心想法\n" prompt += "输出要求:\n" prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n" From 27098321d52aee61bb1204ccc68ab500f03552e7 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 01:46:30 +0800 Subject: [PATCH 41/79] =?UTF-8?q?doc=EF=BC=9A=E6=8F=90=E4=BA=A4=E5=BF=83?= =?UTF-8?q?=E6=B5=81=E7=9A=84readme(=E9=83=A8=E5=88=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/README.md | 276 ++++++++++++++++++++------------------- 1 file changed, 143 insertions(+), 133 deletions(-) diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index dc00a9ff9..db757c568 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -1,157 +1,167 @@ # 心流系统 (Heart Flow System) -## 系统架构 +*在此处简要介绍心流系统的目标和作用* -### 1. 主心流 (Heartflow) -- 位于 `heartflow.py` -- 作为整个系统的主控制器 -- 负责管理和协调多个子心流 -- 维护AI的整体思维状态 -- 定期进行全局思考更新 +## 1. 系统架构 (System Architecture) -### 2. 子心流 (SubHeartflow) -- 位于 `sub_heartflow.py` -- 处理具体的对话场景(如群聊) -- 维护特定场景下的思维状态 -- 通过观察者模式接收和处理信息 -- 能够进行独立的思考和回复判断 +### 1.1. 主心流 (Heartflow) +- **文件**: `heartflow.py` +- **职责**: + - 作为整个系统的主控制器。 + - 持有并管理 `SubHeartflowManager`,用于管理所有子心流。 + - 持有并管理自身状态 `self.current_state: MaiStateInfo`,该状态控制系统的整体行为模式。 + - 统筹管理系统后台任务(如消息存储、资源分配等)。 + - **注意**: 主心流自身不进行周期性的全局思考更新。 -### 3. 观察系统 (Observation) -- 位于 `observation.py` -- 负责收集和处理外部信息 -- 支持多种观察类型(如聊天观察) -- 对信息进行实时总结和更新 +### 1.2. 子心流 (SubHeartflow) +- **文件**: `sub_heartflow.py` +- **职责**: + - 处理具体的交互场景,例如:群聊、私聊、与虚拟主播(vtb)互动、桌面宠物交互等。 + - 维护特定场景下的思维状态和聊天流状态 (`ChatState`)。 + - 通过关联的 `Observation` 实例接收和处理信息。 + - 拥有独立的思考 (`SubMind`) 和回复判断能力。 +- **观察者**: 每个子心流可以拥有一个或多个 `Observation` 实例(目前每个子心流仅使用一个 `ChattingObservation`)。 +- **内部结构**: + - **聊天流状态 (`ChatState`)**: 标记当前子心流的参与模式 (`ABSENT`, `CHAT`, `FOCUSED`),决定是否观察、回复以及使用何种回复模式。 + - **聊天实例 (`NormalChatInstance` / `HeartFlowChatInstance`)**: 根据 `ChatState` 激活对应的实例来处理聊天逻辑。同一时间只有一个实例处于活动状态。 +### 1.3. 观察系统 (Observation) +- **文件**: `observation.py` +- **职责**: + - 定义信息输入的来源和格式。 + - 为子心流提供其所处环境的信息。 +- **当前实现**: + - 目前仅有 `ChattingObservation` 一种观察类型。 + - `ChattingObservation` 负责从数据库拉取指定聊天的最新消息,并将其格式化为可读内容,供 `SubHeartflow` 使用。 -## 工作流程 +### 1.4. 子心流管理器 (SubHeartflowManager) +- **文件**: `subheartflow_manager.py` +- **职责**: + - 作为 `Heartflow` 的成员变量存在。 + - 负责所有 `SubHeartflow` 实例的生命周期管理,包括: + - 创建和获取 (`create_or_get_subheartflow`)。 + - 停止和清理 (`stop_subheartflow`, `cleanup_inactive_subheartflows`)。 + - 根据 `Heartflow` 的状态和限制条件,激活、停用或调整子心流的状态。 -1. 主心流启动并创建必要的子心流 -2. 子心流通过观察者接收外部信息 -3. 系统进行信息处理和思维更新 -4. 根据情感状态和思维结果决定是否回复 -5. 生成合适的回复并更新思维状态 +### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow) +- **关注点分离**: 系统严格区分了接收和处理传入消息的流程与决定和生成回复的流程。 + - **消息处理 (Processing)**: + - 由一个独立的处理器(例如 `HeartFCProcessor`)负责接收原始消息数据。 + - 职责包括:消息解析 (`MessageRecv`)、过滤(屏蔽词、正则表达式)、基于记忆系统的初步兴趣计算 (`HippocampusManager`)、消息存储 (`MessageStorage`) 以及用户关系更新 (`RelationshipManager`)。 + - 处理后的消息信息(如计算出的兴趣度)会传递给对应的 `SubHeartflow`。 + - **回复决策与生成 (Replying)**: + - 由 `SubHeartflow` 及其当前激活的聊天实例 (`NormalChatInstance` 或 `HeartFlowChatInstance`) 负责。 + - 基于其内部状态 (`ChatState`、`SubMind` 的思考结果)、观察到的信息 (`Observation` 提供的内容) 以及 `InterestChatting` 的状态来决定是否回复、何时回复以及如何回复。 +- **消息缓冲 (Message Caching)**: + - `message_buffer` 模块会对某些传入消息进行临时缓存,尤其是在处理连续的多部分消息(如多张图片)时。 + - 这个缓冲机制发生在 `HeartFCProcessor` 处理流程中,确保消息的完整性,然后才进行后续的存储和兴趣计算。 + - 缓存的消息最终仍会流向对应的 `ChatStream`(与 `SubHeartflow` 关联),但核心的消息处理与回复决策仍然是分离的步骤。 -## 使用说明 +## 2. 核心控制与状态管理 (Core Control and State Management) -### 创建新的子心流 -```python -heartflow = Heartflow() -subheartflow = heartflow.create_subheartflow(chat_id) -``` +### 2.1. Heart Flow 整体控制 +- **控制者**: 主心流 (`Heartflow`) +- **核心职责**: + - 通过其成员 `SubHeartflowManager` 创建和管理子心流。 + - 通过其成员 `self.current_state: MaiStateInfo` 控制整体行为模式。 + - 管理系统级后台任务。 -### 添加观察者 -```python -observation = ChattingObservation(chat_id) -subheartflow.add_observation(observation) -``` +### 2.2. Heart Flow 状态 (`MaiStateInfo`) +- **定义与管理**: `Heartflow` 持有 `MaiStateInfo` 的实例 (`self.current_state`) 来管理其状态。状态的枚举定义在 `my_state_manager.py` 中的 `MaiState`。 +- **状态及含义**: + - `MaiState.OFFLINE` (不在线): 不观察任何群消息,不进行主动交互,仅存储消息。 + - `MaiState.PEEKING` (看一眼手机): 有限度地参与聊天(由 `MaiStateInfo` 定义具体的普通/专注群数量限制)。 + - `MaiState.NORMAL_CHAT` (正常看手机): 正常参与聊天,允许 `SubHeartflow` 进入 `CHAT` 或 `FOCUSED` 状态(数量受限)。 + * `MaiState.FOCUSED_CHAT` (专心看手机): 更积极地参与聊天,通常允许更多或更高优先级的 `FOCUSED` 状态子心流。 +- **作用**: `Heartflow` 的状态直接影响 `SubHeartflowManager` 如何管理子心流(如激活数量、允许的状态等)。 -## 配置说明 +### 2.3. 聊天流状态 (`ChatState`) 与转换 +- **管理对象**: 每个 `SubHeartflow` 实例内部维护其 `ChatStateInfo`,包含当前的 `ChatState`。 +- **状态及含义**: + - `ChatState.ABSENT` (不参与/没在看): 初始或停用状态。子心流不观察新信息,不进行思考,也不回复。 + - `ChatState.CHAT` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`。 + * `ChatState.FOCUSED` (专注/激情水群): 专注聊天模式。激活 `HeartFlowChatInstance`。 +- **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。 +- **状态转换机制** (由 `SubHeartflowManager` 驱动): + - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制,选择部分 `ABSENT` 状态的子心流,调用其 `set_chat_state` 方法将其转换为 `CHAT`。 + - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且未达上限,则调用 `set_chat_state` 将其提升为 `FOCUSED`。 + - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `set_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。 -系统的主要配置参数: -- `sub_heart_flow_stop_time`: 子心流停止时间 -- `sub_heart_flow_freeze_time`: 子心流冻结时间 -- `heart_flow_update_interval`: 心流更新间隔 +## 3. 聊天实例详解 (Chat Instances Explained) -## 注意事项 +### 3.1. NormalChatInstance +- **激活条件**: 对应 `SubHeartflow` 的 `ChatState` 为 `CHAT`。 +- **工作流程**: + - 按照系统设定的普通聊天规则处理群消息。 + - 定期检查新消息。 + - 对简单询问、闲聊等进行及时回复。 +- **行为特点**: + - 回复相对常规、简单。 + - 不投入过多计算资源。 + - 侧重于维持基本的交流氛围。 + - 示例:对问候语、日常分享等进行简单回应。 -1. 子心流会在长时间不活跃后自动清理 -2. 需要合理配置更新间隔以平衡性能和响应速度 -3. 观察系统会限制消息处理数量以避免过载 +### 3.2. HeartFlowChatInstance (继承自原 PFC 逻辑) +- **激活条件**: 对应 `SubHeartflow` 的 `ChatState` 为 `FOCUSED`。 +- **工作流程**: + - 基于更复杂的规则(原 PFC 模式)进行深度处理。 + - 对群内话题进行深入分析。 + - 可能主动发起相关话题或引导交流。 +- **行为特点**: + - 回复更积极、深入。 + - 投入更多资源参与聊天。 + - 回复内容可能更详细、有针对性。 + - 对话题参与度高,能带动交流。 + - 示例:对复杂或有争议话题阐述观点,并与人互动。 -# HeartFChatting 与主动回复流程说明 (V2) +## 4. 工作流程示例 (Example Workflow) -本文档描述了 `HeartFChatting` 类及其在 `heartFC_controler` 模块中实现的主动、基于兴趣的回复流程。 +1. **启动**: `Heartflow` 启动,初始化 `MaiStateInfo` (例如 `OFFLINE`) 和 `SubHeartflowManager`。 +2. **状态变化**: 用户操作或内部逻辑使 `Heartflow` 的 `current_state` 变为 `NORMAL_CHAT`。 +3. **管理器响应**: `SubHeartflowManager` 检测到状态变化,根据 `NORMAL_CHAT` 的限制,调用 `create_or_get_subheartflow` 获取或创建子心流,并通过 `set_chat_state` 将部分子心流状态从 `ABSENT` 激活为 `CHAT`。 +4. **子心流激活**: 被激活的 `SubHeartflow` 启动其 `NormalChatInstance`。 +5. **信息接收**: 该 `SubHeartflow` 的 `ChattingObservation` 开始从数据库拉取新消息。 +6. **普通回复**: `NormalChatInstance` 处理观察到的信息,执行普通回复逻辑。 +7. **兴趣评估**: `SubHeartflowManager` 定期评估该子心流的 `InterestChatting` 状态。 +8. **提升状态**: 若兴趣度达标且 `Heartflow` 状态允许,`SubHeartflowManager` 调用该子心流的 `set_chat_state` 将其状态提升为 `FOCUSED`。 +9. **子心流切换**: `SubHeartflow` 内部停止 `NormalChatInstance`,启动 `HeartFlowChatInstance`。 +10. **专注回复**: `HeartFlowChatInstance` 开始根据其逻辑进行更深入的交互。 +11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE`,`SubHeartflowManager` 会调用所有子心流的 `set_chat_state(ChatState.ABSENT)`,使其停止活动。 -## 1. `HeartFChatting` 类概述 +## 5. 使用与配置 (Usage and Configuration) -* **目标**: 管理特定聊天流 (`stream_id`) 的主动回复逻辑,使其行为更像人类的自然交流。 -* **创建时机**: 当 `HeartFC_Chat` 的兴趣监控任务 (`_interest_monitor_loop`) 检测到某个聊天流的兴趣度 (`InterestChatting`) 达到了触发回复评估的条件 (`should_evaluate_reply`) 时,会为该 `stream_id` 获取或创建唯一的 `HeartFChatting` 实例 (`_get_or_create_heartFC_chat`)。 -* **持有**: - * 对应的 `sub_heartflow` 实例引用 (通过 `heartflow.get_subheartflow(stream_id)`)。 - * 对应的 `chat_stream` 实例引用。 - * 对 `HeartFC_Chat` 单例的引用 (用于调用发送消息、处理表情等辅助方法)。 -* **初始化**: `HeartFChatting` 实例在创建后会执行异步初始化 (`_initialize`),这可能包括加载必要的上下文或历史信息(*待确认是否实现了读取历史消息*)。 +### 5.1. 使用说明 (Code Examples) +- **(内部)创建/获取子心流** (由 `SubHeartflowManager` 调用): + ```python + # subheartflow_manager.py + new_subflow = SubHeartflow(subheartflow_id, mai_states) + await new_subflow.initialize() + observation = ChattingObservation(chat_id=subheartflow_id) + new_subflow.add_observation(observation) + ``` +- **(内部)添加观察者** (由 `SubHeartflowManager` 或 `SubHeartflow` 内部调用): + ```python + # sub_heartflow.py + self.observations.append(observation) + ``` -## 2. 核心回复流程 (由 `HeartFC_Chat` 触发) +### 5.2. 配置参数 (Key Parameters) +- `sub_heart_flow_stop_time`: 子心流停止(标记为可清理)的不活跃时间阈值 (似乎由 `SubHeartflowManager.cleanup_inactive_subheartflows` 的参数 `inactive_threshold_seconds` 控制)。 +- `sub_heart_flow_freeze_time`: 子心流冻结时间 (当前文档未明确体现,可能需要审阅代码确认)。 +- `heart_flow_update_interval`: 主心流更新其状态或执行管理操作的频率 (需要审阅 `Heartflow` 代码确认)。 +- `MaiStateInfo` 内的限制: 定义了不同主状态下 `CHAT` 和 `FOCUSED` 子心流的数量上限。 -当 `HeartFC_Chat` 调用 `HeartFChatting` 实例的方法 (例如 `add_time`) 时,会启动内部的回复决策与执行流程: +## 6. 注意事项 (Important Notes) -1. **规划 (Planner):** - * **输入**: 从关联的 `sub_heartflow` 获取观察结果、思考链、记忆片段等上下文信息。 - * **决策**: - * 判断当前是否适合进行回复。 - * 决定回复的形式(纯文本、带表情包等)。 - * 选择合适的回复时机和策略。 - * **实现**: *此部分逻辑待详细实现,可能利用 LLM 的工具调用能力来增强决策的灵活性和智能性。需要考虑机器人的个性化设定。* +1. **自动清理**: `SubHeartflowManager` 会定期检查并清理长时间不活跃的子心流。 +2. **性能平衡**: 主心流执行管理操作的频率(如检查状态、清理、评估兴趣)需要合理配置,以平衡系统性能和响应速度。 +3. **信息过载**: 单个 `ChattingObservation` 会限制一次性从数据库拉取的消息数量 (`max_now_obs_len`)。 -2. **回复生成 (Replier):** - * **输入**: Planner 的决策结果和必要的上下文。 - * **执行**: - * 调用 `ResponseGenerator` (`self.gpt`) 或类似组件生成具体的回复文本内容。 - * 可能根据 Planner 的策略生成多个候选回复。 - * **并发**: 系统支持同时存在多个思考/生成任务(上限由 `global_config.max_concurrent_thinking_messages` 控制)。 +## 7. 待办与未来方向 (TODOs and Future Directions) -3. **检查 (Checker):** - * **时机**: 在回复生成过程中或生成后、发送前执行。 - * **目的**: - * 检查自开始生成回复以来,聊天流中是否出现了新的消息。 - * 评估已生成的候选回复在新的上下文下是否仍然合适、相关。 - * *需要实现相似度比较逻辑,防止发送与近期消息内容相近或重复的回复。* - * **处理**: 如果检查结果认为回复不合适,则该回复将被**抛弃**。 - -4. **发送协调:** - * **执行**: 如果 Checker 通过,`HeartFChatting` 会调用 `HeartFC_Chat` 实例提供的发送接口: - * `_create_thinking_message`: 通知 `MessageManager` 显示"正在思考"状态。 - * `_send_response_messages`: 将最终的回复文本交给 `MessageManager` 进行排队和发送。 - * `_handle_emoji`: 如果需要发送表情包,调用此方法处理表情包的获取和发送。 - * **细节**: 实际的消息发送、排队、间隔控制由 `MessageManager` 和 `MessageSender` 负责。 - -## 3. 与其他模块的交互 - -* **`HeartFC_Chat`**: - * 创建、管理和触发 `HeartFChatting` 实例。 - * 提供发送消息 (`_send_response_messages`)、处理表情 (`_handle_emoji`)、创建思考消息 (`_create_thinking_message`) 的接口给 `HeartFChatting` 调用。 - * 运行兴趣监控循环 (`_interest_monitor_loop`)。 -* **`InterestManager` / `InterestChatting`**: - * `InterestManager` 存储每个 `stream_id` 的 `InterestChatting` 实例。 - * `InterestChatting` 负责计算兴趣衰减和回复概率。 - * `HeartFC_Chat` 查询 `InterestChatting.should_evaluate_reply()` 来决定是否触发 `HeartFChatting`。 -* **`heartflow` / `sub_heartflow`**: - * `HeartFChatting` 从对应的 `sub_heartflow` 获取进行规划所需的核心上下文信息 (观察、思考链等)。 -* **`MessageManager` / `MessageSender`**: - * 接收来自 `HeartFC_Chat` 的发送请求 (思考消息、文本消息、表情包消息)。 - * 管理消息队列 (`MessageContainer`),处理消息发送间隔和实际发送 (`MessageSender`)。 -* **`ResponseGenerator` (`gpt`)**: - * 被 `HeartFChatting` 的 Replier 部分调用,用于生成回复文本。 -* **`MessageStorage`**: - * 存储所有接收和发送的消息。 -* **`HippocampusManager`**: - * `HeartFC_Processor` 使用它计算传入消息的记忆激活率,作为兴趣度计算的输入之一。 - -## 4. 原有问题与状态更新 - -1. **每个 `pfchating` 是否对应一个 `chat_stream`,是否是唯一的?** - * **是**。`HeartFC_Chat._get_or_create_heartFC_chat` 确保了每个 `stream_id` 只有一个 `HeartFChatting` 实例。 (已确认) -2. **`observe_text` 传入进来是纯 str,是不是应该传进来 message 构成的 list?** - * **机制已改变**。当前的触发机制是基于 `InterestManager` 的概率判断。`HeartFChatting` 启动后,应从其关联的 `sub_heartflow` 获取更丰富的上下文信息,而非简单的 `observe_text`。 -3. **检查失败的回复应该怎么处理?** - * **暂定:抛弃**。这是当前 Checker 逻辑的基础设定。 -4. **如何比较相似度?** - * **待实现**。Checker 需要具体的算法来比较候选回复与新消息的相似度。 -5. **Planner 怎么写?** - * **待实现**。这是 `HeartFChatting` 的核心决策逻辑,需要结合 `sub_heartflow` 的输出、LLM 工具调用和个性化配置来设计。 - - -## 6. 未来优化点 - -* 实现 Checker 中的相似度比较算法。 -* 详细设计并实现 Planner 的决策逻辑,包括 LLM 工具调用和个性化。 -* 确认并完善 `HeartFChatting._initialize()` 中的历史消息加载逻辑。 -* 探索更优的检查失败回复处理策略(例如:重新规划、修改回复等)。 -* 优化 `HeartFChatting` 与 `sub_heartflow` 的信息交互。 - - - -BUG: -2.复读,可能是planner还未校准好 -3.planner还未个性化,需要加入bot个性信息,且获取的聊天内容有问题 \ No newline at end of file +* **更新 "与其他模块的交互" 部分**: 详细说明 `SubHeartflowManager`, `SubHeartflow`, `NormalChatInstance`, `HeartFlowChatInstance` 之间以及与 `MessageManager`, `ResponseGenerator`, `InterestManager` 等外部模块的具体交互。 +* **明确 `sub_heart_flow_freeze_time`**: 确认该配置项的实际作用和实现位置。 +* **明确 `heart_flow_update_interval`**: 确认主心流管理循环的实际间隔。 +* **扩展观察类型**: 实现更多 `Observation` 类型(如私聊、系统事件等)。 +* **子心流内部状态转换**: 探索允许子心流根据自身思考结果主动请求状态转换的可能性。 +* **资源管理**: 优化子心流的资源占用和清理策略。 From 577e45484e838cf0036748c9407c9a5524c5c547 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 01:51:49 +0800 Subject: [PATCH 42/79] =?UTF-8?q?better=EF=BC=9A=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/README.md | 6 +- src/heart_flow/heartFC_chatting_logic.md | 124 +++++++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/heart_flow/heartFC_chatting_logic.md diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index db757c568..cf1cd5ac0 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -1,6 +1,10 @@ # 心流系统 (Heart Flow System) -*在此处简要介绍心流系统的目标和作用* +## 通俗易懂的工作流程介绍 + +心流系统就像一个智能聊天管家,它的工作方式可以这样理解: + +心流系统由主控中心(Heartflow)作为大脑协调全局,它通过场景管家(SubHeartflowManager)管理各个聊天场景的"小管家"(SubHeartflow)。当收到消息时,系统会先进行过滤和基础分析(如屏蔽词检查和兴趣度计算),然后将处理好的消息分发给对应场景的小管家。每个小管家会根据当前状态决定回复方式:不参与(ABSENT)时完全不看不回,普通模式(CHAT)进行简单回复,专注模式(FOCUSED)则深入交流。系统会根据聊天活跃度和兴趣度自动调整各场景的参与程度,同时主控中心也能手动调整整体参与度(如在离线、轻度参与和专注聊天之间切换)。整个系统就像一个拥有多个聊天助手的智能管家,能够智能地动态调整参与聊天的深度和范围。 ## 1. 系统架构 (System Architecture) diff --git a/src/heart_flow/heartFC_chatting_logic.md b/src/heart_flow/heartFC_chatting_logic.md new file mode 100644 index 000000000..67a13cc66 --- /dev/null +++ b/src/heart_flow/heartFC_chatting_logic.md @@ -0,0 +1,124 @@ +# HeartFChatting 逻辑详解 + +`HeartFChatting` 类是心流系统(Heart Flow System)中负责**专注聊天**(`ChatState.FOCUSED`)的核心组件。它的主要职责是在特定的聊天流 (`stream_id`) 中,通过一个持续的 **思考(Think)-规划(Plan)-执行(Execute)** 循环来模拟更自然、更深入的对话交互。当关联的 `SubHeartflow` 状态切换为 `FOCUSED` 时,`HeartFChatting` 实例会被创建并启动;当状态切换为其他(如 `CHAT` 或 `ABSENT`)时,它会被关闭。 + +## 1. 初始化 (`__init__`, `_initialize`) + +- **依赖注入**: 在创建时,`HeartFChatting` 接收 `chat_id`(即 `stream_id`)、关联的 `SubMind` 实例以及 `Observation` 实例列表作为参数。 +- **核心组件**: 内部初始化了几个关键组件: + - `ActionManager`: 管理当前循环可用的动作(如回复文本、回复表情、不回复)。 + - `HeartFCGenerator`: (`self.gpt_instance`) 用于生成回复文本。 + - `ToolUser`: (`self.tool_user`) 用于执行 `SubMind` 可能请求的工具调用(虽然在此类中主要用于获取工具定义,实际执行由 `SubMind` 完成)。 + - `HeartFCSender`: (`self.heart_fc_sender`) 专门负责处理消息发送逻辑,包括管理"正在思考"状态。 + - `LLMRequest`: (`self.planner_llm`) 配置用于执行规划任务的大语言模型请求。 +- **状态变量**: + - `_initialized`: 标记是否完成懒初始化。 + - `_processing_lock`: 异步锁,确保同一时间只有一个完整的"思考-规划-执行"周期在运行。 + - `_loop_active`: 标记主循环是否正在运行。 + - `_loop_task`: 指向主循环的 `asyncio.Task` 对象。 + - `_cycle_history`: 一个双端队列 (`deque`),用于存储最近若干次循环的信息 (`CycleInfo`)。 + - `_current_cycle`: 当前正在执行的循环信息 (`CycleInfo`)。 +- **懒初始化 (`_initialize`)**: + - 在首次需要访问 `ChatStream` 前调用(通常在 `start` 方法中)。 + - 根据 `stream_id` 从 `chat_manager` 获取对应的 `ChatStream` 实例。 + - 更新日志前缀,使用聊天流的名称以提高可读性。 + +## 2. 生命周期管理 (`start`, `shutdown`) + +- **启动 (`start`)**: + - 外部调用此方法来启动 `HeartFChatting` 的工作流程。 + - 内部调用 `_start_loop_if_needed` 来安全地启动主循环任务 (`_hfc_loop`)。 +- **关闭 (`shutdown`)**: + - 外部调用此方法来优雅地停止 `HeartFChatting`。 + - 取消正在运行的主循环任务 (`_loop_task`)。 + - 清理内部状态(如 `_loop_active`, `_loop_task`)。 + - 释放可能被持有的处理锁 (`_processing_lock`)。 + +## 3. 核心循环 (`_hfc_loop`) + +`_hfc_loop` 是 `HeartFChatting` 的心脏,它以异步方式无限期运行(直到被 `shutdown` 取消),不断执行以下步骤: + +1. **创建循环记录**: 初始化一个新的 `CycleInfo` 对象来记录本次循环的详细信息(ID、开始时间、计时器、动作、思考内容等)。 +2. **获取处理锁**: 使用 `_processing_lock` 确保并发安全。 +3. **执行思考-规划-执行**: 调用 `_think_plan_execute_loop` 方法。 +4. **处理循环延迟**: 根据本次循环是否执行了实际动作以及循环耗时,智能地引入短暂的 `asyncio.sleep`,防止 CPU 空转或过于频繁的循环。 +5. **记录循环信息**: 将完成的 `CycleInfo` 存入 `_cycle_history`,并记录详细的日志,包括循环耗时和各阶段计时。 + +## 4. 思考-规划-执行周期 (`_think_plan_execute_loop`) + +这是每个循环内部的核心逻辑,按顺序执行: + +### 4.1. 思考阶段 (`_get_submind_thinking`) + +1. **触发观察**: 调用关联的 `Observation` 实例的 `observe()` 方法,使其更新对环境(如聊天室新消息)的观察。 +2. **触发子思维**: 调用关联的 `SubMind` 实例的 `do_thinking_before_reply()` 方法。**关键**: 会将上一个循环的 `CycleInfo` 传递给 `SubMind`,使其了解上一次行动的决策、理由以及是否发生了重新规划,从而实现更连贯的思考。 +3. **获取思考结果**: `SubMind` 返回其当前的内心想法 (`current_mind`)。 + +### 4.2. 规划阶段 (`_planner`) + +1. **输入**: 获取 `SubMind` 的当前想法 (`current_mind`)、`SubMind` 通过工具调用收集到的结构化信息 (`structured_info`) 以及观察到的最新消息。 +2. **构建提示词**: 调用 `_build_planner_prompt` 方法,将上述信息以及机器人个性、当前可用动作等整合进一个专门为规划器设计的提示词中。 +3. **定义动作工具**: 使用 `ActionManager.get_planner_tool_definition()` 获取当前可用动作(如 `no_reply`, `text_reply`, `emoji_reply`)的 JSON Schema,将其作为 "工具" 提供给 LLM。 +4. **调用 LLM**: 使用 `self.planner_llm` 向大模型发送请求,**强制要求**模型调用 `decide_reply_action` 这个"工具",并根据提示词内容决定使用哪个动作以及相应的参数(如 `reasoning`, `emoji_query`)。 +5. **处理 LLM 响应**: 使用 `process_llm_tool_response` 解析 LLM 返回的工具调用请求,提取出决策的动作 (`action`)、理由 (`reasoning`) 和可能的表情查询 (`emoji_query`)。 +6. **检查新消息与重新规划**: + - 调用 `_check_new_messages` 检查自规划阶段开始以来是否有新消息。 + - 如果检测到新消息,有一定概率(当前为 30%)触发**重新规划**。这会再次调用 `_planner`,但会传入一个特殊的提示词片段(通过 `_build_replan_prompt` 生成),告知 LLM 它之前的决策以及现在需要重新考虑。 +7. **输出**: 返回一个包含最终决策结果(`action`, `reasoning`, `emoji_query` 等)的字典。如果 LLM 调用或解析失败,`action` 会被设为 "error"。 + +### 4.3. 执行阶段 (`_handle_action`) + +根据规划阶段返回的 `action`,分派到不同的处理方法: + +- **`_handle_text_reply` (文本回复)**: + 1. `_get_anchor_message`: 获取一个用于回复的锚点消息。**注意**: 当前实现是创建一个系统触发的占位符消息作为锚点,而不是实际观察到的最后一条消息。 + 2. `_create_thinking_message`: 调用 `HeartFCSender` 的 `register_thinking` 方法,标记机器人开始思考,并获取一个 `thinking_id`。 + 3. `_replier_work`: 调用回复器生成回复内容。 + 4. `_sender`: 调用发送器发送生成的文本和可能的表情。 +- **`_handle_emoji_reply` (仅表情回复)**: + 1. 获取锚点消息。 + 2. `_handle_emoji`: 获取表情图片并调用 `HeartFCSender` 发送。 +- **`_handle_no_reply` (不回复)**: + 1. 记录不回复的理由。 + 2. `_wait_for_new_message`: 进入等待状态,直到关联的 `Observation` 检测到新消息或超时(当前 300 秒)。 + +## 5. 回复器逻辑 (`_replier_work`) + +- **输入**: 规划器给出的回复理由 (`reason`)、锚点消息 (`anchor_message`)、思考ID (`thinking_id`),以及通过 `self.sub_mind` 获取的结构化信息和当前想法。 +- **处理**: 调用 `self.gpt_instance` (`HeartFCGenerator`) 的 `generate_response` 方法。这个方法负责构建最终的生成提示词(结合思考、理由、上下文等),调用 LLM 生成回复文本。 +- **输出**: 返回一个包含多段回复文本的列表 (`List[str]`),如果生成失败则返回 `None`。 + +## 6. 发送器逻辑 (`_sender`, `_create_thinking_message`, `_send_response_messages`, `_handle_emoji`) + +`HeartFChatting` 类本身不直接处理 WebSocket 发送,而是将发送任务委托给 `HeartFCSender` 实例 (`self.heart_fc_sender`)。 + +- **`_create_thinking_message`**: 准备一个 `MessageThinking` 对象,并调用 `sender.register_thinking(thinking_message)`。 +- **`_send_response_messages`**: + - 检查对应的 `thinking_id` 是否仍然有效(通过 `sender.get_thinking_start_time`)。 + - 遍历 `_replier_work` 返回的回复文本列表 (`response_set`)。 + - 为每一段文本创建一个 `MessageSending` 对象。 + - 调用 `sender.type_and_send_message(bot_message)` 来发送消息。`HeartFCSender` 内部会处理模拟打字延迟、实际发送和消息存储。 + - 发送完成后,调用 `sender.complete_thinking(chat_id, thinking_id)` 来清理思考状态。 + - 记录实际发送的消息 ID 到 `CycleInfo` 中。 +- **`_handle_emoji`**: + - 使用 `emoji_manager` 根据 `emoji_query` 获取表情图片路径。 + - 将图片转为 Base64。 + - 创建 `MessageSending` 对象(标记为 `is_emoji=True`)。 + - 调用 `sender.send_and_store(bot_message)` 来发送并存储表情消息(这个方法不涉及思考状态)。 + +## 7. 循环信息记录 (`CycleInfo`) + +- `CycleInfo` 类用于记录每一次思考-规划-执行循环的详细信息,包括: + - 循环 ID (`cycle_id`) + - 开始和结束时间 (`start_time`, `end_time`) + - 是否执行了实际动作 (`action_taken`) + - 决策的动作类型 (`action_type`) 和理由 (`reasoning`) + - 各阶段的耗时计时器 (`timers`) + - 关联的思考消息 ID (`thinking_id`) + - 是否发生了重新规划 (`replanned`) + - 详细的响应信息 (`response_info`),包括生成的文本、表情查询、锚点消息 ID、实际发送的消息 ID 列表以及 `SubMind` 的思考内容。 +- `HeartFChatting` 维护一个 `_cycle_history` 队列来保存最近的循环记录,方便调试和分析。 + +## 8. 总结 + +`HeartFChatting` 通过精密的循环控制、阶段分离(思考、规划、执行)、与 `SubMind` 和 `Observation` 的紧密协作,以及对 `HeartFCSender` 和 `HeartFCGenerator` 等专用组件的依赖,实现了在 FOCUSED 状态下的主动、深入且有状态的对话逻辑。它能够根据上下文和内部思考动态调整回复策略,并通过 `ActionManager` 灵活控制可执行的动作范围。 \ No newline at end of file From 08be34a581b7804dc2ea8ebb64a526a124095048 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 12:20:33 +0800 Subject: [PATCH 43/79] =?UTF-8?q?doc=EF=BC=9A=E9=9D=9E=E5=B8=B8=E6=B8=85?= =?UTF-8?q?=E6=99=B0=E7=9A=84=E5=B7=A5=E4=BD=9C=E6=B5=81=E7=A8=8B=E4=BB=8B?= =?UTF-8?q?=E7=BB=8D=EF=BC=8C=E4=BD=A0=E4=B8=80=E5=AE=9A=E7=9C=8B=E5=BE=97?= =?UTF-8?q?=E6=87=82=E5=90=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/README.md | 102 +++++++++++++++------ src/heart_flow/chat_state_info.py | 2 +- src/heart_flow/heartFC_chatting_logic.md | 4 - src/heart_flow/sub_heartflow.py | 105 +++++++++++----------- src/heart_flow/subheartflow_manager.py | 25 +++--- src/plugins/heartFC_chat/heartFC_chat.py | 8 +- src/plugins/heartFC_chat/normal_chat.py | 34 +++---- src/plugins/utils/chat_message_builder.py | 2 +- template/bot_config_template.toml | 4 +- 9 files changed, 157 insertions(+), 129 deletions(-) diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index cf1cd5ac0..ca6603e30 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -1,12 +1,71 @@ # 心流系统 (Heart Flow System) -## 通俗易懂的工作流程介绍 +## 一条消息是怎么到最终回复的?简明易懂的介绍 -心流系统就像一个智能聊天管家,它的工作方式可以这样理解: +1 接受消息,由HeartHC_processor处理消息,存储消息 -心流系统由主控中心(Heartflow)作为大脑协调全局,它通过场景管家(SubHeartflowManager)管理各个聊天场景的"小管家"(SubHeartflow)。当收到消息时,系统会先进行过滤和基础分析(如屏蔽词检查和兴趣度计算),然后将处理好的消息分发给对应场景的小管家。每个小管家会根据当前状态决定回复方式:不参与(ABSENT)时完全不看不回,普通模式(CHAT)进行简单回复,专注模式(FOCUSED)则深入交流。系统会根据聊天活跃度和兴趣度自动调整各场景的参与程度,同时主控中心也能手动调整整体参与度(如在离线、轻度参与和专注聊天之间切换)。整个系统就像一个拥有多个聊天助手的智能管家,能够智能地动态调整参与聊天的深度和范围。 + 1.1 process_message()函数,接受消息 -## 1. 系统架构 (System Architecture) + 1.2 创建消息对应的聊天流(chat_stream)和子心流(sub_heartflow) + + 1.3 进行常规消息处理 + + 1.4 存储消息 store_message() + + 1.5 计算兴趣度Interest + + 1.6 将消息连同兴趣度,存储到内存中的interest_dict(SubHeartflow的属性) + +2 根据 sub_heartflow 的聊天状态,决定后续处理流程 + + 2a ABSENT状态:不做任何处理 + + 2b CHAT状态:送入NormalChat 实例 + + 2c FOCUS状态:送入HeartFChatting 实例 + +b NormalChat工作方式 + + b.1 启动后台任务 _reply_interested_message,持续运行。 + b.2 该任务轮询 InterestChatting 提供的 interest_dict + b.3 对每条消息,结合兴趣度、是否被提及(@)、意愿管理器(WillingManager)计算回复概率。(这部分要改,目前还是用willing计算的,之后要和Interest合并) + b.4 若概率通过: + b.4.1 创建"思考中"消息 (MessageThinking)。 + b.4.2 调用 NormalChatGenerator 生成文本回复。 + b.4.3 通过 message_manager 发送回复 (MessageSending)。 + b.4.4 可能根据配置和文本内容,额外发送一个匹配的表情包。 + b.4.5 更新关系值和全局情绪。 + b.5 处理完成后,从 interest_dict 中移除该消息。 + +c HeartFChatting工作方式 + + c.1 启动主循环 _hfc_loop + c.2 每个循环称为一个周期 (Cycle),执行 think_plan_execute 流程。 + c.3 Think (思考) 阶段: + c.3.1 观察 (Observe): 通过 ChattingObservation,使用 observe() 获取最新的聊天消息。 + c.3.2 思考 (Think): 调用 SubMind 的 do_thinking_before_reply 方法。 + c.3.2.1 SubMind 结合观察到的内容、个性、情绪、上周期动作等信息,生成当前的内心想法 (current_mind)。 + c.3.2.2 在此过程中 SubMind 的LLM可能请求调用工具 (ToolUser) 来获取额外信息或执行操作,结果存储在 structured_info 中。 + c.4 Plan (规划/决策) 阶段: + c.4.1 结合观察到的消息文本、`SubMind` 生成的 `current_mind` 和 `structured_info`、以及 `ActionManager` 提供的可用动作,决定本次周期的行动 (`text_reply`/`emoji_reply`/`no_reply`) 和理由。 + c.4.2 重新规划检查 (Re-plan Check): 如果在 c.3.1 到 c.4.1 期间检测到新消息,可能(有概率)触发重新执行 c.4.1 决策步骤。 + c.5 Execute (执行/回复) 阶段: + c.5.1 如果决策是 text_reply: + c.5.1.1 获取锚点消息。 + c.5.1.2 通过 HeartFCSender 注册"思考中"状态。 + c.5.1.3 调用 HeartFCGenerator (gpt_instance) 生成回复文本。 + c.5.1.4 通过 HeartFCSender 发送回复 + c.5.1.5 如果规划时指定了表情查询 (emoji_query),随后发送表情。 + c.5.2 如果决策是 emoji_reply: + c.5.2.1 获取锚点消息。 + c.5.2.2 通过 HeartFCSender 直接发送匹配查询 (emoji_query) 的表情。 + c.5.3 如果决策是 no_reply: + c.5.3.1 进入等待状态,直到检测到新消息或超时。 + c.6 循环结束后,记录周期信息 (CycleInfo),并根据情况进行短暂休眠,防止CPU空转。 + + + +## 1. 一条消息是怎么到最终回复的?复杂细致的介绍 ### 1.1. 主心流 (Heartflow) - **文件**: `heartflow.py` @@ -24,7 +83,7 @@ - 维护特定场景下的思维状态和聊天流状态 (`ChatState`)。 - 通过关联的 `Observation` 实例接收和处理信息。 - 拥有独立的思考 (`SubMind`) 和回复判断能力。 -- **观察者**: 每个子心流可以拥有一个或多个 `Observation` 实例(目前每个子心流仅使用一个 `ChattingObservation`)。 +- **观察者**: 每个子心流可以拥有一个或多个 `Observation` 实例(目前每个子心流仅使用一个 `ChattingObservation`)。 - **内部结构**: - **聊天流状态 (`ChatState`)**: 标记当前子心流的参与模式 (`ABSENT`, `CHAT`, `FOCUSED`),决定是否观察、回复以及使用何种回复模式。 - **聊天实例 (`NormalChatInstance` / `HeartFlowChatInstance`)**: 根据 `ChatState` 激活对应的实例来处理聊天逻辑。同一时间只有一个实例处于活动状态。 @@ -84,21 +143,25 @@ - **状态及含义**: - `ChatState.ABSENT` (不参与/没在看): 初始或停用状态。子心流不观察新信息,不进行思考,也不回复。 - `ChatState.CHAT` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`。 - * `ChatState.FOCUSED` (专注/激情水群): 专注聊天模式。激活 `HeartFlowChatInstance`。 + * `ChatState.FOCUSED` (专注/认真水群): 专注聊天模式。激活 `HeartFlowChatInstance`。 - **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。 - **状态转换机制** (由 `SubHeartflowManager` 驱动): - - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制,选择部分 `ABSENT` 状态的子心流,调用其 `set_chat_state` 方法将其转换为 `CHAT`。 - - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且未达上限,则调用 `set_chat_state` 将其提升为 `FOCUSED`。 + - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制,选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `set_chat_state` 方法将其转换为 `CHAT`。 + - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**,则调用 `set_chat_state` 将其提升为 `FOCUSED`。 - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `set_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。 + - **注意**: `set_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法)承担。 ## 3. 聊天实例详解 (Chat Instances Explained) ### 3.1. NormalChatInstance - **激活条件**: 对应 `SubHeartflow` 的 `ChatState` 为 `CHAT`。 - **工作流程**: - - 按照系统设定的普通聊天规则处理群消息。 - - 定期检查新消息。 - - 对简单询问、闲聊等进行及时回复。 + - 当 `SubHeartflow` 进入 `CHAT` 状态时,`NormalChatInstance` 会被激活。 + - 实例启动后,会创建一个后台任务 (`_reply_interested_message`)。 + - 该任务持续监控由 `InterestChatting` 传入的、具有一定兴趣度的消息列表 (`interest_dict`)。 + - 对列表中的每条消息,结合是否被提及 (`@`)、消息本身的兴趣度以及当前的回复意愿 (`WillingManager`),计算出一个回复概率。 + - 根据计算出的概率随机决定是否对该消息进行回复。 + - 如果决定回复,则调用 `NormalChatGenerator` 生成回复内容,并可能附带表情包。 - **行为特点**: - 回复相对常规、简单。 - 不投入过多计算资源。 @@ -153,19 +216,4 @@ - `sub_heart_flow_stop_time`: 子心流停止(标记为可清理)的不活跃时间阈值 (似乎由 `SubHeartflowManager.cleanup_inactive_subheartflows` 的参数 `inactive_threshold_seconds` 控制)。 - `sub_heart_flow_freeze_time`: 子心流冻结时间 (当前文档未明确体现,可能需要审阅代码确认)。 - `heart_flow_update_interval`: 主心流更新其状态或执行管理操作的频率 (需要审阅 `Heartflow` 代码确认)。 -- `MaiStateInfo` 内的限制: 定义了不同主状态下 `CHAT` 和 `FOCUSED` 子心流的数量上限。 - -## 6. 注意事项 (Important Notes) - -1. **自动清理**: `SubHeartflowManager` 会定期检查并清理长时间不活跃的子心流。 -2. **性能平衡**: 主心流执行管理操作的频率(如检查状态、清理、评估兴趣)需要合理配置,以平衡系统性能和响应速度。 -3. **信息过载**: 单个 `ChattingObservation` 会限制一次性从数据库拉取的消息数量 (`max_now_obs_len`)。 - -## 7. 待办与未来方向 (TODOs and Future Directions) - -* **更新 "与其他模块的交互" 部分**: 详细说明 `SubHeartflowManager`, `SubHeartflow`, `NormalChatInstance`, `HeartFlowChatInstance` 之间以及与 `MessageManager`, `ResponseGenerator`, `InterestManager` 等外部模块的具体交互。 -* **明确 `sub_heart_flow_freeze_time`**: 确认该配置项的实际作用和实现位置。 -* **明确 `heart_flow_update_interval`**: 确认主心流管理循环的实际间隔。 -* **扩展观察类型**: 实现更多 `Observation` 类型(如私聊、系统事件等)。 -* **子心流内部状态转换**: 探索允许子心流根据自身思考结果主动请求状态转换的可能性。 -* **资源管理**: 优化子心流的资源占用和清理策略。 +- ` \ No newline at end of file diff --git a/src/heart_flow/chat_state_info.py b/src/heart_flow/chat_state_info.py index 14fd33403..619f372fc 100644 --- a/src/heart_flow/chat_state_info.py +++ b/src/heart_flow/chat_state_info.py @@ -5,7 +5,7 @@ import enum class ChatState(enum.Enum): ABSENT = "没在看群" CHAT = "随便水群" - FOCUSED = "激情水群" + FOCUSED = "认真水群" class ChatStateInfo: diff --git a/src/heart_flow/heartFC_chatting_logic.md b/src/heart_flow/heartFC_chatting_logic.md index 67a13cc66..1e178a6ff 100644 --- a/src/heart_flow/heartFC_chatting_logic.md +++ b/src/heart_flow/heartFC_chatting_logic.md @@ -118,7 +118,3 @@ - 是否发生了重新规划 (`replanned`) - 详细的响应信息 (`response_info`),包括生成的文本、表情查询、锚点消息 ID、实际发送的消息 ID 列表以及 `SubMind` 的思考内容。 - `HeartFChatting` 维护一个 `_cycle_history` 队列来保存最近的循环记录,方便调试和分析。 - -## 8. 总结 - -`HeartFChatting` 通过精密的循环控制、阶段分离(思考、规划、执行)、与 `SubMind` 和 `Observation` 的紧密协作,以及对 `HeartFCSender` 和 `HeartFCGenerator` 等专用组件的依赖,实现了在 FOCUSED 状态下的主动、深入且有状态的对话逻辑。它能够根据上下文和内部思考动态调整回复策略,并通过 `ActionManager` 灵活控制可执行的动作范围。 \ No newline at end of file diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index fb1a81c3b..cbdcd2748 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -232,54 +232,68 @@ class SubHeartflow: subheartflow_id: 子心流唯一标识符 parent_heartflow: 父级心流实例 """ - # 基础属性 + # 基础属性,两个值是一样的 self.subheartflow_id = subheartflow_id self.chat_id = subheartflow_id + # 麦麦的状态 self.mai_states = mai_states - # 聊天状态管理 - self.chat_state: ChatStateInfo = ChatStateInfo() # 该sub_heartflow的聊天状态信息 - self.interest_chatting = None # 将在 initialize 中创建 + # 这个聊天流的状态 + self.chat_state: ChatStateInfo = ChatStateInfo() + + # 兴趣检测器 + self.interest_chatting = None # 活动状态管理 self.last_active_time = time.time() # 最后活跃时间 self.should_stop = False # 停止标志 self.task: Optional[asyncio.Task] = None # 后台任务 + + # 随便水群 normal_chat 和 认真水群 heartFC_chat 实例 + # CHAT模式激活 随便水群 FOCUS模式激活 认真水群 self.heart_fc_instance: Optional[HeartFChatting] = None # 该sub_heartflow的HeartFChatting实例 self.normal_chat_instance: Optional[NormalChat] = None # 该sub_heartflow的NormalChat实例 - # 观察和知识系统 + # 观察,目前只有聊天观察,可以载入多个 + # 负责对处理过的消息进行观察 self.observations: List[ChattingObservation] = [] # 观察列表 - self.running_knowledges = [] # 运行中的知识 + # self.running_knowledges = [] # 运行中的知识,待完善 - # LLM模型配置 + # LLM模型配置,负责进行思考 self.sub_mind = SubMind( subheartflow_id=self.subheartflow_id, chat_state=self.chat_state, observations=self.observations ) + # 日志前缀 self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id async def initialize(self): - """异步初始化方法""" + """异步初始化方法,创建兴趣检测器""" self.interest_chatting = await InterestChatting.create(state_change_callback=self.set_chat_state) logger.debug(f"{self.log_prefix} InterestChatting 实例已创建并初始化。") async def add_time_current_state(self, add_time: float): + """增加当前状态的时间""" self.current_state_time += add_time async def change_to_state_chat(self): + """改变到随便水群状态""" self.current_state_time = 120 self._start_normal_chat() async def change_to_state_focused(self): + """改变到认真水群状态""" self.current_state_time = 60 self._start_heart_fc_chat() async def _stop_normal_chat(self): - """停止 NormalChat 的兴趣监控""" + """ + 停止 NormalChat 实例 + 切出 CHAT 状态时使用 + """ if self.normal_chat_instance: - logger.info(f"{self.log_prefix} 停止 NormalChat 兴趣监控...") + logger.info(f"{self.log_prefix} 离开CHAT模式,结束 随便水群") try: await self.normal_chat_instance.stop_chat() # 调用 stop_chat except Exception as e: @@ -287,23 +301,21 @@ class SubHeartflow: logger.error(traceback.format_exc()) async def _start_normal_chat(self) -> bool: - """启动 NormalChat 实例及其兴趣监控,确保 HeartFChatting 已停止""" - await self._stop_heart_fc_chat() # 确保专注聊天已停止 + """ + 启动 NormalChat 实例, + 进入 CHAT 状态时使用 + + 确保 HeartFChatting 已停止 + """ + await self._stop_heart_fc_chat() # 确保 专注聊天已停止 log_prefix = self.log_prefix try: - # 总是尝试创建或获取最新的 stream 和 interest_dict + # 获取聊天流并创建 NormalChat 实例 chat_stream = chat_manager.get_stream(self.chat_id) - if not chat_stream: - logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。") - return False - - # 如果实例不存在或需要更新,则创建新实例 - # if not self.normal_chat_instance: # 或者总是重新创建以获取最新的 interest_dict? self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) - logger.info(f"{log_prefix} 创建或更新 NormalChat 实例。") - logger.info(f"{log_prefix} 启动 NormalChat 兴趣监控...") + logger.info(f"{log_prefix} 启动 NormalChat 随便水群...") await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat return True except Exception as e: @@ -369,7 +381,7 @@ class SubHeartflow: self.heart_fc_instance = None # 创建或初始化异常,清理实例 return False - async def set_chat_state(self, new_state: "ChatState", current_states_num: tuple = ()): + async def set_chat_state(self, new_state: "ChatState"): """更新sub_heartflow的聊天状态,并管理 HeartFChatting 和 NormalChat 实例及任务""" current_state = self.chat_state.chat_status if current_state == new_state: @@ -377,47 +389,30 @@ class SubHeartflow: return log_prefix = self.log_prefix - current_mai_state = self.mai_states.get_current_state() state_changed = False # 标记状态是否实际发生改变 # --- 状态转换逻辑 --- if new_state == ChatState.CHAT: - normal_limit = current_mai_state.get_normal_chat_max_num() - current_chat_count = current_states_num[1] if len(current_states_num) > 1 else 0 - - if current_chat_count >= normal_limit and current_state != ChatState.CHAT: - logger.debug( - f"{log_prefix} 无法从 {current_state.value} 转到 聊天。原因:聊不过来了 ({current_chat_count}/{normal_limit})" - ) - return # 阻止状态转换 + # 移除限额检查逻辑 + logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态") + if await self._start_normal_chat(): + logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。") + state_changed = True else: - logger.debug(f"{log_prefix} 准备进入或保持 聊天 状态 ({current_chat_count}/{normal_limit})") - if await self._start_normal_chat(): - logger.info(f"{log_prefix} 成功进入或保持 NormalChat 状态。") - state_changed = True - else: - logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。") - # 考虑是否需要回滚状态或采取其他措施 - return # 启动失败,不改变状态 + logger.error(f"{log_prefix} 启动 NormalChat 失败,无法进入 CHAT 状态。") + # 考虑是否需要回滚状态或采取其他措施 + return # 启动失败,不改变状态 elif new_state == ChatState.FOCUSED: - focused_limit = current_mai_state.get_focused_chat_max_num() - current_focused_count = current_states_num[2] if len(current_states_num) > 2 else 0 - - if current_focused_count >= focused_limit and current_state != ChatState.FOCUSED: - logger.debug( - f"{log_prefix} 无法从 {current_state.value} 转到 专注。原因:聊不过来了 ({current_focused_count}/{focused_limit})" - ) - return # 阻止状态转换 + # 移除限额检查逻辑 + logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态") + if await self._start_heart_fc_chat(): + logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。") + state_changed = True else: - logger.debug(f"{log_prefix} 准备进入或保持 专注聊天 状态 ({current_focused_count}/{focused_limit})") - if await self._start_heart_fc_chat(): - logger.info(f"{log_prefix} 成功进入或保持 HeartFChatting 状态。") - state_changed = True - else: - logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。") - # 启动失败,状态回滚到之前的状态或ABSENT?这里保持不改变 - return # 启动失败,不改变状态 + logger.error(f"{log_prefix} 启动 HeartFChatting 失败,无法进入 FOCUSED 状态。") + # 启动失败,状态回滚到之前的状态或ABSENT?这里保持不改变 + return # 启动失败,不改变状态 elif new_state == ChatState.ABSENT: logger.info(f"{log_prefix} 进入 ABSENT 状态,停止所有聊天活动...") diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 79f2a0ecf..0bfa40cc7 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -74,8 +74,6 @@ class SubHeartflowManager: # logger.debug(f"获取到已存在的子心流: {subheartflow_id}") return subflow - # 创建新的子心流实例 - # logger.info(f"子心流 {subheartflow_id} 不存在,正在创建...") try: # 初始化子心流 new_subflow = SubHeartflow(subheartflow_id, mai_states) @@ -118,7 +116,7 @@ class SubHeartflowManager: self.count_subflows_by_state(ChatState.CHAT), self.count_subflows_by_state(ChatState.FOCUSED), ) - await subheartflow.set_chat_state(ChatState.ABSENT, states_num) + await subheartflow.set_chat_state(ChatState.ABSENT) else: logger.debug(f"[子心流管理] {stream_name} 已是ABSENT状态") except Exception as e: @@ -235,13 +233,15 @@ class SubHeartflowManager: logger.debug(f"[激活] 正在激活子心流{stream_name}") - states_num = ( - self.count_subflows_by_state(ChatState.ABSENT), - self.count_subflows_by_state(ChatState.CHAT), - self.count_subflows_by_state(ChatState.FOCUSED), - ) + # --- 限额检查 --- # + current_chat_count = self.count_subflows_by_state(ChatState.CHAT) + if current_chat_count >= limit: + logger.warning(f"[激活] 跳过{stream_name}, 普通聊天已达上限 ({current_chat_count}/{limit})") + continue # 跳过此子心流,继续尝试激活下一个 + # --- 结束限额检查 --- # - await flow.set_chat_state(ChatState.CHAT, states_num) + # 移除 states_num 参数 + await flow.set_chat_state(ChatState.CHAT) if flow.chat_state.chat_status == ChatState.CHAT: activated_count += 1 @@ -319,11 +319,11 @@ class SubHeartflowManager: continue logger.info( - f"{log_prefix} [{stream_name}] 触发 激情水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" + f"{log_prefix} [{stream_name}] 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" ) # 执行状态提升 - await current_subflow.set_chat_state(ChatState.FOCUSED, states_num) + await current_subflow.set_chat_state(ChatState.FOCUSED) # 验证提升结果 if ( @@ -372,7 +372,7 @@ class SubHeartflowManager: # --- 状态设置 --- # # 注意:这里传递的状态数量是 *停用前* 的状态数量 - await current_subflow.set_chat_state(ChatState.ABSENT, states_num_before) + await current_subflow.set_chat_state(ChatState.ABSENT) # --- 状态验证 (可选) --- final_subflow = self.subheartflows.get(flow_id) @@ -383,7 +383,6 @@ class SubHeartflowManager: f"{log_prefix_manager} {log_prefix_flow} 成功从 {current_state.value} 停用到 ABSENT 状态" ) deactivated_count += 1 - # 注意:停用后不需要更新 states_num_before,因为它只用于 set_chat_state 的限制检查 else: logger.warning( f"{log_prefix_manager} {log_prefix_flow} 尝试停用到 ABSENT 后状态仍为 {final_state.value}" diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index bd4da95a7..e9577e411 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -216,7 +216,7 @@ class HeartFChatting: self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" self._initialized = True - logger.info(f"麦麦感觉到了,可以开始激情水群{self.log_prefix} ") + logger.info(f"麦麦感觉到了,可以开始认真水群{self.log_prefix} ") return True async def start(self): @@ -224,7 +224,7 @@ class HeartFChatting: 启动 HeartFChatting 的主循环。 注意:调用此方法前必须确保已经成功初始化。 """ - logger.info(f"{self.log_prefix} 开始激情水群(HFC)...") + logger.info(f"{self.log_prefix} 开始认真水群(HFC)...") await self._start_loop_if_needed() async def _start_loop_if_needed(self): @@ -247,7 +247,7 @@ class HeartFChatting: pass # 忽略取消或超时错误 self._loop_task = None # 清理旧任务引用 - logger.info(f"{self.log_prefix} 启动激情水群(HFC)主循环...") + logger.info(f"{self.log_prefix} 启动认真水群(HFC)主循环...") # 创建新的循环任务 self._loop_task = asyncio.create_task(self._hfc_loop()) # 添加完成回调 @@ -320,7 +320,7 @@ class HeartFChatting: ) except asyncio.CancelledError: - logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的激情水群(HFC)被取消了") + logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)被取消了") except Exception as e: logger.error(f"{self.log_prefix} HeartFChatting: 意外错误: {e}") logger.error(traceback.format_exc()) diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 76cba5979..6687421e5 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -164,14 +164,13 @@ class NormalChat: ) self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor) - async def _find_interested_message(self) -> None: + async def _reply_interested_message(self) -> None: """ 后台任务方法,轮询当前实例关联chat的兴趣消息 通常由start_monitoring_interest()启动 """ while True: - await asyncio.sleep(1) # 每秒检查一次 - + await asyncio.sleep(0.5) # 每秒检查一次 # 检查任务是否已被取消 if self._chat_task is None or self._chat_task.cancelled(): logger.info(f"[{self.stream_name}] 兴趣监控任务被取消或置空,退出") @@ -353,36 +352,27 @@ class NormalChat: async def start_chat(self): """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行)。""" if self._chat_task is None or self._chat_task.done(): - logger.info(f"[{self.stream_name}] 启动聊天任务...") - task = asyncio.create_task(self._find_interested_message()) + task = asyncio.create_task(self._reply_interested_message()) task.add_done_callback(lambda t: self._handle_task_completion(t)) # 回调现在是实例方法 self._chat_task = task - # 改为实例方法, 移除 stream_id 参数 def _handle_task_completion(self, task: asyncio.Task): - """兴趣监控任务完成时的回调函数。""" - # 检查完成的任务是否是当前实例的任务 + """任务完成回调处理""" if task is not self._chat_task: - logger.warning(f"[{self.stream_name}] 收到一个未知或过时任务的完成回调。") + logger.warning(f"[{self.stream_name}] 收到未知任务回调") return - try: - # 检查任务是否因异常而结束 - exception = task.exception() - if exception: - logger.error(f"[{self.stream_name}] 兴趣监控任务因异常结束: {exception}") - logger.error(traceback.format_exc()) # 记录完整的 traceback - # else: # 减少日志 - # logger.info(f"[{self.stream_name}] 兴趣监控任务正常结束。") + if exc := task.exception(): + logger.error(f"[{self.stream_name}] 任务异常: {exc}") + logger.error(traceback.format_exc()) except asyncio.CancelledError: - logger.info(f"[{self.stream_name}] 兴趣监控任务被取消。") + logger.info(f"[{self.stream_name}] 任务已取消") except Exception as e: - logger.error(f"[{self.stream_name}] 处理任务完成回调时出错: {e}") + logger.error(f"[{self.stream_name}] 回调处理错误: {e}") finally: - # 标记任务已完成/移除 - if self._chat_task is task: # 再次确认是当前任务 + if self._chat_task is task: self._chat_task = None - logger.debug(f"[{self.stream_name}] 聊天任务已被标记为完成/移除。") + logger.debug(f"[{self.stream_name}] 任务清理完成") # 改为实例方法, 移除 stream_id 参数 async def stop_chat(self): diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index 5d9494488..f510365fa 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -311,7 +311,7 @@ async def build_readable_messages( ) readable_read_mark = translate_timestamp_to_human_readable(read_mark, mode=timestamp_mode) - read_mark_line = f"\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 以下新消息未读---\n" + read_mark_line = f"\n--- 以上消息是你已经思考过的内容已读 (标记时间: {readable_read_mark}) ---\n--- 请关注以下未读的新消息---\n" # 组合结果,确保空部分不引入多余的标记或换行 if formatted_before and formatted_after: diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index a85d9f17a..afb65e89c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -252,7 +252,7 @@ provider = "SILICONFLOW" pri_in = 0 pri_out = 0 -[model.llm_sub_heartflow] #子心流:激情水群时,生成麦麦的内心想法 +[model.llm_sub_heartflow] #子心流:认真水群时,生成麦麦的内心想法 name = "Qwen/Qwen2.5-72B-Instruct" provider = "SILICONFLOW" pri_in = 4.13 @@ -260,7 +260,7 @@ pri_out = 4.13 temp = 0.7 #模型的温度,新V3建议0.1-0.3 -[model.llm_plan] #决策模型:激情水群时,负责决定麦麦该做什么 +[model.llm_plan] #决策模型:认真水群时,负责决定麦麦该做什么 name = "Qwen/Qwen2.5-32B-Instruct" provider = "SILICONFLOW" pri_in = 1.26 From 510aa7a12d8e6fd3821812d28f3e5721974fe302 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 12:21:11 +0800 Subject: [PATCH 44/79] fix:ruff --- src/heart_flow/sub_heartflow.py | 6 +++--- src/heart_flow/subheartflow_manager.py | 20 +------------------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index cbdcd2748..dd9364f30 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -241,7 +241,7 @@ class SubHeartflow: # 这个聊天流的状态 self.chat_state: ChatStateInfo = ChatStateInfo() - + # 兴趣检测器 self.interest_chatting = None @@ -249,7 +249,7 @@ class SubHeartflow: self.last_active_time = time.time() # 最后活跃时间 self.should_stop = False # 停止标志 self.task: Optional[asyncio.Task] = None # 后台任务 - + # 随便水群 normal_chat 和 认真水群 heartFC_chat 实例 # CHAT模式激活 随便水群 FOCUS模式激活 认真水群 self.heart_fc_instance: Optional[HeartFChatting] = None # 该sub_heartflow的HeartFChatting实例 @@ -304,7 +304,7 @@ class SubHeartflow: """ 启动 NormalChat 实例, 进入 CHAT 状态时使用 - + 确保 HeartFChatting 已停止 """ await self._stop_heart_fc_chat() # 确保 专注聊天已停止 diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 0bfa40cc7..ce9ec39a7 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -111,11 +111,6 @@ class SubHeartflowManager: # 设置状态为ABSENT释放资源 if subheartflow.chat_state.chat_status != ChatState.ABSENT: logger.debug(f"[子心流管理] 设置 {stream_name} 状态为ABSENT") - states_num = ( - self.count_subflows_by_state(ChatState.ABSENT), - self.count_subflows_by_state(ChatState.CHAT), - self.count_subflows_by_state(ChatState.FOCUSED), - ) await subheartflow.set_chat_state(ChatState.ABSENT) else: logger.debug(f"[子心流管理] {stream_name} 已是ABSENT状态") @@ -237,7 +232,7 @@ class SubHeartflowManager: current_chat_count = self.count_subflows_by_state(ChatState.CHAT) if current_chat_count >= limit: logger.warning(f"[激活] 跳过{stream_name}, 普通聊天已达上限 ({current_chat_count}/{limit})") - continue # 跳过此子心流,继续尝试激活下一个 + continue # 跳过此子心流,继续尝试激活下一个 # --- 结束限额检查 --- # # 移除 states_num 参数 @@ -284,12 +279,6 @@ class SubHeartflowManager: logger.debug(f"{log_prefix} 已达专注上限 ({current_focused_count}/{focused_limit})") return - states_num = ( - self.count_subflows_by_state(ChatState.ABSENT), - self.count_subflows_by_state(ChatState.CHAT), - current_focused_count, - ) - for sub_hf in list(self.subheartflows.values()): flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id @@ -340,13 +329,6 @@ class SubHeartflowManager: subflows_snapshot = list(self.subheartflows.values()) deactivated_count = 0 - # 预先计算状态数量,因为 set_chat_state 需要 - states_num_before = ( - self.count_subflows_by_state(ChatState.ABSENT), - self.count_subflows_by_state(ChatState.CHAT), - self.count_subflows_by_state(ChatState.FOCUSED), - ) - try: for sub_hf in subflows_snapshot: flow_id = sub_hf.subheartflow_id From 14157bdab2983ef22299c5bdeb8bc4f5119213e7 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 13:25:35 +0800 Subject: [PATCH 45/79] =?UTF-8?q?better=EF=BC=9A=E6=9B=B4=E6=B8=85?= =?UTF-8?q?=E6=99=B0=E7=9A=84=E5=AD=90=E5=BF=83=E6=B5=81=E5=81=9C=E7=94=A8?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interest_monitor_gui.py | 2 +- src/heart_flow/README.md | 32 ++-- src/heart_flow/background_tasks.py | 39 +++-- src/heart_flow/heartflow.py | 17 +- src/heart_flow/interest_logger.py | 2 +- src/heart_flow/sub_heartflow.py | 90 +++++----- src/heart_flow/subheartflow_manager.py | 157 ++++++++---------- .../heartFC_chat/heartflow_processor.py | 7 +- .../heartFC_chat/heartflow_prompt_builder.py | 1 - 9 files changed, 162 insertions(+), 185 deletions(-) diff --git a/interest_monitor_gui.py b/interest_monitor_gui.py index 28c5ecc14..3dbcb28fb 100644 --- a/interest_monitor_gui.py +++ b/interest_monitor_gui.py @@ -246,7 +246,7 @@ class InterestMonitorApp: self.stream_sub_minds[stream_id] = subflow_entry.get("sub_mind", "N/A") self.stream_chat_states[stream_id] = subflow_entry.get("sub_chat_state", "N/A") self.stream_threshold_status[stream_id] = subflow_entry.get("is_above_threshold", False) - self.stream_last_active[stream_id] = subflow_entry.get("last_active_time") # 存储原始时间戳 + self.stream_last_active[stream_id] = subflow_entry.get("last_changed_state_time") # 存储原始时间戳 self.stream_last_interaction[stream_id] = subflow_entry.get( "last_interaction_time" ) # 存储原始时间戳 diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index ca6603e30..a25cbe9fb 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -101,10 +101,12 @@ c HeartFChatting工作方式 - **文件**: `subheartflow_manager.py` - **职责**: - 作为 `Heartflow` 的成员变量存在。 + - **在初始化时接收并持有 `Heartflow` 的 `MaiStateInfo` 实例。** - 负责所有 `SubHeartflow` 实例的生命周期管理,包括: - - 创建和获取 (`create_or_get_subheartflow`)。 - - 停止和清理 (`stop_subheartflow`, `cleanup_inactive_subheartflows`)。 - - 根据 `Heartflow` 的状态和限制条件,激活、停用或调整子心流的状态。 + - 创建和获取 (`get_or_create_subheartflow`)。 + - 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。 + - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `activate_random_subflows_to_chat`, `evaluate_interest_and_promote`)。 + - **注意**: 不再提供直接获取所有 ID (`get_all_subheartflows_ids`) 或单个子心流 (`get_subheartflow`) 的公共方法。 ### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow) - **关注点分离**: 系统严格区分了接收和处理传入消息的流程与决定和生成回复的流程。 @@ -125,9 +127,10 @@ c HeartFChatting工作方式 ### 2.1. Heart Flow 整体控制 - **控制者**: 主心流 (`Heartflow`) - **核心职责**: - - 通过其成员 `SubHeartflowManager` 创建和管理子心流。 + - 通过其成员 `SubHeartflowManager` 创建和管理子心流(**在创建 `SubHeartflowManager` 时会传入自身的 `MaiStateInfo`**)。 - 通过其成员 `self.current_state: MaiStateInfo` 控制整体行为模式。 - 管理系统级后台任务。 + - **注意**: 不再提供直接获取所有子心流 ID (`get_all_subheartflows_streams_ids`) 的公共方法。 ### 2.2. Heart Flow 状态 (`MaiStateInfo`) - **定义与管理**: `Heartflow` 持有 `MaiStateInfo` 的实例 (`self.current_state`) 来管理其状态。状态的枚举定义在 `my_state_manager.py` 中的 `MaiState`。 @@ -146,10 +149,10 @@ c HeartFChatting工作方式 * `ChatState.FOCUSED` (专注/认真水群): 专注聊天模式。激活 `HeartFlowChatInstance`。 - **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。 - **状态转换机制** (由 `SubHeartflowManager` 驱动): - - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制,选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `set_chat_state` 方法将其转换为 `CHAT`。 - - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**,则调用 `set_chat_state` 将其提升为 `FOCUSED`。 - - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `set_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。 - - **注意**: `set_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法)承担。 + - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。 + - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**(通过 `self.mai_state_info` 获取限制),则调用 `change_chat_state` 将其提升为 `FOCUSED`。 + - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。 + - **注意**: `change_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法,这些方法会使用内部存储的 `mai_state_info` 来获取限制)承担。 ## 3. 聊天实例详解 (Chat Instances Explained) @@ -185,23 +188,24 @@ c HeartFChatting工作方式 1. **启动**: `Heartflow` 启动,初始化 `MaiStateInfo` (例如 `OFFLINE`) 和 `SubHeartflowManager`。 2. **状态变化**: 用户操作或内部逻辑使 `Heartflow` 的 `current_state` 变为 `NORMAL_CHAT`。 -3. **管理器响应**: `SubHeartflowManager` 检测到状态变化,根据 `NORMAL_CHAT` 的限制,调用 `create_or_get_subheartflow` 获取或创建子心流,并通过 `set_chat_state` 将部分子心流状态从 `ABSENT` 激活为 `CHAT`。 +3. **管理器响应**: `SubHeartflowManager` 检测到状态变化,根据 `NORMAL_CHAT` 的限制,调用 `get_or_create_subheartflow` 获取或创建子心流,并通过 `change_chat_state` 将部分子心流状态从 `ABSENT` 激活为 `CHAT`。 4. **子心流激活**: 被激活的 `SubHeartflow` 启动其 `NormalChatInstance`。 5. **信息接收**: 该 `SubHeartflow` 的 `ChattingObservation` 开始从数据库拉取新消息。 6. **普通回复**: `NormalChatInstance` 处理观察到的信息,执行普通回复逻辑。 7. **兴趣评估**: `SubHeartflowManager` 定期评估该子心流的 `InterestChatting` 状态。 -8. **提升状态**: 若兴趣度达标且 `Heartflow` 状态允许,`SubHeartflowManager` 调用该子心流的 `set_chat_state` 将其状态提升为 `FOCUSED`。 +8. **提升状态**: 若兴趣度达标且 `Heartflow` 状态允许,`SubHeartflowManager` 调用该子心流的 `change_chat_state` 将其状态提升为 `FOCUSED`。 9. **子心流切换**: `SubHeartflow` 内部停止 `NormalChatInstance`,启动 `HeartFlowChatInstance`。 10. **专注回复**: `HeartFlowChatInstance` 开始根据其逻辑进行更深入的交互。 -11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE`,`SubHeartflowManager` 会调用所有子心流的 `set_chat_state(ChatState.ABSENT)`,使其停止活动。 +11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE`,`SubHeartflowManager` 会调用所有子心流的 `change_chat_state(ChatState.ABSENT)`,使其停止活动。 ## 5. 使用与配置 (Usage and Configuration) ### 5.1. 使用说明 (Code Examples) -- **(内部)创建/获取子心流** (由 `SubHeartflowManager` 调用): +- **(内部)创建/获取子心流** (由 `SubHeartflowManager` 调用, 示例): ```python - # subheartflow_manager.py - new_subflow = SubHeartflow(subheartflow_id, mai_states) + # subheartflow_manager.py (get_or_create_subheartflow 内部) + # 注意:mai_states 现在是 self.mai_state_info + new_subflow = SubHeartflow(subheartflow_id, self.mai_state_info) await new_subflow.initialize() observation = ChattingObservation(chat_id=subheartflow_id) new_subflow.add_observation(observation) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index f5131a59a..c66a6128c 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -49,7 +49,6 @@ class BackgroundTaskManager: self.update_interval = update_interval self.cleanup_interval = cleanup_interval self.log_interval = log_interval - self.inactive_threshold = inactive_threshold # For cleanup task self.interest_eval_interval = interest_eval_interval # 存储兴趣评估间隔 self.random_deactivation_interval = random_deactivation_interval # 存储随机停用间隔 @@ -217,21 +216,35 @@ class BackgroundTaskManager: current_state == self.mai_state_info.mai_status.OFFLINE and previous_status != self.mai_state_info.mai_status.OFFLINE ): - logger.info("[后台任务] 主状态离线,触发子流停用") + logger.info("检测到离线,停用所有子心流") await self.subheartflow_manager.deactivate_all_subflows() async def _perform_cleanup_work(self): - """执行一轮子心流清理操作。""" - flows_to_stop = self.subheartflow_manager.cleanup_inactive_subheartflows(self.inactive_threshold) - if flows_to_stop: - logger.info(f"[Background Task Cleanup] Attempting to stop {len(flows_to_stop)} inactive flows...") - stopped_count = 0 - for flow_id, reason in flows_to_stop: - if await self.subheartflow_manager.stop_subheartflow(flow_id, f"定期清理: {reason}"): - stopped_count += 1 - logger.info(f"[Background Task Cleanup] Cleanup cycle finished. Stopped {stopped_count} inactive flows.") - # else: - # logger.debug("[Background Task Cleanup] Cleanup cycle finished. No inactive flows found.") + """执行子心流清理任务 + 1. 获取需要清理的不活跃子心流列表 + 2. 逐个停止这些子心流 + 3. 记录清理结果 + """ + # 获取需要清理的子心流列表(包含ID和原因) + flows_to_stop = self.subheartflow_manager.get_inactive_subheartflows() + + if not flows_to_stop: + return # 没有需要清理的子心流直接返回 + + logger.info(f"准备删除 {len(flows_to_stop)} 个不活跃(1h)子心流") + stopped_count = 0 + + # 逐个停止子心流 + for flow_id in flows_to_stop: + success = await self.subheartflow_manager.delete_subflow(flow_id) + if success: + stopped_count += 1 + logger.debug(f"[清理任务] 已停止子心流 {flow_id}") + + # 记录最终清理结果 + logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流") + + async def _perform_logging_work(self): """执行一轮状态日志记录。""" diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py index 7fbc0f58a..3f7fa0f12 100644 --- a/src/heart_flow/heartflow.py +++ b/src/heart_flow/heartflow.py @@ -47,8 +47,8 @@ class Heartflow: self.current_state: MaiStateInfo = MaiStateInfo() # 当前状态信息 self.mai_state_manager: MaiStateManager = MaiStateManager() # 状态决策管理器 - # 子心流管理 - self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager() # 子心流管理器 + # 子心流管理 (在初始化时传入 current_state) + self.subheartflow_manager: SubHeartflowManager = SubHeartflowManager(self.current_state) # LLM模型配置 self.llm_model = LLMRequest( @@ -75,23 +75,18 @@ class Heartflow: inactive_threshold=INACTIVE_THRESHOLD_SECONDS, ) - async def create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: + async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: """获取或创建一个新的SubHeartflow实例 - 委托给 SubHeartflowManager""" - return await self.subheartflow_manager.create_or_get_subheartflow(subheartflow_id, self.current_state) + # 不再需要传入 self.current_state + return await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id) - def get_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: - """获取指定ID的SubHeartflow实例""" - return self.subheartflow_manager.get_subheartflow(subheartflow_id) - - def get_all_subheartflows_streams_ids(self) -> list[Any]: - """获取当前所有活跃的子心流的 ID 列表 - 委托给 SubHeartflowManager""" - return self.subheartflow_manager.get_all_subheartflows_ids() async def heartflow_start_working(self): """启动后台任务""" await self.background_task_manager.start_tasks() logger.info("[Heartflow] 后台任务已启动") + # 根本不会用到这个函数吧,那样麦麦直接死了 async def stop_working(self): """停止所有任务和子心流""" logger.info("[Heartflow] 正在停止任务和子心流...") diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 62063f073..7802f87b4 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -58,7 +58,7 @@ class InterestLogger: return results for subheartflow in all_flows: - if self.subheartflow_manager.get_subheartflow(subheartflow.subheartflow_id): + if self.subheartflow_manager.get_or_create_subheartflow(subheartflow.subheartflow_id): tasks.append( asyncio.create_task(subheartflow.get_full_state(), name=f"get_state_{subheartflow.subheartflow_id}") ) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index dd9364f30..b6cbdd228 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -2,7 +2,7 @@ from .observation import Observation, ChattingObservation import asyncio from src.config.config import global_config import time -from typing import Optional, List, Dict, Callable +from typing import Optional, List, Dict, Callable, Tuple import traceback from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 import random @@ -41,7 +41,6 @@ class InterestChatting: increase_rate=probability_increase_rate_per_second, decay_factor=global_config.probability_decay_factor_per_second, max_probability=max_reply_probability, - state_change_callback: Optional[Callable[[ChatState], None]] = None, ): # 基础属性初始化 self.interest_level: float = 0.0 @@ -69,13 +68,23 @@ class InterestChatting: self.above_threshold = False self.start_hfc_probability = 0.0 + + async def initialize(self): + async with self._task_lock: + if self._is_running: + logger.debug("后台兴趣更新任务已在运行中。") + return - @classmethod - async def create(cls, *args, **kwargs): - """异步工厂方法,用于创建并初始化 InterestChatting 实例""" - instance = cls(*args, **kwargs) - await instance.start_updates(instance.update_interval) - return instance + # 清理已完成或已取消的任务 + if self.update_task and (self.update_task.done() or self.update_task.cancelled()): + self.update_task = None + + if not self.update_task: + self._stop_event.clear() + self._is_running = True + self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval)) + logger.debug("后台兴趣更新任务已创建并启动。") + def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool): self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) @@ -123,11 +132,11 @@ class InterestChatting: if self.start_hfc_probability != 0: self.start_hfc_probability -= 0.1 - async def increase_interest(self, current_time: float, value: float): + async def increase_interest(self, value: float): self.interest_level += value self.interest_level = min(self.interest_level, self.max_interest) - async def decrease_interest(self, current_time: float, value: float): + async def decrease_interest(self, value: float): self.interest_level -= value self.interest_level = max(self.interest_level, 0.0) @@ -176,22 +185,6 @@ class InterestChatting: self._is_running = False logger.info("InterestChatting 更新循环已停止。") - async def start_updates(self, update_interval: float = 1.0): - """启动后台更新任务,使用锁确保并发安全""" - async with self._task_lock: - if self._is_running: - logger.debug("后台兴趣更新任务已在运行中。") - return - - # 清理已完成或已取消的任务 - if self.update_task and (self.update_task.done() or self.update_task.cancelled()): - self.update_task = None - - if not self.update_task: - self._stop_event.clear() - self._is_running = True - self.update_task = asyncio.create_task(self._run_update_loop(update_interval)) - logger.debug("后台兴趣更新任务已创建并启动。") async def stop_updates(self): """停止后台更新任务,使用锁确保并发安全""" @@ -241,12 +234,14 @@ class SubHeartflow: # 这个聊天流的状态 self.chat_state: ChatStateInfo = ChatStateInfo() + self.chat_state_changed_time: float = time.time() + self.chat_state_last_time: float = 0 + self.history_chat_state: List[Tuple[ChatState, float]] = [] # 兴趣检测器 - self.interest_chatting = None + self.interest_chatting: InterestChatting = InterestChatting() # 活动状态管理 - self.last_active_time = time.time() # 最后活跃时间 self.should_stop = False # 停止标志 self.task: Optional[asyncio.Task] = None # 后台任务 @@ -269,23 +264,12 @@ class SubHeartflow: self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id async def initialize(self): - """异步初始化方法,创建兴趣检测器""" - self.interest_chatting = await InterestChatting.create(state_change_callback=self.set_chat_state) - logger.debug(f"{self.log_prefix} InterestChatting 实例已创建并初始化。") + """异步初始化方法,创建兴趣流""" + await self.interest_chatting.initialize() + logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。") - async def add_time_current_state(self, add_time: float): - """增加当前状态的时间""" - self.current_state_time += add_time - - async def change_to_state_chat(self): - """改变到随便水群状态""" - self.current_state_time = 120 - self._start_normal_chat() - - async def change_to_state_focused(self): - """改变到认真水群状态""" - self.current_state_time = 60 - self._start_heart_fc_chat() + def update_last_chat_state_time(self): + self.chat_state_last_time = time.time() - self.chat_state_changed_time async def _stop_normal_chat(self): """ @@ -381,11 +365,11 @@ class SubHeartflow: self.heart_fc_instance = None # 创建或初始化异常,清理实例 return False - async def set_chat_state(self, new_state: "ChatState"): + async def change_chat_state(self, new_state: "ChatState"): """更新sub_heartflow的聊天状态,并管理 HeartFChatting 和 NormalChat 实例及任务""" current_state = self.chat_state.chat_status + if current_state == new_state: - # logger.trace(f"{self.log_prefix} 状态已为 {current_state.value}, 无需更改。") # 减少日志噪音 return log_prefix = self.log_prefix @@ -422,9 +406,14 @@ class SubHeartflow: # --- 更新状态和最后活动时间 --- if state_changed: - logger.info(f"{log_prefix} 麦麦的聊天状态从 {current_state.value} 变更为 {new_state.value}") + self.update_last_chat_state_time() + self.history_chat_state.append((current_state, self.chat_state_last_time)) + + logger.info(f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {self.chat_state_last_time} 秒) 变更为 {new_state.value}") + self.chat_state.chat_status = new_state - self.last_active_time = time.time() + self.chat_state_last_time = 0 + self.chat_state_changed_time = time.time() else: # 如果因为某些原因(如启动失败)没有成功改变状态,记录一下 logger.debug( @@ -479,9 +468,6 @@ class SubHeartflow: async def should_evaluate_reply(self) -> bool: return await self.interest_chatting.should_evaluate_reply() - async def add_interest_dict_entry(self, message: MessageRecv, interest_value: float, is_mentioned: bool): - self.interest_chatting.add_interest_dict(message, interest_value, is_mentioned) - def get_interest_dict(self) -> Dict[str, tuple[MessageRecv, float, bool]]: return self.interest_chatting.interest_dict @@ -495,7 +481,7 @@ class SubHeartflow: "interest_state": interest_state, "current_mind": self.sub_mind.current_mind, "chat_state": self.chat_state.chat_status.value, - "last_active_time": self.last_active_time, + "last_changed_state_time": self.last_changed_state_time, } async def shutdown(self): diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index ce9ec39a7..d586fd43b 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -23,41 +23,29 @@ subheartflow_manager_log_config = LogConfig( logger = get_module_logger("subheartflow_manager", config=subheartflow_manager_log_config) # 子心流管理相关常量 -INACTIVE_THRESHOLD_SECONDS = 1200 # 子心流不活跃超时时间(秒) +INACTIVE_THRESHOLD_SECONDS = 3600 # 子心流不活跃超时时间(秒) class SubHeartflowManager: """管理所有活跃的 SubHeartflow 实例。""" - def __init__(self): + def __init__(self, mai_state_info: MaiStateInfo): self.subheartflows: Dict[Any, "SubHeartflow"] = {} self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问 + self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例 def get_all_subheartflows(self) -> List["SubHeartflow"]: """获取所有当前管理的 SubHeartflow 实例列表 (快照)。""" return list(self.subheartflows.values()) - - def get_all_subheartflows_ids(self) -> List[Any]: - """获取所有当前管理的 SubHeartflow ID 列表。""" - return list(self.subheartflows.keys()) - - def get_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: - """获取指定 ID 的 SubHeartflow 实例。""" - # 注意:这里没有加锁,假设读取操作相对安全或在已知上下文中调用 - # 如果并发写操作很多,get 也应该加锁 - subflow = self.subheartflows.get(subheartflow_id) - if subflow: - subflow.last_active_time = time.time() # 获取时更新活动时间 - return subflow - - async def create_or_get_subheartflow( - self, subheartflow_id: Any, mai_states: MaiStateInfo + + async def get_or_create_subheartflow( + self, subheartflow_id: Any ) -> Optional["SubHeartflow"]: """获取或创建指定ID的子心流实例 Args: subheartflow_id: 子心流唯一标识符 - mai_states: 当前麦麦状态信息 + # mai_states 参数已被移除,使用 self.mai_state_info Returns: 成功返回SubHeartflow实例,失败返回None @@ -75,8 +63,8 @@ class SubHeartflowManager: return subflow try: - # 初始化子心流 - new_subflow = SubHeartflow(subheartflow_id, mai_states) + # 初始化子心流, 传入存储的 mai_state_info + new_subflow = SubHeartflow(subheartflow_id, self.mai_state_info) # 异步初始化 await new_subflow.initialize() @@ -98,7 +86,7 @@ class SubHeartflowManager: logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True) return None - async def stop_subheartflow(self, subheartflow_id: Any, reason: str) -> bool: + async def sleep_subheartflow(self, subheartflow_id: Any, reason: str) -> bool: """停止指定的子心流并清理资源""" subheartflow = self.subheartflows.get(subheartflow_id) if not subheartflow: @@ -111,7 +99,7 @@ class SubHeartflowManager: # 设置状态为ABSENT释放资源 if subheartflow.chat_state.chat_status != ChatState.ABSENT: logger.debug(f"[子心流管理] 设置 {stream_name} 状态为ABSENT") - await subheartflow.set_chat_state(ChatState.ABSENT) + await subheartflow.change_chat_state(ChatState.ABSENT) else: logger.debug(f"[子心流管理] {stream_name} 已是ABSENT状态") except Exception as e: @@ -135,27 +123,26 @@ class SubHeartflowManager: logger.warning(f"[子心流管理] {stream_name} 已被提前移除") return False - def cleanup_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): - """识别并返回需要清理的不活跃子心流(id, 原因)""" + def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): + """识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)""" current_time = time.time() flows_to_stop = [] for subheartflow_id, subheartflow in list(self.subheartflows.items()): - # 只检查有interest_chatting的子心流 - if hasattr(subheartflow, "interest_chatting") and subheartflow.interest_chatting: - last_interact = subheartflow.interest_chatting.last_interaction_time - if max_age_seconds and (current_time - last_interact) > max_age_seconds: - reason = f"不活跃时间({current_time - last_interact:.0f}s) > 阈值({max_age_seconds}s)" - name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id - logger.debug(f"[清理] 标记 {name} 待移除: {reason}") - flows_to_stop.append((subheartflow_id, reason)) - - if flows_to_stop: - logger.info(f"[清理] 发现 {len(flows_to_stop)} 个不活跃子心流") + state = subheartflow.chat_state.chat_status + if state != ChatState.ABSENT: + continue + subheartflow.update_last_chat_state_time() + absent_last_time = subheartflow.chat_state_last_time + if max_age_seconds and (current_time - absent_last_time) > max_age_seconds: + flows_to_stop.append(subheartflow_id) + return flows_to_stop - async def enforce_subheartflow_limits(self, current_mai_state: MaiState): + async def enforce_subheartflow_limits(self): """根据主状态限制停止超额子心流(优先停不活跃的)""" + # 使用 self.mai_state_info 获取当前状态和限制 + current_mai_state = self.mai_state_info.get_current_state() normal_limit = current_mai_state.get_normal_chat_max_num() focused_limit = current_mai_state.get_focused_chat_max_num() logger.debug(f"[限制] 状态:{current_mai_state.value}, 普通限:{normal_limit}, 专注限:{focused_limit}") @@ -178,7 +165,7 @@ class SubHeartflowManager: logger.info(f"[限制] 普通聊天超额({len(normal_flows)}>{normal_limit}), 停止{excess}个") normal_flows.sort(key=lambda x: x[1]) for flow_id, _ in normal_flows[:excess]: - if await self.stop_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"): + if await self.sleep_subheartflow(flow_id, f"普通聊天超额(限{normal_limit})"): stopped += 1 # 处理专注聊天超额(需重新统计) @@ -192,7 +179,7 @@ class SubHeartflowManager: logger.info(f"[限制] 专注聊天超额({len(focused_flows)}>{focused_limit}), 停止{excess}个") focused_flows.sort(key=lambda x: x[1]) for flow_id, _ in focused_flows[:excess]: - if await self.stop_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"): + if await self.sleep_subheartflow(flow_id, f"专注聊天超额(限{focused_limit})"): stopped += 1 if stopped: @@ -200,8 +187,10 @@ class SubHeartflowManager: else: logger.debug(f"[限制] 无需停止, 当前总数:{len(self.subheartflows)}") - async def activate_random_subflows_to_chat(self, current_mai_state: MaiState): + async def activate_random_subflows_to_chat(self): """主状态激活时,随机选择ABSENT子心流进入CHAT状态""" + # 使用 self.mai_state_info 获取当前状态和限制 + current_mai_state = self.mai_state_info.get_current_state() limit = current_mai_state.get_normal_chat_max_num() if limit <= 0: logger.info("[激活] 当前状态不允许CHAT子心流") @@ -236,7 +225,7 @@ class SubHeartflowManager: # --- 结束限额检查 --- # # 移除 states_num 参数 - await flow.set_chat_state(ChatState.CHAT) + await flow.change_chat_state(ChatState.CHAT) if flow.chat_state.chat_status == ChatState.CHAT: activated_count += 1 @@ -246,25 +235,43 @@ class SubHeartflowManager: logger.info(f"[激活] 完成, 成功激活{activated_count}个子心流") async def deactivate_all_subflows(self): - """停用所有子心流(主状态变为OFFLINE时调用)""" - logger.info("[停用] 开始停用所有子心流") - flow_ids = list(self.subheartflows.keys()) + """将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)""" + # logger.info("[停用] 开始将所有子心流状态设置为 ABSENT") + # 使用 list() 创建一个当前值的快照,防止在迭代时修改字典 + flows_to_update = list(self.subheartflows.values()) - if not flow_ids: - logger.info("[停用] 无活跃子心流") + if not flows_to_update: + logger.debug("[停用] 无活跃子心流,无需操作") return - stopped_count = 0 - for flow_id in flow_ids: - if await self.stop_subheartflow(flow_id, "主状态离线"): - stopped_count += 1 + changed_count = 0 + for subflow in flows_to_update: + flow_id = subflow.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + # 再次检查子心流是否仍然存在于管理器中,以防万一在迭代过程中被移除 - logger.info(f"[停用] 完成, 尝试停止{len(flow_ids)}个, 成功{stopped_count}个") + if subflow.chat_state.chat_status != ChatState.ABSENT: + logger.debug(f"正在将子心流 {stream_name} 的状态从 {subflow.chat_state.chat_status.value} 更改为 ABSENT") + try: + # 调用 change_chat_state 将状态设置为 ABSENT + await subflow.change_chat_state(ChatState.ABSENT) + # 验证状态是否真的改变了 + if flow_id in self.subheartflows and self.subheartflows[flow_id].chat_state.chat_status == ChatState.ABSENT: + changed_count += 1 + else: + logger.warning(f"[停用] 尝试更改子心流 {stream_name} 状态后,状态仍未变为 ABSENT 或子心流已消失。") + except Exception as e: + logger.error(f"[停用] 更改子心流 {stream_name} 状态为 ABSENT 时出错: {e}", exc_info=True) + else: + logger.debug(f"[停用] 子心流 {stream_name} 已处于 ABSENT 状态,无需更改。") - async def evaluate_interest_and_promote(self, current_mai_state: MaiStateInfo): + logger.info(f"下限完成,共处理 {len(flows_to_update)} 个子心流,成功将 {changed_count} 个子心流的状态更改为 ABSENT。") + + async def evaluate_interest_and_promote(self): """评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)""" log_prefix = "[兴趣评估]" - current_state = current_mai_state.get_current_state() + # 使用 self.mai_state_info 获取当前状态和限制 + current_state = self.mai_state_info.get_current_state() focused_limit = current_state.get_focused_chat_max_num() if int(time.time()) % 20 == 0: # 每20秒输出一次 @@ -312,7 +319,7 @@ class SubHeartflowManager: ) # 执行状态提升 - await current_subflow.set_chat_state(ChatState.FOCUSED) + await current_subflow.change_chat_state(ChatState.FOCUSED) # 验证提升结果 if ( @@ -354,7 +361,7 @@ class SubHeartflowManager: # --- 状态设置 --- # # 注意:这里传递的状态数量是 *停用前* 的状态数量 - await current_subflow.set_chat_state(ChatState.ABSENT) + await current_subflow.change_chat_state(ChatState.ABSENT) # --- 状态验证 (可选) --- final_subflow = self.subheartflows.get(flow_id) @@ -419,44 +426,18 @@ class SubHeartflowManager: ) logger.debug(f"[子心流管理器] 更新了{updated_count}个子心流的主想法") - async def deactivate_subflow(self, subheartflow_id: Any): - """停用并移除指定的子心流。""" + async def delete_subflow(self, subheartflow_id: Any): + """删除指定的子心流。""" async with self._lock: subflow = self.subheartflows.pop(subheartflow_id, None) if subflow: - logger.info(f"正在停用 SubHeartflow: {subheartflow_id}...") + logger.info(f"正在删除 SubHeartflow: {subheartflow_id}...") try: - # --- 调用 shutdown 方法 --- + # 调用 shutdown 方法确保资源释放 await subflow.shutdown() - # --- 结束调用 --- - logger.info(f"SubHeartflow {subheartflow_id} 已成功停用。") + logger.info(f"SubHeartflow {subheartflow_id} 已成功删除。") except Exception as e: - logger.error(f"停用 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True) + logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True) else: - logger.warning(f"尝试停用不存在的 SubHeartflow: {subheartflow_id}") + logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}") - async def cleanup_inactive_subflows(self, inactive_threshold_seconds: int): - """清理长时间不活跃的子心流。""" - current_time = time.time() - inactive_ids = [] - # 不加锁地迭代,识别不活跃的 ID - for sub_id, subflow in self.subheartflows.items(): - # 检查 last_active_time 是否存在且是数值 - last_active = getattr(subflow, "last_active_time", 0) - if isinstance(last_active, (int, float)): - if current_time - last_active > inactive_threshold_seconds: - inactive_ids.append(sub_id) - logger.info( - f"发现不活跃的 SubHeartflow: {sub_id} (上次活跃: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last_active))})" - ) - else: - logger.warning(f"SubHeartflow {sub_id} 的 last_active_time 无效: {last_active}。跳过清理检查。") - - if inactive_ids: - logger.info(f"准备清理 {len(inactive_ids)} 个不活跃的 SubHeartflows: {inactive_ids}") - # 逐个停用(deactivate_subflow 会加锁) - tasks = [self.deactivate_subflow(sub_id) for sub_id in inactive_ids] - await asyncio.gather(*tasks) - logger.info("不活跃的 SubHeartflows 清理完成。") - # else: - # logger.debug("没有发现不活跃的 SubHeartflows 需要清理。") diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index da7b479ba..1f771688d 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -138,7 +138,7 @@ class HeartFCProcessor: group_info=groupinfo, ) - subheartflow = await heartflow.create_subheartflow(chat.stream_id) + subheartflow = await heartflow.get_or_create_subheartflow(chat.stream_id) message.update_chat_stream(chat) await message.process() @@ -166,9 +166,8 @@ class HeartFCProcessor: # 6. 兴趣度计算与更新 interested_rate, is_mentioned = await self._calculate_interest(message) - current_time = time.time() - await subheartflow.interest_chatting.increase_interest(current_time, value=interested_rate) - await subheartflow.add_interest_dict_entry(message, interested_rate, is_mentioned) + await subheartflow.interest_chatting.increase_interest(value=interested_rate) + await subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned) # 7. 日志记录 mes_name = chat.group_info.group_name if chat.group_info else "私聊" diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 661c4e8af..584205a73 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -63,7 +63,6 @@ def init_prompt(): - 话题无关/无聊/不感兴趣 - 最后一条消息是你自己发的且无人回应你 - 讨论你不懂的专业话题 -- 讨论你不想参与的话题 - 你发送了太多消息 2. 文字回复(text_reply)适用: From 2b721e70ee1faf4a304e7becf0ddc4a3dcff8d6e Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 13:28:36 +0800 Subject: [PATCH 46/79] Update README.md --- src/heart_flow/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index a25cbe9fb..24d094cc6 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -106,7 +106,7 @@ c HeartFChatting工作方式 - 创建和获取 (`get_or_create_subheartflow`)。 - 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。 - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `activate_random_subflows_to_chat`, `evaluate_interest_and_promote`)。 - - **注意**: 不再提供直接获取所有 ID (`get_all_subheartflows_ids`) 或单个子心流 (`get_subheartflow`) 的公共方法。 + - **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。 ### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow) - **关注点分离**: 系统严格区分了接收和处理传入消息的流程与决定和生成回复的流程。 @@ -135,7 +135,7 @@ c HeartFChatting工作方式 ### 2.2. Heart Flow 状态 (`MaiStateInfo`) - **定义与管理**: `Heartflow` 持有 `MaiStateInfo` 的实例 (`self.current_state`) 来管理其状态。状态的枚举定义在 `my_state_manager.py` 中的 `MaiState`。 - **状态及含义**: - - `MaiState.OFFLINE` (不在线): 不观察任何群消息,不进行主动交互,仅存储消息。 + - `MaiState.OFFLINE` (不在线): 不观察任何群消息,不进行主动交互,仅存储消息。当主状态变为 `OFFLINE` 时,`SubHeartflowManager` 会将所有子心流的状态设置为 `ChatState.ABSENT`。 - `MaiState.PEEKING` (看一眼手机): 有限度地参与聊天(由 `MaiStateInfo` 定义具体的普通/专注群数量限制)。 - `MaiState.NORMAL_CHAT` (正常看手机): 正常参与聊天,允许 `SubHeartflow` 进入 `CHAT` 或 `FOCUSED` 状态(数量受限)。 * `MaiState.FOCUSED_CHAT` (专心看手机): 更积极地参与聊天,通常允许更多或更高优先级的 `FOCUSED` 状态子心流。 @@ -151,7 +151,7 @@ c HeartFChatting工作方式 - **状态转换机制** (由 `SubHeartflowManager` 驱动): - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。 - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**(通过 `self.mai_state_info` 获取限制),则调用 `change_chat_state` 将其提升为 `FOCUSED`。 - - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。 + - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。 - **注意**: `change_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法,这些方法会使用内部存储的 `mai_state_info` 来获取限制)承担。 ## 3. 聊天实例详解 (Chat Instances Explained) @@ -196,7 +196,7 @@ c HeartFChatting工作方式 8. **提升状态**: 若兴趣度达标且 `Heartflow` 状态允许,`SubHeartflowManager` 调用该子心流的 `change_chat_state` 将其状态提升为 `FOCUSED`。 9. **子心流切换**: `SubHeartflow` 内部停止 `NormalChatInstance`,启动 `HeartFlowChatInstance`。 10. **专注回复**: `HeartFlowChatInstance` 开始根据其逻辑进行更深入的交互。 -11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE`,`SubHeartflowManager` 会调用所有子心流的 `change_chat_state(ChatState.ABSENT)`,使其停止活动。 +11. **状态回落/停用**: 若 `Heartflow` 状态变为 `OFFLINE`,`SubHeartflowManager` 会调用所有活跃子心流的 `change_chat_state(ChatState.ABSENT)`,使其进入 `ABSENT` 状态(它们不会立即被删除,只有在 `ABSENT` 状态持续1小时后才会被清理)。 ## 5. 使用与配置 (Usage and Configuration) @@ -217,7 +217,7 @@ c HeartFChatting工作方式 ``` ### 5.2. 配置参数 (Key Parameters) -- `sub_heart_flow_stop_time`: 子心流停止(标记为可清理)的不活跃时间阈值 (似乎由 `SubHeartflowManager.cleanup_inactive_subheartflows` 的参数 `inactive_threshold_seconds` 控制)。 +- `sub_heart_flow_stop_time`: (已废弃,现在由 `INACTIVE_THRESHOLD_SECONDS` in `subheartflow_manager.py` 控制) 子心流在 `ABSENT` 状态持续多久后被后台任务清理,默认为 3600 秒 (1 小时)。 - `sub_heart_flow_freeze_time`: 子心流冻结时间 (当前文档未明确体现,可能需要审阅代码确认)。 - `heart_flow_update_interval`: 主心流更新其状态或执行管理操作的频率 (需要审阅 `Heartflow` 代码确认)。 - ` \ No newline at end of file From 0e03c2e492d0d86a75bc117475fd59aa694bacee Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 13:29:04 +0800 Subject: [PATCH 47/79] fix:ruff --- interest_monitor_gui.py | 4 +++- src/heart_flow/background_tasks.py | 8 +++---- src/heart_flow/heartflow.py | 1 - src/heart_flow/sub_heartflow.py | 16 +++++++------- src/heart_flow/subheartflow_manager.py | 30 +++++++++++++++----------- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/interest_monitor_gui.py b/interest_monitor_gui.py index 3dbcb28fb..245a0ae99 100644 --- a/interest_monitor_gui.py +++ b/interest_monitor_gui.py @@ -246,7 +246,9 @@ class InterestMonitorApp: self.stream_sub_minds[stream_id] = subflow_entry.get("sub_mind", "N/A") self.stream_chat_states[stream_id] = subflow_entry.get("sub_chat_state", "N/A") self.stream_threshold_status[stream_id] = subflow_entry.get("is_above_threshold", False) - self.stream_last_active[stream_id] = subflow_entry.get("last_changed_state_time") # 存储原始时间戳 + self.stream_last_active[stream_id] = subflow_entry.get( + "last_changed_state_time" + ) # 存储原始时间戳 self.stream_last_interaction[stream_id] = subflow_entry.get( "last_interaction_time" ) # 存储原始时间戳 diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index c66a6128c..85b77579e 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -227,25 +227,23 @@ class BackgroundTaskManager: """ # 获取需要清理的子心流列表(包含ID和原因) flows_to_stop = self.subheartflow_manager.get_inactive_subheartflows() - + if not flows_to_stop: return # 没有需要清理的子心流直接返回 logger.info(f"准备删除 {len(flows_to_stop)} 个不活跃(1h)子心流") stopped_count = 0 - + # 逐个停止子心流 for flow_id in flows_to_stop: success = await self.subheartflow_manager.delete_subflow(flow_id) if success: stopped_count += 1 logger.debug(f"[清理任务] 已停止子心流 {flow_id}") - + # 记录最终清理结果 logger.info(f"[清理任务] 清理完成, 共停止 {stopped_count}/{len(flows_to_stop)} 个子心流") - - async def _perform_logging_work(self): """执行一轮状态日志记录。""" await self.interest_logger.log_all_states() diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py index 3f7fa0f12..7d92ae528 100644 --- a/src/heart_flow/heartflow.py +++ b/src/heart_flow/heartflow.py @@ -80,7 +80,6 @@ class Heartflow: # 不再需要传入 self.current_state return await self.subheartflow_manager.get_or_create_subheartflow(subheartflow_id) - async def heartflow_start_working(self): """启动后台任务""" await self.background_task_manager.start_tasks() diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index b6cbdd228..9cbd7b3a4 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -2,7 +2,7 @@ from .observation import Observation, ChattingObservation import asyncio from src.config.config import global_config import time -from typing import Optional, List, Dict, Callable, Tuple +from typing import Optional, List, Dict, Tuple import traceback from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 import random @@ -68,7 +68,7 @@ class InterestChatting: self.above_threshold = False self.start_hfc_probability = 0.0 - + async def initialize(self): async with self._task_lock: if self._is_running: @@ -84,7 +84,6 @@ class InterestChatting: self._is_running = True self.update_task = asyncio.create_task(self._run_update_loop(self.update_interval)) logger.debug("后台兴趣更新任务已创建并启动。") - def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool): self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) @@ -185,7 +184,6 @@ class InterestChatting: self._is_running = False logger.info("InterestChatting 更新循环已停止。") - async def stop_updates(self): """停止后台更新任务,使用锁确保并发安全""" async with self._task_lock: @@ -368,7 +366,7 @@ class SubHeartflow: async def change_chat_state(self, new_state: "ChatState"): """更新sub_heartflow的聊天状态,并管理 HeartFChatting 和 NormalChat 实例及任务""" current_state = self.chat_state.chat_status - + if current_state == new_state: return @@ -408,9 +406,11 @@ class SubHeartflow: if state_changed: self.update_last_chat_state_time() self.history_chat_state.append((current_state, self.chat_state_last_time)) - - logger.info(f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {self.chat_state_last_time} 秒) 变更为 {new_state.value}") - + + logger.info( + f"{log_prefix} 麦麦的聊天状态从 {current_state.value} (持续了 {self.chat_state_last_time} 秒) 变更为 {new_state.value}" + ) + self.chat_state.chat_status = new_state self.chat_state_last_time = 0 self.chat_state_changed_time = time.time() diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index d586fd43b..cd32136ae 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -11,7 +11,7 @@ from src.plugins.chat.chat_stream import chat_manager # 导入心流相关类 from src.heart_flow.sub_heartflow import SubHeartflow, ChatState -from src.heart_flow.mai_state_manager import MaiState, MaiStateInfo +from src.heart_flow.mai_state_manager import MaiStateInfo from .observation import ChattingObservation # 初始化日志记录器 @@ -32,15 +32,13 @@ class SubHeartflowManager: def __init__(self, mai_state_info: MaiStateInfo): self.subheartflows: Dict[Any, "SubHeartflow"] = {} self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问 - self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例 + self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例 def get_all_subheartflows(self) -> List["SubHeartflow"]: """获取所有当前管理的 SubHeartflow 实例列表 (快照)。""" return list(self.subheartflows.values()) - - async def get_or_create_subheartflow( - self, subheartflow_id: Any - ) -> Optional["SubHeartflow"]: + + async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: """获取或创建指定ID的子心流实例 Args: @@ -136,7 +134,7 @@ class SubHeartflowManager: absent_last_time = subheartflow.chat_state_last_time if max_age_seconds and (current_time - absent_last_time) > max_age_seconds: flows_to_stop.append(subheartflow_id) - + return flows_to_stop async def enforce_subheartflow_limits(self): @@ -251,21 +249,30 @@ class SubHeartflowManager: # 再次检查子心流是否仍然存在于管理器中,以防万一在迭代过程中被移除 if subflow.chat_state.chat_status != ChatState.ABSENT: - logger.debug(f"正在将子心流 {stream_name} 的状态从 {subflow.chat_state.chat_status.value} 更改为 ABSENT") + logger.debug( + f"正在将子心流 {stream_name} 的状态从 {subflow.chat_state.chat_status.value} 更改为 ABSENT" + ) try: # 调用 change_chat_state 将状态设置为 ABSENT await subflow.change_chat_state(ChatState.ABSENT) # 验证状态是否真的改变了 - if flow_id in self.subheartflows and self.subheartflows[flow_id].chat_state.chat_status == ChatState.ABSENT: + if ( + flow_id in self.subheartflows + and self.subheartflows[flow_id].chat_state.chat_status == ChatState.ABSENT + ): changed_count += 1 else: - logger.warning(f"[停用] 尝试更改子心流 {stream_name} 状态后,状态仍未变为 ABSENT 或子心流已消失。") + logger.warning( + f"[停用] 尝试更改子心流 {stream_name} 状态后,状态仍未变为 ABSENT 或子心流已消失。" + ) except Exception as e: logger.error(f"[停用] 更改子心流 {stream_name} 状态为 ABSENT 时出错: {e}", exc_info=True) else: logger.debug(f"[停用] 子心流 {stream_name} 已处于 ABSENT 状态,无需更改。") - logger.info(f"下限完成,共处理 {len(flows_to_update)} 个子心流,成功将 {changed_count} 个子心流的状态更改为 ABSENT。") + logger.info( + f"下限完成,共处理 {len(flows_to_update)} 个子心流,成功将 {changed_count} 个子心流的状态更改为 ABSENT。" + ) async def evaluate_interest_and_promote(self): """评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)""" @@ -440,4 +447,3 @@ class SubHeartflowManager: logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True) else: logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}") - From 06c9ad83edef71f8d4f1313745f7d94c125517f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Sat, 26 Apr 2025 14:23:22 +0800 Subject: [PATCH 48/79] =?UTF-8?q?999=E6=A8=A1=E5=BC=8F=E5=BF=98=E5=85=B3?= =?UTF-8?q?=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/mai_state_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index f8d4341eb..64fd40489 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -13,8 +13,8 @@ mai_state_config = LogConfig( logger = get_module_logger("mai_state_manager", config=mai_state_config) -enable_unlimited_hfc_chat = True -# enable_unlimited_hfc_chat = False +# enable_unlimited_hfc_chat = True +enable_unlimited_hfc_chat = False class MaiState(enum.Enum): From 628c6d1db314a7ac1ef5433169b12e91ee61bc34 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 06:28:09 +0000 Subject: [PATCH 49/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index ef38eb8dc..dc1e6a349 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1,6 +1,7 @@ import time import asyncio import datetime + # from .message_storage import MongoDBMessageStorage from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat from ...config.config import global_config @@ -76,7 +77,7 @@ class Conversation: raise try: logger.info(f"为 {self.stream_id} 加载初始聊天记录...") - initial_messages = await get_raw_msg_before_timestamp_with_chat( # + initial_messages = await get_raw_msg_before_timestamp_with_chat( # chat_id=self.stream_id, timestamp=time.time(), limit=30, # 加载最近30条作为初始上下文,可以调整 From 3ddd55e387069fb6a32b69ce17ce02003e8ac9cb Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 15:23:16 +0800 Subject: [PATCH 50/79] =?UTF-8?q?feat:=E6=8A=8ACHAT=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E4=BA=A4=E7=BB=99LLM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interest_monitor_gui.py | 2 +- src/heart_flow/README.md | 7 +- src/heart_flow/background_tasks.py | 33 ++- src/heart_flow/heartflow.py | 1 - src/heart_flow/interest_logger.py | 2 +- src/heart_flow/sub_heartflow.py | 2 +- src/heart_flow/subheartflow_manager.py | 261 +++++++++++++----- .../heartFC_chat/heartflow_processor.py | 2 +- 8 files changed, 218 insertions(+), 92 deletions(-) diff --git a/interest_monitor_gui.py b/interest_monitor_gui.py index 245a0ae99..fb9e51cf8 100644 --- a/interest_monitor_gui.py +++ b/interest_monitor_gui.py @@ -247,7 +247,7 @@ class InterestMonitorApp: self.stream_chat_states[stream_id] = subflow_entry.get("sub_chat_state", "N/A") self.stream_threshold_status[stream_id] = subflow_entry.get("is_above_threshold", False) self.stream_last_active[stream_id] = subflow_entry.get( - "last_changed_state_time" + "chat_state_changed_time" ) # 存储原始时间戳 self.stream_last_interaction[stream_id] = subflow_entry.get( "last_interaction_time" diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index 24d094cc6..a2540ebf1 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -105,7 +105,8 @@ c HeartFChatting工作方式 - 负责所有 `SubHeartflow` 实例的生命周期管理,包括: - 创建和获取 (`get_or_create_subheartflow`)。 - 停止和清理 (`sleep_subheartflow`, `cleanup_inactive_subheartflows`)。 - - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `activate_random_subflows_to_chat`, `evaluate_interest_and_promote`)。 + - 根据 `Heartflow` 的状态 (`self.mai_state_info`) 和限制条件,激活、停用或调整子心流的状态(例如 `enforce_subheartflow_limits`, `randomly_deactivate_subflows`, `evaluate_interest_and_promote`)。 + - **新增**: 通过调用 `evaluate_and_transition_subflows_by_llm` 方法,使用 LLM (配置与 `Heartflow` 主 LLM 相同) 评估处于 `ABSENT` 或 `CHAT` 状态的子心流,根据观察到的活动摘要和 `Heartflow` 的当前状态,判断是否应在 `ABSENT` 和 `CHAT` 之间进行转换 (同样受限于 `CHAT` 状态的数量上限)。 - **清理机制**: 通过后台任务 (`BackgroundTaskManager`) 定期调用 `cleanup_inactive_subheartflows` 方法,此方法会识别并**删除**那些处于 `ABSENT` 状态超过一小时 (`INACTIVE_THRESHOLD_SECONDS`) 的子心流实例。 ### 1.5. 消息处理与回复流程 (Message Processing vs. Replying Flow) @@ -149,9 +150,9 @@ c HeartFChatting工作方式 * `ChatState.FOCUSED` (专注/认真水群): 专注聊天模式。激活 `HeartFlowChatInstance`。 - **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。 - **状态转换机制** (由 `SubHeartflowManager` 驱动): - - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。 + - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。此外,`evaluate_and_transition_subflows_by_llm` 方法也会根据 LLM 的判断,在未达上限时将 `ABSENT` 状态的子心流激活为 `CHAT`。 - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**(通过 `self.mai_state_info` 获取限制),则调用 `change_chat_state` 将其提升为 `FOCUSED`。 - - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃或随机概率等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。 + - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃、随机概率 (`randomly_deactivate_subflows`) 或 LLM 评估 (`evaluate_and_transition_subflows_by_llm` 判断 `CHAT` 状态子心流应休眠) 等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。 - **注意**: `change_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法,这些方法会使用内部存储的 `mai_state_info` 来获取限制)承担。 ## 3. 聊天实例详解 (Chat Instances Explained) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 85b77579e..a1a226684 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -34,7 +34,6 @@ class BackgroundTaskManager: update_interval: int, cleanup_interval: int, log_interval: int, - inactive_threshold: int, # 新增兴趣评估间隔参数 interest_eval_interval: int = INTEREST_EVAL_INTERVAL_SECONDS, # 新增随机停用间隔参数 @@ -58,6 +57,7 @@ class BackgroundTaskManager: self._logging_task: Optional[asyncio.Task] = None self._interest_eval_task: Optional[asyncio.Task] = None # 新增兴趣评估任务引用 self._random_deactivation_task: Optional[asyncio.Task] = None # 新增随机停用任务引用 + self._hf_judge_state_update_task: Optional[asyncio.Task] = None # 新增状态评估任务引用 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks async def start_tasks(self): @@ -79,12 +79,20 @@ class BackgroundTaskManager: f"聊天状态更新任务已启动 间隔:{self.update_interval}s", "_state_update_task", ), + ( + self._hf_judge_state_update_task, + lambda: self._run_hf_judge_state_update_cycle(300), + "hf_judge_state_update", + "debug", + f"状态评估任务已启动 间隔:{300}s", + "_hf_judge_state_update_task", + ), ( self._cleanup_task, self._run_cleanup_cycle, "hf_cleanup", "info", - f"清理任务已启动 间隔:{self.cleanup_interval}s 阈值:{self.inactive_threshold}s", + f"清理任务已启动 间隔:{self.cleanup_interval}s", "_cleanup_task", ), ( @@ -203,21 +211,21 @@ class BackgroundTaskManager: if state_changed: current_state = self.mai_state_info.get_current_state() - await self.subheartflow_manager.enforce_subheartflow_limits(current_state) + await self.subheartflow_manager.enforce_subheartflow_limits() # 状态转换处理 + if ( - previous_status == self.mai_state_info.mai_status.OFFLINE - and current_state != self.mai_state_info.mai_status.OFFLINE - ): - logger.info("[后台任务] 主状态激活,触发子流激活") - await self.subheartflow_manager.activate_random_subflows_to_chat(current_state) - elif ( current_state == self.mai_state_info.mai_status.OFFLINE and previous_status != self.mai_state_info.mai_status.OFFLINE ): logger.info("检测到离线,停用所有子心流") await self.subheartflow_manager.deactivate_all_subflows() + + async def _perform_hf_judge_state_update_work(self): + """调用llm检测是否转换ABSENT-CHAT状态""" + logger.info("[状态评估任务] 开始基于LLM评估子心流状态...") + await self.subheartflow_manager.evaluate_and_transition_subflows_by_llm() async def _perform_cleanup_work(self): """执行子心流清理任务 @@ -252,7 +260,7 @@ class BackgroundTaskManager: async def _perform_interest_eval_work(self): """执行一轮子心流兴趣评估与提升检查。""" # 直接调用 subheartflow_manager 的方法,并传递当前状态信息 - await self.subheartflow_manager.evaluate_interest_and_promote(self.mai_state_info) + await self.subheartflow_manager.evaluate_interest_and_promote() # --- 结束新增 --- @@ -268,6 +276,11 @@ class BackgroundTaskManager: await self._run_periodic_loop( task_name="State Update", interval=interval, task_func=self._perform_state_update_work ) + + async def _run_hf_judge_state_update_cycle(self, interval: int): + await self._run_periodic_loop( + task_name="State Update", interval=interval, task_func=self._perform_hf_judge_state_update_work + ) async def _run_cleanup_cycle(self): await self._run_periodic_loop( diff --git a/src/heart_flow/heartflow.py b/src/heart_flow/heartflow.py index 7d92ae528..a0fb8e4fa 100644 --- a/src/heart_flow/heartflow.py +++ b/src/heart_flow/heartflow.py @@ -72,7 +72,6 @@ class Heartflow: update_interval=STATE_UPDATE_INTERVAL_SECONDS, cleanup_interval=CLEANUP_INTERVAL_SECONDS, log_interval=3, # Example: Using value directly, ideally get from config - inactive_threshold=INACTIVE_THRESHOLD_SECONDS, ) async def get_or_create_subheartflow(self, subheartflow_id: Any) -> Optional["SubHeartflow"]: diff --git a/src/heart_flow/interest_logger.py b/src/heart_flow/interest_logger.py index 7802f87b4..d4e746e63 100644 --- a/src/heart_flow/interest_logger.py +++ b/src/heart_flow/interest_logger.py @@ -58,7 +58,7 @@ class InterestLogger: return results for subheartflow in all_flows: - if self.subheartflow_manager.get_or_create_subheartflow(subheartflow.subheartflow_id): + if await self.subheartflow_manager.get_or_create_subheartflow(subheartflow.subheartflow_id): tasks.append( asyncio.create_task(subheartflow.get_full_state(), name=f"get_state_{subheartflow.subheartflow_id}") ) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 9cbd7b3a4..33218f5fd 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -481,7 +481,7 @@ class SubHeartflow: "interest_state": interest_state, "current_mind": self.sub_mind.current_mind, "chat_state": self.chat_state.chat_status.value, - "last_changed_state_time": self.last_changed_state_time, + "chat_state_changed_time": self.chat_state_changed_time, } async def shutdown(self): diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index cd32136ae..33fdae4e4 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -12,7 +12,11 @@ from src.plugins.chat.chat_stream import chat_manager # 导入心流相关类 from src.heart_flow.sub_heartflow import SubHeartflow, ChatState from src.heart_flow.mai_state_manager import MaiStateInfo -from .observation import ChattingObservation +from .observation import ChattingObservation, Observation + +# 导入LLM请求工具 +from src.plugins.models.utils_model import LLMRequest +from src.config.config import global_config # 初始化日志记录器 @@ -34,6 +38,15 @@ class SubHeartflowManager: self._lock = asyncio.Lock() # 用于保护 self.subheartflows 的访问 self.mai_state_info: MaiStateInfo = mai_state_info # 存储传入的 MaiStateInfo 实例 + # 为 LLM 状态评估创建一个 LLMRequest 实例 + # 使用与 Heartflow 相同的模型和参数 + self.llm_state_evaluator = LLMRequest( + model=global_config.llm_heartflow, # 与 Heartflow 一致 + temperature=0.6, # 与 Heartflow 一致 + max_tokens=1000, # 与 Heartflow 一致 (虽然可能不需要这么多) + request_type="subheartflow_state_eval" # 保留特定的请求类型 + ) + def get_all_subheartflows(self) -> List["SubHeartflow"]: """获取所有当前管理的 SubHeartflow 实例列表 (快照)。""" return list(self.subheartflows.values()) @@ -103,24 +116,6 @@ class SubHeartflowManager: except Exception as e: logger.error(f"[子心流管理] 设置ABSENT状态失败: {e}") - # 停止子心流内部循环 - subheartflow.should_stop = True - - # 取消后台任务 - task = subheartflow.task - if task and not task.done(): - task.cancel() - logger.debug(f"[子心流管理] 已取消 {stream_name} 的后台任务") - - # 从管理字典中移除 - if subheartflow_id in self.subheartflows: - del self.subheartflows[subheartflow_id] - logger.debug(f"[子心流管理] 已移除 {stream_name}") - return True - else: - logger.warning(f"[子心流管理] {stream_name} 已被提前移除") - return False - def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): """识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)""" current_time = time.time() @@ -185,52 +180,6 @@ class SubHeartflowManager: else: logger.debug(f"[限制] 无需停止, 当前总数:{len(self.subheartflows)}") - async def activate_random_subflows_to_chat(self): - """主状态激活时,随机选择ABSENT子心流进入CHAT状态""" - # 使用 self.mai_state_info 获取当前状态和限制 - current_mai_state = self.mai_state_info.get_current_state() - limit = current_mai_state.get_normal_chat_max_num() - if limit <= 0: - logger.info("[激活] 当前状态不允许CHAT子心流") - return - - # 获取所有ABSENT状态的子心流 - absent_flows = [flow for flow in self.subheartflows.values() if flow.chat_state.chat_status == ChatState.ABSENT] - - num_to_activate = min(limit, len(absent_flows)) - if num_to_activate <= 0: - logger.info(f"[激活] 无可用ABSENT子心流(限额:{limit}, 可用:{len(absent_flows)})") - return - - logger.info(f"[激活] 随机选择{num_to_activate}个ABSENT子心流进入CHAT状态") - activated_count = 0 - - for flow in random.sample(absent_flows, num_to_activate): - flow_id = flow.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id - - if flow_id not in self.subheartflows: - logger.warning(f"[激活] 跳过{stream_name}, 子心流已不存在") - continue - - logger.debug(f"[激活] 正在激活子心流{stream_name}") - - # --- 限额检查 --- # - current_chat_count = self.count_subflows_by_state(ChatState.CHAT) - if current_chat_count >= limit: - logger.warning(f"[激活] 跳过{stream_name}, 普通聊天已达上限 ({current_chat_count}/{limit})") - continue # 跳过此子心流,继续尝试激活下一个 - # --- 结束限额检查 --- # - - # 移除 states_num 参数 - await flow.change_chat_state(ChatState.CHAT) - - if flow.chat_state.chat_status == ChatState.CHAT: - activated_count += 1 - else: - logger.warning(f"[激活] {stream_name}状态设置失败") - - logger.info(f"[激活] 完成, 成功激活{activated_count}个子心流") async def deactivate_all_subflows(self): """将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)""" @@ -394,15 +343,172 @@ class SubHeartflowManager: else: logger.debug(f"{log_prefix_manager} 随机停用周期结束, 未停用任何子心流。") - def count_subflows_by_state(self, state: ChatState) -> int: - """统计指定状态的子心流数量 + + async def evaluate_and_transition_subflows_by_llm(self): + """ + 使用LLM评估每个子心流的状态,并根据LLM的判断执行状态转换(ABSENT <-> CHAT)。 + 注意:此函数包含对假设的LLM函数的调用。 + """ + log_prefix = "[LLM状态评估]" + logger.info(f"{log_prefix} 开始基于LLM评估子心流状态...") + + # 获取当前状态和限制,用于CHAT激活检查 + current_mai_state = self.mai_state_info.get_current_state() + chat_limit = current_mai_state.get_normal_chat_max_num() + + transitioned_to_chat = 0 + transitioned_to_absent = 0 + + async with self._lock: # 在锁内获取快照并迭代 + subflows_snapshot = list(self.subheartflows.values()) + # 使用不上锁的版本,因为我们已经在锁内 + current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) + + if not subflows_snapshot: + logger.info(f"{log_prefix} 当前没有子心流需要评估。") + return + + for sub_hf in subflows_snapshot: + flow_id = sub_hf.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + current_subflow_state = sub_hf.chat_state.chat_status + + # --- 获取观察内容 --- + # 从 sub_hf.observations 获取 ChattingObservation 并提取信息 + observation_summary = "没有可用的观察信息。" # 默认值 + try: + # 检查 observations 列表是否存在且不为空 + + # 假设第一个观察者是 ChattingObservation + first_observation = sub_hf.observations[0] + if isinstance(first_observation, ChattingObservation): + # 组合中期记忆和当前聊天内容 + current_chat = first_observation.talking_message_str or "当前无聊天内容。" + combined_summary = f"当前聊天内容:\n{current_chat}" + else: + logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。") + + + except Exception as e: + logger.warning(f"{log_prefix} [{stream_name}] 获取观察信息失败: {e}", exc_info=True) + # 保留默认值或错误信息 + combined_summary = f"获取观察信息时出错: {e}" + + + # --- 获取麦麦状态 --- + mai_state_description = f"麦麦当前状态: {current_mai_state.value}。" + + # --- 针对 ABSENT 状态 --- + if current_subflow_state == ChatState.ABSENT: + # 构建Prompt + prompt = ( + f"子心流 [{stream_name}] 当前处于非活跃(ABSENT)状态。\n" + f"{mai_state_description}\n" + f"最近观察到的内容摘要:\n---\n{combined_summary}\n---\n" + f"基于以上信息,该子心流是否表现出足够的活跃迹象或重要性," + f"值得将其唤醒并进入常规聊天(CHAT)状态?" + f"请回答 '是' 或 '否'。" + ) + + # 调用LLM评估 + try: + # 使用 self._llm_evaluate_state_transition + should_activate = await self._llm_evaluate_state_transition(prompt) + if should_activate: + # 检查CHAT限额 + if current_chat_count < chat_limit: + logger.info(f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换...") + await sub_hf.change_chat_state(ChatState.CHAT) + if sub_hf.chat_state.chat_status == ChatState.CHAT: + transitioned_to_chat += 1 + current_chat_count += 1 # 更新计数器 + else: + logger.warning(f"{log_prefix} [{stream_name}] 尝试激活到CHAT失败。") + else: + logger.info(f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。") + except Exception as e: + logger.error(f"{log_prefix} [{stream_name}] LLM评估或状态转换(ABSENT->CHAT)时出错: {e}", exc_info=True) + + + # --- 针对 CHAT 状态 --- + elif current_subflow_state == ChatState.CHAT: + # 构建Prompt + prompt = ( + f"子心流 [{stream_name}] 当前处于常规聊天(CHAT)状态。\n" + f"{mai_state_description}\n" + f"最近观察到的内容摘要:\n---\n{combined_summary}\n---\n" + f"基于以上信息,该子心流是否表现出不活跃、对话结束或不再需要关注的迹象," + f"应该让其进入休眠(ABSENT)状态?" + f"请回答 '是' 或 '否'。" + ) + + # 调用LLM评估 + try: + # 使用 self._llm_evaluate_state_transition + should_deactivate = await self._llm_evaluate_state_transition(prompt) + if should_deactivate: + logger.info(f"{log_prefix} [{stream_name}] LLM建议进入ABSENT状态。正在尝试转换...") + await sub_hf.change_chat_state(ChatState.ABSENT) + if sub_hf.chat_state.chat_status == ChatState.ABSENT: + transitioned_to_absent += 1 + current_chat_count -= 1 # 更新计数器 + else: + logger.warning(f"{log_prefix} [{stream_name}] 尝试转换为ABSENT失败。") + except Exception as e: + logger.error(f"{log_prefix} [{stream_name}] LLM评估或状态转换(CHAT->ABSENT)时出错: {e}", exc_info=True) + + # 可以选择性地为 FOCUSED 状态添加评估逻辑,例如判断是否降级回 CHAT 或 ABSENT + + logger.info( + f"{log_prefix} LLM评估周期结束。" + f" 成功转换到CHAT: {transitioned_to_chat}." + f" 成功转换到ABSENT: {transitioned_to_absent}." + ) + + + async def _llm_evaluate_state_transition(self, prompt: str) -> bool: + """ + 使用 LLM 评估是否应进行状态转换。 Args: - state: 要统计的聊天状态枚举值 + prompt: 提供给 LLM 的提示信息。 Returns: - int: 处于该状态的子心流数量 + bool: True 表示应该转换,False 表示不应该转换。 """ + log_prefix = "[LLM状态评估]" + try: + # --- 真实的 LLM 调用 --- + response_text, _, model_name = await self.llm_state_evaluator.generate_response_async(prompt) + logger.debug(f"{log_prefix} 使用模型 {model_name} 评估,原始响应: {response_text}") + # 解析响应 - 这里需要根据你的LLM的确切输出来调整逻辑 + # 假设 LLM 会明确回答 "是" 或 "否" + if response_text and "是" in response_text.strip(): + logger.debug(f"{log_prefix} LLM评估结果: 建议转换 (响应包含 '是')") + return True + elif response_text and "否" in response_text.strip(): + logger.debug(f"{log_prefix} LLM评估结果: 建议不转换 (响应包含 '否')") + return False + else: + logger.warning(f"{log_prefix} LLM 未明确回答 '是' 或 '否',响应: {response_text}") + # 可以设定一个默认行为,例如默认不转换 + return False + # --- 真实的 LLM 调用结束 --- + + # # --- 占位符逻辑:随机返回 True/False --- + # # 请在接入真实 LLM 后移除此部分 + # await asyncio.sleep(0.1) # 模拟LLM调用延迟 + # result = random.choice([True, False]) + # logger.debug(f"{log_prefix} (占位符) LLM评估结果: {'建议转换' if result else '建议不转换'}") + # return result + # # --- 占位符逻辑结束 --- + + except Exception as e: + logger.error(f"{log_prefix} 调用 LLM 进行状态评估时出错: {e}", exc_info=True) + + + def count_subflows_by_state(self, state: ChatState) -> int: + """统计指定状态的子心流数量""" count = 0 # 遍历所有子心流实例 for subheartflow in self.subheartflows.values(): @@ -411,12 +517,19 @@ class SubHeartflowManager: count += 1 return count - def get_active_subflow_minds(self) -> List[str]: - """获取所有活跃(非ABSENT)子心流的当前想法 - - 返回: - List[str]: 包含所有活跃子心流当前想法的列表 + def count_subflows_by_state_nolock(self, state: ChatState) -> int: """ + 统计指定状态的子心流数量 (不上锁版本)。 + 警告:仅应在已持有 self._lock 的上下文中使用此方法。 + """ + count = 0 + for subheartflow in self.subheartflows.values(): + if subheartflow.chat_state.chat_status == state: + count += 1 + return count + + def get_active_subflow_minds(self) -> List[str]: + """获取所有活跃(非ABSENT)子心流的当前想法""" minds = [] for subheartflow in self.subheartflows.values(): # 检查子心流是否活跃(非ABSENT状态) diff --git a/src/plugins/heartFC_chat/heartflow_processor.py b/src/plugins/heartFC_chat/heartflow_processor.py index 1f771688d..c907f98a8 100644 --- a/src/plugins/heartFC_chat/heartflow_processor.py +++ b/src/plugins/heartFC_chat/heartflow_processor.py @@ -167,7 +167,7 @@ class HeartFCProcessor: # 6. 兴趣度计算与更新 interested_rate, is_mentioned = await self._calculate_interest(message) await subheartflow.interest_chatting.increase_interest(value=interested_rate) - await subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned) + subheartflow.interest_chatting.add_interest_dict(message, interested_rate, is_mentioned) # 7. 日志记录 mes_name = chat.group_info.group_name if chat.group_info else "私聊" From 5f4b38e4ddb359f2757505926bbfd8bfd3d7dd51 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 15:31:48 +0800 Subject: [PATCH 51/79] fix --- src/plugins/PFC/conversation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index dc1e6a349..83081e80e 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -77,7 +77,7 @@ class Conversation: raise try: logger.info(f"为 {self.stream_id} 加载初始聊天记录...") - initial_messages = await get_raw_msg_before_timestamp_with_chat( # + initial_messages = get_raw_msg_before_timestamp_with_chat( # chat_id=self.stream_id, timestamp=time.time(), limit=30, # 加载最近30条作为初始上下文,可以调整 From e52959d838f29722798daf74931e631193fd88cc Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 15:40:12 +0800 Subject: [PATCH 52/79] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8Dllm=E7=88=86?= =?UTF-8?q?=E7=82=B8=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/background_tasks.py | 10 ++-- src/heart_flow/subheartflow_manager.py | 64 +++++++++++------------- src/plugins/memory_system/Hippocampus.py | 6 +-- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index a1a226684..7ae4b62f9 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -57,7 +57,7 @@ class BackgroundTaskManager: self._logging_task: Optional[asyncio.Task] = None self._interest_eval_task: Optional[asyncio.Task] = None # 新增兴趣评估任务引用 self._random_deactivation_task: Optional[asyncio.Task] = None # 新增随机停用任务引用 - self._hf_judge_state_update_task: Optional[asyncio.Task] = None # 新增状态评估任务引用 + self._hf_judge_state_update_task: Optional[asyncio.Task] = None # 新增状态评估任务引用 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks async def start_tasks(self): @@ -81,10 +81,10 @@ class BackgroundTaskManager: ), ( self._hf_judge_state_update_task, - lambda: self._run_hf_judge_state_update_cycle(300), + lambda: self._run_hf_judge_state_update_cycle(60), "hf_judge_state_update", "debug", - f"状态评估任务已启动 间隔:{300}s", + f"状态评估任务已启动 间隔:{60}s", "_hf_judge_state_update_task", ), ( @@ -221,7 +221,7 @@ class BackgroundTaskManager: ): logger.info("检测到离线,停用所有子心流") await self.subheartflow_manager.deactivate_all_subflows() - + async def _perform_hf_judge_state_update_work(self): """调用llm检测是否转换ABSENT-CHAT状态""" logger.info("[状态评估任务] 开始基于LLM评估子心流状态...") @@ -276,7 +276,7 @@ class BackgroundTaskManager: await self._run_periodic_loop( task_name="State Update", interval=interval, task_func=self._perform_state_update_work ) - + async def _run_hf_judge_state_update_cycle(self, interval: int): await self._run_periodic_loop( task_name="State Update", interval=interval, task_func=self._perform_hf_judge_state_update_work diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 33fdae4e4..9357ff3b6 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -12,12 +12,14 @@ from src.plugins.chat.chat_stream import chat_manager # 导入心流相关类 from src.heart_flow.sub_heartflow import SubHeartflow, ChatState from src.heart_flow.mai_state_manager import MaiStateInfo -from .observation import ChattingObservation, Observation +from .observation import ChattingObservation # 导入LLM请求工具 from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config +import traceback + # 初始化日志记录器 subheartflow_manager_log_config = LogConfig( @@ -41,10 +43,10 @@ class SubHeartflowManager: # 为 LLM 状态评估创建一个 LLMRequest 实例 # 使用与 Heartflow 相同的模型和参数 self.llm_state_evaluator = LLMRequest( - model=global_config.llm_heartflow, # 与 Heartflow 一致 - temperature=0.6, # 与 Heartflow 一致 - max_tokens=1000, # 与 Heartflow 一致 (虽然可能不需要这么多) - request_type="subheartflow_state_eval" # 保留特定的请求类型 + model=global_config.llm_heartflow, # 与 Heartflow 一致 + temperature=0.6, # 与 Heartflow 一致 + max_tokens=1000, # 与 Heartflow 一致 (虽然可能不需要这么多) + request_type="subheartflow_state_eval", # 保留特定的请求类型 ) def get_all_subheartflows(self) -> List["SubHeartflow"]: @@ -87,7 +89,7 @@ class SubHeartflowManager: # 注册子心流 self.subheartflows[subheartflow_id] = new_subflow heartflow_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id - logger.info(f"[{heartflow_name}] 开始看消息") + logger.info(f"[{heartflow_name}] 开始接收消息") # 启动后台任务 asyncio.create_task(new_subflow.subheartflow_start_working()) @@ -180,7 +182,6 @@ class SubHeartflowManager: else: logger.debug(f"[限制] 无需停止, 当前总数:{len(self.subheartflows)}") - async def deactivate_all_subflows(self): """将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)""" # logger.info("[停用] 开始将所有子心流状态设置为 ABSENT") @@ -343,7 +344,6 @@ class SubHeartflowManager: else: logger.debug(f"{log_prefix_manager} 随机停用周期结束, 未停用任何子心流。") - async def evaluate_and_transition_subflows_by_llm(self): """ 使用LLM评估每个子心流的状态,并根据LLM的判断执行状态转换(ABSENT <-> CHAT)。 @@ -359,7 +359,7 @@ class SubHeartflowManager: transitioned_to_chat = 0 transitioned_to_absent = 0 - async with self._lock: # 在锁内获取快照并迭代 + async with self._lock: # 在锁内获取快照并迭代 subflows_snapshot = list(self.subheartflows.values()) # 使用不上锁的版本,因为我们已经在锁内 current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) @@ -375,26 +375,24 @@ class SubHeartflowManager: # --- 获取观察内容 --- # 从 sub_hf.observations 获取 ChattingObservation 并提取信息 - observation_summary = "没有可用的观察信息。" # 默认值 + _observation_summary = "没有可用的观察信息。" # 默认值 try: # 检查 observations 列表是否存在且不为空 - # 假设第一个观察者是 ChattingObservation + # 假设第一个观察者是 ChattingObservation first_observation = sub_hf.observations[0] if isinstance(first_observation, ChattingObservation): # 组合中期记忆和当前聊天内容 current_chat = first_observation.talking_message_str or "当前无聊天内容。" combined_summary = f"当前聊天内容:\n{current_chat}" else: - logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。") - + logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。") except Exception as e: logger.warning(f"{log_prefix} [{stream_name}] 获取观察信息失败: {e}", exc_info=True) # 保留默认值或错误信息 combined_summary = f"获取观察信息时出错: {e}" - # --- 获取麦麦状态 --- mai_state_description = f"麦麦当前状态: {current_mai_state.value}。" @@ -417,18 +415,23 @@ class SubHeartflowManager: if should_activate: # 检查CHAT限额 if current_chat_count < chat_limit: - logger.info(f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换...") + logger.info( + f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..." + ) await sub_hf.change_chat_state(ChatState.CHAT) if sub_hf.chat_state.chat_status == ChatState.CHAT: transitioned_to_chat += 1 - current_chat_count += 1 # 更新计数器 + current_chat_count += 1 # 更新计数器 else: logger.warning(f"{log_prefix} [{stream_name}] 尝试激活到CHAT失败。") else: - logger.info(f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。") + logger.info( + f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。" + ) except Exception as e: - logger.error(f"{log_prefix} [{stream_name}] LLM评估或状态转换(ABSENT->CHAT)时出错: {e}", exc_info=True) - + logger.error( + f"{log_prefix} [{stream_name}] LLM评估或状态转换(ABSENT->CHAT)时出错: {e}", exc_info=True + ) # --- 针对 CHAT 状态 --- elif current_subflow_state == ChatState.CHAT: @@ -451,11 +454,13 @@ class SubHeartflowManager: await sub_hf.change_chat_state(ChatState.ABSENT) if sub_hf.chat_state.chat_status == ChatState.ABSENT: transitioned_to_absent += 1 - current_chat_count -= 1 # 更新计数器 + current_chat_count -= 1 # 更新计数器 else: logger.warning(f"{log_prefix} [{stream_name}] 尝试转换为ABSENT失败。") except Exception as e: - logger.error(f"{log_prefix} [{stream_name}] LLM评估或状态转换(CHAT->ABSENT)时出错: {e}", exc_info=True) + logger.error( + f"{log_prefix} [{stream_name}] LLM评估或状态转换(CHAT->ABSENT)时出错: {e}", exc_info=True + ) # 可以选择性地为 FOCUSED 状态添加评估逻辑,例如判断是否降级回 CHAT 或 ABSENT @@ -465,7 +470,6 @@ class SubHeartflowManager: f" 成功转换到ABSENT: {transitioned_to_absent}." ) - async def _llm_evaluate_state_transition(self, prompt: str) -> bool: """ 使用 LLM 评估是否应进行状态转换。 @@ -479,8 +483,8 @@ class SubHeartflowManager: log_prefix = "[LLM状态评估]" try: # --- 真实的 LLM 调用 --- - response_text, _, model_name = await self.llm_state_evaluator.generate_response_async(prompt) - logger.debug(f"{log_prefix} 使用模型 {model_name} 评估,原始响应: {response_text}") + response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt) + logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估,原始响应: {response_text}") # 解析响应 - 这里需要根据你的LLM的确切输出来调整逻辑 # 假设 LLM 会明确回答 "是" 或 "否" if response_text and "是" in response_text.strip(): @@ -493,19 +497,11 @@ class SubHeartflowManager: logger.warning(f"{log_prefix} LLM 未明确回答 '是' 或 '否',响应: {response_text}") # 可以设定一个默认行为,例如默认不转换 return False - # --- 真实的 LLM 调用结束 --- - - # # --- 占位符逻辑:随机返回 True/False --- - # # 请在接入真实 LLM 后移除此部分 - # await asyncio.sleep(0.1) # 模拟LLM调用延迟 - # result = random.choice([True, False]) - # logger.debug(f"{log_prefix} (占位符) LLM评估结果: {'建议转换' if result else '建议不转换'}") - # return result - # # --- 占位符逻辑结束 --- except Exception as e: logger.error(f"{log_prefix} 调用 LLM 进行状态评估时出错: {e}", exc_info=True) - + traceback.print_exc() + return False def count_subflows_by_state(self, state: ChatState) -> int: """统计指定状态的子心流数量""" diff --git a/src/plugins/memory_system/Hippocampus.py b/src/plugins/memory_system/Hippocampus.py index 738e47c4e..1e8ad8850 100644 --- a/src/plugins/memory_system/Hippocampus.py +++ b/src/plugins/memory_system/Hippocampus.py @@ -404,7 +404,7 @@ class Hippocampus: # logger.info("没有找到有效的关键词节点") return [] - logger.info(f"有效的关键词: {', '.join(valid_keywords)}") + logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 all_memories = [] @@ -576,7 +576,7 @@ class Hippocampus: # logger.info("没有找到有效的关键词节点") return [] - logger.info(f"有效的关键词: {', '.join(valid_keywords)}") + logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 all_memories = [] @@ -761,7 +761,7 @@ class Hippocampus: # logger.info("没有找到有效的关键词节点") return 0 - logger.info(f"有效的关键词: {', '.join(valid_keywords)}") + logger.debug(f"有效的关键词: {', '.join(valid_keywords)}") # 从每个关键词获取记忆 activate_map = {} # 存储每个词的累计激活值 From 042e969292c50107ade9d4634d781ba0559db124 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 17:35:23 +0800 Subject: [PATCH 53/79] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E9=BA=A6?= =?UTF-8?q?=E9=BA=A6=E5=9B=9E=E5=A4=8D=E8=BF=87=E5=8E=BB=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_heartflow.py | 18 +++ src/heart_flow/subheartflow_manager.py | 143 ++++++++++-------- src/plugins/heartFC_chat/heartFC_chat.py | 16 +- .../heartFC_chat/heartflow_prompt_builder.py | 1 + src/plugins/heartFC_chat/normal_chat.py | 67 +++++++- 5 files changed, 172 insertions(+), 73 deletions(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 33218f5fd..efd0ea1ed 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -86,8 +86,26 @@ class InterestChatting: logger.debug("后台兴趣更新任务已创建并启动。") def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool): + """添加消息到兴趣字典 + + 参数: + message: 接收到的消息 + interest_value: 兴趣值 + is_mentioned: 是否被提及 + + 功能: + 1. 将消息添加到兴趣字典 + 2. 更新最后交互时间 + 3. 如果字典长度超过10,删除最旧的消息 + """ + # 添加新消息 self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) self.last_interaction_time = time.time() + + # 如果字典长度超过10,删除最旧的消息 + if len(self.interest_dict) > 10: + oldest_key = next(iter(self.interest_dict)) + self.interest_dict.pop(oldest_key) async def _calculate_decay(self): """计算兴趣值的衰减 diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 9357ff3b6..62d9e2f7b 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -2,6 +2,7 @@ import asyncio import time import random from typing import Dict, Any, Optional, List +import json # 导入 json 模块 # 导入日志模块 from src.common.logger import get_module_logger, LogConfig, SUBHEARTFLOW_MANAGER_STYLE_CONFIG @@ -400,69 +401,65 @@ class SubHeartflowManager: if current_subflow_state == ChatState.ABSENT: # 构建Prompt prompt = ( - f"子心流 [{stream_name}] 当前处于非活跃(ABSENT)状态。\n" + f"子心流 [{stream_name}] 当前处于非活跃(ABSENT)状态.\n" f"{mai_state_description}\n" f"最近观察到的内容摘要:\n---\n{combined_summary}\n---\n" f"基于以上信息,该子心流是否表现出足够的活跃迹象或重要性," - f"值得将其唤醒并进入常规聊天(CHAT)状态?" - f"请回答 '是' 或 '否'。" + f"值得将其唤醒并进入常规聊天(CHAT)状态?\n" + f"请以 JSON 格式回答,包含一个键 'decision',其值为 true 或 false.\n" + f"例如:{{\"decision\": true}}\n" + f"请只输出有效的 JSON 对象。" ) # 调用LLM评估 - try: - # 使用 self._llm_evaluate_state_transition - should_activate = await self._llm_evaluate_state_transition(prompt) - if should_activate: - # 检查CHAT限额 - if current_chat_count < chat_limit: - logger.info( - f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..." - ) - await sub_hf.change_chat_state(ChatState.CHAT) - if sub_hf.chat_state.chat_status == ChatState.CHAT: - transitioned_to_chat += 1 - current_chat_count += 1 # 更新计数器 - else: - logger.warning(f"{log_prefix} [{stream_name}] 尝试激活到CHAT失败。") + should_activate = await self._llm_evaluate_state_transition(prompt) + if should_activate is None: # 处理解析失败或意外情况 + logger.warning(f"{log_prefix} [{stream_name}] LLM评估返回无效结果,跳过。") + continue + + if should_activate: + # 检查CHAT限额 + # 使用不上锁的版本,因为我们已经在锁内 + current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) + if current_chat_count < chat_limit: + logger.info( + f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..." + ) + await sub_hf.change_chat_state(ChatState.CHAT) + if sub_hf.chat_state.chat_status == ChatState.CHAT: + transitioned_to_chat += 1 else: - logger.info( - f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。" - ) - except Exception as e: - logger.error( - f"{log_prefix} [{stream_name}] LLM评估或状态转换(ABSENT->CHAT)时出错: {e}", exc_info=True - ) + logger.warning(f"{log_prefix} [{stream_name}] 尝试激活到CHAT失败。") + else: + logger.info( + f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。" + ) # --- 针对 CHAT 状态 --- elif current_subflow_state == ChatState.CHAT: # 构建Prompt prompt = ( - f"子心流 [{stream_name}] 当前处于常规聊天(CHAT)状态。\n" + f"子心流 [{stream_name}] 当前处于常规聊天(CHAT)状态.\n" f"{mai_state_description}\n" f"最近观察到的内容摘要:\n---\n{combined_summary}\n---\n" f"基于以上信息,该子心流是否表现出不活跃、对话结束或不再需要关注的迹象," - f"应该让其进入休眠(ABSENT)状态?" - f"请回答 '是' 或 '否'。" + f"应该让其进入休眠(ABSENT)状态?\n" + f"请以 JSON 格式回答,包含一个键 'decision',其值为 true (表示应休眠) 或 false (表示不应休眠).\n" + f"例如:{{\"decision\": true}}\n" + f"请只输出有效的 JSON 对象。" ) # 调用LLM评估 - try: - # 使用 self._llm_evaluate_state_transition - should_deactivate = await self._llm_evaluate_state_transition(prompt) - if should_deactivate: - logger.info(f"{log_prefix} [{stream_name}] LLM建议进入ABSENT状态。正在尝试转换...") - await sub_hf.change_chat_state(ChatState.ABSENT) - if sub_hf.chat_state.chat_status == ChatState.ABSENT: - transitioned_to_absent += 1 - current_chat_count -= 1 # 更新计数器 - else: - logger.warning(f"{log_prefix} [{stream_name}] 尝试转换为ABSENT失败。") - except Exception as e: - logger.error( - f"{log_prefix} [{stream_name}] LLM评估或状态转换(CHAT->ABSENT)时出错: {e}", exc_info=True - ) + should_deactivate = await self._llm_evaluate_state_transition(prompt) + if should_deactivate is None: # 处理解析失败或意外情况 + logger.warning(f"{log_prefix} [{stream_name}] LLM评估返回无效结果,跳过。") + continue - # 可以选择性地为 FOCUSED 状态添加评估逻辑,例如判断是否降级回 CHAT 或 ABSENT + if should_deactivate: + logger.info(f"{log_prefix} [{stream_name}] LLM建议进入ABSENT状态。正在尝试转换...") + await sub_hf.change_chat_state(ChatState.ABSENT) + if sub_hf.chat_state.chat_status == ChatState.ABSENT: + transitioned_to_absent += 1 logger.info( f"{log_prefix} LLM评估周期结束。" @@ -470,38 +467,58 @@ class SubHeartflowManager: f" 成功转换到ABSENT: {transitioned_to_absent}." ) - async def _llm_evaluate_state_transition(self, prompt: str) -> bool: + async def _llm_evaluate_state_transition(self, prompt: str) -> Optional[bool]: """ - 使用 LLM 评估是否应进行状态转换。 + 使用 LLM 评估是否应进行状态转换,期望 LLM 返回 JSON 格式。 Args: - prompt: 提供给 LLM 的提示信息。 + prompt: 提供给 LLM 的提示信息,要求返回 {"decision": true/false}。 Returns: - bool: True 表示应该转换,False 表示不应该转换。 + Optional[bool]: 如果成功解析 LLM 的 JSON 响应并提取了 'decision' 键的值,则返回该布尔值。 + 如果 LLM 调用失败、返回无效 JSON 或 JSON 中缺少 'decision' 键或其值不是布尔型,则返回 None。 """ log_prefix = "[LLM状态评估]" try: # --- 真实的 LLM 调用 --- response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt) - logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估,原始响应: {response_text}") - # 解析响应 - 这里需要根据你的LLM的确切输出来调整逻辑 - # 假设 LLM 会明确回答 "是" 或 "否" - if response_text and "是" in response_text.strip(): - logger.debug(f"{log_prefix} LLM评估结果: 建议转换 (响应包含 '是')") - return True - elif response_text and "否" in response_text.strip(): - logger.debug(f"{log_prefix} LLM评估结果: 建议不转换 (响应包含 '否')") - return False - else: - logger.warning(f"{log_prefix} LLM 未明确回答 '是' 或 '否',响应: {response_text}") - # 可以设定一个默认行为,例如默认不转换 - return False + logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估,原始响应: ```{response_text}```") + + # --- 解析 JSON 响应 --- + try: + # 尝试去除可能的Markdown代码块标记 + cleaned_response = response_text.strip().strip('`').strip() + if cleaned_response.startswith('json'): + cleaned_response = cleaned_response[4:].strip() + + data = json.loads(cleaned_response) + decision = data.get("decision") # 使用 .get() 避免 KeyError + + if isinstance(decision, bool): + logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}") + return decision + else: + logger.warning(f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}") + return None # 值类型不正确 + + except json.JSONDecodeError as json_err: + logger.warning(f"{log_prefix} LLM 返回的响应不是有效的 JSON: {json_err}。响应: {response_text}") + # 尝试在非JSON响应中查找关键词作为后备方案 (可选) + if "true" in response_text.lower(): + logger.debug(f"{log_prefix} 在非JSON响应中找到 'true',解释为建议转换") + return True + if "false" in response_text.lower(): + logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换") + return False + return None # JSON 解析失败,也未找到关键词 + except Exception as parse_err: # 捕获其他可能的解析错误 + logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}") + return None except Exception as e: - logger.error(f"{log_prefix} 调用 LLM 进行状态评估时出错: {e}", exc_info=True) + logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True) traceback.print_exc() - return False + return None # LLM 调用或处理失败 def count_subflows_by_state(self, state: ChatState) -> int: """统计指定状态的子心流数量""" diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index e9577e411..2a33d0671 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -404,10 +404,10 @@ class HeartFChatting: return False, "" # execute:执行 - with Timer("执行动作", cycle_timers): - return await self._handle_action( - action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time - ) + + return await self._handle_action( + action, reasoning, planner_result.get("emoji_query", ""), cycle_timers, planner_start_db_time + ) except PlannerError as e: logger.error(f"{self.log_prefix} 规划错误: {e}") @@ -560,7 +560,7 @@ class HeartFChatting: observation = self.observations[0] if self.observations else None try: - with Timer("Wait New Msg", cycle_timers): + with Timer("等待新消息", cycle_timers): return await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) except asyncio.CancelledError: logger.info(f"{self.log_prefix} 等待被中断") @@ -584,8 +584,8 @@ class HeartFChatting: logger.info(f"{log_prefix} 检测到新消息") return True - if time.monotonic() - wait_start_time > 300: - logger.warning(f"{log_prefix} 等待超时(300秒)") + if time.monotonic() - wait_start_time > 120: + logger.warning(f"{log_prefix} 等待超时(120秒)") return False await asyncio.sleep(1.5) @@ -604,8 +604,6 @@ class HeartFChatting: async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): """处理循环延迟""" cycle_duration = time.monotonic() - cycle_start_time - # if cycle_duration > 0.1: - # logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") try: sleep_duration = 0.0 diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 584205a73..5308ce6e3 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -67,6 +67,7 @@ def init_prompt(): 2. 文字回复(text_reply)适用: - 有实质性内容需要表达 +- 有人提到你,但你还没有回应他 - 可以追加emoji_query表达情绪(格式:情绪描述,如"俏皮的调侃") - 不要追加太多表情 diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 6687421e5..d7be9bef0 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -1,6 +1,7 @@ import time import asyncio import traceback +import statistics # 导入 statistics 模块 from random import random from typing import List, Optional # 导入 Optional @@ -46,6 +47,8 @@ class NormalChat: self.gpt = NormalChatGenerator() self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例 # 存储此实例的兴趣监控任务 + self.start_time = time.time() + self._chat_task: Optional[asyncio.Task] = None logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") @@ -317,6 +320,59 @@ class NormalChat: # 意愿管理器:注销当前message信息 (无论是否回复,只要处理过就删除) willing_manager.delete(message.message_info.message_id) + # --- 新增:处理初始高兴趣消息的私有方法 --- + async def _process_initial_interest_messages(self): + """处理启动时存在于 interest_dict 中的高兴趣消息。""" + items_to_process = list(self.interest_dict.items()) + if not items_to_process: + return # 没有初始消息,直接返回 + + logger.info(f"[{self.stream_name}] 发现 {len(items_to_process)} 条初始兴趣消息,开始处理高兴趣部分...") + interest_values = [item[1][1] for item in items_to_process] # 提取兴趣值列表 + + messages_to_reply = [] # 需要立即回复的消息 + + if len(interest_values) == 1: + # 如果只有一个消息,直接处理 + messages_to_reply.append(items_to_process[0]) + logger.info(f"[{self.stream_name}] 只有一条初始消息,直接处理。") + elif len(interest_values) > 1: + # 计算均值和标准差 + try: + mean_interest = statistics.mean(interest_values) + stdev_interest = statistics.stdev(interest_values) + threshold = mean_interest + stdev_interest + logger.info(f"[{self.stream_name}] 初始兴趣值 均值: {mean_interest:.2f}, 标准差: {stdev_interest:.2f}, 阈值: {threshold:.2f}") + + # 找出高于阈值的消息 + for item in items_to_process: + msg_id, (message, interest_value, is_mentioned) = item + if interest_value > threshold: + messages_to_reply.append(item) + logger.info(f"[{self.stream_name}] 找到 {len(messages_to_reply)} 条高于阈值的初始消息进行处理。") + except statistics.StatisticsError as e: + logger.error(f"[{self.stream_name}] 计算初始兴趣统计值时出错: {e},跳过初始处理。") + + # 处理需要回复的消息 + processed_count = 0 + for item in messages_to_reply: + msg_id, (message, interest_value, is_mentioned) = item + try: + logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})") + await self.normal_response( + message=message, is_mentioned=is_mentioned, interested_rate=interest_value + ) + processed_count += 1 + except Exception as e: + logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\n{traceback.format_exc()}") + finally: + # 无论成功与否都清空兴趣字典 + self.interest_dict.clear() + + + logger.info(f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。") + # --- 新增结束 --- + # 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文 @staticmethod def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool: @@ -350,11 +406,20 @@ class NormalChat: # 改为实例方法, 移除 chat 参数 async def start_chat(self): - """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行)。""" + """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行), + 并在启动前处理一次初始的高兴趣消息。""" if self._chat_task is None or self._chat_task.done(): + # --- 修改:调用新的私有方法处理初始消息 --- + await self._process_initial_interest_messages() + # --- 修改结束 --- + + # 启动后台轮询任务 + logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...") task = asyncio.create_task(self._reply_interested_message()) task.add_done_callback(lambda t: self._handle_task_completion(t)) # 回调现在是实例方法 self._chat_task = task + else: + logger.info(f"[{self.stream_name}] 聊天任务已在运行中。") def _handle_task_completion(self, task: asyncio.Task): """任务完成回调处理""" From 6e4ba27ffb500ffea5478aa1566f3478df7c9927 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 09:35:36 +0000 Subject: [PATCH 54/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_heartflow.py | 6 ++--- src/heart_flow/subheartflow_manager.py | 32 ++++++++++++++----------- src/plugins/heartFC_chat/normal_chat.py | 24 ++++++++++--------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index efd0ea1ed..364303ccd 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -87,12 +87,12 @@ class InterestChatting: def add_interest_dict(self, message: MessageRecv, interest_value: float, is_mentioned: bool): """添加消息到兴趣字典 - + 参数: message: 接收到的消息 interest_value: 兴趣值 is_mentioned: 是否被提及 - + 功能: 1. 将消息添加到兴趣字典 2. 更新最后交互时间 @@ -101,7 +101,7 @@ class InterestChatting: # 添加新消息 self.interest_dict[message.message_info.message_id] = (message, interest_value, is_mentioned) self.last_interaction_time = time.time() - + # 如果字典长度超过10,删除最旧的消息 if len(self.interest_dict) > 10: oldest_key = next(iter(self.interest_dict)) diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 62d9e2f7b..46e34d1d3 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -2,7 +2,7 @@ import asyncio import time import random from typing import Dict, Any, Optional, List -import json # 导入 json 模块 +import json # 导入 json 模块 # 导入日志模块 from src.common.logger import get_module_logger, LogConfig, SUBHEARTFLOW_MANAGER_STYLE_CONFIG @@ -407,13 +407,13 @@ class SubHeartflowManager: f"基于以上信息,该子心流是否表现出足够的活跃迹象或重要性," f"值得将其唤醒并进入常规聊天(CHAT)状态?\n" f"请以 JSON 格式回答,包含一个键 'decision',其值为 true 或 false.\n" - f"例如:{{\"decision\": true}}\n" + f'例如:{{"decision": true}}\n' f"请只输出有效的 JSON 对象。" ) # 调用LLM评估 should_activate = await self._llm_evaluate_state_transition(prompt) - if should_activate is None: # 处理解析失败或意外情况 + if should_activate is None: # 处理解析失败或意外情况 logger.warning(f"{log_prefix} [{stream_name}] LLM评估返回无效结果,跳过。") continue @@ -445,13 +445,13 @@ class SubHeartflowManager: f"基于以上信息,该子心流是否表现出不活跃、对话结束或不再需要关注的迹象," f"应该让其进入休眠(ABSENT)状态?\n" f"请以 JSON 格式回答,包含一个键 'decision',其值为 true (表示应休眠) 或 false (表示不应休眠).\n" - f"例如:{{\"decision\": true}}\n" + f'例如:{{"decision": true}}\n' f"请只输出有效的 JSON 对象。" ) # 调用LLM评估 should_deactivate = await self._llm_evaluate_state_transition(prompt) - if should_deactivate is None: # 处理解析失败或意外情况 + if should_deactivate is None: # 处理解析失败或意外情况 logger.warning(f"{log_prefix} [{stream_name}] LLM评估返回无效结果,跳过。") continue @@ -482,24 +482,28 @@ class SubHeartflowManager: try: # --- 真实的 LLM 调用 --- response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt) - logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估,原始响应: ```{response_text}```") + logger.debug( + f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估,原始响应: ```{response_text}```" + ) # --- 解析 JSON 响应 --- try: # 尝试去除可能的Markdown代码块标记 - cleaned_response = response_text.strip().strip('`').strip() - if cleaned_response.startswith('json'): + cleaned_response = response_text.strip().strip("`").strip() + if cleaned_response.startswith("json"): cleaned_response = cleaned_response[4:].strip() data = json.loads(cleaned_response) - decision = data.get("decision") # 使用 .get() 避免 KeyError + decision = data.get("decision") # 使用 .get() 避免 KeyError if isinstance(decision, bool): logger.debug(f"{log_prefix} LLM评估结果 (来自JSON): {'建议转换' if decision else '建议不转换'}") return decision else: - logger.warning(f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}") - return None # 值类型不正确 + logger.warning( + f"{log_prefix} LLM 返回的 JSON 中 'decision' 键的值不是布尔型: {decision}。响应: {response_text}" + ) + return None # 值类型不正确 except json.JSONDecodeError as json_err: logger.warning(f"{log_prefix} LLM 返回的响应不是有效的 JSON: {json_err}。响应: {response_text}") @@ -510,15 +514,15 @@ class SubHeartflowManager: if "false" in response_text.lower(): logger.debug(f"{log_prefix} 在非JSON响应中找到 'false',解释为建议不转换") return False - return None # JSON 解析失败,也未找到关键词 - except Exception as parse_err: # 捕获其他可能的解析错误 + return None # JSON 解析失败,也未找到关键词 + except Exception as parse_err: # 捕获其他可能的解析错误 logger.warning(f"{log_prefix} 解析 LLM JSON 响应时发生意外错误: {parse_err}。响应: {response_text}") return None except Exception as e: logger.error(f"{log_prefix} 调用 LLM 或处理其响应时出错: {e}", exc_info=True) traceback.print_exc() - return None # LLM 调用或处理失败 + return None # LLM 调用或处理失败 def count_subflows_by_state(self, state: ChatState) -> int: """统计指定状态的子心流数量""" diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index d7be9bef0..912da3520 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -48,7 +48,7 @@ class NormalChat: self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例 # 存储此实例的兴趣监控任务 self.start_time = time.time() - + self._chat_task: Optional[asyncio.Task] = None logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") @@ -325,12 +325,12 @@ class NormalChat: """处理启动时存在于 interest_dict 中的高兴趣消息。""" items_to_process = list(self.interest_dict.items()) if not items_to_process: - return # 没有初始消息,直接返回 + return # 没有初始消息,直接返回 logger.info(f"[{self.stream_name}] 发现 {len(items_to_process)} 条初始兴趣消息,开始处理高兴趣部分...") - interest_values = [item[1][1] for item in items_to_process] # 提取兴趣值列表 + interest_values = [item[1][1] for item in items_to_process] # 提取兴趣值列表 - messages_to_reply = [] # 需要立即回复的消息 + messages_to_reply = [] # 需要立即回复的消息 if len(interest_values) == 1: # 如果只有一个消息,直接处理 @@ -342,7 +342,9 @@ class NormalChat: mean_interest = statistics.mean(interest_values) stdev_interest = statistics.stdev(interest_values) threshold = mean_interest + stdev_interest - logger.info(f"[{self.stream_name}] 初始兴趣值 均值: {mean_interest:.2f}, 标准差: {stdev_interest:.2f}, 阈值: {threshold:.2f}") + logger.info( + f"[{self.stream_name}] 初始兴趣值 均值: {mean_interest:.2f}, 标准差: {stdev_interest:.2f}, 阈值: {threshold:.2f}" + ) # 找出高于阈值的消息 for item in items_to_process: @@ -351,7 +353,7 @@ class NormalChat: messages_to_reply.append(item) logger.info(f"[{self.stream_name}] 找到 {len(messages_to_reply)} 条高于阈值的初始消息进行处理。") except statistics.StatisticsError as e: - logger.error(f"[{self.stream_name}] 计算初始兴趣统计值时出错: {e},跳过初始处理。") + logger.error(f"[{self.stream_name}] 计算初始兴趣统计值时出错: {e},跳过初始处理。") # 处理需要回复的消息 processed_count = 0 @@ -359,18 +361,18 @@ class NormalChat: msg_id, (message, interest_value, is_mentioned) = item try: logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})") - await self.normal_response( - message=message, is_mentioned=is_mentioned, interested_rate=interest_value - ) + await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value) processed_count += 1 except Exception as e: logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\n{traceback.format_exc()}") finally: # 无论成功与否都清空兴趣字典 self.interest_dict.clear() - - logger.info(f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。") + logger.info( + f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。" + ) + # --- 新增结束 --- # 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文 From e7f120319cce81c9affe7ea2f8f33a9ca72c18a3 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 18:13:11 +0800 Subject: [PATCH 55/79] =?UTF-8?q?=E5=B0=86PFC=E7=9A=84=E9=BA=A6=E9=BA=A6?= =?UTF-8?q?=E5=8F=91=E8=A8=80=E5=AD=98=E8=87=B3=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E6=8E=89=E6=89=8B=E5=8A=A8=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 30 ------------------------------ src/plugins/PFC/pfc.py | 1 + 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 83081e80e..37a35b7a9 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -443,36 +443,6 @@ class Conversation: await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content) logger.info(f"消息已发送: {reply_content}") # 可以在发送后加个日志确认 - # --- 添加的立即更新状态逻辑开始 --- - try: - # 内层 try: 专门捕获手动更新状态时可能出现的错误 - # 创建一个代表刚刚发送的消息的字典 - bot_message_info = { - "message_id": f"bot_sent_{current_time}", # 创建一个简单的唯一ID - "time": current_time, - "user_info": UserInfo( # 使用 UserInfo 类构建用户信息 - user_id=str(global_config.BOT_QQ), - user_nickname=global_config.BOT_NICKNAME, - platform=self.chat_stream.platform, # 从 chat_stream 获取平台信息 - ).to_dict(), # 转换为字典格式存储 - "processed_plain_text": reply_content, # 使用发送的内容 - "detailed_plain_text": f"{int(current_time)},{global_config.BOT_NICKNAME}:{reply_content}", # 构造一个简单的详细文本, 时间戳取整 - # 可以根据需要添加其他字段,保持与 observation_info.chat_history 中其他消息结构一致 - } - - # 直接更新 ObservationInfo 实例 - if self.observation_info: - self.observation_info.chat_history.append(bot_message_info) # 将消息添加到历史记录末尾 - self.observation_info.last_bot_speak_time = current_time # 更新 Bot 最后发言时间 - self.observation_info.last_message_time = current_time # 更新最后消息时间 - logger.debug("已手动将Bot发送的消息添加到 ObservationInfo") - else: - logger.warning("无法手动更新 ObservationInfo:实例不存在") - - except Exception as update_err: - logger.error(f"手动更新 ObservationInfo 时出错: {update_err}") - # --- 添加的立即更新状态逻辑结束 --- - # 原有的触发更新和等待代码 self.chat_observer.trigger_update() if not await self.chat_observer.wait_for_update(): diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 033cf8226..ac8338626 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -376,6 +376,7 @@ class DirectMessageSender: # 发送消息 try: await self.send_via_ws(message) + await self.storage.store_message(message, chat_stream) logger.success(f"PFC消息已发送: {content}") except Exception as e: logger.error(f"PFC消息发送失败: {str(e)}") From 293a03960a2e024a6579b05be5e87fcbb83645a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Sat, 26 Apr 2025 18:40:37 +0800 Subject: [PATCH 56/79] fix: Ruff --- src/plugins/PFC/conversation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 37a35b7a9..9e675ac3b 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -4,7 +4,7 @@ import datetime # from .message_storage import MongoDBMessageStorage from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat -from ...config.config import global_config +# from ...config.config import global_config from typing import Dict, Any from ..chat.message import Message from .pfc_types import ConversationState @@ -436,7 +436,7 @@ class Conversation: try: # 外层 try: 捕获发送消息和后续处理中的主要错误 - current_time = time.time() # 获取当前时间戳 + _current_time = time.time() # 获取当前时间戳 reply_content = self.generated_reply # 获取要发送的内容 # 发送消息 From 10282f3d0c217d5be06d45e0b66c00f0383e5bd1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 10:40:50 +0000 Subject: [PATCH 57/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 9e675ac3b..c56cc3e17 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -4,6 +4,7 @@ import datetime # from .message_storage import MongoDBMessageStorage from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat + # from ...config.config import global_config from typing import Dict, Any from ..chat.message import Message From 3763a0ed9e0334a2551b374659d054ba0f2777a1 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 19:03:36 +0800 Subject: [PATCH 58/79] =?UTF-8?q?feat=EF=BC=9A=E6=8F=90=E4=BE=9B=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E8=AE=A9HFC=E7=BB=93=E6=9D=9F=EF=BC=8C=E5=BD=93?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E8=BF=87=E4=B9=85no=5Freply=EF=BC=8C?= =?UTF-8?q?=E4=BC=9A=E5=9B=9E=E5=88=B0ABSENT=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/logger.py | 2 +- src/heart_flow/README.md | 3 +- src/heart_flow/background_tasks.py | 25 ---- src/heart_flow/sub_heartflow.py | 28 +++- src/heart_flow/subheartflow_manager.py | 175 +++++++++++------------ src/plugins/heartFC_chat/heartFC_chat.py | 121 ++++++++++++++-- 6 files changed, 214 insertions(+), 140 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 6ab3505df..176d4629c 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -337,7 +337,7 @@ REMOTE_STYLE_CONFIG = { "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 远程 | {message}", }, "simple": { - "console_format": "{time:MM-DD HH:mm} | 远程 | {message}", + "console_format": "{time:MM-DD HH:mm} | 远程| {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 远程 | {message}", }, } diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index a2540ebf1..4ccb662dd 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -61,6 +61,7 @@ c HeartFChatting工作方式 c.5.2.2 通过 HeartFCSender 直接发送匹配查询 (emoji_query) 的表情。 c.5.3 如果决策是 no_reply: c.5.3.1 进入等待状态,直到检测到新消息或超时。 + c.5.3.2 同时,增加内部连续不回复计数器。如果该计数器达到预设阈值(例如 5 次),则调用初始化时由 `SubHeartflowManager` 提供的回调函数。此回调函数会通知 `SubHeartflowManager` 请求将对应的 `SubHeartflow` 状态转换为 `ABSENT`。如果执行了其他动作(如 `text_reply` 或 `emoji_reply`),则此计数器会被重置。 c.6 循环结束后,记录周期信息 (CycleInfo),并根据情况进行短暂休眠,防止CPU空转。 @@ -152,7 +153,7 @@ c HeartFChatting工作方式 - **状态转换机制** (由 `SubHeartflowManager` 驱动): - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。此外,`evaluate_and_transition_subflows_by_llm` 方法也会根据 LLM 的判断,在未达上限时将 `ABSENT` 状态的子心流激活为 `CHAT`。 - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**(通过 `self.mai_state_info` 获取限制),则调用 `change_chat_state` 将其提升为 `FOCUSED`。 - - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃、随机概率 (`randomly_deactivate_subflows`) 或 LLM 评估 (`evaluate_and_transition_subflows_by_llm` 判断 `CHAT` 状态子心流应休眠) 等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。 + - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃、随机概率 (`randomly_deactivate_subflows`)、LLM 评估 (`evaluate_and_transition_subflows_by_llm` 判断 `CHAT` 状态子心流应休眠) 或收到来自 `HeartFChatting` 的连续不回复回调信号 (`request_absent_transition`) 等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。 - **注意**: `change_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法,这些方法会使用内部存储的 `mai_state_info` 来获取限制)承担。 ## 3. 聊天实例详解 (Chat Instances Explained) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 7ae4b62f9..076f441c9 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -112,15 +112,6 @@ class BackgroundTaskManager: f"兴趣评估任务已启动 间隔:{self.interest_eval_interval}s", "_interest_eval_task", ), - # 新增随机停用任务配置 - ( - self._random_deactivation_task, - self._run_random_deactivation_cycle, - "hf_random_deactivation", - "debug", # 设为debug,避免过多日志 - f"随机停用任务已启动 间隔:{self.random_deactivation_interval}s", - "_random_deactivation_task", - ), ] # 统一启动所有任务 @@ -264,10 +255,6 @@ class BackgroundTaskManager: # --- 结束新增 --- - # --- 新增随机停用工作函数 --- - async def _perform_random_deactivation_work(self): - """执行一轮子心流随机停用检查。""" - await self.subheartflow_manager.randomly_deactivate_subflows() # --- 结束新增 --- @@ -300,15 +287,3 @@ class BackgroundTaskManager: task_func=self._perform_interest_eval_work, ) - # --- 结束新增 --- - - # --- 新增随机停用任务运行器 --- - async def _run_random_deactivation_cycle(self): - """运行随机停用循环。""" - await self._run_periodic_loop( - task_name="Random Deactivation", - interval=self.random_deactivation_interval, - task_func=self._perform_random_deactivation_work, - ) - - # --- 结束新增 --- diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index efd0ea1ed..fdc6bac7d 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -2,7 +2,7 @@ from .observation import Observation, ChattingObservation import asyncio from src.config.config import global_config import time -from typing import Optional, List, Dict, Tuple +from typing import Optional, List, Dict, Tuple, TYPE_CHECKING, Callable, Coroutine import traceback from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 import random @@ -15,6 +15,11 @@ from src.heart_flow.mai_state_manager import MaiStateInfo from src.heart_flow.chat_state_info import ChatState, ChatStateInfo from src.heart_flow.sub_mind import SubMind +# # --- REMOVE: Conditional import --- # +# if TYPE_CHECKING: +# from src.heart_flow.subheartflow_manager import SubHeartflowManager +# # --- END REMOVE --- # + # 定义常量 (从 interest.py 移动过来) MAX_INTEREST = 15.0 @@ -234,16 +239,23 @@ class InterestChatting: class SubHeartflow: - def __init__(self, subheartflow_id, mai_states: MaiStateInfo): + def __init__( + self, + subheartflow_id, + mai_states: MaiStateInfo, + hfc_no_reply_callback: Callable[[], Coroutine[None, None, None]] + ): """子心流初始化函数 Args: subheartflow_id: 子心流唯一标识符 - parent_heartflow: 父级心流实例 + mai_states: 麦麦状态信息实例 + hfc_no_reply_callback: HFChatting 连续不回复时触发的回调 """ # 基础属性,两个值是一样的 self.subheartflow_id = subheartflow_id self.chat_id = subheartflow_id + self.hfc_no_reply_callback = hfc_no_reply_callback # 麦麦的状态 self.mai_states = mai_states @@ -364,11 +376,17 @@ class SubHeartflow: # 如果实例不存在,则创建并启动 logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...") try: + # 创建 HeartFChatting 实例,并传递 从构造函数传入的 回调函数 self.heart_fc_instance = HeartFChatting( - chat_id=self.chat_id, sub_mind=self.sub_mind, observations=self.observations + chat_id=self.subheartflow_id, + sub_mind=self.sub_mind, + observations=self.observations, # 传递所有观察者 + on_consecutive_no_reply_callback=self.hfc_no_reply_callback # <-- Use stored callback ) + + # 初始化并启动 HeartFChatting if await self.heart_fc_instance._initialize(): - await self.heart_fc_instance.start() # 初始化成功后启动循环 + await self.heart_fc_instance.start() logger.info(f"{log_prefix} 麦麦已成功进入专注聊天模式 (新实例已启动)。") return True else: diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 62d9e2f7b..a8403d4f8 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -3,6 +3,7 @@ import time import random from typing import Dict, Any, Optional, List import json # 导入 json 模块 +import functools # <-- 新增导入 # 导入日志模块 from src.common.logger import get_module_logger, LogConfig, SUBHEARTFLOW_MANAGER_STYLE_CONFIG @@ -77,8 +78,17 @@ class SubHeartflowManager: return subflow try: - # 初始化子心流, 传入存储的 mai_state_info - new_subflow = SubHeartflow(subheartflow_id, self.mai_state_info) + # --- 使用 functools.partial 创建 HFC 回调 --- # + # 将 manager 的 _handle_hfc_no_reply 方法与当前的 subheartflow_id 绑定 + hfc_callback = functools.partial(self._handle_hfc_no_reply, subheartflow_id) + # --- 结束创建回调 --- # + + # 初始化子心流, 传入 mai_state_info 和 partial 创建的回调 + new_subflow = SubHeartflow( + subheartflow_id, + self.mai_state_info, + hfc_callback # <-- 传递 partial 创建的回调 + ) # 异步初始化 await new_subflow.initialize() @@ -285,74 +295,11 @@ class SubHeartflowManager: ) and final_subflow.chat_state.chat_status == ChatState.FOCUSED: current_focused_count += 1 - async def randomly_deactivate_subflows(self, deactivation_probability: float = 0.1): - """以一定概率将 FOCUSED 或 CHAT 状态的子心流回退到 ABSENT 状态。""" - log_prefix_manager = "[子心流管理器-随机停用]" - logger.debug(f"{log_prefix_manager} 开始随机停用检查... (概率: {deactivation_probability:.0%})") - - # 使用快照安全遍历 - subflows_snapshot = list(self.subheartflows.values()) - deactivated_count = 0 - - try: - for sub_hf in subflows_snapshot: - flow_id = sub_hf.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id - log_prefix_flow = f"[{stream_name}]" - current_state = sub_hf.chat_state.chat_status - - # 只处理 FOCUSED 或 CHAT 状态 - if current_state not in [ChatState.FOCUSED, ChatState.CHAT]: - continue - - # 检查随机概率 - if random.random() < deactivation_probability: - logger.info( - f"{log_prefix_manager} {log_prefix_flow} 随机触发停用 (从 {current_state.value}) -> ABSENT" - ) - - # 获取当前实例以检查最新状态 - current_subflow = self.subheartflows.get(flow_id) - if not current_subflow or current_subflow.chat_state.chat_status != current_state: - logger.warning(f"{log_prefix_manager} {log_prefix_flow} 尝试停用时状态已改变或实例消失,跳过。") - continue - - # --- 状态设置 --- # - # 注意:这里传递的状态数量是 *停用前* 的状态数量 - await current_subflow.change_chat_state(ChatState.ABSENT) - - # --- 状态验证 (可选) --- - final_subflow = self.subheartflows.get(flow_id) - if final_subflow: - final_state = final_subflow.chat_state.chat_status - if final_state == ChatState.ABSENT: - logger.debug( - f"{log_prefix_manager} {log_prefix_flow} 成功从 {current_state.value} 停用到 ABSENT 状态" - ) - deactivated_count += 1 - else: - logger.warning( - f"{log_prefix_manager} {log_prefix_flow} 尝试停用到 ABSENT 后状态仍为 {final_state.value}" - ) - else: - logger.warning(f"{log_prefix_manager} {log_prefix_flow} 停用后验证时子心流 {flow_id} 消失") - - except Exception as e: - logger.error(f"{log_prefix_manager} 随机停用周期出错: {e}", exc_info=True) - - if deactivated_count > 0: - logger.info(f"{log_prefix_manager} 随机停用周期结束, 成功停用 {deactivated_count} 个子心流。") - else: - logger.debug(f"{log_prefix_manager} 随机停用周期结束, 未停用任何子心流。") - async def evaluate_and_transition_subflows_by_llm(self): """ 使用LLM评估每个子心流的状态,并根据LLM的判断执行状态转换(ABSENT <-> CHAT)。 注意:此函数包含对假设的LLM函数的调用。 """ - log_prefix = "[LLM状态评估]" - logger.info(f"{log_prefix} 开始基于LLM评估子心流状态...") - # 获取当前状态和限制,用于CHAT激活检查 current_mai_state = self.mai_state_info.get_current_state() chat_limit = current_mai_state.get_normal_chat_max_num() @@ -366,33 +313,27 @@ class SubHeartflowManager: current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) if not subflows_snapshot: - logger.info(f"{log_prefix} 当前没有子心流需要评估。") + logger.info("当前没有子心流需要评估。") return for sub_hf in subflows_snapshot: flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id + log_prefix = f"[{stream_name}]" current_subflow_state = sub_hf.chat_state.chat_status - # --- 获取观察内容 --- - # 从 sub_hf.observations 获取 ChattingObservation 并提取信息 + _observation_summary = "没有可用的观察信息。" # 默认值 - try: - # 检查 observations 列表是否存在且不为空 - # 假设第一个观察者是 ChattingObservation - first_observation = sub_hf.observations[0] - if isinstance(first_observation, ChattingObservation): - # 组合中期记忆和当前聊天内容 - current_chat = first_observation.talking_message_str or "当前无聊天内容。" - combined_summary = f"当前聊天内容:\n{current_chat}" - else: - logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。") + first_observation = sub_hf.observations[0] + if isinstance(first_observation, ChattingObservation): + # 组合中期记忆和当前聊天内容 + current_chat = first_observation.talking_message_str or "当前无聊天内容。" + combined_summary = f"当前聊天内容:\n{current_chat}" + else: + logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。") + - except Exception as e: - logger.warning(f"{log_prefix} [{stream_name}] 获取观察信息失败: {e}", exc_info=True) - # 保留默认值或错误信息 - combined_summary = f"获取观察信息时出错: {e}" # --- 获取麦麦状态 --- mai_state_description = f"麦麦当前状态: {current_mai_state.value}。" @@ -414,7 +355,7 @@ class SubHeartflowManager: # 调用LLM评估 should_activate = await self._llm_evaluate_state_transition(prompt) if should_activate is None: # 处理解析失败或意外情况 - logger.warning(f"{log_prefix} [{stream_name}] LLM评估返回无效结果,跳过。") + logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。") continue if should_activate: @@ -423,17 +364,19 @@ class SubHeartflowManager: current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) if current_chat_count < chat_limit: logger.info( - f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..." + f"{log_prefix}LLM建议激活到CHAT状态,且未达上限({current_chat_count}/{chat_limit})。正在尝试转换..." ) await sub_hf.change_chat_state(ChatState.CHAT) if sub_hf.chat_state.chat_status == ChatState.CHAT: transitioned_to_chat += 1 else: - logger.warning(f"{log_prefix} [{stream_name}] 尝试激活到CHAT失败。") + logger.warning(f"{log_prefix}尝试激活到CHAT失败。") else: logger.info( - f"{log_prefix} [{stream_name}] LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。" + f"{log_prefix}LLM建议激活到CHAT状态,但已达到上限({current_chat_count}/{chat_limit})。跳过转换。" ) + else: + logger.info(f"{log_prefix}LLM建议不激活到CHAT状态。") # --- 针对 CHAT 状态 --- elif current_subflow_state == ChatState.CHAT: @@ -452,20 +395,18 @@ class SubHeartflowManager: # 调用LLM评估 should_deactivate = await self._llm_evaluate_state_transition(prompt) if should_deactivate is None: # 处理解析失败或意外情况 - logger.warning(f"{log_prefix} [{stream_name}] LLM评估返回无效结果,跳过。") + logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。") continue if should_deactivate: - logger.info(f"{log_prefix} [{stream_name}] LLM建议进入ABSENT状态。正在尝试转换...") + logger.info(f"{log_prefix}LLM建议进入ABSENT状态。正在尝试转换...") await sub_hf.change_chat_state(ChatState.ABSENT) if sub_hf.chat_state.chat_status == ChatState.ABSENT: transitioned_to_absent += 1 - - logger.info( - f"{log_prefix} LLM评估周期结束。" - f" 成功转换到CHAT: {transitioned_to_chat}." - f" 成功转换到ABSENT: {transitioned_to_absent}." - ) + else: + logger.info(f"{log_prefix}LLM建议不进入ABSENT状态。") + + async def _llm_evaluate_state_transition(self, prompt: str) -> Optional[bool]: """ @@ -482,7 +423,8 @@ class SubHeartflowManager: try: # --- 真实的 LLM 调用 --- response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt) - logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估,原始响应: ```{response_text}```") + logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估") + logger.debug(f"{log_prefix} 原始响应: {response_text}") # --- 解析 JSON 响应 --- try: @@ -573,3 +515,46 @@ class SubHeartflowManager: logger.error(f"删除 SubHeartflow {subheartflow_id} 时出错: {e}", exc_info=True) else: logger.warning(f"尝试删除不存在的 SubHeartflow: {subheartflow_id}") + + # --- 新增:处理 HFC 无回复回调的专用方法 --- # + async def _handle_hfc_no_reply(self, subheartflow_id: Any): + """处理来自 HeartFChatting 的连续无回复信号 (通过 partial 绑定 ID)""" + # 注意:这里不需要再获取锁,因为 request_absent_transition 内部会处理锁 + logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号") + await self.request_absent_transition(subheartflow_id) + # --- 结束新增 --- # + + # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # + async def request_absent_transition(self, subflow_id: Any): + """ + 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。 + 通常在连续多次 "no_reply" 后被调用。 + + Args: + subflow_id: 需要转换状态的子心流 ID。 + """ + async with self._lock: + subflow = self.subheartflows.get(subflow_id) + if not subflow: + logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT") + return + + stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id + current_state = subflow.chat_state.chat_status + + # 仅当子心流处于 FOCUSED 状态时才进行转换 + # 因为 HeartFChatting 只在 FOCUSED 状态下运行 + if current_state == ChatState.FOCUSED: + logger.info(f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 转换为 ABSENT") + try: + await subflow.change_chat_state(ChatState.ABSENT) + logger.info(f"[状态转换请求] {stream_name} 状态已成功转换为 ABSENT") + except Exception as e: + logger.error(f"[状态转换请求] 转换 {stream_name} 到 ABSENT 时出错: {e}", exc_info=True) + elif current_state == ChatState.ABSENT: + logger.debug(f"[状态转换请求] {stream_name} 已处于 ABSENT 状态,无需转换") + else: + logger.warning( + f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换" + ) + # --- 结束新增 --- # diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 2a33d0671..9a2862adb 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -2,7 +2,7 @@ import asyncio import time import traceback import random # <-- 添加导入 -from typing import List, Optional, Dict, Any, Deque +from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from collections import deque from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import Seg # Local import needed after move @@ -25,7 +25,6 @@ import contextlib from src.plugins.utils.chat_message_builder import num_new_messages_since from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo from .heartFC_sender import HeartFCSender -# --- End import --- INITIAL_DURATION = 60.0 @@ -155,18 +154,30 @@ class HeartFChatting: 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ - def __init__(self, chat_id: str, sub_mind: SubMind, observations: Observation): + CONSECUTIVE_NO_REPLY_THRESHOLD = 4 # 连续不回复的阈值 + + def __init__( + self, + chat_id: str, + sub_mind: SubMind, + observations: Observation, + on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]] + ): """ HeartFChatting 初始化函数 参数: chat_id: 聊天流唯一标识符(如stream_id) + sub_mind: 关联的子思维 + observations: 关联的观察列表 + on_consecutive_no_reply_callback: 连续不回复达到阈值时调用的异步回调函数 """ # 基础属性 self.stream_id: str = chat_id # 聊天流ID self.chat_stream: Optional[ChatStream] = None # 关联的聊天流 self.sub_mind: SubMind = sub_mind # 关联的子思维 self.observations: List[Observation] = observations # 关联的观察列表,用于监控聊天流状态 + self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback # 日志前缀 self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]" @@ -198,6 +209,8 @@ class HeartFChatting: self._cycle_counter = 0 self._cycle_history: Deque[CycleInfo] = deque(maxlen=10) # 保留最近10个循环的信息 self._current_cycle: Optional[CycleInfo] = None + self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器 + self._shutting_down: bool = False # <--- 新增:关闭标志位 async def _initialize(self) -> bool: """ @@ -276,6 +289,12 @@ class HeartFChatting: """主循环,持续进行计划并可能回复消息,直到被外部取消。""" try: while True: # 主循环 + # --- 在循环开始处检查关闭标志 --- + if self._shutting_down: + logger.info(f"{self.log_prefix} 检测到关闭标志,退出 HFC 循环。") + break + # -------------------------------- + # 创建新的循环信息 self._cycle_counter += 1 self._current_cycle = CycleInfo(self._cycle_counter) @@ -287,6 +306,12 @@ class HeartFChatting: # 执行规划和处理阶段 async with self._get_cycle_context() as acquired_lock: if not acquired_lock: + # 如果未能获取锁(理论上不太可能,除非 shutdown 过程中释放了但又被抢了?) + # 或者也可以在这里再次检查 self._shutting_down + if self._shutting_down: + break # 再次检查,确保退出 + logger.warning(f"{self.log_prefix} 未能获取循环处理锁,跳过本次循环。") + await asyncio.sleep(0.1) # 短暂等待避免空转 continue # 记录规划开始时间点 @@ -320,7 +345,11 @@ class HeartFChatting: ) except asyncio.CancelledError: - logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)被取消了") + # 设置了关闭标志位后被取消是正常流程 + if not self._shutting_down: + logger.warning(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)循环意外被取消") + else: + logger.info(f"{self.log_prefix} HeartFChatting: 麦麦的认真水群(HFC)循环已取消 (正常关闭)") except Exception as e: logger.error(f"{self.log_prefix} HeartFChatting: 意外错误: {e}") logger.error(traceback.format_exc()) @@ -451,6 +480,8 @@ class HeartFChatting: return await handler(reasoning, planner_start_db_time, cycle_timers), "" except HeartFCError as e: logger.error(f"{self.log_prefix} 处理{action}时出错: {e}") + # 出错时也重置计数器 + self._lian_xu_bu_hui_fu_ci_shu = 0 return False, "" async def _handle_text_reply(self, reasoning: str, emoji_query: str, cycle_timers: dict) -> tuple[bool, str]: @@ -471,6 +502,8 @@ class HeartFChatting: 返回: tuple[bool, str]: (是否回复成功, 思考消息ID) """ + # 重置连续不回复计数器 + self._lian_xu_bu_hui_fu_ci_shu = 0 # 获取锚点消息 anchor_message = await self._get_anchor_message() @@ -544,8 +577,9 @@ class HeartFChatting: 处理不回复的情况 工作流程: - 1. 等待新消息 - 2. 超时或收到新消息时返回 + 1. 等待新消息、超时或关闭信号 + 2. 根据等待结果更新连续不回复计数 + 3. 如果达到阈值,触发回调 参数: reasoning: 不回复的原因 @@ -561,14 +595,36 @@ class HeartFChatting: try: with Timer("等待新消息", cycle_timers): - return await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) + # 等待新消息、超时或关闭信号,并获取结果 + await self._wait_for_new_message(observation, planner_start_db_time, self.log_prefix) + + if not self._shutting_down: + self._lian_xu_bu_hui_fu_ci_shu += 1 + logger.debug(f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}") + + # 检查是否达到阈值 + if self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD: + logger.info(f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次),调用回调请求状态转换") + # 调用回调。注意:这里不重置计数器,依赖回调函数成功改变状态来隐式重置上下文。 + await self.on_consecutive_no_reply_callback() + + + return True + except asyncio.CancelledError: - logger.info(f"{self.log_prefix} 等待被中断") + # 如果在等待过程中任务被取消(可能是因为 shutdown) + logger.info(f"{self.log_prefix} 处理 'no_reply' 时等待被中断 (CancelledError)") + # 让异常向上传播,由 _hfc_loop 的异常处理逻辑接管 raise + except Exception as e: # 捕获调用管理器或其他地方可能发生的错误 + logger.error(f"{self.log_prefix} 处理 'no_reply' 时发生错误: {e}") + logger.error(traceback.format_exc()) + # 发生意外错误时,可以选择是否重置计数器,这里选择不重置 + return False # 表示动作未成功 async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool: """ - 等待新消息 + 等待新消息 或 检测到关闭信号 参数: observation: 观察实例 @@ -576,19 +632,36 @@ class HeartFChatting: log_prefix: 日志前缀 返回: - bool: 是否检测到新消息 + bool: 是否检测到新消息 (如果因关闭信号退出则返回 False) """ wait_start_time = time.monotonic() while True: + # --- 在每次循环开始时检查关闭标志 --- + if self._shutting_down: + logger.info(f"{log_prefix} 等待新消息时检测到关闭信号,中断等待。") + return False # 表示因为关闭而退出 + # ----------------------------------- + + # 检查新消息 if await observation.has_new_messages_since(planner_start_db_time): logger.info(f"{log_prefix} 检测到新消息") return True + # 检查超时 (放在检查新消息和关闭之后) if time.monotonic() - wait_start_time > 120: - logger.warning(f"{log_prefix} 等待超时(120秒)") + logger.warning(f"{log_prefix} 等待新消息超时(20秒)") return False - await asyncio.sleep(1.5) + try: + # 短暂休眠,让其他任务有机会运行,并能更快响应取消或关闭 + await asyncio.sleep(0.5) # 缩短休眠时间 + except asyncio.CancelledError: + # 如果在休眠时被取消,再次检查关闭标志 + # 如果是正常关闭,则不需要警告 + if not self._shutting_down: + logger.warning(f"{log_prefix} _wait_for_new_message 的休眠被意外取消") + # 无论如何,重新抛出异常,让上层处理 + raise async def _log_cycle_timers(self, cycle_timers: dict, log_prefix: str): """记录循环周期的计时器结果""" @@ -599,7 +672,9 @@ class HeartFChatting: timer_strings.append(f"{name}: {formatted_time}") if timer_strings: - logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") + # 在记录前检查关闭标志 + if not self._shutting_down: + logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}") async def _handle_cycle_delay(self, action_taken_this_cycle: bool, cycle_start_time: float, log_prefix: str): """处理循环延迟""" @@ -835,6 +910,7 @@ class HeartFChatting: async def shutdown(self): """优雅关闭HeartFChatting实例,取消活动循环任务""" logger.info(f"{self.log_prefix} 正在关闭HeartFChatting...") + self._shutting_down = True # <-- 在开始关闭时设置标志位 # 取消循环任务 if self._loop_task and not self._loop_task.done(): @@ -865,6 +941,25 @@ class HeartFChatting: action=action, reasoning=reasoning, ) + + # 在记录循环日志前检查关闭标志 + if not self._shutting_down: + self._current_cycle.complete_cycle() + self._cycle_history.append(self._current_cycle) + + # 记录循环信息和计时器结果 + timer_strings = [] + for name, elapsed in self._current_cycle.timers.items(): + formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}秒" + timer_strings.append(f"{name}: {formatted_time}") + + logger.debug( + f"{self.log_prefix} 第 #{self._current_cycle.cycle_id}次思考完成," + f"耗时: {self._current_cycle.end_time - self._current_cycle.start_time:.2f}秒, " + f"动作: {self._current_cycle.action_type}" + + (f"\n计时器详情: {'; '.join(timer_strings)}" if timer_strings else "") + ) + return prompt async def _build_planner_prompt( From d55043a8a59eaedb006b779857c4f8729d1de638 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 20:25:24 +0800 Subject: [PATCH 59/79] =?UTF-8?q?PFC=E6=B6=88=E6=81=AF=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E4=B8=8EHFC=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 26 ++++++++++++-------------- src/plugins/PFC/conversation.py | 18 ++++++++++++------ src/plugins/PFC/observation_info.py | 13 +++++++++++-- src/plugins/PFC/pfc.py | 22 ++++++++++++---------- src/plugins/PFC/reply_generator.py | 22 ++++++++++++---------- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 4e39483bf..2c083ca15 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -8,6 +8,7 @@ from .pfc_utils import get_items_from_json from src.individuality.individuality import Individuality from .observation_info import ObservationInfo from .conversation_info import ConversationInfo +from src.plugins.utils.chat_message_builder import build_readable_messages pfc_action_log_config = LogConfig( console_format=PFC_ACTION_PLANNER_STYLE_CONFIG["console_format"], @@ -132,12 +133,7 @@ class ActionPlanner: chat_history_text = "" try: if hasattr(observation_info, "chat_history") and observation_info.chat_history: - chat_history_list = observation_info.chat_history[-20:] - for msg in chat_history_list: - if isinstance(msg, dict) and "detailed_plain_text" in msg: - chat_history_text += f"{msg.get('detailed_plain_text', '')}\n" - elif isinstance(msg, str): - chat_history_text += f"{msg}\n" + chat_history_text = observation_info.chat_history_str if not chat_history_text: # 如果历史记录是空列表 chat_history_text = "还没有聊天记录。\n" else: @@ -146,12 +142,14 @@ class ActionPlanner: if hasattr(observation_info, "new_messages_count") and observation_info.new_messages_count > 0: if hasattr(observation_info, "unprocessed_messages") and observation_info.unprocessed_messages: new_messages_list = observation_info.unprocessed_messages - chat_history_text += f"--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n" - for msg in new_messages_list: - if isinstance(msg, dict) and "detailed_plain_text" in msg: - chat_history_text += f"{msg.get('detailed_plain_text', '')}\n" - elif isinstance(msg, str): - chat_history_text += f"{msg}\n" + new_messages_str = await build_readable_messages( + new_messages_list, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + ) + chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" # 清理消息应该由调用者或 observation_info 内部逻辑处理,这里不再调用 clear # if hasattr(observation_info, 'clear_unprocessed_messages'): # observation_info.clear_unprocessed_messages() @@ -242,7 +240,7 @@ class ActionPlanner: last_action_context += "- 【重要】失败/取消原因未明确记录。\n" else: last_action_context += f"- 该行动当前状态: {status}\n" - + print(f"chat_history_text:\n{chat_history_text}") # --- 构建最终的 Prompt --- prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识: @@ -304,4 +302,4 @@ end_conversation: 结束对话,对方长时间没回复或者当你觉得对 except Exception as e: logger.error(f"规划行动时调用 LLM 或处理结果出错: {str(e)}") - return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" + return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" \ No newline at end of file diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index c56cc3e17..b389dbcfc 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -3,9 +3,8 @@ import asyncio import datetime # from .message_storage import MongoDBMessageStorage -from src.plugins.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat - -# from ...config.config import global_config +from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat +from ...config.config import global_config from typing import Dict, Any from ..chat.message import Message from .pfc_types import ConversationState @@ -83,9 +82,17 @@ class Conversation: timestamp=time.time(), limit=30, # 加载最近30条作为初始上下文,可以调整 ) + chat_talking_prompt = await build_readable_messages( + initial_messages, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + ) if initial_messages: # 将加载的消息填充到 ObservationInfo 的 chat_history self.observation_info.chat_history = initial_messages + self.observation_info.chat_history_str = chat_talking_prompt + "\n" self.observation_info.chat_history_count = len(initial_messages) # 更新 ObservationInfo 中的时间戳等信息 @@ -163,7 +170,7 @@ class Conversation: if hasattr(self.observation_info, "clear_unprocessed_messages"): # 确保 clear_unprocessed_messages 方法存在 logger.debug(f"准备执行 direct_reply,清理 {initial_new_message_count} 条规划时已知的新消息。") - self.observation_info.clear_unprocessed_messages() + await self.observation_info.clear_unprocessed_messages() # 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个) if hasattr(self.observation_info, "new_messages_count"): self.observation_info.new_messages_count = 0 @@ -442,7 +449,6 @@ class Conversation: # 发送消息 await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content) - logger.info(f"消息已发送: {reply_content}") # 可以在发送后加个日志确认 # 原有的触发更新和等待代码 self.chat_observer.trigger_update() @@ -454,4 +460,4 @@ class Conversation: except Exception as e: # 这是外层 try 对应的 except logger.error(f"发送消息或更新状态时失败: {str(e)}") - self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态 + self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态 \ No newline at end of file diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index 4cb6aaaa8..fa24c1219 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -7,6 +7,7 @@ from dataclasses import dataclass, field from src.common.logger import get_module_logger from .chat_observer import ChatObserver from .chat_states import NotificationHandler, NotificationType +from src.plugins.utils.chat_message_builder import build_readable_messages logger = get_module_logger("observation_info") @@ -97,6 +98,7 @@ class ObservationInfo: # data_list chat_history: List[str] = field(default_factory=list) + chat_history_str: str = "" unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list) active_users: Set[str] = field(default_factory=set) @@ -223,13 +225,20 @@ class ObservationInfo: return None return time.time() - self.last_bot_speak_time - def clear_unprocessed_messages(self): + async def clear_unprocessed_messages(self): """清空未处理消息列表""" # 将未处理消息添加到历史记录中 for message in self.unprocessed_messages: self.chat_history.append(message) + self.chat_history_str = await build_readable_messages( + self.chat_history[-20:] if len(self.chat_history) > 20 else self.chat_history, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + ) # 清空未处理消息列表 self.has_unread_messages = False self.unprocessed_messages.clear() self.chat_history_count = len(self.chat_history) - self.new_messages_count = 0 + self.new_messages_count = 0 \ No newline at end of file diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index ac8338626..e792f16a6 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -19,6 +19,7 @@ from src.individuality.individuality import Individuality from .conversation_info import ConversationInfo from .observation_info import ObservationInfo import time +from src.plugins.utils.chat_message_builder import build_readable_messages if TYPE_CHECKING: pass @@ -80,19 +81,20 @@ class GoalAnalyzer: goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n" # 获取聊天历史记录 - chat_history_list = observation_info.chat_history - chat_history_text = "" - for msg in chat_history_list: - chat_history_text += f"{msg}\n" + chat_history_text = observation_info.chat_history if observation_info.new_messages_count > 0: new_messages_list = observation_info.unprocessed_messages + new_messages_str = await build_readable_messages( + new_messages_list, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + ) + chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" - chat_history_text += f"有{observation_info.new_messages_count}条新消息:\n" - for msg in new_messages_list: - chat_history_text += f"{msg}\n" - - observation_info.clear_unprocessed_messages() + # await observation_info.clear_unprocessed_messages() identity_details_only = self.identity_detail_info identity_addon = "" @@ -379,4 +381,4 @@ class DirectMessageSender: await self.storage.store_message(message, chat_stream) logger.success(f"PFC消息已发送: {content}") except Exception as e: - logger.error(f"PFC消息发送失败: {str(e)}") + logger.error(f"PFC消息发送失败: {str(e)}") \ No newline at end of file diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index fe9dab6f5..04a1e8cb9 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -7,6 +7,7 @@ from .reply_checker import ReplyChecker from src.individuality.individuality import Individuality from .observation_info import ObservationInfo from .conversation_info import ConversationInfo +from src.plugins.utils.chat_message_builder import build_readable_messages logger = get_module_logger("reply_generator") @@ -73,18 +74,19 @@ class ReplyGenerator: if len(observation_info.chat_history) >= 20 else observation_info.chat_history ) - chat_history_text = "" - for msg in chat_history_list: - chat_history_text += f"{msg.get('detailed_plain_text', '')}\n" + chat_history_text = observation_info.chat_history_str if observation_info.new_messages_count > 0: new_messages_list = observation_info.unprocessed_messages - - chat_history_text += f"有{observation_info.new_messages_count}条新消息:\n" - for msg in new_messages_list: - chat_history_text += f"{msg.get('detailed_plain_text', '')}\n" - - observation_info.clear_unprocessed_messages() + new_messages_str = await build_readable_messages( + new_messages_list, + replace_bot_name=True, + merge_messages=False, + timestamp_mode="relative", + read_mark=0.0, + ) + chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" + # await observation_info.clear_unprocessed_messages() identity_details_only = self.identity_detail_info identity_addon = "" @@ -185,4 +187,4 @@ class ReplyGenerator: Returns: Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划) """ - return await self.reply_checker.check(reply, goal, chat_history, retry_count) + return await self.reply_checker.check(reply, goal, chat_history, retry_count) \ No newline at end of file From 6f7074fbb8d51237fabe72b35b6e42d437413f76 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 20:34:31 +0800 Subject: [PATCH 60/79] =?UTF-8?q?checker=E4=B9=9F=E8=A6=81=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 1 + src/plugins/PFC/reply_checker.py | 11 +---------- src/plugins/PFC/reply_generator.py | 4 ++-- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index b389dbcfc..6db3a0f0a 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -280,6 +280,7 @@ class Conversation: reply=self.generated_reply, goal=current_goal_str, chat_history=observation_info.chat_history, + chat_history_str=observation_info.chat_history_str, retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数) ) logger.info( diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 1f6f91ddf..1379a2c6b 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -22,7 +22,7 @@ class ReplyChecker: self.max_retries = 3 # 最大重试次数 async def check( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0 + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_text: str, retry_count: int = 0 ) -> Tuple[bool, str, bool]: """检查生成的回复是否合适 @@ -36,7 +36,6 @@ class ReplyChecker: """ # 不再从 observer 获取,直接使用传入的 chat_history # messages = self.chat_observer.get_cached_messages(limit=20) - chat_history_text = "" try: # 筛选出最近由 Bot 自己发送的消息 bot_messages = [] @@ -82,14 +81,6 @@ class ReplyChecker: logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}") logger.error(traceback.format_exc()) # 打印详细的回溯信息 - for msg in chat_history[-20:]: - time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S") - user_info = UserInfo.from_dict(msg.get("user_info", {})) - sender = user_info.user_nickname or f"用户{user_info.user_id}" - if sender == self.name: - sender = "你说" - chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n" - prompt = f"""请检查以下回复或消息是否合适: 当前对话目标:{goal} diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 04a1e8cb9..ee1e28e1c 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -175,7 +175,7 @@ class ReplyGenerator: return "抱歉,我现在有点混乱,让我重新思考一下..." async def check_reply( - self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0 + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0 ) -> Tuple[bool, str, bool]: """检查回复是否合适 @@ -187,4 +187,4 @@ class ReplyGenerator: Returns: Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划) """ - return await self.reply_checker.check(reply, goal, chat_history, retry_count) \ No newline at end of file + return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) \ No newline at end of file From 2de74c246cdb48fb0d3c7b1feae7482fcc7adf19 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 20:35:19 +0800 Subject: [PATCH 61/79] =?UTF-8?q?=E5=8E=BB=E6=8E=89=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 2c083ca15..5ac6c62d7 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -240,7 +240,7 @@ class ActionPlanner: last_action_context += "- 【重要】失败/取消原因未明确记录。\n" else: last_action_context += f"- 该行动当前状态: {status}\n" - print(f"chat_history_text:\n{chat_history_text}") + # --- 构建最终的 Prompt --- prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识: From b34a0a683a9c020de59bb0f8f8d371b55bd6ddfd Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 20:45:40 +0800 Subject: [PATCH 62/79] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=9B=9E=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/conversation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 6db3a0f0a..df33fc11f 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -4,7 +4,7 @@ import datetime # from .message_storage import MongoDBMessageStorage from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat -from ...config.config import global_config +# from ...config.config import global_config from typing import Dict, Any from ..chat.message import Message from .pfc_types import ConversationState From c452be11f8384b962bb5bc0782a8477111cb9501 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 20:48:26 +0800 Subject: [PATCH 63/79] =?UTF-8?q?=E5=88=A0=E6=8E=89=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=9A=84=E5=8F=98=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/reply_generator.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index ee1e28e1c..663a529c1 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -69,11 +69,6 @@ class ReplyGenerator: goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n" # 获取聊天历史记录 - chat_history_list = ( - observation_info.chat_history[-20:] - if len(observation_info.chat_history) >= 20 - else observation_info.chat_history - ) chat_history_text = observation_info.chat_history_str if observation_info.new_messages_count > 0: From 9469d605e2a8d002f5287025884a0ee3300c10a6 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 20:49:40 +0800 Subject: [PATCH 64/79] 1 --- src/plugins/PFC/reply_checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 1379a2c6b..26b20875c 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -1,5 +1,4 @@ import json -import datetime from typing import Tuple, List, Dict, Any from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest From c33dab01190fb12ac5fe431879e15b4b935677fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 12:49:58 +0000 Subject: [PATCH 65/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/action_planner.py | 6 ++++-- src/plugins/PFC/conversation.py | 3 ++- src/plugins/PFC/observation_info.py | 2 +- src/plugins/PFC/pfc.py | 2 +- src/plugins/PFC/reply_generator.py | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 5ac6c62d7..e29d8c4fc 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -149,7 +149,9 @@ class ActionPlanner: timestamp_mode="relative", read_mark=0.0, ) - chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" + chat_history_text += ( + f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" + ) # 清理消息应该由调用者或 observation_info 内部逻辑处理,这里不再调用 clear # if hasattr(observation_info, 'clear_unprocessed_messages'): # observation_info.clear_unprocessed_messages() @@ -302,4 +304,4 @@ end_conversation: 结束对话,对方长时间没回复或者当你觉得对 except Exception as e: logger.error(f"规划行动时调用 LLM 或处理结果出错: {str(e)}") - return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" \ No newline at end of file + return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index df33fc11f..6bcb53fe2 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -4,6 +4,7 @@ import datetime # from .message_storage import MongoDBMessageStorage from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat + # from ...config.config import global_config from typing import Dict, Any from ..chat.message import Message @@ -461,4 +462,4 @@ class Conversation: except Exception as e: # 这是外层 try 对应的 except logger.error(f"发送消息或更新状态时失败: {str(e)}") - self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态 \ No newline at end of file + self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态 diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index fa24c1219..072b1fb6f 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -241,4 +241,4 @@ class ObservationInfo: self.has_unread_messages = False self.unprocessed_messages.clear() self.chat_history_count = len(self.chat_history) - self.new_messages_count = 0 \ No newline at end of file + self.new_messages_count = 0 diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index e792f16a6..5a70d02f3 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -381,4 +381,4 @@ class DirectMessageSender: await self.storage.store_message(message, chat_stream) logger.success(f"PFC消息已发送: {content}") except Exception as e: - logger.error(f"PFC消息发送失败: {str(e)}") \ No newline at end of file + logger.error(f"PFC消息发送失败: {str(e)}") diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 663a529c1..65afbf64d 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -182,4 +182,4 @@ class ReplyGenerator: Returns: Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划) """ - return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) \ No newline at end of file + return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) From 4f34487b45d9e64f6d31fab04ef622b1d45aec81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Sat, 26 Apr 2025 20:50:22 +0800 Subject: [PATCH 66/79] =?UTF-8?q?docs:=20=E5=9C=A8README.md=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=BA=A6=E9=BA=A6=E4=BB=93=E5=BA=93=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=BE=BD=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在README.md文件中添加了麦麦仓库状态徽章,用于展示仓库的活跃度和贡献情况,方便用户快速了解项目状态。 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index df5c1c94e..65e04a233 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,10 @@ MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献, - [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现 +## 麦麦仓库状态 + +![Alt](https://repobeats.axiom.co/api/embed/9faca9fccfc467931b87dd357b60c6362b5cfae0.svg "Repobeats analytics image") + ### 贡献者 感谢各位大佬! From c012d29cbf8eb86c2f3d369cbf95df4a6c62ba71 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 21:00:21 +0800 Subject: [PATCH 67/79] =?UTF-8?q?dev=EF=BC=9A=E8=AE=A9=E9=BA=A6=E9=BA=A6?= =?UTF-8?q?=E6=9B=B4=E6=84=BF=E6=84=8F=E8=AF=B4=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/sub_mind.py | 2 +- src/heart_flow/subheartflow_manager.py | 173 ++++++++++++++++--------- 2 files changed, 112 insertions(+), 63 deletions(-) diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index be995b843..f340b717b 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -103,7 +103,7 @@ class SubMind: individuality = Individuality.get_instance() # 构建个性部分 - prompt_personality = f"你的名字是{individuality.personality.bot_nickname},你" + prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你" prompt_personality += individuality.personality.personality_core # 随机添加个性侧面 diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index a8403d4f8..314bf7115 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -19,9 +19,10 @@ from .observation import ChattingObservation # 导入LLM请求工具 from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config - +from src.individuality.individuality import Individuality import traceback + # 初始化日志记录器 subheartflow_manager_log_config = LogConfig( @@ -110,24 +111,53 @@ class SubHeartflowManager: logger.error(f"创建子心流 {subheartflow_id} 失败: {e}", exc_info=True) return None + # --- 新增:内部方法,用于尝试将单个子心流设置为 ABSENT --- + async def _try_set_subflow_absent_internal(self, subflow: "SubHeartflow", log_prefix: str) -> bool: + """ + 尝试将给定的子心流对象状态设置为 ABSENT (内部方法,不处理锁)。 + + Args: + subflow: 子心流对象。 + log_prefix: 用于日志记录的前缀 (例如 "[子心流管理]" 或 "[停用]")。 + + Returns: + bool: 如果状态成功变为 ABSENT 或原本就是 ABSENT,返回 True;否则返回 False。 + """ + flow_id = subflow.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + + if subflow.chat_state.chat_status != ChatState.ABSENT: + logger.debug(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT") + try: + await subflow.change_chat_state(ChatState.ABSENT) + # 再次检查以确认状态已更改 (change_chat_state 内部应确保) + if subflow.chat_state.chat_status == ChatState.ABSENT: + return True + else: + logger.warning(f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}") + return False + except Exception as e: + logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True) + return False + else: + logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态") + return True # 已经是目标状态,视为成功 + # --- 结束新增 --- + async def sleep_subheartflow(self, subheartflow_id: Any, reason: str) -> bool: - """停止指定的子心流并清理资源""" - subheartflow = self.subheartflows.get(subheartflow_id) - if not subheartflow: - return False + """停止指定的子心流并将其状态设置为 ABSENT""" + log_prefix = "[子心流管理]" + async with self._lock: # 加锁以安全访问字典 + subheartflow = self.subheartflows.get(subheartflow_id) - stream_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id - logger.info(f"[子心流管理] 正在停止 {stream_name}, 原因: {reason}") + stream_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id + logger.info(f"{log_prefix} 正在停止 {stream_name}, 原因: {reason}") - try: - # 设置状态为ABSENT释放资源 - if subheartflow.chat_state.chat_status != ChatState.ABSENT: - logger.debug(f"[子心流管理] 设置 {stream_name} 状态为ABSENT") - await subheartflow.change_chat_state(ChatState.ABSENT) - else: - logger.debug(f"[子心流管理] {stream_name} 已是ABSENT状态") - except Exception as e: - logger.error(f"[子心流管理] 设置ABSENT状态失败: {e}") + # 调用内部方法处理状态变更 + success = await self._try_set_subflow_absent_internal(subheartflow, log_prefix) + + return success + # 锁在此处自动释放 def get_inactive_subheartflows(self, max_age_seconds=INACTIVE_THRESHOLD_SECONDS): """识别并返回需要清理的不活跃(处于ABSENT状态超过一小时)子心流(id, 原因)""" @@ -195,44 +225,37 @@ class SubHeartflowManager: async def deactivate_all_subflows(self): """将所有子心流的状态更改为 ABSENT (例如主状态变为OFFLINE时调用)""" - # logger.info("[停用] 开始将所有子心流状态设置为 ABSENT") - # 使用 list() 创建一个当前值的快照,防止在迭代时修改字典 - flows_to_update = list(self.subheartflows.values()) - - if not flows_to_update: - logger.debug("[停用] 无活跃子心流,无需操作") - return - + log_prefix = "[停用]" changed_count = 0 - for subflow in flows_to_update: - flow_id = subflow.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id - # 再次检查子心流是否仍然存在于管理器中,以防万一在迭代过程中被移除 + processed_count = 0 - if subflow.chat_state.chat_status != ChatState.ABSENT: - logger.debug( - f"正在将子心流 {stream_name} 的状态从 {subflow.chat_state.chat_status.value} 更改为 ABSENT" - ) - try: - # 调用 change_chat_state 将状态设置为 ABSENT - await subflow.change_chat_state(ChatState.ABSENT) - # 验证状态是否真的改变了 - if ( - flow_id in self.subheartflows - and self.subheartflows[flow_id].chat_state.chat_status == ChatState.ABSENT - ): + async with self._lock: # 获取锁以安全迭代 + # 使用 list() 创建一个当前值的快照,防止在迭代时修改字典 + flows_to_update = list(self.subheartflows.values()) + processed_count = len(flows_to_update) + if not flows_to_update: + logger.debug(f"{log_prefix} 无活跃子心流,无需操作") + return + + for subflow in flows_to_update: + # 记录原始状态,以便统计实际改变的数量 + original_state_was_absent = (subflow.chat_state.chat_status == ChatState.ABSENT) + + + success = await self._try_set_subflow_absent_internal(subflow, log_prefix) + + # 如果成功设置为 ABSENT 且原始状态不是 ABSENT,则计数 + if success and not original_state_was_absent: + if subflow.chat_state.chat_status == ChatState.ABSENT: changed_count += 1 else: - logger.warning( - f"[停用] 尝试更改子心流 {stream_name} 状态后,状态仍未变为 ABSENT 或子心流已消失。" - ) - except Exception as e: - logger.error(f"[停用] 更改子心流 {stream_name} 状态为 ABSENT 时出错: {e}", exc_info=True) - else: - logger.debug(f"[停用] 子心流 {stream_name} 已处于 ABSENT 状态,无需更改。") + # 这种情况理论上不应发生,如果内部方法返回 True 的话 + stream_name = chat_manager.get_stream_name(subflow.subheartflow_id) or subflow.subheartflow_id + logger.warning(f"{log_prefix} 内部方法声称成功但 {stream_name} 状态未变为 ABSENT。") + # 锁在此处自动释放 logger.info( - f"下限完成,共处理 {len(flows_to_update)} 个子心流,成功将 {changed_count} 个子心流的状态更改为 ABSENT。" + f"{log_prefix} 完成,共处理 {processed_count} 个子心流,成功将 {changed_count} 个非 ABSENT 子心流的状态更改为 ABSENT。" ) async def evaluate_interest_and_promote(self): @@ -328,6 +351,7 @@ class SubHeartflowManager: first_observation = sub_hf.observations[0] if isinstance(first_observation, ChattingObservation): # 组合中期记忆和当前聊天内容 + first_observation.observe() current_chat = first_observation.talking_message_str or "当前无聊天内容。" combined_summary = f"当前聊天内容:\n{current_chat}" else: @@ -336,19 +360,40 @@ class SubHeartflowManager: # --- 获取麦麦状态 --- - mai_state_description = f"麦麦当前状态: {current_mai_state.value}。" + mai_state_description = f"你当前状态: {current_mai_state.value}。" + + # 获取个性化信息 + individuality = Individuality.get_instance() + + # 构建个性部分 + prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你" + prompt_personality += individuality.personality.personality_core + + # 随机添加个性侧面 + if individuality.personality.personality_sides: + random_side = random.choice(individuality.personality.personality_sides) + prompt_personality += f",{random_side}" + + # 随机添加身份细节 + if individuality.identity.identity_detail: + random_detail = random.choice(individuality.identity.identity_detail) + prompt_personality += f",{random_detail}" + # --- 针对 ABSENT 状态 --- if current_subflow_state == ChatState.ABSENT: # 构建Prompt prompt = ( - f"子心流 [{stream_name}] 当前处于非活跃(ABSENT)状态.\n" + f"{prompt_personality}\n" + f"你当前没有在: [{stream_name}] 群中聊天。\n" f"{mai_state_description}\n" - f"最近观察到的内容摘要:\n---\n{combined_summary}\n---\n" - f"基于以上信息,该子心流是否表现出足够的活跃迹象或重要性," - f"值得将其唤醒并进入常规聊天(CHAT)状态?\n" - f"请以 JSON 格式回答,包含一个键 'decision',其值为 true 或 false.\n" - f"例如:{{\"decision\": true}}\n" + f"这个群里最近的聊天内容是:\n---\n{combined_summary}\n---\n" + f"基于以上信息,请判断你是否愿意在这个群开始闲聊," + f"进入常规聊天(CHAT)状态?\n" + f"给出你的判断,和理由,然后以 JSON 格式回答" + f"包含键 'decision',如果要开始聊天,值为 true ,否则为 false.\n" + f"包含键 'reason',其值为你的理由。\n" + f"例如:{{\"decision\": true, \"reason\": \"因为我想聊天\"}}\n" f"请只输出有效的 JSON 对象。" ) @@ -382,13 +427,16 @@ class SubHeartflowManager: elif current_subflow_state == ChatState.CHAT: # 构建Prompt prompt = ( - f"子心流 [{stream_name}] 当前处于常规聊天(CHAT)状态.\n" + f"{prompt_personality}\n" + f"你正在在: [{stream_name}] 群中聊天。\n" f"{mai_state_description}\n" - f"最近观察到的内容摘要:\n---\n{combined_summary}\n---\n" - f"基于以上信息,该子心流是否表现出不活跃、对话结束或不再需要关注的迹象," - f"应该让其进入休眠(ABSENT)状态?\n" - f"请以 JSON 格式回答,包含一个键 'decision',其值为 true (表示应休眠) 或 false (表示不应休眠).\n" - f"例如:{{\"decision\": true}}\n" + f"这个群里最近的聊天内容是:\n---\n{combined_summary}\n---\n" + f"基于以上信息,请判断你是否愿意在这个群继续闲聊," + f"还是暂时离开聊天,进入休眠状态?\n" + f"给出你的判断,和理由,然后以 JSON 格式回答" + f"包含键 'decision',如果要离开聊天,值为 true ,否则为 false.\n" + f"包含键 'reason',其值为你的理由。\n" + f"例如:{{\"decision\": true, \"reason\": \"因为我想休息\"}}\n" f"请只输出有效的 JSON 对象。" ) @@ -423,7 +471,8 @@ class SubHeartflowManager: try: # --- 真实的 LLM 调用 --- response_text, _ = await self.llm_state_evaluator.generate_response_async(prompt) - logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估") + # logger.debug(f"{log_prefix} 使用模型 {self.llm_state_evaluator.model_name} 评估") + logger.debug(f"{log_prefix} 原始输入: {prompt}") logger.debug(f"{log_prefix} 原始响应: {response_text}") # --- 解析 JSON 响应 --- From 574c104ef0aff2cac9033565f3ab56763380f282 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Sat, 26 Apr 2025 21:11:46 +0800 Subject: [PATCH 68/79] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/chat_states.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/PFC/chat_states.py b/src/plugins/PFC/chat_states.py index dc7b728b0..1f8ee15fb 100644 --- a/src/plugins/PFC/chat_states.py +++ b/src/plugins/PFC/chat_states.py @@ -98,7 +98,7 @@ class NotificationManager: notification_type: 要处理的通知类型 handler: 处理器实例 """ - print(1145145511114445551111444) + # print(1145145511114445551111444) if target not in self._handlers: # print("没11有target") self._handlers[target] = {} @@ -146,9 +146,9 @@ class NotificationManager: if target in self._handlers: handlers = self._handlers[target].get(notification.type, []) # print(1111111) - print(handlers) + # print(handlers) for handler in handlers: - print(f"调用处理器: {handler}") + # print(f"调用处理器: {handler}") await handler.handle_notification(notification) def get_active_states(self) -> Set[NotificationType]: From e99876a02a390e8ff8a360e9a195f450d2414c0a Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sat, 26 Apr 2025 21:45:09 +0800 Subject: [PATCH 69/79] minor fix --- src/plugins/models/utils_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 2cab7b629..5ab82b42b 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -820,6 +820,7 @@ class LLMRequest: policy = request_content["policy"] payload = request_content["payload"] wait_time = policy["base_wait"] * (2**retry_count) + keep_request = False if retry_count < policy["max_retries"] - 1: keep_request = True if isinstance(exception, RequestAbortException): From dafc5ded95632bae04240dd5bfc9a40a9a928ba4 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sat, 26 Apr 2025 21:46:01 +0800 Subject: [PATCH 70/79] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=8E=89=E7=9A=84=E8=AF=B7=E6=B1=82=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/models/utils_model.py | 389 ------------------------------ 1 file changed, 389 deletions(-) diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index 5ab82b42b..7c87cf946 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -178,395 +178,6 @@ class LLMRequest: output_cost = (completion_tokens / 1000000) * self.pri_out return round(input_cost + output_cost, 6) - ''' - async def _execute_request( - self, - endpoint: str, - prompt: str = None, - image_base64: str = None, - image_format: str = None, - payload: dict = None, - retry_policy: dict = None, - response_handler: callable = None, - user_id: str = "system", - request_type: str = None, - ): - """统一请求执行入口 - Args: - endpoint: API端点路径 (如 "chat/completions") - prompt: prompt文本 - image_base64: 图片的base64编码 - image_format: 图片格式 - payload: 请求体数据 - retry_policy: 自定义重试策略 - response_handler: 自定义响应处理器 - user_id: 用户ID - request_type: 请求类型 - """ - - if request_type is None: - request_type = self.request_type - - # 合并重试策略 - default_retry = { - "max_retries": 3, - "base_wait": 10, - "retry_codes": [429, 413, 500, 503], - "abort_codes": [400, 401, 402, 403], - } - policy = {**default_retry, **(retry_policy or {})} - - # 常见Error Code Mapping - error_code_mapping = { - 400: "参数不正确", - 401: "API key 错误,认证失败,请检查/config/bot_config.toml和.env中的配置是否正确哦~", - 402: "账号余额不足", - 403: "需要实名,或余额不足", - 404: "Not Found", - 429: "请求过于频繁,请稍后再试", - 500: "服务器内部故障", - 503: "服务器负载过高", - } - - api_url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" - # 判断是否为流式 - stream_mode = self.stream - # logger_msg = "进入流式输出模式," if stream_mode else "" - # logger.debug(f"{logger_msg}发送请求到URL: {api_url}") - # logger.info(f"使用模型: {self.model_name}") - - # 构建请求体 - if image_base64: - payload = await self._build_payload(prompt, image_base64, image_format) - elif payload is None: - payload = await self._build_payload(prompt) - - # 流式输出标志 - # 先构建payload,再添加流式输出标志 - if stream_mode: - payload["stream"] = stream_mode - - for retry in range(policy["max_retries"]): - try: - # 使用上下文管理器处理会话 - headers = await self._build_headers() - # 似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响 - if stream_mode: - headers["Accept"] = "text/event-stream" - - async with aiohttp.ClientSession() as session: - try: - async with session.post(api_url, headers=headers, json=payload) as response: - # 处理需要重试的状态码 - if response.status in policy["retry_codes"]: - wait_time = policy["base_wait"] * (2**retry) - logger.warning( - f"模型 {self.model_name} 错误码: {response.status}, 等待 {wait_time}秒后重试" - ) - if response.status == 413: - logger.warning("请求体过大,尝试压缩...") - image_base64 = compress_base64_image_by_scale(image_base64) - payload = await self._build_payload(prompt, image_base64, image_format) - elif response.status in [500, 503]: - logger.error( - f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}" - ) - raise RuntimeError("服务器负载过高,模型恢复失败QAQ") - else: - logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...") - - await asyncio.sleep(wait_time) - continue - elif response.status in policy["abort_codes"]: - logger.error( - f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}" - ) - # 尝试获取并记录服务器返回的详细错误信息 - try: - error_json = await response.json() - if error_json and isinstance(error_json, list) and len(error_json) > 0: - for error_item in error_json: - if "error" in error_item and isinstance(error_item["error"], dict): - error_obj = error_item["error"] - error_code = error_obj.get("code") - error_message = error_obj.get("message") - error_status = error_obj.get("status") - logger.error( - f"服务器错误详情: 代码={error_code}, 状态={error_status}, " - f"消息={error_message}" - ) - elif isinstance(error_json, dict) and "error" in error_json: - # 处理单个错误对象的情况 - error_obj = error_json.get("error", {}) - error_code = error_obj.get("code") - error_message = error_obj.get("message") - error_status = error_obj.get("status") - logger.error( - f"服务器错误详情: 代码={error_code}, 状态={error_status}, 消息={error_message}" - ) - else: - # 记录原始错误响应内容 - logger.error(f"服务器错误响应: {error_json}") - except Exception as e: - logger.warning(f"无法解析服务器错误响应: {str(e)}") - - if response.status == 403: - # 只针对硅基流动的V3和R1进行降级处理 - if ( - self.model_name.startswith("Pro/deepseek-ai") - and self.base_url == "https://api.siliconflow.cn/v1/" - ): - old_model_name = self.model_name - self.model_name = self.model_name[4:] # 移除"Pro/"前缀 - logger.warning( - f"检测到403错误,模型从 {old_model_name} 降级为 {self.model_name}" - ) - - # 对全局配置进行更新 - if global_config.llm_normal.get("name") == old_model_name: - global_config.llm_normal["name"] = self.model_name - logger.warning(f"将全局配置中的 llm_normal 模型临时降级至{self.model_name}") - - if global_config.llm_reasoning.get("name") == old_model_name: - global_config.llm_reasoning["name"] = self.model_name - logger.warning( - f"将全局配置中的 llm_reasoning 模型临时降级至{self.model_name}" - ) - - # 更新payload中的模型名 - if payload and "model" in payload: - payload["model"] = self.model_name - - # 重新尝试请求 - retry -= 1 # 不计入重试次数 - continue - - raise RuntimeError(f"请求被拒绝: {error_code_mapping.get(response.status)}") - - response.raise_for_status() - reasoning_content = "" - - # 将流式输出转化为非流式输出 - if stream_mode: - flag_delta_content_finished = False - accumulated_content = "" - usage = None # 初始化usage变量,避免未定义错误 - - async for line_bytes in response.content: - try: - line = line_bytes.decode("utf-8").strip() - if not line: - continue - if line.startswith("data:"): - data_str = line[5:].strip() - if data_str == "[DONE]": - break - try: - chunk = json.loads(data_str) - if flag_delta_content_finished: - chunk_usage = chunk.get("usage", None) - if chunk_usage: - usage = chunk_usage # 获取token用量 - else: - delta = chunk["choices"][0]["delta"] - delta_content = delta.get("content") - if delta_content is None: - delta_content = "" - accumulated_content += delta_content - # 检测流式输出文本是否结束 - finish_reason = chunk["choices"][0].get("finish_reason") - if delta.get("reasoning_content", None): - reasoning_content += delta["reasoning_content"] - if finish_reason == "stop": - chunk_usage = chunk.get("usage", None) - if chunk_usage: - usage = chunk_usage - break - # 部分平台在文本输出结束前不会返回token用量,此时需要再获取一次chunk - flag_delta_content_finished = True - - except Exception as e: - logger.exception(f"模型 {self.model_name} 解析流式输出错误: {str(e)}") - except GeneratorExit: - logger.warning("模型 {self.model_name} 流式输出被中断,正在清理资源...") - # 确保资源被正确清理 - await response.release() - # 返回已经累积的内容 - result = { - "choices": [ - { - "message": { - "content": accumulated_content, - "reasoning_content": reasoning_content, - # 流式输出可能没有工具调用,此处不需要添加tool_calls字段 - } - } - ], - "usage": usage, - } - return ( - response_handler(result) - if response_handler - else self._default_response_handler(result, user_id, request_type, endpoint) - ) - except Exception as e: - logger.error(f"模型 {self.model_name} 处理流式输出时发生错误: {str(e)}") - # 确保在发生错误时也能正确清理资源 - try: - await response.release() - except Exception as cleanup_error: - logger.error(f"清理资源时发生错误: {cleanup_error}") - # 返回已经累积的内容 - result = { - "choices": [ - { - "message": { - "content": accumulated_content, - "reasoning_content": reasoning_content, - # 流式输出可能没有工具调用,此处不需要添加tool_calls字段 - } - } - ], - "usage": usage, - } - return ( - response_handler(result) - if response_handler - else self._default_response_handler(result, user_id, request_type, endpoint) - ) - content = accumulated_content - think_match = re.search(r"(.*?)", content, re.DOTALL) - if think_match: - reasoning_content = think_match.group(1).strip() - content = re.sub(r".*?", "", content, flags=re.DOTALL).strip() - # 构造一个伪result以便调用自定义响应处理器或默认处理器 - result = { - "choices": [ - { - "message": { - "content": content, - "reasoning_content": reasoning_content, - # 流式输出可能没有工具调用,此处不需要添加tool_calls字段 - } - } - ], - "usage": usage, - } - return ( - response_handler(result) - if response_handler - else self._default_response_handler(result, user_id, request_type, endpoint) - ) - else: - result = await response.json() - # 使用自定义处理器或默认处理 - return ( - response_handler(result) - if response_handler - else self._default_response_handler(result, user_id, request_type, endpoint) - ) - - except (aiohttp.ClientError, asyncio.TimeoutError) as e: - if retry < policy["max_retries"] - 1: - wait_time = policy["base_wait"] * (2**retry) - logger.error(f"模型 {self.model_name} 网络错误,等待{wait_time}秒后重试... 错误: {str(e)}") - await asyncio.sleep(wait_time) - continue - else: - logger.critical(f"模型 {self.model_name} 网络错误达到最大重试次数: {str(e)}") - raise RuntimeError(f"网络请求失败: {str(e)}") from e - except Exception as e: - logger.critical(f"模型 {self.model_name} 未预期的错误: {str(e)}") - raise RuntimeError(f"请求过程中发生错误: {str(e)}") from e - - except aiohttp.ClientResponseError as e: - # 处理aiohttp抛出的响应错误 - if retry < policy["max_retries"] - 1: - wait_time = policy["base_wait"] * (2**retry) - logger.error( - f"模型 {self.model_name} HTTP响应错误,等待{wait_time}秒后重试... 状态码: {e.status}, 错误: {e.message}" - ) - try: - if hasattr(e, "response") and e.response and hasattr(e.response, "text"): - error_text = await e.response.text() - try: - error_json = json.loads(error_text) - if isinstance(error_json, list) and len(error_json) > 0: - for error_item in error_json: - if "error" in error_item and isinstance(error_item["error"], dict): - error_obj = error_item["error"] - logger.error( - f"模型 {self.model_name} 服务器错误详情: 代码={error_obj.get('code')}, " - f"状态={error_obj.get('status')}, " - f"消息={error_obj.get('message')}" - ) - elif isinstance(error_json, dict) and "error" in error_json: - error_obj = error_json.get("error", {}) - logger.error( - f"模型 {self.model_name} 服务器错误详情: 代码={error_obj.get('code')}, " - f"状态={error_obj.get('status')}, " - f"消息={error_obj.get('message')}" - ) - else: - logger.error(f"模型 {self.model_name} 服务器错误响应: {error_json}") - except (json.JSONDecodeError, TypeError) as json_err: - logger.warning( - f"模型 {self.model_name} 响应不是有效的JSON: {str(json_err)}, 原始内容: {error_text[:200]}" - ) - except (AttributeError, TypeError, ValueError) as parse_err: - logger.warning(f"模型 {self.model_name} 无法解析响应错误内容: {str(parse_err)}") - - await asyncio.sleep(wait_time) - else: - logger.critical( - f"模型 {self.model_name} HTTP响应错误达到最大重试次数: 状态码: {e.status}, 错误: {e.message}" - ) - # 安全地检查和记录请求详情 - if ( - image_base64 - and payload - and isinstance(payload, dict) - and "messages" in payload - and len(payload["messages"]) > 0 - ): - if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: - content = payload["messages"][0]["content"] - if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: - payload["messages"][0]["content"][1]["image_url"]["url"] = ( - f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," - f"{image_base64[:10]}...{image_base64[-10:]}" - ) - logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {payload}") - raise RuntimeError(f"模型 {self.model_name} API请求失败: 状态码 {e.status}, {e.message}") from e - except Exception as e: - if retry < policy["max_retries"] - 1: - wait_time = policy["base_wait"] * (2**retry) - logger.error(f"模型 {self.model_name} 请求失败,等待{wait_time}秒后重试... 错误: {str(e)}") - await asyncio.sleep(wait_time) - else: - logger.critical(f"模型 {self.model_name} 请求失败: {str(e)}") - # 安全地检查和记录请求详情 - if ( - image_base64 - and payload - and isinstance(payload, dict) - and "messages" in payload - and len(payload["messages"]) > 0 - ): - if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]: - content = payload["messages"][0]["content"] - if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]: - payload["messages"][0]["content"][1]["image_url"]["url"] = ( - f"data:image/{image_format.lower() if image_format else 'jpeg'};base64," - f"{image_base64[:10]}...{image_base64[-10:]}" - ) - logger.critical(f"请求头: {await self._build_headers(no_key=True)} 请求体: {payload}") - raise RuntimeError(f"模型 {self.model_name} API请求失败: {str(e)}") from e - - logger.error(f"模型 {self.model_name} 达到最大重试次数,请求仍然失败") - raise RuntimeError(f"模型 {self.model_name} 达到最大重试次数,API请求仍然失败") - ''' - async def _prepare_request( self, endpoint: str, From d50c2df0f6063092efe08a8ecb9c70e39213362c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Sat, 26 Apr 2025 21:49:11 +0800 Subject: [PATCH 71/79] fix: Ruff --- src/heart_flow/sub_heartflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 9274acf70..63d4b4a0e 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -2,7 +2,7 @@ from .observation import Observation, ChattingObservation import asyncio from src.config.config import global_config import time -from typing import Optional, List, Dict, Tuple, TYPE_CHECKING, Callable, Coroutine +from typing import Optional, List, Dict, Tuple, Callable, Coroutine import traceback from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 import random From 3931423e8e2b4b777fcbf5a3c710f9f7bbea25c3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 13:49:24 +0000 Subject: [PATCH 72/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/background_tasks.py | 2 -- src/heart_flow/sub_heartflow.py | 6 ++-- src/heart_flow/subheartflow_manager.py | 44 +++++++++++------------- src/plugins/heartFC_chat/heartFC_chat.py | 33 ++++++++++-------- 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 076f441c9..d2bd93213 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -255,7 +255,6 @@ class BackgroundTaskManager: # --- 结束新增 --- - # --- 结束新增 --- # --- Specific Task Runners --- # @@ -286,4 +285,3 @@ class BackgroundTaskManager: interval=self.interest_eval_interval, task_func=self._perform_interest_eval_work, ) - diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 63d4b4a0e..ead07f53c 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -243,7 +243,7 @@ class SubHeartflow: self, subheartflow_id, mai_states: MaiStateInfo, - hfc_no_reply_callback: Callable[[], Coroutine[None, None, None]] + hfc_no_reply_callback: Callable[[], Coroutine[None, None, None]], ): """子心流初始化函数 @@ -380,8 +380,8 @@ class SubHeartflow: self.heart_fc_instance = HeartFChatting( chat_id=self.subheartflow_id, sub_mind=self.sub_mind, - observations=self.observations, # 传递所有观察者 - on_consecutive_no_reply_callback=self.hfc_no_reply_callback # <-- Use stored callback + observations=self.observations, # 传递所有观察者 + on_consecutive_no_reply_callback=self.hfc_no_reply_callback, # <-- Use stored callback ) # 初始化并启动 HeartFChatting diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index cf6e01b6a..50cf38b03 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -2,8 +2,8 @@ import asyncio import time import random from typing import Dict, Any, Optional, List -import json # 导入 json 模块 -import functools # <-- 新增导入 +import json # 导入 json 模块 +import functools # <-- 新增导入 # 导入日志模块 from src.common.logger import get_module_logger, LogConfig, SUBHEARTFLOW_MANAGER_STYLE_CONFIG @@ -88,7 +88,7 @@ class SubHeartflowManager: new_subflow = SubHeartflow( subheartflow_id, self.mai_state_info, - hfc_callback # <-- 传递 partial 创建的回调 + hfc_callback, # <-- 传递 partial 创建的回调 ) # 异步初始化 @@ -134,20 +134,23 @@ class SubHeartflowManager: if subflow.chat_state.chat_status == ChatState.ABSENT: return True else: - logger.warning(f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}") + logger.warning( + f"{log_prefix} 调用 change_chat_state 后,{stream_name} 状态仍为 {subflow.chat_state.chat_status.value}" + ) return False except Exception as e: logger.error(f"{log_prefix} 设置 {stream_name} 状态为 ABSENT 时失败: {e}", exc_info=True) return False else: logger.debug(f"{log_prefix} {stream_name} 已是 ABSENT 状态") - return True # 已经是目标状态,视为成功 + return True # 已经是目标状态,视为成功 + # --- 结束新增 --- async def sleep_subheartflow(self, subheartflow_id: Any, reason: str) -> bool: """停止指定的子心流并将其状态设置为 ABSENT""" log_prefix = "[子心流管理]" - async with self._lock: # 加锁以安全访问字典 + async with self._lock: # 加锁以安全访问字典 subheartflow = self.subheartflows.get(subheartflow_id) stream_name = chat_manager.get_stream_name(subheartflow_id) or subheartflow_id @@ -229,7 +232,7 @@ class SubHeartflowManager: changed_count = 0 processed_count = 0 - async with self._lock: # 获取锁以安全迭代 + async with self._lock: # 获取锁以安全迭代 # 使用 list() 创建一个当前值的快照,防止在迭代时修改字典 flows_to_update = list(self.subheartflows.values()) processed_count = len(flows_to_update) @@ -239,8 +242,7 @@ class SubHeartflowManager: for subflow in flows_to_update: # 记录原始状态,以便统计实际改变的数量 - original_state_was_absent = (subflow.chat_state.chat_status == ChatState.ABSENT) - + original_state_was_absent = subflow.chat_state.chat_status == ChatState.ABSENT success = await self._try_set_subflow_absent_internal(subflow, log_prefix) @@ -345,7 +347,6 @@ class SubHeartflowManager: log_prefix = f"[{stream_name}]" current_subflow_state = sub_hf.chat_state.chat_status - _observation_summary = "没有可用的观察信息。" # 默认值 first_observation = sub_hf.observations[0] @@ -357,12 +358,10 @@ class SubHeartflowManager: else: logger.warning(f"{log_prefix} [{stream_name}] 第一个观察者不是 ChattingObservation 类型。") - - # --- 获取麦麦状态 --- mai_state_description = f"你当前状态: {current_mai_state.value}。" - - # 获取个性化信息 + + # 获取个性化信息 individuality = Individuality.get_instance() # 构建个性部分 @@ -378,7 +377,6 @@ class SubHeartflowManager: if individuality.identity.identity_detail: random_detail = random.choice(individuality.identity.identity_detail) prompt_personality += f",{random_detail}" - # --- 针对 ABSENT 状态 --- if current_subflow_state == ChatState.ABSENT: @@ -392,14 +390,14 @@ class SubHeartflowManager: f"进入常规聊天(CHAT)状态?\n" f"给出你的判断,和理由,然后以 JSON 格式回答" f"包含键 'decision',如果要开始聊天,值为 true ,否则为 false.\n" - f"包含键 'reason',其值为你的理由。\n" - f"例如:{{\"decision\": true, \"reason\": \"因为我想聊天\"}}\n" + f"包含键 'reason',其值为你的理由。\n" + f'例如:{{"decision": true, "reason": "因为我想聊天"}}\n' f"请只输出有效的 JSON 对象。" ) # 调用LLM评估 should_activate = await self._llm_evaluate_state_transition(prompt) - if should_activate is None: # 处理解析失败或意外情况 + if should_activate is None: # 处理解析失败或意外情况 logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。") continue @@ -435,14 +433,14 @@ class SubHeartflowManager: f"还是暂时离开聊天,进入休眠状态?\n" f"给出你的判断,和理由,然后以 JSON 格式回答" f"包含键 'decision',如果要离开聊天,值为 true ,否则为 false.\n" - f"包含键 'reason',其值为你的理由。\n" - f"例如:{{\"decision\": true, \"reason\": \"因为我想休息\"}}\n" + f"包含键 'reason',其值为你的理由。\n" + f'例如:{{"decision": true, "reason": "因为我想休息"}}\n' f"请只输出有效的 JSON 对象。" ) # 调用LLM评估 should_deactivate = await self._llm_evaluate_state_transition(prompt) - if should_deactivate is None: # 处理解析失败或意外情况 + if should_deactivate is None: # 处理解析失败或意外情况 logger.warning(f"{log_prefix}LLM评估返回无效结果,跳过。") continue @@ -453,8 +451,6 @@ class SubHeartflowManager: transitioned_to_absent += 1 else: logger.info(f"{log_prefix}LLM建议不进入ABSENT状态。") - - async def _llm_evaluate_state_transition(self, prompt: str) -> Optional[bool]: """ @@ -573,6 +569,7 @@ class SubHeartflowManager: # 注意:这里不需要再获取锁,因为 request_absent_transition 内部会处理锁 logger.debug(f"[管理器 HFC 处理器] 接收到来自 {subheartflow_id} 的 HFC 无回复信号") await self.request_absent_transition(subheartflow_id) + # --- 结束新增 --- # # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # @@ -608,4 +605,5 @@ class SubHeartflowManager: logger.warning( f"[状态转换请求] 收到对 {stream_name} 的请求,但其状态为 {current_state.value} (非 FOCUSED),不执行转换" ) + # --- 结束新增 --- # diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 9a2862adb..8f376b37f 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -154,14 +154,14 @@ class HeartFChatting: 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ - CONSECUTIVE_NO_REPLY_THRESHOLD = 4 # 连续不回复的阈值 + CONSECUTIVE_NO_REPLY_THRESHOLD = 4 # 连续不回复的阈值 def __init__( self, chat_id: str, sub_mind: SubMind, observations: Observation, - on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]] + on_consecutive_no_reply_callback: Callable[[], Coroutine[None, None, None]], ): """ HeartFChatting 初始化函数 @@ -209,8 +209,8 @@ class HeartFChatting: self._cycle_counter = 0 self._cycle_history: Deque[CycleInfo] = deque(maxlen=10) # 保留最近10个循环的信息 self._current_cycle: Optional[CycleInfo] = None - self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器 - self._shutting_down: bool = False # <--- 新增:关闭标志位 + self._lian_xu_bu_hui_fu_ci_shu: int = 0 # <--- 新增:连续不回复计数器 + self._shutting_down: bool = False # <--- 新增:关闭标志位 async def _initialize(self) -> bool: """ @@ -309,9 +309,9 @@ class HeartFChatting: # 如果未能获取锁(理论上不太可能,除非 shutdown 过程中释放了但又被抢了?) # 或者也可以在这里再次检查 self._shutting_down if self._shutting_down: - break # 再次检查,确保退出 + break # 再次检查,确保退出 logger.warning(f"{self.log_prefix} 未能获取循环处理锁,跳过本次循环。") - await asyncio.sleep(0.1) # 短暂等待避免空转 + await asyncio.sleep(0.1) # 短暂等待避免空转 continue # 记录规划开始时间点 @@ -600,27 +600,30 @@ class HeartFChatting: if not self._shutting_down: self._lian_xu_bu_hui_fu_ci_shu += 1 - logger.debug(f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}") + logger.debug( + f"{self.log_prefix} 连续不回复计数增加: {self._lian_xu_bu_hui_fu_ci_shu}/{self.CONSECUTIVE_NO_REPLY_THRESHOLD}" + ) # 检查是否达到阈值 if self._lian_xu_bu_hui_fu_ci_shu >= self.CONSECUTIVE_NO_REPLY_THRESHOLD: - logger.info(f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次),调用回调请求状态转换") + logger.info( + f"{self.log_prefix} 连续不回复达到阈值 ({self._lian_xu_bu_hui_fu_ci_shu}次),调用回调请求状态转换" + ) # 调用回调。注意:这里不重置计数器,依赖回调函数成功改变状态来隐式重置上下文。 await self.on_consecutive_no_reply_callback() - - return True + return True except asyncio.CancelledError: # 如果在等待过程中任务被取消(可能是因为 shutdown) logger.info(f"{self.log_prefix} 处理 'no_reply' 时等待被中断 (CancelledError)") # 让异常向上传播,由 _hfc_loop 的异常处理逻辑接管 raise - except Exception as e: # 捕获调用管理器或其他地方可能发生的错误 + except Exception as e: # 捕获调用管理器或其他地方可能发生的错误 logger.error(f"{self.log_prefix} 处理 'no_reply' 时发生错误: {e}") logger.error(traceback.format_exc()) # 发生意外错误时,可以选择是否重置计数器,这里选择不重置 - return False # 表示动作未成功 + return False # 表示动作未成功 async def _wait_for_new_message(self, observation, planner_start_db_time: float, log_prefix: str) -> bool: """ @@ -639,7 +642,7 @@ class HeartFChatting: # --- 在每次循环开始时检查关闭标志 --- if self._shutting_down: logger.info(f"{log_prefix} 等待新消息时检测到关闭信号,中断等待。") - return False # 表示因为关闭而退出 + return False # 表示因为关闭而退出 # ----------------------------------- # 检查新消息 @@ -654,7 +657,7 @@ class HeartFChatting: try: # 短暂休眠,让其他任务有机会运行,并能更快响应取消或关闭 - await asyncio.sleep(0.5) # 缩短休眠时间 + await asyncio.sleep(0.5) # 缩短休眠时间 except asyncio.CancelledError: # 如果在休眠时被取消,再次检查关闭标志 # 如果是正常关闭,则不需要警告 @@ -910,7 +913,7 @@ class HeartFChatting: async def shutdown(self): """优雅关闭HeartFChatting实例,取消活动循环任务""" logger.info(f"{self.log_prefix} 正在关闭HeartFChatting...") - self._shutting_down = True # <-- 在开始关闭时设置标志位 + self._shutting_down = True # <-- 在开始关闭时设置标志位 # 取消循环任务 if self._loop_task and not self._loop_task.done(): From 84e87d2886eba1fe771d8c4396b59d993a725f14 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 21:57:26 +0800 Subject: [PATCH 73/79] Update subheartflow_manager.py --- src/heart_flow/subheartflow_manager.py | 96 ++++++++++++++------------ 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 50cf38b03..6d22494d4 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -262,63 +262,67 @@ class SubHeartflowManager: async def evaluate_interest_and_promote(self): """评估子心流兴趣度,满足条件且未达上限则提升到FOCUSED状态(基于start_hfc_probability)""" - log_prefix = "[兴趣评估]" - # 使用 self.mai_state_info 获取当前状态和限制 - current_state = self.mai_state_info.get_current_state() - focused_limit = current_state.get_focused_chat_max_num() + try: + log_prefix = "[兴趣评估]" + # 使用 self.mai_state_info 获取当前状态和限制 + current_state = self.mai_state_info.get_current_state() + focused_limit = current_state.get_focused_chat_max_num() + logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 开始尝试提升到FOCUSED状态") - if int(time.time()) % 20 == 0: # 每20秒输出一次 - logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天") + if int(time.time()) % 20 == 0: # 每20秒输出一次 + logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 可以在{focused_limit}个群激情聊天") - if focused_limit <= 0: - # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流") - return + if focused_limit <= 0: + # logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 不允许 FOCUSED 子心流") + return - current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED) - if current_focused_count >= focused_limit: - logger.debug(f"{log_prefix} 已达专注上限 ({current_focused_count}/{focused_limit})") - return + current_focused_count = self.count_subflows_by_state(ChatState.FOCUSED) + if current_focused_count >= focused_limit: + logger.debug(f"{log_prefix} 已达专注上限 ({current_focused_count}/{focused_limit})") + return - for sub_hf in list(self.subheartflows.values()): - flow_id = sub_hf.subheartflow_id - stream_name = chat_manager.get_stream_name(flow_id) or flow_id + for sub_hf in list(self.subheartflows.values()): + flow_id = sub_hf.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id - # 跳过非CHAT状态或已经是FOCUSED状态的子心流 - if sub_hf.chat_state.chat_status == ChatState.FOCUSED: - continue - - from .mai_state_manager import enable_unlimited_hfc_chat - - if not enable_unlimited_hfc_chat: - if sub_hf.chat_state.chat_status != ChatState.CHAT: + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 + if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue - # 检查是否满足提升概率 - if random.random() >= sub_hf.interest_chatting.start_hfc_probability: - continue + from .mai_state_manager import enable_unlimited_hfc_chat - # 再次检查是否达到上限 - if current_focused_count >= focused_limit: - logger.debug(f"{log_prefix} [{stream_name}] 已达专注上限") - break + if not enable_unlimited_hfc_chat: + if sub_hf.chat_state.chat_status != ChatState.CHAT: + continue - # 获取最新状态并执行提升 - current_subflow = self.subheartflows.get(flow_id) - if not current_subflow: - continue + # 检查是否满足提升概率 + if random.random() >= sub_hf.interest_chatting.start_hfc_probability: + continue - logger.info( - f"{log_prefix} [{stream_name}] 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" - ) + # 再次检查是否达到上限 + if current_focused_count >= focused_limit: + logger.debug(f"{log_prefix} [{stream_name}] 已达专注上限") + break - # 执行状态提升 - await current_subflow.change_chat_state(ChatState.FOCUSED) + # 获取最新状态并执行提升 + current_subflow = self.subheartflows.get(flow_id) + if not current_subflow: + continue - # 验证提升结果 - if ( - final_subflow := self.subheartflows.get(flow_id) - ) and final_subflow.chat_state.chat_status == ChatState.FOCUSED: - current_focused_count += 1 + logger.info( + f"{log_prefix} [{stream_name}] 触发 认真水群 (概率={current_subflow.interest_chatting.start_hfc_probability:.2f})" + ) + + # 执行状态提升 + await current_subflow.change_chat_state(ChatState.FOCUSED) + + # 验证提升结果 + if ( + final_subflow := self.subheartflows.get(flow_id) + ) and final_subflow.chat_state.chat_status == ChatState.FOCUSED: + current_focused_count += 1 + except Exception as e: + logger.error(f"启动HFC 兴趣评估失败: {e}", exc_info=True) async def evaluate_and_transition_subflows_by_llm(self): """ @@ -352,7 +356,7 @@ class SubHeartflowManager: first_observation = sub_hf.observations[0] if isinstance(first_observation, ChattingObservation): # 组合中期记忆和当前聊天内容 - first_observation.observe() + await first_observation.observe() current_chat = first_observation.talking_message_str or "当前无聊天内容。" combined_summary = f"当前聊天内容:\n{current_chat}" else: From 236451d246c52f6cd85375376011f50841c1108a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 22:55:29 +0800 Subject: [PATCH 74/79] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=B2=A1=E6=9C=89=E5=8F=8A=E6=97=B6=E8=BD=AC=E7=A7=BB?= =?UTF-8?q?=EF=BC=8C=E5=92=8C=E6=A6=82=E7=8E=87=E5=8F=AF=E8=83=BD=E4=B8=BA?= =?UTF-8?q?=E8=B4=9F=E7=9A=84BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/mai_state_manager.py | 16 ++++++++-------- src/heart_flow/sub_heartflow.py | 6 +++--- src/heart_flow/subheartflow_manager.py | 4 ++++ src/plugins/heartFC_chat/heartFC_chat.py | 2 +- src/plugins/heartFC_chat/normal_chat.py | 20 ++++++++++++-------- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 64fd40489..ee5cd95ee 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -37,13 +37,13 @@ class MaiState(enum.Enum): return 1000 if self == MaiState.OFFLINE: - return 0 + return 10 elif self == MaiState.PEEKING: - return 2 + return 30 elif self == MaiState.NORMAL_CHAT: - return 3 + return 40 elif self == MaiState.FOCUSED_CHAT: - return 2 + return 30 def get_focused_chat_max_num(self): # 调试用 @@ -51,13 +51,13 @@ class MaiState(enum.Enum): return 1000 if self == MaiState.OFFLINE: - return 0 + return 10 elif self == MaiState.PEEKING: - return 1 + return 20 elif self == MaiState.NORMAL_CHAT: - return 1 + return 30 elif self == MaiState.FOCUSED_CHAT: - return 3 + return 40 class MaiStateInfo: diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index ead07f53c..6ea60ea1f 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -151,8 +151,8 @@ class InterestChatting: if self.above_threshold: self.start_hfc_probability += 0.1 else: - if self.start_hfc_probability != 0: - self.start_hfc_probability -= 0.1 + if self.start_hfc_probability > 0: + self.start_hfc_probability = max(0, self.start_hfc_probability - 0.1) async def increase_interest(self, value: float): self.interest_level += value @@ -170,7 +170,7 @@ class InterestChatting: return { "interest_level": round(interest, 2), "start_hfc_probability": round(self.start_hfc_probability, 4), - "is_above_threshold": self.is_above_threshold, + "above_threshold": self.above_threshold, } async def should_evaluate_reply(self) -> bool: diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 6d22494d4..35efbc550 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -267,6 +267,7 @@ class SubHeartflowManager: # 使用 self.mai_state_info 获取当前状态和限制 current_state = self.mai_state_info.get_current_state() focused_limit = current_state.get_focused_chat_max_num() + logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 开始尝试提升到FOCUSED状态") if int(time.time()) % 20 == 0: # 每20秒输出一次 @@ -285,6 +286,8 @@ class SubHeartflowManager: flow_id = sub_hf.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id + logger.debug(f"{log_prefix} 检查子心流: {stream_name},现在状态: {sub_hf.chat_state.chat_status.value}") + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue @@ -296,6 +299,7 @@ class SubHeartflowManager: continue # 检查是否满足提升概率 + logger.debug(f"{log_prefix} 检查子心流: {stream_name},现在概率: {sub_hf.interest_chatting.start_hfc_probability}") if random.random() >= sub_hf.interest_chatting.start_hfc_probability: continue diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 8f376b37f..4f96353fd 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -154,7 +154,7 @@ class HeartFChatting: 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ - CONSECUTIVE_NO_REPLY_THRESHOLD = 4 # 连续不回复的阈值 + CONSECUTIVE_NO_REPLY_THRESHOLD = 1 # 连续不回复的阈值 def __init__( self, diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 912da3520..b54b28187 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -409,19 +409,23 @@ class NormalChat: async def start_chat(self): """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行), - 并在启动前处理一次初始的高兴趣消息。""" + 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。 if self._chat_task is None or self._chat_task.done(): - # --- 修改:调用新的私有方法处理初始消息 --- - await self._process_initial_interest_messages() + # --- 修改:使用 create_task 启动初始消息处理 --- + logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") + # 创建一个任务来处理初始消息,不阻塞当前流程 + initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) + # 可以考虑给这个任务也添加完成回调来记录日志或处理错误 + # initial_process_task.add_done_callback(...) # --- 修改结束 --- - # 启动后台轮询任务 + # 启动后台轮询任务 (这部分不变) logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...") - task = asyncio.create_task(self._reply_interested_message()) - task.add_done_callback(lambda t: self._handle_task_completion(t)) # 回调现在是实例方法 - self._chat_task = task + polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分 + polling_task.add_done_callback(lambda t: self._handle_task_completion(t)) + self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务 else: - logger.info(f"[{self.stream_name}] 聊天任务已在运行中。") + logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。") def _handle_task_completion(self, task: asyncio.Task): """任务完成回调处理""" From 0ee2c04e06475a7fc96ed95d00f3ecaf8038990d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 23:22:59 +0800 Subject: [PATCH 75/79] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=AB=9E?= =?UTF-8?q?=E4=BA=89bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/README.md | 72 ++++++++++++++++++- .../heartFC_chat/heartflow_prompt_builder.py | 3 +- src/plugins/heartFC_chat/normal_chat.py | 19 +++-- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index 4ccb662dd..7f6906aea 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -141,6 +141,7 @@ c HeartFChatting工作方式 - `MaiState.PEEKING` (看一眼手机): 有限度地参与聊天(由 `MaiStateInfo` 定义具体的普通/专注群数量限制)。 - `MaiState.NORMAL_CHAT` (正常看手机): 正常参与聊天,允许 `SubHeartflow` 进入 `CHAT` 或 `FOCUSED` 状态(数量受限)。 * `MaiState.FOCUSED_CHAT` (专心看手机): 更积极地参与聊天,通常允许更多或更高优先级的 `FOCUSED` 状态子心流。 +- **当前转换逻辑**: 目前,`MaiState` 之间的转换由 `MaiStateManager` 管理,主要基于状态持续时间和随机概率。这是一种临时的实现方式,未来计划进行改进。 - **作用**: `Heartflow` 的状态直接影响 `SubHeartflowManager` 如何管理子心流(如激活数量、允许的状态等)。 ### 2.3. 聊天流状态 (`ChatState`) 与转换 @@ -222,4 +223,73 @@ c HeartFChatting工作方式 - `sub_heart_flow_stop_time`: (已废弃,现在由 `INACTIVE_THRESHOLD_SECONDS` in `subheartflow_manager.py` 控制) 子心流在 `ABSENT` 状态持续多久后被后台任务清理,默认为 3600 秒 (1 小时)。 - `sub_heart_flow_freeze_time`: 子心流冻结时间 (当前文档未明确体现,可能需要审阅代码确认)。 - `heart_flow_update_interval`: 主心流更新其状态或执行管理操作的频率 (需要审阅 `Heartflow` 代码确认)。 -- ` \ No newline at end of file + +### 5.3. 之后可以做的 (Future Work) +- **智能化 MaiState 状态转换**: + - 当前 `MaiState` (整体状态,如 `OFFLINE`, `NORMAL_CHAT` 等) 的转换逻辑 (`MaiStateManager`) 较为简单,主要依赖时间和随机性。 + - 未来的计划是让主心流 (`Heartflow`) 负责决策自身的 `MaiState`。 + - 该决策将综合考虑以下信息: + - 各个子心流 (`SubHeartflow`) 的活动状态和信息摘要。 + - 主心流自身的状态和历史信息。 + - (可能) 结合预设的日程安排 (Schedule) 信息。 + - 目标是让 Mai 的整体状态变化更符合逻辑和上下文。 (计划在 064 实现) + +- **参数化与动态调整聊天行为**: + - 将 `NormalChatInstance` 和 `HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。 + - 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。 + - 开发机制,使得这些参数能够被动态调整: + - 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。 + - 基于环境分析:例如,根据群消息的活跃度自动调整参与度。 + - 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。 + - 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。 + +- **动态 Prompt 生成与人格塑造**: + - 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。 + - Prompt 内容可根据以下因素调整: + - **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。 + - **当前情绪**: 将实时情绪状态融入 Prompt,使回复更符合当下心境。 + - 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。 + - 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。 + +- **扩展观察系统 (Observation System)**: + - 目前主要依赖 `ChattingObservation` 获取消息。 + - 计划引入更多 `Observation` 类型,为 `SubHeartflow` 提供更丰富的上下文: + - Mai 的全局状态 (`MaiStateInfo`)。 + - `SubHeartflow` 自身的聊天状态 (`ChatStateInfo`) 和参数配置。 + - Mai 的系统配置、连接平台信息。 + - 其他相关聊天或系统的聚合信息。 + - 目标:让 `SubHeartflow` 基于更全面的信息进行决策。 + +- **增强工具调用能力 (Enhanced Tool Usage)**: + - 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。 + - 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如: + - 修改自身或其他 `SubHeartflow` 的聊天参数。 + - 请求改变 Mai 的全局状态 (`MaiState`)。 + - 管理日程或执行更复杂的分析任务。 + - 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。 + +- **基于历史学习的行为模式应用**: + - **学习**: 分析过往聊天记录,提取和学习具体的行为模式(如特定梗的用法、情境化回应风格等)。可能需要专门的分析模块。 + - **存储与匹配**: 需要有效的方法存储学习到的行为模式,并开发强大的 **匹配** 机制,在运行时根据当前情境检索最合适的模式。**(匹配的准确性是关键)** + - **应用与评估**: 将匹配到的行为模式融入 HFC 的决策和回复生成(例如,将其整合进 Prompt)。之后需评估该行为模式应用的实际效果。 + - **人格塑造**: 通过学习到的实际行为来动态塑造人格,作为静态人设描述的补充或替代,使其更生动自然。 + +- **标准化人设生成 (Standardized Persona Generation)**: + - **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。 + - **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。 + - **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的: + - **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。 + - **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。 + - **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。 + - **实现途径**: + - 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。 + - 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。 + - **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。 + +- **优化表情包处理与理解 (Enhanced Emoji Handling and Understanding)**: + - **面临挑战**: + - **历史记录表示**: 如何在聊天历史中有效表示表情包,供 LLM 理解。 + - **语义理解**: 如何让 LLM 准确把握表情包的含义、情感和语境。 + - **场景判断与选择**: 如何让 LLM 判断何时适合使用表情包,并选择最贴切的一个。 + - **目标**: 提升 Mai 理解和运用表情包的能力,使交互更自然生动。 + - **说明**: 可能需要较多时间进行数据处理和模型调优,但对改善体验潜力巨大。 \ No newline at end of file diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 5308ce6e3..959dbc365 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -63,7 +63,7 @@ def init_prompt(): - 话题无关/无聊/不感兴趣 - 最后一条消息是你自己发的且无人回应你 - 讨论你不懂的专业话题 -- 你发送了太多消息 +- 你发送了太多消息,且无人回复 2. 文字回复(text_reply)适用: - 有实质性内容需要表达 @@ -81,6 +81,7 @@ def init_prompt(): - 不要和自己聊天 【必须遵守】 +- 遵守回复原则 - 必须调用工具并包含action和reasoning - 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) - 选择text_reply或emoji_reply时必须提供emoji_query diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index b54b28187..55f7cb133 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -357,17 +357,24 @@ class NormalChat: # 处理需要回复的消息 processed_count = 0 - for item in messages_to_reply: + # --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 --- + messages_to_process_initially = list(messages_to_reply) # 创建副本 + # --- 修改结束 --- + for item in messages_to_process_initially: # 使用副本迭代 msg_id, (message, interest_value, is_mentioned) = item + # --- 修改:在处理前尝试 pop,防止竞争 --- + popped_item = self.interest_dict.pop(msg_id, None) + if popped_item is None: + logger.warning(f"[{self.stream_name}] 初始兴趣消息 {msg_id} 在处理前已被移除,跳过。") + continue # 如果消息已被其他任务处理(pop),则跳过 + # --- 修改结束 --- + try: logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})") await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value) processed_count += 1 except Exception as e: - logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\n{traceback.format_exc()}") - finally: - # 无论成功与否都清空兴趣字典 - self.interest_dict.clear() + logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}") logger.info( f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。" @@ -414,7 +421,7 @@ class NormalChat: # --- 修改:使用 create_task 启动初始消息处理 --- logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") # 创建一个任务来处理初始消息,不阻塞当前流程 - initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) + _initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) # 可以考虑给这个任务也添加完成回调来记录日志或处理错误 # initial_process_task.add_done_callback(...) # --- 修改结束 --- From 9b99ea3cb26e480ec1c825b03e48d9c12eef28b2 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sat, 26 Apr 2025 23:23:11 +0800 Subject: [PATCH 76/79] fixLruff --- src/heart_flow/subheartflow_manager.py | 8 +++++--- src/plugins/heartFC_chat/normal_chat.py | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 35efbc550..6d994d769 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -267,7 +267,7 @@ class SubHeartflowManager: # 使用 self.mai_state_info 获取当前状态和限制 current_state = self.mai_state_info.get_current_state() focused_limit = current_state.get_focused_chat_max_num() - + logger.debug(f"{log_prefix} 当前状态 ({current_state.value}) 开始尝试提升到FOCUSED状态") if int(time.time()) % 20 == 0: # 每20秒输出一次 @@ -287,7 +287,7 @@ class SubHeartflowManager: stream_name = chat_manager.get_stream_name(flow_id) or flow_id logger.debug(f"{log_prefix} 检查子心流: {stream_name},现在状态: {sub_hf.chat_state.chat_status.value}") - + # 跳过非CHAT状态或已经是FOCUSED状态的子心流 if sub_hf.chat_state.chat_status == ChatState.FOCUSED: continue @@ -299,7 +299,9 @@ class SubHeartflowManager: continue # 检查是否满足提升概率 - logger.debug(f"{log_prefix} 检查子心流: {stream_name},现在概率: {sub_hf.interest_chatting.start_hfc_probability}") + logger.debug( + f"{log_prefix} 检查子心流: {stream_name},现在概率: {sub_hf.interest_chatting.start_hfc_probability}" + ) if random.random() >= sub_hf.interest_chatting.start_hfc_probability: continue diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 55f7cb133..890b5a82a 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -358,15 +358,15 @@ class NormalChat: # 处理需要回复的消息 processed_count = 0 # --- 修改:迭代前创建要处理的ID列表副本,防止迭代时修改 --- - messages_to_process_initially = list(messages_to_reply) # 创建副本 + messages_to_process_initially = list(messages_to_reply) # 创建副本 # --- 修改结束 --- - for item in messages_to_process_initially: # 使用副本迭代 + for item in messages_to_process_initially: # 使用副本迭代 msg_id, (message, interest_value, is_mentioned) = item # --- 修改:在处理前尝试 pop,防止竞争 --- popped_item = self.interest_dict.pop(msg_id, None) if popped_item is None: logger.warning(f"[{self.stream_name}] 初始兴趣消息 {msg_id} 在处理前已被移除,跳过。") - continue # 如果消息已被其他任务处理(pop),则跳过 + continue # 如果消息已被其他任务处理(pop),则跳过 # --- 修改结束 --- try: @@ -416,7 +416,7 @@ class NormalChat: async def start_chat(self): """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行), - 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。 + 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。 if self._chat_task is None or self._chat_task.done(): # --- 修改:使用 create_task 启动初始消息处理 --- logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") @@ -428,9 +428,9 @@ class NormalChat: # 启动后台轮询任务 (这部分不变) logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...") - polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分 + polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分 polling_task.add_done_callback(lambda t: self._handle_task_completion(t)) - self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务 + self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务 else: logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。") From 940a6784dbcdc3a28b463d80f45af8de490d3a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A2=A8=E6=A2=93=E6=9F=92?= <1787882683@qq.com> Date: Sun, 27 Apr 2025 00:01:38 +0800 Subject: [PATCH 77/79] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86=E4=BB=A5=E5=A2=9E=E5=BC=BA=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E8=8E=B7=E5=8F=96=E7=9A=84=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../heartFC_chat/heartflow_prompt_builder.py | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 5308ce6e3..75ee2d99f 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -535,23 +535,34 @@ class PromptBuilder: logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}") # 从LPMM知识库获取知识 - found_knowledge_from_lpmm = qa_manager.get_knowledge(message) + try: + found_knowledge_from_lpmm = qa_manager.get_knowledge(message) - end_time = time.time() - if found_knowledge_from_lpmm is not None: - logger.debug( - f"从LPMM知识库获取知识,相关信息:{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}" - ) - related_info += found_knowledge_from_lpmm - logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}秒") - logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") - return related_info - else: - logger.debug("从LPMM知识库获取知识失败,使用旧版数据库进行检索") - knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38) - related_info += knowledge_from_old - logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") - return related_info + end_time = time.time() + if found_knowledge_from_lpmm is not None: + logger.debug( + f"从LPMM知识库获取知识,相关信息:{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}" + ) + related_info += found_knowledge_from_lpmm + logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}秒") + logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") + return related_info + else: + logger.debug("从LPMM知识库获取知识失败,使用旧版数据库进行检索") + knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38) + related_info += knowledge_from_old + logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") + return related_info + except Exception as e: + logger.error(f"获取知识库内容时发生异常: {str(e)}") + try: + knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38) + related_info += knowledge_from_old + logger.debug(f"异常后使用旧版数据库获取知识,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") + return related_info + except Exception as e2: + logger.error(f"使用旧版数据库获取知识时也发生异常: {str(e2)}") + return "" @staticmethod def get_info_from_db( From 060744b552bcd87ee14215e952b2a37323f43725 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 16:01:54 +0000 Subject: [PATCH 78/79] =?UTF-8?q?=F0=9F=A4=96=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartflow_prompt_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 75ee2d99f..033eee0db 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -558,7 +558,9 @@ class PromptBuilder: try: knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38) related_info += knowledge_from_old - logger.debug(f"异常后使用旧版数据库获取知识,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") + logger.debug( + f"异常后使用旧版数据库获取知识,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}" + ) return related_info except Exception as e2: logger.error(f"使用旧版数据库获取知识时也发生异常: {str(e2)}") From 20c11c0e0d8dc1e241207ccd66cb647708e0787f Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Sun, 27 Apr 2025 00:43:53 +0800 Subject: [PATCH 79/79] =?UTF-8?q?doc=EF=BC=9A=E6=9B=B4=E6=96=B0=E4=BA=86?= =?UTF-8?q?=E5=BE=88=E5=A4=9Adoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelogs/changelog.md | 75 +++++++++++ src/heart_flow/0.6.3TODO.md | 48 +++++++ src/heart_flow/0.6Bing.md | 84 ++++++++++++ src/heart_flow/README.md | 104 ++++----------- src/heart_flow/heartFC_chatting_logic.md | 120 ------------------ src/heart_flow/mai_state_manager.py | 4 +- src/plugins/heartFC_chat/heartFC_chat.py | 2 +- .../heartFC_chat/heartFC_chatting_logic.md | 92 ++++++++++++++ 8 files changed, 327 insertions(+), 202 deletions(-) create mode 100644 src/heart_flow/0.6.3TODO.md create mode 100644 src/heart_flow/0.6Bing.md delete mode 100644 src/heart_flow/heartFC_chatting_logic.md create mode 100644 src/plugins/heartFC_chat/heartFC_chatting_logic.md diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 0ddb486bf..4fed6fb17 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,5 +1,80 @@ # Changelog +## [0.6.3] - 2025-4-15 + +### 摘要 +- MaiBot 0.6.3 版本发布!核心重构回复逻辑,统一为心流系统管理,智能切换交互模式。 +- 引入全新的 LPMM 知识库系统,大幅提升信息获取能力。 +- 新增昵称系统,改善群聊中的身份识别。 +- 提供独立的桌宠适配器连接程序。 +- 优化日志输出,修复若干问题。 + +### 🌟 核心功能增强 +#### 统一回复逻辑 (Unified Reply Logic) +- **核心重构**: 移除了经典 (Reasoning) 与心流 (Heart Flow) 模式的区分,将回复逻辑完全整合到 `SubHeartflow` 中进行统一管理,由主心流统一调控。保留 Heart FC 模式的特色功能。 +- **智能交互模式**: `SubHeartflow` 现在可以根据情境智能选择不同的交互模式: + - **普通聊天 (Normal Chat)**: 类似于之前的 Reasoning 模式,进行常规回复(激活逻辑暂未改变)。 + - **心流聊天 (Heart Flow Chat)**: 基于改进的 PFC 模式,能更好地理解上下文,减少重复和认错人的情况,并支持**工具调用**以获取额外信息。 + - **离线模式 (Offline/Absent)**: 在特定情况下,麦麦可能会选择暂时不查看或回复群聊消息。 +- **状态管理**: 交互模式的切换由 `SubHeartflow` 内部逻辑和 `SubHeartflowManager` 根据整体状态 (`MaiState`) 和配置进行管理。 +- **流程优化**: 拆分了子心流的思考模块,使整体对话流程更加清晰。 +- **状态判断改进**: 将 CHAT 状态判断交给 LLM 处理,使对话更自然。 +- **回复机制**: 实现更为灵活的概率回复机制,使机器人能够自然地融入群聊环境。 + +#### 全新知识库系统 (New Knowledge Base System - LPMM) +- **引入 LPMM**: 新增了 **LPMM (Large Psychology Model Maker)** 知识库系统,具有强大的信息检索能力,能显著提升麦麦获取和利用知识的效率。 +- **功能集成**: 集成了 LPMM 知识库查询功能,进一步扩展信息检索能力。 +- **推荐使用**: 强烈建议使用新的 LPMM 系统以获得最佳体验。旧的知识库系统仍然可用作为备选。 + +#### 昵称系统 (Nickname System) +- **自动取名**: 麦麦现在会尝试给群友取昵称,减少对易变的群昵称的依赖,从而降低认错人的概率。 +- **持续完善**: 该系统目前仍处于早期阶段,会持续进行优化。 + +#### 记忆与上下文增强 (Memory and Context Enhancement) +- **聊天记录压缩**: 大幅优化聊天记录压缩系统,使机器人能够处理5倍于之前的上下文记忆量。 +- **记忆提取**: 优化记忆提取功能,提高对历史对话的理解和引用能力。 +- **中期记忆调用**: 完善中期记忆调用机制,使机器人能够更自然地回忆和引用较早前的对话。 + +#### 私聊 PFC 功能增强 (Private Chat PFC Enhancement) +- **功能修复与优化**: 修复了私聊 PFC 载入聊天记录缺失的 bug,优化了 prompt 构建,增加了审核机制,调整了重试次数,并将机器人发言存入数据库。 +- **实验性质**: 请注意,PFC 仍然是一个实验性功能,可能在未来版本中被修改或移除,目前不接受相关 Bug 反馈。 + +#### 情感与互动增强 (Emotion and Interaction Enhancement) +- **全新表情包系统**: 新的表情包系统上线,表情含义更丰富,发送更快速。 +- **提示词优化**: 优化提示词(prompt)构建,增强对话质量和情感表达。 +- **积极性配置**: 优化"让麦麦更愿意说话"的相关配置,使机器人更积极参与对话。 +- **命名统一**: 实现统一命名功能,自动替换 prompt 内唯一标识符,优化 prompt 效果。 +- **颜文字保护**: 保护颜文字处理机制,确保表情正确显示。 + +#### 工具与集成 (Tools and Integration) +- **动态更新**: 使用工具调用来更新关系和心情,取代原先的固定更新机制。 +- **智能调用**: 工具调用时会考虑上下文,使调用更加智能。 +- **知识库依赖**: 添加 LPMM 知识库依赖,扩展知识检索工具。 + +### 💻 系统架构优化 +#### 日志优化 (Logging Optimization) +- **输出更清晰**: 优化了日志信息的格式和内容,使其更易于阅读和理解。 + +#### 模型与消息整合 (Model and Message Integration) +- **模型合并**: 合并工具调用模型和心流模型,提高整体一致性。 +- **消息规范**: 全面改用 `maim_message`,移除对 `rest` 的支持。 + +#### (临时) 简易 GUI (Temporary Simple GUI) +- **运行状态查看**: 提供了一个非常基础的图形用户界面,用于查看麦麦的运行状态。 +- **临时方案**: 这是一个临时性的解决方案,功能简陋,**将在 0.6.4 版本中被全新的 Web UI 所取代**。此 GUI 不会包含在主程序包中,而是通过一键包提供,并且不接受 Bug 反馈。 + +### 🐛 问题修复 +- **记忆检索优化**: 提高了记忆检索的准确性和效率。 +- 修复了一些其他小问题。 + +### 🔧 其他改进 +#### 桌宠适配器 (Bug Catcher Adapter) +- **独立适配器**: 提供了一个"桌宠"独立适配器,用于连接麦麦和桌宠。 +- **获取方式**: 可在 MaiBot 的 GitHub 组织中找到该适配器,不包含在主程序内。 + +#### 一键包内容 (One-Click Package Contents) +- **辅助程序**: 一键包中包含了简易 GUI 和 **麦麦帮助配置** 等辅助程序,后者可在配置出现问题时提供帮助。 + ## [0.6.2] - 2025-4-14 ### 摘要 diff --git a/src/heart_flow/0.6.3TODO.md b/src/heart_flow/0.6.3TODO.md new file mode 100644 index 000000000..4048fec8f --- /dev/null +++ b/src/heart_flow/0.6.3TODO.md @@ -0,0 +1,48 @@ +# 0.6.3 版本发布前待办事项 + +- **统一化人格配置:** + - 检查代码中是否存在硬编码的人格相关配置。 + - 将所有硬编码的人格配置替换为使用 `individual` 模块进行管理。 + +- **在 Planner 中添加回复计数信息:** + - 修改 `HeartFlowChatInstance` 的 `Plan` 阶段逻辑。 + - 将当前周期的回复计数(或其他相关统计信息)作为输入提供给 Planner。 + - 目的是为 Planner 提供负反馈,减少连续回复或不当回复的可能性。 + +- **恢复/检查被停止的功能:** + - 全面审查代码,特别是对比之前的版本或设计文档。 + - 识别并重新启用那些暂时被禁用但应该恢复的功能。 + - 确认没有核心功能意外丢失。 + +- **参数提取与配置化:** + - 识别代码中散落的各种可调参数(例如:概率阈值、时间间隔、次数限制、LLM 模型名称等)。 + - 将这些参数统一提取到模块或类的顶部。 + - 最终将这些参数移至外部配置文件(如 YAML 或 JSON 文件),方便用户自定义。 + +- **提供 HFC (HeartFlowChatInstance) 开启/关闭选项:** + - 增加一个全局或针对特定子心流的配置选项。 + - 允许用户控制是否启用 `FOCUSED` 状态以及关联的 `HeartFlowChatInstance`。 + - 如果禁用 HFC,子心流可能只会在 `ABSENT` 和 `CHAT` 状态间切换。 + +- **添加防破线机制 (针对接收消息):** + - 在消息处理流程的早期阶段 (例如 `HeartHC_processor` 或类似模块),增加对接收到的消息文本长度的检查。 + - 对超过预设长度阈值的*接收*消息进行截断处理。 + - 目的是防止过长的输入(可能包含"破限"提示词)影响后续的兴趣计算、LLM 回复生成等环节。 + +- **NormalChat 模式下的记忆与 Prompt 优化:** + - 重点审视 `NormalChatInstance` (闲聊/推理模式) 中记忆调用 (例如 `HippocampusManager` 的使用) 的方式。 + - 评估在该模式下引入工具调用 (Tool Calling) 机制以更结构化访问记忆的必要性。 + - 优化 `NormalChatInstance` 中与记忆检索、应用相关的 Prompt。 + +- **完善简易兴趣监控 GUI:** + - 改进现有的、用于监控聊天兴趣度 (`InterestChatting`?) 的简单 GUI 界面。 + - 使其能更清晰地展示关键参数和状态,作为查看日志之外的更直观的监控方式。 + - 作为完整外部 UI 开发完成前的临时替代方案。 + +- **修复/完善中期记忆 (Midterm Memory):** + - 检查当前中期记忆模块的状态。 + - 修复已知问题,使其能够稳定运行。 + - (优先级视开发时间而定) + + +对于有些群频繁激活HFC,却不回复,需要处理一下 diff --git a/src/heart_flow/0.6Bing.md b/src/heart_flow/0.6Bing.md new file mode 100644 index 000000000..6af057e29 --- /dev/null +++ b/src/heart_flow/0.6Bing.md @@ -0,0 +1,84 @@ +- **智能化 MaiState 状态转换**: + - 当前 `MaiState` (整体状态,如 `OFFLINE`, `NORMAL_CHAT` 等) 的转换逻辑 (`MaiStateManager`) 较为简单,主要依赖时间和随机性。 + - 未来的计划是让主心流 (`Heartflow`) 负责决策自身的 `MaiState`。 + - 该决策将综合考虑以下信息: + - 各个子心流 (`SubHeartflow`) 的活动状态和信息摘要。 + - 主心流自身的状态和历史信息。 + - (可能) 结合预设的日程安排 (Schedule) 信息。 + - 目标是让 Mai 的整体状态变化更符合逻辑和上下文。 (计划在 064 实现) + +- **参数化与动态调整聊天行为**: + - 将 `NormalChatInstance` 和 `HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。 + - 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。 + - 开发机制,使得这些参数能够被动态调整: + - 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。 + - 基于环境分析:例如,根据群消息的活跃度自动调整参与度。 + - 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。 + - 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。 + +- **动态 Prompt 生成与人格塑造**: + - 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。 + - Prompt 内容可根据以下因素调整: + - **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。 + - **当前情绪**: 将实时情绪状态融入 Prompt,使回复更符合当下心境。 + - 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。 + - 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。 + +- **扩展观察系统 (Observation System)**: + - 目前主要依赖 `ChattingObservation` 获取消息。 + - 计划引入更多 `Observation` 类型,为 `SubHeartflow` 提供更丰富的上下文: + - Mai 的全局状态 (`MaiStateInfo`)。 + - `SubHeartflow` 自身的聊天状态 (`ChatStateInfo`) 和参数配置。 + - Mai 的系统配置、连接平台信息。 + - 其他相关聊天或系统的聚合信息。 + - 目标:让 `SubHeartflow` 基于更全面的信息进行决策。 + +- **增强工具调用能力 (Enhanced Tool Usage)**: + - 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。 + - 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如: + - 修改自身或其他 `SubHeartflow` 的聊天参数。 + - 请求改变 Mai 的全局状态 (`MaiState`)。 + - 管理日程或执行更复杂的分析任务。 + - 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。 + +- **基于历史学习的行为模式应用**: + - **学习**: 分析过往聊天记录,提取和学习具体的行为模式(如特定梗的用法、情境化回应风格等)。可能需要专门的分析模块。 + - **存储与匹配**: 需要有效的方法存储学习到的行为模式,并开发强大的 **匹配** 机制,在运行时根据当前情境检索最合适的模式。**(匹配的准确性是关键)** + - **应用与评估**: 将匹配到的行为模式融入 HFC 的决策和回复生成(例如,将其整合进 Prompt)。之后需评估该行为模式应用的实际效果。 + - **人格塑造**: 通过学习到的实际行为来动态塑造人格,作为静态人设描述的补充或替代,使其更生动自然。 + +- **标准化人设生成 (Standardized Persona Generation)**: + - **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。 + - **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。 + - **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的: + - **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。 + - **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。 + - **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。 + - **实现途径**: + - 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。 + - 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。 + - **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。 + +- **优化表情包处理与理解 (Enhanced Emoji Handling and Understanding)**: + - **面临挑战**: + - **历史记录表示**: 如何在聊天历史中有效表示表情包,供 LLM 理解。 + - **语义理解**: 如何让 LLM 准确把握表情包的含义、情感和语境。 + - **场景判断与选择**: 如何让 LLM 判断何时适合使用表情包,并选择最贴切的一个。 + - **目标**: 提升 Mai 理解和运用表情包的能力,使交互更自然生动。 + - **说明**: 可能需要较多时间进行数据处理和模型调优,但对改善体验潜力巨大。 + +- **探索高级记忆检索机制 (GE 系统概念):** + - 研究超越简单关键词/近期性检索的记忆模型。 + - 考虑引入基于事件关联、相对时间线索和绝对时间锚点的检索方式。 + - 可能涉及设计新的事件表示或记忆结构。 + + +- **实现 SubHeartflow 级记忆缓存池:** + - 在 `SubHeartflow` 层级或更高层级设计并实现一个缓存池,存储已检索的记忆/信息。 + - 避免在 HFC 等循环中重复进行相同的记忆检索调用。 + - 确保存储的信息能有效服务于当前交互上下文。 + +- **基于人格生成预设知识:** + - 开发利用 LLM 和人格配置生成背景知识的功能。 + - 这些知识应符合角色的行为风格和可能的经历。 + - 作为一种"冷启动"或丰富角色深度的方式。 \ No newline at end of file diff --git a/src/heart_flow/README.md b/src/heart_flow/README.md index 7f6906aea..fdb3a1928 100644 --- a/src/heart_flow/README.md +++ b/src/heart_flow/README.md @@ -151,11 +151,31 @@ c HeartFChatting工作方式 - `ChatState.CHAT` (随便看看/水群): 普通聊天模式。激活 `NormalChatInstance`。 * `ChatState.FOCUSED` (专注/认真水群): 专注聊天模式。激活 `HeartFlowChatInstance`。 - **选择**: 子心流可以根据外部指令(来自 `SubHeartflowManager`)或内部逻辑(未来的扩展)选择进入 `ABSENT` 状态(不回复不观察),或进入 `CHAT` / `FOCUSED` 中的一种回复模式。 -- **状态转换机制** (由 `SubHeartflowManager` 驱动): - - **激活 `CHAT`**: 当 `Heartflow` 状态从 `OFFLINE` 变为允许聊天的状态时,`SubHeartflowManager` 会根据限制(通过 `self.mai_state_info` 获取),选择部分 `ABSENT` 状态的子心流,**检查当前 CHAT 状态数量是否达到上限**,如果未达上限,则调用其 `change_chat_state` 方法将其转换为 `CHAT`。此外,`evaluate_and_transition_subflows_by_llm` 方法也会根据 LLM 的判断,在未达上限时将 `ABSENT` 状态的子心流激活为 `CHAT`。 - - **激活 `FOCUSED`**: `SubHeartflowManager` 会定期评估处于 `CHAT` 状态的子心流的兴趣度 (`InterestChatting.start_hfc_probability`),若满足条件且**检查当前 FOCUSED 状态数量未达上限**(通过 `self.mai_state_info` 获取限制),则调用 `change_chat_state` 将其提升为 `FOCUSED`。 - - **停用/回退**: `SubHeartflowManager` 可能因 `Heartflow` 状态变化、达到数量限制、长时间不活跃、随机概率 (`randomly_deactivate_subflows`)、LLM 评估 (`evaluate_and_transition_subflows_by_llm` 判断 `CHAT` 状态子心流应休眠) 或收到来自 `HeartFChatting` 的连续不回复回调信号 (`request_absent_transition`) 等原因,调用 `change_chat_state` 将子心流状态设置为 `ABSENT` 或从 `FOCUSED` 回退到 `CHAT`。当子心流进入 `ABSENT` 状态后,如果持续一小时不活跃,才会被后台清理任务删除。 - - **注意**: `change_chat_state` 方法本身只负责执行状态转换和管理内部聊天实例(`NormalChatInstance`/`HeartFlowChatInstance`),不再进行限额检查。限额检查的责任完全由调用方(即 `SubHeartflowManager` 中的相关方法,这些方法会使用内部存储的 `mai_state_info` 来获取限制)承担。 +- **状态转换机制** (由 `SubHeartflowManager` 驱动,更细致的说明): + - **初始状态**: 新创建的 `SubHeartflow` 默认为 `ABSENT` 状态。 + - **`ABSENT` -> `CHAT` (激活闲聊)**: + - **触发条件**: `Heartflow` 的主状态 (`MaiState`) 允许 `CHAT` 模式,且当前 `CHAT` 状态的子心流数量未达上限。 + - **判定机制**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用大模型(LLM)。LLM 读取该群聊的近期内容和结合自身个性信息,判断是否"想"在该群开始聊天。 + - **执行**: 若 LLM 判断为是,且名额未满,`SubHeartflowManager` 调用 `change_chat_state(ChatState.CHAT)`。 + - **`CHAT` -> `FOCUSED` (激活专注)**: + - **触发条件**: 子心流处于 `CHAT` 状态,其内部维护的"开屎热聊"概率 (`InterestChatting.start_hfc_probability`) 达到预设阈值(表示对当前聊天兴趣浓厚),同时 `Heartflow` 的主状态允许 `FOCUSED` 模式,且 `FOCUSED` 名额未满。 + - **判定机制**: `SubHeartflowManager` 中的 `evaluate_interest_and_promote` 方法定期检查满足条件的 `CHAT` 子心流。 + - **执行**: 若满足所有条件,`SubHeartflowManager` 调用 `change_chat_state(ChatState.FOCUSED)`。 + - **注意**: 无法从 `ABSENT` 直接跳到 `FOCUSED`,必须先经过 `CHAT`。 + - **`FOCUSED` -> `ABSENT` (退出专注)**: + - **主要途径 (内部驱动)**: 在 `FOCUSED` 状态下运行的 `HeartFlowChatInstance` 连续多次决策为 `no_reply` (例如达到 5 次,次数可配),它会通过回调函数 (`request_absent_transition`) 请求 `SubHeartflowManager` 将其状态**直接**设置为 `ABSENT`。 + - **其他途径 (外部驱动)**: + - `Heartflow` 主状态变为 `OFFLINE`,`SubHeartflowManager` 强制所有子心流变为 `ABSENT`。 + - `SubHeartflowManager` 因 `FOCUSED` 名额超限 (`enforce_subheartflow_limits`) 或随机停用 (`randomly_deactivate_subflows`) 而将其设置为 `ABSENT`。 + - **`CHAT` -> `ABSENT` (退出闲聊)**: + - **主要途径 (内部驱动)**: `SubHeartflowManager` 中的 `evaluate_and_transition_subflows_by_llm` 方法调用 LLM。LLM 读取群聊内容和结合自身状态,判断是否"不想"继续在此群闲聊。 + - **执行**: 若 LLM 判断为是,`SubHeartflowManager` 调用 `change_chat_state(ChatState.ABSENT)`。 + - **其他途径 (外部驱动)**: + - `Heartflow` 主状态变为 `OFFLINE`。 + - `SubHeartflowManager` 因 `CHAT` 名额超限或随机停用。 + - **全局强制 `ABSENT`**: 当 `Heartflow` 的 `MaiState` 变为 `OFFLINE` 时,`SubHeartflowManager` 会调用所有子心流的 `change_chat_state(ChatState.ABSENT)`,强制它们全部停止活动。 + - **状态变更执行者**: `change_chat_state` 方法仅负责执行状态的切换和对应聊天实例的启停,不进行名额检查。名额检查的责任由 `SubHeartflowManager` 中的各个决策方法承担。 + - **最终清理**: 进入 `ABSENT` 状态的子心流不会立即被删除,只有在 `ABSENT` 状态持续一小时 (`INACTIVE_THRESHOLD_SECONDS`) 后,才会被后台清理任务 (`cleanup_inactive_subheartflows`) 删除。 ## 3. 聊天实例详解 (Chat Instances Explained) @@ -219,77 +239,3 @@ c HeartFChatting工作方式 self.observations.append(observation) ``` -### 5.2. 配置参数 (Key Parameters) -- `sub_heart_flow_stop_time`: (已废弃,现在由 `INACTIVE_THRESHOLD_SECONDS` in `subheartflow_manager.py` 控制) 子心流在 `ABSENT` 状态持续多久后被后台任务清理,默认为 3600 秒 (1 小时)。 -- `sub_heart_flow_freeze_time`: 子心流冻结时间 (当前文档未明确体现,可能需要审阅代码确认)。 -- `heart_flow_update_interval`: 主心流更新其状态或执行管理操作的频率 (需要审阅 `Heartflow` 代码确认)。 - -### 5.3. 之后可以做的 (Future Work) -- **智能化 MaiState 状态转换**: - - 当前 `MaiState` (整体状态,如 `OFFLINE`, `NORMAL_CHAT` 等) 的转换逻辑 (`MaiStateManager`) 较为简单,主要依赖时间和随机性。 - - 未来的计划是让主心流 (`Heartflow`) 负责决策自身的 `MaiState`。 - - 该决策将综合考虑以下信息: - - 各个子心流 (`SubHeartflow`) 的活动状态和信息摘要。 - - 主心流自身的状态和历史信息。 - - (可能) 结合预设的日程安排 (Schedule) 信息。 - - 目标是让 Mai 的整体状态变化更符合逻辑和上下文。 (计划在 064 实现) - -- **参数化与动态调整聊天行为**: - - 将 `NormalChatInstance` 和 `HeartFlowChatInstance` 中的关键行为参数(例如:回复概率、思考频率、兴趣度阈值、状态转换条件等)提取出来,使其更易于配置。 - - 允许每个 `SubHeartflow` (即每个聊天场景) 拥有其独立的参数配置,实现"千群千面"。 - - 开发机制,使得这些参数能够被动态调整: - - 基于外部反馈:例如,根据用户评价("话太多"或"太冷淡")调整回复频率。 - - 基于环境分析:例如,根据群消息的活跃度自动调整参与度。 - - 基于学习:通过分析历史交互数据,优化特定群聊下的行为模式。 - - 目标是让 Mai 在不同群聊中展现出更适应环境、更个性化的交互风格。 - -- **动态 Prompt 生成与人格塑造**: - - 当前 Prompt (提示词) 相对静态。计划实现动态或半结构化的 Prompt 生成。 - - Prompt 内容可根据以下因素调整: - - **人格特质**: 通过参数化配置(如友善度、严谨性等),影响 Prompt 的措辞、语气和思考倾向,塑造更稳定和独特的人格。 - - **当前情绪**: 将实时情绪状态融入 Prompt,使回复更符合当下心境。 - - 目标:提升 `HeartFlowChatInstance` (HFC) 回复的多样性、一致性和真实感。 - - 前置:需要重构 Prompt 构建逻辑,可能引入 `PromptBuilder` 并提供标准接口 (认为是必须步骤)。 - -- **扩展观察系统 (Observation System)**: - - 目前主要依赖 `ChattingObservation` 获取消息。 - - 计划引入更多 `Observation` 类型,为 `SubHeartflow` 提供更丰富的上下文: - - Mai 的全局状态 (`MaiStateInfo`)。 - - `SubHeartflow` 自身的聊天状态 (`ChatStateInfo`) 和参数配置。 - - Mai 的系统配置、连接平台信息。 - - 其他相关聊天或系统的聚合信息。 - - 目标:让 `SubHeartflow` 基于更全面的信息进行决策。 - -- **增强工具调用能力 (Enhanced Tool Usage)**: - - 扩展 `HeartFlowChatInstance` (HFC) 可用的工具集。 - - 考虑引入"元工具"或分层工具机制,允许 HFC 在需要时(如深度思考)访问更强大的工具,例如: - - 修改自身或其他 `SubHeartflow` 的聊天参数。 - - 请求改变 Mai 的全局状态 (`MaiState`)。 - - 管理日程或执行更复杂的分析任务。 - - 目标:提升 HFC 的自主决策和行动能力,即使会增加一定的延迟。 - -- **基于历史学习的行为模式应用**: - - **学习**: 分析过往聊天记录,提取和学习具体的行为模式(如特定梗的用法、情境化回应风格等)。可能需要专门的分析模块。 - - **存储与匹配**: 需要有效的方法存储学习到的行为模式,并开发强大的 **匹配** 机制,在运行时根据当前情境检索最合适的模式。**(匹配的准确性是关键)** - - **应用与评估**: 将匹配到的行为模式融入 HFC 的决策和回复生成(例如,将其整合进 Prompt)。之后需评估该行为模式应用的实际效果。 - - **人格塑造**: 通过学习到的实际行为来动态塑造人格,作为静态人设描述的补充或替代,使其更生动自然。 - -- **标准化人设生成 (Standardized Persona Generation)**: - - **目标**: 解决手动配置 `人设` 文件缺乏标准、难以全面描述个性的问题,并生成更丰富、可操作的人格资源。 - - **方法**: 利用大型语言模型 (LLM) 辅助生成标准化的、结构化的人格**资源包**。 - - **生成内容**: 不仅生成描述性文本(替代现有 `individual` 配置),还可以同时生成与该人格配套的: - - **相关工具 (Tools)**: 该人格倾向于使用的工具或能力。 - - **初始记忆/知识库 (Memories/Knowledge)**: 定义其背景和知识基础。 - - **核心行为模式 (Core Behavior Patterns)**: 预置一些典型的行为方式,可作为行为学习的起点。 - - **实现途径**: - - 通过与 LLM 的交互式对话来定义和细化人格及其配套资源。 - - 让 LLM 分析提供的文本材料(如小说、背景故事)来提取人格特质和相关信息。 - - **优势**: 替代易出错且标准不一的手动配置,生成更丰富、一致、包含配套资源且易于系统理解和应用的人格包。 - -- **优化表情包处理与理解 (Enhanced Emoji Handling and Understanding)**: - - **面临挑战**: - - **历史记录表示**: 如何在聊天历史中有效表示表情包,供 LLM 理解。 - - **语义理解**: 如何让 LLM 准确把握表情包的含义、情感和语境。 - - **场景判断与选择**: 如何让 LLM 判断何时适合使用表情包,并选择最贴切的一个。 - - **目标**: 提升 Mai 理解和运用表情包的能力,使交互更自然生动。 - - **说明**: 可能需要较多时间进行数据处理和模型调优,但对改善体验潜力巨大。 \ No newline at end of file diff --git a/src/heart_flow/heartFC_chatting_logic.md b/src/heart_flow/heartFC_chatting_logic.md deleted file mode 100644 index 1e178a6ff..000000000 --- a/src/heart_flow/heartFC_chatting_logic.md +++ /dev/null @@ -1,120 +0,0 @@ -# HeartFChatting 逻辑详解 - -`HeartFChatting` 类是心流系统(Heart Flow System)中负责**专注聊天**(`ChatState.FOCUSED`)的核心组件。它的主要职责是在特定的聊天流 (`stream_id`) 中,通过一个持续的 **思考(Think)-规划(Plan)-执行(Execute)** 循环来模拟更自然、更深入的对话交互。当关联的 `SubHeartflow` 状态切换为 `FOCUSED` 时,`HeartFChatting` 实例会被创建并启动;当状态切换为其他(如 `CHAT` 或 `ABSENT`)时,它会被关闭。 - -## 1. 初始化 (`__init__`, `_initialize`) - -- **依赖注入**: 在创建时,`HeartFChatting` 接收 `chat_id`(即 `stream_id`)、关联的 `SubMind` 实例以及 `Observation` 实例列表作为参数。 -- **核心组件**: 内部初始化了几个关键组件: - - `ActionManager`: 管理当前循环可用的动作(如回复文本、回复表情、不回复)。 - - `HeartFCGenerator`: (`self.gpt_instance`) 用于生成回复文本。 - - `ToolUser`: (`self.tool_user`) 用于执行 `SubMind` 可能请求的工具调用(虽然在此类中主要用于获取工具定义,实际执行由 `SubMind` 完成)。 - - `HeartFCSender`: (`self.heart_fc_sender`) 专门负责处理消息发送逻辑,包括管理"正在思考"状态。 - - `LLMRequest`: (`self.planner_llm`) 配置用于执行规划任务的大语言模型请求。 -- **状态变量**: - - `_initialized`: 标记是否完成懒初始化。 - - `_processing_lock`: 异步锁,确保同一时间只有一个完整的"思考-规划-执行"周期在运行。 - - `_loop_active`: 标记主循环是否正在运行。 - - `_loop_task`: 指向主循环的 `asyncio.Task` 对象。 - - `_cycle_history`: 一个双端队列 (`deque`),用于存储最近若干次循环的信息 (`CycleInfo`)。 - - `_current_cycle`: 当前正在执行的循环信息 (`CycleInfo`)。 -- **懒初始化 (`_initialize`)**: - - 在首次需要访问 `ChatStream` 前调用(通常在 `start` 方法中)。 - - 根据 `stream_id` 从 `chat_manager` 获取对应的 `ChatStream` 实例。 - - 更新日志前缀,使用聊天流的名称以提高可读性。 - -## 2. 生命周期管理 (`start`, `shutdown`) - -- **启动 (`start`)**: - - 外部调用此方法来启动 `HeartFChatting` 的工作流程。 - - 内部调用 `_start_loop_if_needed` 来安全地启动主循环任务 (`_hfc_loop`)。 -- **关闭 (`shutdown`)**: - - 外部调用此方法来优雅地停止 `HeartFChatting`。 - - 取消正在运行的主循环任务 (`_loop_task`)。 - - 清理内部状态(如 `_loop_active`, `_loop_task`)。 - - 释放可能被持有的处理锁 (`_processing_lock`)。 - -## 3. 核心循环 (`_hfc_loop`) - -`_hfc_loop` 是 `HeartFChatting` 的心脏,它以异步方式无限期运行(直到被 `shutdown` 取消),不断执行以下步骤: - -1. **创建循环记录**: 初始化一个新的 `CycleInfo` 对象来记录本次循环的详细信息(ID、开始时间、计时器、动作、思考内容等)。 -2. **获取处理锁**: 使用 `_processing_lock` 确保并发安全。 -3. **执行思考-规划-执行**: 调用 `_think_plan_execute_loop` 方法。 -4. **处理循环延迟**: 根据本次循环是否执行了实际动作以及循环耗时,智能地引入短暂的 `asyncio.sleep`,防止 CPU 空转或过于频繁的循环。 -5. **记录循环信息**: 将完成的 `CycleInfo` 存入 `_cycle_history`,并记录详细的日志,包括循环耗时和各阶段计时。 - -## 4. 思考-规划-执行周期 (`_think_plan_execute_loop`) - -这是每个循环内部的核心逻辑,按顺序执行: - -### 4.1. 思考阶段 (`_get_submind_thinking`) - -1. **触发观察**: 调用关联的 `Observation` 实例的 `observe()` 方法,使其更新对环境(如聊天室新消息)的观察。 -2. **触发子思维**: 调用关联的 `SubMind` 实例的 `do_thinking_before_reply()` 方法。**关键**: 会将上一个循环的 `CycleInfo` 传递给 `SubMind`,使其了解上一次行动的决策、理由以及是否发生了重新规划,从而实现更连贯的思考。 -3. **获取思考结果**: `SubMind` 返回其当前的内心想法 (`current_mind`)。 - -### 4.2. 规划阶段 (`_planner`) - -1. **输入**: 获取 `SubMind` 的当前想法 (`current_mind`)、`SubMind` 通过工具调用收集到的结构化信息 (`structured_info`) 以及观察到的最新消息。 -2. **构建提示词**: 调用 `_build_planner_prompt` 方法,将上述信息以及机器人个性、当前可用动作等整合进一个专门为规划器设计的提示词中。 -3. **定义动作工具**: 使用 `ActionManager.get_planner_tool_definition()` 获取当前可用动作(如 `no_reply`, `text_reply`, `emoji_reply`)的 JSON Schema,将其作为 "工具" 提供给 LLM。 -4. **调用 LLM**: 使用 `self.planner_llm` 向大模型发送请求,**强制要求**模型调用 `decide_reply_action` 这个"工具",并根据提示词内容决定使用哪个动作以及相应的参数(如 `reasoning`, `emoji_query`)。 -5. **处理 LLM 响应**: 使用 `process_llm_tool_response` 解析 LLM 返回的工具调用请求,提取出决策的动作 (`action`)、理由 (`reasoning`) 和可能的表情查询 (`emoji_query`)。 -6. **检查新消息与重新规划**: - - 调用 `_check_new_messages` 检查自规划阶段开始以来是否有新消息。 - - 如果检测到新消息,有一定概率(当前为 30%)触发**重新规划**。这会再次调用 `_planner`,但会传入一个特殊的提示词片段(通过 `_build_replan_prompt` 生成),告知 LLM 它之前的决策以及现在需要重新考虑。 -7. **输出**: 返回一个包含最终决策结果(`action`, `reasoning`, `emoji_query` 等)的字典。如果 LLM 调用或解析失败,`action` 会被设为 "error"。 - -### 4.3. 执行阶段 (`_handle_action`) - -根据规划阶段返回的 `action`,分派到不同的处理方法: - -- **`_handle_text_reply` (文本回复)**: - 1. `_get_anchor_message`: 获取一个用于回复的锚点消息。**注意**: 当前实现是创建一个系统触发的占位符消息作为锚点,而不是实际观察到的最后一条消息。 - 2. `_create_thinking_message`: 调用 `HeartFCSender` 的 `register_thinking` 方法,标记机器人开始思考,并获取一个 `thinking_id`。 - 3. `_replier_work`: 调用回复器生成回复内容。 - 4. `_sender`: 调用发送器发送生成的文本和可能的表情。 -- **`_handle_emoji_reply` (仅表情回复)**: - 1. 获取锚点消息。 - 2. `_handle_emoji`: 获取表情图片并调用 `HeartFCSender` 发送。 -- **`_handle_no_reply` (不回复)**: - 1. 记录不回复的理由。 - 2. `_wait_for_new_message`: 进入等待状态,直到关联的 `Observation` 检测到新消息或超时(当前 300 秒)。 - -## 5. 回复器逻辑 (`_replier_work`) - -- **输入**: 规划器给出的回复理由 (`reason`)、锚点消息 (`anchor_message`)、思考ID (`thinking_id`),以及通过 `self.sub_mind` 获取的结构化信息和当前想法。 -- **处理**: 调用 `self.gpt_instance` (`HeartFCGenerator`) 的 `generate_response` 方法。这个方法负责构建最终的生成提示词(结合思考、理由、上下文等),调用 LLM 生成回复文本。 -- **输出**: 返回一个包含多段回复文本的列表 (`List[str]`),如果生成失败则返回 `None`。 - -## 6. 发送器逻辑 (`_sender`, `_create_thinking_message`, `_send_response_messages`, `_handle_emoji`) - -`HeartFChatting` 类本身不直接处理 WebSocket 发送,而是将发送任务委托给 `HeartFCSender` 实例 (`self.heart_fc_sender`)。 - -- **`_create_thinking_message`**: 准备一个 `MessageThinking` 对象,并调用 `sender.register_thinking(thinking_message)`。 -- **`_send_response_messages`**: - - 检查对应的 `thinking_id` 是否仍然有效(通过 `sender.get_thinking_start_time`)。 - - 遍历 `_replier_work` 返回的回复文本列表 (`response_set`)。 - - 为每一段文本创建一个 `MessageSending` 对象。 - - 调用 `sender.type_and_send_message(bot_message)` 来发送消息。`HeartFCSender` 内部会处理模拟打字延迟、实际发送和消息存储。 - - 发送完成后,调用 `sender.complete_thinking(chat_id, thinking_id)` 来清理思考状态。 - - 记录实际发送的消息 ID 到 `CycleInfo` 中。 -- **`_handle_emoji`**: - - 使用 `emoji_manager` 根据 `emoji_query` 获取表情图片路径。 - - 将图片转为 Base64。 - - 创建 `MessageSending` 对象(标记为 `is_emoji=True`)。 - - 调用 `sender.send_and_store(bot_message)` 来发送并存储表情消息(这个方法不涉及思考状态)。 - -## 7. 循环信息记录 (`CycleInfo`) - -- `CycleInfo` 类用于记录每一次思考-规划-执行循环的详细信息,包括: - - 循环 ID (`cycle_id`) - - 开始和结束时间 (`start_time`, `end_time`) - - 是否执行了实际动作 (`action_taken`) - - 决策的动作类型 (`action_type`) 和理由 (`reasoning`) - - 各阶段的耗时计时器 (`timers`) - - 关联的思考消息 ID (`thinking_id`) - - 是否发生了重新规划 (`replanned`) - - 详细的响应信息 (`response_info`),包括生成的文本、表情查询、锚点消息 ID、实际发送的消息 ID 列表以及 `SubMind` 的思考内容。 -- `HeartFChatting` 维护一个 `_cycle_history` 队列来保存最近的循环记录,方便调试和分析。 diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index ee5cd95ee..4341e3ded 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -37,7 +37,7 @@ class MaiState(enum.Enum): return 1000 if self == MaiState.OFFLINE: - return 10 + return 0 elif self == MaiState.PEEKING: return 30 elif self == MaiState.NORMAL_CHAT: @@ -51,7 +51,7 @@ class MaiState(enum.Enum): return 1000 if self == MaiState.OFFLINE: - return 10 + return 0 elif self == MaiState.PEEKING: return 20 elif self == MaiState.NORMAL_CHAT: diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 4f96353fd..903667b41 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -154,7 +154,7 @@ class HeartFChatting: 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ - CONSECUTIVE_NO_REPLY_THRESHOLD = 1 # 连续不回复的阈值 + CONSECUTIVE_NO_REPLY_THRESHOLD = 5 # 连续不回复的阈值 def __init__( self, diff --git a/src/plugins/heartFC_chat/heartFC_chatting_logic.md b/src/plugins/heartFC_chat/heartFC_chatting_logic.md new file mode 100644 index 000000000..6d51c978b --- /dev/null +++ b/src/plugins/heartFC_chat/heartFC_chatting_logic.md @@ -0,0 +1,92 @@ +# HeartFChatting 逻辑详解 + +`HeartFChatting` 类是心流系统(Heart Flow System)中实现**专注聊天**(`ChatState.FOCUSED`)功能的核心。顾名思义,其职责乃是在特定聊天流(`stream_id`)中,模拟更为连贯深入之对话。此非凭空臆造,而是依赖一个持续不断的 **思考(Think)-规划(Plan)-执行(Execute)** 循环。当其所系的 `SubHeartflow` 进入 `FOCUSED` 状态时,便会创建并启动 `HeartFChatting` 实例;若状态转为他途(譬如 `CHAT` 或 `ABSENT`),则会将其关闭。 + +## 1. 初始化简述 (`__init__`, `_initialize`) + +创生之初,`HeartFChatting` 需注入若干关键之物:`chat_id`(亦即 `stream_id`)、关联的 `SubMind` 实例,以及 `Observation` 实例(用以观察环境)。 + +其内部核心组件包括: + +- `ActionManager`: 管理当前循环可选之策(如:不应、言语、表情)。 +- `HeartFCGenerator` (`self.gpt_instance`): 专司生成回复文本之职。 +- `ToolUser` (`self.tool_user`): 虽主要用于获取工具定义,然亦备 `SubMind` 调用之需(实际执行由 `SubMind` 操持)。 +- `HeartFCSender` (`self.heart_fc_sender`): 负责消息发送诸般事宜,含"正在思考"之态。 +- `LLMRequest` (`self.planner_llm`): 配置用于执行"规划"任务的大语言模型。 + +*初始化过程采取懒加载策略,仅在首次需要访问 `ChatStream` 时(通常在 `start` 方法中)进行。* + +## 2. 生命周期 (`start`, `shutdown`) + +- **启动 (`start`)**: 外部调用此法,以启 `HeartFChatting` 之流程。内部会安全地启动主循环任务。 +- **关闭 (`shutdown`)**: 外部调用此法,以止其运行。会取消主循环任务,清理状态,并释放锁。 + +## 3. 核心循环 (`_hfc_loop`) 与 循环记录 (`CycleInfo`) + +`_hfc_loop` 乃 `HeartFChatting` 之脉搏,以异步方式不舍昼夜运行(直至 `shutdown` 被调用)。其核心在于周而复始地执行 **思考-规划-执行** 之周期。 + +每一轮循环,皆会创建一个 `CycleInfo` 对象。此对象犹如史官,详细记载该次循环之点滴: + +- **身份标识**: 循环 ID (`cycle_id`)。 +- **时间轨迹**: 起止时刻 (`start_time`, `end_time`)。 +- **行动细节**: 是否执行动作 (`action_taken`)、动作类型 (`action_type`)、决策理由 (`reasoning`)。 +- **耗时考量**: 各阶段计时 (`timers`)。 +- **关联信息**: 思考消息 ID (`thinking_id`)、是否重新规划 (`replanned`)、详尽响应信息 (`response_info`,含生成文本、表情、锚点、实际发送ID、`SubMind`思考等)。 + +这些 `CycleInfo` 被存入一个队列 (`_cycle_history`),近者得观。此记录不仅便于调试,更关键的是,它会作为**上下文信息**传递给下一次循环的"思考"阶段,使得 `SubMind` 能鉴往知来,做出更连贯的决策。 + +*循环间会根据执行情况智能引入延迟,避免空耗资源。* + +## 4. 思考-规划-执行周期 (`_think_plan_execute_loop`) + +此乃 `HeartFChatting` 最核心的逻辑单元,每一循环皆按序执行以下三步: + +### 4.1. 思考 (`_get_submind_thinking`) + +* **第一步:观察环境**: 调用 `Observation` 的 `observe()` 方法,感知聊天室是否有新动态(如新消息)。 +* **第二步:触发子思维**: 调用关联 `SubMind` 的 `do_thinking_before_reply()` 方法。 + * **关键点**: 会将**上一个循环**的 `CycleInfo` 传入,让 `SubMind` 了解上次行动的决策、理由及是否重新规划,从而实现"承前启后"的思考。 + * `SubMind` 在此阶段不仅进行思考,还可能**调用其配置的工具**来收集信息。 +* **第三步:获取成果**: `SubMind` 返回两部分重要信息: + 1. 当前的内心想法 (`current_mind`)。 + 2. 通过工具调用收集到的结构化信息 (`structured_info`)。 + +### 4.2. 规划 (`_planner`) + +* **输入**: 接收来自"思考"阶段的 `current_mind` 和 `structured_info`,以及"观察"到的最新消息。 +* **目标**: 基于当前想法、已知信息、聊天记录、机器人个性以及可用动作,决定**接下来要做什么**。 +* **决策方式**: + 1. 构建一个精心设计的提示词 (`_build_planner_prompt`)。 + 2. 获取 `ActionManager` 中定义的当前可用动作(如 `no_reply`, `text_reply`, `emoji_reply`)作为"工具"选项。 + 3. 调用大语言模型 (`self.planner_llm`),**强制**其选择一个动作"工具"并提供理由。可选动作包括: + * `no_reply`: 不回复(例如,自己刚说过话或对方未回应)。 + * `text_reply`: 发送文本回复。 + * `emoji_reply`: 仅发送表情。 + * 文本回复亦可附带表情(通过 `emoji_query` 参数指定)。 +* **动态调整(重新规划)**: + * 在做出初步决策后,会检查自规划开始后是否有新消息 (`_check_new_messages`)。 + * 若有新消息,则有一定概率触发**重新规划**。此时会再次调用规划器,但提示词会包含之前决策的信息,要求 LLM 重新考虑。 +* **输出**: 返回一个包含最终决策的字典,主要包括: + * `action`: 选定的动作类型。 + * `reasoning`: 做出此决策的理由。 + * `emoji_query`: (可选) 如果需要发送表情,指定表情的主题。 + +### 4.3. 执行 (`_handle_action`) + +* **输入**: 接收"规划"阶段输出的 `action`、`reasoning` 和 `emoji_query`。 +* **行动**: 根据 `action` 的类型,分派到不同的处理函数: + * **文本回复 (`_handle_text_reply`)**: + 1. 获取锚点消息(当前实现为系统触发的占位符)。 + 2. 调用 `HeartFCSender` 的 `register_thinking` 标记开始思考。 + 3. 调用 `HeartFCGenerator` (`_replier_work`) 生成回复文本。**注意**: 回复器逻辑 (`_replier_work`) 本身并非独立复杂组件,主要是调用 `HeartFCGenerator` 完成文本生成。 + 4. 调用 `HeartFCSender` (`_sender`) 发送生成的文本和可能的表情。**注意**: 发送逻辑 (`_sender`, `_send_response_messages`, `_handle_emoji`) 同样委托给 `HeartFCSender` 实例处理,包含模拟打字、实际发送、存储消息等细节。 + * **仅表情回复 (`_handle_emoji_reply`)**: + 1. 获取锚点消息。 + 2. 调用 `HeartFCSender` 发送表情。 + * **不回复 (`_handle_no_reply`)**: + 1. 记录理由。 + 2. 进入等待状态 (`_wait_for_new_message`),直到检测到新消息或超时(目前300秒),期间会监听关闭信号。 + +## 总结 + +`HeartFChatting` 通过 **观察 -> 思考(含工具)-> 规划 -> 执行** 的闭环,并利用 `CycleInfo` 进行上下文传递,实现了更加智能和连贯的专注聊天行为。其核心在于利用 `SubMind` 进行深度思考和信息收集,再通过 LLM 规划器进行决策,最后由 `HeartFCSender` 可靠地执行消息发送任务。