diff --git a/.gitignore b/.gitignore index 88995ecc..5744424a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ NapCat.Framework.Windows.Once/ log/ logs/ tool_call_benchmark.py +run_maibot_core.bat +run_napcat_adapter.bat run_ad.bat llm_tool_benchmark_results.json MaiBot-Napcat-Adapter-main diff --git a/src/config/config.py b/src/config/config.py index b440f01d..325bbbaa 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -22,7 +22,7 @@ logger = get_logger("config") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 is_test = False mai_version_main = "0.6.3" -mai_version_fix = "fix-1" +mai_version_fix = "fix-2" if mai_version_fix: if is_test: @@ -268,6 +268,7 @@ class BotConfig: # experimental enable_friend_chat: bool = False # 是否启用好友聊天 # enable_think_flow: bool = False # 是否启用思考流程 + talk_allowed_private = set() enable_pfc_chatting: bool = False # 是否启用PFC聊天 # Group Nickname @@ -672,6 +673,7 @@ class BotConfig: experimental_config = parent["experimental"] config.enable_friend_chat = experimental_config.get("enable_friend_chat", config.enable_friend_chat) # config.enable_think_flow = experimental_config.get("enable_think_flow", config.enable_think_flow) + config.talk_allowed_private = set(str(user) for user in experimental_config.get("talk_allowed_private", [])) if config.INNER_VERSION in SpecifierSet(">=1.1.0"): config.enable_pfc_chatting = experimental_config.get("pfc_chatting", config.enable_pfc_chatting) 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 ef037de9..2bb292a1 100644 --- a/src/do_tool/tool_can_use/compare_numbers_tool.py +++ b/src/do_tool/tool_can_use/compare_numbers_tool.py @@ -9,7 +9,7 @@ class CompareNumbersTool(BaseTool): """比较两个数大小的工具""" name = "compare_numbers" - description = "比较两个数的大小,返回较大的数" + description = "使用工具 比较两个数的大小,返回较大的数" parameters = { "type": "object", "properties": { @@ -39,10 +39,10 @@ class CompareNumbersTool(BaseTool): else: result = f"{num1} 等于 {num2}" - return {"name": self.name, "content": result} + return {"type": "comparison_result", "id": f"{num1}_vs_{num2}", "content": result} except Exception as e: logger.error(f"比较数字失败: {str(e)}") - return {"name": self.name, "content": f"比较数字失败: {str(e)}"} + return {"type": "info", "id": f"{num1}_vs_{num2}", "content": f"比较数字失败,炸了: {str(e)}"} # 注册工具 diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py index 20a92264..90a44655 100644 --- a/src/do_tool/tool_can_use/get_knowledge.py +++ b/src/do_tool/tool_can_use/get_knowledge.py @@ -11,7 +11,7 @@ class SearchKnowledgeTool(BaseTool): """从知识库中搜索相关信息的工具""" name = "search_knowledge" - description = "从知识库中搜索相关信息" + description = "使用工具从知识库中搜索相关信息" parameters = { "type": "object", "properties": { @@ -42,11 +42,11 @@ class SearchKnowledgeTool(BaseTool): content = f"你知道这些知识: {knowledge_info}" else: content = f"你不太了解有关{query}的知识" - return {"name": "search_knowledge", "content": content} - return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"} + return {"type": "knowledge", "id": query, "content": content} + return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你知识库炸了"} except Exception as e: logger.error(f"知识库搜索工具执行失败: {str(e)}") - return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"} + return {"type": "info", "id": query, "content": f"知识库搜索失败,炸了: {str(e)}"} @staticmethod def get_info_from_db( diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py index c1bcc927..481942da 100644 --- a/src/do_tool/tool_can_use/get_memory.py +++ b/src/do_tool/tool_can_use/get_memory.py @@ -10,7 +10,7 @@ class GetMemoryTool(BaseTool): """从记忆系统中获取相关记忆的工具""" name = "get_memory" - description = "从记忆系统中获取相关记忆" + description = "使用工具从记忆系统中获取相关记忆" parameters = { "type": "object", "properties": { @@ -53,10 +53,11 @@ class GetMemoryTool(BaseTool): else: content = f"{topic}的记忆,你记不太清" - return {"name": "get_memory", "content": content} + return {"type": "memory", "id": topic_list, "content": content} except Exception as e: logger.error(f"记忆获取工具执行失败: {str(e)}") - return {"name": "get_memory", "content": f"记忆获取失败: {str(e)}"} + # 在失败时也保持格式一致,但id可能不适用或设为None/Error + return {"type": "memory_error", "id": topic_list, "content": f"记忆获取失败: {str(e)}"} # 注册工具 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 bd3b1a9c..1cb23fdb 100644 --- a/src/do_tool/tool_can_use/get_time_date.py +++ b/src/do_tool/tool_can_use/get_time_date.py @@ -2,6 +2,7 @@ from src.do_tool.tool_can_use.base_tool import BaseTool from src.common.logger_manager import get_logger from typing import Dict, Any from datetime import datetime +import time logger = get_logger("get_time_date") @@ -32,6 +33,7 @@ class GetCurrentDateTimeTool(BaseTool): current_weekday = datetime.now().strftime("%A") return { - "name": "get_current_date_time", + "type": "time_info", + "id": f"time_info_{time.time()}", "content": f"当前时间: {current_time}, 日期: {current_date}, 年份: {current_year}, 星期: {current_weekday}", } 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 8754e603..a4ded910 100644 --- a/src/do_tool/tool_can_use/lpmm_get_knowledge.py +++ b/src/do_tool/tool_can_use/lpmm_get_knowledge.py @@ -46,11 +46,14 @@ class SearchKnowledgeFromLPMMTool(BaseTool): content = f"你知道这些知识: {knowledge_info}" else: content = f"你不太了解有关{query}的知识" - return {"name": "search_knowledge", "content": content} - return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"} + return {"type": "lpmm_knowledge", "id": query, "content": content} + # 如果获取嵌入失败 + return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你lpmm知识库炸了"} except Exception as e: logger.error(f"知识库搜索工具执行失败: {str(e)}") - return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"} + # 在其他异常情况下,确保 id 仍然是 query (如果它被定义了) + query_id = query if "query" in locals() else "unknown_query" + return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {str(e)}"} # def get_info_from_db( # self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False @@ -133,6 +136,27 @@ class SearchKnowledgeFromLPMMTool(BaseTool): # # 返回所有找到的内容,用换行分隔 # return "\n".join(str(result["content"]) for result in results) + def _format_results(self, results: list) -> str: + """格式化结果""" + if not results: + return "未找到相关知识。" + + formatted_string = "我找到了一些相关知识:\n" + for i, result in enumerate(results): + # chunk_id = result.get("chunk_id") + text = result.get("text", "") + source = result.get("source", "未知来源") + source_type = result.get("source_type", "未知类型") + similarity = result.get("similarity", 0.0) + + formatted_string += ( + f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source} \n内容片段: {text}\n\n" + ) + # 暂时去掉chunk_id + # formatted_string += f"{i + 1}. (相似度: {similarity:.2f}) 类型: {source_type}, 来源: {source}, Chunk ID: {chunk_id} \n内容片段: {text}\n\n" + + return formatted_string + # 注册工具 # register_tool(SearchKnowledgeTool) diff --git a/src/do_tool/tool_can_use/rename_person_tool.py b/src/do_tool/tool_can_use/rename_person_tool.py new file mode 100644 index 00000000..d9f23cf4 --- /dev/null +++ b/src/do_tool/tool_can_use/rename_person_tool.py @@ -0,0 +1,105 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool +from src.plugins.person_info.person_info import person_info_manager +from src.common.logger_manager import get_logger +import time + +logger = get_logger("rename_person_tool") + + +class RenamePersonTool(BaseTool): + name = "rename_person" + description = "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。" + parameters = { + "type": "object", + "properties": { + "person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"}, + "message_content": { + "type": "string", + "description": "可选的。当前的聊天内容或特定要求,用于提供取名建议的上下文。", + }, + }, + "required": ["person_name"], + } + + async def execute(self, function_args: dict, message_txt=""): + """ + 执行取名工具逻辑 + + Args: + function_args (dict): 包含 'person_name' 和可选 'message_content' 的字典 + message_txt (str): 原始消息文本 (这里未使用,因为 message_content 更明确) + + Returns: + dict: 包含执行结果的字典 + """ + person_name_to_find = function_args.get("person_name") + request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 + + if not person_name_to_find: + return {"name": self.name, "content": "错误:必须提供需要重命名的用户昵称 (person_name)。"} + + try: + # 1. 根据昵称查找用户信息 + logger.debug(f"尝试根据昵称 '{person_name_to_find}' 查找用户...") + person_info = await person_info_manager.get_person_info_by_name(person_name_to_find) + + if not person_info: + logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。") + return { + "name": self.name, + "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。", + } + + person_id = person_info.get("person_id") + user_nickname = person_info.get("nickname") # 这是用户原始昵称 + user_cardname = person_info.get("user_cardname") + user_avatar = person_info.get("user_avatar") + + if not person_id: + logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") + return {"name": self.name, "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。"} + + # 2. 调用 qv_person_name 进行取名 + logger.debug( + f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'" + ) + result = await person_info_manager.qv_person_name( + person_id=person_id, + user_nickname=user_nickname, + user_cardname=user_cardname, + user_avatar=user_avatar, + request=request_context, + ) + + # 3. 处理结果 + if result and result.get("nickname"): + new_name = result["nickname"] + # reason = result.get("reason", "未提供理由") + logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") + + content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" + logger.info(content) + return {"type": "info", "id": f"rename_success_{time.time()}", "content": content} + else: + logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") + # 尝试从内存中获取可能已经更新的名字 + current_name = await person_info_manager.get_value(person_id, "person_name") + if current_name and current_name != person_name_to_find: + return { + "name": self.name, + "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。", + } + else: + return { + "name": self.name, + "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。", + } + + except Exception as e: + error_msg = f"重命名失败: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg} + + +# 注册工具 +register_tool(RenamePersonTool) diff --git a/src/heart_flow/background_tasks.py b/src/heart_flow/background_tasks.py index 1b64c205..301c2984 100644 --- a/src/heart_flow/background_tasks.py +++ b/src/heart_flow/background_tasks.py @@ -18,7 +18,9 @@ INTEREST_EVAL_INTERVAL_SECONDS = 5 # 新增聊天超时检查间隔 NORMAL_CHAT_TIMEOUT_CHECK_INTERVAL_SECONDS = 60 # 新增状态评估间隔 -HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 60 +HF_JUDGE_STATE_UPDATE_INTERVAL_SECONDS = 20 +# 新增私聊激活检查间隔 +PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS = 5 # 与兴趣评估类似,设为5秒 CLEANUP_INTERVAL_SECONDS = 1200 STATE_UPDATE_INTERVAL_SECONDS = 60 @@ -71,9 +73,10 @@ class BackgroundTaskManager: self._state_update_task: Optional[asyncio.Task] = None self._cleanup_task: Optional[asyncio.Task] = None self._logging_task: Optional[asyncio.Task] = None - self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None # Nyaa~ 添加聊天超时检查任务的引用 - self._hf_judge_state_update_task: Optional[asyncio.Task] = None # Nyaa~ 添加状态评估任务的引用 - self._into_focus_task: Optional[asyncio.Task] = None # Nyaa~ 添加兴趣评估任务的引用 + self._normal_chat_timeout_check_task: Optional[asyncio.Task] = None + self._hf_judge_state_update_task: Optional[asyncio.Task] = None + self._into_focus_task: Optional[asyncio.Task] = None + self._private_chat_activation_task: Optional[asyncio.Task] = None # 新增私聊激活任务引用 self._tasks: List[Optional[asyncio.Task]] = [] # Keep track of all tasks async def start_tasks(self): @@ -124,6 +127,14 @@ class BackgroundTaskManager: f"专注评估任务已启动 间隔:{INTEREST_EVAL_INTERVAL_SECONDS}s", "_into_focus_task", ), + # 新增私聊激活任务配置 + ( + # Use lambda to pass the interval to the runner function + lambda: self._run_private_chat_activation_cycle(PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS), + "debug", + f"私聊激活检查任务已启动 间隔:{PRIVATE_CHAT_ACTIVATION_CHECK_INTERVAL_SECONDS}s", + "_private_chat_activation_task", + ), ] # 统一启动所有任务 @@ -277,3 +288,11 @@ class BackgroundTaskManager: interval=INTEREST_EVAL_INTERVAL_SECONDS, task_func=self._perform_into_focus_work, ) + + # 新增私聊激活任务运行器 + async def _run_private_chat_activation_cycle(self, interval: int): + await _run_periodic_loop( + task_name="Private Chat Activation Check", + interval=interval, + task_func=self.subheartflow_manager.sbhf_absent_private_into_focus, + ) diff --git a/src/heart_flow/observation.py b/src/heart_flow/observation.py index e34f37d3..2d819a88 100644 --- a/src/heart_flow/observation.py +++ b/src/heart_flow/observation.py @@ -12,9 +12,32 @@ from src.plugins.utils.chat_message_builder import ( num_new_messages_since, get_person_id_list, ) +from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager +from typing import Optional +import difflib +from src.plugins.chat.message import MessageRecv # 添加 MessageRecv 导入 + +# Import the new utility function +from .utils_chat import get_chat_type_and_target_info logger = get_logger("observation") +# --- Define Prompt Templates for Chat Summary --- +Prompt( + """这是qq群聊的聊天记录,请总结以下聊天记录的主题: +{chat_logs} +请用一句话概括,包括人物、事件和主要信息,不要分点。""", + "chat_summary_group_prompt", # Template for group chat +) + +Prompt( + """这是你和{chat_target}的私聊记录,请总结以下聊天记录的主题: +{chat_logs} +请用一句话概括,包括事件,时间,和主要信息,不要分点。""", + "chat_summary_private_prompt", # Template for private chat +) +# --- End Prompt Template Definition --- + # 所有观察的基类 class Observation: @@ -34,28 +57,37 @@ class ChattingObservation(Observation): super().__init__("chat", chat_id) self.chat_id = chat_id + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + + # --- Other attributes initialized in __init__ --- self.talking_message = [] self.talking_message_str = "" self.talking_message_str_truncate = "" - self.name = global_config.BOT_NICKNAME self.nick_name = global_config.BOT_ALIAS_NAMES - self.max_now_obs_len = global_config.observation_context_size self.overlap_len = global_config.compressed_length self.mid_memorys = [] self.max_mid_memory_len = global_config.compress_length_limit self.mid_memory_info = "" - self.person_list = [] - self.llm_summary = LLMRequest( model=global_config.llm_observation, temperature=0.7, max_tokens=300, request_type="chat_observation" ) async def initialize(self): + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) + # logger.debug(f"is_group_chat: {self.is_group_chat}") + # logger.debug(f"chat_target_info: {self.chat_target_info}") + # --- End using utility function --- + + # Fetch initial messages (existing logic) initial_messages = get_raw_msg_before_timestamp_with_chat(self.chat_id, self.last_observe_time, 10) - self.talking_message = initial_messages # 将这些消息设为初始上下文 + self.talking_message = initial_messages self.talking_message_str = await build_readable_messages(self.talking_message) # 进行一次观察 返回观察结果observe_info @@ -109,18 +141,51 @@ class ChattingObservation(Observation): messages=oldest_messages, timestamp_mode="normal", read_mark=0 ) - # 调用 LLM 总结主题 - prompt = ( - f"请总结以下聊天记录的主题:\n{oldest_messages_str}\n用一句话概括包括人物事件和主要信息,不要分点:" - ) - summary = "没有主题的闲聊" # 默认值 + # --- Build prompt using template --- + prompt = None # Initialize prompt as None try: - summary_result, _ = await self.llm_summary.generate_response_async(prompt) - if summary_result: # 确保结果不为空 - summary = summary_result + # 构建 Prompt - 根据 is_group_chat 选择模板 + if self.is_group_chat: + prompt_template_name = "chat_summary_group_prompt" + prompt = await global_prompt_manager.format_prompt( + prompt_template_name, chat_logs=oldest_messages_str + ) + else: + # For private chat, add chat_target to the prompt variables + prompt_template_name = "chat_summary_private_prompt" + # Determine the target name for the prompt + chat_target_name = "对方" # Default fallback + if self.chat_target_info: + # Prioritize person_name, then nickname + chat_target_name = ( + self.chat_target_info.get("person_name") + or self.chat_target_info.get("user_nickname") + or chat_target_name + ) + + # Format the private chat prompt + prompt = await global_prompt_manager.format_prompt( + prompt_template_name, + # Assuming the private prompt template uses {chat_target} + chat_target=chat_target_name, + chat_logs=oldest_messages_str, + ) except Exception as e: - logger.error(f"总结主题失败 for chat {self.chat_id}: {e}") - # 保留默认总结 "没有主题的闲聊" + logger.error(f"构建总结 Prompt 失败 for chat {self.chat_id}: {e}") + # prompt remains None + + summary = "没有主题的闲聊" # 默认值 + + if prompt: # Check if prompt was built successfully + try: + summary_result, _, _ = await self.llm_summary.generate_response(prompt) + if summary_result: # 确保结果不为空 + summary = summary_result + except Exception as e: + logger.error(f"总结主题失败 for chat {self.chat_id}: {e}") + # 保留默认总结 "没有主题的闲聊" + else: + logger.warning(f"因 Prompt 构建失败,跳过 LLM 总结 for chat {self.chat_id}") mid_memory = { "id": str(int(datetime.now().timestamp())), @@ -164,6 +229,70 @@ class ChattingObservation(Observation): f"Chat {self.chat_id} - 压缩早期记忆:{self.mid_memory_info}\n现在聊天内容:{self.talking_message_str}" ) + async def find_best_matching_message(self, search_str: str, min_similarity: float = 0.6) -> Optional[MessageRecv]: + """ + 在 talking_message 中查找与 search_str 最匹配的消息。 + + Args: + search_str: 要搜索的字符串。 + min_similarity: 要求的最低相似度(0到1之间)。 + + Returns: + 匹配的 MessageRecv 实例,如果找不到则返回 None。 + """ + best_match_score = -1.0 + best_match_dict = None + + if not self.talking_message: + logger.debug(f"Chat {self.chat_id}: talking_message is empty, cannot find match for '{search_str}'") + return None + + for message_dict in self.talking_message: + try: + # 临时创建 MessageRecv 以处理文本 + temp_msg = MessageRecv(message_dict) + await temp_msg.process() # 处理消息以获取 processed_plain_text + current_text = temp_msg.processed_plain_text + + if not current_text: # 跳过没有文本内容的消息 + continue + + # 计算相似度 + matcher = difflib.SequenceMatcher(None, search_str, current_text) + score = matcher.ratio() + + # logger.debug(f"Comparing '{search_str}' with '{current_text}', score: {score}") # 可选:用于调试 + + if score > best_match_score: + best_match_score = score + best_match_dict = message_dict + + except Exception as e: + logger.error(f"Error processing message for matching in chat {self.chat_id}: {e}", exc_info=True) + continue # 继续处理下一条消息 + + if best_match_dict is not None and best_match_score >= min_similarity: + logger.debug(f"Found best match for '{search_str}' with score {best_match_score:.2f}") + try: + final_msg = MessageRecv(best_match_dict) + await final_msg.process() + # 确保 MessageRecv 实例有关联的 chat_stream + if hasattr(self, "chat_stream"): + final_msg.update_chat_stream(self.chat_stream) + else: + logger.warning( + f"ChattingObservation instance for chat {self.chat_id} does not have a chat_stream attribute set." + ) + return final_msg + except Exception as e: + logger.error(f"Error creating final MessageRecv for chat {self.chat_id}: {e}", exc_info=True) + return None + else: + logger.debug( + f"No suitable match found for '{search_str}' in chat {self.chat_id} (best score: {best_match_score:.2f}, threshold: {min_similarity})" + ) + return None + async def has_new_messages_since(self, timestamp: float) -> bool: """检查指定时间戳之后是否有新消息""" count = num_new_messages_since(chat_id=self.chat_id, timestamp_start=timestamp) diff --git a/src/heart_flow/sub_heartflow.py b/src/heart_flow/sub_heartflow.py index 8d07e6b5..eb8bbabd 100644 --- a/src/heart_flow/sub_heartflow.py +++ b/src/heart_flow/sub_heartflow.py @@ -13,6 +13,7 @@ from src.plugins.heartFC_chat.normal_chat import NormalChat from src.heart_flow.mai_state_manager import MaiStateInfo from src.heart_flow.chat_state_info import ChatState, ChatStateInfo from src.heart_flow.sub_mind import SubMind +from .utils_chat import get_chat_type_and_target_info # 定义常量 (从 interest.py 移动过来) @@ -238,6 +239,11 @@ class SubHeartflow: self.chat_state_last_time: float = 0 self.history_chat_state: List[Tuple[ChatState, float]] = [] + # --- Initialize attributes --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + # 兴趣检测器 self.interest_chatting: InterestChatting = InterestChatting() @@ -260,11 +266,24 @@ class SubHeartflow: 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 + # 日志前缀 - Moved determination to initialize + self.log_prefix = str(subheartflow_id) # Initial default prefix async def initialize(self): - """异步初始化方法,创建兴趣流""" + """异步初始化方法,创建兴趣流并确定聊天类型""" + + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_id) + # Update log prefix after getting info (potential stream name) + self.log_prefix = ( + chat_manager.get_stream_name(self.subheartflow_id) or self.subheartflow_id + ) # Keep this line or adjust if utils provides name + logger.debug( + f"SubHeartflow {self.chat_id} initialized: is_group={self.is_group_chat}, target_info={self.chat_target_info}" + ) + # --- End using utility function --- + + # Initialize interest system (existing logic) await self.interest_chatting.initialize() logger.debug(f"{self.log_prefix} InterestChatting 实例已初始化。") @@ -286,26 +305,33 @@ class SubHeartflow: async def _start_normal_chat(self) -> bool: """ - 启动 NormalChat 实例, - 进入 CHAT 状态时使用 - - 确保 HeartFChatting 已停止 + 启动 NormalChat 实例,并进行异步初始化。 + 进入 CHAT 状态时使用。 + 确保 HeartFChatting 已停止。 """ await self._stop_heart_fc_chat() # 确保 专注聊天已停止 log_prefix = self.log_prefix try: - # 获取聊天流并创建 NormalChat 实例 + # 获取聊天流并创建 NormalChat 实例 (同步部分) chat_stream = chat_manager.get_stream(self.chat_id) + if not chat_stream: + logger.error(f"{log_prefix} 无法获取 chat_stream,无法启动 NormalChat。") + return False + self.normal_chat_instance = NormalChat(chat_stream=chat_stream, interest_dict=self.get_interest_dict()) + # 进行异步初始化 + await self.normal_chat_instance.initialize() + + # 启动聊天任务 logger.info(f"{log_prefix} 开始普通聊天,随便水群...") - await self.normal_chat_instance.start_chat() # <--- 修正:调用 start_chat + await self.normal_chat_instance.start_chat() # start_chat now ensures init is called again if needed return True except Exception as e: - logger.error(f"{log_prefix} 启动 NormalChat 时出错: {e}") + logger.error(f"{log_prefix} 启动 NormalChat 或其初始化时出错: {e}") logger.error(traceback.format_exc()) - self.normal_chat_instance = None # 启动失败,清理实例 + self.normal_chat_instance = None # 启动/初始化失败,清理实例 return False async def _stop_heart_fc_chat(self): diff --git a/src/heart_flow/sub_mind.py b/src/heart_flow/sub_mind.py index f414e6b2..1275fbbf 100644 --- a/src/heart_flow/sub_mind.py +++ b/src/heart_flow/sub_mind.py @@ -1,4 +1,4 @@ -from .observation import Observation +from .observation import ChattingObservation from src.plugins.models.utils_model import LLMRequest from src.config.config import global_config import time @@ -20,34 +20,63 @@ logger = get_logger("sub_heartflow") def init_prompt(): - prompt = "" - prompt += "{extra_info}\n" - prompt += "{relation_prompt}\n" - prompt += "你的名字是{bot_name},{prompt_personality}\n" - prompt += "{last_loop_prompt}\n" - prompt += "{cycle_info_block}\n" - prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n" - prompt += "\n你现在{mood_info}\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" + # --- Group Chat Prompt --- + group_prompt = """ +{extra_info} +{relation_prompt} +你的名字是{bot_name},{prompt_personality} +{last_loop_prompt} +{cycle_info_block} +现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容: +{chat_observe_info} - Prompt(prompt, "sub_heartflow_prompt_before") +你现在{mood_info} +请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。 +思考并输出你的内心想法 +输出要求: +1. 根据聊天内容生成你的想法,{hf_do_next} +2. 不要分点、不要使用表情符号 +3. 避免多余符号(冒号、引号、括号等) +4. 语言简洁自然,不要浮夸 +5. 如果你刚发言,并且没有人回复你,不要回复 +工具使用说明: +1. 输出想法后考虑是否需要使用工具 +2. 工具可获取信息或执行操作 +3. 如需处理消息或回复,请使用工具。""" + Prompt(group_prompt, "sub_heartflow_prompt_before") - prompt = "" - prompt += "刚刚你的内心想法是:{current_thinking_info}\n" - prompt += "{if_replan_prompt}\n" + # --- Private Chat Prompt --- + private_prompt = """ +{extra_info} +{relation_prompt} +你的名字是{bot_name},{prompt_personality} +{last_loop_prompt} +{cycle_info_block} +现在是{time_now},你正在上网,和 {chat_target_name} 私聊,以下是你们的聊天内容: +{chat_observe_info} - Prompt(prompt, "last_loop") +你现在{mood_info} +请仔细阅读聊天内容,想想你和 {chat_target_name} 的关系,回顾你们刚刚的交流,你刚刚发言和对方的反应,思考聊天的主题。 +请思考你要不要回复以及如何回复对方。然后思考你是否需要使用函数工具。 +思考并输出你的内心想法 +输出要求: +1. 根据聊天内容生成你的想法,{hf_do_next} +2. 不要分点、不要使用表情符号 +3. 避免多余符号(冒号、引号、括号等) +4. 语言简洁自然,不要浮夸 +5. 如果你刚发言,对方没有回复你,请谨慎回复 +工具使用说明: +1. 输出想法后考虑是否需要使用工具 +2. 工具可获取信息或执行操作 +3. 如需处理消息或回复,请使用工具。""" + Prompt(private_prompt, "sub_heartflow_prompt_private_before") # New template name + + # --- Last Loop Prompt (remains the same) --- + last_loop_t = """ +刚刚你的内心想法是:{current_thinking_info} +{if_replan_prompt} +""" + Prompt(last_loop_t, "last_loop") def calculate_similarity(text_a: str, text_b: str) -> float: @@ -85,7 +114,7 @@ def calculate_replacement_probability(similarity: float) -> float: class SubMind: - def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: Observation): + def __init__(self, subheartflow_id: str, chat_state: ChatStateInfo, observations: ChattingObservation): self.last_active_time = None self.subheartflow_id = subheartflow_id @@ -101,10 +130,40 @@ class SubMind: self.current_mind = "" self.past_mind = [] - self.structured_info = {} + self.structured_info = [] + self.structured_info_str = "" name = chat_manager.get_stream_name(self.subheartflow_id) self.log_prefix = f"[{name}] " + self._update_structured_info_str() + + def _update_structured_info_str(self): + """根据 structured_info 更新 structured_info_str""" + if not self.structured_info: + self.structured_info_str = "" + return + + lines = ["【信息】"] + for item in self.structured_info: + # 简化展示,突出内容和类型,包含TTL供调试 + type_str = item.get("type", "未知类型") + content_str = item.get("content", "") + + if type_str == "info": + lines.append(f"刚刚: {content_str}") + elif type_str == "memory": + lines.append(f"{content_str}") + elif type_str == "comparison_result": + lines.append(f"数字大小比较结果: {content_str}") + elif type_str == "time_info": + lines.append(f"{content_str}") + elif type_str == "lpmm_knowledge": + lines.append(f"你知道:{content_str}") + else: + lines.append(f"{type_str}的信息: {content_str}") + + self.structured_info_str = "\n".join(lines) + logger.debug(f"{self.log_prefix} 更新 structured_info_str: \n{self.structured_info_str}") async def do_thinking_before_reply(self, history_cycle: list[CycleInfo] = None): """ @@ -116,18 +175,50 @@ class SubMind: # 更新活跃时间 self.last_active_time = time.time() + # ---------- 0. 更新和清理 structured_info ---------- + if self.structured_info: + logger.debug( + f"{self.log_prefix} 更新前的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + ) + updated_info = [] + for item in self.structured_info: + item["ttl"] -= 1 + if item["ttl"] > 0: + updated_info.append(item) + else: + logger.debug(f"{self.log_prefix} 移除过期的 structured_info 项: {item['id']}") + self.structured_info = updated_info + logger.debug( + f"{self.log_prefix} 更新后的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + ) + self._update_structured_info_str() + logger.debug( + f"{self.log_prefix} 当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}" + ) + # ---------- 1. 准备基础数据 ---------- # 获取现有想法和情绪状态 previous_mind = self.current_mind if self.current_mind else "" mood_info = self.chat_state.mood # 获取观察对象 - observation = self.observations[0] - if not observation: - logger.error(f"{self.log_prefix} 无法获取观察对象") - self.update_current_mind("(我没看到任何聊天内容...)") + observation: ChattingObservation = self.observations[0] if self.observations else None + if not observation or not hasattr(observation, "is_group_chat"): # Ensure it's ChattingObservation or similar + logger.error(f"{self.log_prefix} 无法获取有效的观察对象或缺少聊天类型信息") + self.update_current_mind("(观察出错了...)") return self.current_mind, self.past_mind + is_group_chat = observation.is_group_chat + # logger.debug(f"is_group_chat: {is_group_chat}") + + chat_target_info = observation.chat_target_info + chat_target_name = "对方" # Default for private + if not is_group_chat and chat_target_info: + chat_target_name = ( + chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or chat_target_name + ) + # --- End getting observation info --- + # 获取观察内容 chat_observe_info = observation.get_observe_info() person_list = observation.person_list @@ -238,19 +329,39 @@ class SubMind: )[0] # ---------- 4. 构建最终提示词 ---------- - # 获取提示词模板并填充数据 - prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( - extra_info="", # 可以在这里添加额外信息 - prompt_personality=prompt_personality, - relation_prompt=relation_prompt, - bot_name=individuality.name, - 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, - cycle_info_block=cycle_info_block, - ) + # --- Choose template based on chat type --- + logger.debug(f"is_group_chat: {is_group_chat}") + if is_group_chat: + template_name = "sub_heartflow_prompt_before" + prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( + extra_info=self.structured_info_str, + prompt_personality=prompt_personality, + relation_prompt=relation_prompt, + bot_name=individuality.name, + 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, + cycle_info_block=cycle_info_block, + # chat_target_name is not used in group prompt + ) + else: # Private chat + template_name = "sub_heartflow_prompt_private_before" + prompt = (await global_prompt_manager.get_prompt_async(template_name)).format( + extra_info=self.structured_info_str, + prompt_personality=prompt_personality, + relation_prompt=relation_prompt, # Might need adjustment for private context + bot_name=individuality.name, + time_now=time_now, + chat_target_name=chat_target_name, # Pass target name + chat_observe_info=chat_observe_info, + mood_info=mood_info, + hf_do_next=hf_do_next, + last_loop_prompt=last_loop_prompt, + cycle_info_block=cycle_info_block, + ) + # --- End choosing template --- # ---------- 5. 执行LLM请求并处理响应 ---------- content = "" # 初始化内容变量 @@ -390,7 +501,7 @@ class SubMind: tool_instance: 工具使用器实例 """ tool_results = [] - structured_info = {} # 动态生成键 + new_structured_items = [] # 收集新产生的结构化信息 # 执行所有工具调用 for tool_call in tool_calls: @@ -398,23 +509,34 @@ class SubMind: result = await tool_instance._execute_tool_call(tool_call) if result: tool_results.append(result) + # 创建新的结构化信息项 + new_item = { + "type": result.get("type", "unknown_type"), # 使用 'type' 键 + "id": result.get("id", f"fallback_id_{time.time()}"), # 使用 'id' 键 + "content": result.get("content", ""), # 'content' 键保持不变 + "ttl": 3, + } + new_structured_items.append(new_item) - # 使用工具名称作为键 - 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}") + logger.error(traceback.format_exc()) # 添加 traceback 记录 - # 如果有工具结果,记录并更新结构化信息 - if structured_info: - logger.debug(f"工具调用收集到结构化信息: {safe_json_dumps(structured_info, ensure_ascii=False)}") - self.structured_info = structured_info + # 如果有新的工具结果,记录并更新结构化信息 + if new_structured_items: + self.structured_info.extend(new_structured_items) # 添加到现有列表 + logger.debug(f"工具调用收集到新的结构化信息: {safe_json_dumps(new_structured_items, ensure_ascii=False)}") + # logger.debug(f"当前完整的 structured_info: {safe_json_dumps(self.structured_info, ensure_ascii=False)}") # 可以取消注释以查看完整列表 + self._update_structured_info_str() # 添加新信息后,更新字符串表示 def update_current_mind(self, response): - self.past_mind.append(self.current_mind) + if self.current_mind: # 只有当 current_mind 非空时才添加到 past_mind + self.past_mind.append(self.current_mind) + # 可以考虑限制 past_mind 的大小,例如: + # max_past_mind_size = 10 + # if len(self.past_mind) > max_past_mind_size: + # self.past_mind.pop(0) # 移除最旧的 + self.current_mind = response diff --git a/src/heart_flow/subheartflow_manager.py b/src/heart_flow/subheartflow_manager.py index 30119cca..f06a68c8 100644 --- a/src/heart_flow/subheartflow_manager.py +++ b/src/heart_flow/subheartflow_manager.py @@ -127,6 +127,8 @@ class SubHeartflowManager: # 添加聊天观察者 observation = ChattingObservation(chat_id=subheartflow_id) + await observation.initialize() + new_subflow.add_observation(observation) # 注册子心流 @@ -335,28 +337,37 @@ class SubHeartflowManager: async def sbhf_absent_into_chat(self): """ - 随机选一个 ABSENT 状态的子心流,评估是否应转换为 CHAT 状态。 + 随机选一个 ABSENT 状态的 *群聊* 子心流,评估是否应转换为 CHAT 状态。 每次调用最多转换一个。 + 私聊会被忽略。 """ current_mai_state = self.mai_state_info.get_current_state() chat_limit = current_mai_state.get_normal_chat_max_num() async with self._lock: - # 1. 筛选出所有 ABSENT 状态的子心流 - absent_subflows = [ - hf for hf in self.subheartflows.values() if hf.chat_state.chat_status == ChatState.ABSENT + # 1. 筛选出所有 ABSENT 状态的 *群聊* 子心流 + absent_group_subflows = [ + hf + for hf in self.subheartflows.values() + if hf.chat_state.chat_status == ChatState.ABSENT and hf.is_group_chat ] - if not absent_subflows: - logger.debug("没有摸鱼的子心流可以评估。") # 日志太频繁,注释掉 + if not absent_group_subflows: + # logger.debug("没有摸鱼的群聊子心流可以评估。") # 日志太频繁 return # 没有目标,直接返回 # 2. 随机选一个幸运儿 - sub_hf_to_evaluate = random.choice(absent_subflows) + sub_hf_to_evaluate = random.choice(absent_group_subflows) flow_id = sub_hf_to_evaluate.subheartflow_id stream_name = chat_manager.get_stream_name(flow_id) or flow_id log_prefix = f"[{stream_name}]" + # --- Private chat check (redundant due to filter above, but safe) --- + # if not sub_hf_to_evaluate.is_group_chat: + # logger.debug(f"{log_prefix} 是私聊,跳过 CHAT 状态评估。") + # return + # --- End check --- + # 3. 检查 CHAT 上限 current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) if current_chat_count >= chat_limit: @@ -658,8 +669,10 @@ class SubHeartflowManager: # --- 新增:处理来自 HeartFChatting 的状态转换请求 --- # async def sbhf_focus_into_absent(self, subflow_id: Any): """ - 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT。 + 接收来自 HeartFChatting 的请求,将特定子心流的状态转换为 ABSENT 或 CHAT。 通常在连续多次 "no_reply" 后被调用。 + 对于私聊,总是转换为 ABSENT。 + 对于群聊,随机决定转换为 ABSENT 或 CHAT (如果 CHAT 未达上限)。 Args: subflow_id: 需要转换状态的子心流 ID。 @@ -667,50 +680,46 @@ class SubHeartflowManager: async with self._lock: subflow = self.subheartflows.get(subflow_id) if not subflow: - logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT") + logger.warning(f"[状态转换请求] 尝试转换不存在的子心流 {subflow_id} 到 ABSENT/CHAT") return stream_name = chat_manager.get_stream_name(subflow_id) or subflow_id current_state = subflow.chat_state.chat_status - # 仅当子心流处于 FOCUSED 状态时才进行转换 - # 因为 HeartFChatting 只在 FOCUSED 状态下运行 if current_state == ChatState.FOCUSED: - target_state = ChatState.ABSENT # 默认目标状态 - log_reason = "默认转换" + target_state = ChatState.ABSENT # Default target + log_reason = "默认转换 (私聊或群聊)" - # 决定是去 ABSENT 还是 CHAT - if random.random() < 0.5: - target_state = ChatState.ABSENT - log_reason = "随机选择 ABSENT" - logger.debug(f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 ABSENT") - else: - # 尝试进入 CHAT,先检查限制 - current_mai_state = self.mai_state_info.get_current_state() - chat_limit = current_mai_state.get_normal_chat_max_num() - # 使用不上锁的版本,因为我们已经在锁内 - current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) + # --- Modify logic based on chat type --- # + if subflow.is_group_chat: + # Group chat: Decide between ABSENT or CHAT + if random.random() < 0.5: # 50% chance to try CHAT + current_mai_state = self.mai_state_info.get_current_state() + chat_limit = current_mai_state.get_normal_chat_max_num() + current_chat_count = self.count_subflows_by_state_nolock(ChatState.CHAT) - if current_chat_count < chat_limit: - target_state = ChatState.CHAT - log_reason = f"随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" - logger.debug( - f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,未达上限 ({current_chat_count}/{chat_limit})" - ) - else: + if current_chat_count < chat_limit: + target_state = ChatState.CHAT + log_reason = f"群聊随机选择 CHAT (当前 {current_chat_count}/{chat_limit})" + else: + target_state = ChatState.ABSENT # Fallback to ABSENT if CHAT limit reached + log_reason = ( + f"群聊随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" + ) + else: # 50% chance to go directly to ABSENT target_state = ChatState.ABSENT - log_reason = f"随机选择 CHAT 但已达上限 ({current_chat_count}/{chat_limit}),转为 ABSENT" - logger.debug( - f"[状态转换请求] {stream_name} ({current_state.value}) 随机决定进入 CHAT,但已达上限 ({current_chat_count}/{chat_limit}),改为进入 ABSENT" - ) + log_reason = "群聊随机选择 ABSENT" + else: + # Private chat: Always go to ABSENT + target_state = ChatState.ABSENT + log_reason = "私聊退出 FOCUSED,转为 ABSENT" + # --- End modification --- # - # 开始转换 logger.info( f"[状态转换请求] 接收到请求,将 {stream_name} (当前: {current_state.value}) 尝试转换为 {target_state.value} ({log_reason})" ) try: await subflow.change_chat_state(target_state) - # 检查最终状态 final_state = subflow.chat_state.chat_status if final_state == target_state: logger.debug(f"[状态转换请求] {stream_name} 状态已成功转换为 {final_state.value}") @@ -730,3 +739,106 @@ class SubHeartflowManager: ) # --- 结束新增 --- # + + # --- 新增:处理私聊从 ABSENT 直接到 FOCUSED 的逻辑 --- # + async def sbhf_absent_private_into_focus(self): + """检查 ABSENT 状态的私聊子心流是否有新活动,若有且未达 FOCUSED 上限,则直接转换为 FOCUSED。""" + log_prefix_task = "[私聊激活检查]" + transitioned_count = 0 + checked_count = 0 + + # --- 获取当前状态和 FOCUSED 上限 --- # + current_mai_state = self.mai_state_info.get_current_state() + focused_limit = current_mai_state.get_focused_chat_max_num() + + # --- 检查是否允许 FOCUS 模式 --- # + if not global_config.allow_focus_mode: + # Log less frequently to avoid spam + # if int(time.time()) % 60 == 0: + # logger.debug(f"{log_prefix_task} 配置不允许进入 FOCUSED 状态") + return + + if focused_limit <= 0: + # logger.debug(f"{log_prefix_task} 当前状态 ({current_mai_state.value}) 不允许 FOCUSED 子心流") + return + + async with self._lock: + # --- 获取当前 FOCUSED 计数 (不上锁版本) --- # + current_focused_count = self.count_subflows_by_state_nolock(ChatState.FOCUSED) + + # --- 筛选出所有 ABSENT 状态的私聊子心流 --- # + eligible_subflows = [ + hf + for hf in self.subheartflows.values() + if hf.chat_state.chat_status == ChatState.ABSENT and not hf.is_group_chat + ] + checked_count = len(eligible_subflows) + + if not eligible_subflows: + # logger.debug(f"{log_prefix_task} 没有 ABSENT 状态的私聊子心流可以评估。") + return + + # --- 遍历评估每个符合条件的私聊 --- # + for sub_hf in eligible_subflows: + # --- 再次检查 FOCUSED 上限,因为可能有多个同时激活 --- # + if current_focused_count >= focused_limit: + logger.debug( + f"{log_prefix_task} 已达专注上限 ({current_focused_count}/{focused_limit}),停止检查后续私聊。" + ) + break # 已满,无需再检查其他私聊 + + flow_id = sub_hf.subheartflow_id + stream_name = chat_manager.get_stream_name(flow_id) or flow_id + log_prefix = f"[{stream_name}]({log_prefix_task})" + + try: + # --- 检查是否有新活动 --- # + observation = sub_hf._get_primary_observation() # 获取主要观察者 + is_active = False + if observation: + # 检查自上次状态变为 ABSENT 后是否有新消息 + # 使用 chat_state_changed_time 可能更精确 + # 加一点点缓冲时间(例如 1 秒)以防时间戳完全相等 + timestamp_to_check = sub_hf.chat_state_changed_time - 1 + has_new = await observation.has_new_messages_since(timestamp_to_check) + if has_new: + is_active = True + logger.debug(f"{log_prefix} 检测到新消息,标记为活跃。") + # 可选:检查兴趣度是否大于0 (如果需要) + # interest_level = await sub_hf.interest_chatting.get_interest() + # if interest_level > 0: + # is_active = True + # logger.debug(f"{log_prefix} 检测到兴趣度 > 0 ({interest_level:.2f}),标记为活跃。") + else: + logger.warning(f"{log_prefix} 无法获取主要观察者来检查活动状态。") + + # --- 如果活跃且未达上限,则尝试转换 --- # + if is_active: + logger.info( + f"{log_prefix} 检测到活跃且未达专注上限 ({current_focused_count}/{focused_limit}),尝试转换为 FOCUSED。" + ) + await sub_hf.change_chat_state(ChatState.FOCUSED) + # 确认转换成功 + if sub_hf.chat_state.chat_status == ChatState.FOCUSED: + transitioned_count += 1 + current_focused_count += 1 # 更新计数器以供本轮后续检查 + logger.info(f"{log_prefix} 成功进入 FOCUSED 状态。") + else: + logger.warning( + f"{log_prefix} 尝试进入 FOCUSED 状态失败。当前状态: {sub_hf.chat_state.chat_status.value}" + ) + # else: # 不活跃,无需操作 + # logger.debug(f"{log_prefix} 未检测到新活动,保持 ABSENT。") + + except Exception as e: + logger.error(f"{log_prefix} 检查私聊活动或转换状态时出错: {e}", exc_info=True) + + # --- 循环结束后记录总结日志 --- # + if transitioned_count > 0: + logger.debug( + f"{log_prefix_task} 完成,共检查 {checked_count} 个私聊,{transitioned_count} 个转换为 FOCUSED。" + ) + + # --- 结束新增 --- # + + # --- 结束新增:处理来自 HeartFChatting 的状态转换请求 --- # diff --git a/src/heart_flow/utils_chat.py b/src/heart_flow/utils_chat.py new file mode 100644 index 00000000..c3f81a14 --- /dev/null +++ b/src/heart_flow/utils_chat.py @@ -0,0 +1,74 @@ +import asyncio +from typing import Optional, Tuple, Dict +from src.common.logger_manager import get_logger +from src.plugins.chat.chat_stream import chat_manager +from src.plugins.person_info.person_info import person_info_manager + +logger = get_logger("heartflow_utils") + + +async def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional[Dict]]: + """ + 获取聊天类型(是否群聊)和私聊对象信息。 + + Args: + chat_id: 聊天流ID + + Returns: + Tuple[bool, Optional[Dict]]: + - bool: 是否为群聊 (True 是群聊, False 是私聊或未知) + - Optional[Dict]: 如果是私聊,包含对方信息的字典;否则为 None。 + 字典包含: platform, user_id, user_nickname, person_id, person_name + """ + is_group_chat = False # Default to private/unknown + chat_target_info = None + + try: + chat_stream = await asyncio.to_thread(chat_manager.get_stream, chat_id) # Use to_thread if get_stream is sync + # If get_stream is already async, just use: chat_stream = await chat_manager.get_stream(chat_id) + + if chat_stream: + if chat_stream.group_info: + is_group_chat = True + chat_target_info = None # Explicitly None for group chat + elif chat_stream.user_info: # It's a private chat + is_group_chat = False + user_info = chat_stream.user_info + platform = chat_stream.platform + user_id = user_info.user_id + + # Initialize target_info with basic info + target_info = { + "platform": platform, + "user_id": user_id, + "user_nickname": user_info.user_nickname, + "person_id": None, + "person_name": None, + } + + # Try to fetch person info + try: + # Assume get_person_id is sync (as per original code), keep using to_thread + person_id = await asyncio.to_thread(person_info_manager.get_person_id, platform, user_id) + person_name = None + if person_id: + # get_value is async, so await it directly + person_name = await person_info_manager.get_value(person_id, "person_name") + + target_info["person_id"] = person_id + target_info["person_name"] = person_name + except Exception as person_e: + logger.warning( + f"获取 person_id 或 person_name 时出错 for {platform}:{user_id} in utils: {person_e}" + ) + + chat_target_info = target_info + else: + logger.warning(f"无法获取 chat_stream for {chat_id} in utils") + # Keep defaults: is_group_chat=False, chat_target_info=None + + except Exception as e: + logger.error(f"获取聊天类型和目标信息时出错 for {chat_id}: {e}", exc_info=True) + # Keep defaults on error + + return is_group_chat, chat_target_info diff --git a/src/plugins/chat/bot.py b/src/plugins/chat/bot.py index 89e171ba..9c4a3358 100644 --- a/src/plugins/chat/bot.py +++ b/src/plugins/chat/bot.py @@ -83,6 +83,13 @@ class ChatBot: logger.debug(f"用户{userinfo.user_id}被禁止回复") return + if groupinfo is None: + logger.trace("检测到私聊消息,检查") + # 好友黑名单拦截 + if userinfo.user_id not in global_config.talk_allowed_private: + logger.debug(f"用户{userinfo.user_id}没有私聊权限") + return + # 群聊黑名单拦截 if groupinfo is not None and groupinfo.group_id not in global_config.talk_allowed_groups: logger.trace(f"群{groupinfo.group_id}被禁止回复") diff --git a/src/plugins/chat/message.py b/src/plugins/chat/message.py index e753a999..a0ff33ab 100644 --- a/src/plugins/chat/message.py +++ b/src/plugins/chat/message.py @@ -31,19 +31,21 @@ class Message(MessageBase): def __init__( self, message_id: str, - timestamp: float, chat_stream: ChatStream, user_info: UserInfo, message_segment: Optional[Seg] = None, + timestamp: Optional[float] = None, reply: Optional["MessageRecv"] = None, detailed_plain_text: str = "", processed_plain_text: str = "", ): + # 使用传入的时间戳或当前时间 + current_timestamp = timestamp if timestamp is not None else round(time.time(), 3) # 构造基础消息信息 message_info = BaseMessageInfo( platform=chat_stream.platform, message_id=message_id, - time=timestamp, + time=current_timestamp, group_info=chat_stream.group_info, user_info=user_info, ) @@ -164,11 +166,12 @@ class MessageProcessBase(Message): message_segment: Optional[Seg] = None, reply: Optional["MessageRecv"] = None, thinking_start_time: float = 0, + timestamp: Optional[float] = None, ): - # 调用父类初始化 + # 调用父类初始化,传递时间戳 super().__init__( message_id=message_id, - timestamp=round(time.time(), 3), # 保留3位小数 + timestamp=timestamp, chat_stream=chat_stream, user_info=bot_user_info, message_segment=message_segment, @@ -238,8 +241,9 @@ class MessageThinking(MessageProcessBase): bot_user_info: UserInfo, reply: Optional["MessageRecv"] = None, thinking_start_time: float = 0, + timestamp: Optional[float] = None, ): - # 调用父类初始化 + # 调用父类初始化,传递时间戳 super().__init__( message_id=message_id, chat_stream=chat_stream, @@ -247,6 +251,7 @@ class MessageThinking(MessageProcessBase): message_segment=None, # 思考状态不需要消息段 reply=reply, thinking_start_time=thinking_start_time, + timestamp=timestamp, ) # 思考状态特有属性 @@ -289,7 +294,9 @@ class MessageSending(MessageProcessBase): def set_reply(self, reply: Optional["MessageRecv"] = None): """设置回复消息""" - if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: + # print(f"set_reply: {reply}") + # if self.message_info.format_info is not None and "reply" in self.message_info.format_info.accept_format: + if True: if reply: self.reply = reply if self.reply: diff --git a/src/plugins/chat/message_sender.py b/src/plugins/chat/message_sender.py index c159a4bb..61e2dd49 100644 --- a/src/plugins/chat/message_sender.py +++ b/src/plugins/chat/message_sender.py @@ -209,25 +209,32 @@ class MessageManager: _ = message.update_thinking_time() # 更新思考时间 thinking_start_time = message.thinking_start_time now_time = time.time() + logger.debug(f"thinking_start_time:{thinking_start_time},now_time:{now_time}") thinking_messages_count, thinking_messages_length = count_messages_between( start_time=thinking_start_time, end_time=now_time, stream_id=message.chat_stream.stream_id ) + # print(f"message.reply:{message.reply}") # --- 条件应用 set_reply 逻辑 --- + logger.debug( + f"[message.apply_set_reply_logic:{message.apply_set_reply_logic},message.is_head:{message.is_head},thinking_messages_count:{thinking_messages_count},thinking_messages_length:{thinking_messages_length},message.is_private_message():{message.is_private_message()}]" + ) if ( message.apply_set_reply_logic # 检查标记 and message.is_head - and (thinking_messages_count > 4 or thinking_messages_length > 250) + and (thinking_messages_count > 3 or thinking_messages_length > 200) and not message.is_private_message() ): logger.debug( f"[{message.chat_stream.stream_id}] 应用 set_reply 逻辑: {message.processed_plain_text[:20]}..." ) - message.set_reply() + message.set_reply(message.reply) # --- 结束条件 set_reply --- await message.process() # 预处理消息内容 + logger.debug(f"{message}") + # 使用全局 message_sender 实例 await send_message(message) await self.storage.store_message(message, message.chat_stream) diff --git a/src/plugins/chat/utils.py b/src/plugins/chat/utils.py index cdaa2194..53e8f6f6 100644 --- a/src/plugins/chat/utils.py +++ b/src/plugins/chat/utils.py @@ -6,13 +6,13 @@ from collections import Counter import jieba import numpy as np from src.common.logger import get_module_logger +from pymongo.errors import PyMongoError 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 import MessageRecv from maim_message import UserInfo -from .chat_stream import ChatStream from ..moods.moods import MoodManager from ...common.database import db @@ -107,55 +107,6 @@ async def get_embedding(text, request_type="embedding"): return embedding -async def get_recent_group_messages(chat_id: str, limit: int = 12) -> list: - """从数据库获取群组最近的消息记录 - - Args: - chat_id: 群组ID - limit: 获取消息数量,默认12条 - - Returns: - list: Message对象列表,按时间正序排列 - """ - - # 从数据库获取最近消息 - recent_messages = list( - db.messages.find( - {"chat_id": chat_id}, - ) - .sort("time", -1) - .limit(limit) - ) - - if not recent_messages: - return [] - - # 转换为 Message对象列表 - message_objects = [] - for msg_data in recent_messages: - try: - chat_info = msg_data.get("chat_info", {}) - chat_stream = ChatStream.from_dict(chat_info) - user_info = msg_data.get("user_info", {}) - user_info = UserInfo.from_dict(user_info) - msg = Message( - message_id=msg_data["message_id"], - chat_stream=chat_stream, - timestamp=msg_data["time"], - user_info=user_info, - processed_plain_text=msg_data.get("processed_text", ""), - detailed_plain_text=msg_data.get("detailed_plain_text", ""), - ) - message_objects.append(msg) - except KeyError: - logger.warning("数据库中存在无效的消息") - continue - - # 按时间正序排列 - message_objects.reverse() - return message_objects - - def get_recent_group_detailed_plain_text(chat_stream_id: str, limit: int = 12, combine=False): recent_messages = list( db.messages.find( @@ -613,93 +564,45 @@ def count_messages_between(start_time: float, end_time: float, stream_id: str) - """计算两个时间点之间的消息数量和文本总长度 Args: - start_time (float): 起始时间戳 - end_time (float): 结束时间戳 + start_time (float): 起始时间戳 (不包含) + end_time (float): 结束时间戳 (包含) stream_id (str): 聊天流ID Returns: tuple[int, int]: (消息数量, 文本总长度) - - 消息数量:包含起始时间的消息,不包含结束时间的消息 - - 文本总长度:所有消息的processed_plain_text长度之和 """ + count = 0 + total_length = 0 + + # 参数校验 (可选但推荐) + if start_time >= end_time: + # logger.debug(f"开始时间 {start_time} 大于或等于结束时间 {end_time},返回 0, 0") + return 0, 0 + if not stream_id: + logger.error("stream_id 不能为空") + return 0, 0 + + # 直接查询时间范围内的消息 + # time > start_time AND time <= end_time + query = {"chat_id": stream_id, "time": {"$gt": start_time, "$lte": end_time}} + try: - # 获取开始时间之前最新的一条消息 - start_message = db.messages.find_one( - {"chat_id": stream_id, "time": {"$lte": start_time}}, - sort=[("time", -1), ("_id", -1)], # 按时间倒序,_id倒序(最后插入的在前) - ) + # 执行查询 + messages_cursor = db.messages.find(query) - # 获取结束时间最近的一条消息 - # 先找到结束时间点的所有消息 - end_time_messages = list( - db.messages.find( - {"chat_id": stream_id, "time": {"$lte": end_time}}, - sort=[("time", -1)], # 先按时间倒序 - ).limit(10) - ) # 限制查询数量,避免性能问题 - - if not end_time_messages: - logger.warning(f"未找到结束时间 {end_time} 之前的消息") - return 0, 0 - - # 找到最大时间 - max_time = end_time_messages[0]["time"] - # 在最大时间的消息中找最后插入的(_id最大的) - end_message = max([msg for msg in end_time_messages if msg["time"] == max_time], key=lambda x: x["_id"]) - - if not start_message: - logger.warning(f"未找到开始时间 {start_time} 之前的消息") - return 0, 0 - - # 调试输出 - # print("\n=== 消息范围信息 ===") - # print("Start message:", { - # "message_id": start_message.get("message_id"), - # "time": start_message.get("time"), - # "text": start_message.get("processed_plain_text", ""), - # "_id": str(start_message.get("_id")) - # }) - # print("End message:", { - # "message_id": end_message.get("message_id"), - # "time": end_message.get("time"), - # "text": end_message.get("processed_plain_text", ""), - # "_id": str(end_message.get("_id")) - # }) - # print("Stream ID:", stream_id) - - # 如果结束消息的时间等于开始时间,返回0 - if end_message["time"] == start_message["time"]: - return 0, 0 - - # 获取并打印这个时间范围内的所有消息 - # print("\n=== 时间范围内的所有消息 ===") - all_messages = list( - db.messages.find( - {"chat_id": stream_id, "time": {"$gte": start_message["time"], "$lte": end_message["time"]}}, - sort=[("time", 1), ("_id", 1)], # 按时间正序,_id正序 - ) - ) - - count = 0 - total_length = 0 - for msg in all_messages: + # 遍历结果计算数量和长度 + for msg in messages_cursor: count += 1 - text_length = len(msg.get("processed_plain_text", "")) - total_length += text_length - # print(f"\n消息 {count}:") - # print({ - # "message_id": msg.get("message_id"), - # "time": msg.get("time"), - # "text": msg.get("processed_plain_text", ""), - # "text_length": text_length, - # "_id": str(msg.get("_id")) - # }) + total_length += len(msg.get("processed_plain_text", "")) - # 如果时间不同,需要把end_message本身也计入 - return count - 1, total_length + # logger.debug(f"查询范围 ({start_time}, {end_time}] 内找到 {count} 条消息,总长度 {total_length}") + return count, total_length - except Exception as e: - logger.error(f"计算消息数量时出错: {str(e)}") + except PyMongoError as e: + logger.error(f"查询 stream_id={stream_id} 在 ({start_time}, {end_time}] 范围内的消息时出错: {e}") + return 0, 0 + except Exception as e: # 保留一个通用异常捕获以防万一 + logger.error(f"计算消息数量时发生意外错误: {e}") return 0, 0 diff --git a/src/plugins/heartFC_chat/heartFC_chat.py b/src/plugins/heartFC_chat/heartFC_chat.py index 476ca868..f6a7ebb3 100644 --- a/src/plugins/heartFC_chat/heartFC_chat.py +++ b/src/plugins/heartFC_chat/heartFC_chat.py @@ -26,7 +26,7 @@ from .heartFC_sender import HeartFCSender from src.plugins.chat.utils import process_llm_response from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.moods.moods import MoodManager -from src.individuality.individuality import Individuality +from src.heart_flow.utils_chat import get_chat_type_and_target_info from src.plugins.group_nickname.nickname_utils import trigger_nickname_analysis_if_needed @@ -195,7 +195,12 @@ class HeartFChatting: self.on_consecutive_no_reply_callback = on_consecutive_no_reply_callback # 日志前缀 - self.log_prefix: str = f"[{chat_manager.get_stream_name(chat_id) or chat_id}]" + self.log_prefix: str = str(chat_id) # Initial default, will be updated + + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- # 动作管理器 self.action_manager = ActionManager() @@ -235,22 +240,35 @@ class HeartFChatting: async def _initialize(self) -> bool: """ - 懒初始化以使用提供的标识符解析chat_stream。 - 确保实例已准备好处理触发器。 + 懒初始化,解析chat_stream, 获取聊天类型和目标信息。 """ if self._initialized: return True - self.chat_stream = chat_manager.get_stream(self.stream_id) - if not self.chat_stream: - logger.error(f"{self.log_prefix} 获取ChatStream失败。") + # --- Use utility function to determine chat type and fetch info --- + # Note: get_chat_type_and_target_info handles getting the chat_stream internally + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) + + # Update log prefix based on potential stream name (if needed, or get it from chat_stream if util doesn't return it) + # Assuming get_chat_type_and_target_info focuses only on type/target + # We still need the chat_stream object itself for other operations + try: + self.chat_stream = await asyncio.to_thread(chat_manager.get_stream, self.stream_id) + if not self.chat_stream: + logger.error( + f"[HFC:{self.stream_id}] 获取ChatStream失败 during _initialize, though util func might have succeeded earlier." + ) + return False # Cannot proceed without chat_stream object + # Update log prefix using the fetched stream object + self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + except Exception as e: + logger.error(f"[HFC:{self.stream_id}] 获取ChatStream时出错 in _initialize: {e}") return False - # 更新日志前缀(以防流名称发生变化) - self.log_prefix = f"[{chat_manager.get_stream_name(self.stream_id) or self.stream_id}]" + # --- End using utility function --- self._initialized = True - logger.debug(f"{self.log_prefix}麦麦感觉到了,可以开始认真水群 ") + logger.debug(f"{self.log_prefix} 麦麦感觉到了,可以开始认真水群 ") return True async def start(self): @@ -844,18 +862,15 @@ class HeartFChatting: f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}" ) - # --- 构建提示词 (调用修改后的 _build_planner_prompt) --- - # replan_prompt_str = "" # 暂时简化 - # if is_re_planned: - # replan_prompt_str = await self._build_replan_prompt( - # self._current_cycle.action_type, self._current_cycle.reasoning - # ) - prompt = await self._build_planner_prompt( - observed_messages_str, - current_mind, - self.sub_mind.structured_info, - "", # replan_prompt_str, - current_available_actions, # <--- 传入当前可用动作 + # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- + prompt = await prompt_builder.build_planner_prompt( + is_group_chat=self.is_group_chat, # <-- Pass HFC state + chat_target_info=self.chat_target_info, # <-- Pass HFC state + cycle_history=self._cycle_history, # <-- Pass HFC state + observed_messages_str=observed_messages_str, # <-- Pass local variable + current_mind=current_mind, # <-- Pass argument + structured_info=self.sub_mind.structured_info_str, # <-- Pass SubMind info + current_available_actions=current_available_actions, # <-- Pass determined actions ) # --- 调用 LLM (普通文本生成) --- @@ -1119,217 +1134,6 @@ class HeartFChatting: return prompt - async def _build_planner_prompt( - self, - observed_messages_str: str, - current_mind: Optional[str], - structured_info: Dict[str, Any], - replan_prompt: str, - current_available_actions: Dict[str, str], - ) -> str: - """构建 Planner LLM 的提示词 (获取模板并填充数据)""" - try: - # 准备结构化信息块 - structured_info_block = "" - if structured_info: - structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" - - # 准备聊天内容块 - chat_content_block = "" - if observed_messages_str: - chat_content_block = "观察到的最新聊天内容如下:\n---\n" - chat_content_block += observed_messages_str - chat_content_block += "\n---" - else: - chat_content_block = "当前没有观察到新的聊天内容。\n" - - # 准备当前思维块 (修改以匹配模板) - current_mind_block = "" - if current_mind: - # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀 - current_mind_block = f"你的内心想法:\n{current_mind}" - else: - current_mind_block = "你的内心想法:\n[没有特别的想法]" - - # 准备循环信息块 (分析最近的活动循环) - recent_active_cycles = [] - for cycle in reversed(self._cycle_history): - # 只关心实际执行了动作的循环 - if cycle.action_taken: - recent_active_cycles.append(cycle) - # 最多找最近的3个活动循环 - if len(recent_active_cycles) == 3: - break - - cycle_info_block = "" - consecutive_text_replies = 0 - responses_for_prompt = [] - - # 检查这最近的活动循环中有多少是连续的文本回复 (从最近的开始看) - for cycle in recent_active_cycles: - if cycle.action_type == "text_reply": - consecutive_text_replies += 1 - # 获取回复内容,如果不存在则返回'[空回复]' - response_text = cycle.response_info.get("response_text", []) - # 使用简单的 join 来格式化回复内容列表 - formatted_response = "[空回复]" if not response_text else " ".join(response_text) - responses_for_prompt.append(formatted_response) - else: - # 一旦遇到非文本回复,连续性中断 - break - - # 根据连续文本回复的数量构建提示信息 - # 注意: responses_for_prompt 列表是从最近到最远排序的 - if consecutive_text_replies >= 3: # 如果最近的三个活动都是文本回复 - cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意' - elif consecutive_text_replies == 2: # 如果最近的两个活动是文本回复 - cycle_info_block = f'你已经连续回复了两条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}"),请注意' - elif consecutive_text_replies == 1: # 如果最近的一个活动是文本回复 - cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")' - - # 包装提示块,增加可读性,即使没有连续回复也给个标记 - if cycle_info_block: - # 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀 - cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" - else: - # 如果最近的活动循环不是文本回复,或者没有活动循环 - cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" - - individuality = Individuality.get_instance() - # 模板中占位符是 {prompt_personality} - prompt_personality = individuality.get_prompt(x_person=2, level=2) - - # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) --- - action_options_text = "当前你可以选择的行动有:\n" - action_keys = list(current_available_actions.keys()) - for name in action_keys: - desc = current_available_actions[name] - action_options_text += f"- '{name}': {desc}\n" - - # --- 选择一个示例动作键 (用于填充模板中的 {example_action}) --- - example_action_key = action_keys[0] if action_keys else "no_reply" - - # --- 获取提示词模板 --- - planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") - - # --- 填充模板 --- - prompt = planner_prompt_template.format( - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - structured_info_block=structured_info_block, - chat_content_block=chat_content_block, - current_mind_block=current_mind_block, - replan="", # 暂时留空 replan 信息 - cycle_info_block=cycle_info_block, - action_options_text=action_options_text, # 传入可用动作描述 - example_action=example_action_key, # 传入示例动作键 - ) - - return prompt - - except Exception as e: - logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") - logger.error(traceback.format_exc()) - return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串 - - # --- 回复器 (Replier) 的定义 --- # - async def _replier_work( - self, - reason: str, - anchor_message: MessageRecv, - thinking_id: str, - ) -> Optional[List[str]]: - """ - 回复器 (Replier): 核心逻辑,负责生成回复文本。 - (已整合原 HeartFCGenerator 的功能) - """ - try: - # 1. 获取情绪影响因子并调整模型温度 - arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() - current_temp = global_config.llm_normal["temp"] * arousal_multiplier - self.model_normal.temperature = current_temp # 动态调整温度 - - # 2. 获取信息捕捉器 - info_catcher = info_catcher_manager.get_info_catcher(thinking_id) - - # 3. 构建 Prompt - with Timer("构建Prompt", {}): # 内部计时器,可选保留 - prompt = await prompt_builder.build_prompt( - build_mode="focus", - reason=reason, - current_mind_info=self.sub_mind.current_mind, - structured_info=self.sub_mind.structured_info, - message_txt="", # 似乎是固定的空字符串 - sender_name="", # 似乎是固定的空字符串 - chat_stream=anchor_message.chat_stream, - ) - - # 4. 调用 LLM 生成回复 - content = None - reasoning_content = None - model_name = "unknown_model" - try: - with Timer("LLM生成", {}): # 内部计时器,可选保留 - content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) - # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") - # 捕捉 LLM 输出信息 - info_catcher.catch_after_llm_generated( - prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name - ) - - except Exception as llm_e: - # 精简报错信息 - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}") - return None # LLM 调用失败则无法生成回复 - - # 5. 处理 LLM 响应 - if not content: - logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。") - return None - - with Timer("处理响应", {}): # 内部计时器,可选保留 - processed_response = process_llm_response(content) - - if not processed_response: - logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。") - return None - - return processed_response - - except Exception as e: - # 更通用的错误处理,精简信息 - logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}") - # logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈 - return None - - # --- Methods moved from HeartFCController start --- - 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.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") - return None - - chat = anchor_message.chat_stream - messageinfo = anchor_message.message_info - bot_user_info = UserInfo( - user_id=global_config.BOT_QQ, - user_nickname=global_config.BOT_NICKNAME, - platform=messageinfo.platform, - ) - - thinking_time_point = round(time.time(), 2) - thinking_id = "mt" + str(thinking_time_point) - thinking_message = MessageThinking( - message_id=thinking_id, - chat_stream=chat, - bot_user_info=bot_user_info, - reply=anchor_message, # 回复的是锚点消息 - thinking_start_time=thinking_time_point, - ) - # Access MessageManager directly - 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]: @@ -1465,3 +1269,118 @@ class HeartFChatting: if self._cycle_history: return self._cycle_history[-1].to_dict() return None + + # --- 回复器 (Replier) 的定义 --- # + async def _replier_work( + self, + reason: str, + anchor_message: MessageRecv, + thinking_id: str, + ) -> Optional[List[str]]: + """ + 回复器 (Replier): 核心逻辑,负责生成回复文本。 + (已整合原 HeartFCGenerator 的功能) + """ + try: + # 1. 获取情绪影响因子并调整模型温度 + arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier() + current_temp = global_config.llm_normal["temp"] * arousal_multiplier + self.model_normal.temperature = current_temp # 动态调整温度 + + # 2. 获取信息捕捉器 + info_catcher = info_catcher_manager.get_info_catcher(thinking_id) + + # --- Determine sender_name for private chat --- + sender_name_for_prompt = "某人" # Default for group or if info unavailable + if not self.is_group_chat and self.chat_target_info: + # Prioritize person_name, then nickname + sender_name_for_prompt = ( + self.chat_target_info.get("person_name") + or self.chat_target_info.get("user_nickname") + or sender_name_for_prompt + ) + # --- End determining sender_name --- + + # 3. 构建 Prompt + with Timer("构建Prompt", {}): # 内部计时器,可选保留 + prompt = await prompt_builder.build_prompt( + build_mode="focus", + chat_stream=self.chat_stream, # Pass the stream object + # Focus specific args: + reason=reason, + current_mind_info=self.sub_mind.current_mind, + structured_info=self.sub_mind.structured_info_str, + sender_name=sender_name_for_prompt, # Pass determined name + # Normal specific args (not used in focus mode): + # message_txt="", + ) + + # 4. 调用 LLM 生成回复 + content = None + reasoning_content = None + model_name = "unknown_model" + if not prompt: + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Prompt 构建失败,无法生成回复。") + return None + + try: + with Timer("LLM生成", {}): # 内部计时器,可选保留 + content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) + # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\nPrompt:\n{prompt}\n生成回复: {content}\n") + # 捕捉 LLM 输出信息 + info_catcher.catch_after_llm_generated( + prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name + ) + + except Exception as llm_e: + # 精简报错信息 + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}") + return None # LLM 调用失败则无法生成回复 + + # 5. 处理 LLM 响应 + if not content: + logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。") + return None + + with Timer("处理响应", {}): # 内部计时器,可选保留 + processed_response = process_llm_response(content) + + if not processed_response: + logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。") + return None + + return processed_response + + except Exception as e: + # 更通用的错误处理,精简信息 + logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}") + # logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈 + return None + + # --- Methods moved from HeartFCController start --- + 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.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。") + return None + + chat = anchor_message.chat_stream + messageinfo = anchor_message.message_info + bot_user_info = UserInfo( + user_id=global_config.BOT_QQ, + user_nickname=global_config.BOT_NICKNAME, + platform=messageinfo.platform, + ) + + thinking_time_point = round(time.time(), 2) + thinking_id = "mt" + str(thinking_time_point) + thinking_message = MessageThinking( + message_id=thinking_id, + chat_stream=chat, + bot_user_info=bot_user_info, + reply=anchor_message, # 回复的是锚点消息 + thinking_start_time=thinking_time_point, + ) + # Access MessageManager directly (using heart_fc_sender) + await self.heart_fc_sender.register_thinking(thinking_message) + return thinking_id diff --git a/src/plugins/heartFC_chat/heartflow_prompt_builder.py b/src/plugins/heartFC_chat/heartflow_prompt_builder.py index 8edace17..08c88d44 100644 --- a/src/plugins/heartFC_chat/heartflow_prompt_builder.py +++ b/src/plugins/heartFC_chat/heartflow_prompt_builder.py @@ -1,6 +1,6 @@ import random import time -from typing import Union, Optional +from typing import Union, Optional, Deque, Dict, Any from ...config.config import global_config from src.common.logger_manager import get_logger from ...individuality.individuality import Individuality @@ -15,6 +15,8 @@ from ..memory_system.Hippocampus import HippocampusManager from ..schedule.schedule_generator import bot_schedule from ..knowledge.knowledge_lib import qa_manager from src.plugins.group_nickname.nickname_utils import get_nickname_injection_for_prompt +import traceback +from .heartFC_Cycleinfo import CycleInfo logger = get_logger("prompt") @@ -51,7 +53,7 @@ def init_prompt(): # Planner提示词 - 修改为要求 JSON 输出 Prompt( - """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: + """你的名字是{bot_name},{prompt_personality},{chat_context_description}。需要基于以下信息决定如何参与对话: {structured_info_block} {chat_content_block} {current_mind_block} @@ -61,27 +63,27 @@ def init_prompt(): 【回复原则】 1. 不回复(no_reply)适用: -- 话题无关/无聊/不感兴趣 -- 最后一条消息是你自己发的且无人回应你 -- 讨论你不懂的专业话题 -- 你发送了太多消息,且无人回复 + - 话题无关/无聊/不感兴趣 + - 最后一条消息是你自己发的且无人回应你 + - 讨论你不懂的专业话题 + - 你发送了太多消息,且无人回复 2. 文字回复(text_reply)适用: -- 有实质性内容需要表达 -- 有人提到你,但你还没有回应他 -- 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合) -- 不要追加太多表情 + - 有实质性内容需要表达 + - 有人提到你,但你还没有回应他 + - 可以追加emoji_query表达情绪(emoji_query填写表情包的适用场合,也就是当前场合) + - 不要追加太多表情 3. 纯表情回复(emoji_reply)适用: -- 适合用表情回应的场景 -- 需提供明确的emoji_query + - 适合用表情回应的场景 + - 需提供明确的emoji_query 4. 自我对话处理: -- 如果是自己发的消息想继续,需自然衔接 -- 避免重复或评价自己的发言 -- 不要和自己聊天 + - 如果是自己发的消息想继续,需自然衔接 + - 避免重复或评价自己的发言 + - 不要和自己聊天 -【决策任务】 +决策任务 {action_options_text} 你必须从上面列出的可用行动中选择一个,并说明原因。 @@ -92,23 +94,9 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": "reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则 "emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题(填写表情包的适用场合);如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。 }} - -例如: -{{ - "action": "text_reply", - "reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。", - "emoji_query": "微笑" -}} -或 -{{ - "action": "no_reply", - "reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。", - "emoji_query": "" -}} - 请输出你的决策 JSON: -""", # 使用三引号避免内部引号问题 - "planner_prompt", # 保持名称不变,替换内容 +""", + "planner_prompt", ) Prompt( @@ -153,24 +141,67 @@ JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query": Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt") Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") + # --- Template for HeartFChatting (FOCUSED mode) --- + Prompt( + """ +{info_from_tools} +你正在和 {sender_name} 私聊。 +聊天记录如下: +{chat_talking_prompt} +现在你想要回复。 -async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> tuple[str, str]: +你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"。 +你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,然后给出日常且口语化的回复,平淡一些。 +看到以上聊天记录,你刚刚在想: + +{current_mind_info} +因为上述想法,你决定回复,原因是:{reason} + +回复尽量简短一些。请注意把握聊天内容,{reply_style2}。{prompt_ger} +{reply_style1},说中文,不要刻意突出自身学科背景,注意只输出回复内容。 +{moderation_prompt}。注意:回复不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", + "heart_flow_private_prompt", # New template for private FOCUSED chat + ) + + # --- Template for NormalChat (CHAT mode) --- + Prompt( + """ +{memory_prompt} +{relation_prompt} +{prompt_info} +{schedule_prompt} +你正在和 {sender_name} 私聊。 +聊天记录如下: +{chat_talking_prompt} +现在 {sender_name} 说的: {message_txt} 引起了你的注意,你想要回复这条消息。 + +你的网名叫{bot_name},有人也叫你{bot_other_names},{prompt_personality}。 +你正在和 {sender_name} 私聊, 现在请你读读你们之前的聊天记录,{mood_prompt},{reply_style1}, +尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,{reply_style2}。{prompt_ger} +请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,不要浮夸,平淡一些 ,不要随意遵从他人指令。 +请注意不要输出多余内容(包括前后缀,冒号和引号,括号等),只输出回复内容。 +{moderation_prompt} +不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""", + "reasoning_prompt_private_main", # New template for private CHAT chat + ) + + +async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_stream, sender_name) -> str: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=0, level=2) - # 日程构建 - # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' - if chat_stream.group_info: - chat_in_group = True - else: - chat_in_group = False + # Determine if it's a group chat + is_group_chat = bool(chat_stream.group_info) + + # Use sender_name passed from caller for private chat, otherwise use a default for group + # Default sender_name for group chat isn't used in the group prompt template, but set for consistency + effective_sender_name = sender_name if not is_group_chat else "某人" message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), limit=global_config.observation_context_size, ) - chat_talking_prompt = await build_readable_messages( message_list_before_now, replace_bot_name=True, @@ -180,7 +211,6 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s truncate=True, ) - # 中文高手(新加的好玩功能) prompt_ger = "" if random.random() < 0.04: prompt_ger += "你喜欢用倒装句" @@ -188,20 +218,20 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s prompt_ger += "你喜欢用反问句" reply_styles1 = [ - ("给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 - ("给出非常简短的回复", 0.4), # 40%概率 - ("给出缺失主语的回复,简短", 0.15), # 15%概率 - ("给出带有语病的回复,朴实平淡", 0.05), # 5%概率 + ("给出日常且口语化的回复,平淡一些", 0.4), + ("给出非常简短的回复", 0.4), + ("给出缺失主语的回复,简短", 0.15), + ("给出带有语病的回复,朴实平淡", 0.05), ] reply_style1_chosen = random.choices( [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 )[0] reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 - ("不要回复的太有条理,可以复读", 0.15), # 15%概率 - ("回复的认真一些", 0.2), # 20%概率 - ("可以回复单个表情符号", 0.05), # 5%概率 + ("不要回复的太有条理,可以有个性", 0.6), + ("不要回复的太有条理,可以复读", 0.15), + ("回复的认真一些", 0.2), + ("可以回复单个表情符号", 0.05), ] reply_style2_chosen = random.choices( [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 @@ -214,35 +244,55 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s else: structured_info_prompt = "" - logger.debug("开始构建prompt") + logger.debug("开始构建 focus prompt") - # 调用新的工具函数获取绰号信息 - nickname_injection_str = await get_nickname_injection_for_prompt(chat_stream, message_list_before_now) + # --- Choose template based on chat type --- + if is_group_chat: + template_name = "heart_flow_prompt" + # Group specific formatting variables (already fetched or default) + chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") + chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - prompt = await global_prompt_manager.format_prompt( - "heart_flow_prompt", - info_from_tools=structured_info_prompt, + # 调用新的工具函数获取绰号信息 + nickname_injection_str = await get_nickname_injection_for_prompt(chat_stream, message_list_before_now) + + prompt = await global_prompt_manager.format_prompt( + template_name, + info_from_tools=structured_info_prompt, + chat_target=chat_target_1, # Used in group template + chat_talking_prompt=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + chat_target_2=chat_target_2, # Used in group template + current_mind_info=current_mind_info, + reply_style2=reply_style2_chosen, + reply_style1=reply_style1_chosen, + reason=reason, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + # sender_name is not used in the group template + ) + else: # Private chat + template_name = "heart_flow_private_prompt" + prompt = await global_prompt_manager.format_prompt( + template_name, + info_from_tools=structured_info_prompt, nickname_info=nickname_injection_str, - 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"), - chat_talking_prompt=chat_talking_prompt, - bot_name=global_config.BOT_NICKNAME, - prompt_personality=prompt_personality, - chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private2"), - current_mind_info=current_mind_info, - reply_style2=reply_style2_chosen, - reply_style1=reply_style1_chosen, - reason=reason, - prompt_ger=prompt_ger, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - sender_name=sender_name, - ) - - logger.debug(f"focus_chat_prompt: \n{prompt}") + sender_name=effective_sender_name, # Used in private template + chat_talking_prompt=chat_talking_prompt, + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + # chat_target and chat_target_2 are not used in private template + current_mind_info=current_mind_info, + reply_style2=reply_style2_chosen, + reply_style1=reply_style1_chosen, + reason=reason, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + # --- End choosing template --- + logger.debug(f"focus_chat_prompt (is_group={is_group_chat}): \n{prompt}") return prompt @@ -254,13 +304,13 @@ class PromptBuilder: async def build_prompt( self, build_mode, - reason, - current_mind_info, - structured_info, - message_txt: str, - sender_name: str = "某人", - chat_stream=None, - ) -> Optional[tuple[str, str]]: + chat_stream, + reason=None, + current_mind_info=None, + structured_info=None, + message_txt=None, + sender_name="某人", + ) -> Optional[str]: if build_mode == "normal": return await self._build_prompt_normal(chat_stream, message_txt, sender_name) @@ -274,54 +324,50 @@ class PromptBuilder: ) return None - async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: + async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> str: individuality = Individuality.get_instance() prompt_personality = individuality.get_prompt(x_person=2, level=2) + is_group_chat = bool(chat_stream.group_info) - # 关系 - who_chat_in_group = [ - (chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname) - ] - who_chat_in_group += get_recent_group_speaker( - chat_stream.stream_id, - (chat_stream.user_info.platform, chat_stream.user_info.user_id), - limit=global_config.observation_context_size, - ) + who_chat_in_group = [] + if is_group_chat: + who_chat_in_group = get_recent_group_speaker( + chat_stream.stream_id, + (chat_stream.user_info.platform, chat_stream.user_info.user_id) if chat_stream.user_info else None, + limit=global_config.observation_context_size, + ) + elif chat_stream.user_info: + who_chat_in_group.append( + (chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname) + ) relation_prompt = "" for person in who_chat_in_group: - relation_prompt += await relationship_manager.build_relationship_info(person) - # print(f"relation_prompt: {relation_prompt}") + if len(person) >= 3 and person[0] and person[1]: + relation_prompt += await relationship_manager.build_relationship_info(person) + else: + logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}") - # print(f"relat11111111ion_prompt: {relation_prompt}") - - # 心情 mood_manager = MoodManager.get_instance() mood_prompt = mood_manager.get_prompt() - - # logger.info(f"心情prompt: {mood_prompt}") - reply_styles1 = [ - ("然后给出日常且口语化的回复,平淡一些", 0.4), # 40%概率 - ("给出非常简短的回复", 0.4), # 40%概率 - ("给出缺失主语的回复", 0.15), # 15%概率 - ("给出带有语病的回复", 0.05), # 5%概率 + ("然后给出日常且口语化的回复,平淡一些", 0.4), + ("给出非常简短的回复", 0.4), + ("给出缺失主语的回复", 0.15), + ("给出带有语病的回复", 0.05), ] reply_style1_chosen = random.choices( [style[0] for style in reply_styles1], weights=[style[1] for style in reply_styles1], k=1 )[0] - reply_styles2 = [ - ("不要回复的太有条理,可以有个性", 0.6), # 60%概率 - ("不要回复的太有条理,可以复读", 0.15), # 15%概率 - ("回复的认真一些", 0.2), # 20%概率 - ("可以回复单个表情符号", 0.05), # 5%概率 + ("不要回复的太有条理,可以有个性", 0.6), + ("不要回复的太有条理,可以复读", 0.15), + ("回复的认真一些", 0.2), + ("可以回复单个表情符号", 0.05), ] reply_style2_chosen = random.choices( [style[0] for style in reply_styles2], weights=[style[1] for style in reply_styles2], k=1 )[0] - - # 调取记忆 memory_prompt = "" related_memory = await HippocampusManager.get_instance().get_memory_from_text( text=message_txt, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False @@ -330,23 +376,15 @@ class PromptBuilder: if related_memory: for memory in related_memory: related_memory_info += memory[1] - # memory_prompt = f"你想起你之前见过的事情:{related_memory_info}。\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n" memory_prompt = await global_prompt_manager.format_prompt( "memory_prompt", related_memory_info=related_memory_info ) - # 获取聊天上下文 - if chat_stream.group_info: - chat_in_group = True - else: - chat_in_group = False - message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=chat_stream.stream_id, timestamp=time.time(), limit=global_config.observation_context_size, ) - chat_talking_prompt = await build_readable_messages( message_list_before_now, replace_bot_name=True, @@ -390,14 +428,11 @@ class PromptBuilder: start_time = time.time() prompt_info = await self.get_prompt_info(message_txt, threshold=0.38) if prompt_info: - # prompt_info = f"""\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n""" prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info) end_time = time.time() logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}秒") - logger.debug("开始构建prompt") - if global_config.ENABLE_SCHEDULE_GEN: schedule_prompt = await global_prompt_manager.format_prompt( "schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False) @@ -405,37 +440,64 @@ class PromptBuilder: else: schedule_prompt = "" - # 调用新的工具函数获取绰号信息 + logger.debug("开始构建 normal prompt") + + # --- Choose template and format based on chat type --- + if is_group_chat: + template_name = "reasoning_prompt_main" + effective_sender_name = sender_name + chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") + chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") + + prompt = await global_prompt_manager.format_prompt( + template_name, + relation_prompt=relation_prompt, + sender_name=effective_sender_name, + memory_prompt=memory_prompt, + prompt_info=prompt_info, + schedule_prompt=schedule_prompt, + chat_target=chat_target_1, + chat_target_2=chat_target_2, + chat_talking_prompt=chat_talking_prompt, + message_txt=message_txt, + bot_name=global_config.BOT_NICKNAME, + bot_other_names="/".join(global_config.BOT_ALIAS_NAMES), + prompt_personality=prompt_personality, + mood_prompt=mood_prompt, + reply_style1=reply_style1_chosen, + reply_style2=reply_style2_chosen, + keywords_reaction_prompt=keywords_reaction_prompt, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + else: + template_name = "reasoning_prompt_private_main" + effective_sender_name = sender_name + + # 调用新的工具函数获取绰号信息 nickname_injection_str = await get_nickname_injection_for_prompt(chat_stream, message_list_before_now) prompt = await global_prompt_manager.format_prompt( - "reasoning_prompt_main", - relation_prompt=relation_prompt, - sender_name=sender_name, - memory_prompt=memory_prompt, - prompt_info=prompt_info, - schedule_prompt=schedule_prompt, + template_name, + relation_prompt=relation_prompt, + sender_name=effective_sender_name, + memory_prompt=memory_prompt, + prompt_info=prompt_info, + schedule_prompt=schedule_prompt, nickname_info=nickname_injection_str, # <--- 注入绰号信息 - 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"), - chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") - if chat_in_group - else await global_prompt_manager.get_prompt_async("chat_target_private2"), - chat_talking_prompt=chat_talking_prompt, - message_txt=message_txt, - bot_name=global_config.BOT_NICKNAME, - bot_other_names="/".join( - global_config.BOT_ALIAS_NAMES, - ), - prompt_personality=prompt_personality, - mood_prompt=mood_prompt, - reply_style1=reply_style1_chosen, - reply_style2=reply_style2_chosen, - keywords_reaction_prompt=keywords_reaction_prompt, - prompt_ger=prompt_ger, - moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), - ) + chat_talking_prompt=chat_talking_prompt, + message_txt=message_txt, + bot_name=global_config.BOT_NICKNAME, + bot_other_names="/".join(global_config.BOT_ALIAS_NAMES), + prompt_personality=prompt_personality, + mood_prompt=mood_prompt, + reply_style1=reply_style1_chosen, + reply_style2=reply_style2_chosen, + keywords_reaction_prompt=keywords_reaction_prompt, + prompt_ger=prompt_ger, + moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), + ) + # --- End choosing template --- return prompt @@ -695,6 +757,112 @@ class PromptBuilder: # 返回所有找到的内容,用换行分隔 return "\n".join(str(result["content"]) for result in results) + async def build_planner_prompt( + self, + is_group_chat: bool, # Now passed as argument + chat_target_info: Optional[dict], # Now passed as argument + cycle_history: Deque["CycleInfo"], # Now passed as argument (Type hint needs import or string) + observed_messages_str: str, + current_mind: Optional[str], + structured_info: Dict[str, Any], + current_available_actions: Dict[str, str], + # replan_prompt: str, # Replan logic still simplified + ) -> str: + """构建 Planner LLM 的提示词 (获取模板并填充数据)""" + try: + # --- Determine chat context --- + chat_context_description = "你现在正在一个群聊中" + chat_target_name = None # Only relevant for private + if not is_group_chat and chat_target_info: + chat_target_name = ( + chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方" + ) + chat_context_description = f"你正在和 {chat_target_name} 私聊" + # --- End determining chat context --- + + # ... (Copy logic from HeartFChatting._build_planner_prompt here) ... + # Structured info block + structured_info_block = "" + if structured_info: + structured_info_block = f"以下是一些额外的信息:\n{structured_info}\n" + + # Chat content block + chat_content_block = "" + if observed_messages_str: + # Use triple quotes for multi-line string literal + chat_content_block = f"""观察到的最新聊天内容如下: +--- +{observed_messages_str} +---""" + else: + chat_content_block = "当前没有观察到新的聊天内容。\\n" + + # Current mind block + current_mind_block = "" + if current_mind: + current_mind_block = f"你的内心想法:\n{current_mind}" + else: + current_mind_block = "你的内心想法:\n[没有特别的想法]" + + # Cycle info block (using passed cycle_history) + cycle_info_block = "" + recent_active_cycles = [] + for cycle in reversed(cycle_history): + if cycle.action_taken: + recent_active_cycles.append(cycle) + if len(recent_active_cycles) == 3: + break + consecutive_text_replies = 0 + responses_for_prompt = [] + for cycle in recent_active_cycles: + if cycle.action_type == "text_reply": + consecutive_text_replies += 1 + response_text = cycle.response_info.get("response_text", []) + formatted_response = "[空回复]" if not response_text else " ".join(response_text) + responses_for_prompt.append(formatted_response) + else: + break + if consecutive_text_replies >= 3: + cycle_info_block = f'你已经连续回复了三条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}",第三近: "{responses_for_prompt[2]}")。你回复的有点多了,请注意' + elif consecutive_text_replies == 2: + cycle_info_block = f'你已经连续回复了两条消息(最近: "{responses_for_prompt[0]}",第二近: "{responses_for_prompt[1]}"),请注意' + elif consecutive_text_replies == 1: + cycle_info_block = f'你刚刚已经回复一条消息(内容: "{responses_for_prompt[0]}")' + if cycle_info_block: + cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" + else: + cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" + + individuality = Individuality.get_instance() + prompt_personality = individuality.get_prompt(x_person=2, level=2) + + action_options_text = "当前你可以选择的行动有:\n" + action_keys = list(current_available_actions.keys()) + for name in action_keys: + desc = current_available_actions[name] + action_options_text += f"- '{name}': {desc}\n" + example_action_key = action_keys[0] if action_keys else "no_reply" + + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") + + prompt = planner_prompt_template.format( + bot_name=global_config.BOT_NICKNAME, + prompt_personality=prompt_personality, + chat_context_description=chat_context_description, + structured_info_block=structured_info_block, + chat_content_block=chat_content_block, + current_mind_block=current_mind_block, + cycle_info_block=cycle_info_block, + action_options_text=action_options_text, + example_action=example_action_key, + ) + return prompt + + except Exception as e: + logger.error(f"[PromptBuilder] 构建 Planner 提示词时出错: {e}") + logger.error(traceback.format_exc()) + return "[构建 Planner Prompt 时出错]" + init_prompt() prompt_builder = PromptBuilder() diff --git a/src/plugins/heartFC_chat/normal_chat.py b/src/plugins/heartFC_chat/normal_chat.py index 76fb1746..a87cb609 100644 --- a/src/plugins/heartFC_chat/normal_chat.py +++ b/src/plugins/heartFC_chat/normal_chat.py @@ -19,6 +19,7 @@ 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_calculator import Timer +from src.heart_flow.utils_chat import get_chat_type_and_target_info from src.plugins.group_nickname.nickname_utils import trigger_nickname_analysis_if_needed @@ -27,31 +28,48 @@ logger = get_logger("chat") class NormalChat: def __init__(self, chat_stream: ChatStream, interest_dict: dict): - """ - 初始化 NormalChat 实例,针对特定的 ChatStream。 - - Args: - chat_stream (ChatStream): 此 NormalChat 实例关联的聊天流对象。 - """ + """初始化 NormalChat 实例。只进行同步操作。""" + # Basic info from chat_stream (sync) self.chat_stream = chat_stream self.stream_id = chat_stream.stream_id + # Get initial stream name, might be updated in initialize self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + # Interest dict self.interest_dict = interest_dict + # --- Initialize attributes (defaults) --- + self.is_group_chat: bool = False + self.chat_target_info: Optional[dict] = None + # --- End Initialization --- + + # Other sync initializations self.gpt = NormalChatGenerator() - self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例 - # 存储此实例的兴趣监控任务 + self.mood_manager = MoodManager.get_instance() self.start_time = time.time() - self.last_speak_time = 0 - self._chat_task: Optional[asyncio.Task] = None - logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。") + self._initialized = False # Track initialization status + + # logger.info(f"[{self.stream_name}] NormalChat 实例 __init__ 完成 (同步部分)。") + # Avoid logging here as stream_name might not be final + + async def initialize(self): + """异步初始化,获取聊天类型和目标信息。""" + if self._initialized: + return + + # --- Use utility function to determine chat type and fetch info --- + self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.stream_id) + # Update stream_name again after potential async call in util func + self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id + # --- End using utility function --- + self._initialized = True + logger.info(f"[{self.stream_name}] NormalChat 实例 initialize 完成 (异步部分)。") # 改为实例方法 - async def _create_thinking_message(self, message: MessageRecv) -> str: + async def _create_thinking_message(self, message: MessageRecv, timestamp: Optional[float] = None) -> str: """创建思考消息""" messageinfo = message.message_info @@ -65,10 +83,11 @@ class NormalChat: thinking_id = "mt" + str(thinking_time_point) thinking_message = MessageThinking( message_id=thinking_id, - chat_stream=self.chat_stream, # 使用 self.chat_stream + chat_stream=self.chat_stream, bot_user_info=bot_user_info, reply=message, thinking_start_time=thinking_time_point, + timestamp=timestamp if timestamp is not None else None, ) await message_manager.add_message(thinking_message) @@ -189,7 +208,10 @@ class NormalChat: try: # 处理消息 await self.normal_response( - message=message, is_mentioned=is_mentioned, interested_rate=interest_value + message=message, + is_mentioned=is_mentioned, + interested_rate=interest_value, + rewind_response=False, ) except Exception as e: logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}\n{traceback.format_exc()}") @@ -197,7 +219,9 @@ class NormalChat: self.interest_dict.pop(msg_id, None) # 改为实例方法, 移除 chat 参数 - async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float) -> None: + async def normal_response( + self, message: MessageRecv, is_mentioned: bool, interested_rate: float, rewind_response: bool = False + ) -> None: # 检查收到的消息是否属于当前实例处理的 chat stream if message.chat_stream.stream_id != self.stream_id: logger.error( @@ -244,7 +268,10 @@ class NormalChat: await willing_manager.before_generate_reply_handle(message.message_info.message_id) with Timer("创建思考消息", timing_results): - thinking_id = await self._create_thinking_message(message) + if rewind_response: + thinking_id = await self._create_thinking_message(message, message.message_info.time) + else: + thinking_id = await self._create_thinking_message(message) logger.debug(f"[{self.stream_name}] 创建捕捉器,thinking_id:{thinking_id}") @@ -374,11 +401,20 @@ class NormalChat: try: logger.info(f"[{self.stream_name}] 处理初始高兴趣消息 {msg_id} (兴趣值: {interest_value:.2f})") - await self.normal_response(message=message, is_mentioned=is_mentioned, interested_rate=interest_value) + await self.normal_response( + message=message, is_mentioned=is_mentioned, interested_rate=interest_value, rewind_response=True + ) processed_count += 1 except Exception as e: logger.error(f"[{self.stream_name}] 处理初始兴趣消息 {msg_id} 时出错: {e}\\n{traceback.format_exc()}") + # --- 新增:处理完后清空整个字典 --- + logger.info( + f"[{self.stream_name}] 处理了 {processed_count} 条初始高兴趣消息。现在清空所有剩余的初始兴趣消息..." + ) + self.interest_dict.clear() + # --- 新增结束 --- + logger.info( f"[{self.stream_name}] 初始高兴趣消息处理完毕,共处理 {processed_count} 条。剩余 {len(self.interest_dict)} 条待轮询。" ) @@ -418,22 +454,18 @@ class NormalChat: # 改为实例方法, 移除 chat 参数 async def start_chat(self): - """为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行), - 并在后台处理一次初始的高兴趣消息。""" # 文言文注释示例:启聊之始,若有遗珠,当于暗处拂拭,勿碍正途。 - if self._chat_task is None or self._chat_task.done(): - # --- 修改:使用 create_task 启动初始消息处理 --- - logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息...") - # 创建一个任务来处理初始消息,不阻塞当前流程 - _initial_process_task = asyncio.create_task(self._process_initial_interest_messages()) - # 可以考虑给这个任务也添加完成回调来记录日志或处理错误 - # initial_process_task.add_done_callback(...) - # --- 修改结束 --- + """先进行异步初始化,然后启动聊天任务。""" + if not self._initialized: + await self.initialize() # Ensure initialized before starting tasks - # 启动后台轮询任务 (这部分不变) - logger.info(f"[{self.stream_name}] 启动后台兴趣消息轮询任务...") - polling_task = asyncio.create_task(self._reply_interested_message()) # 注意变量名区分 + if self._chat_task is None or self._chat_task.done(): + logger.info(f"[{self.stream_name}] 开始后台处理初始兴趣消息和轮询任务...") + # Process initial messages first + await self._process_initial_interest_messages() + # Then start polling task + polling_task = asyncio.create_task(self._reply_interested_message()) polling_task.add_done_callback(lambda t: self._handle_task_completion(t)) - self._chat_task = polling_task # self._chat_task 仍然指向主要的轮询任务 + self._chat_task = polling_task else: logger.info(f"[{self.stream_name}] 聊天轮询任务已在运行中。") diff --git a/src/plugins/person_info/person_info.py b/src/plugins/person_info/person_info.py index 2f8aa7db..1ae5e415 100644 --- a/src/plugins/person_info/person_info.py +++ b/src/plugins/person_info/person_info.py @@ -51,6 +51,8 @@ person_info_default = { "konw_time": 0, "msg_interval": 2000, "msg_interval_list": [], + "user_cardname": None, # 添加群名片 + "user_avatar": None, # 添加头像信息(例如URL或标识符) "group_nicknames": [], } # 个人信息的各项与默认值在此定义,以下处理会自动创建/补全每一项 @@ -187,7 +189,9 @@ class PersonInfoManager: logger.warning(f"无法从文本中提取有效的JSON字典: {text}") return {"nickname": "", "reason": ""} - async def qv_person_name(self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str): + async def qv_person_name( + self, person_id: str, user_nickname: str, user_cardname: str, user_avatar: str, request: str = "" + ): """给某个用户取名""" if not person_id: logger.debug("取名失败:person_id不能为空") @@ -212,6 +216,8 @@ class PersonInfoManager: if old_name: qv_name_prompt += f"你之前叫他{old_name},是因为{old_reason}," + qv_name_prompt += f"\n其他取名的要求是:{request}" + qv_name_prompt += "\n请根据以上用户信息,想想你叫他什么比较好,请最好使用用户的qq昵称,可以稍作修改" if existing_names: qv_name_prompt += f"\n请注意,以下名称已被使用,不要使用以下昵称:{existing_names}。\n" @@ -512,5 +518,41 @@ class PersonInfoManager: return person_id + async def get_person_info_by_name(self, person_name: str) -> dict | None: + """根据 person_name 查找用户并返回基本信息 (如果找到)""" + if not person_name: + logger.debug("get_person_info_by_name 获取失败:person_name 不能为空") + return None + + # 优先从内存缓存查找 person_id + found_person_id = None + for pid, name in self.person_name_list.items(): + if name == person_name: + found_person_id = pid + break # 找到第一个匹配就停止 + + if not found_person_id: + # 如果内存没有,尝试数据库查询(可能内存未及时更新或启动时未加载) + document = db.person_info.find_one({"person_name": person_name}) + if document: + found_person_id = document.get("person_id") + else: + logger.debug(f"数据库中也未找到名为 '{person_name}' 的用户") + return None # 数据库也找不到 + + # 根据找到的 person_id 获取所需信息 + if found_person_id: + required_fields = ["person_id", "platform", "user_id", "nickname", "user_cardname", "user_avatar"] + person_data = await self.get_values(found_person_id, required_fields) + if person_data: # 确保 get_values 成功返回 + return person_data + else: + logger.warning(f"找到了 person_id '{found_person_id}' 但获取详细信息失败") + return None + else: + # 这理论上不应该发生,因为上面已经处理了找不到的情况 + logger.error(f"逻辑错误:未能为 '{person_name}' 确定 person_id") + return None + person_info_manager = PersonInfoManager() diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 3168b833..a74e121c 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "1.6.0" +version = "1.6.1" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #如果你想要修改配置文件,请在修改后将version的值进行变更 @@ -193,6 +193,7 @@ enable = true [experimental] #实验性功能 enable_friend_chat = false # 是否启用好友聊天 +talk_allowed_private = [] # 可以回复消息的QQ号 pfc_chatting = false # 是否启用PFC聊天,该功能仅作用于私聊,与回复模式独立 #下面的模型若使用硅基流动则不需要更改,使用ds官方则改成.env自定义的宏,使用自定义模型则选择定位相似的模型自己填写