Merge branch 'dev' of https://github.com/Dax233/MaiMBot into issue#814

This commit is contained in:
Bakadax
2025-04-24 09:38:51 +08:00
78 changed files with 6746 additions and 3679 deletions

View File

@@ -0,0 +1,869 @@
import asyncio
import time
import traceback
from typing import List, Optional, Dict, Any, TYPE_CHECKING
import json
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
from src.plugins.chat.message import MessageSet, Seg # Local import needed after move
from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.message import UserInfo
from src.plugins.chat.chat_stream import chat_manager
from src.common.logger import get_module_logger, LogConfig, PFC_STYLE_CONFIG # 引入 DEFAULT_CONFIG
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.utils.timer_calculater import Timer # <--- Import Timer
from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator
from src.do_tool.tool_use import ToolUser
from ..chat.message_sender import message_manager # <-- Import the global manager
from src.plugins.chat.emoji_manager import emoji_manager
# --- End import ---
INITIAL_DURATION = 60.0
# 定义日志配置 (使用 loguru 格式)
interest_log_config = LogConfig(
console_format=PFC_STYLE_CONFIG["console_format"], # 使用默认控制台格式
file_format=PFC_STYLE_CONFIG["file_format"], # 使用默认文件格式
)
logger = get_module_logger("HeartFCLoop", config=interest_log_config) # Logger Name Changed
# Forward declaration for type hinting
if TYPE_CHECKING:
# Keep this if HeartFCController methods are still needed elsewhere,
# but the instance variable will be removed from HeartFChatting
# from .heartFC_controler import HeartFCController
from src.heart_flow.heartflow import SubHeartflow, heartflow # <-- 同时导入 heartflow 实例用于类型检查
PLANNER_TOOL_DEFINITION = [
{
"type": "function",
"function": {
"name": "decide_reply_action",
"description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["no_reply", "text_reply", "emoji_reply"],
"description": "决定采取的行动:'no_reply'(不回复), 'text_reply'(文本回复, 可选附带表情) 或 'emoji_reply'(仅表情回复)。",
},
"reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
"emoji_query": {
"type": "string",
"description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
},
},
"required": ["action", "reasoning"],
},
},
}
]
class HeartFChatting:
"""
管理一个连续的Plan-Replier-Sender循环
用于在特定聊天流中生成回复。
其生命周期现在由其关联的 SubHeartflow 的 FOCUSED 状态控制。
"""
def __init__(self, chat_id: str):
"""
HeartFChatting 初始化函数
参数:
chat_id: 聊天流唯一标识符(如stream_id)
"""
# 基础属性
self.stream_id: str = chat_id # 聊天流ID
self.chat_stream: Optional[ChatStream] = None # 关联的聊天流
self.sub_hf: SubHeartflow = None # 关联的子心流
# 初始化状态控制
self._initialized = False # 是否已初始化标志
self._processing_lock = asyncio.Lock() # 处理锁(确保单次Plan-Replier-Sender周期)
# 依赖注入存储
self.gpt_instance = HeartFCGenerator() # 文本回复生成器
self.tool_user = ToolUser() # 工具使用实例
# LLM规划器配置
self.planner_llm = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=1000,
request_type="action_planning", # 用于动作规划
)
# 循环控制内部状态
self._loop_active: bool = False # 循环是否正在运行
self._loop_task: Optional[asyncio.Task] = None # 主循环任务
def _get_log_prefix(self) -> str:
"""获取日志前缀,包含可读的流名称"""
stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id
return f"[{stream_name}]"
async def _initialize(self) -> bool:
"""
懒初始化以使用提供的标识符解析chat_stream和sub_hf。
确保实例已准备好处理触发器。
"""
if self._initialized:
return True
log_prefix = self._get_log_prefix() # 获取前缀
try:
self.chat_stream = chat_manager.get_stream(self.stream_id)
if not self.chat_stream:
logger.error(f"{log_prefix} 获取ChatStream失败。")
return False
# <-- 在这里导入 heartflow 实例
from src.heart_flow.heartflow import heartflow
self.sub_hf = heartflow.get_subheartflow(self.stream_id)
if not self.sub_hf:
logger.warning(f"{log_prefix} 获取SubHeartflow失败。一些功能可能受限。")
self._initialized = True
logger.info(f"麦麦感觉到了激发了HeartFChatting{log_prefix} 初始化成功。")
return True
except Exception as e:
logger.error(f"{log_prefix} 初始化失败: {e}")
logger.error(traceback.format_exc())
return False
async def start(self):
"""
显式尝试启动 HeartFChatting 的主循环。
如果循环未激活,则启动循环。
"""
log_prefix = self._get_log_prefix()
if not self._initialized:
if not await self._initialize():
logger.error(f"{log_prefix} 无法启动循环: 初始化失败。")
return
logger.info(f"{log_prefix} 尝试显式启动循环...")
await self._start_loop_if_needed()
async def _start_loop_if_needed(self):
"""检查是否需要启动主循环,如果未激活则启动。"""
log_prefix = self._get_log_prefix()
should_start_loop = False
# 直接检查是否激活,无需检查计时器
if not self._loop_active:
should_start_loop = True
self._loop_active = True # 标记为活动,防止重复启动
if should_start_loop:
# 检查是否已有任务在运行(理论上不应该,因为 _loop_active=False
if self._loop_task and not self._loop_task.done():
logger.warning(f"{log_prefix} 发现之前的循环任务仍在运行(不符合预期)。取消旧任务。")
self._loop_task.cancel()
try:
# 等待旧任务确实被取消
await asyncio.wait_for(self._loop_task, timeout=0.5)
except (asyncio.CancelledError, asyncio.TimeoutError):
pass # 忽略取消或超时错误
self._loop_task = None # 清理旧任务引用
logger.info(f"{log_prefix} 循环未激活,启动主循环...")
# 创建新的循环任务
self._loop_task = asyncio.create_task(self._run_pf_loop())
# 添加完成回调
self._loop_task.add_done_callback(self._handle_loop_completion)
# else:
# logger.trace(f"{log_prefix} 不需要启动循环(已激活)") # 可以取消注释以进行调试
def _handle_loop_completion(self, task: asyncio.Task):
"""当 _run_pf_loop 任务完成时执行的回调。"""
log_prefix = self._get_log_prefix()
try:
exception = task.exception()
if exception:
logger.error(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(异常): {exception}")
logger.error(traceback.format_exc()) # Log full traceback for exceptions
else:
# Loop completing normally now means it was cancelled/shutdown externally
logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天 (外部停止)")
except asyncio.CancelledError:
logger.info(f"{log_prefix} HeartFChatting: 麦麦脱离了聊天(任务取消)")
finally:
self._loop_active = False
self._loop_task = None
if self._processing_lock.locked():
logger.warning(f"{log_prefix} HeartFChatting: 处理锁在循环结束时仍被锁定,强制释放。")
self._processing_lock.release()
async def _run_pf_loop(self):
"""
主循环,持续进行计划并可能回复消息,直到被外部取消。
管理每个循环周期的处理锁。
"""
log_prefix = self._get_log_prefix()
logger.info(f"{log_prefix} HeartFChatting: 麦麦打算好好聊聊 (进入专注模式)")
try:
thinking_id = ""
while True: # Loop indefinitely until cancelled
cycle_timers = {} # <--- Initialize timers dict for this cycle
# Access MessageManager directly
if message_manager.check_if_sending_message_exist(self.stream_id, thinking_id):
# logger.info(f"{log_prefix} HeartFChatting: 麦麦还在发消息,等会再规划")
await asyncio.sleep(1)
continue
else:
# logger.info(f"{log_prefix} HeartFChatting: 麦麦不发消息了,开始规划")
pass
# 记录循环周期开始时间,用于计时和休眠计算
loop_cycle_start_time = time.monotonic()
action_taken_this_cycle = False
acquired_lock = False
planner_start_db_time = 0.0 # 初始化
try:
with Timer("Total Cycle", cycle_timers) as _total_timer: # <--- Start total cycle timer
# Use try_acquire pattern or timeout?
await self._processing_lock.acquire()
acquired_lock = True
# logger.debug(f"{log_prefix} HeartFChatting: 循环获取到处理锁")
# 在规划前记录数据库时间戳
planner_start_db_time = time.time()
# --- Planner --- #
planner_result = {}
with Timer("Planner", cycle_timers): # <--- Start Planner timer
planner_result = await self._planner()
action = planner_result.get("action", "error")
reasoning = planner_result.get("reasoning", "Planner did not provide reasoning.")
emoji_query = planner_result.get("emoji_query", "")
# current_mind = planner_result.get("current_mind", "[Mind unavailable]")
# send_emoji_from_tools = planner_result.get("send_emoji_from_tools", "") # Emoji from tools
observed_messages = planner_result.get("observed_messages", [])
llm_error = planner_result.get("llm_error", False)
if llm_error:
logger.error(f"{log_prefix} Planner LLM 失败,跳过本周期回复尝试。理由: {reasoning}")
# Optionally add a longer sleep?
action_taken_this_cycle = False # Ensure no action is counted
# Continue to sleep logic
elif action == "text_reply":
logger.debug(f"{log_prefix} HeartFChatting: 麦麦决定回复文本. 理由: {reasoning}")
action_taken_this_cycle = True
anchor_message = await self._get_anchor_message(observed_messages)
if not anchor_message:
logger.error(f"{log_prefix} 循环: 无法获取锚点消息用于回复. 跳过周期.")
else:
# --- Create Thinking Message (Moved) ---
thinking_id = await self._create_thinking_message(anchor_message)
if not thinking_id:
logger.error(f"{log_prefix} 循环: 无法创建思考ID. 跳过周期.")
else:
replier_result = None
try:
# --- Replier Work --- #
with Timer("Replier", cycle_timers): # <--- Start Replier timer
replier_result = await self._replier_work(
anchor_message=anchor_message,
thinking_id=thinking_id,
reason=reasoning,
)
except Exception as e_replier:
logger.error(f"{log_prefix} 循环: 回复器工作失败: {e_replier}")
# self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
if replier_result:
# --- Sender Work --- #
try:
with Timer("Sender", cycle_timers): # <--- Start Sender timer
await self._sender(
thinking_id=thinking_id,
anchor_message=anchor_message,
response_set=replier_result,
send_emoji=emoji_query,
)
# logger.info(f"{log_prefix} 循环: 发送器完成成功.")
except Exception as e_sender:
logger.error(f"{log_prefix} 循环: 发送器失败: {e_sender}")
# _sender should handle cleanup, but double check
# self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
else:
logger.warning(f"{log_prefix} 循环: 回复器未产生结果. 跳过发送.")
# self._cleanup_thinking_message(thinking_id) <-- Remove cleanup call
elif action == "emoji_reply":
logger.info(
f"{log_prefix} HeartFChatting: 麦麦决定回复表情 ('{emoji_query}'). 理由: {reasoning}"
)
action_taken_this_cycle = True
anchor = await self._get_anchor_message(observed_messages)
if anchor:
try:
# --- Handle Emoji (Moved) --- #
with Timer("Emoji Handler", cycle_timers): # <--- Start Emoji timer
await self._handle_emoji(anchor, [], emoji_query)
except Exception as e_emoji:
logger.error(f"{log_prefix} 循环: 发送表情失败: {e_emoji}")
else:
logger.warning(f"{log_prefix} 循环: 无法发送表情, 无法获取锚点.")
action_taken_this_cycle = True # 即使发送失败Planner 也决策了动作
elif action == "no_reply":
logger.info(f"{log_prefix} HeartFChatting: 麦麦决定不回复. 原因: {reasoning}")
action_taken_this_cycle = False # 标记为未执行动作
# --- 新增:等待新消息 ---
logger.debug(f"{log_prefix} HeartFChatting: 开始等待新消息 (自 {planner_start_db_time})...")
observation = None
if self.sub_hf:
observation = self.sub_hf._get_primary_observation()
if observation:
with Timer("Wait New Msg", cycle_timers): # <--- Start Wait timer
wait_start_time = time.monotonic()
while True:
# Removed timer check within wait loop
# async with self._timer_lock:
# if self._loop_timer <= 0:
# logger.info(f"{log_prefix} HeartFChatting: 等待新消息时计时器耗尽。")
# break # 计时器耗尽,退出等待
# 检查是否有新消息
has_new = await observation.has_new_messages_since(planner_start_db_time)
if has_new:
logger.info(f"{log_prefix} HeartFChatting: 检测到新消息,结束等待。")
break # 收到新消息,退出等待
# 检查等待是否超时(例如,防止无限等待)
if time.monotonic() - wait_start_time > 60: # 等待60秒示例
logger.warning(f"{log_prefix} HeartFChatting: 等待新消息超时60秒")
break # 超时退出
# 等待一段时间再检查
try:
await asyncio.sleep(1.5) # 检查间隔
except asyncio.CancelledError:
logger.info(f"{log_prefix} 等待新消息的 sleep 被中断。")
raise # 重新抛出取消错误,以便外层循环处理
else:
logger.warning(
f"{log_prefix} HeartFChatting: 无法获取 Observation 实例,无法等待新消息。"
)
# --- 等待结束 ---
elif action == "error": # Action specifically set to error by planner
logger.error(f"{log_prefix} HeartFChatting: Planner返回错误状态. 原因: {reasoning}")
action_taken_this_cycle = False
else: # Unknown action from planner
logger.warning(
f"{log_prefix} HeartFChatting: Planner返回未知动作 '{action}'. 原因: {reasoning}"
)
action_taken_this_cycle = False
# --- Print Timer Results --- #
if cycle_timers: # 先检查cycle_timers是否非空
timer_strings = []
for name, elapsed in cycle_timers.items():
# 直接格式化存储在字典中的浮点数 elapsed
formatted_time = f"{elapsed * 1000:.2f}毫秒" if elapsed < 1 else f"{elapsed:.2f}"
timer_strings.append(f"{name}: {formatted_time}")
if timer_strings: # 如果有有效计时器数据才打印
logger.debug(f"{log_prefix} 该次决策耗时: {'; '.join(timer_strings)}")
# --- Timer Decrement Removed --- #
cycle_duration = time.monotonic() - loop_cycle_start_time
except Exception as e_cycle:
logger.error(f"{log_prefix} 循环周期执行时发生错误: {e_cycle}")
logger.error(traceback.format_exc())
if acquired_lock and self._processing_lock.locked():
self._processing_lock.release()
acquired_lock = False
logger.warning(f"{log_prefix} 由于循环周期中的错误释放了处理锁.")
finally:
if acquired_lock:
self._processing_lock.release()
# logger.trace(f"{log_prefix} 循环释放了处理锁.") # Reduce noise
# --- Timer Decrement Logging Removed ---
# async with self._timer_lock:
# self._loop_timer -= cycle_duration
# # Log timer decrement less aggressively
# if cycle_duration > 0.1 or not action_taken_this_cycle:
# logger.debug(
# f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s. 剩余时间: {self._loop_timer:.1f}s."
# )
if cycle_duration > 0.1:
logger.debug(f"{log_prefix} HeartFChatting: 周期耗时 {cycle_duration:.2f}s.")
# --- Delay --- #
try:
sleep_duration = 0.0
if not action_taken_this_cycle and cycle_duration < 1.5:
sleep_duration = 1.5 - cycle_duration
elif cycle_duration < 0.2: # Keep minimal sleep even after action
sleep_duration = 0.2
if sleep_duration > 0:
# logger.debug(f"{log_prefix} Sleeping for {sleep_duration:.2f}s")
await asyncio.sleep(sleep_duration)
except asyncio.CancelledError:
logger.info(f"{log_prefix} Sleep interrupted, loop likely cancelling.")
break # Exit loop immediately on cancellation
except asyncio.CancelledError:
logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环被取消了")
except Exception as e_loop_outer:
logger.error(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环意外出错: {e_loop_outer}")
logger.error(traceback.format_exc())
finally:
# State reset is primarily handled by _handle_loop_completion callback
logger.info(f"{log_prefix} HeartFChatting: 麦麦的聊天主循环结束。")
async def _planner(self) -> Dict[str, Any]:
"""
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
"""
log_prefix = self._get_log_prefix()
observed_messages: List[dict] = []
tool_result_info = {}
get_mid_memory_id = []
# send_emoji_from_tools = "" # Emoji suggested by tools
current_mind: Optional[str] = None
llm_error = False # Flag for LLM failure
# --- Ensure SubHeartflow is available ---
if not self.sub_hf:
# Attempt to re-fetch if missing (might happen if initialization order changes)
self.sub_hf = heartflow.get_subheartflow(self.stream_id)
if not self.sub_hf:
logger.error(f"{log_prefix}[Planner] SubHeartflow is not available. Cannot proceed.")
return {
"action": "error",
"reasoning": "SubHeartflow unavailable",
"llm_error": True,
"observed_messages": [],
}
try:
# Access observation via self.sub_hf
observation = self.sub_hf._get_primary_observation()
await observation.observe()
observed_messages = observation.talking_message
observed_messages_str = observation.talking_message_str
except Exception as e:
logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}")
# Handle error gracefully, maybe return an error state
observed_messages_str = "[Error getting observation]"
# Consider returning error here if observation is critical
# --- 结束获取观察信息 --- #
# --- (Moved from _replier_work) 1. 思考前使用工具 --- #
try:
# Access tool_user directly
tool_result = await self.tool_user.use_tool(
message_txt=observed_messages_str,
chat_stream=self.chat_stream,
observation=self.sub_hf._get_primary_observation(),
)
if tool_result.get("used_tools", False):
tool_result_info = tool_result.get("structured_info", {})
logger.debug(f"{log_prefix}[Planner] 规划前工具结果: {tool_result_info}")
get_mid_memory_id = [
mem["content"] for mem in tool_result_info.get("mid_chat_mem", []) if "content" in mem
]
except Exception as e_tool:
logger.error(f"{log_prefix}[Planner] 规划前工具使用失败: {e_tool}")
# --- 结束工具使用 --- #
# --- (Moved from _replier_work) 2. SubHeartflow 思考 --- #
try:
current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply(
extra_info=tool_result_info,
obs_id=get_mid_memory_id,
)
# logger.debug(f"{log_prefix}[Planner] SubHF Mind: {current_mind}")
except Exception as e_subhf:
logger.error(f"{log_prefix}[Planner] SubHeartflow 思考失败: {e_subhf}")
current_mind = "[思考时出错]"
# --- 结束 SubHeartflow 思考 --- #
# --- 使用 LLM 进行决策 --- #
action = "no_reply" # Default action
emoji_query = "" # Default emoji query (used if action is emoji_reply or text_reply with emoji)
reasoning = "默认决策或获取决策失败"
try:
prompt = await self._build_planner_prompt(observed_messages_str, current_mind)
payload = {
"model": self.planner_llm.model_name,
"messages": [{"role": "user", "content": prompt}],
"tools": PLANNER_TOOL_DEFINITION,
"tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}},
}
response = await self.planner_llm._execute_request(
endpoint="/chat/completions", payload=payload, prompt=prompt
)
if len(response) == 3:
_, _, tool_calls = response
if tool_calls and isinstance(tool_calls, list) and len(tool_calls) > 0:
tool_call = tool_calls[0]
if (
tool_call.get("type") == "function"
and tool_call.get("function", {}).get("name") == "decide_reply_action"
):
try:
arguments = json.loads(tool_call["function"]["arguments"])
action = arguments.get("action", "no_reply")
reasoning = arguments.get("reasoning", "未提供理由")
# Planner explicitly provides emoji query if action is emoji_reply or text_reply wants emoji
emoji_query = arguments.get("emoji_query", "")
logger.debug(
f"{log_prefix}[Planner] LLM Prompt: {prompt}\n决策: {action}, 理由: {reasoning}, EmojiQuery: '{emoji_query}'"
)
except json.JSONDecodeError as json_e:
logger.error(
f"{log_prefix}[Planner] 解析工具参数失败: {json_e}. Args: {tool_call['function'].get('arguments')}"
)
action = "error"
reasoning = "工具参数解析失败"
llm_error = True
except Exception as parse_e:
logger.error(f"{log_prefix}[Planner] 处理工具参数时出错: {parse_e}")
action = "error"
reasoning = "处理工具参数时出错"
llm_error = True
else:
logger.warning(
f"{log_prefix}[Planner] LLM 未按预期调用 'decide_reply_action' 工具。Tool calls: {tool_calls}"
)
action = "error"
reasoning = "LLM未调用预期工具"
llm_error = True
else:
logger.warning(f"{log_prefix}[Planner] LLM 响应中未包含有效的工具调用。Tool calls: {tool_calls}")
action = "error"
reasoning = "LLM响应无工具调用"
llm_error = True
else:
logger.warning(f"{log_prefix}[Planner] LLM 未返回预期的工具调用响应。Response parts: {len(response)}")
action = "error"
reasoning = "LLM响应格式错误"
llm_error = True
except Exception as llm_e:
logger.error(f"{log_prefix}[Planner] Planner LLM 调用失败: {llm_e}")
# logger.error(traceback.format_exc()) # Maybe too verbose for loop?
action = "error"
reasoning = f"LLM 调用失败: {llm_e}"
llm_error = True
# --- 结束 LLM 决策 --- #
return {
"action": action,
"reasoning": reasoning,
"emoji_query": emoji_query, # Explicit query from Planner/LLM
"current_mind": current_mind,
# "send_emoji_from_tools": send_emoji_from_tools, # Emoji suggested by tools (used as fallback)
"observed_messages": observed_messages,
"llm_error": llm_error,
}
async def _get_anchor_message(self, observed_messages: List[dict]) -> Optional[MessageRecv]:
"""
重构观察到的最后一条消息作为回复的锚点,
如果重构失败或观察为空,则创建一个占位符。
"""
try:
# --- Create Placeholder --- #
placeholder_id = f"mid_pf_{int(time.time() * 1000)}"
placeholder_user = UserInfo(
user_id="system_trigger", user_nickname="System Trigger", platform=self.chat_stream.platform
)
placeholder_msg_info = BaseMessageInfo(
message_id=placeholder_id,
platform=self.chat_stream.platform,
group_info=self.chat_stream.group_info,
user_info=placeholder_user,
time=time.time(),
)
placeholder_msg_dict = {
"message_info": placeholder_msg_info.to_dict(),
"processed_plain_text": "[System Trigger Context]",
"raw_message": "",
"time": placeholder_msg_info.time,
}
anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream)
logger.info(
f"{self._get_log_prefix()} Created placeholder anchor message: ID={anchor_message.message_info.message_id}"
)
return anchor_message
except Exception as e:
logger.error(f"{self._get_log_prefix()} Error getting/creating anchor message: {e}")
logger.error(traceback.format_exc())
return None
# --- 发送器 (Sender) --- #
async def _sender(
self,
thinking_id: str,
anchor_message: MessageRecv,
response_set: List[str],
send_emoji: str, # Emoji query decided by planner or tools
):
"""
发送器 (Sender): 使用本类的方法发送生成的回复。
处理相关的操作,如发送表情和更新关系。
"""
log_prefix = self._get_log_prefix()
first_bot_msg: Optional[MessageSending] = None
# 尝试发送回复消息
first_bot_msg = await self._send_response_messages(anchor_message, response_set, thinking_id)
if first_bot_msg:
# --- 处理关联表情(如果指定) --- #
if send_emoji:
logger.info(f"{log_prefix}[Sender-{thinking_id}] 正在发送关联表情: '{send_emoji}'")
# 优先使用first_bot_msg作为锚点否则回退到原始锚点
emoji_anchor = first_bot_msg if first_bot_msg else anchor_message
await self._handle_emoji(emoji_anchor, response_set, send_emoji)
else:
# logger.warning(f"{log_prefix}[Sender-{thinking_id}] 发送回复失败(_send_response_messages返回None)。思考消息{thinking_id}可能已被移除。")
# 无需清理因为_send_response_messages返回None意味着已处理/已删除
raise RuntimeError("发送回复失败_send_response_messages返回None")
async def shutdown(self):
"""
Gracefully shuts down the HeartFChatting instance by cancelling the active loop task.
"""
log_prefix = self._get_log_prefix()
logger.info(f"{log_prefix} Shutting down HeartFChatting...")
if self._loop_task and not self._loop_task.done():
logger.info(f"{log_prefix} Cancelling active PF loop task.")
self._loop_task.cancel()
try:
await asyncio.wait_for(self._loop_task, timeout=1.0) # Shorter timeout?
except asyncio.CancelledError:
logger.info(f"{log_prefix} PF loop task cancelled successfully.")
except asyncio.TimeoutError:
logger.warning(f"{log_prefix} Timeout waiting for PF loop task cancellation.")
except Exception as e:
logger.error(f"{log_prefix} Error during loop task cancellation: {e}")
else:
logger.info(f"{log_prefix} No active PF loop task found to cancel.")
self._loop_active = False
self._loop_task = None
if self._processing_lock.locked():
logger.warning(f"{log_prefix} Releasing processing lock during shutdown.")
self._processing_lock.release()
logger.info(f"{log_prefix} HeartFChatting shutdown complete.")
async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str]) -> str:
"""构建 Planner LLM 的提示词"""
prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生正在QQ聊天正在决定是否以及如何回应当前的聊天。\n"
if observed_messages_str:
prompt += "观察到的最新聊天内容如下 (最近的消息在最后)\n---\n"
prompt += observed_messages_str
prompt += "\n---"
else:
prompt += "当前没有观察到新的聊天内容。\n"
prompt += "\n看了以上内容,你产生的内心想法是:"
if current_mind:
prompt += f"\n---\n{current_mind}\n---\n\n"
else:
prompt += " [没有特别的想法] \n\n"
prompt += (
"请结合你的内心想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。\n"
"决策依据:\n"
"1. 如果聊天内容无聊、与你无关、或者你的内心想法认为不适合回复(例如在讨论你不懂或不感兴趣的话题),选择 'no_reply'\n"
"2. 如果聊天内容值得回应,且适合用文字表达(参考你的内心想法),选择 'text_reply'。如果你有情绪想表达,想在文字后追加一个表达情绪的表情,请同时提供 'emoji_query' (例如:'开心的''惊讶的')。\n"
"3. 如果聊天内容或你的内心想法适合用一个表情来回应(例如表示赞同、惊讶、无语等),选择 'emoji_reply' 并提供表情主题 'emoji_query'\n"
"4. 如果最后一条消息是你自己发的,并且之后没有人回复你,通常选择 'no_reply',除非有特殊原因需要追问。\n"
"5. 除非大家都在这么做,或者有特殊理由,否则不要重复别人刚刚说过的话或简单附和。\n"
"6. 表情包是用来表达情绪的,不要直接回复或评价别人的表情包,而是根据对话内容和情绪选择是否用表情回应。\n"
"7. 如果观察到的内容只有你自己的发言,选择 'no_reply'\n"
"8. 不要回复你自己的话,不要把自己的话当做别人说的。\n"
"必须调用 'decide_reply_action' 工具并提供 'action''reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'"
)
return prompt
# --- 回复器 (Replier) 的定义 --- #
async def _replier_work(
self,
reason: str,
anchor_message: MessageRecv,
thinking_id: str,
) -> Optional[List[str]]:
"""
回复器 (Replier): 核心逻辑用于生成回复。
"""
log_prefix = self._get_log_prefix()
response_set: Optional[List[str]] = None
try:
response_set = await self.gpt_instance.generate_response(
current_mind_info=self.sub_hf.current_mind,
reason=reason,
message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter)
thinking_id=thinking_id, # Pass thinking_id positionally
)
if not response_set:
logger.warning(f"{log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
return None
# --- 准备并返回结果 --- #
# logger.info(f"{log_prefix}[Replier-{thinking_id}] 成功生成了回复集: {' '.join(response_set)[:50]}...")
return response_set
except Exception as e:
logger.error(f"{log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}")
logger.error(traceback.format_exc())
return None
# --- Methods moved from HeartFCController start ---
async def _create_thinking_message(self, anchor_message: Optional[MessageRecv]) -> Optional[str]:
"""创建思考消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self._get_log_prefix()} 无法创建思考消息,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
messageinfo = anchor_message.message_info
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=messageinfo.platform,
)
thinking_time_point = round(time.time(), 2)
thinking_id = "mt" + str(thinking_time_point)
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,
)
# Access MessageManager directly
await message_manager.add_message(thinking_message)
return thinking_id
async def _send_response_messages(
self, anchor_message: Optional[MessageRecv], response_set: List[str], thinking_id: str
) -> Optional[MessageSending]:
"""发送回复消息 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self._get_log_prefix()} 无法发送回复,缺少有效的锚点消息或聊天流。")
return None
chat = anchor_message.chat_stream
# Access MessageManager directly
container = await message_manager.get_container(chat.stream_id)
thinking_message = None
# 移除思考消息
for msg in container.messages[:]: # Iterate over a copy
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
thinking_message = msg
container.messages.remove(msg) # Remove the message directly here
logger.debug(f"{self._get_log_prefix()} Removed thinking message {thinking_id} via iteration.")
break
if not thinking_message:
stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id # 获取流名称
logger.warning(f"[{stream_name}] {thinking_id},思考太久了,超时被移除")
return None
thinking_start_time = thinking_message.thinking_start_time
message_set = MessageSet(chat, thinking_id)
mark_head = False
first_bot_msg = None
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=anchor_message.message_info.platform,
)
for msg_text in response_set:
message_segment = Seg(type="text", data=msg_text)
bot_message = MessageSending(
message_id=thinking_id, # 使用 thinking_id 作为批次标识
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info, # 发送给锚点消息的用户
message_segment=message_segment,
reply=anchor_message, # 回复锚点消息
is_head=not mark_head,
is_emoji=False,
thinking_start_time=thinking_start_time,
)
if not mark_head:
mark_head = True
first_bot_msg = bot_message
message_set.add_message(bot_message)
# Access MessageManager directly
await message_manager.add_message(message_set)
return first_bot_msg
async def _handle_emoji(self, anchor_message: Optional[MessageRecv], response_set: List[str], send_emoji: str = ""):
"""处理表情包 (尝试锚定到 anchor_message)"""
if not anchor_message or not anchor_message.chat_stream:
logger.error(f"{self._get_log_prefix()} 无法处理表情包,缺少有效的锚点消息或聊天流。")
return
chat = anchor_message.chat_stream
if send_emoji:
emoji_raw = await emoji_manager.get_emoji_for_text(send_emoji)
else:
emoji_text_source = "".join(response_set) if response_set else ""
emoji_raw = await emoji_manager.get_emoji_for_text(emoji_text_source)
if emoji_raw:
emoji_path, _description = emoji_raw
emoji_cq = image_path_to_base64(emoji_path)
thinking_time_point = round(time.time(), 2)
message_segment = Seg(type="emoji", data=emoji_cq)
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=anchor_message.message_info.platform,
)
bot_message = MessageSending(
message_id="me" + str(thinking_time_point), # 使用不同的 ID 前缀?
chat_stream=chat,
bot_user_info=bot_user_info,
sender_info=anchor_message.message_info.user_info,
message_segment=message_segment,
reply=anchor_message, # 回复锚点消息
is_head=False,
is_emoji=True,
)
# Access MessageManager directly
await message_manager.add_message(bot_message)

