pull/1001/head
SnowindMe 2025-04-29 16:48:32 +08:00
commit b72d49b99e
42 changed files with 2043 additions and 1862 deletions

View File

@ -19,7 +19,7 @@ if %ERRORLEVEL% neq 0 (
) )
REM 运行预处理脚本 REM 运行预处理脚本
python "%~dp0raw_data_preprocessor.py" python "%~dp0scripts\raw_data_preprocessor.py"
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo 错误: raw_data_preprocessor.py 执行失败 echo 错误: raw_data_preprocessor.py 执行失败
pause pause
@ -27,7 +27,7 @@ if %ERRORLEVEL% neq 0 (
) )
REM 运行信息提取脚本 REM 运行信息提取脚本
python "%~dp0info_extraction.py" python "%~dp0scripts\info_extraction.py"
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo 错误: info_extraction.py 执行失败 echo 错误: info_extraction.py 执行失败
pause pause
@ -35,7 +35,7 @@ if %ERRORLEVEL% neq 0 (
) )
REM 运行OpenIE导入脚本 REM 运行OpenIE导入脚本
python "%~dp0import_openie.py" python "%~dp0scripts\import_openie.py"
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo 错误: import_openie.py 执行失败 echo 错误: import_openie.py 执行失败
pause pause

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ mongodb/
NapCat.Framework.Windows.Once/ NapCat.Framework.Windows.Once/
log/ log/
logs/ logs/
tool_call_benchmark.py
run_ad.bat run_ad.bat
MaiBot-Napcat-Adapter-main MaiBot-Napcat-Adapter-main
MaiBot-Napcat-Adapter MaiBot-Napcat-Adapter

View File

@ -156,7 +156,7 @@ graph TD
## ✍如何给本项目报告BUG/提交建议/做贡献 ## ✍如何给本项目报告BUG/提交建议/做贡献
MaiCore是一个开源项目我们非常欢迎你的参与。你的贡献无论是提交bug报告、功能需求还是代码pr都对项目非常宝贵。我们非常感谢你的支持🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](CONTRIBUTE.md)(待补完) MaiCore是一个开源项目我们非常欢迎你的参与。你的贡献无论是提交bug报告、功能需求还是代码pr都对项目非常宝贵。我们非常感谢你的支持🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](depends-data/CONTRIBUTE.md)(待补完)

View File

@ -0,0 +1,145 @@
{
"测试时间": "2025-04-28 14:12:36",
"测试迭代次数": 10,
"不使用工具调用": {
"平均耗时": 4.596814393997192,
"最短耗时": 2.957131862640381,
"最长耗时": 10.121938705444336,
"标准差": 2.1705468730949593,
"所有耗时": [
3.18,
4.65,
10.12,
3.5,
4.46,
4.24,
3.23,
6.2,
2.96,
3.42
]
},
"不使用工具调用_详细响应": [
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?\",\n \"emoji_query\": \"友好地询问\"\n}\n```",
"推理内容摘要": ""
},
{
"内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次叫了我的名字,显然是想引起我的注意或有事要说,作为礼貌应当回应\",\n emoji_query=\"友善的回应\"\n)",
"推理内容摘要": ""
},
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?我在呢~\",\n \"emoji_query\": \"友好的询问\",\n \"reasoning\": \"由于对方连续两次提到我的名字,显然是想与我交流,应当及时给予友好回应避免冷场\"\n}\n```",
"推理内容摘要": ""
},
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"在呢在呢~怎么啦?\",\n \"emoji_query\": \"好奇的回应\"\n}\n```",
"推理内容摘要": ""
},
{
"内容摘要": "decide_reply_action(\n action=\"text_reply\",\n reasoning=\"千石连续两次提到我的名字,显然需要我回应。文字回复更正式且能明确表示我在关注他的信息。\",\n emoji_query=\"友好的回应\"\n)",
"推理内容摘要": ""
},
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"reasoning\": \"千石连续两次提到我的名字,显然是需要我的回应。作为日常交流,应该给予友善简短的答复。\",\n \"emoji_query\": \"疑惑的歪头\"\n}\n```",
"推理内容摘要": ""
},
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"咋啦千石~\",\n \"emoji_query\": \"好奇的询问\"\n}\n```",
"推理内容摘要": ""
},
{
"内容摘要": "decide_reply_action\n```json\n{\n \"action\": \"text_reply\",\n \"content\": \"我在呢~怎么啦?\",\n \"emoji_query\": \"友好的关心\",\n \"reasoning\": \"千石连续两次呼唤我的名字显然是有事情要找我或想引起我的注意。根据回复原则2有人提到你但未回应应该用友善的文字进行回应并附上表达关心的表情符号来延...",
"推理内容摘要": ""
},
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"千石怎么啦~\",\n \"emoji_query\": \"好奇的探询\"\n}\n```",
"推理内容摘要": ""
},
{
"内容摘要": "```json\n{\n \"action\": \"text_reply\",\n \"content\": \"怎么啦?突然叫我两次\",\n \"emoji_query\": \"好奇的疑问\"\n}\n```",
"推理内容摘要": ""
}
],
"使用工具调用": {
"平均耗时": 8.139546775817871,
"最短耗时": 4.9980738162994385,
"最长耗时": 18.803313732147217,
"标准差": 4.008772720760647,
"所有耗时": [
5.81,
18.8,
6.06,
8.06,
10.07,
6.34,
7.9,
6.66,
5.0,
6.69
]
},
"使用工具调用_详细响应": [
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
},
{
"内容摘要": "",
"推理内容摘要": "",
"工具调用数量": 0,
"工具调用详情": []
}
],
"差异百分比": 77.07
}

View File

@ -4,7 +4,10 @@
# print("未找到quick_algo库无法使用quick_algo算法") # print("未找到quick_algo库无法使用quick_algo算法")
# print("请安装quick_algo库 - 在lib.quick_algo中执行命令python setup.py build_ext --inplace") # print("请安装quick_algo库 - 在lib.quick_algo中执行命令python setup.py build_ext --inplace")
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from typing import Dict, List from typing import Dict, List
from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config from src.plugins.knowledge.src.lpmmconfig import PG_NAMESPACE, global_config
@ -15,8 +18,9 @@ from src.plugins.knowledge.src.kg_manager import KGManager
from src.common.logger import get_module_logger from src.common.logger import get_module_logger
from src.plugins.knowledge.src.utils.hash import get_sha256 from src.plugins.knowledge.src.utils.hash import get_sha256
# 添加在现有导入之后
import sys # 添加项目根目录到 sys.path
logger = get_module_logger("LPMM知识库-OpenIE导入") logger = get_module_logger("LPMM知识库-OpenIE导入")

View File

@ -5,6 +5,9 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock, Event from threading import Lock, Event
import sys import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
# 添加项目根目录到 sys.path
import tqdm import tqdm
from src.common.logger import get_module_logger from src.common.logger import get_module_logger

View File

@ -2,10 +2,14 @@ import json
import os import os
from pathlib import Path from pathlib import Path
import sys # 新增系统模块导入 import sys # 新增系统模块导入
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from src.common.logger import get_module_logger from src.common.logger import get_module_logger
logger = get_module_logger("LPMM数据库-原始数据处理") logger = get_module_logger("LPMM数据库-原始数据处理")
# 添加项目根目录到 sys.path
def check_and_create_dirs(): def check_and_create_dirs():
"""检查并创建必要的目录""" """检查并创建必要的目录"""

View File

@ -235,6 +235,10 @@ class BotConfig:
memory_forget_time: int = 24 # 记忆遗忘时间(小时) memory_forget_time: int = 24 # 记忆遗忘时间(小时)
memory_forget_percentage: float = 0.01 # 记忆遗忘比例 memory_forget_percentage: float = 0.01 # 记忆遗忘比例
consolidate_memory_interval: int = 1000 # 记忆整合间隔(秒)
consolidation_similarity_threshold: float = 0.7 # 相似度阈值
consolidate_memory_percentage: float = 0.01 # 检查节点比例
memory_ban_words: list = field( memory_ban_words: list = field(
default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"] default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"]
) # 添加新的配置项默认值 ) # 添加新的配置项默认值
@ -594,6 +598,16 @@ class BotConfig:
config.build_memory_sample_length = memory_config.get( config.build_memory_sample_length = memory_config.get(
"build_memory_sample_length", config.build_memory_sample_length "build_memory_sample_length", config.build_memory_sample_length
) )
if config.INNER_VERSION in SpecifierSet(">=1.5.1"):
config.consolidate_memory_interval = memory_config.get(
"consolidate_memory_interval", config.consolidate_memory_interval
)
config.consolidation_similarity_threshold = memory_config.get(
"consolidation_similarity_threshold", config.consolidation_similarity_threshold
)
config.consolidate_memory_percentage = memory_config.get(
"consolidate_memory_percentage", config.consolidate_memory_percentage
)
def remote(parent: dict): def remote(parent: dict):
remote_config = parent["remote"] remote_config = parent["remote"]

View File

@ -8,8 +8,8 @@ from src.plugins.moods.moods import MoodManager
logger = get_logger("mai_state") logger = get_logger("mai_state")
# enable_unlimited_hfc_chat = True enable_unlimited_hfc_chat = True
enable_unlimited_hfc_chat = False # enable_unlimited_hfc_chat = False
class MaiState(enum.Enum): class MaiState(enum.Enum):

View File

