Files
Mofox-Core/src/chat/planner_actions/planner.py
minecraft1024a 132354804c feat(planner): 实现大小脑规划器分离以优化决策流程
将规划器(Planner)拆分为“大脑”和“小脑”两个部分,以实现更精细化的决策控制。

- **大脑(BIG_BRAIN)**: 负责宏观决策,如是否回复、是否需要@人等高层级意图。
- **小脑(SMALL_BRAIN)**: 负责具体的功能性动作执行。

此重构引入了 `PlannerType` 枚举,并更新了动作(Action)定义,允许将动作明确分配给大脑或小脑,从而提升了AI回复的逻辑性和条理性。同时,新增了 `no_action` 类型,用于在规划阶段明确表示“无动作”,提高了处理流程的清晰度。
2025-09-06 20:49:56 +08:00

839 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import orjson
import time
import traceback
import asyncio
import math
import random
import json
from typing import Dict, Any, Optional, Tuple, List, TYPE_CHECKING
from rich.traceback import install
from datetime import datetime
from json_repair import repair_json
from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config
from src.common.logger import get_logger
from src.chat.utils.prompt import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import (
build_readable_actions,
get_actions_by_timestamp_with_chat,
build_readable_messages_with_id,
get_raw_msg_before_timestamp_with_chat,
)
from src.chat.utils.utils import get_chat_type_and_target_info
from src.chat.planner_actions.action_manager import ActionManager
from src.chat.message_receive.chat_stream import get_chat_manager
from src.plugin_system.base.component_types import (
ActionInfo,
ChatMode,
ComponentType,
ActionActivationType,
PlannerType,
)
from src.plugin_system.core.component_registry import component_registry
from src.schedule.schedule_manager import schedule_manager
from src.mood.mood_manager import mood_manager
from src.chat.memory_system.Hippocampus import hippocampus_manager
if TYPE_CHECKING:
pass
logger = get_logger("planner")
install(extra_lines=3)
def init_prompt():
Prompt(
"""
{schedule_block}
{mood_block}
{time_block}
{identity_block}
{custom_prompt_block}
{chat_context_description},以下是具体的聊天内容。
{chat_content_block}
{moderation_prompt}
现在请你根据聊天内容和用户的最新消息选择合适的action和触发action的消息:
{actions_before_now_block}
{no_action_block}
动作reply
动作描述:参与聊天回复,发送文本进行表达
- 你想要闲聊或者随便附
- {mentioned_bonus}
- 如果你刚刚进行了回复,不要对同一个话题重复回应
- 不要回复自己发送的消息
{{
"action": "reply",
"target_message_id":"触发action的消息id",
"reason":"回复的原因"
}}
{action_options_text}
你必须从上面列出的可用action中选择一个并说明触发action的消息id不是消息原文和选择该action的原因。消息id格式:m+数字
请根据动作示例,以严格的 JSON 格式输出不要输出markdown格式```json等内容直接输出且仅包含 JSON 内容:
""",
"planner_prompt",
)
Prompt(
"""
# 主动思考决策
## 你的内部状态
{time_block}
{identity_block}
{schedule_block}
{mood_block}
## 长期记忆摘要
{long_term_memory_block}
## 最近的聊天内容
{chat_content_block}
## 任务
基于以上所有信息(特别是最近的聊天内容),分析当前情况,决定是否适合主动开启一个**新的、但又与当前氛围相关**的话题。
## 可用动作
动作proactive_reply
动作描述:在当前对话的基础上,主动发起一个新的对话,分享一个有趣的想法、见闻或者对未来的计划。
- 当你觉得可以说些什么来活跃气氛,并且内容与当前聊天氛围不冲突时
- 当你有一些新的想法或计划想要分享,并且可以自然地衔接当前话题时
{{
"action": "proactive_reply",
"reason": "决定主动发起对话的具体原因",
"topic": "你想要发起对话的主题或内容(需要简洁)"
}}
动作do_nothing
动作描述:保持沉默,不主动发起任何动作或对话。
- 当你分析了所有信息后,觉得当前不是一个发起互动的好时机时
- 当最近的聊天内容很连贯,你的插入会打断别人时
{{
"action": "do_nothing",
"reason":"决定保持沉默的具体原因"
}}
你必须从上面列出的可用action中选择一个。
请以严格的 JSON 格式输出,且仅包含 JSON 内容:
""",
"proactive_planner_prompt",
)
Prompt(
"""
动作:{action_name}
动作描述:{action_description}
{action_require}
{{
"action": "{action_name}",{action_parameters},
"target_message_id":"触发action的消息id",
"reason":"触发action的原因"
}}
""",
"action_prompt",
)
Prompt(
"""
{name_block}
{chat_context_description}{time_block}现在请你根据以下聊天内容选择一个或多个合适的action。如果没有合适的action请选择no_action。,
{chat_content_block}
**要求**
1.action必须符合使用条件如果符合条件就选择
2.如果聊天内容不适合使用action即使符合条件也不要使用
3.{moderation_prompt}
4.请注意如果相同的内容已经被执行,请不要重复执行
这是你最近执行过的动作:
{actions_before_now_block}
**可用的action**
no_action不选择任何动作
{{
"action": "no_action",
"reason":"不动作的原因"
}}
{action_options_text}
请选择并说明触发action的消息id和选择该action的原因。消息id格式:m+数字
请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
""",
"sub_planner_prompt",
)
class ActionPlanner:
def __init__(self, chat_id: str, action_manager: ActionManager):
self.chat_id = chat_id
self.log_prefix = f"[{get_chat_manager().get_stream_name(chat_id) or chat_id}]"
self.action_manager = action_manager
# LLM规划器配置
# --- 大脑 ---
self.planner_llm = LLMRequest(
model_set=model_config.model_task_config.planner, request_type="planner"
)
# --- 小脑 (新增) ---
self.planner_small_llm = LLMRequest(
model_set=model_config.model_task_config.planner_small, request_type="planner_small"
)
self.last_obs_time_mark = 0.0
async def _get_long_term_memory_context(self) -> str:
"""
获取长期记忆上下文
"""
try:
# 1. 生成时间相关的关键词
now = datetime.now()
keywords = ["今天", "日程", "计划"]
if 5 <= now.hour < 12:
keywords.append("早上")
elif 12 <= now.hour < 18:
keywords.append("中午")
else:
keywords.append("晚上")
# TODO: 添加与聊天对象相关的关键词
# 2. 调用 hippocampus_manager 检索记忆
retrieved_memories = await hippocampus_manager.get_memory_from_topic(
valid_keywords=keywords, max_memory_num=5, max_memory_length=1
)
if not retrieved_memories:
return "最近没有什么特别的记忆。"
# 3. 格式化记忆
memory_statements = []
for topic, memory_item in retrieved_memories:
memory_statements.append(f"关于'{topic}', 你记得'{memory_item}'")
return " ".join(memory_statements)
except Exception as e:
logger.error(f"获取长期记忆时出错: {e}")
return "回忆时出现了一些问题。"
async def _build_action_options(
self,
current_available_actions: Dict[str, ActionInfo],
mode: ChatMode,
target_prompt: str = "",
) -> str:
"""
构建动作选项
"""
action_options_block = ""
if mode == ChatMode.PROACTIVE:
action_options_block += """动作do_nothing
动作描述:保持沉默,不主动发起任何动作或对话。
- 当你分析了所有信息后,觉得当前不是一个发起互动的好时机时
{{
"action": "do_nothing",
"reason":"决定保持沉默的具体原因"
}}
"""
for action_name, action_info in current_available_actions.items():
# TODO: 增加一个字段来判断action是否支持在PROACTIVE模式下使用
param_text = ""
if action_info.action_parameters:
param_text = "\n" + "\n".join(
f' "{p_name}":"{p_desc}"' for p_name, p_desc in action_info.action_parameters.items()
)
require_text = "\n".join(f"- {req}" for req in action_info.action_require)
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
action_options_block += using_action_prompt.format(
action_name=action_name,
action_description=action_info.description,
action_parameters=param_text,
action_require=require_text,
)
return action_options_block
def find_message_by_id(self, message_id: str, message_id_list: list) -> Optional[Dict[str, Any]]:
# sourcery skip: use-next
"""
根据message_id从message_id_list中查找对应的原始消息
Args:
message_id: 要查找的消息ID
message_id_list: 消息ID列表格式为[{'id': str, 'message': dict}, ...]
Returns:
找到的原始消息字典如果未找到则返回None
"""
# 检测message_id 是否为纯数字
if message_id.isdigit():
message_id = f"m{message_id}"
for item in message_id_list:
if item.get("id") == message_id:
return item.get("message")
return None
def get_latest_message(self, message_id_list: list) -> Optional[Dict[str, Any]]:
"""
获取消息列表中的最新消息
Args:
message_id_list: 消息ID列表格式为[{'id': str, 'message': dict}, ...]
Returns:
最新的消息字典如果列表为空则返回None
"""
if not message_id_list:
return None
# 假设消息列表是按时间顺序排列的,最后一个是最新的
return message_id_list[-1].get("message")
def _parse_single_action(
self,
action_json: dict,
message_id_list: list, # 使用 planner.py 的 list of dict
current_available_actions: list, # 使用 planner.py 的 list of tuple
) -> List[Dict[str, Any]]:
"""
[注释] 解析单个小脑LLM返回的action JSON并将其转换为标准化的字典。
"""
parsed_actions = []
try:
action = action_json.get("action", "no_action")
reasoning = action_json.get("reason", "未提供原因")
action_data = {k: v for k, v in action_json.items() if k not in ["action", "reason"]}
target_message = None
if action != "no_action":
if target_message_id := action_json.get("target_message_id"):
target_message = self.find_message_by_id(target_message_id, message_id_list)
if target_message is None:
logger.warning(f"{self.log_prefix}无法找到target_message_id '{target_message_id}'")
target_message = self.get_latest_message(message_id_list)
else:
logger.warning(f"{self.log_prefix}动作'{action}'缺少target_message_id")
available_action_names = [name for name, _ in current_available_actions]
if action not in ["no_action", "reply"] and action not in available_action_names:
logger.warning(
f"{self.log_prefix}LLM 返回了当前不可用或无效的动作: '{action}' (可用: {available_action_names}),将强制使用 'no_action'"
)
reasoning = f"LLM 返回了当前不可用的动作 '{action}' (可用: {available_action_names})。原始理由: {reasoning}"
action = "no_action"
# 将列表转换为字典格式以供将来使用
available_actions_dict = dict(current_available_actions)
parsed_actions.append(
{
"action_type": action,
"reasoning": reasoning,
"action_data": action_data,
"action_message": target_message,
"available_actions": available_actions_dict,
}
)
except Exception as e:
logger.error(f"{self.log_prefix}解析单个action时出错: {e}")
parsed_actions.append(
{
"action_type": "no_action",
"reasoning": f"解析action时出错: {e}",
"action_data": {},
"action_message": None,
"available_actions": dict(current_available_actions),
}
)
return parsed_actions
def _filter_no_actions(self, action_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
[注释] 从一个action字典列表中过滤掉所有的 'no_action'
如果过滤后列表为空, 则返回一个空的列表, 或者根据需要返回一个默认的no_action字典。
"""
non_no_actions = [a for a in action_list if a.get("action_type") not in ["no_action", "no_reply"]]
if non_no_actions:
return non_no_actions
# 如果都是 no_action则返回一个包含第一个 no_action 的列表,以保留 reason
return action_list[:1] if action_list else []
async def sub_plan(
self,
action_list: list, # 使用 planner.py 的 list of tuple
chat_content_block: str,
message_id_list: list, # 使用 planner.py 的 list of dict
is_group_chat: bool = False,
chat_target_info: Optional[dict] = None,
) -> List[Dict[str, Any]]:
"""
[注释] "小脑"规划器。接收一小组actions使用轻量级LLM判断其中哪些应该被触发。
这是一个独立的、并行的思考单元。返回一个包含action字典的列表。
"""
try:
actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=self.chat_id,
timestamp_start=time.time() - 1200,
timestamp_end=time.time(),
limit=20,
)
action_names_in_list = [name for name, _ in action_list]
filtered_actions = [
record for record in actions_before_now if record.get("action_name") in action_names_in_list
]
actions_before_now_block = build_readable_actions(actions=filtered_actions)
chat_context_description = "你现在正在一个群聊中"
if not is_group_chat and chat_target_info:
chat_target_name = chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方"
chat_context_description = f"你正在和 {chat_target_name} 私聊"
action_options_block = ""
for using_actions_name, using_actions_info in action_list:
param_text = ""
if using_actions_info.action_parameters:
param_text = "\n" + "\n".join(
f' "{p_name}":"{p_desc}"'
for p_name, p_desc in using_actions_info.action_parameters.items()
)
require_text = "\n".join(f"- {req}" for req in using_actions_info.action_require)
using_action_prompt = await global_prompt_manager.get_prompt_async("action_prompt")
action_options_block += using_action_prompt.format(
action_name=using_actions_name,
action_description=using_actions_info.description,
action_parameters=param_text,
action_require=require_text,
)
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
bot_name = global_config.bot.nickname
bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else ""
name_block = f"你的名字是{bot_name}{bot_nickname},请注意哪些是你自己的发言。"
planner_prompt_template = await global_prompt_manager.get_prompt_async("sub_planner_prompt")
prompt = planner_prompt_template.format(
time_block=time_block,
chat_context_description=chat_context_description,
chat_content_block=chat_content_block,
actions_before_now_block=actions_before_now_block,
action_options_text=action_options_block,
moderation_prompt=moderation_prompt_block,
name_block=name_block,
)
except Exception as e:
logger.error(f"构建小脑提示词时出错: {e}\n{traceback.format_exc()}")
return [{"action_type": "no_action", "reasoning": f"构建小脑Prompt时出错: {e}"}]
action_dicts: List[Dict[str, Any]] = []
try:
llm_content, (reasoning_content, _, _) = await self.planner_small_llm.generate_response_async(prompt=prompt)
if global_config.debug.show_prompt:
logger.info(f"{self.log_prefix}小脑原始提示词: {prompt}")
logger.info(f"{self.log_prefix}小脑原始响应: {llm_content}")
else:
logger.debug(f"{self.log_prefix}小脑原始响应: {llm_content}")
if llm_content:
parsed_json = orjson.loads(repair_json(llm_content))
if isinstance(parsed_json, list):
for item in parsed_json:
if isinstance(item, dict):
action_dicts.extend(self._parse_single_action(item, message_id_list, action_list))
elif isinstance(parsed_json, dict):
action_dicts.extend(self._parse_single_action(parsed_json, message_id_list, action_list))
except Exception as e:
logger.warning(f"{self.log_prefix}解析小脑响应JSON失败: {e}. LLM原始输出: '{llm_content}'")
action_dicts.append({"action_type": "no_action", "reasoning": f"解析小脑响应失败: {e}"})
if not action_dicts:
action_dicts.append({"action_type": "no_action", "reasoning": "小脑未返回有效action"})
return action_dicts
async def plan(
self,
mode: ChatMode = ChatMode.FOCUS,
loop_start_time: float = 0.0,
available_actions: Optional[Dict[str, ActionInfo]] = None,
) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]:
"""
[注释] "大脑"规划器。
1. 启动多个并行的"小脑"(sub_plan)来决定是否执行具体的actions。
2. 自己(大脑)则专注于决定是否进行聊天回复(reply)。
3. 整合大脑和小脑的决策,返回最终要执行的动作列表。
"""
# --- 1. 准备上下文信息 ---
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.6),
)
# 大脑使用较长的上下文
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=message_list_before_now,
timestamp_mode="normal",
read_mark=self.last_obs_time_mark,
truncate=True,
show_actions=True,
)
# 小脑使用较短、较新的上下文
message_list_before_now_short = message_list_before_now[-int(global_config.chat.max_context_size * 0.3) :]
chat_content_block_short, message_id_list_short = build_readable_messages_with_id(
messages=message_list_before_now_short,
timestamp_mode="normal",
truncate=False,
show_actions=False,
)
self.last_obs_time_mark = time.time()
is_group_chat, chat_target_info, current_available_actions = self.get_necessary_info()
if available_actions is None:
available_actions = current_available_actions
# --- 2. 启动小脑并行思考 ---
all_sub_planner_results: List[Dict[str, Any]] = []
try:
sub_planner_actions: Dict[str, ActionInfo] = {}
for action_name, action_info in available_actions.items():
if action_info.planner_type not in [PlannerType.SMALL_BRAIN, PlannerType.ALL]:
continue
if action_info.activation_type in [ActionActivationType.LLM_JUDGE, ActionActivationType.ALWAYS]:
sub_planner_actions[action_name] = action_info
elif action_info.activation_type == ActionActivationType.RANDOM:
if random.random() < action_info.random_activation_probability:
sub_planner_actions[action_name] = action_info
elif action_info.activation_type == ActionActivationType.KEYWORD:
if any(keyword in chat_content_block_short for keyword in action_info.activation_keywords):
sub_planner_actions[action_name] = action_info
if sub_planner_actions:
sub_planner_actions_num = len(sub_planner_actions)
planner_size_config = global_config.chat.planner_size
sub_planner_size = int(planner_size_config) + (
1 if random.random() < planner_size_config - int(planner_size_config) else 0
)
sub_planner_num = math.ceil(sub_planner_actions_num / sub_planner_size)
logger.info(f"{self.log_prefix}使用{sub_planner_num}个小脑进行思考 (尺寸: {sub_planner_size})")
action_items = list(sub_planner_actions.items())
random.shuffle(action_items)
sub_planner_lists = [action_items[i::sub_planner_num] for i in range(sub_planner_num)]
sub_plan_tasks = [
self.sub_plan(
action_list=action_group,
chat_content_block=chat_content_block_short,
message_id_list=message_id_list_short,
is_group_chat=is_group_chat,
chat_target_info=chat_target_info,
)
for action_group in sub_planner_lists
]
sub_plan_results = await asyncio.gather(*sub_plan_tasks)
for sub_result in sub_plan_results:
all_sub_planner_results.extend(sub_result)
sub_actions_str = ", ".join(
a["action_type"] for a in all_sub_planner_results if a["action_type"] != "no_action"
) or "no_action"
logger.info(f"{self.log_prefix}小脑决策: [{sub_actions_str}]")
except Exception as e:
logger.error(f"{self.log_prefix}小脑调度过程中出错: {e}\n{traceback.format_exc()}")
# --- 3. 大脑独立思考是否回复 ---
action, reasoning, action_data, target_message = "no_reply", "大脑初始化默认", {}, None
try:
big_brain_actions = {
name: info
for name, info in available_actions.items()
if info.planner_type in [PlannerType.BIG_BRAIN, PlannerType.ALL]
}
prompt, _ = await self.build_planner_prompt(
is_group_chat=is_group_chat,
chat_target_info=chat_target_info,
current_available_actions=big_brain_actions,
mode=mode,
chat_content_block_override=chat_content_block,
message_id_list_override=message_id_list,
)
llm_content, _ = await self.planner_llm.generate_response_async(prompt=prompt)
if llm_content:
parsed_json = orjson.loads(repair_json(llm_content))
parsed_json = parsed_json[-1] if isinstance(parsed_json, list) and parsed_json else parsed_json
if isinstance(parsed_json, dict):
action = parsed_json.get("action", "no_reply")
reasoning = parsed_json.get("reason", "未提供原因")
action_data = {k: v for k, v in parsed_json.items() if k not in ["action", "reason"]}
if action != "no_reply":
if target_id := parsed_json.get("target_message_id"):
target_message = self.find_message_by_id(target_id, message_id_list)
if not target_message:
target_message = self.get_latest_message(message_id_list)
logger.info(f"{self.log_prefix}大脑决策: [{action}]")
except Exception as e:
logger.error(f"{self.log_prefix}大脑处理过程中发生意外错误: {e}\n{traceback.format_exc()}")
action, reasoning = "no_reply", f"大脑处理错误: {e}"
# --- 4. 整合大脑和小脑的决策 ---
is_parallel = True
for info in all_sub_planner_results:
action_type = info.get("action_type")
if action_type and action_type not in ["no_action", "no_reply"]:
action_info = available_actions.get(action_type)
if action_info and not action_info.parallel_action:
is_parallel = False
break
action_data["loop_start_time"] = loop_start_time
final_actions: List[Dict[str, Any]] = []
if is_parallel:
logger.info(f"{self.log_prefix}决策模式: 大脑与小脑并行")
if action not in ["no_action", "no_reply"]:
final_actions.append(
{
"action_type": action,
"reasoning": reasoning,
"action_data": action_data,
"action_message": target_message,
"available_actions": available_actions,
}
)
final_actions.extend(all_sub_planner_results)
else:
logger.info(f"{self.log_prefix}决策模式: 小脑优先 (检测到非并行action)")
final_actions.extend(all_sub_planner_results)
final_actions = self._filter_no_actions(final_actions)
if not final_actions:
final_actions = [
{
"action_type": "no_action",
"reasoning": "所有规划器都选择不执行动作",
"action_data": {}, "action_message": None, "available_actions": available_actions
}
]
final_target_message = target_message
if not final_target_message and final_actions:
final_target_message = next((act.get("action_message") for act in final_actions if act.get("action_message")), None)
actions_str = ", ".join([a.get('action_type', 'N/A') for a in final_actions])
logger.info(f"{self.log_prefix}最终执行动作 ({len(final_actions)}): [{actions_str}]")
return final_actions, final_target_message
async def build_planner_prompt(
self,
is_group_chat: bool,
chat_target_info: Optional[dict],
current_available_actions: Dict[str, ActionInfo],
mode: ChatMode = ChatMode.FOCUS,
chat_content_block_override: Optional[str] = None,
message_id_list_override: Optional[List] = None,
refresh_time: bool = False, # 添加缺失的参数
) -> tuple[str, list]:
"""构建 Planner LLM 的提示词 (获取模板并填充数据)"""
try:
# --- 通用信息获取 ---
time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
bot_name = global_config.bot.nickname
bot_nickname = (
f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else ""
)
bot_core_personality = global_config.personality.personality_core
identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}"
schedule_block = ""
if global_config.planning_system.schedule_enable:
if current_activity := schedule_manager.get_current_activity():
schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。"
mood_block = ""
if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(self.chat_id)
mood_block = f"你现在的心情是:{chat_mood.mood_state}"
# --- 根据模式构建不同的Prompt ---
if mode == ChatMode.PROACTIVE:
long_term_memory_block = await self._get_long_term_memory_context()
# 获取最近的聊天记录用于主动思考决策
message_list_short = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.2), # 主动思考时只看少量最近消息
)
chat_content_block, _ = build_readable_messages_with_id(
messages=message_list_short,
timestamp_mode="normal",
truncate=False,
show_actions=False,
)
prompt_template = await global_prompt_manager.get_prompt_async("proactive_planner_prompt")
prompt = prompt_template.format(
time_block=time_block,
identity_block=identity_block,
schedule_block=schedule_block,
mood_block=mood_block,
long_term_memory_block=long_term_memory_block,
chat_content_block=chat_content_block or "最近没有聊天内容。",
)
return prompt, []
# --- FOCUS 和 NORMAL 模式的逻辑 ---
message_list_before_now = get_raw_msg_before_timestamp_with_chat(
chat_id=self.chat_id,
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.6),
)
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=message_list_before_now,
timestamp_mode="normal",
read_mark=self.last_obs_time_mark,
truncate=True,
show_actions=True,
)
actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=self.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
if refresh_time:
self.last_obs_time_mark = time.time()
mentioned_bonus = ""
if global_config.chat.mentioned_bot_inevitable_reply:
mentioned_bonus = "\n- 有人提到你"
if global_config.chat.at_bot_inevitable_reply:
mentioned_bonus = "\n- 有人提到你或者at你"
if mode == ChatMode.FOCUS:
no_action_block = """
- 'no_reply' 表示不进行回复,等待合适的回复时机
- 当你刚刚发送了消息没有人回复时选择no_reply
- 当你一次发送了太多消息为了避免打扰聊天节奏选择no_reply
动作no_reply
动作描述:不进行回复,等待合适的回复时机
- 当你刚刚发送了消息没有人回复时选择no_reply
- 当你一次发送了太多消息为了避免打扰聊天节奏选择no_reply
{{
"action": "no_reply",
"reason":"不回复的原因"
}}
"""
else: # NORMAL Mode
no_action_block = """重要说明:
- 'reply' 表示只进行普通聊天回复,不执行任何额外动作
- 其他action表示在普通回复的基础上执行相应的额外动作
{{
"action": "reply",
"target_message_id":"触发action的消息id",
"reason":"回复的原因"
}}"""
chat_context_description = "你现在正在一个群聊中"
chat_target_name = None
if not is_group_chat and chat_target_info:
chat_target_name = (
chat_target_info.get("person_name") or chat_target_info.get("user_nickname") or "对方"
)
chat_context_description = f"你正在和 {chat_target_name} 私聊"
action_options_block = await self._build_action_options(current_available_actions, mode)
moderation_prompt_block = "请不要输出违法违规内容,不要输出色情,暴力,政治相关内容,如有敏感内容,请规避。"
custom_prompt_block = ""
if global_config.custom_prompt.planner_custom_prompt_content:
custom_prompt_block = global_config.custom_prompt.planner_custom_prompt_content
planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt")
prompt = planner_prompt_template.format(
schedule_block=schedule_block,
mood_block=mood_block,
time_block=time_block,
chat_context_description=chat_context_description,
chat_content_block=chat_content_block,
actions_before_now_block=actions_before_now_block,
mentioned_bonus=mentioned_bonus,
no_action_block=no_action_block,
action_options_text=action_options_block,
moderation_prompt=moderation_prompt_block,
identity_block=identity_block,
custom_prompt_block=custom_prompt_block,
bot_name=bot_name,
)
return prompt, message_id_list
except Exception as e:
logger.error(f"构建 Planner 提示词时出错: {e}")
logger.error(traceback.format_exc())
return "构建 Planner Prompt 时出错", []
def get_necessary_info(self) -> Tuple[bool, Optional[dict], Dict[str, ActionInfo]]:
"""
获取 Planner 需要的必要信息
"""
is_group_chat = True
is_group_chat, chat_target_info = get_chat_type_and_target_info(self.chat_id)
logger.debug(f"{self.log_prefix}获取到聊天信息 - 群聊: {is_group_chat}, 目标信息: {chat_target_info}")
current_available_actions_dict = self.action_manager.get_using_actions()
# 获取完整的动作信息
all_registered_actions: Dict[str, ActionInfo] = component_registry.get_components_by_type( # type: ignore
ComponentType.ACTION
)
current_available_actions = {}
for action_name in current_available_actions_dict:
if action_name in all_registered_actions:
current_available_actions[action_name] = all_registered_actions[action_name]
else:
logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到")
# 将no_reply作为系统级特殊动作添加到可用动作中
# no_reply虽然是系统级决策但需要让规划器认为它是可用的
no_reply_info = ActionInfo(
name="no_reply",
component_type=ComponentType.ACTION,
description="系统级动作:选择不回复消息的决策",
action_parameters={},
activation_keywords=[],
plugin_name="SYSTEM",
enabled=True, # 始终启用
parallel_action=False,
)
current_available_actions["no_reply"] = no_reply_info
return is_group_chat, chat_target_info, current_available_actions
init_prompt()