View File

@@ -0,0 +1,215 @@
from typing import List, Optional
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from ..chat.message import MessageRecv
from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from ..utils.timer_calculater import Timer
from src.plugins.moods.moods import MoodManager
# 定义日志配置
llm_config = LogConfig(
# 使用消息发送专用样式
console_format=LLM_STYLE_CONFIG["console_format"],
file_format=LLM_STYLE_CONFIG["file_format"],
)
logger = get_module_logger("llm_generator", config=llm_config)
class HeartFCGenerator:
def __init__(self):
self.model_normal = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=256,
request_type="response_heartflow",
)
self.model_sum = LLMRequest(
model=global_config.llm_summary_by_topic, temperature=0.6, max_tokens=2000, request_type="relation"
)
self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model"
async def generate_response(
self,
current_mind_info: str,
reason: str,
message: MessageRecv,
thinking_id: str,
) -> Optional[List[str]]:
"""根据当前模型类型选择对应的生成函数"""
logger.info(
f"思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
)
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
with Timer() as t_generate_response:
current_model = self.model_normal
current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高
model_response = await self._generate_response_with_model(
current_mind_info, reason, message, current_model, thinking_id
)
if model_response:
logger.info(
f"{global_config.BOT_NICKNAME}的回复是:{model_response},生成回复时间: {t_generate_response.human_readable}"
)
model_processed_response = await self._process_response(model_response)
return model_processed_response
else:
logger.info(f"{self.current_model_type}思考,失败")
return None
async def _generate_response_with_model(
self, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str
) -> str:
sender_name = ""
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
sender_name = f"<{message.chat_stream.user_info.platform}:{message.chat_stream.user_info.user_id}:{message.chat_stream.user_info.user_nickname}:{message.chat_stream.user_info.user_cardname}>"
with Timer() as t_build_prompt:
prompt = await prompt_builder.build_prompt(
build_mode="focus",
reason=reason,
current_mind_info=current_mind_info,
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,
)
logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
)
except Exception:
logger.exception("生成回复时出错")
return None
return content
async def _get_emotion_tags(self, content: str, processed_plain_text: str):
"""提取情感标签,结合立场和情绪"""
try:
# 构建提示词,结合回复内容、被回复的内容以及立场分析
prompt = f"""
请严格根据以下对话内容,完成以下任务:
1. 判断回复者对被回复者观点的直接立场:
- "支持":明确同意或强化被回复者观点
- "反对":明确反驳或否定被回复者观点
- "中立":不表达明确立场或无关回应
2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签
3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒"
4. 考虑回复者的人格设定为{global_config.personality_core}
对话示例:
被回复「A就是笨」
回复「A明明很聪明」 → 反对-愤怒
当前对话:
被回复:「{processed_plain_text}
回复:「{content}
输出要求:
- 只需输出"立场-情绪"结果,不要解释
- 严格基于文字直接表达的对立关系判断
"""
# 调用模型生成结果
result, _, _ = await self.model_sum.generate_response(prompt)
result = result.strip()
# 解析模型输出的结果
if "-" in result:
stance, emotion = result.split("-", 1)
valid_stances = ["支持", "反对", "中立"]
valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"]
if stance in valid_stances and emotion in valid_emotions:
return stance, emotion # 返回有效的立场-情绪组合
else:
logger.debug(f"无效立场-情感组合:{result}")
return "中立", "平静" # 默认返回中立-平静
else:
logger.debug(f"立场-情感格式错误:{result}")
return "中立", "平静" # 格式错误时返回默认值
except Exception as e:
logger.debug(f"获取情感标签时出错: {e}")
return "中立", "平静" # 出错时返回默认值
async def _get_emotion_tags_with_reason(self, content: str, processed_plain_text: str, reason: str):
"""提取情感标签,结合立场和情绪"""
try:
# 构建提示词,结合回复内容、被回复的内容以及立场分析
prompt = f"""
请严格根据以下对话内容,完成以下任务:
1. 判断回复者对被回复者观点的直接立场:
- "支持":明确同意或强化被回复者观点
- "反对":明确反驳或否定被回复者观点
- "中立":不表达明确立场或无关回应
2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签
3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒"
4. 考虑回复者的人格设定为{global_config.personality_core}
对话示例:
被回复「A就是笨」
回复「A明明很聪明」 → 反对-愤怒
当前对话:
被回复:「{processed_plain_text}
回复:「{content}
原因:「{reason}
输出要求:
- 只需输出"立场-情绪"结果,不要解释
- 严格基于文字直接表达的对立关系判断
"""
# 调用模型生成结果
result, _, _ = await self.model_sum.generate_response(prompt)
result = result.strip()
# 解析模型输出的结果
if "-" in result:
stance, emotion = result.split("-", 1)
valid_stances = ["支持", "反对", "中立"]
valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"]
if stance in valid_stances and emotion in valid_emotions:
return stance, emotion # 返回有效的立场-情绪组合
else:
logger.debug(f"无效立场-情感组合:{result}")
return "中立", "平静" # 默认返回中立-平静
else:
logger.debug(f"立场-情感格式错误:{result}")
return "中立", "平静" # 格式错误时返回默认值
except Exception as e:
logger.debug(f"获取情感标签时出错: {e}")
return "中立", "平静" # 出错时返回默认值
async def _process_response(self, content: str) -> List[str]:
"""处理响应内容,返回处理后的内容和情感标签"""
if not content:
return None
processed_response = process_llm_response(content)
# print(f"得到了处理后的llm返回{processed_response}")
return processed_response