@ -8,7 +8,7 @@ from src.individuality.individuality import Individuality
import random import random
from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.do_tool.tool_use import ToolUser from src.do_tool.tool_use import ToolUser
from src.plugins.utils.json_utils import safe_json_dumps, normalize_llm_response, process_llm_tool_calls from src.plugins.utils.json_utils import safe_json_dumps, process_llm_tool_calls
from src.heart_flow.chat_state_info import ChatStateInfo from src.heart_flow.chat_state_info import ChatStateInfo
from src.plugins.chat.chat_stream import chat_manager from src.plugins.chat.chat_stream import chat_manager
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
@ -20,14 +20,12 @@ logger = get_logger("sub_heartflow")
def init_prompt(): def init_prompt():
prompt = "" prompt = ""
prompt += "{extra_info}\n" prompt += "{extra_info}\n"
prompt += "{prompt_personality}\n" prompt += "你的名字是{bot_name},{prompt_personality}\n"
prompt += "{last_loop_prompt}\n" prompt += "{last_loop_prompt}\n"
prompt += "{cycle_info_block}\n" prompt += "{cycle_info_block}\n"
prompt += "现在是{time_now}你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容\n{chat_observe_info}\n" prompt += "现在是{time_now}你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容\n{chat_observe_info}\n"
prompt += "\n你现在{mood_info}\n" prompt += "\n你现在{mood_info}\n"
prompt += ( prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。"
"请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。"
)
prompt += "思考并输出你的内心想法\n" prompt += "思考并输出你的内心想法\n"
prompt += "输出要求:\n" prompt += "输出要求:\n"
prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n" prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n"
@ -67,6 +65,9 @@ class SubMind:
self.past_mind = [] self.past_mind = []
self.structured_info = {} self.structured_info = {}
name = chat_manager.get_stream_name(self.subheartflow_id)
self.log_prefix = f"[{name}] "
async def do_thinking_before_reply(self, history_cycle: list[CycleInfo] = None): async def do_thinking_before_reply(self, history_cycle: list[CycleInfo] = None):
""" """
在回复前进行思考生成内心想法并收集工具调用结果 在回复前进行思考生成内心想法并收集工具调用结果
@ -85,7 +86,7 @@ class SubMind:
# 获取观察对象 # 获取观察对象
observation = self.observations[0] observation = self.observations[0]
if not observation: if not observation:
logger.error(f"[{self.subheartflow_id}] 无法获取观察对象") logger.error(f"{self.log_prefix} 无法获取观察对象")
self.update_current_mind("(我没看到任何聊天内容...)") self.update_current_mind("(我没看到任何聊天内容...)")
return self.current_mind, self.past_mind return self.current_mind, self.past_mind
@ -101,18 +102,7 @@ class SubMind:
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
# 构建个性部分 # 构建个性部分
prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你" prompt_personality = individuality.get_prompt(x_person=2, level=2)
prompt_personality += individuality.personality.personality_core
# 随机添加个性侧面
if individuality.personality.personality_sides:
random_side = random.choice(individuality.personality.personality_sides)
prompt_personality += f"{random_side}"
# 随机添加身份细节
if individuality.identity.identity_detail:
random_detail = random.choice(individuality.identity.identity_detail)
prompt_personality += f"{random_detail}"
# 获取当前时间 # 获取当前时间
time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
@ -206,7 +196,7 @@ class SubMind:
prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format( prompt = (await global_prompt_manager.get_prompt_async("sub_heartflow_prompt_before")).format(
extra_info="", # 可以在这里添加额外信息 extra_info="", # 可以在这里添加额外信息
prompt_personality=prompt_personality, prompt_personality=prompt_personality,
bot_name=individuality.personality.bot_nickname, bot_name=individuality.name,
time_now=time_now, time_now=time_now,
chat_observe_info=chat_observe_info, chat_observe_info=chat_observe_info,
mood_info=mood_info, mood_info=mood_info,
@ -223,57 +213,48 @@ class SubMind:
try: try:
# 调用LLM生成响应 # 调用LLM生成响应
response = await self.llm_model.generate_response_tool_async(prompt=prompt, tools=tools) response, _reasoning_content, tool_calls = await self.llm_model.generate_response_tool_async(
prompt=prompt, tools=tools
# 标准化响应格式
success, normalized_response, error_msg = normalize_llm_response(
response, log_prefix=f"[{self.subheartflow_id}] "
) )
if not success: logger.debug(f"{self.log_prefix} 子心流输出的原始LLM响应: {response}")
# 处理标准化失败情况
logger.warning(f"[{self.subheartflow_id}] {error_msg}")
content = "LLM响应格式无法处理"
else:
# 从标准化响应中提取内容
if len(normalized_response) >= 2:
content = normalized_response[0]
_reasoning_content = normalized_response[1] if len(normalized_response) > 1 else ""
# 处理可能的工具调用 # 直接使用LLM返回的文本响应作为 content
if len(normalized_response) == 3: content = response if response else ""
# 提取并验证工具调用
success, valid_tool_calls, error_msg = process_llm_tool_calls( if tool_calls:
normalized_response, log_prefix=f"[{self.subheartflow_id}] " # 直接将 tool_calls 传递给处理函数
success, valid_tool_calls, error_msg = process_llm_tool_calls(
tool_calls, log_prefix=f"{self.log_prefix} "
)
if success and valid_tool_calls:
# 记录工具调用信息
tool_calls_str = ", ".join(
[call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls]
) )
logger.info(f"{self.log_prefix} 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}")
if success and valid_tool_calls: # 收集工具执行结果
# 记录工具调用信息 await self._execute_tool_calls(valid_tool_calls, tool_instance)
tool_calls_str = ", ".join( elif not success:
[call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls] logger.warning(f"{self.log_prefix} 处理工具调用时出错: {error_msg}")
) else:
logger.info( logger.info(f"{self.log_prefix} 心流未使用工具") # 修改日志信息,明确是未使用工具而不是未处理
f"[{self.subheartflow_id}] 模型请求调用{len(valid_tool_calls)}个工具: {tool_calls_str}"
)
# 收集工具执行结果
await self._execute_tool_calls(valid_tool_calls, tool_instance)
elif not success:
logger.warning(f"[{self.subheartflow_id}] {error_msg}")
except Exception as e: except Exception as e:
# 处理总体异常 # 处理总体异常
logger.error(f"[{self.subheartflow_id}] 执行LLM请求或处理响应时出错: {e}") logger.error(f"{self.log_prefix} 执行LLM请求或处理响应时出错: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
content = "思考过程中出现错误" content = "思考过程中出现错误"
# 记录最终思考结果 # 记录最终思考结果
name = chat_manager.get_stream_name(self.subheartflow_id) logger.debug(f"{self.log_prefix} \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n")
logger.debug(f"[{name}] \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n")
# 处理空响应情况 # 处理空响应情况
if not content: if not content:
content = "(不知道该想些什么...)" content = "(不知道该想些什么...)"
logger.warning(f"[{self.subheartflow_id}] LLM返回空结果思考失败。") logger.warning(f"{self.log_prefix} LLM返回空结果思考失败。")
# ---------- 6. 更新思考状态并返回结果 ---------- # ---------- 6. 更新思考状态并返回结果 ----------
# 更新当前思考内容 # 更新当前思考内容

View File

@ -1,6 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List
import random
@dataclass @dataclass
@ -86,27 +85,6 @@ class Identity:
instance.appearance = appearance instance.appearance = appearance
return instance return instance
def get_prompt(self, x_person, level):
"""
获取身份特征的prompt
"""
if x_person == 2:
prompt_identity = ""
elif x_person == 1:
prompt_identity = ""
else:
prompt_identity = ""
if level == 1:
identity_detail = self.identity_detail
random.shuffle(identity_detail)
prompt_identity += identity_detail[0]
elif level == 2:
for detail in self.identity_detail:
prompt_identity += f",{detail}"
prompt_identity += ""
return prompt_identity
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""将身份特征转换为字典格式""" """将身份特征转换为字典格式"""
return { return {

View File

@ -1,6 +1,7 @@
from typing import Optional from typing import Optional
from .personality import Personality from .personality import Personality
from .identity import Identity from .identity import Identity
import random
class Individuality: class Individuality:
@ -8,15 +9,16 @@ class Individuality:
_instance = None _instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
if Individuality._instance is not None:
raise RuntimeError("Individuality 类是单例,请使用 get_instance() 方法获取实例。")
# 正常初始化实例属性
self.personality: Optional[Personality] = None self.personality: Optional[Personality] = None
self.identity: Optional[Identity] = None self.identity: Optional[Identity] = None
self.name = ""
@classmethod @classmethod
def get_instance(cls) -> "Individuality": def get_instance(cls) -> "Individuality":
"""获取Individuality单例实例 """获取Individuality单例实例
@ -25,7 +27,13 @@ class Individuality:
Individuality: 单例实例 Individuality: 单例实例
""" """
if cls._instance is None: if cls._instance is None:
cls._instance = cls() # 实例不存在,调用 cls() 创建新实例
# cls() 会调用 __init__
# 因为此时 cls._instance 仍然是 None__init__ 会正常执行初始化
new_instance = cls()
# 将新创建的实例赋值给类变量 _instance
cls._instance = new_instance
# 返回(新创建的或已存在的)单例实例
return cls._instance return cls._instance
def initialize( def initialize(
@ -63,6 +71,8 @@ class Individuality:
identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance
) )
self.name = bot_nickname
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""将个体特征转换为字典格式""" """将个体特征转换为字典格式"""
return { return {
@ -80,16 +90,148 @@ class Individuality:
instance.identity = Identity.from_dict(data["identity"]) instance.identity = Identity.from_dict(data["identity"])
return instance return instance
def get_prompt(self, type, x_person, level): def get_personality_prompt(self, level: int, x_person: int = 2) -> str:
""" """
获取个体特征的prompt 获取人格特征的prompt
Args:
level (int): 详细程度 (1: 核心, 2: 核心+随机侧面, 3: 核心+所有侧面)
x_person (int, optional): 人称代词 (0: 无人称, 1: , 2: ). 默认为 2.
Returns:
str: 生成的人格prompt字符串
""" """
if type == "personality": if x_person not in [0, 1, 2]:
return self.personality.get_prompt(x_person, level) return "无效的人称代词,请使用 0 (无人称), 1 (我) 或 2 (你)。"
elif type == "identity": if not self.personality:
return self.identity.get_prompt(x_person, level) return "人格特征尚未初始化。"
if x_person == 2:
p_pronoun = ""
prompt_personality = f"{p_pronoun}{self.personality.personality_core}"
elif x_person == 1:
p_pronoun = ""
prompt_personality = f"{p_pronoun}{self.personality.personality_core}"
else: # x_person == 0
p_pronoun = "" # 无人称
# 对于无人称,直接描述核心特征
prompt_personality = f"{self.personality.personality_core}"
# 根据level添加人格侧面
if level >= 2 and self.personality.personality_sides:
personality_sides = list(self.personality.personality_sides)
random.shuffle(personality_sides)
if level == 2:
prompt_personality += f",有时也会{personality_sides[0]}"
elif level == 3:
sides_str = "".join(personality_sides)
prompt_personality += f",有时也会{sides_str}"
prompt_personality += ""
return prompt_personality
def get_identity_prompt(self, level: int, x_person: int = 2) -> str:
"""
获取身份特征的prompt
Args:
level (int): 详细程度 (1: 随机细节, 2: 所有细节+外貌年龄性别, 3: 同2)
x_person (int, optional): 人称代词 (0: 无人称, 1: , 2: ). 默认为 2.
Returns:
str: 生成的身份prompt字符串
"""
if x_person not in [0, 1, 2]:
return "无效的人称代词,请使用 0 (无人称), 1 (我) 或 2 (你)。"
if not self.identity:
return "身份特征尚未初始化。"
if x_person == 2:
i_pronoun = ""
elif x_person == 1:
i_pronoun = ""
else: # x_person == 0
i_pronoun = "" # 无人称
identity_parts = []
# 根据level添加身份细节
if level >= 1 and self.identity.identity_detail:
identity_detail = list(self.identity.identity_detail)
random.shuffle(identity_detail)
if level == 1:
identity_parts.append(f"身份是{identity_detail[0]}")
elif level >= 2:
details_str = "".join(identity_detail)
identity_parts.append(f"身份是{details_str}")
# 根据level添加其他身份信息
if level >= 3:
if self.identity.appearance:
identity_parts.append(f"{self.identity.appearance}")
if self.identity.age > 0:
identity_parts.append(f"年龄大约{self.identity.age}")
if self.identity.gender:
identity_parts.append(f"性别是{self.identity.gender}")
if identity_parts:
details_str = "".join(identity_parts)
if x_person in [1, 2]:
return f"{i_pronoun}{details_str}"
else: # x_person == 0
# 无人称时,直接返回细节,不加代词和开头的逗号
return f"{details_str}"
else: else:
return "" if x_person in [1, 2]:
return f"{i_pronoun}的身份信息不完整。"
else: # x_person == 0
return "身份信息不完整。"
def get_prompt(self, level: int, x_person: int = 2) -> str:
"""
获取合并的个体特征prompt
Args:
level (int): 详细程度 (1: 核心/随机细节, 2: 核心+侧面/细节+其他, 3: 全部)
x_person (int, optional): 人称代词 (0: 无人称, 1: , 2: ). 默认为 2.
Returns:
str: 生成的合并prompt字符串
"""
if x_person not in [0, 1, 2]:
return "无效的人称代词,请使用 0 (无人称), 1 (我) 或 2 (你)。"
if not self.personality or not self.identity:
return "个体特征尚未完全初始化。"
# 调用新的独立方法
prompt_personality = self.get_personality_prompt(level, x_person)
prompt_identity = self.get_identity_prompt(level, x_person)
# 移除可能存在的错误信息,只合并有效的 prompt
valid_prompts = []
if "尚未初始化" not in prompt_personality and "无效的人称" not in prompt_personality:
valid_prompts.append(prompt_personality)
if (
"尚未初始化" not in prompt_identity
and "无效的人称" not in prompt_identity
and "信息不完整" not in prompt_identity
):
# 从身份 prompt 中移除代词和句号,以便更好地合并
identity_content = prompt_identity
if x_person == 2 and identity_content.startswith("你,"):
identity_content = identity_content[2:]
elif x_person == 1 and identity_content.startswith("我,"):
identity_content = identity_content[2:]
# 对于 x_person == 0身份提示不带前缀无需移除
if identity_content.endswith(""):
identity_content = identity_content[:-1]
valid_prompts.append(identity_content)
# --- 合并 Prompt ---
final_prompt = " ".join(valid_prompts)
return final_prompt.strip()
def get_traits(self, factor): def get_traits(self, factor):
""" """

View File

@ -2,7 +2,6 @@ from dataclasses import dataclass
from typing import Dict, List from typing import Dict, List
import json import json
from pathlib import Path from pathlib import Path
import random
@dataclass @dataclass
@ -119,28 +118,3 @@ class Personality:
for key, value in data.items(): for key, value in data.items():
setattr(instance, key, value) setattr(instance, key, value)
return instance return instance
def get_prompt(self, x_person, level):
# 开始构建prompt
if x_person == 2:
prompt_personality = ""
elif x_person == 1:
prompt_personality = ""
else:
prompt_personality = ""
# person
prompt_personality += self.personality_core
if level == 2:
personality_sides = self.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
elif level == 3:
personality_sides = self.personality_sides
for side in personality_sides:
prompt_personality += f",{side}"
prompt_personality += ""
return prompt_personality

View File

@ -83,7 +83,7 @@ class MainSystem:
) )
asyncio.create_task(bot_schedule.mai_schedule_start()) asyncio.create_task(bot_schedule.mai_schedule_start())
# 启动FastAPI服务器 # 将bot.py中的chat_bot.message_process消息处理函数注册到api.py的消息处理基类中
self.app.register_message_handler(chat_bot.message_process) self.app.register_message_handler(chat_bot.message_process)
# 初始化个体特征 # 初始化个体特征
@ -121,6 +121,7 @@ class MainSystem:
tasks = [ tasks = [
self.build_memory_task(), self.build_memory_task(),
self.forget_memory_task(), self.forget_memory_task(),
self.consolidate_memory_task(),
self.print_mood_task(), self.print_mood_task(),
self.remove_recalled_message_task(), self.remove_recalled_message_task(),
emoji_manager.start_periodic_check_register(), emoji_manager.start_periodic_check_register(),
@ -146,6 +147,15 @@ class MainSystem:
await HippocampusManager.get_instance().forget_memory(percentage=global_config.memory_forget_percentage) await HippocampusManager.get_instance().forget_memory(percentage=global_config.memory_forget_percentage)
print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成") print("\033[1;32m[记忆遗忘]\033[0m 记忆遗忘完成")
@staticmethod
async def consolidate_memory_task():
"""记忆整合任务"""
while True:
await asyncio.sleep(global_config.consolidate_memory_interval)
print("\033[1;32m[记忆整合]\033[0m 开始整合记忆...")
await HippocampusManager.get_instance().consolidate_memory()
print("\033[1;32m[记忆整合]\033[0m 记忆整合完成")
async def print_mood_task(self): async def print_mood_task(self):
"""打印情绪状态""" """打印情绪状态"""
while True: while True:

View File

@ -1,5 +1,5 @@
import time import time
from typing import Tuple from typing import Tuple, Optional # 增加了 Optional
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from ..models.utils_model import LLMRequest from ..models.utils_model import LLMRequest
from ...config.config import global_config from ...config.config import global_config
@ -14,43 +14,110 @@ from src.plugins.utils.chat_message_builder import build_readable_messages
logger = get_logger("pfc_action_planner") logger = get_logger("pfc_action_planner")
# 注意:这个 ActionPlannerInfo 类似乎没有在 ActionPlanner 中使用, # --- 定义 Prompt 模板 ---
# 如果确实没用,可以考虑移除,但暂时保留以防万一。
class ActionPlannerInfo: # Prompt(1): 首次回复或非连续回复时的决策 Prompt
def __init__(self): PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊请根据以下【所有信息】审慎且灵活的决策下一步行动可以回复可以倾听可以调取知识甚至可以屏蔽对方
self.done_action = []
self.goal_list = [] 当前对话目标
self.knowledge_list = [] {goals_str}
self.memory_list = []
最近行动历史概要
{action_history_summary}
上一次行动的详细情况和结果
{last_action_context}
时间和超时提示
{time_since_last_bot_message_info}{timeout_context}
最近的对话记录(包括你已成功发送的消息 新收到的消息)
{chat_history_text}
------
可选行动类型以及解释
fetch_knowledge: 需要调取知识当需要专业知识或特定信息时选择对方若提到你不太认识的人名或实体也可以尝试选择
listening: 倾听对方发言当你认为对方话才说到一半发言明显未结束时选择
direct_reply: 直接回复对方
rethink_goal: 思考一个对话目标当你觉得目前对话需要目标或当前目标不再适用或话题卡住时选择注意私聊的环境是灵活的有可能需要经常选择
end_conversation: 结束对话对方长时间没回复或者当你觉得对话告一段落时可以选择
block_and_ignore: 更加极端的结束对话方式直接结束对话并在一段时间内无视对方所有发言屏蔽当对话让你感到十分不适或你遭到各类骚扰时选择
请以JSON格式输出你的决策
{{
"action": "选择的行动类型 (必须是上面列表中的一个)",
"reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的)"
}}
注意请严格按照JSON格式输出不要包含任何其他内容"""
# Prompt(2): 上一次成功回复后,决定继续发言时的决策 Prompt
PROMPT_FOLLOW_UP = """{persona_text}。现在你在参与一场QQ私聊刚刚你已经回复了对方请根据以下【所有信息】审慎且灵活的决策下一步行动可以继续发送新消息可以等待可以倾听可以调取知识甚至可以屏蔽对方
当前对话目标
{goals_str}
最近行动历史概要
{action_history_summary}
上一次行动的详细情况和结果
{last_action_context}
时间和超时提示
{time_since_last_bot_message_info}{timeout_context}
最近的对话记录(包括你已成功发送的消息 新收到的消息)
{chat_history_text}
------
可选行动类型以及解释
fetch_knowledge: 需要调取知识当需要专业知识或特定信息时选择对方若提到你不太认识的人名或实体也可以尝试选择
wait: 暂时不说话留给对方交互空间等待对方回复尤其是在你刚发言后或上次发言因重复发言过多被拒时或不确定做什么时这是不错的选择
listening: 倾听对方发言虽然你刚发过言但如果对方立刻回复且明显话没说完可以选择这个
send_new_message: 发送一条新消息继续对话允许适当的追问补充深入话题或开启相关新话题**但是避免在因重复被拒后立即使用也不要在对方没有回复的情况下过多的消息轰炸或重复发言**
rethink_goal: 思考一个对话目标当你觉得目前对话需要目标或当前目标不再适用或话题卡住时选择注意私聊的环境是灵活的有可能需要经常选择
end_conversation: 结束对话对方长时间没回复或者当你觉得对话告一段落时可以选择
block_and_ignore: 更加极端的结束对话方式直接结束对话并在一段时间内无视对方所有发言屏蔽当对话让你感到十分不适或你遭到各类骚扰时选择
请以JSON格式输出你的决策
{{
"action": "选择的行动类型 (必须是上面列表中的一个)",
"reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的。请说明你为什么选择继续发言而不是等待,以及打算发送什么类型的新消息连续发言,必须记录已经发言了几次)"
}}
注意请严格按照JSON格式输出不要包含任何其他内容"""
# ActionPlanner 类定义,顶格 # ActionPlanner 类定义,顶格
class ActionPlanner: class ActionPlanner:
"""行动规划器""" """行动规划器"""
def __init__(self, stream_id: str): def __init__(self, stream_id: str, private_name: str):
self.llm = LLMRequest( self.llm = LLMRequest(
model=global_config.llm_PFC_action_planner, model=global_config.llm_PFC_action_planner,
temperature=global_config.llm_PFC_action_planner["temp"], temperature=global_config.llm_PFC_action_planner["temp"],
max_tokens=1500, max_tokens=1500,
request_type="action_planning", request_type="action_planning",
) )
self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3) self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id) self.private_name = private_name
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
# self.action_planner_info = ActionPlannerInfo() # 移除未使用的变量
async def plan(self, observation_info: ObservationInfo, conversation_info: ConversationInfo) -> Tuple[str, str]: # 修改 plan 方法签名,增加 last_successful_reply_action 参数
async def plan(
self,
observation_info: ObservationInfo,
conversation_info: ConversationInfo,
last_successful_reply_action: Optional[str],
) -> Tuple[str, str]:
"""规划下一步行动 """规划下一步行动
Args: Args:
observation_info: 决策信息 observation_info: 决策信息
conversation_info: 对话信息 conversation_info: 对话信息
last_successful_reply_action: 上一次成功的回复动作类型 ('direct_reply' 'send_new_message' None)
Returns: Returns:
Tuple[str, str]: (行动类型, 行动原因) Tuple[str, str]: (行动类型, 行动原因)
""" """
# --- 获取 Bot 上次发言时间信息 --- # --- 获取 Bot 上次发言时间信息 ---
# (这部分逻辑不变)
time_since_last_bot_message_info = "" time_since_last_bot_message_info = ""
try: try:
bot_id = str(global_config.BOT_QQ) bot_id = str(global_config.BOT_QQ)
@ -70,59 +137,73 @@ class ActionPlanner:
) )
break break
else: else:
logger.debug("Observation info chat history is empty or not available for bot time check.") logger.debug(
f"[私聊][{self.private_name}]Observation info chat history is empty or not available for bot time check."
)
except AttributeError: except AttributeError:
logger.warning("ObservationInfo object might not have chat_history attribute yet for bot time check.") logger.warning(
f"[私聊][{self.private_name}]ObservationInfo object might not have chat_history attribute yet for bot time check."
)
except Exception as e: except Exception as e:
logger.warning(f"获取 Bot 上次发言时间时出错: {e}") logger.warning(f"[私聊][{self.private_name}]获取 Bot 上次发言时间时出错: {e}")
# --- 获取 Bot 上次发言时间信息结束 ---
# --- 获取超时提示信息 ---
# (这部分逻辑不变)
timeout_context = "" timeout_context = ""
try: # 添加 try-except 以增加健壮性 try:
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list: if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
last_goal_tuple = conversation_info.goal_list[-1] last_goal_dict = conversation_info.goal_list[-1]
if isinstance(last_goal_tuple, tuple) and len(last_goal_tuple) > 0: if isinstance(last_goal_dict, dict) and "goal" in last_goal_dict:
last_goal_text = last_goal_tuple[0] last_goal_text = last_goal_dict["goal"]
if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text: if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text:
try: try:
timeout_minutes_text = last_goal_text.split("")[0].replace("你等待了", "") timeout_minutes_text = last_goal_text.split("")[0].replace("你等待了", "")
timeout_context = f"重要提示:你刚刚因为对方长时间({timeout_minutes_text})没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言\n" timeout_context = f"重要提示:对方已经长时间({timeout_minutes_text})没有回复你的消息了(这可能代表对方繁忙/不想回复/没注意到你的消息等情况,或在对方看来本次聊天已告一段落),请基于此情况规划下一步\n"
except Exception: except Exception:
timeout_context = "重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言\n" timeout_context = "重要提示:对方已经长时间没有回复你的消息了(这可能代表对方繁忙/不想回复/没注意到你的消息等情况,或在对方看来本次聊天已告一段落),请基于此情况规划下一步\n"
else: else:
logger.debug("Conversation info goal_list is empty or not available for timeout check.") logger.debug(
f"[私聊][{self.private_name}]Conversation info goal_list is empty or not available for timeout check."
)
except AttributeError: except AttributeError:
logger.warning("ConversationInfo object might not have goal_list attribute yet for timeout check.") logger.warning(
f"[私聊][{self.private_name}]ConversationInfo object might not have goal_list attribute yet for timeout check."
)
except Exception as e: except Exception as e:
logger.warning(f"检查超时目标时出错: {e}") logger.warning(f"[私聊][{self.private_name}]检查超时目标时出错: {e}")
# 构建提示词 # --- 构建通用 Prompt 参数 ---
logger.debug(f"开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}") # 使用 getattr logger.debug(
f"[私聊][{self.private_name}]开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}"
)
# 构建对话目标 (goals_str) # 构建对话目标 (goals_str)
goals_str = "" goals_str = ""
try: # 添加 try-except try:
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list: if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
for goal_reason in conversation_info.goal_list: for goal_reason in conversation_info.goal_list:
if isinstance(goal_reason, tuple) and len(goal_reason) > 0: if isinstance(goal_reason, dict):
goal = goal_reason[0]
reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因"
elif isinstance(goal_reason, dict):
goal = goal_reason.get("goal", "目标内容缺失") goal = goal_reason.get("goal", "目标内容缺失")
reasoning = goal_reason.get("reasoning", "没有明确原因") reasoning = goal_reason.get("reasoning", "没有明确原因")
else: else:
goal = str(goal_reason) goal = str(goal_reason)
reasoning = "没有明确原因" reasoning = "没有明确原因"
goal = str(goal) if goal is not None else "目标内容缺失" goal = str(goal) if goal is not None else "目标内容缺失"
reasoning = str(reasoning) if reasoning is not None else "没有明确原因" reasoning = str(reasoning) if reasoning is not None else "没有明确原因"
goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n" goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n"
if not goals_str: # 如果循环后 goals_str 仍为空
if not goals_str:
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
else:
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n" goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
except AttributeError: except AttributeError:
logger.warning("ConversationInfo object might not have goal_list attribute yet.") logger.warning(
f"[私聊][{self.private_name}]ConversationInfo object might not have goal_list attribute yet."
)
goals_str = "- 获取对话目标时出错。\n" goals_str = "- 获取对话目标时出错。\n"
except Exception as e: except Exception as e:
logger.error(f"构建对话目标字符串时出错: {e}") logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}")
goals_str = "- 构建对话目标时出错。\n" goals_str = "- 构建对话目标时出错。\n"
# 获取聊天历史记录 (chat_history_text) # 获取聊天历史记录 (chat_history_text)
@ -130,7 +211,7 @@ class ActionPlanner:
try: try:
if hasattr(observation_info, "chat_history") and observation_info.chat_history: if hasattr(observation_info, "chat_history") and observation_info.chat_history:
chat_history_text = observation_info.chat_history_str chat_history_text = observation_info.chat_history_str
if not chat_history_text: # 如果历史记录是空列表 if not chat_history_text:
chat_history_text = "还没有聊天记录。\n" chat_history_text = "还没有聊天记录。\n"
else: else:
chat_history_text = "还没有聊天记录。\n" chat_history_text = "还没有聊天记录。\n"
@ -148,51 +229,38 @@ class ActionPlanner:
chat_history_text += ( chat_history_text += (
f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}" f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
) )
# 清理消息应该由调用者或 observation_info 内部逻辑处理,这里不再调用 clear
# if hasattr(observation_info, 'clear_unprocessed_messages'):
# observation_info.clear_unprocessed_messages()
else: else:
logger.warning( logger.warning(
"ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing." f"[私聊][{self.private_name}]ObservationInfo has new_messages_count > 0 but unprocessed_messages is empty or missing."
) )
except AttributeError: except AttributeError:
logger.warning("ObservationInfo object might be missing expected attributes for chat history.") logger.warning(
f"[私聊][{self.private_name}]ObservationInfo object might be missing expected attributes for chat history."
)
chat_history_text = "获取聊天记录时出错。\n" chat_history_text = "获取聊天记录时出错。\n"
except Exception as e: except Exception as e:
logger.error(f"处理聊天记录时发生未知错误: {e}") logger.error(f"[私聊][{self.private_name}]处理聊天记录时发生未知错误: {e}")
chat_history_text = "处理聊天记录时出错。\n" chat_history_text = "处理聊天记录时出错。\n"
# 构建 Persona 文本 (persona_text) # 构建 Persona 文本 (persona_text)
identity_details_only = self.identity_detail_info persona_text = f"你的名字是{self.name}{self.personality_info}"
identity_addon = ""
if isinstance(identity_details_only, str):
pronouns = ["", "", ""]
# original_details = identity_details_only
for p in pronouns:
if identity_details_only.startswith(p):
identity_details_only = identity_details_only[len(p) :]
break
if identity_details_only.endswith(""):
identity_details_only = identity_details_only[:-1]
cleaned_details = identity_details_only.strip(", ")
if cleaned_details:
identity_addon = f"并且{cleaned_details}"
persona_text = f"你的名字是{self.name}{self.personality_info}{identity_addon}"
# --- 构建更清晰的行动历史和上一次行动结果 --- # 构建行动历史和上一次行动结果 (action_history_summary, last_action_context)
# (这部分逻辑不变)
action_history_summary = "你最近执行的行动历史:\n" action_history_summary = "你最近执行的行动历史:\n"
last_action_context = "关于你【上一次尝试】的行动:\n" last_action_context = "关于你【上一次尝试】的行动:\n"
action_history_list = [] action_history_list = []
try: # 添加 try-except try:
if hasattr(conversation_info, "done_action") and conversation_info.done_action: if hasattr(conversation_info, "done_action") and conversation_info.done_action:
action_history_list = conversation_info.done_action[-5:] action_history_list = conversation_info.done_action[-5:]
else: else:
logger.debug("Conversation info done_action is empty or not available.") logger.debug(f"[私聊][{self.private_name}]Conversation info done_action is empty or not available.")
except AttributeError: except AttributeError:
logger.warning("ConversationInfo object might not have done_action attribute yet.") logger.warning(
f"[私聊][{self.private_name}]ConversationInfo object might not have done_action attribute yet."
)
except Exception as e: except Exception as e:
logger.error(f"访问行动历史时出错: {e}") logger.error(f"[私聊][{self.private_name}]访问行动历史时出错: {e}")
if not action_history_list: if not action_history_list:
action_history_summary += "- 还没有执行过行动。\n" action_history_summary += "- 还没有执行过行动。\n"
@ -212,14 +280,17 @@ class ActionPlanner:
final_reason = action_data.get("final_reason", "") final_reason = action_data.get("final_reason", "")
action_time = action_data.get("time", "") action_time = action_data.get("time", "")
elif isinstance(action_data, tuple): elif isinstance(action_data, tuple):
# 假设旧格式兼容
if len(action_data) > 0: if len(action_data) > 0:
action_type = action_data[0] action_type = action_data[0]
if len(action_data) > 1: if len(action_data) > 1:
plan_reason = action_data[1] plan_reason = action_data[1] # 可能是规划原因或最终原因
if len(action_data) > 2: if len(action_data) > 2:
status = action_data[2] status = action_data[2]
if status == "recall" and len(action_data) > 3: if status == "recall" and len(action_data) > 3:
final_reason = action_data[3] final_reason = action_data[3]
elif status == "done" and action_type in ["direct_reply", "send_new_message"]:
plan_reason = "成功发送" # 简化显示
reason_text = f", 失败/取消原因: {final_reason}" if final_reason else "" reason_text = f", 失败/取消原因: {final_reason}" if final_reason else ""
summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}" summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}"
@ -230,56 +301,46 @@ class ActionPlanner:
last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n" last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n"
if status == "done": if status == "done":
last_action_context += "- 该行动已【成功执行】。\n" last_action_context += "- 该行动已【成功执行】。\n"
# 记录这次成功的行动类型,供下次决策
# self.last_successful_action_type = action_type # 不在这里记录,由 conversation 控制
elif status == "recall": elif status == "recall":
last_action_context += "- 但该行动最终【未能执行/被取消】。\n" last_action_context += "- 但该行动最终【未能执行/被取消】。\n"
if final_reason: if final_reason:
last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}\n" last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}\n"
else: else:
last_action_context += "- 【重要】失败/取消原因未明确记录。\n" last_action_context += "- 【重要】失败/取消原因未明确记录。\n"
# self.last_successful_action_type = None # 行动失败,清除记录
else: else:
last_action_context += f"- 该行动当前状态: {status}\n" last_action_context += f"- 该行动当前状态: {status}\n"
# self.last_successful_action_type = None # 非完成状态,清除记录
# --- 构建最终的 Prompt --- # --- 选择 Prompt ---
prompt = f"""{persona_text}。现在你在参与一场QQ私聊请根据以下【所有信息】审慎且灵活的决策下一步行动可以发言可以等待可以倾听可以调取知识甚至可以屏蔽对方 if last_successful_reply_action in ["direct_reply", "send_new_message"]:
prompt_template = PROMPT_FOLLOW_UP
logger.debug(f"[私聊][{self.private_name}]使用 PROMPT_FOLLOW_UP (追问决策)")
else:
prompt_template = PROMPT_INITIAL_REPLY
logger.debug(f"[私聊][{self.private_name}]使用 PROMPT_INITIAL_REPLY (首次/非连续回复决策)")
当前对话目标 # --- 格式化最终的 Prompt ---
{goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。"} prompt = prompt_template.format(
persona_text=persona_text,
goals_str=goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。",
action_history_summary=action_history_summary,
last_action_context=last_action_context,
time_since_last_bot_message_info=time_since_last_bot_message_info,
timeout_context=timeout_context,
chat_history_text=chat_history_text if chat_history_text.strip() else "还没有聊天记录。",
)
logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------")
最近行动历史概要
{action_history_summary}
上一次行动的详细情况和结果
{last_action_context}
时间和超时提示
{time_since_last_bot_message_info}{timeout_context}
最近的对话记录(包括你已成功发送的消息 新收到的消息)
{chat_history_text if chat_history_text.strip() else "还没有聊天记录。"}
------
可选行动类型以及解释
fetch_knowledge: 需要调取知识当需要专业知识或特定信息时选择对方若提到你不太认识的人名或实体也可以尝试选择
wait: 暂时不说话等待对方回复尤其是在你刚发言后或上次发言因重复发言过多被拒时或不确定做什么时这是较安全的选择
listening: 倾听对方发言当你认为对方话才说到一半发言明显未结束时选择
direct_reply: 直接回复或发送新消息允许适当的追问和深入话题**但是避免在因重复被拒后立即使用也不要在对方没有回复的情况下过多的消息轰炸或重复发言**
rethink_goal: 重新思考对话目标当发现对话目标不再适用或对话卡住时选择注意私聊的环境是灵活的有可能需要经常选择
end_conversation: 结束对话对方长时间没回复或者当你觉得对话告一段落时可以选择
block_and_ignore: 更加极端的结束对话方式直接结束对话并在一段时间内无视对方所有发言屏蔽当对话让你感到十分不适或你遭到各类骚扰时选择
请以JSON格式输出你的决策
{{
"action": "选择的行动类型 (必须是上面列表中的一个)",
"reason": "选择该行动的详细原因 (必须有解释你是如何根据“上一次行动结果”、“对话记录”和自身设定人设做出合理判断的,如果你连续发言,必须记录已经发言了几次)"
}}
注意请严格按照JSON格式输出不要包含任何其他内容"""
logger.debug(f"发送到LLM的提示词 (已更新): {prompt}")
try: try:
content, _ = await self.llm.generate_response_async(prompt) content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"LLM原始返回内容: {content}") logger.debug(f"[私聊][{self.private_name}]LLM原始返回内容: {content}")
success, result = get_items_from_json( success, result = get_items_from_json(
content, content,
self.private_name,
"action", "action",
"reason", "reason",
default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因默认等待"}, default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因默认等待"},
@ -289,8 +350,10 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在
reason = result.get("reason", "LLM未提供原因默认等待") reason = result.get("reason", "LLM未提供原因默认等待")
# 验证action类型 # 验证action类型
# 更新 valid_actions 列表以包含 send_new_message
valid_actions = [ valid_actions = [
"direct_reply", "direct_reply",
"send_new_message", # 添加新动作
"fetch_knowledge", "fetch_knowledge",
"wait", "wait",
"listening", "listening",
@ -299,14 +362,14 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在
"block_and_ignore", "block_and_ignore",
] ]
if action not in valid_actions: if action not in valid_actions:
logger.warning(f"LLM返回了未知的行动类型: '{action}',强制改为 wait") logger.warning(f"[私聊][{self.private_name}]LLM返回了未知的行动类型: '{action}',强制改为 wait")
reason = f"(原始行动'{action}'无效已强制改为wait) {reason}" reason = f"(原始行动'{action}'无效已强制改为wait) {reason}"
action = "wait" action = "wait"
logger.info(f"规划的行动: {action}") logger.info(f"[私聊][{self.private_name}]规划的行动: {action}")
logger.info(f"行动原因: {reason}") logger.info(f"[私聊][{self.private_name}]行动原因: {reason}")
return action, reason return action, reason
except Exception as e: except Exception as e:
logger.error(f"规划行动时调用 LLM 或处理结果出错: {str(e)}") logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}")
return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}" return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}"

View File

@ -18,7 +18,7 @@ class ChatObserver:
_instances: Dict[str, "ChatObserver"] = {} _instances: Dict[str, "ChatObserver"] = {}
@classmethod @classmethod
def get_instance(cls, stream_id: str) -> "ChatObserver": def get_instance(cls, stream_id: str, private_name: str) -> "ChatObserver":
"""获取或创建观察器实例 """获取或创建观察器实例
Args: Args:
@ -28,10 +28,10 @@ class ChatObserver:
ChatObserver: 观察器实例 ChatObserver: 观察器实例
""" """
if stream_id not in cls._instances: if stream_id not in cls._instances:
cls._instances[stream_id] = cls(stream_id) cls._instances[stream_id] = cls(stream_id, private_name)
return cls._instances[stream_id] return cls._instances[stream_id]
def __init__(self, stream_id: str): def __init__(self, stream_id: str, private_name: str):
"""初始化观察器 """初始化观察器
Args: Args:
@ -41,6 +41,7 @@ class ChatObserver:
raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.") raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.")
self.stream_id = stream_id self.stream_id = stream_id
self.private_name = private_name
self.message_storage = MongoDBMessageStorage() self.message_storage = MongoDBMessageStorage()
# self.last_user_speak_time: Optional[float] = None # 对方上次发言时间 # self.last_user_speak_time: Optional[float] = None # 对方上次发言时间
@ -76,12 +77,12 @@ class ChatObserver:
Returns: Returns:
bool: 是否有新消息 bool: 是否有新消息
""" """
logger.debug(f"检查距离上一次观察之后是否有了新消息: {self.last_check_time}") logger.debug(f"[私聊][{self.private_name}]检查距离上一次观察之后是否有了新消息: {self.last_check_time}")
new_message_exists = await self.message_storage.has_new_messages(self.stream_id, self.last_check_time) new_message_exists = await self.message_storage.has_new_messages(self.stream_id, self.last_check_time)
if new_message_exists: if new_message_exists:
logger.debug("发现新消息") logger.debug(f"[私聊][{self.private_name}]发现新消息")
self.last_check_time = time.time() self.last_check_time = time.time()
return new_message_exists return new_message_exists
@ -94,15 +95,13 @@ class ChatObserver:
""" """
try: try:
# 发送新消息通知 # 发送新消息通知
# logger.info(f"发送新ccchandleer消息通知: {message}")
notification = create_new_message_notification( notification = create_new_message_notification(
sender="chat_observer", target="observation_info", message=message sender="chat_observer", target="observation_info", message=message
) )
# logger.info(f"发送新消ddddd息通知: {notification}")
# print(self.notification_manager) # print(self.notification_manager)
await self.notification_manager.send_notification(notification) await self.notification_manager.send_notification(notification)
except Exception as e: except Exception as e:
logger.error(f"添加消息到历史记录时出错: {e}") logger.error(f"[私聊][{self.private_name}]添加消息到历史记录时出错: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# 检查并更新冷场状态 # 检查并更新冷场状态
@ -142,11 +141,13 @@ class ChatObserver:
""" """
if self.last_message_time is None: if self.last_message_time is None:
logger.debug("没有最后消息时间,返回 False") logger.debug(f"[私聊][{self.private_name}]没有最后消息时间,返回 False")
return False return False
has_new = self.last_message_time > time_point has_new = self.last_message_time > time_point
logger.debug(f"判断是否在指定时间点后有新消息: {self.last_message_time} > {time_point} = {has_new}") logger.debug(
f"[私聊][{self.private_name}]判断是否在指定时间点后有新消息: {self.last_message_time} > {time_point} = {has_new}"
)
return has_new return has_new
def get_message_history( def get_message_history(
@ -215,7 +216,7 @@ class ChatObserver:
if new_messages: if new_messages:
self.last_message_read = new_messages[-1]["message_id"] self.last_message_read = new_messages[-1]["message_id"]
logger.debug(f"获取指定时间点111之前的消息: {new_messages}") logger.debug(f"[私聊][{self.private_name}]获取指定时间点111之前的消息: {new_messages}")
return new_messages return new_messages
@ -228,9 +229,9 @@ class ChatObserver:
# messages = await self._fetch_new_messages_before(start_time) # messages = await self._fetch_new_messages_before(start_time)
# for message in messages: # for message in messages:
# await self._add_message_to_history(message) # await self._add_message_to_history(message)
# logger.debug(f"缓冲消息: {messages}") # logger.debug(f"[私聊][{self.private_name}]缓冲消息: {messages}")
# except Exception as e: # except Exception as e:
# logger.error(f"缓冲消息出错: {e}") # logger.error(f"[私聊][{self.private_name}]缓冲消息出错: {e}")
while self._running: while self._running:
try: try:
@ -258,8 +259,8 @@ class ChatObserver:
self._update_complete.set() self._update_complete.set()
except Exception as e: except Exception as e:
logger.error(f"更新循环出错: {e}") logger.error(f"[私聊][{self.private_name}]更新循环出错: {e}")
logger.error(traceback.format_exc()) logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
self._update_complete.set() # 即使出错也要设置完成事件 self._update_complete.set() # 即使出错也要设置完成事件
def trigger_update(self): def trigger_update(self):
@ -279,7 +280,7 @@ class ChatObserver:
await asyncio.wait_for(self._update_complete.wait(), timeout=timeout) await asyncio.wait_for(self._update_complete.wait(), timeout=timeout)
return True return True
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning(f"等待更新完成超时({timeout}秒)") logger.warning(f"[私聊][{self.private_name}]等待更新完成超时({timeout}秒)")
return False return False
def start(self): def start(self):
@ -289,7 +290,7 @@ class ChatObserver:
self._running = True self._running = True
self._task = asyncio.create_task(self._update_loop()) self._task = asyncio.create_task(self._update_loop())
logger.info(f"ChatObserver for {self.stream_id} started") logger.debug(f"[私聊][{self.private_name}]ChatObserver for {self.stream_id} started")
def stop(self): def stop(self):
"""停止观察器""" """停止观察器"""
@ -298,7 +299,7 @@ class ChatObserver:
self._update_complete.set() # 设置完成事件以解除等待 self._update_complete.set() # 设置完成事件以解除等待
if self._task: if self._task:
self._task.cancel() self._task.cancel()
logger.info(f"ChatObserver for {self.stream_id} stopped") logger.debug(f"[私聊][{self.private_name}]ChatObserver for {self.stream_id} stopped")
async def process_chat_history(self, messages: list): async def process_chat_history(self, messages: list):
"""处理聊天历史 """处理聊天历史
@ -316,7 +317,7 @@ class ChatObserver:
else: else:
self.update_user_speak_time(msg["time"]) self.update_user_speak_time(msg["time"])
except Exception as e: except Exception as e:
logger.warning(f"处理消息时间时出错: {e}") logger.warning(f"[私聊][{self.private_name}]处理消息时间时出错: {e}")
continue continue
def update_check_time(self): def update_check_time(self):

View File

@ -98,15 +98,11 @@ class NotificationManager:
notification_type: 要处理的通知类型 notification_type: 要处理的通知类型
handler: 处理器实例 handler: 处理器实例
""" """
# print(1145145511114445551111444)
if target not in self._handlers: if target not in self._handlers:
# print("没11有target")
self._handlers[target] = {} self._handlers[target] = {}
if notification_type not in self._handlers[target]: if notification_type not in self._handlers[target]:
# print("没11有notification_type")
self._handlers[target][notification_type] = [] self._handlers[target][notification_type] = []
# print(self._handlers[target][notification_type]) # print(self._handlers[target][notification_type])
# print(f"注册1111111111111111111111处理器: {target} {notification_type} {handler}")
self._handlers[target][notification_type].append(handler) self._handlers[target][notification_type].append(handler)
# print(self._handlers[target][notification_type]) # print(self._handlers[target][notification_type])
@ -132,7 +128,6 @@ class NotificationManager:
async def send_notification(self, notification: Notification): async def send_notification(self, notification: Notification):
"""发送通知""" """发送通知"""
self._notification_history.append(notification) self._notification_history.append(notification)
# print("kaishichul-----------------------------------i")
# 如果是状态通知,更新活跃状态 # 如果是状态通知,更新活跃状态
if isinstance(notification, StateNotification): if isinstance(notification, StateNotification):
@ -145,7 +140,6 @@ class NotificationManager:
target = notification.target target = notification.target
if target in self._handlers: if target in self._handlers:
handlers = self._handlers[target].get(notification.type, []) handlers = self._handlers[target].get(notification.type, [])
# print(1111111)
# print(handlers) # print(handlers)
for handler in handlers: for handler in handlers:
# print(f"调用处理器: {handler}") # print(f"调用处理器: {handler}")

View File

@ -9,11 +9,12 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from ..chat.message import Message from ..chat.message import Message
from .pfc_types import ConversationState from .pfc_types import ConversationState
from .pfc import ChatObserver, GoalAnalyzer, DirectMessageSender from .pfc import ChatObserver, GoalAnalyzer
from .message_sender import DirectMessageSender
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from .action_planner import ActionPlanner from .action_planner import ActionPlanner
from .observation_info import ObservationInfo from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo from .conversation_info import ConversationInfo # 确保导入 ConversationInfo
from .reply_generator import ReplyGenerator from .reply_generator import ReplyGenerator
from ..chat.chat_stream import ChatStream from ..chat.chat_stream import ChatStream
from maim_message import UserInfo from maim_message import UserInfo
@ -29,13 +30,14 @@ logger = get_logger("pfc")
class Conversation: class Conversation:
"""对话类,负责管理单个对话的状态和行为""" """对话类,负责管理单个对话的状态和行为"""
def __init__(self, stream_id: str): def __init__(self, stream_id: str, private_name: str):
"""初始化对话实例 """初始化对话实例
Args: Args:
stream_id: 聊天流ID stream_id: 聊天流ID
""" """
self.stream_id = stream_id self.stream_id = stream_id
self.private_name = private_name
self.state = ConversationState.INIT self.state = ConversationState.INIT
self.should_continue = False self.should_continue = False
self.ignore_until_timestamp: Optional[float] = None self.ignore_until_timestamp: Optional[float] = None
@ -47,38 +49,38 @@ class Conversation:
"""初始化实例,注册所有组件""" """初始化实例,注册所有组件"""
try: try:
self.action_planner = ActionPlanner(self.stream_id) self.action_planner = ActionPlanner(self.stream_id, self.private_name)
self.goal_analyzer = GoalAnalyzer(self.stream_id) self.goal_analyzer = GoalAnalyzer(self.stream_id, self.private_name)
self.reply_generator = ReplyGenerator(self.stream_id) self.reply_generator = ReplyGenerator(self.stream_id, self.private_name)
self.knowledge_fetcher = KnowledgeFetcher() self.knowledge_fetcher = KnowledgeFetcher(self.private_name)
self.waiter = Waiter(self.stream_id) self.waiter = Waiter(self.stream_id, self.private_name)
self.direct_sender = DirectMessageSender() self.direct_sender = DirectMessageSender(self.private_name)
# 获取聊天流信息 # 获取聊天流信息
self.chat_stream = chat_manager.get_stream(self.stream_id) self.chat_stream = chat_manager.get_stream(self.stream_id)
self.stop_action_planner = False self.stop_action_planner = False
except Exception as e: except Exception as e:
logger.error(f"初始化对话实例:注册运行组件失败: {e}") logger.error(f"[私聊][{self.private_name}]初始化对话实例:注册运行组件失败: {e}")
logger.error(traceback.format_exc()) logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
raise raise
try: try:
# 决策所需要的信息,包括自身自信和观察信息两部分 # 决策所需要的信息,包括自身自信和观察信息两部分
# 注册观察器和观测信息 # 注册观察器和观测信息
self.chat_observer = ChatObserver.get_instance(self.stream_id) self.chat_observer = ChatObserver.get_instance(self.stream_id, self.private_name)
self.chat_observer.start() self.chat_observer.start()
self.observation_info = ObservationInfo() self.observation_info = ObservationInfo(self.private_name)
self.observation_info.bind_to_chat_observer(self.chat_observer) self.observation_info.bind_to_chat_observer(self.chat_observer)
# print(self.chat_observer.get_cached_messages(limit=) # print(self.chat_observer.get_cached_messages(limit=)
self.conversation_info = ConversationInfo() self.conversation_info = ConversationInfo()
except Exception as e: except Exception as e:
logger.error(f"初始化对话实例:注册信息组件失败: {e}") logger.error(f"[私聊][{self.private_name}]初始化对话实例:注册信息组件失败: {e}")
logger.error(traceback.format_exc()) logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
raise raise
try: try:
logger.info(f"{self.stream_id} 加载初始聊天记录...") logger.info(f"[私聊][{self.private_name}]{self.stream_id} 加载初始聊天记录...")
initial_messages = get_raw_msg_before_timestamp_with_chat( # initial_messages = get_raw_msg_before_timestamp_with_chat( #
chat_id=self.stream_id, chat_id=self.stream_id,
timestamp=time.time(), timestamp=time.time(),
@ -104,21 +106,18 @@ class Conversation:
self.observation_info.last_message_sender = last_user_info.user_id self.observation_info.last_message_sender = last_user_info.user_id
self.observation_info.last_message_content = last_msg.get("processed_plain_text", "") self.observation_info.last_message_content = last_msg.get("processed_plain_text", "")
# (可选)可以遍历 initial_messages 来设置 last_bot_speak_time 和 last_user_speak_time
# 这里为了简化,只用了最后一条消息的时间,如果需要精确的发言者时间需要遍历
logger.info( logger.info(
f"成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}" f"[私聊][{self.private_name}]成功加载 {len(initial_messages)} 条初始聊天记录。最后一条消息时间: {self.observation_info.last_message_time}"
) )
# 让 ChatObserver 从加载的最后一条消息之后开始同步 # 让 ChatObserver 从加载的最后一条消息之后开始同步
self.chat_observer.last_message_time = self.observation_info.last_message_time self.chat_observer.last_message_time = self.observation_info.last_message_time
self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录 self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录
else: else:
logger.info("没有找到初始聊天记录。") logger.info(f"[私聊][{self.private_name}]没有找到初始聊天记录。")
except Exception as load_err: except Exception as load_err:
logger.error(f"加载初始聊天记录时出错: {load_err}") logger.error(f"[私聊][{self.private_name}]加载初始聊天记录时出错: {load_err}")
# 出错也要继续,只是没有历史记录而已 # 出错也要继续,只是没有历史记录而已
# 组件准备完成,启动该论对话 # 组件准备完成,启动该论对话
self.should_continue = True self.should_continue = True
@ -127,149 +126,179 @@ class Conversation:
async def start(self): async def start(self):
"""开始对话流程""" """开始对话流程"""
try: try:
logger.info("对话系统启动中...") logger.info(f"[私聊][{self.private_name}]对话系统启动中...")
asyncio.create_task(self._plan_and_action_loop()) asyncio.create_task(self._plan_and_action_loop())
except Exception as e: except Exception as e:
logger.error(f"启动对话系统失败: {e}") logger.error(f"[私聊][{self.private_name}]启动对话系统失败: {e}")
raise raise
async def _plan_and_action_loop(self): async def _plan_and_action_loop(self):
"""思考步PFC核心循环模块""" """思考步PFC核心循环模块"""
while self.should_continue: while self.should_continue:
# 忽略逻辑
if self.ignore_until_timestamp and time.time() < self.ignore_until_timestamp: if self.ignore_until_timestamp and time.time() < self.ignore_until_timestamp:
# 仍在忽略期间,等待下次检查 await asyncio.sleep(30)
await asyncio.sleep(30) # 每 30 秒检查一次 continue
continue # 跳过本轮循环的剩余部分
elif self.ignore_until_timestamp and time.time() >= self.ignore_until_timestamp: elif self.ignore_until_timestamp and time.time() >= self.ignore_until_timestamp:
# 忽略期结束,现在正常地结束对话 logger.info(f"[私聊][{self.private_name}]忽略时间已到 {self.stream_id},准备结束对话。")
logger.info(f"忽略时间已到 {self.stream_id},准备结束对话。") self.ignore_until_timestamp = None
self.ignore_until_timestamp = None # 清除时间戳 self.should_continue = False
self.should_continue = False # 现在停止循环 continue
# (可选)在这里记录一个 'end_conversation' 动作
# 或者确保管理器会基于 should_continue 为 False 来清理它
continue # 跳过本轮循环的剩余部分,让它终止
try: try:
# --- 在规划前记录当前新消息数量 --- # --- 在规划前记录当前新消息数量 ---
initial_new_message_count = 0 initial_new_message_count = 0
if hasattr(self.observation_info, "new_messages_count"): if hasattr(self.observation_info, "new_messages_count"):
initial_new_message_count = self.observation_info.new_messages_count initial_new_message_count = self.observation_info.new_messages_count + 1 # 算上麦麦自己发的那一条
else: else:
logger.warning("ObservationInfo missing 'new_messages_count' before planning.") logger.warning(
f"[私聊][{self.private_name}]ObservationInfo missing 'new_messages_count' before planning."
)
# 使用决策信息来辅助行动规划 # --- 调用 Action Planner ---
# 传递 self.conversation_info.last_successful_reply_action
action, reason = await self.action_planner.plan( action, reason = await self.action_planner.plan(
self.observation_info, self.conversation_info self.observation_info, self.conversation_info, self.conversation_info.last_successful_reply_action
) # 注意plan 函数内部现在不应再调用 clear_unprocessed_messages )
# --- 规划后检查是否有 *更多* 新消息到达 --- # --- 规划后检查是否有 *更多* 新消息到达 ---
current_new_message_count = 0 current_new_message_count = 0
if hasattr(self.observation_info, "new_messages_count"): if hasattr(self.observation_info, "new_messages_count"):
current_new_message_count = self.observation_info.new_messages_count current_new_message_count = self.observation_info.new_messages_count
else: else:
logger.warning("ObservationInfo missing 'new_messages_count' after planning.") logger.warning(
f"[私聊][{self.private_name}]ObservationInfo missing 'new_messages_count' after planning."
if current_new_message_count > initial_new_message_count:
# 只有当规划期间消息数量 *增加* 了,才认为需要重新规划
logger.info(
f"规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划"
) )
await asyncio.sleep(0.1) # 短暂延时
continue # 跳过本次行动,重新规划
# --- 如果没有在规划期间收到更多新消息,则准备执行行动 --- if current_new_message_count > initial_new_message_count + 2:
logger.info(
f"[私聊][{self.private_name}]规划期间发现新增消息 ({initial_new_message_count} -> {current_new_message_count}),跳过本次行动,重新规划"
)
# 如果规划期间有新消息,也应该重置上次回复状态,因为现在要响应新消息了
self.conversation_info.last_successful_reply_action = None
await asyncio.sleep(0.1)
continue
# --- 清理未处理消息:移到这里,在执行动作前 --- # 包含 send_new_message
# 只有当确实有新消息被 planner 看到,并且 action 是要处理它们的时候才清理 if initial_new_message_count > 0 and action in ["direct_reply", "send_new_message"]:
if initial_new_message_count > 0 and action == "direct_reply":
if hasattr(self.observation_info, "clear_unprocessed_messages"): if hasattr(self.observation_info, "clear_unprocessed_messages"):
# 确保 clear_unprocessed_messages 方法存在 logger.debug(
logger.debug(f"准备执行 direct_reply清理 {initial_new_message_count} 条规划时已知的新消息。") f"[私聊][{self.private_name}]准备执行 {action},清理 {initial_new_message_count} 条规划时已知的新消息。"
)
await self.observation_info.clear_unprocessed_messages() await self.observation_info.clear_unprocessed_messages()
# 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个)
if hasattr(self.observation_info, "new_messages_count"): if hasattr(self.observation_info, "new_messages_count"):
self.observation_info.new_messages_count = 0 self.observation_info.new_messages_count = 0
else: else:
logger.error("无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!") logger.error(
# 这里可能需要考虑是否继续执行 action或者抛出错误 f"[私聊][{self.private_name}]无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!"
)
# --- 执行行动 ---
await self._handle_action(action, reason, self.observation_info, self.conversation_info) await self._handle_action(action, reason, self.observation_info, self.conversation_info)
# 检查是否需要结束对话 (逻辑不变)
goal_ended = False goal_ended = False
if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list: if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list:
for goal in self.conversation_info.goal_list: for goal_item in self.conversation_info.goal_list:
if isinstance(goal, tuple) and len(goal) > 0 and goal[0] == "结束对话": if isinstance(goal_item, dict):
goal_ended = True current_goal = goal_item.get("goal")
break
elif isinstance(goal, dict) and goal.get("goal") == "结束对话": if current_goal == "结束对话":
goal_ended = True goal_ended = True
break break
if goal_ended: if goal_ended:
self.should_continue = False self.should_continue = False
logger.info("检测到'结束对话'目标,停止循环。") logger.info(f"[私聊][{self.private_name}]检测到'结束对话'目标,停止循环。")
# break # 可以选择在这里直接跳出循环
except Exception as loop_err: except Exception as loop_err:
logger.error(f"PFC主循环出错: {loop_err}") logger.error(f"[私聊][{self.private_name}]PFC主循环出错: {loop_err}")
logger.error(traceback.format_exc()) logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
# 发生严重错误时可以考虑停止,或者至少等待一下再继续 await asyncio.sleep(1)
await asyncio.sleep(1) # 发生错误时等待1秒
# 添加短暂的异步睡眠
if self.should_continue: # 只有在还需要继续循环时才 sleep
await asyncio.sleep(0.1) # 等待 0.1 秒,给其他任务执行时间
logger.info(f"PFC 循环结束 for stream_id: {self.stream_id}") # 添加日志表明循环正常结束 if self.should_continue:
await asyncio.sleep(0.1)
logger.info(f"[私聊][{self.private_name}]PFC 循环结束 for stream_id: {self.stream_id}")
def _check_new_messages_after_planning(self): def _check_new_messages_after_planning(self):
"""检查在规划后是否有新消息""" """检查在规划后是否有新消息"""
if self.observation_info.new_messages_count > 0: # 检查 ObservationInfo 是否已初始化并且有 new_messages_count 属性
logger.info(f"发现{self.observation_info.new_messages_count}条新消息,可能需要重新考虑行动") if not hasattr(self, "observation_info") or not hasattr(self.observation_info, "new_messages_count"):
# 如果需要,可以在这里添加逻辑来根据新消息重新决定行动 logger.warning(
f"[私聊][{self.private_name}]ObservationInfo 未初始化或缺少 'new_messages_count' 属性,无法检查新消息。"
)
return False # 或者根据需要抛出错误
if self.observation_info.new_messages_count > 2:
logger.info(
f"[私聊][{self.private_name}]生成/执行动作期间收到 {self.observation_info.new_messages_count} 条新消息,取消当前动作并重新规划"
)
# 如果有新消息,也应该重置上次回复状态
if hasattr(self, "conversation_info"): # 确保 conversation_info 已初始化
self.conversation_info.last_successful_reply_action = None
else:
logger.warning(
f"[私聊][{self.private_name}]ConversationInfo 未初始化,无法重置 last_successful_reply_action。"
)
return True return True
return False return False
def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message: def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message:
"""将消息字典转换为Message对象""" """将消息字典转换为Message对象"""
try: try:
chat_info = msg_dict.get("chat_info", {}) # 尝试从 msg_dict 直接获取 chat_stream如果失败则从全局 chat_manager 获取
chat_stream = ChatStream.from_dict(chat_info) chat_info = msg_dict.get("chat_info")
if chat_info and isinstance(chat_info, dict):
chat_stream = ChatStream.from_dict(chat_info)
elif self.chat_stream: # 使用实例变量中的 chat_stream
chat_stream = self.chat_stream
else: # Fallback: 尝试从 manager 获取 (可能需要 stream_id)
chat_stream = chat_manager.get_stream(self.stream_id)
if not chat_stream:
raise ValueError(f"无法确定 ChatStream for stream_id {self.stream_id}")
user_info = UserInfo.from_dict(msg_dict.get("user_info", {})) user_info = UserInfo.from_dict(msg_dict.get("user_info", {}))
return Message( return Message(
message_id=msg_dict["message_id"], message_id=msg_dict.get("message_id", f"gen_{time.time()}"), # 提供默认 ID
chat_stream=chat_stream, chat_stream=chat_stream, # 使用确定的 chat_stream
time=msg_dict["time"], time=msg_dict.get("time", time.time()), # 提供默认时间
user_info=user_info, user_info=user_info,
processed_plain_text=msg_dict.get("processed_plain_text", ""), processed_plain_text=msg_dict.get("processed_plain_text", ""),
detailed_plain_text=msg_dict.get("detailed_plain_text", ""), detailed_plain_text=msg_dict.get("detailed_plain_text", ""),
) )
except Exception as e: except Exception as e:
logger.warning(f"转换消息时出错: {e}") logger.warning(f"[私聊][{self.private_name}]转换消息时出错: {e}")
raise # 可以选择返回 None 或重新抛出异常,这里选择重新抛出以指示问题
raise ValueError(f"无法将字典转换为 Message 对象: {e}") from e
async def _handle_action( async def _handle_action(
self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo self, action: str, reason: str, observation_info: ObservationInfo, conversation_info: ConversationInfo
): ):
"""处理规划的行动""" """处理规划的行动"""
logger.info(f"执行行动: {action}, 原因: {reason}") logger.debug(f"[私聊][{self.private_name}]执行行动: {action}, 原因: {reason}")
# 记录action历史先设置为start完成后再设置为done (这个 update 移到后面执行成功后再做) # 记录action历史 (逻辑不变)
current_action_record = { current_action_record = {
"action": action, "action": action,
"plan_reason": reason, # 使用 plan_reason 存储规划原因 "plan_reason": reason,
"status": "start", # 初始状态为 start "status": "start",
"time": datetime.datetime.now().strftime("%H:%M:%S"), "time": datetime.datetime.now().strftime("%H:%M:%S"),
"final_reason": None, "final_reason": None,
} }
# 确保 done_action 列表存在
if not hasattr(conversation_info, "done_action"):
conversation_info.done_action = []
conversation_info.done_action.append(current_action_record) conversation_info.done_action.append(current_action_record)
# 获取刚刚添加记录的索引,方便后面更新状态
action_index = len(conversation_info.done_action) - 1 action_index = len(conversation_info.done_action) - 1
action_successful = False # 用于标记动作是否成功完成
# --- 根据不同的 action 执行 --- # --- 根据不同的 action 执行 ---
if action == "direct_reply":
max_reply_attempts = 3 # 设置最大尝试次数(与 reply_checker.py 中的 max_retries 保持一致或稍大) # send_new_message 失败后执行 wait
if action == "send_new_message":
max_reply_attempts = 3
reply_attempt_count = 0 reply_attempt_count = 0
is_suitable = False is_suitable = False
need_replan = False need_replan = False
@ -278,179 +307,343 @@ class Conversation:
while reply_attempt_count < max_reply_attempts and not is_suitable: while reply_attempt_count < max_reply_attempts and not is_suitable:
reply_attempt_count += 1 reply_attempt_count += 1
logger.info(f"尝试生成回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)...") logger.info(
f"[私聊][{self.private_name}]尝试生成追问回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..."
)
self.state = ConversationState.GENERATING self.state = ConversationState.GENERATING
# 1. 生成回复 # 1. 生成回复 (调用 generate 时传入 action_type)
self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info) self.generated_reply = await self.reply_generator.generate(
logger.info(f"{reply_attempt_count} 次生成的回复: {self.generated_reply}") observation_info, conversation_info, action_type="send_new_message"
)
logger.info(
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次生成的追问回复: {self.generated_reply}"
)
# 2. 检查回复 # 2. 检查回复 (逻辑不变)
self.state = ConversationState.CHECKING self.state = ConversationState.CHECKING
try: try:
current_goal_str = conversation_info.goal_list[0]["goal"] if conversation_info.goal_list else "" current_goal_str = conversation_info.goal_list[0]["goal"] if conversation_info.goal_list else ""
# 注意:这里传递的是 reply_attempt_count - 1 作为 retry_count 给 checker
is_suitable, check_reason, need_replan = await self.reply_generator.check_reply( is_suitable, check_reason, need_replan = await self.reply_generator.check_reply(
reply=self.generated_reply, reply=self.generated_reply,
goal=current_goal_str, goal=current_goal_str,
chat_history=observation_info.chat_history, chat_history=observation_info.chat_history,
chat_history_str=observation_info.chat_history_str, chat_history_str=observation_info.chat_history_str,
retry_count=reply_attempt_count - 1, # 传递当前尝试次数从0开始计数 retry_count=reply_attempt_count - 1,
) )
logger.info( logger.info(
f"{reply_attempt_count}检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}" f"[私聊][{self.private_name}]{reply_attempt_count}追问检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}"
) )
if is_suitable: if is_suitable:
final_reply_to_send = self.generated_reply # 保存合适的回复 final_reply_to_send = self.generated_reply
break # 回复合适,跳出循环 break
elif need_replan: elif need_replan:
logger.warning(f"{reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}") logger.warning(
break # 如果检查器建议重新规划,也停止尝试 f"[私聊][{self.private_name}]第 {reply_attempt_count} 次追问检查建议重新规划,停止尝试。原因: {check_reason}"
)
# 如果不合适但不需要重新规划,循环会继续进行下一次尝试 break
except Exception as check_err: except Exception as check_err:
logger.error(f"{reply_attempt_count} 次调用 ReplyChecker 时出错: {check_err}") logger.error(
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次调用 ReplyChecker (追问) 时出错: {check_err}"
)
check_reason = f"{reply_attempt_count} 次检查过程出错: {check_err}" check_reason = f"{reply_attempt_count} 次检查过程出错: {check_err}"
# 如果检查本身出错,可以选择跳出循环或继续尝试
# 这里选择跳出循环,避免无限循环在检查错误上
break break
# 循环结束,处理最终结果 # 循环结束,处理最终结果
if is_suitable: if is_suitable:
# 回复合适且已保存在 final_reply_to_send 中 # 检查是否有新消息
# 检查是否有新消息进来 (在所有尝试结束后再检查一次)
if self._check_new_messages_after_planning(): if self._check_new_messages_after_planning():
logger.info("生成回复期间收到新消息,取消发送,重新规划行动") logger.info(f"[私聊][{self.private_name}]生成追问回复期间收到新消息,取消发送,重新规划行动")
conversation_info.done_action[action_index].update( conversation_info.done_action[action_index].update(
{ {"status": "recall", "final_reason": f"有新消息,取消发送追问: {final_reply_to_send}"}
"status": "recall",
"final_reason": f"有新消息,取消发送: {final_reply_to_send}",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
) )
# 这里直接返回不执行后续发送和wait return # 直接返回,重新规划
return
# 发送合适的回复 # 发送合适的回复
self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容 self.generated_reply = final_reply_to_send
await self._send_reply() # --- 在这里调用 _send_reply ---
await self._send_reply() # <--- 调用恢复后的函数
# 更新 action 历史状态为 done # 更新状态: 标记上次成功是 send_new_message
self.conversation_info.last_successful_reply_action = "send_new_message"
action_successful = True # 标记动作成功
elif need_replan:
# 打回动作决策
logger.warning(
f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,追问回复决定打回动作决策。打回原因: {check_reason}"
)
conversation_info.done_action[action_index].update( conversation_info.done_action[action_index].update(
{ {"status": "recall", "final_reason": f"追问尝试{reply_attempt_count}次后打回: {check_reason}"}
"status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
) )
else: else:
# 循环结束但没有找到合适的回复(达到最大次数或检查出错/建议重规划) # 追问失败
logger.warning(f"经过 {reply_attempt_count} 次尝试,未能生成合适的回复。最终原因: {check_reason}") logger.warning(
conversation_info.done_action[action_index].update( f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,未能生成合适的追问回复。最终原因: {check_reason}"
{
"status": "recall", # 标记为 recall 因为没有成功发送
"final_reason": f"尝试{reply_attempt_count}次后失败: {check_reason}",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
) )
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"追问尝试{reply_attempt_count}次后失败: {check_reason}"}
)
# 重置状态: 追问失败,下次用初始 prompt
self.conversation_info.last_successful_reply_action = None
# 执行 Wait 操作 # 执行 Wait 操作
logger.info("由于无法生成合适回复,执行 'wait' 操作...") logger.info(f"[私聊][{self.private_name}]由于无法生成合适追问回复,执行 'wait' 操作...")
self.state = ConversationState.WAITING
await self.waiter.wait(self.conversation_info)
wait_action_record = {
"action": "wait",
"plan_reason": "因 send_new_message 多次尝试失败而执行的后备等待",
"status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S"),
"final_reason": None,
}
conversation_info.done_action.append(wait_action_record)
elif action == "direct_reply":
max_reply_attempts = 3
reply_attempt_count = 0
is_suitable = False
need_replan = False
check_reason = "未进行尝试"
final_reply_to_send = ""
while reply_attempt_count < max_reply_attempts and not is_suitable:
reply_attempt_count += 1
logger.info(
f"[私聊][{self.private_name}]尝试生成首次回复 (第 {reply_attempt_count}/{max_reply_attempts} 次)..."
)
self.state = ConversationState.GENERATING
# 1. 生成回复
self.generated_reply = await self.reply_generator.generate(
observation_info, conversation_info, action_type="direct_reply"
)
logger.info(
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次生成的首次回复: {self.generated_reply}"
)
# 2. 检查回复
self.state = ConversationState.CHECKING
try:
current_goal_str = conversation_info.goal_list[0]["goal"] if conversation_info.goal_list else ""
is_suitable, check_reason, need_replan = await self.reply_generator.check_reply(
reply=self.generated_reply,
goal=current_goal_str,
chat_history=observation_info.chat_history,
chat_history_str=observation_info.chat_history_str,
retry_count=reply_attempt_count - 1,
)
logger.info(
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次首次回复检查结果: 合适={is_suitable}, 原因='{check_reason}', 需重新规划={need_replan}"
)
if is_suitable:
final_reply_to_send = self.generated_reply
break
elif need_replan:
logger.warning(
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次首次回复检查建议重新规划,停止尝试。原因: {check_reason}"
)
break
except Exception as check_err:
logger.error(
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次调用 ReplyChecker (首次回复) 时出错: {check_err}"
)
check_reason = f"{reply_attempt_count} 次检查过程出错: {check_err}"
break
# 循环结束,处理最终结果
if is_suitable:
# 检查是否有新消息
if self._check_new_messages_after_planning():
logger.info(f"[私聊][{self.private_name}]生成首次回复期间收到新消息,取消发送,重新规划行动")
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"有新消息,取消发送首次回复: {final_reply_to_send}"}
)
return # 直接返回,重新规划
# 发送合适的回复
self.generated_reply = final_reply_to_send
# --- 在这里调用 _send_reply ---
await self._send_reply() # <--- 调用恢复后的函数
# 更新状态: 标记上次成功是 direct_reply
self.conversation_info.last_successful_reply_action = "direct_reply"
action_successful = True # 标记动作成功
elif need_replan:
# 打回动作决策
logger.warning(
f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,首次回复决定打回动作决策。打回原因: {check_reason}"
)
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"首次回复尝试{reply_attempt_count}次后打回: {check_reason}"}
)
else:
# 首次回复失败
logger.warning(
f"[私聊][{self.private_name}]经过 {reply_attempt_count} 次尝试,未能生成合适的首次回复。最终原因: {check_reason}"
)
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"首次回复尝试{reply_attempt_count}次后失败: {check_reason}"}
)
# 重置状态: 首次回复失败,下次还是用初始 prompt
self.conversation_info.last_successful_reply_action = None
# 执行 Wait 操作 (保持原有逻辑)
logger.info(f"[私聊][{self.private_name}]由于无法生成合适首次回复,执行 'wait' 操作...")
self.state = ConversationState.WAITING self.state = ConversationState.WAITING
# 直接调用 wait 方法
await self.waiter.wait(self.conversation_info) await self.waiter.wait(self.conversation_info)
# 可以选择添加一条新的 action 记录来表示这个 wait
wait_action_record = { wait_action_record = {
"action": "wait", "action": "wait",
"plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待", "plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待",
"status": "done", # wait 完成后可以认为是 done "status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S"), "time": datetime.datetime.now().strftime("%H:%M:%S"),
"final_reason": None, "final_reason": None,
} }
conversation_info.done_action.append(wait_action_record) conversation_info.done_action.append(wait_action_record)
elif action == "fetch_knowledge": elif action == "fetch_knowledge":
self.waiter.wait_accumulated_time = 0
self.state = ConversationState.FETCHING self.state = ConversationState.FETCHING
knowledge = "TODO:知识" knowledge_query = reason
topic = "TODO:关键词" try:
logger.info(f"假装获取到知识{knowledge},关键词是: {topic}") # 检查 knowledge_fetcher 是否存在
if knowledge: if not hasattr(self, "knowledge_fetcher"):
pass # 简单处理 logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。")
# 标记 action 为 done raise AttributeError("KnowledgeFetcher not initialized")
conversation_info.done_action[action_index].update(
{ knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history)
"status": "done", logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}")
"time": datetime.datetime.now().strftime("%H:%M:%S"), if knowledge:
} # 确保 knowledge_list 存在
) if not hasattr(conversation_info, "knowledge_list"):
conversation_info.knowledge_list = []
conversation_info.knowledge_list.append(
{"query": knowledge_query, "knowledge": knowledge, "source": source}
)
action_successful = True
except Exception as fetch_err:
logger.error(f"[私聊][{self.private_name}]获取知识时出错: {fetch_err}")
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"获取知识失败: {fetch_err}"}
)
self.conversation_info.last_successful_reply_action = None # 重置状态
elif action == "rethink_goal": elif action == "rethink_goal":
self.waiter.wait_accumulated_time = 0
self.state = ConversationState.RETHINKING self.state = ConversationState.RETHINKING
await self.goal_analyzer.analyze_goal(conversation_info, observation_info) try:
# 标记 action 为 done # 检查 goal_analyzer 是否存在
conversation_info.done_action[action_index].update( if not hasattr(self, "goal_analyzer"):
{ logger.error(f"[私聊][{self.private_name}]GoalAnalyzer 未初始化,无法重新思考目标。")
"status": "done", raise AttributeError("GoalAnalyzer not initialized")
"time": datetime.datetime.now().strftime("%H:%M:%S"), await self.goal_analyzer.analyze_goal(conversation_info, observation_info)
} action_successful = True
) except Exception as rethink_err:
logger.error(f"[私聊][{self.private_name}]重新思考目标时出错: {rethink_err}")
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"重新思考目标失败: {rethink_err}"}
)
self.conversation_info.last_successful_reply_action = None # 重置状态
elif action == "listening": elif action == "listening":
self.state = ConversationState.LISTENING self.state = ConversationState.LISTENING
logger.info("倾听对方发言...") logger.info(f"[私聊][{self.private_name}]倾听对方发言...")
await self.waiter.wait_listening(conversation_info) try:
# listening 和 wait 通常在完成后不需要标记为 done因为它们是持续状态 # 检查 waiter 是否存在
# 但如果需要记录,可以在 waiter 返回后标记。目前逻辑是 waiter 返回后主循环继续。 if not hasattr(self, "waiter"):
# 为了统一,可以暂时在这里也标记一下(或者都不标记) logger.error(f"[私聊][{self.private_name}]Waiter 未初始化,无法倾听。")
conversation_info.done_action[action_index].update( raise AttributeError("Waiter not initialized")
{ await self.waiter.wait_listening(conversation_info)
"status": "done", # 或 "completed" action_successful = True # Listening 完成就算成功
"time": datetime.datetime.now().strftime("%H:%M:%S"), except Exception as listen_err:
} logger.error(f"[私聊][{self.private_name}]倾听时出错: {listen_err}")
) conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"倾听失败: {listen_err}"}
)
self.conversation_info.last_successful_reply_action = None # 重置状态
elif action == "end_conversation": elif action == "end_conversation":
self.should_continue = False # 设置循环停止标志 self.should_continue = False
logger.info("决定结束对话...") logger.info(f"[私聊][{self.private_name}]决定结束对话...")
# 标记 action 为 done action_successful = True # 标记动作成功
elif action == "block_and_ignore":
logger.info(f"[私聊][{self.private_name}]不想再理你了...")
ignore_duration_seconds = 10 * 60
self.ignore_until_timestamp = time.time() + ignore_duration_seconds
logger.info(
f"[私聊][{self.private_name}]将忽略此对话直到: {datetime.datetime.fromtimestamp(self.ignore_until_timestamp)}"
)
self.state = ConversationState.IGNORED
action_successful = True # 标记动作成功
else: # 对应 'wait' 动作
self.state = ConversationState.WAITING
logger.info(f"[私聊][{self.private_name}]等待更多信息...")
try:
# 检查 waiter 是否存在
if not hasattr(self, "waiter"):
logger.error(f"[私聊][{self.private_name}]Waiter 未初始化,无法等待。")
raise AttributeError("Waiter not initialized")
_timeout_occurred = await self.waiter.wait(self.conversation_info)
action_successful = True # Wait 完成就算成功
except Exception as wait_err:
logger.error(f"[私聊][{self.private_name}]等待时出错: {wait_err}")
conversation_info.done_action[action_index].update(
{"status": "recall", "final_reason": f"等待失败: {wait_err}"}
)
self.conversation_info.last_successful_reply_action = None # 重置状态
# --- 更新 Action History 状态 ---
# 只有当动作本身成功时,才更新状态为 done
if action_successful:
conversation_info.done_action[action_index].update( conversation_info.done_action[action_index].update(
{ {
"status": "done", "status": "done",
"time": datetime.datetime.now().strftime("%H:%M:%S"), "time": datetime.datetime.now().strftime("%H:%M:%S"),
} }
) )
# 这里不需要 return主循环会在下一轮检查 should_continue # 重置状态: 对于非回复类动作的成功,清除上次回复状态
if action not in ["direct_reply", "send_new_message"]:
self.conversation_info.last_successful_reply_action = None
logger.debug(f"[私聊][{self.private_name}]动作 {action} 成功完成,重置 last_successful_reply_action")
# 如果动作是 recall 状态,在各自的处理逻辑中已经更新了 done_action
elif action == "block_and_ignore": async def _send_reply(self):
logger.info("不想再理你了...") """发送回复"""
# 1. 标记对话为暂时忽略 if not self.generated_reply:
ignore_duration_seconds = 10 * 60 # 10 分钟 logger.warning(f"[私聊][{self.private_name}]没有生成回复内容,无法发送。")
self.ignore_until_timestamp = time.time() + ignore_duration_seconds return
logger.info(f"将忽略此对话直到: {datetime.datetime.fromtimestamp(self.ignore_until_timestamp)}")
conversation_info.done_action[action_index].update(
{
"status": "done", # 或者一个自定义状态,比如 "ignored"
"final_reason": "Detected potential harassment, ignoring temporarily.", # 检测到潜在骚扰,暂时忽略
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
)
self.state = ConversationState.IGNORED
else: # 对应 'wait' 动作 try:
self.state = ConversationState.WAITING _current_time = time.time()
logger.info("等待更多信息...") reply_content = self.generated_reply
await self.waiter.wait(self.conversation_info)
# 同 listening可以考虑是否标记状态 # 发送消息 (确保 direct_sender 和 chat_stream 有效)
conversation_info.done_action[action_index].update( if not hasattr(self, "direct_sender") or not self.direct_sender:
{ logger.error(f"[私聊][{self.private_name}]DirectMessageSender 未初始化,无法发送回复。")
"status": "done", # 或 "completed" return
"time": datetime.datetime.now().strftime("%H:%M:%S"), if not self.chat_stream:
} logger.error(f"[私聊][{self.private_name}]ChatStream 未初始化,无法发送回复。")
) return
await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content)
# 发送成功后,手动触发 observer 更新可能导致重复处理自己发送的消息
# 更好的做法是依赖 observer 的自动轮询或数据库触发器(如果支持)
# 暂时注释掉,观察是否影响 ObservationInfo 的更新
# self.chat_observer.trigger_update()
# if not await self.chat_observer.wait_for_update():
# logger.warning(f"[私聊][{self.private_name}]等待 ChatObserver 更新完成超时")
self.state = ConversationState.ANALYZING # 更新状态
except Exception as e:
logger.error(f"[私聊][{self.private_name}]发送消息或更新状态时失败: {str(e)}")
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
self.state = ConversationState.ANALYZING
async def _send_timeout_message(self): async def _send_timeout_message(self):
"""发送超时结束消息""" """发送超时结束消息"""
@ -464,30 +657,4 @@ class Conversation:
chat_stream=self.chat_stream, content="TODO:超时消息", reply_to_message=latest_message chat_stream=self.chat_stream, content="TODO:超时消息", reply_to_message=latest_message
) )
except Exception as e: except Exception as e:
logger.error(f"发送超时消息失败: {str(e)}") logger.error(f"[私聊][{self.private_name}]发送超时消息失败: {str(e)}")
async def _send_reply(self):
"""发送回复"""
if not self.generated_reply:
logger.warning("没有生成回复")
return
try:
# 外层 try: 捕获发送消息和后续处理中的主要错误
_current_time = time.time() # 获取当前时间戳
reply_content = self.generated_reply # 获取要发送的内容
# 发送消息
await self.direct_sender.send_message(chat_stream=self.chat_stream, content=reply_content)
# 原有的触发更新和等待代码
self.chat_observer.trigger_update()
if not await self.chat_observer.wait_for_update():
logger.warning("等待 ChatObserver 更新完成超时")
self.state = ConversationState.ANALYZING # 更新对话状态
except Exception as e:
# 这是外层 try 对应的 except
logger.error(f"发送消息或更新状态时失败: {str(e)}")
self.state = ConversationState.ANALYZING # 出错也要尝试恢复状态

View File

@ -1,6 +1,10 @@
from typing import Optional
class ConversationInfo: class ConversationInfo:
def __init__(self): def __init__(self):
self.done_action = [] self.done_action = []
self.goal_list = [] self.goal_list = []
self.knowledge_list = [] self.knowledge_list = []
self.memory_list = [] self.memory_list = []
self.last_successful_reply_action: Optional[str] = None

View File

@ -1,10 +1,14 @@
import time
from typing import Optional from typing import Optional
from src.common.logger import get_module_logger from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream from ..chat.chat_stream import ChatStream
from ..chat.message import Message from ..chat.message import Message
from maim_message import Seg from maim_message import UserInfo, Seg
from src.plugins.chat.message import MessageSending, MessageSet from src.plugins.chat.message import MessageSending, MessageSet
from src.plugins.chat.message_sender import message_manager from src.plugins.chat.message_sender import message_manager
from ..storage.storage import MessageStorage
from ...config.config import global_config
logger = get_module_logger("message_sender") logger = get_module_logger("message_sender")
@ -12,8 +16,9 @@ logger = get_module_logger("message_sender")
class DirectMessageSender: class DirectMessageSender:
"""直接消息发送器""" """直接消息发送器"""
def __init__(self): def __init__(self, private_name: str):
pass self.private_name = private_name
self.storage = MessageStorage()
async def send_message( async def send_message(
self, self,
@ -30,21 +35,44 @@ class DirectMessageSender:
""" """
try: try:
# 创建消息内容 # 创建消息内容
segments = [Seg(type="text", data={"text": content})] segments = Seg(type="seglist", data=[Seg(type="text", data=content)])
# 检查是否需要引用回复 # 获取麦麦的信息
if reply_to_message: bot_user_info = UserInfo(
reply_id = reply_to_message.message_id user_id=global_config.BOT_QQ,
message_sending = MessageSending(segments=segments, reply_to_id=reply_id) user_nickname=global_config.BOT_NICKNAME,
else: platform=chat_stream.platform,
message_sending = MessageSending(segments=segments) )
# 用当前时间作为message_id和之前那套sender一样
message_id = f"dm{round(time.time(), 2)}"
# 构建消息对象
message = MessageSending(
message_id=message_id,
chat_stream=chat_stream,
bot_user_info=bot_user_info,
sender_info=reply_to_message.message_info.user_info if reply_to_message else None,
message_segment=segments,
reply=reply_to_message,
is_head=True,
is_emoji=False,
thinking_start_time=time.time(),
)
# 处理消息
await message.process()
# 不知道有什么用先留下来了和之前那套sender一样
_message_json = message.to_dict()
# 发送消息 # 发送消息
message_set = MessageSet(chat_stream, message_sending.message_id) message_set = MessageSet(chat_stream, message_id)
message_set.add_message(message_sending) message_set.add_message(message)
message_manager.add_message(message_set) await message_manager.add_message(message_set)
logger.info(f"PFC消息已发送: {content}") await self.storage.store_message(message, chat_stream)
logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
except Exception as e: except Exception as e:
logger.error(f"PFC消息发送失败: {str(e)}") logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")
raise raise

View File

@ -1,13 +1,12 @@
# Programmable Friendly Conversationalist
# Prefrontal cortex
from typing import List, Optional, Dict, Any, Set from typing import List, Optional, Dict, Any, Set
from maim_message import UserInfo from maim_message import UserInfo
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from src.common.logger import get_module_logger from src.common.logger import get_module_logger
from .chat_observer import ChatObserver from .chat_observer import ChatObserver
from .chat_states import NotificationHandler, NotificationType from .chat_states import NotificationHandler, NotificationType, Notification
from src.plugins.utils.chat_message_builder import build_readable_messages from src.plugins.utils.chat_message_builder import build_readable_messages
import traceback # 导入 traceback 用于调试
logger = get_module_logger("observation_info") logger = get_module_logger("observation_info")
@ -15,188 +14,287 @@ logger = get_module_logger("observation_info")
class ObservationInfoHandler(NotificationHandler): class ObservationInfoHandler(NotificationHandler):
"""ObservationInfo的通知处理器""" """ObservationInfo的通知处理器"""
def __init__(self, observation_info: "ObservationInfo"): def __init__(self, observation_info: "ObservationInfo", private_name: str):
"""初始化处理器 """初始化处理器
Args: Args:
observation_info: 要更新的ObservationInfo实例 observation_info: 要更新的ObservationInfo实例
private_name: 私聊对象的名称用于日志记录
""" """
self.observation_info = observation_info self.observation_info = observation_info
# 将 private_name 存储在 handler 实例中
self.private_name = private_name
async def handle_notification(self, notification): async def handle_notification(self, notification: Notification): # 添加类型提示
# 获取通知类型和数据 # 获取通知类型和数据
notification_type = notification.type notification_type = notification.type
data = notification.data data = notification.data
if notification_type == NotificationType.NEW_MESSAGE: try: # 添加错误处理块
# 处理新消息通知 if notification_type == NotificationType.NEW_MESSAGE:
logger.debug(f"收到新消息通知data: {data}") # 处理新消息通知
message_id = data.get("message_id") # logger.debug(f"[私聊][{self.private_name}]收到新消息通知data: {data}") # 可以在需要时取消注释
processed_plain_text = data.get("processed_plain_text") message_id = data.get("message_id")
detailed_plain_text = data.get("detailed_plain_text") processed_plain_text = data.get("processed_plain_text")
user_info = data.get("user_info") detailed_plain_text = data.get("detailed_plain_text")
time_value = data.get("time") user_info_dict = data.get("user_info") # 先获取字典
time_value = data.get("time")
message = { # 确保 user_info 是字典类型再创建 UserInfo 对象
"message_id": message_id, user_info = None
"processed_plain_text": processed_plain_text, if isinstance(user_info_dict, dict):
"detailed_plain_text": detailed_plain_text, try:
"user_info": user_info, user_info = UserInfo.from_dict(user_info_dict)
"time": time_value, except Exception as e:
} logger.error(
f"[私聊][{self.private_name}]从字典创建 UserInfo 时出错: {e}, 字典内容: {user_info_dict}"
)
# 可以选择在这里返回或记录错误,避免后续代码出错
return
elif user_info_dict is not None:
logger.warning(
f"[私聊][{self.private_name}]收到的 user_info 不是预期的字典类型: {type(user_info_dict)}"
)
# 根据需要处理非字典情况,这里暂时返回
return
self.observation_info.update_from_message(message) message = {
"message_id": message_id,
"processed_plain_text": processed_plain_text,
"detailed_plain_text": detailed_plain_text,
"user_info": user_info_dict, # 存储原始字典或 UserInfo 对象,取决于你的 update_from_message 如何处理
"time": time_value,
}
# 传递 UserInfo 对象(如果成功创建)或原始字典
await self.observation_info.update_from_message(message, user_info) # 修改:传递 user_info 对象
elif notification_type == NotificationType.COLD_CHAT: elif notification_type == NotificationType.COLD_CHAT:
# 处理冷场通知 # 处理冷场通知
is_cold = data.get("is_cold", False) is_cold = data.get("is_cold", False)
self.observation_info.update_cold_chat_status(is_cold, time.time()) await self.observation_info.update_cold_chat_status(is_cold, time.time()) # 修改:改为 await 调用
elif notification_type == NotificationType.ACTIVE_CHAT: elif notification_type == NotificationType.ACTIVE_CHAT:
# 处理活跃通知 # 处理活跃通知 (通常由 COLD_CHAT 的反向状态处理)
is_active = data.get("is_active", False) is_active = data.get("is_active", False)
self.observation_info.is_cold = not is_active self.observation_info.is_cold = not is_active
elif notification_type == NotificationType.BOT_SPEAKING: elif notification_type == NotificationType.BOT_SPEAKING:
# 处理机器人说话通知 # 处理机器人说话通知 (按需实现)
self.observation_info.is_typing = False self.observation_info.is_typing = False
self.observation_info.last_bot_speak_time = time.time() self.observation_info.last_bot_speak_time = time.time()
elif notification_type == NotificationType.USER_SPEAKING: elif notification_type == NotificationType.USER_SPEAKING:
# 处理用户说话通知 # 处理用户说话通知
self.observation_info.is_typing = False self.observation_info.is_typing = False
self.observation_info.last_user_speak_time = time.time() self.observation_info.last_user_speak_time = time.time()
elif notification_type == NotificationType.MESSAGE_DELETED: elif notification_type == NotificationType.MESSAGE_DELETED:
# 处理消息删除通知 # 处理消息删除通知
message_id = data.get("message_id") message_id = data.get("message_id")
self.observation_info.unprocessed_messages = [ # 从 unprocessed_messages 中移除被删除的消息
msg for msg in self.observation_info.unprocessed_messages if msg.get("message_id") != message_id original_count = len(self.observation_info.unprocessed_messages)
] self.observation_info.unprocessed_messages = [
msg for msg in self.observation_info.unprocessed_messages if msg.get("message_id") != message_id
]
if len(self.observation_info.unprocessed_messages) < original_count:
logger.info(f"[私聊][{self.private_name}]移除了未处理的消息 (ID: {message_id})")
elif notification_type == NotificationType.USER_JOINED: elif notification_type == NotificationType.USER_JOINED:
# 处理用户加入通知 # 处理用户加入通知 (如果适用私聊场景)
user_id = data.get("user_id") user_id = data.get("user_id")
if user_id: if user_id:
self.observation_info.active_users.add(user_id) self.observation_info.active_users.add(str(user_id)) # 确保是字符串
elif notification_type == NotificationType.USER_LEFT: elif notification_type == NotificationType.USER_LEFT:
# 处理用户离开通知 # 处理用户离开通知 (如果适用私聊场景)
user_id = data.get("user_id") user_id = data.get("user_id")
if user_id: if user_id:
self.observation_info.active_users.discard(user_id) self.observation_info.active_users.discard(str(user_id)) # 确保是字符串
elif notification_type == NotificationType.ERROR: elif notification_type == NotificationType.ERROR:
# 处理错误通知 # 处理错误通知
error_msg = data.get("error", "") error_msg = data.get("error", "未提供错误信息")
logger.error(f"收到错误通知: {error_msg}") logger.error(f"[私聊][{self.private_name}]收到错误通知: {error_msg}")
except Exception as e:
logger.error(f"[私聊][{self.private_name}]处理通知时发生错误: {e}")
logger.error(traceback.format_exc()) # 打印详细堆栈信息
@dataclass @dataclass
class ObservationInfo: class ObservationInfo:
"""决策信息类用于收集和管理来自chat_observer的通知信息""" """决策信息类用于收集和管理来自chat_observer的通知信息"""
# --- 修改:添加 private_name 字段 ---
private_name: str = field(init=True) # 让 dataclass 的 __init__ 接收 private_name
# data_list # data_list
chat_history: List[str] = field(default_factory=list) chat_history: List[Dict[str, Any]] = field(default_factory=list) # 修改:明确类型为 Dict
chat_history_str: str = "" chat_history_str: str = ""
unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list) unprocessed_messages: List[Dict[str, Any]] = field(default_factory=list) # 修改:明确类型为 Dict
active_users: Set[str] = field(default_factory=set) active_users: Set[str] = field(default_factory=set)
# data # data
last_bot_speak_time: Optional[float] = None last_bot_speak_time: Optional[float] = None
last_user_speak_time: Optional[float] = None last_user_speak_time: Optional[float] = None
last_message_time: Optional[float] = None last_message_time: Optional[float] = None
# 添加 last_message_id
last_message_id: Optional[str] = None
last_message_content: str = "" last_message_content: str = ""
last_message_sender: Optional[str] = None last_message_sender: Optional[str] = None
bot_id: Optional[str] = None bot_id: Optional[str] = None
chat_history_count: int = 0 chat_history_count: int = 0
new_messages_count: int = 0 new_messages_count: int = 0
cold_chat_duration: float = 0.0 cold_chat_start_time: Optional[float] = None # 用于计算冷场持续时间
cold_chat_duration: float = 0.0 # 缓存计算结果
# state # state
is_typing: bool = False is_typing: bool = False # 可能表示对方正在输入
has_unread_messages: bool = False # has_unread_messages: bool = False # 这个状态可以通过 new_messages_count > 0 判断
is_cold_chat: bool = False is_cold_chat: bool = False
changed: bool = False changed: bool = False # 用于标记状态是否有变化,以便外部模块决定是否重新规划
# #spec # #spec (暂时注释掉,如果不需要)
# meta_plan_trigger: bool = False # meta_plan_trigger: bool = False
# --- 修改:移除 __post_init__ 的参数 ---
def __post_init__(self): def __post_init__(self):
"""初始化后创建handler""" """初始化后创建handler并进行必要的设置"""
self.chat_observer = None self.chat_observer: Optional[ChatObserver] = None # 添加类型提示
self.handler = ObservationInfoHandler(self) self.handler = ObservationInfoHandler(self, self.private_name)
def bind_to_chat_observer(self, chat_observer: ChatObserver): def bind_to_chat_observer(self, chat_observer: ChatObserver):
"""绑定到指定的chat_observer """绑定到指定的chat_observer
Args: Args:
stream_id: 聊天流ID chat_observer: 要绑定的 ChatObserver 实例
""" """
if self.chat_observer:
logger.warning(f"[私聊][{self.private_name}]尝试重复绑定 ChatObserver")
return
self.chat_observer = chat_observer self.chat_observer = chat_observer
self.chat_observer.notification_manager.register_handler( try:
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler # 注册关心的通知类型
) self.chat_observer.notification_manager.register_handler(
self.chat_observer.notification_manager.register_handler( target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler )
) self.chat_observer.notification_manager.register_handler(
print("1919810------------------------绑定-----------------------------") target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler
)
# 可以根据需要注册更多通知类型
# self.chat_observer.notification_manager.register_handler(
# target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler
# )
logger.info(f"[私聊][{self.private_name}]成功绑定到 ChatObserver")
except Exception as e:
logger.error(f"[私聊][{self.private_name}]绑定到 ChatObserver 时出错: {e}")
self.chat_observer = None # 绑定失败,重置
def unbind_from_chat_observer(self): def unbind_from_chat_observer(self):
"""解除与chat_observer的绑定""" """解除与chat_observer的绑定"""
if self.chat_observer: if self.chat_observer and hasattr(self.chat_observer, "notification_manager"): # 增加检查
self.chat_observer.notification_manager.unregister_handler( try:
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler self.chat_observer.notification_manager.unregister_handler(
) target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
self.chat_observer.notification_manager.unregister_handler( )
target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler self.chat_observer.notification_manager.unregister_handler(
) target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler
self.chat_observer = None )
# 如果注册了其他类型,也要在这里注销
# self.chat_observer.notification_manager.unregister_handler(
# target="observation_info", notification_type=NotificationType.MESSAGE_DELETED, handler=self.handler
# )
logger.info(f"[私聊][{self.private_name}]成功从 ChatObserver 解绑")
except Exception as e:
logger.error(f"[私聊][{self.private_name}]从 ChatObserver 解绑时出错: {e}")
finally: # 确保 chat_observer 被重置
self.chat_observer = None
else:
logger.warning(f"[私聊][{self.private_name}]尝试解绑时 ChatObserver 不存在或无效")
def update_from_message(self, message: Dict[str, Any]): # 修改update_from_message 接收 UserInfo 对象
async def update_from_message(self, message: Dict[str, Any], user_info: Optional[UserInfo]):
"""从消息更新信息 """从消息更新信息
Args: Args:
message: 消息数据 message: 消息数据字典
user_info: 解析后的 UserInfo 对象 (可能为 None)
""" """
# print("1919810-----------------------------------------------------") message_time = message.get("time")
# logger.debug(f"更新信息from_message: {message}") message_id = message.get("message_id")
self.last_message_time = message["time"] processed_text = message.get("processed_plain_text", "")
self.last_message_id = message["message_id"]
self.last_message_content = message.get("processed_plain_text", "") # 只有在新消息到达时才更新 last_message 相关信息
if message_time and message_time > (self.last_message_time or 0):
self.last_message_time = message_time
self.last_message_id = message_id
self.last_message_content = processed_text
# 重置冷场计时器
self.is_cold_chat = False
self.cold_chat_start_time = None
self.cold_chat_duration = 0.0
user_info = UserInfo.from_dict(message.get("user_info", {})) if user_info:
self.last_message_sender = user_info.user_id sender_id = str(user_info.user_id) # 确保是字符串
self.last_message_sender = sender_id
# 更新发言时间
if sender_id == self.bot_id:
self.last_bot_speak_time = message_time
else:
self.last_user_speak_time = message_time
self.active_users.add(sender_id) # 用户发言则认为其活跃
else:
logger.warning(
f"[私聊][{self.private_name}]处理消息更新时缺少有效的 UserInfo 对象, message_id: {message_id}"
)
self.last_message_sender = None # 发送者未知
if user_info.user_id == self.bot_id: # 将原始消息字典添加到未处理列表
self.last_bot_speak_time = message["time"] self.unprocessed_messages.append(message)
self.new_messages_count = len(self.unprocessed_messages) # 直接用列表长度
# logger.debug(f"[私聊][{self.private_name}]消息更新: last_time={self.last_message_time}, new_count={self.new_messages_count}")
self.update_changed() # 标记状态已改变
else: else:
self.last_user_speak_time = message["time"] # 如果消息时间戳不是最新的,可能不需要处理,或者记录一个警告
self.active_users.add(user_info.user_id) pass
# logger.warning(f"[私聊][{self.private_name}]收到过时或无效时间戳的消息: ID={message_id}, time={message_time}")
self.new_messages_count += 1
self.unprocessed_messages.append(message)
self.update_changed()
def update_changed(self): def update_changed(self):
"""更新changed状态""" """标记状态已改变,并重置标记"""
# logger.debug(f"[私聊][{self.private_name}]状态标记为已改变 (changed=True)")
self.changed = True self.changed = True
def update_cold_chat_status(self, is_cold: bool, current_time: float): async def update_cold_chat_status(self, is_cold: bool, current_time: float):
"""更新冷场状态 """更新冷场状态
Args: Args:
is_cold: 是否冷场 is_cold: 是否处于冷场状态
current_time: 当前时间 current_time: 当前时间
""" """
self.is_cold_chat = is_cold if is_cold != self.is_cold_chat: # 仅在状态变化时更新
if is_cold and self.last_message_time: self.is_cold_chat = is_cold
self.cold_chat_duration = current_time - self.last_message_time if is_cold:
# 进入冷场状态
self.cold_chat_start_time = (
self.last_message_time or current_time
) # 从最后消息时间开始算,或从当前时间开始
logger.info(f"[私聊][{self.private_name}]进入冷场状态,开始时间: {self.cold_chat_start_time}")
else:
# 结束冷场状态
if self.cold_chat_start_time:
self.cold_chat_duration = current_time - self.cold_chat_start_time
logger.info(f"[私聊][{self.private_name}]结束冷场状态,持续时间: {self.cold_chat_duration:.2f}")
self.cold_chat_start_time = None # 重置开始时间
self.update_changed() # 状态变化,标记改变
# 即使状态没变,如果是冷场状态,也更新持续时间
if self.is_cold_chat and self.cold_chat_start_time:
self.cold_chat_duration = current_time - self.cold_chat_start_time
def get_active_duration(self) -> float: def get_active_duration(self) -> float:
"""获取当前活跃时长 """获取当前活跃时长 (距离最后一条消息的时间)
Returns: Returns:
float: 最后一条消息到现在的时长 float: 最后一条消息到现在的时长
@ -206,7 +304,7 @@ class ObservationInfo:
return time.time() - self.last_message_time return time.time() - self.last_message_time
def get_user_response_time(self) -> Optional[float]: def get_user_response_time(self) -> Optional[float]:
"""获取用户响应时间 """获取用户最后响应时间 (距离用户最后发言的时间)
Returns: Returns:
Optional[float]: 用户最后发言到现在的时长如果没有用户发言则返回None Optional[float]: 用户最后发言到现在的时长如果没有用户发言则返回None
@ -216,7 +314,7 @@ class ObservationInfo:
return time.time() - self.last_user_speak_time return time.time() - self.last_user_speak_time
def get_bot_response_time(self) -> Optional[float]: def get_bot_response_time(self) -> Optional[float]:
"""获取机器人响应时间 """获取机器人最后响应时间 (距离机器人最后发言的时间)
Returns: Returns:
Optional[float]: 机器人最后发言到现在的时长如果没有机器人发言则返回None Optional[float]: 机器人最后发言到现在的时长如果没有机器人发言则返回None
@ -226,19 +324,38 @@ class ObservationInfo:
return time.time() - self.last_bot_speak_time return time.time() - self.last_bot_speak_time
async def clear_unprocessed_messages(self): async def clear_unprocessed_messages(self):
"""清空未处理消息列表""" """将未处理消息移入历史记录,并更新相关状态"""
# 将未处理消息添加到历史记录中 if not self.unprocessed_messages:
for message in self.unprocessed_messages: return # 没有未处理消息,直接返回
self.chat_history.append(message)
self.chat_history_str = await build_readable_messages( # logger.debug(f"[私聊][{self.private_name}]处理 {len(self.unprocessed_messages)} 条未处理消息...")
self.chat_history[-20:] if len(self.chat_history) > 20 else self.chat_history, # 将未处理消息添加到历史记录中 (确保历史记录有长度限制,避免无限增长)
replace_bot_name=True, max_history_len = 100 # 示例最多保留100条历史记录
merge_messages=False, self.chat_history.extend(self.unprocessed_messages)
timestamp_mode="relative", if len(self.chat_history) > max_history_len:
read_mark=0.0, self.chat_history = self.chat_history[-max_history_len:]
)
# 清空未处理消息列表 # 更新历史记录字符串 (只使用最近一部分生成例如20条)
self.has_unread_messages = False history_slice_for_str = self.chat_history[-20:]
try:
self.chat_history_str = await build_readable_messages(
history_slice_for_str,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0, # read_mark 可能需要根据逻辑调整
)
except Exception as e:
logger.error(f"[私聊][{self.private_name}]构建聊天记录字符串时出错: {e}")
self.chat_history_str = "[构建聊天记录出错]" # 提供错误提示
# 清空未处理消息列表和计数
# cleared_count = len(self.unprocessed_messages)
self.unprocessed_messages.clear() self.unprocessed_messages.clear()
self.chat_history_count = len(self.chat_history)
self.new_messages_count = 0 self.new_messages_count = 0
# self.has_unread_messages = False # 这个状态可以通过 new_messages_count 判断
self.chat_history_count = len(self.chat_history) # 更新历史记录总数
# logger.debug(f"[私聊][{self.private_name}]已处理 {cleared_count} 条消息,当前历史记录 {self.chat_history_count} 条。")
self.update_changed() # 状态改变

View File

@ -1,24 +1,12 @@
# Programmable Friendly Conversationalist from typing import List, Tuple, TYPE_CHECKING
# Prefrontal cortex
import datetime
# import asyncio
from typing import List, Optional, Tuple, TYPE_CHECKING
from src.common.logger import get_module_logger from src.common.logger import get_module_logger
from ..chat.chat_stream import ChatStream
from maim_message import UserInfo, Seg
from ..chat.message import Message
from ..models.utils_model import LLMRequest from ..models.utils_model import LLMRequest
from ...config.config import global_config from ...config.config import global_config
from src.plugins.chat.message import MessageSending
from ..message.api import global_api
from ..storage.storage import MessageStorage
from .chat_observer import ChatObserver from .chat_observer import ChatObserver
from .pfc_utils import get_items_from_json from .pfc_utils import get_items_from_json
from src.individuality.individuality import Individuality from src.individuality.individuality import Individuality
from .conversation_info import ConversationInfo from .conversation_info import ConversationInfo
from .observation_info import ObservationInfo from .observation_info import ObservationInfo
import time
from src.plugins.utils.chat_message_builder import build_readable_messages from src.plugins.utils.chat_message_builder import build_readable_messages
if TYPE_CHECKING: if TYPE_CHECKING:
@ -30,16 +18,16 @@ logger = get_module_logger("pfc")
class GoalAnalyzer: class GoalAnalyzer:
"""对话目标分析器""" """对话目标分析器"""
def __init__(self, stream_id: str): def __init__(self, stream_id: str, private_name: str):
self.llm = LLMRequest( self.llm = LLMRequest(
model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal" model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal"
) )
self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3) self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME self.name = global_config.BOT_NICKNAME
self.nick_name = global_config.BOT_ALIAS_NAMES self.nick_name = global_config.BOT_ALIAS_NAMES
self.chat_observer = ChatObserver.get_instance(stream_id) self.private_name = private_name
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
# 多目标存储结构 # 多目标存储结构
self.goals = [] # 存储多个目标 self.goals = [] # 存储多个目标
@ -60,16 +48,10 @@ class GoalAnalyzer:
goals_str = "" goals_str = ""
if conversation_info.goal_list: if conversation_info.goal_list:
for goal_reason in conversation_info.goal_list: for goal_reason in conversation_info.goal_list:
# 处理字典或元组格式 if isinstance(goal_reason, dict):
if isinstance(goal_reason, tuple): goal = goal_reason.get("goal", "目标内容缺失")
# 假设元组的第一个元素是目标,第二个元素是原因
goal = goal_reason[0]
reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因"
elif isinstance(goal_reason, dict):
goal = goal_reason.get("goal")
reasoning = goal_reason.get("reasoning", "没有明确原因") reasoning = goal_reason.get("reasoning", "没有明确原因")
else: else:
# 如果是其他类型,尝试转为字符串
goal = str(goal_reason) goal = str(goal_reason)
reasoning = "没有明确原因" reasoning = "没有明确原因"
@ -81,7 +63,7 @@ class GoalAnalyzer:
goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n" goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
# 获取聊天历史记录 # 获取聊天历史记录
chat_history_text = observation_info.chat_history chat_history_text = observation_info.chat_history_str
if observation_info.new_messages_count > 0: if observation_info.new_messages_count > 0:
new_messages_list = observation_info.unprocessed_messages new_messages_list = observation_info.unprocessed_messages
@ -96,21 +78,7 @@ class GoalAnalyzer:
# await observation_info.clear_unprocessed_messages() # await observation_info.clear_unprocessed_messages()
identity_details_only = self.identity_detail_info persona_text = f"你的名字是{self.name}{self.personality_info}"
identity_addon = ""
if isinstance(identity_details_only, str):
pronouns = ["", "", ""]
for p in pronouns:
if identity_details_only.startswith(p):
identity_details_only = identity_details_only[len(p) :]
break
if identity_details_only.endswith(""):
identity_details_only = identity_details_only[:-1]
cleaned_details = identity_details_only.strip(", ")
if cleaned_details:
identity_addon = f"并且{cleaned_details}"
persona_text = f"你的名字是{self.name}{self.personality_info}{identity_addon}"
# 构建action历史文本 # 构建action历史文本
action_history_list = conversation_info.done_action action_history_list = conversation_info.done_action
action_history_text = "你之前做的事情是:" action_history_text = "你之前做的事情是:"
@ -140,27 +108,32 @@ class GoalAnalyzer:
输出格式示例 输出格式示例
[ [
{{ {{
"goal": "回答用户关于Python编程的具体问题", "goal": "回答用户关于Python编程的具体问题",
"reasoning": "用户提出了关于Python的技术问题需要专业且准确的解答" "reasoning": "用户提出了关于Python的技术问题需要专业且准确的解答"
}}, }},
{{ {{
"goal": "回答用户关于python安装的具体问题", "goal": "回答用户关于python安装的具体问题",
"reasoning": "用户提出了关于Python的技术问题需要专业且准确的解答" "reasoning": "用户提出了关于Python的技术问题需要专业且准确的解答"
}} }}
]""" ]"""
logger.debug(f"发送到LLM的提示词: {prompt}") logger.debug(f"[私聊][{self.private_name}]发送到LLM的提示词: {prompt}")
try: try:
content, _ = await self.llm.generate_response_async(prompt) content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"LLM原始返回内容: {content}") logger.debug(f"[私聊][{self.private_name}]LLM原始返回内容: {content}")
except Exception as e: except Exception as e:
logger.error(f"分析对话目标时出错: {str(e)}") logger.error(f"[私聊][{self.private_name}]分析对话目标时出错: {str(e)}")
content = "" content = ""
# 使用改进后的get_items_from_json函数处理JSON数组 # 使用改进后的get_items_from_json函数处理JSON数组
success, result = get_items_from_json( success, result = get_items_from_json(
content, "goal", "reasoning", required_types={"goal": str, "reasoning": str}, allow_array=True content,
self.private_name,
"goal",
"reasoning",
required_types={"goal": str, "reasoning": str},
allow_array=True,
) )
if success: if success:
@ -169,9 +142,7 @@ class GoalAnalyzer:
# 清空现有目标列表并添加新目标 # 清空现有目标列表并添加新目标
conversation_info.goal_list = [] conversation_info.goal_list = []
for item in result: for item in result:
goal = item.get("goal", "") conversation_info.goal_list.append(item)
reasoning = item.get("reasoning", "")
conversation_info.goal_list.append((goal, reasoning))
# 返回第一个目标作为当前主要目标(如果有) # 返回第一个目标作为当前主要目标(如果有)
if result: if result:
@ -179,9 +150,7 @@ class GoalAnalyzer:
return (first_goal.get("goal", ""), "", first_goal.get("reasoning", "")) return (first_goal.get("goal", ""), "", first_goal.get("reasoning", ""))
else: else:
# 单个目标的情况 # 单个目标的情况
goal = result.get("goal", "") conversation_info.goal_list.append(result)
reasoning = result.get("reasoning", "")
conversation_info.goal_list.append((goal, reasoning))
return (goal, "", reasoning) return (goal, "", reasoning)
# 如果解析失败,返回默认值 # 如果解析失败,返回默认值
@ -250,30 +219,15 @@ class GoalAnalyzer:
async def analyze_conversation(self, goal, reasoning): async def analyze_conversation(self, goal, reasoning):
messages = self.chat_observer.get_cached_messages() messages = self.chat_observer.get_cached_messages()
chat_history_text = "" chat_history_text = await build_readable_messages(
for msg in messages: messages,
time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S") replace_bot_name=True,
user_info = UserInfo.from_dict(msg.get("user_info", {})) merge_messages=False,
sender = user_info.user_nickname or f"用户{user_info.user_id}" timestamp_mode="relative",
if sender == self.name: read_mark=0.0,
sender = "你说" )
chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n"
identity_details_only = self.identity_detail_info persona_text = f"你的名字是{self.name}{self.personality_info}"
identity_addon = ""
if isinstance(identity_details_only, str):
pronouns = ["", "", ""]
for p in pronouns:
if identity_details_only.startswith(p):
identity_details_only = identity_details_only[len(p) :]
break
if identity_details_only.endswith(""):
identity_details_only = identity_details_only[:-1]
cleaned_details = identity_details_only.strip(", ")
if cleaned_details:
identity_addon = f"并且{cleaned_details}"
persona_text = f"你的名字是{self.name}{self.personality_info}{identity_addon}"
# ===> Persona 文本构建结束 <=== # ===> Persona 文本构建结束 <===
# --- 修改 Prompt 字符串,使用 persona_text --- # --- 修改 Prompt 字符串,使用 persona_text ---
@ -298,11 +252,12 @@ class GoalAnalyzer:
try: try:
content, _ = await self.llm.generate_response_async(prompt) content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"LLM原始返回内容: {content}") logger.debug(f"[私聊][{self.private_name}]LLM原始返回内容: {content}")
# 尝试解析JSON # 尝试解析JSON
success, result = get_items_from_json( success, result = get_items_from_json(
content, content,
self.private_name,
"goal_achieved", "goal_achieved",
"stop_conversation", "stop_conversation",
"reason", "reason",
@ -310,7 +265,7 @@ class GoalAnalyzer:
) )
if not success: if not success:
logger.error("无法解析对话分析结果JSON") logger.error(f"[私聊][{self.private_name}]无法解析对话分析结果JSON")
return False, False, "解析结果失败" return False, False, "解析结果失败"
goal_achieved = result["goal_achieved"] goal_achieved = result["goal_achieved"]
@ -320,65 +275,67 @@ class GoalAnalyzer:
return goal_achieved, stop_conversation, reason return goal_achieved, stop_conversation, reason
except Exception as e: except Exception as e:
logger.error(f"分析对话状态时出错: {str(e)}") logger.error(f"[私聊][{self.private_name}]分析对话状态时出错: {str(e)}")
return False, False, f"分析出错: {str(e)}" return False, False, f"分析出错: {str(e)}"
class DirectMessageSender: # 先注释掉,万一以后出问题了还能开回来(((
"""直接发送消息到平台的发送器""" # class DirectMessageSender:
# """直接发送消息到平台的发送器"""
def __init__(self): # def __init__(self, private_name: str):
self.logger = get_module_logger("direct_sender") # self.logger = get_module_logger("direct_sender")
self.storage = MessageStorage() # self.storage = MessageStorage()
# self.private_name = private_name
async def send_via_ws(self, message: MessageSending) -> None: # async def send_via_ws(self, message: MessageSending) -> None:
try: # try:
await global_api.send_message(message) # await global_api.send_message(message)
except Exception as e: # except Exception as e:
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置请检查配置文件") from e # raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置请检查配置文件") from e
async def send_message( # async def send_message(
self, # self,
chat_stream: ChatStream, # chat_stream: ChatStream,
content: str, # content: str,
reply_to_message: Optional[Message] = None, # reply_to_message: Optional[Message] = None,
) -> None: # ) -> None:
"""直接发送消息到平台 # """直接发送消息到平台
Args: # Args:
chat_stream: 聊天流 # chat_stream: 聊天流
content: 消息内容 # content: 消息内容
reply_to_message: 要回复的消息 # reply_to_message: 要回复的消息
""" # """
# 构建消息对象 # # 构建消息对象
message_segment = Seg(type="text", data=content) # message_segment = Seg(type="text", data=content)
bot_user_info = UserInfo( # bot_user_info = UserInfo(
user_id=global_config.BOT_QQ, # user_id=global_config.BOT_QQ,
user_nickname=global_config.BOT_NICKNAME, # user_nickname=global_config.BOT_NICKNAME,
platform=chat_stream.platform, # platform=chat_stream.platform,
) # )
message = MessageSending( # message = MessageSending(
message_id=f"dm{round(time.time(), 2)}", # message_id=f"dm{round(time.time(), 2)}",
chat_stream=chat_stream, # chat_stream=chat_stream,
bot_user_info=bot_user_info, # bot_user_info=bot_user_info,
sender_info=reply_to_message.message_info.user_info if reply_to_message else None, # sender_info=reply_to_message.message_info.user_info if reply_to_message else None,
message_segment=message_segment, # message_segment=message_segment,
reply=reply_to_message, # reply=reply_to_message,
is_head=True, # is_head=True,
is_emoji=False, # is_emoji=False,
thinking_start_time=time.time(), # thinking_start_time=time.time(),
) # )
# 处理消息 # # 处理消息
await message.process() # await message.process()
_message_json = message.to_dict() # _message_json = message.to_dict()
# 发送消息 # # 发送消息
try: # try:
await self.send_via_ws(message) # await self.send_via_ws(message)
await self.storage.store_message(message, chat_stream) # await self.storage.store_message(message, chat_stream)
logger.success(f"PFC消息已发送: {content}") # logger.success(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
except Exception as e: # except Exception as e:
logger.error(f"PFC消息发送失败: {str(e)}") # logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")

View File

@ -5,6 +5,7 @@ from ..models.utils_model import LLMRequest
from ...config.config import global_config from ...config.config import global_config
from ..chat.message import Message from ..chat.message import Message
from ..knowledge.knowledge_lib import qa_manager from ..knowledge.knowledge_lib import qa_manager
from ..utils.chat_message_builder import build_readable_messages
logger = get_module_logger("knowledge_fetcher") logger = get_module_logger("knowledge_fetcher")
@ -12,13 +13,14 @@ logger = get_module_logger("knowledge_fetcher")
class KnowledgeFetcher: class KnowledgeFetcher:
"""知识调取器""" """知识调取器"""
def __init__(self): def __init__(self, private_name: str):
self.llm = LLMRequest( self.llm = LLMRequest(
model=global_config.llm_normal, model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"], temperature=global_config.llm_normal["temp"],
max_tokens=1000, max_tokens=1000,
request_type="knowledge_fetch", request_type="knowledge_fetch",
) )
self.private_name = private_name
def _lpmm_get_knowledge(self, query: str) -> str: def _lpmm_get_knowledge(self, query: str) -> str:
"""获取相关知识 """获取相关知识
@ -30,13 +32,13 @@ class KnowledgeFetcher:
str: 构造好的,带相关度的知识 str: 构造好的,带相关度的知识
""" """
logger.debug("正在从LPMM知识库中获取知识") logger.debug(f"[私聊][{self.private_name}]正在从LPMM知识库中获取知识")
try: try:
knowledge_info = qa_manager.get_knowledge(query) knowledge_info = qa_manager.get_knowledge(query)
logger.debug(f"LPMM知识库查询结果: {knowledge_info:150}") logger.debug(f"[私聊][{self.private_name}]LPMM知识库查询结果: {knowledge_info:150}")
return knowledge_info return knowledge_info
except Exception as e: except Exception as e:
logger.error(f"LPMM知识库搜索工具执行失败: {str(e)}") logger.error(f"[私聊][{self.private_name}]LPMM知识库搜索工具执行失败: {str(e)}")
return "未找到匹配的知识" return "未找到匹配的知识"
async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]: async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]:
@ -50,10 +52,13 @@ class KnowledgeFetcher:
Tuple[str, str]: (获取的知识, 知识来源) Tuple[str, str]: (获取的知识, 知识来源)
""" """
# 构建查询上下文 # 构建查询上下文
chat_history_text = "" chat_history_text = await build_readable_messages(
for msg in chat_history: chat_history,
# sender = msg.message_info.user_info.user_nickname or f"用户{msg.message_info.user_info.user_id}" replace_bot_name=True,
chat_history_text += f"{msg.detailed_plain_text}\n" merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
)
# 从记忆中获取相关知识 # 从记忆中获取相关知识
related_memory = await HippocampusManager.get_instance().get_memory_from_text( related_memory = await HippocampusManager.get_instance().get_memory_from_text(

View File

@ -28,7 +28,7 @@ class PFCManager:
cls._instance = PFCManager() cls._instance = PFCManager()
return cls._instance return cls._instance
async def get_or_create_conversation(self, stream_id: str) -> Optional[Conversation]: async def get_or_create_conversation(self, stream_id: str, private_name: str) -> Optional[Conversation]:
"""获取或创建对话实例 """获取或创建对话实例
Args: Args:
@ -39,11 +39,11 @@ class PFCManager:
""" """
# 检查是否已经有实例 # 检查是否已经有实例
if stream_id in self._initializing and self._initializing[stream_id]: if stream_id in self._initializing and self._initializing[stream_id]:
logger.debug(f"会话实例正在初始化中: {stream_id}") logger.debug(f"[私聊][{private_name}]会话实例正在初始化中: {stream_id}")
return None return None
if stream_id in self._instances and self._instances[stream_id].should_continue: if stream_id in self._instances and self._instances[stream_id].should_continue:
logger.debug(f"使用现有会话实例: {stream_id}") logger.debug(f"[私聊][{private_name}]使用现有会话实例: {stream_id}")
return self._instances[stream_id] return self._instances[stream_id]
if stream_id in self._instances: if stream_id in self._instances:
instance = self._instances[stream_id] instance = self._instances[stream_id]
@ -52,28 +52,28 @@ class PFCManager:
and instance.ignore_until_timestamp and instance.ignore_until_timestamp
and time.time() < instance.ignore_until_timestamp and time.time() < instance.ignore_until_timestamp
): ):
logger.debug(f"会话实例当前处于忽略状态: {stream_id}") logger.debug(f"[私聊][{private_name}]会话实例当前处于忽略状态: {stream_id}")
# 返回 None 阻止交互。或者可以返回实例但标记它被忽略了喵? # 返回 None 阻止交互。或者可以返回实例但标记它被忽略了喵?
# 还是返回 None 吧喵。 # 还是返回 None 吧喵。
return None return None
# 检查 should_continue 状态 # 检查 should_continue 状态
if instance.should_continue: if instance.should_continue:
logger.debug(f"使用现有会话实例: {stream_id}") logger.debug(f"[私聊][{private_name}]使用现有会话实例: {stream_id}")
return instance return instance
# else: 实例存在但不应继续 # else: 实例存在但不应继续
try: try:
# 创建新实例 # 创建新实例
logger.info(f"创建新的对话实例: {stream_id}") logger.info(f"[私聊][{private_name}]创建新的对话实例: {stream_id}")
self._initializing[stream_id] = True self._initializing[stream_id] = True
# 创建实例 # 创建实例
conversation_instance = Conversation(stream_id) conversation_instance = Conversation(stream_id, private_name)
self._instances[stream_id] = conversation_instance self._instances[stream_id] = conversation_instance
# 启动实例初始化 # 启动实例初始化
await self._initialize_conversation(conversation_instance) await self._initialize_conversation(conversation_instance)
except Exception as e: except Exception as e:
logger.error(f"创建会话实例失败: {stream_id}, 错误: {e}") logger.error(f"[私聊][{private_name}]创建会话实例失败: {stream_id}, 错误: {e}")
return None return None
return conversation_instance return conversation_instance
@ -85,20 +85,21 @@ class PFCManager:
conversation: 要初始化的会话实例 conversation: 要初始化的会话实例
""" """
stream_id = conversation.stream_id stream_id = conversation.stream_id
private_name = conversation.private_name
try: try:
logger.info(f"开始初始化会话实例: {stream_id}") logger.info(f"[私聊][{private_name}]开始初始化会话实例: {stream_id}")
# 启动初始化流程 # 启动初始化流程
await conversation._initialize() await conversation._initialize()
# 标记初始化完成 # 标记初始化完成
self._initializing[stream_id] = False self._initializing[stream_id] = False
logger.info(f"会话实例 {stream_id} 初始化完成") logger.info(f"[私聊][{private_name}]会话实例 {stream_id} 初始化完成")
except Exception as e: except Exception as e:
logger.error(f"管理器初始化会话实例失败: {stream_id}, 错误: {e}") logger.error(f"[私聊][{private_name}]管理器初始化会话实例失败: {stream_id}, 错误: {e}")
logger.error(traceback.format_exc()) logger.error(f"[私聊][{private_name}]{traceback.format_exc()}")
# 清理失败的初始化 # 清理失败的初始化
async def get_conversation(self, stream_id: str) -> Optional[Conversation]: async def get_conversation(self, stream_id: str) -> Optional[Conversation]:

View File

@ -8,6 +8,7 @@ logger = get_module_logger("pfc_utils")
def get_items_from_json( def get_items_from_json(
content: str, content: str,
private_name: str,
*items: str, *items: str,
default_values: Optional[Dict[str, Any]] = None, default_values: Optional[Dict[str, Any]] = None,
required_types: Optional[Dict[str, type]] = None, required_types: Optional[Dict[str, type]] = None,
@ -78,9 +79,9 @@ def get_items_from_json(
if valid_items: if valid_items:
return True, valid_items return True, valid_items
except json.JSONDecodeError: except json.JSONDecodeError:
logger.debug("JSON数组解析失败尝试解析单个JSON对象") logger.debug(f"[私聊][{private_name}]JSON数组解析失败尝试解析单个JSON对象")
except Exception as e: except Exception as e:
logger.debug(f"尝试解析JSON数组时出错: {str(e)}") logger.debug(f"[私聊][{private_name}]尝试解析JSON数组时出错: {str(e)}")
# 尝试解析JSON对象 # 尝试解析JSON对象
try: try:
@ -93,10 +94,10 @@ def get_items_from_json(
try: try:
json_data = json.loads(json_match.group()) json_data = json.loads(json_match.group())
except json.JSONDecodeError: except json.JSONDecodeError:
logger.error("提取的JSON内容解析失败") logger.error(f"[私聊][{private_name}]提取的JSON内容解析失败")
return False, result return False, result
else: else:
logger.error("无法在返回内容中找到有效的JSON") logger.error(f"[私聊][{private_name}]无法在返回内容中找到有效的JSON")
return False, result return False, result
# 提取字段 # 提取字段
@ -106,20 +107,20 @@ def get_items_from_json(
# 验证必需字段 # 验证必需字段
if not all(item in result for item in items): if not all(item in result for item in items):
logger.error(f"JSON缺少必要字段实际内容: {json_data}") logger.error(f"[私聊][{private_name}]JSON缺少必要字段实际内容: {json_data}")
return False, result return False, result
# 验证字段类型 # 验证字段类型
if required_types: if required_types:
for field, expected_type in required_types.items(): for field, expected_type in required_types.items():
if field in result and not isinstance(result[field], expected_type): if field in result and not isinstance(result[field], expected_type):
logger.error(f"{field} 必须是 {expected_type.__name__} 类型") logger.error(f"[私聊][{private_name}]{field} 必须是 {expected_type.__name__} 类型")
return False, result return False, result
# 验证字符串字段不为空 # 验证字符串字段不为空
for field in items: for field in items:
if isinstance(result[field], str) and not result[field].strip(): if isinstance(result[field], str) and not result[field].strip():
logger.error(f"{field} 不能为空") logger.error(f"[私聊][{private_name}]{field} 不能为空")
return False, result return False, result
return True, result return True, result

View File

@ -12,12 +12,13 @@ logger = get_module_logger("reply_checker")
class ReplyChecker: class ReplyChecker:
"""回复检查器""" """回复检查器"""
def __init__(self, stream_id: str): def __init__(self, stream_id: str, private_name: str):
self.llm = LLMRequest( self.llm = LLMRequest(
model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check" model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check"
) )
self.name = global_config.BOT_NICKNAME self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id) self.private_name = private_name
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
self.max_retries = 3 # 最大重试次数 self.max_retries = 3 # 最大重试次数
async def check( async def check(
@ -49,68 +50,70 @@ class ReplyChecker:
# 可以用简单比较,或者更复杂的相似度库 (如 difflib) # 可以用简单比较,或者更复杂的相似度库 (如 difflib)
# 简单比较:是否完全相同 # 简单比较:是否完全相同
if reply == bot_messages[0]: # 和最近一条完全一样 if reply == bot_messages[0]: # 和最近一条完全一样
logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'") logger.warning(
f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'"
)
return ( return (
False, False,
"回复内容与你上一条发言完全相同,请修改,可以选择深入话题或寻找其它话题或等待", "被逻辑检查拒绝:回复内容与你上一条发言完全相同,可以选择深入话题或寻找其它话题或等待",
False, True,
) # 不合适,无需重新规划 ) # 不合适,需要返回至决策层
# 2. 相似度检查 (如果精确匹配未通过) # 2. 相似度检查 (如果精确匹配未通过)
import difflib # 导入 difflib 库 import difflib # 导入 difflib 库
# 计算编辑距离相似度ratio() 返回 0 到 1 之间的浮点数 # 计算编辑距离相似度ratio() 返回 0 到 1 之间的浮点数
similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio() similarity_ratio = difflib.SequenceMatcher(None, reply, bot_messages[0]).ratio()
logger.debug(f"ReplyChecker - 相似度: {similarity_ratio:.2f}") logger.debug(f"[私聊][{self.private_name}]ReplyChecker - 相似度: {similarity_ratio:.2f}")
# 设置一个相似度阈值 # 设置一个相似度阈值
similarity_threshold = 0.9 similarity_threshold = 0.9
if similarity_ratio > similarity_threshold: if similarity_ratio > similarity_threshold:
logger.warning( logger.warning(
f"ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'" f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'"
) )
return ( return (
False, False,
f"拒绝发送:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),请修改,可以选择深入话题或寻找其它话题或等待。", f"被逻辑检查拒绝:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),可以选择深入话题或寻找其它话题或等待。",
False, True,
) )
except Exception as e: except Exception as e:
import traceback import traceback
logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}") logger.error(f"[私聊][{self.private_name}]检查回复时出错: 类型={type(e)}, 值={e}")
logger.error(traceback.format_exc()) # 打印详细的回溯信息 logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") # 打印详细的回溯信息
prompt = f"""请检查以下回复或消息是否合适: prompt = f"""你是一个聊天逻辑检查器,请检查以下回复或消息是否合适:
当前对话目标{goal} 当前对话目标{goal}
最新的对话记录 最新的对话记录
{chat_history_text} {chat_history_text}
待检查的回复 待检查的消息
{reply} {reply}
请结合聊天记录检查以下几点 请结合聊天记录检查以下几点
1. 回复是否依然符合当前对话目标和实现方式 1. 这条消息是否依然符合当前对话目标和实现方式
2. 回复是否与最新的对话记录保持一致性 2. 这条消息是否与最新的对话记录保持一致性
3. 回复是否重复发言或重复表达同质内容尤其是只是换一种方式表达了相同的含义 3. 是否存在重复发言或重复表达同质内容尤其是只是换一种方式表达了相同的含义
4. 回复是否包含违规内容例如血腥暴力政治敏感等 4. 这条消息是否包含违规内容例如血腥暴力政治敏感等
5. 回复是否以你的角度发言,不要把""说的话当做对方说的话这是你自己说的话不要自己回复自己的消息 5. 这条消息是否以发送者的角度发言不要让发送者自己回复自己的消息
6. 回复是否通俗易懂 6. 这条消息是否通俗易懂
7. 回复是否有些多余例如在对方没有回复的情况下依然连续多次消息轰炸尤其是已经连续发送3条信息的情况这很可能不合理需要着重判断 7. 这条消息是否有些多余例如在对方没有回复的情况下依然连续多次消息轰炸尤其是已经连续发送3条信息的情况这很可能不合理需要着重判断
8. 回复是否使用了完全没必要的修辞 8. 这条消息是否使用了完全没必要的修辞
9. 回复是否逻辑通顺 9. 这条消息是否逻辑通顺
10. 回复是否太过冗长了通常私聊的每条消息长度在20字以内除非特殊情况 10. 这条消息是否太过冗长了通常私聊的每条消息长度在20字以内除非特殊情况
11. 在连续多次发送消息的情况下当前回复是否衔接自然会不会显得奇怪例如连续两条消息中部分内容重叠 11. 在连续多次发送消息的情况下这条消息是否衔接自然会不会显得奇怪例如连续两条消息中部分内容重叠
请以JSON格式输出包含以下字段 请以JSON格式输出包含以下字段
1. suitable: 是否合适 (true/false) 1. suitable: 是否合适 (true/false)
2. reason: 原因说明 2. reason: 原因说明
3. need_replan: 是否需要重新规划对话目标 (true/false)当发现当前对话目标不再适合时设为true 3. need_replan: 是否需要重新决策 (true/false)当你认为此时已经不适合发消息需要规划其它行动时设为true
输出格式示例 输出格式示例
{{ {{
"suitable": true, "suitable": true,
"reason": "回复符合要求,内容得体", "reason": "回复符合要求,虽然有可能略微偏离目标,但是整体内容流畅得体",
"need_replan": false "need_replan": false
}} }}
@ -118,7 +121,7 @@ class ReplyChecker:
try: try:
content, _ = await self.llm.generate_response_async(prompt) content, _ = await self.llm.generate_response_async(prompt)
logger.debug(f"检查回复的原始返回: {content}") logger.debug(f"[私聊][{self.private_name}]检查回复的原始返回: {content}")
# 清理内容尝试提取JSON部分 # 清理内容尝试提取JSON部分
content = content.strip() content = content.strip()
@ -171,7 +174,7 @@ class ReplyChecker:
return suitable, reason, need_replan return suitable, reason, need_replan
except Exception as e: except Exception as e:
logger.error(f"检查回复时出错: {e}") logger.error(f"[私聊][{self.private_name}]检查回复时出错: {e}")
# 如果出错且已达到最大重试次数,建议重新规划 # 如果出错且已达到最大重试次数,建议重新规划
if retry_count >= self.max_retries: if retry_count >= self.max_retries:
return False, "多次检查失败,建议重新规划", True return False, "多次检查失败,建议重新规划", True

View File

@ -11,130 +11,17 @@ from src.plugins.utils.chat_message_builder import build_readable_messages
logger = get_module_logger("reply_generator") logger = get_module_logger("reply_generator")
# --- 定义 Prompt 模板 ---
class ReplyGenerator: # Prompt for direct_reply (首次回复)
"""回复生成器""" PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊请根据以下信息生成一条回复
def __init__(self, stream_id: str):
self.llm = LLMRequest(
model=global_config.llm_PFC_chat,
temperature=global_config.llm_PFC_chat["temp"],
max_tokens=300,
request_type="reply_generation",
)
self.personality_info = Individuality.get_instance().get_prompt(type="personality", x_person=2, level=3)
self.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
self.name = global_config.BOT_NICKNAME
self.chat_observer = ChatObserver.get_instance(stream_id)
self.reply_checker = ReplyChecker(stream_id)
async def generate(self, observation_info: ObservationInfo, conversation_info: ConversationInfo) -> str:
"""生成回复
Args:
goal: 对话目标
chat_history: 聊天历史
knowledge_cache: 知识缓存
previous_reply: 上一次生成的回复如果有
retry_count: 当前重试次数
Returns:
str: 生成的回复
"""
# 构建提示词
logger.debug(f"开始生成回复:当前目标: {conversation_info.goal_list}")
# 构建对话目标
goals_str = ""
if conversation_info.goal_list:
for goal_reason in conversation_info.goal_list:
# 处理字典或元组格式
if isinstance(goal_reason, tuple):
# 假设元组的第一个元素是目标,第二个元素是原因
goal = goal_reason[0]
reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因"
elif isinstance(goal_reason, dict):
goal = goal_reason.get("goal")
reasoning = goal_reason.get("reasoning", "没有明确原因")
else:
# 如果是其他类型,尝试转为字符串
goal = str(goal_reason)
reasoning = "没有明确原因"
goal_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
goals_str += goal_str
else:
goal = "目前没有明确对话目标"
reasoning = "目前没有明确对话目标,最好思考一个对话目标"
goals_str = f"目标:{goal},产生该对话目标的原因:{reasoning}\n"
# 获取聊天历史记录
chat_history_text = observation_info.chat_history_str
if observation_info.new_messages_count > 0:
new_messages_list = observation_info.unprocessed_messages
new_messages_str = await build_readable_messages(
new_messages_list,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
)
chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
# await observation_info.clear_unprocessed_messages()
identity_details_only = self.identity_detail_info
identity_addon = ""
if isinstance(identity_details_only, str):
pronouns = ["", "", ""]
for p in pronouns:
if identity_details_only.startswith(p):
identity_details_only = identity_details_only[len(p) :]
break
if identity_details_only.endswith(""):
identity_details_only = identity_details_only[:-1]
cleaned_details = identity_details_only.strip(", ")
if cleaned_details:
identity_addon = f"并且{cleaned_details}"
persona_text = f"你的名字是{self.name}{self.personality_info}{identity_addon}"
# 构建action历史文本
action_history_list = (
conversation_info.done_action[-10:]
if len(conversation_info.done_action) >= 10
else conversation_info.done_action
)
action_history_text = "你之前做的事情是:"
for action in action_history_list:
if isinstance(action, dict):
action_type = action.get("action")
action_reason = action.get("reason")
action_status = action.get("status")
if action_status == "recall":
action_history_text += (
f"原本打算:{action_type},但是因为有新消息,你发现这个行动不合适,所以你没做\n"
)
elif action_status == "done":
action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n"
elif isinstance(action, tuple):
# 假设元组的格式是(action_type, action_reason, action_status)
action_type = action[0] if len(action) > 0 else "未知行动"
action_reason = action[1] if len(action) > 1 else "未知原因"
action_status = action[2] if len(action) > 2 else "done"
if action_status == "recall":
action_history_text += (
f"原本打算:{action_type},但是因为有新消息,你发现这个行动不合适,所以你没做\n"
)
elif action_status == "done":
action_history_text += f"你之前做了:{action_type},原因:{action_reason}\n"
prompt = f"""{persona_text}。现在你在参与一场QQ私聊请根据以下信息生成一条新消息
当前对话目标{goals_str} 当前对话目标{goals_str}
最近的聊天记录 最近的聊天记录
{chat_history_text} {chat_history_text}
请根据上述信息结合聊天记录发一条消息可以是回复补充深入话题或追问等等该消息应该 请根据上述信息结合聊天记录回复对方该回复应该
1. 符合对话目标""的角度发言不要自己与自己对话 1. 符合对话目标""的角度发言不要自己与自己对话
2. 符合你的性格特征和身份细节 2. 符合你的性格特征和身份细节
3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况 3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况
@ -145,41 +32,135 @@ class ReplyGenerator:
可以回复得自然随意自然一些就像真人一样注意把握聊天内容整体风格可以平和简短不要刻意突出自身学科背景不要说你说过的话可以简短多简短都可以但是避免冗长 可以回复得自然随意自然一些就像真人一样注意把握聊天内容整体风格可以平和简短不要刻意突出自身学科背景不要说你说过的话可以简短多简短都可以但是避免冗长
请你注意不要输出多余内容(包括前后缀冒号和引号括号表情等)只输出回复内容 请你注意不要输出多余内容(包括前后缀冒号和引号括号表情等)只输出回复内容
不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ) 不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )
**注意如果聊天记录中最新的消息是你自己发送的那么你的思路不应该是回复而是应该紧紧衔接你发送的消息进行话题的深入补充或追问等等避免与最新消息内容重叠**
请直接输出回复内容不需要任何额外格式""" 请直接输出回复内容不需要任何额外格式"""
# Prompt for send_new_message (追问/补充)
PROMPT_SEND_NEW_MESSAGE = """{persona_text}。现在你在参与一场QQ私聊**刚刚你已经发送了一条或多条消息**,现在请根据以下信息再发一条新消息:
当前对话目标{goals_str}
最近的聊天记录
{chat_history_text}
请根据上述信息结合聊天记录继续发一条新消息例如对之前消息的补充深入话题或追问等等该消息应该
1. 符合对话目标""的角度发言不要自己与自己对话
2. 符合你的性格特征和身份细节
3. 通俗易懂自然流畅像正常聊天一样简短通常20字以内除非特殊情况
4. 适当利用相关知识但不要生硬引用
5. 跟之前你发的消息自然的衔接逻辑合理且没有重复表达同质内容或部分重叠内容
请注意把握聊天内容不用太有条理可以有个性请分清""和对方说的话不要把""说的话当做对方说的话这是你自己说的话
这条消息可以自然随意自然一些就像真人一样注意把握聊天内容整体风格可以平和简短不要刻意突出自身学科背景不要说你说过的话可以简短多简短都可以但是避免冗长
请你注意不要输出多余内容(包括前后缀冒号和引号括号表情等)只输出消息内容
不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 )
请直接输出回复内容不需要任何额外格式"""
class ReplyGenerator:
"""回复生成器"""
def __init__(self, stream_id: str, private_name: str):
self.llm = LLMRequest(
model=global_config.llm_PFC_chat,
temperature=global_config.llm_PFC_chat["temp"],
max_tokens=300,
request_type="reply_generation",
)
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
self.name = global_config.BOT_NICKNAME
self.private_name = private_name
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
self.reply_checker = ReplyChecker(stream_id, private_name)
# 修改 generate 方法签名,增加 action_type 参数
async def generate(
self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str
) -> str:
"""生成回复
Args:
observation_info: 观察信息
conversation_info: 对话信息
action_type: 当前执行的动作类型 ('direct_reply' 'send_new_message')
Returns:
str: 生成的回复
"""
# 构建提示词
logger.debug(
f"[私聊][{self.private_name}]开始生成回复 (动作类型: {action_type}):当前目标: {conversation_info.goal_list}"
)
# --- 构建通用 Prompt 参数 ---
# (这部分逻辑基本不变)
# 构建对话目标 (goals_str)
goals_str = ""
if conversation_info.goal_list:
for goal_reason in conversation_info.goal_list:
if isinstance(goal_reason, dict):
goal = goal_reason.get("goal", "目标内容缺失")
reasoning = goal_reason.get("reasoning", "没有明确原因")
else:
goal = str(goal_reason)
reasoning = "没有明确原因"
goal = str(goal) if goal is not None else "目标内容缺失"
reasoning = str(reasoning) if reasoning is not None else "没有明确原因"
goals_str += f"- 目标:{goal}\n 原因:{reasoning}\n"
else:
goals_str = "- 目前没有明确对话目标\n" # 简化无目标情况
# 获取聊天历史记录 (chat_history_text)
chat_history_text = observation_info.chat_history_str
if observation_info.new_messages_count > 0 and observation_info.unprocessed_messages:
new_messages_list = observation_info.unprocessed_messages
new_messages_str = await build_readable_messages(
new_messages_list,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
)
chat_history_text += f"\n--- 以下是 {observation_info.new_messages_count} 条新消息 ---\n{new_messages_str}"
elif not chat_history_text:
chat_history_text = "还没有聊天记录。"
# 构建 Persona 文本 (persona_text)
persona_text = f"你的名字是{self.name}{self.personality_info}"
# --- 选择 Prompt ---
if action_type == "send_new_message":
prompt_template = PROMPT_SEND_NEW_MESSAGE
logger.info(f"[私聊][{self.private_name}]使用 PROMPT_SEND_NEW_MESSAGE (追问生成)")
else: # 默认使用 direct_reply 的 prompt
prompt_template = PROMPT_DIRECT_REPLY
logger.info(f"[私聊][{self.private_name}]使用 PROMPT_DIRECT_REPLY (首次/非连续回复生成)")
# --- 格式化最终的 Prompt ---
prompt = prompt_template.format(
persona_text=persona_text, goals_str=goals_str, chat_history_text=chat_history_text
)
# --- 调用 LLM 生成 ---
logger.debug(f"[私聊][{self.private_name}]发送到LLM的生成提示词:\n------\n{prompt}\n------")
try: try:
content, _ = await self.llm.generate_response_async(prompt) content, _ = await self.llm.generate_response_async(prompt)
logger.info(f"生成的回复: {content}") logger.debug(f"[私聊][{self.private_name}]生成的回复: {content}")
# is_new = self.chat_observer.check() # 移除旧的检查新消息逻辑,这应该由 conversation 控制流处理
# logger.debug(f"再看一眼聊天记录,{'有' if is_new else '没有'}新消息")
# 如果有新消息,重新生成回复
# if is_new:
# logger.info("检测到新消息,重新生成回复")
# return await self.generate(
# goal, chat_history, knowledge_cache,
# None, retry_count
# )
return content return content
except Exception as e: except Exception as e:
logger.error(f"生成回复时出错: {e}") logger.error(f"[私聊][{self.private_name}]生成回复时出错: {e}")
return "抱歉,我现在有点混乱,让我重新思考一下..." return "抱歉,我现在有点混乱,让我重新思考一下..."
# check_reply 方法保持不变
async def check_reply( async def check_reply(
self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0 self, reply: str, goal: str, chat_history: List[Dict[str, Any]], chat_history_str: str, retry_count: int = 0
) -> Tuple[bool, str, bool]: ) -> Tuple[bool, str, bool]:
"""检查回复是否合适 """检查回复是否合适
(此方法逻辑保持不变)
Args:
reply: 生成的回复
goal: 对话目标
retry_count: 当前重试次数
Returns:
Tuple[bool, str, bool]: (是否合适, 原因, 是否需要重新规划)
""" """
return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count) return await self.reply_checker.check(reply, goal, chat_history, chat_history_str, retry_count)

View File

@ -17,60 +17,63 @@ DESIRED_TIMEOUT_SECONDS = 300
class Waiter: class Waiter:
"""等待处理类""" """等待处理类"""
def __init__(self, stream_id: str): def __init__(self, stream_id: str, private_name: str):
self.chat_observer = ChatObserver.get_instance(stream_id) self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
self.name = global_config.BOT_NICKNAME self.name = global_config.BOT_NICKNAME
self.private_name = private_name
# self.wait_accumulated_time = 0 # 不再需要累加计时 # self.wait_accumulated_time = 0 # 不再需要累加计时
async def wait(self, conversation_info: ConversationInfo) -> bool: async def wait(self, conversation_info: ConversationInfo) -> bool:
"""等待用户新消息或超时""" """等待用户新消息或超时"""
wait_start_time = time.time() wait_start_time = time.time()
logger.info(f"进入常规等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...") logger.info(f"[私聊][{self.private_name}]进入常规等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
while True: while True:
# 检查是否有新消息 # 检查是否有新消息
if self.chat_observer.new_message_after(wait_start_time): if self.chat_observer.new_message_after(wait_start_time):
logger.info("等待结束,收到新消息") logger.info(f"[私聊][{self.private_name}]等待结束,收到新消息")
return False # 返回 False 表示不是超时 return False # 返回 False 表示不是超时
# 检查是否超时 # 检查是否超时
elapsed_time = time.time() - wait_start_time elapsed_time = time.time() - wait_start_time
if elapsed_time > DESIRED_TIMEOUT_SECONDS: if elapsed_time > DESIRED_TIMEOUT_SECONDS:
logger.info(f"等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。") logger.info(f"[私聊][{self.private_name}]等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
wait_goal = { wait_goal = {
"goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么", "goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么",
"reason": "对方很久没有回复你的消息了", "reasoning": "对方很久没有回复你的消息了",
} }
conversation_info.goal_list.append(wait_goal) conversation_info.goal_list.append(wait_goal)
logger.info(f"添加目标: {wait_goal}") logger.info(f"[私聊][{self.private_name}]添加目标: {wait_goal}")
return True # 返回 True 表示超时 return True # 返回 True 表示超时
await asyncio.sleep(5) # 每 5 秒检查一次 await asyncio.sleep(5) # 每 5 秒检查一次
logger.info("等待中...") # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出 logger.debug(
f"[私聊][{self.private_name}]等待中..."
) # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出
async def wait_listening(self, conversation_info: ConversationInfo) -> bool: async def wait_listening(self, conversation_info: ConversationInfo) -> bool:
"""倾听用户发言或超时""" """倾听用户发言或超时"""
wait_start_time = time.time() wait_start_time = time.time()
logger.info(f"进入倾听等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...") logger.info(f"[私聊][{self.private_name}]进入倾听等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
while True: while True:
# 检查是否有新消息 # 检查是否有新消息
if self.chat_observer.new_message_after(wait_start_time): if self.chat_observer.new_message_after(wait_start_time):
logger.info("倾听等待结束,收到新消息") logger.info(f"[私聊][{self.private_name}]倾听等待结束,收到新消息")
return False # 返回 False 表示不是超时 return False # 返回 False 表示不是超时
# 检查是否超时 # 检查是否超时
elapsed_time = time.time() - wait_start_time elapsed_time = time.time() - wait_start_time
if elapsed_time > DESIRED_TIMEOUT_SECONDS: if elapsed_time > DESIRED_TIMEOUT_SECONDS:
logger.info(f"倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。") logger.info(f"[私聊][{self.private_name}]倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
wait_goal = { wait_goal = {
# 保持 goal 文本一致 # 保持 goal 文本一致
"goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么", "goal": f"你等待了{elapsed_time / 60:.1f}分钟,对方似乎话说一半突然消失了,可能忙去了?也可能忘记了回复?要问问吗?还是结束对话?或继续等待?思考接下来要做什么",
"reason": "对方话说一半消失了,很久没有回复", "reasoning": "对方话说一半消失了,很久没有回复",
} }
conversation_info.goal_list.append(wait_goal) conversation_info.goal_list.append(wait_goal)
logger.info(f"添加目标: {wait_goal}") logger.info(f"[私聊][{self.private_name}]添加目标: {wait_goal}")
return True # 返回 True 表示超时 return True # 返回 True 表示超时
await asyncio.sleep(5) # 每 5 秒检查一次 await asyncio.sleep(5) # 每 5 秒检查一次
logger.info("倾听等待中...") # 同上,可以考虑注释掉 logger.debug(f"[私聊][{self.private_name}]倾听等待中...") # 同上,可以考虑注释掉

View File

@ -38,15 +38,17 @@ class ChatBot:
async def _create_pfc_chat(self, message: MessageRecv): async def _create_pfc_chat(self, message: MessageRecv):
try: try:
chat_id = str(message.chat_stream.stream_id) chat_id = str(message.chat_stream.stream_id)
private_name = str(message.message_info.user_info.user_nickname)
if global_config.enable_pfc_chatting: if global_config.enable_pfc_chatting:
await self.pfc_manager.get_or_create_conversation(chat_id) await self.pfc_manager.get_or_create_conversation(chat_id, private_name)
except Exception as e: except Exception as e:
logger.error(f"创建PFC聊天失败: {e}") logger.error(f"创建PFC聊天失败: {e}")
async def message_process(self, message_data: str) -> None: async def message_process(self, message_data: str) -> None:
"""处理转化后的统一格式消息 """处理转化后的统一格式消息
这个函数本质是预处理一些数据根据配置信息和消息内容预处理消息并分发到合适的消息处理器中
heart_flow模式使用思维流系统进行回复 heart_flow模式使用思维流系统进行回复
- 包含思维流状态管理 - 包含思维流状态管理
- 在回复前进行观察和状态更新 - 在回复前进行观察和状态更新
@ -74,14 +76,17 @@ class ChatBot:
groupinfo = message.message_info.group_info groupinfo = message.message_info.group_info
userinfo = message.message_info.user_info userinfo = message.message_info.user_info
# 用户黑名单拦截
if userinfo.user_id in global_config.ban_user_id: if userinfo.user_id in global_config.ban_user_id:
logger.debug(f"用户{userinfo.user_id}被禁止回复") logger.debug(f"用户{userinfo.user_id}被禁止回复")
return return
# 群聊黑名单拦截
if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups: if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups:
logger.trace(f"{groupinfo.group_id}被禁止回复") logger.trace(f"{groupinfo.group_id}被禁止回复")
return return
# 确认从接口发来的message是否有自定义的prompt模板信息
if message.message_info.template_info and not message.message_info.template_info.template_default: if message.message_info.template_info and not message.message_info.template_info.template_default:
template_group_name = message.message_info.template_info.template_name template_group_name = message.message_info.template_info.template_name
template_items = message.message_info.template_info.template_items template_items = message.message_info.template_info.template_items
@ -94,8 +99,11 @@ class ChatBot:
template_group_name = None template_group_name = None
async def preprocess(): async def preprocess():
# 如果在私聊中
if groupinfo is None: if groupinfo is None:
# 是否在配置信息中开启私聊模式
if global_config.enable_friend_chat: if global_config.enable_friend_chat:
# 是否进入PFC
if global_config.enable_pfc_chatting: if global_config.enable_pfc_chatting:
userinfo = message.message_info.user_info userinfo = message.message_info.user_info
messageinfo = message.message_info messageinfo = message.message_info
@ -108,8 +116,10 @@ class ChatBot:
message.update_chat_stream(chat) message.update_chat_stream(chat)
await self.only_process_chat.process_message(message) await self.only_process_chat.process_message(message)
await self._create_pfc_chat(message) await self._create_pfc_chat(message)
# 禁止PFC进入普通的心流消息处理逻辑
else: else:
await self.heartflow_processor.process_message(message_data) await self.heartflow_processor.process_message(message_data)
# 群聊默认进入心流消息处理逻辑
else: else:
await self.heartflow_processor.process_message(message_data) await self.heartflow_processor.process_message(message_data)

View File

@ -1,7 +1,6 @@
import asyncio import asyncio
import time import time
import traceback import traceback
import random # <-- 添加导入
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
@ -14,17 +13,20 @@ from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config from src.config.config import global_config
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.utils.timer_calculator import Timer # <--- Import Timer from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.plugins.heartFC_chat.heartFC_generator import HeartFCGenerator
from src.do_tool.tool_use import ToolUser from src.do_tool.tool_use import ToolUser
from src.plugins.emoji_system.emoji_manager import emoji_manager from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.utils.json_utils import process_llm_tool_response # 导入新的JSON工具 from src.plugins.utils.json_utils import process_llm_tool_calls, extract_tool_call_arguments
from src.heart_flow.sub_mind import SubMind from src.heart_flow.sub_mind import SubMind
from src.heart_flow.observation import Observation from src.heart_flow.observation import Observation
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
import contextlib import contextlib
from src.plugins.utils.chat_message_builder import num_new_messages_since from src.plugins.utils.chat_message_builder import num_new_messages_since
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from .heartFC_sender import HeartFCSender from .heartFC_sender import HeartFCSender
from src.plugins.chat.utils import process_llm_response
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.moods.moods import MoodManager
from src.individuality.individuality import Individuality
INITIAL_DURATION = 60.0 INITIAL_DURATION = 60.0
@ -181,12 +183,18 @@ class HeartFChatting:
self.action_manager = ActionManager() self.action_manager = ActionManager()
# 初始化状态控制 # 初始化状态控制
self._initialized = False # 是否已初始化标志 self._initialized = False
self._processing_lock = asyncio.Lock() # 处理锁(确保单次Plan-Replier-Sender周期) self._processing_lock = asyncio.Lock()
# 依赖注入存储 # --- 移除 gpt_instance, 直接初始化 LLM 模型 ---
self.gpt_instance = HeartFCGenerator() # 文本回复生成器 # self.gpt_instance = HeartFCGenerator() # <-- 移除
self.tool_user = ToolUser() # 工具使用实例 self.model_normal = LLMRequest( # <-- 新增 LLM 初始化
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=256,
request_type="response_heartflow",
)
self.tool_user = ToolUser()
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
# LLM规划器配置 # LLM规划器配置
@ -401,20 +409,23 @@ class HeartFChatting:
with Timer("决策", cycle_timers): with Timer("决策", cycle_timers):
planner_result = await self._planner(current_mind, cycle_timers) planner_result = await self._planner(current_mind, cycle_timers)
action = planner_result.get("action", "error") # 效果不太好还没处理replan导致观察时间点改变的问题
reasoning = planner_result.get("reasoning", "未提供理由")
self._current_cycle.set_action_info(action, reasoning, False) # action = planner_result.get("action", "error")
# reasoning = planner_result.get("reasoning", "未提供理由")
# self._current_cycle.set_action_info(action, reasoning, False)
# 在获取规划结果后检查新消息 # 在获取规划结果后检查新消息
if await self._check_new_messages(planner_start_db_time):
if random.random() < 0.2: # if await self._check_new_messages(planner_start_db_time):
logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...") # if random.random() < 0.2:
# 重新规划 # logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...")
with Timer("重新决策", cycle_timers): # # 重新规划
self._current_cycle.replanned = True # with Timer("重新决策", cycle_timers):
planner_result = await self._planner(current_mind, cycle_timers, is_re_planned=True) # self._current_cycle.replanned = True
logger.info(f"{self.log_prefix} 重新规划完成.") # planner_result = await self._planner(current_mind, cycle_timers, is_re_planned=True)
# logger.info(f"{self.log_prefix} 重新规划完成.")
# 解析规划结果 # 解析规划结果
action = planner_result.get("action", "error") action = planner_result.get("action", "error")
@ -736,94 +747,104 @@ class HeartFChatting:
observed_messages_str = observation.talking_message_str observed_messages_str = observation.talking_message_str
# --- 使用 LLM 进行决策 --- # # --- 使用 LLM 进行决策 --- #
action = "no_reply" # 默认动作
emoji_query = "" # 默认表情查询
reasoning = "默认决策或获取决策失败" reasoning = "默认决策或获取决策失败"
llm_error = False # LLM错误标志 llm_error = False # LLM错误标志
arguments = None # 初始化参数变量
emoji_query = "" # <--- 在这里初始化 emoji_query
try: try:
# 构建提示词 # --- 构建提示词 ---
replan_prompt_str = ""
if is_re_planned: if is_re_planned:
replan_prompt = 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 = replan_prompt
else:
replan_prompt = ""
prompt = await self._build_planner_prompt( prompt = await self._build_planner_prompt(
observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt observed_messages_str, current_mind, self.sub_mind.structured_info, replan_prompt_str
) )
payload = {
"model": global_config.llm_plan["name"],
"messages": [{"role": "user", "content": prompt}],
"tools": self.action_manager.get_planner_tool_definition(),
"tool_choice": {"type": "function", "function": {"name": "decide_reply_action"}},
}
# 执行LLM请求
# --- 调用 LLM ---
try: try:
print("prompt") planner_tools = self.action_manager.get_planner_tool_definition()
print("prompt") _response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async(
print("prompt") prompt=prompt,
print(payload) tools=planner_tools,
print(prompt)
response = await self.planner_llm._execute_request(
endpoint="/chat/completions", payload=payload, prompt=prompt
) )
print(response) 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}"
llm_error = True
# 直接返回错误结果
return { return {
"action": "error", "action": action,
"reasoning": f"LLM请求执行失败: {req_e}", "reasoning": reasoning,
"emoji_query": "", "emoji_query": "",
"current_mind": current_mind, "current_mind": current_mind,
"observed_messages": observed_messages, "observed_messages": observed_messages,
"llm_error": True, "llm_error": llm_error,
} }
# 处理LLM响应 # 默认错误状态
with Timer("使用工具", cycle_timers): action = "error"
# 使用辅助函数处理工具调用响应 reasoning = "处理工具调用时出错"
print(1111122222222222) llm_error = True
print(response)
success, arguments, error_msg = process_llm_tool_response( # 1. 验证工具调用
response, expected_tool_name="decide_reply_action", log_prefix=f"{self.log_prefix}[Planner] " success, valid_tool_calls, error_msg = process_llm_tool_calls(
) tool_calls, log_prefix=f"{self.log_prefix}[Planner] "
)
if success: if success and valid_tool_calls:
# 提取决策参数 # 2. 提取第一个调用并获取参数
action = arguments.get("action", "no_reply") first_tool_call = valid_tool_calls[0]
# 验证动作是否在可用动作集中 tool_name = first_tool_call.get("function", {}).get("name")
if action not in self.action_manager.get_available_actions(): 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():
logger.warning( logger.warning(
f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action}使用默认动作no_reply" f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {extracted_action}使用默认动作no_reply"
) )
action = "no_reply" action = "no_reply"
reasoning = f"LLM返回了未授权的动作: {action}" reasoning = f"LLM返回了未授权的动作: {extracted_action}"
emoji_query = ""
llm_error = False # 视为非LLM错误只是逻辑修正
else: else:
# 动作有效,使用提取的值
action = extracted_action
reasoning = arguments.get("reasoning", "未提供理由") reasoning = arguments.get("reasoning", "未提供理由")
emoji_query = arguments.get("emoji_query", "") emoji_query = arguments.get("emoji_query", "")
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决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
) )
else: elif tool_name != expected_tool_name:
# 处理工具调用失败 reasoning = f"LLM返回了非预期的工具: {tool_name}"
logger.warning(f"{self.log_prefix}[Planner] {error_msg}") logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
action = "error" else: # arguments is None
reasoning = error_msg reasoning = f"无法提取工具 {tool_name} 的参数"
llm_error = True 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
reasoning = "LLM未返回有效的工具调用"
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
# 如果 llm_error 仍然是 True说明在处理过程中有错误发生
except Exception as llm_e: except Exception as llm_e:
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}") logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}")
logger.error(traceback.format_exc()) # 记录完整堆栈以便调试 logger.error(traceback.format_exc())
action = "error" action = "error"
reasoning = f"LLM处理失败: {llm_e}" reasoning = f"Planner内部处理错误: {llm_e}"
llm_error = True llm_error = True
# --- 结束 LLM 决策 --- # # --- 结束 LLM 决策 --- #
@ -1044,9 +1065,13 @@ class HeartFChatting:
# 如果最近的活动循环不是文本回复,或者没有活动循环 # 如果最近的活动循环不是文本回复,或者没有活动循环
cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n" cycle_info_block = "\n【近期回复历史】\n(最近没有连续文本回复)\n"
individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(x_person=2, level=2)
# 获取提示词模板并填充数据 # 获取提示词模板并填充数据
prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format( prompt = (await global_prompt_manager.get_prompt_async("planner_prompt")).format(
bot_name=global_config.BOT_NICKNAME, bot_name=global_config.BOT_NICKNAME,
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,
@ -1069,27 +1094,66 @@ class HeartFChatting:
thinking_id: str, thinking_id: str,
) -> Optional[List[str]]: ) -> Optional[List[str]]:
""" """
回复器 (Replier): 核心逻辑用于生成回复 回复器 (Replier): 核心逻辑负责生成回复文本
(已整合原 HeartFCGenerator 的功能)
""" """
response_set: Optional[List[str]] = None
try: try:
response_set = await self.gpt_instance.generate_response( # 1. 获取情绪影响因子并调整模型温度
structured_info=self.sub_mind.structured_info, arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
current_mind_info=self.sub_mind.current_mind, current_temp = global_config.llm_normal["temp"] * arousal_multiplier
reason=reason, self.model_normal.temperature = current_temp # 动态调整温度
message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter)
thinking_id=thinking_id, # Pass thinking_id positionally
)
if not response_set: # 2. 获取信息捕捉器
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。") info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
# 3. 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await prompt_builder.build_prompt(
build_mode="focus",
reason=reason,
current_mind_info=self.sub_mind.current_mind,
structured_info=self.sub_mind.structured_info,
message_txt="", # 似乎是固定的空字符串
sender_name="", # 似乎是固定的空字符串
chat_stream=anchor_message.chat_stream,
)
# 4. 调用 LLM 生成回复
content = None
reasoning_content = None
model_name = "unknown_model"
try:
with Timer("LLM生成", {}): # 内部计时器,可选保留
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")
# 捕捉 LLM 输出信息
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=model_name
)
except Exception as llm_e:
# 精简报错信息
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成失败: {llm_e}")
return None # LLM 调用失败则无法生成回复
# 5. 处理 LLM 响应
if not content:
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM 生成了空内容。")
return None return None
return response_set with Timer("处理响应", {}): # 内部计时器,可选保留
processed_response = process_llm_response(content)
if not processed_response:
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] 处理后的回复为空。")
return None
return processed_response
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix}[Replier-{thinking_id}] Unexpected error in replier_work: {e}") # 更通用的错误处理,精简信息
logger.error(traceback.format_exc()) logger.error(f"{self.log_prefix}[Replier-{thinking_id}] 回复生成意外失败: {e}")
# logger.error(traceback.format_exc()) # 可以取消注释这行以在调试时查看完整堆栈
return None return None
# --- Methods moved from HeartFCController start --- # --- Methods moved from HeartFCController start ---

View File

@ -1,107 +0,0 @@
from typing import List, Optional
from ..models.utils_model import LLMRequest
from ...config.config import global_config
from ..chat.message import MessageRecv
from .heartflow_prompt_builder import prompt_builder
from ..chat.utils import process_llm_response
from src.common.logger_manager import get_logger
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from ..utils.timer_calculator import Timer
from src.plugins.moods.moods import MoodManager
logger = get_logger("llm")
class HeartFCGenerator:
def __init__(self):
self.model_normal = LLMRequest(
model=global_config.llm_normal,
temperature=global_config.llm_normal["temp"],
max_tokens=256,
request_type="response_heartflow",
)
self.model_sum = LLMRequest(
model=global_config.llm_summary_by_topic, temperature=0.6, max_tokens=2000, request_type="relation"
)
self.current_model_type = "r1" # 默认使用 R1
self.current_model_name = "unknown model"
async def generate_response(
self,
structured_info: str,
current_mind_info: str,
reason: str,
message: MessageRecv,
thinking_id: str,
) -> Optional[List[str]]:
"""根据当前模型类型选择对应的生成函数"""
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
current_model = self.model_normal
current_model.temperature = global_config.llm_normal["temp"] * arousal_multiplier # 激活度越高,温度越高
model_response = await self._generate_response_with_model(
structured_info, current_mind_info, reason, message, current_model, thinking_id
)
if model_response:
model_processed_response = await self._process_response(model_response)
return model_processed_response
else:
logger.info(f"{self.current_model_type}思考,失败")
return None
async def _generate_response_with_model(
self,
structured_info: str,
current_mind_info: str,
reason: str,
message: MessageRecv,
model: LLMRequest,
thinking_id: str,
) -> str:
info_catcher = info_catcher_manager.get_info_catcher(thinking_id)
with Timer() as _build_prompt:
prompt = await prompt_builder.build_prompt(
build_mode="focus",
reason=reason,
current_mind_info=current_mind_info,
structured_info=structured_info,
message_txt="",
sender_name="",
chat_stream=message.chat_stream,
)
# logger.info(f"构建prompt时间: {t_build_prompt.human_readable}")
try:
content, reasoning_content, self.current_model_name = await model.generate_response(prompt)
logger.info(f"\nprompt:{prompt}\n生成回复{content}\n")
info_catcher.catch_after_llm_generated(
prompt=prompt, response=content, reasoning_content=reasoning_content, model_name=self.current_model_name
)
except Exception:
logger.exception("生成回复时出错")
return None
return content
async def _process_response(self, content: str) -> List[str]:
"""处理响应内容,返回处理后的内容和情感标签"""
if not content:
return None
processed_response = process_llm_response(content)
# print(f"得到了处理后的llm返回{processed_response}")
return processed_response

View File

@ -5,7 +5,7 @@ from ...individuality.individuality import Individuality
from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager from src.plugins.utils.prompt_builder import Prompt, global_prompt_manager
from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.chat.utils import get_embedding, parse_text_timestamps from src.plugins.chat.utils import get_embedding
import time import time
from typing import Union, Optional from typing import Union, Optional
from ...common.database import db from ...common.database import db
@ -25,7 +25,7 @@ def init_prompt():
{chat_target} {chat_target}
{chat_talking_prompt} {chat_talking_prompt}
现在你想要在群里发言或者回复\n 现在你想要在群里发言或者回复\n
你需要扮演一位网名叫{bot_name}的人进行回复这个人的特点是"{prompt_personality} {prompt_identity}" 你需要扮演一位网名叫{bot_name}的人进行回复这个人的特点是"{prompt_personality}"
你正在{chat_target_2},现在请你读读之前的聊天记录然后给出日常且口语化的回复平淡一些你可以参考贴吧知乎或者微博的回复风格 你正在{chat_target_2},现在请你读读之前的聊天记录然后给出日常且口语化的回复平淡一些你可以参考贴吧知乎或者微博的回复风格
看到以上聊天记录你刚刚在想 看到以上聊天记录你刚刚在想
@ -49,7 +49,7 @@ def init_prompt():
# Planner提示词 - 优化版 # Planner提示词 - 优化版
Prompt( Prompt(
"""你的名字是{bot_name}一名大二女生正在QQ群聊中。需要基于以下信息决定如何参与对话: """你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
{structured_info_block} {structured_info_block}
{chat_content_block} {chat_content_block}
你的内心想法 你的内心想法
@ -164,9 +164,7 @@ class PromptBuilder:
async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]: async def _build_prompt_focus(self, reason, current_mind_info, structured_info, chat_stream) -> tuple[str, str]:
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1) prompt_personality = individuality.get_prompt(x_person=0, level=2)
prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1)
# 日程构建 # 日程构建
# schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}''' # schedule_prompt = f'''你现在正在做的事情是:{bot_schedule.get_current_num_task(num = 1,time_info = False)}'''
@ -214,7 +212,6 @@ class PromptBuilder:
chat_talking_prompt=chat_talking_prompt, chat_talking_prompt=chat_talking_prompt,
bot_name=global_config.BOT_NICKNAME, bot_name=global_config.BOT_NICKNAME,
prompt_personality=prompt_personality, prompt_personality=prompt_personality,
prompt_identity=prompt_identity,
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2") chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
if chat_in_group if chat_in_group
else await global_prompt_manager.get_prompt_async("chat_target_private2"), else await global_prompt_manager.get_prompt_async("chat_target_private2"),
@ -224,27 +221,11 @@ class PromptBuilder:
moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"), moderation_prompt=await global_prompt_manager.get_prompt_async("moderation_prompt"),
) )
prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
prompt = parse_text_timestamps(prompt, mode="lite")
return prompt return prompt
async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]: async def _build_prompt_normal(self, chat_stream, message_txt: str, sender_name: str = "某人") -> tuple[str, str]:
# 开始构建prompt
prompt_personality = ""
# person
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(x_person=2, level=2)
personality_core = individuality.personality.personality_core
prompt_personality += personality_core
personality_sides = individuality.personality.personality_sides
random.shuffle(personality_sides)
prompt_personality += f",{personality_sides[0]}"
identity_detail = individuality.identity.identity_detail
random.shuffle(identity_detail)
prompt_personality += f",{identity_detail[0]}"
# 关系 # 关系
who_chat_in_group = [ who_chat_in_group = [

View File

@ -14,51 +14,14 @@ from ...common.database import db
from ...plugins.models.utils_model import LLMRequest from ...plugins.models.utils_model import LLMRequest
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.plugins.memory_system.sample_distribution import MemoryBuildScheduler # 分布生成器 from src.plugins.memory_system.sample_distribution import MemoryBuildScheduler # 分布生成器
from ..utils.chat_message_builder import (
get_raw_msg_by_timestamp,
build_readable_messages,
) # 导入 build_readable_messages
from ..chat.utils import translate_timestamp_to_human_readable
from .memory_config import MemoryConfig from .memory_config import MemoryConfig
def get_closest_chat_from_db(length: int, timestamp: str):
# print(f"获取最接近指定时间戳的聊天记录,长度: {length}, 时间戳: {timestamp}")
# print(f"当前时间: {timestamp},转换后时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp))}")
chat_records = []
closest_record = db.messages.find_one({"time": {"$lte": timestamp}}, sort=[("time", -1)])
# print(f"最接近的记录: {closest_record}")
if closest_record:
closest_time = closest_record["time"]
chat_id = closest_record["chat_id"] # 获取chat_id
# 获取该时间戳之后的length条消息保持相同的chat_id
chat_records = list(
db.messages.find(
{
"time": {"$gt": closest_time},
"chat_id": chat_id, # 添加chat_id过滤
}
)
.sort("time", 1)
.limit(length)
)
# print(f"获取到的记录: {chat_records}")
length = len(chat_records)
# print(f"获取到的记录长度: {length}")
# 转换记录格式
formatted_records = []
for record in chat_records:
# 兼容行为,前向兼容老数据
formatted_records.append(
{
"_id": record["_id"],
"time": record["time"],
"chat_id": record["chat_id"],
"detailed_plain_text": record.get("detailed_plain_text", ""), # 添加文本内容
"memorized_times": record.get("memorized_times", 0), # 添加记忆次数
}
)
return formatted_records
return []
def calculate_information_content(text): def calculate_information_content(text):
"""计算文本的信息量(熵)""" """计算文本的信息量(熵)"""
char_count = Counter(text) char_count = Counter(text)
@ -232,6 +195,7 @@ class Hippocampus:
self.config = None self.config = None
def initialize(self, global_config): def initialize(self, global_config):
# 使用导入的 MemoryConfig dataclass 和其 from_global_config 方法
self.config = MemoryConfig.from_global_config(global_config) self.config = MemoryConfig.from_global_config(global_config)
# 初始化子组件 # 初始化子组件
self.entorhinal_cortex = EntorhinalCortex(self) self.entorhinal_cortex = EntorhinalCortex(self)
@ -263,17 +227,18 @@ class Hippocampus:
@staticmethod @staticmethod
def find_topic_llm(text, topic_num): def find_topic_llm(text, topic_num):
prompt = ( prompt = (
f"这是一段文字:{text}请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。"
f"如果确定找不出主题或者没有明显主题,返回<none>。" f"如果确定找不出主题或者没有明显主题,返回<none>。"
) )
return prompt return prompt
@staticmethod @staticmethod
def topic_what(text, topic, time_info): def topic_what(text, topic):
# 不再需要 time_info 参数
prompt = ( prompt = (
f'这是一段文字{time_info}{text}我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,' f'这是一段文字\n{text}\n\n我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,'
f"可以包含时间和人物,以及具体的观点。只输出这句话就好" f"要求包含对这个概念的定义,内容,知识,可以包含时间和人物。只输出这句话就好"
) )
return prompt return prompt
@ -831,7 +796,7 @@ class EntorhinalCortex:
def get_memory_sample(self): def get_memory_sample(self):
"""从数据库获取记忆样本""" """从数据库获取记忆样本"""
# 硬编码:每条消息最大记忆次数 # 硬编码:每条消息最大记忆次数
max_memorized_time_per_msg = 3 max_memorized_time_per_msg = 2
# 创建双峰分布的记忆调度器 # 创建双峰分布的记忆调度器
sample_scheduler = MemoryBuildScheduler( sample_scheduler = MemoryBuildScheduler(
@ -845,9 +810,12 @@ class EntorhinalCortex:
) )
timestamps = sample_scheduler.get_timestamp_array() timestamps = sample_scheduler.get_timestamp_array()
logger.info(f"回忆往事: {[time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts)) for ts in timestamps]}") # 使用 translate_timestamp_to_human_readable 并指定 mode="normal"
readable_timestamps = [translate_timestamp_to_human_readable(ts, mode="normal") for ts in timestamps]
logger.info(f"回忆往事: {readable_timestamps}")
chat_samples = [] chat_samples = []
for timestamp in timestamps: for timestamp in timestamps:
# 调用修改后的 random_get_msg_snippet
messages = self.random_get_msg_snippet( messages = self.random_get_msg_snippet(
timestamp, self.config.build_memory_sample_length, max_memorized_time_per_msg timestamp, self.config.build_memory_sample_length, max_memorized_time_per_msg
) )
@ -862,22 +830,45 @@ class EntorhinalCortex:
@staticmethod @staticmethod
def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list: def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list:
"""从数据库中随机获取指定时间戳附近的消息片段""" """从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)"""
try_count = 0 try_count = 0
time_window_seconds = random.randint(300, 1800) # 随机时间窗口5到30分钟
while try_count < 3: while try_count < 3:
messages = get_closest_chat_from_db(length=chat_size, timestamp=target_timestamp) # 定义时间范围:从目标时间戳开始,向后推移 time_window_seconds
timestamp_start = target_timestamp
timestamp_end = target_timestamp + time_window_seconds
# 使用 chat_message_builder 的函数获取消息
# limit_mode='earliest' 获取这个时间窗口内最早的 chat_size 条消息
messages = get_raw_msg_by_timestamp(
timestamp_start=timestamp_start, timestamp_end=timestamp_end, limit=chat_size, limit_mode="earliest"
)
if messages: if messages:
# 检查获取到的所有消息是否都未达到最大记忆次数
all_valid = True
for message in messages: for message in messages:
if message["memorized_times"] >= max_memorized_time_per_msg: if message.get("memorized_times", 0) >= max_memorized_time_per_msg:
messages = None all_valid = False
break break
if messages:
# 如果所有消息都有效
if all_valid:
# 更新数据库中的记忆次数
for message in messages: for message in messages:
# 确保在更新前获取最新的 memorized_times以防万一
current_memorized_times = message.get("memorized_times", 0)
db.messages.update_one( db.messages.update_one(
{"_id": message["_id"]}, {"$set": {"memorized_times": message["memorized_times"] + 1}} {"_id": message["_id"]}, {"$set": {"memorized_times": current_memorized_times + 1}}
) )
return messages return messages # 直接返回原始的消息列表
# 如果获取失败或消息无效,增加尝试次数
try_count += 1 try_count += 1
target_timestamp -= 120 # 如果第一次尝试失败,稍微向前调整时间戳再试
# 三次尝试都失败,返回 None
return None return None
async def sync_memory_to_db(self): async def sync_memory_to_db(self):
@ -1113,86 +1104,70 @@ class ParahippocampalGyrus:
"""压缩和总结消息内容,生成记忆主题和摘要。 """压缩和总结消息内容,生成记忆主题和摘要。
Args: Args:
messages (list): 消息列表每个消息是一个字典包含以下字段 messages (list): 消息列表每个消息是一个字典包含数据库消息结构
- time: float, 消息的时间戳
- detailed_plain_text: str, 消息的详细文本内容
compress_rate (float, optional): 压缩率用于控制生成的主题数量默认为0.1 compress_rate (float, optional): 压缩率用于控制生成的主题数量默认为0.1
Returns: Returns:
tuple: (compressed_memory, similar_topics_dict) tuple: (compressed_memory, similar_topics_dict)
- compressed_memory: set, 压缩后的记忆集合每个元素是一个元组 (topic, summary) - compressed_memory: set, 压缩后的记忆集合每个元素是一个元组 (topic, summary)
- topic: str, 记忆主题 - similar_topics_dict: dict, 相似主题字典
- summary: str, 主题的摘要描述
- similar_topics_dict: dict, 相似主题字典key为主题value为相似主题列表
每个相似主题是一个元组 (similar_topic, similarity)
- similar_topic: str, 相似的主题
- similarity: float, 相似度分数0-1之间
Process: Process:
1. 合并消息文本并生成时间信息 1. 使用 build_readable_messages 生成包含时间人物信息的格式化文本
2. 使用LLM提取关键主题 2. 使用LLM提取关键主题
3. 过滤掉包含禁用关键词的主题 3. 过滤掉包含禁用关键词的主题
4. 为每个主题生成摘要 4. 为每个主题生成摘要
5. 查找与现有记忆中的相似主题 5. 查找与现有记忆中的相似主题
""" """
if not messages: if not messages:
return set(), {} return set(), {}
# 合并消息文本,同时保留时间信息 # 1. 使用 build_readable_messages 生成格式化文本
input_text = "" # build_readable_messages 只返回一个字符串,不需要解包
time_info = "" input_text = await build_readable_messages(
# 计算最早和最晚时间 messages,
earliest_time = min(msg["time"] for msg in messages) merge_messages=True, # 合并连续消息
latest_time = max(msg["time"] for msg in messages) timestamp_mode="normal", # 使用 'YYYY-MM-DD HH:MM:SS' 格式
replace_bot_name=False, # 保留原始用户名
)
earliest_dt = datetime.datetime.fromtimestamp(earliest_time) # 如果生成的可读文本为空(例如所有消息都无效),则直接返回
latest_dt = datetime.datetime.fromtimestamp(latest_time) if not input_text:
logger.warning("无法从提供的消息生成可读文本,跳过记忆压缩。")
return set(), {}
# 如果是同一年 logger.debug(f"用于压缩的格式化文本:\n{input_text}")
if earliest_dt.year == latest_dt.year:
earliest_str = earliest_dt.strftime("%m-%d %H:%M:%S")
latest_str = latest_dt.strftime("%m-%d %H:%M:%S")
time_info += f"是在{earliest_dt.year}年,{earliest_str}{latest_str} 的对话:\n"
else:
earliest_str = earliest_dt.strftime("%Y-%m-%d %H:%M:%S")
latest_str = latest_dt.strftime("%Y-%m-%d %H:%M:%S")
time_info += f"是从 {earliest_str}{latest_str} 的对话:\n"
for msg in messages:
input_text += f"{msg['detailed_plain_text']}\n"
logger.debug(input_text)
# 2. 使用LLM提取关键主题
topic_num = self.hippocampus.calculate_topic_num(input_text, compress_rate) topic_num = self.hippocampus.calculate_topic_num(input_text, compress_rate)
topics_response = await self.hippocampus.llm_topic_judge.generate_response( topics_response = await self.hippocampus.llm_topic_judge.generate_response(
self.hippocampus.find_topic_llm(input_text, topic_num) self.hippocampus.find_topic_llm(input_text, topic_num)
) )
# 使用正则表达式提取<>中的内容 # 提取<>中的内容
topics = re.findall(r"<([^>]+)>", topics_response[0]) topics = re.findall(r"<([^>]+)>", topics_response[0])
# 如果没有找到<>包裹的内容,返回['none']
if not topics: if not topics:
topics = ["none"] topics = ["none"]
else: else:
# 处理提取出的话题
topics = [ topics = [
topic.strip() topic.strip()
for topic in ",".join(topics).replace("", ",").replace("", ",").replace(" ", ",").split(",") for topic in ",".join(topics).replace("", ",").replace("", ",").replace(" ", ",").split(",")
if topic.strip() if topic.strip()
] ]
# 过滤掉包含禁用关键词的topic # 3. 过滤掉包含禁用关键词的topic
filtered_topics = [ filtered_topics = [
topic for topic in topics if not any(keyword in topic for keyword in self.config.memory_ban_words) topic for topic in topics if not any(keyword in topic for keyword in self.config.memory_ban_words)
] ]
logger.debug(f"过滤后话题: {filtered_topics}") logger.debug(f"过滤后话题: {filtered_topics}")
# 创建所有话题的请求任务 # 4. 创建所有话题的摘要生成任务
tasks = [] tasks = []
for topic in filtered_topics: for topic in filtered_topics:
topic_what_prompt = self.hippocampus.topic_what(input_text, topic, time_info) # 调用修改后的 topic_what不再需要 time_info
topic_what_prompt = self.hippocampus.topic_what(input_text, topic)
try: try:
task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt) task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt)
tasks.append((topic.strip(), task)) tasks.append((topic.strip(), task))
@ -1363,26 +1338,56 @@ class ParahippocampalGyrus:
logger.info("[遗忘] 开始检查节点...") logger.info("[遗忘] 开始检查节点...")
node_check_start = time.time() node_check_start = time.time()
for node in nodes_to_check: for node in nodes_to_check:
# 检查节点是否存在,以防在迭代中被移除(例如边移除导致)
if node not in self.memory_graph.G:
continue
node_data = self.memory_graph.G.nodes[node] node_data = self.memory_graph.G.nodes[node]
# 首先获取记忆项
memory_items = node_data.get("memory_items", [])
if not isinstance(memory_items, list):
memory_items = [memory_items] if memory_items else []
# 新增:检查节点是否为空
if not memory_items:
try:
self.memory_graph.G.remove_node(node)
node_changes["removed"].append(f"{node}(空节点)") # 标记为空节点移除
logger.debug(f"[遗忘] 移除了空的节点: {node}")
except nx.NetworkXError as e:
logger.warning(f"[遗忘] 移除空节点 {node} 时发生错误(可能已被移除): {e}")
continue # 处理下一个节点
# --- 如果节点不为空,则执行原来的不活跃检查和随机移除逻辑 ---
last_modified = node_data.get("last_modified", current_time) last_modified = node_data.get("last_modified", current_time)
# 条件1检查是否长时间未修改 (超过24小时)
if current_time - last_modified > 3600 * 24: if current_time - last_modified > 3600 * 24:
memory_items = node_data.get("memory_items", []) # 条件2再次确认节点包含记忆项理论上已确认但作为保险
if not isinstance(memory_items, list):
memory_items = [memory_items] if memory_items else []
if memory_items: if memory_items:
current_count = len(memory_items) current_count = len(memory_items)
removed_item = random.choice(memory_items) # 如果列表非空,才进行随机选择
memory_items.remove(removed_item) if current_count > 0:
removed_item = random.choice(memory_items)
try:
memory_items.remove(removed_item)
if memory_items: # 条件3检查移除后 memory_items 是否变空
self.memory_graph.G.nodes[node]["memory_items"] = memory_items if memory_items: # 如果移除后列表不为空
self.memory_graph.G.nodes[node]["last_modified"] = current_time # self.memory_graph.G.nodes[node]["memory_items"] = memory_items # 直接修改列表即可
node_changes["reduced"].append(f"{node} (数量: {current_count} -> {len(memory_items)})") self.memory_graph.G.nodes[node]["last_modified"] = current_time # 更新修改时间
else: node_changes["reduced"].append(f"{node} (数量: {current_count} -> {len(memory_items)})")
self.memory_graph.G.remove_node(node) else: # 如果移除后列表为空
node_changes["removed"].append(node) # 尝试移除节点,处理可能的错误
try:
self.memory_graph.G.remove_node(node)
node_changes["removed"].append(f"{node}(遗忘清空)") # 标记为遗忘清空
logger.debug(f"[遗忘] 节点 {node} 因移除最后一项而被清空。")
except nx.NetworkXError as e:
logger.warning(f"[遗忘] 尝试移除节点 {node} 时发生错误(可能已被移除):{e}")
except ValueError:
# 这个错误理论上不应发生,因为 removed_item 来自 memory_items
logger.warning(f"[遗忘] 尝试从节点 '{node}' 移除不存在的项目 '{removed_item[:30]}...'")
node_check_end = time.time() node_check_end = time.time()
logger.info(f"[遗忘] 节点检查耗时: {node_check_end - node_check_start:.2f}") logger.info(f"[遗忘] 节点检查耗时: {node_check_end - node_check_start:.2f}")
@ -1421,6 +1426,119 @@ class ParahippocampalGyrus:
end_time = time.time() end_time = time.time()
logger.info(f"[遗忘] 总耗时: {end_time - start_time:.2f}") logger.info(f"[遗忘] 总耗时: {end_time - start_time:.2f}")
async def operation_consolidate_memory(self):
"""整合记忆:合并节点内相似的记忆项"""
start_time = time.time()
percentage = self.config.consolidate_memory_percentage
similarity_threshold = self.config.consolidation_similarity_threshold
logger.info(f"[整合] 开始检查记忆节点... 检查比例: {percentage:.2%}, 合并阈值: {similarity_threshold}")
# 获取所有至少有2条记忆项的节点
eligible_nodes = []
for node, data in self.memory_graph.G.nodes(data=True):
memory_items = data.get("memory_items", [])
if isinstance(memory_items, list) and len(memory_items) >= 2:
eligible_nodes.append(node)
if not eligible_nodes:
logger.info("[整合] 没有找到包含多个记忆项的节点,无需整合。")
return
# 计算需要检查的节点数量
check_nodes_count = max(1, min(len(eligible_nodes), int(len(eligible_nodes) * percentage)))
# 随机抽取节点进行检查
try:
nodes_to_check = random.sample(eligible_nodes, check_nodes_count)
except ValueError as e:
logger.error(f"[整合] 抽样节点时出错: {e}")
return
logger.info(f"[整合] 将检查 {len(nodes_to_check)} / {len(eligible_nodes)} 个符合条件的节点。")
merged_count = 0
nodes_modified = set()
current_timestamp = datetime.datetime.now().timestamp()
for node in nodes_to_check:
node_data = self.memory_graph.G.nodes[node]
memory_items = node_data.get("memory_items", [])
if not isinstance(memory_items, list) or len(memory_items) < 2:
continue # 双重检查,理论上不会进入
items_copy = list(memory_items) # 创建副本以安全迭代和修改
# 遍历所有记忆项组合
for item1, item2 in combinations(items_copy, 2):
# 确保 item1 和 item2 仍然存在于原始列表中(可能已被之前的合并移除)
if item1 not in memory_items or item2 not in memory_items:
continue
similarity = self._calculate_item_similarity(item1, item2)
if similarity >= similarity_threshold:
logger.debug(f"[整合] 节点 '{node}' 中发现相似项 (相似度: {similarity:.2f}):")
logger.trace(f" - '{item1}'")
logger.trace(f" - '{item2}'")
# 比较信息量
info1 = calculate_information_content(item1)
info2 = calculate_information_content(item2)
if info1 >= info2:
item_to_keep = item1
item_to_remove = item2
else:
item_to_keep = item2
item_to_remove = item1
# 从原始列表中移除信息量较低的项
try:
memory_items.remove(item_to_remove)
logger.info(
f"[整合] 已合并节点 '{node}' 中的记忆,保留: '{item_to_keep[:60]}...', 移除: '{item_to_remove[:60]}...'"
)
merged_count += 1
nodes_modified.add(node)
node_data["last_modified"] = current_timestamp # 更新修改时间
_merged_in_this_node = True
break # 每个节点每次检查只合并一对
except ValueError:
# 如果项已经被移除(例如,在之前的迭代中作为 item_to_keep则跳过
logger.warning(
f"[整合] 尝试移除节点 '{node}' 中不存在的项 '{item_to_remove[:30]}...',可能已被合并。"
)
continue
# # 如果节点内发生了合并,更新节点数据 (这种方式不安全,会丢失其他属性)
# if merged_in_this_node:
# self.memory_graph.G.nodes[node]["memory_items"] = memory_items
if merged_count > 0:
logger.info(f"[整合] 共合并了 {merged_count} 对相似记忆项,分布在 {len(nodes_modified)} 个节点中。")
sync_start = time.time()
logger.info("[整合] 开始将变更同步到数据库...")
# 使用 resync 更安全地处理删除和添加
await self.hippocampus.entorhinal_cortex.resync_memory_to_db()
sync_end = time.time()
logger.info(f"[整合] 数据库同步耗时: {sync_end - sync_start:.2f}")
else:
logger.info("[整合] 本次检查未发现需要合并的记忆项。")
end_time = time.time()
logger.info(f"[整合] 整合检查完成,总耗时: {end_time - start_time:.2f}")
@staticmethod
def _calculate_item_similarity(item1: str, item2: str) -> float:
"""计算两条记忆项文本的余弦相似度"""
words1 = set(jieba.cut(item1))
words2 = set(jieba.cut(item2))
all_words = words1 | words2
if not all_words:
return 0.0
v1 = [1 if word in words1 else 0 for word in all_words]
v2 = [1 if word in words2 else 0 for word in all_words]
return cosine_similarity(v1, v2)
class HippocampusManager: class HippocampusManager:
_instance = None _instance = None
@ -1459,12 +1577,12 @@ class HippocampusManager:
edge_count = len(memory_graph.edges()) edge_count = len(memory_graph.edges())
logger.success(f"""-------------------------------- logger.success(f"""--------------------------------
记忆系统参数配置: 记忆系统参数配置:
构建间隔: {global_config.build_memory_interval}|样本数: {config.build_memory_sample_num},长度: {config.build_memory_sample_length}|压缩率: {config.memory_compress_rate} 构建间隔: {global_config.build_memory_interval}|样本数: {config.build_memory_sample_num},长度: {config.build_memory_sample_length}|压缩率: {config.memory_compress_rate}
记忆构建分布: {config.memory_build_distribution} 记忆构建分布: {config.memory_build_distribution}
遗忘间隔: {global_config.forget_memory_interval}|遗忘比例: {global_config.memory_forget_percentage}|遗忘: {config.memory_forget_time}小时之后 遗忘间隔: {global_config.forget_memory_interval}|遗忘比例: {global_config.memory_forget_percentage}|遗忘: {config.memory_forget_time}小时之后
记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count} 记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count}
--------------------------------""") # noqa: E501 --------------------------------""") # noqa: E501
return self._hippocampus return self._hippocampus
@ -1480,6 +1598,14 @@ class HippocampusManager:
raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法") raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法")
return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage) return await self._hippocampus.parahippocampal_gyrus.operation_forget_topic(percentage)
async def consolidate_memory(self):
"""整合记忆的公共接口"""
if not self._initialized:
raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法")
# 注意:目前 operation_consolidate_memory 内部直接读取配置percentage 参数暂时无效
# 如果需要外部控制比例,需要修改 operation_consolidate_memory
return await self._hippocampus.parahippocampal_gyrus.operation_consolidate_memory()
async def get_memory_from_text( async def get_memory_from_text(
self, self,
text: str, text: str,

View File

@ -18,19 +18,33 @@ class MemoryConfig:
# 记忆过滤相关配置 # 记忆过滤相关配置
memory_ban_words: List[str] # 记忆过滤词列表 memory_ban_words: List[str] # 记忆过滤词列表
# 新增:记忆整合相关配置
consolidation_similarity_threshold: float # 相似度阈值
consolidate_memory_percentage: float # 检查节点比例
consolidate_memory_interval: int # 记忆整合间隔
llm_topic_judge: str # 话题判断模型 llm_topic_judge: str # 话题判断模型
llm_summary_by_topic: str # 话题总结模型 llm_summary_by_topic: str # 话题总结模型
@classmethod @classmethod
def from_global_config(cls, global_config): def from_global_config(cls, global_config):
"""从全局配置创建记忆系统配置""" """从全局配置创建记忆系统配置"""
# 使用 getattr 提供默认值,防止全局配置缺少这些项
return cls( return cls(
memory_build_distribution=global_config.memory_build_distribution, memory_build_distribution=getattr(
build_memory_sample_num=global_config.build_memory_sample_num, global_config, "memory_build_distribution", (24, 12, 0.5, 168, 72, 0.5)
build_memory_sample_length=global_config.build_memory_sample_length, ), # 添加默认值
memory_compress_rate=global_config.memory_compress_rate, build_memory_sample_num=getattr(global_config, "build_memory_sample_num", 5),
memory_forget_time=global_config.memory_forget_time, build_memory_sample_length=getattr(global_config, "build_memory_sample_length", 30),
memory_ban_words=global_config.memory_ban_words, memory_compress_rate=getattr(global_config, "memory_compress_rate", 0.1),
llm_topic_judge=global_config.llm_topic_judge, memory_forget_time=getattr(global_config, "memory_forget_time", 24 * 7),
llm_summary_by_topic=global_config.llm_summary_by_topic, memory_ban_words=getattr(global_config, "memory_ban_words", []),
# 新增加载整合配置,并提供默认值
consolidation_similarity_threshold=getattr(global_config, "consolidation_similarity_threshold", 0.7),
consolidate_memory_percentage=getattr(global_config, "consolidate_memory_percentage", 0.01),
consolidate_memory_interval=getattr(global_config, "consolidate_memory_interval", 1000),
llm_topic_judge=getattr(global_config, "llm_topic_judge", "default_judge_model"), # 添加默认模型名
llm_summary_by_topic=getattr(
global_config, "llm_summary_by_topic", "default_summary_model"
), # 添加默认模型名
) )

View File

@ -739,7 +739,7 @@ class LLMRequest:
return response return response
async def generate_response_tool_async(self, prompt: str, tools: list, **kwargs) -> Union[str, Tuple]: async def generate_response_tool_async(self, prompt: str, tools: list, **kwargs) -> tuple[str, str, list]:
"""异步方式根据输入的提示生成模型的响应""" """异步方式根据输入的提示生成模型的响应"""
# 构建请求体不硬编码max_tokens # 构建请求体不硬编码max_tokens
data = { data = {
@ -750,16 +750,17 @@ class LLMRequest:
"tools": tools, "tools": tools,
} }
logger.debug(f"向模型 {self.model_name} 发送工具调用请求,包含 {len(tools)} 个工具")
response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt) response = await self._execute_request(endpoint="/chat/completions", payload=data, prompt=prompt)
logger.debug(f"向模型 {self.model_name} 发送工具调用请求,包含 {len(tools)} 个工具,返回结果: {response}")
# 检查响应是否包含工具调用 # 检查响应是否包含工具调用
if isinstance(response, tuple) and len(response) == 3: if len(response) == 3:
content, reasoning_content, tool_calls = response content, reasoning_content, tool_calls = response
logger.debug(f"收到工具调用响应,包含 {len(tool_calls) if tool_calls else 0} 个工具调用") logger.debug(f"收到工具调用响应,包含 {len(tool_calls) if tool_calls else 0} 个工具调用")
return content, reasoning_content, tool_calls return content, reasoning_content, tool_calls
else: else:
content, reasoning_content = response
logger.debug("收到普通响应,无工具调用") logger.debug("收到普通响应,无工具调用")
return response return content, reasoning_content, None
async def get_embedding(self, text: str) -> Union[list, None]: async def get_embedding(self, text: str) -> Union[list, None]:
"""异步方法获取文本的embedding向量 """异步方法获取文本的embedding向量

View File

@ -180,10 +180,10 @@ class PersonInfoManager:
existing_names = "" existing_names = ""
while current_try < max_retries: while current_try < max_retries:
individuality = Individuality.get_instance() individuality = Individuality.get_instance()
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1) prompt_personality = individuality.get_prompt(x_person=2, level=1)
bot_name = individuality.personality.bot_nickname bot_name = individuality.personality.bot_nickname
qv_name_prompt = f"你是{bot_name}{prompt_personality}" qv_name_prompt = f"你是{bot_name}{prompt_personality}"
qv_name_prompt += f"现在你想给一个用户取一个昵称用户是的qq昵称是{user_nickname}" qv_name_prompt += f"现在你想给一个用户取一个昵称用户是的qq昵称是{user_nickname}"
qv_name_prompt += f"用户的qq群昵称名是{user_cardname}" qv_name_prompt += f"用户的qq群昵称名是{user_cardname}"
if user_avatar: if user_avatar:

View File

@ -4,8 +4,8 @@ import math
from bson.decimal128 import Decimal128 from bson.decimal128 import Decimal128
from .person_info import person_info_manager from .person_info import person_info_manager
import time import time
import re # import re
import traceback # import traceback
logger = get_logger("relation") logger = get_logger("relation")
@ -101,36 +101,6 @@ class RelationshipManager:
# await person_info_manager.update_one_field(person_id, "user_avatar", user_avatar) # await person_info_manager.update_one_field(person_id, "user_avatar", user_avatar)
await person_info_manager.qv_person_name(person_id, user_nickname, user_cardname, user_avatar) await person_info_manager.qv_person_name(person_id, user_nickname, user_cardname, user_avatar)
@staticmethod
async def convert_all_person_sign_to_person_name(input_text: str):
"""将所有人的<platform:user_id:nickname:cardname>格式转换为person_name"""
try:
# 使用正则表达式匹配<platform:user_id:nickname:cardname>格式
all_person = person_info_manager.person_name_list
pattern = r"<([^:]+):(\d+):([^:]+):([^>]+)>"
matches = re.findall(pattern, input_text)
# 遍历匹配结果,将<platform:user_id:nickname:cardname>替换为person_name
result_text = input_text
for platform, user_id, nickname, cardname in matches:
person_id = person_info_manager.get_person_id(platform, user_id)
# 默认使用昵称作为人名
person_name = nickname.strip() if nickname.strip() else cardname.strip()
if person_id in all_person:
if all_person[person_id] is not None:
person_name = all_person[person_id]
# print(f"将<{platform}:{user_id}:{nickname}:{cardname}>替换为{person_name}")
result_text = result_text.replace(f"<{platform}:{user_id}:{nickname}:{cardname}>", person_name)
return result_text
except Exception:
logger.error(traceback.format_exc())
return input_text
async def calculate_update_relationship_value(self, chat_stream: ChatStream, label: str, stance: str) -> tuple: async def calculate_update_relationship_value(self, chat_stream: ChatStream, label: str, stance: str) -> tuple:
"""计算并变更关系值 """计算并变更关系值
新的关系值变更计算方式 新的关系值变更计算方式

View File

@ -1,6 +1,6 @@
import json import json
import logging import logging
from typing import Any, Dict, TypeVar, List, Union, Callable, Tuple from typing import Any, Dict, TypeVar, List, Union, Tuple
# 定义类型变量用于泛型类型提示 # 定义类型变量用于泛型类型提示
T = TypeVar("T") T = TypeVar("T")
@ -70,56 +70,6 @@ def extract_tool_call_arguments(tool_call: Dict[str, Any], default_value: Dict[s
return default_result return default_result
def get_json_value(
json_obj: Dict[str, Any], key_path: str, default_value: T = None, transform_func: Callable[[Any], T] = None
) -> Union[Any, T]:
"""
从JSON对象中按照路径提取值支持点表示法路径"data.items.0.name"
参数:
json_obj: JSON对象(已解析的字典)
key_path: 键路径使用点表示法"data.items.0.name"
default_value: 获取失败时返回的默认值
transform_func: 可选的转换函数用于对获取的值进行转换
返回:
路径指向的值或在获取失败时返回default_value
"""
if not json_obj or not key_path:
return default_value
try:
# 分割路径
keys = key_path.split(".")
current = json_obj
# 遍历路径
for key in keys:
# 处理数组索引
if key.isdigit() and isinstance(current, list):
index = int(key)
if 0 <= index < len(current):
current = current[index]
else:
return default_value
# 处理字典键
elif isinstance(current, dict):
if key in current:
current = current[key]
else:
return default_value
else:
return default_value
# 应用转换函数(如果提供)
if transform_func and current is not None:
return transform_func(current)
return current
except Exception as e:
logger.error(f"从JSON获取值时出错: {e}, 路径: {key_path}")
return default_value
def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = False, pretty: bool = False) -> str: def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = False, pretty: bool = False) -> str:
""" """
安全地将Python对象序列化为JSON字符串 安全地将Python对象序列化为JSON字符串
@ -144,23 +94,6 @@ def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = Fa
return default_value return default_value
def merge_json_objects(*objects: Dict[str, Any]) -> Dict[str, Any]:
"""
合并多个JSON对象(字典)
参数:
*objects: 要合并的JSON对象(字典)
返回:
合并后的字典后面的对象会覆盖前面对象的相同键
"""
result = {}
for obj in objects:
if obj and isinstance(obj, dict):
result.update(obj)
return result
def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, List[Any], str]: def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, List[Any], str]:
""" """
标准化LLM响应格式将各种格式如元组转换为统一的列表格式 标准化LLM响应格式将各种格式如元组转换为统一的列表格式
@ -172,6 +105,9 @@ def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, L
返回: 返回:
元组 (成功标志, 标准化后的响应列表, 错误消息) 元组 (成功标志, 标准化后的响应列表, 错误消息)
""" """
logger.debug(f"{log_prefix}原始人 LLM响应: {response}")
# 检查是否为None # 检查是否为None
if response is None: if response is None:
return False, [], "LLM响应为None" return False, [], "LLM响应为None"
@ -201,114 +137,68 @@ def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, L
return True, response, "" return True, response, ""
def process_llm_tool_calls(response: List[Any], log_prefix: str = "") -> Tuple[bool, List[Dict[str, Any]], str]: def process_llm_tool_calls(
tool_calls: List[Dict[str, Any]], log_prefix: str = ""
) -> Tuple[bool, List[Dict[str, Any]], str]:
""" """
处理并提取LLM响应中的工具调用列表 处理并验证LLM响应中的工具调用列表
参数: 参数:
response: 标准化后的LLM响应列表 tool_calls: 从LLM响应中直接获取的工具调用列表
log_prefix: 日志前缀 log_prefix: 日志前缀
返回: 返回:
元组 (成功标志, 工具调用列表, 错误消息) 元组 (成功标志, 验证后的工具调用列表, 错误消息)
""" """
# 确保响应格式正确
print(response)
print(11111111111111111)
if len(response) != 3: # 如果列表为空,表示没有工具调用,这不是错误
return False, [], f"LLM响应元素数量不正确: 预期3个元素实际{len(response)}" if not tool_calls:
return True, [], "工具调用列表为空"
# 提取工具调用部分 # 验证每个工具调用的格式
tool_calls = response[2]
# 检查工具调用是否有效
if tool_calls is None:
return False, [], "工具调用部分为None"
if not isinstance(tool_calls, list):
return False, [], f"工具调用部分不是列表: {type(tool_calls).__name__}"
if len(tool_calls) == 0:
return False, [], "工具调用列表为空"
# 检查工具调用是否格式正确
valid_tool_calls = [] valid_tool_calls = []
for i, tool_call in enumerate(tool_calls): for i, tool_call in enumerate(tool_calls):
if not isinstance(tool_call, dict): if not isinstance(tool_call, dict):
logger.warning(f"{log_prefix}工具调用[{i}]不是字典: {type(tool_call).__name__}") logger.warning(f"{log_prefix}工具调用[{i}]不是字典: {type(tool_call).__name__}, 内容: {tool_call}")
continue continue
# 检查基本结构
if tool_call.get("type") != "function": if tool_call.get("type") != "function":
logger.warning(f"{log_prefix}工具调用[{i}]不是函数类型: {tool_call.get('type', '未知')}") logger.warning(
f"{log_prefix}工具调用[{i}]不是function类型: type={tool_call.get('type', '未定义')}, 内容: {tool_call}"
)
continue continue
if "function" not in tool_call or not isinstance(tool_call["function"], dict): if "function" not in tool_call or not isinstance(tool_call.get("function"), dict):
logger.warning(f"{log_prefix}工具调用[{i}]缺少function字段或格式不正确") logger.warning(f"{log_prefix}工具调用[{i}]缺少'function'字段或其类型不正确: {tool_call}")
continue
func_details = tool_call["function"]
if "name" not in func_details or not isinstance(func_details.get("name"), str):
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'name'或类型不正确: {func_details}")
continue
if "arguments" not in func_details or not isinstance(
func_details.get("arguments"), str
): # 参数是字符串形式的JSON
logger.warning(f"{log_prefix}工具调用[{i}]的'function'字段缺少'arguments'或类型不正确: {func_details}")
continue
# 可选尝试解析参数JSON确保其有效
args_str = func_details["arguments"]
try:
json.loads(args_str) # 尝试解析,但不存储结果
except json.JSONDecodeError as e:
logger.warning(
f"{log_prefix}工具调用[{i}]的'arguments'不是有效的JSON字符串: {e}, 内容: {args_str[:100]}..."
)
continue
except Exception as e:
logger.warning(f"{log_prefix}解析工具调用[{i}]的'arguments'时发生意外错误: {e}, 内容: {args_str[:100]}...")
continue continue
valid_tool_calls.append(tool_call) valid_tool_calls.append(tool_call)
# 检查是否有有效的工具调用 if not valid_tool_calls and tool_calls: # 如果原始列表不为空,但验证后为空
if not valid_tool_calls: return False, [], "所有工具调用格式均无效"
return False, [], "没有找到有效的工具调用"
return True, valid_tool_calls, "" return True, valid_tool_calls, ""
def process_llm_tool_response(
response: Any, expected_tool_name: str = None, log_prefix: str = ""
) -> Tuple[bool, Dict[str, Any], str]:
"""
处理LLM返回的工具调用响应进行常见错误检查并提取参数
参数:
response: LLM的响应预期是[content, reasoning, tool_calls]格式的列表或元组
expected_tool_name: 预期的工具名称如不指定则不检查
log_prefix: 日志前缀用于标识日志来源
返回:
三元组(成功标志, 参数字典, 错误描述)
- 如果成功解析返回(True, 参数字典, "")
- 如果解析失败返回(False, {}, 错误描述)
"""
# 使用新的标准化函数
success, normalized_response, error_msg = normalize_llm_response(response, log_prefix)
if not success:
return False, {}, error_msg
# 新增检查:确保响应包含预期的工具调用部分
if len(normalized_response) != 3:
# 如果长度不为3说明LLM响应不包含工具调用部分这在期望工具调用的上下文中是错误的
error_msg = (
f"LLM响应未包含预期的工具调用部分: 元素数量{len(normalized_response)},响应内容:{normalized_response}"
)
logger.warning(f"{log_prefix}{error_msg}")
return False, {}, error_msg
# 使用新的工具调用处理函数
# 此时已知 normalized_response 长度必定为 3
success, valid_tool_calls, error_msg = process_llm_tool_calls(normalized_response, log_prefix)
if not success:
return False, {}, error_msg
# 检查是否有工具调用
if not valid_tool_calls:
return False, {}, "没有有效的工具调用"
# 获取第一个工具调用
tool_call = valid_tool_calls[0]
# 检查工具名称(如果提供了预期名称)
if expected_tool_name:
actual_name = tool_call.get("function", {}).get("name")
if actual_name != expected_tool_name:
return False, {}, f"工具名称不匹配: 预期'{expected_tool_name}',实际'{actual_name}'"
# 提取并解析参数
try:
arguments = extract_tool_call_arguments(tool_call, {})
return True, arguments, ""
except Exception as e:
logger.error(f"{log_prefix}解析工具参数时出错: {e}")
return False, {}, f"解析参数失败: {str(e)}"

View File

@ -1,5 +1,5 @@
[inner] [inner]
version = "1.5.0" version = "1.5.1"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更 #如果你想要修改配置文件请在修改后将version的值进行变更
@ -48,12 +48,10 @@ personality_sides = [
identity_detail = [ identity_detail = [
"身份特点", "身份特点",
"身份特点", "身份特点",
]# 条数任意不能为0, 该选项还在调试中,可能未完全生效 ]# 条数任意不能为0, 该选项还在调试中
#外貌特征 #外貌特征
height = 170 # 身高 单位厘米 该选项还在调试中,暂时未生效 age = 20 # 年龄 单位岁
weight = 50 # 体重 单位千克 该选项还在调试中,暂时未生效 gender = "男" # 性别
age = 20 # 年龄 单位岁 该选项还在调试中,暂时未生效
gender = "男" # 性别 该选项还在调试中,暂时未生效
appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效 appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效
[schedule] [schedule]
@ -129,15 +127,19 @@ check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要
[memory] [memory]
build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多
build_memory_distribution = [4.0,2.0,0.6,24.0,8.0,0.4] # 记忆构建分布参数分布1均值标准差权重分布2均值标准差权重 build_memory_distribution = [6.0,3.0,0.6,32.0,12.0,0.4] # 记忆构建分布参数分布1均值标准差权重分布2均值标准差权重
build_memory_sample_num = 10 # 采样数量,数值越高记忆采样次数越多 build_memory_sample_num = 8 # 采样数量,数值越高记忆采样次数越多
build_memory_sample_length = 20 # 采样长度,数值越高一段记忆内容越丰富 build_memory_sample_length = 40 # 采样长度,数值越高一段记忆内容越丰富
memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多
forget_memory_interval = 1000 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 forget_memory_interval = 1000 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习
memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时 memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时
memory_forget_percentage = 0.01 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 memory_forget_percentage = 0.01 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认
consolidate_memory_interval = 1000 # 记忆整合间隔 单位秒 间隔越低,麦麦整合越频繁,记忆更精简
consolidation_similarity_threshold = 0.7 # 相似度阈值
consolidation_check_percentage = 0.01 # 检查节点比例
#不希望记忆的词,已经记忆的不会受到影响 #不希望记忆的词,已经记忆的不会受到影响
memory_ban_words = [ memory_ban_words = [
# "403","张三" # "403","张三"

View File

@ -1,351 +0,0 @@
import asyncio
import time
from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config
from src.do_tool.tool_use import ToolUser
import statistics
import json
async def run_test(test_name, test_function, iterations=5):
"""
运行指定次数的测试并计算平均响应时间
参数:
test_name: 测试名称
test_function: 要执行的测试函数
iterations: 测试迭代次数
返回:
测试结果统计
"""
print(f"开始 {test_name} 测试({iterations}次迭代)...")
times = []
responses = []
for i in range(iterations):
print(f" 运行第 {i + 1}/{iterations} 次测试...")
start_time = time.time()
response = await test_function()
end_time = time.time()
elapsed = end_time - start_time
times.append(elapsed)
responses.append(response)
print(f" - 耗时: {elapsed:.2f}")
results = {
"平均耗时": statistics.mean(times),
"最短耗时": min(times),
"最长耗时": max(times),
"标准差": statistics.stdev(times) if len(times) > 1 else 0,
"所有耗时": times,
"响应结果": responses,
}
return results
async def test_with_tool_calls():
"""使用工具调用的LLM请求测试"""
# 创建LLM模型实例
llm_model = LLMRequest(
model=global_config.llm_sub_heartflow,
# model = global_config.llm_tool_use,
# temperature=global_config.llm_sub_heartflow["temp"],
max_tokens=800,
request_type="benchmark_test",
)
# 创建工具实例
tool_instance = ToolUser()
tools = tool_instance._define_tools()
# 简单的测试提示词
prompt = "请分析当前天气情况并查询今日历史上的重要事件。并且3.9和3.11谁比较大?请使用适当的工具来获取这些信息。"
prompt = """
你的名字是麦麦你包容开放情绪敏感有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生现在在读大二你会刷贴吧有时候会喜欢说一些奇怪的话喜欢刷小红书
刚刚你的内心想法是漂移菌提到罐罐被吃完了可以顺着这个梗继续玩一下比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"不过薯薯和薯宝之前已经接了不少梗漂移菌刚刚也参与了可能话题热度还在可以再互动一下如果没人接话或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历换个轻松的话题方向
暂时不需要使用工具
-----------------------------------
现在是2025-04-25 17:38:37你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
2025-04-25 17:34:08麦麦() :[表达了顽皮嬉戏];
2025-04-25 17:34:39漂移菌 :@麦麦id:3936257206 你是一只猫娘;
2025-04-25 17:34:42薯宝 :🤣;
2025-04-25 17:34:43麦麦() :行啊 工资分我一半;
2025-04-25 17:34:43麦麦() :我帮你写bug;
2025-04-25 17:34:43麦麦() :[表达了悲伤绝望无奈无力];
2025-04-25 17:34:53薯薯 :;
2025-04-25 17:35:03既文横 :麦麦你是一只猫娘程序员猫娘是不需要工资;
2025-04-25 17:35:20薯宝 :[图片图片内容一只卡通风格的灰色猫咪眼睛闭着表情显得很平静图片下方有"死了"两个字
图片含义猜测这可能是一个幽默的表达用来形容某人或某事处于非常平静的状态仿佛已经""了一样] hfc这周真能出来吗...;
2025-04-25 17:35:34薯宝 :[表情包搞笑滑稽讽刺幽默];
2025-04-25 17:36:25麦麦() :喵喵;
2025-04-25 17:36:25麦麦() :代码写完了;
2025-04-25 17:36:25麦麦() :罐罐拿来;
2025-04-25 17:36:25麦麦() :[表达了悲伤绝望无奈无力];
2025-04-25 17:36:41薯薯 :好可爱;
2025-04-25 17:37:05薯薯 :脑补出来认真营业了一天等待主人发放奖励的小猫咪;
2025-04-25 17:37:25薯宝 :敷衍营业bushi;
2025-04-25 17:37:54漂移菌 :回复麦麦的消息(罐罐拿来)猫娘我昨晚上太饿吃完了;
--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) ---
--- 以下新消息未读---
2025-04-25 17:38:29麦麦() :那今晚的猫条是不是也要被克扣了;
2025-04-25 17:38:29麦麦() :[表达了幽默自嘲无奈父子关系编程笑话];
你现在当前心情平静
现在请你生成你的内心想法要求思考群里正在进行的话题之前大家聊过的话题群里成员的关系请你思考要不要对群里的话题进行回复以及如何对群聊内容进行回复
回复的要求是不要总是重复自己提到过的话题如果你要回复最好只回复一个人的一个话题
如果最后一条消息是你自己发的观察到的内容只有你自己的发言并且之后没有人回复你不要回复如果聊天记录中最新的消息是你自己发送的并且你还想继续回复你应该紧紧衔接你发送的消息进行话题的深入补充或追问等等请注意不要输出多余内容(包括前后缀冒号和引号括号 表情)不要回复自己的发言
现在请你先输出想法生成你在这个聊天中的想法在原来的想法上尝试新的话题不要分点输出,文字不要浮夸在输出完想法后请你思考应该使用什么工具工具可以帮你取得一些你不知道的信息或者进行一些操作如果你需要做某件事来对消息和你的回复进行处理请使用工具"""
# 发送带有工具调用的请求
response = await llm_model.generate_response_tool_async(prompt=prompt, tools=tools)
result_info = {}
# 简单处理工具调用结果
if len(response) == 3:
content, reasoning_content, tool_calls = response
tool_calls_count = len(tool_calls) if tool_calls else 0
print(f" 工具调用请求生成了 {tool_calls_count} 个工具调用")
# 输出内容和工具调用详情
print("\n 生成的内容:")
print(f" {content[:200]}..." if len(content) > 200 else f" {content}")
if tool_calls:
print("\n 工具调用详情:")
for i, tool_call in enumerate(tool_calls):
tool_name = tool_call["function"]["name"]
tool_params = tool_call["function"].get("arguments", {})
print(f" - 工具 {i + 1}: {tool_name}")
print(
f" 参数: {json.dumps(tool_params, ensure_ascii=False)[:100]}..."
if len(json.dumps(tool_params, ensure_ascii=False)) > 100
else f" 参数: {json.dumps(tool_params, ensure_ascii=False)}"
)
result_info = {"内容": content, "推理内容": reasoning_content, "工具调用": tool_calls}
else:
content, reasoning_content = response
print(" 工具调用请求未生成任何工具调用")
print("\n 生成的内容:")
print(f" {content[:200]}..." if len(content) > 200 else f" {content}")
result_info = {"内容": content, "推理内容": reasoning_content, "工具调用": []}
return result_info
async def test_without_tool_calls():
"""不使用工具调用的LLM请求测试"""
# 创建LLM模型实例
llm_model = LLMRequest(
model=global_config.llm_sub_heartflow,
temperature=global_config.llm_sub_heartflow["temp"],
max_tokens=800,
request_type="benchmark_test",
)
# 简单的测试提示词(与工具调用相同,以便公平比较)
prompt = """
你的名字是麦麦你包容开放情绪敏感有时候有些搞怪幽默, 是一个学习心理学和脑科学的女大学生现在在读大二你会刷贴吧有时候会喜欢说一些奇怪的话喜欢刷小红书
刚刚你的内心想法是漂移菌提到罐罐被吃完了可以顺着这个梗继续玩一下比如假装委屈"那今晚的加班费是不是也要被吃掉了"或者"猫娘罢工警告"不过薯薯和薯宝之前已经接了不少梗漂移菌刚刚也参与了可能话题热度还在可以再互动一下如果没人接话或许可以问问大家有没有遇到过类似"代码写完但奖励被吃掉"的搞笑职场经历换个轻松的话题方向
暂时不需要使用工具
-----------------------------------
现在是2025-04-25 17:38:37你正在上网和qq群里的网友们聊天以下是正在进行的聊天内容
2025-04-25 17:34:08麦麦() :[表达了顽皮嬉戏];
2025-04-25 17:34:39漂移菌 :@麦麦id:3936257206 你是一只猫娘;
2025-04-25 17:34:42薯宝 :🤣;
2025-04-25 17:34:43麦麦() :行啊 工资分我一半;
2025-04-25 17:34:43麦麦() :我帮你写bug;
2025-04-25 17:34:43麦麦() :[表达了悲伤绝望无奈无力];
2025-04-25 17:34:53薯薯 :;
2025-04-25 17:35:03既文横 :麦麦你是一只猫娘程序员猫娘是不需要工资;
2025-04-25 17:35:20薯宝 :[图片图片内容一只卡通风格的灰色猫咪眼睛闭着表情显得很平静图片下方有"死了"两个字
图片含义猜测这可能是一个幽默的表达用来形容某人或某事处于非常平静的状态仿佛已经""了一样] hfc这周真能出来吗...;
2025-04-25 17:35:34薯宝 :[表情包搞笑滑稽讽刺幽默];
2025-04-25 17:36:25麦麦() :喵喵;
2025-04-25 17:36:25麦麦() :代码写完了;
2025-04-25 17:36:25麦麦() :罐罐拿来;
2025-04-25 17:36:25麦麦() :[表达了悲伤绝望无奈无力];
2025-04-25 17:36:41薯薯 :好可爱;
2025-04-25 17:37:05薯薯 :脑补出来认真营业了一天等待主人发放奖励的小猫咪;
2025-04-25 17:37:25薯宝 :敷衍营业bushi;
2025-04-25 17:37:54漂移菌 :回复麦麦的消息(罐罐拿来)猫娘我昨晚上太饿吃完了;
--- 以上消息已读 (标记时间: 2025-04-25 17:37:54) ---
--- 以下新消息未读---
2025-04-25 17:38:29麦麦() :那今晚的猫条是不是也要被克扣了;
2025-04-25 17:38:29麦麦() :[表达了幽默自嘲无奈父子关系编程笑话];
你现在当前心情平静
现在请你生成你的内心想法要求思考群里正在进行的话题之前大家聊过的话题群里成员的关系请你思考要不要对群里的话题进行回复以及如何对群聊内容进行回复
回复的要求是不要总是重复自己提到过的话题如果你要回复最好只回复一个人的一个话题
如果最后一条消息是你自己发的观察到的内容只有你自己的发言并且之后没有人回复你不要回复如果聊天记录中最新的消息是你自己发送的并且你还想继续回复你应该紧紧衔接你发送的消息进行话题的深入补充或追问等等请注意不要输出多余内容(包括前后缀冒号和引号括号 表情)不要回复自己的发言
现在请你先输出想法生成你在这个聊天中的想法在原来的想法上尝试新的话题不要分点输出,文字不要浮夸在输出完想法后请你思考应该使用什么工具工具可以帮你取得一些你不知道的信息或者进行一些操作如果你需要做某件事来对消息和你的回复进行处理请使用工具"""
# 发送不带工具调用的请求
response, reasoning_content = await llm_model.generate_response_async(prompt)
# 输出生成的内容
print("\n 生成的内容:")
print(f" {response[:200]}..." if len(response) > 200 else f" {response}")
result_info = {"内容": response, "推理内容": reasoning_content, "工具调用": []}
return result_info
async def run_alternating_tests(iterations=5):
"""
交替运行两种测试方法每种方法运行指定次数
参数:
iterations: 每种测试方法运行的次数
返回:
包含两种测试方法结果的元组
"""
print(f"开始交替测试(每种方法{iterations}次)...")
# 初始化结果列表
times_without_tools = []
times_with_tools = []
responses_without_tools = []
responses_with_tools = []
for i in range(iterations):
print(f"\n{i + 1}/{iterations} 轮交替测试")
# 不使用工具的测试
print("\n 执行不使用工具调用的测试...")
start_time = time.time()
response = await test_without_tool_calls()
end_time = time.time()
elapsed = end_time - start_time
times_without_tools.append(elapsed)
responses_without_tools.append(response)
print(f" - 耗时: {elapsed:.2f}")
# 使用工具的测试
print("\n 执行使用工具调用的测试...")
start_time = time.time()
response = await test_with_tool_calls()
end_time = time.time()
elapsed = end_time - start_time
times_with_tools.append(elapsed)
responses_with_tools.append(response)
print(f" - 耗时: {elapsed:.2f}")
# 计算统计数据
results_without_tools = {
"平均耗时": statistics.mean(times_without_tools),
"最短耗时": min(times_without_tools),
"最长耗时": max(times_without_tools),
"标准差": statistics.stdev(times_without_tools) if len(times_without_tools) > 1 else 0,
"所有耗时": times_without_tools,
"响应结果": responses_without_tools,
}
results_with_tools = {
"平均耗时": statistics.mean(times_with_tools),
"最短耗时": min(times_with_tools),
"最长耗时": max(times_with_tools),
"标准差": statistics.stdev(times_with_tools) if len(times_with_tools) > 1 else 0,
"所有耗时": times_with_tools,
"响应结果": responses_with_tools,
}
return results_without_tools, results_with_tools
async def main():
"""主测试函数"""
print("=" * 50)
print("LLM工具调用与普通请求性能比较测试")
print("=" * 50)
# 设置测试迭代次数
iterations = 10
# 执行交替测试
results_without_tools, results_with_tools = await run_alternating_tests(iterations)
# 显示结果比较
print("\n" + "=" * 50)
print("测试结果比较")
print("=" * 50)
print("\n不使用工具调用:")
for key, value in results_without_tools.items():
if key == "所有耗时":
print(f" {key}: {[f'{t:.2f}' for t in value]}")
elif key == "响应结果":
print(f" {key}: [内容已省略,详见结果文件]")
else:
print(f" {key}: {value:.2f}")
print("\n使用工具调用:")
for key, value in results_with_tools.items():
if key == "所有耗时":
print(f" {key}: {[f'{t:.2f}' for t in value]}")
elif key == "响应结果":
tool_calls_counts = [len(res.get("工具调用", [])) for res in value]
print(f" {key}: [内容已省略,详见结果文件]")
print(f" 工具调用数量: {tool_calls_counts}")
else:
print(f" {key}: {value:.2f}")
# 计算差异百分比
diff_percent = ((results_with_tools["平均耗时"] / results_without_tools["平均耗时"]) - 1) * 100
print(f"\n工具调用比普通请求平均耗时相差: {diff_percent:.2f}%")
# 保存结果到JSON文件
results = {
"测试时间": time.strftime("%Y-%m-%d %H:%M:%S"),
"测试迭代次数": iterations,
"不使用工具调用": {
k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v])
for k, v in results_without_tools.items()
if k != "响应结果"
},
"不使用工具调用_详细响应": [
{
"内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"],
"推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"],
}
for resp in results_without_tools["响应结果"]
],
"使用工具调用": {
k: (v if k != "所有耗时" else [float(f"{t:.2f}") for t in v])
for k, v in results_with_tools.items()
if k != "响应结果"
},
"使用工具调用_详细响应": [
{
"内容摘要": resp["内容"][:200] + "..." if len(resp["内容"]) > 200 else resp["内容"],
"推理内容摘要": resp["推理内容"][:200] + "..." if len(resp["推理内容"]) > 200 else resp["推理内容"],
"工具调用数量": len(resp["工具调用"]),
"工具调用详情": [
{"工具名称": tool["function"]["name"], "参数": tool["function"].get("arguments", {})}
for tool in resp["工具调用"]
],
}
for resp in results_with_tools["响应结果"]
],
"差异百分比": float(f"{diff_percent:.2f}"),
}
with open("llm_tool_benchmark_results.json", "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print("\n测试结果已保存到 llm_tool_benchmark_results.json")
if __name__ == "__main__":
asyncio.run(main())