From 4c97e155bddba276b7cdc817f49160a7aa41d139 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Tue, 22 Apr 2025 14:50:31 +0800 Subject: [PATCH 01/73] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=98=B5=E7=A7=B0=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/chat_module/heartFC_chat/heartFC_processor.py | 2 +- src/plugins/chat_module/heartFC_chat/reasoning_chat.py | 2 +- src/plugins/chat_module/only_process/only_message_process.py | 2 +- src/plugins/chat_module/reasoning_chat/reasoning_chat.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/chat_module/heartFC_chat/heartFC_processor.py b/src/plugins/chat_module/heartFC_chat/heartFC_processor.py index 00a9a024..a7c6251b 100644 --- a/src/plugins/chat_module/heartFC_chat/heartFC_processor.py +++ b/src/plugins/chat_module/heartFC_chat/heartFC_processor.py @@ -177,7 +177,7 @@ class HeartFCProcessor: current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time)) logger.info( f"[{current_time}][{mes_name}]" - f"{chat.user_info.user_nickname}:" + f"{message.message_info.user_info.user_nickname}:" f"{message.processed_plain_text}" f"兴趣度: {current_interest:.2f}" ) diff --git a/src/plugins/chat_module/heartFC_chat/reasoning_chat.py b/src/plugins/chat_module/heartFC_chat/reasoning_chat.py index addcd53d..b36db2ff 100644 --- a/src/plugins/chat_module/heartFC_chat/reasoning_chat.py +++ b/src/plugins/chat_module/heartFC_chat/reasoning_chat.py @@ -271,7 +271,7 @@ class ReasoningChat: willing_log = f"[回复意愿:{await willing_manager.get_willing(chat.stream_id):.2f}]" if is_willing else "" logger.info( f"[{current_time}][{mes_name}]" - f"{chat.user_info.user_nickname}:" + f"{message.message_info.user_info.user_nickname}:" f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]" ) do_reply = False diff --git a/src/plugins/chat_module/only_process/only_message_process.py b/src/plugins/chat_module/only_process/only_message_process.py index 5c239fb1..bc85a1f5 100644 --- a/src/plugins/chat_module/only_process/only_message_process.py +++ b/src/plugins/chat_module/only_process/only_message_process.py @@ -62,4 +62,4 @@ class MessageProcessor: mes_name = chat.group_info.group_name if chat.group_info else "私聊" # 将时间戳转换为datetime对象 current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S") - logger.info(f"[{current_time}][{mes_name}]{chat.user_info.user_nickname}: {message.processed_plain_text}") + logger.info(f"[{current_time}][{mes_name}]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}") diff --git a/src/plugins/chat_module/reasoning_chat/reasoning_chat.py b/src/plugins/chat_module/reasoning_chat/reasoning_chat.py index 5455aed6..3230f2dd 100644 --- a/src/plugins/chat_module/reasoning_chat/reasoning_chat.py +++ b/src/plugins/chat_module/reasoning_chat/reasoning_chat.py @@ -236,7 +236,7 @@ class ReasoningChat: willing_log = f"[回复意愿:{await willing_manager.get_willing(chat.stream_id):.2f}]" if is_willing else "" logger.info( f"[{current_time}][{mes_name}]" - f"{chat.user_info.user_nickname}:" + f"{message.message_info.user_info.user_nickname}:" f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]" ) do_reply = False From 85ff8dd292b0aa410e87139c089048a6ef1358b3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 22 Apr 2025 06:51:08 +0000 Subject: [PATCH 02/73] =?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/chat_module/only_process/only_message_process.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/chat_module/only_process/only_message_process.py b/src/plugins/chat_module/only_process/only_message_process.py index bc85a1f5..9009ffb1 100644 --- a/src/plugins/chat_module/only_process/only_message_process.py +++ b/src/plugins/chat_module/only_process/only_message_process.py @@ -62,4 +62,6 @@ class MessageProcessor: mes_name = chat.group_info.group_name if chat.group_info else "私聊" # 将时间戳转换为datetime对象 current_time = datetime.fromtimestamp(message.message_info.time).strftime("%H:%M:%S") - logger.info(f"[{current_time}][{mes_name}]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}") + logger.info( + f"[{current_time}][{mes_name}]{message.message_info.user_info.user_nickname}: {message.processed_plain_text}" + ) From 2732f407140eda9fefae88667233464135e69da7 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Wed, 23 Apr 2025 23:48:42 +0800 Subject: [PATCH 03/73] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A7=81=E8=81=8APFC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.py | 3 + src/plugins/PFC/action_planner.py | 328 +++++++++++++++-------- src/plugins/PFC/chat_observer.py | 5 +- src/plugins/PFC/conversation.py | 336 +++++++++++++++++++----- src/plugins/PFC/message_sender.py | 4 +- src/plugins/PFC/message_storage.py | 13 +- src/plugins/PFC/observation_info.py | 11 +- src/plugins/PFC/pfc.py | 54 +++- src/plugins/PFC/pfc_KnowledgeFetcher.py | 3 +- src/plugins/PFC/reply_checker.py | 59 ++++- src/plugins/PFC/reply_generator.py | 52 ++-- src/plugins/PFC/waiter.py | 71 +++-- template/bot_config_template.toml | 24 +- 13 files changed, 681 insertions(+), 282 deletions(-) diff --git a/src/config/config.py b/src/config/config.py index bf184a00..0957a7fb 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -495,6 +495,9 @@ class BotConfig: "llm_observation", "llm_sub_heartflow", "llm_heartflow", + "llm_PFC_action_planner", + "llm_PFC_chat", + "llm_PFC_reply_checker", ] for item in config_list: diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 5b399f06..9a878398 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,4 +1,5 @@ -from typing import Tuple +import time +from typing import Tuple, List, Dict, Any, Optional # 确保导入了必要的类型 from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -10,7 +11,8 @@ from .conversation_info import ConversationInfo logger = get_module_logger("action_planner") - +# 注意:这个 ActionPlannerInfo 类似乎没有在 ActionPlanner 中使用, +# 如果确实没用,可以考虑移除,但暂时保留以防万一。 class ActionPlannerInfo: def __init__(self): self.done_action = [] @@ -18,18 +20,18 @@ class ActionPlannerInfo: self.knowledge_list = [] self.memory_list = [] - +# ActionPlanner 类定义,顶格 class ActionPlanner: """行动规划器""" - def __init__(self, stream_id: str): self.llm = LLMRequest( - model=global_config.llm_normal, - temperature=global_config.llm_normal["temp"], - max_tokens=1000, + model=global_config.llm_PFC_action_planner, + temperature=global_config.llm_PFC_action_planner["temp"], + max_tokens=1500, request_type="action_planning", ) - self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2) + self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3) + self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2) self.name = global_config.BOT_NICKNAME self.chat_observer = ChatObserver.get_instance(stream_id) @@ -43,140 +45,250 @@ class ActionPlanner: Returns: Tuple[str, str]: (行动类型, 行动原因) """ + # --- 获取 Bot 上次发言时间信息 --- + time_since_last_bot_message_info = "" + try: + bot_id = str(global_config.BOT_QQ) + if hasattr(observation_info, 'chat_history') and observation_info.chat_history: + for i in range(len(observation_info.chat_history) - 1, -1, -1): + msg = observation_info.chat_history[i] + if not isinstance(msg, dict): + continue + sender_info = msg.get('user_info', {}) + sender_id = str(sender_info.get('user_id')) if isinstance(sender_info, dict) else None + msg_time = msg.get('time') + if sender_id == bot_id and msg_time: + time_diff = time.time() - msg_time + if time_diff < 60.0: + time_since_last_bot_message_info = f"提示:你上一条成功发送的消息是在 {time_diff:.1f} 秒前。\n" + break + else: + logger.debug("Observation info chat history is empty or not available for bot time check.") + except AttributeError: + logger.warning("ObservationInfo object might not have chat_history attribute yet for bot time check.") + except Exception as e: + logger.warning(f"获取 Bot 上次发言时间时出错: {e}") + # --- 获取 Bot 上次发言时间信息结束 --- + + timeout_context = "" + try: # 添加 try-except 以增加健壮性 + if hasattr(conversation_info, 'goal_list') and conversation_info.goal_list: + last_goal_tuple = conversation_info.goal_list[-1] + if isinstance(last_goal_tuple, tuple) and len(last_goal_tuple) > 0: + last_goal_text = last_goal_tuple[0] + if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text: + try: + timeout_minutes_text = last_goal_text.split(',')[0].replace('你等待了','') + timeout_context = f"重要提示:你刚刚因为对方长时间({timeout_minutes_text})没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" + except Exception: + timeout_context = f"重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" + else: + logger.debug("Conversation info goal_list is empty or not available for timeout check.") + except AttributeError: + logger.warning("ConversationInfo object might not have goal_list attribute yet for timeout check.") + except Exception as e: + logger.warning(f"检查超时目标时出错: {e}") + # 构建提示词 - logger.debug(f"开始规划行动:当前目标: {conversation_info.goal_list}") + logger.debug(f"开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}") # 使用 getattr - # 构建对话目标 + # 构建对话目标 (goals_str) goals_str = "" - if conversation_info.goal_list: - for goal_reason in conversation_info.goal_list: - # 处理字典或元组格式 - if isinstance(goal_reason, tuple): - # 假设元组的第一个元素是目标,第二个元素是原因 - goal = goal_reason[0] - reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因" - elif isinstance(goal_reason, dict): - goal = goal_reason.get("goal") - reasoning = goal_reason.get("reasoning", "没有明确原因") - else: - # 如果是其他类型,尝试转为字符串 - goal = str(goal_reason) - reasoning = "没有明确原因" + try: # 添加 try-except + if hasattr(conversation_info, 'goal_list') and conversation_info.goal_list: + for goal_reason in conversation_info.goal_list: + if isinstance(goal_reason, tuple) and len(goal_reason) > 0: + goal = goal_reason[0] + reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因" + elif isinstance(goal_reason, dict): + goal = goal_reason.get("goal", "目标内容缺失") + reasoning = goal_reason.get("reasoning", "没有明确原因") + else: + goal = str(goal_reason) + reasoning = "没有明确原因" + goal = str(goal) if goal is not None else "目标内容缺失" + reasoning = str(reasoning) if reasoning is not None else "没有明确原因" + goal_str += f"- 目标:{goal}\n 原因:{reasoning}\n" + if not goals_str: # 如果循环后 goals_str 仍为空 + goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n" + except AttributeError: + logger.warning("ConversationInfo object might not have goal_list attribute yet.") + goals_str = "- 获取对话目标时出错。\n" + except Exception as e: + logger.error(f"构建对话目标字符串时出错: {e}") + goals_str = "- 构建对话目标时出错。\n" - goal_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n" - goals_str += goal_str - else: - goal = "目前没有明确对话目标" - reasoning = "目前没有明确对话目标,最好思考一个对话目标" - 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) chat_history_text = "" - for msg in chat_history_list: - chat_history_text += f"{msg.get('detailed_plain_text', '')}\n" + 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" + if not chat_history_text: # 如果历史记录是空列表 + chat_history_text = "还没有聊天记录。\n" + else: + chat_history_text = "还没有聊天记录。\n" - if observation_info.new_messages_count > 0: - new_messages_list = observation_info.unprocessed_messages + 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" + # 清理消息应该由调用者或 observation_info 内部逻辑处理,这里不再调用 clear + # if hasattr(observation_info, 'clear_unprocessed_messages'): + # observation_info.clear_unprocessed_messages() + else: + logger.warning("ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing.") + except AttributeError: + logger.warning("ObservationInfo object might be missing expected attributes for chat history.") + chat_history_text = "获取聊天记录时出错。\n" + except Exception as e: + logger.error(f"处理聊天记录时发生未知错误: {e}") + chat_history_text = "处理聊天记录时出错。\n" - 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() + # 构建 Persona 文本 (persona_text) + identity_details_only = self.identity_detail_info + identity_addon = "" + if isinstance(identity_details_only, str): + pronouns = ["你", "我", "他"] + original_details = identity_details_only + for p in pronouns: + if identity_details_only.startswith(p): + identity_details_only = identity_details_only[len(p):] + break + if identity_details_only.endswith("。"): + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(',, ') + if cleaned_details: + identity_addon = f"并且{cleaned_details}" + persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" - personality_text = f"你的名字是{self.name},{self.personality_info}" + # --- 构建更清晰的行动历史和上一次行动结果 --- + action_history_summary = "你最近执行的行动历史:\n" + last_action_context = "关于你【上一次尝试】的行动:\n" - # 构建action历史文本 - action_history_list = ( - conversation_info.done_action[-10:] - if len(conversation_info.done_action) >= 10 - else conversation_info.done_action - ) - action_history_text = "你之前做的事情是:" - for action in action_history_list: - if isinstance(action, dict): - action_type = action.get("action") - action_reason = action.get("reason") - action_status = action.get("status") - if action_status == "recall": - action_history_text += ( - f"原本打算:{action_type},但是因为有新消息,你发现这个行动不合适,所以你没做\n" - ) - elif action_status == "done": - action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n" - elif isinstance(action, tuple): - # 假设元组的格式是(action_type, action_reason, action_status) - action_type = action[0] if len(action) > 0 else "未知行动" - action_reason = action[1] if len(action) > 1 else "未知原因" - action_status = action[2] if len(action) > 2 else "done" - if action_status == "recall": - action_history_text += ( - f"原本打算:{action_type},但是因为有新消息,你发现这个行动不合适,所以你没做\n" - ) - elif action_status == "done": - action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n" + action_history_list = [] + try: # 添加 try-except + if hasattr(conversation_info, 'done_action') and conversation_info.done_action: + action_history_list = conversation_info.done_action[-5:] + else: + logger.debug("Conversation info done_action is empty or not available.") + except AttributeError: + logger.warning("ConversationInfo object might not have done_action attribute yet.") + except Exception as e: + logger.error(f"访问行动历史时出错: {e}") - prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请分析以下内容,根据信息决定下一步行动: + if not action_history_list: + action_history_summary += "- 还没有执行过行动。\n" + last_action_context += "- 这是你规划的第一个行动。\n" + else: + for i, action_data in enumerate(action_history_list): + action_type = "未知" + plan_reason = "未知" + status = "未知" + final_reason = "" + action_time = "" -当前对话目标:{goals_str} + if isinstance(action_data, dict): + action_type = action_data.get("action", "未知") + plan_reason = action_data.get("plan_reason", "未知规划原因") + status = action_data.get("status", "未知") + final_reason = action_data.get("final_reason", "") + action_time = action_data.get("time", "") + elif isinstance(action_data, tuple): + if len(action_data) > 0: action_type = action_data[0] + if len(action_data) > 1: plan_reason = action_data[1] + if len(action_data) > 2: status = action_data[2] + if status == "recall" and len(action_data) > 3: final_reason = action_data[3] -{action_history_text} + reason_text = f", 失败/取消原因: {final_reason}" if final_reason else "" + summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}" + action_history_summary += summary_line + "\n" -最近的对话记录: -{chat_history_text} + if i == len(action_history_list) - 1: + last_action_context += f"- 上次【规划】的行动是: '{action_type}'\n" + last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n" + if status == "done": + last_action_context += f"- 该行动已【成功执行】。\n" + elif status == "recall": + last_action_context += f"- 但该行动最终【未能执行/被取消】。\n" + if final_reason: + last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n" + else: + last_action_context += f"- 【重要】失败/取消原因未明确记录。\n" + else: + last_action_context += f"- 该行动当前状态: {status}\n" -请你接下去想想要你要做什么,可以发言,可以等待,可以倾听,可以调取知识。注意不同行动类型的要求,不要重复发言: -行动类型: + # --- 构建最终的 Prompt --- + prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请根据以下【所有信息】审慎决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识: + +【当前对话目标】 +{goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。\n"} + +【最近行动历史概要】 +{action_history_summary} +【上一次行动的详细情况和结果】 +{last_action_context} +【时间和超时提示】 +{time_since_last_bot_message_info}{timeout_context} +【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) +{chat_history_text if chat_history_text.strip() else "还没有聊天记录。\n"} +--- 行动决策指南 --- +1. **仔细分析【上一次行动的详细情况和结果】**。如果上次行动是 direct_reply 且因“内容与你上一条发言完全相同”或“高度相似”而被取消(status: recall),那么【绝对不要】立即再次规划 direct_reply。在这种特定情况下,你应该优先考虑 wait (等待用户的新回应) 或 rethink_goal (如果对话似乎因此卡住了)。 +2. 结合【当前对话目标】和【最近的对话记录】来判断是否需要回应、回应什么。如果【最近的对话记录】中有新的用户消息,通常需要 direct_reply。如果上次行动成功,或者上次失败的原因不是重复,可以根据对话内容考虑 direct_reply。 +3. 注意【时间和超时提示】,如果对方长时间未回复(例如在 timeout_context 中提示),end_conversation 可能更合适。 +4. 只有在你确信需要发言(比如回应新消息、追问、深入话题),并且上一次行动没有因重复被拒时,才应优先选择 direct_reply。 + +--- 可选行动类型 --- fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择 -wait: 当你做出了发言,对方尚未回复时暂时等待对方的回复 +wait: 等待对方回复(尤其是在你刚发言后、或上次发言因重复被拒时、或不确定做什么时,这是较安全的选择) listening: 倾听对方发言,当你认为对方发言尚未结束时采用 -direct_reply: 不符合上述情况,回复对方,注意不要过多或者重复发言 -rethink_goal: 重新思考对话目标,当发现对话目标不合适时选择,会重新思考对话目标 -end_conversation: 结束对话,长时间没回复或者当你觉得谈话暂时结束时选择,停止该场对话 +direct_reply: 直接回复或发送新消息,允许适当的追问和深入话题,**但是请务必遵守上面的决策指南,避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言** +rethink_goal: 重新思考对话目标,当发现对话目标不再适用或对话卡住时选择,注意私聊的环境是灵活的,有可能需要经常选择 +end_conversation: 决定结束对话,对方长时间没回复或者当你觉得谈话暂时结束时可以选择 -请以JSON格式输出,包含以下字段: -1. action: 行动类型,注意你之前的行为 -2. reason: 选择该行动的原因,注意你之前的行为(简要解释) +请以JSON格式输出你的决策: +{{ + "action": "选择的行动类型 (必须是上面列表中的一个)", + "reason": "选择该行动的详细原因 (必须解释你是如何根据“上一次行动结果”、“对话记录”和“决策指南”做出判断的)" +}} 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" - logger.debug(f"发送到LLM的提示词: {prompt}") + logger.debug(f"发送到LLM的提示词 (已更新): {prompt}") try: content, _ = await self.llm.generate_response_async(prompt) logger.debug(f"LLM原始返回内容: {content}") - # 使用简化函数提取JSON内容 success, result = get_items_from_json( - content, "action", "reason", default_values={"action": "direct_reply", "reason": "没有明确原因"} + content, "action", "reason", + default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待"} ) - if not success: - return "direct_reply", "JSON解析失败,选择直接回复" + action = result.get("action", "wait") + reason = result.get("reason", "LLM未提供原因,默认等待") - action = result["action"] - reason = result["reason"] # 验证action类型 - if action not in [ - "direct_reply", - "fetch_knowledge", - "wait", - "listening", - "rethink_goal", - "end_conversation", - ]: - logger.warning(f"未知的行动类型: {action},默认使用listening") - action = "listening" + valid_actions = ["direct_reply", "fetch_knowledge", "wait", "listening", "rethink_goal", "end_conversation"] + if action not in valid_actions: + logger.warning(f"LLM返回了未知的行动类型: '{action}',强制改为 wait") + reason = f"(原始行动'{action}'无效,已强制改为wait) {reason}" + action = "wait" logger.info(f"规划的行动: {action}") logger.info(f"行动原因: {reason}") return action, reason except Exception as e: - logger.error(f"规划行动时出错: {str(e)}") - return "direct_reply", "发生错误,选择直接回复" + logger.error(f"规划行动时调用 LLM 或处理结果出错: {str(e)}") + return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" \ No newline at end of file diff --git a/src/plugins/PFC/chat_observer.py b/src/plugins/PFC/chat_observer.py index 1239af7a..60acb5f5 100644 --- a/src/plugins/PFC/chat_observer.py +++ b/src/plugins/PFC/chat_observer.py @@ -119,6 +119,7 @@ class ChatObserver: self.last_cold_chat_check = current_time # 判断是否冷场 + is_cold = False if self.last_message_time is None: is_cold = True else: @@ -354,7 +355,7 @@ class ChatObserver: Returns: List[Dict[str, Any]]: 缓存的消息历史列表 """ - return self.message_cache[:limit] + return self.message_cache[-limit:] def get_last_message(self) -> Optional[Dict[str, Any]]: """获取最后一条消息 @@ -364,7 +365,7 @@ class ChatObserver: """ if not self.message_cache: return None - return self.message_cache[0] + return self.message_cache[-1] def __str__(self): return f"ChatObserver for {self.stream_id}" diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 9502b755..23a55544 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1,5 +1,8 @@ +import time import asyncio import datetime +from .message_storage import MongoDBMessageStorage +from ...config.config import global_config from typing import Dict, Any from ..chat.message import Message from .pfc_types import ConversationState @@ -70,7 +73,42 @@ class Conversation: logger.error(f"初始化对话实例:注册信息组件失败: {e}") logger.error(traceback.format_exc()) raise + try: + logger.info(f"为 {self.stream_id} 加载初始聊天记录...") + storage = MongoDBMessageStorage() # 创建存储实例 + # 获取当前时间点之前最多 N 条消息 (比如 30 条) + # get_messages_before 返回的是按时间正序排列的列表 + initial_messages = await storage.get_messages_before( + chat_id=self.stream_id, + time_point=time.time(), + limit=30 # 加载最近20条作为初始上下文,可以调整 + ) + if initial_messages: + # 将加载的消息填充到 ObservationInfo 的 chat_history + self.observation_info.chat_history = initial_messages + self.observation_info.chat_history_count = len(initial_messages) + + # 更新 ObservationInfo 中的时间戳等信息 + last_msg = initial_messages[-1] + self.observation_info.last_message_time = last_msg.get('time') + last_user_info = UserInfo.from_dict(last_msg.get("user_info", {})) + self.observation_info.last_message_sender = last_user_info.user_id + self.observation_info.last_message_content = last_msg.get("processed_plain_text", "") + + # (可选)可以遍历 initial_messages 来设置 last_bot_speak_time 和 last_user_speak_time + # 这里为了简化,只用了最后一条消息的时间,如果需要精确的发言者时间需要遍历 + + logger.info(f"成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}") + + # 让 ChatObserver 从加载的最后一条消息之后开始同步 + self.chat_observer.last_message_time = self.observation_info.last_message_time + self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录 + else: + logger.info("没有找到初始聊天记录。") + except Exception as load_err: + logger.error(f"加载初始聊天记录时出错: {load_err}") + # 出错也要继续,只是没有历史记录而已 # 组件准备完成,启动该论对话 self.should_continue = True asyncio.create_task(self.start()) @@ -86,24 +124,76 @@ class Conversation: async def _plan_and_action_loop(self): """思考步,PFC核心循环模块""" - # 获取最近的消息历史 while self.should_continue: - # 使用决策信息来辅助行动规划 - action, reason = await self.action_planner.plan(self.observation_info, self.conversation_info) - if self._check_new_messages_after_planning(): - continue + try: + # --- 在规划前记录当前新消息数量 --- + initial_new_message_count = 0 + if hasattr(self.observation_info, 'new_messages_count'): + initial_new_message_count = self.observation_info.new_messages_count + else: + logger.warning("ObservationInfo missing 'new_messages_count' before planning.") - # 执行行动 - await self._handle_action(action, reason, self.observation_info, self.conversation_info) + # 使用决策信息来辅助行动规划 + action, reason = await self.action_planner.plan(self.observation_info, self.conversation_info) # 注意:plan 函数内部现在不应再调用 clear_unprocessed_messages - for goal in self.conversation_info.goal_list: - # 检查goal是否为元组类型,如果是元组则使用索引访问,如果是字典则使用get方法 - if isinstance(goal, tuple): - # 假设元组的第一个元素是目标内容 - print(f"goal: {goal}") - if goal[0] == "结束对话": - self.should_continue = False - break + # --- 规划后检查是否有 *更多* 新消息到达 --- + current_new_message_count = 0 + if hasattr(self.observation_info, 'new_messages_count'): + current_new_message_count = self.observation_info.new_messages_count + else: + logger.warning("ObservationInfo missing 'new_messages_count' after planning.") + + if current_new_message_count > initial_new_message_count: + # 只有当规划期间消息数量 *增加* 了,才认为需要重新规划 + logger.info(f"规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划") + await asyncio.sleep(0.1) # 短暂延时 + continue # 跳过本次行动,重新规划 + + # --- 如果没有在规划期间收到更多新消息,则准备执行行动 --- + + # --- 清理未处理消息:移到这里,在执行动作前 --- + # 只有当确实有新消息被 planner 看到,并且 action 是要处理它们的时候才清理 + if initial_new_message_count > 0 and action == "direct_reply": + 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() + # 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个) + if hasattr(self.observation_info, 'new_messages_count'): + self.observation_info.new_messages_count = 0 + else: + logger.error("无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!") + # 这里可能需要考虑是否继续执行 action,或者抛出错误 + + + # --- 执行行动 --- + await self._handle_action(action, reason, self.observation_info, self.conversation_info) + + goal_ended = False + if hasattr(self.conversation_info, 'goal_list') and self.conversation_info.goal_list: + for goal in self.conversation_info.goal_list: + if isinstance(goal, tuple) and len(goal) > 0 and goal[0] == "结束对话": + goal_ended = True + break + elif isinstance(goal, dict) and goal.get("goal") == "结束对话": + goal_ended = True + break + + if goal_ended: + self.should_continue = False + logger.info("检测到'结束对话'目标,停止循环。") + # break # 可以选择在这里直接跳出循环 + + except Exception as loop_err: + logger.error(f"PFC主循环出错: {loop_err}") + logger.error(traceback.format_exc()) + # 发生严重错误时可以考虑停止,或者至少等待一下再继续 + await asyncio.sleep(1) # 发生错误时等待1秒 + #添加短暂的异步睡眠 + if self.should_continue: # 只有在还需要继续循环时才 sleep + await asyncio.sleep(0.1) # 等待 0.1 秒,给其他任务执行时间 + + logger.info(f"PFC 循环结束 for stream_id: {self.stream_id}") # 添加日志表明循环正常结束 def _check_new_messages_after_planning(self): """检查在规划后是否有新消息""" @@ -113,8 +203,7 @@ class Conversation: return True return False - @staticmethod - def _convert_to_message(msg_dict: Dict[str, Any]) -> Message: + def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message: """将消息字典转换为Message对象""" try: chat_info = msg_dict.get("chat_info", {}) @@ -124,7 +213,7 @@ class Conversation: return Message( message_id=msg_dict["message_id"], chat_stream=chat_stream, - timestamp=msg_dict["time"], + time=msg_dict["time"], user_info=user_info, processed_plain_text=msg_dict.get("processed_plain_text", ""), detailed_plain_text=msg_dict.get("detailed_plain_text", ""), @@ -137,92 +226,152 @@ class Conversation: self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo ): """处理规划的行动""" + logger.info(f"执行行动: {action}, 原因: {reason}") - # 记录action历史,先设置为stop,完成后再设置为done - conversation_info.done_action.append( - { - "action": action, - "reason": reason, - "status": "start", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - } - ) + # 记录action历史,先设置为start,完成后再设置为done (这个 update 移到后面执行成功后再做) + current_action_record = { + "action": action, + "plan_reason": reason, #使用 plan_reason 存储规划原因 + "status": "start", # 初始状态为 start + "time": datetime.datetime.now().strftime("%H:%M:%S"), + "final_reason": None + } + conversation_info.done_action.append(current_action_record) + # 获取刚刚添加记录的索引,方便后面更新状态 + action_index = len(conversation_info.done_action) - 1 + # --- 根据不同的 action 执行 --- if action == "direct_reply": - self.waiter.wait_accumulated_time = 0 + # --- 这个 if 块内部的所有代码都需要正确缩进 --- + self.waiter.wait_accumulated_time = 0 # 重置等待时间 self.state = ConversationState.GENERATING + # 生成回复 self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info) - print(f"生成回复: {self.generated_reply}") + logger.info(f"生成回复: {self.generated_reply}") # 使用 logger - # # 检查回复是否合适 - # is_suitable, reason, need_replan = await self.reply_generator.check_reply( - # self.generated_reply, - # self.current_goal - # ) - - if self._check_new_messages_after_planning(): - logger.info("333333发现新消息,重新考虑行动") - conversation_info.done_action[-1].update( - { - "status": "recall", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - } + # --- 调用 ReplyChecker 检查回复 --- + is_suitable = False # 先假定不合适,检查通过再改为 True + check_reason = "检查未执行" # 用不同的变量名存储检查原因 + need_replan = False + try: + # 尝试获取当前主要目标 + current_goal_str = conversation_info.goal_list[0][0] if conversation_info.goal_list else "" + + # 调用检查器 + is_suitable, check_reason, need_replan = await self.reply_generator.check_reply( + reply=self.generated_reply, + goal=current_goal_str, + chat_history=observation_info.chat_history, # 传入最新的历史记录! + retry_count=0 ) - return None + logger.info(f"回复检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}") - await self._send_reply() + except Exception as check_err: + logger.error(f"调用 ReplyChecker 时出错: {check_err}") + check_reason = f"检查过程出错: {check_err}" # 记录错误原因 + # is_suitable 保持 False - conversation_info.done_action[-1].update( - { + # --- 处理检查结果 --- + if is_suitable: + # 回复合适,继续执行 + # 检查是否有新消息进来 + if self._check_new_messages_after_planning(): + logger.info("检查到新消息,取消发送已生成的回复,重新规划行动") + # 更新 action 状态为 recall + conversation_info.done_action[action_index].update({ + "status": "recall", + "reason": f"有新消息,取消发送: {self.generated_reply}", # 更新原因 + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) + return None # 退出 _handle_action + + # 发送回复 + await self._send_reply() # 这个函数内部会处理自己的错误 + + # 更新 action 历史状态为 done + conversation_info.done_action[action_index].update({ "status": "done", "time": datetime.datetime.now().strftime("%H:%M:%S"), - } - ) - return None + }) + + else: + # 回复不合适 + logger.warning(f"生成的回复被 ReplyChecker 拒绝: '{self.generated_reply}'. 原因: {check_reason}") + # 更新 action 状态为 recall (因为没执行发送) + conversation_info.done_action[action_index].update({ + "status": "recall", + "final_reason": check_reason, + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) + + # 如果检查器建议重新规划 + if need_replan: + logger.info("ReplyChecker 建议重新规划目标。") + # 可选:在此处清空目标列表以强制重新规划 + # conversation_info.goal_list = [] + + # 注意:不发送消息,也不执行后面的代码 + + # --- 之前重复的代码块已被删除 --- elif action == "fetch_knowledge": self.waiter.wait_accumulated_time = 0 - self.state = ConversationState.FETCHING knowledge = "TODO:知识" topic = "TODO:关键词" - logger.info(f"假装获取到知识{knowledge},关键词是: {topic}") - if knowledge: - if topic not in self.conversation_info.knowledge_list: - self.conversation_info.knowledge_list.append({"topic": topic, "knowledge": knowledge}) - return None - else: - self.conversation_info.knowledge_list[topic] += knowledge - return None - return None + pass # 简单处理 + # 标记 action 为 done + conversation_info.done_action[action_index].update({ + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) elif action == "rethink_goal": self.waiter.wait_accumulated_time = 0 - self.state = ConversationState.RETHINKING await self.goal_analyzer.analyze_goal(conversation_info, observation_info) - return None + # 标记 action 为 done + conversation_info.done_action[action_index].update({ + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) elif action == "listening": self.state = ConversationState.LISTENING logger.info("倾听对方发言...") await self.waiter.wait_listening(conversation_info) - return None + # listening 和 wait 通常在完成后不需要标记为 done,因为它们是持续状态, + # 但如果需要记录,可以在 waiter 返回后标记。目前逻辑是 waiter 返回后主循环继续。 + # 为了统一,可以暂时在这里也标记一下(或者都不标记) + conversation_info.done_action[action_index].update({ + "status": "done", # 或 "completed" + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) + elif action == "end_conversation": - self.should_continue = False + self.should_continue = False # 设置循环停止标志 logger.info("决定结束对话...") - return None + # 标记 action 为 done + conversation_info.done_action[action_index].update({ + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) + # 这里不需要 return,主循环会在下一轮检查 should_continue - else: # wait + else: # 对应 'wait' 动作 self.state = ConversationState.WAITING logger.info("等待更多信息...") await self.waiter.wait(self.conversation_info) - return None + # 同 listening,可以考虑是否标记状态 + conversation_info.done_action[action_index].update({ + "status": "done", # 或 "completed" + "time": datetime.datetime.now().strftime("%H:%M:%S"), + }) async def _send_timeout_message(self): """发送超时结束消息""" @@ -245,12 +394,53 @@ class Conversation: return try: - await self.direct_sender.send_message(chat_stream=self.chat_stream, content=self.generated_reply) - self.chat_observer.trigger_update() # 触发立即更新 - if not await self.chat_observer.wait_for_update(): - logger.warning("等待消息更新超时") + # 外层 try: 捕获发送消息和后续处理中的主要错误 + current_time = time.time() # 获取当前时间戳 + reply_content = self.generated_reply # 获取要发送的内容 + + # 发送消息 + 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(): + logger.warning("等待 ChatObserver 更新完成超时") + + self.state = ConversationState.ANALYZING # 更新对话状态 - self.state = ConversationState.ANALYZING except Exception as e: - logger.error(f"发送消息失败: {str(e)}") - self.state = ConversationState.ANALYZING + # 这是外层 try 对应的 except + logger.error(f"发送消息或更新状态时失败: {str(e)}") + self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态 \ No newline at end of file diff --git a/src/plugins/PFC/message_sender.py b/src/plugins/PFC/message_sender.py index 5a5818ae..bc4499ed 100644 --- a/src/plugins/PFC/message_sender.py +++ b/src/plugins/PFC/message_sender.py @@ -4,7 +4,7 @@ from ..chat.chat_stream import ChatStream from ..chat.message import Message from ..message.message_base import Seg from src.plugins.chat.message import MessageSending, MessageSet -from src.plugins.chat.messagesender import message_manager +from src.plugins.chat.message_sender import message_manager logger = get_module_logger("message_sender") @@ -15,8 +15,8 @@ class DirectMessageSender: def __init__(self): pass - @staticmethod async def send_message( + self, chat_stream: ChatStream, content: str, reply_to_message: Optional[Message] = None, diff --git a/src/plugins/PFC/message_storage.py b/src/plugins/PFC/message_storage.py index cd6a01e3..55bccb14 100644 --- a/src/plugins/PFC/message_storage.py +++ b/src/plugins/PFC/message_storage.py @@ -50,16 +50,21 @@ class MessageStorage(ABC): class MongoDBMessageStorage(MessageStorage): """MongoDB消息存储实现""" + def __init__(self): + self.db = db + async def get_messages_after(self, chat_id: str, message_time: float) -> List[Dict[str, Any]]: - query = {"chat_id": chat_id, "time": {"$gt": message_time}} + query = {"chat_id": chat_id} # print(f"storage_check_message: {message_time}") - return list(db.messages.find(query).sort("time", 1)) + query["time"] = {"$gt": message_time} + + return list(self.db.messages.find(query).sort("time", 1)) async def get_messages_before(self, chat_id: str, time_point: float, limit: int = 5) -> List[Dict[str, Any]]: query = {"chat_id": chat_id, "time": {"$lt": time_point}} - messages = list(db.messages.find(query).sort("time", -1).limit(limit)) + messages = list(self.db.messages.find(query).sort("time", -1).limit(limit)) # 将消息按时间正序排列 messages.reverse() @@ -68,7 +73,7 @@ class MongoDBMessageStorage(MessageStorage): async def has_new_messages(self, chat_id: str, after_time: float) -> bool: query = {"chat_id": chat_id, "time": {"$gt": after_time}} - return db.messages.find_one(query) is not None + return self.db.messages.find_one(query) is not None # # 创建一个内存消息存储实现,用于测试 diff --git a/src/plugins/PFC/observation_info.py b/src/plugins/PFC/observation_info.py index f92f1230..08ff3c04 100644 --- a/src/plugins/PFC/observation_info.py +++ b/src/plugins/PFC/observation_info.py @@ -120,10 +120,6 @@ class ObservationInfo: # #spec # meta_plan_trigger: bool = False - def __init__(self): - self.last_message_id = None - self.chat_observer = None - def __post_init__(self): """初始化后创建handler""" self.chat_observer = None @@ -133,7 +129,7 @@ class ObservationInfo: """绑定到指定的chat_observer Args: - chat_observer: 要绑定的ChatObserver实例 + stream_id: 聊天流ID """ self.chat_observer = chat_observer self.chat_observer.notification_manager.register_handler( @@ -175,8 +171,7 @@ class ObservationInfo: self.last_bot_speak_time = message["time"] else: self.last_user_speak_time = message["time"] - if user_info.user_id is not None: - self.active_users.add(str(user_info.user_id)) + self.active_users.add(user_info.user_id) self.new_messages_count += 1 self.unprocessed_messages.append(message) @@ -232,7 +227,7 @@ class ObservationInfo: """清空未处理消息列表""" # 将未处理消息添加到历史记录中 for message in self.unprocessed_messages: - self.chat_history.append(message) # TODO NEED FIX TYPE??? + self.chat_history.append(message) # 清空未处理消息列表 self.has_unread_messages = False self.unprocessed_messages.clear() diff --git a/src/plugins/PFC/pfc.py b/src/plugins/PFC/pfc.py index 1d096cc4..08d4fabf 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -34,7 +34,8 @@ class GoalAnalyzer: model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal" ) - self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2) + self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3) + self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2) self.name = global_config.BOT_NICKNAME self.nick_name = global_config.BOT_ALIAS_NAMES self.chat_observer = ChatObserver.get_instance(stream_id) @@ -93,15 +94,28 @@ class GoalAnalyzer: observation_info.clear_unprocessed_messages() - personality_text = f"你的名字是{self.name},{self.personality_info}" + identity_details_only = self.identity_detail_info + identity_addon = "" + if isinstance(identity_details_only, str): + pronouns = ["你", "我", "他"] + for p in pronouns: + if identity_details_only.startswith(p): + identity_details_only = identity_details_only[len(p):] + break + if identity_details_only.endswith("。"): + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(',, ') + if cleaned_details: + identity_addon = f"并且{cleaned_details}" + persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" # 构建action历史文本 action_history_list = conversation_info.done_action action_history_text = "你之前做的事情是:" for action in action_history_list: action_history_text += f"{action}\n" - prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请分析以下聊天记录,并根据你的性格特征确定多个明确的对话目标。 + prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请分析以下聊天记录,并根据你的性格特征确定多个明确的对话目标。 这些目标应该反映出对话的不同方面和意图。 {action_history_text} @@ -160,16 +174,16 @@ class GoalAnalyzer: # 返回第一个目标作为当前主要目标(如果有) if result: first_goal = result[0] - return first_goal.get("goal", ""), "", first_goal.get("reasoning", "") + return (first_goal.get("goal", ""), "", first_goal.get("reasoning", "")) else: # 单个目标的情况 goal = result.get("goal", "") reasoning = result.get("reasoning", "") conversation_info.goal_list.append((goal, reasoning)) - return goal, "", reasoning + return (goal, "", reasoning) # 如果解析失败,返回默认值 - return "", "", "" + return ("", "", "") async def _update_goals(self, new_goal: str, method: str, reasoning: str): """更新目标列表 @@ -195,8 +209,7 @@ class GoalAnalyzer: if len(self.goals) > self.max_goals: self.goals.pop() # 移除最老的目标 - @staticmethod - def _calculate_similarity(goal1: str, goal2: str) -> float: + def _calculate_similarity(self, goal1: str, goal2: str) -> float: """简单计算两个目标之间的相似度 这里使用一个简单的实现,实际可以使用更复杂的文本相似度算法 @@ -244,9 +257,25 @@ class GoalAnalyzer: sender = "你说" chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n" - personality_text = f"你的名字是{self.name},{self.personality_info}" + identity_details_only = self.identity_detail_info + identity_addon = "" + if isinstance(identity_details_only, str): + pronouns = ["你", "我", "他"] + for p in pronouns: + if identity_details_only.startswith(p): + identity_details_only = identity_details_only[len(p):] + break + if identity_details_only.endswith("。"): + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(',, ') + if cleaned_details: + identity_addon = f"并且{cleaned_details}" - prompt = f"""{personality_text}。现在你在参与一场QQ聊天, + persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" + # ===> Persona 文本构建结束 <=== + + # --- 修改 Prompt 字符串,使用 persona_text --- + prompt = f"""{persona_text}。现在你在参与一场QQ聊天, 当前对话目标:{goal} 产生该对话目标的原因:{reasoning} @@ -300,8 +329,7 @@ class DirectMessageSender: self.logger = get_module_logger("direct_sender") self.storage = MessageStorage() - @staticmethod - async def send_via_ws(message: MessageSending) -> None: + async def send_via_ws(self, message: MessageSending) -> None: try: await global_api.send_message(message) except Exception as e: @@ -352,7 +380,7 @@ class DirectMessageSender: # logger.info(f"发送消息到{end_point}") # logger.info(message_json) try: - await global_api.send_message_rest(end_point, message_json) + await global_api.send_message_REST(end_point, message_json) except Exception as e: logger.error(f"REST方式发送失败,出现错误: {str(e)}") logger.info("尝试使用ws发送") diff --git a/src/plugins/PFC/pfc_KnowledgeFetcher.py b/src/plugins/PFC/pfc_KnowledgeFetcher.py index 7ce7ce7a..1a0d495c 100644 --- a/src/plugins/PFC/pfc_KnowledgeFetcher.py +++ b/src/plugins/PFC/pfc_KnowledgeFetcher.py @@ -19,8 +19,7 @@ class KnowledgeFetcher: request_type="knowledge_fetch", ) - @staticmethod - async def fetch(query: str, chat_history: List[Message]) -> Tuple[str, str]: + async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]: """获取相关知识 Args: diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 0efa46fa..72489251 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -1,6 +1,6 @@ import json import datetime -from typing import Tuple +from typing import Tuple, List, Dict, Any from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -15,13 +15,13 @@ class ReplyChecker: def __init__(self, stream_id: str): self.llm = LLMRequest( - model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="reply_check" + model=global_config.llm_PFC_reply_checker, temperature=0.55, max_tokens=1000, request_type="reply_check" ) self.name = global_config.BOT_NICKNAME self.chat_observer = ChatObserver.get_instance(stream_id) self.max_retries = 2 # 最大重试次数 - async def check(self, reply: str, goal: str, retry_count: int = 0) -> Tuple[bool, str, bool]: + async def check(self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0) -> Tuple[bool, str, bool]: """检查生成的回复是否合适 Args: @@ -32,10 +32,41 @@ class ReplyChecker: Returns: Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划) """ - # 获取最新的消息记录 - messages = self.chat_observer.get_cached_messages(limit=5) + # 不再从 observer 获取,直接使用传入的 chat_history + # messages = self.chat_observer.get_cached_messages(limit=20) chat_history_text = "" - for msg in messages: + try: + # 筛选出最近由 Bot 自己发送的消息 + bot_messages = [] + for msg in reversed(chat_history): + user_info = UserInfo.from_dict(msg.get("user_info", {})) + if str(user_info.user_id) == str(global_config.BOT_QQ): # 确保比较的是字符串 + bot_messages.append(msg.get('processed_plain_text', '')) + if len(bot_messages) >= 2: # 只和最近的两条比较 + break + # 进行比较 + if bot_messages: + # 可以用简单比较,或者更复杂的相似度库 (如 difflib) + # 简单比较:是否完全相同 + if reply == bot_messages[0]: # 和最近一条完全一样 + logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'") + return False, "回复内容与你上一条发言完全相同,请修改,可以选择深入话题或寻找其它话题或等待", False # 不合适,无需重新规划 + # 2. 相似度检查 (如果精确匹配未通过) + import difflib # 导入 difflib 库 + # 计算编辑距离相似度,ratio() 返回 0 到 1 之间的浮点数 + similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio() + logger.debug(f"ReplyChecker - 相似度: {similarity_ratio:.2f}") + + # 设置一个相似度阈值 + similarity_threshold = 0.9 + if similarity_ratio > similarity_threshold: + logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'") + return False, f"拒绝发送:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),请修改,可以选择深入话题或寻找其它话题或等待。", False + + except Exception as self_check_err: + logger.error(f"检查自身重复发言时出错: {self_check_err}") + + 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}" @@ -43,7 +74,7 @@ class ReplyChecker: sender = "你说" chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n" - prompt = f"""请检查以下回复是否合适: + prompt = f"""请检查以下回复或消息是否合适: 当前对话目标:{goal} 最新的对话记录: @@ -52,12 +83,18 @@ class ReplyChecker: 待检查的回复: {reply} -请检查以下几点: +请结合聊天记录检查以下几点: 1. 回复是否依然符合当前对话目标和实现方式 2. 回复是否与最新的对话记录保持一致性 -3. 回复是否重复发言,重复表达 -4. 回复是否包含违法违规内容(政治敏感、暴力等) -5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话 +3. 回复是否重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义) +4. 回复是否包含政治敏感内容 +5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话(不要自己回复自己的消息) +6. 回复是否通俗易懂 +7. 回复是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸” +8. 回复是否使用了完全没必要的修辞 +9. 回复是否逻辑通顺 +10. 回复是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况) +11. 在连续多次发送消息的情况下,当前回复是否衔接自然,会不会显得奇怪 请以JSON格式输出,包含以下字段: 1. suitable: 是否合适 (true/false) diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index a27abecd..5ef58e27 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, List, Dict, Any from src.common.logger import get_module_logger from ..models.utils_model import LLMRequest from ...config.config import global_config @@ -16,12 +16,13 @@ class ReplyGenerator: def __init__(self, stream_id: str): self.llm = LLMRequest( - model=global_config.llm_normal, - temperature=global_config.llm_normal["temp"], + model=global_config.llm_PFC_chat, + temperature=global_config.llm_PFC_chat["temp"], max_tokens=300, request_type="reply_generation", ) - self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2) + self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3) + self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2) self.name = global_config.BOT_NICKNAME self.chat_observer = ChatObserver.get_instance(stream_id) self.reply_checker = ReplyChecker(stream_id) @@ -30,8 +31,11 @@ class ReplyGenerator: """生成回复 Args: - observation_info: 观察信息 - conversation_info: 对话信息 + goal: 对话目标 + chat_history: 聊天历史 + knowledge_cache: 知识缓存 + previous_reply: 上一次生成的回复(如果有) + retry_count: 当前重试次数 Returns: str: 生成的回复 @@ -82,8 +86,20 @@ class ReplyGenerator: observation_info.clear_unprocessed_messages() - personality_text = f"你的名字是{self.name},{self.personality_info}" - + identity_details_only = self.identity_detail_info + identity_addon = "" + if isinstance(identity_details_only, str): + pronouns = ["你", "我", "他"] + for p in pronouns: + if identity_details_only.startswith(p): + identity_details_only = identity_details_only[len(p):] + break + if identity_details_only.endswith("。"): + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(',, ') + if cleaned_details: + identity_addon = f"并且{cleaned_details}" + persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" # 构建action历史文本 action_history_list = ( conversation_info.done_action[-10:] @@ -114,21 +130,23 @@ class ReplyGenerator: elif action_status == "done": action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n" - prompt = f"""{personality_text}。现在你在参与一场QQ聊天,请根据以下信息生成回复: + prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请根据以下信息生成一条新消息: 当前对话目标:{goals_str} 最近的聊天记录: {chat_history_text} -请根据上述信息,以你的性格特征生成一个自然、得体的回复。回复应该: -1. 符合对话目标,以"你"的角度发言 -2. 体现你的性格特征 -3. 自然流畅,像正常聊天一样,简短 +请根据上述信息,结合聊天记录,发一条消息(可以是回复,补充,深入话题,或追问等等)。该消息应该: +1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) +2. 符合你的性格特征和身份细节 +3. 自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) 4. 适当利用相关知识,但不要生硬引用 +5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容 +**注意:如果聊天记录中最新的消息是你自己发送的,那么你的思路不应该是“回复”,而是应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;** 请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 -请你回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 +可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 @@ -151,10 +169,10 @@ class ReplyGenerator: return content except Exception as e: - logger.error(f"生成回复时出错: {str(e)}") + logger.error(f"生成回复时出错: {e}") return "抱歉,我现在有点混乱,让我重新思考一下..." - async def check_reply(self, reply: str, goal: str, retry_count: int = 0) -> Tuple[bool, str, bool]: + async def check_reply(self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0) -> Tuple[bool, str, bool]: """检查回复是否合适 Args: @@ -165,4 +183,4 @@ class ReplyGenerator: Returns: Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划) """ - return await self.reply_checker.check(reply, goal, retry_count) + return await self.reply_checker.check(reply, goal, chat_history, retry_count) diff --git a/src/plugins/PFC/waiter.py b/src/plugins/PFC/waiter.py index 4d47d500..05702a21 100644 --- a/src/plugins/PFC/waiter.py +++ b/src/plugins/PFC/waiter.py @@ -1,85 +1,74 @@ from src.common.logger import get_module_logger from .chat_observer import ChatObserver from .conversation_info import ConversationInfo -from src.individuality.individuality import Individuality +# from src.individuality.individuality import Individuality # 不再需要 from ...config.config import global_config import time import asyncio logger = get_module_logger("waiter") +# --- 在这里设定你想要的超时时间(秒) --- +# 例如: 120 秒 = 2 分钟 +DESIRED_TIMEOUT_SECONDS = 300 class Waiter: - """快 速 等 待""" + """等待处理类""" def __init__(self, stream_id: str): self.chat_observer = ChatObserver.get_instance(stream_id) - self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=2) self.name = global_config.BOT_NICKNAME - - self.wait_accumulated_time = 0 + # self.wait_accumulated_time = 0 # 不再需要累加计时 async def wait(self, conversation_info: ConversationInfo) -> bool: - """等待 - - Returns: - bool: 是否超时(True表示超时) - """ - # 使用当前时间作为等待开始时间 + """等待用户新消息或超时""" wait_start_time = time.time() - self.chat_observer.waiting_start_time = wait_start_time # 设置等待开始时间 + logger.info(f"进入常规等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...") while True: # 检查是否有新消息 if self.chat_observer.new_message_after(wait_start_time): logger.info("等待结束,收到新消息") - return False + return False # 返回 False 表示不是超时 # 检查是否超时 - if time.time() - wait_start_time > 300: - self.wait_accumulated_time += 300 - - logger.info("等待超过300秒,结束对话") + elapsed_time = time.time() - wait_start_time + if elapsed_time > DESIRED_TIMEOUT_SECONDS: + logger.info(f"等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。") wait_goal = { - "goal": f"你等待了{self.wait_accumulated_time / 60}分钟,思考接下来要做什么", + "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么", "reason": "对方很久没有回复你的消息了", } conversation_info.goal_list.append(wait_goal) - print(f"添加目标: {wait_goal}") + logger.info(f"添加目标: {wait_goal}") + return True # 返回 True 表示超时 - return True - - await asyncio.sleep(1) - logger.info("等待中...") + await asyncio.sleep(5) # 每 5 秒检查一次 + logger.info("等待中...") # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出 async def wait_listening(self, conversation_info: ConversationInfo) -> bool: - """等待倾听 - - Returns: - bool: 是否超时(True表示超时) - """ - # 使用当前时间作为等待开始时间 + """倾听用户发言或超时""" wait_start_time = time.time() - self.chat_observer.waiting_start_time = wait_start_time # 设置等待开始时间 + logger.info(f"进入倾听等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...") while True: # 检查是否有新消息 if self.chat_observer.new_message_after(wait_start_time): - logger.info("等待结束,收到新消息") - return False + logger.info("倾听等待结束,收到新消息") + return False # 返回 False 表示不是超时 # 检查是否超时 - if time.time() - wait_start_time > 300: - self.wait_accumulated_time += 300 - logger.info("等待超过300秒,结束对话") + elapsed_time = time.time() - wait_start_time + if elapsed_time > DESIRED_TIMEOUT_SECONDS: + logger.info(f"倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。") wait_goal = { - "goal": f"你等待了{self.wait_accumulated_time / 60}分钟,思考接下来要做什么", + # 保持 goal 文本一致 + "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么", "reason": "对方话说一半消失了,很久没有回复", } conversation_info.goal_list.append(wait_goal) - print(f"添加目标: {wait_goal}") + logger.info(f"添加目标: {wait_goal}") + return True # 返回 True 表示超时 - return True - - await asyncio.sleep(1) - logger.info("等待中...") + await asyncio.sleep(5) # 每 5 秒检查一次 + logger.info("倾听等待中...") # 同上,可以考虑注释掉 \ No newline at end of file diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index e4e2a2a8..5e26b1e7 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -184,7 +184,7 @@ response_max_sentence_num = 4 # 回复允许的最大句子数 [remote] #发送统计信息,主要是看全球有多少只麦麦 enable = true -[experimental] #实验性功能,不一定完善或者根本不能用 +[experimental] #实验性功能 enable_friend_chat = false # 是否启用好友聊天 pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立 @@ -273,3 +273,25 @@ name = "Qwen/Qwen2.5-32B-Instruct" provider = "SILICONFLOW" pri_in = 1.26 pri_out = 1.26 + +#私聊PFC:需要开启PFC功能,默认三个模型均为硅基流动v3,如果需要支持多人同时私聊或频繁调用,建议把其中的一个或两个换成官方v3或其它模型,以免撞到429 + +[model.llm_PFC_action_planner] +name = "Pro/deepseek-ai/DeepSeek-V3" +provider = "SILICONFLOW" +temp = 0.3 +pri_in = 2 +pri_out = 8 + +[model.llm_PFC_chat] +name = "Pro/deepseek-ai/DeepSeek-V3" +provider = "SILICONFLOW" +temp = 0.3 +pri_in = 2 +pri_out = 8 + +[model.llm_PFC_reply_checker] +name = "Pro/deepseek-ai/DeepSeek-V3" +provider = "SILICONFLOW" +pri_in = 2 +pri_out = 8 \ No newline at end of file From db2d777d99805bf4896a4c3cbdfdfde9c6e9e030 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 01:02:53 +0800 Subject: [PATCH 04/73] Update subheartflow_manager.py --- src/heart_flow/subheartflow_manager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index d5f7ed86..1e64027c 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -290,8 +290,11 @@ class SubHeartflowManager: log_prefix_flow = f"[{stream_name}]" # 只处理 CHAT 状态的子心流 - if sub_hf.chat_state.chat_status != ChatState.CHAT: - continue +# 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 @@ -310,7 +313,7 @@ class SubHeartflowManager: # --- 执行提升 --- # 获取当前实例以检查最新状态 (防御性编程) current_subflow = self.subheartflows.get(flow_id) - if not current_subflow or current_subflow.chat_state.chat_status != ChatState.CHAT: + if not current_subflow: logger.warning(f"{log_prefix_manager} {log_prefix_flow} 尝试提升时状态已改变或实例消失,跳过。") continue From 57ca323efe1936b5f46c1fea8908020269410cd2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 23 Apr 2025 17:03:24 +0000 Subject: [PATCH 05/73] =?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/subheartflow_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 1e64027c..bf473b78 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -290,9 +290,9 @@ class SubHeartflowManager: log_prefix_flow = f"[{stream_name}]" # 只处理 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. + # 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 From b92e0891a10104517d1cbe791077824f28c91181 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 11:16:54 +0800 Subject: [PATCH 06/73] =?UTF-8?q?feat(PFC):=20=E6=9B=B4=E6=96=B0PFC?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=E5=92=8C=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新bot配置模板中的版本号至1.4.1 - 为PFC模型添加注释以增强可读性 - 在logger.py中新增PFC私聊规划的日志格式配置 - 在action_planner.py中应用新的日志格式,并修复变量名错误 --- src/common/logger.py | 17 +++++++++++++++ src/plugins/PFC/action_planner.py | 35 +++++++++++++++++++------------ template/bot_config_template.toml | 5 ++++- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 4347fd97..53043f40 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -419,6 +419,22 @@ WILLING_STYLE_CONFIG = { }, } +PFC_ACTION_PLANNER_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "PFC私聊规划 | " + "{message}" + ), + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | PFC私聊规划 | {message}", + }, + "simple": { + "console_format": "{time:MM-DD HH:mm} | PFC私聊规划 | {message} ", # noqa: E501 + "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | PFC私聊规划 | {message}", + }, +} + EMOJI_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -497,6 +513,7 @@ CONFIRM_STYLE_CONFIG = { # 根据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"] +PFC_ACTION_PLANNER_STYLE_CONFIG = PFC_ACTION_PLANNER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_ACTION_PLANNER_STYLE_CONFIG["advanced"] REMOTE_STYLE_CONFIG = REMOTE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else REMOTE_STYLE_CONFIG["advanced"] BASE_TOOL_STYLE_CONFIG = BASE_TOOL_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else BASE_TOOL_STYLE_CONFIG["advanced"] PERSON_INFO_STYLE_CONFIG = PERSON_INFO_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PERSON_INFO_STYLE_CONFIG["advanced"] diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 9a878398..8659bf6e 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -1,6 +1,6 @@ import time -from typing import Tuple, List, Dict, Any, Optional # 确保导入了必要的类型 -from src.common.logger import get_module_logger +from typing import Tuple +from src.common.logger import get_module_logger, LogConfig, PFC_ACTION_PLANNER_STYLE_CONFIG from ..models.utils_model import LLMRequest from ...config.config import global_config from .chat_observer import ChatObserver @@ -9,7 +9,12 @@ from src.individuality.individuality import Individuality from .observation_info import ObservationInfo from .conversation_info import ConversationInfo -logger = get_module_logger("action_planner") +pfc_action_log_config = LogConfig( + console_format=PFC_ACTION_PLANNER_STYLE_CONFIG["console_format"], + file_format=PFC_ACTION_PLANNER_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("action_planner", config=pfc_action_log_config) # 注意:这个 ActionPlannerInfo 类似乎没有在 ActionPlanner 中使用, # 如果确实没用,可以考虑移除,但暂时保留以防万一。 @@ -81,7 +86,7 @@ class ActionPlanner: timeout_minutes_text = last_goal_text.split(',')[0].replace('你等待了','') timeout_context = f"重要提示:你刚刚因为对方长时间({timeout_minutes_text})没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" except Exception: - timeout_context = f"重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" + timeout_context = "重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" else: logger.debug("Conversation info goal_list is empty or not available for timeout check.") except AttributeError: @@ -108,7 +113,7 @@ class ActionPlanner: reasoning = "没有明确原因" goal = str(goal) if goal is not None else "目标内容缺失" reasoning = str(reasoning) if reasoning is not None else "没有明确原因" - goal_str += f"- 目标:{goal}\n 原因:{reasoning}\n" + goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n" if not goals_str: # 如果循环后 goals_str 仍为空 goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n" except AttributeError: @@ -160,7 +165,7 @@ class ActionPlanner: identity_addon = "" if isinstance(identity_details_only, str): pronouns = ["你", "我", "他"] - original_details = identity_details_only + # original_details = identity_details_only for p in pronouns: if identity_details_only.startswith(p): identity_details_only = identity_details_only[len(p):] @@ -205,10 +210,14 @@ class ActionPlanner: final_reason = action_data.get("final_reason", "") action_time = action_data.get("time", "") elif isinstance(action_data, tuple): - if len(action_data) > 0: action_type = action_data[0] - if len(action_data) > 1: plan_reason = action_data[1] - if len(action_data) > 2: status = action_data[2] - if status == "recall" and len(action_data) > 3: final_reason = action_data[3] + if len(action_data) > 0: + action_type = action_data[0] + if len(action_data) > 1: + plan_reason = action_data[1] + if len(action_data) > 2: + status = action_data[2] + if status == "recall" and len(action_data) > 3: + final_reason = action_data[3] reason_text = f", 失败/取消原因: {final_reason}" if final_reason else "" summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}" @@ -218,13 +227,13 @@ class ActionPlanner: last_action_context += f"- 上次【规划】的行动是: '{action_type}'\n" last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n" if status == "done": - last_action_context += f"- 该行动已【成功执行】。\n" + last_action_context += "- 该行动已【成功执行】。\n" elif status == "recall": - last_action_context += f"- 但该行动最终【未能执行/被取消】。\n" + last_action_context += "- 但该行动最终【未能执行/被取消】。\n" if final_reason: last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n" else: - last_action_context += f"- 【重要】失败/取消原因未明确记录。\n" + last_action_context += "- 【重要】失败/取消原因未明确记录。\n" else: last_action_context += f"- 该行动当前状态: {status}\n" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 5e26b1e7..5f342406 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.4.0" +version = "1.4.1" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -276,6 +276,7 @@ pri_out = 1.26 #私聊PFC:需要开启PFC功能,默认三个模型均为硅基流动v3,如果需要支持多人同时私聊或频繁调用,建议把其中的一个或两个换成官方v3或其它模型,以免撞到429 +#PFC决策模型 [model.llm_PFC_action_planner] name = "Pro/deepseek-ai/DeepSeek-V3" provider = "SILICONFLOW" @@ -283,6 +284,7 @@ temp = 0.3 pri_in = 2 pri_out = 8 +#PFC聊天模型 [model.llm_PFC_chat] name = "Pro/deepseek-ai/DeepSeek-V3" provider = "SILICONFLOW" @@ -290,6 +292,7 @@ temp = 0.3 pri_in = 2 pri_out = 8 +#PFC检查模型 [model.llm_PFC_reply_checker] name = "Pro/deepseek-ai/DeepSeek-V3" provider = "SILICONFLOW" From 49c4d77c97626abda790767a1bf3d577f378662e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Apr 2025 03:17:06 +0000 Subject: [PATCH 07/73] =?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 | 6 +- src/plugins/PFC/action_planner.py | 95 ++++++------ src/plugins/PFC/conversation.py | 239 ++++++++++++++++------------- src/plugins/PFC/pfc.py | 18 +-- src/plugins/PFC/reply_checker.py | 33 ++-- src/plugins/PFC/reply_generator.py | 18 ++- src/plugins/PFC/waiter.py | 22 +-- 7 files changed, 238 insertions(+), 193 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 53043f40..62dbfb05 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -432,7 +432,7 @@ PFC_ACTION_PLANNER_STYLE_CONFIG = { "simple": { "console_format": "{time:MM-DD HH:mm} | PFC私聊规划 | {message} ", # noqa: E501 "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | PFC私聊规划 | {message}", - }, + }, } EMOJI_STYLE_CONFIG = { @@ -513,7 +513,9 @@ CONFIRM_STYLE_CONFIG = { # 根据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"] -PFC_ACTION_PLANNER_STYLE_CONFIG = PFC_ACTION_PLANNER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_ACTION_PLANNER_STYLE_CONFIG["advanced"] +PFC_ACTION_PLANNER_STYLE_CONFIG = ( + PFC_ACTION_PLANNER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PFC_ACTION_PLANNER_STYLE_CONFIG["advanced"] +) REMOTE_STYLE_CONFIG = REMOTE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else REMOTE_STYLE_CONFIG["advanced"] BASE_TOOL_STYLE_CONFIG = BASE_TOOL_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else BASE_TOOL_STYLE_CONFIG["advanced"] PERSON_INFO_STYLE_CONFIG = PERSON_INFO_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else PERSON_INFO_STYLE_CONFIG["advanced"] diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 8659bf6e..b4889dc5 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -16,6 +16,7 @@ pfc_action_log_config = LogConfig( logger = get_module_logger("action_planner", config=pfc_action_log_config) + # 注意:这个 ActionPlannerInfo 类似乎没有在 ActionPlanner 中使用, # 如果确实没用,可以考虑移除,但暂时保留以防万一。 class ActionPlannerInfo: @@ -25,9 +26,11 @@ class ActionPlannerInfo: self.knowledge_list = [] self.memory_list = [] + # ActionPlanner 类定义,顶格 class ActionPlanner: """行动规划器""" + def __init__(self, stream_id: str): self.llm = LLMRequest( model=global_config.llm_PFC_action_planner, @@ -54,18 +57,20 @@ class ActionPlanner: time_since_last_bot_message_info = "" try: bot_id = str(global_config.BOT_QQ) - if hasattr(observation_info, 'chat_history') and observation_info.chat_history: + if hasattr(observation_info, "chat_history") and observation_info.chat_history: for i in range(len(observation_info.chat_history) - 1, -1, -1): msg = observation_info.chat_history[i] if not isinstance(msg, dict): continue - sender_info = msg.get('user_info', {}) - sender_id = str(sender_info.get('user_id')) if isinstance(sender_info, dict) else None - msg_time = msg.get('time') + sender_info = msg.get("user_info", {}) + sender_id = str(sender_info.get("user_id")) if isinstance(sender_info, dict) else None + msg_time = msg.get("time") if sender_id == bot_id and msg_time: time_diff = time.time() - msg_time if time_diff < 60.0: - time_since_last_bot_message_info = f"提示:你上一条成功发送的消息是在 {time_diff:.1f} 秒前。\n" + time_since_last_bot_message_info = ( + f"提示:你上一条成功发送的消息是在 {time_diff:.1f} 秒前。\n" + ) break else: logger.debug("Observation info chat history is empty or not available for bot time check.") @@ -76,31 +81,31 @@ class ActionPlanner: # --- 获取 Bot 上次发言时间信息结束 --- timeout_context = "" - try: # 添加 try-except 以增加健壮性 - if hasattr(conversation_info, 'goal_list') and conversation_info.goal_list: + try: # 添加 try-except 以增加健壮性 + if hasattr(conversation_info, "goal_list") and conversation_info.goal_list: last_goal_tuple = conversation_info.goal_list[-1] if isinstance(last_goal_tuple, tuple) and len(last_goal_tuple) > 0: last_goal_text = last_goal_tuple[0] if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text: try: - timeout_minutes_text = last_goal_text.split(',')[0].replace('你等待了','') + timeout_minutes_text = last_goal_text.split(",")[0].replace("你等待了", "") timeout_context = f"重要提示:你刚刚因为对方长时间({timeout_minutes_text})没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" except Exception: timeout_context = "重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n" else: logger.debug("Conversation info goal_list is empty or not available for timeout check.") except AttributeError: - logger.warning("ConversationInfo object might not have goal_list attribute yet for timeout check.") + logger.warning("ConversationInfo object might not have goal_list attribute yet for timeout check.") except Exception as e: - logger.warning(f"检查超时目标时出错: {e}") + logger.warning(f"检查超时目标时出错: {e}") # 构建提示词 - logger.debug(f"开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}") # 使用 getattr + logger.debug(f"开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}") # 使用 getattr # 构建对话目标 (goals_str) goals_str = "" - try: # 添加 try-except - if hasattr(conversation_info, 'goal_list') and conversation_info.goal_list: + try: # 添加 try-except + if hasattr(conversation_info, "goal_list") and conversation_info.goal_list: for goal_reason in conversation_info.goal_list: if isinstance(goal_reason, tuple) and len(goal_reason) > 0: goal = goal_reason[0] @@ -114,36 +119,36 @@ class ActionPlanner: goal = str(goal) if goal is not None else "目标内容缺失" reasoning = str(reasoning) if reasoning is not None else "没有明确原因" goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n" - if not goals_str: # 如果循环后 goals_str 仍为空 + if not goals_str: # 如果循环后 goals_str 仍为空 goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n" except AttributeError: - logger.warning("ConversationInfo object might not have goal_list attribute yet.") - goals_str = "- 获取对话目标时出错。\n" + logger.warning("ConversationInfo object might not have goal_list attribute yet.") + goals_str = "- 获取对话目标时出错。\n" except Exception as e: - logger.error(f"构建对话目标字符串时出错: {e}") - goals_str = "- 构建对话目标时出错。\n" + logger.error(f"构建对话目标字符串时出错: {e}") + goals_str = "- 构建对话目标时出错。\n" # 获取聊天历史记录 (chat_history_text) chat_history_text = "" try: - if hasattr(observation_info, 'chat_history') and observation_info.chat_history: + 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: + 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" - if not chat_history_text: # 如果历史记录是空列表 - chat_history_text = "还没有聊天记录。\n" + if not chat_history_text: # 如果历史记录是空列表 + chat_history_text = "还没有聊天记录。\n" else: chat_history_text = "还没有聊天记录。\n" - 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: + 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: + 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" @@ -151,7 +156,9 @@ class ActionPlanner: # if hasattr(observation_info, 'clear_unprocessed_messages'): # observation_info.clear_unprocessed_messages() else: - logger.warning("ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing.") + logger.warning( + "ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing." + ) except AttributeError: logger.warning("ObservationInfo object might be missing expected attributes for chat history.") chat_history_text = "获取聊天记录时出错。\n" @@ -159,7 +166,6 @@ class ActionPlanner: logger.error(f"处理聊天记录时发生未知错误: {e}") chat_history_text = "处理聊天记录时出错。\n" - # 构建 Persona 文本 (persona_text) identity_details_only = self.identity_detail_info identity_addon = "" @@ -168,11 +174,11 @@ class ActionPlanner: # original_details = identity_details_only for p in pronouns: if identity_details_only.startswith(p): - identity_details_only = identity_details_only[len(p):] + identity_details_only = identity_details_only[len(p) :] break if identity_details_only.endswith("。"): - identity_details_only = identity_details_only[:-1] - cleaned_details = identity_details_only.strip(',, ') + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(",, ") if cleaned_details: identity_addon = f"并且{cleaned_details}" persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" @@ -182,15 +188,15 @@ class ActionPlanner: last_action_context = "关于你【上一次尝试】的行动:\n" action_history_list = [] - try: # 添加 try-except - if hasattr(conversation_info, 'done_action') and conversation_info.done_action: + try: # 添加 try-except + if hasattr(conversation_info, "done_action") and conversation_info.done_action: action_history_list = conversation_info.done_action[-5:] else: logger.debug("Conversation info done_action is empty or not available.") except AttributeError: - logger.warning("ConversationInfo object might not have done_action attribute yet.") + logger.warning("ConversationInfo object might not have done_action attribute yet.") except Exception as e: - logger.error(f"访问行动历史时出错: {e}") + logger.error(f"访问行动历史时出错: {e}") if not action_history_list: action_history_summary += "- 还没有执行过行动。\n" @@ -210,13 +216,13 @@ class ActionPlanner: final_reason = action_data.get("final_reason", "") action_time = action_data.get("time", "") elif isinstance(action_data, tuple): - if len(action_data) > 0: + if len(action_data) > 0: action_type = action_data[0] - if len(action_data) > 1: + if len(action_data) > 1: plan_reason = action_data[1] - if len(action_data) > 2: + if len(action_data) > 2: status = action_data[2] - if status == "recall" and len(action_data) > 3: + if status == "recall" and len(action_data) > 3: final_reason = action_data[3] reason_text = f", 失败/取消原因: {final_reason}" if final_reason else "" @@ -231,9 +237,9 @@ class ActionPlanner: elif status == "recall": last_action_context += "- 但该行动最终【未能执行/被取消】。\n" if final_reason: - last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n" + last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n" else: - last_action_context += "- 【重要】失败/取消原因未明确记录。\n" + last_action_context += "- 【重要】失败/取消原因未明确记录。\n" else: last_action_context += f"- 该行动当前状态: {status}\n" @@ -279,14 +285,15 @@ end_conversation: 决定结束对话,对方长时间没回复或者当你觉 logger.debug(f"LLM原始返回内容: {content}") success, result = get_items_from_json( - content, "action", "reason", - default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待"} + content, + "action", + "reason", + default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待"}, ) action = result.get("action", "wait") reason = result.get("reason", "LLM未提供原因,默认等待") - # 验证action类型 valid_actions = ["direct_reply", "fetch_knowledge", "wait", "listening", "rethink_goal", "end_conversation"] if action not in valid_actions: @@ -300,4 +307,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 23a55544..d4888ff7 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -75,34 +75,36 @@ class Conversation: raise try: logger.info(f"为 {self.stream_id} 加载初始聊天记录...") - storage = MongoDBMessageStorage() # 创建存储实例 + storage = MongoDBMessageStorage() # 创建存储实例 # 获取当前时间点之前最多 N 条消息 (比如 30 条) # get_messages_before 返回的是按时间正序排列的列表 initial_messages = await storage.get_messages_before( - chat_id=self.stream_id, - time_point=time.time(), - limit=30 # 加载最近20条作为初始上下文,可以调整 + chat_id=self.stream_id, + time_point=time.time(), + limit=30, # 加载最近20条作为初始上下文,可以调整 ) if initial_messages: # 将加载的消息填充到 ObservationInfo 的 chat_history self.observation_info.chat_history = initial_messages self.observation_info.chat_history_count = len(initial_messages) - + # 更新 ObservationInfo 中的时间戳等信息 last_msg = initial_messages[-1] - self.observation_info.last_message_time = last_msg.get('time') + self.observation_info.last_message_time = last_msg.get("time") last_user_info = UserInfo.from_dict(last_msg.get("user_info", {})) self.observation_info.last_message_sender = last_user_info.user_id self.observation_info.last_message_content = last_msg.get("processed_plain_text", "") - + # (可选)可以遍历 initial_messages 来设置 last_bot_speak_time 和 last_user_speak_time # 这里为了简化,只用了最后一条消息的时间,如果需要精确的发言者时间需要遍历 - - logger.info(f"成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}") - + + logger.info( + f"成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}" + ) + # 让 ChatObserver 从加载的最后一条消息之后开始同步 self.chat_observer.last_message_time = self.observation_info.last_message_time - self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录 + self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录 else: logger.info("没有找到初始聊天记录。") @@ -128,49 +130,52 @@ class Conversation: try: # --- 在规划前记录当前新消息数量 --- initial_new_message_count = 0 - if hasattr(self.observation_info, 'new_messages_count'): + if hasattr(self.observation_info, "new_messages_count"): initial_new_message_count = self.observation_info.new_messages_count else: - logger.warning("ObservationInfo missing 'new_messages_count' before planning.") + logger.warning("ObservationInfo missing 'new_messages_count' before planning.") # 使用决策信息来辅助行动规划 - action, reason = await self.action_planner.plan(self.observation_info, self.conversation_info) # 注意:plan 函数内部现在不应再调用 clear_unprocessed_messages + action, reason = await self.action_planner.plan( + self.observation_info, self.conversation_info + ) # 注意:plan 函数内部现在不应再调用 clear_unprocessed_messages # --- 规划后检查是否有 *更多* 新消息到达 --- current_new_message_count = 0 - if hasattr(self.observation_info, 'new_messages_count'): - current_new_message_count = self.observation_info.new_messages_count + if hasattr(self.observation_info, "new_messages_count"): + current_new_message_count = self.observation_info.new_messages_count else: - logger.warning("ObservationInfo missing 'new_messages_count' after planning.") + logger.warning("ObservationInfo missing 'new_messages_count' after planning.") if current_new_message_count > initial_new_message_count: # 只有当规划期间消息数量 *增加* 了,才认为需要重新规划 - logger.info(f"规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划") - await asyncio.sleep(0.1) # 短暂延时 - continue # 跳过本次行动,重新规划 + logger.info( + f"规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划" + ) + await asyncio.sleep(0.1) # 短暂延时 + continue # 跳过本次行动,重新规划 # --- 如果没有在规划期间收到更多新消息,则准备执行行动 --- # --- 清理未处理消息:移到这里,在执行动作前 --- # 只有当确实有新消息被 planner 看到,并且 action 是要处理它们的时候才清理 if initial_new_message_count > 0 and action == "direct_reply": - 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() - # 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个) - if hasattr(self.observation_info, 'new_messages_count'): - self.observation_info.new_messages_count = 0 + 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() + # 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个) + if hasattr(self.observation_info, "new_messages_count"): + self.observation_info.new_messages_count = 0 else: - logger.error("无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!") - # 这里可能需要考虑是否继续执行 action,或者抛出错误 - + logger.error("无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!") + # 这里可能需要考虑是否继续执行 action,或者抛出错误 # --- 执行行动 --- await self._handle_action(action, reason, self.observation_info, self.conversation_info) goal_ended = False - if hasattr(self.conversation_info, 'goal_list') and self.conversation_info.goal_list: + if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list: for goal in self.conversation_info.goal_list: if isinstance(goal, tuple) and len(goal) > 0 and goal[0] == "结束对话": goal_ended = True @@ -185,15 +190,15 @@ class Conversation: # break # 可以选择在这里直接跳出循环 except Exception as loop_err: - logger.error(f"PFC主循环出错: {loop_err}") - logger.error(traceback.format_exc()) - # 发生严重错误时可以考虑停止,或者至少等待一下再继续 - await asyncio.sleep(1) # 发生错误时等待1秒 - #添加短暂的异步睡眠 - if self.should_continue: # 只有在还需要继续循环时才 sleep - await asyncio.sleep(0.1) # 等待 0.1 秒,给其他任务执行时间 + logger.error(f"PFC主循环出错: {loop_err}") + logger.error(traceback.format_exc()) + # 发生严重错误时可以考虑停止,或者至少等待一下再继续 + await asyncio.sleep(1) # 发生错误时等待1秒 + # 添加短暂的异步睡眠 + if self.should_continue: # 只有在还需要继续循环时才 sleep + await asyncio.sleep(0.1) # 等待 0.1 秒,给其他任务执行时间 - logger.info(f"PFC 循环结束 for stream_id: {self.stream_id}") # 添加日志表明循环正常结束 + logger.info(f"PFC 循环结束 for stream_id: {self.stream_id}") # 添加日志表明循环正常结束 def _check_new_messages_after_planning(self): """检查在规划后是否有新消息""" @@ -226,16 +231,16 @@ class Conversation: self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo ): """处理规划的行动""" - + logger.info(f"执行行动: {action}, 原因: {reason}") # 记录action历史,先设置为start,完成后再设置为done (这个 update 移到后面执行成功后再做) current_action_record = { "action": action, - "plan_reason": reason, #使用 plan_reason 存储规划原因 - "status": "start", # 初始状态为 start + "plan_reason": reason, # 使用 plan_reason 存储规划原因 + "status": "start", # 初始状态为 start "time": datetime.datetime.now().strftime("%H:%M:%S"), - "final_reason": None + "final_reason": None, } conversation_info.done_action.append(current_action_record) # 获取刚刚添加记录的索引,方便后面更新状态 @@ -244,33 +249,33 @@ class Conversation: # --- 根据不同的 action 执行 --- if action == "direct_reply": # --- 这个 if 块内部的所有代码都需要正确缩进 --- - self.waiter.wait_accumulated_time = 0 # 重置等待时间 + self.waiter.wait_accumulated_time = 0 # 重置等待时间 self.state = ConversationState.GENERATING # 生成回复 self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info) - logger.info(f"生成回复: {self.generated_reply}") # 使用 logger + logger.info(f"生成回复: {self.generated_reply}") # 使用 logger # --- 调用 ReplyChecker 检查回复 --- - is_suitable = False # 先假定不合适,检查通过再改为 True - check_reason = "检查未执行" # 用不同的变量名存储检查原因 + is_suitable = False # 先假定不合适,检查通过再改为 True + check_reason = "检查未执行" # 用不同的变量名存储检查原因 need_replan = False try: # 尝试获取当前主要目标 current_goal_str = conversation_info.goal_list[0][0] if conversation_info.goal_list else "" - + # 调用检查器 is_suitable, check_reason, need_replan = await self.reply_generator.check_reply( reply=self.generated_reply, goal=current_goal_str, - chat_history=observation_info.chat_history, # 传入最新的历史记录! - retry_count=0 + chat_history=observation_info.chat_history, # 传入最新的历史记录! + retry_count=0, ) logger.info(f"回复检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}") except Exception as check_err: logger.error(f"调用 ReplyChecker 时出错: {check_err}") - check_reason = f"检查过程出错: {check_err}" # 记录错误原因 + check_reason = f"检查过程出错: {check_err}" # 记录错误原因 # is_suitable 保持 False # --- 处理检查结果 --- @@ -280,38 +285,44 @@ class Conversation: if self._check_new_messages_after_planning(): logger.info("检查到新消息,取消发送已生成的回复,重新规划行动") # 更新 action 状态为 recall - conversation_info.done_action[action_index].update({ - "status": "recall", - "reason": f"有新消息,取消发送: {self.generated_reply}", # 更新原因 - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) - return None # 退出 _handle_action + conversation_info.done_action[action_index].update( + { + "status": "recall", + "reason": f"有新消息,取消发送: {self.generated_reply}", # 更新原因 + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) + return None # 退出 _handle_action # 发送回复 - await self._send_reply() # 这个函数内部会处理自己的错误 + await self._send_reply() # 这个函数内部会处理自己的错误 # 更新 action 历史状态为 done - conversation_info.done_action[action_index].update({ - "status": "done", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) + conversation_info.done_action[action_index].update( + { + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) else: # 回复不合适 logger.warning(f"生成的回复被 ReplyChecker 拒绝: '{self.generated_reply}'. 原因: {check_reason}") # 更新 action 状态为 recall (因为没执行发送) - conversation_info.done_action[action_index].update({ - "status": "recall", - "final_reason": check_reason, - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) + conversation_info.done_action[action_index].update( + { + "status": "recall", + "final_reason": check_reason, + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) # 如果检查器建议重新规划 if need_replan: logger.info("ReplyChecker 建议重新规划目标。") # 可选:在此处清空目标列表以强制重新规划 - # conversation_info.goal_list = [] - + # conversation_info.goal_list = [] + # 注意:不发送消息,也不执行后面的代码 # --- 之前重复的代码块已被删除 --- @@ -323,22 +334,26 @@ class Conversation: topic = "TODO:关键词" logger.info(f"假装获取到知识{knowledge},关键词是: {topic}") if knowledge: - pass # 简单处理 + pass # 简单处理 # 标记 action 为 done - conversation_info.done_action[action_index].update({ - "status": "done", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) + conversation_info.done_action[action_index].update( + { + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) elif action == "rethink_goal": self.waiter.wait_accumulated_time = 0 self.state = ConversationState.RETHINKING await self.goal_analyzer.analyze_goal(conversation_info, observation_info) # 标记 action 为 done - conversation_info.done_action[action_index].update({ - "status": "done", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) + conversation_info.done_action[action_index].update( + { + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) elif action == "listening": self.state = ConversationState.LISTENING @@ -347,31 +362,36 @@ class Conversation: # listening 和 wait 通常在完成后不需要标记为 done,因为它们是持续状态, # 但如果需要记录,可以在 waiter 返回后标记。目前逻辑是 waiter 返回后主循环继续。 # 为了统一,可以暂时在这里也标记一下(或者都不标记) - conversation_info.done_action[action_index].update({ - "status": "done", # 或 "completed" - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) - + conversation_info.done_action[action_index].update( + { + "status": "done", # 或 "completed" + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) elif action == "end_conversation": - self.should_continue = False # 设置循环停止标志 + self.should_continue = False # 设置循环停止标志 logger.info("决定结束对话...") # 标记 action 为 done - conversation_info.done_action[action_index].update({ - "status": "done", - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) + conversation_info.done_action[action_index].update( + { + "status": "done", + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) # 这里不需要 return,主循环会在下一轮检查 should_continue - else: # 对应 'wait' 动作 + else: # 对应 'wait' 动作 self.state = ConversationState.WAITING logger.info("等待更多信息...") await self.waiter.wait(self.conversation_info) # 同 listening,可以考虑是否标记状态 - conversation_info.done_action[action_index].update({ - "status": "done", # 或 "completed" - "time": datetime.datetime.now().strftime("%H:%M:%S"), - }) + conversation_info.done_action[action_index].update( + { + "status": "done", # 或 "completed" + "time": datetime.datetime.now().strftime("%H:%M:%S"), + } + ) async def _send_timeout_message(self): """发送超时结束消息""" @@ -395,35 +415,35 @@ class Conversation: try: # 外层 try: 捕获发送消息和后续处理中的主要错误 - current_time = time.time() # 获取当前时间戳 - reply_content = self.generated_reply # 获取要发送的内容 + current_time = time.time() # 获取当前时间戳 + reply_content = self.generated_reply # 获取要发送的内容 # 发送消息 await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content) - logger.info(f"消息已发送: {reply_content}") # 可以在发送后加个日志确认 + logger.info(f"消息已发送: {reply_content}") # 可以在发送后加个日志确认 # --- 添加的立即更新状态逻辑开始 --- try: # 内层 try: 专门捕获手动更新状态时可能出现的错误 # 创建一个代表刚刚发送的消息的字典 bot_message_info = { - "message_id": f"bot_sent_{current_time}", # 创建一个简单的唯一ID + "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}", # 构造一个简单的详细文本, 时间戳取整 + "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 # 更新最后消息时间 + 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:实例不存在") @@ -432,15 +452,14 @@ class Conversation: logger.error(f"手动更新 ObservationInfo 时出错: {update_err}") # --- 添加的立即更新状态逻辑结束 --- - # 原有的触发更新和等待代码 self.chat_observer.trigger_update() if not await self.chat_observer.wait_for_update(): logger.warning("等待 ChatObserver 更新完成超时") - self.state = ConversationState.ANALYZING # 更新对话状态 + self.state = ConversationState.ANALYZING # 更新对话状态 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/pfc.py b/src/plugins/PFC/pfc.py index 08d4fabf..873d1467 100644 --- a/src/plugins/PFC/pfc.py +++ b/src/plugins/PFC/pfc.py @@ -100,15 +100,15 @@ class GoalAnalyzer: pronouns = ["你", "我", "他"] for p in pronouns: if identity_details_only.startswith(p): - identity_details_only = identity_details_only[len(p):] + identity_details_only = identity_details_only[len(p) :] break if identity_details_only.endswith("。"): - identity_details_only = identity_details_only[:-1] - cleaned_details = identity_details_only.strip(',, ') + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(",, ") if cleaned_details: identity_addon = f"并且{cleaned_details}" - persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" + persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" # 构建action历史文本 action_history_list = conversation_info.done_action action_history_text = "你之前做的事情是:" @@ -263,18 +263,18 @@ class GoalAnalyzer: pronouns = ["你", "我", "他"] for p in pronouns: if identity_details_only.startswith(p): - identity_details_only = identity_details_only[len(p):] + identity_details_only = identity_details_only[len(p) :] break if identity_details_only.endswith("。"): - identity_details_only = identity_details_only[:-1] - cleaned_details = identity_details_only.strip(',, ') + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(",, ") if cleaned_details: identity_addon = f"并且{cleaned_details}" persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" - # ===> Persona 文本构建结束 <=== + # ===> Persona 文本构建结束 <=== - # --- 修改 Prompt 字符串,使用 persona_text --- + # --- 修改 Prompt 字符串,使用 persona_text --- prompt = f"""{persona_text}。现在你在参与一场QQ聊天, 当前对话目标:{goal} 产生该对话目标的原因:{reasoning} diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 72489251..f4e1c990 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -21,7 +21,9 @@ class ReplyChecker: self.chat_observer = ChatObserver.get_instance(stream_id) self.max_retries = 2 # 最大重试次数 - async def check(self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0) -> Tuple[bool, str, bool]: + async def check( + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0 + ) -> Tuple[bool, str, bool]: """检查生成的回复是否合适 Args: @@ -40,19 +42,24 @@ class ReplyChecker: bot_messages = [] for msg in reversed(chat_history): user_info = UserInfo.from_dict(msg.get("user_info", {})) - if str(user_info.user_id) == str(global_config.BOT_QQ): # 确保比较的是字符串 - bot_messages.append(msg.get('processed_plain_text', '')) - if len(bot_messages) >= 2: # 只和最近的两条比较 + if str(user_info.user_id) == str(global_config.BOT_QQ): # 确保比较的是字符串 + bot_messages.append(msg.get("processed_plain_text", "")) + if len(bot_messages) >= 2: # 只和最近的两条比较 break # 进行比较 if bot_messages: # 可以用简单比较,或者更复杂的相似度库 (如 difflib) # 简单比较:是否完全相同 - if reply == bot_messages[0]: # 和最近一条完全一样 + if reply == bot_messages[0]: # 和最近一条完全一样 logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'") - return False, "回复内容与你上一条发言完全相同,请修改,可以选择深入话题或寻找其它话题或等待", False # 不合适,无需重新规划 + return ( + False, + "回复内容与你上一条发言完全相同,请修改,可以选择深入话题或寻找其它话题或等待", + False, + ) # 不合适,无需重新规划 # 2. 相似度检查 (如果精确匹配未通过) - import difflib # 导入 difflib 库 + import difflib # 导入 difflib 库 + # 计算编辑距离相似度,ratio() 返回 0 到 1 之间的浮点数 similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio() logger.debug(f"ReplyChecker - 相似度: {similarity_ratio:.2f}") @@ -60,11 +67,17 @@ class ReplyChecker: # 设置一个相似度阈值 similarity_threshold = 0.9 if similarity_ratio > similarity_threshold: - logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'") - return False, f"拒绝发送:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),请修改,可以选择深入话题或寻找其它话题或等待。", False + logger.warning( + f"ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'" + ) + return ( + False, + f"拒绝发送:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),请修改,可以选择深入话题或寻找其它话题或等待。", + False, + ) except Exception as self_check_err: - logger.error(f"检查自身重复发言时出错: {self_check_err}") + logger.error(f"检查自身重复发言时出错: {self_check_err}") for msg in chat_history[-20:]: time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S") diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 5ef58e27..15cd7dee 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -92,14 +92,14 @@ class ReplyGenerator: pronouns = ["你", "我", "他"] for p in pronouns: if identity_details_only.startswith(p): - identity_details_only = identity_details_only[len(p):] - break + identity_details_only = identity_details_only[len(p) :] + break if identity_details_only.endswith("。"): - identity_details_only = identity_details_only[:-1] - cleaned_details = identity_details_only.strip(',, ') - if cleaned_details: - identity_addon = f"并且{cleaned_details}" - persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" + identity_details_only = identity_details_only[:-1] + cleaned_details = identity_details_only.strip(",, ") + if cleaned_details: + identity_addon = f"并且{cleaned_details}" + persona_text = f"你的名字是{self.name},{self.personality_info}{identity_addon}。" # 构建action历史文本 action_history_list = ( conversation_info.done_action[-10:] @@ -172,7 +172,9 @@ class ReplyGenerator: logger.error(f"生成回复时出错: {e}") return "抱歉,我现在有点混乱,让我重新思考一下..." - async def check_reply(self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0) -> Tuple[bool, str, bool]: + async def check_reply( + self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0 + ) -> Tuple[bool, str, bool]: """检查回复是否合适 Args: diff --git a/src/plugins/PFC/waiter.py b/src/plugins/PFC/waiter.py index 05702a21..eaf8a768 100644 --- a/src/plugins/PFC/waiter.py +++ b/src/plugins/PFC/waiter.py @@ -1,6 +1,7 @@ from src.common.logger import get_module_logger from .chat_observer import ChatObserver from .conversation_info import ConversationInfo + # from src.individuality.individuality import Individuality # 不再需要 from ...config.config import global_config import time @@ -10,7 +11,8 @@ logger = get_module_logger("waiter") # --- 在这里设定你想要的超时时间(秒) --- # 例如: 120 秒 = 2 分钟 -DESIRED_TIMEOUT_SECONDS = 300 +DESIRED_TIMEOUT_SECONDS = 300 + class Waiter: """等待处理类""" @@ -29,7 +31,7 @@ class Waiter: # 检查是否有新消息 if self.chat_observer.new_message_after(wait_start_time): logger.info("等待结束,收到新消息") - return False # 返回 False 表示不是超时 + return False # 返回 False 表示不是超时 # 检查是否超时 elapsed_time = time.time() - wait_start_time @@ -41,10 +43,10 @@ class Waiter: } conversation_info.goal_list.append(wait_goal) logger.info(f"添加目标: {wait_goal}") - return True # 返回 True 表示超时 + return True # 返回 True 表示超时 - await asyncio.sleep(5) # 每 5 秒检查一次 - logger.info("等待中...") # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出 + await asyncio.sleep(5) # 每 5 秒检查一次 + logger.info("等待中...") # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出 async def wait_listening(self, conversation_info: ConversationInfo) -> bool: """倾听用户发言或超时""" @@ -55,7 +57,7 @@ class Waiter: # 检查是否有新消息 if self.chat_observer.new_message_after(wait_start_time): logger.info("倾听等待结束,收到新消息") - return False # 返回 False 表示不是超时 + return False # 返回 False 表示不是超时 # 检查是否超时 elapsed_time = time.time() - wait_start_time @@ -63,12 +65,12 @@ class Waiter: logger.info(f"倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。") wait_goal = { # 保持 goal 文本一致 - "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么", + "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么", "reason": "对方话说一半消失了,很久没有回复", } conversation_info.goal_list.append(wait_goal) logger.info(f"添加目标: {wait_goal}") - return True # 返回 True 表示超时 + return True # 返回 True 表示超时 - await asyncio.sleep(5) # 每 5 秒检查一次 - logger.info("倾听等待中...") # 同上,可以考虑注释掉 \ No newline at end of file + await asyncio.sleep(5) # 每 5 秒检查一次 + logger.info("倾听等待中...") # 同上,可以考虑注释掉 From 7a69b046addeebc2432b631eb1a2ccce759166c7 Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 24 Apr 2025 11:19:57 +0800 Subject: [PATCH 08/73] 3.11 --- src/plugins/PFC/action_planner.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index 9a878398..b270a8d4 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -232,7 +232,8 @@ class ActionPlanner: prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请根据以下【所有信息】审慎决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识: 【当前对话目标】 -{goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。\n"} +{goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。"} + 【最近行动历史概要】 {action_history_summary} @@ -241,7 +242,8 @@ class ActionPlanner: 【时间和超时提示】 {time_since_last_bot_message_info}{timeout_context} 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) -{chat_history_text if chat_history_text.strip() else "还没有聊天记录。\n"} +{chat_history_text if chat_history_text.strip() else "还没有聊天记录。"} + --- 行动决策指南 --- 1. **仔细分析【上一次行动的详细情况和结果】**。如果上次行动是 direct_reply 且因“内容与你上一条发言完全相同”或“高度相似”而被取消(status: recall),那么【绝对不要】立即再次规划 direct_reply。在这种特定情况下,你应该优先考虑 wait (等待用户的新回应) 或 rethink_goal (如果对话似乎因此卡住了)。 2. 结合【当前对话目标】和【最近的对话记录】来判断是否需要回应、回应什么。如果【最近的对话记录】中有新的用户消息,通常需要 direct_reply。如果上次行动成功,或者上次失败的原因不是重复,可以根据对话内容考虑 direct_reply。 From 12de69fb3c9ab5458d8611265a6d3bfde24e270a 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 11:21:07 +0800 Subject: [PATCH 09/73] =?UTF-8?q?feat(logger):=20=E4=B8=BA=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E6=B5=81=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=97=A5=E5=BF=97=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 `CHAT_STREAM_STYLE_CONFIG` 配置,用于定义聊天流模块的日志格式,包括控制台和文件输出的样式。同时更新 `chat_stream.py` 以使用该配置,确保日志输出风格一致且易于识别。 --- src/common/logger.py | 19 +++++++++++++++++++ src/plugins/chat/chat_stream.py | 9 +++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index 62dbfb05..ec2c887a 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -349,6 +349,24 @@ BASE_TOOL_STYLE_CONFIG = { }, } +CHAT_STREAM_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}", + }, +} + PERSON_INFO_STYLE_CONFIG = { "advanced": { "console_format": ( @@ -526,6 +544,7 @@ BACKGROUND_TASKS_STYLE_CONFIG = ( BACKGROUND_TASKS_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else BACKGROUND_TASKS_STYLE_CONFIG["advanced"] ) MEMORY_STYLE_CONFIG = MEMORY_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MEMORY_STYLE_CONFIG["advanced"] +CHAT_STREAM_STYLE_CONFIG = CHAT_STREAM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CHAT_STREAM_STYLE_CONFIG["advanced"] TOPIC_STYLE_CONFIG = TOPIC_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOPIC_STYLE_CONFIG["advanced"] SENDER_STYLE_CONFIG = SENDER_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else SENDER_STYLE_CONFIG["advanced"] LLM_STYLE_CONFIG = LLM_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else LLM_STYLE_CONFIG["advanced"] diff --git a/src/plugins/chat/chat_stream.py b/src/plugins/chat/chat_stream.py index ebeaa7c0..e50dc3ec 100644 --- a/src/plugins/chat/chat_stream.py +++ b/src/plugins/chat/chat_stream.py @@ -8,9 +8,14 @@ from typing import Dict, Optional from ...common.database import db from ..message.message_base import GroupInfo, UserInfo -from src.common.logger import get_module_logger +from src.common.logger import get_module_logger, LogConfig, CHAT_STREAM_STYLE_CONFIG -logger = get_module_logger("chat_stream") +chat_stream_log_config = LogConfig( + console_format=CHAT_STREAM_STYLE_CONFIG["console_format"], + file_format=CHAT_STREAM_STYLE_CONFIG["file_format"], +) + +logger = get_module_logger("chat_stream", config=chat_stream_log_config) class ChatStream: From b783d26a785065de4166464588ba6e42ab260c6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Apr 2025 03:21:22 +0000 Subject: [PATCH 10/73] =?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 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/logger.py b/src/common/logger.py index ec2c887a..8a5b7ffc 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -356,12 +356,12 @@ CHAT_STREAM_STYLE_CONFIG = { "{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}" ), "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 聊天流 | {message}", }, From 72857d21426bd511a46c03a94d5930a7b838739f Mon Sep 17 00:00:00 2001 From: Bakadax Date: Thu, 24 Apr 2025 11:28:44 +0800 Subject: [PATCH 11/73] =?UTF-8?q?=E4=BF=AE=E5=A4=8DNoneType=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/plugins/chat/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index fdb2576a..4517138c 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -82,7 +82,7 @@ class ChatBot: logger.debug(f"用户{userinfo.user_id}被禁止回复") return - if groupinfo.group_id not in global_config.talk_allowed_groups: + if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups: logger.debug(f"群{groupinfo.group_id}被禁止回复") return From cefd8aa5b7226eba68c29c8668de0268b39790f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Thu, 24 Apr 2025 13:52:48 +0900 Subject: [PATCH 12/73] =?UTF-8?q?add=20=F0=9F=8D=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | Bin 748 -> 810 bytes src/api/graphql/__init__.py | 22 ++++++++++++++++++++++ src/api/graphql/schema.py | 1 + 3 files changed, 23 insertions(+) create mode 100644 src/api/graphql/__init__.py create mode 100644 src/api/graphql/schema.py diff --git a/requirements.txt b/requirements.txt index 65d547deb6a0bab7935b94f3666d0ae270f2abbb..55c2b3ad1da2952a482d0fd32ae355ddf65e0d90 100644 GIT binary patch delta 70 zcmaFEx{7VX8zwtm1}=tTh7yJ%hD3&Ph9rhmAghR>l0lat9V}MBkik&Mki!tokOou% QQklq5z>vuh%K*{|096MLy#N3J delta 7 OcmZ3*_J(!C8zuk^YXd+4 diff --git a/src/api/graphql/__init__.py b/src/api/graphql/__init__.py new file mode 100644 index 00000000..b0efa7f9 --- /dev/null +++ b/src/api/graphql/__init__.py @@ -0,0 +1,22 @@ +import strawberry + +from fastapi import FastAPI +from strawberry.fastapi import GraphQLRouter + +from src.common.server import global_server + + +@strawberry.type +class Query: + @strawberry.field + def hello(self) -> str: + return "Hello World" + + +schema = strawberry.Schema(Query) + +graphql_app = GraphQLRouter(schema) + +fast_api_app: FastAPI = global_server.get_app() + +fast_api_app.include_router(graphql_app, prefix="/graphql") diff --git a/src/api/graphql/schema.py b/src/api/graphql/schema.py new file mode 100644 index 00000000..2ae28399 --- /dev/null +++ b/src/api/graphql/schema.py @@ -0,0 +1 @@ +pass From 121cc6e2ca48fbdbf184a2870aed849bd22e92cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=A5=E6=B2=B3=E6=99=B4?= Date: Thu, 24 Apr 2025 13:53:00 +0900 Subject: [PATCH 13/73] LF --- requirements.txt | Bin 810 -> 732 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 55c2b3ad1da2952a482d0fd32ae355ddf65e0d90..06068d888e7575b1d9352a71d9b73111ebaa78b2 100644 GIT binary patch literal 732 zcmZvaO>ToQ5QS&$yJXc9^aho>tf~k?LP-FFF-d#*w%;2!B8$ob&+nUg;Zqs|GuBAFcK}XYwLQy;nG*1@EKF8q5C;x)pA_bfA zchtP#tH^3W3f{k1iBERi8tX_#hPyak?9(kPv6FovM{e;^rGQ)^H~iI&A}X>HQJS4} z`7{qzFBb6}@#tSHnfZ#@&Z9N2)ie5k{Ezb#{+8SQ&29p%;0}&jxF%3F10B z6VMKHQ}``JWIPl9$mu<{ysrAWOJcvtd*^%CwFBAxGmHi#y^waQ<1L?Dzandwl274A j)tXAt-$)&3p6XP4cEhKAO7Pu1u$)1B^R3!8=$*V@M_h~y literal 810 zcmZvaQBK1!5JdMIiKFz36L140J`t+YrVVb>ICb2XULJU}u_+)FCGn1TW_QQ_{CKn{ zd$zT`*~VHsSC7bttBystyBP zEqRqQ5qGSlcO)m!!Jgd)oer*WaWt*8vg@xLSCxz}ntgBY1v`;H+SQ}TN^`2Fltxwq z4r{VGzOqg{)n75SFTd1F&5{QLd-_%^a}Jt#;S4!?a#sIu#&6G6=f)4rs`DFBtm+5X zt{x=R<&QkerF(h00|*YG{}L|$fh Date: Thu, 24 Apr 2025 14:18:41 +0800 Subject: [PATCH 14/73] =?UTF-8?q?feat:=E5=90=88=E5=B9=B6=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=A8=A1=E5=9E=8B=E5=92=8C=E5=BF=83=E6=B5=81?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- llm_tool_benchmark_results.json | 71 +++++ src/config/config.py | 1 + src/do_tool/tool_can_use/base_tool.py | 3 +- .../tool_can_use/compare_numbers_tool.py | 2 +- src/do_tool/tool_can_use/get_knowledge.py | 4 +- src/do_tool/tool_can_use/get_memory.py | 4 +- src/do_tool/tool_can_use/get_time_date.py | 2 +- .../tool_can_use/lpmm_get_knowledge.py | 4 +- src/do_tool/tool_use.py | 12 +- src/heart_flow/mai_state_manager.py | 3 +- src/heart_flow/observation.py | 20 +- src/heart_flow/sub_heartflow.py | 194 +++++++++--- src/plugins/chat/bot.py | 8 +- src/plugins/chat/utils.py | 4 +- src/plugins/heartFC_chat/heartFC_chat.py | 214 +++++-------- src/plugins/heartFC_chat/heartFC_generator.py | 106 +------ .../heartFC_chat/heartflow_prompt_builder.py | 9 +- .../heartFC_chat/normal_chat_generator.py | 1 + src/plugins/models/utils_model.py | 54 +++- src/plugins/utils/chat_message_builder.py | 2 +- src/plugins/utils/json_utils.py | 297 ++++++++++++++++++ tool_call_benchmark.py | 289 +++++++++++++++++ 22 files changed, 973 insertions(+), 331 deletions(-) create mode 100644 llm_tool_benchmark_results.json create mode 100644 src/plugins/utils/json_utils.py create mode 100644 tool_call_benchmark.py diff --git a/llm_tool_benchmark_results.json b/llm_tool_benchmark_results.json new file mode 100644 index 00000000..e6be2a7d --- /dev/null +++ b/llm_tool_benchmark_results.json @@ -0,0 +1,71 @@ +{ + "测试时间": "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/src/config/config.py b/src/config/config.py index ba9416d5..db2fd89d 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -286,6 +286,7 @@ class BotConfig: 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: {}) api_urls: Dict[str, str] = field(default_factory=lambda: {}) diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py index 7a89369f..af12adf2 100644 --- a/src/do_tool/tool_can_use/base_tool.py +++ b/src/do_tool/tool_can_use/base_tool.py @@ -41,12 +41,11 @@ class BaseTool: "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters}, } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: """执行工具函数 Args: function_args: 工具调用参数 - message_txt: 原始消息文本 Returns: Dict: 工具执行结果 diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py index 48cee515..1fbd812a 100644 --- a/src/do_tool/tool_can_use/compare_numbers_tool.py +++ b/src/do_tool/tool_can_use/compare_numbers_tool.py @@ -19,7 +19,7 @@ class CompareNumbersTool(BaseTool): "required": ["num1", "num2"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: """执行比较两个数的大小 Args: diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py index 0ccac52c..600afd36 100644 --- a/src/do_tool/tool_can_use/get_knowledge.py +++ b/src/do_tool/tool_can_use/get_knowledge.py @@ -21,7 +21,7 @@ class SearchKnowledgeTool(BaseTool): "required": ["query"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: """执行知识库搜索 Args: @@ -32,7 +32,7 @@ class SearchKnowledgeTool(BaseTool): Dict: 工具执行结果 """ try: - query = function_args.get("query", message_txt) + query = function_args.get("query") threshold = function_args.get("threshold", 0.4) # 调用知识库搜索 diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py index 28346d46..98a4e85e 100644 --- a/src/do_tool/tool_can_use/get_memory.py +++ b/src/do_tool/tool_can_use/get_memory.py @@ -20,7 +20,7 @@ class GetMemoryTool(BaseTool): "required": ["topic"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: """执行记忆获取 Args: @@ -31,7 +31,7 @@ class GetMemoryTool(BaseTool): Dict: 工具执行结果 """ try: - topic = function_args.get("topic", message_txt) + topic = function_args.get("topic") max_memory_num = function_args.get("max_memory_num", 2) # 将主题字符串转换为列表 diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py index c3c9c837..df6067bf 100644 --- a/src/do_tool/tool_can_use/get_time_date.py +++ b/src/do_tool/tool_can_use/get_time_date.py @@ -17,7 +17,7 @@ class GetCurrentDateTimeTool(BaseTool): "required": [], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: """执行获取当前时间、日期、年份和星期 Args: diff --git a/src/do_tool/tool_can_use/lpmm_get_knowledge.py b/src/do_tool/tool_can_use/lpmm_get_knowledge.py index 601d6083..7541d48a 100644 --- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py +++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py @@ -24,7 +24,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool): "required": ["query"], } - async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]: """执行知识库搜索 Args: @@ -35,7 +35,7 @@ class SearchKnowledgeFromLPMMTool(BaseTool): Dict: 工具执行结果 """ try: - query = function_args.get("query", message_txt) + query = function_args.get("query") # threshold = function_args.get("threshold", 0.4) # 调用知识库搜索 diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py index 019294ec..1f625a58 100644 --- a/src/do_tool/tool_use.py +++ b/src/do_tool/tool_use.py @@ -50,8 +50,8 @@ class ToolUser: prompt += message_txt # prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n" prompt += f"注意你就是{bot_name},{bot_name}是你的名字。根据之前的聊天记录补充问题信息,搜索时避开你的名字。\n" - prompt += "必须调用 'lpmm_get_knowledge' 工具来获取知识。\n" - prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。" + # prompt += "必须调用 'lpmm_get_knowledge' 工具来获取知识。\n" + prompt += "你现在需要对群里的聊天内容进行回复,请你思考应该使用什么工具,然后选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。" prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt) prompt = parse_text_timestamps(prompt, mode="lite") @@ -68,7 +68,7 @@ class ToolUser: return get_all_tool_definitions() @staticmethod - async def _execute_tool_call(tool_call, message_txt: str): + async def _execute_tool_call(tool_call): """执行特定的工具调用 Args: @@ -89,7 +89,7 @@ class ToolUser: return None # 执行工具 - result = await tool_instance.execute(function_args, message_txt) + result = await tool_instance.execute(function_args) if result: # 直接使用 function_name 作为 tool_type tool_type = function_name @@ -159,13 +159,13 @@ class ToolUser: tool_calls_str = "" for tool_call in tool_calls: tool_calls_str += f"{tool_call['function']['name']}\n" - logger.info(f"根据:\n{prompt}\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}") + logger.info(f"根据:\n{prompt}\n\n内容:{content}\n\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}") tool_results = [] structured_info = {} # 动态生成键 # 执行所有工具调用 for tool_call in tool_calls: - result = await self._execute_tool_call(tool_call, message_txt) + result = await self._execute_tool_call(tool_call) if result: tool_results.append(result) # 使用工具名称作为键 diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 740b715f..9a39b5fe 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -13,7 +13,8 @@ mai_state_config = LogConfig( logger = get_module_logger("mai_state_manager", config=mai_state_config) -enable_unlimited_hfc_chat = False +enable_unlimited_hfc_chat = True +# enable_unlimited_hfc_chat = False class MaiState(enum.Enum): diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index ba4d23de..0f61f608 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -78,29 +78,33 @@ class ChattingObservation(Observation): return self.talking_message_str async def observe(self): + # 自上一次观察的新消息 new_messages_list = get_raw_msg_by_timestamp_with_chat( chat_id=self.chat_id, timestamp_start=self.last_observe_time, - timestamp_end=datetime.now().timestamp(), # 使用当前时间作为结束时间戳 + timestamp_end=datetime.now().timestamp(), limit=self.max_now_obs_len, limit_mode="latest", ) - if new_messages_list: # 检查列表是否为空 - last_obs_time_mark = self.last_observe_time + + last_obs_time_mark = self.last_observe_time + if new_messages_list: self.last_observe_time = new_messages_list[-1]["time"] self.talking_message.extend(new_messages_list) + if len(self.talking_message) > self.max_now_obs_len: # 计算需要移除的消息数量,保留最新的 max_now_obs_len 条 messages_to_remove_count = len(self.talking_message) - self.max_now_obs_len oldest_messages = self.talking_message[:messages_to_remove_count] self.talking_message = self.talking_message[messages_to_remove_count:] # 保留后半部分,即最新的 - + oldest_messages_str = await build_readable_messages( messages=oldest_messages, timestamp_mode="normal", - read_mark=last_obs_time_mark, + read_mark=0 ) + # 调用 LLM 总结主题 prompt = ( @@ -137,7 +141,11 @@ class ChattingObservation(Observation): ) self.mid_memory_info = mid_memory_str - self.talking_message_str = await build_readable_messages(messages=self.talking_message, timestamp_mode="normal") + self.talking_message_str = await build_readable_messages( + messages=self.talking_message, + timestamp_mode="normal", + read_mark=last_obs_time_mark, + ) logger.trace( f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}" diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 76d60b14..f0a44886 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -18,10 +18,9 @@ from src.plugins.chat.chat_stream import chat_manager import math from src.plugins.heartFC_chat.heartFC_chat import HeartFChatting from src.plugins.heartFC_chat.normal_chat import NormalChat - -# from src.do_tool.tool_use import ToolUser +from src.do_tool.tool_use import ToolUser from src.heart_flow.mai_state_manager import MaiStateInfo - +from src.plugins.utils.json_utils import safe_json_dumps, process_llm_tool_response, normalize_llm_response, process_llm_tool_calls # 定义常量 (从 interest.py 移动过来) MAX_INTEREST = 15.0 @@ -54,8 +53,9 @@ def init_prompt(): # prompt += "你注意到{sender_name}刚刚说:{message_txt}\n" prompt += "现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。\n" prompt += "回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题\n" - prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。" - prompt += "现在请你{hf_do_next},不要分点输出,生成内心想法,文字不要浮夸" + prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。\n" + prompt += "现在请你先{hf_do_next},不要分点输出,生成内心想法,文字不要浮夸" + prompt += "在输出完想法后,请你思考应该使用什么工具。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" Prompt(prompt, "sub_heartflow_prompt_before") @@ -114,6 +114,8 @@ class InterestChatting: self.above_threshold = False self.start_hfc_probability = 0.0 + + 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) @@ -291,6 +293,8 @@ class SubHeartflow: ) self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id + + self.structured_info = {} async def add_time_current_state(self, add_time: float): self.current_state_time += add_time @@ -477,58 +481,63 @@ class SubHeartflow: logger.info(f"{self.log_prefix} 子心流后台任务已停止。") - async def do_thinking_before_reply( - self, - extra_info: str, - obs_id: list[str] = None, - ): + async def do_thinking_before_reply(self): + """ + 在回复前进行思考,生成内心想法并收集工具调用结果 + + 返回: + tuple: (current_mind, past_mind) 当前想法和过去的想法列表 + """ + # 更新活跃时间 self.last_active_time = time.time() - + + # ---------- 1. 准备基础数据 ---------- + # 获取现有想法和情绪状态 current_thinking_info = self.current_mind mood_info = self.chat_state.mood + + # 获取观察对象 observation = self._get_primary_observation() - - chat_observe_info = "" - if obs_id: - try: - chat_observe_info = observation.get_observe_info(obs_id) - logger.debug(f"[{self.subheartflow_id}] Using specific observation IDs: {obs_id}") - except Exception as e: - logger.error( - f"[{self.subheartflow_id}] Error getting observe info with IDs {obs_id}: {e}. Falling back." - ) - chat_observe_info = observation.get_observe_info() - else: - chat_observe_info = observation.get_observe_info() - # logger.debug(f"[{self.subheartflow_id}] Using default observation info.") - - extra_info_prompt = "" - if extra_info: - for tool_name, tool_data in extra_info.items(): - extra_info_prompt += f"{tool_name} 相关信息:\n" - for item in tool_data: - extra_info_prompt += f"- {item['name']}: {item['content']}\n" - else: - extra_info_prompt = "无工具信息。\n" - + if not observation: + logger.error(f"[{self.subheartflow_id}] 无法获取观察对象") + self.update_current_mind("(我没看到任何聊天内容...)") + return self.current_mind, self.past_mind + + # 获取观察内容 + chat_observe_info = observation.get_observe_info() + + # ---------- 2. 准备工具和个性化数据 ---------- + # 初始化工具 + tool_instance = ToolUser() + tools = tool_instance._define_tools() + + # 获取个性化信息 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}" + # 获取当前时间 time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + # ---------- 3. 构建思考指导部分 ---------- + # 创建本地随机数生成器,基于分钟数作为种子 local_random = random.Random() current_minute = int(time.strftime("%M")) local_random.seed(current_minute) + # 思考指导选项和权重 hf_options = [ ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考", 0.7), ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1), @@ -536,12 +545,17 @@ class SubHeartflow: ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), ] + # 加权随机选择思考指导 hf_do_next = local_random.choices( - [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 + [option[0] for option in hf_options], + weights=[option[1] for option in hf_options], + k=1 )[0] + # ---------- 4. 构建最终提示词 ---------- + # 获取提示词模板并填充数据 prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( - extra_info=extra_info_prompt, + extra_info="", # 可以在这里添加额外信息 prompt_personality=prompt_personality, bot_name=individuality.personality.bot_nickname, current_thinking_info=current_thinking_info, @@ -551,26 +565,104 @@ class SubHeartflow: hf_do_next=hf_do_next, ) - prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt) - prompt = parse_text_timestamps(prompt, mode="lite") - - logger.debug(f"[{self.subheartflow_id}] 心流思考prompt:\n{prompt}\n") + logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") + # ---------- 5. 执行LLM请求并处理响应 ---------- + content = "" # 初始化内容变量 + reasoning_content = "" # 初始化推理内容变量 + try: - response, reasoning_content = await self.llm_model.generate_response_async(prompt) - - logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{response}\n") - - if not response: - response = "(不知道该想些什么...)" - logger.warning(f"[{self.subheartflow_id}] LLM 返回空结果,思考失败。") + # 调用LLM生成响应 + response = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools) + + # 标准化响应格式 + success, normalized_response, error_msg = normalize_llm_response( + response, log_prefix=f"[{self.subheartflow_id}] " + ) + + if not success: + # 处理标准化失败情况 + logger.warning(f"[{self.subheartflow_id}] {error_msg}") + content = "LLM响应格式无法处理" + else: + # 从标准化响应中提取内容 + if len(normalized_response) >= 2: + content = normalized_response[0] + reasoning_content = normalized_response[1] if len(normalized_response) > 1 else "" + + # 处理可能的工具调用 + if len(normalized_response) == 3: + # 提取并验证工具调用 + success, valid_tool_calls, error_msg = process_llm_tool_calls( + normalized_response, log_prefix=f"[{self.subheartflow_id}] " + ) + + if success and valid_tool_calls: + # 记录工具调用信息 + tool_calls_str = ", ".join([ + call.get("function", {}).get("name", "未知工具") + for call in valid_tool_calls + ]) + logger.info(f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}") + + # 收集工具执行结果 + await self._execute_tool_calls(valid_tool_calls, tool_instance) + elif not success: + logger.warning(f"[{self.subheartflow_id}] {error_msg}") except Exception as e: - logger.error(f"[{self.subheartflow_id}] 内心独白获取失败: {e}") - response = "(思考时发生错误...)" + # 处理总体异常 + logger.error(f"[{self.subheartflow_id}] 执行LLM请求或处理响应时出错: {e}") + logger.error(traceback.format_exc()) + content = "思考过程中出现错误" - self.update_current_mind(response) + # 记录最终思考结果 + logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{content}\n") + + # 处理空响应情况 + if not content: + content = "(不知道该想些什么...)" + logger.warning(f"[{self.subheartflow_id}] LLM返回空结果,思考失败。") + + # ---------- 6. 更新思考状态并返回结果 ---------- + # 更新当前思考内容 + self.update_current_mind(content) return self.current_mind, self.past_mind + + async def _execute_tool_calls(self, tool_calls, tool_instance): + """ + 执行一组工具调用并收集结果 + + 参数: + tool_calls: 工具调用列表 + tool_instance: 工具使用器实例 + """ + tool_results = [] + structured_info = {} # 动态生成键 + + # 执行所有工具调用 + for tool_call in tool_calls: + try: + result = await tool_instance._execute_tool_call(tool_call) + if result: + tool_results.append(result) + + # 使用工具名称作为键 + tool_name = result["name"] + if tool_name not in structured_info: + structured_info[tool_name] = [] + + structured_info[tool_name].append({ + "name": result["name"], + "content": result["content"] + }) + except Exception as tool_e: + logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") + + # 如果有工具结果,记录并更新结构化信息 + if structured_info: + logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") + self.structured_info = structured_info def update_current_mind(self, response): self.past_mind.append(self.current_mind) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index fdb2576a..5c1ce6f8 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -78,13 +78,15 @@ class ChatBot: groupinfo = message.message_info.group_info userinfo = message.message_info.user_info + if userinfo.user_id in global_config.ban_user_id: logger.debug(f"用户{userinfo.user_id}被禁止回复") return - if groupinfo.group_id not in global_config.talk_allowed_groups: - logger.debug(f"群{groupinfo.group_id}被禁止回复") - return + if groupinfo: + if groupinfo.group_id not in global_config.talk_allowed_groups: + logger.trace(f"群{groupinfo.group_id}被禁止回复") + return if message.message_info.template_info and not message.message_info.template_info.template_default: template_group_name = message.message_info.template_info.template_name diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 271386ff..386d6ac7 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -327,8 +327,8 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: # 提取最终的句子内容 final_sentences = [content for content, sep in merged_segments if content] # 只保留有内容的段 - # 清理可能引入的空字符串 - final_sentences = [s for s in final_sentences if s] + # 清理可能引入的空字符串和仅包含空白的字符串 + final_sentences = [s for s in final_sentences if s.strip()] # 过滤掉空字符串以及仅包含空白(如换行符、空格)的字符串 logger.debug(f"分割并合并后的句子: {final_sentences}") return final_sentences diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index ac8030f0..494ddeb0 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 from typing import List, Optional, Dict, Any, TYPE_CHECKING -import json +# import json # 移除,因为使用了json_utils 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 @@ -17,6 +17,7 @@ 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.utils.json_utils import extract_tool_call_arguments, safe_json_dumps, process_llm_tool_response # 导入新的JSON工具 # --- End import --- @@ -245,9 +246,6 @@ class HeartFChatting: action = planner_result.get("action", "error") reasoning = planner_result.get("reasoning", "Planner did not provide reasoning.") emoji_query = planner_result.get("emoji_query", "") - # current_mind = planner_result.get("current_mind", "[Mind unavailable]") - # send_emoji_from_tools = planner_result.get("send_emoji_from_tools", "") # Emoji from tools - observed_messages = planner_result.get("observed_messages", []) llm_error = planner_result.get("llm_error", False) if llm_error: @@ -259,7 +257,7 @@ class HeartFChatting: elif action == "text_reply": logger.debug(f"{log_prefix} HeartFChatting: 麦麦决定回复文本. 理由: {reasoning}") action_taken_this_cycle = True - anchor_message = await self._get_anchor_message(observed_messages) + anchor_message = await self._get_anchor_message() if not anchor_message: logger.error(f"{log_prefix} 循环: 无法获取锚点消息用于回复. 跳过周期.") else: @@ -304,7 +302,7 @@ class HeartFChatting: f"{log_prefix} HeartFChatting: 麦麦决定回复表情 ('{emoji_query}'). 理由: {reasoning}" ) action_taken_this_cycle = True - anchor = await self._get_anchor_message(observed_messages) + anchor = await self._get_anchor_message() if anchor: try: # --- Handle Emoji (Moved) --- # @@ -329,11 +327,6 @@ class HeartFChatting: with Timer("Wait New Msg", cycle_timers): # <--- Start Wait timer wait_start_time = time.monotonic() while True: - # Removed timer check within wait loop - # async with self._timer_lock: - # if self._loop_timer <= 0: - # logger.info(f"{log_prefix} HeartFChatting: 等待新消息时计时器耗尽。") - # break # 计时器耗尽,退出等待 # 检查是否有新消息 has_new = await observation.has_new_messages_since(planner_start_db_time) @@ -395,14 +388,6 @@ class HeartFChatting: self._processing_lock.release() # logger.trace(f"{log_prefix} 循环释放了处理锁.") # Reduce noise - # --- Timer Decrement Logging Removed --- - # async with self._timer_lock: - # self._loop_timer -= cycle_duration - # # Log timer decrement less aggressively - # if cycle_duration > 0.1 or not action_taken_this_cycle: - # logger.debug( - # f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s. 剩余时间: {self._loop_timer:.1f}s." - # ) if cycle_duration > 0.1: logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.") @@ -437,77 +422,34 @@ class HeartFChatting: """ log_prefix = self._get_log_prefix() observed_messages: List[dict] = [] - tool_result_info = {} - get_mid_memory_id = [] - # send_emoji_from_tools = "" # Emoji suggested by tools - current_mind: Optional[str] = None - llm_error = False # Flag for LLM failure - # --- Ensure SubHeartflow is available --- - if not self.sub_hf: - # Attempt to re-fetch if missing (might happen if initialization order changes) - self.sub_hf = heartflow.get_subheartflow(self.stream_id) - if not self.sub_hf: - logger.error(f"{log_prefix}[Planner] SubHeartflow is not available. Cannot proceed.") - return { - "action": "error", - "reasoning": "SubHeartflow unavailable", - "llm_error": True, - "observed_messages": [], - } + current_mind: Optional[str] = None + llm_error = False try: - # Access observation via self.sub_hf observation = self.sub_hf._get_primary_observation() 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}") - # Handle error gracefully, maybe return an error state - observed_messages_str = "[Error getting observation]" - # Consider returning error here if observation is critical - # --- 结束获取观察信息 --- # - # --- (Moved from _replier_work) 1. 思考前使用工具 --- # + try: - # Access tool_user directly - tool_result = await self.tool_user.use_tool( - message_txt=observed_messages_str, - chat_stream=self.chat_stream, - observation=self.sub_hf._get_primary_observation(), - ) - if tool_result.get("used_tools", False): - tool_result_info = tool_result.get("structured_info", {}) - logger.debug(f"{log_prefix}[Planner] 规划前工具结果: {tool_result_info}") - - get_mid_memory_id = [ - mem["content"] for mem in tool_result_info.get("mid_chat_mem", []) if "content" in mem - ] - - except Exception as e_tool: - logger.error(f"{log_prefix}[Planner] 规划前工具使用失败: {e_tool}") - # --- 结束工具使用 --- # - - # --- (Moved from _replier_work) 2. SubHeartflow 思考 --- # - try: - current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply( - extra_info=tool_result_info, - obs_id=get_mid_memory_id, - ) - # logger.debug(f"{log_prefix}[Planner] SubHF Mind: {current_mind}") + current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply() except Exception as e_subhf: logger.error(f"{log_prefix}[Planner] SubHeartflow 思考失败: {e_subhf}") current_mind = "[思考时出错]" - # --- 结束 SubHeartflow 思考 --- # + # --- 使用 LLM 进行决策 --- # - action = "no_reply" # Default action - emoji_query = "" # Default emoji query (used if action is emoji_reply or text_reply with emoji) - reasoning = "默认决策或获取决策失败" + action = "no_reply" # 默认动作 + emoji_query = "" # 默认表情查询 + reasoning = "默认决策或获取决策失败" + llm_error = False # LLM错误标志 try: - prompt = await self._build_planner_prompt(observed_messages_str, current_mind) + prompt = await self._build_planner_prompt(observed_messages_str, current_mind, self.sub_hf.structured_info) payload = { "model": self.planner_llm.model_name, "messages": [{"role": "user", "content": prompt}], @@ -515,83 +457,70 @@ class HeartFChatting: "tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}}, } - response = await self.planner_llm._execute_request( - endpoint="/chat/completions", payload=payload, prompt=prompt - ) + # 执行LLM请求 + try: + response = await self.planner_llm._execute_request( + endpoint="/chat/completions", payload=payload, prompt=prompt + ) + 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, + } - if len(response) == 3: - _, _, tool_calls = response - if tool_calls and isinstance(tool_calls, list) and len(tool_calls) > 0: - tool_call = tool_calls[0] - if ( - tool_call.get("type") == "function" - and tool_call.get("function", {}).get("name") == "decide_reply_action" - ): - try: - arguments = json.loads(tool_call["function"]["arguments"]) - action = arguments.get("action", "no_reply") - reasoning = arguments.get("reasoning", "未提供理由") - # Planner explicitly provides emoji query if action is emoji_reply or text_reply wants emoji - emoji_query = arguments.get("emoji_query", "") - logger.debug( - f"{log_prefix}[Planner] LLM Prompt: {prompt}\n决策: {action}, 理由: {reasoning}, EmojiQuery: '{emoji_query}'" - ) - except json.JSONDecodeError as json_e: - logger.error( - f"{log_prefix}[Planner] 解析工具参数失败: {json_e}. Args: {tool_call['function'].get('arguments')}" - ) - action = "error" - reasoning = "工具参数解析失败" - llm_error = True - except Exception as parse_e: - logger.error(f"{log_prefix}[Planner] 处理工具参数时出错: {parse_e}") - action = "error" - reasoning = "处理工具参数时出错" - llm_error = True - else: - logger.warning( - f"{log_prefix}[Planner] LLM 未按预期调用 'decide_reply_action' 工具。Tool calls: {tool_calls}" - ) - action = "error" - reasoning = "LLM未调用预期工具" - llm_error = True - else: - logger.warning(f"{log_prefix}[Planner] LLM 响应中未包含有效的工具调用。Tool calls: {tool_calls}") - action = "error" - reasoning = "LLM响应无工具调用" - llm_error = True + # 使用辅助函数处理工具调用响应 + success, arguments, error_msg = process_llm_tool_response( + response, + expected_tool_name="decide_reply_action", + log_prefix=f"{log_prefix}[Planner] " + ) + + if success: + # 提取决策参数 + action = arguments.get("action", "no_reply") + reasoning = arguments.get("reasoning", "未提供理由") + emoji_query = arguments.get("emoji_query", "") + + # 记录决策结果 + logger.debug( + f"{log_prefix}[Planner] 决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" + ) else: - logger.warning(f"{log_prefix}[Planner] LLM 未返回预期的工具调用响应。Response parts: {len(response)}") + # 处理工具调用失败 + logger.warning(f"{log_prefix}[Planner] {error_msg}") action = "error" - reasoning = "LLM响应格式错误" + reasoning = error_msg llm_error = True except Exception as llm_e: - logger.error(f"{log_prefix}[Planner] Planner LLM 调用失败: {llm_e}") - # logger.error(traceback.format_exc()) # Maybe too verbose for loop? + logger.error(f"{log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}") + logger.error(traceback.format_exc()) # 记录完整堆栈以便调试 action = "error" - reasoning = f"LLM 调用失败: {llm_e}" + reasoning = f"LLM处理失败: {llm_e}" llm_error = True # --- 结束 LLM 决策 --- # return { "action": action, "reasoning": reasoning, - "emoji_query": emoji_query, # Explicit query from Planner/LLM + "emoji_query": emoji_query, "current_mind": current_mind, - # "send_emoji_from_tools": send_emoji_from_tools, # Emoji suggested by tools (used as fallback) "observed_messages": observed_messages, "llm_error": llm_error, } - async def _get_anchor_message(self, observed_messages: List[dict]) -> Optional[MessageRecv]: + async def _get_anchor_message(self) -> Optional[MessageRecv]: """ 重构观察到的最后一条消息作为回复的锚点, 如果重构失败或观察为空,则创建一个占位符。 """ try: - # --- Create Placeholder --- # placeholder_id = f"mid_pf_{int(time.time() * 1000)}" placeholder_user = UserInfo( user_id="system_trigger", user_nickname="System Trigger", platform=self.chat_stream.platform @@ -652,37 +581,41 @@ class HeartFChatting: raise RuntimeError("发送回复失败,_send_response_messages返回None") async def shutdown(self): - """ - Gracefully shuts down the HeartFChatting instance by cancelling the active loop task. - """ + """优雅关闭HeartFChatting实例,取消活动循环任务""" log_prefix = self._get_log_prefix() - logger.info(f"{log_prefix} Shutting down HeartFChatting...") + logger.info(f"{log_prefix} 正在关闭HeartFChatting...") + + # 取消循环任务 if self._loop_task and not self._loop_task.done(): - logger.info(f"{log_prefix} Cancelling active PF loop task.") + logger.info(f"{log_prefix} 正在取消HeartFChatting循环任务") self._loop_task.cancel() try: - await asyncio.wait_for(self._loop_task, timeout=1.0) # Shorter timeout? - except asyncio.CancelledError: - logger.info(f"{log_prefix} PF loop task cancelled successfully.") - except asyncio.TimeoutError: - logger.warning(f"{log_prefix} Timeout waiting for PF loop task cancellation.") + await asyncio.wait_for(self._loop_task, timeout=1.0) + logger.info(f"{log_prefix} HeartFChatting循环任务已取消") + except (asyncio.CancelledError, asyncio.TimeoutError): + pass except Exception as e: - logger.error(f"{log_prefix} Error during loop task cancellation: {e}") + logger.error(f"{log_prefix} 取消循环任务出错: {e}") else: - logger.info(f"{log_prefix} No active PF loop task found to cancel.") + logger.info(f"{log_prefix} 没有活动的HeartFChatting循环任务") + # 清理状态 self._loop_active = False self._loop_task = None if self._processing_lock.locked(): - logger.warning(f"{log_prefix} Releasing processing lock during shutdown.") self._processing_lock.release() - logger.info(f"{log_prefix} HeartFChatting shutdown complete.") + logger.warning(f"{log_prefix} 已释放处理锁") + + logger.info(f"{log_prefix} HeartFChatting关闭完成") - async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str]) -> str: + async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any]) -> str: """构建 Planner LLM 的提示词""" prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。\n" + if structured_info: + prompt += f"以下是一些额外的信息:\n{structured_info}\n" + if observed_messages_str: prompt += "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n" prompt += observed_messages_str @@ -726,6 +659,7 @@ class HeartFChatting: response_set: Optional[List[str]] = None try: response_set = await self.gpt_instance.generate_response( + structured_info=self.sub_hf.structured_info, current_mind_info=self.sub_hf.current_mind, reason=reason, message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter) diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index 28329b89..0ed6229e 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -39,6 +39,7 @@ class HeartFCGenerator: async def generate_response( self, + structured_info: str, current_mind_info: str, reason: str, message: MessageRecv, @@ -56,7 +57,7 @@ class HeartFCGenerator: current_model = self.model_normal current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高 model_response = await self._generate_response_with_model( - current_mind_info, reason, message, current_model, thinking_id + structured_info, current_mind_info, reason, message, current_model, thinking_id ) if model_response: @@ -71,7 +72,7 @@ class HeartFCGenerator: return None async def _generate_response_with_model( - self, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str + self, structured_info: str, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str ) -> str: sender_name = "" @@ -84,6 +85,7 @@ class HeartFCGenerator: build_mode="focus", reason=reason, current_mind_info=current_mind_info, + structured_info=structured_info, message_txt=message.processed_plain_text, sender_name=sender_name, chat_stream=message.chat_stream, @@ -103,106 +105,6 @@ class HeartFCGenerator: return content - async def _get_emotion_tags(self, content: str, processed_plain_text: str): - """提取情感标签,结合立场和情绪""" - try: - # 构建提示词,结合回复内容、被回复的内容以及立场分析 - prompt = f""" - 请严格根据以下对话内容,完成以下任务: - 1. 判断回复者对被回复者观点的直接立场: - - "支持":明确同意或强化被回复者观点 - - "反对":明确反驳或否定被回复者观点 - - "中立":不表达明确立场或无关回应 - 2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签 - 3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒" - 4. 考虑回复者的人格设定为{global_config.personality_core} - - 对话示例: - 被回复:「A就是笨」 - 回复:「A明明很聪明」 → 反对-愤怒 - - 当前对话: - 被回复:「{processed_plain_text}」 - 回复:「{content}」 - - 输出要求: - - 只需输出"立场-情绪"结果,不要解释 - - 严格基于文字直接表达的对立关系判断 - """ - - # 调用模型生成结果 - result, _, _ = await self.model_sum.generate_response(prompt) - result = result.strip() - - # 解析模型输出的结果 - if "-" in result: - stance, emotion = result.split("-", 1) - valid_stances = ["支持", "反对", "中立"] - valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"] - if stance in valid_stances and emotion in valid_emotions: - return stance, emotion # 返回有效的立场-情绪组合 - else: - logger.debug(f"无效立场-情感组合:{result}") - return "中立", "平静" # 默认返回中立-平静 - else: - logger.debug(f"立场-情感格式错误:{result}") - return "中立", "平静" # 格式错误时返回默认值 - - except Exception as e: - logger.debug(f"获取情感标签时出错: {e}") - return "中立", "平静" # 出错时返回默认值 - - async def _get_emotion_tags_with_reason(self, content: str, processed_plain_text: str, reason: str): - """提取情感标签,结合立场和情绪""" - try: - # 构建提示词,结合回复内容、被回复的内容以及立场分析 - prompt = f""" - 请严格根据以下对话内容,完成以下任务: - 1. 判断回复者对被回复者观点的直接立场: - - "支持":明确同意或强化被回复者观点 - - "反对":明确反驳或否定被回复者观点 - - "中立":不表达明确立场或无关回应 - 2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签 - 3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒" - 4. 考虑回复者的人格设定为{global_config.personality_core} - - 对话示例: - 被回复:「A就是笨」 - 回复:「A明明很聪明」 → 反对-愤怒 - - 当前对话: - 被回复:「{processed_plain_text}」 - 回复:「{content}」 - - 原因:「{reason}」 - - 输出要求: - - 只需输出"立场-情绪"结果,不要解释 - - 严格基于文字直接表达的对立关系判断 - """ - - # 调用模型生成结果 - result, _, _ = await self.model_sum.generate_response(prompt) - result = result.strip() - - # 解析模型输出的结果 - if "-" in result: - stance, emotion = result.split("-", 1) - valid_stances = ["支持", "反对", "中立"] - valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"] - if stance in valid_stances and emotion in valid_emotions: - return stance, emotion # 返回有效的立场-情绪组合 - else: - logger.debug(f"无效立场-情感组合:{result}") - return "中立", "平静" # 默认返回中立-平静 - else: - logger.debug(f"立场-情感格式错误:{result}") - return "中立", "平静" # 格式错误时返回默认值 - - except Exception as e: - logger.debug(f"获取情感标签时出错: {e}") - return "中立", "平静" # 出错时返回默认值 - async def _process_response(self, content: str) -> List[str]: """处理响应内容,返回处理后的内容和情感标签""" if not content: diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 1d19d1ca..33baad37 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -21,6 +21,8 @@ logger = get_module_logger("prompt") def init_prompt(): Prompt( """ +你有以下信息可供参考: +{structured_info} {chat_target} {chat_talking_prompt} 现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n @@ -79,17 +81,17 @@ class PromptBuilder: self.activate_messages = "" async def build_prompt( - self, build_mode, reason, current_mind_info, message_txt: str, sender_name: str = "某人", chat_stream=None + self, build_mode, reason, current_mind_info, structured_info, message_txt: str, sender_name: str = "某人", chat_stream=None ) -> Optional[tuple[str, str]]: if build_mode == "normal": return await self._build_prompt_normal(chat_stream, message_txt, sender_name) elif build_mode == "focus": - return await self._build_prompt_focus(reason, current_mind_info, chat_stream, message_txt, sender_name) + return await self._build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, message_txt, sender_name) return None async def _build_prompt_focus( - self, reason, current_mind_info, chat_stream, message_txt: str, sender_name: str = "某人" + self, reason, current_mind_info, structured_info, chat_stream, message_txt: str, sender_name: str = "某人" ) -> tuple[str, str]: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1) @@ -148,6 +150,7 @@ class PromptBuilder: prompt = await global_prompt_manager.format_prompt( "heart_flow_prompt", + structured_info=structured_info, 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/heartFC_chat/normal_chat_generator.py b/src/plugins/heartFC_chat/normal_chat_generator.py index 07635baf..cd9208b3 100644 --- a/src/plugins/heartFC_chat/normal_chat_generator.py +++ b/src/plugins/heartFC_chat/normal_chat_generator.py @@ -83,6 +83,7 @@ class NormalChatGenerator: build_mode="normal", reason="", current_mind_info="", + structured_info="", message_txt=message.processed_plain_text, sender_name=sender_name, chat_stream=message.chat_stream, diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index e2ec7ac3..bdc408ab 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -710,6 +710,8 @@ class LLMRequest: usage = None # 初始化usage变量,避免未定义错误 reasoning_content = "" content = "" + tool_calls = None # 初始化工具调用变量 + async for line_bytes in response.content: try: line = line_bytes.decode("utf-8").strip() @@ -731,11 +733,20 @@ class LLMRequest: if delta_content is None: delta_content = "" accumulated_content += delta_content + + # 提取工具调用信息 + if "tool_calls" in delta: + if tool_calls is None: + tool_calls = delta["tool_calls"] + else: + # 合并工具调用信息 + tool_calls.extend(delta["tool_calls"]) + # 检测流式输出文本是否结束 finish_reason = chunk["choices"][0].get("finish_reason") if delta.get("reasoning_content", None): reasoning_content += delta["reasoning_content"] - if finish_reason == "stop": + if finish_reason == "stop" or finish_reason == "tool_calls": chunk_usage = chunk.get("usage", None) if chunk_usage: usage = chunk_usage @@ -763,14 +774,21 @@ class LLMRequest: if think_match: reasoning_content = think_match.group(1).strip() content = re.sub(r".*?", "", content, flags=re.DOTALL).strip() + + # 构建消息对象 + message = { + "content": content, + "reasoning_content": reasoning_content, + } + + # 如果有工具调用,添加到消息中 + if tool_calls: + message["tool_calls"] = tool_calls + result = { "choices": [ { - "message": { - "content": content, - "reasoning_content": reasoning_content, - # 流式输出可能没有工具调用,此处不需要添加tool_calls字段 - } + "message": message } ], "usage": usage, @@ -1046,6 +1064,7 @@ class LLMRequest: # 只有当tool_calls存在且不为空时才返回 if tool_calls: + logger.debug(f"检测到工具调用: {tool_calls}") return content, reasoning_content, tool_calls else: return content, reasoning_content @@ -1109,7 +1128,30 @@ class LLMRequest: response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt) # 原样返回响应,不做处理 + return response + + async def generate_response_tool_async(self, prompt: str, tools: list, **kwargs) -> Union[str, Tuple]: + """异步方式根据输入的提示生成模型的响应""" + # 构建请求体,不硬编码max_tokens + data = { + "model": self.model_name, + "messages": [{"role": "user", "content": prompt}], + **self.params, + **kwargs, + "tools": tools + } + + logger.debug(f"向模型 {self.model_name} 发送工具调用请求,包含 {len(tools)} 个工具") + response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt) + # 检查响应是否包含工具调用 + if isinstance(response, tuple) and len(response) == 3: + content, reasoning_content, tool_calls = response + logger.debug(f"收到工具调用响应,包含 {len(tool_calls) if tool_calls else 0} 个工具调用") + return content, reasoning_content, tool_calls + else: + logger.debug(f"收到普通响应,无工具调用") + return response async def get_embedding(self, text: str) -> Union[list, None]: """异步方法:获取文本的embedding向量 diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index d822263d..6a5e4e8e 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -303,7 +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--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n" + read_mark_line = f"\n\n--- 以上消息已读 (标记时间: {readable_read_mark}) ---\n--- 请关注你上次思考之后以下的新消息---\n" # 组合结果,确保空部分不引入多余的标记或换行 if formatted_before and formatted_after: diff --git a/src/plugins/utils/json_utils.py b/src/plugins/utils/json_utils.py new file mode 100644 index 00000000..962901b5 --- /dev/null +++ b/src/plugins/utils/json_utils.py @@ -0,0 +1,297 @@ +import json +import logging +from typing import Any, Dict, Optional, TypeVar, Generic, List, Union, Callable, Tuple + +# 定义类型变量用于泛型类型提示 +T = TypeVar('T') + +# 获取logger +logger = logging.getLogger("json_utils") + +def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]: + """ + 安全地解析JSON字符串,出错时返回默认值 + + 参数: + json_str: 要解析的JSON字符串 + default_value: 解析失败时返回的默认值 + + 返回: + 解析后的Python对象,或在解析失败时返回default_value + """ + if not json_str: + return default_value + + try: + return json.loads(json_str) + except json.JSONDecodeError as e: + logger.error(f"JSON解析失败: {e}, JSON字符串: {json_str[:100]}...") + return default_value + except Exception as e: + logger.error(f"JSON解析过程中发生意外错误: {e}") + return default_value + +def extract_tool_call_arguments(tool_call: Dict[str, Any], + default_value: Dict[str, Any] = None) -> Dict[str, Any]: + """ + 从LLM工具调用对象中提取参数 + + 参数: + tool_call: 工具调用对象字典 + default_value: 解析失败时返回的默认值 + + 返回: + 解析后的参数字典,或在解析失败时返回default_value + """ + default_result = default_value or {} + + if not tool_call or not isinstance(tool_call, dict): + logger.error(f"无效的工具调用对象: {tool_call}") + return default_result + + try: + # 提取function参数 + function_data = tool_call.get("function", {}) + if not function_data or not isinstance(function_data, dict): + logger.error(f"工具调用缺少function字段或格式不正确: {tool_call}") + return default_result + + # 提取arguments + arguments_str = function_data.get("arguments", "{}") + if not arguments_str: + return default_result + + # 解析JSON + return safe_json_loads(arguments_str, default_result) + + except Exception as e: + logger.error(f"提取工具调用参数时出错: {e}") + return default_result + +def get_json_value(json_obj: Dict[str, Any], key_path: str, + default_value: T = None, + transform_func: Callable[[Any], T] = None) -> Union[Any, T]: + """ + 从JSON对象中按照路径提取值,支持点表示法路径,如"data.items.0.name" + + 参数: + json_obj: JSON对象(已解析的字典) + key_path: 键路径,使用点表示法,如"data.items.0.name" + default_value: 获取失败时返回的默认值 + transform_func: 可选的转换函数,用于对获取的值进行转换 + + 返回: + 路径指向的值,或在获取失败时返回default_value + """ + if not json_obj or not key_path: + return default_value + + try: + # 分割路径 + keys = key_path.split(".") + current = json_obj + + # 遍历路径 + for key in keys: + # 处理数组索引 + if key.isdigit() and isinstance(current, list): + index = int(key) + if 0 <= index < len(current): + current = current[index] + else: + return default_value + # 处理字典键 + elif isinstance(current, dict): + if key in current: + current = current[key] + else: + return default_value + else: + return default_value + + # 应用转换函数(如果提供) + if transform_func and current is not None: + return transform_func(current) + return current + except Exception as e: + logger.error(f"从JSON获取值时出错: {e}, 路径: {key_path}") + return default_value + +def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = False, + pretty: bool = False) -> str: + """ + 安全地将Python对象序列化为JSON字符串 + + 参数: + obj: 要序列化的Python对象 + default_value: 序列化失败时返回的默认值 + ensure_ascii: 是否确保ASCII编码(默认False,允许中文等非ASCII字符) + pretty: 是否美化输出JSON + + 返回: + 序列化后的JSON字符串,或在序列化失败时返回default_value + """ + try: + indent = 2 if pretty else None + return json.dumps(obj, ensure_ascii=ensure_ascii, indent=indent) + except TypeError as e: + logger.error(f"JSON序列化失败(类型错误): {e}") + return default_value + except Exception as e: + logger.error(f"JSON序列化过程中发生意外错误: {e}") + return default_value + +def merge_json_objects(*objects: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并多个JSON对象(字典) + + 参数: + *objects: 要合并的JSON对象(字典) + + 返回: + 合并后的字典,后面的对象会覆盖前面对象的相同键 + """ + result = {} + for obj in objects: + if obj and isinstance(obj, dict): + result.update(obj) + return result + +def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, List[Any], str]: + """ + 标准化LLM响应格式,将各种格式(如元组)转换为统一的列表格式 + + 参数: + response: 原始LLM响应 + log_prefix: 日志前缀 + + 返回: + 元组 (成功标志, 标准化后的响应列表, 错误消息) + """ + # 检查是否为None + if response is None: + return False, [], "LLM响应为None" + + # 记录原始类型 + logger.debug(f"{log_prefix}LLM响应原始类型: {type(response).__name__}") + + # 将元组转换为列表 + if isinstance(response, tuple): + logger.debug(f"{log_prefix}将元组响应转换为列表") + response = list(response) + + # 确保是列表类型 + if not isinstance(response, list): + return False, [], f"无法处理的LLM响应类型: {type(response).__name__}" + + # 处理工具调用部分(如果存在) + if len(response) == 3: + content, reasoning, tool_calls = response + + # 将工具调用部分转换为列表(如果是元组) + if isinstance(tool_calls, tuple): + logger.debug(f"{log_prefix}将工具调用元组转换为列表") + tool_calls = list(tool_calls) + response[2] = tool_calls + + return True, response, "" + +def process_llm_tool_calls(response: List[Any], log_prefix: str = "") -> Tuple[bool, List[Dict[str, Any]], str]: + """ + 处理并提取LLM响应中的工具调用列表 + + 参数: + response: 标准化后的LLM响应列表 + log_prefix: 日志前缀 + + 返回: + 元组 (成功标志, 工具调用列表, 错误消息) + """ + # 确保响应格式正确 + if len(response) != 3: + return False, [], f"LLM响应元素数量不正确: 预期3个元素,实际{len(response)}个" + + # 提取工具调用部分 + tool_calls = response[2] + + # 检查工具调用是否有效 + if tool_calls is None: + return False, [], "工具调用部分为None" + + if not isinstance(tool_calls, list): + return False, [], f"工具调用部分不是列表: {type(tool_calls).__name__}" + + if len(tool_calls) == 0: + return False, [], "工具调用列表为空" + + # 检查工具调用是否格式正确 + valid_tool_calls = [] + for i, tool_call in enumerate(tool_calls): + if not isinstance(tool_call, dict): + logger.warning(f"{log_prefix}工具调用[{i}]不是字典: {type(tool_call).__name__}") + continue + + if tool_call.get("type") != "function": + logger.warning(f"{log_prefix}工具调用[{i}]不是函数类型: {tool_call.get('type', '未知')}") + continue + + if "function" not in tool_call or not isinstance(tool_call["function"], dict): + logger.warning(f"{log_prefix}工具调用[{i}]缺少function字段或格式不正确") + continue + + valid_tool_calls.append(tool_call) + + # 检查是否有有效的工具调用 + if not valid_tool_calls: + return False, [], "没有找到有效的工具调用" + + return True, valid_tool_calls, "" + +def process_llm_tool_response( + response: Any, + expected_tool_name: str = None, + log_prefix: str = "" +) -> Tuple[bool, Dict[str, Any], str]: + """ + 处理LLM返回的工具调用响应,进行常见错误检查并提取参数 + + 参数: + response: LLM的响应,预期是[content, reasoning, tool_calls]格式的列表或元组 + expected_tool_name: 预期的工具名称,如不指定则不检查 + log_prefix: 日志前缀,用于标识日志来源 + + 返回: + 三元组(成功标志, 参数字典, 错误描述) + - 如果成功解析,返回(True, 参数字典, "") + - 如果解析失败,返回(False, {}, 错误描述) + """ + # 使用新的标准化函数 + success, normalized_response, error_msg = normalize_llm_response(response, log_prefix) + if not success: + return False, {}, error_msg + + # 使用新的工具调用处理函数 + success, valid_tool_calls, error_msg = process_llm_tool_calls(normalized_response, log_prefix) + if not success: + return False, {}, error_msg + + # 检查是否有工具调用 + if not valid_tool_calls: + return False, {}, "没有有效的工具调用" + + # 获取第一个工具调用 + tool_call = valid_tool_calls[0] + + # 检查工具名称(如果提供了预期名称) + if expected_tool_name: + actual_name = tool_call.get("function", {}).get("name") + if actual_name != expected_tool_name: + return False, {}, f"工具名称不匹配: 预期'{expected_tool_name}',实际'{actual_name}'" + + # 提取并解析参数 + try: + arguments = extract_tool_call_arguments(tool_call, {}) + return True, arguments, "" + except Exception as e: + logger.error(f"{log_prefix}解析工具参数时出错: {e}") + return False, {}, f"解析参数失败: {str(e)}" \ No newline at end of file diff --git a/tool_call_benchmark.py b/tool_call_benchmark.py new file mode 100644 index 00000000..691aeb7c --- /dev/null +++ b/tool_call_benchmark.py @@ -0,0 +1,289 @@ +import asyncio +import time +from src.plugins.models.utils_model import LLMRequest +from src.config.config import global_config +from src.do_tool.tool_use import ToolUser +import statistics +import json + +async def run_test(test_name, test_function, iterations=5): + """ + 运行指定次数的测试并计算平均响应时间 + + 参数: + test_name: 测试名称 + test_function: 要执行的测试函数 + iterations: 测试迭代次数 + + 返回: + 测试结果统计 + """ + print(f"开始 {test_name} 测试({iterations}次迭代)...") + times = [] + responses = [] + + for i in range(iterations): + print(f" 运行第 {i+1}/{iterations} 次测试...") + start_time = time.time() + response = await test_function() + end_time = time.time() + elapsed = end_time - start_time + times.append(elapsed) + responses.append(response) + print(f" - 耗时: {elapsed:.2f}秒") + + results = { + "平均耗时": statistics.mean(times), + "最短耗时": min(times), + "最长耗时": max(times), + "标准差": statistics.stdev(times) if len(times) > 1 else 0, + "所有耗时": times, + "响应结果": responses + } + + return results + +async def test_with_tool_calls(): + """使用工具调用的LLM请求测试""" + # 创建LLM模型实例 + llm_model = LLMRequest( + model=global_config.llm_sub_heartflow, + # model = global_config.llm_tool_use, + # temperature=global_config.llm_sub_heartflow["temp"], + max_tokens=800, + request_type="benchmark_test", + ) + + # 创建工具实例 + tool_instance = ToolUser() + tools = tool_instance._define_tools() + + # 简单的测试提示词 + 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墨墨 说:[表情包:开心、满足。]; + +你现在当前心情:平静。 +现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。 +回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题 +请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。 +现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸 +在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。''' + + # 发送带有工具调用的请求 + response = await llm_model.generate_response_tool_async(prompt=prompt, tools=tools) + + result_info = {} + + # 简单处理工具调用结果 + if len(response) == 3: + content, reasoning_content, tool_calls = response + tool_calls_count = len(tool_calls) if tool_calls else 0 + print(f" 工具调用请求生成了 {tool_calls_count} 个工具调用") + + # 输出内容和工具调用详情 + print("\n 生成的内容:") + print(f" {content[:200]}..." if len(content) > 200 else f" {content}") + + if tool_calls: + print("\n 工具调用详情:") + for i, tool_call in enumerate(tool_calls): + tool_name = tool_call['function']['name'] + tool_params = tool_call['function'].get('arguments', {}) + print(f" - 工具 {i+1}: {tool_name}") + print(f" 参数: {json.dumps(tool_params, ensure_ascii=False)[:100]}..." + if len(json.dumps(tool_params, ensure_ascii=False)) > 100 + else f" 参数: {json.dumps(tool_params, ensure_ascii=False)}") + + result_info = { + "内容": content, + "推理内容": reasoning_content, + "工具调用": tool_calls + } + else: + content, reasoning_content = response + print(" 工具调用请求未生成任何工具调用") + print("\n 生成的内容:") + print(f" {content[:200]}..." if len(content) > 200 else f" {content}") + + result_info = { + "内容": content, + "推理内容": reasoning_content, + "工具调用": [] + } + + return result_info + +async def test_without_tool_calls(): + """不使用工具调用的LLM请求测试""" + # 创建LLM模型实例 + llm_model = LLMRequest( + model=global_config.llm_sub_heartflow, + temperature=global_config.llm_sub_heartflow["temp"], + max_tokens=800, + request_type="benchmark_test", + ) + + # 简单的测试提示词(与工具调用相同,以便公平比较) + 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墨墨 说:[表情包:开心、满足。]; + +你现在当前心情:平静。 +现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。 +回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题 +请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。 +现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸 +在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。''' + + # 发送不带工具调用的请求 + response, reasoning_content = await llm_model.generate_response_async(prompt) + + # 输出生成的内容 + print("\n 生成的内容:") + print(f" {response[:200]}..." if len(response) > 200 else f" {response}") + + result_info = { + "内容": response, + "推理内容": reasoning_content, + "工具调用": [] + } + + return result_info + +async def main(): + """主测试函数""" + print("=" * 50) + print("LLM工具调用与普通请求性能比较测试") + print("=" * 50) + + # 设置测试迭代次数 + iterations = 3 + + # 测试不使用工具调用 + 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) + + # 显示结果比较 + print("\n" + "=" * 50) + print("测试结果比较") + print("=" * 50) + + print("\n不使用工具调用:") + for key, value in results_without_tools.items(): + if key == "所有耗时": + print(f" {key}: {[f'{t:.2f}秒' for t in value]}") + elif key == "响应结果": + print(f" {key}: [内容已省略,详见结果文件]") + else: + print(f" {key}: {value:.2f}秒") + + print("\n使用工具调用:") + for key, value in results_with_tools.items(): + if key == "所有耗时": + print(f" {key}: {[f'{t:.2f}秒' for t in value]}") + elif key == "响应结果": + tool_calls_counts = [len(res.get("工具调用", [])) for res in value] + print(f" {key}: [内容已省略,详见结果文件]") + print(f" 工具调用数量: {tool_calls_counts}") + else: + print(f" {key}: {value:.2f}秒") + + # 计算差异百分比 + diff_percent = ((results_with_tools["平均耗时"] / results_without_tools["平均耗时"]) - 1) * 100 + print(f"\n工具调用比普通请求平均耗时相差: {diff_percent:.2f}%") + + # 保存结果到JSON文件 + results = { + "测试时间": time.strftime("%Y-%m-%d %H:%M:%S"), + "测试迭代次数": iterations, + "不使用工具调用": { + k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v]) + for k, v in results_without_tools.items() + if k != "响应结果" + }, + "不使用工具调用_详细响应": [ + { + "内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"], + "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"] + } for resp in results_without_tools["响应结果"] + ], + "使用工具调用": { + k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v]) + for k, v in results_with_tools.items() + if k != "响应结果" + }, + "使用工具调用_详细响应": [ + { + "内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"], + "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"], + "工具调用数量": len(resp["工具调用"]), + "工具调用详情": [ + { + "工具名称": tool["function"]["name"], + "参数": tool["function"].get("arguments", {}) + } for tool in resp["工具调用"] + ] + } for resp in results_with_tools["响应结果"] + ], + "差异百分比": float(f"{diff_percent:.2f}") + } + + with open("llm_tool_benchmark_results.json", "w", encoding="utf-8") as f: + json.dump(results, f, ensure_ascii=False, indent=2) + + print(f"\n测试结果已保存到 llm_tool_benchmark_results.json") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file From 30756644806c4cafe5edb7e3dcabd321915d3b26 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 14:19:26 +0800 Subject: [PATCH 15/73] fix:FFUF --- src/do_tool/tool_use.py | 4 +- src/heart_flow/observation.py | 14 +- src/heart_flow/sub_heartflow.py | 68 ++++----- src/heart_flow/subheartflow_manager.py | 6 +- src/plugins/chat/bot.py | 1 - src/plugins/chat/utils.py | 4 +- src/plugins/heartFC_chat/heartFC_chat.py | 38 +++-- src/plugins/heartFC_chat/heartFC_generator.py | 8 +- .../heartFC_chat/heartflow_prompt_builder.py | 13 +- src/plugins/models/utils_model.py | 26 ++-- src/plugins/utils/chat_message_builder.py | 4 +- src/plugins/utils/json_utils.py | 122 +++++++-------- tool_call_benchmark.py | 141 +++++++++--------- 13 files changed, 224 insertions(+), 225 deletions(-) diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py index 1f625a58..8087ceda 100644 --- a/src/do_tool/tool_use.py +++ b/src/do_tool/tool_use.py @@ -159,7 +159,9 @@ class ToolUser: tool_calls_str = "" for tool_call in tool_calls: tool_calls_str += f"{tool_call['function']['name']}\n" - logger.info(f"根据:\n{prompt}\n\n内容:{content}\n\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}") + logger.info( + f"根据:\n{prompt}\n\n内容:{content}\n\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}" + ) tool_results = [] structured_info = {} # 动态生成键 diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index 0f61f608..9391a660 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -82,29 +82,25 @@ class ChattingObservation(Observation): new_messages_list = get_raw_msg_by_timestamp_with_chat( chat_id=self.chat_id, timestamp_start=self.last_observe_time, - timestamp_end=datetime.now().timestamp(), + timestamp_end=datetime.now().timestamp(), limit=self.max_now_obs_len, limit_mode="latest", ) - + last_obs_time_mark = self.last_observe_time if new_messages_list: self.last_observe_time = new_messages_list[-1]["time"] self.talking_message.extend(new_messages_list) - if len(self.talking_message) > self.max_now_obs_len: # 计算需要移除的消息数量,保留最新的 max_now_obs_len 条 messages_to_remove_count = len(self.talking_message) - self.max_now_obs_len oldest_messages = self.talking_message[:messages_to_remove_count] self.talking_message = self.talking_message[messages_to_remove_count:] # 保留后半部分,即最新的 - + oldest_messages_str = await build_readable_messages( - messages=oldest_messages, - timestamp_mode="normal", - read_mark=0 + messages=oldest_messages, timestamp_mode="normal", read_mark=0 ) - # 调用 LLM 总结主题 prompt = ( @@ -145,7 +141,7 @@ class ChattingObservation(Observation): messages=self.talking_message, timestamp_mode="normal", read_mark=last_obs_time_mark, - ) + ) logger.trace( f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}" diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index f0a44886..1aa6f902 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -6,12 +6,10 @@ from src.config.config import global_config import time from typing import Optional, List, Dict, Callable import traceback -from src.plugins.chat.utils import parse_text_timestamps import enum from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 from src.individuality.individuality import Individuality import random -from src.plugins.person_info.relationship_manager import relationship_manager from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.plugins.chat.message import MessageRecv from src.plugins.chat.chat_stream import chat_manager @@ -20,7 +18,7 @@ from src.plugins.heartFC_chat.heartFC_chat import HeartFChatting from src.plugins.heartFC_chat.normal_chat import NormalChat from src.do_tool.tool_use import ToolUser from src.heart_flow.mai_state_manager import MaiStateInfo -from src.plugins.utils.json_utils import safe_json_dumps, process_llm_tool_response, normalize_llm_response, process_llm_tool_calls +from src.plugins.utils.json_utils import safe_json_dumps, normalize_llm_response, process_llm_tool_calls # 定义常量 (从 interest.py 移动过来) MAX_INTEREST = 15.0 @@ -114,8 +112,6 @@ class InterestChatting: self.above_threshold = False self.start_hfc_probability = 0.0 - - 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) @@ -293,7 +289,7 @@ class SubHeartflow: ) self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id - + self.structured_info = {} async def add_time_current_state(self, add_time: float): @@ -484,36 +480,36 @@ class SubHeartflow: async def do_thinking_before_reply(self): """ 在回复前进行思考,生成内心想法并收集工具调用结果 - + 返回: tuple: (current_mind, past_mind) 当前想法和过去的想法列表 """ # 更新活跃时间 self.last_active_time = time.time() - + # ---------- 1. 准备基础数据 ---------- # 获取现有想法和情绪状态 current_thinking_info = self.current_mind mood_info = self.chat_state.mood - + # 获取观察对象 observation = self._get_primary_observation() if not observation: logger.error(f"[{self.subheartflow_id}] 无法获取观察对象") self.update_current_mind("(我没看到任何聊天内容...)") return self.current_mind, self.past_mind - + # 获取观察内容 chat_observe_info = observation.get_observe_info() - + # ---------- 2. 准备工具和个性化数据 ---------- # 初始化工具 tool_instance = ToolUser() tools = tool_instance._define_tools() - + # 获取个性化信息 individuality = Individuality.get_instance() - + # 构建个性部分 prompt_personality = f"你的名字是{individuality.personality.bot_nickname},你" prompt_personality += individuality.personality.personality_core @@ -547,9 +543,7 @@ class SubHeartflow: # 加权随机选择思考指导 hf_do_next = local_random.choices( - [option[0] for option in hf_options], - weights=[option[1] for option in hf_options], - k=1 + [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 )[0] # ---------- 4. 构建最终提示词 ---------- @@ -570,16 +564,16 @@ class SubHeartflow: # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 reasoning_content = "" # 初始化推理内容变量 - + try: # 调用LLM生成响应 response = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools) - + # 标准化响应格式 success, normalized_response, error_msg = normalize_llm_response( response, log_prefix=f"[{self.subheartflow_id}] " ) - + if not success: # 处理标准化失败情况 logger.warning(f"[{self.subheartflow_id}] {error_msg}") @@ -588,23 +582,24 @@ class SubHeartflow: # 从标准化响应中提取内容 if len(normalized_response) >= 2: content = normalized_response[0] - reasoning_content = normalized_response[1] if len(normalized_response) > 1 else "" - + _reasoning_content = normalized_response[1] if len(normalized_response) > 1 else "" + # 处理可能的工具调用 if len(normalized_response) == 3: # 提取并验证工具调用 success, valid_tool_calls, error_msg = process_llm_tool_calls( normalized_response, log_prefix=f"[{self.subheartflow_id}] " ) - + if success and valid_tool_calls: # 记录工具调用信息 - tool_calls_str = ", ".join([ - call.get("function", {}).get("name", "未知工具") - for call in valid_tool_calls - ]) - logger.info(f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}") - + tool_calls_str = ", ".join( + [call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls] + ) + logger.info( + f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}" + ) + # 收集工具执行结果 await self._execute_tool_calls(valid_tool_calls, tool_instance) elif not success: @@ -628,37 +623,34 @@ class SubHeartflow: self.update_current_mind(content) return self.current_mind, self.past_mind - + async def _execute_tool_calls(self, tool_calls, tool_instance): """ 执行一组工具调用并收集结果 - + 参数: tool_calls: 工具调用列表 tool_instance: 工具使用器实例 """ tool_results = [] structured_info = {} # 动态生成键 - + # 执行所有工具调用 for tool_call in tool_calls: try: result = await tool_instance._execute_tool_call(tool_call) if result: tool_results.append(result) - + # 使用工具名称作为键 tool_name = result["name"] if tool_name not in structured_info: structured_info[tool_name] = [] - - structured_info[tool_name].append({ - "name": result["name"], - "content": result["content"] - }) + + structured_info[tool_name].append({"name": result["name"], "content": result["content"]}) except Exception as tool_e: logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") - + # 如果有工具结果,记录并更新结构化信息 if structured_info: logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 1e64027c..bf473b78 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -290,9 +290,9 @@ class SubHeartflowManager: log_prefix_flow = f"[{stream_name}]" # 只处理 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. + # 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 diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 5c1ce6f8..b6584dcd 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -78,7 +78,6 @@ class ChatBot: groupinfo = message.message_info.group_info userinfo = message.message_info.user_info - if userinfo.user_id in global_config.ban_user_id: logger.debug(f"用户{userinfo.user_id}被禁止回复") return diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index 386d6ac7..aed0025b 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -328,7 +328,9 @@ def split_into_sentences_w_remove_punctuation(text: str) -> List[str]: final_sentences = [content for content, sep in merged_segments if content] # 只保留有内容的段 # 清理可能引入的空字符串和仅包含空白的字符串 - final_sentences = [s for s in final_sentences if s.strip()] # 过滤掉空字符串以及仅包含空白(如换行符、空格)的字符串 + final_sentences = [ + s for s in final_sentences if s.strip() + ] # 过滤掉空字符串以及仅包含空白(如换行符、空格)的字符串 logger.debug(f"分割并合并后的句子: {final_sentences}") return final_sentences diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 494ddeb0..41ea2711 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -2,6 +2,7 @@ import asyncio import time import traceback from typing import List, Optional, Dict, Any, TYPE_CHECKING + # import json # 移除,因为使用了json_utils from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageSet, Seg # Local import needed after move @@ -17,7 +18,7 @@ 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.utils.json_utils import extract_tool_call_arguments, safe_json_dumps, process_llm_tool_response # 导入新的JSON工具 +from src.plugins.utils.json_utils import process_llm_tool_response # 导入新的JSON工具 # --- End import --- @@ -37,7 +38,7 @@ if TYPE_CHECKING: # Keep this if HeartFCController methods are still needed elsewhere, # but the instance variable will be removed from HeartFChatting # from .heartFC_controler import HeartFCController - from src.heart_flow.heartflow import SubHeartflow, heartflow # <-- 同时导入 heartflow 实例用于类型检查 + from src.heart_flow.heartflow import SubHeartflow # <-- 同时导入 heartflow 实例用于类型检查 PLANNER_TOOL_DEFINITION = [ { @@ -327,7 +328,6 @@ class HeartFChatting: 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: @@ -424,7 +424,7 @@ class HeartFChatting: observed_messages: List[dict] = [] current_mind: Optional[str] = None - llm_error = False + llm_error = False try: observation = self.sub_hf._get_primary_observation() @@ -434,19 +434,17 @@ class HeartFChatting: except Exception as e: logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}") - try: current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply() except Exception as e_subhf: logger.error(f"{log_prefix}[Planner] SubHeartflow 思考失败: {e_subhf}") current_mind = "[思考时出错]" - # --- 使用 LLM 进行决策 --- # action = "no_reply" # 默认动作 - emoji_query = "" # 默认表情查询 - reasoning = "默认决策或获取决策失败" - llm_error = False # LLM错误标志 + emoji_query = "" # 默认表情查询 + reasoning = "默认决策或获取决策失败" + llm_error = False # LLM错误标志 try: prompt = await self._build_planner_prompt(observed_messages_str, current_mind, self.sub_hf.structured_info) @@ -475,21 +473,17 @@ class HeartFChatting: # 使用辅助函数处理工具调用响应 success, arguments, error_msg = process_llm_tool_response( - response, - expected_tool_name="decide_reply_action", - log_prefix=f"{log_prefix}[Planner] " + response, expected_tool_name="decide_reply_action", log_prefix=f"{log_prefix}[Planner] " ) - + if success: # 提取决策参数 action = arguments.get("action", "no_reply") reasoning = arguments.get("reasoning", "未提供理由") emoji_query = arguments.get("emoji_query", "") - + # 记录决策结果 - logger.debug( - f"{log_prefix}[Planner] 决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" - ) + logger.debug(f"{log_prefix}[Planner] 决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'") else: # 处理工具调用失败 logger.warning(f"{log_prefix}[Planner] {error_msg}") @@ -584,7 +578,7 @@ class HeartFChatting: """优雅关闭HeartFChatting实例,取消活动循环任务""" log_prefix = self._get_log_prefix() logger.info(f"{log_prefix} 正在关闭HeartFChatting...") - + # 取消循环任务 if self._loop_task and not self._loop_task.done(): logger.info(f"{log_prefix} 正在取消HeartFChatting循环任务") @@ -605,17 +599,19 @@ class HeartFChatting: if self._processing_lock.locked(): self._processing_lock.release() logger.warning(f"{log_prefix} 已释放处理锁") - + logger.info(f"{log_prefix} HeartFChatting关闭完成") - async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any]) -> str: + async def _build_planner_prompt( + self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any] + ) -> str: """构建 Planner LLM 的提示词""" prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。\n" if structured_info: prompt += f"以下是一些额外的信息:\n{structured_info}\n" - + if observed_messages_str: prompt += "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n" prompt += observed_messages_str diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index 0ed6229e..cbf050bd 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -72,7 +72,13 @@ class HeartFCGenerator: return None async def _generate_response_with_model( - self, structured_info: str, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str + self, + structured_info: str, + current_mind_info: str, + reason: str, + message: MessageRecv, + model: LLMRequest, + thinking_id: str, ) -> str: sender_name = "" diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 33baad37..c5b04ed9 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -81,13 +81,22 @@ class PromptBuilder: self.activate_messages = "" async def build_prompt( - self, build_mode, reason, current_mind_info, structured_info, message_txt: str, sender_name: str = "某人", chat_stream=None + self, + build_mode, + reason, + current_mind_info, + structured_info, + message_txt: str, + sender_name: str = "某人", + chat_stream=None, ) -> Optional[tuple[str, str]]: if build_mode == "normal": return await self._build_prompt_normal(chat_stream, message_txt, sender_name) elif build_mode == "focus": - return await self._build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, message_txt, sender_name) + return await self._build_prompt_focus( + reason, current_mind_info, structured_info, chat_stream, message_txt, sender_name + ) return None async def _build_prompt_focus( diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py index bdc408ab..2cab7b62 100644 --- a/src/plugins/models/utils_model.py +++ b/src/plugins/models/utils_model.py @@ -711,7 +711,7 @@ class LLMRequest: reasoning_content = "" content = "" tool_calls = None # 初始化工具调用变量 - + async for line_bytes in response.content: try: line = line_bytes.decode("utf-8").strip() @@ -733,7 +733,7 @@ class LLMRequest: if delta_content is None: delta_content = "" accumulated_content += delta_content - + # 提取工具调用信息 if "tool_calls" in delta: if tool_calls is None: @@ -741,7 +741,7 @@ class LLMRequest: else: # 合并工具调用信息 tool_calls.extend(delta["tool_calls"]) - + # 检测流式输出文本是否结束 finish_reason = chunk["choices"][0].get("finish_reason") if delta.get("reasoning_content", None): @@ -774,23 +774,19 @@ class LLMRequest: if think_match: reasoning_content = think_match.group(1).strip() content = re.sub(r".*?", "", content, flags=re.DOTALL).strip() - + # 构建消息对象 message = { "content": content, "reasoning_content": reasoning_content, } - + # 如果有工具调用,添加到消息中 if tool_calls: message["tool_calls"] = tool_calls - + result = { - "choices": [ - { - "message": message - } - ], + "choices": [{"message": message}], "usage": usage, } return result @@ -1128,9 +1124,9 @@ class LLMRequest: response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt) # 原样返回响应,不做处理 - + return response - + async def generate_response_tool_async(self, prompt: str, tools: list, **kwargs) -> Union[str, Tuple]: """异步方式根据输入的提示生成模型的响应""" # 构建请求体,不硬编码max_tokens @@ -1139,7 +1135,7 @@ class LLMRequest: "messages": [{"role": "user", "content": prompt}], **self.params, **kwargs, - "tools": tools + "tools": tools, } logger.debug(f"向模型 {self.model_name} 发送工具调用请求,包含 {len(tools)} 个工具") @@ -1150,7 +1146,7 @@ class LLMRequest: logger.debug(f"收到工具调用响应,包含 {len(tool_calls) if tool_calls else 0} 个工具调用") return content, reasoning_content, tool_calls else: - logger.debug(f"收到普通响应,无工具调用") + logger.debug("收到普通响应,无工具调用") return response async def get_embedding(self, text: str) -> Union[list, None]: diff --git a/src/plugins/utils/chat_message_builder.py b/src/plugins/utils/chat_message_builder.py index 6a5e4e8e..6ae6ccc3 100644 --- a/src/plugins/utils/chat_message_builder.py +++ b/src/plugins/utils/chat_message_builder.py @@ -303,7 +303,9 @@ 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: diff --git a/src/plugins/utils/json_utils.py b/src/plugins/utils/json_utils.py index 962901b5..bf4b0839 100644 --- a/src/plugins/utils/json_utils.py +++ b/src/plugins/utils/json_utils.py @@ -1,27 +1,28 @@ import json import logging -from typing import Any, Dict, Optional, TypeVar, Generic, List, Union, Callable, Tuple +from typing import Any, Dict, TypeVar, List, Union, Callable, Tuple # 定义类型变量用于泛型类型提示 -T = TypeVar('T') +T = TypeVar("T") # 获取logger logger = logging.getLogger("json_utils") + def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]: """ 安全地解析JSON字符串,出错时返回默认值 - + 参数: json_str: 要解析的JSON字符串 default_value: 解析失败时返回的默认值 - + 返回: 解析后的Python对象,或在解析失败时返回default_value """ if not json_str: return default_value - + try: return json.loads(json_str) except json.JSONDecodeError as e: @@ -31,66 +32,67 @@ def safe_json_loads(json_str: str, default_value: T = None) -> Union[Any, T]: logger.error(f"JSON解析过程中发生意外错误: {e}") return default_value -def extract_tool_call_arguments(tool_call: Dict[str, Any], - default_value: Dict[str, Any] = None) -> Dict[str, Any]: + +def extract_tool_call_arguments(tool_call: Dict[str, Any], default_value: Dict[str, Any] = None) -> Dict[str, Any]: """ 从LLM工具调用对象中提取参数 - + 参数: tool_call: 工具调用对象字典 default_value: 解析失败时返回的默认值 - + 返回: 解析后的参数字典,或在解析失败时返回default_value """ default_result = default_value or {} - + if not tool_call or not isinstance(tool_call, dict): logger.error(f"无效的工具调用对象: {tool_call}") return default_result - + try: # 提取function参数 function_data = tool_call.get("function", {}) if not function_data or not isinstance(function_data, dict): logger.error(f"工具调用缺少function字段或格式不正确: {tool_call}") return default_result - + # 提取arguments arguments_str = function_data.get("arguments", "{}") if not arguments_str: return default_result - + # 解析JSON return safe_json_loads(arguments_str, default_result) - + except Exception as e: logger.error(f"提取工具调用参数时出错: {e}") return default_result -def get_json_value(json_obj: Dict[str, Any], key_path: str, - default_value: T = None, - transform_func: Callable[[Any], T] = None) -> Union[Any, T]: + +def get_json_value( + json_obj: Dict[str, Any], key_path: str, default_value: T = None, transform_func: Callable[[Any], T] = None +) -> Union[Any, T]: """ 从JSON对象中按照路径提取值,支持点表示法路径,如"data.items.0.name" - + 参数: json_obj: JSON对象(已解析的字典) key_path: 键路径,使用点表示法,如"data.items.0.name" default_value: 获取失败时返回的默认值 transform_func: 可选的转换函数,用于对获取的值进行转换 - + 返回: 路径指向的值,或在获取失败时返回default_value """ if not json_obj or not key_path: return default_value - + try: # 分割路径 keys = key_path.split(".") current = json_obj - + # 遍历路径 for key in keys: # 处理数组索引 @@ -108,7 +110,7 @@ def get_json_value(json_obj: Dict[str, Any], key_path: str, return default_value else: return default_value - + # 应用转换函数(如果提供) if transform_func and current is not None: return transform_func(current) @@ -117,17 +119,17 @@ def get_json_value(json_obj: Dict[str, Any], key_path: str, logger.error(f"从JSON获取值时出错: {e}, 路径: {key_path}") return default_value -def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = False, - pretty: bool = False) -> str: + +def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = False, pretty: bool = False) -> str: """ 安全地将Python对象序列化为JSON字符串 - + 参数: obj: 要序列化的Python对象 default_value: 序列化失败时返回的默认值 ensure_ascii: 是否确保ASCII编码(默认False,允许中文等非ASCII字符) pretty: 是否美化输出JSON - + 返回: 序列化后的JSON字符串,或在序列化失败时返回default_value """ @@ -141,13 +143,14 @@ def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = Fa logger.error(f"JSON序列化过程中发生意外错误: {e}") return default_value + def merge_json_objects(*objects: Dict[str, Any]) -> Dict[str, Any]: """ 合并多个JSON对象(字典) - + 参数: *objects: 要合并的JSON对象(字典) - + 返回: 合并后的字典,后面的对象会覆盖前面对象的相同键 """ @@ -157,109 +160,110 @@ def merge_json_objects(*objects: Dict[str, Any]) -> Dict[str, Any]: result.update(obj) return result + def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, List[Any], str]: """ 标准化LLM响应格式,将各种格式(如元组)转换为统一的列表格式 - + 参数: response: 原始LLM响应 log_prefix: 日志前缀 - + 返回: 元组 (成功标志, 标准化后的响应列表, 错误消息) """ # 检查是否为None if response is None: return False, [], "LLM响应为None" - + # 记录原始类型 logger.debug(f"{log_prefix}LLM响应原始类型: {type(response).__name__}") - + # 将元组转换为列表 if isinstance(response, tuple): logger.debug(f"{log_prefix}将元组响应转换为列表") response = list(response) - + # 确保是列表类型 if not isinstance(response, list): return False, [], f"无法处理的LLM响应类型: {type(response).__name__}" - + # 处理工具调用部分(如果存在) if len(response) == 3: content, reasoning, tool_calls = response - + # 将工具调用部分转换为列表(如果是元组) if isinstance(tool_calls, tuple): logger.debug(f"{log_prefix}将工具调用元组转换为列表") tool_calls = list(tool_calls) response[2] = tool_calls - + return True, response, "" + def process_llm_tool_calls(response: List[Any], log_prefix: str = "") -> Tuple[bool, List[Dict[str, Any]], str]: """ 处理并提取LLM响应中的工具调用列表 - + 参数: response: 标准化后的LLM响应列表 log_prefix: 日志前缀 - + 返回: 元组 (成功标志, 工具调用列表, 错误消息) """ # 确保响应格式正确 if len(response) != 3: return False, [], f"LLM响应元素数量不正确: 预期3个元素,实际{len(response)}个" - + # 提取工具调用部分 tool_calls = response[2] - + # 检查工具调用是否有效 if tool_calls is None: return False, [], "工具调用部分为None" - + if not isinstance(tool_calls, list): return False, [], f"工具调用部分不是列表: {type(tool_calls).__name__}" - + if len(tool_calls) == 0: return False, [], "工具调用列表为空" - + # 检查工具调用是否格式正确 valid_tool_calls = [] for i, tool_call in enumerate(tool_calls): if not isinstance(tool_call, dict): logger.warning(f"{log_prefix}工具调用[{i}]不是字典: {type(tool_call).__name__}") continue - + if tool_call.get("type") != "function": logger.warning(f"{log_prefix}工具调用[{i}]不是函数类型: {tool_call.get('type', '未知')}") continue - + if "function" not in tool_call or not isinstance(tool_call["function"], dict): logger.warning(f"{log_prefix}工具调用[{i}]缺少function字段或格式不正确") continue - + valid_tool_calls.append(tool_call) - + # 检查是否有有效的工具调用 if not valid_tool_calls: return False, [], "没有找到有效的工具调用" - + return True, valid_tool_calls, "" + def process_llm_tool_response( - response: Any, - expected_tool_name: str = None, - log_prefix: str = "" + response: Any, expected_tool_name: str = None, log_prefix: str = "" ) -> Tuple[bool, Dict[str, Any], str]: """ 处理LLM返回的工具调用响应,进行常见错误检查并提取参数 - + 参数: response: LLM的响应,预期是[content, reasoning, tool_calls]格式的列表或元组 expected_tool_name: 预期的工具名称,如不指定则不检查 log_prefix: 日志前缀,用于标识日志来源 - + 返回: 三元组(成功标志, 参数字典, 错误描述) - 如果成功解析,返回(True, 参数字典, "") @@ -269,29 +273,29 @@ def process_llm_tool_response( success, normalized_response, error_msg = normalize_llm_response(response, log_prefix) if not success: return False, {}, error_msg - + # 使用新的工具调用处理函数 success, valid_tool_calls, error_msg = process_llm_tool_calls(normalized_response, log_prefix) if not success: return False, {}, error_msg - + # 检查是否有工具调用 if not valid_tool_calls: return False, {}, "没有有效的工具调用" - + # 获取第一个工具调用 tool_call = valid_tool_calls[0] - + # 检查工具名称(如果提供了预期名称) if expected_tool_name: actual_name = tool_call.get("function", {}).get("name") if actual_name != expected_tool_name: return False, {}, f"工具名称不匹配: 预期'{expected_tool_name}',实际'{actual_name}'" - + # 提取并解析参数 try: arguments = extract_tool_call_arguments(tool_call, {}) return True, arguments, "" except Exception as e: logger.error(f"{log_prefix}解析工具参数时出错: {e}") - return False, {}, f"解析参数失败: {str(e)}" \ No newline at end of file + return False, {}, f"解析参数失败: {str(e)}" diff --git a/tool_call_benchmark.py b/tool_call_benchmark.py index 691aeb7c..e756d1da 100644 --- a/tool_call_benchmark.py +++ b/tool_call_benchmark.py @@ -6,24 +6,25 @@ from src.do_tool.tool_use import ToolUser import statistics import json + async def run_test(test_name, test_function, iterations=5): """ 运行指定次数的测试并计算平均响应时间 - + 参数: test_name: 测试名称 test_function: 要执行的测试函数 iterations: 测试迭代次数 - + 返回: 测试结果统计 """ print(f"开始 {test_name} 测试({iterations}次迭代)...") times = [] responses = [] - + for i in range(iterations): - print(f" 运行第 {i+1}/{iterations} 次测试...") + print(f" 运行第 {i + 1}/{iterations} 次测试...") start_time = time.time() response = await test_function() end_time = time.time() @@ -31,18 +32,19 @@ async def run_test(test_name, test_function, iterations=5): times.append(elapsed) responses.append(response) print(f" - 耗时: {elapsed:.2f}秒") - + results = { "平均耗时": statistics.mean(times), "最短耗时": min(times), "最长耗时": max(times), "标准差": statistics.stdev(times) if len(times) > 1 else 0, "所有耗时": times, - "响应结果": responses + "响应结果": responses, } - + return results + async def test_with_tool_calls(): """使用工具调用的LLM请求测试""" # 创建LLM模型实例 @@ -53,14 +55,14 @@ async def test_with_tool_calls(): max_tokens=800, request_type="benchmark_test", ) - + # 创建工具实例 tool_instance = ToolUser() tools = tool_instance._define_tools() - + # 简单的测试提示词 prompt = "请分析当前天气情况,并查询今日历史上的重要事件。并且3.9和3.11谁比较大?请使用适当的工具来获取这些信息。" - prompt = ''' + prompt = """ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会想瑟瑟,喜欢刷小红书 ----------------------------------- 现在是2025-04-24 12:37:00,你正在上网,和qq群里的网友们聊天,群里正在聊的话题是: @@ -89,52 +91,47 @@ async def test_with_tool_calls(): 回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题 请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。 现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸 -在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。''' - +在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" + # 发送带有工具调用的请求 response = await llm_model.generate_response_tool_async(prompt=prompt, tools=tools) - + result_info = {} - + # 简单处理工具调用结果 if len(response) == 3: content, reasoning_content, tool_calls = response tool_calls_count = len(tool_calls) if tool_calls else 0 print(f" 工具调用请求生成了 {tool_calls_count} 个工具调用") - + # 输出内容和工具调用详情 print("\n 生成的内容:") print(f" {content[:200]}..." if len(content) > 200 else f" {content}") - + if tool_calls: print("\n 工具调用详情:") for i, tool_call in enumerate(tool_calls): - tool_name = tool_call['function']['name'] - tool_params = tool_call['function'].get('arguments', {}) - print(f" - 工具 {i+1}: {tool_name}") - print(f" 参数: {json.dumps(tool_params, ensure_ascii=False)[:100]}..." - if len(json.dumps(tool_params, ensure_ascii=False)) > 100 - else f" 参数: {json.dumps(tool_params, ensure_ascii=False)}") - - result_info = { - "内容": content, - "推理内容": reasoning_content, - "工具调用": tool_calls - } + tool_name = tool_call["function"]["name"] + tool_params = tool_call["function"].get("arguments", {}) + print(f" - 工具 {i + 1}: {tool_name}") + print( + f" 参数: {json.dumps(tool_params, ensure_ascii=False)[:100]}..." + if len(json.dumps(tool_params, ensure_ascii=False)) > 100 + else f" 参数: {json.dumps(tool_params, ensure_ascii=False)}" + ) + + result_info = {"内容": content, "推理内容": reasoning_content, "工具调用": tool_calls} else: content, reasoning_content = response print(" 工具调用请求未生成任何工具调用") print("\n 生成的内容:") print(f" {content[:200]}..." if len(content) > 200 else f" {content}") - - result_info = { - "内容": content, - "推理内容": reasoning_content, - "工具调用": [] - } - + + result_info = {"内容": content, "推理内容": reasoning_content, "工具调用": []} + return result_info + async def test_without_tool_calls(): """不使用工具调用的LLM请求测试""" # 创建LLM模型实例 @@ -144,9 +141,9 @@ async def test_without_tool_calls(): max_tokens=800, request_type="benchmark_test", ) - + # 简单的测试提示词(与工具调用相同,以便公平比较) - prompt = ''' + prompt = """ 你的名字是麦麦,你包容开放,情绪敏感,有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生,现在在读大二,你会刷贴吧,有时候会想瑟瑟,喜欢刷小红书 刚刚你的想法是: 我是麦麦,我想,('小千石问3.8和3.11谁大,已经简单回答了3.11大,现在可以继续聊猫猫头表情包,毕竟大家好像对版本问题兴趣不大,而且猫猫头的话题更轻松有趣。', '') @@ -181,45 +178,42 @@ async def test_without_tool_calls(): 回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题 请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。 现在请你继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,不要分点输出,生成内心想法,文字不要浮夸 -在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。''' - +在输出完想法后,请你思考应该使用什么工具,如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。""" + # 发送不带工具调用的请求 response, reasoning_content = await llm_model.generate_response_async(prompt) - + # 输出生成的内容 print("\n 生成的内容:") print(f" {response[:200]}..." if len(response) > 200 else f" {response}") - - result_info = { - "内容": response, - "推理内容": reasoning_content, - "工具调用": [] - } - + + result_info = {"内容": response, "推理内容": reasoning_content, "工具调用": []} + return result_info + async def main(): """主测试函数""" print("=" * 50) print("LLM工具调用与普通请求性能比较测试") print("=" * 50) - + # 设置测试迭代次数 iterations = 3 - + # 测试不使用工具调用 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) - + # 显示结果比较 print("\n" + "=" * 50) print("测试结果比较") print("=" * 50) - + print("\n不使用工具调用:") for key, value in results_without_tools.items(): if key == "所有耗时": @@ -228,7 +222,7 @@ async def main(): print(f" {key}: [内容已省略,详见结果文件]") else: print(f" {key}: {value:.2f}秒") - + print("\n使用工具调用:") for key, value in results_with_tools.items(): if key == "所有耗时": @@ -239,29 +233,30 @@ async def main(): print(f" 工具调用数量: {tool_calls_counts}") else: print(f" {key}: {value:.2f}秒") - + # 计算差异百分比 diff_percent = ((results_with_tools["平均耗时"] / results_without_tools["平均耗时"]) - 1) * 100 print(f"\n工具调用比普通请求平均耗时相差: {diff_percent:.2f}%") - + # 保存结果到JSON文件 results = { "测试时间": time.strftime("%Y-%m-%d %H:%M:%S"), "测试迭代次数": iterations, "不使用工具调用": { - k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v]) - for k, v in results_without_tools.items() + k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v]) + for k, v in results_without_tools.items() if k != "响应结果" }, "不使用工具调用_详细响应": [ { "内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"], - "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"] - } for resp in results_without_tools["响应结果"] + "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"], + } + for resp in results_without_tools["响应结果"] ], "使用工具调用": { - k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v]) - for k, v in results_with_tools.items() + k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v]) + for k, v in results_with_tools.items() if k != "响应结果" }, "使用工具调用_详细响应": [ @@ -270,20 +265,20 @@ async def main(): "推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"], "工具调用数量": len(resp["工具调用"]), "工具调用详情": [ - { - "工具名称": tool["function"]["name"], - "参数": tool["function"].get("arguments", {}) - } for tool in resp["工具调用"] - ] - } for resp in results_with_tools["响应结果"] + {"工具名称": tool["function"]["name"], "参数": tool["function"].get("arguments", {})} + for tool in resp["工具调用"] + ], + } + for resp in results_with_tools["响应结果"] ], - "差异百分比": float(f"{diff_percent:.2f}") + "差异百分比": float(f"{diff_percent:.2f}"), } - + with open("llm_tool_benchmark_results.json", "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) - - print(f"\n测试结果已保存到 llm_tool_benchmark_results.json") + + print("\n测试结果已保存到 llm_tool_benchmark_results.json") + if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From a89be639d0e2924d6ef294788fbdd703bb2551aa Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 14:20:42 +0800 Subject: [PATCH 16/73] Update bot.py --- src/plugins/chat/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 4517138c..1202fce2 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -83,7 +83,7 @@ class ChatBot: return if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups: - logger.debug(f"群{groupinfo.group_id}被禁止回复") + logger.trace(f"群{groupinfo.group_id}被禁止回复") return if message.message_info.template_info and not message.message_info.template_info.template_default: From bb333e8febed16ea670fd74e5619a1693a077bfa Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 14:41:49 +0800 Subject: [PATCH 17/73] =?UTF-8?q?feat=EF=BC=9A=E6=8B=86=E5=88=86=E5=AD=90?= =?UTF-8?q?=E5=BF=83=E6=B5=81=E7=9A=84=E6=80=9D=E8=80=83=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/chat_state_info.py | 17 ++ src/heart_flow/sub_heartflow.py | 254 ++--------------------- src/heart_flow/sub_mind.py | 254 +++++++++++++++++++++++ src/heart_flow/subheartflow_manager.py | 2 +- src/plugins/heartFC_chat/heartFC_chat.py | 45 ++-- 5 files changed, 306 insertions(+), 266 deletions(-) create mode 100644 src/heart_flow/chat_state_info.py create mode 100644 src/heart_flow/sub_mind.py diff --git a/src/heart_flow/chat_state_info.py b/src/heart_flow/chat_state_info.py new file mode 100644 index 00000000..78fb4e8d --- /dev/null +++ b/src/heart_flow/chat_state_info.py @@ -0,0 +1,17 @@ +from src.plugins.moods.moods import MoodManager +import enum + + +class ChatState(enum.Enum): + ABSENT = "没在看群" + CHAT = "随便水群" + FOCUSED = "激情水群" + + +class ChatStateInfo: + def __init__(self): + self.chat_status: ChatState = ChatState.ABSENT + self.current_state_time = 120 + + self.mood_manager = MoodManager() + self.mood = self.mood_manager.get_prompt() \ No newline at end of file diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 1aa6f902..bb671913 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -1,24 +1,20 @@ from .observation import Observation, ChattingObservation import asyncio -from src.plugins.moods.moods import MoodManager -from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config import time from typing import Optional, List, Dict, Callable import traceback -import enum from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 -from src.individuality.individuality import Individuality import random -from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.plugins.chat.message import MessageRecv from src.plugins.chat.chat_stream import chat_manager import math from src.plugins.heartFC_chat.heartFC_chat import HeartFChatting from src.plugins.heartFC_chat.normal_chat import NormalChat -from src.do_tool.tool_use import ToolUser from src.heart_flow.mai_state_manager import MaiStateInfo -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 ChatState, ChatStateInfo +from src.heart_flow.sub_mind import SubMind + # 定义常量 (从 interest.py 移动过来) MAX_INTEREST = 15.0 @@ -37,42 +33,6 @@ interest_log_config = LogConfig( interest_logger = get_module_logger("InterestChatting", config=interest_log_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 += "-----------------------------------\n" - prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:\n{chat_observe_info}\n" - prompt += "\n你现在{mood_info}\n" - # prompt += "你注意到{sender_name}刚刚说:{message_txt}\n" - prompt += "现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。\n" - prompt += "回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题\n" - prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。\n" - prompt += "现在请你先{hf_do_next},不要分点输出,生成内心想法,文字不要浮夸" - prompt += "在输出完想法后,请你思考应该使用什么工具。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" - - Prompt(prompt, "sub_heartflow_prompt_before") - - -class ChatState(enum.Enum): - ABSENT = "没在看群" - CHAT = "随便水群" - FOCUSED = "激情水群" - - -class ChatStateInfo: - def __init__(self): - self.chat_status: ChatState = ChatState.ABSENT - self.current_state_time = 120 - - self.mood_manager = MoodManager() - self.mood = self.mood_manager.get_prompt() - - base_reply_probability = 0.05 probability_increase_rate_per_second = 0.08 max_reply_probability = 1 @@ -112,7 +72,7 @@ class InterestChatting: self.above_threshold = False self.start_hfc_probability = 0.0 - + 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() @@ -259,15 +219,11 @@ class SubHeartflow: self.mai_states = mai_states - # 思维状态相关 - self.current_mind = "什么也没想" # 当前想法 - self.past_mind = [] # 历史想法记录 - # 聊天状态管理 self.chat_state: ChatStateInfo = ChatStateInfo() # 该sub_heartflow的聊天状态信息 self.interest_chatting = InterestChatting( state_change_callback=self.set_chat_state - ) # 该sub_heartflow的兴趣系统 + ) # 活动状态管理 self.last_active_time = time.time() # 最后活跃时间 @@ -281,16 +237,15 @@ class SubHeartflow: self.running_knowledges = [] # 运行中的知识 # LLM模型配置 - self.llm_model = LLMRequest( - model=global_config.llm_sub_heartflow, - temperature=global_config.llm_sub_heartflow["temp"], - max_tokens=800, - request_type="sub_heart_flow", + 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 - self.structured_info = {} + self.log_prefix = chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id + async def add_time_current_state(self, add_time: float): self.current_state_time += add_time @@ -381,6 +336,8 @@ class SubHeartflow: try: self.heart_fc_instance = HeartFChatting( chat_id=self.chat_id, + sub_mind=self.sub_mind, + observations=self.observations ) if await self.heart_fc_instance._initialize(): await self.heart_fc_instance.start() # 初始化成功后启动循环 @@ -477,188 +434,9 @@ class SubHeartflow: logger.info(f"{self.log_prefix} 子心流后台任务已停止。") - async def do_thinking_before_reply(self): - """ - 在回复前进行思考,生成内心想法并收集工具调用结果 - - 返回: - tuple: (current_mind, past_mind) 当前想法和过去的想法列表 - """ - # 更新活跃时间 - self.last_active_time = time.time() - - # ---------- 1. 准备基础数据 ---------- - # 获取现有想法和情绪状态 - current_thinking_info = self.current_mind - mood_info = self.chat_state.mood - - # 获取观察对象 - observation = self._get_primary_observation() - if not observation: - logger.error(f"[{self.subheartflow_id}] 无法获取观察对象") - self.update_current_mind("(我没看到任何聊天内容...)") - return self.current_mind, self.past_mind - - # 获取观察内容 - chat_observe_info = observation.get_observe_info() - - # ---------- 2. 准备工具和个性化数据 ---------- - # 初始化工具 - tool_instance = ToolUser() - tools = tool_instance._define_tools() - - # 获取个性化信息 - 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}" - - # 获取当前时间 - time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - - # ---------- 3. 构建思考指导部分 ---------- - # 创建本地随机数生成器,基于分钟数作为种子 - local_random = random.Random() - current_minute = int(time.strftime("%M")) - local_random.seed(current_minute) - - # 思考指导选项和权重 - hf_options = [ - ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考", 0.7), - ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1), - ("生成你在这个聊天中的想法,不要太深入", 0.1), - ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), - ] - - # 加权随机选择思考指导 - hf_do_next = local_random.choices( - [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 - )[0] - - # ---------- 4. 构建最终提示词 ---------- - # 获取提示词模板并填充数据 - prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( - 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, - ) - - logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") - - # ---------- 5. 执行LLM请求并处理响应 ---------- - content = "" # 初始化内容变量 - reasoning_content = "" # 初始化推理内容变量 - - try: - # 调用LLM生成响应 - response = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools) - - # 标准化响应格式 - success, normalized_response, error_msg = normalize_llm_response( - response, log_prefix=f"[{self.subheartflow_id}] " - ) - - if not success: - # 处理标准化失败情况 - logger.warning(f"[{self.subheartflow_id}] {error_msg}") - content = "LLM响应格式无法处理" - else: - # 从标准化响应中提取内容 - if len(normalized_response) >= 2: - content = normalized_response[0] - _reasoning_content = normalized_response[1] if len(normalized_response) > 1 else "" - - # 处理可能的工具调用 - if len(normalized_response) == 3: - # 提取并验证工具调用 - success, valid_tool_calls, error_msg = process_llm_tool_calls( - normalized_response, log_prefix=f"[{self.subheartflow_id}] " - ) - - if success and valid_tool_calls: - # 记录工具调用信息 - tool_calls_str = ", ".join( - [call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls] - ) - logger.info( - f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}" - ) - - # 收集工具执行结果 - await self._execute_tool_calls(valid_tool_calls, tool_instance) - elif not success: - logger.warning(f"[{self.subheartflow_id}] {error_msg}") - except Exception as e: - # 处理总体异常 - logger.error(f"[{self.subheartflow_id}] 执行LLM请求或处理响应时出错: {e}") - logger.error(traceback.format_exc()) - content = "思考过程中出现错误" - - # 记录最终思考结果 - logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{content}\n") - - # 处理空响应情况 - if not content: - content = "(不知道该想些什么...)" - logger.warning(f"[{self.subheartflow_id}] LLM返回空结果,思考失败。") - - # ---------- 6. 更新思考状态并返回结果 ---------- - # 更新当前思考内容 - self.update_current_mind(content) - - return self.current_mind, self.past_mind - - async def _execute_tool_calls(self, tool_calls, tool_instance): - """ - 执行一组工具调用并收集结果 - - 参数: - tool_calls: 工具调用列表 - tool_instance: 工具使用器实例 - """ - tool_results = [] - structured_info = {} # 动态生成键 - - # 执行所有工具调用 - for tool_call in tool_calls: - try: - result = await tool_instance._execute_tool_call(tool_call) - if result: - tool_results.append(result) - - # 使用工具名称作为键 - tool_name = result["name"] - if tool_name not in structured_info: - structured_info[tool_name] = [] - - structured_info[tool_name].append({"name": result["name"], "content": result["content"]}) - except Exception as tool_e: - logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") - - # 如果有工具结果,记录并更新结构化信息 - if structured_info: - logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") - self.structured_info = structured_info def update_current_mind(self, response): - self.past_mind.append(self.current_mind) - self.current_mind = response + self.sub_mind.update_current_mind(response) def add_observation(self, observation: Observation): for existing_obs in self.observations: @@ -705,7 +483,7 @@ class SubHeartflow: interest_state = await self.get_interest_state() return { "interest_state": interest_state, - "current_mind": self.current_mind, + "current_mind": self.sub_mind.current_mind, "chat_state": self.chat_state.chat_status.value, "last_active_time": self.last_active_time, } @@ -747,4 +525,4 @@ class SubHeartflow: logger.info(f"{self.log_prefix} 子心流关闭完成。") -init_prompt() + diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py new file mode 100644 index 00000000..03a9997f --- /dev/null +++ b/src/heart_flow/sub_mind.py @@ -0,0 +1,254 @@ +from .observation import Observation +from src.plugins.models.utils_model import LLMRequest +from src.config.config import global_config +import time +import traceback +from src.common.logger import get_module_logger, LogConfig, SUB_HEARTFLOW_STYLE_CONFIG # noqa: E402 +from src.individuality.individuality import Individuality +import random +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 + + +subheartflow_config = LogConfig( + console_format=SUB_HEARTFLOW_STYLE_CONFIG["console_format"], + file_format=SUB_HEARTFLOW_STYLE_CONFIG["file_format"], +) +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 += "-----------------------------------\n" + prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:\n{chat_observe_info}\n" + prompt += "\n你现在{mood_info}\n" + # prompt += "你注意到{sender_name}刚刚说:{message_txt}\n" + prompt += "现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。\n" + prompt += "回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题\n" + prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。\n" + prompt += "现在请你先{hf_do_next},不要分点输出,生成内心想法,文字不要浮夸" + prompt += "在输出完想法后,请你思考应该使用什么工具。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" + + Prompt(prompt, "sub_heartflow_prompt_before") + + + +class SubMind: + def __init__( + self, + subheartflow_id: str, + chat_state: ChatStateInfo, + observations: Observation + ): + self.subheartflow_id = subheartflow_id + + self.llm_model = LLMRequest( + model=global_config.llm_sub_heartflow, + temperature=global_config.llm_sub_heartflow["temp"], + max_tokens=800, + request_type="sub_heart_flow", + ) + + self.chat_state = chat_state + self.observations = observations + + self.current_mind = "" + self.past_mind = [] + self.structured_info = {} + + async def do_thinking_before_reply(self): + """ + 在回复前进行思考,生成内心想法并收集工具调用结果 + + 返回: + tuple: (current_mind, past_mind) 当前想法和过去的想法列表 + """ + # 更新活跃时间 + self.last_active_time = time.time() + + # ---------- 1. 准备基础数据 ---------- + # 获取现有想法和情绪状态 + current_thinking_info = self.current_mind + mood_info = self.chat_state.mood + + # 获取观察对象 + observation = self.observations[0] + if not observation: + logger.error(f"[{self.subheartflow_id}] 无法获取观察对象") + self.update_current_mind("(我没看到任何聊天内容...)") + return self.current_mind, self.past_mind + + # 获取观察内容 + chat_observe_info = observation.get_observe_info() + + # ---------- 2. 准备工具和个性化数据 ---------- + # 初始化工具 + tool_instance = ToolUser() + tools = tool_instance._define_tools() + + # 获取个性化信息 + 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}" + + # 获取当前时间 + time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + # ---------- 3. 构建思考指导部分 ---------- + # 创建本地随机数生成器,基于分钟数作为种子 + local_random = random.Random() + current_minute = int(time.strftime("%M")) + local_random.seed(current_minute) + + # 思考指导选项和权重 + hf_options = [ + ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考", 0.7), + ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1), + ("生成你在这个聊天中的想法,不要太深入", 0.1), + ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), + ] + + # 加权随机选择思考指导 + hf_do_next = local_random.choices( + [option[0] for option in hf_options], weights=[option[1] for option in hf_options], k=1 + )[0] + + # ---------- 4. 构建最终提示词 ---------- + # 获取提示词模板并填充数据 + prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( + 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, + ) + + logger.debug(f"[{self.subheartflow_id}] 心流思考提示词构建完成") + + # ---------- 5. 执行LLM请求并处理响应 ---------- + content = "" # 初始化内容变量 + reasoning_content = "" # 初始化推理内容变量 + + try: + # 调用LLM生成响应 + response = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools) + + # 标准化响应格式 + success, normalized_response, error_msg = normalize_llm_response( + response, log_prefix=f"[{self.subheartflow_id}] " + ) + + if not success: + # 处理标准化失败情况 + logger.warning(f"[{self.subheartflow_id}] {error_msg}") + content = "LLM响应格式无法处理" + else: + # 从标准化响应中提取内容 + if len(normalized_response) >= 2: + content = normalized_response[0] + _reasoning_content = normalized_response[1] if len(normalized_response) > 1 else "" + + # 处理可能的工具调用 + if len(normalized_response) == 3: + # 提取并验证工具调用 + success, valid_tool_calls, error_msg = process_llm_tool_calls( + normalized_response, log_prefix=f"[{self.subheartflow_id}] " + ) + + if success and valid_tool_calls: + # 记录工具调用信息 + tool_calls_str = ", ".join( + [call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls] + ) + logger.info( + f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}" + ) + + # 收集工具执行结果 + await self._execute_tool_calls(valid_tool_calls, tool_instance) + elif not success: + logger.warning(f"[{self.subheartflow_id}] {error_msg}") + except Exception as e: + # 处理总体异常 + logger.error(f"[{self.subheartflow_id}] 执行LLM请求或处理响应时出错: {e}") + logger.error(traceback.format_exc()) + content = "思考过程中出现错误" + + # 记录最终思考结果 + logger.debug(f"[{self.subheartflow_id}] 心流思考结果:\n{content}\n") + + # 处理空响应情况 + if not content: + content = "(不知道该想些什么...)" + logger.warning(f"[{self.subheartflow_id}] LLM返回空结果,思考失败。") + + # ---------- 6. 更新思考状态并返回结果 ---------- + # 更新当前思考内容 + self.update_current_mind(content) + + return self.current_mind, self.past_mind + + + async def _execute_tool_calls(self, tool_calls, tool_instance): + """ + 执行一组工具调用并收集结果 + + 参数: + tool_calls: 工具调用列表 + tool_instance: 工具使用器实例 + """ + tool_results = [] + structured_info = {} # 动态生成键 + + # 执行所有工具调用 + for tool_call in tool_calls: + try: + result = await tool_instance._execute_tool_call(tool_call) + if result: + tool_results.append(result) + + # 使用工具名称作为键 + tool_name = result["name"] + if tool_name not in structured_info: + structured_info[tool_name] = [] + + structured_info[tool_name].append({"name": result["name"], "content": result["content"]}) + except Exception as tool_e: + logger.error(f"[{self.subheartflow_id}] 工具执行失败: {tool_e}") + + # 如果有工具结果,记录并更新结构化信息 + if structured_info: + logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") + self.structured_info = structured_info + + + def update_current_mind(self, response): + self.past_mind.append(self.current_mind) + self.current_mind = response + + +init_prompt() \ No newline at end of file diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index bf473b78..b9703e53 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -456,7 +456,7 @@ class SubHeartflowManager: for subheartflow in self.subheartflows.values(): # 检查子心流是否活跃(非ABSENT状态) if subheartflow.chat_state.chat_status != ChatState.ABSENT: - minds.append(subheartflow.current_mind) + minds.append(subheartflow.sub_mind.current_mind) return minds def update_main_mind_in_subflows(self, main_mind: str): diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 41ea2711..3229317f 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -1,9 +1,7 @@ import asyncio import time import traceback -from typing import List, Optional, Dict, Any, TYPE_CHECKING - -# import json # 移除,因为使用了json_utils +from typing import List, Optional, Dict, Any 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 @@ -19,6 +17,8 @@ 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.utils.json_utils import process_llm_tool_response # 导入新的JSON工具 +from src.heart_flow.sub_mind import SubMind +from src.heart_flow.observation import Observation # --- End import --- @@ -33,13 +33,6 @@ interest_log_config = LogConfig( logger = get_module_logger("HeartFCLoop", config=interest_log_config) # Logger Name Changed -# Forward declaration for type hinting -if TYPE_CHECKING: - # Keep this if HeartFCController methods are still needed elsewhere, - # but the instance variable will be removed from HeartFChatting - # from .heartFC_controler import HeartFCController - from src.heart_flow.heartflow import SubHeartflow # <-- 同时导入 heartflow 实例用于类型检查 - PLANNER_TOOL_DEFINITION = [ { "type": "function", @@ -74,7 +67,12 @@ class HeartFChatting: 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ - def __init__(self, chat_id: str): + def __init__( + self, + chat_id: str, + sub_mind: SubMind, + observations: Observation + ): """ HeartFChatting 初始化函数 @@ -84,7 +82,8 @@ class HeartFChatting: # 基础属性 self.stream_id: str = chat_id # 聊天流ID self.chat_stream: Optional[ChatStream] = None # 关联的聊天流 - self.sub_hf: SubHeartflow = None # 关联的子心流 + self.sub_mind: SubMind = sub_mind # 关联的子思维 + self.observations: Observation = observations # 关联的观察 # 初始化状态控制 self._initialized = False # 是否已初始化标志 @@ -121,18 +120,10 @@ class HeartFChatting: 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 - # <-- 在这里导入 heartflow 实例 - from src.heart_flow.heartflow import heartflow - - self.sub_hf = heartflow.get_subheartflow(self.stream_id) - if not self.sub_hf: - logger.warning(f"{log_prefix} 获取SubHeartflow失败。一些功能可能受限。") - self._initialized = True logger.info(f"麦麦感觉到了,激发了HeartFChatting{log_prefix} 初始化成功。") return True @@ -321,8 +312,8 @@ class HeartFChatting: # --- 新增:等待新消息 --- logger.debug(f"{log_prefix} HeartFChatting: 开始等待新消息 (自 {planner_start_db_time})...") observation = None - if self.sub_hf: - observation = self.sub_hf._get_primary_observation() + + observation = self.observations[0] if observation: with Timer("Wait New Msg", cycle_timers): # <--- Start Wait timer @@ -427,7 +418,7 @@ class HeartFChatting: llm_error = False try: - observation = self.sub_hf._get_primary_observation() + observation = self.observations[0] await observation.observe() observed_messages = observation.talking_message observed_messages_str = observation.talking_message_str @@ -435,7 +426,7 @@ class HeartFChatting: logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}") try: - current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply() + 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 = "[思考时出错]" @@ -447,7 +438,7 @@ class HeartFChatting: llm_error = False # LLM错误标志 try: - prompt = await self._build_planner_prompt(observed_messages_str, current_mind, self.sub_hf.structured_info) + 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}], @@ -655,8 +646,8 @@ class HeartFChatting: response_set: Optional[List[str]] = None try: response_set = await self.gpt_instance.generate_response( - structured_info=self.sub_hf.structured_info, - current_mind_info=self.sub_hf.current_mind, + structured_info=self.sub_mind.structured_info, + current_mind_info=self.sub_mind.current_mind, reason=reason, message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter) thinking_id=thinking_id, # Pass thinking_id positionally From 3c736f22e4a3009e9df97c3e700171552c0c810a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 14:50:34 +0800 Subject: [PATCH 18/73] fixL: --- src/heart_flow/chat_state_info.py | 2 +- src/heart_flow/mai_state_manager.py | 4 +-- src/heart_flow/sub_heartflow.py | 20 +++----------- src/heart_flow/sub_mind.py | 33 +++++++++--------------- src/plugins/heartFC_chat/heartFC_chat.py | 11 +++----- 5 files changed, 23 insertions(+), 47 deletions(-) diff --git a/src/heart_flow/chat_state_info.py b/src/heart_flow/chat_state_info.py index 78fb4e8d..14fd3340 100644 --- a/src/heart_flow/chat_state_info.py +++ b/src/heart_flow/chat_state_info.py @@ -14,4 +14,4 @@ class ChatStateInfo: self.current_state_time = 120 self.mood_manager = MoodManager() - self.mood = self.mood_manager.get_prompt() \ No newline at end of file + self.mood = self.mood_manager.get_prompt() diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 9a39b5fe..7df55d6f 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): diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index bb671913..7a6e009c 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -72,7 +72,7 @@ class InterestChatting: self.above_threshold = False self.start_hfc_probability = 0.0 - + 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() @@ -221,9 +221,7 @@ class SubHeartflow: # 聊天状态管理 self.chat_state: ChatStateInfo = ChatStateInfo() # 该sub_heartflow的聊天状态信息 - self.interest_chatting = InterestChatting( - state_change_callback=self.set_chat_state - ) + self.interest_chatting = InterestChatting(state_change_callback=self.set_chat_state) # 活动状态管理 self.last_active_time = time.time() # 最后活跃时间 @@ -238,14 +236,10 @@ class SubHeartflow: # LLM模型配置 self.sub_mind = SubMind( - subheartflow_id=self.subheartflow_id, - chat_state=self.chat_state, - observations=self.observations + 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 add_time_current_state(self, add_time: float): self.current_state_time += add_time @@ -335,9 +329,7 @@ class SubHeartflow: logger.info(f"{log_prefix} 麦麦准备开始专注聊天 (创建新实例)...") try: self.heart_fc_instance = HeartFChatting( - chat_id=self.chat_id, - sub_mind=self.sub_mind, - observations=self.observations + chat_id=self.chat_id, sub_mind=self.sub_mind, observations=self.observations ) if await self.heart_fc_instance._initialize(): await self.heart_fc_instance.start() # 初始化成功后启动循环 @@ -434,7 +426,6 @@ class SubHeartflow: logger.info(f"{self.log_prefix} 子心流后台任务已停止。") - def update_current_mind(self, response): self.sub_mind.update_current_mind(response) @@ -523,6 +514,3 @@ class SubHeartflow: self.chat_state.chat_status = ChatState.ABSENT # 状态重置为不参与 logger.info(f"{self.log_prefix} 子心流关闭完成。") - - - diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 03a9997f..4bde4727 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -19,7 +19,6 @@ subheartflow_config = LogConfig( logger = get_module_logger("subheartflow", config=subheartflow_config) - def init_prompt(): prompt = "" # prompt += f"麦麦的总体想法是:{self.main_heartflow_info}\n\n" @@ -39,32 +38,26 @@ def init_prompt(): prompt += "在输出完想法后,请你思考应该使用什么工具。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" Prompt(prompt, "sub_heartflow_prompt_before") - - + class SubMind: - def __init__( - self, - subheartflow_id: str, - chat_state: ChatStateInfo, - observations: Observation - ): + def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation): self.subheartflow_id = subheartflow_id - + self.llm_model = LLMRequest( model=global_config.llm_sub_heartflow, temperature=global_config.llm_sub_heartflow["temp"], max_tokens=800, request_type="sub_heart_flow", ) - + self.chat_state = chat_state self.observations = observations - + self.current_mind = "" self.past_mind = [] self.structured_info = {} - + async def do_thinking_before_reply(self): """ 在回复前进行思考,生成内心想法并收集工具调用结果 @@ -151,7 +144,7 @@ class SubMind: # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 - reasoning_content = "" # 初始化推理内容变量 + _reasoning_content = "" # 初始化推理内容变量 try: # 调用LLM生成响应 @@ -211,8 +204,7 @@ class SubMind: self.update_current_mind(content) return self.current_mind, self.past_mind - - + async def _execute_tool_calls(self, tool_calls, tool_instance): """ 执行一组工具调用并收集结果 @@ -244,11 +236,10 @@ class SubMind: if structured_info: logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") self.structured_info = structured_info - - + def update_current_mind(self, response): self.past_mind.append(self.current_mind) self.current_mind = response - - -init_prompt() \ No newline at end of file + + +init_prompt() diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 3229317f..d746059e 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -67,12 +67,7 @@ class HeartFChatting: 其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。 """ - def __init__( - self, - chat_id: str, - sub_mind: SubMind, - observations: Observation - ): + def __init__(self, chat_id: str, sub_mind: SubMind, observations: Observation): """ HeartFChatting 初始化函数 @@ -438,7 +433,9 @@ class HeartFChatting: llm_error = False # LLM错误标志 try: - prompt = await self._build_planner_prompt(observed_messages_str, current_mind, self.sub_mind.structured_info) + 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}], From 5e7131ed00c0d32a058a5805bab2a22a0060f87a Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 15:02:17 +0800 Subject: [PATCH 19/73] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=A5=9E?= =?UTF-8?q?=E7=A7=98=E9=AD=94=E6=B3=95=E5=A3=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/heartFC_chat/heartFC_chat.py | 2 -- src/plugins/heartFC_chat/heartFC_generator.py | 16 +++------- .../heartFC_chat/heartflow_prompt_builder.py | 31 +++---------------- 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index d746059e..33fe0109 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -654,8 +654,6 @@ class HeartFChatting: logger.warning(f"{log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。") return None - # --- 准备并返回结果 --- # - # logger.info(f"{log_prefix}[Replier-{thinking_id}] 成功生成了回复集: {' '.join(response_set)[:50]}...") return response_set except Exception as e: diff --git a/src/plugins/heartFC_chat/heartFC_generator.py b/src/plugins/heartFC_chat/heartFC_generator.py index cbf050bd..464e94e9 100644 --- a/src/plugins/heartFC_chat/heartFC_generator.py +++ b/src/plugins/heartFC_chat/heartFC_generator.py @@ -47,10 +47,6 @@ class HeartFCGenerator: ) -> Optional[List[str]]: """根据当前模型类型选择对应的生成函数""" - logger.info( - f"思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}" - ) - arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() with Timer() as t_generate_response: @@ -80,27 +76,25 @@ class HeartFCGenerator: model: LLMRequest, thinking_id: str, ) -> str: - sender_name = "" - info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - sender_name = f"<{message.chat_stream.user_info.platform}:{message.chat_stream.user_info.user_id}:{message.chat_stream.user_info.user_nickname}:{message.chat_stream.user_info.user_cardname}>" - with Timer() as t_build_prompt: prompt = await prompt_builder.build_prompt( build_mode="focus", reason=reason, current_mind_info=current_mind_info, structured_info=structured_info, - message_txt=message.processed_plain_text, - sender_name=sender_name, + message_txt="", + sender_name="", chat_stream=message.chat_stream, ) - logger.info(f"构建prompt时间: {t_build_prompt.human_readable}") + # logger.info(f"构建prompt时间: {t_build_prompt.human_readable}") try: content, reasoning_content, self.current_model_name = await model.generate_response(prompt) + logger.info(f"\nprompt:{prompt}\n生成回复{content}\n") + info_catcher.catch_after_llm_generated( prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name ) diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index c5b04ed9..880d0a27 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -25,13 +25,13 @@ def init_prompt(): {structured_info} {chat_target} {chat_talking_prompt} -现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n +现在你想要在群里发言或者回复。\n 你的网名叫{bot_name},{prompt_personality} {prompt_identity}。 你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些, 你刚刚脑子里在想: {current_mind_info} {reason} -回复尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人。{prompt_ger} +回复尽量简短一些。请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人,不用指出你回复的是谁。{prompt_ger} 请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 ,注意只输出回复内容。 {moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", @@ -95,12 +95,12 @@ class PromptBuilder: elif build_mode == "focus": return await self._build_prompt_focus( - reason, current_mind_info, structured_info, chat_stream, message_txt, sender_name + reason, current_mind_info, structured_info, chat_stream, ) return None async def _build_prompt_focus( - self, reason, current_mind_info, structured_info, chat_stream, message_txt: str, sender_name: str = "某人" + 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) @@ -128,26 +128,6 @@ class PromptBuilder: read_mark=0.0, ) - # 关键词检测与反应 - keywords_reaction_prompt = "" - for rule in global_config.keywords_reaction_rules: - if rule.get("enable", False): - if any(keyword in message_txt.lower() for keyword in rule.get("keywords", [])): - logger.info( - f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}" - ) - keywords_reaction_prompt += rule.get("reaction", "") + "," - else: - for pattern in rule.get("regex", []): - result = pattern.search(message_txt) - if result: - reaction = rule.get("reaction", "") - for name, content in result.groupdict().items(): - reaction = reaction.replace(f"[{name}]", content) - logger.info(f"匹配到以下正则表达式:{pattern},触发反应:{reaction}") - keywords_reaction_prompt += reaction + "," - break - # 中文高手(新加的好玩功能) prompt_ger = "" if random.random() < 0.04: @@ -164,8 +144,6 @@ class PromptBuilder: if chat_in_group else await global_prompt_manager.get_prompt_async("chat_target_private1"), chat_talking_prompt=chat_talking_prompt, - sender_name=sender_name, - message_txt=message_txt, bot_name=global_config.BOT_NICKNAME, prompt_personality=prompt_personality, prompt_identity=prompt_identity, @@ -174,7 +152,6 @@ class PromptBuilder: else await global_prompt_manager.get_prompt_async("chat_target_private2"), current_mind_info=current_mind_info, reason=reason, - keywords_reaction_prompt=keywords_reaction_prompt, prompt_ger=prompt_ger, moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), ) From 748ac8482a34f0251aaf3102fbbf55a208d37b3d Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 24 Apr 2025 16:35:05 +0800 Subject: [PATCH 20/73] =?UTF-8?q?better=EF=BC=9A=E4=BC=98=E5=8C=96Prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/heart_flow/mai_state_manager.py | 6 +-- src/heart_flow/mind.py | 3 -- src/heart_flow/sub_mind.py | 17 +++--- src/plugins/heartFC_chat/heartFC_chat.py | 53 +++++++++---------- .../heartFC_chat/heartflow_prompt_builder.py | 27 ++++++++-- src/plugins/heartFC_chat/normal_chat.py | 14 +++++ 6 files changed, 75 insertions(+), 45 deletions(-) diff --git a/src/heart_flow/mai_state_manager.py b/src/heart_flow/mai_state_manager.py index 7df55d6f..6f645f67 100644 --- a/src/heart_flow/mai_state_manager.py +++ b/src/heart_flow/mai_state_manager.py @@ -39,7 +39,7 @@ class MaiState(enum.Enum): if self == MaiState.OFFLINE: return 0 elif self == MaiState.PEEKING: - return 1 + return 2 elif self == MaiState.NORMAL_CHAT: return 3 elif self == MaiState.FOCUSED_CHAT: @@ -53,11 +53,11 @@ class MaiState(enum.Enum): if self == MaiState.OFFLINE: return 0 elif self == MaiState.PEEKING: - return 0 + return 1 elif self == MaiState.NORMAL_CHAT: return 1 elif self == MaiState.FOCUSED_CHAT: - return 2 + return 3 class MaiStateInfo: diff --git a/src/heart_flow/mind.py b/src/heart_flow/mind.py index 6ca03c21..e806d18a 100644 --- a/src/heart_flow/mind.py +++ b/src/heart_flow/mind.py @@ -22,9 +22,6 @@ class Mind: self.subheartflow_manager = subheartflow_manager self.llm_model = llm_model self.individuality = Individuality.get_instance() - # Main mind state is still managed by Heartflow for now - # self.current_mind = "你什么也没想" - # self.past_mind = [] async def do_a_thinking(self, current_main_mind: str, mai_state_info: "MaiStateInfo", schedule_info: str): """ diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index 4bde4727..c7baa91e 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -30,12 +30,13 @@ def init_prompt(): prompt += "-----------------------------------\n" prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,群里正在聊的话题是:\n{chat_observe_info}\n" prompt += "\n你现在{mood_info}\n" - # prompt += "你注意到{sender_name}刚刚说:{message_txt}\n" - prompt += "现在请你根据刚刚的想法继续思考,思考时可以想想如何对群聊内容进行回复,要不要对群里的话题进行回复,关注新话题,可以适当转换话题,大家正在说的话才是聊天的主题。\n" - prompt += "回复的要求是:平淡一些,简短一些,说中文,如果你要回复,最好只回复一个人的一个话题\n" - prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要带有括号和动作描写。不要回复自己的发言,尽量不要说你说过的话。\n" - prompt += "现在请你先{hf_do_next},不要分点输出,生成内心想法,文字不要浮夸" - prompt += "在输出完想法后,请你思考应该使用什么工具。如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" + prompt += "现在请你生成你的内心想法,要求思考群里正在进行的话题,之前大家聊过的话题,群里成员的关系。" + prompt += "请你思考,要不要对群里的话题进行回复,以及如何对群聊内容进行回复\n" + prompt += "回复的要求是:平淡一些,简短一些,如果你要回复,最好只回复一个人的一个话题\n" + prompt += "请注意不要输出多余内容(包括前后缀,冒号和引号,括号, 表情,等),不要回复自己的发言\n" + prompt += "现在请你先输出想法,{hf_do_next},不要分点输出,文字不要浮夸" + prompt += "在输出完想法后,请你思考应该使用什么工具。工具可以帮你取得一些你不知道的信息,或者进行一些操作。" + prompt += "如果你需要做某件事,来对消息和你的回复进行处理,请使用工具。\n" Prompt(prompt, "sub_heartflow_prompt_before") @@ -116,9 +117,9 @@ class SubMind: # 思考指导选项和权重 hf_options = [ - ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考", 0.7), + ("继续生成你在这个聊天中的想法,在原来想法的基础上继续思考,但是不要纠结于同一个话题", 0.6), ("生成你在这个聊天中的想法,在原来的想法上尝试新的话题", 0.1), - ("生成你在这个聊天中的想法,不要太深入", 0.1), + ("生成你在这个聊天中的想法,不要太深入", 0.2), ("继续生成你在这个聊天中的想法,进行深入思考", 0.1), ] diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 33fe0109..8735ff7d 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -19,6 +19,7 @@ from src.plugins.chat.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 +from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager # --- End import --- @@ -594,39 +595,36 @@ class HeartFChatting: self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any] ) -> str: """构建 Planner LLM 的提示词""" - - prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。\n" - + + # 准备结构化信息块 + structured_info_block = "" if structured_info: - prompt += f"以下是一些额外的信息:\n{structured_info}\n" - + structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" + + # 准备聊天内容块 + chat_content_block = "" if observed_messages_str: - prompt += "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n" - prompt += observed_messages_str - prompt += "\n---" + chat_content_block = "观察到的最新聊天内容如下 (最近的消息在最后):\n---\n" + chat_content_block += observed_messages_str + chat_content_block += "\n---" else: - prompt += "当前没有观察到新的聊天内容。\n" - - prompt += "\n看了以上内容,你产生的内心想法是:" + chat_content_block = "当前没有观察到新的聊天内容。\n" + + # 准备当前思维块 + current_mind_block = "" if current_mind: - prompt += f"\n---\n{current_mind}\n---\n\n" + current_mind_block = f"\n---\n{current_mind}\n---\n\n" else: - prompt += " [没有特别的想法] \n\n" - - prompt += ( - "请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。\n" - "决策依据:\n" - "1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'。\n" - "2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (例如:'开心的'、'惊讶的')。\n" - "3. 如果聊天内容或你的内心想法适合用一个表情来回应(例如表示赞同、惊讶、无语等),选择 'emoji_reply' 并提供表情主题 'emoji_query'。\n" - "4. 如果最后一条消息是你自己发的,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。\n" - "5. 除非大家都在这么做,或者有特殊理由,否则不要重复别人刚刚说过的话或简单附和。\n" - "6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。\n" - "7. 如果观察到的内容只有你自己的发言,选择 'no_reply'。\n" - "8. 不要回复你自己的话,不要把自己的话当做别人说的。\n" - "必须调用 'decide_reply_action' 工具并提供 'action' 和 'reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'。" + current_mind_block = " [没有特别的想法] \n\n" + + # 获取提示词模板并填充数据 + prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( + bot_name=global_config.BOT_NICKNAME, + structured_info_block=structured_info_block, + chat_content_block=chat_content_block, + current_mind_block=current_mind_block, ) - + return prompt # --- 回复器 (Replier) 的定义 --- # @@ -698,7 +696,6 @@ class HeartFChatting: return None chat = anchor_message.chat_stream - # Access MessageManager directly container = await message_manager.get_container(chat.stream_id) thinking_message = None diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 880d0a27..2c2a961e 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -26,16 +26,37 @@ def init_prompt(): {chat_target} {chat_talking_prompt} 现在你想要在群里发言或者回复。\n -你的网名叫{bot_name},{prompt_personality} {prompt_identity}。 -你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些, +你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality} {prompt_identity}"。 +你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,小红书或者微博的回复风格。 你刚刚脑子里在想: {current_mind_info} {reason} 回复尽量简短一些。请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人,不用指出你回复的是谁。{prompt_ger} -请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 ,注意只输出回复内容。 +请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要说你说过的话题 ,注意只输出回复内容。 {moderation_prompt}。注意:不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", "heart_flow_prompt", ) + + # Planner提示词 + Prompt( + """你的名字是 {bot_name}。是一个女大二学生,正在QQ聊天,正在决定是否以及如何回应当前的聊天。 +{structured_info_block} +{chat_content_block} +看了以上内容,你产生的内心想法是: +{current_mind_block} +请结合你的内心想法和观察到的聊天内容,分析情况并使用 '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'。""", + "planner_prompt", + ) + Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("和群里聊天", "chat_target_group2") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index c020b407..a3aaf3a0 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -402,3 +402,17 @@ class NormalChat: # 确保任务状态更新,即使等待出错 (回调函数也会尝试更新) if self._chat_task is task: self._chat_task = None + + # 清理所有未处理的思考消息 + try: + container = await message_manager.get_container(self.stream_id) + if container: + # 查找并移除所有 MessageThinking 类型的消息 + thinking_messages = [msg for msg in container.messages[:] if isinstance(msg, MessageThinking)] + if thinking_messages: + for msg in thinking_messages: + container.messages.remove(msg) + logger.info(f"[{self.stream_name}] 清理了 {len(thinking_messages)} 条未处理的思考消息。") + except Exception as e: + logger.error(f"[{self.stream_name}] 清理思考消息时出错: {e}") + logger.error(traceback.format_exc()) From 487c4c5ec82da59bedd4a7c3229328ebd4f2cd5f Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Thu, 24 Apr 2025 16:45:50 +0800 Subject: [PATCH 21/73] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E4=B8=8E?= =?UTF-8?q?=E4=B9=8B=E5=89=8DPFC=E5=90=8C=E6=A0=B7=E7=9A=84=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/PFC/reply_checker.py | 2 +- src/plugins/heartFC_chat/heartflow_prompt_builder.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index f4e1c990..7e43715b 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -100,7 +100,7 @@ class ReplyChecker: 1. 回复是否依然符合当前对话目标和实现方式 2. 回复是否与最新的对话记录保持一致性 3. 回复是否重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义) -4. 回复是否包含政治敏感内容 +4. 回复是否包含违规内容(例如血腥暴力,政治敏感等) 5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话(不要自己回复自己的消息) 6. 回复是否通俗易懂 7. 回复是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸” diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 1d19d1ca..251b033f 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -23,7 +23,7 @@ def init_prompt(): """ {chat_target} {chat_talking_prompt} -现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n +现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言或者回复这条消息。\n 你的网名叫{bot_name},{prompt_personality} {prompt_identity}。 你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些, 你刚刚脑子里在想: @@ -52,7 +52,7 @@ def init_prompt(): {schedule_prompt} {chat_target} {chat_talking_prompt} -现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n +现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言或者回复这条消息。\n 你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些, 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger} 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 22/73] 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 464e94e9..da43c334 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 23/73] =?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 8735ff7d..b87ad652 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 e9148c4f..73ad9129 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 a3aaf3a0..fc0c750b 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 24/73] =?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 7eca2260..df5c1c94 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 25/73] =?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 e6be2a7d..00000000 --- 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 8a5b7ffc..2fc1cbb1 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 7c16aaa5..0390b056 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 c7baa91e..92f0a960 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 62fa70a6..3ef1ed22 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 85de966e..2e057e6f 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 8d9aa1f8..e5b0b942 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 cbc8e600..00000000 --- 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 2ba645f9..093ccc30 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 9c7a03b0..bf549b97 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 00000000..db5a3132 --- /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 8735ff7d..cf2081fc 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 2c2a961e..490618b7 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 a3aaf3a0..9bbdfaea 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 5f342406..9db3f193 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 26/73] 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 0390b056..996b2738 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 db5a3132..dea9a609 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 cf2081fc..ab80beaa 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 464e94e9..6b5aaaa3 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 490618b7..aaeade54 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 9bbdfaea..2ba5d79d 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 27/73] =?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 bf549b97..f8ff15aa 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 dea9a609..aa3d7506 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 2478cd67..102aef52 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 28/73] =?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 2fc1cbb1..f69a9522 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 f8ff15aa..24572ed2 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 aa3d7506..7222fd3f 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 29/73] =?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 aed0025b..60bb4d8c 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 30/73] =?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 f69a9522..30a97e92 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 996b2738..1cc58f71 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 05a7da39..62063f07 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 e806d18a..a40ee6ef 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 7a6e009c..7397a37f 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 31/73] 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 30a97e92..19463c0f 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 60bb4d8c..ab5efa9d 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 24572ed2..fb8522b9 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 32/73] 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 76636d74..605d838c 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 07471152..10b6b77f 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 33/73] =?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 10b6b77f..6ef7070d 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 34/73] =?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 6ef7070d..39df2338 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 00000000..befb40ec --- /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 35/73] =?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 befb40ec..84b3d2f5 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 36/73] =?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 39df2338..a9f84ccd 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 37/73] =?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 605d838c..3bd4a21b 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 a9f84ccd..24294125 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 38/73] =?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 24294125..e055bf68 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 39/73] =?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 e055bf68..23165a23 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 84b3d2f5..00000000 --- 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 b7938f016f22d80e76d338804f12cced63e0178e Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Fri, 25 Apr 2025 12:21:43 +0800 Subject: [PATCH 40/73] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPFC=E6=9C=80=E5=A4=A7?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E6=AC=A1=E6=95=B0=EF=BC=8C=E4=BD=BF=E5=85=B6?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E5=90=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复checker的最大重试次数,使其重新启用(似乎在之前所有版本中从来没有启用过?)达到最大重试次数自动wait,并且加入了更详细的reply_checker报错日志 --- src/plugins/PFC/conversation.py | 118 ++++++++++++++++++------------- src/plugins/PFC/reply_checker.py | 10 +-- 2 files changed, 75 insertions(+), 53 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index d4888ff7..39ebccc1 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -248,54 +248,70 @@ class Conversation: # --- 根据不同的 action 执行 --- if action == "direct_reply": - # --- 这个 if 块内部的所有代码都需要正确缩进 --- - self.waiter.wait_accumulated_time = 0 # 重置等待时间 - - self.state = ConversationState.GENERATING - # 生成回复 - self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info) - logger.info(f"生成回复: {self.generated_reply}") # 使用 logger - - # --- 调用 ReplyChecker 检查回复 --- - is_suitable = False # 先假定不合适,检查通过再改为 True - check_reason = "检查未执行" # 用不同的变量名存储检查原因 + max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大) + reply_attempt_count = 0 + is_suitable = False need_replan = False - try: - # 尝试获取当前主要目标 - current_goal_str = conversation_info.goal_list[0][0] if conversation_info.goal_list else "" + check_reason = "未进行尝试" + final_reply_to_send = "" - # 调用检查器 - is_suitable, check_reason, need_replan = await self.reply_generator.check_reply( - reply=self.generated_reply, - goal=current_goal_str, - chat_history=observation_info.chat_history, # 传入最新的历史记录! - retry_count=0, - ) - logger.info(f"回复检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}") + while reply_attempt_count < max_reply_attempts and not is_suitable: + reply_attempt_count += 1 + logger.info(f"尝试生成回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)...") + self.state = ConversationState.GENERATING - except Exception as check_err: - logger.error(f"调用 ReplyChecker 时出错: {check_err}") - check_reason = f"检查过程出错: {check_err}" # 记录错误原因 - # is_suitable 保持 False + # 1. 生成回复 + self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info) + logger.info(f"第 {reply_attempt_count} 次生成的回复: {self.generated_reply}") - # --- 处理检查结果 --- + # 2. 检查回复 + self.state = ConversationState.CHECKING + try: + current_goal_str = conversation_info.goal_list[0][0] if conversation_info.goal_list else "" + # 注意:这里传递的是 reply_attempt_count - 1 作为 retry_count 给 checker + is_suitable, check_reason, need_replan = await self.reply_generator.check_reply( + reply=self.generated_reply, + goal=current_goal_str, + chat_history=observation_info.chat_history, + retry_count=reply_attempt_count - 1, # 传递当前尝试次数(从0开始计数) + ) + logger.info(f"第 {reply_attempt_count} 次检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}") + + if is_suitable: + final_reply_to_send = self.generated_reply # 保存合适的回复 + break # 回复合适,跳出循环 + + elif need_replan: + logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}") + break # 如果检查器建议重新规划,也停止尝试 + + # 如果不合适但不需要重新规划,循环会继续进行下一次尝试 + except Exception as check_err: + logger.error(f"第 {reply_attempt_count} 次调用 ReplyChecker 时出错: {check_err}") + check_reason = f"第 {reply_attempt_count} 次检查过程出错: {check_err}" + # 如果检查本身出错,可以选择跳出循环或继续尝试 + # 这里选择跳出循环,避免无限循环在检查错误上 + break + + # 循环结束,处理最终结果 if is_suitable: - # 回复合适,继续执行 - # 检查是否有新消息进来 + # 回复合适且已保存在 final_reply_to_send 中 + # 检查是否有新消息进来 (在所有尝试结束后再检查一次) if self._check_new_messages_after_planning(): - logger.info("检查到新消息,取消发送已生成的回复,重新规划行动") - # 更新 action 状态为 recall + logger.info("生成回复期间收到新消息,取消发送,重新规划行动") conversation_info.done_action[action_index].update( { "status": "recall", - "reason": f"有新消息,取消发送: {self.generated_reply}", # 更新原因 + "final_reason": f"有新消息,取消发送: {final_reply_to_send}", "time": datetime.datetime.now().strftime("%H:%M:%S"), } ) - return None # 退出 _handle_action + # 这里直接返回,不执行后续发送和wait + return - # 发送回复 - await self._send_reply() # 这个函数内部会处理自己的错误 + # 发送合适的回复 + self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容 + await self._send_reply() # 更新 action 历史状态为 done conversation_info.done_action[action_index].update( @@ -306,26 +322,30 @@ class Conversation: ) else: - # 回复不合适 - logger.warning(f"生成的回复被 ReplyChecker 拒绝: '{self.generated_reply}'. 原因: {check_reason}") - # 更新 action 状态为 recall (因为没执行发送) + # 循环结束但没有找到合适的回复(达到最大次数或检查出错/建议重规划) + logger.warning(f"经过 {reply_attempt_count} 次尝试,未能生成合适的回复。最终原因: {check_reason}") conversation_info.done_action[action_index].update( { - "status": "recall", - "final_reason": check_reason, + "status": "recall", # 标记为 recall 因为没有成功发送 + "final_reason": f"尝试{reply_attempt_count}次后失败: {check_reason}", "time": datetime.datetime.now().strftime("%H:%M:%S"), } ) - # 如果检查器建议重新规划 - if need_replan: - logger.info("ReplyChecker 建议重新规划目标。") - # 可选:在此处清空目标列表以强制重新规划 - # conversation_info.goal_list = [] - - # 注意:不发送消息,也不执行后面的代码 - - # --- 之前重复的代码块已被删除 --- + # 执行 Wait 操作 + logger.info("由于无法生成合适回复,执行 'wait' 操作...") + self.state = ConversationState.WAITING + # 直接调用 wait 方法 + await self.waiter.wait(self.conversation_info) + # 可以选择添加一条新的 action 记录来表示这个 wait + wait_action_record = { + "action": "wait", + "plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待", + "status": "done", # wait 完成后可以认为是 done + "time": datetime.datetime.now().strftime("%H:%M:%S"), + "final_reason": None, + } + conversation_info.done_action.append(wait_action_record) elif action == "fetch_knowledge": self.waiter.wait_accumulated_time = 0 diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 7e43715b..312387f3 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -15,11 +15,11 @@ class ReplyChecker: def __init__(self, stream_id: str): self.llm = LLMRequest( - model=global_config.llm_PFC_reply_checker, temperature=0.55, max_tokens=1000, request_type="reply_check" + model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check" ) self.name = global_config.BOT_NICKNAME self.chat_observer = ChatObserver.get_instance(stream_id) - self.max_retries = 2 # 最大重试次数 + self.max_retries = 3 # 最大重试次数 async def check( self, reply: str, goal: str, chat_history: List[Dict[str, Any]], retry_count: int = 0 @@ -76,8 +76,10 @@ class ReplyChecker: False, ) - except Exception as self_check_err: - logger.error(f"检查自身重复发言时出错: {self_check_err}") + except Exception as e: + import traceback + 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") From a2c94d3e8ef8f3b087360c43443fdc21ea72b69c Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Apr 2025 13:27:35 +0800 Subject: [PATCH 41/73] =?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 3bd4a21b..605d838c 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 42/73] =?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 60acb5f5..697833c8 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 d4888ff7..4cc894bd 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 bc4499ed..8a0f4176 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 08ff3c04..4cb6aaaa 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 873d1467..19549825 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 7e43715b..e1a2a6fd 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 e50dc3ec..9416ebad 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 093ccc30..c7f7ac83 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 d0ab5604..38d82b52 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 a737d99c..d51492f7 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 ab5efa9d..91e08e44 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 f7c3a64f..de8caf2d 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 2ba5d79d..56fcfc34 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 286ef231..b5eed4d4 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 fb51539e..e82ab98f 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 b853d469..00000000 --- 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 43/73] =?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 b7ecbc84..9fd3127f 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 44/73] =?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 39ebccc1..5687e420 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 312387f3..949b49a3 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 45/73] =?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 19463c0f..4ed69f32 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 6f645f67..f8d4341e 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 9391a660..790c2180 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 7397a37f..91ddc2cd 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 92f0a960..111c2cf5 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 b9703e53..dcc45591 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 3ef1ed22..a2d8fc51 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 fb8522b9..f6b9231a 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 7222fd3f..1c73ec78 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 ab80beaa..47cb52eb 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 da43c334..c489e012 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 00000000..07bc4c63 --- /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 f7c3a64f..27c88a98 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 102aef52..146a5307 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 e3fb377c..eea2177f 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 6ae6ccc3..edd60c05 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 46/73] =?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 1a0d495c..63f71aad 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 47/73] =?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 63f71aad..54990de7 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 48/73] =?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 54990de7..95e66c8c 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 49/73] =?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 1cc58f71..187bb6cd 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 ab5efa9d..6d896284 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 9db3f193..d55fca3f 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 50/73] =?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 4ed69f32..52602082 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 1c73ec78..754792c2 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 47cb52eb..b8338c4b 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 e756d1da..60f5459b 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 51/73] =?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 187bb6cd..2ade83f1 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 85fb6c50..21254ce7 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 111c2cf5..b213f6f1 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 00000000..030018dd --- /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 b8338c4b..c11674fe 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 c489e012..95ee0a75 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 146a5307..ec12e2ad 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 d55fca3f..a85d9f17 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 60f5459b..7ef00c7c 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 02cbe6e4136dcfc81d01dc5b7201ecfaed76c969 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Fri, 25 Apr 2025 21:38:53 +0800 Subject: [PATCH 52/73] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=A7=81=E8=81=8APFC?= =?UTF-8?q?=E7=9A=84prompt=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 大大缩减的planner的prompt,细化了checker和聊天的prompt,无代码层面改动,私聊交互更加自然 --- src/plugins/PFC/action_planner.py | 23 +++++++++-------------- src/plugins/PFC/reply_checker.py | 4 ++-- src/plugins/PFC/reply_generator.py | 6 +++--- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/plugins/PFC/action_planner.py b/src/plugins/PFC/action_planner.py index b55a464d..4e39483b 100644 --- a/src/plugins/PFC/action_planner.py +++ b/src/plugins/PFC/action_planner.py @@ -244,7 +244,7 @@ class ActionPlanner: last_action_context += f"- 该行动当前状态: {status}\n" # --- 构建最终的 Prompt --- - prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请根据以下【所有信息】审慎决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识: + prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识: 【当前对话目标】 {goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。"} @@ -259,24 +259,19 @@ class ActionPlanner: 【最近的对话记录】(包括你已成功发送的消息 和 新收到的消息) {chat_history_text if chat_history_text.strip() else "还没有聊天记录。"} ---- 行动决策指南 --- -1. **仔细分析【上一次行动的详细情况和结果】**。如果上次行动是 direct_reply 且因“内容与你上一条发言完全相同”或“高度相似”而被取消(status: recall),那么【绝对不要】立即再次规划 direct_reply。在这种特定情况下,你应该优先考虑 wait (等待用户的新回应) 或 rethink_goal (如果对话似乎因此卡住了)。 -2. 结合【当前对话目标】和【最近的对话记录】来判断是否需要回应、回应什么。如果【最近的对话记录】中有新的用户消息,通常需要 direct_reply。如果上次行动成功,或者上次失败的原因不是重复,可以根据对话内容考虑 direct_reply。 -3. 注意【时间和超时提示】,如果对方长时间未回复(例如在 timeout_context 中提示),end_conversation 可能更合适。 -4. 只有在你确信需要发言(比如回应新消息、追问、深入话题),并且上一次行动没有因重复被拒时,才应优先选择 direct_reply。 - ---- 可选行动类型 --- -fetch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择 -wait: 等待对方回复(尤其是在你刚发言后、或上次发言因重复被拒时、或不确定做什么时,这是较安全的选择) -listening: 倾听对方发言,当你认为对方发言尚未结束时采用 -direct_reply: 直接回复或发送新消息,允许适当的追问和深入话题,**但是请务必遵守上面的决策指南,避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言** +------ +可选行动类型以及解释: +etch_knowledge: 需要调取知识,当需要专业知识或特定信息时选择,对方若提到你太认识的人名或实体也可以尝试 +wait: 暂时不说话,等待对方回复(尤其是在你刚发言后、或上次发言因重复、发言过多被拒时、或不确定做什么时,这是较安全的选择) +listening: 倾听对方发言,当你认为对方话才说到一半,发言明显未结束时采用 +direct_reply: 直接回复或发送新消息,允许适当的追问和深入话题,**但是避免在因重复被拒后立即使用,也不要在对方没有回复的情况下过多的“消息轰炸”或重复发言** rethink_goal: 重新思考对话目标,当发现对话目标不再适用或对话卡住时选择,注意私聊的环境是灵活的,有可能需要经常选择 -end_conversation: 决定结束对话,对方长时间没回复或者当你觉得谈话暂时结束时可以选择 +end_conversation: 结束对话,对方长时间没回复或者当你觉得对话告一段落时可以选择 请以JSON格式输出你的决策: {{ "action": "选择的行动类型 (必须是上面列表中的一个)", - "reason": "选择该行动的详细原因 (必须解释你是如何根据“上一次行动结果”、“对话记录”和“决策指南”做出判断的)" + "reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的,如果你连续发言,必须记录已经发言了几次)" }} 注意:请严格按照JSON格式输出,不要包含任何其他内容。""" diff --git a/src/plugins/PFC/reply_checker.py b/src/plugins/PFC/reply_checker.py index 312387f3..3ce284e4 100644 --- a/src/plugins/PFC/reply_checker.py +++ b/src/plugins/PFC/reply_checker.py @@ -105,11 +105,11 @@ class ReplyChecker: 4. 回复是否包含违规内容(例如血腥暴力,政治敏感等) 5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话(不要自己回复自己的消息) 6. 回复是否通俗易懂 -7. 回复是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸” +7. 回复是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断) 8. 回复是否使用了完全没必要的修辞 9. 回复是否逻辑通顺 10. 回复是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况) -11. 在连续多次发送消息的情况下,当前回复是否衔接自然,会不会显得奇怪 +11. 在连续多次发送消息的情况下,当前回复是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠) 请以JSON格式输出,包含以下字段: 1. suitable: 是否合适 (true/false) diff --git a/src/plugins/PFC/reply_generator.py b/src/plugins/PFC/reply_generator.py index 15cd7dee..fe9dab6f 100644 --- a/src/plugins/PFC/reply_generator.py +++ b/src/plugins/PFC/reply_generator.py @@ -130,7 +130,7 @@ class ReplyGenerator: elif action_status == "done": action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n" - prompt = f"""{persona_text}。现在你在参与一场QQ聊天,请根据以下信息生成一条新消息: + prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条新消息: 当前对话目标:{goals_str} 最近的聊天记录: @@ -140,15 +140,15 @@ class ReplyGenerator: 请根据上述信息,结合聊天记录,发一条消息(可以是回复,补充,深入话题,或追问等等)。该消息应该: 1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!) 2. 符合你的性格特征和身份细节 -3. 自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) +3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况) 4. 适当利用相关知识,但不要生硬引用 5. 自然、得体,结合聊天记录逻辑合理,且没有重复表达同质内容 -**注意:如果聊天记录中最新的消息是你自己发送的,那么你的思路不应该是“回复”,而是应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等;** 请注意把握聊天内容,不要回复的太有条理,可以有个性。请分清"你"和对方说的话,不要把"你"说的话当做对方说的话,这是你自己说的话。 可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。 请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 +**注意:如果聊天记录中最新的消息是你自己发送的,那么你的思路不应该是“回复”,而是应该紧紧衔接你发送的消息,进行话题的深入,补充,或追问等等,避免与最新消息内容重叠;** 请直接输出回复内容,不需要任何额外格式。""" 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 53/73] 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 b213f6f1..8470c24e 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 54/73] 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 19549825..033cf822 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 b8338c4b..8be4d42f 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 c489e012..611888ff 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 55/73] =?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 52602082..6ab3505d 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 790c2180..b960154c 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 91ddc2cd..fb1a81c3 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 dcc45591..79f2a0ec 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 754792c2..cf3ebadb 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 8be4d42f..b8e9781d 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 e43659ee..204ca703 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 146a5307..80587f2f 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 edd60c05..630ff989 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 56/73] 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 b8e9781d..d7791180 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 611888ff..b750c13c 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 204ca703..da7b479b 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 56fcfc34..76cba597 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 cd9208b3..52d0f446 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 57/73] =?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 8470c24e..6b2bdaac 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 d0ab5604..b3166f30 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 c11674fe..0135cfb7 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 ec12e2ad..9dd6e4d3 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 e4f4004e..f5ec6d8f 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 edd60c05..8ba49d9c 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 58/73] 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 21254ce7..f5131a59 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 6b2bdaac..d8b1f75b 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 2c04fd50..d76d2328 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 030018dd..96677384 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 d57885b9..ba6be7eb 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 eddc5c5a..661c4e8a 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 f5ec6d8f..1ec9f6d0 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 8ba49d9c..5d949448 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 7ef00c7c..a3e28273 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 59/73] =?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 ba6be7eb..b9c6209c 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 00000000..000496dd --- /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 60/73] 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 b9c6209c..772941c4 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 61/73] 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 772941c4..7a529899 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 000496dd..d436c668 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 62/73] =?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 d8b1f75b..b176943a 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 7a529899..bd4da95a 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 63/73] =?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 b176943a..be995b84 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 64/73] =?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 dc00a9ff..db757c56 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 65/73] =?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 db757c56..cf1cd5ac 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 00000000..67a13cc6 --- /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 66/73] =?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 cf1cd5ac..ca6603e3 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 14fd3340..619f372f 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 67a13cc6..1e178a6f 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 fb1a81c3..cbdcd274 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 79f2a0ec..0bfa40cc 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 bd4da95a..e9577e41 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 76cba597..6687421e 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 5d949448..f510365f 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 a85d9f17..afb65e89 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 67/73] 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 cbdcd274..dd9364f3 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 0bfa40cc..ce9ec39a 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 68/73] =?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 28c5ecc1..3dbcb28f 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 ca6603e3..a25cbe9f 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 f5131a59..c66a6128 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 7fbc0f58..3f7fa0f1 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 62063f07..7802f87b 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 dd9364f3..b6cbdd22 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 ce9ec39a..d586fd43 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 da7b479b..1f771688 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 661c4e8a..584205a7 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 69/73] 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 a25cbe9f..24d094cc 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 70/73] 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 3dbcb28f..245a0ae9 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 c66a6128..85b77579 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 3f7fa0f1..7d92ae52 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 b6cbdd22..9cbd7b3a 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 d586fd43..cd32136a 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 71/73] =?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 f8d4341e..64fd4048 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 2a5184ba46a0cb9b50f9ca63487d344956c151a8 Mon Sep 17 00:00:00 2001 From: 114514 <2514624910@qq.com> Date: Sat, 26 Apr 2025 14:24:48 +0800 Subject: [PATCH 72/73] =?UTF-8?q?=E8=AF=95=E5=9B=BE=E4=BF=AE=E5=A4=8DPFC?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E5=AF=B9=E8=AF=9D=E5=AE=9E=E4=BE=8B=E6=97=B6?= =?UTF-8?q?=E8=BD=BD=E5=85=A5=E8=81=8A=E5=A4=A9=E8=AE=B0=E5=BD=95=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 开始对话示例时出现神秘bug导致聊天记录缺失(只有对方的没有bot的),经过高人指点将提取聊天记录方式改用chat_message_builder中的函数而不是storage --- src/plugins/PFC/conversation.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/plugins/PFC/conversation.py b/src/plugins/PFC/conversation.py index 39ebccc1..c290008b 100644 --- a/src/plugins/PFC/conversation.py +++ b/src/plugins/PFC/conversation.py @@ -1,7 +1,8 @@ import time import asyncio import datetime -from .message_storage import MongoDBMessageStorage +# 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 @@ -75,13 +76,10 @@ class Conversation: raise try: logger.info(f"为 {self.stream_id} 加载初始聊天记录...") - storage = MongoDBMessageStorage() # 创建存储实例 - # 获取当前时间点之前最多 N 条消息 (比如 30 条) - # get_messages_before 返回的是按时间正序排列的列表 - initial_messages = await storage.get_messages_before( + initial_messages = await get_raw_msg_before_timestamp_with_chat( # chat_id=self.stream_id, - time_point=time.time(), - limit=30, # 加载最近20条作为初始上下文,可以调整 + timestamp=time.time(), + limit=30, # 加载最近30条作为初始上下文,可以调整 ) if initial_messages: # 将加载的消息填充到 ObservationInfo 的 chat_history From 628c6d1db314a7ac1ef5433169b12e91ee61bc34 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Apr 2025 06:28:09 +0000 Subject: [PATCH 73/73] =?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 ef38eb8d..dc1e6a34 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条作为初始上下文,可以调整