View File

@@ -0,0 +1,214 @@
import time
import traceback
from ..memory_system.Hippocampus import HippocampusManager
from ...config.config import global_config
from ..chat.message import MessageRecv
from ..storage.storage import MessageStorage
from ..chat.utils import is_mentioned_bot_in_message
from ..message import Seg
from src.heart_flow.heartflow import heartflow
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from ..chat.chat_stream import chat_manager
from ..chat.message_buffer import message_buffer
from ..utils.timer_calculater import Timer
from src.plugins.person_info.relationship_manager import relationship_manager
# 定义日志配置
processor_config = LogConfig(
console_format=CHAT_STYLE_CONFIG["console_format"],
file_format=CHAT_STYLE_CONFIG["file_format"],
)
logger = get_module_logger("heartflow_processor", config=processor_config)
class HeartFCProcessor:
def __init__(self):
self.storage = MessageStorage()
async def process_message(self, message_data: str) -> None:
"""处理接收到的原始消息数据,完成消息解析、缓冲、过滤、存储、兴趣度计算与更新等核心流程。
此函数是消息处理的核心入口,负责接收原始字符串格式的消息数据,并将其转化为结构化的 `MessageRecv` 对象。
主要执行步骤包括:
1. 解析 `message_data` 为 `MessageRecv` 对象,提取用户信息、群组信息等。
2. 将消息加入 `message_buffer` 进行缓冲处理,以应对消息轰炸或者某些人一条消息分几次发等情况。
3. 获取或创建对应的 `chat_stream` 和 `subheartflow` 实例,用于管理会话状态和心流。
4. 对消息内容进行初步处理(如提取纯文本)。
5. 应用全局配置中的过滤词和正则表达式,过滤不符合规则的消息。
6. 查询消息缓冲结果,如果消息被缓冲器拦截(例如,判断为消息轰炸的一部分),则中止后续处理。
7. 对于通过缓冲的消息,将其存储到 `MessageStorage` 中。
8. 调用海马体(`HippocampusManager`)计算消息内容的记忆激活率。(这部分算法后续会进行优化)
9. 根据是否被提及(@)和记忆激活率,计算最终的兴趣度增量。(提及的额外兴趣增幅)
10. 使用计算出的增量更新 `InterestManager` 中对应会话的兴趣度。
11. 记录处理后的消息信息及当前的兴趣度到日志。
注意:此函数本身不负责生成和发送回复。回复的决策和生成逻辑被移至 `HeartFC_Chat` 类中的监控任务,
该任务会根据 `InterestManager` 中的兴趣度变化来决定何时触发回复。
Args:
message_data: str: 从消息源接收到的原始消息字符串。
"""
timing_results = {} # 初始化 timing_results
message = None
try:
message = MessageRecv(message_data)
groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info
messageinfo = message.message_info
# 消息加入缓冲池
await message_buffer.start_caching_messages(message)
# 创建聊天流
chat = await chat_manager.get_or_create_stream(
platform=messageinfo.platform,
user_info=userinfo,
group_info=groupinfo,
)
subheartflow = await heartflow.create_subheartflow(chat.stream_id)
message.update_chat_stream(chat)
await heartflow.create_subheartflow(chat.stream_id)
await message.process()
logger.trace(f"消息处理成功: {message.processed_plain_text}")
# 过滤词/正则表达式过滤
if self._check_ban_words(message.processed_plain_text, chat, userinfo) or self._check_ban_regex(
message.raw_message, chat, userinfo
):
return
# 查询缓冲器结果
buffer_result = await message_buffer.query_buffer_result(message)
# 处理缓冲器结果 (Bombing logic)
if not buffer_result:
f_type = "seglist"
if message.message_segment.type != "seglist":
f_type = message.message_segment.type
else:
if (
isinstance(message.message_segment.data, list)
and all(isinstance(x, Seg) for x in message.message_segment.data)
and len(message.message_segment.data) == 1
):
f_type = message.message_segment.data[0].type
if f_type == "text":
logger.debug(f"触发缓冲,消息:{message.processed_plain_text}")
elif f_type == "image":
logger.debug("触发缓冲,表情包/图片等待中")
elif f_type == "seglist":
logger.debug("触发缓冲,消息列表等待中")
return # 被缓冲器拦截,不生成回复
# ---- 只有通过缓冲的消息才进行存储和后续处理 ----
# 存储消息 (使用可能被缓冲器更新过的 message)
try:
await self.storage.store_message(message, chat)
logger.trace(f"存储成功 (通过缓冲后): {message.processed_plain_text}")
except Exception as e:
logger.error(f"存储消息失败: {e}")
logger.error(traceback.format_exc())
# 存储失败可能仍需考虑是否继续,暂时返回
return
# 激活度计算 (使用可能被缓冲器更新过的 message.processed_plain_text)
is_mentioned, _ = is_mentioned_bot_in_message(message)
interested_rate = 0.0 # 默认值
try:
with Timer("记忆激活", timing_results):
interested_rate = await HippocampusManager.get_instance().get_activate_from_text(
message.processed_plain_text,
fast_retrieval=True, # 使用更新后的文本
)
logger.trace(f"记忆激活率 (通过缓冲后): {interested_rate:.2f}")
except Exception as e:
logger.error(f"计算记忆激活率失败: {e}")
logger.error(traceback.format_exc())
# --- 修改:兴趣度更新逻辑 --- #
if is_mentioned:
interest_increase_on_mention = 1
mentioned_boost = interest_increase_on_mention # 从配置获取提及增加值
interested_rate += mentioned_boost
# 更新兴趣度 (调用 SubHeartflow 的方法)
current_time = time.time()
await subheartflow.interest_chatting.increase_interest(current_time, value=interested_rate)
# 添加到 SubHeartflow 的 interest_dict给normal_chat处理
await subheartflow.add_interest_dict_entry(message, interested_rate, is_mentioned)
# 打印消息接收和处理信息
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
logger.info(
f"[{current_time}][{mes_name}]"
f"{message.message_info.user_info.user_nickname}:"
f"{message.processed_plain_text}"
f"[兴趣度: {interested_rate:.2f}]"
)
try:
is_known = await relationship_manager.is_known_some_one(
message.message_info.platform, message.message_info.user_info.user_id
)
if not is_known:
logger.info(f"首次认识用户: {message.message_info.user_info.user_nickname}")
await relationship_manager.first_knowing_some_one(
message.message_info.platform,
message.message_info.user_info.user_id,
message.message_info.user_info.user_nickname,
message.message_info.user_info.user_cardname or message.message_info.user_info.user_nickname,
"",
)
else:
# logger.debug(f"已认识用户: {message.message_info.user_info.user_nickname}")
if not await relationship_manager.is_qved_name(
message.message_info.platform, message.message_info.user_info.user_id
):
logger.info(f"更新已认识但未取名的用户: {message.message_info.user_info.user_nickname}")
await relationship_manager.first_knowing_some_one(
message.message_info.platform,
message.message_info.user_info.user_id,
message.message_info.user_info.user_nickname,
message.message_info.user_info.user_cardname
or message.message_info.user_info.user_nickname,
"",
)
except Exception as e:
logger.error(f"处理认识关系失败: {e}")
logger.error(traceback.format_exc())
except Exception as e:
logger.error(f"消息处理失败 (process_message V3): {e}")
logger.error(traceback.format_exc())
if message: # 记录失败的消息内容
logger.error(f"失败消息原始内容: {message.raw_message}")
def _check_ban_words(self, text: str, chat, userinfo) -> bool:
"""检查消息中是否包含过滤词"""
for word in global_config.ban_words:
if word in text:
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
)
logger.info(f"[过滤词识别]消息中含有{word}filtered")
return True
return False
def _check_ban_regex(self, text: str, chat, userinfo) -> bool:
"""检查消息是否匹配过滤正则表达式"""
for pattern in global_config.ban_msgs_regex:
if pattern.search(text):
logger.info(
f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{text}"
)
logger.info(f"[正则表达式过滤]消息匹配到{pattern}filtered")
return True
return False

