feat:采用直接输出法构建动作planner

This commit is contained in:
SengokuCola
2025-05-01 01:21:48 +08:00
parent bbbbe41476
commit 6db8dc01d5
5 changed files with 192 additions and 195 deletions

View File

@@ -634,12 +634,14 @@ HFC_STYLE_CONFIG = {
}, },
"simple": { "simple": {
"console_format": ( "console_format": (
"<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天</light-green> | <light-green>{message}</light-green>" "<level>{time:MM-DD HH:mm}</level> | <light-green>专注聊天 | {message}</light-green>"
), ),
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 专注聊天 | {message}",
}, },
} }
CONFIRM_STYLE_CONFIG = { CONFIRM_STYLE_CONFIG = {
"console_format": "<RED>{message}</RED>", # noqa: E501 "console_format": "<RED>{message}</RED>", # noqa: E501
"file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}", "file_format": "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}",

View File

@@ -10,8 +10,11 @@ logger = get_logger("mai_state")
# -- 状态相关的可配置参数 (可以从 glocal_config 加载) -- # -- 状态相关的可配置参数 (可以从 glocal_config 加载) --
# enable_unlimited_hfc_chat = True # 调试用:无限专注聊天 # The line `enable_unlimited_hfc_chat = False` is setting a configuration parameter that controls
enable_unlimited_hfc_chat = False # whether a specific debugging feature is enabled or not. When `enable_unlimited_hfc_chat` is set to
# `False`, it means that the debugging feature for unlimited focused chatting is disabled.
enable_unlimited_hfc_chat = True # 调试用:无限专注聊天
# enable_unlimited_hfc_chat = False
prevent_offline_state = True prevent_offline_state = True
# 目前默认不启用OFFLINE状态 # 目前默认不启用OFFLINE状态

View File

@@ -18,7 +18,7 @@ from src.heart_flow.sub_mind import SubMind
# 定义常量 (从 interest.py 移动过来) # 定义常量 (从 interest.py 移动过来)
MAX_INTEREST = 15.0 MAX_INTEREST = 15.0
logger = get_logger("subheartflow") logger = get_logger("sub_heartflow")
PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1 PROBABILITY_INCREASE_RATE_PER_SECOND = 0.1
PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1 PROBABILITY_DECREASE_RATE_PER_SECOND = 0.1

View File

@@ -2,6 +2,7 @@ import asyncio
import time import time
import traceback import traceback
import random # <--- 添加导入 import random # <--- 添加导入
import json # <--- 确保导入 json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque from collections import deque
from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
@@ -37,7 +38,7 @@ EMOJI_SEND_PRO = 0.3 # 设置一个概率,比如 30% 才真的发
CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值 CONSECUTIVE_NO_REPLY_THRESHOLD = 3 # 连续不回复的阈值
logger = get_logger("HFC") # Logger Name Changed logger = get_logger("hfc") # Logger Name Changed
# 默认动作定义 # 默认动作定义
@@ -119,35 +120,6 @@ class ActionManager:
"""重置为默认动作集""" """重置为默认动作集"""
self._available_actions = DEFAULT_ACTIONS.copy() self._available_actions = DEFAULT_ACTIONS.copy()
def get_planner_tool_definition(self) -> List[Dict[str, Any]]:
"""获取当前动作集对应的规划器工具定义"""
return [
{
"type": "function",
"function": {
"name": "decide_reply_action",
"description": "根据当前聊天内容和上下文,决定机器人是否应该回复以及如何回复。",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": list(self._available_actions.keys()),
"description": "决定采取的行动:"
+ ", ".join([f"'{k}'({v})" for k, v in self._available_actions.items()]),
},
"reasoning": {"type": "string", "description": "做出此决定的简要理由。"},
"emoji_query": {
"type": "string",
"description": "如果行动是'emoji_reply',指定表情的主题或概念。如果行动是'text_reply'且希望在文本后追加表情,也在此指定表情主题。",
},
},
"required": ["action", "reasoning"],
},
},
}
]
# 在文件开头添加自定义异常类 # 在文件开头添加自定义异常类
class HeartFCError(Exception): class HeartFCError(Exception):
@@ -222,7 +194,6 @@ class HeartFChatting:
max_tokens=256, max_tokens=256,
request_type="response_heartflow", request_type="response_heartflow",
) )
self.tool_user = ToolUser()
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
# LLM规划器配置 # LLM规划器配置
@@ -784,225 +755,208 @@ class HeartFChatting:
async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]: async def _planner(self, current_mind: str, cycle_timers: dict, is_re_planned: bool = False) -> Dict[str, Any]:
""" """
规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。 规划器 (Planner): 使用LLM根据上下文决定是否和如何回复。
重构为让LLM返回结构化JSON文本然后在代码中解析。
参数: 参数:
current_mind: 子思维的当前思考结果 current_mind: 子思维的当前思考结果
cycle_timers: 计时器字典 cycle_timers: 计时器字典
is_re_planned: 是否为重新规划 is_re_planned: 是否为重新规划 (此重构中暂时简化,不处理 is_re_planned 的特殊逻辑)
""" """
logger.info(f"{self.log_prefix}[Planner] 开始{'重新' if is_re_planned else ''}执行规划器") logger.info(f"{self.log_prefix}[Planner] 开始执行规划器 (JSON解析模式)")
# --- 新增:检查历史动作并调整可用动作 ---
lian_xu_wen_ben_hui_fu = 0 # 连续文本回复次数
actions_to_remove_temporarily = [] actions_to_remove_temporarily = []
probability_roll = random.random() # 在循环外掷骰子一次,用于概率判断 # --- 检查历史动作并决定临时移除动作 (逻辑保持不变) ---
lian_xu_wen_ben_hui_fu = 0
# 反向遍历最近的循环历史 probability_roll = random.random()
for cycle in reversed(self._cycle_history): for cycle in reversed(self._cycle_history):
# 只关心实际执行了动作的循环
if cycle.action_taken: if cycle.action_taken:
if cycle.action_type == "text_reply": if cycle.action_type == "text_reply":
lian_xu_wen_ben_hui_fu += 1 lian_xu_wen_ben_hui_fu += 1
else: else:
break # 遇到非文本回复,中断计数 break
# 检查最近的3个循环即可避免检查过多历史 (如果历史很长)
if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + ( if len(self._cycle_history) > 0 and cycle.cycle_id <= self._cycle_history[0].cycle_id + (
len(self._cycle_history) - 4 len(self._cycle_history) - 4
): ):
break break
logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}") logger.debug(f"{self.log_prefix}[Planner] 检测到连续文本回复次数: {lian_xu_wen_ben_hui_fu}")
# 根据连续次数决定临时移除哪些动作
if lian_xu_wen_ben_hui_fu >= 3: if lian_xu_wen_ben_hui_fu >= 3:
logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply") logger.info(f"{self.log_prefix}[Planner] 连续回复 >= 3 次,强制移除 text_reply 和 emoji_reply")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
elif lian_xu_wen_ben_hui_fu == 2: elif lian_xu_wen_ben_hui_fu == 2:
if probability_roll < 0.8: # 80% 概率 if probability_roll < 0.8:
logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (触发)")
actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"]) actions_to_remove_temporarily.extend(["text_reply", "emoji_reply"])
else: else:
logger.info( logger.info(f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)")
f"{self.log_prefix}[Planner] 连续回复 2 次80% 概率移除 text_reply 和 emoji_reply (未触发)"
)
elif lian_xu_wen_ben_hui_fu == 1: elif lian_xu_wen_ben_hui_fu == 1:
if probability_roll < 0.4: # 40% 概率 if probability_roll < 0.4:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (触发)")
actions_to_remove_temporarily.append("text_reply") actions_to_remove_temporarily.append("text_reply")
else: else:
logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)") logger.info(f"{self.log_prefix}[Planner] 连续回复 1 次40% 概率移除 text_reply (未触发)")
# 如果 lian_xu_wen_ben_hui_fu == 0则不移除任何动作 # --- 结束检查历史动作 ---
# --- 结束:检查历史动作 ---
# 获取观察信息 # 获取观察信息
observation = self.observations[0] observation = self.observations[0]
if is_re_planned: # if is_re_planned: # 暂时简化,不处理重新规划
await observation.observe() # await observation.observe()
observed_messages = observation.talking_message observed_messages = observation.talking_message
observed_messages_str = observation.talking_message_str_truncate observed_messages_str = observation.talking_message_str_truncate
# --- 使用 LLM 进行决策 --- # # --- 使用 LLM 进行决策 (JSON 输出模式) --- #
reasoning = "默认决策或获取决策失败" action = "no_reply" # 默认动作
llm_error = False # LLM错误标志 reasoning = "规划器初始化默认"
arguments = None # 初始化参数变量 emoji_query = ""
emoji_query = "" # <--- 在这里初始化 emoji_query llm_error = False # LLM 请求或解析错误标志
# 获取我们将传递给 prompt 构建器和用于验证的当前可用动作
current_available_actions = self.action_manager.get_available_actions()
try: try:
# --- 新增:应用临时动作移除 --- # --- 应用临时动作移除 ---
if actions_to_remove_temporarily: if actions_to_remove_temporarily:
self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily) self.action_manager.temporarily_remove_actions(actions_to_remove_temporarily)
# 更新 current_available_actions 以反映移除后的状态
current_available_actions = self.action_manager.get_available_actions()
logger.debug( logger.debug(
f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(self.action_manager.get_available_actions().keys())}" f"{self.log_prefix}[Planner] 临时移除的动作: {actions_to_remove_temporarily}, 当前可用: {list(current_available_actions.keys())}"
) )
# --- 构建提示词 --- # --- 构建提示词 (调用修改后的 _build_planner_prompt) ---
replan_prompt_str = "" # replan_prompt_str = "" # 暂时简化
if is_re_planned: # if is_re_planned:
replan_prompt_str = await self._build_replan_prompt( # replan_prompt_str = await self._build_replan_prompt(
self._current_cycle.action_type, self._current_cycle.reasoning # self._current_cycle.action_type, self._current_cycle.reasoning
) # )
prompt = await self._build_planner_prompt( prompt = await self._build_planner_prompt(
observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str observed_messages_str,
current_mind,
self.sub_mind.structured_info,
"", # replan_prompt_str,
current_available_actions # <--- 传入当前可用动作
) )
# --- 调用 LLM --- # --- 调用 LLM (普通文本生成) ---
llm_content = None
try: try:
planner_tools = self.action_manager.get_planner_tool_definition() # 假设 LLMRequest 有 generate_response 方法返回 (content, reasoning, model_name)
logger.debug(f"{self.log_prefix}[Planner] 本次使用的工具定义: {planner_tools}") # 记录本次使用的工具 # 我们只需要 content
_response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async( # !! 注意:这里假设 self.planner_llmgenerate_response 方法
prompt=prompt, # !! 如果你的 LLMRequest 类使用的是其他方法名,请相应修改
tools=planner_tools, llm_content, _, _ = await self.planner_llm.generate_response(prompt=prompt)
) logger.debug(f"{self.log_prefix}[Planner] LLM 原始 JSON 响应 (预期): {llm_content}")
logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
except Exception as req_e: except Exception as req_e:
logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}") logger.error(f"{self.log_prefix}[Planner] LLM 请求执行失败: {req_e}")
action = "error"
reasoning = f"LLM 请求失败: {req_e}" reasoning = f"LLM 请求失败: {req_e}"
llm_error = True llm_error = True
# 直接返回错误结果 # 直接使用默认动作返回错误结果
return { action = "no_reply" # 明确设置为默认值
"action": action, emoji_query = "" # 明确设置为空
"reasoning": reasoning, # 不再立即返回,而是继续执行 finally 块以恢复动作
"emoji_query": "", # return { ... }
"current_mind": current_mind,
"observed_messages": observed_messages,
"llm_error": llm_error,
}
# 默认错误状态 # --- 解析 LLM 返回的 JSON (仅当 LLM 请求未出错时进行) ---
action = "error" if not llm_error and llm_content:
reasoning = "处理工具调用时出错" try:
llm_error = True # 尝试去除可能的 markdown 代码块标记
cleaned_content = llm_content.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
if not cleaned_content:
raise json.JSONDecodeError("Cleaned content is empty", cleaned_content, 0)
parsed_json = json.loads(cleaned_content)
# 1. 验证工具调用 # 提取决策,提供默认值
success, valid_tool_calls, error_msg = process_llm_tool_calls( extracted_action = parsed_json.get("action", "no_reply")
tool_calls, log_prefix=f"{self.log_prefix}[Planner] " extracted_reasoning = parsed_json.get("reasoning", "LLM未提供理由")
) extracted_emoji_query = parsed_json.get("emoji_query", "")
if success and valid_tool_calls: # 验证动作是否在当前可用列表中
# 2. 提取第一个调用并获取参数 # !! 使用调用 prompt 时实际可用的动作列表进行验证
first_tool_call = valid_tool_calls[0] if extracted_action not in current_available_actions:
tool_name = first_tool_call.get("function", {}).get("name")
arguments = extract_tool_call_arguments(first_tool_call, None)
# 3. 检查名称和参数
expected_tool_name = "decide_reply_action"
if tool_name == expected_tool_name and arguments is not None:
# 4. 成功,提取决策
extracted_action = arguments.get("action", "no_reply")
# 验证动作
if extracted_action not in self.action_manager.get_available_actions():
# 如果LLM返回了一个此时不该用的动作因为被临时移除了
# 或者完全无效的动作
logger.warning( logger.warning(
f"{self.log_prefix}[Planner] LLM返回了当前不可用或无效的动作: {extracted_action},将强制使用 'no_reply'" f"{self.log_prefix}[Planner] LLM 返回了当前不可用或无效的动作: '{extracted_action}' (可用: {list(current_available_actions.keys())}),将强制使用 'no_reply'"
) )
action = "no_reply" action = "no_reply"
reasoning = f"LLM返回了当前不可用的动作: {extracted_action}" reasoning = f"LLM 返回了当前不可用的动作 '{extracted_action}' (可用: {list(current_available_actions.keys())})。原始理由: {extracted_reasoning}"
emoji_query = "" emoji_query = ""
llm_error = False # 视为逻辑修正而非 LLM 错误 # 检查 no_reply 是否也恰好被移除了 (极端情况)
# --- 检查 'no_reply' 是否也恰好被移除了 (极端情况) --- if "no_reply" not in current_available_actions:
if "no_reply" not in self.action_manager.get_available_actions(): logger.error(f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。")
logger.error(
f"{self.log_prefix}[Planner] 严重错误:'no_reply' 动作也不可用!无法执行任何动作。"
)
action = "error" # 回退到错误状态 action = "error" # 回退到错误状态
reasoning = "无法执行任何有效动作,包括 no_reply" reasoning = "无法执行任何有效动作,包括 no_reply"
llm_error = True llm_error = True # 标记为严重错误
else: else:
# 动作有效且可用,使用提取的值 llm_error = False # 视为逻辑修正而非 LLM 错误
else:
# 动作有效且可用
action = extracted_action action = extracted_action
reasoning = arguments.get("reasoning", "未提供理由") reasoning = extracted_reasoning
emoji_query = arguments.get("emoji_query", "") emoji_query = extracted_emoji_query
llm_error = False # 成功处理 llm_error = False # 解析成功
# 记录决策结果
logger.debug( logger.debug(
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'" f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果 (来自JSON): {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
) )
elif tool_name != expected_tool_name:
reasoning = f"LLM返回了非预期的工具: {tool_name}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # arguments is None
reasoning = f"无法提取工具 {tool_name} 的参数"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
elif not success:
reasoning = f"验证工具调用失败: {error_msg}"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
else: # not valid_tool_calls
# 如果没有有效的工具调用,我们需要检查 'no_reply' 是否是当前唯一可用的动作
available_actions = list(self.action_manager.get_available_actions().keys())
if available_actions == ["no_reply"]:
logger.info(
f"{self.log_prefix}[Planner] LLM未返回工具调用但当前唯一可用动作是 'no_reply',将执行 'no_reply'"
)
action = "no_reply"
reasoning = "LLM未返回工具调用且当前仅 'no_reply' 可用"
emoji_query = ""
llm_error = False # 视为逻辑选择而非错误
else:
reasoning = "LLM未返回有效的工具调用"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
# llm_error 保持为 True
# 如果 llm_error 仍然是 True说明在处理过程中有错误发生
except Exception as llm_e: except json.JSONDecodeError as json_e:
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}") logger.warning(f"{self.log_prefix}[Planner] 解析LLM响应JSON失败: {json_e}. LLM原始输出: '{llm_content}'")
logger.error(traceback.format_exc()) reasoning = f"解析LLM响应JSON失败: {json_e}. 将使用默认动作 'no_reply'."
action = "error" action = "no_reply" # 解析失败则默认不回复
reasoning = f"Planner内部处理错误: {llm_e}" emoji_query = ""
llm_error = True # 标记解析错误
except Exception as parse_e:
logger.error(f"{self.log_prefix}[Planner] 处理LLM响应时发生意外错误: {parse_e}")
reasoning = f"处理LLM响应时发生意外错误: {parse_e}. 将使用默认动作 'no_reply'."
action = "no_reply"
emoji_query = ""
llm_error = True
elif not llm_error and not llm_content:
# LLM 请求成功但返回空内容
logger.warning(f"{self.log_prefix}[Planner] LLM 返回了空内容。")
reasoning = "LLM 返回了空内容,使用默认动作 'no_reply'."
action = "no_reply"
emoji_query = ""
llm_error = True # 标记为空响应错误
# 如果 llm_error 在此阶段为 True意味着请求成功但解析失败或返回空
# 如果 llm_error 在请求阶段就为 True则跳过了此解析块
except Exception as outer_e:
logger.error(f"{self.log_prefix}[Planner] Planner 处理过程中发生意外错误: {outer_e}")
logger.error(traceback.format_exc())
action = "error" # 发生未知错误,标记为 error 动作
reasoning = f"Planner 内部处理错误: {outer_e}"
emoji_query = ""
llm_error = True llm_error = True
# --- 新增:确保动作恢复 ---
finally: finally:
if actions_to_remove_temporarily: # 只有当确实移除了动作时才需要恢复 # --- 确保动作恢复 ---
# 检查 self._original_actions_backup 是否有值来判断是否需要恢复
if self.action_manager._original_actions_backup is not None:
self.action_manager.restore_actions() self.action_manager.restore_actions()
logger.debug( logger.debug(
f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}" f"{self.log_prefix}[Planner] 恢复了原始动作集, 当前可用: {list(self.action_manager.get_available_actions().keys())}"
) )
# --- 结束确保动作恢复 --- # --- 结束确保动作恢复 ---
# --- 新增:概率性忽略文本回复附带的表情(正确的位置)---
# --- 概率性忽略文本回复附带的表情 (逻辑保持不变) ---
if action == "text_reply" and emoji_query: if action == "text_reply" and emoji_query:
logger.debug(f"{self.log_prefix}[Planner] 大模型想让麦麦发文字时带表情: '{emoji_query}'") logger.debug(f"{self.log_prefix}[Planner] 大模型建议文字回复带表情: '{emoji_query}'")
# 掷骰子看看要不要听它的
if random.random() > EMOJI_SEND_PRO: if random.random() > EMOJI_SEND_PRO:
logger.info( logger.info(
f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'" f"{self.log_prefix}[Planner] 但是麦麦这次不想加表情 ({1 - EMOJI_SEND_PRO:.0%}),忽略表情 '{emoji_query}'"
) )
emoji_query = "" # 表情请求清空,就不发了 emoji_query = "" # 清空表情请求
else: else:
logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'") logger.info(f"{self.log_prefix}[Planner] 好吧,加上表情 '{emoji_query}'")
# --- 结束概率性忽略 --- # --- 结束概率性忽略 ---
# --- 结束 LLM 决策 --- #
# 返回结果字典
return { return {
"action": action, "action": action,
"reasoning": reasoning, "reasoning": reasoning,
"emoji_query": emoji_query, "emoji_query": emoji_query,
"current_mind": current_mind, "current_mind": current_mind,
"observed_messages": observed_messages, "observed_messages": observed_messages,
"llm_error": llm_error, "llm_error": llm_error, # 返回错误状态
} }
async def _get_anchor_message(self) -> Optional[MessageRecv]: async def _get_anchor_message(self) -> Optional[MessageRecv]:
@@ -1146,8 +1100,9 @@ class HeartFChatting:
current_mind: Optional[str], current_mind: Optional[str],
structured_info: Dict[str, Any], structured_info: Dict[str, Any],
replan_prompt: str, replan_prompt: str,
current_available_actions: Dict[str, str],
) -> str: ) -> str:
"""构建 Planner LLM 的提示词""" """构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try: try:
# 准备结构化信息块 # 准备结构化信息块
structured_info_block = "" structured_info_block = ""
@@ -1163,12 +1118,13 @@ class HeartFChatting:
else: else:
chat_content_block = "当前没有观察到新的聊天内容。\n" chat_content_block = "当前没有观察到新的聊天内容。\n"
# 准备当前思维块 # 准备当前思维块 (修改以匹配模板)
current_mind_block = "" current_mind_block = ""
if current_mind: if current_mind:
current_mind_block = f"{current_mind}" # 模板中占位符是 {current_mind_block},它期望包含"你的内心想法:"的前缀
current_mind_block = f"你的内心想法:\n{current_mind}"
else: else:
current_mind_block = "[没有特别的想法]" current_mind_block = "你的内心想法:\n[没有特别的想法]"
# 准备循环信息块 (分析最近的活动循环) # 准备循环信息块 (分析最近的活动循环)
recent_active_cycles = [] recent_active_cycles = []
@@ -1208,23 +1164,40 @@ class HeartFChatting:
# 包装提示块,增加可读性,即使没有连续回复也给个标记 # 包装提示块,增加可读性,即使没有连续回复也给个标记
if cycle_info_block: if cycle_info_block:
# 模板中占位符是 {cycle_info_block},它期望包含"【近期回复历史】"的前缀
cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n" cycle_info_block = f"\n【近期回复历史】\n{cycle_info_block}\n"
else: else:
# 如果最近的活动循环不是文本回复,或者没有活动循环 # 如果最近的活动循环不是文本回复,或者没有活动循环
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
# 模板中占位符是 {prompt_personality}
prompt_personality = individuality.get_prompt(x_person=2, level=2) prompt_personality = individuality.get_prompt(x_person=2, level=2)
# 获取提示词模板并填充数据 # --- 构建可用动作描述 (用于填充模板中的 {action_options_text}) ---
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( action_options_text = "当前你可以选择的行动有:\n"
action_keys = list(current_available_actions.keys())
for name in action_keys:
desc = current_available_actions[name]
action_options_text += f"- '{name}': {desc}\n"
# --- 选择一个示例动作键 (用于填充模板中的 {example_action}) ---
example_action_key = action_keys[0] if action_keys else "no_reply"
# --- 获取提示词模板 ---
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
# --- 填充模板 ---
prompt = planner_prompt_template.format(
bot_name=global_config.BOT_NICKNAME, bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality, prompt_personality=prompt_personality,
structured_info_block=structured_info_block, structured_info_block=structured_info_block,
chat_content_block=chat_content_block, chat_content_block=chat_content_block,
current_mind_block=current_mind_block, current_mind_block=current_mind_block,
replan=replan_prompt, replan="", # 暂时留空 replan 信息
cycle_info_block=cycle_info_block, cycle_info_block=cycle_info_block,
action_options_text=action_options_text, # 传入可用动作描述
example_action=example_action_key # 传入示例动作键
) )
return prompt return prompt
@@ -1232,7 +1205,7 @@ class HeartFChatting:
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}") logger.error(f"{self.log_prefix}[Planner] 构建提示词时出错: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return "" return "[构建 Planner Prompt 时出错]" # 返回错误提示,避免空字符串
# --- 回复器 (Replier) 的定义 --- # # --- 回复器 (Replier) 的定义 --- #
async def _replier_work( async def _replier_work(
@@ -1273,7 +1246,7 @@ class HeartFChatting:
try: try:
with Timer("LLM生成", {}): # 内部计时器,可选保留 with Timer("LLM生成", {}): # 内部计时器,可选保留
content, reasoning_content, model_name = await self.model_normal.generate_response(prompt) content, reasoning_content, model_name = await self.model_normal.generate_response(prompt)
logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n") # logger.info(f"{self.log_prefix}[Replier-{thinking_id}]\\nPrompt:\\n{prompt}\\n生成回复: {content}\\n")
# 捕捉 LLM 输出信息 # 捕捉 LLM 输出信息
info_catcher.catch_after_llm_generated( info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name

View File

@@ -7,13 +7,14 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_
from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.chat.utils import get_embedding from src.plugins.chat.utils import get_embedding
import time import time
from typing import Union, Optional from typing import Union, Optional, Dict, Any
from ...common.database import db from ...common.database import db
from ..chat.utils import get_recent_group_speaker from ..chat.utils import get_recent_group_speaker
from ..moods.moods import MoodManager from ..moods.moods import MoodManager
from ..memory_system.Hippocampus import HippocampusManager from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager from ..knowledge.knowledge_lib import qa_manager
import traceback
logger = get_logger("prompt") logger = get_logger("prompt")
@@ -47,17 +48,15 @@ def init_prompt():
"info_from_tools", "info_from_tools",
) )
# Planner提示词 - 优化版 # Planner提示词 - 修改为要求 JSON 输出
Prompt( Prompt(
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话: '''你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
{structured_info_block} {structured_info_block}
{chat_content_block} {chat_content_block}
你的内心想法:
{current_mind_block} {current_mind_block}
{replan}
{cycle_info_block} {cycle_info_block}
请综合分析聊天内容和你看到的新消息,参考内心想法,使用'decide_reply_action'工具做出决策。决策时请注意: 请综合分析聊天内容和你看到的新消息,参考内心想法,并根据以下原则和可用动作做出决策。
【回复原则】 【回复原则】
1. 不回复(no_reply)适用: 1. 不回复(no_reply)适用:
@@ -81,14 +80,34 @@ def init_prompt():
- 避免重复或评价自己的发言 - 避免重复或评价自己的发言
- 不要和自己聊天 - 不要和自己聊天
【必须遵守 【决策任务
- 遵守回复原则 {action_options_text}
- 必须调用工具并包含action和reasoning
- 你可以选择文字回复(text_reply),纯表情回复(emoji_reply),不回复(no_reply) 你必须从上面列出的可用行动中选择一个,并说明原因。
- 并不是所有选择都可用 你的决策必须以严格的 JSON 格式输出,且仅包含 JSON 内容,不要有任何其他文字或解释。
- 选择text_reply或emoji_reply时必须提供emoji_query JSON 结构如下,包含三个字段 "action", "reasoning", "emoji_query":
- 保持回复自然,符合日常聊天习惯""", {{
"planner_prompt", "action": "string", // 必须是上面提供的可用行动之一 (例如: '{example_action}')
"reasoning": "string", // 做出此决定的详细理由和思考过程,说明你如何应用了回复原则
"emoji_query": "string" // 可选。如果行动是 'emoji_reply',必须提供表情主题;如果行动是 'text_reply' 且你想附带表情,也在此提供表情主题,否则留空字符串 ""。遵循回复原则,不要滥用。
}}
例如:
{{
"action": "text_reply",
"reasoning": "用户提到了我,且问题比较具体,适合用文本回复。考虑到内容,可以带上一个微笑表情。",
"emoji_query": "微笑"
}}
{{
"action": "no_reply",
"reasoning": "我已经连续回复了两次,而且这个话题我不太感兴趣,根据回复原则,选择不回复,等待其他人发言。",
"emoji_query": ""
}}
请输出你的决策 JSON
''', # 使用三引号避免内部引号问题
"planner_prompt", # 保持名称不变,替换内容
) )
Prompt( Prompt(