From 9d78cbb1f19a4d011d515d3bf873cda6cdac3559 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Sat, 13 Sep 2025 11:20:47 +0800 Subject: [PATCH] =?UTF-8?q?typing=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/heart_flow/heartFC_chat.py | 54 ++++---- .../heart_flow/heartflow_message_processor.py | 11 +- src/chat/planner_actions/planner.py | 121 ++++++++---------- src/common/data_models/info_data_model.py | 1 + src/config/official_configs.py | 3 - src/main.py | 14 +- src/plugins/built_in/relation/relation.py | 2 +- 7 files changed, 97 insertions(+), 109 deletions(-) diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 68939ca7..f7d730ef 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -108,9 +108,9 @@ class HeartFChatting: self._current_cycle_detail: CycleDetail = None # type: ignore self.last_read_time = time.time() - 10 - + self.talk_threshold = global_config.chat.talk_value - + self.no_reply_until_call = False async def start(self): @@ -172,7 +172,7 @@ class HeartFChatting: f"耗时: {self._current_cycle_detail.end_time - self._current_cycle_detail.start_time:.1f}秒" # type: ignore + (f"\n详情: {'; '.join(timer_strings)}" if timer_strings else "") ) - + def get_talk_threshold(self): talk_value = global_config.chat.talk_value # 处理talk_value:取整数部分和小数部分 @@ -183,7 +183,7 @@ class HeartFChatting: self.talk_threshold = think_len logger.info(f"{self.log_prefix} 思考频率阈值: {self.talk_threshold}") - async def _loopbody(self): + async def _loopbody(self): # sourcery skip: hoist-if-from-if recent_messages_list = message_api.get_messages_by_time_in_chat( chat_id=self.stream_id, start_time=self.last_read_time, @@ -195,11 +195,15 @@ class HeartFChatting: ) if len(recent_messages_list) >= self.talk_threshold: - # !处理no_reply_until_call逻辑 if self.no_reply_until_call: for message in recent_messages_list: - if message.is_mentioned or message.is_at or len(recent_messages_list) >= 8 or time.time() - self.last_read_time > 600: + if ( + message.is_mentioned + or message.is_at + or len(recent_messages_list) >= 8 + or time.time() - self.last_read_time > 600 + ): self.no_reply_until_call = False break # 没有提到,继续保持沉默 @@ -207,8 +211,7 @@ class HeartFChatting: # logger.info(f"{self.log_prefix} 没有提到,继续保持沉默") await asyncio.sleep(1) return True - - + self.last_read_time = time.time() await self._observe( recent_messages_list=recent_messages_list, @@ -271,9 +274,9 @@ class HeartFChatting: return loop_info, reply_text, cycle_timers async def _observe( - self, # interest_value: float = 0.0, - recent_messages_list: Optional[List["DatabaseMessages"]] = None - ) -> bool: + self, # interest_value: float = 0.0, + recent_messages_list: Optional[List["DatabaseMessages"]] = None, + ) -> bool: # sourcery skip: merge-else-if-into-elif, remove-redundant-if if recent_messages_list is None: recent_messages_list = [] reply_text = "" # 初始化reply_text变量,避免UnboundLocalError @@ -283,7 +286,7 @@ class HeartFChatting: async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): await self.expression_learner.trigger_learning_for_chat() - + cycle_timers, thinking_id = self.start_cycle() logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") @@ -326,27 +329,25 @@ class HeartFChatting: return False if modified_message and modified_message._modify_flags.modify_llm_prompt: prompt_info = (modified_message.llm_prompt, prompt_info[1]) - - + with Timer("规划器", cycle_timers): action_to_use_info, _ = await self.action_planner.plan( loop_start_time=self.last_read_time, available_actions=available_actions, ) - - + # !此处使at或者提及必定回复 metioned_message = None for message in recent_messages_list: if (message.is_mentioned or message.is_at) and global_config.chat.mentioned_bot_reply: metioned_message = message - + has_reply = False for action in action_to_use_info: if action.action_type == "reply": - has_reply =True + has_reply = True break - + if not has_reply and metioned_message: action_to_use_info.append( ActionPlannerInfo( @@ -357,7 +358,6 @@ class HeartFChatting: available_actions=available_actions, ) ) - # 3. 并行执行所有动作 action_tasks = [ @@ -521,10 +521,9 @@ class HeartFChatting: reply_text = "" first_replied = False for reply_content in reply_set.reply_data: - if reply_content.content_type != ReplyContentType.TEXT: continue - data: str = reply_content.content # type: ignore + data: str = reply_content.content # type: ignore if not first_replied: await send_api.text_to_stream( text=data, @@ -574,17 +573,18 @@ class HeartFChatting: action_name="no_action", ) return {"action_type": "no_action", "success": True, "reply_text": "", "command": ""} - + elif action_planner_info.action_type == "wait_time": + action_planner_info.action_data = action_planner_info.action_data or {} logger.info(f"{self.log_prefix} 等待{action_planner_info.action_data['time']}秒后回复") await asyncio.sleep(action_planner_info.action_data["time"]) return {"action_type": "wait_time", "success": True, "reply_text": "", "command": ""} - + elif action_planner_info.action_type == "no_reply_until_call": logger.info(f"{self.log_prefix} 保持沉默,直到有人直接叫的名字") self.no_reply_until_call = True return {"action_type": "no_reply_until_call", "success": True, "reply_text": "", "command": ""} - + elif action_planner_info.action_type == "reply": try: success, llm_response = await generator_api.generate_reply( @@ -624,7 +624,7 @@ class HeartFChatting: "reply_text": reply_text, "loop_info": loop_info, } - + # 其他动作 else: # 执行普通动作 @@ -643,7 +643,7 @@ class HeartFChatting: "reply_text": reply_text, "command": command, } - + except Exception as e: logger.error(f"{self.log_prefix} 执行动作时出错: {e}") logger.error(f"{self.log_prefix} 错误信息: {traceback.format_exc()}") diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 7e5527ac..dbc99fd9 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -10,7 +10,6 @@ from src.chat.message_receive.message import MessageRecv from src.chat.message_receive.storage import MessageStorage from src.chat.heart_flow.heartflow import heartflow from src.chat.utils.utils import is_mentioned_bot_in_message -from src.chat.utils.timer_calculator import Timer from src.chat.utils.chat_message_builder import replace_user_references from src.common.logger import get_logger from src.mood.mood_manager import mood_manager @@ -36,7 +35,7 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, list[str]]: return 0.0, [] is_mentioned, is_at, reply_probability_boost = is_mentioned_bot_in_message(message) - interested_rate = 0.0 + # interested_rate = 0.0 keywords = [] # with Timer("记忆激活"): # interested_rate, keywords, keywords_lite = await hippocampus_manager.get_activate_from_text( @@ -153,10 +152,10 @@ class HeartFCMessageReceiver: logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[{interested_rate:.2f}]") # type: ignore _ = Person.register_person( - platform=message.message_info.platform, - user_id=message.message_info.user_info.user_id, - nickname=userinfo.user_nickname, - ) # type: ignore + platform=message.message_info.platform, # type: ignore + user_id=message.message_info.user_info.user_id, # type: ignore + nickname=userinfo.user_nickname, # type: ignore + ) except Exception as e: logger.error(f"消息处理失败: {e}") diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 8cdae2b0..9239fe8e 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -22,12 +22,12 @@ from src.chat.utils.chat_message_builder import ( from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.planner_actions.action_manager import ActionManager from src.chat.message_receive.chat_stream import get_chat_manager -from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType, ActionActivationType +from src.plugin_system.base.component_types import ActionInfo, ComponentType, ActionActivationType from src.plugin_system.core.component_registry import component_registry if TYPE_CHECKING: from src.common.data_models.info_data_model import TargetPersonInfo - from src.common.data_models.database_data_model import DatabaseMessages, DatabaseActionRecords + from src.common.data_models.database_data_model import DatabaseMessages logger = get_logger("planner") @@ -121,7 +121,6 @@ no_reply_until_call ) - class ActionPlanner: def __init__(self, chat_id: str, action_manager: ActionManager): self.chat_id = chat_id @@ -168,7 +167,7 @@ class ActionPlanner: action_data = {key: value for key, value in action_json.items() if key not in ["action", "reason"]} # 非no_action动作需要target_message_id target_message = None - + if target_message_id := action_json.get("target_message_id"): # 根据target_message_id查找原始消息 target_message = self.find_message_by_id(target_message_id, message_id_list) @@ -179,12 +178,11 @@ class ActionPlanner: else: target_message = message_id_list[-1][1] logger.info(f"{self.log_prefix}动作'{action}'缺少target_message_id,使用最新消息作为target_message") - # 验证action是否可用 available_action_names = [action_name for action_name, _ in current_available_actions] internal_action_names = ["no_reply", "reply", "wait_time", "no_reply_until_call"] - + if action not in internal_action_names and action not in available_action_names: logger.warning( f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_reply'" @@ -223,18 +221,17 @@ class ActionPlanner: return action_planner_infos - async def plan( self, available_actions: Dict[str, ActionInfo], loop_start_time: float = 0.0, ) -> Tuple[List[ActionPlannerInfo], Optional["DatabaseMessages"]]: + # sourcery skip: use-named-expression """ 规划器 (Planner): 使用LLM根据上下文决定做出什么动作。 """ target_message: Optional["DatabaseMessages"] = None - - + # 获取聊天上下文 message_list_before_now = get_raw_msg_before_timestamp_with_chat( chat_id=self.chat_id, @@ -249,7 +246,7 @@ class ActionPlanner: truncate=True, show_actions=True, ) - + message_list_before_now_short = message_list_before_now[-int(global_config.chat.max_context_size * 0.3) :] chat_content_block_short, message_id_list_short = build_readable_messages_with_id( messages=message_list_before_now_short, @@ -257,17 +254,15 @@ class ActionPlanner: truncate=False, show_actions=False, ) - + self.last_obs_time_mark = time.time() - + # 获取必要信息 is_group_chat, chat_target_info, current_available_actions = self.get_necessary_info() - + # 应用激活类型过滤 - filtered_actions = self._filter_actions_by_activation_type( - available_actions, chat_content_block_short - ) - + filtered_actions = self._filter_actions_by_activation_type(available_actions, chat_content_block_short) + logger.info(f"{self.log_prefix}过滤后有{len(filtered_actions)}个可用动作") # 构建包含所有动作的提示词 @@ -279,21 +274,21 @@ class ActionPlanner: message_id_list=message_id_list, interest=global_config.personality.interest, ) - + # 调用LLM获取决策 actions = await self._execute_main_planner( prompt=prompt, message_id_list=message_id_list, filtered_actions=filtered_actions, available_actions=available_actions, - loop_start_time=loop_start_time + loop_start_time=loop_start_time, ) - + # 获取target_message(如果有非no_action的动作) non_no_actions = [a for a in actions if a.action_type != "no_reply"] if non_no_actions: target_message = non_no_actions[0].action_message - + return actions, target_message async def build_planner_prompt( @@ -333,7 +328,9 @@ class ActionPlanner: moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" bot_name = global_config.bot.nickname - bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" + bot_nickname = ( + f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" + ) name_block = f"你的名字是{bot_name}{bot_nickname},请注意哪些是你自己的发言。" # 获取主规划器模板并填充 @@ -379,15 +376,12 @@ class ActionPlanner: return is_group_chat, chat_target_info, current_available_actions - def _filter_actions_by_activation_type( - self, - available_actions: Dict[str, ActionInfo], - chat_content_block: str + self, available_actions: Dict[str, ActionInfo], chat_content_block: str ) -> Dict[str, ActionInfo]: """根据激活类型过滤动作""" filtered_actions = {} - + for action_name, action_info in available_actions.items(): if action_info.activation_type == ActionActivationType.NEVER: logger.debug(f"{self.log_prefix}动作 {action_name} 设置为 NEVER 激活类型,跳过") @@ -405,14 +399,15 @@ class ActionPlanner: break else: logger.warning(f"{self.log_prefix}未知的激活类型: {action_info.activation_type},跳过处理") - + return filtered_actions - + async def _build_action_options_block(self, current_available_actions: Dict[str, ActionInfo]) -> str: + # sourcery skip: use-join """构建动作选项块""" if not current_available_actions: return "" - + action_options_block = "" for action_name, action_info in current_available_actions.items(): # 构建参数文本 @@ -422,13 +417,13 @@ class ActionPlanner: for param_name, param_description in action_info.action_parameters.items(): param_text += f' "{param_name}":"{param_description}"\n' param_text = param_text.rstrip("\n") - + # 构建要求文本 require_text = "" for require_item in action_info.action_require: require_text += f"- {require_item}\n" require_text = require_text.rstrip("\n") - + # 获取动作提示模板并填充 using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt") using_action_prompt = using_action_prompt.format( @@ -437,30 +432,30 @@ class ActionPlanner: action_parameters=param_text, action_require=require_text, ) - + action_options_block += using_action_prompt - + return action_options_block - + async def _execute_main_planner( self, prompt: str, message_id_list: List[Tuple[str, "DatabaseMessages"]], filtered_actions: Dict[str, ActionInfo], available_actions: Dict[str, ActionInfo], - loop_start_time: float + loop_start_time: float, ) -> List[ActionPlannerInfo]: """执行主规划器""" llm_content = None actions: List[ActionPlannerInfo] = [] - + try: # 调用LLM llm_content, (reasoning_content, _, _) = await self.planner_llm.generate_response_async(prompt=prompt) - + logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") - + if global_config.debug.show_prompt: logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") logger.info(f"{self.log_prefix}规划器原始响应: {llm_content}") @@ -471,7 +466,7 @@ class ActionPlanner: logger.debug(f"{self.log_prefix}规划器原始响应: {llm_content}") if reasoning_content: logger.debug(f"{self.log_prefix}规划器推理: {reasoning_content}") - + except Exception as req_e: logger.error(f"{self.log_prefix}LLM 请求执行失败: {req_e}") return [ @@ -483,41 +478,38 @@ class ActionPlanner: available_actions=available_actions, ) ] - + # 解析LLM响应 if llm_content: try: - # 处理新的格式:多个```json包裹的JSON对象 - json_objects = self._extract_json_from_markdown(llm_content) - - if json_objects: + if json_objects := self._extract_json_from_markdown(llm_content): logger.info(f"{self.log_prefix}从响应中提取到{len(json_objects)}个JSON对象") filtered_actions_list = list(filtered_actions.items()) for json_obj in json_objects: - actions.extend( - self._parse_single_action(json_obj, message_id_list, filtered_actions_list) - ) + actions.extend(self._parse_single_action(json_obj, message_id_list, filtered_actions_list)) else: # 尝试解析为直接的JSON logger.warning(f"{self.log_prefix}LLM没有返回可用动作: {llm_content}") actions = self._create_no_reply("LLM没有返回可用动作", available_actions) - + except Exception as json_e: logger.warning(f"{self.log_prefix}解析LLM响应JSON失败 {json_e}. LLM原始输出: '{llm_content}'") actions = self._create_no_reply(f"解析LLM响应JSON失败: {json_e}", available_actions) traceback.print_exc() else: actions = self._create_no_reply("规划器没有获得LLM响应", available_actions) - - + # 添加循环开始时间到所有非no_action动作 for action in actions: + action.action_data = action.action_data or {} action.action_data["loop_start_time"] = loop_start_time - - logger.info(f"{self.log_prefix}规划器决定执行{len(actions)}个动作: {' '.join([a.action_type for a in actions])}") - + + logger.info( + f"{self.log_prefix}规划器决定执行{len(actions)}个动作: {' '.join([a.action_type for a in actions])}" + ) + return actions - + def _create_no_reply(self, reasoning: str, available_actions: Dict[str, ActionInfo]) -> List[ActionPlannerInfo]: """创建no_action""" return [ @@ -529,23 +521,22 @@ class ActionPlanner: available_actions=available_actions, ) ] - + def _extract_json_from_markdown(self, content: str) -> List[dict]: + # sourcery skip: for-append-to-extend """从Markdown格式的内容中提取JSON对象""" json_objects = [] - + # 使用正则表达式查找```json包裹的JSON内容 - json_pattern = r'```json\s*(.*?)\s*```' + json_pattern = r"```json\s*(.*?)\s*```" matches = re.findall(json_pattern, content, re.DOTALL) - + for match in matches: try: # 清理可能的注释和格式问题 - json_str = re.sub(r'//.*?\n', '\n', match) # 移除单行注释 - json_str = re.sub(r'/\*.*?\*/', '', json_str, flags=re.DOTALL) # 移除多行注释 - json_str = json_str.strip() - - if json_str: + json_str = re.sub(r"//.*?\n", "\n", match) # 移除单行注释 + json_str = re.sub(r"/\*.*?\*/", "", json_str, flags=re.DOTALL) # 移除多行注释 + if json_str := json_str.strip(): json_obj = json.loads(repair_json(json_str)) if isinstance(json_obj, dict): json_objects.append(json_obj) @@ -556,7 +547,7 @@ class ActionPlanner: except Exception as e: logger.warning(f"解析JSON块失败: {e}, 块内容: {match[:100]}...") continue - + return json_objects diff --git a/src/common/data_models/info_data_model.py b/src/common/data_models/info_data_model.py index 0f7b1f95..156f021c 100644 --- a/src/common/data_models/info_data_model.py +++ b/src/common/data_models/info_data_model.py @@ -23,3 +23,4 @@ class ActionPlannerInfo(BaseDataModel): action_data: Optional[Dict] = None action_message: Optional["DatabaseMessages"] = None available_actions: Optional[Dict[str, "ActionInfo"]] = None + loop_start_time: Optional[float] = None diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 0cf8014a..4566ec9f 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -72,9 +72,6 @@ class ChatConfig(ConfigBase): interest_rate_mode: Literal["fast", "accurate"] = "fast" """兴趣值计算模式,fast为快速计算,accurate为精确计算""" - mentioned_bot_reply: float = 1 - """提及 bot 必然回复,1为100%回复,0为不额外增幅""" - planner_size: float = 1.5 """副规划器大小,越小,麦麦的动作执行能力越精细,但是消耗更多token,调大可以缓解429类错误""" diff --git a/src/main.py b/src/main.py index 2f33818e..fb293d0e 100644 --- a/src/main.py +++ b/src/main.py @@ -142,13 +142,13 @@ class MainSystem: await asyncio.gather(*tasks) - async def forget_memory_task(self): - """记忆遗忘任务""" - while True: - await asyncio.sleep(global_config.memory.forget_memory_interval) - logger.info("[记忆遗忘] 开始遗忘记忆...") - await self.hippocampus_manager.forget_memory(percentage=global_config.memory.memory_forget_percentage) # type: ignore - logger.info("[记忆遗忘] 记忆遗忘完成") + # async def forget_memory_task(self): + # """记忆遗忘任务""" + # while True: + # await asyncio.sleep(global_config.memory.forget_memory_interval) + # logger.info("[记忆遗忘] 开始遗忘记忆...") + # await self.hippocampus_manager.forget_memory(percentage=global_config.memory.memory_forget_percentage) # type: ignore + # logger.info("[记忆遗忘] 记忆遗忘完成") async def main(): diff --git a/src/plugins/built_in/relation/relation.py b/src/plugins/built_in/relation/relation.py index bc65b1aa..5edf46c3 100644 --- a/src/plugins/built_in/relation/relation.py +++ b/src/plugins/built_in/relation/relation.py @@ -179,7 +179,7 @@ class BuildRelationAction(BaseAction): chat_model_config = models.get("utils") success, update_memory, _, _ = await llm_api.generate_with_model( prompt, - model_config=chat_model_config, + model_config=chat_model_config, # type: ignore request_type="relation.category.update", # type: ignore )