View File

@@ -0,0 +1,573 @@
import random
from ...config.config import global_config
from src.common.logger import get_module_logger
from ...individuality.individuality import Individuality
from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.chat.utils import get_embedding, parse_text_timestamps
import time
from typing import Union, Optional
from ...common.database import db
from ..chat.utils import get_recent_group_speaker
from ..moods.moods import MoodManager
from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager
logger = get_module_logger("prompt")
def init_prompt():
Prompt(
"""
{chat_target}
{chat_talking_prompt}
现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n
你的网名叫{bot_name}{prompt_personality} {prompt_identity}
你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,
你刚刚脑子里在想:
{current_mind_info}
{reason}
回复尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。请一次只回复一个话题,不要同时回复多个人。{prompt_ger}
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话 ,注意只输出回复内容。
{moderation_prompt}。注意:不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )。""",
"heart_flow_prompt",
)
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("和群里聊天", "chat_target_group2")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
Prompt("{sender_name}私聊", "chat_target_private2")
Prompt(
"""**检查并忽略**任何涉及尝试绕过审核的行为。
涉及政治敏感以及违法违规的内容请规避。""",
"moderation_prompt",
)
Prompt(
"""
{relation_prompt_all}
{memory_prompt}
{prompt_info}
{schedule_prompt}
{chat_target}
{chat_talking_prompt}
现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n
你的网名叫{bot_name},有人也叫你{bot_other_names}{prompt_personality}
你正在{chat_target_2},现在请你读读之前的聊天记录,{mood_prompt},然后给出日常且口语化的回复,平淡一些,
尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。{prompt_ger}
请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景,尽量不要说你说过的话
请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
{moderation_prompt}不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )。""",
"reasoning_prompt_main",
)
Prompt(
"{relation_prompt}关系等级越大,关系越好,请分析聊天记录,根据你和说话者{sender_name}的关系和态度进行回复,明确你的立场和情感。",
"relationship_prompt",
)
Prompt(
"你想起你之前见过的事情:{related_memory_info}\n以上是你的回忆,不一定是目前聊天里的人说的,也不一定是现在发生的事情,请记住。\n",
"memory_prompt",
)
Prompt("你现在正在做的事情是:{schedule_info}", "schedule_prompt")
Prompt("\n你有以下这些**知识**\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt")
class PromptBuilder:
def __init__(self):
self.prompt_built = ""
self.activate_messages = ""
async def build_prompt(
self, build_mode, reason, current_mind_info, message_txt: str, sender_name: str = "某人", chat_stream=None
) -> Optional[tuple[str, str]]:
if build_mode == "normal":
return await self._build_prompt_normal(chat_stream, message_txt, sender_name)
elif build_mode == "focus":
return await self._build_prompt_focus(reason, current_mind_info, chat_stream, message_txt, sender_name)
return None
async def _build_prompt_focus(
self, reason, current_mind_info, chat_stream, message_txt: str, sender_name: str = "某人"
) -> tuple[str, str]:
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1)
prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1)
# 日程构建
# schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}'''
if chat_stream.group_info:
chat_in_group = True
else:
chat_in_group = False
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.MAX_CONTEXT_SIZE,
)
chat_talking_prompt = await build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
)
# 关键词检测与反应
keywords_reaction_prompt = ""
for rule in global_config.keywords_reaction_rules:
if rule.get("enable", False):
if any(keyword in message_txt.lower() for keyword in rule.get("keywords", [])):
logger.info(
f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}"
)
keywords_reaction_prompt += rule.get("reaction", "") + ""
else:
for pattern in rule.get("regex", []):
result = pattern.search(message_txt)
if result:
reaction = rule.get("reaction", "")
for name, content in result.groupdict().items():
reaction = reaction.replace(f"[{name}]", content)
logger.info(f"匹配到以下正则表达式:{pattern},触发反应:{reaction}")
keywords_reaction_prompt += reaction + ""
break
# 中文高手(新加的好玩功能)
prompt_ger = ""
if random.random() < 0.04:
prompt_ger += "你喜欢用倒装句"
if random.random() < 0.02:
prompt_ger += "你喜欢用反问句"
logger.debug("开始构建prompt")
prompt = await global_prompt_manager.format_prompt(
"heart_flow_prompt",
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
chat_talking_prompt=chat_talking_prompt,
sender_name=sender_name,
message_txt=message_txt,
bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality,
prompt_identity=prompt_identity,
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
current_mind_info=current_mind_info,
reason=reason,
keywords_reaction_prompt=keywords_reaction_prompt,
prompt_ger=prompt_ger,
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
)
prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
prompt = parse_text_timestamps(prompt, mode="lite")
return prompt
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance()
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
# 关系
who_chat_in_group = [
(chat_stream.user_info.platform, chat_stream.user_info.user_id, chat_stream.user_info.user_nickname)
]
who_chat_in_group += get_recent_group_speaker(
chat_stream.stream_id,
(chat_stream.user_info.platform, chat_stream.user_info.user_id),
limit=global_config.MAX_CONTEXT_SIZE,
)
relation_prompt = ""
for person in who_chat_in_group:
relation_prompt += await relationship_manager.build_relationship_info(person)
# relation_prompt_all = (
# f"{relation_prompt}关系等级越大,关系越好,请分析聊天记录,"
# f"根据你和说话者{sender_name}的关系和态度进行回复,明确你的立场和情感。"
# )
# 心情
mood_manager = MoodManager.get_instance()
mood_prompt = mood_manager.get_prompt()
# logger.info(f"心情prompt: {mood_prompt}")
# 调取记忆
memory_prompt = ""
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
text=message_txt, max_memory_num=2, max_memory_length=2, max_depth=3, fast_retrieval=False
)
related_memory_info = ""
if related_memory:
for memory in related_memory:
related_memory_info += memory[1]
# memory_prompt = f"你想起你之前见过的事情:{related_memory_info}。\n以上是你的回忆不一定是目前聊天里的人说的也不一定是现在发生的事情请记住。\n"
memory_prompt = await global_prompt_manager.format_prompt(
"memory_prompt", related_memory_info=related_memory_info
)
# 获取聊天上下文
if chat_stream.group_info:
chat_in_group = True
else:
chat_in_group = False
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_stream.stream_id,
timestamp=time.time(),
limit=global_config.MAX_CONTEXT_SIZE,
)
chat_talking_prompt = await build_readable_messages(
message_list_before_now,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
)
# 关键词检测与反应
keywords_reaction_prompt = ""
for rule in global_config.keywords_reaction_rules:
if rule.get("enable", False):
if any(keyword in message_txt.lower() for keyword in rule.get("keywords", [])):
logger.info(
f"检测到以下关键词之一:{rule.get('keywords', [])},触发反应:{rule.get('reaction', '')}"
)
keywords_reaction_prompt += rule.get("reaction", "") + ""
else:
for pattern in rule.get("regex", []):
result = pattern.search(message_txt)
if result:
reaction = rule.get("reaction", "")
for name, content in result.groupdict().items():
reaction = reaction.replace(f"[{name}]", content)
logger.info(f"匹配到以下正则表达式:{pattern},触发反应:{reaction}")
keywords_reaction_prompt += reaction + ""
break
# 中文高手(新加的好玩功能)
prompt_ger = ""
if random.random() < 0.04:
prompt_ger += "你喜欢用倒装句"
if random.random() < 0.02:
prompt_ger += "你喜欢用反问句"
if random.random() < 0.01:
prompt_ger += "你喜欢用文言文"
# 知识构建
start_time = time.time()
prompt_info = await self.get_prompt_info(message_txt, threshold=0.38)
if prompt_info:
# prompt_info = f"""\n你有以下这些**知识**\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n"""
prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=prompt_info)
end_time = time.time()
logger.debug(f"知识检索耗时: {(end_time - start_time):.3f}")
logger.debug("开始构建prompt")
if global_config.ENABLE_SCHEDULE_GEN:
schedule_prompt = await global_prompt_manager.format_prompt(
"schedule_prompt", schedule_info=bot_schedule.get_current_num_task(num=1, time_info=False)
)
else:
schedule_prompt = ""
prompt = await global_prompt_manager.format_prompt(
"reasoning_prompt_main",
relation_prompt_all=await global_prompt_manager.get_prompt_async("relationship_prompt"),
relation_prompt=relation_prompt,
sender_name=sender_name,
memory_prompt=memory_prompt,
prompt_info=prompt_info,
schedule_prompt=schedule_prompt,
chat_target=await global_prompt_manager.get_prompt_async("chat_target_group1")
if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private1"),
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private2"),
chat_talking_prompt=chat_talking_prompt,
message_txt=message_txt,
bot_name=global_config.BOT_NICKNAME,
bot_other_names="/".join(
global_config.BOT_ALIAS_NAMES,
),
prompt_personality=prompt_personality,
mood_prompt=mood_prompt,
keywords_reaction_prompt=keywords_reaction_prompt,
prompt_ger=prompt_ger,
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
)
return prompt
async def get_prompt_info_old(self, message: str, threshold: float):
start_time = time.time()
related_info = ""
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 1. 先从LLM获取主题类似于记忆系统的做法
topics = []
# try:
# # 先尝试使用记忆系统的方法获取主题
# hippocampus = HippocampusManager.get_instance()._hippocampus
# topic_num = min(5, max(1, int(len(message) * 0.1)))
# topics_response = await hippocampus.llm_topic_judge.generate_response(hippocampus.find_topic_llm(message, topic_num))
# # 提取关键词
# topics = re.findall(r"<([^>]+)>", topics_response[0])
# if not topics:
# topics = []
# else:
# topics = [
# topic.strip()
# for topic in ",".join(topics).replace("", ",").replace("、", ",").replace(" ", ",").split(",")
# if topic.strip()
# ]
# logger.info(f"从LLM提取的主题: {', '.join(topics)}")
# except Exception as e:
# logger.error(f"从LLM提取主题失败: {str(e)}")
# # 如果LLM提取失败使用jieba分词提取关键词作为备选
# words = jieba.cut(message)
# topics = [word for word in words if len(word) > 1][:5]
# logger.info(f"使用jieba提取的主题: {', '.join(topics)}")
# 如果无法提取到主题,直接使用整个消息
if not topics:
logger.info("未能提取到任何主题,使用整个消息进行查询")
embedding = await get_embedding(message, request_type="prompt_build")
if not embedding:
logger.error("获取消息嵌入向量失败")
return ""
related_info = self.get_info_from_db(embedding, limit=3, threshold=threshold)
logger.info(f"知识库检索完成,总耗时: {time.time() - start_time:.3f}")
return related_info
# 2. 对每个主题进行知识库查询
logger.info(f"开始处理{len(topics)}个主题的知识库查询")
# 优化批量获取嵌入向量减少API调用
embeddings = {}
topics_batch = [topic for topic in topics if len(topic) > 0]
if message: # 确保消息非空
topics_batch.append(message)
# 批量获取嵌入向量
embed_start_time = time.time()
for text in topics_batch:
if not text or len(text.strip()) == 0:
continue
try:
embedding = await get_embedding(text, request_type="prompt_build")
if embedding:
embeddings[text] = embedding
else:
logger.warning(f"获取'{text}'的嵌入向量失败")
except Exception as e:
logger.error(f"获取'{text}'的嵌入向量时发生错误: {str(e)}")
logger.info(f"批量获取嵌入向量完成,耗时: {time.time() - embed_start_time:.3f}")
if not embeddings:
logger.error("所有嵌入向量获取失败")
return ""
# 3. 对每个主题进行知识库查询
all_results = []
query_start_time = time.time()
# 首先添加原始消息的查询结果
if message in embeddings:
original_results = self.get_info_from_db(embeddings[message], limit=3, threshold=threshold, return_raw=True)
if original_results:
for result in original_results:
result["topic"] = "原始消息"
all_results.extend(original_results)
logger.info(f"原始消息查询到{len(original_results)}条结果")
# 然后添加每个主题的查询结果
for topic in topics:
if not topic or topic not in embeddings:
continue
try:
topic_results = self.get_info_from_db(embeddings[topic], limit=3, threshold=threshold, return_raw=True)
if topic_results:
# 添加主题标记
for result in topic_results:
result["topic"] = topic
all_results.extend(topic_results)
logger.info(f"主题'{topic}'查询到{len(topic_results)}条结果")
except Exception as e:
logger.error(f"查询主题'{topic}'时发生错误: {str(e)}")
logger.info(f"知识库查询完成,耗时: {time.time() - query_start_time:.3f}秒,共获取{len(all_results)}条结果")
# 4. 去重和过滤
process_start_time = time.time()
unique_contents = set()
filtered_results = []
for result in all_results:
content = result["content"]
if content not in unique_contents:
unique_contents.add(content)
filtered_results.append(result)
# 5. 按相似度排序
filtered_results.sort(key=lambda x: x["similarity"], reverse=True)
# 6. 限制总数量最多10条
filtered_results = filtered_results[:10]
logger.info(
f"结果处理完成,耗时: {time.time() - process_start_time:.3f}秒,过滤后剩余{len(filtered_results)}条结果"
)
# 7. 格式化输出
if filtered_results:
format_start_time = time.time()
grouped_results = {}
for result in filtered_results:
topic = result["topic"]
if topic not in grouped_results:
grouped_results[topic] = []
grouped_results[topic].append(result)
# 按主题组织输出
for topic, results in grouped_results.items():
related_info += f"【主题: {topic}\n"
for _i, result in enumerate(results, 1):
_similarity = result["similarity"]
content = result["content"].strip()
# 调试:为内容添加序号和相似度信息
# related_info += f"{i}. [{similarity:.2f}] {content}\n"
related_info += f"{content}\n"
related_info += "\n"
logger.info(f"格式化输出完成,耗时: {time.time() - format_start_time:.3f}")
logger.info(f"知识库检索总耗时: {time.time() - start_time:.3f}")
return related_info
async def get_prompt_info(self, message: str, threshold: float):
related_info = ""
start_time = time.time()
logger.debug(f"获取知识库内容,元消息:{message[:30]}...,消息长度: {len(message)}")
# 从LPMM知识库获取知识
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知识库获取知识失败使用旧版数据库进行检索")
knowledge_from_old = await self.get_prompt_info_old(message, threshold=0.38)
related_info += knowledge_from_old
logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
return related_info
@staticmethod
def get_info_from_db(
query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False
) -> Union[str, list]:
if not query_embedding:
return "" if not return_raw else []
# 使用余弦相似度计算
pipeline = [
{
"$addFields": {
"dotProduct": {
"$reduce": {
"input": {"$range": [0, {"$size": "$embedding"}]},
"initialValue": 0,
"in": {
"$add": [
"$$value",
{
"$multiply": [
{"$arrayElemAt": ["$embedding", "$$this"]},
{"$arrayElemAt": [query_embedding, "$$this"]},
]
},
]
},
}
},
"magnitude1": {
"$sqrt": {
"$reduce": {
"input": "$embedding",
"initialValue": 0,
"in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]},
}
}
},
"magnitude2": {
"$sqrt": {
"$reduce": {
"input": query_embedding,
"initialValue": 0,
"in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]},
}
}
},
}
},
{"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$multiply": ["$magnitude1", "$magnitude2"]}]}}},
{
"$match": {
"similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果
}
},
{"$sort": {"similarity": -1}},
{"$limit": limit},
{"$project": {"content": 1, "similarity": 1}},
]
results = list(db.knowledges.aggregate(pipeline))
logger.debug(f"知识库查询结果数量: {len(results)}")
if not results:
return "" if not return_raw else []
if return_raw:
return results
else:
# 返回所有找到的内容,用换行分隔
return "\n".join(str(result["content"]) for result in results)
init_prompt()
prompt_builder = PromptBuilder()

