mirror of https://github.com/Mai-with-u/MaiBot.git
Merge branch 'dev' of https://github.com/SnowindMe/MaiBot into dev
commit
b72d49b99e
|
|
@ -19,7 +19,7 @@ if %ERRORLEVEL% neq 0 (
|
|||
)
|
||||
|
||||
REM 运行预处理脚本
|
||||
python "%~dp0raw_data_preprocessor.py"
|
||||
python "%~dp0scripts\raw_data_preprocessor.py"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo 错误: raw_data_preprocessor.py 执行失败
|
||||
pause
|
||||
|
|
@ -27,7 +27,7 @@ if %ERRORLEVEL% neq 0 (
|
|||
)
|
||||
|
||||
REM 运行信息提取脚本
|
||||
python "%~dp0info_extraction.py"
|
||||
python "%~dp0scripts\info_extraction.py"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo 错误: info_extraction.py 执行失败
|
||||
pause
|
||||
|
|
@ -35,7 +35,7 @@ if %ERRORLEVEL% neq 0 (
|
|||
)
|
||||
|
||||
REM 运行OpenIE导入脚本
|
||||
python "%~dp0import_openie.py"
|
||||
python "%~dp0scripts\import_openie.py"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo 错误: import_openie.py 执行失败
|
||||
pause
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ mongodb/
|
|||
NapCat.Framework.Windows.Once/
|
||||
log/
|
||||
logs/
|
||||
tool_call_benchmark.py
|
||||
run_ad.bat
|
||||
MaiBot-Napcat-Adapter-main
|
||||
MaiBot-Napcat-Adapter
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ graph TD
|
|||
|
||||
## ✍️如何给本项目报告BUG/提交建议/做贡献
|
||||
|
||||
MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交bug报告、功能需求还是代码pr,都对项目非常宝贵。我们非常感谢你的支持!🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](CONTRIBUTE.md)(待补完)
|
||||
MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交bug报告、功能需求还是代码pr,都对项目非常宝贵。我们非常感谢你的支持!🎉 但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](depends-data/CONTRIBUTE.md)(待补完)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,7 +4,10 @@
|
|||
# print("未找到quick_algo库,无法使用quick_algo算法")
|
||||
# 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 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.plugins.knowledge.src.utils.hash import get_sha256
|
||||
|
||||
# 添加在现有导入之后
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到 sys.path
|
||||
|
||||
|
||||
logger = get_module_logger("LPMM知识库-OpenIE导入")
|
||||
|
||||
|
|
@ -5,6 +5,9 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
|
|||
from threading import Lock, Event
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
# 添加项目根目录到 sys.path
|
||||
|
||||
import tqdm
|
||||
|
||||
from src.common.logger import get_module_logger
|
||||
|
|
@ -2,10 +2,14 @@ import json
|
|||
import os
|
||||
from pathlib import Path
|
||||
import sys # 新增系统模块导入
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
from src.common.logger import get_module_logger
|
||||
|
||||
logger = get_module_logger("LPMM数据库-原始数据处理")
|
||||
|
||||
# 添加项目根目录到 sys.path
|
||||
|
||||
|
||||
def check_and_create_dirs():
|
||||
"""检查并创建必要的目录"""
|
||||
|
|
@ -235,6 +235,10 @@ class BotConfig:
|
|||
memory_forget_time: int = 24 # 记忆遗忘时间(小时)
|
||||
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(
|
||||
default_factory=lambda: ["表情包", "图片", "回复", "聊天记录"]
|
||||
) # 添加新的配置项默认值
|
||||
|
|
@ -594,6 +598,16 @@ class BotConfig:
|
|||
config.build_memory_sample_length = memory_config.get(
|
||||
"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):
|
||||
remote_config = parent["remote"]
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ from src.plugins.moods.moods import MoodManager
|
|||
logger = get_logger("mai_state")
|
||||
|
||||
|
||||
# enable_unlimited_hfc_chat = True
|
||||
enable_unlimited_hfc_chat = False
|
||||
enable_unlimited_hfc_chat = True
|
||||
# enable_unlimited_hfc_chat = False
|
||||
|
||||
|
||||
class MaiState(enum.Enum):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from src.individuality.individuality import Individuality
|
|||
import random
|
||||
from ..plugins.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
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.plugins.chat.chat_stream import chat_manager
|
||||
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
|
||||
|
|
@ -20,14 +20,12 @@ logger = get_logger("sub_heartflow")
|
|||
def init_prompt():
|
||||
prompt = ""
|
||||
prompt += "{extra_info}\n"
|
||||
prompt += "{prompt_personality}\n"
|
||||
prompt += "你的名字是{bot_name},{prompt_personality}\n"
|
||||
prompt += "{last_loop_prompt}\n"
|
||||
prompt += "{cycle_info_block}\n"
|
||||
prompt += "现在是{time_now},你正在上网,和qq群里的网友们聊天,以下是正在进行的聊天内容:\n{chat_observe_info}\n"
|
||||
prompt += "\n你现在{mood_info}\n"
|
||||
prompt += (
|
||||
"请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。"
|
||||
)
|
||||
prompt += "请仔细阅读当前群聊内容,分析讨论话题和群成员关系,分析你刚刚发言和别人对你的发言的反应,思考你要不要回复。然后思考你是否需要使用函数工具。"
|
||||
prompt += "思考并输出你的内心想法\n"
|
||||
prompt += "输出要求:\n"
|
||||
prompt += "1. 根据聊天内容生成你的想法,{hf_do_next}\n"
|
||||
|
|
@ -67,6 +65,9 @@ class SubMind:
|
|||
self.past_mind = []
|
||||
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):
|
||||
"""
|
||||
在回复前进行思考,生成内心想法并收集工具调用结果
|
||||
|
|
@ -85,7 +86,7 @@ class SubMind:
|
|||
# 获取观察对象
|
||||
observation = self.observations[0]
|
||||
if not observation:
|
||||
logger.error(f"[{self.subheartflow_id}] 无法获取观察对象")
|
||||
logger.error(f"{self.log_prefix} 无法获取观察对象")
|
||||
self.update_current_mind("(我没看到任何聊天内容...)")
|
||||
return self.current_mind, self.past_mind
|
||||
|
||||
|
|
@ -101,18 +102,7 @@ class SubMind:
|
|||
individuality = Individuality.get_instance()
|
||||
|
||||
# 构建个性部分
|
||||
prompt_personality = f"你正在扮演名为{individuality.personality.bot_nickname}的人类,你"
|
||||
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}"
|
||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||
|
||||
# 获取当前时间
|
||||
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(
|
||||
extra_info="", # 可以在这里添加额外信息
|
||||
prompt_personality=prompt_personality,
|
||||
bot_name=individuality.personality.bot_nickname,
|
||||
bot_name=individuality.name,
|
||||
time_now=time_now,
|
||||
chat_observe_info=chat_observe_info,
|
||||
mood_info=mood_info,
|
||||
|
|
@ -223,57 +213,48 @@ class SubMind:
|
|||
|
||||
try:
|
||||
# 调用LLM生成响应
|
||||
response = 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}] "
|
||||
response, _reasoning_content, tool_calls = await self.llm_model.generate_response_tool_async(
|
||||
prompt=prompt, tools=tools
|
||||
)
|
||||
|
||||
if not success:
|
||||
# 处理标准化失败情况
|
||||
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 ""
|
||||
logger.debug(f"{self.log_prefix} 子心流输出的原始LLM响应: {response}")
|
||||
|
||||
# 处理可能的工具调用
|
||||
if len(normalized_response) == 3:
|
||||
# 提取并验证工具调用
|
||||
success, valid_tool_calls, error_msg = process_llm_tool_calls(
|
||||
normalized_response, log_prefix=f"[{self.subheartflow_id}] "
|
||||
# 直接使用LLM返回的文本响应作为 content
|
||||
content = response if response else ""
|
||||
|
||||
if tool_calls:
|
||||
# 直接将 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:
|
||||
# 记录工具调用信息
|
||||
tool_calls_str = ", ".join(
|
||||
[call.get("function", {}).get("name", "未知工具") for call in valid_tool_calls]
|
||||
)
|
||||
logger.info(
|
||||
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.log_prefix} 处理工具调用时出错: {error_msg}")
|
||||
else:
|
||||
logger.info(f"{self.log_prefix} 心流未使用工具") # 修改日志信息,明确是未使用工具而不是未处理
|
||||
|
||||
# 收集工具执行结果
|
||||
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:
|
||||
# 处理总体异常
|
||||
logger.error(f"[{self.subheartflow_id}] 执行LLM请求或处理响应时出错: {e}")
|
||||
logger.error(f"{self.log_prefix} 执行LLM请求或处理响应时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
content = "思考过程中出现错误"
|
||||
|
||||
# 记录最终思考结果
|
||||
name = chat_manager.get_stream_name(self.subheartflow_id)
|
||||
logger.debug(f"[{name}] \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n")
|
||||
logger.debug(f"{self.log_prefix} \nPrompt:\n{prompt}\n\n心流思考结果:\n{content}\n")
|
||||
|
||||
# 处理空响应情况
|
||||
if not content:
|
||||
content = "(不知道该想些什么...)"
|
||||
logger.warning(f"[{self.subheartflow_id}] LLM返回空结果,思考失败。")
|
||||
logger.warning(f"{self.log_prefix} LLM返回空结果,思考失败。")
|
||||
|
||||
# ---------- 6. 更新思考状态并返回结果 ----------
|
||||
# 更新当前思考内容
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
import random
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -86,27 +85,6 @@ class Identity:
|
|||
instance.appearance = appearance
|
||||
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:
|
||||
"""将身份特征转换为字典格式"""
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Optional
|
||||
from .personality import Personality
|
||||
from .identity import Identity
|
||||
import random
|
||||
|
||||
|
||||
class Individuality:
|
||||
|
|
@ -8,15 +9,16 @@ class Individuality:
|
|||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if Individuality._instance is not None:
|
||||
raise RuntimeError("Individuality 类是单例,请使用 get_instance() 方法获取实例。")
|
||||
|
||||
# 正常初始化实例属性
|
||||
self.personality: Optional[Personality] = None
|
||||
self.identity: Optional[Identity] = None
|
||||
|
||||
self.name = ""
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "Individuality":
|
||||
"""获取Individuality单例实例
|
||||
|
|
@ -25,7 +27,13 @@ class Individuality:
|
|||
Individuality: 单例实例
|
||||
"""
|
||||
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
|
||||
|
||||
def initialize(
|
||||
|
|
@ -63,6 +71,8 @@ class Individuality:
|
|||
identity_detail=identity_detail, height=height, weight=weight, age=age, gender=gender, appearance=appearance
|
||||
)
|
||||
|
||||
self.name = bot_nickname
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""将个体特征转换为字典格式"""
|
||||
return {
|
||||
|
|
@ -80,16 +90,148 @@ class Individuality:
|
|||
instance.identity = Identity.from_dict(data["identity"])
|
||||
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":
|
||||
return self.personality.get_prompt(x_person, level)
|
||||
elif type == "identity":
|
||||
return self.identity.get_prompt(x_person, level)
|
||||
if x_person not in [0, 1, 2]:
|
||||
return "无效的人称代词,请使用 0 (无人称), 1 (我) 或 2 (你)。"
|
||||
if not self.personality:
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from dataclasses import dataclass
|
|||
from typing import Dict, List
|
||||
import json
|
||||
from pathlib import Path
|
||||
import random
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -119,28 +118,3 @@ class Personality:
|
|||
for key, value in data.items():
|
||||
setattr(instance, key, value)
|
||||
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
|
||||
|
|
|
|||
12
src/main.py
12
src/main.py
|
|
@ -83,7 +83,7 @@ class MainSystem:
|
|||
)
|
||||
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)
|
||||
|
||||
# 初始化个体特征
|
||||
|
|
@ -121,6 +121,7 @@ class MainSystem:
|
|||
tasks = [
|
||||
self.build_memory_task(),
|
||||
self.forget_memory_task(),
|
||||
self.consolidate_memory_task(),
|
||||
self.print_mood_task(),
|
||||
self.remove_recalled_message_task(),
|
||||
emoji_manager.start_periodic_check_register(),
|
||||
|
|
@ -146,6 +147,15 @@ class MainSystem:
|
|||
await HippocampusManager.get_instance().forget_memory(percentage=global_config.memory_forget_percentage)
|
||||
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):
|
||||
"""打印情绪状态"""
|
||||
while True:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import time
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Optional # 增加了 Optional
|
||||
from src.common.logger_manager import get_logger
|
||||
from ..models.utils_model import LLMRequest
|
||||
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")
|
||||
|
||||
|
||||
# 注意:这个 ActionPlannerInfo 类似乎没有在 ActionPlanner 中使用,
|
||||
# 如果确实没用,可以考虑移除,但暂时保留以防万一。
|
||||
class ActionPlannerInfo:
|
||||
def __init__(self):
|
||||
self.done_action = []
|
||||
self.goal_list = []
|
||||
self.knowledge_list = []
|
||||
self.memory_list = []
|
||||
# --- 定义 Prompt 模板 ---
|
||||
|
||||
# Prompt(1): 首次回复或非连续回复时的决策 Prompt
|
||||
PROMPT_INITIAL_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以回复,可以倾听,可以调取知识,甚至可以屏蔽对方:
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str}
|
||||
|
||||
【最近行动历史概要】
|
||||
{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 类定义,顶格
|
||||
class ActionPlanner:
|
||||
"""行动规划器"""
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
self.llm = LLMRequest(
|
||||
model=global_config.llm_PFC_action_planner,
|
||||
temperature=global_config.llm_PFC_action_planner["temp"],
|
||||
max_tokens=1500,
|
||||
request_type="action_planning",
|
||||
)
|
||||
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.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
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:
|
||||
observation_info: 决策信息
|
||||
conversation_info: 对话信息
|
||||
last_successful_reply_action: 上一次成功的回复动作类型 ('direct_reply' 或 'send_new_message' 或 None)
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: (行动类型, 行动原因)
|
||||
"""
|
||||
# --- 获取 Bot 上次发言时间信息 ---
|
||||
# (这部分逻辑不变)
|
||||
time_since_last_bot_message_info = ""
|
||||
try:
|
||||
bot_id = str(global_config.BOT_QQ)
|
||||
|
|
@ -70,59 +137,73 @@ class ActionPlanner:
|
|||
)
|
||||
break
|
||||
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:
|
||||
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:
|
||||
logger.warning(f"获取 Bot 上次发言时间时出错: {e}")
|
||||
# --- 获取 Bot 上次发言时间信息结束 ---
|
||||
logger.warning(f"[私聊][{self.private_name}]获取 Bot 上次发言时间时出错: {e}")
|
||||
|
||||
# --- 获取超时提示信息 ---
|
||||
# (这部分逻辑不变)
|
||||
timeout_context = ""
|
||||
try: # 添加 try-except 以增加健壮性
|
||||
try:
|
||||
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
|
||||
last_goal_tuple = conversation_info.goal_list[-1]
|
||||
if isinstance(last_goal_tuple, tuple) and len(last_goal_tuple) > 0:
|
||||
last_goal_text = last_goal_tuple[0]
|
||||
last_goal_dict = conversation_info.goal_list[-1]
|
||||
if isinstance(last_goal_dict, dict) and "goal" in last_goal_dict:
|
||||
last_goal_text = last_goal_dict["goal"]
|
||||
if isinstance(last_goal_text, str) and "分钟,思考接下来要做什么" in last_goal_text:
|
||||
try:
|
||||
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:
|
||||
timeout_context = "重要提示:你刚刚因为对方长时间没有回复而结束了等待,这可能代表在对方看来本次聊天已结束,请基于此情况规划下一步,不要重复等待前的发言。\n"
|
||||
timeout_context = "重要提示:对方已经长时间没有回复你的消息了(这可能代表对方繁忙/不想回复/没注意到你的消息等情况,或在对方看来本次聊天已告一段落),请基于此情况规划下一步。\n"
|
||||
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:
|
||||
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:
|
||||
logger.warning(f"检查超时目标时出错: {e}")
|
||||
logger.warning(f"[私聊][{self.private_name}]检查超时目标时出错: {e}")
|
||||
|
||||
# 构建提示词
|
||||
logger.debug(f"开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}") # 使用 getattr
|
||||
# --- 构建通用 Prompt 参数 ---
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]开始规划行动:当前目标: {getattr(conversation_info, 'goal_list', '不可用')}"
|
||||
)
|
||||
|
||||
# 构建对话目标 (goals_str)
|
||||
goals_str = ""
|
||||
try: # 添加 try-except
|
||||
try:
|
||||
if hasattr(conversation_info, "goal_list") and conversation_info.goal_list:
|
||||
for goal_reason in conversation_info.goal_list:
|
||||
if isinstance(goal_reason, tuple) and len(goal_reason) > 0:
|
||||
goal = goal_reason[0]
|
||||
reasoning = goal_reason[1] if len(goal_reason) > 1 else "没有明确原因"
|
||||
elif isinstance(goal_reason, dict):
|
||||
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"
|
||||
if not goals_str: # 如果循环后 goals_str 仍为空
|
||||
|
||||
if not goals_str:
|
||||
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
|
||||
else:
|
||||
goals_str = "- 目前没有明确对话目标,请考虑设定一个。\n"
|
||||
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"
|
||||
except Exception as e:
|
||||
logger.error(f"构建对话目标字符串时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]构建对话目标字符串时出错: {e}")
|
||||
goals_str = "- 构建对话目标时出错。\n"
|
||||
|
||||
# 获取聊天历史记录 (chat_history_text)
|
||||
|
|
@ -130,7 +211,7 @@ class ActionPlanner:
|
|||
try:
|
||||
if hasattr(observation_info, "chat_history") and observation_info.chat_history:
|
||||
chat_history_text = observation_info.chat_history_str
|
||||
if not chat_history_text: # 如果历史记录是空列表
|
||||
if not chat_history_text:
|
||||
chat_history_text = "还没有聊天记录。\n"
|
||||
else:
|
||||
chat_history_text = "还没有聊天记录。\n"
|
||||
|
|
@ -148,51 +229,38 @@ class ActionPlanner:
|
|||
chat_history_text += (
|
||||
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:
|
||||
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:
|
||||
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"
|
||||
except Exception as e:
|
||||
logger.error(f"处理聊天记录时发生未知错误: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]处理聊天记录时发生未知错误: {e}")
|
||||
chat_history_text = "处理聊天记录时出错。\n"
|
||||
|
||||
# 构建 Persona 文本 (persona_text)
|
||||
identity_details_only = self.identity_detail_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}。"
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
|
||||
# --- 构建更清晰的行动历史和上一次行动结果 ---
|
||||
# 构建行动历史和上一次行动结果 (action_history_summary, last_action_context)
|
||||
# (这部分逻辑不变)
|
||||
action_history_summary = "你最近执行的行动历史:\n"
|
||||
last_action_context = "关于你【上一次尝试】的行动:\n"
|
||||
|
||||
action_history_list = []
|
||||
try: # 添加 try-except
|
||||
try:
|
||||
if hasattr(conversation_info, "done_action") and conversation_info.done_action:
|
||||
action_history_list = conversation_info.done_action[-5:]
|
||||
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:
|
||||
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:
|
||||
logger.error(f"访问行动历史时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]访问行动历史时出错: {e}")
|
||||
|
||||
if not action_history_list:
|
||||
action_history_summary += "- 还没有执行过行动。\n"
|
||||
|
|
@ -212,14 +280,17 @@ class ActionPlanner:
|
|||
final_reason = action_data.get("final_reason", "")
|
||||
action_time = action_data.get("time", "")
|
||||
elif isinstance(action_data, tuple):
|
||||
# 假设旧格式兼容
|
||||
if len(action_data) > 0:
|
||||
action_type = action_data[0]
|
||||
if len(action_data) > 1:
|
||||
plan_reason = action_data[1]
|
||||
plan_reason = action_data[1] # 可能是规划原因或最终原因
|
||||
if len(action_data) > 2:
|
||||
status = action_data[2]
|
||||
if status == "recall" and len(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 ""
|
||||
summary_line = f"- 时间:{action_time}, 尝试行动:'{action_type}', 状态:{status}{reason_text}"
|
||||
|
|
@ -230,56 +301,46 @@ class ActionPlanner:
|
|||
last_action_context += f"- 当时规划的【原因】是: {plan_reason}\n"
|
||||
if status == "done":
|
||||
last_action_context += "- 该行动已【成功执行】。\n"
|
||||
# 记录这次成功的行动类型,供下次决策
|
||||
# self.last_successful_action_type = action_type # 不在这里记录,由 conversation 控制
|
||||
elif status == "recall":
|
||||
last_action_context += "- 但该行动最终【未能执行/被取消】。\n"
|
||||
if final_reason:
|
||||
last_action_context += f"- 【重要】失败/取消的具体原因是: “{final_reason}”\n"
|
||||
else:
|
||||
last_action_context += "- 【重要】失败/取消原因未明确记录。\n"
|
||||
# self.last_successful_action_type = None # 行动失败,清除记录
|
||||
else:
|
||||
last_action_context += f"- 该行动当前状态: {status}\n"
|
||||
# self.last_successful_action_type = None # 非完成状态,清除记录
|
||||
|
||||
# --- 构建最终的 Prompt ---
|
||||
prompt = f"""{persona_text}。现在你在参与一场QQ私聊,请根据以下【所有信息】审慎且灵活的决策下一步行动,可以发言,可以等待,可以倾听,可以调取知识,甚至可以屏蔽对方:
|
||||
# --- 选择 Prompt ---
|
||||
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 (首次/非连续回复决策)")
|
||||
|
||||
【当前对话目标】
|
||||
{goals_str if goals_str.strip() else "- 目前没有明确对话目标,请考虑设定一个。"}
|
||||
# --- 格式化最终的 Prompt ---
|
||||
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 "还没有聊天记录。",
|
||||
)
|
||||
|
||||
|
||||
【最近行动历史概要】
|
||||
{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}")
|
||||
logger.debug(f"[私聊][{self.private_name}]发送到LLM的最终提示词:\n------\n{prompt}\n------")
|
||||
try:
|
||||
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(
|
||||
content,
|
||||
self.private_name,
|
||||
"action",
|
||||
"reason",
|
||||
default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因,默认等待"},
|
||||
|
|
@ -289,8 +350,10 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在
|
|||
reason = result.get("reason", "LLM未提供原因,默认等待")
|
||||
|
||||
# 验证action类型
|
||||
# 更新 valid_actions 列表以包含 send_new_message
|
||||
valid_actions = [
|
||||
"direct_reply",
|
||||
"send_new_message", # 添加新动作
|
||||
"fetch_knowledge",
|
||||
"wait",
|
||||
"listening",
|
||||
|
|
@ -299,14 +362,14 @@ block_and_ignore: 更加极端的结束对话方式,直接结束对话并在
|
|||
"block_and_ignore",
|
||||
]
|
||||
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}"
|
||||
action = "wait"
|
||||
|
||||
logger.info(f"规划的行动: {action}")
|
||||
logger.info(f"行动原因: {reason}")
|
||||
logger.info(f"[私聊][{self.private_name}]规划的行动: {action}")
|
||||
logger.info(f"[私聊][{self.private_name}]行动原因: {reason}")
|
||||
return action, reason
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"规划行动时调用 LLM 或处理结果出错: {str(e)}")
|
||||
logger.error(f"[私聊][{self.private_name}]规划行动时调用 LLM 或处理结果出错: {str(e)}")
|
||||
return "wait", f"行动规划处理中发生错误,暂时等待: {str(e)}"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class ChatObserver:
|
|||
_instances: Dict[str, "ChatObserver"] = {}
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, stream_id: str) -> "ChatObserver":
|
||||
def get_instance(cls, stream_id: str, private_name: str) -> "ChatObserver":
|
||||
"""获取或创建观察器实例
|
||||
|
||||
Args:
|
||||
|
|
@ -28,10 +28,10 @@ class ChatObserver:
|
|||
ChatObserver: 观察器实例
|
||||
"""
|
||||
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]
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
"""初始化观察器
|
||||
|
||||
Args:
|
||||
|
|
@ -41,6 +41,7 @@ class ChatObserver:
|
|||
raise RuntimeError(f"ChatObserver for {stream_id} already exists. Use get_instance() instead.")
|
||||
|
||||
self.stream_id = stream_id
|
||||
self.private_name = private_name
|
||||
self.message_storage = MongoDBMessageStorage()
|
||||
|
||||
# self.last_user_speak_time: Optional[float] = None # 对方上次发言时间
|
||||
|
|
@ -76,12 +77,12 @@ class ChatObserver:
|
|||
Returns:
|
||||
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)
|
||||
|
||||
if new_message_exists:
|
||||
logger.debug("发现新消息")
|
||||
logger.debug(f"[私聊][{self.private_name}]发现新消息")
|
||||
self.last_check_time = time.time()
|
||||
|
||||
return new_message_exists
|
||||
|
|
@ -94,15 +95,13 @@ class ChatObserver:
|
|||
"""
|
||||
try:
|
||||
# 发送新消息通知
|
||||
# logger.info(f"发送新ccchandleer消息通知: {message}")
|
||||
notification = create_new_message_notification(
|
||||
sender="chat_observer", target="observation_info", message=message
|
||||
)
|
||||
# logger.info(f"发送新消ddddd息通知: {notification}")
|
||||
# print(self.notification_manager)
|
||||
await self.notification_manager.send_notification(notification)
|
||||
except Exception as e:
|
||||
logger.error(f"添加消息到历史记录时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]添加消息到历史记录时出错: {e}")
|
||||
print(traceback.format_exc())
|
||||
|
||||
# 检查并更新冷场状态
|
||||
|
|
@ -142,11 +141,13 @@ class ChatObserver:
|
|||
"""
|
||||
|
||||
if self.last_message_time is None:
|
||||
logger.debug("没有最后消息时间,返回 False")
|
||||
logger.debug(f"[私聊][{self.private_name}]没有最后消息时间,返回 False")
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
def get_message_history(
|
||||
|
|
@ -215,7 +216,7 @@ class ChatObserver:
|
|||
if new_messages:
|
||||
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
|
||||
|
||||
|
|
@ -228,9 +229,9 @@ class ChatObserver:
|
|||
# messages = await self._fetch_new_messages_before(start_time)
|
||||
# for message in messages:
|
||||
# await self._add_message_to_history(message)
|
||||
# logger.debug(f"缓冲消息: {messages}")
|
||||
# logger.debug(f"[私聊][{self.private_name}]缓冲消息: {messages}")
|
||||
# except Exception as e:
|
||||
# logger.error(f"缓冲消息出错: {e}")
|
||||
# logger.error(f"[私聊][{self.private_name}]缓冲消息出错: {e}")
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
|
|
@ -258,8 +259,8 @@ class ChatObserver:
|
|||
self._update_complete.set()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新循环出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"[私聊][{self.private_name}]更新循环出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
|
||||
self._update_complete.set() # 即使出错也要设置完成事件
|
||||
|
||||
def trigger_update(self):
|
||||
|
|
@ -279,7 +280,7 @@ class ChatObserver:
|
|||
await asyncio.wait_for(self._update_complete.wait(), timeout=timeout)
|
||||
return True
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"等待更新完成超时({timeout}秒)")
|
||||
logger.warning(f"[私聊][{self.private_name}]等待更新完成超时({timeout}秒)")
|
||||
return False
|
||||
|
||||
def start(self):
|
||||
|
|
@ -289,7 +290,7 @@ class ChatObserver:
|
|||
|
||||
self._running = True
|
||||
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):
|
||||
"""停止观察器"""
|
||||
|
|
@ -298,7 +299,7 @@ class ChatObserver:
|
|||
self._update_complete.set() # 设置完成事件以解除等待
|
||||
if self._task:
|
||||
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):
|
||||
"""处理聊天历史
|
||||
|
|
@ -316,7 +317,7 @@ class ChatObserver:
|
|||
else:
|
||||
self.update_user_speak_time(msg["time"])
|
||||
except Exception as e:
|
||||
logger.warning(f"处理消息时间时出错: {e}")
|
||||
logger.warning(f"[私聊][{self.private_name}]处理消息时间时出错: {e}")
|
||||
continue
|
||||
|
||||
def update_check_time(self):
|
||||
|
|
|
|||
|
|
@ -98,15 +98,11 @@ class NotificationManager:
|
|||
notification_type: 要处理的通知类型
|
||||
handler: 处理器实例
|
||||
"""
|
||||
# print(1145145511114445551111444)
|
||||
if target not in self._handlers:
|
||||
# print("没11有target")
|
||||
self._handlers[target] = {}
|
||||
if notification_type not in self._handlers[target]:
|
||||
# print("没11有notification_type")
|
||||
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)
|
||||
# print(self._handlers[target][notification_type])
|
||||
|
||||
|
|
@ -132,7 +128,6 @@ class NotificationManager:
|
|||
async def send_notification(self, notification: Notification):
|
||||
"""发送通知"""
|
||||
self._notification_history.append(notification)
|
||||
# print("kaishichul-----------------------------------i")
|
||||
|
||||
# 如果是状态通知,更新活跃状态
|
||||
if isinstance(notification, StateNotification):
|
||||
|
|
@ -145,7 +140,6 @@ class NotificationManager:
|
|||
target = notification.target
|
||||
if target in self._handlers:
|
||||
handlers = self._handlers[target].get(notification.type, [])
|
||||
# print(1111111)
|
||||
# print(handlers)
|
||||
for handler in handlers:
|
||||
# print(f"调用处理器: {handler}")
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ from src.plugins.utils.chat_message_builder import build_readable_messages, get_
|
|||
from typing import Dict, Any, Optional
|
||||
from ..chat.message import Message
|
||||
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 .action_planner import ActionPlanner
|
||||
from .observation_info import ObservationInfo
|
||||
from .conversation_info import ConversationInfo
|
||||
from .conversation_info import ConversationInfo # 确保导入 ConversationInfo
|
||||
from .reply_generator import ReplyGenerator
|
||||
from ..chat.chat_stream import ChatStream
|
||||
from maim_message import UserInfo
|
||||
|
|
@ -29,13 +30,14 @@ logger = get_logger("pfc")
|
|||
class Conversation:
|
||||
"""对话类,负责管理单个对话的状态和行为"""
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
"""初始化对话实例
|
||||
|
||||
Args:
|
||||
stream_id: 聊天流ID
|
||||
"""
|
||||
self.stream_id = stream_id
|
||||
self.private_name = private_name
|
||||
self.state = ConversationState.INIT
|
||||
self.should_continue = False
|
||||
self.ignore_until_timestamp: Optional[float] = None
|
||||
|
|
@ -47,38 +49,38 @@ class Conversation:
|
|||
"""初始化实例,注册所有组件"""
|
||||
|
||||
try:
|
||||
self.action_planner = ActionPlanner(self.stream_id)
|
||||
self.goal_analyzer = GoalAnalyzer(self.stream_id)
|
||||
self.reply_generator = ReplyGenerator(self.stream_id)
|
||||
self.knowledge_fetcher = KnowledgeFetcher()
|
||||
self.waiter = Waiter(self.stream_id)
|
||||
self.direct_sender = DirectMessageSender()
|
||||
self.action_planner = ActionPlanner(self.stream_id, self.private_name)
|
||||
self.goal_analyzer = GoalAnalyzer(self.stream_id, self.private_name)
|
||||
self.reply_generator = ReplyGenerator(self.stream_id, self.private_name)
|
||||
self.knowledge_fetcher = KnowledgeFetcher(self.private_name)
|
||||
self.waiter = Waiter(self.stream_id, self.private_name)
|
||||
self.direct_sender = DirectMessageSender(self.private_name)
|
||||
|
||||
# 获取聊天流信息
|
||||
self.chat_stream = chat_manager.get_stream(self.stream_id)
|
||||
|
||||
self.stop_action_planner = False
|
||||
except Exception as e:
|
||||
logger.error(f"初始化对话实例:注册运行组件失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"[私聊][{self.private_name}]初始化对话实例:注册运行组件失败: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
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.observation_info = ObservationInfo()
|
||||
self.observation_info = ObservationInfo(self.private_name)
|
||||
self.observation_info.bind_to_chat_observer(self.chat_observer)
|
||||
# print(self.chat_observer.get_cached_messages(limit=)
|
||||
|
||||
self.conversation_info = ConversationInfo()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化对话实例:注册信息组件失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"[私聊][{self.private_name}]初始化对话实例:注册信息组件失败: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
|
||||
raise
|
||||
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( #
|
||||
chat_id=self.stream_id,
|
||||
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_content = last_msg.get("processed_plain_text", "")
|
||||
|
||||
# (可选)可以遍历 initial_messages 来设置 last_bot_speak_time 和 last_user_speak_time
|
||||
# 这里为了简化,只用了最后一条消息的时间,如果需要精确的发言者时间需要遍历
|
||||
|
||||
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 从加载的最后一条消息之后开始同步
|
||||
self.chat_observer.last_message_time = self.observation_info.last_message_time
|
||||
self.chat_observer.last_message_read = last_msg # 更新 observer 的最后读取记录
|
||||
else:
|
||||
logger.info("没有找到初始聊天记录。")
|
||||
logger.info(f"[私聊][{self.private_name}]没有找到初始聊天记录。")
|
||||
|
||||
except Exception as load_err:
|
||||
logger.error(f"加载初始聊天记录时出错: {load_err}")
|
||||
logger.error(f"[私聊][{self.private_name}]加载初始聊天记录时出错: {load_err}")
|
||||
# 出错也要继续,只是没有历史记录而已
|
||||
# 组件准备完成,启动该论对话
|
||||
self.should_continue = True
|
||||
|
|
@ -127,149 +126,179 @@ class Conversation:
|
|||
async def start(self):
|
||||
"""开始对话流程"""
|
||||
try:
|
||||
logger.info("对话系统启动中...")
|
||||
logger.info(f"[私聊][{self.private_name}]对话系统启动中...")
|
||||
asyncio.create_task(self._plan_and_action_loop())
|
||||
except Exception as e:
|
||||
logger.error(f"启动对话系统失败: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]启动对话系统失败: {e}")
|
||||
raise
|
||||
|
||||
async def _plan_and_action_loop(self):
|
||||
"""思考步,PFC核心循环模块"""
|
||||
while self.should_continue:
|
||||
# 忽略逻辑
|
||||
if self.ignore_until_timestamp and time.time() < self.ignore_until_timestamp:
|
||||
# 仍在忽略期间,等待下次检查
|
||||
await asyncio.sleep(30) # 每 30 秒检查一次
|
||||
continue # 跳过本轮循环的剩余部分
|
||||
await asyncio.sleep(30)
|
||||
continue
|
||||
elif self.ignore_until_timestamp and time.time() >= self.ignore_until_timestamp:
|
||||
# 忽略期结束,现在正常地结束对话
|
||||
logger.info(f"忽略时间已到 {self.stream_id},准备结束对话。")
|
||||
self.ignore_until_timestamp = None # 清除时间戳
|
||||
self.should_continue = False # 现在停止循环
|
||||
# (可选)在这里记录一个 'end_conversation' 动作
|
||||
# 或者确保管理器会基于 should_continue 为 False 来清理它
|
||||
continue # 跳过本轮循环的剩余部分,让它终止
|
||||
logger.info(f"[私聊][{self.private_name}]忽略时间已到 {self.stream_id},准备结束对话。")
|
||||
self.ignore_until_timestamp = None
|
||||
self.should_continue = False
|
||||
continue
|
||||
try:
|
||||
# --- 在规划前记录当前新消息数量 ---
|
||||
initial_new_message_count = 0
|
||||
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:
|
||||
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(
|
||||
self.observation_info, self.conversation_info
|
||||
) # 注意:plan 函数内部现在不应再调用 clear_unprocessed_messages
|
||||
self.observation_info, self.conversation_info, self.conversation_info.last_successful_reply_action
|
||||
)
|
||||
|
||||
# --- 规划后检查是否有 *更多* 新消息到达 ---
|
||||
current_new_message_count = 0
|
||||
if hasattr(self.observation_info, "new_messages_count"):
|
||||
current_new_message_count = self.observation_info.new_messages_count
|
||||
else:
|
||||
logger.warning("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}),跳过本次行动,重新规划"
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ObservationInfo missing 'new_messages_count' after planning."
|
||||
)
|
||||
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
|
||||
|
||||
# --- 清理未处理消息:移到这里,在执行动作前 ---
|
||||
# 只有当确实有新消息被 planner 看到,并且 action 是要处理它们的时候才清理
|
||||
if initial_new_message_count > 0 and action == "direct_reply":
|
||||
# 包含 send_new_message
|
||||
if initial_new_message_count > 0 and action in ["direct_reply", "send_new_message"]:
|
||||
if hasattr(self.observation_info, "clear_unprocessed_messages"):
|
||||
# 确保 clear_unprocessed_messages 方法存在
|
||||
logger.debug(f"准备执行 direct_reply,清理 {initial_new_message_count} 条规划时已知的新消息。")
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]准备执行 {action},清理 {initial_new_message_count} 条规划时已知的新消息。"
|
||||
)
|
||||
await self.observation_info.clear_unprocessed_messages()
|
||||
# 手动重置计数器,确保状态一致性(理想情况下 clear 方法会做这个)
|
||||
if hasattr(self.observation_info, "new_messages_count"):
|
||||
self.observation_info.new_messages_count = 0
|
||||
else:
|
||||
logger.error("无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!")
|
||||
# 这里可能需要考虑是否继续执行 action,或者抛出错误
|
||||
logger.error(
|
||||
f"[私聊][{self.private_name}]无法清理未处理消息: ObservationInfo 缺少 clear_unprocessed_messages 方法!"
|
||||
)
|
||||
|
||||
# --- 执行行动 ---
|
||||
await self._handle_action(action, reason, self.observation_info, self.conversation_info)
|
||||
|
||||
# 检查是否需要结束对话 (逻辑不变)
|
||||
goal_ended = False
|
||||
if hasattr(self.conversation_info, "goal_list") and self.conversation_info.goal_list:
|
||||
for goal in self.conversation_info.goal_list:
|
||||
if isinstance(goal, tuple) and len(goal) > 0 and goal[0] == "结束对话":
|
||||
goal_ended = True
|
||||
break
|
||||
elif isinstance(goal, dict) and goal.get("goal") == "结束对话":
|
||||
for goal_item in self.conversation_info.goal_list:
|
||||
if isinstance(goal_item, dict):
|
||||
current_goal = goal_item.get("goal")
|
||||
|
||||
if current_goal == "结束对话":
|
||||
goal_ended = True
|
||||
break
|
||||
|
||||
if goal_ended:
|
||||
self.should_continue = False
|
||||
logger.info("检测到'结束对话'目标,停止循环。")
|
||||
# break # 可以选择在这里直接跳出循环
|
||||
logger.info(f"[私聊][{self.private_name}]检测到'结束对话'目标,停止循环。")
|
||||
|
||||
except Exception as loop_err:
|
||||
logger.error(f"PFC主循环出错: {loop_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
# 发生严重错误时可以考虑停止,或者至少等待一下再继续
|
||||
await asyncio.sleep(1) # 发生错误时等待1秒
|
||||
# 添加短暂的异步睡眠
|
||||
if self.should_continue: # 只有在还需要继续循环时才 sleep
|
||||
await asyncio.sleep(0.1) # 等待 0.1 秒,给其他任务执行时间
|
||||
logger.error(f"[私聊][{self.private_name}]PFC主循环出错: {loop_err}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}")
|
||||
await asyncio.sleep(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):
|
||||
"""检查在规划后是否有新消息"""
|
||||
if self.observation_info.new_messages_count > 0:
|
||||
logger.info(f"发现{self.observation_info.new_messages_count}条新消息,可能需要重新考虑行动")
|
||||
# 如果需要,可以在这里添加逻辑来根据新消息重新决定行动
|
||||
# 检查 ObservationInfo 是否已初始化并且有 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 False
|
||||
|
||||
def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message:
|
||||
"""将消息字典转换为Message对象"""
|
||||
try:
|
||||
chat_info = msg_dict.get("chat_info", {})
|
||||
chat_stream = ChatStream.from_dict(chat_info)
|
||||
# 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 chat_manager 获取
|
||||
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", {}))
|
||||
|
||||
return Message(
|
||||
message_id=msg_dict["message_id"],
|
||||
chat_stream=chat_stream,
|
||||
time=msg_dict["time"],
|
||||
message_id=msg_dict.get("message_id", f"gen_{time.time()}"), # 提供默认 ID
|
||||
chat_stream=chat_stream, # 使用确定的 chat_stream
|
||||
time=msg_dict.get("time", time.time()), # 提供默认时间
|
||||
user_info=user_info,
|
||||
processed_plain_text=msg_dict.get("processed_plain_text", ""),
|
||||
detailed_plain_text=msg_dict.get("detailed_plain_text", ""),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"转换消息时出错: {e}")
|
||||
raise
|
||||
logger.warning(f"[私聊][{self.private_name}]转换消息时出错: {e}")
|
||||
# 可以选择返回 None 或重新抛出异常,这里选择重新抛出以指示问题
|
||||
raise ValueError(f"无法将字典转换为 Message 对象: {e}") from e
|
||||
|
||||
async def _handle_action(
|
||||
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 = {
|
||||
"action": action,
|
||||
"plan_reason": reason, # 使用 plan_reason 存储规划原因
|
||||
"status": "start", # 初始状态为 start
|
||||
"plan_reason": reason,
|
||||
"status": "start",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
"final_reason": None,
|
||||
}
|
||||
# 确保 done_action 列表存在
|
||||
if not hasattr(conversation_info, "done_action"):
|
||||
conversation_info.done_action = []
|
||||
conversation_info.done_action.append(current_action_record)
|
||||
# 获取刚刚添加记录的索引,方便后面更新状态
|
||||
action_index = len(conversation_info.done_action) - 1
|
||||
|
||||
action_successful = False # 用于标记动作是否成功完成
|
||||
|
||||
# --- 根据不同的 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
|
||||
is_suitable = False
|
||||
need_replan = False
|
||||
|
|
@ -278,179 +307,343 @@ class Conversation:
|
|||
|
||||
while reply_attempt_count < max_reply_attempts and not is_suitable:
|
||||
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
|
||||
|
||||
# 1. 生成回复
|
||||
self.generated_reply = await self.reply_generator.generate(observation_info, conversation_info)
|
||||
logger.info(f"第 {reply_attempt_count} 次生成的回复: {self.generated_reply}")
|
||||
# 1. 生成回复 (调用 generate 时传入 action_type)
|
||||
self.generated_reply = await self.reply_generator.generate(
|
||||
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
|
||||
try:
|
||||
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(
|
||||
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, # 传递当前尝试次数(从0开始计数)
|
||||
retry_count=reply_attempt_count - 1,
|
||||
)
|
||||
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:
|
||||
final_reply_to_send = self.generated_reply # 保存合适的回复
|
||||
break # 回复合适,跳出循环
|
||||
|
||||
final_reply_to_send = self.generated_reply
|
||||
break
|
||||
elif need_replan:
|
||||
logger.warning(f"第 {reply_attempt_count} 次检查建议重新规划,停止尝试。原因: {check_reason}")
|
||||
break # 如果检查器建议重新规划,也停止尝试
|
||||
|
||||
# 如果不合适但不需要重新规划,循环会继续进行下一次尝试
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]第 {reply_attempt_count} 次追问检查建议重新规划,停止尝试。原因: {check_reason}"
|
||||
)
|
||||
break
|
||||
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}"
|
||||
# 如果检查本身出错,可以选择跳出循环或继续尝试
|
||||
# 这里选择跳出循环,避免无限循环在检查错误上
|
||||
break
|
||||
|
||||
# 循环结束,处理最终结果
|
||||
if is_suitable:
|
||||
# 回复合适且已保存在 final_reply_to_send 中
|
||||
# 检查是否有新消息进来 (在所有尝试结束后再检查一次)
|
||||
# 检查是否有新消息
|
||||
if self._check_new_messages_after_planning():
|
||||
logger.info("生成回复期间收到新消息,取消发送,重新规划行动")
|
||||
logger.info(f"[私聊][{self.private_name}]生成追问回复期间收到新消息,取消发送,重新规划行动")
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": "recall",
|
||||
"final_reason": f"有新消息,取消发送: {final_reply_to_send}",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
{"status": "recall", "final_reason": f"有新消息,取消发送追问: {final_reply_to_send}"}
|
||||
)
|
||||
# 这里直接返回,不执行后续发送和wait
|
||||
return
|
||||
return # 直接返回,重新规划
|
||||
|
||||
# 发送合适的回复
|
||||
self.generated_reply = final_reply_to_send # 确保 self.generated_reply 是最终要发送的内容
|
||||
await self._send_reply()
|
||||
self.generated_reply = final_reply_to_send
|
||||
# --- 在这里调用 _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(
|
||||
{
|
||||
"status": "done",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
{"status": "recall", "final_reason": f"追问尝试{reply_attempt_count}次后打回: {check_reason}"}
|
||||
)
|
||||
|
||||
else:
|
||||
# 循环结束但没有找到合适的回复(达到最大次数或检查出错/建议重规划)
|
||||
logger.warning(f"经过 {reply_attempt_count} 次尝试,未能生成合适的回复。最终原因: {check_reason}")
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": "recall", # 标记为 recall 因为没有成功发送
|
||||
"final_reason": f"尝试{reply_attempt_count}次后失败: {check_reason}",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
# 追问失败
|
||||
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("由于无法生成合适回复,执行 '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
|
||||
# 直接调用 wait 方法
|
||||
await self.waiter.wait(self.conversation_info)
|
||||
# 可以选择添加一条新的 action 记录来表示这个 wait
|
||||
wait_action_record = {
|
||||
"action": "wait",
|
||||
"plan_reason": "因 direct_reply 多次尝试失败而执行的后备等待",
|
||||
"status": "done", # wait 完成后可以认为是 done
|
||||
"status": "done",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
"final_reason": None,
|
||||
}
|
||||
conversation_info.done_action.append(wait_action_record)
|
||||
|
||||
elif action == "fetch_knowledge":
|
||||
self.waiter.wait_accumulated_time = 0
|
||||
self.state = ConversationState.FETCHING
|
||||
knowledge = "TODO:知识"
|
||||
topic = "TODO:关键词"
|
||||
logger.info(f"假装获取到知识{knowledge},关键词是: {topic}")
|
||||
if knowledge:
|
||||
pass # 简单处理
|
||||
# 标记 action 为 done
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": "done",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
)
|
||||
knowledge_query = reason
|
||||
try:
|
||||
# 检查 knowledge_fetcher 是否存在
|
||||
if not hasattr(self, "knowledge_fetcher"):
|
||||
logger.error(f"[私聊][{self.private_name}]KnowledgeFetcher 未初始化,无法获取知识。")
|
||||
raise AttributeError("KnowledgeFetcher not initialized")
|
||||
|
||||
knowledge, source = await self.knowledge_fetcher.fetch(knowledge_query, observation_info.chat_history)
|
||||
logger.info(f"[私聊][{self.private_name}]获取到知识: {knowledge[:100]}..., 来源: {source}")
|
||||
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":
|
||||
self.waiter.wait_accumulated_time = 0
|
||||
self.state = ConversationState.RETHINKING
|
||||
await self.goal_analyzer.analyze_goal(conversation_info, observation_info)
|
||||
# 标记 action 为 done
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": "done",
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
# 检查 goal_analyzer 是否存在
|
||||
if not hasattr(self, "goal_analyzer"):
|
||||
logger.error(f"[私聊][{self.private_name}]GoalAnalyzer 未初始化,无法重新思考目标。")
|
||||
raise AttributeError("GoalAnalyzer not initialized")
|
||||
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":
|
||||
self.state = ConversationState.LISTENING
|
||||
logger.info("倾听对方发言...")
|
||||
await self.waiter.wait_listening(conversation_info)
|
||||
# listening 和 wait 通常在完成后不需要标记为 done,因为它们是持续状态,
|
||||
# 但如果需要记录,可以在 waiter 返回后标记。目前逻辑是 waiter 返回后主循环继续。
|
||||
# 为了统一,可以暂时在这里也标记一下(或者都不标记)
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": "done", # 或 "completed"
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
)
|
||||
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")
|
||||
await self.waiter.wait_listening(conversation_info)
|
||||
action_successful = True # Listening 完成就算成功
|
||||
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":
|
||||
self.should_continue = False # 设置循环停止标志
|
||||
logger.info("决定结束对话...")
|
||||
# 标记 action 为 done
|
||||
self.should_continue = False
|
||||
logger.info(f"[私聊][{self.private_name}]决定结束对话...")
|
||||
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(
|
||||
{
|
||||
"status": "done",
|
||||
"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":
|
||||
logger.info("不想再理你了...")
|
||||
# 1. 标记对话为暂时忽略
|
||||
ignore_duration_seconds = 10 * 60 # 10 分钟
|
||||
self.ignore_until_timestamp = time.time() + ignore_duration_seconds
|
||||
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
|
||||
async def _send_reply(self):
|
||||
"""发送回复"""
|
||||
if not self.generated_reply:
|
||||
logger.warning(f"[私聊][{self.private_name}]没有生成回复内容,无法发送。")
|
||||
return
|
||||
|
||||
else: # 对应 'wait' 动作
|
||||
self.state = ConversationState.WAITING
|
||||
logger.info("等待更多信息...")
|
||||
await self.waiter.wait(self.conversation_info)
|
||||
# 同 listening,可以考虑是否标记状态
|
||||
conversation_info.done_action[action_index].update(
|
||||
{
|
||||
"status": "done", # 或 "completed"
|
||||
"time": datetime.datetime.now().strftime("%H:%M:%S"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
_current_time = time.time()
|
||||
reply_content = self.generated_reply
|
||||
|
||||
# 发送消息 (确保 direct_sender 和 chat_stream 有效)
|
||||
if not hasattr(self, "direct_sender") or not self.direct_sender:
|
||||
logger.error(f"[私聊][{self.private_name}]DirectMessageSender 未初始化,无法发送回复。")
|
||||
return
|
||||
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):
|
||||
"""发送超时结束消息"""
|
||||
|
|
@ -464,30 +657,4 @@ class Conversation:
|
|||
chat_stream=self.chat_stream, content="TODO:超时消息", reply_to_message=latest_message
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"发送超时消息失败: {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 # 出错也要尝试恢复状态
|
||||
logger.error(f"[私聊][{self.private_name}]发送超时消息失败: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
from typing import Optional
|
||||
|
||||
|
||||
class ConversationInfo:
|
||||
def __init__(self):
|
||||
self.done_action = []
|
||||
self.goal_list = []
|
||||
self.knowledge_list = []
|
||||
self.memory_list = []
|
||||
self.last_successful_reply_action: Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import time
|
||||
from typing import Optional
|
||||
from src.common.logger import get_module_logger
|
||||
from ..chat.chat_stream import ChatStream
|
||||
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_sender import message_manager
|
||||
from ..storage.storage import MessageStorage
|
||||
from ...config.config import global_config
|
||||
|
||||
|
||||
logger = get_module_logger("message_sender")
|
||||
|
||||
|
|
@ -12,8 +16,9 @@ logger = get_module_logger("message_sender")
|
|||
class DirectMessageSender:
|
||||
"""直接消息发送器"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, private_name: str):
|
||||
self.private_name = private_name
|
||||
self.storage = MessageStorage()
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
|
|
@ -30,21 +35,44 @@ class DirectMessageSender:
|
|||
"""
|
||||
try:
|
||||
# 创建消息内容
|
||||
segments = [Seg(type="text", data={"text": content})]
|
||||
segments = Seg(type="seglist", data=[Seg(type="text", data=content)])
|
||||
|
||||
# 检查是否需要引用回复
|
||||
if reply_to_message:
|
||||
reply_id = reply_to_message.message_id
|
||||
message_sending = MessageSending(segments=segments, reply_to_id=reply_id)
|
||||
else:
|
||||
message_sending = MessageSending(segments=segments)
|
||||
# 获取麦麦的信息
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.BOT_QQ,
|
||||
user_nickname=global_config.BOT_NICKNAME,
|
||||
platform=chat_stream.platform,
|
||||
)
|
||||
|
||||
# 用当前时间作为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.add_message(message_sending)
|
||||
message_manager.add_message(message_set)
|
||||
logger.info(f"PFC消息已发送: {content}")
|
||||
message_set = MessageSet(chat_stream, message_id)
|
||||
message_set.add_message(message)
|
||||
await message_manager.add_message(message_set)
|
||||
await self.storage.store_message(message, chat_stream)
|
||||
logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PFC消息发送失败: {str(e)}")
|
||||
logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
# Programmable Friendly Conversationalist
|
||||
# Prefrontal cortex
|
||||
from typing import List, Optional, Dict, Any, Set
|
||||
from maim_message import UserInfo
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from src.common.logger import get_module_logger
|
||||
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
|
||||
import traceback # 导入 traceback 用于调试
|
||||
|
||||
logger = get_module_logger("observation_info")
|
||||
|
||||
|
|
@ -15,188 +14,287 @@ logger = get_module_logger("observation_info")
|
|||
class ObservationInfoHandler(NotificationHandler):
|
||||
"""ObservationInfo的通知处理器"""
|
||||
|
||||
def __init__(self, observation_info: "ObservationInfo"):
|
||||
def __init__(self, observation_info: "ObservationInfo", private_name: str):
|
||||
"""初始化处理器
|
||||
|
||||
Args:
|
||||
observation_info: 要更新的ObservationInfo实例
|
||||
private_name: 私聊对象的名称,用于日志记录
|
||||
"""
|
||||
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
|
||||
data = notification.data
|
||||
|
||||
if notification_type == NotificationType.NEW_MESSAGE:
|
||||
# 处理新消息通知
|
||||
logger.debug(f"收到新消息通知data: {data}")
|
||||
message_id = data.get("message_id")
|
||||
processed_plain_text = data.get("processed_plain_text")
|
||||
detailed_plain_text = data.get("detailed_plain_text")
|
||||
user_info = data.get("user_info")
|
||||
time_value = data.get("time")
|
||||
try: # 添加错误处理块
|
||||
if notification_type == NotificationType.NEW_MESSAGE:
|
||||
# 处理新消息通知
|
||||
# logger.debug(f"[私聊][{self.private_name}]收到新消息通知data: {data}") # 可以在需要时取消注释
|
||||
message_id = data.get("message_id")
|
||||
processed_plain_text = data.get("processed_plain_text")
|
||||
detailed_plain_text = data.get("detailed_plain_text")
|
||||
user_info_dict = data.get("user_info") # 先获取字典
|
||||
time_value = data.get("time")
|
||||
|
||||
message = {
|
||||
"message_id": message_id,
|
||||
"processed_plain_text": processed_plain_text,
|
||||
"detailed_plain_text": detailed_plain_text,
|
||||
"user_info": user_info,
|
||||
"time": time_value,
|
||||
}
|
||||
# 确保 user_info 是字典类型再创建 UserInfo 对象
|
||||
user_info = None
|
||||
if isinstance(user_info_dict, dict):
|
||||
try:
|
||||
user_info = UserInfo.from_dict(user_info_dict)
|
||||
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:
|
||||
# 处理冷场通知
|
||||
is_cold = data.get("is_cold", False)
|
||||
self.observation_info.update_cold_chat_status(is_cold, time.time())
|
||||
elif notification_type == NotificationType.COLD_CHAT:
|
||||
# 处理冷场通知
|
||||
is_cold = data.get("is_cold", False)
|
||||
await self.observation_info.update_cold_chat_status(is_cold, time.time()) # 修改:改为 await 调用
|
||||
|
||||
elif notification_type == NotificationType.ACTIVE_CHAT:
|
||||
# 处理活跃通知
|
||||
is_active = data.get("is_active", False)
|
||||
self.observation_info.is_cold = not is_active
|
||||
elif notification_type == NotificationType.ACTIVE_CHAT:
|
||||
# 处理活跃通知 (通常由 COLD_CHAT 的反向状态处理)
|
||||
is_active = data.get("is_active", False)
|
||||
self.observation_info.is_cold = not is_active
|
||||
|
||||
elif notification_type == NotificationType.BOT_SPEAKING:
|
||||
# 处理机器人说话通知
|
||||
self.observation_info.is_typing = False
|
||||
self.observation_info.last_bot_speak_time = time.time()
|
||||
elif notification_type == NotificationType.BOT_SPEAKING:
|
||||
# 处理机器人说话通知 (按需实现)
|
||||
self.observation_info.is_typing = False
|
||||
self.observation_info.last_bot_speak_time = time.time()
|
||||
|
||||
elif notification_type == NotificationType.USER_SPEAKING:
|
||||
# 处理用户说话通知
|
||||
self.observation_info.is_typing = False
|
||||
self.observation_info.last_user_speak_time = time.time()
|
||||
elif notification_type == NotificationType.USER_SPEAKING:
|
||||
# 处理用户说话通知
|
||||
self.observation_info.is_typing = False
|
||||
self.observation_info.last_user_speak_time = time.time()
|
||||
|
||||
elif notification_type == NotificationType.MESSAGE_DELETED:
|
||||
# 处理消息删除通知
|
||||
message_id = data.get("message_id")
|
||||
self.observation_info.unprocessed_messages = [
|
||||
msg for msg in self.observation_info.unprocessed_messages if msg.get("message_id") != message_id
|
||||
]
|
||||
elif notification_type == NotificationType.MESSAGE_DELETED:
|
||||
# 处理消息删除通知
|
||||
message_id = data.get("message_id")
|
||||
# 从 unprocessed_messages 中移除被删除的消息
|
||||
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:
|
||||
# 处理用户加入通知
|
||||
user_id = data.get("user_id")
|
||||
if user_id:
|
||||
self.observation_info.active_users.add(user_id)
|
||||
elif notification_type == NotificationType.USER_JOINED:
|
||||
# 处理用户加入通知 (如果适用私聊场景)
|
||||
user_id = data.get("user_id")
|
||||
if user_id:
|
||||
self.observation_info.active_users.add(str(user_id)) # 确保是字符串
|
||||
|
||||
elif notification_type == NotificationType.USER_LEFT:
|
||||
# 处理用户离开通知
|
||||
user_id = data.get("user_id")
|
||||
if user_id:
|
||||
self.observation_info.active_users.discard(user_id)
|
||||
elif notification_type == NotificationType.USER_LEFT:
|
||||
# 处理用户离开通知 (如果适用私聊场景)
|
||||
user_id = data.get("user_id")
|
||||
if user_id:
|
||||
self.observation_info.active_users.discard(str(user_id)) # 确保是字符串
|
||||
|
||||
elif notification_type == NotificationType.ERROR:
|
||||
# 处理错误通知
|
||||
error_msg = data.get("error", "")
|
||||
logger.error(f"收到错误通知: {error_msg}")
|
||||
elif notification_type == NotificationType.ERROR:
|
||||
# 处理错误通知
|
||||
error_msg = data.get("error", "未提供错误信息")
|
||||
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
|
||||
class ObservationInfo:
|
||||
"""决策信息类,用于收集和管理来自chat_observer的通知信息"""
|
||||
|
||||
# --- 修改:添加 private_name 字段 ---
|
||||
private_name: str = field(init=True) # 让 dataclass 的 __init__ 接收 private_name
|
||||
|
||||
# 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 = ""
|
||||
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)
|
||||
|
||||
# data
|
||||
last_bot_speak_time: Optional[float] = None
|
||||
last_user_speak_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_sender: Optional[str] = None
|
||||
bot_id: Optional[str] = None
|
||||
chat_history_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
|
||||
is_typing: bool = False
|
||||
has_unread_messages: bool = False
|
||||
is_typing: bool = False # 可能表示对方正在输入
|
||||
# has_unread_messages: bool = False # 这个状态可以通过 new_messages_count > 0 判断
|
||||
is_cold_chat: bool = False
|
||||
changed: bool = False
|
||||
changed: bool = False # 用于标记状态是否有变化,以便外部模块决定是否重新规划
|
||||
|
||||
# #spec
|
||||
# #spec (暂时注释掉,如果不需要)
|
||||
# meta_plan_trigger: bool = False
|
||||
|
||||
# --- 修改:移除 __post_init__ 的参数 ---
|
||||
def __post_init__(self):
|
||||
"""初始化后创建handler"""
|
||||
self.chat_observer = None
|
||||
self.handler = ObservationInfoHandler(self)
|
||||
"""初始化后创建handler并进行必要的设置"""
|
||||
self.chat_observer: Optional[ChatObserver] = None # 添加类型提示
|
||||
self.handler = ObservationInfoHandler(self, self.private_name)
|
||||
|
||||
def bind_to_chat_observer(self, chat_observer: ChatObserver):
|
||||
"""绑定到指定的chat_observer
|
||||
|
||||
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.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
|
||||
)
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.COLD_CHAT, handler=self.handler
|
||||
)
|
||||
print("1919810------------------------绑定-----------------------------")
|
||||
try:
|
||||
# 注册关心的通知类型
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
target="observation_info", notification_type=NotificationType.NEW_MESSAGE, handler=self.handler
|
||||
)
|
||||
self.chat_observer.notification_manager.register_handler(
|
||||
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):
|
||||
"""解除与chat_observer的绑定"""
|
||||
if self.chat_observer:
|
||||
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 = None
|
||||
if self.chat_observer and hasattr(self.chat_observer, "notification_manager"): # 增加检查
|
||||
try:
|
||||
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.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:
|
||||
message: 消息数据
|
||||
message: 消息数据字典
|
||||
user_info: 解析后的 UserInfo 对象 (可能为 None)
|
||||
"""
|
||||
# print("1919810-----------------------------------------------------")
|
||||
# logger.debug(f"更新信息from_message: {message}")
|
||||
self.last_message_time = message["time"]
|
||||
self.last_message_id = message["message_id"]
|
||||
message_time = message.get("time")
|
||||
message_id = message.get("message_id")
|
||||
processed_text = message.get("processed_plain_text", "")
|
||||
|
||||
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", {}))
|
||||
self.last_message_sender = user_info.user_id
|
||||
if user_info:
|
||||
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:
|
||||
self.last_user_speak_time = message["time"]
|
||||
self.active_users.add(user_info.user_id)
|
||||
|
||||
self.new_messages_count += 1
|
||||
self.unprocessed_messages.append(message)
|
||||
|
||||
self.update_changed()
|
||||
# 如果消息时间戳不是最新的,可能不需要处理,或者记录一个警告
|
||||
pass
|
||||
# logger.warning(f"[私聊][{self.private_name}]收到过时或无效时间戳的消息: ID={message_id}, time={message_time}")
|
||||
|
||||
def update_changed(self):
|
||||
"""更新changed状态"""
|
||||
"""标记状态已改变,并重置标记"""
|
||||
# logger.debug(f"[私聊][{self.private_name}]状态标记为已改变 (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:
|
||||
is_cold: 是否冷场
|
||||
current_time: 当前时间
|
||||
is_cold: 是否处于冷场状态
|
||||
current_time: 当前时间戳
|
||||
"""
|
||||
self.is_cold_chat = is_cold
|
||||
if is_cold and self.last_message_time:
|
||||
self.cold_chat_duration = current_time - self.last_message_time
|
||||
if is_cold != self.is_cold_chat: # 仅在状态变化时更新
|
||||
self.is_cold_chat = is_cold
|
||||
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:
|
||||
"""获取当前活跃时长
|
||||
"""获取当前活跃时长 (距离最后一条消息的时间)
|
||||
|
||||
Returns:
|
||||
float: 最后一条消息到现在的时长(秒)
|
||||
|
|
@ -206,7 +304,7 @@ class ObservationInfo:
|
|||
return time.time() - self.last_message_time
|
||||
|
||||
def get_user_response_time(self) -> Optional[float]:
|
||||
"""获取用户响应时间
|
||||
"""获取用户最后响应时间 (距离用户最后发言的时间)
|
||||
|
||||
Returns:
|
||||
Optional[float]: 用户最后发言到现在的时长(秒),如果没有用户发言则返回None
|
||||
|
|
@ -216,7 +314,7 @@ class ObservationInfo:
|
|||
return time.time() - self.last_user_speak_time
|
||||
|
||||
def get_bot_response_time(self) -> Optional[float]:
|
||||
"""获取机器人响应时间
|
||||
"""获取机器人最后响应时间 (距离机器人最后发言的时间)
|
||||
|
||||
Returns:
|
||||
Optional[float]: 机器人最后发言到现在的时长(秒),如果没有机器人发言则返回None
|
||||
|
|
@ -226,19 +324,38 @@ class ObservationInfo:
|
|||
return time.time() - self.last_bot_speak_time
|
||||
|
||||
async def clear_unprocessed_messages(self):
|
||||
"""清空未处理消息列表"""
|
||||
# 将未处理消息添加到历史记录中
|
||||
for message in self.unprocessed_messages:
|
||||
self.chat_history.append(message)
|
||||
self.chat_history_str = await build_readable_messages(
|
||||
self.chat_history[-20:] if len(self.chat_history) > 20 else self.chat_history,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
# 清空未处理消息列表
|
||||
self.has_unread_messages = False
|
||||
"""将未处理消息移入历史记录,并更新相关状态"""
|
||||
if not self.unprocessed_messages:
|
||||
return # 没有未处理消息,直接返回
|
||||
|
||||
# logger.debug(f"[私聊][{self.private_name}]处理 {len(self.unprocessed_messages)} 条未处理消息...")
|
||||
# 将未处理消息添加到历史记录中 (确保历史记录有长度限制,避免无限增长)
|
||||
max_history_len = 100 # 示例:最多保留100条历史记录
|
||||
self.chat_history.extend(self.unprocessed_messages)
|
||||
if len(self.chat_history) > max_history_len:
|
||||
self.chat_history = self.chat_history[-max_history_len:]
|
||||
|
||||
# 更新历史记录字符串 (只使用最近一部分生成,例如20条)
|
||||
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.chat_history_count = len(self.chat_history)
|
||||
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() # 状态改变
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
# Programmable Friendly Conversationalist
|
||||
# Prefrontal cortex
|
||||
import datetime
|
||||
|
||||
# import asyncio
|
||||
from typing import List, Optional, Tuple, TYPE_CHECKING
|
||||
from typing import List, Tuple, TYPE_CHECKING
|
||||
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 ...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 .pfc_utils import get_items_from_json
|
||||
from src.individuality.individuality import Individuality
|
||||
from .conversation_info import ConversationInfo
|
||||
from .observation_info import ObservationInfo
|
||||
import time
|
||||
from src.plugins.utils.chat_message_builder import build_readable_messages
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -30,16 +18,16 @@ logger = get_module_logger("pfc")
|
|||
class GoalAnalyzer:
|
||||
"""对话目标分析器"""
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
self.llm = LLMRequest(
|
||||
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.identity_detail_info = Individuality.get_instance().get_prompt(type="identity", x_person=2, level=2)
|
||||
self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
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 = [] # 存储多个目标
|
||||
|
|
@ -60,16 +48,10 @@ class GoalAnalyzer:
|
|||
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")
|
||||
if isinstance(goal_reason, dict):
|
||||
goal = goal_reason.get("goal", "目标内容缺失")
|
||||
reasoning = goal_reason.get("reasoning", "没有明确原因")
|
||||
else:
|
||||
# 如果是其他类型,尝试转为字符串
|
||||
goal = str(goal_reason)
|
||||
reasoning = "没有明确原因"
|
||||
|
||||
|
|
@ -81,7 +63,7 @@ class GoalAnalyzer:
|
|||
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:
|
||||
new_messages_list = observation_info.unprocessed_messages
|
||||
|
|
@ -96,21 +78,7 @@ class GoalAnalyzer:
|
|||
|
||||
# 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}。"
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
# 构建action历史文本
|
||||
action_history_list = conversation_info.done_action
|
||||
action_history_text = "你之前做的事情是:"
|
||||
|
|
@ -140,27 +108,32 @@ class GoalAnalyzer:
|
|||
|
||||
输出格式示例:
|
||||
[
|
||||
{{
|
||||
{{
|
||||
"goal": "回答用户关于Python编程的具体问题",
|
||||
"reasoning": "用户提出了关于Python的技术问题,需要专业且准确的解答"
|
||||
}},
|
||||
{{
|
||||
}},
|
||||
{{
|
||||
"goal": "回答用户关于python安装的具体问题",
|
||||
"reasoning": "用户提出了关于Python的技术问题,需要专业且准确的解答"
|
||||
}}
|
||||
}}
|
||||
]"""
|
||||
|
||||
logger.debug(f"发送到LLM的提示词: {prompt}")
|
||||
logger.debug(f"[私聊][{self.private_name}]发送到LLM的提示词: {prompt}")
|
||||
try:
|
||||
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:
|
||||
logger.error(f"分析对话目标时出错: {str(e)}")
|
||||
logger.error(f"[私聊][{self.private_name}]分析对话目标时出错: {str(e)}")
|
||||
content = ""
|
||||
|
||||
# 使用改进后的get_items_from_json函数处理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:
|
||||
|
|
@ -169,9 +142,7 @@ class GoalAnalyzer:
|
|||
# 清空现有目标列表并添加新目标
|
||||
conversation_info.goal_list = []
|
||||
for item in result:
|
||||
goal = item.get("goal", "")
|
||||
reasoning = item.get("reasoning", "")
|
||||
conversation_info.goal_list.append((goal, reasoning))
|
||||
conversation_info.goal_list.append(item)
|
||||
|
||||
# 返回第一个目标作为当前主要目标(如果有)
|
||||
if result:
|
||||
|
|
@ -179,9 +150,7 @@ class GoalAnalyzer:
|
|||
return (first_goal.get("goal", ""), "", first_goal.get("reasoning", ""))
|
||||
else:
|
||||
# 单个目标的情况
|
||||
goal = result.get("goal", "")
|
||||
reasoning = result.get("reasoning", "")
|
||||
conversation_info.goal_list.append((goal, reasoning))
|
||||
conversation_info.goal_list.append(result)
|
||||
return (goal, "", reasoning)
|
||||
|
||||
# 如果解析失败,返回默认值
|
||||
|
|
@ -250,30 +219,15 @@ class GoalAnalyzer:
|
|||
|
||||
async def analyze_conversation(self, goal, reasoning):
|
||||
messages = self.chat_observer.get_cached_messages()
|
||||
chat_history_text = ""
|
||||
for msg in messages:
|
||||
time_str = datetime.datetime.fromtimestamp(msg["time"]).strftime("%H:%M:%S")
|
||||
user_info = UserInfo.from_dict(msg.get("user_info", {}))
|
||||
sender = user_info.user_nickname or f"用户{user_info.user_id}"
|
||||
if sender == self.name:
|
||||
sender = "你说"
|
||||
chat_history_text += f"{time_str},{sender}:{msg.get('processed_plain_text', '')}\n"
|
||||
chat_history_text = await build_readable_messages(
|
||||
messages,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
|
||||
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}。"
|
||||
persona_text = f"你的名字是{self.name},{self.personality_info}。"
|
||||
# ===> Persona 文本构建结束 <===
|
||||
|
||||
# --- 修改 Prompt 字符串,使用 persona_text ---
|
||||
|
|
@ -298,11 +252,12 @@ class GoalAnalyzer:
|
|||
|
||||
try:
|
||||
content, _ = await self.llm.generate_response_async(prompt)
|
||||
logger.debug(f"LLM原始返回内容: {content}")
|
||||
logger.debug(f"[私聊][{self.private_name}]LLM原始返回内容: {content}")
|
||||
|
||||
# 尝试解析JSON
|
||||
success, result = get_items_from_json(
|
||||
content,
|
||||
self.private_name,
|
||||
"goal_achieved",
|
||||
"stop_conversation",
|
||||
"reason",
|
||||
|
|
@ -310,7 +265,7 @@ class GoalAnalyzer:
|
|||
)
|
||||
|
||||
if not success:
|
||||
logger.error("无法解析对话分析结果JSON")
|
||||
logger.error(f"[私聊][{self.private_name}]无法解析对话分析结果JSON")
|
||||
return False, False, "解析结果失败"
|
||||
|
||||
goal_achieved = result["goal_achieved"]
|
||||
|
|
@ -320,65 +275,67 @@ class GoalAnalyzer:
|
|||
return goal_achieved, stop_conversation, reason
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分析对话状态时出错: {str(e)}")
|
||||
logger.error(f"[私聊][{self.private_name}]分析对话状态时出错: {str(e)}")
|
||||
return False, False, f"分析出错: {str(e)}"
|
||||
|
||||
|
||||
class DirectMessageSender:
|
||||
"""直接发送消息到平台的发送器"""
|
||||
# 先注释掉,万一以后出问题了还能开回来(((
|
||||
# class DirectMessageSender:
|
||||
# """直接发送消息到平台的发送器"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = get_module_logger("direct_sender")
|
||||
self.storage = MessageStorage()
|
||||
# def __init__(self, private_name: str):
|
||||
# self.logger = get_module_logger("direct_sender")
|
||||
# self.storage = MessageStorage()
|
||||
# self.private_name = private_name
|
||||
|
||||
async def send_via_ws(self, message: MessageSending) -> None:
|
||||
try:
|
||||
await global_api.send_message(message)
|
||||
except Exception as e:
|
||||
raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
# async def send_via_ws(self, message: MessageSending) -> None:
|
||||
# try:
|
||||
# await global_api.send_message(message)
|
||||
# except Exception as e:
|
||||
# raise ValueError(f"未找到平台:{message.message_info.platform} 的url配置,请检查配置文件") from e
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
chat_stream: ChatStream,
|
||||
content: str,
|
||||
reply_to_message: Optional[Message] = None,
|
||||
) -> None:
|
||||
"""直接发送消息到平台
|
||||
# async def send_message(
|
||||
# self,
|
||||
# chat_stream: ChatStream,
|
||||
# content: str,
|
||||
# reply_to_message: Optional[Message] = None,
|
||||
# ) -> None:
|
||||
# """直接发送消息到平台
|
||||
|
||||
Args:
|
||||
chat_stream: 聊天流
|
||||
content: 消息内容
|
||||
reply_to_message: 要回复的消息
|
||||
"""
|
||||
# 构建消息对象
|
||||
message_segment = Seg(type="text", data=content)
|
||||
bot_user_info = UserInfo(
|
||||
user_id=global_config.BOT_QQ,
|
||||
user_nickname=global_config.BOT_NICKNAME,
|
||||
platform=chat_stream.platform,
|
||||
)
|
||||
# Args:
|
||||
# chat_stream: 聊天流
|
||||
# content: 消息内容
|
||||
# reply_to_message: 要回复的消息
|
||||
# """
|
||||
# # 构建消息对象
|
||||
# message_segment = Seg(type="text", data=content)
|
||||
# bot_user_info = UserInfo(
|
||||
# user_id=global_config.BOT_QQ,
|
||||
# user_nickname=global_config.BOT_NICKNAME,
|
||||
# platform=chat_stream.platform,
|
||||
# )
|
||||
|
||||
message = MessageSending(
|
||||
message_id=f"dm{round(time.time(), 2)}",
|
||||
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=message_segment,
|
||||
reply=reply_to_message,
|
||||
is_head=True,
|
||||
is_emoji=False,
|
||||
thinking_start_time=time.time(),
|
||||
)
|
||||
# message = MessageSending(
|
||||
# message_id=f"dm{round(time.time(), 2)}",
|
||||
# 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=message_segment,
|
||||
# reply=reply_to_message,
|
||||
# is_head=True,
|
||||
# is_emoji=False,
|
||||
# thinking_start_time=time.time(),
|
||||
# )
|
||||
|
||||
# 处理消息
|
||||
await message.process()
|
||||
# # 处理消息
|
||||
# await message.process()
|
||||
|
||||
_message_json = message.to_dict()
|
||||
# _message_json = message.to_dict()
|
||||
|
||||
# 发送消息
|
||||
try:
|
||||
await self.send_via_ws(message)
|
||||
await self.storage.store_message(message, chat_stream)
|
||||
logger.success(f"PFC消息已发送: {content}")
|
||||
except Exception as e:
|
||||
logger.error(f"PFC消息发送失败: {str(e)}")
|
||||
# # 发送消息
|
||||
# try:
|
||||
# await self.send_via_ws(message)
|
||||
# await self.storage.store_message(message, chat_stream)
|
||||
# logger.success(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
|
||||
# except Exception as e:
|
||||
# logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from ..models.utils_model import LLMRequest
|
|||
from ...config.config import global_config
|
||||
from ..chat.message import Message
|
||||
from ..knowledge.knowledge_lib import qa_manager
|
||||
from ..utils.chat_message_builder import build_readable_messages
|
||||
|
||||
logger = get_module_logger("knowledge_fetcher")
|
||||
|
||||
|
|
@ -12,13 +13,14 @@ logger = get_module_logger("knowledge_fetcher")
|
|||
class KnowledgeFetcher:
|
||||
"""知识调取器"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, private_name: str):
|
||||
self.llm = LLMRequest(
|
||||
model=global_config.llm_normal,
|
||||
temperature=global_config.llm_normal["temp"],
|
||||
max_tokens=1000,
|
||||
request_type="knowledge_fetch",
|
||||
)
|
||||
self.private_name = private_name
|
||||
|
||||
def _lpmm_get_knowledge(self, query: str) -> str:
|
||||
"""获取相关知识
|
||||
|
|
@ -30,13 +32,13 @@ class KnowledgeFetcher:
|
|||
str: 构造好的,带相关度的知识
|
||||
"""
|
||||
|
||||
logger.debug("正在从LPMM知识库中获取知识")
|
||||
logger.debug(f"[私聊][{self.private_name}]正在从LPMM知识库中获取知识")
|
||||
try:
|
||||
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
|
||||
except Exception as e:
|
||||
logger.error(f"LPMM知识库搜索工具执行失败: {str(e)}")
|
||||
logger.error(f"[私聊][{self.private_name}]LPMM知识库搜索工具执行失败: {str(e)}")
|
||||
return "未找到匹配的知识"
|
||||
|
||||
async def fetch(self, query: str, chat_history: List[Message]) -> Tuple[str, str]:
|
||||
|
|
@ -50,10 +52,13 @@ class KnowledgeFetcher:
|
|||
Tuple[str, str]: (获取的知识, 知识来源)
|
||||
"""
|
||||
# 构建查询上下文
|
||||
chat_history_text = ""
|
||||
for msg in chat_history:
|
||||
# sender = msg.message_info.user_info.user_nickname or f"用户{msg.message_info.user_info.user_id}"
|
||||
chat_history_text += f"{msg.detailed_plain_text}\n"
|
||||
chat_history_text = await build_readable_messages(
|
||||
chat_history,
|
||||
replace_bot_name=True,
|
||||
merge_messages=False,
|
||||
timestamp_mode="relative",
|
||||
read_mark=0.0,
|
||||
)
|
||||
|
||||
# 从记忆中获取相关知识
|
||||
related_memory = await HippocampusManager.get_instance().get_memory_from_text(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class PFCManager:
|
|||
cls._instance = PFCManager()
|
||||
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:
|
||||
|
|
@ -39,11 +39,11 @@ class PFCManager:
|
|||
"""
|
||||
# 检查是否已经有实例
|
||||
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
|
||||
|
||||
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]
|
||||
if stream_id in self._instances:
|
||||
instance = self._instances[stream_id]
|
||||
|
|
@ -52,28 +52,28 @@ class PFCManager:
|
|||
and 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 吧喵。
|
||||
return None
|
||||
|
||||
# 检查 should_continue 状态
|
||||
if instance.should_continue:
|
||||
logger.debug(f"使用现有会话实例: {stream_id}")
|
||||
logger.debug(f"[私聊][{private_name}]使用现有会话实例: {stream_id}")
|
||||
return instance
|
||||
# else: 实例存在但不应继续
|
||||
try:
|
||||
# 创建新实例
|
||||
logger.info(f"创建新的对话实例: {stream_id}")
|
||||
logger.info(f"[私聊][{private_name}]创建新的对话实例: {stream_id}")
|
||||
self._initializing[stream_id] = True
|
||||
# 创建实例
|
||||
conversation_instance = Conversation(stream_id)
|
||||
conversation_instance = Conversation(stream_id, private_name)
|
||||
self._instances[stream_id] = conversation_instance
|
||||
|
||||
# 启动实例初始化
|
||||
await self._initialize_conversation(conversation_instance)
|
||||
except Exception as e:
|
||||
logger.error(f"创建会话实例失败: {stream_id}, 错误: {e}")
|
||||
logger.error(f"[私聊][{private_name}]创建会话实例失败: {stream_id}, 错误: {e}")
|
||||
return None
|
||||
|
||||
return conversation_instance
|
||||
|
|
@ -85,20 +85,21 @@ class PFCManager:
|
|||
conversation: 要初始化的会话实例
|
||||
"""
|
||||
stream_id = conversation.stream_id
|
||||
private_name = conversation.private_name
|
||||
|
||||
try:
|
||||
logger.info(f"开始初始化会话实例: {stream_id}")
|
||||
logger.info(f"[私聊][{private_name}]开始初始化会话实例: {stream_id}")
|
||||
# 启动初始化流程
|
||||
await conversation._initialize()
|
||||
|
||||
# 标记初始化完成
|
||||
self._initializing[stream_id] = False
|
||||
|
||||
logger.info(f"会话实例 {stream_id} 初始化完成")
|
||||
logger.info(f"[私聊][{private_name}]会话实例 {stream_id} 初始化完成")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"管理器初始化会话实例失败: {stream_id}, 错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"[私聊][{private_name}]管理器初始化会话实例失败: {stream_id}, 错误: {e}")
|
||||
logger.error(f"[私聊][{private_name}]{traceback.format_exc()}")
|
||||
# 清理失败的初始化
|
||||
|
||||
async def get_conversation(self, stream_id: str) -> Optional[Conversation]:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ logger = get_module_logger("pfc_utils")
|
|||
|
||||
def get_items_from_json(
|
||||
content: str,
|
||||
private_name: str,
|
||||
*items: str,
|
||||
default_values: Optional[Dict[str, Any]] = None,
|
||||
required_types: Optional[Dict[str, type]] = None,
|
||||
|
|
@ -78,9 +79,9 @@ def get_items_from_json(
|
|||
if valid_items:
|
||||
return True, valid_items
|
||||
except json.JSONDecodeError:
|
||||
logger.debug("JSON数组解析失败,尝试解析单个JSON对象")
|
||||
logger.debug(f"[私聊][{private_name}]JSON数组解析失败,尝试解析单个JSON对象")
|
||||
except Exception as e:
|
||||
logger.debug(f"尝试解析JSON数组时出错: {str(e)}")
|
||||
logger.debug(f"[私聊][{private_name}]尝试解析JSON数组时出错: {str(e)}")
|
||||
|
||||
# 尝试解析JSON对象
|
||||
try:
|
||||
|
|
@ -93,10 +94,10 @@ def get_items_from_json(
|
|||
try:
|
||||
json_data = json.loads(json_match.group())
|
||||
except json.JSONDecodeError:
|
||||
logger.error("提取的JSON内容解析失败")
|
||||
logger.error(f"[私聊][{private_name}]提取的JSON内容解析失败")
|
||||
return False, result
|
||||
else:
|
||||
logger.error("无法在返回内容中找到有效的JSON")
|
||||
logger.error(f"[私聊][{private_name}]无法在返回内容中找到有效的JSON")
|
||||
return False, result
|
||||
|
||||
# 提取字段
|
||||
|
|
@ -106,20 +107,20 @@ def get_items_from_json(
|
|||
|
||||
# 验证必需字段
|
||||
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
|
||||
|
||||
# 验证字段类型
|
||||
if required_types:
|
||||
for field, expected_type in required_types.items():
|
||||
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
|
||||
|
||||
# 验证字符串字段不为空
|
||||
for field in items:
|
||||
if isinstance(result[field], str) and not result[field].strip():
|
||||
logger.error(f"{field} 不能为空")
|
||||
logger.error(f"[私聊][{private_name}]{field} 不能为空")
|
||||
return False, result
|
||||
|
||||
return True, result
|
||||
|
|
|
|||
|
|
@ -12,12 +12,13 @@ logger = get_module_logger("reply_checker")
|
|||
class ReplyChecker:
|
||||
"""回复检查器"""
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
self.llm = LLMRequest(
|
||||
model=global_config.llm_PFC_reply_checker, temperature=0.50, max_tokens=1000, request_type="reply_check"
|
||||
)
|
||||
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 # 最大重试次数
|
||||
|
||||
async def check(
|
||||
|
|
@ -49,68 +50,70 @@ class ReplyChecker:
|
|||
# 可以用简单比较,或者更复杂的相似度库 (如 difflib)
|
||||
# 简单比较:是否完全相同
|
||||
if reply == bot_messages[0]: # 和最近一条完全一样
|
||||
logger.warning(f"ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'")
|
||||
logger.warning(
|
||||
f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息完全相同: '{reply}'"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
"回复内容与你上一条发言完全相同,请修改,可以选择深入话题或寻找其它话题或等待",
|
||||
False,
|
||||
) # 不合适,无需重新规划
|
||||
"被逻辑检查拒绝:回复内容与你上一条发言完全相同,可以选择深入话题或寻找其它话题或等待",
|
||||
True,
|
||||
) # 不合适,需要返回至决策层
|
||||
# 2. 相似度检查 (如果精确匹配未通过)
|
||||
import difflib # 导入 difflib 库
|
||||
|
||||
# 计算编辑距离相似度,ratio() 返回 0 到 1 之间的浮点数
|
||||
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
|
||||
if similarity_ratio > similarity_threshold:
|
||||
logger.warning(
|
||||
f"ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'"
|
||||
f"[私聊][{self.private_name}]ReplyChecker 检测到回复与上一条 Bot 消息高度相似 (相似度 {similarity_ratio:.2f}): '{reply}'"
|
||||
)
|
||||
return (
|
||||
False,
|
||||
f"拒绝发送:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),请修改,可以选择深入话题或寻找其它话题或等待。",
|
||||
False,
|
||||
f"被逻辑检查拒绝:回复内容与你上一条发言高度相似 (相似度 {similarity_ratio:.2f}),可以选择深入话题或寻找其它话题或等待。",
|
||||
True,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
logger.error(f"检查回复时出错: 类型={type(e)}, 值={e}")
|
||||
logger.error(traceback.format_exc()) # 打印详细的回溯信息
|
||||
logger.error(f"[私聊][{self.private_name}]检查回复时出错: 类型={type(e)}, 值={e}")
|
||||
logger.error(f"[私聊][{self.private_name}]{traceback.format_exc()}") # 打印详细的回溯信息
|
||||
|
||||
prompt = f"""请检查以下回复或消息是否合适:
|
||||
prompt = f"""你是一个聊天逻辑检查器,请检查以下回复或消息是否合适:
|
||||
|
||||
当前对话目标:{goal}
|
||||
最新的对话记录:
|
||||
{chat_history_text}
|
||||
|
||||
待检查的回复:
|
||||
待检查的消息:
|
||||
{reply}
|
||||
|
||||
请结合聊天记录检查以下几点:
|
||||
1. 回复是否依然符合当前对话目标和实现方式
|
||||
2. 回复是否与最新的对话记录保持一致性
|
||||
3. 回复是否重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义)
|
||||
4. 回复是否包含违规内容(例如血腥暴力,政治敏感等)
|
||||
5. 回复是否以你的角度发言,不要把"你"说的话当做对方说的话,这是你自己说的话(不要自己回复自己的消息)
|
||||
6. 回复是否通俗易懂
|
||||
7. 回复是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断)
|
||||
8. 回复是否使用了完全没必要的修辞
|
||||
9. 回复是否逻辑通顺
|
||||
10. 回复是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况)
|
||||
11. 在连续多次发送消息的情况下,当前回复是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠)
|
||||
1. 这条消息是否依然符合当前对话目标和实现方式
|
||||
2. 这条消息是否与最新的对话记录保持一致性
|
||||
3. 是否存在重复发言,或重复表达同质内容(尤其是只是换一种方式表达了相同的含义)
|
||||
4. 这条消息是否包含违规内容(例如血腥暴力,政治敏感等)
|
||||
5. 这条消息是否以发送者的角度发言(不要让发送者自己回复自己的消息)
|
||||
6. 这条消息是否通俗易懂
|
||||
7. 这条消息是否有些多余,例如在对方没有回复的情况下,依然连续多次“消息轰炸”(尤其是已经连续发送3条信息的情况,这很可能不合理,需要着重判断)
|
||||
8. 这条消息是否使用了完全没必要的修辞
|
||||
9. 这条消息是否逻辑通顺
|
||||
10. 这条消息是否太过冗长了(通常私聊的每条消息长度在20字以内,除非特殊情况)
|
||||
11. 在连续多次发送消息的情况下,这条消息是否衔接自然,会不会显得奇怪(例如连续两条消息中部分内容重叠)
|
||||
|
||||
请以JSON格式输出,包含以下字段:
|
||||
1. suitable: 是否合适 (true/false)
|
||||
2. reason: 原因说明
|
||||
3. need_replan: 是否需要重新规划对话目标 (true/false),当发现当前对话目标不再适合时设为true
|
||||
3. need_replan: 是否需要重新决策 (true/false),当你认为此时已经不适合发消息,需要规划其它行动时,设为true
|
||||
|
||||
输出格式示例:
|
||||
{{
|
||||
"suitable": true,
|
||||
"reason": "回复符合要求,内容得体",
|
||||
"reason": "回复符合要求,虽然有可能略微偏离目标,但是整体内容流畅得体",
|
||||
"need_replan": false
|
||||
}}
|
||||
|
||||
|
|
@ -118,7 +121,7 @@ class ReplyChecker:
|
|||
|
||||
try:
|
||||
content, _ = await self.llm.generate_response_async(prompt)
|
||||
logger.debug(f"检查回复的原始返回: {content}")
|
||||
logger.debug(f"[私聊][{self.private_name}]检查回复的原始返回: {content}")
|
||||
|
||||
# 清理内容,尝试提取JSON部分
|
||||
content = content.strip()
|
||||
|
|
@ -171,7 +174,7 @@ class ReplyChecker:
|
|||
return suitable, reason, need_replan
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检查回复时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]检查回复时出错: {e}")
|
||||
# 如果出错且已达到最大重试次数,建议重新规划
|
||||
if retry_count >= self.max_retries:
|
||||
return False, "多次检查失败,建议重新规划", True
|
||||
|
|
|
|||
|
|
@ -11,130 +11,17 @@ from src.plugins.utils.chat_message_builder import build_readable_messages
|
|||
|
||||
logger = get_module_logger("reply_generator")
|
||||
|
||||
# --- 定义 Prompt 模板 ---
|
||||
|
||||
class ReplyGenerator:
|
||||
"""回复生成器"""
|
||||
|
||||
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私聊,请根据以下信息生成一条新消息:
|
||||
# Prompt for direct_reply (首次回复)
|
||||
PROMPT_DIRECT_REPLY = """{persona_text}。现在你在参与一场QQ私聊,请根据以下信息生成一条回复:
|
||||
|
||||
当前对话目标:{goals_str}
|
||||
最近的聊天记录:
|
||||
{chat_history_text}
|
||||
|
||||
|
||||
请根据上述信息,结合聊天记录,发一条消息(可以是回复,补充,深入话题,或追问等等)。该消息应该:
|
||||
请根据上述信息,结合聊天记录,回复对方。该回复应该:
|
||||
1. 符合对话目标,以"你"的角度发言(不要自己与自己对话!)
|
||||
2. 符合你的性格特征和身份细节
|
||||
3. 通俗易懂,自然流畅,像正常聊天一样,简短(通常20字以内,除非特殊情况)
|
||||
|
|
@ -145,41 +32,135 @@ class ReplyGenerator:
|
|||
可以回复得自然随意自然一些,就像真人一样,注意把握聊天内容,整体风格可以平和、简短,不要刻意突出自身学科背景,不要说你说过的话,可以简短,多简短都可以,但是避免冗长。
|
||||
请你注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。
|
||||
不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,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:
|
||||
content, _ = await self.llm.generate_response_async(prompt)
|
||||
logger.info(f"生成的回复: {content}")
|
||||
# is_new = self.chat_observer.check()
|
||||
# logger.debug(f"再看一眼聊天记录,{'有' if is_new else '没有'}新消息")
|
||||
|
||||
# 如果有新消息,重新生成回复
|
||||
# if is_new:
|
||||
# logger.info("检测到新消息,重新生成回复")
|
||||
# return await self.generate(
|
||||
# goal, chat_history, knowledge_cache,
|
||||
# None, retry_count
|
||||
# )
|
||||
|
||||
logger.debug(f"[私聊][{self.private_name}]生成的回复: {content}")
|
||||
# 移除旧的检查新消息逻辑,这应该由 conversation 控制流处理
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成回复时出错: {e}")
|
||||
logger.error(f"[私聊][{self.private_name}]生成回复时出错: {e}")
|
||||
return "抱歉,我现在有点混乱,让我重新思考一下..."
|
||||
|
||||
# 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
|
||||
) -> 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)
|
||||
|
|
|
|||
|
|
@ -17,60 +17,63 @@ DESIRED_TIMEOUT_SECONDS = 300
|
|||
class Waiter:
|
||||
"""等待处理类"""
|
||||
|
||||
def __init__(self, stream_id: str):
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id)
|
||||
def __init__(self, stream_id: str, private_name: str):
|
||||
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
|
||||
self.name = global_config.BOT_NICKNAME
|
||||
self.private_name = private_name
|
||||
# self.wait_accumulated_time = 0 # 不再需要累加计时
|
||||
|
||||
async def wait(self, conversation_info: ConversationInfo) -> bool:
|
||||
"""等待用户新消息或超时"""
|
||||
wait_start_time = time.time()
|
||||
logger.info(f"进入常规等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
|
||||
logger.info(f"[私聊][{self.private_name}]进入常规等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
|
||||
|
||||
while True:
|
||||
# 检查是否有新消息
|
||||
if self.chat_observer.new_message_after(wait_start_time):
|
||||
logger.info("等待结束,收到新消息")
|
||||
logger.info(f"[私聊][{self.private_name}]等待结束,收到新消息")
|
||||
return False # 返回 False 表示不是超时
|
||||
|
||||
# 检查是否超时
|
||||
elapsed_time = time.time() - wait_start_time
|
||||
if elapsed_time > DESIRED_TIMEOUT_SECONDS:
|
||||
logger.info(f"等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
|
||||
logger.info(f"[私聊][{self.private_name}]等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
|
||||
wait_goal = {
|
||||
"goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么",
|
||||
"reason": "对方很久没有回复你的消息了",
|
||||
"reasoning": "对方很久没有回复你的消息了",
|
||||
}
|
||||
conversation_info.goal_list.append(wait_goal)
|
||||
logger.info(f"添加目标: {wait_goal}")
|
||||
logger.info(f"[私聊][{self.private_name}]添加目标: {wait_goal}")
|
||||
return True # 返回 True 表示超时
|
||||
|
||||
await asyncio.sleep(5) # 每 5 秒检查一次
|
||||
logger.info("等待中...") # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出
|
||||
logger.debug(
|
||||
f"[私聊][{self.private_name}]等待中..."
|
||||
) # 可以考虑把这个频繁日志注释掉,只在超时或收到消息时输出
|
||||
|
||||
async def wait_listening(self, conversation_info: ConversationInfo) -> bool:
|
||||
"""倾听用户发言或超时"""
|
||||
wait_start_time = time.time()
|
||||
logger.info(f"进入倾听等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
|
||||
logger.info(f"[私聊][{self.private_name}]进入倾听等待状态 (超时: {DESIRED_TIMEOUT_SECONDS} 秒)...")
|
||||
|
||||
while True:
|
||||
# 检查是否有新消息
|
||||
if self.chat_observer.new_message_after(wait_start_time):
|
||||
logger.info("倾听等待结束,收到新消息")
|
||||
logger.info(f"[私聊][{self.private_name}]倾听等待结束,收到新消息")
|
||||
return False # 返回 False 表示不是超时
|
||||
|
||||
# 检查是否超时
|
||||
elapsed_time = time.time() - wait_start_time
|
||||
if elapsed_time > DESIRED_TIMEOUT_SECONDS:
|
||||
logger.info(f"倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
|
||||
logger.info(f"[私聊][{self.private_name}]倾听等待超过 {DESIRED_TIMEOUT_SECONDS} 秒...添加思考目标。")
|
||||
wait_goal = {
|
||||
# 保持 goal 文本一致
|
||||
"goal": f"你等待了{elapsed_time / 60:.1f}分钟,注意可能在对方看来聊天已经结束,思考接下来要做什么",
|
||||
"reason": "对方话说一半消失了,很久没有回复",
|
||||
"goal": f"你等待了{elapsed_time / 60:.1f}分钟,对方似乎话说一半突然消失了,可能忙去了?也可能忘记了回复?要问问吗?还是结束对话?或继续等待?思考接下来要做什么",
|
||||
"reasoning": "对方话说一半消失了,很久没有回复",
|
||||
}
|
||||
conversation_info.goal_list.append(wait_goal)
|
||||
logger.info(f"添加目标: {wait_goal}")
|
||||
logger.info(f"[私聊][{self.private_name}]添加目标: {wait_goal}")
|
||||
return True # 返回 True 表示超时
|
||||
|
||||
await asyncio.sleep(5) # 每 5 秒检查一次
|
||||
logger.info("倾听等待中...") # 同上,可以考虑注释掉
|
||||
logger.debug(f"[私聊][{self.private_name}]倾听等待中...") # 同上,可以考虑注释掉
|
||||
|
|
|
|||
|
|
@ -38,15 +38,17 @@ class ChatBot:
|
|||
async def _create_pfc_chat(self, message: MessageRecv):
|
||||
try:
|
||||
chat_id = str(message.chat_stream.stream_id)
|
||||
private_name = str(message.message_info.user_info.user_nickname)
|
||||
|
||||
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:
|
||||
logger.error(f"创建PFC聊天失败: {e}")
|
||||
|
||||
async def message_process(self, message_data: str) -> None:
|
||||
"""处理转化后的统一格式消息
|
||||
这个函数本质是预处理一些数据,根据配置信息和消息内容,预处理消息,并分发到合适的消息处理器中
|
||||
heart_flow模式:使用思维流系统进行回复
|
||||
- 包含思维流状态管理
|
||||
- 在回复前进行观察和状态更新
|
||||
|
|
@ -74,14 +76,17 @@ class ChatBot:
|
|||
groupinfo = message.message_info.group_info
|
||||
userinfo = message.message_info.user_info
|
||||
|
||||
# 用户黑名单拦截
|
||||
if userinfo.user_id in global_config.ban_user_id:
|
||||
logger.debug(f"用户{userinfo.user_id}被禁止回复")
|
||||
return
|
||||
|
||||
# 群聊黑名单拦截
|
||||
if groupinfo != None and groupinfo.group_id not in global_config.talk_allowed_groups:
|
||||
logger.trace(f"群{groupinfo.group_id}被禁止回复")
|
||||
return
|
||||
|
||||
# 确认从接口发来的message是否有自定义的prompt模板信息
|
||||
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_items = message.message_info.template_info.template_items
|
||||
|
|
@ -94,8 +99,11 @@ class ChatBot:
|
|||
template_group_name = None
|
||||
|
||||
async def preprocess():
|
||||
# 如果在私聊中
|
||||
if groupinfo is None:
|
||||
# 是否在配置信息中开启私聊模式
|
||||
if global_config.enable_friend_chat:
|
||||
# 是否进入PFC
|
||||
if global_config.enable_pfc_chatting:
|
||||
userinfo = message.message_info.user_info
|
||||
messageinfo = message.message_info
|
||||
|
|
@ -108,8 +116,10 @@ class ChatBot:
|
|||
message.update_chat_stream(chat)
|
||||
await self.only_process_chat.process_message(message)
|
||||
await self._create_pfc_chat(message)
|
||||
# 禁止PFC,进入普通的心流消息处理逻辑
|
||||
else:
|
||||
await self.heartflow_processor.process_message(message_data)
|
||||
# 群聊默认进入心流消息处理逻辑
|
||||
else:
|
||||
await self.heartflow_processor.process_message(message_data)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
import random # <-- 添加导入
|
||||
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
|
||||
from collections import deque
|
||||
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.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.heartFC_chat.heartFC_generator import HeartFCGenerator
|
||||
from src.do_tool.tool_use import ToolUser
|
||||
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.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
|
||||
from src.plugins.utils.chat_message_builder import num_new_messages_since
|
||||
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
|
||||
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
|
||||
|
|
@ -181,12 +183,18 @@ class HeartFChatting:
|
|||
self.action_manager = ActionManager()
|
||||
|
||||
# 初始化状态控制
|
||||
self._initialized = False # 是否已初始化标志
|
||||
self._processing_lock = asyncio.Lock() # 处理锁(确保单次Plan-Replier-Sender周期)
|
||||
self._initialized = False
|
||||
self._processing_lock = asyncio.Lock()
|
||||
|
||||
# 依赖注入存储
|
||||
self.gpt_instance = HeartFCGenerator() # 文本回复生成器
|
||||
self.tool_user = ToolUser() # 工具使用实例
|
||||
# --- 移除 gpt_instance, 直接初始化 LLM 模型 ---
|
||||
# self.gpt_instance = HeartFCGenerator() # <-- 移除
|
||||
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()
|
||||
|
||||
# LLM规划器配置
|
||||
|
|
@ -401,20 +409,23 @@ class HeartFChatting:
|
|||
with Timer("决策", cycle_timers):
|
||||
planner_result = await self._planner(current_mind, cycle_timers)
|
||||
|
||||
action = planner_result.get("action", "error")
|
||||
reasoning = planner_result.get("reasoning", "未提供理由")
|
||||
# 效果不太好,还没处理replan导致观察时间点改变的问题
|
||||
|
||||
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:
|
||||
logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...")
|
||||
# 重新规划
|
||||
with Timer("重新决策", cycle_timers):
|
||||
self._current_cycle.replanned = True
|
||||
planner_result = await self._planner(current_mind, cycle_timers, is_re_planned=True)
|
||||
logger.info(f"{self.log_prefix} 重新规划完成.")
|
||||
|
||||
# if await self._check_new_messages(planner_start_db_time):
|
||||
# if random.random() < 0.2:
|
||||
# logger.info(f"{self.log_prefix} 看到了新消息,麦麦决定重新观察和规划...")
|
||||
# # 重新规划
|
||||
# with Timer("重新决策", cycle_timers):
|
||||
# self._current_cycle.replanned = True
|
||||
# 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")
|
||||
|
|
@ -736,94 +747,104 @@ class HeartFChatting:
|
|||
observed_messages_str = observation.talking_message_str
|
||||
|
||||
# --- 使用 LLM 进行决策 --- #
|
||||
action = "no_reply" # 默认动作
|
||||
emoji_query = "" # 默认表情查询
|
||||
reasoning = "默认决策或获取决策失败"
|
||||
llm_error = False # LLM错误标志
|
||||
arguments = None # 初始化参数变量
|
||||
emoji_query = "" # <--- 在这里初始化 emoji_query
|
||||
|
||||
try:
|
||||
# 构建提示词
|
||||
|
||||
# --- 构建提示词 ---
|
||||
replan_prompt_str = ""
|
||||
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
|
||||
)
|
||||
prompt = replan_prompt
|
||||
else:
|
||||
replan_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:
|
||||
print("prompt")
|
||||
print("prompt")
|
||||
print("prompt")
|
||||
print(payload)
|
||||
print(prompt)
|
||||
response = await self.planner_llm._execute_request(
|
||||
endpoint="/chat/completions", payload=payload, prompt=prompt
|
||||
planner_tools = self.action_manager.get_planner_tool_definition()
|
||||
_response_text, _reasoning_content, tool_calls = await self.planner_llm.generate_response_tool_async(
|
||||
prompt=prompt,
|
||||
tools=planner_tools,
|
||||
)
|
||||
print(response)
|
||||
logger.debug(f"{self.log_prefix}[Planner] 原始人 LLM响应: {_response_text}")
|
||||
except Exception as req_e:
|
||||
logger.error(f"{self.log_prefix}[Planner] LLM请求执行失败: {req_e}")
|
||||
action = "error"
|
||||
reasoning = f"LLM请求失败: {req_e}"
|
||||
llm_error = True
|
||||
# 直接返回错误结果
|
||||
return {
|
||||
"action": "error",
|
||||
"reasoning": f"LLM请求执行失败: {req_e}",
|
||||
"action": action,
|
||||
"reasoning": reasoning,
|
||||
"emoji_query": "",
|
||||
"current_mind": current_mind,
|
||||
"observed_messages": observed_messages,
|
||||
"llm_error": True,
|
||||
"llm_error": llm_error,
|
||||
}
|
||||
|
||||
# 处理LLM响应
|
||||
with Timer("使用工具", cycle_timers):
|
||||
# 使用辅助函数处理工具调用响应
|
||||
print(1111122222222222)
|
||||
print(response)
|
||||
# 默认错误状态
|
||||
action = "error"
|
||||
reasoning = "处理工具调用时出错"
|
||||
llm_error = True
|
||||
|
||||
success, arguments, error_msg = process_llm_tool_response(
|
||||
response, expected_tool_name="decide_reply_action", log_prefix=f"{self.log_prefix}[Planner] "
|
||||
)
|
||||
# 1. 验证工具调用
|
||||
success, valid_tool_calls, error_msg = process_llm_tool_calls(
|
||||
tool_calls, log_prefix=f"{self.log_prefix}[Planner] "
|
||||
)
|
||||
|
||||
if success:
|
||||
# 提取决策参数
|
||||
action = arguments.get("action", "no_reply")
|
||||
# 验证动作是否在可用动作集中
|
||||
if action not in self.action_manager.get_available_actions():
|
||||
if success and valid_tool_calls:
|
||||
# 2. 提取第一个调用并获取参数
|
||||
first_tool_call = valid_tool_calls[0]
|
||||
tool_name = first_tool_call.get("function", {}).get("name")
|
||||
arguments = extract_tool_call_arguments(first_tool_call, None)
|
||||
|
||||
# 3. 检查名称和参数
|
||||
expected_tool_name = "decide_reply_action"
|
||||
if tool_name == expected_tool_name and arguments is not None:
|
||||
# 4. 成功,提取决策
|
||||
extracted_action = arguments.get("action", "no_reply")
|
||||
# 验证动作
|
||||
if extracted_action not in self.action_manager.get_available_actions():
|
||||
logger.warning(
|
||||
f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {action},使用默认动作no_reply"
|
||||
f"{self.log_prefix}[Planner] LLM返回了未授权的动作: {extracted_action},使用默认动作no_reply"
|
||||
)
|
||||
action = "no_reply"
|
||||
reasoning = f"LLM返回了未授权的动作: {action}"
|
||||
reasoning = f"LLM返回了未授权的动作: {extracted_action}"
|
||||
emoji_query = ""
|
||||
llm_error = False # 视为非LLM错误,只是逻辑修正
|
||||
else:
|
||||
# 动作有效,使用提取的值
|
||||
action = extracted_action
|
||||
reasoning = arguments.get("reasoning", "未提供理由")
|
||||
emoji_query = arguments.get("emoji_query", "")
|
||||
|
||||
# 记录决策结果
|
||||
logger.debug(
|
||||
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
|
||||
)
|
||||
else:
|
||||
# 处理工具调用失败
|
||||
logger.warning(f"{self.log_prefix}[Planner] {error_msg}")
|
||||
action = "error"
|
||||
reasoning = error_msg
|
||||
llm_error = True
|
||||
llm_error = False # 成功处理
|
||||
# 记录决策结果
|
||||
logger.debug(
|
||||
f"{self.log_prefix}[要做什么]\nPrompt:\n{prompt}\n\n决策结果: {action}, 理由: {reasoning}, 表情查询: '{emoji_query}'"
|
||||
)
|
||||
elif tool_name != expected_tool_name:
|
||||
reasoning = f"LLM返回了非预期的工具: {tool_name}"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
else: # arguments is None
|
||||
reasoning = f"无法提取工具 {tool_name} 的参数"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
elif not success:
|
||||
reasoning = f"验证工具调用失败: {error_msg}"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
else: # not valid_tool_calls
|
||||
reasoning = "LLM未返回有效的工具调用"
|
||||
logger.warning(f"{self.log_prefix}[Planner] {reasoning}")
|
||||
# 如果 llm_error 仍然是 True,说明在处理过程中有错误发生
|
||||
|
||||
except Exception as llm_e:
|
||||
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中出错: {llm_e}")
|
||||
logger.error(traceback.format_exc()) # 记录完整堆栈以便调试
|
||||
logger.error(f"{self.log_prefix}[Planner] Planner LLM处理过程中发生意外错误: {llm_e}")
|
||||
logger.error(traceback.format_exc())
|
||||
action = "error"
|
||||
reasoning = f"LLM处理失败: {llm_e}"
|
||||
reasoning = f"Planner内部处理错误: {llm_e}"
|
||||
llm_error = True
|
||||
# --- 结束 LLM 决策 --- #
|
||||
|
||||
|
|
@ -1044,9 +1065,13 @@ class HeartFChatting:
|
|||
# 如果最近的活动循环不是文本回复,或者没有活动循环
|
||||
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(
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
structured_info_block=structured_info_block,
|
||||
chat_content_block=chat_content_block,
|
||||
current_mind_block=current_mind_block,
|
||||
|
|
@ -1069,27 +1094,66 @@ class HeartFChatting:
|
|||
thinking_id: str,
|
||||
) -> Optional[List[str]]:
|
||||
"""
|
||||
回复器 (Replier): 核心逻辑用于生成回复。
|
||||
回复器 (Replier): 核心逻辑,负责生成回复文本。
|
||||
(已整合原 HeartFCGenerator 的功能)
|
||||
"""
|
||||
response_set: Optional[List[str]] = None
|
||||
try:
|
||||
response_set = await self.gpt_instance.generate_response(
|
||||
structured_info=self.sub_mind.structured_info,
|
||||
current_mind_info=self.sub_mind.current_mind,
|
||||
reason=reason,
|
||||
message=anchor_message, # Pass anchor_message positionally (matches 'message' parameter)
|
||||
thinking_id=thinking_id, # Pass thinking_id positionally
|
||||
)
|
||||
# 1. 获取情绪影响因子并调整模型温度
|
||||
arousal_multiplier = MoodManager.get_instance().get_arousal_multiplier()
|
||||
current_temp = global_config.llm_normal["temp"] * arousal_multiplier
|
||||
self.model_normal.temperature = current_temp # 动态调整温度
|
||||
|
||||
if not response_set:
|
||||
logger.warning(f"{self.log_prefix}[Replier-{thinking_id}] LLM生成了一个空回复集。")
|
||||
# 2. 获取信息捕捉器
|
||||
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 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:
|
||||
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
|
||||
|
||||
# --- Methods moved from HeartFCController start ---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -5,7 +5,7 @@ from ...individuality.individuality import Individuality
|
|||
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.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
|
||||
from typing import Union, Optional
|
||||
from ...common.database import db
|
||||
|
|
@ -25,7 +25,7 @@ def init_prompt():
|
|||
{chat_target}
|
||||
{chat_talking_prompt}
|
||||
现在你想要在群里发言或者回复。\n
|
||||
你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality} {prompt_identity}"。
|
||||
你需要扮演一位网名叫{bot_name}的人进行回复,这个人的特点是:"{prompt_personality}"。
|
||||
你正在{chat_target_2},现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,你可以参考贴吧,知乎或者微博的回复风格。
|
||||
看到以上聊天记录,你刚刚在想:
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ def init_prompt():
|
|||
|
||||
# Planner提示词 - 优化版
|
||||
Prompt(
|
||||
"""你的名字是{bot_name},一名大二女生,正在QQ群聊中。需要基于以下信息决定如何参与对话:
|
||||
"""你的名字是{bot_name},{prompt_personality},你现在正在一个群聊中。需要基于以下信息决定如何参与对话:
|
||||
{structured_info_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]:
|
||||
individuality = Individuality.get_instance()
|
||||
prompt_personality = individuality.get_prompt(type="personality", x_person=2, level=1)
|
||||
prompt_identity = individuality.get_prompt(type="identity", x_person=2, level=1)
|
||||
|
||||
prompt_personality = individuality.get_prompt(x_person=0, level=2)
|
||||
# 日程构建
|
||||
# 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,
|
||||
bot_name=global_config.BOT_NICKNAME,
|
||||
prompt_personality=prompt_personality,
|
||||
prompt_identity=prompt_identity,
|
||||
chat_target_2=await global_prompt_manager.get_prompt_async("chat_target_group2")
|
||||
if chat_in_group
|
||||
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"),
|
||||
)
|
||||
|
||||
prompt = await relationship_manager.convert_all_person_sign_to_person_name(prompt)
|
||||
prompt = parse_text_timestamps(prompt, mode="lite")
|
||||
|
||||
return prompt
|
||||
|
||||
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()
|
||||
|
||||
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]}"
|
||||
prompt_personality = individuality.get_prompt(x_person=2, level=2)
|
||||
|
||||
# 关系
|
||||
who_chat_in_group = [
|
||||
|
|
|
|||
|
|
@ -14,51 +14,14 @@ from ...common.database import db
|
|||
from ...plugins.models.utils_model import LLMRequest
|
||||
from src.common.logger_manager import get_logger
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""计算文本的信息量(熵)"""
|
||||
char_count = Counter(text)
|
||||
|
|
@ -232,6 +195,7 @@ class Hippocampus:
|
|||
self.config = None
|
||||
|
||||
def initialize(self, global_config):
|
||||
# 使用导入的 MemoryConfig dataclass 和其 from_global_config 方法
|
||||
self.config = MemoryConfig.from_global_config(global_config)
|
||||
# 初始化子组件
|
||||
self.entorhinal_cortex = EntorhinalCortex(self)
|
||||
|
|
@ -263,17 +227,18 @@ class Hippocampus:
|
|||
@staticmethod
|
||||
def find_topic_llm(text, topic_num):
|
||||
prompt = (
|
||||
f"这是一段文字:{text}。请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
|
||||
f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
|
||||
f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。"
|
||||
f"如果确定找不出主题或者没有明显主题,返回<none>。"
|
||||
)
|
||||
return prompt
|
||||
|
||||
@staticmethod
|
||||
def topic_what(text, topic, time_info):
|
||||
def topic_what(text, topic):
|
||||
# 不再需要 time_info 参数
|
||||
prompt = (
|
||||
f'这是一段文字,{time_info}:{text}。我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,'
|
||||
f"可以包含时间和人物,以及具体的观点。只输出这句话就好"
|
||||
f'这是一段文字:\n{text}\n\n我想让你基于这段文字来概括"{topic}"这个概念,帮我总结成一句自然的话,'
|
||||
f"要求包含对这个概念的定义,内容,知识,可以包含时间和人物。只输出这句话就好"
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
|
@ -831,7 +796,7 @@ class EntorhinalCortex:
|
|||
def get_memory_sample(self):
|
||||
"""从数据库获取记忆样本"""
|
||||
# 硬编码:每条消息最大记忆次数
|
||||
max_memorized_time_per_msg = 3
|
||||
max_memorized_time_per_msg = 2
|
||||
|
||||
# 创建双峰分布的记忆调度器
|
||||
sample_scheduler = MemoryBuildScheduler(
|
||||
|
|
@ -845,9 +810,12 @@ class EntorhinalCortex:
|
|||
)
|
||||
|
||||
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 = []
|
||||
for timestamp in timestamps:
|
||||
# 调用修改后的 random_get_msg_snippet
|
||||
messages = self.random_get_msg_snippet(
|
||||
timestamp, self.config.build_memory_sample_length, max_memorized_time_per_msg
|
||||
)
|
||||
|
|
@ -862,22 +830,45 @@ class EntorhinalCortex:
|
|||
|
||||
@staticmethod
|
||||
def random_get_msg_snippet(target_timestamp: float, chat_size: int, max_memorized_time_per_msg: int) -> list:
|
||||
"""从数据库中随机获取指定时间戳附近的消息片段"""
|
||||
"""从数据库中随机获取指定时间戳附近的消息片段 (使用 chat_message_builder)"""
|
||||
try_count = 0
|
||||
time_window_seconds = random.randint(300, 1800) # 随机时间窗口,5到30分钟
|
||||
|
||||
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:
|
||||
# 检查获取到的所有消息是否都未达到最大记忆次数
|
||||
all_valid = True
|
||||
for message in messages:
|
||||
if message["memorized_times"] >= max_memorized_time_per_msg:
|
||||
messages = None
|
||||
if message.get("memorized_times", 0) >= max_memorized_time_per_msg:
|
||||
all_valid = False
|
||||
break
|
||||
if messages:
|
||||
|
||||
# 如果所有消息都有效
|
||||
if all_valid:
|
||||
# 更新数据库中的记忆次数
|
||||
for message in messages:
|
||||
# 确保在更新前获取最新的 memorized_times,以防万一
|
||||
current_memorized_times = message.get("memorized_times", 0)
|
||||
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
|
||||
target_timestamp -= 120 # 如果第一次尝试失败,稍微向前调整时间戳再试
|
||||
|
||||
# 三次尝试都失败,返回 None
|
||||
return None
|
||||
|
||||
async def sync_memory_to_db(self):
|
||||
|
|
@ -1113,86 +1104,70 @@ class ParahippocampalGyrus:
|
|||
"""压缩和总结消息内容,生成记忆主题和摘要。
|
||||
|
||||
Args:
|
||||
messages (list): 消息列表,每个消息是一个字典,包含以下字段:
|
||||
- time: float, 消息的时间戳
|
||||
- detailed_plain_text: str, 消息的详细文本内容
|
||||
messages (list): 消息列表,每个消息是一个字典,包含数据库消息结构。
|
||||
compress_rate (float, optional): 压缩率,用于控制生成的主题数量。默认为0.1。
|
||||
|
||||
Returns:
|
||||
tuple: (compressed_memory, similar_topics_dict)
|
||||
- compressed_memory: set, 压缩后的记忆集合,每个元素是一个元组 (topic, summary)
|
||||
- topic: str, 记忆主题
|
||||
- summary: str, 主题的摘要描述
|
||||
- similar_topics_dict: dict, 相似主题字典,key为主题,value为相似主题列表
|
||||
每个相似主题是一个元组 (similar_topic, similarity)
|
||||
- similar_topic: str, 相似的主题
|
||||
- similarity: float, 相似度分数(0-1之间)
|
||||
- similar_topics_dict: dict, 相似主题字典
|
||||
|
||||
Process:
|
||||
1. 合并消息文本并生成时间信息
|
||||
2. 使用LLM提取关键主题
|
||||
3. 过滤掉包含禁用关键词的主题
|
||||
4. 为每个主题生成摘要
|
||||
5. 查找与现有记忆中的相似主题
|
||||
1. 使用 build_readable_messages 生成包含时间、人物信息的格式化文本。
|
||||
2. 使用LLM提取关键主题。
|
||||
3. 过滤掉包含禁用关键词的主题。
|
||||
4. 为每个主题生成摘要。
|
||||
5. 查找与现有记忆中的相似主题。
|
||||
"""
|
||||
if not messages:
|
||||
return set(), {}
|
||||
|
||||
# 合并消息文本,同时保留时间信息
|
||||
input_text = ""
|
||||
time_info = ""
|
||||
# 计算最早和最晚时间
|
||||
earliest_time = min(msg["time"] for msg in messages)
|
||||
latest_time = max(msg["time"] for msg in messages)
|
||||
# 1. 使用 build_readable_messages 生成格式化文本
|
||||
# build_readable_messages 只返回一个字符串,不需要解包
|
||||
input_text = await build_readable_messages(
|
||||
messages,
|
||||
merge_messages=True, # 合并连续消息
|
||||
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(), {}
|
||||
|
||||
# 如果是同一年
|
||||
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)
|
||||
logger.debug(f"用于压缩的格式化文本:\n{input_text}")
|
||||
|
||||
# 2. 使用LLM提取关键主题
|
||||
topic_num = self.hippocampus.calculate_topic_num(input_text, compress_rate)
|
||||
topics_response = await self.hippocampus.llm_topic_judge.generate_response(
|
||||
self.hippocampus.find_topic_llm(input_text, topic_num)
|
||||
)
|
||||
|
||||
# 使用正则表达式提取<>中的内容
|
||||
# 提取<>中的内容
|
||||
topics = re.findall(r"<([^>]+)>", topics_response[0])
|
||||
|
||||
# 如果没有找到<>包裹的内容,返回['none']
|
||||
if not topics:
|
||||
topics = ["none"]
|
||||
else:
|
||||
# 处理提取出的话题
|
||||
topics = [
|
||||
topic.strip()
|
||||
for topic in ",".join(topics).replace(",", ",").replace("、", ",").replace(" ", ",").split(",")
|
||||
if topic.strip()
|
||||
]
|
||||
|
||||
# 过滤掉包含禁用关键词的topic
|
||||
# 3. 过滤掉包含禁用关键词的topic
|
||||
filtered_topics = [
|
||||
topic for topic in topics if not any(keyword in topic for keyword in self.config.memory_ban_words)
|
||||
]
|
||||
|
||||
logger.debug(f"过滤后话题: {filtered_topics}")
|
||||
|
||||
# 创建所有话题的请求任务
|
||||
# 4. 创建所有话题的摘要生成任务
|
||||
tasks = []
|
||||
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:
|
||||
task = self.hippocampus.llm_summary_by_topic.generate_response_async(topic_what_prompt)
|
||||
tasks.append((topic.strip(), task))
|
||||
|
|
@ -1363,26 +1338,56 @@ class ParahippocampalGyrus:
|
|||
logger.info("[遗忘] 开始检查节点...")
|
||||
node_check_start = time.time()
|
||||
for node in nodes_to_check:
|
||||
# 检查节点是否存在,以防在迭代中被移除(例如边移除导致)
|
||||
if node not in self.memory_graph.G:
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
# 条件1:检查是否长时间未修改 (超过24小时)
|
||||
if current_time - last_modified > 3600 * 24:
|
||||
memory_items = node_data.get("memory_items", [])
|
||||
if not isinstance(memory_items, list):
|
||||
memory_items = [memory_items] if memory_items else []
|
||||
|
||||
# 条件2:再次确认节点包含记忆项(理论上已确认,但作为保险)
|
||||
if 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:
|
||||
self.memory_graph.G.nodes[node]["memory_items"] = memory_items
|
||||
self.memory_graph.G.nodes[node]["last_modified"] = current_time
|
||||
node_changes["reduced"].append(f"{node} (数量: {current_count} -> {len(memory_items)})")
|
||||
else:
|
||||
self.memory_graph.G.remove_node(node)
|
||||
node_changes["removed"].append(node)
|
||||
# 条件3:检查移除后 memory_items 是否变空
|
||||
if memory_items: # 如果移除后列表不为空
|
||||
# self.memory_graph.G.nodes[node]["memory_items"] = memory_items # 直接修改列表即可
|
||||
self.memory_graph.G.nodes[node]["last_modified"] = current_time # 更新修改时间
|
||||
node_changes["reduced"].append(f"{node} (数量: {current_count} -> {len(memory_items)})")
|
||||
else: # 如果移除后列表为空
|
||||
# 尝试移除节点,处理可能的错误
|
||||
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()
|
||||
logger.info(f"[遗忘] 节点检查耗时: {node_check_end - node_check_start:.2f}秒")
|
||||
|
||||
|
|
@ -1421,6 +1426,119 @@ class ParahippocampalGyrus:
|
|||
end_time = time.time()
|
||||
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:
|
||||
_instance = None
|
||||
|
|
@ -1459,12 +1577,12 @@ class HippocampusManager:
|
|||
edge_count = len(memory_graph.edges())
|
||||
|
||||
logger.success(f"""--------------------------------
|
||||
记忆系统参数配置:
|
||||
构建间隔: {global_config.build_memory_interval}秒|样本数: {config.build_memory_sample_num},长度: {config.build_memory_sample_length}|压缩率: {config.memory_compress_rate}
|
||||
记忆构建分布: {config.memory_build_distribution}
|
||||
遗忘间隔: {global_config.forget_memory_interval}秒|遗忘比例: {global_config.memory_forget_percentage}|遗忘: {config.memory_forget_time}小时之后
|
||||
记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count}
|
||||
--------------------------------""") # noqa: E501
|
||||
记忆系统参数配置:
|
||||
构建间隔: {global_config.build_memory_interval}秒|样本数: {config.build_memory_sample_num},长度: {config.build_memory_sample_length}|压缩率: {config.memory_compress_rate}
|
||||
记忆构建分布: {config.memory_build_distribution}
|
||||
遗忘间隔: {global_config.forget_memory_interval}秒|遗忘比例: {global_config.memory_forget_percentage}|遗忘: {config.memory_forget_time}小时之后
|
||||
记忆图统计信息: 节点数量: {node_count}, 连接数量: {edge_count}
|
||||
--------------------------------""") # noqa: E501
|
||||
|
||||
return self._hippocampus
|
||||
|
||||
|
|
@ -1480,6 +1598,14 @@ class HippocampusManager:
|
|||
raise RuntimeError("HippocampusManager 尚未初始化,请先调用 initialize 方法")
|
||||
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(
|
||||
self,
|
||||
text: str,
|
||||
|
|
|
|||
|
|
@ -18,19 +18,33 @@ class MemoryConfig:
|
|||
# 记忆过滤相关配置
|
||||
memory_ban_words: List[str] # 记忆过滤词列表
|
||||
|
||||
# 新增:记忆整合相关配置
|
||||
consolidation_similarity_threshold: float # 相似度阈值
|
||||
consolidate_memory_percentage: float # 检查节点比例
|
||||
consolidate_memory_interval: int # 记忆整合间隔
|
||||
|
||||
llm_topic_judge: str # 话题判断模型
|
||||
llm_summary_by_topic: str # 话题总结模型
|
||||
|
||||
@classmethod
|
||||
def from_global_config(cls, global_config):
|
||||
"""从全局配置创建记忆系统配置"""
|
||||
# 使用 getattr 提供默认值,防止全局配置缺少这些项
|
||||
return cls(
|
||||
memory_build_distribution=global_config.memory_build_distribution,
|
||||
build_memory_sample_num=global_config.build_memory_sample_num,
|
||||
build_memory_sample_length=global_config.build_memory_sample_length,
|
||||
memory_compress_rate=global_config.memory_compress_rate,
|
||||
memory_forget_time=global_config.memory_forget_time,
|
||||
memory_ban_words=global_config.memory_ban_words,
|
||||
llm_topic_judge=global_config.llm_topic_judge,
|
||||
llm_summary_by_topic=global_config.llm_summary_by_topic,
|
||||
memory_build_distribution=getattr(
|
||||
global_config, "memory_build_distribution", (24, 12, 0.5, 168, 72, 0.5)
|
||||
), # 添加默认值
|
||||
build_memory_sample_num=getattr(global_config, "build_memory_sample_num", 5),
|
||||
build_memory_sample_length=getattr(global_config, "build_memory_sample_length", 30),
|
||||
memory_compress_rate=getattr(global_config, "memory_compress_rate", 0.1),
|
||||
memory_forget_time=getattr(global_config, "memory_forget_time", 24 * 7),
|
||||
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"
|
||||
), # 添加默认模型名
|
||||
)
|
||||
|
|
|
|||
|
|
@ -739,7 +739,7 @@ class LLMRequest:
|
|||
|
||||
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
|
||||
data = {
|
||||
|
|
@ -750,16 +750,17 @@ class LLMRequest:
|
|||
"tools": tools,
|
||||
}
|
||||
|
||||
logger.debug(f"向模型 {self.model_name} 发送工具调用请求,包含 {len(tools)} 个工具")
|
||||
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
|
||||
logger.debug(f"收到工具调用响应,包含 {len(tool_calls) if tool_calls else 0} 个工具调用")
|
||||
return content, reasoning_content, tool_calls
|
||||
else:
|
||||
content, reasoning_content = response
|
||||
logger.debug("收到普通响应,无工具调用")
|
||||
return response
|
||||
return content, reasoning_content, None
|
||||
|
||||
async def get_embedding(self, text: str) -> Union[list, None]:
|
||||
"""异步方法:获取文本的embedding向量
|
||||
|
|
|
|||
|
|
@ -180,10 +180,10 @@ class PersonInfoManager:
|
|||
existing_names = ""
|
||||
while current_try < max_retries:
|
||||
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
|
||||
|
||||
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_cardname},"
|
||||
if user_avatar:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import math
|
|||
from bson.decimal128 import Decimal128
|
||||
from .person_info import person_info_manager
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
# import re
|
||||
# import traceback
|
||||
|
||||
|
||||
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.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:
|
||||
"""计算并变更关系值
|
||||
新的关系值变更计算方式:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, TypeVar, List, Union, Callable, Tuple
|
||||
from typing import Any, Dict, TypeVar, List, Union, Tuple
|
||||
|
||||
# 定义类型变量用于泛型类型提示
|
||||
T = TypeVar("T")
|
||||
|
|
@ -70,56 +70,6 @@ def extract_tool_call_arguments(tool_call: Dict[str, Any], default_value: Dict[s
|
|||
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:
|
||||
"""
|
||||
安全地将Python对象序列化为JSON字符串
|
||||
|
|
@ -144,23 +94,6 @@ def safe_json_dumps(obj: Any, default_value: str = "{}", ensure_ascii: bool = Fa
|
|||
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]:
|
||||
"""
|
||||
标准化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
|
||||
if response is None:
|
||||
return False, [], "LLM响应为None"
|
||||
|
|
@ -201,114 +137,68 @@ def normalize_llm_response(response: Any, log_prefix: str = "") -> Tuple[bool, L
|
|||
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: 日志前缀
|
||||
|
||||
返回:
|
||||
元组 (成功标志, 工具调用列表, 错误消息)
|
||||
元组 (成功标志, 验证后的工具调用列表, 错误消息)
|
||||
"""
|
||||
# 确保响应格式正确
|
||||
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 = []
|
||||
for i, tool_call in enumerate(tool_calls):
|
||||
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
|
||||
|
||||
# 检查基本结构
|
||||
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
|
||||
|
||||
if "function" not in tool_call or not isinstance(tool_call["function"], dict):
|
||||
logger.warning(f"{log_prefix}工具调用[{i}]缺少function字段或格式不正确")
|
||||
if "function" not in tool_call or not isinstance(tool_call.get("function"), dict):
|
||||
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
|
||||
|
||||
valid_tool_calls.append(tool_call)
|
||||
|
||||
# 检查是否有有效的工具调用
|
||||
if not valid_tool_calls:
|
||||
return False, [], "没有找到有效的工具调用"
|
||||
if not valid_tool_calls and tool_calls: # 如果原始列表不为空,但验证后为空
|
||||
return False, [], "所有工具调用格式均无效"
|
||||
|
||||
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)}"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[inner]
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
|
||||
#如果你想要修改配置文件,请在修改后将version的值进行变更
|
||||
|
|
@ -48,12 +48,10 @@ personality_sides = [
|
|||
identity_detail = [
|
||||
"身份特点",
|
||||
"身份特点",
|
||||
]# 条数任意,不能为0, 该选项还在调试中,可能未完全生效
|
||||
]# 条数任意,不能为0, 该选项还在调试中
|
||||
#外貌特征
|
||||
height = 170 # 身高 单位厘米 该选项还在调试中,暂时未生效
|
||||
weight = 50 # 体重 单位千克 该选项还在调试中,暂时未生效
|
||||
age = 20 # 年龄 单位岁 该选项还在调试中,暂时未生效
|
||||
gender = "男" # 性别 该选项还在调试中,暂时未生效
|
||||
age = 20 # 年龄 单位岁
|
||||
gender = "男" # 性别
|
||||
appearance = "用几句话描述外貌特征" # 外貌特征 该选项还在调试中,暂时未生效
|
||||
|
||||
[schedule]
|
||||
|
|
@ -129,15 +127,19 @@ check_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要
|
|||
|
||||
[memory]
|
||||
build_memory_interval = 2000 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多
|
||||
build_memory_distribution = [4.0,2.0,0.6,24.0,8.0,0.4] # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重
|
||||
build_memory_sample_num = 10 # 采样数量,数值越高记忆采样次数越多
|
||||
build_memory_sample_length = 20 # 采样长度,数值越高一段记忆内容越丰富
|
||||
build_memory_distribution = [6.0,3.0,0.6,32.0,12.0,0.4] # 记忆构建分布,参数:分布1均值,标准差,权重,分布2均值,标准差,权重
|
||||
build_memory_sample_num = 8 # 采样数量,数值越高记忆采样次数越多
|
||||
build_memory_sample_length = 40 # 采样长度,数值越高一段记忆内容越丰富
|
||||
memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多
|
||||
|
||||
forget_memory_interval = 1000 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习
|
||||
memory_forget_time = 24 #多长时间后的记忆会被遗忘 单位小时
|
||||
memory_forget_percentage = 0.01 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认
|
||||
|
||||
consolidate_memory_interval = 1000 # 记忆整合间隔 单位秒 间隔越低,麦麦整合越频繁,记忆更精简
|
||||
consolidation_similarity_threshold = 0.7 # 相似度阈值
|
||||
consolidation_check_percentage = 0.01 # 检查节点比例
|
||||
|
||||
#不希望记忆的词,已经记忆的不会受到影响
|
||||
memory_ban_words = [
|
||||
# "403","张三"
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
Loading…
Reference in New Issue