Files
Mofox-Core/src/chat/replyer/default_generator.py
LuiKlee 8f77465bc3 ruff
2025-12-13 16:39:25 +08:00

2140 lines
92 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
默认回复生成器 - 集成统一Prompt系统
使用重构后的统一Prompt系统替换原有的复杂提示词构建逻辑
"""
import asyncio
import random
import re
import time
import traceback
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, Literal
from src.chat.express.expression_selector import expression_selector
from src.chat.message_receive.uni_message_sender import HeartFCSender
from src.chat.utils.chat_message_builder import (
build_readable_messages,
get_raw_msg_before_timestamp_with_chat,
replace_user_references_async,
)
# 导入新的统一Prompt系统
from src.chat.utils.prompt import Prompt, global_prompt_manager
from src.chat.utils.prompt_params import PromptParameters
from src.chat.utils.timer_calculator import Timer
from src.chat.utils.utils import get_chat_type_and_target_info
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.logger import get_logger
from src.config.config import global_config, model_config
from src.individuality.individuality import get_individuality
from src.llm_models.utils_model import LLMRequest
from src.mood.mood_manager import mood_manager
from src.person_info.person_info import get_person_info_manager
from src.plugin_system.apis import llm_api
from src.plugin_system.apis.permission_api import permission_api
from src.plugin_system.base.component_types import ActionInfo, EventType
if TYPE_CHECKING:
from src.chat.message_receive.chat_stream import ChatStream
logger = get_logger("replyer")
# 用于存储后台任务的集合,防止被垃圾回收
_background_tasks: set[asyncio.Task] = set()
def init_prompt():
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
Prompt("在群里聊天", "chat_target_group2")
Prompt("{sender_name}聊天", "chat_target_private2")
Prompt(
"""
{expression_habits_block}
{relation_info_block}
{chat_target}
{time_block}
{chat_info}
{identity}
{auth_role_prompt_block}
你正在{chat_target_2},{reply_target_block}
对这条消息,你想表达,原句:{raw_reply},原因是:{reason}。你现在要思考怎么组织回复
你现在的心情是:{mood_state}
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯
{reply_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
{keywords_reaction_prompt}
{moderation_prompt}
不要复读你前面发过的内容,意思相近也不行。
*你叫{bot_name},也有人叫你{bot_nickname}*
现在,你说:
""",
"default_expressor_prompt",
)
# s4u 风格的 prompt 模板
Prompt(
"""
# 人设:{identity}
## 当前状态
- 你现在的心情是:{mood_state}
- {schedule_block}
## 历史记录
{read_history_prompt}
{cross_context_block}
{unread_history_prompt}
{notice_block}
## 表达方式
- *你需要参考你的回复风格:*
{reply_style}
{keywords_reaction_prompt}
{expression_habits_block}
{tool_info_block}
{knowledge_prompt}
## 其他信息
{memory_block}
{relation_info_block}
{extra_info_block}
{auth_role_prompt_block}
{action_descriptions}
## 任务
*{chat_scene}*
### 核心任务
- 你现在的主要任务是和 {sender_name} 聊天。同时,也有其他用户会参与聊天,你可以参考他们的回复内容,但是你现在想回复{sender_name}的发言。
- {reply_target_block} 你需要生成一段紧密相关且与历史消息相关的回复。
## 规则
{safety_guidelines_block}
{group_chat_reminder_block}
- 在称呼用户时,请使用更自然的昵称或简称。对于长英文名,可使用首字母缩写;对于中文名,可提炼合适的简称。禁止直接复述复杂的用户名或输出用户名中的任何符号,让称呼更像人类习惯,注意,简称不是必须的,合理的使用。
你的回复应该是一条简短、且口语化的回复。
--------------------------------
{time_block}
请注意不要输出多余内容(包括前后缀,冒号和引号,系统格式化文字)。只输出回复内容。
不要模仿任何系统消息的格式,你的回复应该是自然的对话内容,例如:
- 当你想要打招呼时,直接输出“你好!”而不是“[回复<xxx>] 用户你好!”
- 当你想要提及某人时,直接叫对方名字,而不是“@xxx”
你只能输出文字,不能输出任何表情包、图片、文件等内容!如果用户要求你发送非文字内容,请输出"PASS",而不是[表情包:xxx]
{moderation_prompt}
*你叫{bot_name},也有人叫你{bot_nickname},请你清楚你的身份,分清对方到底有没有叫你*
现在,你说:
""",
"s4u_style_prompt",
)
Prompt(
"""
你是一个专门获取知识的助手。你的名字是{bot_name}。现在是{time_now}
群里正在进行的聊天内容:
{chat_history}
现在,{sender}发送了内容:{target_message},你想要回复ta。
请仔细分析聊天内容,考虑以下几点:
1. 内容中是否包含需要查询信息的问题
2. 是否有明确的知识获取指令
If you need to use the search tool, please directly call the function "lpmm_search_knowledge". If you do not need to use any tool, simply output "No tool needed".
""",
name="lpmm_get_knowledge_prompt",
)
# normal 版 prompt 模板(参考 s4u 格式,用于统一回应未读消息)
logger.debug("[Prompt模式调试] 正在注册normal_style_prompt模板")
Prompt(
"""
# 人设:{identity}
## 当前状态
- 你现在的心情是:{mood_state}
{schedule_block}
## 历史记录
{read_history_prompt}
{cross_context_block}
{unread_history_prompt}
{notice_block}
## 表达方式
- *你需要参考你的回复风格:*
{reply_style}
{keywords_reaction_prompt}
{expression_habits_block}
{tool_info_block}
{knowledge_prompt}
## 其他信息
{memory_block}
{relation_info_block}
{extra_info_block}
{auth_role_prompt_block}
{action_descriptions}
## 任务
*{chat_scene}*
### 核心任务
- 你需要对以上未读历史消息用一句简单的话统一回应。这些消息可能来自不同的参与者,你需要理解整体对话动态,生成一段自然、连贯的回复。
## 规则
{safety_guidelines_block}
{group_chat_reminder_block}
- 在称呼用户时,请使用更自然的昵称或简称。对于长英文名,可使用首字母缩写;对于中文名,可提炼合适的简称。禁止直接复述复杂的用户名或输出用户名中的任何符号,让称呼更像人类习惯,注意,简称不是必须的,合理的使用。
你的回复应该是一条简短、且口语化的回复。
--------------------------------
{time_block}
请注意不要输出多余内容(包括前后缀,冒号和引号,系统格式化文字)。只输出回复内容。
不要模仿任何系统消息的格式,你的回复应该是自然的对话内容,例如:
- 当你想要打招呼时,直接输出“你好!”而不是“[回复<xxx>] 用户你好!”
- 当你想要提及某人时,直接叫对方名字,而不是“@xxx”
你只能输出文字,不能输出任何表情包、图片、文件等内容!如果用户要求你发送非文字内容,请输出"PASS",而不是[表情包:xxx]
{moderation_prompt}
*你叫{bot_name},也有人叫你{bot_nickname},请你清楚你的身份,分清对方到底有没有叫你*
现在,你说:
""",
"normal_style_prompt",
)
logger.debug("[Prompt模式调试] normal_style_prompt模板注册完成")
class DefaultReplyer:
def __init__(
self,
chat_stream: "ChatStream",
request_type: str = "replyer",
):
assert global_config is not None
assert model_config is not None
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 = False
self.chat_target_info = None
self._chat_info_initialized = False
self.heart_fc_sender = HeartFCSender()
self._chat_info_initialized = False
async def _initialize_chat_info(self):
"""异步初始化聊天信息"""
if not self._chat_info_initialized:
self.is_group_chat, self.chat_target_info = await get_chat_type_and_target_info(self.chat_stream.stream_id)
self._chat_info_initialized = True
# self.memory_activator = EnhancedMemoryActivator()
self.memory_activator = None # 暂时禁用记忆激活器
# 旧的即时记忆系统已被移除,现在使用增强记忆系统
# self.instant_memory = VectorInstantMemoryV2(chat_id=self.chat_stream.stream_id, retention_hours=1)
from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor不然会循环依赖
self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id)
async def _build_auth_role_prompt(self) -> str:
"""根据主人配置生成额外提示词"""
assert global_config is not None
master_config = global_config.permission.master_prompt
if not master_config or not master_config.enable:
return ""
if not self.chat_stream.user_info:
return ""
platform, user_id = self.chat_stream.platform, self.chat_stream.user_info.user_id
try:
if user_id:
is_master = await permission_api.is_master(platform, user_id)
hint = master_config.master_hint if is_master else master_config.non_master_hint
return hint.strip()
else:
logger.info("无法获得id")
return ""
except Exception as e:
logger.warning(f"检测主人身份失败: {e}")
return ""
async def generate_reply_with_context(
self,
reply_to: str = "",
extra_info: str = "",
available_actions: dict[str, ActionInfo] | None = None,
enable_tool: bool = True,
from_plugin: bool = True,
stream_id: str | None = None,
reply_message: DatabaseMessages | None = None,
) -> tuple[bool, dict[str, Any] | None, str | None]:
# sourcery skip: merge-nested-ifs
"""
回复器 (Replier): 负责生成回复文本的核心逻辑。
Args:
reply_to: 回复对象,格式为 "发送者:消息内容"
extra_info: 额外信息,用于补充上下文
available_actions: 可用的动作信息字典
enable_tool: 是否启用工具调用
from_plugin: 是否来自插件
Returns:
Tuple[bool, Optional[Dict[str, Any]], Optional[str]]: (是否成功, 生成的回复, 使用的prompt)
"""
# 安全检测:在生成回复前检测消息
if reply_message:
from src.chat.security import get_security_manager
security_manager = get_security_manager()
message_text = reply_message.processed_plain_text or ""
# 执行安全检测
security_result = await security_manager.check_message(
message=message_text,
context={
"stream_id": stream_id or self.chat_stream.stream_id,
"user_id": getattr(reply_message, "user_id", ""),
"platform": getattr(reply_message, "platform", ""),
"message_id": getattr(reply_message, "message_id", ""),
},
mode="sequential", # 快速失败模式
)
# 如果检测到风险,记录并可能拒绝处理
if not security_result.is_safe:
logger.warning(
f"[安全检测] 检测到风险消息 (级别: {security_result.level.value}, "
f"置信度: {security_result.confidence:.2f}): {security_result.reason}"
)
# 根据安全动作决定是否继续
from src.chat.security.interfaces import SecurityAction
if security_result.action == SecurityAction.BLOCK:
logger.warning("[安全检测] 消息被拦截,拒绝生成回复")
return False, None, None
# SHIELD 模式:修改消息内容但继续处理
# MONITOR 模式:仅记录,继续正常处理
# 初始化聊天信息
await self._initialize_chat_info()
# 子任务跟踪 - 用于取消管理
child_tasks = set()
prompt = None
if available_actions is None:
available_actions = {}
llm_response = None
try:
# 从available_actions中提取prompt_mode由action_manager传递
# 如果没有指定默认使用s4u模式
prompt_mode_value: Any = "s4u"
if available_actions and "_prompt_mode" in available_actions:
mode = available_actions.get("_prompt_mode", "s4u")
# 确保类型安全
if isinstance(mode, str):
prompt_mode_value = mode
# 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_reply_context(
reply_to=reply_to,
extra_info=extra_info,
available_actions=available_actions,
enable_tool=enable_tool,
reply_message=reply_message,
prompt_mode=prompt_mode_value, # 传递prompt_mode
)
if not prompt:
logger.warning("构建prompt失败跳过回复生成")
return False, None, None
from src.plugin_system.core.event_manager import event_manager
# 触发 POST_LLM 事件(请求 LLM 之前)
if not from_plugin:
result = await event_manager.trigger_event(
EventType.POST_LLM, permission_group="SYSTEM", prompt=prompt, stream_id=stream_id
)
if result and not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于请求前中断了内容生成")
# 4. 调用 LLM 生成回复
content = None
reasoning_content = None
model_name = "unknown_model"
try:
# 设置正在回复的状态
self.chat_stream.context.is_replying = True
content, reasoning_content, model_name, tool_call = await self.llm_generate_content(prompt)
logger.debug(f"replyer生成内容: {content}")
llm_response = {
"content": content,
"reasoning": reasoning_content,
"model": model_name,
"tool_calls": tool_call,
}
except UserWarning as e:
raise e
except Exception as llm_e:
# 精简报错信息
logger.error(f"LLM 生成失败: {llm_e}")
return False, None, prompt # LLM 调用失败则无法生成回复
finally:
# 重置正在回复的状态
self.chat_stream.context.is_replying = False
# 触发 AFTER_LLM 事件
if not from_plugin:
result = await event_manager.trigger_event(
EventType.AFTER_LLM,
permission_group="SYSTEM",
prompt=prompt,
llm_response=llm_response,
stream_id=stream_id,
)
if result and not result.all_continue_process():
raise UserWarning(
f"插件{result.get_summary().get('stopped_handlers', '')}于请求后取消了内容生成"
)
# 旧的自动记忆存储已移除,现在使用记忆图系统通过工具创建记忆
# 记忆由LLM在对话过程中通过CreateMemoryTool主动创建而非自动存储
pass
return True, llm_response, prompt
except asyncio.CancelledError:
logger.info(f"回复生成被取消: {self.chat_stream.stream_id}")
# 取消所有子任务
for child_task in child_tasks:
if not child_task.done():
child_task.cancel()
raise
except UserWarning as uw:
raise uw
except Exception as e:
logger.error(f"回复生成意外失败: {e}")
traceback.print_exc()
# 异常时也要清理子任务
for child_task in child_tasks:
if not child_task.done():
child_task.cancel()
return False, None, prompt
async def rewrite_reply_with_context(
self,
raw_reply: str = "",
reason: str = "",
reply_to: str = "",
return_prompt: bool = False,
) -> tuple[bool, str | None, str | None]:
"""
表达器 (Expressor): 负责重写和优化回复文本。
Args:
raw_reply: 原始回复内容
reason: 回复原因
reply_to: 回复对象,格式为 "发送者:消息内容"
relation_info: 关系信息
Returns:
Tuple[bool, Optional[str]]: (是否成功, 重写后的回复内容)
"""
prompt = None
try:
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_rewrite_context(
raw_reply=raw_reply,
reason=reason,
reply_to=reply_to,
)
content = None
if not prompt:
logger.error("Prompt 构建失败,无法生成回复。")
return False, None, None
try:
content, _reasoning_content, _model_name, _ = await self.llm_generate_content(prompt)
logger.info(f"想要表达:{raw_reply}||理由:{reason}||生成回复: {content}\n")
except Exception as llm_e:
# 精简报错信息
logger.error(f"LLM 生成失败: {llm_e}")
return False, None, prompt if return_prompt else None # LLM 调用失败则无法生成回复
return True, content, prompt if return_prompt else None
except Exception as e:
logger.error(f"回复生成意外失败: {e}")
traceback.print_exc()
return False, None, prompt if return_prompt else None
async def build_expression_habits(self, chat_history: str, target: str) -> str:
"""构建表达习惯块
Args:
chat_history: 聊天历史记录
target: 目标消息内容
Returns:
str: 表达习惯信息字符串
"""
assert global_config is not None
# 检查是否允许在此聊天流中使用表达
use_expression, _, _ = global_config.expression.get_expression_config_for_chat(self.chat_stream.stream_id)
if not use_expression:
return ""
style_habits = []
grammar_habits = []
# 使用统一的表达方式选择入口支持classic和exp_model模式
selected_expressions = await expression_selector.select_suitable_expressions(
chat_id=self.chat_stream.stream_id,
chat_history=chat_history,
target_message=target,
max_num=8,
min_num=2
)
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:
expr_type = expr.get("type", "style")
if expr_type == "grammar":
grammar_habits.append(f"{expr['situation']}时,使用 {expr['style']}")
else:
style_habits.append(f"{expr['situation']}时,使用 {expr['style']}")
else:
logger.debug("没有从处理器获得表达方式,将使用空的表达方式")
# 不再在replyer中进行随机选择全部交给处理器处理
style_habits_str = "\n".join(style_habits)
grammar_habits_str = "\n".join(grammar_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"
if grammar_habits_str.strip():
expression_habits_title = (
"你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:"
)
expression_habits_block += f"{grammar_habits_str}\n"
if style_habits_str.strip() and grammar_habits_str.strip():
expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中。"
return f"{expression_habits_title}\n{expression_habits_block}"
async def build_memory_block(
self,
chat_history: str,
target: str,
recent_messages: list[dict[str, Any]] | None = None,
) -> str:
"""构建记忆块(使用三层记忆系统)
Args:
chat_history: 聊天历史记录
target: 目标消息内容
recent_messages: 原始聊天消息列表(用于构建查询块)
Returns:
str: 记忆信息字符串
"""
assert global_config is not None
# 检查是否启用三层记忆系统
if not (global_config.memory and global_config.memory.enable):
return ""
try:
from src.memory_graph.manager_singleton import (
ensure_unified_memory_manager_initialized,
)
from src.memory_graph.utils.three_tier_formatter import memory_formatter
unified_manager = await ensure_unified_memory_manager_initialized()
if not unified_manager:
logger.debug("[三层记忆] 管理器初始化失败或未启用")
return ""
# 目标查询改为使用最近多条消息的组合块
query_text = self._build_memory_query_text(target, recent_messages)
# 使用统一管理器的智能检索Judge模型决策
search_result = await unified_manager.search_memories(
query_text=query_text,
use_judge=global_config.memory.use_judge,
recent_chat_history=chat_history, # 传递最近聊天历史
)
if not search_result:
logger.debug("[三层记忆] 未找到相关记忆")
return ""
# 分类记忆块
perceptual_blocks = search_result.get("perceptual_blocks", [])
short_term_memories = search_result.get("short_term_memories", [])
long_term_memories = search_result.get("long_term_memories", [])
# 使用新的三级记忆格式化器
formatted_memories = await memory_formatter.format_all_tiers(
perceptual_blocks=perceptual_blocks,
short_term_memories=short_term_memories,
long_term_memories=long_term_memories
)
total_count = len(perceptual_blocks) + len(short_term_memories) + len(long_term_memories)
if total_count > 0:
logger.info(
f"[三层记忆] 检索到 {total_count} 条记忆 "
f"(感知:{len(perceptual_blocks)}, 短期:{len(short_term_memories)}, 长期:{len(long_term_memories)})"
)
# 添加标题并返回格式化后的记忆
if formatted_memories.strip():
return "### 🧠 相关记忆 (Relevant Memories)\n\n" + formatted_memories
return ""
except Exception as e:
logger.error(f"[三层记忆] 检索失败: {e}")
return ""
def _build_memory_query_text(
self,
fallback_text: str,
recent_messages: list[dict[str, Any]] | None,
block_size: int = 5,
) -> str:
"""
将最近若干条消息拼接为一个查询块,用于生成语义向量。
Args:
fallback_text: 如果无法拼接消息块时使用的后备文本
recent_messages: 最近的消息列表
block_size: 组合的消息数量
Returns:
str: 用于检索的查询文本
"""
if not recent_messages:
return fallback_text
lines: list[str] = []
for message in recent_messages[-block_size:]:
sender = (
message.get("sender_name")
or message.get("person_name")
or message.get("user_nickname")
or message.get("user_cardname")
or message.get("nickname")
or message.get("sender")
)
if not sender and isinstance(message.get("user_info"), dict):
user_info = message["user_info"]
sender = user_info.get("user_nickname") or user_info.get("user_cardname")
sender = sender or message.get("user_id") or "未知"
content = (
message.get("processed_plain_text")
or message.get("display_message")
or message.get("content")
or message.get("message")
or message.get("text")
or ""
)
content = str(content).strip()
if content:
lines.append(f"{sender}: {content}")
fallback_clean = fallback_text.strip()
if not lines:
return fallback_clean or fallback_text
return "\n".join(lines[-block_size:])
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_history_str = self.tool_executor.history_manager.format_for_prompt(max_records=3, include_results=True)
# 然后执行工具调用
tool_results, _, _ = await self.tool_executor.execute_from_chat_message(
sender=sender, target_message=target, chat_history=chat_history, return_details=False
)
info_parts = []
# 显示之前的工具调用历史(不包括当前这次调用)
if tool_history_str:
info_parts.append(tool_history_str)
# 显示当前工具调用的结果(简要信息)
if tool_results:
current_results_parts = ["## 🔧 刚获取的工具信息"]
for tool_result in tool_results:
tool_name = tool_result.get("tool_name", "unknown")
content = tool_result.get("content", "")
tool_result.get("type", "tool_result")
# 不进行截断,让工具自己处理结果长度
current_results_parts.append(f"- **{tool_name}**: {content}")
info_parts.append("\n".join(current_results_parts))
logger.info(f"获取到 {len(tool_results)} 个工具结果")
# 如果没有任何信息,返回空字符串
if not info_parts:
logger.debug("未获取到任何工具结果或历史记录")
return ""
return "\n\n".join(info_parts)
except Exception as e:
logger.error(f"工具信息获取失败: {e}")
return ""
def _parse_reply_target(self, target_message: str) -> tuple[str, str]:
"""解析回复目标消息 - 使用共享工具"""
from src.chat.utils.prompt import Prompt
if target_message is None:
logger.warning("target_message为None返回默认值")
return "未知用户", "(无消息内容)"
return Prompt.parse_reply_target(target_message)
async def build_keywords_reaction_prompt(self, target: str | None) -> str:
"""构建关键词反应提示
该方法根据配置的关键词和正则表达式规则,
检查目标消息内容是否触发了任何反应。
如果匹配成功,它会生成一个包含所有触发反应的提示字符串,
用于指导LLM的回复。
Args:
target: 目标消息内容
Returns:
str: 关键词反应提示字符串,如果没有触发任何反应则为空字符串
"""
assert global_config is not None
if target is None:
return ""
reaction_prompt = ""
try:
current_chat_stream_id_str = self.chat_stream.get_raw_id()
# 2. 筛选适用的规则(全局规则 + 特定于当前聊天的规则)
applicable_rules = []
for rule in global_config.reaction.rules:
if rule.chat_stream_id == "" or rule.chat_stream_id == current_chat_stream_id_str:
applicable_rules.append(rule) # noqa: PERF401
# 3. 遍历适用规则并执行匹配
for rule in applicable_rules:
matched = False
if rule.rule_type == "keyword":
if any(keyword in target for keyword in rule.patterns):
logger.info(f"检测到关键词规则:{rule.patterns},触发反应:{rule.reaction}")
reaction_prompt += f"{rule.reaction}"
matched = True
elif rule.rule_type == "regex":
for pattern_str in rule.patterns:
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}")
reaction_prompt += f"{reaction}"
matched = True
break # 一个正则规则里只要有一个 pattern 匹配成功即可
except re.error as e:
logger.error(f"正则表达式编译错误: {pattern_str}, 错误信息: {e!s}")
continue
if matched:
# 如果需要每条消息只触发一个反应规则,可以在这里 break
pass
except Exception as e:
logger.error(f"关键词检测与反应时发生异常: {e!s}")
return reaction_prompt
async def build_notice_block(self, chat_id: str) -> str:
"""构建notice信息块
使用全局notice管理器获取notice消息并格式化展示
Args:
chat_id: 聊天ID即stream_id
Returns:
str: 格式化的notice信息文本如果没有notice或未启用则返回空字符串
"""
assert global_config is not None
try:
logger.debug(f"开始构建notice块chat_id={chat_id}")
# 检查是否启用notice in prompt
if not hasattr(global_config, "notice"):
logger.debug("notice配置不存在")
return ""
if not global_config.notice.notice_in_prompt:
logger.debug("notice_in_prompt配置未启用")
return ""
# 使用全局notice管理器获取notice文本
from src.chat.message_manager.message_manager import message_manager
limit = getattr(global_config.notice, "notice_prompt_limit", 5)
logger.debug(f"获取notice文本limit={limit}")
notice_text = message_manager.get_notice_text(chat_id, limit)
if notice_text and notice_text.strip():
# 添加标题和格式化
notice_lines = []
notice_lines.append("## 📢 最近的系统通知")
notice_lines.append(notice_text)
notice_lines.append("")
result = "\n".join(notice_lines)
return result
else:
logger.debug(f"没有可用的notice文本chat_id={chat_id}")
return ""
except Exception as e:
logger.error(f"构建notice块失败chat_id={chat_id}: {e}")
return ""
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
async def build_s4u_chat_history_prompts(
self, message_list_before_now: list[dict[str, Any]], target_user_id: str, sender: str, chat_id: str
) -> tuple[str, str]:
"""
构建 s4u 风格的已读/未读历史消息 prompt
Args:
message_list_before_now: 历史消息列表
target_user_id: 目标用户ID当前对话对象
sender: 发送者名称
chat_id: 聊天ID
Returns:
Tuple[str, str]: (已读历史消息prompt, 未读历史消息prompt)
"""
assert global_config is not None
try:
# 从message_manager获取真实的已读/未读消息
# 获取聊天流的上下文
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream = await chat_manager.get_stream(chat_id)
if chat_stream:
stream_context = chat_stream.context
# 确保历史消息已从数据库加载
await stream_context.ensure_history_initialized()
# 直接使用内存中的已读和未读消息,无需再查询数据库
read_messages = stream_context.history_messages # 已读消息(已从数据库加载)
unread_messages = stream_context.get_unread_messages() # 未读消息
# 构建已读历史消息 prompt
read_history_prompt = ""
if read_messages:
# 将 DatabaseMessages 对象转换为字典格式,以便使用 build_readable_messages
read_messages_dicts = [msg.flatten() for msg in read_messages]
# 按时间排序并限制数量
sorted_messages = sorted(read_messages_dicts, key=lambda x: x.get("time", 0))
final_history = sorted_messages[-global_config.chat.max_context_size:] # 使用配置的上下文长度
read_content = await build_readable_messages(
final_history,
replace_bot_name=True,
timestamp_mode="normal_no_YMD",
truncate=True,
)
read_history_prompt = f"这是已读历史消息,仅作为当前聊天情景的参考:\n{read_content}"
logger.debug(f"使用内存中的 {len(final_history)} 条历史消息构建prompt")
else:
read_history_prompt = "暂无已读历史消息"
logger.debug("内存中没有历史消息")
# 构建未读历史消息 prompt
unread_history_prompt = ""
if unread_messages:
unread_lines = []
for msg in unread_messages:
msg_time = time.strftime("%H:%M:%S", time.localtime(msg.time))
msg_content = msg.processed_plain_text
# 使用与已读历史消息相同的方法获取用户名
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
# 获取用户信息
user_info = getattr(msg, "user_info", {})
platform = getattr(user_info, "platform", "") or getattr(msg, "platform", "")
user_id = getattr(user_info, "user_id", "") or getattr(msg, "user_id", "")
# 获取用户名
if platform and user_id:
person_id = PersonInfoManager.get_person_id(platform, user_id)
person_info_manager = get_person_info_manager()
sender_name = await person_info_manager.get_value(person_id, "person_name") or "未知用户"
# 检查是否是机器人自己,如果是则显示为(你)
if user_id == str(global_config.bot.qq_account):
sender_name = f"{global_config.bot.nickname}(你)"
else:
sender_name = "未知用户"
# 处理消息内容中的用户引用确保bot回复在消息内容中也正确显示
from src.chat.utils.chat_message_builder import replace_user_references_async
if msg_content:
msg_content = await replace_user_references_async(
msg_content,
platform,
replace_bot_name=True
)
# 不显示兴趣度replyer只需要关注消息内容本身
unread_lines.append(f"{msg_time} {sender_name}: {msg_content}")
unread_history_prompt_str = "\n".join(unread_lines)
unread_history_prompt = f"这是未读历史消息:\n{unread_history_prompt_str}"
else:
unread_history_prompt = "暂无未读历史消息"
return f"### 📜 已读历史消息\n{read_history_prompt}", f"### 📬 未读历史消息\n{unread_history_prompt}"
else:
# 回退到传统方法
return await self._fallback_build_chat_history_prompts(message_list_before_now, target_user_id, sender)
except Exception as e:
logger.warning(f"获取已读/未读历史消息失败,使用回退方法: {e}")
return await self._fallback_build_chat_history_prompts(message_list_before_now, target_user_id, sender)
async def _fallback_build_chat_history_prompts(
self, message_list_before_now: list[dict[str, Any]], target_user_id: str, sender: str
) -> tuple[str, str]:
"""
回退的已读/未读历史消息构建方法
"""
assert global_config is not None
# 通过is_read字段分离已读和未读消息
read_messages = []
unread_messages = []
bot_id = str(global_config.bot.qq_account)
# 第一次遍历:按 is_read 字段分离
for msg_dict in message_list_before_now:
msg_user_id = str(msg_dict.get("user_id", ""))
if msg_dict.get("is_read", False):
read_messages.append(msg_dict)
else:
unread_messages.append(msg_dict)
# 如果没有is_read字段使用原有的逻辑
if not read_messages and not unread_messages:
# 使用原有的核心对话逻辑
core_dialogue_list = []
for msg_dict in message_list_before_now:
msg_user_id = str(msg_dict.get("user_id", ""))
reply_to = msg_dict.get("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:
core_dialogue_list.append(msg_dict)
read_messages = [msg for msg in message_list_before_now if msg not in core_dialogue_list]
unread_messages = core_dialogue_list
# 构建已读历史消息 prompt
read_history_prompt = ""
if read_messages:
read_content = await build_readable_messages(
read_messages[-global_config.chat.max_context_size:],
replace_bot_name=True,
timestamp_mode="normal_no_YMD",
truncate=True,
)
read_history_prompt = f"这是已读历史消息,仅作为当前聊天情景的参考:\n{read_content}"
else:
read_history_prompt = "暂无已读历史消息"
# 构建未读历史消息 prompt
unread_history_prompt = ""
if unread_messages:
unread_lines = []
for msg in unread_messages:
msg.get("message_id", "")
msg_time = time.strftime("%H:%M:%S", time.localtime(msg.get("time", time.time())))
msg_content = msg.get("processed_plain_text", "")
# 使用与已读历史消息相同的方法获取用户名
from src.person_info.person_info import PersonInfoManager, get_person_info_manager
# 获取用户信息
user_info = msg.get("user_info", {})
platform = user_info.get("platform") or msg.get("platform", "")
user_id = user_info.get("user_id") or msg.get("user_id", "")
# 获取用户名
if platform and user_id:
person_id = PersonInfoManager.get_person_id(platform, user_id)
person_info_manager = get_person_info_manager()
sender_name = await person_info_manager.get_value(person_id, "person_name") or "未知用户"
# 检查是否是机器人自己,如果是则显示为(你)
if user_id == str(global_config.bot.qq_account):
sender_name = f"{global_config.bot.nickname}(你)"
else:
sender_name = "未知用户"
# 处理消息内容中的用户引用确保bot回复在消息内容中也正确显示
from src.chat.utils.chat_message_builder import replace_user_references_async
msg_content = await replace_user_references_async(
msg_content,
platform,
replace_bot_name=True
)
# 不显示兴趣度replyer只需要关注消息内容本身
unread_lines.append(f"{msg_time} {sender_name}: {msg_content}")
unread_history_prompt_str = "\n".join(unread_lines)
unread_history_prompt = (
f"这是未读历史消息:\n{unread_history_prompt_str}"
)
else:
unread_history_prompt = "暂无未读历史消息"
return f"### 📜 已读历史消息\n{read_history_prompt}", f"### 📬 未读历史消息\n{unread_history_prompt}"
async def build_prompt_reply_context(
self,
reply_to: str,
extra_info: str = "",
available_actions: dict[str, ActionInfo] | None = None,
enable_tool: bool = True,
reply_message: DatabaseMessages | None = None,
prompt_mode: Literal["s4u", "normal", "minimal"] = "s4u", # 新增参数s4u 或 normal
) -> str:
"""
构建回复器上下文
Args:
reply_to: 回复对象,格式为 "发送者:消息内容"
extra_info: 额外信息,用于补充上下文
available_actions: 可用动作
enable_timeout: 是否启用超时处理
enable_tool: 是否启用工具调用
reply_message: 回复的原始消息
prompt_mode: 提示词模式,"s4u"(针对单条消息回复)或"normal"(统一回应未读消息)
Returns:
str: 构建好的上下文
"""
assert global_config is not None
if available_actions is None:
available_actions = {}
chat_stream = self.chat_stream
chat_id = chat_stream.stream_id
person_info_manager = get_person_info_manager()
is_group_chat = bool(chat_stream.group_info)
mood_prompt = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
mood_prompt = chat_mood.mood_state
if reply_to:
# 兼容旧的reply_to
sender, target = self._parse_reply_target(reply_to)
# 回退逻辑:为 'reply_to' 路径提供 platform 和 user_id 的回退值,以修复 UnboundLocalError
# 这样就不再强制要求必须有 user_id解决了QQ空间插件等场景下的崩溃问题
platform = chat_stream.platform
user_id = ""
else:
# 对于 respond 动作reply_message 可能为 None统一回应未读消息
# 对于 reply 动作reply_message 必须存在(针对特定消息回复)
if reply_message is None:
# respond 模式:没有特定目标消息,使用通用的 sender 和 target
if prompt_mode == "normal":
# 从未读消息中获取最新的消息作为参考
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream_obj = await chat_manager.get_stream(chat_id)
if chat_stream_obj:
unread_messages = chat_stream_obj.context.get_unread_messages()
if unread_messages:
# 使用最后一条未读消息作为参考
last_msg = unread_messages[-1]
platform = last_msg.chat_info.platform if hasattr(last_msg, "chat_info") else chat_stream.platform
user_id = last_msg.user_info.user_id if hasattr(last_msg, "user_info") else ""
user_nickname = last_msg.user_info.user_nickname if hasattr(last_msg, "user_info") else ""
user_cardname = last_msg.user_info.user_cardname if hasattr(last_msg, "user_info") else ""
processed_plain_text = last_msg.processed_plain_text or ""
else:
# 没有未读消息,使用默认值
platform = chat_stream.platform
user_id = ""
user_nickname = ""
user_cardname = ""
processed_plain_text = ""
else:
# 无法获取 chat_stream使用默认值
platform = chat_stream.platform
user_id = ""
user_nickname = ""
user_cardname = ""
processed_plain_text = ""
else:
# reply 模式下 reply_message 为 None 是错误的
logger.warning("reply_message 为 None但处于 reply 模式无法构建prompt")
return ""
else:
# 有 reply_message正常处理
platform = reply_message.chat_info.platform
user_id = reply_message.user_info.user_id
user_nickname = reply_message.user_info.user_nickname
user_cardname = reply_message.user_info.user_cardname
processed_plain_text = reply_message.processed_plain_text
person_id = person_info_manager.get_person_id(
platform, # type: ignore
user_id, # type: ignore
)
person_name = await person_info_manager.get_value(person_id, "person_name")
# 如果person_name为None使用fallback值
if person_name is None:
# 尝试从reply_message获取用户名
await person_info_manager.first_knowing_some_one(
platform, # type: ignore
user_id, # type: ignore
user_nickname or "",
user_cardname or "",
)
# 检查是否是bot自己的名字如果是则替换为"(你)"
bot_user_id = str(global_config.bot.qq_account)
current_user_id = await person_info_manager.get_value(person_id, "user_id")
current_platform = platform
if str(current_user_id) == bot_user_id and current_platform == global_config.bot.platform:
sender = f"{person_name}(你)"
else:
# 如果不是bot自己直接使用person_name
sender = person_name
target = processed_plain_text
# 最终的空值检查确保sender和target不为None
if sender is None:
logger.warning("sender为None使用默认值'未知用户'")
sender = "未知用户"
if target is None:
logger.warning("target为None使用默认值'(无消息内容)'")
target = "(无消息内容)"
person_info_manager = get_person_info_manager()
person_id = await person_info_manager.get_person_id_by_person_name(sender)
platform = chat_stream.platform
target = await replace_user_references_async(target, chat_stream.platform, replace_bot_name=True)
# 构建action描述告诉回复器已选取的动作
action_descriptions = ""
if available_actions:
# 过滤掉特殊键以_开头
action_items = {k: v for k, v in available_actions.items() if not k.startswith("_")}
# 提取目标消息信息(如果存在)
target_msg_info = available_actions.get("_target_message") # type: ignore
if action_items:
if len(action_items) == 1:
# 单个动作
action_name, action_info = next(iter(action_items.items()))
action_desc = action_info.description
# 构建基础决策信息
action_descriptions = f"## 决策信息\n\n你已经决定要执行 **{action_name}** 动作({action_desc})。\n\n"
# 只有需要目标消息的动作才显示目标消息详情
# respond 动作是统一回应所有未读消息,不应该显示特定目标消息
if action_name not in ["respond"] and target_msg_info and isinstance(target_msg_info, dict):
import time as time_module
sender = target_msg_info.get("sender", "未知用户")
content = target_msg_info.get("content", "")
msg_time = target_msg_info.get("time", 0)
time_str = time_module.strftime("%H:%M:%S", time_module.localtime(msg_time)) if msg_time else "未知时间"
action_descriptions += f"**目标消息**: {time_str} {sender} 说: {content}\n\n"
else:
# 多个动作
action_descriptions = "## 决策信息\n\n你已经决定同时执行以下动作:\n\n"
for action_name, action_info in action_items.items():
action_desc = action_info.description
action_descriptions += f"- **{action_name}**: {action_desc}\n"
action_descriptions += "\n"
# 从内存获取历史消息,避免重复查询数据库
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream_obj = await chat_manager.get_stream(chat_id)
if chat_stream_obj:
# 确保历史消息已初始化
await chat_stream_obj.context.ensure_history_initialized()
# 获取所有消息(历史+未读)
all_messages = (
chat_stream_obj.context.history_messages +
chat_stream_obj.context.get_unread_messages()
)
# 转换为字典格式
message_list_before_now_long = [msg.flatten() for msg in all_messages[-(global_config.chat.max_context_size * 2):]]
message_list_before_short = [msg.flatten() for msg in all_messages[-int(global_config.chat.max_context_size):]]
logger.debug(f"使用内存中的消息: long={len(message_list_before_now_long)}, short={len(message_list_before_short)}")
else:
# 回退到数据库查询
logger.warning(f"无法获取chat_stream回退到数据库查询: {chat_id}")
message_list_before_now_long = await get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id,
timestamp=time.time(),
limit=global_config.chat.max_context_size * 2,
)
message_list_before_short = await get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size),
)
chat_talking_prompt_short = await build_readable_messages(
message_list_before_short,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
show_actions=True,
)
# 获取目标用户信息用于s4u模式
target_user_info = None
if sender:
target_user_info = await person_info_manager.get_person_info_by_name(sender)
from src.chat.utils.prompt import Prompt
# 并行执行任务
tasks = {
"expression_habits": asyncio.create_task(
self._time_and_run_task(
self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits"
)
),
"relation_info": asyncio.create_task(
self._time_and_run_task(self.build_relation_info(sender, target), "relation_info")
),
"memory_block": asyncio.create_task(
self._time_and_run_task(
self.build_memory_block(chat_talking_prompt_short, target, message_list_before_short),
"memory_block",
)
),
"tool_info": asyncio.create_task(
self._time_and_run_task(
self.build_tool_info(chat_talking_prompt_short, sender, target, enable_tool=enable_tool),
"tool_info",
)
),
"prompt_info": asyncio.create_task(
self._time_and_run_task(self.get_prompt_info(chat_talking_prompt_short, sender, target), "prompt_info")
),
"cross_context": asyncio.create_task(
self._time_and_run_task(
# cross_context 的构建已移至 prompt.py
asyncio.sleep(0, result=""), "cross_context"
)
),
"notice_block": asyncio.create_task(
self._time_and_run_task(self.build_notice_block(chat_id), "notice_block")
),
}
# 设置超时
timeout = 45.0 # 秒
async def get_task_result(task_name, task):
try:
return await asyncio.wait_for(task, timeout=timeout)
except asyncio.TimeoutError:
logger.warning(f"构建任务{task_name}超时 ({timeout}s),使用默认值")
# 为超时任务提供默认值
default_values = {
"expression_habits": "",
"relation_info": "",
"memory_block": "",
"tool_info": "",
"prompt_info": "",
"cross_context": "",
"notice_block": "",
}
logger.info(f"为超时任务 {task_name} 提供默认值")
return task_name, default_values[task_name], timeout
try:
task_results = await asyncio.gather(*(get_task_result(name, task) for name, task in tasks.items()))
except asyncio.CancelledError:
logger.info("Prompt构建任务被取消正在清理子任务")
# 取消所有未完成的子任务
for name, task in tasks.items():
if not task.done():
task.cancel()
raise
# 任务名称中英文映射
task_name_mapping = {
"expression_habits": "选取表达方式",
"relation_info": "感受关系",
"memory_block": "回忆",
"tool_info": "使用工具",
"prompt_info": "获取知识",
}
# 处理结果
timing_logs = []
results_dict = {}
for name, result, duration in task_results:
results_dict[name] = result
chinese_name = task_name_mapping.get(name, name)
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)}")
expression_habits_block = results_dict["expression_habits"]
relation_info = results_dict["relation_info"]
memory_block = results_dict["memory_block"]
tool_info = results_dict["tool_info"]
prompt_info = results_dict["prompt_info"]
cross_context_block = results_dict["cross_context"]
notice_block = results_dict["notice_block"]
# 使用统一的记忆块(已整合三层记忆系统)
combined_memory_block = memory_block if memory_block else ""
# 检查是否为视频分析结果,并注入引导语
if target and ("[视频内容]" in target or "好的,我将根据您提供的" in target):
video_prompt_injection = (
"\n请注意,以上内容是你刚刚观看的视频,请以第一人称分享你的观后感,而不是在分析一份报告。"
)
combined_memory_block += video_prompt_injection
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')}"
identity_block = await get_individuality().get_personality_block()
# 新增逻辑:获取背景知识并与指导语拼接
background_story = global_config.personality.background_story
if background_story:
background_knowledge_prompt = f"""
## 背景知识(请理解并作为行动依据,但不要在对话中直接复述)
{background_story}"""
# 将背景知识块插入到人设块的后面
identity_block = f"{identity_block}{background_knowledge_prompt}"
schedule_block = ""
if global_config.planning_system.schedule_enable:
from src.schedule.schedule_manager import schedule_manager
activity_info = schedule_manager.get_current_activity()
if activity_info:
activity = activity_info.get("activity")
time_range = activity_info.get("time_range")
now = datetime.now()
if time_range:
try:
start_str, end_str = time_range.split("-")
start_time = datetime.strptime(start_str.strip(), "%H:%M").replace(
year=now.year, month=now.month, day=now.day
)
end_time = datetime.strptime(end_str.strip(), "%H:%M").replace(
year=now.year, month=now.month, day=now.day
)
if end_time < start_time:
end_time += timedelta(days=1)
if now < start_time:
now += timedelta(days=1)
duration_minutes = (now - start_time).total_seconds() / 60
remaining_minutes = (end_time - now).total_seconds() / 60
schedule_block = (
f"- 你当前正在进行“{activity}”,"
f"计划时间从{start_time.strftime('%H:%M')}{end_time.strftime('%H:%M')}"
f"这项活动已经开始了{duration_minutes:.0f}分钟,"
f"预计还有{remaining_minutes:.0f}分钟结束。"
"(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
)
except (ValueError, AttributeError):
schedule_block = f"- 你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
else:
schedule_block = f"- 你当前正在进行“{activity}”。(此为你的当前状态,仅供参考。除非被直接询问,否则不要在对话中主动提及。)"
moderation_prompt_block = (
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
)
# 新增逻辑:构建安全准则块
safety_guidelines = global_config.personality.safety_guidelines
safety_guidelines_block = ""
if safety_guidelines:
guidelines_text = "\n".join(f"{i + 1}. {line}" for i, line in enumerate(safety_guidelines))
safety_guidelines_block = f"""### 互动规则
在任何情况下,你都必须遵守以下由你的设定者为你定义的原则:
{guidelines_text}
如果遇到违反上述原则的请求,请在保持你核心人设的同时,以合适的方式进行回应。
"""
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_scene_prompt = "你正在一个QQ群里聊天你需要理解整个群的聊天动态和话题走向并做出自然的回应。"
else:
chat_scene_prompt = f"你正在和 {sender} 私下聊天,你需要理解你们的对话并做出自然的回应。"
auth_role_prompt_block = await self._build_auth_role_prompt()
# 动态构建群聊提醒
group_chat_reminder_block = ""
if is_group_chat:
group_chat_reminder_block = "注意:在规划回复时,务必确定对方是不是真的在叫自己。聊天时往往有数百甚至数千个用户,请务必认清自己的身份和角色,避免误以为对方在和自己对话而贸然插入回复,导致尴尬局面。"
# 使用新的统一Prompt系统 - 创建PromptParameters
prompt_parameters = PromptParameters(
platform=platform,
user_id=user_id,
chat_scene=chat_scene_prompt,
chat_id=chat_id,
is_group_chat=is_group_chat,
sender=sender,
target=target,
reply_to=reply_to,
extra_info=extra_info,
available_actions=available_actions,
enable_tool=enable_tool,
chat_target_info=self.chat_target_info,
prompt_mode=prompt_mode, # 使用传入的prompt_mode参数
message_list_before_now_long=message_list_before_now_long,
message_list_before_short=message_list_before_short,
chat_talking_prompt_short=chat_talking_prompt_short,
target_user_info=target_user_info,
# 传递已构建的参数
expression_habits_block=expression_habits_block,
relation_info_block=relation_info,
memory_block=combined_memory_block, # 使用合并后的记忆块
tool_info_block=tool_info,
knowledge_prompt=prompt_info,
cross_context_block=cross_context_block,
notice_block=notice_block,
keywords_reaction_prompt=keywords_reaction_prompt,
extra_info_block=extra_info_block,
time_block=time_block,
identity_block=identity_block,
schedule_block=schedule_block,
moderation_prompt_block=moderation_prompt_block,
safety_guidelines_block=safety_guidelines_block,
reply_target_block=reply_target_block,
mood_prompt=mood_prompt,
auth_role_prompt_block=auth_role_prompt_block,
action_descriptions=action_descriptions,
group_chat_reminder_block=group_chat_reminder_block,
bot_name=global_config.bot.nickname,
bot_nickname=",".join(global_config.bot.alias_names) if global_config.bot.alias_names else "",
)
# 使用新的统一Prompt系统 - 根据prompt_mode选择模板
# s4u: 针对单条消息的深度回复
# normal: 对未读消息的统一回应
template_name = "s4u_style_prompt" if prompt_mode == "s4u" else "normal_style_prompt"
# 获取模板内容
template_prompt = await global_prompt_manager.get_prompt_async(template_name)
prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters)
prompt_text = await prompt.build()
# --- 动态添加分割指令 ---
if global_config.response_splitter.enable and global_config.response_splitter.split_mode == "llm":
split_instruction = """
## 关于回复分割的一些小建议
这个指令的**唯一目的**是为了**提高可读性**,将一个**单一、完整的回复**拆分成视觉上更易读的短句,**而不是让你生成多个不同的回复**。
请在思考好的、连贯的回复中,找到合适的停顿点插入 `[SPLIT]` 标记。
**最重要的原则:**
- **禁止内容重复**:分割后的各个部分必须是**一个连贯思想的不同阶段**,绝不能是相似意思的重复表述。
**一些可以参考的分割时机:**
1. **短句优先**: 整体上,让每个分割后的句子长度在 20-30 字左右会显得很自然。
2. **自然停顿**: 在自然的标点符号(如逗号、问号)后,或者在逻辑转折词(如“而且”、“不过”)后,都是不错的分割点。
3. **保留连贯性**: 请确保所有被 `[SPLIT]` 分隔的句子能无缝拼接成一个逻辑通顺的完整回复。如果一句话很短,或者分割会破坏语感,就不要分割。
"""
# 将分段指令添加到提示词顶部
prompt_text = f"{split_instruction}\n{prompt_text}"
return prompt_text
async def build_prompt_rewrite_context(
self,
raw_reply: str,
reason: str,
reply_to: str,
reply_message: dict[str, Any] | DatabaseMessages | None = None,
) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if
assert global_config is not None
chat_stream = self.chat_stream
chat_id = chat_stream.stream_id
is_group_chat = bool(chat_stream.group_info)
if reply_message:
if isinstance(reply_message, DatabaseMessages):
# 从 DatabaseMessages 对象获取 sender 和 target
# 注意: DatabaseMessages 没有直接的 sender/target 字段
# 需要根据实际情况构造
sender = reply_message.user_info.user_nickname or reply_message.user_info.user_id
target = reply_message.processed_plain_text or ""
else:
sender = reply_message.get("sender")
target = reply_message.get("target")
else:
sender, target = self._parse_reply_target(reply_to)
# 添加空值检查确保sender和target不为None
if sender is None:
logger.warning("build_rewrite_context: sender为None使用默认值'未知用户'")
sender = "未知用户"
if target is None:
logger.warning("build_rewrite_context: target为None使用默认值'(无消息内容)'")
target = "(无消息内容)"
# 添加情绪状态获取
mood_prompt = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
mood_prompt = chat_mood.mood_state
# 从内存获取历史消息,避免重复查询数据库
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream_obj = await chat_manager.get_stream(chat_id)
if chat_stream_obj:
# 确保历史消息已初始化
await chat_stream_obj.context.ensure_history_initialized()
# 获取所有消息(历史+未读)
all_messages = (
chat_stream_obj.context.history_messages +
chat_stream_obj.context.get_unread_messages()
)
# 转换为字典格式,限制数量
limit = int(global_config.chat.max_context_size)
message_list_before_now_half = [msg.flatten() for msg in all_messages[-limit:]]
logger.debug(f"Rewrite使用内存中的 {len(message_list_before_now_half)} 条消息")
else:
# 回退到数据库查询
logger.warning(f"无法获取chat_stream回退到数据库查询: {chat_id}")
message_list_before_now_half = await get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size),
)
chat_talking_prompt_half = await build_readable_messages(
message_list_before_now_half,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
show_actions=True,
)
# 并行执行2个构建任务
try:
expression_habits_block, relation_info = await asyncio.gather(
self.build_expression_habits(chat_talking_prompt_half, target),
self.build_relation_info(sender, target),
)
except asyncio.CancelledError:
logger.info("表达式和关系信息构建被取消")
raise
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
identity_block = await get_individuality().get_personality_block()
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 = ""
# 构建notice_block
notice_block = await self.build_notice_block(chat_id)
if is_group_chat:
await global_prompt_manager.get_prompt_async("chat_target_group1")
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.get("person_name") or self.chat_target_info.get("user_nickname") or "对方"
)
await global_prompt_manager.format_prompt("chat_target_private1", sender_name=chat_target_name)
await global_prompt_manager.format_prompt("chat_target_private2", sender_name=chat_target_name)
auth_role_prompt_block = await self._build_auth_role_prompt()
# 使用新的统一Prompt系统 - Expressor模式创建PromptParameters
prompt_parameters = PromptParameters(
chat_id=chat_id,
is_group_chat=is_group_chat,
sender=sender,
target=raw_reply, # Expressor模式使用raw_reply作为target
reply_to=f"{sender}:{target}" if sender and target else reply_to,
extra_info="", # Expressor模式不需要额外信息
prompt_mode="minimal", # Expressor使用minimal模式
chat_talking_prompt_short=chat_talking_prompt_half,
time_block=time_block,
identity_block=identity_block,
reply_target_block=reply_target_block,
mood_prompt=mood_prompt,
keywords_reaction_prompt=keywords_reaction_prompt,
moderation_prompt_block=moderation_prompt_block,
auth_role_prompt_block=auth_role_prompt_block,
# 添加已构建的表达习惯和关系信息
expression_habits_block=expression_habits_block,
relation_info_block=relation_info,
notice_block=notice_block,
bot_name=global_config.bot.nickname,
bot_nickname=",".join(global_config.bot.alias_names) if global_config.bot.alias_names else "",
)
# 使用新的统一Prompt系统 - Expressor模式
template_prompt = await global_prompt_manager.get_prompt_async("default_expressor_prompt")
prompt = Prompt(template=template_prompt.template, parameters=prompt_parameters)
prompt_text = await prompt.build()
return prompt_text
async def llm_generate_content(self, prompt: str):
assert global_config is not None
with Timer("LLM生成", {}): # 内部计时器,可选保留
# 直接使用已初始化的模型实例
logger.info(f"使用模型集生成回复: {self.express_model.model_for_task}")
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
)
if content:
if not global_config.response_splitter.enable or global_config.response_splitter.split_mode != "llm":
# 移除 [SPLIT] 标记,防止消息被分割
content = content.replace("[SPLIT]", "")
# 应用统一的格式过滤器
from src.chat.utils.utils import filter_system_format_content
content = filter_system_format_content(content)
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):
assert global_config is not None
assert model_config is not None
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"获取知识库内容时发生异常: {e!s}")
return ""
async def build_relation_info(self, sender: str, target: str):
assert global_config is not None
# 获取用户ID
if sender == f"{global_config.bot.nickname}(你)":
return "你将要回复的是你自己发送的消息。"
person_info_manager = get_person_info_manager()
person_id = await person_info_manager.get_person_id_by_person_name(sender)
if not person_id:
logger.warning(f"未找到用户 {sender} 的ID跳过信息提取")
return f"你完全不认识{sender}不理解ta的相关信息。"
# 使用 RelationshipFetcher 获取完整关系信息(包含新字段)
try:
from src.person_info.relationship_fetcher import relationship_fetcher_manager
# 获取 chat_id
chat_id = self.chat_stream.stream_id
# 获取 RelationshipFetcher 实例
relationship_fetcher = relationship_fetcher_manager.get_fetcher(chat_id)
# 构建用户关系信息(包含别名、偏好关键词等新字段)
user_relation_info = await relationship_fetcher.build_relation_info(person_id, points_num=5)
# 构建聊天流印象信息
stream_impression = await relationship_fetcher.build_chat_stream_impression(chat_id)
# 组合两部分信息
if user_relation_info and stream_impression:
return "\n\n".join([user_relation_info, stream_impression])
elif user_relation_info:
return user_relation_info
elif stream_impression:
return stream_impression
else:
return f"你完全不认识{sender},这是第一次互动。"
except Exception as e:
logger.error(f"获取关系信息失败: {e}")
# 降级到基本信息
try:
from src.plugin_system.apis import person_api
user_info = await person_info_manager.get_values(person_id, ["user_id", "platform"])
user_id = user_info.get("user_id", "unknown")
relationship_data = await person_api.get_user_relationship_data(user_id)
if relationship_data:
relationship_text = relationship_data.get("relationship_text", "")
relationship_score = relationship_data.get("relationship_score", 0.3)
if relationship_text:
if relationship_score >= 0.8:
relationship_level = "非常亲密的朋友"
elif relationship_score >= 0.6:
relationship_level = "好朋友"
elif relationship_score >= 0.4:
relationship_level = "普通朋友"
elif relationship_score >= 0.2:
relationship_level = "认识的人"
else:
relationship_level = "陌生人"
return f"你与{sender}的关系:{relationship_level}(关系分:{relationship_score:.2f}/1.0)。{relationship_text}"
else:
return f"你与{sender}是初次见面,关系分:{relationship_score:.2f}/1.0。"
except Exception:
pass
return f"你与{sender}是普通朋友关系。"
# 已废弃:旧的自动记忆存储逻辑
# 新的记忆图系统通过LLM工具(CreateMemoryTool)主动创建记忆,而非自动存储
async def _store_chat_memory_async(self, reply_to: str, reply_message: DatabaseMessages | dict[str, Any] | None = None):
"""
[已废弃] 异步存储聊天记忆从build_memory_block迁移而来
此函数已被记忆图系统的工具调用方式替代。
记忆现在由LLM在对话过程中通过CreateMemoryTool主动创建。
Args:
reply_to: 回复对象
reply_message: 回复的原始消息
"""
assert global_config is not None
return # 已禁用,保留函数签名以防其他地方有引用
# 以下代码已废弃,不再执行
try:
if not global_config.memory.enable_memory:
return
# 使用统一记忆系统存储记忆
stream = self.chat_stream
user_info_obj = getattr(stream, "user_info", None)
group_info_obj = getattr(stream, "group_info", None)
memory_user_id = str(stream.stream_id)
memory_user_display = None
memory_aliases = []
user_info_dict = {}
if user_info_obj is not None:
raw_user_id = getattr(user_info_obj, "user_id", None)
if raw_user_id:
memory_user_id = str(raw_user_id)
if hasattr(user_info_obj, "to_dict"):
try:
user_info_dict = user_info_obj.to_dict() # type: ignore[attr-defined]
except Exception:
user_info_dict = {}
candidate_keys = [
"user_cardname",
"user_nickname",
"nickname",
"remark",
"display_name",
"user_name",
]
for key in candidate_keys:
value = user_info_dict.get(key)
if isinstance(value, str) and value.strip():
stripped = value.strip()
if memory_user_display is None:
memory_user_display = stripped
elif stripped not in memory_aliases:
memory_aliases.append(stripped)
attr_keys = [
"user_cardname",
"user_nickname",
"nickname",
"remark",
"display_name",
"name",
]
for attr in attr_keys:
value = getattr(user_info_obj, attr, None)
if isinstance(value, str) and value.strip():
stripped = value.strip()
if memory_user_display is None:
memory_user_display = stripped
elif stripped not in memory_aliases:
memory_aliases.append(stripped)
alias_values = (
user_info_dict.get("aliases") or user_info_dict.get("alias_names") or user_info_dict.get("alias")
)
if isinstance(alias_values, list | tuple | set):
for alias in alias_values:
if isinstance(alias, str) and alias.strip():
stripped = alias.strip()
if stripped not in memory_aliases and stripped != memory_user_display:
memory_aliases.append(stripped)
memory_context = {
"user_id": memory_user_id,
"user_display_name": memory_user_display or "",
"user_name": memory_user_display or "",
"nickname": memory_user_display or "",
"sender_name": memory_user_display or "",
"platform": getattr(stream, "platform", None),
"chat_id": stream.stream_id,
"stream_id": stream.stream_id,
}
if memory_aliases:
memory_context["user_aliases"] = memory_aliases
if group_info_obj is not None:
group_name = getattr(group_info_obj, "group_name", None) or getattr(
group_info_obj, "group_nickname", None
)
if group_name:
memory_context["group_name"] = str(group_name)
group_id = getattr(group_info_obj, "group_id", None)
if group_id:
memory_context["group_id"] = str(group_id)
memory_context = {key: value for key, value in memory_context.items() if value}
# 从内存获取聊天历史用于存储,避免重复查询数据库
from src.plugin_system.apis.chat_api import get_chat_manager
chat_manager = get_chat_manager()
chat_stream_obj = await chat_manager.get_stream(stream.stream_id)
if chat_stream_obj:
# 确保历史消息已初始化
await chat_stream_obj.context.ensure_history_initialized()
# 获取所有消息(历史+未读)
all_messages = (
chat_stream_obj.context.history_messages +
chat_stream_obj.context.get_unread_messages()
)
# 转换为字典格式,限制数量
limit = int(global_config.chat.max_context_size)
message_list_before_short = [msg.flatten() for msg in all_messages[-limit:]]
logger.debug(f"记忆存储使用内存中的 {len(message_list_before_short)} 条消息")
else:
# 回退到数据库查询
logger.warning(f"记忆存储无法获取chat_stream回退到数据库查询: {stream.stream_id}")
message_list_before_short = await get_raw_msg_before_timestamp_with_chat(
chat_id=stream.stream_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size),
)
await build_readable_messages(
message_list_before_short,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
show_actions=True,
)
# 旧记忆系统的自动存储已禁用
# 新记忆系统通过 LLM 工具调用create_memory来创建记忆
logger.debug(f"记忆创建通过 LLM 工具调用进行,用户: {memory_user_display or memory_user_id}")
except asyncio.CancelledError:
logger.debug("记忆存储任务被取消")
# 这是正常情况,不需要清理子任务,因为是叶子节点
raise
except Exception as e:
logger.error(f"存储聊天记忆失败: {e}")
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
init_prompt()