diff --git a/src/chat/normal_chat/normal_chat.py b/src/chat/normal_chat/normal_chat.py index 07b4f2908..7f1788591 100644 --- a/src/chat/normal_chat/normal_chat.py +++ b/src/chat/normal_chat/normal_chat.py @@ -19,6 +19,7 @@ from src.person_info.relationship_builder_manager import relationship_builder_ma from .priority_manager import PriorityManager import traceback from src.chat.planner_actions.planner_normal import NormalChatPlanner +from src.chat.planner_actions.planner_focus import ActionPlanner from src.chat.planner_actions.action_modifier import ActionModifier from src.chat.utils.utils import get_chat_type_and_target_info @@ -70,7 +71,7 @@ class NormalChat: # Planner相关初始化 self.action_manager = ActionManager() - self.planner = NormalChatPlanner(self.stream_name, self.action_manager) + self.planner = ActionPlanner(self.stream_id, self.action_manager, mode="normal") self.action_modifier = ActionModifier(self.action_manager, self.stream_id) self.enable_planner = global_config.normal_chat.enable_planner # 从配置中读取是否启用planner @@ -525,7 +526,7 @@ class NormalChat: return no_action # 执行规划 - plan_result = await self.planner.plan(message) + plan_result = await self.planner.plan() action_type = plan_result["action_result"]["action_type"] action_data = plan_result["action_result"]["action_data"] reasoning = plan_result["action_result"]["reasoning"] diff --git a/src/chat/planner_actions/planner_focus.py b/src/chat/planner_actions/planner.py similarity index 73% rename from src/chat/planner_actions/planner_focus.py rename to src/chat/planner_actions/planner.py index 0f5e84097..2c2fcf007 100644 --- a/src/chat/planner_actions/planner_focus.py +++ b/src/chat/planner_actions/planner.py @@ -29,13 +29,15 @@ def init_prompt(): {chat_content_block} {moderation_prompt} -现在请你根据聊天内容选择合适的action: - +现在请你根据{by_what}选择合适的action: +{no_action_block} {action_options_text} +你必须从上面列出的可用action中选择一个,并说明原因。 + 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: """, - "simple_planner_prompt", + "planner_prompt", ) Prompt( @@ -52,20 +54,15 @@ def init_prompt(): class ActionPlanner: - def __init__(self, chat_id: str, action_manager: ActionManager): + def __init__(self, chat_id: str, action_manager: ActionManager, mode: str = "focus"): self.chat_id = chat_id self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]" - + self.mode = mode self.action_manager = action_manager # LLM规划器配置 self.planner_llm = LLMRequest( model=global_config.model.planner, - request_type="focus.planner", # 用于动作规划 - ) - - self.utils_llm = LLMRequest( - model=global_config.model.utils_small, - request_type="focus.planner", # 用于动作规划 + request_type=f"{self.mode}.planner", # 用于动作规划 ) self.last_obs_time_mark = 0.0 @@ -82,37 +79,10 @@ class ActionPlanner: try: is_group_chat = True - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=self.chat_id, - timestamp=time.time(), - limit=global_config.chat.max_context_size, - ) + is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id) + logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}") - chat_context = build_readable_messages( - messages=message_list_before_now, - timestamp_mode="normal_no_YMD", - read_mark=self.last_obs_time_mark, - truncate=True, - show_actions=True, - ) - - self.last_obs_time_mark = time.time() - - # 获取聊天类型和目标信息 - chat_target_info = None - - try: - # 重新获取更准确的聊天信息 - is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id) - logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}") - except Exception as e: - logger.warning(f"{self.log_prefix}获取聊天目标信息失败: {e}") - chat_target_info = None - - # 获取经过modify_actions处理后的最终可用动作集 - # 注意:动作的激活判定现在在主循环的modify_actions中完成 - # 使用Focus模式过滤动作 - current_available_actions_dict = self.action_manager.get_using_actions_for_mode("focus") + current_available_actions_dict = self.action_manager.get_using_actions_for_mode(self.mode) # 获取完整的动作信息 all_registered_actions = self.action_manager.get_registered_actions() @@ -130,7 +100,6 @@ class ActionPlanner: action = "no_reply" reasoning = "没有可用的动作" if not current_available_actions else "只有no_reply动作可用,跳过规划" logger.info(f"{self.log_prefix}{reasoning}") - self.action_manager.restore_actions() logger.debug( f"{self.log_prefix}[focus]沉默后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}" ) @@ -142,14 +111,12 @@ class ActionPlanner: prompt = await self.build_planner_prompt( is_group_chat=is_group_chat, # <-- Pass HFC state chat_target_info=chat_target_info, # <-- 传递获取到的聊天目标信息 - observed_messages_str=chat_context, # <-- Pass local variable current_available_actions=current_available_actions, # <-- Pass determined actions ) # --- 调用 LLM (普通文本生成) --- llm_content = None try: - prompt = f"{prompt}" llm_content, (reasoning_content, _) = await self.planner_llm.generate_response_async(prompt=prompt) logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") @@ -164,34 +131,21 @@ class ActionPlanner: if llm_content: try: - fixed_json_string = repair_json(llm_content) - if isinstance(fixed_json_string, str): - try: - parsed_json = json.loads(fixed_json_string) - except json.JSONDecodeError as decode_error: - logger.error(f"JSON解析错误: {str(decode_error)}") - parsed_json = {} - else: - # 如果repair_json直接返回了字典对象,直接使用 - parsed_json = fixed_json_string + parsed_json = json.loads(repair_json(llm_content)) - # 处理repair_json可能返回列表的情况 if isinstance(parsed_json, list): if parsed_json: - # 取列表中最后一个元素(通常是最完整的) parsed_json = parsed_json[-1] logger.warning(f"{self.log_prefix}LLM返回了多个JSON对象,使用最后一个: {parsed_json}") else: parsed_json = {} - # 确保parsed_json是字典 if not isinstance(parsed_json, dict): logger.error(f"{self.log_prefix}解析后的JSON不是字典类型: {type(parsed_json)}") parsed_json = {} - # 提取决策,提供默认值 - extracted_action = parsed_json.get("action", "no_reply") - extracted_reasoning = "" + action = parsed_json.get("action", "no_reply") + reasoning = parsed_json.get("reasoning", "未提供原因") # 将所有其他属性添加到action_data action_data = {} @@ -199,16 +153,16 @@ class ActionPlanner: if key not in ["action", "reasoning"]: action_data[key] = value - if extracted_action not in current_available_actions: + if action == "no_action": + action = "no_reply" + reasoning = "决定不使用额外动作" + + if action not in current_available_actions and action != "no_action": logger.warning( - f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'" + f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'" ) action = "no_reply" - reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}" - else: - # 动作有效且可用 - action = extracted_action - reasoning = extracted_reasoning + reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {list(current_available_actions.keys())})。原始理由: {reasoning}" except Exception as json_e: logger.warning(f"{self.log_prefix}解析LLM响应JSON失败 {json_e}. LLM原始输出: '{llm_content}'") @@ -222,13 +176,19 @@ class ActionPlanner: action = "no_reply" reasoning = f"Planner 内部处理错误: {outer_e}" - # 恢复到默认动作集 - self.action_manager.restore_actions() - logger.debug( - f"{self.log_prefix}规划后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}" - ) - action_result = {"action_type": action, "action_data": action_data, "reasoning": reasoning} + is_parallel = False + if action in current_available_actions: + action_info = current_available_actions[action] + is_parallel = action_info.get("parallel_action", False) + + action_result = { + "action_type": action, + "action_data": action_data, + "reasoning": reasoning, + "timestamp": time.time(), + "is_parallel": is_parallel, + } plan_result = { "action_result": action_result, @@ -241,11 +201,36 @@ class ActionPlanner: self, is_group_chat: bool, # Now passed as argument chat_target_info: Optional[dict], # Now passed as argument - observed_messages_str: str, current_available_actions, ) -> str: """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: + message_list_before_now = get_raw_msg_before_timestamp_with_chat( + chat_id=self.chat_id, + timestamp=time.time(), + limit=global_config.chat.max_context_size, + ) + + chat_content_block = build_readable_messages( + messages=message_list_before_now, + timestamp_mode="normal_no_YMD", + read_mark=self.last_obs_time_mark, + truncate=True, + show_actions=True, + ) + + self.last_obs_time_mark = time.time() + + + if self.mode == "focus": + by_what = "聊天内容" + no_action_block = "" + else: + by_what = "聊天内容和用户的最新消息" + no_action_block = """重要说明: +- 'no_action' 表示只进行普通聊天回复,不执行任何额外动作 +- 其他action表示在普通回复的基础上,执行相应的额外动作""" + chat_context_description = "你现在正在一个群聊中" chat_target_name = None # Only relevant for private if not is_group_chat and chat_target_info: @@ -254,11 +239,6 @@ class ActionPlanner: ) chat_context_description = f"你正在和 {chat_target_name} 私聊" - chat_content_block = "" - if observed_messages_str: - chat_content_block = f"\n{observed_messages_str}" - else: - chat_content_block = "你还未开始聊天" action_options_block = "" @@ -286,10 +266,8 @@ class ActionPlanner: action_options_block += using_action_prompt - # moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" - moderation_prompt_block = "" + moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" - # 获取当前时间 time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" bot_name = global_config.bot.nickname @@ -300,11 +278,13 @@ class ActionPlanner: bot_core_personality = global_config.personality.personality_core indentify_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" - planner_prompt_template = await global_prompt_manager.get_prompt_async("simple_planner_prompt") + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") prompt = planner_prompt_template.format( time_block=time_block, + by_what=by_what, chat_context_description=chat_context_description, chat_content_block=chat_content_block, + no_action_block=no_action_block, action_options_text=action_options_block, moderation_prompt=moderation_prompt_block, indentify_block=indentify_block, diff --git a/src/chat/planner_actions/planner_normal.py b/src/chat/planner_actions/planner_normal.py deleted file mode 100644 index fce446b58..000000000 --- a/src/chat/planner_actions/planner_normal.py +++ /dev/null @@ -1,306 +0,0 @@ -import json -from typing import Dict, Any -from rich.traceback import install -from src.llm_models.utils_model import LLMRequest -from src.config.config import global_config -from src.common.logger import get_logger -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.individuality.individuality import get_individuality -from src.chat.planner_actions.action_manager import ActionManager -from src.chat.message_receive.message import MessageThinking -from json_repair import repair_json -from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat -import time -import traceback - -logger = get_logger("normal_chat_planner") - -install(extra_lines=3) - - -def init_prompt(): - Prompt( - """ -你的自我认知是: -{self_info_block} -请记住你的性格,身份和特点。 - -你是群内的一员,你现在正在参与群内的闲聊,以下是群内的聊天内容: -{chat_context} - -基于以上聊天上下文和用户的最新消息,选择最合适的action。 - -注意,除了下面动作选项之外,你在聊天中不能做其他任何事情,这是你能力的边界,现在请你选择合适的action: - -{action_options_text} - -重要说明: -- "no_action" 表示只进行普通聊天回复,不执行任何额外动作 -- 其他action表示在普通回复的基础上,执行相应的额外动作 - -你必须从上面列出的可用action中选择一个,并说明原因。 -{moderation_prompt} - -请以动作的输出要求,以严格的 JSON 格式输出,且仅包含 JSON 内容。不要有任何其他文字或解释: -""", - "normal_chat_planner_prompt", - ) - - Prompt( - """ -动作:{action_name} -动作描述:{action_description} -{action_require} -{{ - "action": "{action_name}",{action_parameters} -}} -""", - "normal_chat_action_prompt", - ) - - -class NormalChatPlanner: - def __init__(self, log_prefix: str, action_manager: ActionManager): - self.log_prefix = log_prefix - # LLM规划器配置 - self.planner_llm = LLMRequest( - model=global_config.model.planner, - request_type="normal.planner", # 用于normal_chat动作规划 - ) - - self.action_manager = action_manager - - async def plan(self, message: MessageThinking) -> Dict[str, Any]: - """ - Normal Chat 规划器: 使用LLM根据上下文决定做出什么动作。 - - 参数: - message: 思考消息对象 - sender_name: 发送者名称 - """ - - action = "no_action" # 默认动作改为no_action - reasoning = "规划器初始化默认" - action_data = {} - - try: - # 设置默认值 - nickname_str = "" - for nicknames in global_config.bot.alias_names: - nickname_str += f"{nicknames}," - name_block = f"你的名字是{global_config.bot.nickname},你的昵称有{nickname_str},有人也会用这些昵称称呼你。" - - personality_block = get_individuality().get_personality_prompt(x_person=2, level=2) - identity_block = get_individuality().get_identity_prompt(x_person=2, level=2) - - self_info = name_block + personality_block + identity_block - - # 获取当前可用的动作,使用Normal模式过滤 - current_available_actions = self.action_manager.get_using_actions_for_mode("normal") - - # 注意:动作的激活判定现在在 normal_chat_action_modifier 中完成 - # 这里直接使用经过 action_modifier 处理后的最终动作集 - # 符合职责分离原则:ActionModifier负责动作管理,Planner专注于决策 - - # 如果没有可用动作,直接返回no_action - if not current_available_actions: - logger.debug(f"{self.log_prefix}规划器: 没有可用动作,返回no_action") - return { - "action_result": { - "action_type": action, - "action_data": action_data, - "reasoning": reasoning, - "is_parallel": True, - }, - "chat_context": "", - "action_prompt": "", - } - - # 构建normal_chat的上下文 (使用与normal_chat相同的prompt构建方法) - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=message.chat_stream.stream_id, - timestamp=time.time(), - limit=global_config.chat.max_context_size, - ) - - chat_context = build_readable_messages( - message_list_before_now, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="relative", - read_mark=0.0, - show_actions=True, - ) - - # 构建planner的prompt - prompt = await self.build_planner_prompt( - self_info_block=self_info, - chat_context=chat_context, - current_available_actions=current_available_actions, - ) - - if not prompt: - logger.warning(f"{self.log_prefix}规划器: 构建提示词失败") - return { - "action_result": { - "action_type": action, - "action_data": action_data, - "reasoning": reasoning, - "is_parallel": False, - }, - "chat_context": chat_context, - "action_prompt": "", - } - - # 使用LLM生成动作决策 - try: - content, (reasoning_content, model_name) = await self.planner_llm.generate_response_async(prompt) - - logger.info(f"{self.log_prefix}规划器原始提示词: {prompt}") - logger.info(f"{self.log_prefix}规划器原始响应: {content}") - if reasoning_content: - logger.info(f"{self.log_prefix}规划器推理: {reasoning_content}") - - # 解析JSON响应 - try: - # 尝试修复JSON - fixed_json = repair_json(content) - action_result = json.loads(fixed_json) - - action = action_result.get("action", "no_action") - reasoning = action_result.get("reasoning", "未提供原因") - - # 提取其他参数作为action_data - action_data = {k: v for k, v in action_result.items() if k not in ["action", "reasoning"]} - - # 验证动作是否在可用动作列表中,或者是特殊动作 - if action not in current_available_actions: - logger.warning(f"{self.log_prefix}规划器选择了不可用的动作: {action}, 回退到no_action") - action = "no_action" - reasoning = f"选择的动作{action}不在可用列表中,回退到no_action" - action_data = {} - - except json.JSONDecodeError as e: - logger.warning(f"{self.log_prefix}规划器JSON解析失败: {e}, 内容: {content}") - action = "no_action" - reasoning = "JSON解析失败,使用默认动作" - action_data = {} - - except Exception as e: - logger.error(f"{self.log_prefix}规划器LLM调用失败: {e}") - action = "no_action" - reasoning = "LLM调用失败,使用默认动作" - action_data = {} - - except Exception as outer_e: - logger.error(f"{self.log_prefix}规划器异常: {outer_e}") - # 设置异常时的默认值 - current_available_actions = {} - chat_context = "无法获取聊天上下文" - prompt = "" - action = "no_action" - reasoning = "规划器出现异常,使用默认动作" - action_data = {} - - # 检查动作是否支持并行执行 - is_parallel = False - if action in current_available_actions: - action_info = current_available_actions[action] - is_parallel = action_info.get("parallel_action", False) - - logger.debug( - f"{self.log_prefix}规划器决策动作:{action}, 动作信息: '{action_data}', 理由: {reasoning}, 并行执行: {is_parallel}" - ) - - # 恢复到默认动作集 - self.action_manager.restore_actions() - logger.debug( - f"{self.log_prefix}规划后恢复到默认动作集, 当前可用: {list(self.action_manager.get_using_actions().keys())}" - ) - - # 构建 action 记录 - action_record = { - "action_type": action, - "action_data": action_data, - "reasoning": reasoning, - "timestamp": time.time(), - "model_name": model_name if "model_name" in locals() else None, - } - - action_result = { - "action_type": action, - "action_data": action_data, - "reasoning": reasoning, - "is_parallel": is_parallel, - "action_record": json.dumps(action_record, ensure_ascii=False), - } - - plan_result = { - "action_result": action_result, - "chat_context": chat_context, - "action_prompt": prompt, - } - - return plan_result - - async def build_planner_prompt( - self, - self_info_block: str, - chat_context: str, - current_available_actions: Dict[str, Any], - ) -> str: - """构建 Normal Chat Planner LLM 的提示词""" - try: - # 构建动作选项文本 - action_options_text = "" - - for action_name, action_info in current_available_actions.items(): - action_description = action_info.get("description", "") - action_parameters = action_info.get("parameters", {}) - action_require = action_info.get("require", []) - - if action_parameters: - param_text = "\n" - # print(action_parameters) - for param_name, param_description in action_parameters.items(): - param_text += f' "{param_name}":"{param_description}"\n' - param_text = param_text.rstrip("\n") - else: - param_text = "" - - require_text = "" - for require_item in action_require: - require_text += f"- {require_item}\n" - require_text = require_text.rstrip("\n") - - # 构建单个动作的提示 - action_prompt = await global_prompt_manager.format_prompt( - "normal_chat_action_prompt", - action_name=action_name, - action_description=action_description, - action_parameters=param_text, - action_require=require_text, - ) - action_options_text += action_prompt + "\n\n" - - # 审核提示 - moderation_prompt = "请确保你的回复符合平台规则,避免不当内容。" - - # 使用模板构建最终提示词 - prompt = await global_prompt_manager.format_prompt( - "normal_chat_planner_prompt", - self_info_block=self_info_block, - action_options_text=action_options_text, - moderation_prompt=moderation_prompt, - chat_context=chat_context, - ) - - return prompt - - except Exception as e: - logger.error(f"{self.log_prefix}构建Planner提示词失败: {e}") - traceback.print_exc() - return "" - - -init_prompt() diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index e269cdddf..b8781cea9 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -61,7 +61,7 @@ enable_relationship = true # 是否启用关系系统 relation_frequency = 1 # 关系频率,麦麦构建关系的速度,仅在normal_chat模式下有效 [chat] #麦麦的聊天通用设置 -chat_mode = "normal" # 聊天模式 —— 普通模式:normal,专注模式:focus,在普通模式和专注模式之间自动切换 +chat_mode = "normal" # 聊天模式 —— 普通模式:normal,专注模式:focus,auto模式:在普通模式和专注模式之间自动切换 auto_focus_threshold = 1 # 自动切换到专注聊天的阈值,越低越容易进入专注聊天 exit_focus_threshold = 1 # 自动退出专注聊天的阈值,越低越容易退出专注聊天 # 普通模式下,麦麦会针对感兴趣的消息进行回复,token消耗量较低