From 750bdfe04dc785077abc70a5358077e8274ba167 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Tue, 16 Sep 2025 15:02:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=A7=81=E8=81=8A=E7=8B=AC=E6=9C=89replye?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/replyer/default_generator.py | 6 +- src/chat/replyer/private_generator.py | 1054 +++++++++++++++++ src/chat/replyer/{ => prompt}/lpmm_prompt.py | 0 .../replyer/{ => prompt}/replyer_prompt.py | 20 + .../replyer/{ => prompt}/rewrite_prompt.py | 0 src/chat/replyer/replyer_manager.py | 20 +- 6 files changed, 1091 insertions(+), 9 deletions(-) create mode 100644 src/chat/replyer/private_generator.py rename src/chat/replyer/{ => prompt}/lpmm_prompt.py (100%) rename src/chat/replyer/{ => prompt}/replyer_prompt.py (73%) rename src/chat/replyer/{ => prompt}/rewrite_prompt.py (100%) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 28cea041..4efa576c 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -32,9 +32,9 @@ from src.person_info.person_info import Person, is_person_known from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.apis import llm_api -from src.chat.replyer.lpmm_prompt import init_lpmm_prompt -from src.chat.replyer.replyer_prompt import init_replyer_prompt -from src.chat.replyer.rewrite_prompt import init_rewrite_prompt +from src.chat.replyer.prompt.lpmm_prompt import init_lpmm_prompt +from src.chat.replyer.prompt.replyer_prompt import init_replyer_prompt +from src.chat.replyer.prompt.rewrite_prompt import init_rewrite_prompt init_lpmm_prompt() init_replyer_prompt() diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py new file mode 100644 index 00000000..7b81a28c --- /dev/null +++ b/src/chat/replyer/private_generator.py @@ -0,0 +1,1054 @@ +import traceback +import time +import asyncio +import random +import re + +from typing import List, Optional, Dict, Any, Tuple +from datetime import datetime +from src.mais4u.mai_think import mai_thinking_manager +from src.common.logger import get_logger +from src.common.data_models.database_data_model import DatabaseMessages +from src.common.data_models.info_data_model import ActionPlannerInfo +from src.common.data_models.llm_data_model import LLMGenerationDataModel +from src.config.config import global_config, model_config +from src.llm_models.utils_model import LLMRequest +from src.chat.message_receive.message import UserInfo, Seg, MessageRecv, MessageSending +from src.chat.message_receive.chat_stream import ChatStream +from src.chat.message_receive.uni_message_sender import UniversalMessageSender +from src.chat.utils.timer_calculator import Timer # <--- Import Timer +from src.chat.utils.utils import get_chat_type_and_target_info +from src.chat.utils.prompt_builder import global_prompt_manager +from src.chat.utils.chat_message_builder import ( + build_readable_messages, + get_raw_msg_before_timestamp_with_chat, + replace_user_references, +) +from src.chat.express.expression_selector import expression_selector + +# from src.chat.memory_system.memory_activator import MemoryActivator +from src.mood.mood_manager import mood_manager +from src.person_info.person_info import Person, is_person_known +from src.plugin_system.base.component_types import ActionInfo, EventType +from src.plugin_system.apis import llm_api + +from src.chat.replyer.prompt.lpmm_prompt import init_lpmm_prompt +from src.chat.replyer.prompt.replyer_prompt import init_replyer_prompt +from src.chat.replyer.prompt.rewrite_prompt import init_rewrite_prompt + +init_lpmm_prompt() +init_replyer_prompt() +init_rewrite_prompt() + + +logger = get_logger("replyer") + +class PrivateReplyer: + def __init__( + self, + chat_stream: ChatStream, + request_type: str = "replyer", + ): + self.express_model = LLMRequest(model_set=model_config.model_task_config.replyer, request_type=request_type) + self.chat_stream = chat_stream + self.is_group_chat, self.chat_target_info = get_chat_type_and_target_info(self.chat_stream.stream_id) + self.heart_fc_sender = UniversalMessageSender() + # self.memory_activator = MemoryActivator() + + from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 + + self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=True, cache_ttl=3) + + async def generate_reply_with_context( + self, + extra_info: str = "", + reply_reason: str = "", + available_actions: Optional[Dict[str, ActionInfo]] = None, + chosen_actions: Optional[List[ActionPlannerInfo]] = None, + enable_tool: bool = True, + from_plugin: bool = True, + stream_id: Optional[str] = None, + reply_message: Optional[DatabaseMessages] = None, + ) -> Tuple[bool, LLMGenerationDataModel]: + # sourcery skip: merge-nested-ifs + """ + 回复器 (Replier): 负责生成回复文本的核心逻辑。 + + Args: + reply_to: 回复对象,格式为 "发送者:消息内容" + extra_info: 额外信息,用于补充上下文 + reply_reason: 回复原因 + available_actions: 可用的动作信息字典 + chosen_actions: 已选动作 + enable_tool: 是否启用工具调用 + from_plugin: 是否来自插件 + + Returns: + Tuple[bool, Optional[Dict[str, Any]], Optional[str]]: (是否成功, 生成的回复, 使用的prompt) + """ + + prompt = None + selected_expressions: Optional[List[int]] = None + llm_response = LLMGenerationDataModel() + if available_actions is None: + available_actions = {} + try: + # 3. 构建 Prompt + with Timer("构建Prompt", {}): # 内部计时器,可选保留 + prompt, selected_expressions = await self.build_prompt_reply_context( + extra_info=extra_info, + available_actions=available_actions, + chosen_actions=chosen_actions, + enable_tool=enable_tool, + reply_message=reply_message, + reply_reason=reply_reason, + ) + llm_response.prompt = prompt + llm_response.selected_expressions = selected_expressions + + if not prompt: + logger.warning("构建prompt失败,跳过回复生成") + return False, llm_response + from src.plugin_system.core.events_manager import events_manager + + if not from_plugin: + continue_flag, modified_message = await events_manager.handle_mai_events( + EventType.POST_LLM, None, prompt, None, stream_id=stream_id + ) + if not continue_flag: + raise UserWarning("插件于请求前中断了内容生成") + if modified_message and modified_message._modify_flags.modify_llm_prompt: + llm_response.prompt = modified_message.llm_prompt + prompt = str(modified_message.llm_prompt) + + # 4. 调用 LLM 生成回复 + content = None + reasoning_content = None + model_name = "unknown_model" + + try: + content, reasoning_content, model_name, tool_call = await self.llm_generate_content(prompt) + logger.debug(f"replyer生成内容: {content}") + llm_response.content = content + llm_response.reasoning = reasoning_content + llm_response.model = model_name + llm_response.tool_calls = tool_call + continue_flag, modified_message = await events_manager.handle_mai_events( + EventType.AFTER_LLM, None, prompt, llm_response, stream_id=stream_id + ) + if not from_plugin and not continue_flag: + raise UserWarning("插件于请求后取消了内容生成") + if modified_message: + if modified_message._modify_flags.modify_llm_prompt: + logger.warning("警告:插件在内容生成后才修改了prompt,此修改不会生效") + llm_response.prompt = modified_message.llm_prompt # 虽然我不知道为什么在这里需要改prompt + if modified_message._modify_flags.modify_llm_response_content: + llm_response.content = modified_message.llm_response_content + if modified_message._modify_flags.modify_llm_response_reasoning: + llm_response.reasoning = modified_message.llm_response_reasoning + except UserWarning as e: + raise e + except Exception as llm_e: + # 精简报错信息 + logger.error(f"LLM 生成失败: {llm_e}") + return False, llm_response # LLM 调用失败则无法生成回复 + + return True, llm_response + + except UserWarning as uw: + raise uw + except Exception as e: + logger.error(f"回复生成意外失败: {e}") + traceback.print_exc() + return False, llm_response + + async def rewrite_reply_with_context( + self, + raw_reply: str = "", + reason: str = "", + reply_to: str = "", + ) -> Tuple[bool, LLMGenerationDataModel]: + """ + 表达器 (Expressor): 负责重写和优化回复文本。 + + Args: + raw_reply: 原始回复内容 + reason: 回复原因 + reply_to: 回复对象,格式为 "发送者:消息内容" + relation_info: 关系信息 + + Returns: + Tuple[bool, Optional[str]]: (是否成功, 重写后的回复内容) + """ + llm_response = LLMGenerationDataModel() + try: + with Timer("构建Prompt", {}): # 内部计时器,可选保留 + prompt = await self.build_prompt_rewrite_context( + raw_reply=raw_reply, + reason=reason, + reply_to=reply_to, + ) + llm_response.prompt = prompt + + content = None + reasoning_content = None + model_name = "unknown_model" + if not prompt: + logger.error("Prompt 构建失败,无法生成回复。") + return False, llm_response + + try: + content, reasoning_content, model_name, _ = await self.llm_generate_content(prompt) + logger.info(f"想要表达:{raw_reply}||理由:{reason}||生成回复: {content}\n") + llm_response.content = content + llm_response.reasoning = reasoning_content + llm_response.model = model_name + + except Exception as llm_e: + # 精简报错信息 + logger.error(f"LLM 生成失败: {llm_e}") + return False, llm_response # LLM 调用失败则无法生成回复 + + return True, llm_response + + except Exception as e: + logger.error(f"回复生成意外失败: {e}") + traceback.print_exc() + return False, llm_response + + async def build_relation_info(self, chat_content: str, sender: str): + if not global_config.relationship.enable_relationship: + return "" + + if not sender: + return "" + + if sender == global_config.bot.nickname: + return "" + + # 获取用户ID + person = Person(person_name=sender) + if not is_person_known(person_name=sender): + logger.warning(f"未找到用户 {sender} 的ID,跳过信息提取") + return f"你完全不认识{sender},不理解ta的相关信息。" + + sender_relation = await person.build_relationship(chat_content) + + return f"{sender_relation}" + + async def build_expression_habits(self, chat_history: str, target: str) -> Tuple[str, List[int]]: + # sourcery skip: for-append-to-extend + """构建表达习惯块 + + Args: + chat_history: 聊天历史记录 + target: 目标消息内容 + + Returns: + str: 表达习惯信息字符串 + """ + # 检查是否允许在此聊天流中使用表达 + use_expression, _, _ = global_config.expression.get_expression_config_for_chat(self.chat_stream.stream_id) + if not use_expression: + return "", [] + style_habits = [] + # 使用从处理器传来的选中表达方式 + # LLM模式:调用LLM选择5-10个,然后随机选5个 + selected_expressions, selected_ids = await expression_selector.select_suitable_expressions_llm( + self.chat_stream.stream_id, chat_history, max_num=8, target_message=target + ) + + if selected_expressions: + logger.debug(f"使用处理器选中的{len(selected_expressions)}个表达方式") + for expr in selected_expressions: + if isinstance(expr, dict) and "situation" in expr and "style" in expr: + style_habits.append(f"当{expr['situation']}时,使用 {expr['style']}") + else: + logger.debug("没有从处理器获得表达方式,将使用空的表达方式") + # 不再在replyer中进行随机选择,全部交给处理器处理 + + style_habits_str = "\n".join(style_habits) + + # 动态构建expression habits块 + expression_habits_block = "" + expression_habits_title = "" + if style_habits_str.strip(): + expression_habits_title = ( + "在回复时,你可以参考以下的语言习惯,不要生硬使用:" + ) + expression_habits_block += f"{style_habits_str}\n" + + return f"{expression_habits_title}\n{expression_habits_block}", selected_ids + + # async def build_memory_block(self, chat_history: List[DatabaseMessages], target: str) -> str: + # """构建记忆块 + + # Args: + # chat_history: 聊天历史记录 + # target: 目标消息内容 + + # Returns: + # str: 记忆信息字符串 + # """ + + # if not global_config.memory.enable_memory: + # return "" + + # instant_memory = None + + # running_memories = await self.memory_activator.activate_memory_with_chat_history( + # target_message=target, chat_history=chat_history + # ) + # if not running_memories: + # return "" + + # memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" + # for running_memory in running_memories: + # keywords, content = running_memory + # memory_str += f"- {keywords}:{content}\n" + + # if instant_memory: + # memory_str += f"- {instant_memory}\n" + + # return memory_str + + async def build_tool_info(self, chat_history: str, sender: str, target: str, enable_tool: bool = True) -> str: + """构建工具信息块 + + Args: + chat_history: 聊天历史记录 + reply_to: 回复对象,格式为 "发送者:消息内容" + enable_tool: 是否启用工具调用 + + Returns: + str: 工具信息字符串 + """ + + if not enable_tool: + return "" + + try: + # 使用工具执行器获取信息 + tool_results, _, _ = await self.tool_executor.execute_from_chat_message( + sender=sender, target_message=target, chat_history=chat_history, return_details=False + ) + + if tool_results: + tool_info_str = "以下是你通过工具获取到的实时信息:\n" + for tool_result in tool_results: + tool_name = tool_result.get("tool_name", "unknown") + content = tool_result.get("content", "") + result_type = tool_result.get("type", "tool_result") + + tool_info_str += f"- 【{tool_name}】{result_type}: {content}\n" + + tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。" + logger.info(f"获取到 {len(tool_results)} 个工具结果") + + return tool_info_str + else: + logger.debug("未获取到任何工具结果") + return "" + + except Exception as e: + logger.error(f"工具信息获取失败: {e}") + return "" + + def _parse_reply_target(self, target_message: Optional[str]) -> Tuple[str, str]: + """解析回复目标消息 + + Args: + target_message: 目标消息,格式为 "发送者:消息内容" 或 "发送者:消息内容" + + Returns: + Tuple[str, str]: (发送者名称, 消息内容) + """ + sender = "" + target = "" + # 添加None检查,防止NoneType错误 + if target_message is None: + return sender, target + if ":" in target_message or ":" in target_message: + # 使用正则表达式匹配中文或英文冒号 + parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1) + if len(parts) == 2: + sender = parts[0].strip() + target = parts[1].strip() + return sender, target + + async def build_keywords_reaction_prompt(self, target: Optional[str]) -> str: + """构建关键词反应提示 + + Args: + target: 目标消息内容 + + Returns: + str: 关键词反应提示字符串 + """ + # 关键词检测与反应 + keywords_reaction_prompt = "" + try: + # 添加None检查,防止NoneType错误 + if target is None: + return keywords_reaction_prompt + + # 处理关键词规则 + for rule in global_config.keyword_reaction.keyword_rules: + if any(keyword in target for keyword in rule.keywords): + logger.info(f"检测到关键词规则:{rule.keywords},触发反应:{rule.reaction}") + keywords_reaction_prompt += f"{rule.reaction}," + + # 处理正则表达式规则 + for rule in global_config.keyword_reaction.regex_rules: + for pattern_str in rule.regex: + try: + pattern = re.compile(pattern_str) + if result := pattern.search(target): + reaction = rule.reaction + for name, content in result.groupdict().items(): + reaction = reaction.replace(f"[{name}]", content) + logger.info(f"匹配到正则表达式:{pattern_str},触发反应:{reaction}") + keywords_reaction_prompt += f"{reaction}," + break + except re.error as e: + logger.error(f"正则表达式编译错误: {pattern_str}, 错误信息: {str(e)}") + continue + except Exception as e: + logger.error(f"关键词检测与反应时发生异常: {str(e)}", exc_info=True) + + return keywords_reaction_prompt + + async def _time_and_run_task(self, coroutine, name: str) -> Tuple[str, Any, float]: + """计时并运行异步任务的辅助函数 + + Args: + coroutine: 要执行的协程 + name: 任务名称 + + Returns: + Tuple[str, Any, float]: (任务名称, 任务结果, 执行耗时) + """ + start_time = time.time() + result = await coroutine + end_time = time.time() + duration = end_time - start_time + return name, result, duration + + def build_s4u_chat_history_prompts( + self, message_list_before_now: List[DatabaseMessages], target_user_id: str, sender: str + ) -> Tuple[str, str]: + """ + 构建 s4u 风格的分离对话 prompt + + Args: + message_list_before_now: 历史消息列表 + target_user_id: 目标用户ID(当前对话对象) + + Returns: + Tuple[str, str]: (核心对话prompt, 背景对话prompt) + """ + core_dialogue_list: List[DatabaseMessages] = [] + bot_id = str(global_config.bot.qq_account) + + # 过滤消息:分离bot和目标用户的对话 vs 其他用户的对话 + for msg in message_list_before_now: + try: + msg_user_id = str(msg.user_info.user_id) + reply_to = msg.reply_to + _platform, reply_to_user_id = self._parse_reply_target(reply_to) + if (msg_user_id == bot_id and reply_to_user_id == target_user_id) or msg_user_id == target_user_id: + # bot 和目标用户的对话 + core_dialogue_list.append(msg) + except Exception as e: + logger.error(f"处理消息记录时出错: {msg}, 错误: {e}") + + # 构建核心对话 prompt + core_dialogue_prompt = "" + if core_dialogue_list: + # 检查最新五条消息中是否包含bot自己说的消息 + latest_5_messages = core_dialogue_list[-5:] if len(core_dialogue_list) >= 5 else core_dialogue_list + has_bot_message = any(str(msg.user_info.user_id) == bot_id for msg in latest_5_messages) + + # logger.info(f"最新五条消息:{latest_5_messages}") + # logger.info(f"最新五条消息中是否包含bot自己说的消息:{has_bot_message}") + + # 如果最新五条消息中不包含bot的消息,则返回空字符串 + if not has_bot_message: + core_dialogue_prompt = "" + else: + core_dialogue_list = core_dialogue_list[ + -int(global_config.chat.max_context_size * 0.6) : + ] # 限制消息数量 + + core_dialogue_prompt_str = build_readable_messages( + core_dialogue_list, + replace_bot_name=True, + timestamp_mode="normal_no_YMD", + read_mark=0.0, + truncate=True, + show_actions=True, + ) + core_dialogue_prompt = f"""-------------------------------- +这是你和{sender}的对话,你们正在交流中: +{core_dialogue_prompt_str} +-------------------------------- +""" + + + # 构建背景对话 prompt + all_dialogue_prompt = "" + if message_list_before_now: + latest_25_msgs = message_list_before_now[-int(global_config.chat.max_context_size) :] + all_dialogue_prompt_str = build_readable_messages( + latest_25_msgs, + replace_bot_name=True, + timestamp_mode="normal_no_YMD", + truncate=True, + ) + if core_dialogue_prompt: + all_dialogue_prompt = f"所有用户的发言:\n{all_dialogue_prompt_str}" + else: + all_dialogue_prompt = f"{all_dialogue_prompt_str}" + + return core_dialogue_prompt, all_dialogue_prompt + + def build_mai_think_context( + self, + chat_id: str, + memory_block: str, + relation_info: str, + time_block: str, + chat_target_1: str, + chat_target_2: str, + mood_prompt: str, + identity_block: str, + sender: str, + target: str, + chat_info: str, + ) -> Any: + """构建 mai_think 上下文信息 + + Args: + chat_id: 聊天ID + memory_block: 记忆块内容 + relation_info: 关系信息 + time_block: 时间块内容 + chat_target_1: 聊天目标1 + chat_target_2: 聊天目标2 + mood_prompt: 情绪提示 + identity_block: 身份块内容 + sender: 发送者名称 + target: 目标消息内容 + chat_info: 聊天信息 + + Returns: + Any: mai_think 实例 + """ + mai_think = mai_thinking_manager.get_mai_think(chat_id) + mai_think.memory_block = memory_block + mai_think.relation_info_block = relation_info + mai_think.time_block = time_block + mai_think.chat_target = chat_target_1 + mai_think.chat_target_2 = chat_target_2 + mai_think.chat_info = chat_info + mai_think.mood_state = mood_prompt + mai_think.identity = identity_block + mai_think.sender = sender + mai_think.target = target + return mai_think + + async def build_actions_prompt( + self, available_actions: Dict[str, ActionInfo], chosen_actions_info: Optional[List[ActionPlannerInfo]] = None + ) -> str: + """构建动作提示""" + + action_descriptions = "" + skip_names = ["emoji", "build_memory", "build_relation", "reply"] + if available_actions: + action_descriptions = "除了进行回复之外,你可以做以下这些动作,不过这些动作由另一个模型决定,:\n" + for action_name, action_info in available_actions.items(): + if action_name in skip_names: + continue + action_description = action_info.description + action_descriptions += f"- {action_name}: {action_description}\n" + action_descriptions += "\n" + + chosen_action_descriptions = "" + if chosen_actions_info: + for action_plan_info in chosen_actions_info: + action_name = action_plan_info.action_type + if action_name in skip_names: + continue + action_description: str = "无描述" + reasoning: str = "无原因" + if action := available_actions.get(action_name): + action_description = action.description or action_description + reasoning = action_plan_info.reasoning or reasoning + + chosen_action_descriptions += f"- {action_name}: {action_description},原因:{reasoning}\n" + + if chosen_action_descriptions: + action_descriptions += "根据聊天情况,另一个模型决定在回复的同时做以下这些动作:\n" + action_descriptions += chosen_action_descriptions + + return action_descriptions + + async def build_personality_prompt(self) -> str: + bot_name = global_config.bot.nickname + if global_config.bot.alias_names: + bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" + else: + bot_nickname = "" + + prompt_personality = f"{global_config.personality.personality};" + return f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" + + async def build_prompt_reply_context( + self, + reply_message: Optional[DatabaseMessages] = None, + extra_info: str = "", + reply_reason: str = "", + available_actions: Optional[Dict[str, ActionInfo]] = None, + chosen_actions: Optional[List[ActionPlannerInfo]] = None, + enable_tool: bool = True, + ) -> Tuple[str, List[int]]: + """ + 构建回复器上下文 + + Args: + extra_info: 额外信息,用于补充上下文 + reply_reason: 回复原因 + available_actions: 可用动作 + chosen_actions: 已选动作 + enable_timeout: 是否启用超时处理 + enable_tool: 是否启用工具调用 + reply_message: 回复的原始消息 + Returns: + str: 构建好的上下文 + """ + if available_actions is None: + available_actions = {} + chat_stream = self.chat_stream + chat_id = chat_stream.stream_id + is_group_chat = bool(chat_stream.group_info) + platform = chat_stream.platform + + user_id = "用户ID" + person_name = "用户" + sender = "用户" + target = "消息" + + if reply_message: + user_id = reply_message.user_info.user_id + person = Person(platform=platform, user_id=user_id) + person_name = person.person_name or user_id + sender = person_name + target = reply_message.processed_plain_text + + mood_prompt: str = "" + if global_config.mood.enable_mood: + chat_mood = mood_manager.get_mood_by_chat_id(chat_id) + mood_prompt = chat_mood.mood_state + + target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) + target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) + + message_list_before_now_long = get_raw_msg_before_timestamp_with_chat( + chat_id=chat_id, + timestamp=time.time(), + limit=global_config.chat.max_context_size * 1, + ) + + message_list_before_short = get_raw_msg_before_timestamp_with_chat( + chat_id=chat_id, + timestamp=time.time(), + limit=int(global_config.chat.max_context_size * 0.33), + ) + + person_list_short: List[Person] = [] + for msg in message_list_before_short: + if ( + global_config.bot.qq_account == msg.user_info.user_id + and global_config.bot.platform == msg.user_info.platform + ): + continue + if ( + reply_message + and reply_message.user_info.user_id == msg.user_info.user_id + and reply_message.user_info.platform == msg.user_info.platform + ): + continue + person = Person(platform=msg.user_info.platform, user_id=msg.user_info.user_id) + if person.is_known: + person_list_short.append(person) + + for person in person_list_short: + print(person.person_name) + + chat_talking_prompt_short = build_readable_messages( + message_list_before_short, + replace_bot_name=True, + timestamp_mode="relative", + read_mark=0.0, + show_actions=True, + ) + + # 并行执行五个构建任务 + task_results = await asyncio.gather( + self._time_and_run_task( + self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits" + ), + self._time_and_run_task( + self.build_relation_info(chat_talking_prompt_short, sender), "relation_info" + ), + # self._time_and_run_task(self.build_memory_block(message_list_before_short, target), "memory_block"), + self._time_and_run_task( + self.build_tool_info(chat_talking_prompt_short, sender, target, enable_tool=enable_tool), "tool_info" + ), + self._time_and_run_task(self.get_prompt_info(chat_talking_prompt_short, sender, target), "prompt_info"), + self._time_and_run_task(self.build_actions_prompt(available_actions, chosen_actions), "actions_info"), + self._time_and_run_task(self.build_personality_prompt(), "personality_prompt"), + ) + + # 任务名称中英文映射 + task_name_mapping = { + "expression_habits": "选取表达方式", + "relation_info": "感受关系", + # "memory_block": "回忆", + "tool_info": "使用工具", + "prompt_info": "获取知识", + "actions_info": "动作信息", + "personality_prompt": "人格信息", + } + + # 处理结果 + timing_logs = [] + results_dict = {} + + almost_zero_str = "" + for name, result, duration in task_results: + results_dict[name] = result + chinese_name = task_name_mapping.get(name, name) + if duration < 0.1: + almost_zero_str += f"{chinese_name}," + continue + + timing_logs.append(f"{chinese_name}: {duration:.1f}s") + if duration > 8: + logger.warning(f"回复生成前信息获取耗时过长: {chinese_name} 耗时: {duration:.1f}s,请使用更快的模型") + logger.info(f"回复准备: {'; '.join(timing_logs)}; {almost_zero_str} <0.1s") + + expression_habits_block, selected_expressions = results_dict["expression_habits"] + expression_habits_block: str + selected_expressions: List[int] + relation_info: str = results_dict["relation_info"] + # memory_block: str = results_dict["memory_block"] + tool_info: str = results_dict["tool_info"] + prompt_info: str = results_dict["prompt_info"] # 直接使用格式化后的结果 + actions_info: str = results_dict["actions_info"] + personality_prompt: str = results_dict["personality_prompt"] + keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) + + if extra_info: + extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策" + else: + extra_info_block = "" + + time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。" + + reply_target_block = ( + f"现在对方说的:{target}。引起了你的注意" + ) + + + # 构建分离的对话 prompt + core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts( + message_list_before_now_long, user_id, sender + ) + + if global_config.bot.qq_account == user_id and platform == global_config.bot.platform: + return await global_prompt_manager.format_prompt( + "private_replyer_self_prompt", + expression_habits_block=expression_habits_block, + tool_info_block=tool_info, + knowledge_prompt=prompt_info, + # memory_block=memory_block, + relation_info_block=relation_info, + extra_info_block=extra_info_block, + identity=personality_prompt, + action_descriptions=actions_info, + mood_state=mood_prompt, + background_dialogue_prompt=background_dialogue_prompt, + time_block=time_block, + target=target, + reason=reply_reason, + sender = sender, + reply_style=global_config.personality.reply_style, + keywords_reaction_prompt=keywords_reaction_prompt, + moderation_prompt=moderation_prompt_block, + ), selected_expressions + else: + return await global_prompt_manager.format_prompt( + "replyer_prompt", + expression_habits_block=expression_habits_block, + tool_info_block=tool_info, + knowledge_prompt=prompt_info, + # memory_block=memory_block, + relation_info_block=relation_info, + extra_info_block=extra_info_block, + identity=personality_prompt, + action_descriptions=actions_info, + sender_name=sender, + mood_state=mood_prompt, + dialogue_prompt=background_dialogue_prompt, + time_block=time_block, + sender = sender, + reply_target_block=reply_target_block, + reply_style=global_config.personality.reply_style, + keywords_reaction_prompt=keywords_reaction_prompt, + moderation_prompt=moderation_prompt_block, + ), selected_expressions + + async def build_prompt_rewrite_context( + self, + raw_reply: str, + reason: str, + reply_to: str, + ) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if + chat_stream = self.chat_stream + chat_id = chat_stream.stream_id + is_group_chat = bool(chat_stream.group_info) + + sender, target = self._parse_reply_target(reply_to) + target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) + target = re.sub(r"\\[picid:[^\\]]+\\]", "[图片]", target) + + # 添加情绪状态获取 + if global_config.mood.enable_mood: + chat_mood = mood_manager.get_mood_by_chat_id(chat_id) + mood_prompt = chat_mood.mood_state + else: + mood_prompt = "" + + message_list_before_now_half = get_raw_msg_before_timestamp_with_chat( + chat_id=chat_id, + timestamp=time.time(), + limit=min(int(global_config.chat.max_context_size * 0.33), 15), + ) + chat_talking_prompt_half = build_readable_messages( + message_list_before_now_half, + replace_bot_name=True, + timestamp_mode="relative", + read_mark=0.0, + show_actions=True, + ) + + # 并行执行2个构建任务 + (expression_habits_block, _), relation_info, personality_prompt = await asyncio.gather( + self.build_expression_habits(chat_talking_prompt_half, target), + self.build_relation_info(chat_talking_prompt_half, sender), + self.build_personality_prompt(), + ) + + keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) + + time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + moderation_prompt_block = ( + "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。" + ) + + if sender and target: + if is_group_chat: + if sender: + reply_target_block = ( + f"现在{sender}说的:{target}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + elif target: + reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" + else: + reply_target_block = "现在,你想要在群里发言或者回复消息。" + else: # private chat + if sender: + reply_target_block = f"现在{sender}说的:{target}。引起了你的注意,针对这条消息回复。" + elif target: + reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。" + else: + reply_target_block = "现在,你想要回复。" + else: + reply_target_block = "" + + if is_group_chat: + 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") + else: + chat_target_name = "对方" + if self.chat_target_info: + chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" + chat_target_1 = await global_prompt_manager.format_prompt( + "chat_target_private1", sender_name=chat_target_name + ) + chat_target_2 = await global_prompt_manager.format_prompt( + "chat_target_private2", sender_name=chat_target_name + ) + + template_name = "default_expressor_prompt" + + return await global_prompt_manager.format_prompt( + template_name, + expression_habits_block=expression_habits_block, + relation_info_block=relation_info, + chat_target=chat_target_1, + time_block=time_block, + chat_info=chat_talking_prompt_half, + identity=personality_prompt, + chat_target_2=chat_target_2, + reply_target_block=reply_target_block, + raw_reply=raw_reply, + reason=reason, + mood_state=mood_prompt, # 添加情绪状态参数 + reply_style=global_config.personality.reply_style, + keywords_reaction_prompt=keywords_reaction_prompt, + moderation_prompt=moderation_prompt_block, + ) + + async def _build_single_sending_message( + self, + message_id: str, + message_segment: Seg, + reply_to: bool, + is_emoji: bool, + thinking_start_time: float, + display_message: str, + anchor_message: Optional[MessageRecv] = None, + ) -> MessageSending: + """构建单个发送消息""" + + bot_user_info = UserInfo( + user_id=global_config.bot.qq_account, + user_nickname=global_config.bot.nickname, + platform=self.chat_stream.platform, + ) + + # await anchor_message.process() + sender_info = anchor_message.message_info.user_info if anchor_message else None + + return MessageSending( + message_id=message_id, # 使用片段的唯一ID + chat_stream=self.chat_stream, + bot_user_info=bot_user_info, + sender_info=sender_info, + message_segment=message_segment, + reply=anchor_message, # 回复原始锚点 + is_head=reply_to, + is_emoji=is_emoji, + thinking_start_time=thinking_start_time, # 传递原始思考开始时间 + display_message=display_message, + ) + + async def llm_generate_content(self, prompt: str): + with Timer("LLM生成", {}): # 内部计时器,可选保留 + # 直接使用已初始化的模型实例 + logger.info(f"\n{prompt}\n") + + if global_config.debug.show_prompt: + logger.info(f"\n{prompt}\n") + else: + logger.debug(f"\n{prompt}\n") + + content, (reasoning_content, model_name, tool_calls) = await self.express_model.generate_response_async( + prompt + ) + + logger.debug(f"replyer生成内容: {content}") + return content, reasoning_content, model_name, tool_calls + + async def get_prompt_info(self, message: str, sender: str, target: str): + related_info = "" + start_time = time.time() + from src.plugins.built_in.knowledge.lpmm_get_knowledge import SearchKnowledgeFromLPMMTool + + logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}") + # 从LPMM知识库获取知识 + try: + # 检查LPMM知识库是否启用 + if not global_config.lpmm_knowledge.enable: + logger.debug("LPMM知识库未启用,跳过获取知识库内容") + return "" + time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + bot_name = global_config.bot.nickname + + prompt = await global_prompt_manager.format_prompt( + "lpmm_get_knowledge_prompt", + bot_name=bot_name, + time_now=time_now, + chat_history=message, + sender=sender, + target_message=target, + ) + _, _, _, _, tool_calls = await llm_api.generate_with_model_with_tools( + prompt, + model_config=model_config.model_task_config.tool_use, + tool_options=[SearchKnowledgeFromLPMMTool.get_tool_definition()], + ) + if tool_calls: + result = await self.tool_executor.execute_tool_call(tool_calls[0], SearchKnowledgeFromLPMMTool()) + end_time = time.time() + if not result or not result.get("content"): + logger.debug("从LPMM知识库获取知识失败,返回空知识...") + return "" + found_knowledge_from_lpmm = result.get("content", "") + logger.debug( + f"从LPMM知识库获取知识,相关信息:{found_knowledge_from_lpmm[:100]}...,信息长度: {len(found_knowledge_from_lpmm)}" + ) + related_info += found_knowledge_from_lpmm + logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}秒") + logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") + + return f"你有以下这些**知识**:\n{related_info}\n请你**记住上面的知识**,之后可能会用到。\n" + else: + logger.debug("模型认为不需要使用LPMM知识库") + return "" + except Exception as e: + logger.error(f"获取知识库内容时发生异常: {str(e)}") + return "" + + +def weighted_sample_no_replacement(items, weights, k) -> list: + """ + 加权且不放回地随机抽取k个元素。 + + 参数: + items: 待抽取的元素列表 + weights: 每个元素对应的权重(与items等长,且为正数) + k: 需要抽取的元素个数 + 返回: + selected: 按权重加权且不重复抽取的k个元素组成的列表 + + 如果 items 中的元素不足 k 个,就只会返回所有可用的元素 + + 实现思路: + 每次从当前池中按权重加权随机选出一个元素,选中后将其从池中移除,重复k次。 + 这样保证了: + 1. count越大被选中概率越高 + 2. 不会重复选中同一个元素 + """ + selected = [] + pool = list(zip(items, weights, strict=False)) + for _ in range(min(k, len(pool))): + total = sum(w for _, w in pool) + r = random.uniform(0, total) + upto = 0 + for idx, (item, weight) in enumerate(pool): + upto += weight + if upto >= r: + selected.append(item) + pool.pop(idx) + break + return selected + + + diff --git a/src/chat/replyer/lpmm_prompt.py b/src/chat/replyer/prompt/lpmm_prompt.py similarity index 100% rename from src/chat/replyer/lpmm_prompt.py rename to src/chat/replyer/prompt/lpmm_prompt.py diff --git a/src/chat/replyer/replyer_prompt.py b/src/chat/replyer/prompt/replyer_prompt.py similarity index 73% rename from src/chat/replyer/replyer_prompt.py rename to src/chat/replyer/prompt/replyer_prompt.py index d3de1935..8ce7a0b5 100644 --- a/src/chat/replyer/replyer_prompt.py +++ b/src/chat/replyer/prompt/replyer_prompt.py @@ -52,4 +52,24 @@ def init_replyer_prompt(): {moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 """, "replyer_self_prompt", + ) + + + + Prompt( +"""{knowledge_prompt}{relation_info_block}{tool_info_block}{extra_info_block} +{expression_habits_block} + +你正在和{sender_name}聊天,这是你们之前聊的内容: +{time_block} +{dialogue_prompt} + +{reply_target_block}。 +{identity} +你正在和{sender_name}聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些, +尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。 +{reply_style} +请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 +{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", + "private_replyer_prompt", ) \ No newline at end of file diff --git a/src/chat/replyer/rewrite_prompt.py b/src/chat/replyer/prompt/rewrite_prompt.py similarity index 100% rename from src/chat/replyer/rewrite_prompt.py rename to src/chat/replyer/prompt/rewrite_prompt.py diff --git a/src/chat/replyer/replyer_manager.py b/src/chat/replyer/replyer_manager.py index 2f64ab07..b5e7350d 100644 --- a/src/chat/replyer/replyer_manager.py +++ b/src/chat/replyer/replyer_manager.py @@ -3,20 +3,21 @@ from typing import Dict, Optional from src.common.logger import get_logger from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.chat.replyer.default_generator import DefaultReplyer +from src.chat.replyer.private_generator import PrivateReplyer logger = get_logger("ReplyerManager") class ReplyerManager: def __init__(self): - self._repliers: Dict[str, DefaultReplyer] = {} + self._repliers: Dict[str, DefaultReplyer | PrivateReplyer] = {} def get_replyer( self, chat_stream: Optional[ChatStream] = None, chat_id: Optional[str] = None, request_type: str = "replyer", - ) -> Optional[DefaultReplyer]: + ) -> Optional[DefaultReplyer | PrivateReplyer]: """ 获取或创建回复器实例。 @@ -46,10 +47,17 @@ class ReplyerManager: return None # model_configs 只在此时(初始化时)生效 - replyer = DefaultReplyer( - chat_stream=target_stream, - request_type=request_type, - ) + if target_stream.group_info: + replyer = DefaultReplyer( + chat_stream=target_stream, + request_type=request_type, + ) + else: + replyer = PrivateReplyer( + chat_stream=target_stream, + request_type=request_type, + ) + self._repliers[stream_id] = replyer return replyer