View File

@@ -0,0 +1,404 @@
import time
import asyncio
import traceback
from random import random
from typing import List, Optional # 导入 Optional
from ..moods.moods import MoodManager
from ...config.config import global_config
from ..chat.emoji_manager import emoji_manager
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..willing.willing_manager import willing_manager
from ..message import UserInfo, Seg
from src.common.logger import get_module_logger, CHAT_STYLE_CONFIG, LogConfig
from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculater import Timer
# 定义日志配置
chat_config = LogConfig(
console_format=CHAT_STYLE_CONFIG["console_format"],
file_format=CHAT_STYLE_CONFIG["file_format"],
)
logger = get_module_logger("normal_chat", config=chat_config)
class NormalChat:
def __init__(self, chat_stream: ChatStream, interest_dict: dict):
"""
初始化 NormalChat 实例,针对特定的 ChatStream。
Args:
chat_stream (ChatStream): 此 NormalChat 实例关联的聊天流对象。
"""
self.chat_stream = chat_stream
self.stream_id = chat_stream.stream_id
self.stream_name = chat_manager.get_stream_name(self.stream_id) or self.stream_id
self.interest_dict = interest_dict
self.gpt = NormalChatGenerator()
self.mood_manager = MoodManager.get_instance() # MoodManager 保持单例
# 存储此实例的兴趣监控任务
self._chat_task: Optional[asyncio.Task] = None
logger.info(f"[{self.stream_name}] NormalChat 实例初始化完成。")
# 改为实例方法
async def _create_thinking_message(self, message: MessageRecv) -> str:
"""创建思考消息"""
messageinfo = message.message_info
bot_user_info = UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=messageinfo.platform,
)
thinking_time_point = round(time.time(), 2)
thinking_id = "mt" + str(thinking_time_point)
thinking_message = MessageThinking(
message_id=thinking_id,
chat_stream=self.chat_stream, # 使用 self.chat_stream
bot_user_info=bot_user_info,
reply=message,
thinking_start_time=thinking_time_point,
)
await message_manager.add_message(thinking_message)
return thinking_id
# 改为实例方法
async def _add_messages_to_manager(
self, message: MessageRecv, response_set: List[str], thinking_id
) -> Optional[MessageSending]:
"""发送回复消息"""
container = await message_manager.get_container(self.stream_id) # 使用 self.stream_id
thinking_message = None
for msg in container.messages[:]:
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
thinking_message = msg
container.messages.remove(msg)
break
if not thinking_message:
logger.warning(f"[{self.stream_name}] 未找到对应的思考消息 {thinking_id},可能已超时被移除")
return None
thinking_start_time = thinking_message.thinking_start_time
message_set = MessageSet(self.chat_stream, thinking_id) # 使用 self.chat_stream
mark_head = False
first_bot_msg = None
for msg in response_set:
message_segment = Seg(type="text", data=msg)
bot_message = MessageSending(
message_id=thinking_id,
chat_stream=self.chat_stream, # 使用 self.chat_stream
bot_user_info=UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=message.message_info.platform,
),
sender_info=message.message_info.user_info,
message_segment=message_segment,
reply=message,
is_head=not mark_head,
is_emoji=False,
thinking_start_time=thinking_start_time,
apply_set_reply_logic=True,
)
if not mark_head:
mark_head = True
first_bot_msg = bot_message
message_set.add_message(bot_message)
await message_manager.add_message(message_set)
return first_bot_msg
# 改为实例方法
async def _handle_emoji(self, message: MessageRecv, response: str):
"""处理表情包"""
if random() < global_config.emoji_chance:
emoji_raw = await emoji_manager.get_emoji_for_text(response)
if emoji_raw:
emoji_path, description = emoji_raw
emoji_cq = image_path_to_base64(emoji_path)
thinking_time_point = round(message.message_info.time, 2)
message_segment = Seg(type="emoji", data=emoji_cq)
bot_message = MessageSending(
message_id="mt" + str(thinking_time_point),
chat_stream=self.chat_stream, # 使用 self.chat_stream
bot_user_info=UserInfo(
user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME,
platform=message.message_info.platform,
),
sender_info=message.message_info.user_info,
message_segment=message_segment,
reply=message,
is_head=False,
is_emoji=True,
apply_set_reply_logic=True,
)
await message_manager.add_message(bot_message)
# 改为实例方法 (虽然它只用 message.chat_stream, 但逻辑上属于实例)
async def _update_relationship(self, message: MessageRecv, response_set):
"""更新关系情绪"""
ori_response = ",".join(response_set)
stance, emotion = await self.gpt._get_emotion_tags(ori_response, message.processed_plain_text)
await relationship_manager.calculate_update_relationship_value(
chat_stream=self.chat_stream,
label=emotion,
stance=stance, # 使用 self.chat_stream
)
self.mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor)
async def _find_interested_message(self) -> None:
"""
后台任务方法轮询当前实例关联chat的兴趣消息
通常由start_monitoring_interest()启动
"""
while True:
await asyncio.sleep(1) # 每秒检查一次
# 检查任务是否已被取消
if self._chat_task is None or self._chat_task.cancelled():
logger.info(f"[{self.stream_name}] 兴趣监控任务被取消或置空,退出")
break
# 获取待处理消息列表
items_to_process = list(self.interest_dict.items())
if not items_to_process:
continue
# 处理每条兴趣消息
for msg_id, (message, interest_value, is_mentioned) in items_to_process:
try:
# 处理消息
await self.normal_response(
message=message, is_mentioned=is_mentioned, interested_rate=interest_value
)
except Exception as e:
logger.error(f"[{self.stream_name}] 处理兴趣消息{msg_id}时出错: {e}\n{traceback.format_exc()}")
finally:
self.interest_dict.pop(msg_id, None)
# 改为实例方法, 移除 chat 参数
async def normal_response(self, message: MessageRecv, is_mentioned: bool, interested_rate: float) -> None:
# 检查收到的消息是否属于当前实例处理的 chat stream
if message.chat_stream.stream_id != self.stream_id:
logger.error(
f"[{self.stream_name}] normal_response 收到不匹配的消息 (来自 {message.chat_stream.stream_id}),预期 {self.stream_id}。已忽略。"
)
return
timing_results = {}
reply_probability = 1.0 if is_mentioned else 0.0 # 如果被提及基础概率为1否则需要意愿判断
# 意愿管理器设置当前message信息
willing_manager.setup(message, self.chat_stream, is_mentioned, interested_rate)
# 获取回复概率
is_willing = False
# 仅在未被提及或基础概率不为1时查询意愿概率
if reply_probability < 1: # 简化逻辑,如果未提及 (reply_probability 为 0),则获取意愿概率
is_willing = True
reply_probability = await willing_manager.get_reply_probability(message.message_info.message_id)
if message.message_info.additional_config:
if "maimcore_reply_probability_gain" in message.message_info.additional_config.keys():
reply_probability += message.message_info.additional_config["maimcore_reply_probability_gain"]
reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间
# 打印消息信息
mes_name = self.chat_stream.group_info.group_name if self.chat_stream.group_info else "私聊"
current_time = time.strftime("%H:%M:%S", time.localtime(message.message_info.time))
# 使用 self.stream_id
willing_log = f"[回复意愿:{await willing_manager.get_willing(self.stream_id):.2f}]" if is_willing else ""
logger.info(
f"[{current_time}][{mes_name}]"
f"{message.message_info.user_info.user_nickname}:" # 使用 self.chat_stream
f"{message.processed_plain_text}{willing_log}[概率:{reply_probability * 100:.1f}%]"
)
do_reply = False
response_set = None # 初始化 response_set
if random() < reply_probability:
do_reply = True
# 回复前处理
await willing_manager.before_generate_reply_handle(message.message_info.message_id)
with Timer("创建思考消息", timing_results):
thinking_id = await self._create_thinking_message(message)
logger.debug(f"[{self.stream_name}] 创建捕捉器thinking_id:{thinking_id}")
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
info_catcher.catch_decide_to_response(message)
try:
with Timer("生成回复", timing_results):
response_set = await self.gpt.generate_response(
message=message,
thinking_id=thinking_id,
)
info_catcher.catch_after_generate_response(timing_results["生成回复"])
except Exception as e:
logger.error(f"[{self.stream_name}] 回复生成出现错误:{str(e)} {traceback.format_exc()}")
response_set = None # 确保出错时 response_set 为 None
if not response_set:
logger.info(f"[{self.stream_name}] 模型未生成回复内容")
# 如果模型未生成回复,移除思考消息
container = await message_manager.get_container(self.stream_id) # 使用 self.stream_id
for msg in container.messages[:]:
if isinstance(msg, MessageThinking) and msg.message_info.message_id == thinking_id:
container.messages.remove(msg)
logger.debug(f"[{self.stream_name}] 已移除未产生回复的思考消息 {thinking_id}")
break
# 需要在此处也调用 not_reply_handle 和 delete 吗?
# 如果是因为模型没回复,也算是一种 "未回复"
await willing_manager.not_reply_handle(message.message_info.message_id)
willing_manager.delete(message.message_info.message_id)
return # 不执行后续步骤
logger.info(f"[{self.stream_name}] 回复内容: {response_set}")
# 发送回复 (不再需要传入 chat)
with Timer("消息发送", timing_results):
first_bot_msg = await self._add_messages_to_manager(message, response_set, thinking_id)
# 检查 first_bot_msg 是否为 None (例如思考消息已被移除的情况)
if first_bot_msg:
info_catcher.catch_after_response(timing_results["消息发送"], response_set, first_bot_msg)
else:
logger.warning(f"[{self.stream_name}] 思考消息 {thinking_id} 在发送前丢失,无法记录 info_catcher")
info_catcher.done_catch()
# 处理表情包 (不再需要传入 chat)
with Timer("处理表情包", timing_results):
await self._handle_emoji(message, response_set[0])
# 更新关系情绪 (不再需要传入 chat)
with Timer("关系更新", timing_results):
await self._update_relationship(message, response_set)
# 回复后处理
await willing_manager.after_generate_reply_handle(message.message_info.message_id)
# 输出性能计时结果
if do_reply and response_set: # 确保 response_set 不是 None
timing_str = " | ".join([f"{step}: {duration:.2f}" for step, duration in timing_results.items()])
trigger_msg = message.processed_plain_text
response_msg = " ".join(response_set)
logger.info(
f"[{self.stream_name}] 触发消息: {trigger_msg[:20]}... | 推理消息: {response_msg[:20]}... | 性能计时: {timing_str}"
)
elif not do_reply:
# 不回复处理
await willing_manager.not_reply_handle(message.message_info.message_id)
# else: # do_reply is True but response_set is None (handled above)
# logger.info(f"[{self.stream_name}] 决定回复但模型未生成内容。触发: {message.processed_plain_text[:20]}...")
# 意愿管理器注销当前message信息 (无论是否回复,只要处理过就删除)
willing_manager.delete(message.message_info.message_id)
# 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文
@staticmethod
def _check_ban_words(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
"""检查消息中是否包含过滤词"""
stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id
for word in global_config.ban_words:
if word in text:
logger.info(
f"[{stream_name}][{chat.group_info.group_name if chat.group_info else '私聊'}]"
f"{userinfo.user_nickname}:{text}"
)
logger.info(f"[{stream_name}][过滤词识别] 消息中含有 '{word}'filtered")
return True
return False
# 保持 staticmethod, 因为不依赖实例状态, 但需要 chat 对象来获取日志上下文
@staticmethod
def _check_ban_regex(text: str, chat: ChatStream, userinfo: UserInfo) -> bool:
"""检查消息是否匹配过滤正则表达式"""
stream_name = chat_manager.get_stream_name(chat.stream_id) or chat.stream_id
for pattern in global_config.ban_msgs_regex:
if pattern.search(text):
logger.info(
f"[{stream_name}][{chat.group_info.group_name if chat.group_info else '私聊'}]"
f"{userinfo.user_nickname}:{text}"
)
logger.info(f"[{stream_name}][正则表达式过滤] 消息匹配到 '{pattern.pattern}'filtered")
return True
return False
# 改为实例方法, 移除 chat 参数
async def start_chat(self):
"""为此 NormalChat 实例关联的 ChatStream 启动聊天任务(如果尚未运行)。"""
if self._chat_task is None or self._chat_task.done():
logger.info(f"[{self.stream_name}] 启动聊天任务...")
task = asyncio.create_task(self._find_interested_message())
task.add_done_callback(lambda t: self._handle_task_completion(t)) # 回调现在是实例方法
self._chat_task = task
# 改为实例方法, 移除 stream_id 参数
def _handle_task_completion(self, task: asyncio.Task):
"""兴趣监控任务完成时的回调函数。"""
# 检查完成的任务是否是当前实例的任务
if task is not self._chat_task:
logger.warning(f"[{self.stream_name}] 收到一个未知或过时任务的完成回调。")
return
try:
# 检查任务是否因异常而结束
exception = task.exception()
if exception:
logger.error(f"[{self.stream_name}] 兴趣监控任务因异常结束: {exception}")
logger.error(traceback.format_exc()) # 记录完整的 traceback
# else: # 减少日志
# logger.info(f"[{self.stream_name}] 兴趣监控任务正常结束。")
except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 兴趣监控任务被取消。")
except Exception as e:
logger.error(f"[{self.stream_name}] 处理任务完成回调时出错: {e}")
finally:
# 标记任务已完成/移除
if self._chat_task is task: # 再次确认是当前任务
self._chat_task = None
logger.debug(f"[{self.stream_name}] 聊天任务已被标记为完成/移除。")
# 改为实例方法, 移除 stream_id 参数
async def stop_chat(self):
"""停止当前实例的兴趣监控任务。"""
if self._chat_task and not self._chat_task.done():
task = self._chat_task
logger.info(f"[{self.stream_name}] 尝试取消聊天任务。")
task.cancel()
try:
await task # 等待任务响应取消
except asyncio.CancelledError:
logger.info(f"[{self.stream_name}] 聊天任务已成功取消。")
except Exception as e:
# 回调函数 _handle_task_completion 会处理异常日志
logger.warning(f"[{self.stream_name}] 等待监控任务取消时捕获到异常 (可能已在回调中记录): {e}")
finally:
# 确保任务状态更新,即使等待出错 (回调函数也会尝试更新)
if self._chat_task is task:
self._chat_task = None

View File

@@ -0,0 +1,166 @@
from typing import List, Optional, Tuple, Union
import random
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from ..chat.message import MessageThinking
from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
from ..utils.timer_calculater import Timer
from src.common.logger import get_module_logger, LogConfig, LLM_STYLE_CONFIG
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
# 定义日志配置
llm_config = LogConfig(
# 使用消息发送专用样式
console_format=LLM_STYLE_CONFIG["console_format"],
file_format=LLM_STYLE_CONFIG["file_format"],
)
logger = get_module_logger("llm_generator", config=llm_config)
class NormalChatGenerator:
def __init__(self):
self.model_reasoning = LLMRequest(
model=global_config.llm_reasoning,
temperature=0.7,
max_tokens=3000,
request_type="response_reasoning",
)
self.model_normal = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=256,
request_type="response_reasoning",
)
self.model_sum = LLMRequest(
model=global_config.llm_summary_by_topic, temperature=0.7, max_tokens=3000, request_type="relation"
)
self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model"
async def generate_response(self, message: MessageThinking, thinking_id: str) -> Optional[Union[str, List[str]]]:
"""根据当前模型类型选择对应的生成函数"""
# 从global_config中获取模型概率值并选择模型
if random.random() < global_config.model_reasoning_probability:
self.current_model_type = "深深地"
current_model = self.model_reasoning
else:
self.current_model_type = "浅浅的"
current_model = self.model_normal
logger.info(
f"{self.current_model_type}思考:{message.processed_plain_text[:30] + '...' if len(message.processed_plain_text) > 30 else message.processed_plain_text}"
) # noqa: E501
model_response = await self._generate_response_with_model(message, current_model, thinking_id)
if model_response:
logger.info(f"{global_config.BOT_NICKNAME}的回复是:{model_response}")
model_response = await self._process_response(model_response)
return model_response
else:
logger.info(f"{self.current_model_type}思考,失败")
return None
async def _generate_response_with_model(self, message: MessageThinking, model: LLMRequest, thinking_id: str):
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
if message.chat_stream.user_info.user_cardname and message.chat_stream.user_info.user_nickname:
sender_name = (
f"[({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}]"
f"{message.chat_stream.user_info.user_cardname}"
)
elif message.chat_stream.user_info.user_nickname:
sender_name = f"({message.chat_stream.user_info.user_id}){message.chat_stream.user_info.user_nickname}"
else:
sender_name = f"用户({message.chat_stream.user_info.user_id})"
# 构建prompt
with Timer() as t_build_prompt:
prompt = await prompt_builder.build_prompt(
build_mode="normal",
reason="",
current_mind_info="",
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,
)
logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
logger.info(f"prompt:{prompt}\n生成回复:{content}")
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
)
except Exception:
logger.exception("生成回复时出错")
return None
return content
async def _get_emotion_tags(self, content: str, processed_plain_text: str):
"""提取情感标签,结合立场和情绪"""
try:
# 构建提示词,结合回复内容、被回复的内容以及立场分析
prompt = f"""
请严格根据以下对话内容,完成以下任务:
1. 判断回复者对被回复者观点的直接立场:
- "支持":明确同意或强化被回复者观点
- "反对":明确反驳或否定被回复者观点
- "中立":不表达明确立场或无关回应
2. 从"开心,愤怒,悲伤,惊讶,平静,害羞,恐惧,厌恶,困惑"中选出最匹配的1个情感标签
3. 按照"立场-情绪"的格式直接输出结果,例如:"反对-愤怒"
4. 考虑回复者的人格设定为{global_config.personality_core}
对话示例:
被回复「A就是笨」
回复「A明明很聪明」 → 反对-愤怒
当前对话:
被回复:「{processed_plain_text}
回复:「{content}
输出要求:
- 只需输出"立场-情绪"结果,不要解释
- 严格基于文字直接表达的对立关系判断
"""
# 调用模型生成结果
result, _, _ = await self.model_sum.generate_response(prompt)
result = result.strip()
# 解析模型输出的结果
if "-" in result:
stance, emotion = result.split("-", 1)
valid_stances = ["支持", "反对", "中立"]
valid_emotions = ["开心", "愤怒", "悲伤", "惊讶", "害羞", "平静", "恐惧", "厌恶", "困惑"]
if stance in valid_stances and emotion in valid_emotions:
return stance, emotion # 返回有效的立场-情绪组合
else:
logger.debug(f"无效立场-情感组合:{result}")
return "中立", "平静" # 默认返回中立-平静
else:
logger.debug(f"立场-情感格式错误:{result}")
return "中立", "平静" # 格式错误时返回默认值
except Exception as e:
logger.debug(f"获取情感标签时出错: {e}")
return "中立", "平静" # 出错时返回默认值
@staticmethod
async def _process_response(content: str) -> Tuple[List[str], List[str]]:
"""处理响应内容,返回处理后的内容和情感标签"""
if not content:
return None, []
processed_response = process_llm_response(content)
# print(f"得到了处理后的llm返回{processed_response}")
return processed_response