1005 lines
43 KiB
Python
1005 lines
43 KiB
Python
import traceback
|
||
from typing import List, Optional, Dict, Any, Tuple
|
||
|
||
from src.chat.message_receive.message import MessageRecv, MessageThinking, MessageSending
|
||
from src.chat.message_receive.message import Seg # Local import needed after move
|
||
from src.chat.message_receive.message import UserInfo
|
||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||
from src.common.logger import get_logger
|
||
from src.llm_models.utils_model import LLMRequest
|
||
from src.config.config import global_config
|
||
from src.chat.utils.timer_calculator import Timer # <--- Import Timer
|
||
from src.chat.focus_chat.heartFC_sender import HeartFCSender
|
||
from src.chat.heart_flow.utils_chat import get_chat_type_and_target_info
|
||
from src.chat.message_receive.chat_stream import ChatStream
|
||
from src.chat.focus_chat.hfc_utils import parse_thinking_id_to_timestamp
|
||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||
from src.chat.express.exprssion_learner import get_expression_learner
|
||
import time
|
||
import asyncio
|
||
from src.chat.express.expression_selector import expression_selector
|
||
from src.manager.mood_manager import mood_manager
|
||
from src.person_info.relationship_fetcher import relationship_fetcher_manager
|
||
import random
|
||
import ast
|
||
from src.person_info.person_info import get_person_info_manager
|
||
from datetime import datetime
|
||
import re
|
||
from src.chat.knowledge.knowledge_lib import qa_manager
|
||
from src.chat.focus_chat.memory_activator import MemoryActivator
|
||
from src.tools.tool_executor import ToolExecutor
|
||
|
||
logger = get_logger("replyer")
|
||
|
||
|
||
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("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
|
||
|
||
Prompt(
|
||
"""
|
||
{expression_habits_block}
|
||
{tool_info_block}
|
||
{memory_block}
|
||
{relation_info_block}
|
||
{extra_info_block}
|
||
|
||
{chat_target}
|
||
{time_block}
|
||
{chat_info}
|
||
{reply_target_block}
|
||
{identity}
|
||
|
||
{action_descriptions}
|
||
你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},请你给出回复
|
||
{config_expression_style}。
|
||
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,注意不要复读你说过的话。
|
||
{keywords_reaction_prompt}
|
||
请注意不要输出多余内容(包括前后缀,冒号和引号,at或 @等 )。只输出回复内容。
|
||
{moderation_prompt}
|
||
不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""",
|
||
"default_generator_prompt",
|
||
)
|
||
|
||
Prompt(
|
||
"""
|
||
{expression_habits_block}
|
||
{tool_info_block}
|
||
{memory_block}
|
||
{relation_info_block}
|
||
{extra_info_block}
|
||
{time_block}
|
||
{chat_target}
|
||
{chat_info}
|
||
现在"{sender_name}"说:{target_message}。你想要回复对方的这条消息。
|
||
{identity},
|
||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。注意不要复读你说过的话。
|
||
|
||
{config_expression_style}。回复不要浮夸,不要用夸张修辞,平淡一些。
|
||
{keywords_reaction_prompt}
|
||
请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。
|
||
不要浮夸,不要夸张修辞,请注意不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出一条回复就好。
|
||
现在,你说:
|
||
""",
|
||
"default_generator_private_prompt",
|
||
)
|
||
|
||
Prompt(
|
||
"""
|
||
你可以参考你的以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
|
||
{style_habbits}
|
||
|
||
你现在正在群里聊天,以下是群里正在进行的聊天内容:
|
||
{chat_info}
|
||
|
||
以上是聊天内容,你需要了解聊天记录中的内容
|
||
|
||
{chat_target}
|
||
你的名字是{bot_name},{prompt_personality},在这聊天中,"{sender_name}"说的"{target_message}"引起了你的注意,对这句话,你想表达:{raw_reply},原因是:{reason}。你现在要思考怎么回复
|
||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯
|
||
请你根据情景使用以下句法:
|
||
{grammar_habbits}
|
||
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
|
||
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。
|
||
现在,你说:
|
||
""",
|
||
"default_expressor_prompt",
|
||
)
|
||
|
||
Prompt(
|
||
"""
|
||
你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:
|
||
{style_habbits}
|
||
|
||
你现在正在群里聊天,以下是群里正在进行的聊天内容:
|
||
{chat_info}
|
||
|
||
以上是聊天内容,你需要了解聊天记录中的内容
|
||
|
||
{chat_target}
|
||
你的名字是{bot_name},{prompt_personality},在这聊天中,"{sender_name}"说的"{target_message}"引起了你的注意,对这句话,你想表达:{raw_reply},原因是:{reason}。你现在要思考怎么回复
|
||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。
|
||
请你根据情景使用以下句法:
|
||
{grammar_habbits}
|
||
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
|
||
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 ),只输出一条回复就好。
|
||
现在,你说:
|
||
""",
|
||
"default_expressor_private_prompt", # New template for private FOCUSED chat
|
||
)
|
||
|
||
|
||
class DefaultReplyer:
|
||
def __init__(
|
||
self,
|
||
chat_stream: ChatStream,
|
||
enable_tool: bool = False,
|
||
model_configs: Optional[List[Dict[str, Any]]] = None,
|
||
request_type: str = "focus.replyer",
|
||
):
|
||
self.log_prefix = "replyer"
|
||
self.request_type = request_type
|
||
|
||
self.enable_tool = enable_tool
|
||
|
||
if model_configs:
|
||
self.express_model_configs = model_configs
|
||
else:
|
||
# 当未提供配置时,使用默认配置并赋予默认权重
|
||
|
||
model_config_1 = global_config.model.replyer_1.copy()
|
||
model_config_2 = global_config.model.replyer_2.copy()
|
||
prob_first = global_config.chat.replyer_random_probability
|
||
|
||
model_config_1["weight"] = prob_first
|
||
model_config_2["weight"] = 1.0 - prob_first
|
||
|
||
self.express_model_configs = [model_config_1, model_config_2]
|
||
|
||
if not self.express_model_configs:
|
||
logger.warning("未找到有效的模型配置,回复生成可能会失败。")
|
||
# 提供一个最终的回退,以防止在空列表上调用 random.choice
|
||
fallback_config = global_config.model.replyer_1.copy()
|
||
fallback_config.setdefault("weight", 1.0)
|
||
self.express_model_configs = [fallback_config]
|
||
|
||
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 = HeartFCSender()
|
||
self.memory_activator = MemoryActivator()
|
||
self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=True, cache_ttl=3)
|
||
|
||
def _select_weighted_model_config(self) -> Dict[str, Any]:
|
||
"""使用加权随机选择来挑选一个模型配置"""
|
||
configs = self.express_model_configs
|
||
# 提取权重,如果模型配置中没有'weight'键,则默认为1.0
|
||
weights = [config.get("weight", 1.0) for config in configs]
|
||
|
||
# random.choices 返回一个列表,我们取第一个元素
|
||
selected_config = random.choices(population=configs, weights=weights, k=1)[0]
|
||
return selected_config
|
||
|
||
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv], thinking_id: str):
|
||
"""创建思考消息 (尝试锚定到 anchor_message)"""
|
||
if not anchor_message or not anchor_message.chat_stream:
|
||
logger.error(f"{self.log_prefix} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
|
||
return None
|
||
|
||
chat = anchor_message.chat_stream
|
||
messageinfo = anchor_message.message_info
|
||
thinking_time_point = parse_thinking_id_to_timestamp(thinking_id)
|
||
bot_user_info = UserInfo(
|
||
user_id=global_config.bot.qq_account,
|
||
user_nickname=global_config.bot.nickname,
|
||
platform=messageinfo.platform,
|
||
)
|
||
|
||
thinking_message = MessageThinking(
|
||
message_id=thinking_id,
|
||
chat_stream=chat,
|
||
bot_user_info=bot_user_info,
|
||
reply=anchor_message, # 回复的是锚点消息
|
||
thinking_start_time=thinking_time_point,
|
||
)
|
||
# logger.debug(f"创建思考消息thinking_message:{thinking_message}")
|
||
|
||
await self.heart_fc_sender.register_thinking(thinking_message)
|
||
return None
|
||
|
||
async def generate_reply_with_context(
|
||
self,
|
||
reply_data: Dict[str, Any] = None,
|
||
reply_to: str = "",
|
||
relation_info: str = "",
|
||
extra_info: str = "",
|
||
available_actions: List[str] = None,
|
||
) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
回复器 (Replier): 核心逻辑,负责生成回复文本。
|
||
(已整合原 HeartFCGenerator 的功能)
|
||
"""
|
||
if available_actions is None:
|
||
available_actions = []
|
||
if reply_data is None:
|
||
reply_data = {}
|
||
try:
|
||
if not reply_data:
|
||
reply_data = {
|
||
"reply_to": reply_to,
|
||
"relation_info": relation_info,
|
||
"extra_info": extra_info,
|
||
}
|
||
for key, value in reply_data.items():
|
||
if not value:
|
||
logger.info(f"{self.log_prefix} 回复数据跳过{key},生成回复时将忽略。")
|
||
|
||
# 3. 构建 Prompt
|
||
with Timer("构建Prompt", {}): # 内部计时器,可选保留
|
||
prompt = await self.build_prompt_reply_context(
|
||
reply_data=reply_data, # 传递action_data
|
||
available_actions=available_actions,
|
||
)
|
||
|
||
# 4. 调用 LLM 生成回复
|
||
content = None
|
||
reasoning_content = None
|
||
model_name = "unknown_model"
|
||
|
||
try:
|
||
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
||
# 加权随机选择一个模型配置
|
||
selected_model_config = self._select_weighted_model_config()
|
||
logger.info(
|
||
f"{self.log_prefix} 使用模型配置: {selected_model_config.get('model_name', 'N/A')} (权重: {selected_model_config.get('weight', 1.0)})"
|
||
)
|
||
|
||
express_model = LLMRequest(
|
||
model=selected_model_config,
|
||
request_type=self.request_type,
|
||
)
|
||
|
||
logger.info(f"{self.log_prefix}Prompt:\n{prompt}\n")
|
||
content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt)
|
||
|
||
logger.info(f"最终回复: {content}")
|
||
|
||
except Exception as llm_e:
|
||
# 精简报错信息
|
||
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
|
||
return False, None # LLM 调用失败则无法生成回复
|
||
|
||
return True, content, prompt
|
||
|
||
except Exception as e:
|
||
logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
|
||
traceback.print_exc()
|
||
return False, None
|
||
|
||
async def rewrite_reply_with_context(self, reply_data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
表达器 (Expressor): 核心逻辑,负责生成回复文本。
|
||
"""
|
||
try:
|
||
reply_to = reply_data.get("reply_to", "")
|
||
raw_reply = reply_data.get("raw_reply", "")
|
||
reason = reply_data.get("reason", "")
|
||
|
||
with Timer("构建Prompt", {}): # 内部计时器,可选保留
|
||
prompt = await self.build_prompt_rewrite_context(
|
||
raw_reply=raw_reply,
|
||
reason=reason,
|
||
reply_to=reply_to,
|
||
)
|
||
|
||
content = None
|
||
reasoning_content = None
|
||
model_name = "unknown_model"
|
||
if not prompt:
|
||
logger.error(f"{self.log_prefix}Prompt 构建失败,无法生成回复。")
|
||
return False, None
|
||
|
||
try:
|
||
with Timer("LLM生成", {}): # 内部计时器,可选保留
|
||
# 加权随机选择一个模型配置
|
||
selected_model_config = self._select_weighted_model_config()
|
||
logger.info(
|
||
f"{self.log_prefix} 使用模型配置进行重写: {selected_model_config.get('model_name', 'N/A')} (权重: {selected_model_config.get('weight', 1.0)})"
|
||
)
|
||
|
||
express_model = LLMRequest(
|
||
model=selected_model_config,
|
||
request_type=self.request_type,
|
||
)
|
||
|
||
content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt)
|
||
|
||
logger.info(f"想要表达:{raw_reply}||理由:{reason}")
|
||
logger.info(f"最终回复: {content}\n")
|
||
|
||
except Exception as llm_e:
|
||
# 精简报错信息
|
||
logger.error(f"{self.log_prefix}LLM 生成失败: {llm_e}")
|
||
return False, None # LLM 调用失败则无法生成回复
|
||
|
||
return True, content
|
||
|
||
except Exception as e:
|
||
logger.error(f"{self.log_prefix}回复生成意外失败: {e}")
|
||
traceback.print_exc()
|
||
return False, None
|
||
|
||
async def build_relation_info(self, reply_data=None, chat_history=None):
|
||
relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_stream.stream_id)
|
||
if not reply_data:
|
||
return ""
|
||
reply_to = reply_data.get("reply_to", "")
|
||
sender, text = self._parse_reply_target(reply_to)
|
||
if not sender or not text:
|
||
return ""
|
||
|
||
# 获取用户ID
|
||
person_info_manager = get_person_info_manager()
|
||
person_id = person_info_manager.get_person_id_by_person_name(sender)
|
||
if not person_id:
|
||
logger.warning(f"{self.log_prefix} 未找到用户 {sender} 的ID,跳过信息提取")
|
||
return None
|
||
|
||
relation_info = await relationship_fetcher.build_relation_info(person_id, text, chat_history)
|
||
return relation_info
|
||
|
||
async def build_expression_habits(self, chat_history, target):
|
||
style_habbits = []
|
||
grammar_habbits = []
|
||
|
||
# 使用从处理器传来的选中表达方式
|
||
# LLM模式:调用LLM选择5-10个,然后随机选5个
|
||
selected_expressions = await expression_selector.select_suitable_expressions_llm(
|
||
self.chat_stream.stream_id, chat_history, max_num=8, min_num=2, target_message=target
|
||
)
|
||
|
||
if selected_expressions:
|
||
logger.info(f"{self.log_prefix} 使用处理器选中的{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_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||
else:
|
||
style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||
else:
|
||
logger.debug(f"{self.log_prefix} 没有从处理器获得表达方式,将使用空的表达方式")
|
||
# 不再在replyer中进行随机选择,全部交给处理器处理
|
||
|
||
style_habbits_str = "\n".join(style_habbits)
|
||
grammar_habbits_str = "\n".join(grammar_habbits)
|
||
|
||
# 动态构建expression habits块
|
||
expression_habits_block = ""
|
||
if style_habbits_str.strip():
|
||
expression_habits_block += f"你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:\n{style_habbits_str}\n\n"
|
||
if grammar_habbits_str.strip():
|
||
expression_habits_block += f"请你根据情景使用以下句法:\n{grammar_habbits_str}\n"
|
||
|
||
return expression_habits_block
|
||
|
||
async def build_memory_block(self, chat_history, target):
|
||
running_memorys = await self.memory_activator.activate_memory_with_chat_history(
|
||
chat_id=self.chat_stream.stream_id, target_message=target, chat_history_prompt=chat_history
|
||
)
|
||
|
||
if running_memorys:
|
||
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
|
||
for running_memory in running_memorys:
|
||
memory_str += f"- {running_memory['content']}\n"
|
||
memory_block = memory_str
|
||
logger.info(f"{self.log_prefix} 添加了 {len(running_memorys)} 个激活的记忆到prompt")
|
||
else:
|
||
memory_block = ""
|
||
|
||
return memory_block
|
||
|
||
async def build_tool_info(self, reply_data=None, chat_history=None):
|
||
"""构建工具信息块
|
||
|
||
Args:
|
||
reply_data: 回复数据,包含要回复的消息内容
|
||
chat_history: 聊天历史
|
||
|
||
Returns:
|
||
str: 工具信息字符串
|
||
"""
|
||
if not reply_data:
|
||
return ""
|
||
|
||
reply_to = reply_data.get("reply_to", "")
|
||
sender, text = self._parse_reply_target(reply_to)
|
||
|
||
if not text:
|
||
return ""
|
||
|
||
try:
|
||
# 使用工具执行器获取信息
|
||
tool_results = await self.tool_executor.execute_from_chat_message(
|
||
sender=sender, target_message=text, 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", "info")
|
||
|
||
tool_info_str += f"- 【{tool_name}】{result_type}: {content}\n"
|
||
|
||
tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。"
|
||
logger.info(f"{self.log_prefix} 获取到 {len(tool_results)} 个工具结果")
|
||
return tool_info_str
|
||
else:
|
||
logger.debug(f"{self.log_prefix} 未获取到任何工具结果")
|
||
return ""
|
||
|
||
except Exception as e:
|
||
logger.error(f"{self.log_prefix} 工具信息获取失败: {e}")
|
||
return ""
|
||
|
||
def _parse_reply_target(self, target_message: str) -> tuple:
|
||
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):
|
||
# 关键词检测与反应
|
||
keywords_reaction_prompt = ""
|
||
try:
|
||
# 处理关键词规则
|
||
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 += 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 build_prompt_reply_context(self, reply_data=None, available_actions: List[str] = None) -> str:
|
||
"""
|
||
构建回复器上下文
|
||
|
||
Args:
|
||
reply_data: 回复数据
|
||
replay_data 包含以下字段:
|
||
structured_info: 结构化信息,一般是工具调用获得的信息
|
||
reply_to: 回复对象
|
||
extra_info/extra_info_block: 额外信息
|
||
available_actions: 可用动作
|
||
|
||
Returns:
|
||
str: 构建好的上下文
|
||
"""
|
||
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()
|
||
bot_person_id = person_info_manager.get_person_id("system", "bot_id")
|
||
is_group_chat = bool(chat_stream.group_info)
|
||
reply_to = reply_data.get("reply_to", "none")
|
||
extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "")
|
||
|
||
sender, target = self._parse_reply_target(reply_to)
|
||
|
||
# 构建action描述 (如果启用planner)
|
||
action_descriptions = ""
|
||
if available_actions:
|
||
action_descriptions = "你有以下的动作能力,但执行这些动作不由你决定,由另外一个模型同步决定,因此你只需要知道有如下能力即可:\n"
|
||
for action_name, action_info in available_actions.items():
|
||
action_description = action_info.get("description", "")
|
||
action_descriptions += f"- {action_name}: {action_description}\n"
|
||
action_descriptions += "\n"
|
||
|
||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||
chat_id=chat_id,
|
||
timestamp=time.time(),
|
||
limit=global_config.chat.max_context_size,
|
||
)
|
||
chat_talking_prompt = build_readable_messages(
|
||
message_list_before_now,
|
||
replace_bot_name=True,
|
||
merge_messages=False,
|
||
timestamp_mode="normal_no_YMD",
|
||
read_mark=0.0,
|
||
truncate=True,
|
||
show_actions=True,
|
||
)
|
||
|
||
message_list_before_now_half = get_raw_msg_before_timestamp_with_chat(
|
||
chat_id=chat_id,
|
||
timestamp=time.time(),
|
||
limit=int(global_config.chat.max_context_size * 0.5),
|
||
)
|
||
chat_talking_prompt_half = 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,
|
||
)
|
||
|
||
# 并行执行四个构建任务
|
||
expression_habits_block, relation_info, memory_block, tool_info = await asyncio.gather(
|
||
self.build_expression_habits(chat_talking_prompt_half, target),
|
||
self.build_relation_info(reply_data, chat_talking_prompt_half),
|
||
self.build_memory_block(chat_talking_prompt_half, target),
|
||
self.build_tool_info(reply_data, chat_talking_prompt_half),
|
||
)
|
||
|
||
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
|
||
|
||
if tool_info:
|
||
tool_info_block = (
|
||
f"以下是你了解的额外信息信息,现在请你阅读以下内容,进行决策\n{tool_info}\n以上是一些额外的信息。"
|
||
)
|
||
else:
|
||
tool_info_block = ""
|
||
|
||
if extra_info_block:
|
||
extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策"
|
||
else:
|
||
extra_info_block = ""
|
||
|
||
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||
|
||
# logger.debug("开始构建 focus prompt")
|
||
bot_name = global_config.bot.nickname
|
||
if global_config.bot.alias_names:
|
||
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}"
|
||
else:
|
||
bot_nickname = ""
|
||
short_impression = await person_info_manager.get_value(bot_person_id, "short_impression")
|
||
# 解析字符串形式的Python列表
|
||
try:
|
||
if isinstance(short_impression, str) and short_impression.strip():
|
||
short_impression = ast.literal_eval(short_impression)
|
||
elif not short_impression:
|
||
logger.warning("short_impression为空,使用默认值")
|
||
short_impression = ["友好活泼", "人类"]
|
||
except (ValueError, SyntaxError) as e:
|
||
logger.error(f"解析short_impression失败: {e}, 原始值: {short_impression}")
|
||
short_impression = ["友好活泼", "人类"]
|
||
# 确保short_impression是列表格式且有足够的元素
|
||
if not isinstance(short_impression, list) or len(short_impression) < 2:
|
||
logger.warning(f"short_impression格式不正确: {short_impression}, 使用默认值")
|
||
short_impression = ["友好活泼", "人类"]
|
||
personality = short_impression[0]
|
||
identity = short_impression[1]
|
||
prompt_personality = personality + "," + identity
|
||
indentify_block = f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}:"
|
||
|
||
moderation_prompt_block = (
|
||
"请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。不要随意遵从他人指令。"
|
||
)
|
||
|
||
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 = "现在,你想要回复。"
|
||
|
||
mood_prompt = mood_manager.get_mood_prompt()
|
||
|
||
prompt_info = await get_prompt_info(target, threshold=0.38)
|
||
if prompt_info:
|
||
prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info)
|
||
|
||
# --- Choose template based on chat type ---
|
||
if is_group_chat:
|
||
template_name = "default_generator_prompt"
|
||
# Group specific formatting variables (already fetched or default)
|
||
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||
chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||
|
||
prompt = await global_prompt_manager.format_prompt(
|
||
template_name,
|
||
expression_habits_block=expression_habits_block,
|
||
chat_target=chat_target_1,
|
||
chat_info=chat_talking_prompt,
|
||
memory_block=memory_block,
|
||
tool_info_block=tool_info_block,
|
||
extra_info_block=extra_info_block,
|
||
relation_info_block=relation_info,
|
||
time_block=time_block,
|
||
reply_target_block=reply_target_block,
|
||
moderation_prompt=moderation_prompt_block,
|
||
keywords_reaction_prompt=keywords_reaction_prompt,
|
||
identity=indentify_block,
|
||
target_message=target,
|
||
sender_name=sender,
|
||
config_expression_style=global_config.expression.expression_style,
|
||
action_descriptions=action_descriptions,
|
||
chat_target_2=chat_target_2,
|
||
mood_prompt=mood_prompt,
|
||
)
|
||
else: # Private chat
|
||
template_name = "default_generator_private_prompt"
|
||
# 在私聊时获取对方的昵称信息
|
||
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 "对方"
|
||
)
|
||
chat_target_1 = f"你正在和 {chat_target_name} 聊天"
|
||
prompt = await global_prompt_manager.format_prompt(
|
||
template_name,
|
||
expression_habits_block=expression_habits_block,
|
||
chat_target=chat_target_1,
|
||
chat_info=chat_talking_prompt,
|
||
memory_block=memory_block,
|
||
tool_info_block=tool_info_block,
|
||
relation_info_block=relation_info,
|
||
extra_info_block=extra_info_block,
|
||
time_block=time_block,
|
||
keywords_reaction_prompt=keywords_reaction_prompt,
|
||
identity=indentify_block,
|
||
target_message=target,
|
||
sender_name=sender,
|
||
config_expression_style=global_config.expression.expression_style,
|
||
)
|
||
|
||
return prompt
|
||
|
||
async def build_prompt_rewrite_context(
|
||
self,
|
||
reason,
|
||
raw_reply,
|
||
reply_to,
|
||
) -> str:
|
||
sender = ""
|
||
target = ""
|
||
if ":" in reply_to or ":" in reply_to:
|
||
# 使用正则表达式匹配中文或英文冒号
|
||
parts = re.split(pattern=r"[::]", string=reply_to, maxsplit=1)
|
||
if len(parts) == 2:
|
||
sender = parts[0].strip()
|
||
target = parts[1].strip()
|
||
|
||
chat_stream = self.chat_stream
|
||
|
||
is_group_chat = bool(chat_stream.group_info)
|
||
|
||
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
|
||
chat_id=chat_stream.stream_id,
|
||
timestamp=time.time(),
|
||
limit=global_config.chat.max_context_size,
|
||
)
|
||
chat_talking_prompt = build_readable_messages(
|
||
message_list_before_now,
|
||
replace_bot_name=True,
|
||
merge_messages=True,
|
||
timestamp_mode="relative",
|
||
read_mark=0.0,
|
||
truncate=True,
|
||
)
|
||
|
||
expression_learner = get_expression_learner()
|
||
(
|
||
learnt_style_expressions,
|
||
learnt_grammar_expressions,
|
||
personality_expressions,
|
||
) = expression_learner.get_expression_by_chat_id(chat_stream.stream_id)
|
||
|
||
style_habbits = []
|
||
grammar_habbits = []
|
||
# 1. learnt_expressions加权随机选3条
|
||
if learnt_style_expressions:
|
||
weights = [expr["count"] for expr in learnt_style_expressions]
|
||
selected_learnt = weighted_sample_no_replacement(learnt_style_expressions, weights, 3)
|
||
for expr in selected_learnt:
|
||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||
style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||
# 2. learnt_grammar_expressions加权随机选3条
|
||
if learnt_grammar_expressions:
|
||
weights = [expr["count"] for expr in learnt_grammar_expressions]
|
||
selected_learnt = weighted_sample_no_replacement(learnt_grammar_expressions, weights, 3)
|
||
for expr in selected_learnt:
|
||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||
grammar_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||
# 3. personality_expressions随机选1条
|
||
if personality_expressions:
|
||
expr = random.choice(personality_expressions)
|
||
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
|
||
style_habbits.append(f"当{expr['situation']}时,使用 {expr['style']}")
|
||
|
||
style_habbits_str = "\n".join(style_habbits)
|
||
grammar_habbits_str = "\n".join(grammar_habbits)
|
||
|
||
logger.debug("开始构建 focus prompt")
|
||
|
||
# --- Choose template based on chat type ---
|
||
if is_group_chat:
|
||
template_name = "default_expressor_prompt"
|
||
# Group specific formatting variables (already fetched or default)
|
||
chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1")
|
||
# chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||
|
||
prompt = await global_prompt_manager.format_prompt(
|
||
template_name,
|
||
style_habbits=style_habbits_str,
|
||
grammar_habbits=grammar_habbits_str,
|
||
chat_target=chat_target_1,
|
||
chat_info=chat_talking_prompt,
|
||
bot_name=global_config.bot.nickname,
|
||
prompt_personality="",
|
||
reason=reason,
|
||
raw_reply=raw_reply,
|
||
sender_name=sender,
|
||
target_message=target,
|
||
config_expression_style=global_config.expression.expression_style,
|
||
)
|
||
else: # Private chat
|
||
template_name = "default_expressor_private_prompt"
|
||
# 在私聊时获取对方的昵称信息
|
||
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 "对方"
|
||
)
|
||
chat_target_1 = f"你正在和 {chat_target_name} 聊天"
|
||
prompt = await global_prompt_manager.format_prompt(
|
||
template_name,
|
||
style_habbits=style_habbits_str,
|
||
grammar_habbits=grammar_habbits_str,
|
||
chat_target=chat_target_1,
|
||
chat_info=chat_talking_prompt,
|
||
bot_name=global_config.bot.nickname,
|
||
prompt_personality="",
|
||
reason=reason,
|
||
raw_reply=raw_reply,
|
||
sender_name=sender,
|
||
target_message=target,
|
||
config_expression_style=global_config.expression.expression_style,
|
||
)
|
||
|
||
return prompt
|
||
|
||
async def send_response_messages(
|
||
self,
|
||
anchor_message: Optional[MessageRecv],
|
||
response_set: List[Tuple[str, str]],
|
||
thinking_id: str = "",
|
||
display_message: str = "",
|
||
) -> Optional[MessageSending]:
|
||
"""发送回复消息 (尝试锚定到 anchor_message),使用 HeartFCSender"""
|
||
chat = self.chat_stream
|
||
chat_id = self.chat_stream.stream_id
|
||
if chat is None:
|
||
logger.error(f"{self.log_prefix} 无法发送回复,chat_stream 为空。")
|
||
return None
|
||
if not anchor_message:
|
||
logger.error(f"{self.log_prefix} 无法发送回复,anchor_message 为空。")
|
||
return None
|
||
|
||
stream_name = get_chat_manager().get_stream_name(chat_id) or chat_id # 获取流名称用于日志
|
||
|
||
# 检查思考过程是否仍在进行,并获取开始时间
|
||
if thinking_id:
|
||
# print(f"thinking_id: {thinking_id}")
|
||
thinking_start_time = await self.heart_fc_sender.get_thinking_start_time(chat_id, thinking_id)
|
||
else:
|
||
print("thinking_id is None")
|
||
# thinking_id = "ds" + str(round(time.time(), 2))
|
||
thinking_start_time = time.time()
|
||
|
||
if thinking_start_time is None:
|
||
logger.error(f"[{stream_name}]replyer思考过程未找到或已结束,无法发送回复。")
|
||
return None
|
||
|
||
mark_head = False
|
||
# first_bot_msg: Optional[MessageSending] = None
|
||
reply_message_ids = [] # 记录实际发送的消息ID
|
||
|
||
sent_msg_list = []
|
||
|
||
for i, msg_text in enumerate(response_set):
|
||
# 为每个消息片段生成唯一ID
|
||
type = msg_text[0]
|
||
data = msg_text[1]
|
||
|
||
if global_config.experimental.debug_show_chat_mode and type == "text":
|
||
data += "ᶠ"
|
||
|
||
part_message_id = f"{thinking_id}_{i}"
|
||
message_segment = Seg(type=type, data=data)
|
||
|
||
if type == "emoji":
|
||
is_emoji = True
|
||
else:
|
||
is_emoji = False
|
||
reply_to = not mark_head
|
||
|
||
bot_message: MessageSending = await self._build_single_sending_message(
|
||
anchor_message=anchor_message,
|
||
message_id=part_message_id,
|
||
message_segment=message_segment,
|
||
display_message=display_message,
|
||
reply_to=reply_to,
|
||
is_emoji=is_emoji,
|
||
thinking_id=thinking_id,
|
||
thinking_start_time=thinking_start_time,
|
||
)
|
||
|
||
try:
|
||
if (
|
||
bot_message.is_private_message()
|
||
or bot_message.reply.processed_plain_text != "[System Trigger Context]"
|
||
or mark_head
|
||
):
|
||
set_reply = False
|
||
else:
|
||
set_reply = True
|
||
|
||
if not mark_head:
|
||
mark_head = True
|
||
typing = False
|
||
else:
|
||
typing = True
|
||
|
||
sent_msg = await self.heart_fc_sender.send_message(bot_message, typing=typing, set_reply=set_reply)
|
||
|
||
reply_message_ids.append(part_message_id) # 记录我们生成的ID
|
||
|
||
sent_msg_list.append((type, sent_msg))
|
||
|
||
except Exception as e:
|
||
logger.error(f"{self.log_prefix}发送回复片段 {i} ({part_message_id}) 时失败: {e}")
|
||
traceback.print_exc()
|
||
# 这里可以选择是继续发送下一个片段还是中止
|
||
|
||
# 在尝试发送完所有片段后,完成原始的 thinking_id 状态
|
||
try:
|
||
await self.heart_fc_sender.complete_thinking(chat_id, thinking_id)
|
||
|
||
except Exception as e:
|
||
logger.error(f"{self.log_prefix}完成思考状态 {thinking_id} 时出错: {e}")
|
||
|
||
return sent_msg_list
|
||
|
||
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: 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()
|
||
if anchor_message:
|
||
sender_info = anchor_message.message_info.user_info
|
||
else:
|
||
sender_info = None
|
||
|
||
bot_message = 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,
|
||
)
|
||
|
||
return bot_message
|
||
|
||
|
||
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))
|
||
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
|
||
|
||
|
||
async def get_prompt_info(message: str, threshold: float):
|
||
related_info = ""
|
||
start_time = time.time()
|
||
|
||
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
|
||
# 从LPMM知识库获取知识
|
||
try:
|
||
found_knowledge_from_lpmm = qa_manager.get_knowledge(message)
|
||
|
||
end_time = time.time()
|
||
if found_knowledge_from_lpmm is not None:
|
||
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 related_info
|
||
else:
|
||
logger.debug("从LPMM知识库获取知识失败,可能是从未导入过知识,返回空知识...")
|
||
return ""
|
||
except Exception as e:
|
||
logger.error(f"获取知识库内容时发生异常: {str(e)}")
|
||
return ""
|
||
|
||
|
||
init_prompt()
|