feat:合并工具调用模型和心流模型

This commit is contained in:
SengokuCola
2025-04-24 14:18:41 +08:00
parent 2871e4efc2
commit f8450f705a
22 changed files with 973 additions and 331 deletions

View File

@@ -2,7 +2,7 @@ import asyncio
import time
import traceback
from typing import List, Optional, Dict, Any, TYPE_CHECKING
import json
# import json # 移除因为使用了json_utils
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
@@ -17,6 +17,7 @@ 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
from src.plugins.utils.json_utils import extract_tool_call_arguments, safe_json_dumps, process_llm_tool_response # 导入新的JSON工具
# --- End import ---
@@ -245,9 +246,6 @@ class HeartFChatting:
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:
@@ -259,7 +257,7 @@ class HeartFChatting:
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)
anchor_message = await self._get_anchor_message()
if not anchor_message:
logger.error(f"{log_prefix} 循环: 无法获取锚点消息用于回复. 跳过周期.")
else:
@@ -304,7 +302,7 @@ class HeartFChatting:
f"{log_prefix} HeartFChatting: 麦麦决定回复表情 ('{emoji_query}'). 理由: {reasoning}"
)
action_taken_this_cycle = True
anchor = await self._get_anchor_message(observed_messages)
anchor = await self._get_anchor_message()
if anchor:
try:
# --- Handle Emoji (Moved) --- #
@@ -329,11 +327,6 @@ class HeartFChatting:
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)
@@ -395,14 +388,6 @@ class HeartFChatting:
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.")
@@ -437,77 +422,34 @@ class HeartFChatting:
"""
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": [],
}
current_mind: Optional[str] = None
llm_error = False
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}")
current_mind, _past_mind = await self.sub_hf.do_thinking_before_reply()
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 = "默认决策或获取决策失败"
action = "no_reply" # 默认动作
emoji_query = "" # 默认表情查询
reasoning = "默认决策或获取决策失败"
llm_error = False # LLM错误标志
try:
prompt = await self._build_planner_prompt(observed_messages_str, current_mind)
prompt = await self._build_planner_prompt(observed_messages_str, current_mind, self.sub_hf.structured_info)
payload = {
"model": self.planner_llm.model_name,
"messages": [{"role": "user", "content": prompt}],
@@ -515,83 +457,70 @@ class HeartFChatting:
"tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}},
}
response = await self.planner_llm._execute_request(
endpoint="/chat/completions", payload=payload, prompt=prompt
)
# 执行LLM请求
try:
response = await self.planner_llm._execute_request(
endpoint="/chat/completions", payload=payload, prompt=prompt
)
except Exception as req_e:
logger.error(f"{log_prefix}[Planner] LLM请求执行失败: {req_e}")
return {
"action": "error",
"reasoning": f"LLM请求执行失败: {req_e}",
"emoji_query": "",
"current_mind": current_mind,
"observed_messages": observed_messages,
"llm_error": True,
}
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
# 使用辅助函数处理工具调用响应
success, arguments, error_msg = process_llm_tool_response(
response,
expected_tool_name="decide_reply_action",
log_prefix=f"{log_prefix}[Planner] "
)
if success:
# 提取决策参数
action = arguments.get("action", "no_reply")
reasoning = arguments.get("reasoning", "未提供理由")
emoji_query = arguments.get("emoji_query", "")
# 记录决策结果
logger.debug(
f"{log_prefix}[Planner] 决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
)
else:
logger.warning(f"{log_prefix}[Planner] LLM 未返回预期的工具调用响应。Response parts: {len(response)}")
# 处理工具调用失败
logger.warning(f"{log_prefix}[Planner] {error_msg}")
action = "error"
reasoning = "LLM响应格式错误"
reasoning = error_msg
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?
logger.error(f"{log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}")
logger.error(traceback.format_exc()) # 记录完整堆栈以便调试
action = "error"
reasoning = f"LLM 调用失败: {llm_e}"
reasoning = f"LLM处理失败: {llm_e}"
llm_error = True
# --- 结束 LLM 决策 --- #
return {
"action": action,
"reasoning": reasoning,
"emoji_query": emoji_query, # Explicit query from Planner/LLM
"emoji_query": emoji_query,
"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]:
async def _get_anchor_message(self) -> 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
@@ -652,37 +581,41 @@ class HeartFChatting:
raise RuntimeError("发送回复失败_send_response_messages返回None")
async def shutdown(self):
"""
Gracefully shuts down the HeartFChatting instance by cancelling the active loop task.
"""
"""优雅关闭HeartFChatting实例取消活动循环任务"""
log_prefix = self._get_log_prefix()
logger.info(f"{log_prefix} Shutting down HeartFChatting...")
logger.info(f"{log_prefix} 正在关闭HeartFChatting...")
# 取消循环任务
if self._loop_task and not self._loop_task.done():
logger.info(f"{log_prefix} Cancelling active PF loop task.")
logger.info(f"{log_prefix} 正在取消HeartFChatting循环任务")
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.")
await asyncio.wait_for(self._loop_task, timeout=1.0)
logger.info(f"{log_prefix} HeartFChatting循环任务已取消")
except (asyncio.CancelledError, asyncio.TimeoutError):
pass
except Exception as e:
logger.error(f"{log_prefix} Error during loop task cancellation: {e}")
logger.error(f"{log_prefix} 取消循环任务出错: {e}")
else:
logger.info(f"{log_prefix} No active PF loop task found to cancel.")
logger.info(f"{log_prefix} 没有活动的HeartFChatting循环任务")
# 清理状态
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.")
logger.warning(f"{log_prefix} 已释放处理锁")
logger.info(f"{log_prefix} HeartFChatting关闭完成")
async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str]) -> str:
async def _build_planner_prompt(self, observed_messages_str: str, current_mind: Optional[str], structured_info: Dict[str, Any]) -> str:
"""构建 Planner LLM 的提示词"""
prompt = f"你的名字是 {global_config.BOT_NICKNAME}。是一个女大二学生正在QQ聊天正在决定是否以及如何回应当前的聊天。\n"
if structured_info:
prompt += f"以下是一些额外的信息:\n{structured_info}\n"
if observed_messages_str:
prompt += "观察到的最新聊天内容如下 (最近的消息在最后)\n---\n"
prompt += observed_messages_str
@@ -726,6 +659,7 @@ class HeartFChatting:
response_set: Optional[List[str]] = None
try:
response_set = await self.gpt_instance.generate_response(
structured_info=self.sub_hf.structured_info,
current_mind_info=self.sub_hf.current_mind,
reason=reason,
message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter)

View File

@@ -39,6 +39,7 @@ class HeartFCGenerator:
async def generate_response(
self,
structured_info: str,
current_mind_info: str,
reason: str,
message: MessageRecv,
@@ -56,7 +57,7 @@ class HeartFCGenerator:
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
structured_info, current_mind_info, reason, message, current_model, thinking_id
)
if model_response:
@@ -71,7 +72,7 @@ class HeartFCGenerator:
return None
async def _generate_response_with_model(
self, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str
self, structured_info: str, current_mind_info: str, reason: str, message: MessageRecv, model: LLMRequest, thinking_id: str
) -> str:
sender_name = ""
@@ -84,6 +85,7 @@ class HeartFCGenerator:
build_mode="focus",
reason=reason,
current_mind_info=current_mind_info,
structured_info=structured_info,
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,
@@ -103,106 +105,6 @@ class HeartFCGenerator:
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:

View File

@@ -21,6 +21,8 @@ logger = get_module_logger("prompt")
def init_prompt():
Prompt(
"""
你有以下信息可供参考:
{structured_info}
{chat_target}
{chat_talking_prompt}
现在"{sender_name}"说的:{message_txt}。引起了你的注意,你想要在群里发言发言或者回复这条消息。\n
@@ -79,17 +81,17 @@ class PromptBuilder:
self.activate_messages = ""
async def build_prompt(
self, build_mode, reason, current_mind_info, message_txt: str, sender_name: str = "某人", chat_stream=None
self, build_mode, reason, current_mind_info, structured_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 await self._build_prompt_focus(reason, current_mind_info, structured_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 = "某人"
self, reason, current_mind_info, structured_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)
@@ -148,6 +150,7 @@ class PromptBuilder:
prompt = await global_prompt_manager.format_prompt(
"heart_flow_prompt",
structured_info=structured_info,
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"),

View File

@@ -83,6 +83,7 @@ class NormalChatGenerator:
build_mode="normal",
reason="",
current_mind_info="",
structured_info="",
message_txt=message.processed_plain_text,
sender_name=sender_name,
chat_stream=message.chat_stream,