MaiBot/src/plugins/chat_module/heartFC_chat/pf_chatting.py

792 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import asyncio
import time
import traceback
from typing import List, Optional, Dict, Any, TYPE_CHECKING
import json
from ....config.config import global_config
from ...chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
from ...chat.chat_stream import ChatStream
from ...message import UserInfo
from src.heart_flow.heartflow import heartflow, SubHeartflow
from src.plugins.chat.chat_stream import chat_manager
from .messagesender import MessageManager
from src.common.logger import get_module_logger, LogConfig, DEFAULT_CONFIG # 引入 DEFAULT_CONFIG
from src.plugins.models.utils_model import LLMRequest
from src.plugins.chat.utils import parse_text_timestamps
from src.plugins.person_info.relationship_manager import relationship_manager
# 定义日志配置 (使用 loguru 格式)
interest_log_config = LogConfig(
console_format=DEFAULT_CONFIG["console_format"], # 使用默认控制台格式
file_format=DEFAULT_CONFIG["file_format"], # 使用默认文件格式
)
logger = get_module_logger("PFChattingLoop", config=interest_log_config) # Logger Name Changed
# Forward declaration for type hinting
if TYPE_CHECKING:
from .heartFC_chat import HeartFC_Chat
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 PFChatting:
"""
Manages a continuous Plan-Filter-Check (now Plan-Replier-Sender) loop
for generating replies within a specific chat stream, controlled by a timer.
The loop runs as long as the timer > 0.
"""
def __init__(self, chat_id: str, heartfc_chat_instance: "HeartFC_Chat"):
"""
初始化PFChatting实例。
Args:
chat_id: The identifier for the chat stream (e.g., stream_id).
heartfc_chat_instance: 访问共享资源和方法的主HeartFC_Chat实例。
"""
self.heartfc_chat = heartfc_chat_instance # 访问logger, gpt, tool_user, _send_response_messages等。
self.stream_id: str = chat_id
self.chat_stream: Optional[ChatStream] = None
self.sub_hf: Optional[SubHeartflow] = None
self._initialized = False
self._init_lock = asyncio.Lock() # Ensure initialization happens only once
self._processing_lock = asyncio.Lock() # 确保只有一个 Plan-Replier-Sender 周期在运行
self._timer_lock = asyncio.Lock() # 用于安全更新计时器
self.planner_llm = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=1000,
request_type="action_planning",
)
# Internal state for loop control
self._loop_timer: float = 0.0 # Remaining time for the loop in seconds
self._loop_active: bool = False # Is the loop currently running?
self._loop_task: Optional[asyncio.Task] = None # Stores the main loop task
self._trigger_count_this_activation: int = 0 # Counts triggers within an active period
self._initial_duration: float = 30.0 # 首次触发增加的时间
self._last_added_duration: float = self._initial_duration # <--- 新增:存储上次增加的时间
# Removed pending_replies as processing is now serial within the loop
# self.pending_replies: Dict[str, PendingReply] = {}
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。
确保实例已准备好处理触发器。
"""
async with self._init_lock:
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
# 子心流(SubHeartflow)可能初始不存在但后续会被创建
# 在需要它的方法中应优雅处理其可能缺失的情况
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"麦麦感觉到了激发了PFChatting{log_prefix} 初始化成功。")
return True
except Exception as e:
logger.error(f"{log_prefix} 初始化失败: {e}")
logger.error(traceback.format_exc())
return False
async def add_time(self):
"""
为麦麦添加时间,麦麦有兴趣时,时间增加。
"""
log_prefix = self._get_log_prefix()
if not self._initialized:
if not await self._initialize():
logger.error(f"{log_prefix} 无法添加时间: 未初始化。")
return
async with self._timer_lock:
duration_to_add: float = 0.0
if not self._loop_active: # First trigger for this activation cycle
duration_to_add = self._initial_duration # 使用初始值
self._last_added_duration = duration_to_add # 更新上次增加的值
self._trigger_count_this_activation = 1 # Start counting
logger.info(
f"{log_prefix} 麦麦有兴趣! #{self._trigger_count_this_activation}. 麦麦打算聊: {duration_to_add:.2f}s."
)
else: # Loop is already active, apply 50% reduction
self._trigger_count_this_activation += 1
duration_to_add = self._last_added_duration * 0.5
if duration_to_add < 0.5:
duration_to_add = 0.5
self._last_added_duration = duration_to_add # 更新上次增加的值
else:
self._last_added_duration = duration_to_add # 更新上次增加的值
logger.info(
f"{log_prefix} 麦麦兴趣增加! #{self._trigger_count_this_activation}. 想继续聊: {duration_to_add:.2f}s,麦麦还能聊: {self._loop_timer:.1f}s."
)
# 添加计算出的时间
new_timer_value = self._loop_timer + duration_to_add
self._loop_timer = max(0, new_timer_value)
if self._loop_timer % 5 == 0:
logger.info(f"{log_prefix} 麦麦现在想聊{self._loop_timer:.1f}")
# Start the loop if it wasn't active and timer is positive
if not self._loop_active and self._loop_timer > 0:
# logger.info(f"{log_prefix} 麦麦有兴趣!开始聊天")
self._loop_active = True
if self._loop_task and not self._loop_task.done():
logger.warning(f"{log_prefix} 发现意外的循环任务正在进行。取消它。")
self._loop_task.cancel()
self._loop_task = asyncio.create_task(self._run_pf_loop())
self._loop_task.add_done_callback(self._handle_loop_completion)
elif self._loop_active:
logger.trace(f"{log_prefix} 循环已经激活。计时器延长。")
def _handle_loop_completion(self, task: asyncio.Task):
"""当 _run_pf_loop 任务完成时执行的回调。"""
log_prefix = self._get_log_prefix()
try:
# Check if the task raised an exception
exception = task.exception()
if exception:
logger.error(f"{log_prefix} PFChatting: 麦麦脱离了聊天(异常)")
logger.error(traceback.format_exc())
else:
logger.debug(f"{log_prefix} PFChatting: 麦麦脱离了聊天")
except asyncio.CancelledError:
logger.info(f"{log_prefix} PFChatting: 麦麦脱离了聊天(异常取消)")
finally:
# Reset state regardless of how the task finished
self._loop_active = False
self._loop_task = None
self._last_added_duration = self._initial_duration # <--- 重置下次首次触发的增加时间
self._trigger_count_this_activation = 0 # 重置计数器
# Ensure lock is released if the loop somehow exited while holding it
if self._processing_lock.locked():
logger.warning(f"{log_prefix} PFChatting: 锁没有正常释放")
self._processing_lock.release()
async def _run_pf_loop(self):
"""
主循环,当计时器>0时持续进行计划并可能回复消息
管理每个循环周期的处理锁
"""
logger.info(f"{self._get_log_prefix()} PFChatting: 麦麦打算好好聊聊")
try:
while True:
# 使用计时器锁安全地检查当前计时器值
async with self._timer_lock:
current_timer = self._loop_timer
if current_timer <= 0:
logger.info(
f"{self._get_log_prefix()} PFChatting: 聊太久了,麦麦打算休息一下(已经聊了{current_timer:.1f}秒)退出PFChatting"
)
break # 退出条件:计时器到期
# 记录循环开始时间
loop_cycle_start_time = time.monotonic()
# 标记本周期是否执行了操作
action_taken_this_cycle = False
# 获取处理锁,确保每个计划-回复-发送周期独占执行
acquired_lock = False
try:
await self._processing_lock.acquire()
acquired_lock = True
# logger.debug(f"{self._get_log_prefix()} PFChatting: 循环获取到处理锁")
# --- Planner ---
# Planner decides action, reasoning, emoji_query, etc.
planner_result = await self._planner() # Modify planner to return decision dict
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", "")
observed_messages = planner_result.get("observed_messages", []) # Planner needs to return this
if action == "text_reply":
logger.info(f"{self._get_log_prefix()} PFChatting: 麦麦决定回复文本.")
action_taken_this_cycle = True
# --- 回复器 ---
anchor_message = await self._get_anchor_message(observed_messages)
if not anchor_message:
logger.error(f"{self._get_log_prefix()} 循环: 无法获取锚点消息用于回复. 跳过周期.")
else:
thinking_id = await self.heartfc_chat._create_thinking_message(anchor_message)
if not thinking_id:
logger.error(f"{self._get_log_prefix()} 循环: 无法创建思考ID. 跳过周期.")
else:
replier_result = None
try:
# 直接 await 回复器工作
replier_result = await self._replier_work(
observed_messages=observed_messages,
anchor_message=anchor_message,
thinking_id=thinking_id,
current_mind=current_mind,
send_emoji=send_emoji_from_tools,
)
except Exception as e_replier:
logger.error(f"{self._get_log_prefix()} 循环: 回复器工作失败: {e_replier}")
self._cleanup_thinking_message(thinking_id) # 清理思考消息
# 继续循环, 视为非操作周期
if replier_result:
# --- Sender ---
try:
await self._sender(thinking_id, anchor_message, replier_result)
logger.info(f"{self._get_log_prefix()} 循环: 发送器完成成功.")
except Exception as e_sender:
logger.error(f"{self._get_log_prefix()} 循环: 发送器失败: {e_sender}")
self._cleanup_thinking_message(thinking_id) # 确保发送失败时清理
# 继续循环, 视为非操作周期
else:
# Replier failed to produce result
logger.warning(f"{self._get_log_prefix()} 循环: 回复器未产生结果. 跳过发送.")
self._cleanup_thinking_message(thinking_id) # 清理思考消息
elif action == "emoji_reply":
logger.info(f"{self._get_log_prefix()} PFChatting: 麦麦决定回复表情 ('{emoji_query}').")
action_taken_this_cycle = True
anchor = await self._get_anchor_message(observed_messages)
if anchor:
try:
await self.heartfc_chat._handle_emoji(anchor, [], emoji_query)
except Exception as e_emoji:
logger.error(f"{self._get_log_prefix()} 循环: 发送表情失败: {e_emoji}")
else:
logger.warning(f"{self._get_log_prefix()} 循环: 无法发送表情, 无法获取锚点.")
elif action == "no_reply":
logger.info(f"{self._get_log_prefix()} PFChatting: 麦麦决定不回复. 原因: {reasoning}")
# Do nothing else, action_taken_this_cycle remains False
elif action == "error":
logger.error(f"{self._get_log_prefix()} PFChatting: 麦麦回复出错. 原因: {reasoning}")
# 视为非操作周期
else: # Unknown action
logger.warning(f"{self._get_log_prefix()} PFChatting: 麦麦做了奇怪的事情. 原因: {reasoning}")
# 视为非操作周期
except Exception as e_cycle:
# Catch errors occurring within the locked section (e.g., planner crash)
logger.error(f"{self._get_log_prefix()} 循环周期执行时发生错误: {e_cycle}")
logger.error(traceback.format_exc())
# Ensure lock is released if an error occurs before the finally block
if acquired_lock and self._processing_lock.locked():
self._processing_lock.release()
acquired_lock = False # 防止在 finally 块中重复释放
logger.warning(f"{self._get_log_prefix()} 由于循环周期中的错误释放了处理锁.")
finally:
# Ensure the lock is always released after a cycle
if acquired_lock:
self._processing_lock.release()
logger.debug(f"{self._get_log_prefix()} 循环释放了处理锁.")
# --- Timer Decrement ---
cycle_duration = time.monotonic() - loop_cycle_start_time
async with self._timer_lock:
self._loop_timer -= cycle_duration
logger.debug(
f"{self._get_log_prefix()} PFChatting: 麦麦聊了{cycle_duration:.2f}秒. 还能聊: {self._loop_timer:.1f}s."
)
# --- Delay ---
# Add a small delay, especially if no action was taken, to prevent busy-waiting
try:
if not action_taken_this_cycle and cycle_duration < 1.5:
# If nothing happened and cycle was fast, wait a bit longer
await asyncio.sleep(1.5 - cycle_duration)
elif cycle_duration < 0.2: # Minimum delay even if action was taken
await asyncio.sleep(0.2)
except asyncio.CancelledError:
logger.info(f"{self._get_log_prefix()} Sleep interrupted, likely loop cancellation.")
break # Exit loop if cancelled during sleep
except asyncio.CancelledError:
logger.info(f"{self._get_log_prefix()} PFChatting: 麦麦的聊天被取消了")
except Exception as e_loop_outer:
# Catch errors outside the main cycle lock (should be rare)
logger.error(f"{self._get_log_prefix()} PFChatting: 麦麦的聊天出错了: {e_loop_outer}")
logger.error(traceback.format_exc())
finally:
# Reset trigger count when loop finishes
async with self._timer_lock:
self._trigger_count_this_activation = 0
logger.debug(f"{self._get_log_prefix()} Trigger count reset to 0 as loop finishes.")
logger.info(f"{self._get_log_prefix()} PFChatting: 麦麦的聊天结束了")
# State reset (_loop_active, _loop_task) is handled by _handle_loop_completion callback
async def _planner(self) -> Dict[str, Any]:
"""
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
返回:
dict: 包含决策和上下文的字典,结构如下:
{
'action': str, # 执行动作 (不回复/文字回复/表情包)
'reasoning': str, # 决策理由
'emoji_query': str, # 表情包查询词
'current_mind': str, # 当前心理状态
'send_emoji_from_tools': str, # 工具推荐的表情包
'observed_messages': List[dict] # 观察到的消息列表
}
"""
log_prefix = self._get_log_prefix()
observed_messages: List[dict] = []
tool_result_info = {}
get_mid_memory_id = []
send_emoji_from_tools = "" # Renamed for clarity
current_mind: Optional[str] = None
# --- 获取最新的观察信息 ---
try:
observation = self.sub_hf._get_primary_observation() # Call only once
if observation: # Now check if the result is truthy
# logger.debug(f"{log_prefix}[Planner] 调用 observation.observe()...")
await observation.observe() # 主动观察以获取最新消息
observed_messages = observation.talking_message # 获取更新后的消息列表
logger.debug(f"{log_prefix}[Planner] 观察获取到 {len(observed_messages)} 条消息。")
else:
logger.warning(f"{log_prefix}[Planner] 无法获取 Observation。")
except Exception as e:
logger.error(f"{log_prefix}[Planner] 获取观察信息时出错: {e}")
logger.error(traceback.format_exc())
# --- 结束获取观察信息 ---
# --- (Moved from _replier_work) 1. 思考前使用工具 ---
try:
observation_context_text = ""
if observed_messages:
context_texts = [
msg.get("detailed_plain_text", "") for msg in observed_messages if msg.get("detailed_plain_text")
]
observation_context_text = " ".join(context_texts)
# logger.debug(f"{log_prefix}[Planner] Context for tools: {observation_context_text[:100]}...")
tool_result = await self.heartfc_chat.tool_user.use_tool(
message_txt=observation_context_text, chat_stream=self.chat_stream, sub_heartflow=self.sub_hf
)
if tool_result.get("used_tools", False):
tool_result_info = tool_result.get("structured_info", {})
logger.debug(f"{log_prefix}[Planner] 规划前工具结果: {tool_result_info}")
if "mid_chat_mem" in tool_result_info:
get_mid_memory_id = [mem["content"] for mem in tool_result_info["mid_chat_mem"] if "content" in mem]
except Exception as e_tool:
logger.error(f"{log_prefix}[Planner] 规划前工具使用失败: {e_tool}")
# --- 结束工具使用 ---
current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply(
chat_stream=self.chat_stream,
extra_info=tool_result_info,
obs_id=get_mid_memory_id,
)
# --- 使用 LLM 进行决策 ---
action = "no_reply" # Default action
emoji_query = ""
reasoning = "默认决策或获取决策失败"
llm_error = False # Flag for LLM failure
try:
# 构建提示 (Now includes current_mind)
prompt = await self._build_planner_prompt(observed_messages, current_mind)
logger.debug(f"{log_prefix}[Planner] 规划器 Prompt: {prompt}")
# 准备 LLM 请求 Payload
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"}}, # 强制调用此工具
}
# 调用 LLM
response = await self.planner_llm._execute_request(
endpoint="/chat/completions", payload=payload, prompt=prompt
)
# 解析 LLM 响应
if len(response) == 3: # 期望返回 content, reasoning_content, tool_calls
_, _, tool_calls = response
if tool_calls and isinstance(tool_calls, list) and len(tool_calls) > 0:
# 通常强制调用后只会有一个 tool_call
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", "未提供理由")
if action == "emoji_reply":
# Planner's decision overrides tool's emoji if action is emoji_reply
emoji_query = arguments.get(
"emoji_query", send_emoji_from_tools
) # Use tool emoji as default if planner asks for emoji
logger.info(
f"{log_prefix}[Planner] LLM 决策: {action}, 理由: {reasoning}, EmojiQuery: '{emoji_query}'"
)
except json.JSONDecodeError as json_e:
logger.error(
f"{log_prefix}[Planner] 解析工具参数失败: {json_e}. Arguments: {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())
action = "error"
reasoning = f"LLM 调用失败: {llm_e}"
llm_error = True
# --- 返回决策结果 ---
# Note: Lock release is handled by the loop now
return {
"action": action,
"reasoning": reasoning,
"emoji_query": emoji_query, # Specific query if action is emoji_reply
"current_mind": current_mind,
"send_emoji_from_tools": send_emoji_from_tools, # Emoji suggested by pre-thinking tools
"observed_messages": observed_messages,
"llm_error": llm_error, # Indicate if LLM decision process failed
}
async def _get_anchor_message(self, observed_messages: List[dict]) -> Optional[MessageRecv]:
"""
重构观察到的最后一条消息作为回复的锚点,
如果重构失败或观察为空,则创建一个占位符。
"""
if not self.chat_stream:
logger.error(f"{self._get_log_prefix()} 无法获取锚点消息: ChatStream 不可用.")
return None
try:
last_msg_dict = None
if observed_messages:
last_msg_dict = observed_messages[-1]
if last_msg_dict:
try:
# Attempt reconstruction from the last observed message dictionary
anchor_message = MessageRecv(last_msg_dict, chat_stream=self.chat_stream)
# Basic validation
if not (
anchor_message
and anchor_message.message_info
and anchor_message.message_info.message_id
and anchor_message.message_info.user_info
):
raise ValueError("重构的 MessageRecv 缺少必要信息.")
logger.debug(
f"{self._get_log_prefix()} 重构的锚点消息: ID={anchor_message.message_info.message_id}"
)
return anchor_message
except Exception as e_reconstruct:
logger.warning(
f"{self._get_log_prefix()} 从观察到的消息重构 MessageRecv 失败: {e_reconstruct}. 创建占位符."
)
else:
logger.warning(f"{self._get_log_prefix()} observed_messages 为空. 创建占位符锚点消息.")
# --- 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]", # Placeholder text
"raw_message": "",
"time": placeholder_msg_info.time,
}
anchor_message = MessageRecv(placeholder_msg_dict)
anchor_message.update_chat_stream(self.chat_stream) # Associate with the 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
def _cleanup_thinking_message(self, thinking_id: str):
"""Safely removes the thinking message."""
try:
container = MessageManager().get_container(self.stream_id)
container.remove_message(thinking_id, msg_type=MessageThinking)
logger.debug(f"{self._get_log_prefix()} Cleaned up thinking message {thinking_id}.")
except Exception as e:
logger.error(f"{self._get_log_prefix()} Error cleaning up thinking message {thinking_id}: {e}")
async def _sender(self, thinking_id: str, anchor_message: MessageRecv, replier_result: Dict[str, Any]):
"""
发送器 (Sender): 使用HeartFC_Chat的方法发送生成的回复。
被 _run_pf_loop 直接调用和 await。
也处理相关的操作,如发送表情和更新关系。
Raises exception on failure to signal the loop.
"""
# replier_result should contain 'response_set' and 'send_emoji'
response_set = replier_result.get("response_set")
send_emoji = replier_result.get("send_emoji", "") # Emoji determined by tools, passed via replier
if not response_set:
logger.error(f"{self._get_log_prefix()}[Sender-{thinking_id}] Called with empty response_set.")
# Clean up thinking message before raising error
self._cleanup_thinking_message(thinking_id)
raise ValueError("Sender called with no response_set") # Signal failure to loop
first_bot_msg: Optional[MessageSending] = None
send_success = False
try:
# --- Send the main text response ---
logger.debug(f"{self._get_log_prefix()}[Sender-{thinking_id}] Sending response messages...")
# This call implicitly handles replacing the MessageThinking with MessageSending/MessageSet
first_bot_msg = await self.heartfc_chat._send_response_messages(anchor_message, response_set, thinking_id)
if first_bot_msg:
send_success = True # Mark success
logger.info(f"{self._get_log_prefix()}[Sender-{thinking_id}] Successfully sent reply.")
# --- Handle associated emoji (if determined by tools) ---
if send_emoji:
logger.info(
f"{self._get_log_prefix()}[Sender-{thinking_id}] Sending associated emoji: {send_emoji}"
)
try:
# Use first_bot_msg as anchor if available, otherwise fallback to original anchor
emoji_anchor = first_bot_msg if first_bot_msg else anchor_message
await self.heartfc_chat._handle_emoji(emoji_anchor, response_set, send_emoji)
except Exception as e_emoji:
logger.error(
f"{self._get_log_prefix()}[Sender-{thinking_id}] Failed to send associated emoji: {e_emoji}"
)
# Log error but don't fail the whole send process for emoji failure
# --- Update relationship ---
try:
await self.heartfc_chat._update_relationship(anchor_message, response_set)
logger.debug(f"{self._get_log_prefix()}[Sender-{thinking_id}] Updated relationship.")
except Exception as e_rel:
logger.error(
f"{self._get_log_prefix()}[Sender-{thinking_id}] Failed to update relationship: {e_rel}"
)
# Log error but don't fail the whole send process for relationship update failure
else:
# Sending failed (e.g., _send_response_messages found thinking message already gone)
send_success = False
logger.warning(
f"{self._get_log_prefix()}[Sender-{thinking_id}] Failed to send reply (maybe thinking message expired or was removed?)."
)
# No need to clean up thinking message here, _send_response_messages implies it's gone or handled
raise RuntimeError("Sending reply failed, _send_response_messages returned None.") # Signal failure
except Exception as e:
# Catch potential errors during sending or post-send actions
logger.error(f"{self._get_log_prefix()}[Sender-{thinking_id}] Error during sending process: {e}")
logger.error(traceback.format_exc())
# Ensure thinking message is cleaned up if send failed mid-way and wasn't handled
if not send_success:
self._cleanup_thinking_message(thinking_id)
raise # Re-raise the exception to signal failure to the loop
# No finally block needed for lock management
async def shutdown(self):
"""
Gracefully shuts down the PFChatting instance by cancelling the active loop task.
"""
logger.info(f"{self._get_log_prefix()} Shutting down PFChatting...")
if self._loop_task and not self._loop_task.done():
logger.info(f"{self._get_log_prefix()} Cancelling active PF loop task.")
self._loop_task.cancel()
try:
# Wait briefly for the task to acknowledge cancellation
await asyncio.wait_for(self._loop_task, timeout=5.0)
except asyncio.CancelledError:
logger.info(f"{self._get_log_prefix()} PF loop task cancelled successfully.")
except asyncio.TimeoutError:
logger.warning(f"{self._get_log_prefix()} Timeout waiting for PF loop task cancellation.")
except Exception as e:
logger.error(f"{self._get_log_prefix()} Error during loop task cancellation: {e}")
else:
logger.info(f"{self._get_log_prefix()} No active PF loop task found to cancel.")
# Ensure loop state is reset even if task wasn't running or cancellation failed
self._loop_active = False
self._loop_task = None
# Double-check lock state (should be released by loop completion/cancellation handler)
if self._processing_lock.locked():
logger.warning(f"{self._get_log_prefix()} Releasing processing lock during shutdown.")
self._processing_lock.release()
logger.info(f"{self._get_log_prefix()} PFChatting shutdown complete.")
async def _build_planner_prompt(self, observed_messages: List[dict], current_mind: Optional[str]) -> str:
"""构建 Planner LLM 的提示词 (现在包含 current_mind)"""
prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生正在QQ聊天正在决定是否以及如何回应当前的聊天。\n"
# Add current mind state if available
if observed_messages:
context_text = " ".join(
[msg.get("detailed_plain_text", "") for msg in observed_messages if msg.get("detailed_plain_text")]
)
prompt += "观察到的最新聊天内容如下:\n---\n"
prompt += context_text[:1500] # Limit context length
prompt += "\n---\n"
else:
prompt += "当前没有观察到新的聊天内容。\n"
prompt += "\n看了这些内容,你的想法是:"
if current_mind:
prompt += f"\n---\n{current_mind}\n---\n\n"
prompt += (
"\n请结合你的内部想法和观察到的聊天内容,分析情况并使用 'decide_reply_action' 工具来决定你的最终行动。\n"
)
prompt += "决策依据:\n"
prompt += "1. 如果聊天内容无聊、与你无关、或者你的内部想法认为不适合回复,选择 'no_reply'\n"
prompt += "2. 如果聊天内容值得回应,且适合用文字表达(参考你的内部想法),选择 'text_reply'。如果想在文字后追加一个表情,请同时提供 'emoji_query'\n"
prompt += (
"3. 如果聊天内容或你的内部想法适合用一个表情来回应,选择 'emoji_reply' 并提供表情主题 'emoji_query'\n"
)
prompt += "4. 如果你已经回复过消息,也没有人又回复你,选择'no_reply'\n"
prompt += "5. 除非大家都在这么做,否则不要重复聊相同的内容。\n"
prompt += "6. 表情包是用来表示情绪的,不要直接回复或者评价别人的表情包。\n"
prompt += "必须调用 'decide_reply_action' 工具并提供 'action''reasoning'。如果选择了 'emoji_reply' 或者选择了 'text_reply' 并想追加表情,则必须提供 'emoji_query'"
prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
prompt = parse_text_timestamps(prompt, mode="lite")
return prompt
# --- 回复器 (Replier) 的定义 --- #
async def _replier_work(
self,
observed_messages: List[dict],
anchor_message: MessageRecv,
thinking_id: str,
current_mind: Optional[str],
send_emoji: str,
) -> Optional[Dict[str, Any]]:
"""
回复器 (Replier): 核心逻辑用于生成回复。
被 _run_pf_loop 直接调用和 await。
Returns dict with 'response_set' and 'send_emoji' or None on failure.
"""
log_prefix = self._get_log_prefix()
response_set: Optional[List[str]] = None
try:
# --- Tool Use and SubHF Thinking are now in _planner ---
# --- Generate Response with LLM ---
# logger.debug(f"{log_prefix}[Replier-{thinking_id}] Calling LLM to generate response...")
# 注意:实际的生成调用是在 self.heartfc_chat.gpt.generate_response 中
response_set = await self.heartfc_chat.gpt.generate_response(
anchor_message,
thinking_id,
# current_mind 不再直接传递给 gpt.generate_response
# 因为 generate_response 内部会通过 thinking_id 或其他方式获取所需上下文
)
if not response_set:
logger.warning(f"{log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
return None # Indicate failure
# --- 准备并返回结果 ---
logger.info(f"{log_prefix}[Replier-{thinking_id}] 成功生成了回复集: {' '.join(response_set)[:100]}...")
return {
"response_set": response_set,
"send_emoji": send_emoji, # Pass through the emoji determined earlier (usually by tools)
}
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 # Indicate failure