+
## 🎉 介绍
-**🍔MaiCore 是一个基于大语言模型的可交互智能体**
+**🍔 MaiCore 是一个基于大语言模型的可交互智能体**
-- 💭 **拟人构建的prompt**:使用自然语言风格构建回复器的prompt,实现近似人类言语习惯的回复。
-- 💭 **行为规划**:在合适的时间说话,使用合适的动作
-- 🧠 **表达学习**:学习群友的说话风格和表达方式,学会真实人类的说话风格
-- 🤔 **黑话学习**:自主的学习没有见过的词语,尝试理解并认知含义
-- 🔌 **插件系统**:提供API和事件系统,可编写丰富插件。
-- 💝 **情感表达**:情绪系统和表情包系统。
+MaiBot 不仅仅是一个机器人,她致力于成为一个活跃在 QQ 群聊中的“生命体”。她不追求完美,但追求真实。
-
-
+- 💭 **拟人构建**:使用自然语言风格构建 Prompt,回复贴近人类习惯。
+- 🎭 **行为规划**:懂得在合适的时间说话,使用合适的动作。
+- 🧠 **表达学习**:模仿群友的说话风格,学习黑话,不断进化。
+- 🔌 **插件系统**:提供强大的 API 和事件系统,无限扩展可能。
+- 💝 **情感表达**:拥有独立的情绪系统和表情包互动能力。
+
+### 🚀 快速导航
+
+ 🌟 演示视频 |
+ 📦 快速入门 |
+ 📃 核心文档 |
+ 💬 加入社区
+
+
+
+
+
+
+---
+
## 🔥 更新和安装
+> **最新版本: v0.12.2** ([📄 更新日志](changelogs/changelog.md))
-**最新版本: v0.12.0** ([更新日志](changelogs/changelog.md))
+- **下载**: 前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
+- **启动器**: [Mailauncher](https://github.com/MaiM-with-u/mailauncher/releases/) (仅支持 MacOS, 早期开发中)
+| 分支 | 说明 |
+| :--- | :--- |
+| `main` | ✅ **稳定发布版本 (推荐)** |
+| `dev` | 🚧 开发测试版本 (不稳定) |
+| `classical` | 🛑 经典版本 (停止维护) |
-可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
-
-可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器
-
-注意,启动器处于早期开发版本,仅支持MacOS
-
-**GitHub 分支说明:**
-- `main`: 稳定发布版本(推荐)
-
-- `dev`: 开发测试版本(不稳定)
-
-- `classical`: 经典版本(停止维护)
-
-### 最新版本部署教程
-- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于 MaiCore 的新版本部署方式(与旧版本不兼容)
+### 📚 部署教程
+👉 **[🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html)**
+*(注意:MaiCore 新版本部署方式与旧版本不兼容)*
> [!WARNING]
-> - 项目处于活跃开发阶段,功能和 API 可能随时调整。
-> - 有问题可以提交 Issue 。
-> - QQ 机器人存在被限制风险,请自行了解,谨慎使用。
-> - 由于程序处于开发中,可能消耗较多 token。
+> - ⚠️ 项目处于活跃开发阶段,API 可能随时调整。
+> - ⚠️ QQ 机器人存在风控风险,请谨慎使用。
+> - ⚠️ AI 模型运行可能消耗较多 Token。
-## 💬 讨论
+---
-**技术交流群/答疑群:**
- [麦麦脑电图](https://qm.qq.com/q/RzmCiRtHEW) |
- [麦麦大脑磁共振](https://qm.qq.com/q/VQ3XZrWgMs) |
- [麦麦要当VTB](https://qm.qq.com/q/wGePTl1UyY) |
+## 💬 讨论与社区
- 为了维持技术交流和互帮互助的氛围,请不要在技术交流群讨论过多无关内容~
+我们欢迎所有对 MaiBot 感兴趣的朋友加入!
-**聊天吹水群:**
-- [麦麦之闲聊群](https://qm.qq.com/q/JxvHZnxyec)
+| 类别 | 群组 | 说明 |
+| :--- | :--- | :--- |
+| **技术交流** | [麦麦脑电图](https://qm.qq.com/q/RzmCiRtHEW) | 技术交流/答疑 |
+| **技术交流** | [麦麦大脑磁共振](https://qm.qq.com/q/VQ3XZrWgMs) | 技术交流/答疑 |
+| **技术交流** | [麦麦要当VTB](https://qm.qq.com/q/wGePTl1UyY) | 技术交流/答疑 |
+| **闲聊吹水** | [麦麦之闲聊群](https://qm.qq.com/q/JxvHZnxyec) | 仅限闲聊,不答疑 |
+| **插件开发** | [插件开发群](https://qm.qq.com/q/1036092828) | 进阶开发与测试 |
- 麦麦相关闲聊群,此群仅用于聊天,提问部署/技术问题可能不会快速得到答案
-
-**插件开发/测试版讨论群:**
-- [插件开发群](https://qm.qq.com/q/1036092828)
-
- 进阶内容,包括插件开发,测试版使用等等
+---
## 📚 文档
-**部分内容可能更新不够及时,请注意版本对应**
+> [!NOTE]
+> 部分内容可能更新不够及时,请注意版本对应。
-- [📚 核心 Wiki 文档](https://docs.mai-mai.org) - 项目最全面的文档中心,你可以了解麦麦有关的一切。
+- **[📚 核心 Wiki 文档](https://docs.mai-mai.org)**: 最全面的文档中心,了解麦麦的一切。
+### 🧩 衍生项目
-## 📚 衍生项目
+- **[MaiCraft](https://github.com/MaiM-with-u/Maicraft)**: 让麦麦陪你玩 Minecraft (早期开发中)。
+- **[MoFox_Bot](https://github.com/MoFox-Studio/MoFox-Core)**: 基于 MaiCore 0.10.0 的增强型 Fork,更稳定更有趣。
-### MaiCraft(早期开发)
-[MaiCraft](https://github.com/MaiM-with-u/Maicraft)
-> 让麦麦具有玩MC能力的项目
-> 交流群:1058573197
+---
-### MoFox_Bot
-[MoFox - 仓库地址](https://github.com/MoFox-Studio/MoFox-Core)
-> MoFox_Bot 是一个基于 MaiCore 0.10.0 snapshot.5 的增强型 fork 项目
-> 我们保留了原项目几乎所有核心功能,并在此基础上进行了深度优化与功能扩展,致力于打造一个更稳定、更智能、更具趣味性的 AI 智能体。
-
-
-
-## 设计理念(原始时代的火花)
+## 💡 设计理念 (原始时代的火花)
> **千石可乐说:**
> - 这个项目最初只是为了给牛牛 bot 添加一点额外的功能,但是功能越写越多,最后决定重写。其目的是为了创造一个活跃在 QQ 群聊的"生命体"。目的并不是为了写一个功能齐全的机器人,而是一个尽可能让人感知到真实的类人存在。
@@ -119,41 +116,39 @@
> - 如果人类真的需要一个 AI 来陪伴自己,并不是所有人都需要一个完美的,能解决所有问题的"helpful assistant",而是一个会犯错的,拥有自己感知和想法的"生命形式"。
> - 代码会保持开源和开放,但个人希望 MaiMbot 的运行时数据保持封闭,尽量避免以显式命令来对其进行控制和调试。我认为一个你无法完全掌控的个体才更能让你感觉到它的自主性,而视其成为一个对话机器。
> - SengokuCola~~纯编程外行,面向 cursor 编程,很多代码写得不好多多包涵~~已得到大脑升级。
+> *Code is open, but the soul is yours.*
+
+---
## 🙋 贡献和致谢
-你可以阅读[开发文档](https://docs.mai-mai.org/develop/)来更好的了解麦麦!
-MaiCore 是一个开源项目,我们非常欢迎你的参与。你的贡献,无论是提交 bug 报告、功能需求还是代码 pr,都对项目非常宝贵。我们非常感谢你的支持!🎉
-但无序的讨论会降低沟通效率,进而影响问题的解决速度,因此在提交任何贡献前,请务必先阅读本项目的[贡献指南](docs-src/CONTRIBUTE.md)。(待补完)
-### 贡献者
+欢迎参与贡献!请先阅读 [贡献指南](docs-src/CONTRIBUTE.md)。
-感谢各位大佬!
+### 🌟 贡献者
-### 致谢
+### ❤️ 特别致谢
-- [略nd](https://space.bilibili.com/1344099355): 为麦麦绘制人设。
-- [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现。
+- **[略nd](https://space.bilibili.com/1344099355)**: 🎨 为麦麦绘制精美人设。
+- **[NapCat](https://github.com/NapNeko/NapCatQQ)**: 🚀 现代化的基于 NTQQ 的 Bot 协议实现。
-**也感谢每一位给麦麦发展提出宝贵意见与建议的用户,感谢陪伴麦麦走到现在的你们!**
+---
-## 📌 注意事项
-
-> [!WARNING]
-> 使用本项目前必须阅读和同意[用户协议](EULA.md)和[隐私协议](PRIVACY.md)。
-> 本应用生成内容来自人工智能模型,由 AI 生成,请仔细甄别,请勿用于违反法律的用途,AI 生成内容不代表本项目团队的观点和立场。
-
-## 麦麦仓库状态
+## 📊 仓库状态

### Star 趋势
-
[](https://starchart.cc/MaiM-with-u/MaiBot)
-## License
+---
-GPL-3.0
+## 📌 注意事项 & License
+
+> [!IMPORTANT]
+> 使用前请阅读 [用户协议 (EULA)](EULA.md) 和 [隐私协议](PRIVACY.md)。AI 生成内容请仔细甄别。
+
+**License**: GPL-3.0
diff --git a/changelogs/changelog.md b/changelogs/changelog.md
index a615d7c1..1321694f 100644
--- a/changelogs/changelog.md
+++ b/changelogs/changelog.md
@@ -1,4 +1,13 @@
# Changelog
+## [0.12.2] - 2025-1-11
+### 功能更改
+- 优化私聊wait逻辑
+- 超时时强制引用回复
+- 修复部分适配器断联问题
+- 修复表达反思配置未生效
+- 优化记忆检索逻辑
+- 更新readme
+
## [0.12.1] - 2025-12-31
### 🌟 主要更新
- 添加年度总结!可以在webui查看
diff --git a/depends-data/maimai-v2.png b/depends-data/maimai-v2.png
new file mode 100644
index 00000000..dba16aee
Binary files /dev/null and b/depends-data/maimai-v2.png differ
diff --git a/scripts/compare_finish_search_token.py b/scripts/compare_finish_search_token.py
deleted file mode 100644
index b122cfb7..00000000
--- a/scripts/compare_finish_search_token.py
+++ /dev/null
@@ -1,507 +0,0 @@
-import argparse
-import asyncio
-import os
-import sys
-import time
-import json
-import importlib
-from typing import Dict, Any
-from datetime import datetime
-
-# 强制使用 utf-8,避免控制台编码报错
-try:
- if hasattr(sys.stdout, "reconfigure"):
- sys.stdout.reconfigure(encoding="utf-8")
- if hasattr(sys.stderr, "reconfigure"):
- sys.stderr.reconfigure(encoding="utf-8")
-except Exception:
- pass
-
-# 确保能导入 src.*
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
-
-from src.common.logger import initialize_logging, get_logger
-from src.common.database.database import db
-from src.common.database.database_model import LLMUsage
-
-logger = get_logger("compare_finish_search_token")
-
-
-def get_token_usage_since(start_time: float) -> Dict[str, Any]:
- """获取从指定时间开始的token使用情况
-
- Args:
- start_time: 开始时间戳
-
- Returns:
- 包含token使用统计的字典
- """
- try:
- start_datetime = datetime.fromtimestamp(start_time)
-
- # 查询从开始时间到现在的所有memory相关的token使用记录
- records = (
- LLMUsage.select()
- .where(
- (LLMUsage.timestamp >= start_datetime)
- & (
- (LLMUsage.request_type.like("%memory%"))
- | (LLMUsage.request_type == "memory.question")
- | (LLMUsage.request_type == "memory.react")
- | (LLMUsage.request_type == "memory.react.final")
- )
- )
- .order_by(LLMUsage.timestamp.asc())
- )
-
- total_prompt_tokens = 0
- total_completion_tokens = 0
- total_tokens = 0
- total_cost = 0.0
- request_count = 0
- model_usage = {} # 按模型统计
-
- for record in records:
- total_prompt_tokens += record.prompt_tokens or 0
- total_completion_tokens += record.completion_tokens or 0
- total_tokens += record.total_tokens or 0
- total_cost += record.cost or 0.0
- request_count += 1
-
- # 按模型统计
- model_name = record.model_name or "unknown"
- if model_name not in model_usage:
- model_usage[model_name] = {
- "prompt_tokens": 0,
- "completion_tokens": 0,
- "total_tokens": 0,
- "cost": 0.0,
- "request_count": 0,
- }
- model_usage[model_name]["prompt_tokens"] += record.prompt_tokens or 0
- model_usage[model_name]["completion_tokens"] += record.completion_tokens or 0
- model_usage[model_name]["total_tokens"] += record.total_tokens or 0
- model_usage[model_name]["cost"] += record.cost or 0.0
- model_usage[model_name]["request_count"] += 1
-
- return {
- "total_prompt_tokens": total_prompt_tokens,
- "total_completion_tokens": total_completion_tokens,
- "total_tokens": total_tokens,
- "total_cost": total_cost,
- "request_count": request_count,
- "model_usage": model_usage,
- }
- except Exception as e:
- logger.error(f"获取token使用情况失败: {e}")
- return {
- "total_prompt_tokens": 0,
- "total_completion_tokens": 0,
- "total_tokens": 0,
- "total_cost": 0.0,
- "request_count": 0,
- "model_usage": {},
- }
-
-
-def _import_memory_retrieval():
- """使用 importlib 动态导入 memory_retrieval 模块,避免循环导入"""
- try:
- # 先导入 prompt_builder,检查 prompt 是否已经初始化
- from src.chat.utils.prompt_builder import global_prompt_manager
-
- # 检查 memory_retrieval 相关的 prompt 是否已经注册
- # 如果已经注册,说明模块可能已经通过其他路径初始化过了
- prompt_already_init = "memory_retrieval_question_prompt" in global_prompt_manager._prompts
-
- module_name = "src.memory_system.memory_retrieval"
-
- # 如果 prompt 已经初始化,尝试直接使用已加载的模块
- if prompt_already_init and module_name in sys.modules:
- existing_module = sys.modules[module_name]
- if hasattr(existing_module, 'init_memory_retrieval_prompt'):
- return (
- existing_module.init_memory_retrieval_prompt,
- existing_module._react_agent_solve_question,
- )
-
- # 如果模块已经在 sys.modules 中但部分初始化,先移除它
- if module_name in sys.modules:
- existing_module = sys.modules[module_name]
- if not hasattr(existing_module, 'init_memory_retrieval_prompt'):
- # 模块部分初始化,移除它
- logger.warning(f"检测到部分初始化的模块 {module_name},尝试重新导入")
- del sys.modules[module_name]
- # 清理可能相关的部分初始化模块
- keys_to_remove = []
- for key in sys.modules.keys():
- if key.startswith('src.memory_system.') and key != 'src.memory_system':
- keys_to_remove.append(key)
- for key in keys_to_remove:
- try:
- del sys.modules[key]
- except KeyError:
- pass
-
- # 在导入 memory_retrieval 之前,先确保所有可能触发循环导入的模块都已完全加载
- # 这些模块在导入时可能会触发 memory_retrieval 的导入,所以我们需要先加载它们
- try:
- # 先导入可能触发循环导入的模块,让它们完成初始化
- import src.config.config
- import src.chat.utils.prompt_builder
- # 尝试导入可能触发循环导入的模块(这些模块可能在模块级别导入了 memory_retrieval)
- # 如果它们已经导入,就确保它们完全初始化
- try:
- import src.chat.replyer.group_generator # noqa: F401
- except (ImportError, AttributeError):
- pass # 如果导入失败,继续
- try:
- import src.chat.replyer.private_generator # noqa: F401
- except (ImportError, AttributeError):
- pass # 如果导入失败,继续
- except Exception as e:
- logger.warning(f"预加载依赖模块时出现警告: {e}")
-
- # 现在尝试导入 memory_retrieval
- # 如果此时仍然触发循环导入,说明有其他模块在模块级别导入了 memory_retrieval
- memory_retrieval_module = importlib.import_module(module_name)
-
- return (
- memory_retrieval_module.init_memory_retrieval_prompt,
- memory_retrieval_module._react_agent_solve_question,
- )
- except (ImportError, AttributeError) as e:
- logger.error(f"导入 memory_retrieval 模块失败: {e}", exc_info=True)
- raise
-
-
-def _init_tools_without_finish_search():
- """初始化工具但不注册 finish_search"""
- from src.memory_system.retrieval_tools import (
- register_query_chat_history,
- register_query_person_info,
- register_query_words,
- )
- from src.memory_system.retrieval_tools.tool_registry import get_tool_registry
- from src.config.config import global_config
-
- # 清空工具注册器
- tool_registry = get_tool_registry()
- tool_registry.tools.clear()
-
- # 注册除 finish_search 外的所有工具
- register_query_chat_history()
- register_query_person_info()
- register_query_words()
-
- # 如果启用 LPMM agent 模式,也注册 LPMM 工具
- if global_config.lpmm_knowledge.lpmm_mode == "agent":
- from src.memory_system.retrieval_tools.query_lpmm_knowledge import register_tool as register_lpmm_knowledge
- register_lpmm_knowledge()
-
- logger.info("已初始化工具(不包含 finish_search)")
-
-
-def _init_tools_with_finish_search():
- """初始化工具并注册 finish_search"""
- from src.memory_system.retrieval_tools.tool_registry import get_tool_registry
- from src.memory_system.retrieval_tools import init_all_tools
-
- # 清空工具注册器
- tool_registry = get_tool_registry()
- tool_registry.tools.clear()
-
- # 初始化所有工具(包括 finish_search)
- init_all_tools()
- logger.info("已初始化工具(包含 finish_search)")
-
-
-async def get_prompt_tokens_for_tools(
- question: str,
- chat_id: str,
- use_finish_search: bool,
-) -> Dict[str, Any]:
- """获取使用不同工具配置时的prompt token消耗
-
- Args:
- question: 要查询的问题
- chat_id: 聊天ID
- use_finish_search: 是否使用 finish_search 工具
-
- Returns:
- 包含prompt token信息的字典
- """
- # 先初始化 prompt(如果还未初始化)
- # 注意:init_memory_retrieval_prompt 会调用 init_all_tools,所以我们需要在它之后重新设置工具
- from src.chat.utils.prompt_builder import global_prompt_manager
- if "memory_retrieval_question_prompt" not in global_prompt_manager._prompts:
- init_memory_retrieval_prompt, _ = _import_memory_retrieval()
- init_memory_retrieval_prompt()
-
- # 初始化工具(根据参数决定是否包含 finish_search)
- # 必须在 init_memory_retrieval_prompt 之后调用,因为它会调用 init_all_tools
- if use_finish_search:
- _init_tools_with_finish_search()
- else:
- _init_tools_without_finish_search()
-
- # 获取工具注册器
- from src.memory_system.retrieval_tools.tool_registry import get_tool_registry
- tool_registry = get_tool_registry()
- tool_definitions = tool_registry.get_tool_definitions()
-
- # 验证工具列表(调试用)
- tool_names = [tool["name"] for tool in tool_definitions]
- if use_finish_search:
- if "finish_search" not in tool_names:
- logger.warning("期望包含 finish_search 工具,但工具列表中未找到")
- else:
- if "finish_search" in tool_names:
- logger.warning("期望不包含 finish_search 工具,但工具列表中找到了,将移除")
- # 移除 finish_search 工具
- tool_registry.tools.pop("finish_search", None)
- tool_definitions = tool_registry.get_tool_definitions()
- tool_names = [tool["name"] for tool in tool_definitions]
-
- # 构建第一次调用的prompt(模拟_react_agent_solve_question的第一次调用)
- from src.config.config import global_config
- bot_name = global_config.bot.nickname
- time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
-
- # 构建head_prompt
- head_prompt = await global_prompt_manager.format_prompt(
- "memory_retrieval_react_prompt_head",
- bot_name=bot_name,
- time_now=time_now,
- question=question,
- collected_info="",
- current_iteration=1,
- remaining_iterations=global_config.memory.max_agent_iterations - 1,
- max_iterations=global_config.memory.max_agent_iterations,
- )
-
- # 构建消息列表(只包含system message,模拟第一次调用)
- from src.llm_models.payload_content.message import MessageBuilder, RoleType
- messages = []
- system_builder = MessageBuilder()
- system_builder.set_role(RoleType.System)
- system_builder.add_text_content(head_prompt)
- messages.append(system_builder.build())
-
- # 调用LLM API来计算token(只调用一次,不实际执行)
- from src.llm_models.utils_model import LLMRequest, RequestType
- from src.config.config import model_config
-
- # 创建LLM请求对象
- llm_request = LLMRequest(model_set=model_config.model_task_config.tool_use, request_type="memory.react.compare")
-
- # 构建工具选项
- tool_built = llm_request._build_tool_options(tool_definitions)
-
- # 直接调用 _execute_request 以获取完整的响应对象(包含 usage)
- response, model_info = await llm_request._execute_request(
- request_type=RequestType.RESPONSE,
- message_factory=lambda _client, *, _messages=messages: _messages,
- temperature=None,
- max_tokens=None,
- tool_options=tool_built,
- )
-
- # 从响应中获取token使用情况
- prompt_tokens = 0
- completion_tokens = 0
- total_tokens = 0
-
- if response and hasattr(response, 'usage') and response.usage:
- prompt_tokens = response.usage.prompt_tokens or 0
- completion_tokens = response.usage.completion_tokens or 0
- total_tokens = response.usage.total_tokens or 0
-
- return {
- "use_finish_search": use_finish_search,
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_tokens,
- "total_tokens": total_tokens,
- "tool_count": len(tool_definitions),
- "tool_names": [tool["name"] for tool in tool_definitions],
- }
-
-
-async def compare_prompt_tokens(
- question: str,
- chat_id: str = "compare_finish_search",
-) -> Dict[str, Any]:
- """对比使用 finish_search 工具与否的输入 token 差异
-
- 只运行一次,只计算输入 token 的差异,确保除了工具定义外其他内容一致
-
- Args:
- question: 要查询的问题
- chat_id: 聊天ID
-
- Returns:
- 包含对比结果的字典
- """
- print("\n" + "=" * 80)
- print("finish_search 工具 输入 Token 消耗对比测试")
- print("=" * 80)
- print(f"\n[测试问题] {question}")
- print(f"[聊天ID] {chat_id}")
- print("\n注意: 只对比第一次LLM调用的输入token差异,不运行完整迭代流程")
-
- # 第一次测试:不使用 finish_search
- print("\n" + "-" * 80)
- print("[测试 1/2] 不使用 finish_search 工具")
- print("-" * 80)
- result_without = await get_prompt_tokens_for_tools(
- question=question,
- chat_id=f"{chat_id}_without",
- use_finish_search=False,
- )
-
- print(f"\n[结果]")
- print(f" 工具数量: {result_without['tool_count']}")
- print(f" 工具列表: {', '.join(result_without['tool_names'])}")
- print(f" 输入 Prompt Tokens: {result_without['prompt_tokens']:,}")
-
- # 等待一下,确保数据库记录已写入
- await asyncio.sleep(1)
-
- # 第二次测试:使用 finish_search
- print("\n" + "-" * 80)
- print("[测试 2/2] 使用 finish_search 工具")
- print("-" * 80)
- result_with = await get_prompt_tokens_for_tools(
- question=question,
- chat_id=f"{chat_id}_with",
- use_finish_search=True,
- )
-
- print(f"\n[结果]")
- print(f" 工具数量: {result_with['tool_count']}")
- print(f" 工具列表: {', '.join(result_with['tool_names'])}")
- print(f" 输入 Prompt Tokens: {result_with['prompt_tokens']:,}")
-
- # 对比结果
- print("\n" + "=" * 80)
- print("[对比结果]")
- print("=" * 80)
-
- prompt_token_diff = result_with['prompt_tokens'] - result_without['prompt_tokens']
- prompt_token_diff_percent = (prompt_token_diff / result_without['prompt_tokens'] * 100) if result_without['prompt_tokens'] > 0 else 0
-
- tool_count_diff = result_with['tool_count'] - result_without['tool_count']
-
- print(f"\n[输入 Prompt Token 对比]")
- print(f" 不使用 finish_search: {result_without['prompt_tokens']:,} tokens")
- print(f" 使用 finish_search: {result_with['prompt_tokens']:,} tokens")
- print(f" 差异: {prompt_token_diff:+,} tokens ({prompt_token_diff_percent:+.2f}%)")
-
- print(f"\n[工具数量对比]")
- print(f" 不使用 finish_search: {result_without['tool_count']} 个工具")
- print(f" 使用 finish_search: {result_with['tool_count']} 个工具")
- print(f" 差异: {tool_count_diff:+d} 个工具")
-
- print(f"\n[工具列表对比]")
- without_tools = set(result_without['tool_names'])
- with_tools = set(result_with['tool_names'])
- only_with = with_tools - without_tools
- only_without = without_tools - with_tools
-
- if only_with:
- print(f" 仅在 '使用 finish_search' 中的工具: {', '.join(only_with)}")
- if only_without:
- print(f" 仅在 '不使用 finish_search' 中的工具: {', '.join(only_without)}")
- if not only_with and not only_without:
- print(f" 工具列表相同(除了 finish_search)")
-
- # 显示其他token信息
- print(f"\n[其他 Token 信息]")
- print(f" Completion Tokens (不使用 finish_search): {result_without.get('completion_tokens', 0):,}")
- print(f" Completion Tokens (使用 finish_search): {result_with.get('completion_tokens', 0):,}")
- print(f" 总 Tokens (不使用 finish_search): {result_without.get('total_tokens', 0):,}")
- print(f" 总 Tokens (使用 finish_search): {result_with.get('total_tokens', 0):,}")
-
- print("\n" + "=" * 80)
-
- return {
- "question": question,
- "without_finish_search": result_without,
- "with_finish_search": result_with,
- "comparison": {
- "prompt_token_diff": prompt_token_diff,
- "prompt_token_diff_percent": prompt_token_diff_percent,
- "tool_count_diff": tool_count_diff,
- },
- }
-
-
-def main() -> None:
- parser = argparse.ArgumentParser(
- description="对比使用 finish_search 工具与否的 token 消耗差异"
- )
- parser.add_argument(
- "--chat-id",
- default="compare_finish_search",
- help="测试用的聊天ID(默认: compare_finish_search)",
- )
- parser.add_argument(
- "--output",
- "-o",
- help="将结果保存到JSON文件(可选)",
- )
-
- args = parser.parse_args()
-
- # 初始化日志(使用较低的详细程度,避免输出过多日志)
- initialize_logging(verbose=False)
-
- # 交互式输入问题
- print("\n" + "=" * 80)
- print("finish_search 工具 Token 消耗对比测试工具")
- print("=" * 80)
- question = input("\n请输入要查询的问题: ").strip()
- if not question:
- print("错误: 问题不能为空")
- return
-
- # 连接数据库
- try:
- db.connect(reuse_if_open=True)
- except Exception as e:
- logger.error(f"数据库连接失败: {e}")
- print(f"错误: 数据库连接失败: {e}")
- return
-
- # 运行对比测试
- try:
- result = asyncio.run(
- compare_prompt_tokens(
- question=question,
- chat_id=args.chat_id,
- )
- )
-
- # 如果指定了输出文件,保存结果
- if args.output:
- # 将thinking_steps转换为可序列化的格式
- output_result = result.copy()
- with open(args.output, "w", encoding="utf-8") as f:
- json.dump(output_result, f, ensure_ascii=False, indent=2)
- print(f"\n[结果已保存] {args.output}")
-
- except KeyboardInterrupt:
- print("\n\n[中断] 用户中断测试")
- except Exception as e:
- logger.error(f"测试失败: {e}", exc_info=True)
- print(f"\n[错误] 测试失败: {e}")
- finally:
- try:
- db.close()
- except Exception:
- pass
-
-
-if __name__ == "__main__":
- main()
-
diff --git a/scripts/evaluate_expressions_v5.py b/scripts/evaluate_expressions_v5.py
deleted file mode 100644
index 0f1a814c..00000000
--- a/scripts/evaluate_expressions_v5.py
+++ /dev/null
@@ -1,476 +0,0 @@
-"""
-表达方式评估脚本
-
-功能:
-1. 随机读取指定数量的表达方式,获取其situation和style
-2. 先进行人工评估(逐条手动评估)
-3. 然后使用LLM进行评估
-4. 对比人工评估和LLM评估的正确率、精确率、召回率、F1分数等指标(以人工评估为标准)
-5. 不真正修改数据库,只是做评估
-"""
-
-import asyncio
-import random
-import json
-import sys
-import os
-from typing import List, Dict
-
-# 添加项目根目录到路径
-project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
-sys.path.insert(0, project_root)
-
-from src.common.database.database_model import Expression
-from src.common.database.database import db
-from src.llm_models.utils_model import LLMRequest
-from src.config.config import model_config
-from src.common.logger import get_logger
-
-logger = get_logger("expression_evaluator_comparison")
-
-
-def get_random_expressions(count: int = 10) -> List[Expression]:
- """
- 随机读取指定数量的表达方式
-
- Args:
- count: 要读取的数量,默认10条
-
- Returns:
- 表达方式列表
- """
- try:
- # 查询所有表达方式
- all_expressions = list(Expression.select())
-
- if not all_expressions:
- logger.warning("数据库中没有表达方式记录")
- return []
-
- # 如果总数少于请求数量,返回所有
- if len(all_expressions) <= count:
- logger.info(f"数据库中共有 {len(all_expressions)} 条表达方式,全部返回")
- return all_expressions
-
- # 随机选择指定数量
- selected = random.sample(all_expressions, count)
- logger.info(f"从 {len(all_expressions)} 条表达方式中随机选择了 {len(selected)} 条")
- return selected
-
- except Exception as e:
- logger.error(f"随机读取表达方式失败: {e}")
- import traceback
- logger.error(traceback.format_exc())
- return []
-
-
-def manual_evaluate_expression(expression: Expression, index: int, total: int) -> Dict:
- """
- 人工评估单个表达方式
-
- Args:
- expression: 表达方式对象
- index: 当前索引(从1开始)
- total: 总数
-
- Returns:
- 评估结果字典,包含:
- - expression_id: 表达方式ID
- - situation: 情境
- - style: 风格
- - suitable: 是否合适(人工评估)
- - reason: 评估理由(始终为None)
- """
- print("\n" + "=" * 60)
- print(f"人工评估 [{index}/{total}]")
- print("=" * 60)
- print(f"Situation: {expression.situation}")
- print(f"Style: {expression.style}")
- print("\n请评估该表达方式是否合适:")
- print(" 输入 'y' 或 'yes' 或 '1' 表示合适(通过)")
- print(" 输入 'n' 或 'no' 或 '0' 表示不合适(不通过)")
- print(" 输入 'q' 或 'quit' 退出评估")
-
- while True:
- user_input = input("\n您的评估 (y/n/q): ").strip().lower()
-
- if user_input in ['q', 'quit']:
- print("退出评估")
- return None
-
- if user_input in ['y', 'yes', '1', '是', '通过']:
- suitable = True
- break
- elif user_input in ['n', 'no', '0', '否', '不通过']:
- suitable = False
- break
- else:
- print("输入无效,请重新输入 (y/n/q)")
-
- result = {
- "expression_id": expression.id,
- "situation": expression.situation,
- "style": expression.style,
- "suitable": suitable,
- "reason": None,
- "evaluator": "manual"
- }
-
- print(f"\n✓ 已记录:{'通过' if suitable else '不通过'}")
-
- return result
-
-
-def create_evaluation_prompt(situation: str, style: str) -> str:
- """
- 创建评估提示词
-
- Args:
- situation: 情境
- style: 风格
-
- Returns:
- 评估提示词
- """
- prompt = f"""请评估以下表达方式是否合适:
-
-情境(situation):{situation}
-风格(style):{style}
-
-请从以下方面进行评估:
-1. 情境描述是否清晰、准确
-2. 风格表达是否合理、自然
-3. 情境和风格是否匹配
-4. 允许部分语法错误出现
-5. 允许口头化或缺省表达
-6. 允许部分上下文缺失
-
-请以JSON格式输出评估结果:
-{{
- "suitable": true/false,
- "reason": "评估理由(如果不合适,请说明原因)"
-}}
-
-如果合适,suitable设为true;如果不合适,suitable设为false,并在reason中说明原因。
-请严格按照JSON格式输出,不要包含其他内容。"""
-
- return prompt
-
-
-async def _single_llm_evaluation(expression: Expression, llm: LLMRequest) -> tuple[bool, str, str | None]:
- """
- 执行单次LLM评估
-
- Args:
- expression: 表达方式对象
- llm: LLM请求实例
-
- Returns:
- (suitable, reason, error) 元组,如果出错则 suitable 为 False,error 包含错误信息
- """
- try:
- prompt = create_evaluation_prompt(expression.situation, expression.style)
- logger.debug(f"正在评估表达方式 ID: {expression.id}")
-
- response, (reasoning, model_name, _) = await llm.generate_response_async(
- prompt=prompt,
- temperature=0.6,
- max_tokens=1024
- )
-
- logger.debug(f"LLM响应: {response}")
-
- # 解析JSON响应
- try:
- evaluation = json.loads(response)
- except json.JSONDecodeError:
- import re
- json_match = re.search(r'\{[^{}]*"suitable"[^{}]*\}', response, re.DOTALL)
- if json_match:
- evaluation = json.loads(json_match.group())
- else:
- raise ValueError("无法从响应中提取JSON格式的评估结果")
-
- suitable = evaluation.get("suitable", False)
- reason = evaluation.get("reason", "未提供理由")
-
- logger.debug(f"评估结果: {'通过' if suitable else '不通过'}")
- return suitable, reason, None
-
- except Exception as e:
- logger.error(f"评估表达方式 ID: {expression.id} 时出错: {e}")
- return False, f"评估过程出错: {str(e)}", str(e)
-
-
-async def evaluate_expression_llm(expression: Expression, llm: LLMRequest) -> Dict:
- """
- 使用LLM评估单个表达方式
-
- Args:
- expression: 表达方式对象
- llm: LLM请求实例
-
- Returns:
- 评估结果字典
- """
- logger.info(f"开始评估表达方式 ID: {expression.id}")
-
- suitable, reason, error = await _single_llm_evaluation(expression, llm)
-
- if error:
- suitable = False
-
- logger.info(f"评估完成: {'通过' if suitable else '不通过'}")
-
- return {
- "expression_id": expression.id,
- "situation": expression.situation,
- "style": expression.style,
- "suitable": suitable,
- "reason": reason,
- "error": error,
- "evaluator": "llm"
- }
-
-
-def compare_evaluations(manual_results: List[Dict], llm_results: List[Dict], method_name: str) -> Dict:
- """
- 对比人工评估和LLM评估的结果
-
- Args:
- manual_results: 人工评估结果列表
- llm_results: LLM评估结果列表
- method_name: 评估方法名称(用于标识)
-
- Returns:
- 对比分析结果字典
- """
- # 按expression_id建立映射
- llm_dict = {r["expression_id"]: r for r in llm_results}
-
- total = len(manual_results)
- matched = 0
- true_positives = 0
- true_negatives = 0
- false_positives = 0
- false_negatives = 0
-
- for manual_result in manual_results:
- llm_result = llm_dict.get(manual_result["expression_id"])
- if llm_result is None:
- continue
-
- manual_suitable = manual_result["suitable"]
- llm_suitable = llm_result["suitable"]
-
- if manual_suitable == llm_suitable:
- matched += 1
-
- if manual_suitable and llm_suitable:
- true_positives += 1
- elif not manual_suitable and not llm_suitable:
- true_negatives += 1
- elif not manual_suitable and llm_suitable:
- false_positives += 1
- elif manual_suitable and not llm_suitable:
- false_negatives += 1
-
- accuracy = (matched / total * 100) if total > 0 else 0
- precision = (true_positives / (true_positives + false_positives) * 100) if (true_positives + false_positives) > 0 else 0
- recall = (true_positives / (true_positives + false_negatives) * 100) if (true_positives + false_negatives) > 0 else 0
- f1_score = (2 * precision * recall / (precision + recall)) if (precision + recall) > 0 else 0
- specificity = (true_negatives / (true_negatives + false_positives) * 100) if (true_negatives + false_positives) > 0 else 0
-
- random_baseline = 50.0
- accuracy_above_random = accuracy - random_baseline
- accuracy_improvement_ratio = (accuracy / random_baseline) if random_baseline > 0 else 0
-
- return {
- "method": method_name,
- "total": total,
- "matched": matched,
- "accuracy": accuracy,
- "accuracy_above_random": accuracy_above_random,
- "accuracy_improvement_ratio": accuracy_improvement_ratio,
- "true_positives": true_positives,
- "true_negatives": true_negatives,
- "false_positives": false_positives,
- "false_negatives": false_negatives,
- "precision": precision,
- "recall": recall,
- "f1_score": f1_score,
- "specificity": specificity
- }
-
-
-
-
-async def main():
- """主函数"""
- logger.info("=" * 60)
- logger.info("开始表达方式评估")
- logger.info("=" * 60)
-
- # 初始化数据库连接
- try:
- db.connect(reuse_if_open=True)
- logger.info("数据库连接成功")
- except Exception as e:
- logger.error(f"数据库连接失败: {e}")
- return
-
- # 1. 随机读取表达方式
- logger.info("\n步骤1: 随机读取表达方式")
- expressions = get_random_expressions(10)
- if not expressions:
- logger.error("没有可用的表达方式,退出")
- return
- logger.info(f"成功读取 {len(expressions)} 条表达方式")
-
- # 2. 人工评估
- print("\n" + "=" * 60)
- print("开始人工评估")
- print("=" * 60)
- print(f"共需要评估 {len(expressions)} 条表达方式")
- print("请逐条进行评估...\n")
-
- manual_results = []
- for i, expression in enumerate(expressions, 1):
- manual_result = manual_evaluate_expression(expression, i, len(expressions))
- if manual_result is None:
- print("\n评估已中断")
- return
- manual_results.append(manual_result)
-
- print("\n" + "=" * 60)
- print("人工评估完成")
- print("=" * 60)
-
- # 3. 创建LLM实例并评估
- logger.info("\n步骤3: 创建LLM实例")
- try:
- llm = LLMRequest(
- model_set=model_config.model_task_config.tool_use,
- request_type="expression_evaluator_comparison"
- )
- except Exception as e:
- logger.error(f"创建LLM实例失败: {e}")
- import traceback
- logger.error(traceback.format_exc())
- return
-
- logger.info("\n步骤4: 开始LLM评估")
- llm_results = []
- for i, expression in enumerate(expressions, 1):
- logger.info(f"LLM评估进度: {i}/{len(expressions)}")
- llm_results.append(await evaluate_expression_llm(expression, llm))
- await asyncio.sleep(0.3)
-
- # 4. 对比分析并输出结果
- comparison = compare_evaluations(manual_results, llm_results, "LLM评估")
-
- print("\n" + "=" * 60)
- print("评估结果(以人工评估为标准)")
- print("=" * 60)
- print("\n评估目标:")
- print(" 1. 核心能力:将不合适的项目正确提取出来(特定负类召回率)")
- print(" 2. 次要能力:尽可能少的误删合适的项目(召回率)")
-
- # 详细评估结果(核心指标优先)
- print("\n【详细对比】")
- print(f"\n--- {comparison['method']} ---")
- print(f" 总数: {comparison['total']} 条")
- print()
- print(" 【核心能力指标】")
- print(f" ⭐ 特定负类召回率: {comparison['specificity']:.2f}% (将不合适项目正确提取出来的能力)")
- print(f" - 计算: TN / (TN + FP) = {comparison['true_negatives']} / ({comparison['true_negatives']} + {comparison['false_positives']})")
- print(f" - 含义: 在 {comparison['true_negatives'] + comparison['false_positives']} 个实际不合适的项目中,正确识别出 {comparison['true_negatives']} 个")
- print(f" - 随机水平: 50.00% (当前高于随机: {comparison['specificity'] - 50.0:+.2f}%)")
- print()
- print(f" ⭐ 召回率: {comparison['recall']:.2f}% (尽可能少的误删合适项目的能力)")
- print(f" - 计算: TP / (TP + FN) = {comparison['true_positives']} / ({comparison['true_positives']} + {comparison['false_negatives']})")
- print(f" - 含义: 在 {comparison['true_positives'] + comparison['false_negatives']} 个实际合适的项目中,正确识别出 {comparison['true_positives']} 个")
- print(f" - 随机水平: 50.00% (当前高于随机: {comparison['recall'] - 50.0:+.2f}%)")
- print()
- print(" 【其他指标】")
- print(f" 准确率: {comparison['accuracy']:.2f}% (整体判断正确率)")
- print(f" 精确率: {comparison['precision']:.2f}% (判断为合适的项目中,实际合适的比例)")
- print(f" F1分数: {comparison['f1_score']:.2f} (精确率和召回率的调和平均)")
- print(f" 匹配数: {comparison['matched']}/{comparison['total']}")
- print()
- print(" 【分类统计】")
- print(f" TP (正确识别为合适): {comparison['true_positives']}")
- print(f" TN (正确识别为不合适): {comparison['true_negatives']} ⭐")
- print(f" FP (误判为合适): {comparison['false_positives']} ⚠️")
- print(f" FN (误删合适项目): {comparison['false_negatives']} ⚠️")
-
- # 5. 输出人工评估不通过但LLM误判为通过的详细信息
- print("\n" + "=" * 60)
- print("人工评估不通过但LLM误判为通过的项目(FP - False Positive)")
- print("=" * 60)
-
- # 按expression_id建立映射
- llm_dict = {r["expression_id"]: r for r in llm_results}
-
- fp_items = []
- for manual_result in manual_results:
- llm_result = llm_dict.get(manual_result["expression_id"])
- if llm_result is None:
- continue
-
- # 人工评估不通过,但LLM评估通过(FP情况)
- if not manual_result["suitable"] and llm_result["suitable"]:
- fp_items.append({
- "expression_id": manual_result["expression_id"],
- "situation": manual_result["situation"],
- "style": manual_result["style"],
- "manual_suitable": manual_result["suitable"],
- "llm_suitable": llm_result["suitable"],
- "llm_reason": llm_result.get("reason", "未提供理由"),
- "llm_error": llm_result.get("error")
- })
-
- if fp_items:
- print(f"\n共找到 {len(fp_items)} 条误判项目:\n")
- for idx, item in enumerate(fp_items, 1):
- print(f"--- [{idx}] 项目 ID: {item['expression_id']} ---")
- print(f"Situation: {item['situation']}")
- print(f"Style: {item['style']}")
- print("人工评估: 不通过 ❌")
- print("LLM评估: 通过 ✅ (误判)")
- if item.get('llm_error'):
- print(f"LLM错误: {item['llm_error']}")
- print(f"LLM理由: {item['llm_reason']}")
- print()
- else:
- print("\n✓ 没有误判项目(所有人工评估不通过的项目都被LLM正确识别为不通过)")
-
- # 6. 保存结果到JSON文件
- output_file = os.path.join(project_root, "data", "expression_evaluation_comparison.json")
- try:
- os.makedirs(os.path.dirname(output_file), exist_ok=True)
- with open(output_file, "w", encoding="utf-8") as f:
- json.dump({
- "manual_results": manual_results,
- "llm_results": llm_results,
- "comparison": comparison
- }, f, ensure_ascii=False, indent=2)
- logger.info(f"\n评估结果已保存到: {output_file}")
- except Exception as e:
- logger.warning(f"保存结果到文件失败: {e}")
-
- print("\n" + "=" * 60)
- print("评估完成")
- print("=" * 60)
-
- # 关闭数据库连接
- try:
- db.close()
- logger.info("数据库连接已关闭")
- except Exception as e:
- logger.warning(f"关闭数据库连接时出错: {e}")
-
-
-if __name__ == "__main__":
- asyncio.run(main())
-
diff --git a/src/bw_learner/expression_reflector.py b/src/bw_learner/expression_reflector.py
index c98f0012..d1902f55 100644
--- a/src/bw_learner/expression_reflector.py
+++ b/src/bw_learner/expression_reflector.py
@@ -28,7 +28,7 @@ class ExpressionReflector:
try:
logger.debug(f"[Expression Reflection] 开始检查是否需要提问 (stream_id: {self.chat_id})")
- if not global_config.expression.expression_self_reflect:
+ if not global_config.expression.expression_manual_reflect:
logger.debug("[Expression Reflection] 表达反思功能未启用,跳过")
return False
diff --git a/src/chat/brain_chat/PFC/action_planner.py b/src/chat/brain_chat/PFC/action_planner.py
index 4770c6ce..3b71d2b9 100644
--- a/src/chat/brain_chat/PFC/action_planner.py
+++ b/src/chat/brain_chat/PFC/action_planner.py
@@ -1,14 +1,14 @@
import time
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
+from src.common.logger import get_logger
+from src.llm_models.utils_model import LLMRequest
+from src.config.config import global_config
+import random
from .chat_observer import ChatObserver
from .pfc_utils import get_items_from_json
-from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
-from src.plugins.utils.chat_message_builder import build_readable_messages
+from src.chat.utils.chat_message_builder import build_readable_messages
logger = get_logger("pfc_action_planner")
@@ -113,12 +113,27 @@ class ActionPlanner:
max_tokens=1500,
request_type="action_planning",
)
- self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
+ self.personality_info = self._get_personality_prompt()
self.name = global_config.BOT_NICKNAME
self.private_name = private_name
self.chat_observer = ChatObserver.get_instance(stream_id, private_name)
# self.action_planner_info = ActionPlannerInfo() # 移除未使用的变量
+ def _get_personality_prompt(self) -> str:
+ """获取个性提示信息"""
+ prompt_personality = global_config.personality.personality
+
+ # 检查是否需要随机替换为状态
+ if (
+ global_config.personality.states
+ and global_config.personality.state_probability > 0
+ and random.random() < global_config.personality.state_probability
+ ):
+ prompt_personality = random.choice(global_config.personality.states)
+
+ bot_name = global_config.BOT_NICKNAME
+ return f"你的名字是{bot_name},你{prompt_personality};"
+
# 修改 plan 方法签名,增加 last_successful_reply_action 参数
async def plan(
self,
diff --git a/src/chat/brain_chat/PFC/conversation.py b/src/chat/brain_chat/PFC/conversation.py
index 0bc4cae8..8553ee96 100644
--- a/src/chat/brain_chat/PFC/conversation.py
+++ b/src/chat/brain_chat/PFC/conversation.py
@@ -3,22 +3,22 @@ import asyncio
import datetime
# from .message_storage import MongoDBMessageStorage
-from src.plugins.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
+from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
-# from ...config.config import global_config
+# from src.config.config import global_config
from typing import Dict, Any, Optional
-from ..chat.message import Message
+from src.chat.message_receive.message import Message
from .pfc_types import ConversationState
from .pfc import ChatObserver, GoalAnalyzer
from .message_sender import DirectMessageSender
-from src.common.logger_manager import get_logger
+from src.common.logger import get_logger
from .action_planner import ActionPlanner
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo # 确保导入 ConversationInfo
from .reply_generator import ReplyGenerator
-from ..chat.chat_stream import ChatStream
+from src.chat.message_receive.chat_stream import ChatStream
from maim_message import UserInfo
-from src.plugins.chat.chat_stream import chat_manager
+from src.chat.message_receive.chat_stream import get_chat_manager
from .pfc_KnowledgeFetcher import KnowledgeFetcher
from .waiter import Waiter
@@ -60,7 +60,7 @@ class Conversation:
self.direct_sender = DirectMessageSender(self.private_name)
# 获取聊天流信息
- self.chat_stream = chat_manager.get_stream(self.stream_id)
+ self.chat_stream = get_chat_manager().get_stream(self.stream_id)
self.stop_action_planner = False
except Exception as e:
@@ -248,14 +248,14 @@ class Conversation:
def _convert_to_message(self, msg_dict: Dict[str, Any]) -> Message:
"""将消息字典转换为Message对象"""
try:
- # 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 chat_manager 获取
+ # 尝试从 msg_dict 直接获取 chat_stream,如果失败则从全局 get_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)
+ chat_stream = get_chat_manager().get_stream(self.stream_id)
if not chat_stream:
raise ValueError(f"无法确定 ChatStream for stream_id {self.stream_id}")
diff --git a/src/chat/brain_chat/PFC/message_sender.py b/src/chat/brain_chat/PFC/message_sender.py
index 12c2143e..0e2b96cb 100644
--- a/src/chat/brain_chat/PFC/message_sender.py
+++ b/src/chat/brain_chat/PFC/message_sender.py
@@ -1,13 +1,11 @@
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 src.chat.message_receive.chat_stream import ChatStream
+from src.chat.message_receive.message import Message, MessageSending
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
+from src.chat.message_receive.storage import MessageStorage
+from src.config.config import global_config
from rich.traceback import install
install(extra_lines=3)
@@ -66,15 +64,18 @@ class DirectMessageSender:
# 处理消息
await message.process()
- # 不知道有什么用,先留下来了,和之前那套sender一样
- _message_json = message.to_dict()
-
- # 发送消息
- 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}")
+ # 发送消息(直接调用底层 API)
+ from src.chat.message_receive.uni_message_sender import _send_message
+
+ sent = await _send_message(message, show_log=True)
+
+ if sent:
+ # 存储消息
+ await self.storage.store_message(message, chat_stream)
+ logger.info(f"[私聊][{self.private_name}]PFC消息已发送: {content}")
+ else:
+ logger.error(f"[私聊][{self.private_name}]PFC消息发送失败")
+ raise RuntimeError("消息发送失败")
except Exception as e:
logger.error(f"[私聊][{self.private_name}]PFC消息发送失败: {str(e)}")
diff --git a/src/chat/brain_chat/PFC/observation_info.py b/src/chat/brain_chat/PFC/observation_info.py
index c7572955..d72f8207 100644
--- a/src/chat/brain_chat/PFC/observation_info.py
+++ b/src/chat/brain_chat/PFC/observation_info.py
@@ -4,7 +4,7 @@ import time
from src.common.logger import get_module_logger
from .chat_observer import ChatObserver
from .chat_states import NotificationHandler, NotificationType, Notification
-from src.plugins.utils.chat_message_builder import build_readable_messages
+from src.chat.utils.chat_message_builder import build_readable_messages
import traceback # 导入 traceback 用于调试
logger = get_module_logger("observation_info")
diff --git a/src/chat/brain_chat/PFC/pfc.py b/src/chat/brain_chat/PFC/pfc.py
index b17ee21d..7095a7a2 100644
--- a/src/chat/brain_chat/PFC/pfc.py
+++ b/src/chat/brain_chat/PFC/pfc.py
@@ -1,13 +1,13 @@
from typing import List, Tuple, TYPE_CHECKING
from src.common.logger import get_module_logger
-from ..models.utils_model import LLMRequest
-from ...config.config import global_config
+from src.llm_models.utils_model import LLMRequest
+from src.config.config import global_config
+import random
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
-from src.plugins.utils.chat_message_builder import build_readable_messages
+from src.chat.utils.chat_message_builder import build_readable_messages
from rich.traceback import install
install(extra_lines=3)
@@ -46,7 +46,7 @@ class GoalAnalyzer:
model=global_config.llm_normal, temperature=0.7, max_tokens=1000, request_type="conversation_goal"
)
- self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
+ self.personality_info = self._get_personality_prompt()
self.name = global_config.BOT_NICKNAME
self.nick_name = global_config.BOT_ALIAS_NAMES
self.private_name = private_name
@@ -57,6 +57,21 @@ class GoalAnalyzer:
self.max_goals = 3 # 同时保持的最大目标数量
self.current_goal_and_reason = None
+ def _get_personality_prompt(self) -> str:
+ """获取个性提示信息"""
+ prompt_personality = global_config.personality.personality
+
+ # 检查是否需要随机替换为状态
+ if (
+ global_config.personality.states
+ and global_config.personality.state_probability > 0
+ and random.random() < global_config.personality.state_probability
+ ):
+ prompt_personality = random.choice(global_config.personality.states)
+
+ bot_name = global_config.bot.nickname
+ return f"你的名字是{bot_name},你{prompt_personality};"
+
async def analyze_goal(self, conversation_info: ConversationInfo, observation_info: ObservationInfo):
"""分析对话历史并设定目标
diff --git a/src/chat/brain_chat/PFC/pfc_KnowledgeFetcher.py b/src/chat/brain_chat/PFC/pfc_KnowledgeFetcher.py
index 0989339d..2954b063 100644
--- a/src/chat/brain_chat/PFC/pfc_KnowledgeFetcher.py
+++ b/src/chat/brain_chat/PFC/pfc_KnowledgeFetcher.py
@@ -1,11 +1,11 @@
from typing import List, Tuple
from src.common.logger import get_module_logger
from src.plugins.memory_system.Hippocampus import HippocampusManager
-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
+from src.llm_models.utils_model import LLMRequest
+from src.config.config import global_config
+from src.chat.message_receive.message import Message
+from src.chat.knowledge.knowledge_lib import qa_manager
+from src.chat.utils.chat_message_builder import build_readable_messages
logger = get_module_logger("knowledge_fetcher")
diff --git a/src/chat/brain_chat/PFC/reply_checker.py b/src/chat/brain_chat/PFC/reply_checker.py
index 35e9af50..182c0ab5 100644
--- a/src/chat/brain_chat/PFC/reply_checker.py
+++ b/src/chat/brain_chat/PFC/reply_checker.py
@@ -1,8 +1,8 @@
import json
from typing import Tuple, List, Dict, Any
from src.common.logger import get_module_logger
-from ..models.utils_model import LLMRequest
-from ...config.config import global_config
+from src.llm_models.utils_model import LLMRequest
+from src.config.config import global_config
from .chat_observer import ChatObserver
from maim_message import UserInfo
diff --git a/src/chat/brain_chat/PFC/reply_generator.py b/src/chat/brain_chat/PFC/reply_generator.py
index 890f807c..e1bd65e2 100644
--- a/src/chat/brain_chat/PFC/reply_generator.py
+++ b/src/chat/brain_chat/PFC/reply_generator.py
@@ -1,15 +1,15 @@
from typing import Tuple, List, Dict, Any
-from src.common.logger import get_module_logger
-from ..models.utils_model import LLMRequest
-from ...config.config import global_config
+from src.common.logger import get_logger
+from src.llm_models.utils_model import LLMRequest
+from src.config.config import global_config
+import random
from .chat_observer import ChatObserver
from .reply_checker import ReplyChecker
-from src.individuality.individuality import Individuality
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
-from src.plugins.utils.chat_message_builder import build_readable_messages
+from src.chat.utils.chat_message_builder import build_readable_messages
-logger = get_module_logger("reply_generator")
+logger = get_logger("reply_generator")
# --- 定义 Prompt 模板 ---
@@ -92,12 +92,27 @@ class ReplyGenerator:
max_tokens=300,
request_type="reply_generation",
)
- self.personality_info = Individuality.get_instance().get_prompt(x_person=2, level=3)
+ self.personality_info = self._get_personality_prompt()
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)
+ def _get_personality_prompt(self) -> str:
+ """获取个性提示信息"""
+ prompt_personality = global_config.personality.personality
+
+ # 检查是否需要随机替换为状态
+ if (
+ global_config.personality.states
+ and global_config.personality.state_probability > 0
+ and random.random() < global_config.personality.state_probability
+ ):
+ prompt_personality = random.choice(global_config.personality.states)
+
+ bot_name = global_config.BOT_NICKNAME
+ return f"你的名字是{bot_name},你{prompt_personality};"
+
# 修改 generate 方法签名,增加 action_type 参数
async def generate(
self, observation_info: ObservationInfo, conversation_info: ConversationInfo, action_type: str
diff --git a/src/chat/brain_chat/PFC/waiter.py b/src/chat/brain_chat/PFC/waiter.py
index 0f5881fc..af5cf7ad 100644
--- a/src/chat/brain_chat/PFC/waiter.py
+++ b/src/chat/brain_chat/PFC/waiter.py
@@ -3,7 +3,7 @@ from .chat_observer import ChatObserver
from .conversation_info import ConversationInfo
# from src.individuality.individuality import Individuality # 不再需要
-from ...config.config import global_config
+from src.config.config import global_config
import time
import asyncio
diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py
index b555bf0f..8fd2de94 100644
--- a/src/chat/brain_chat/brain_chat.py
+++ b/src/chat/brain_chat/brain_chat.py
@@ -87,6 +87,7 @@ class BrainChatting:
# 循环控制内部状态
self.running: bool = False
self._loop_task: Optional[asyncio.Task] = None # 主循环任务
+ self._new_message_event = asyncio.Event() # 新消息事件,用于打断 wait
# 添加循环信息管理相关的属性
self.history_loop: List[CycleDetail] = []
@@ -173,9 +174,10 @@ class BrainChatting:
filter_intercept_message_level=1,
)
- # 如果有新消息,更新 last_read_time
+ # 如果有新消息,更新 last_read_time 并触发事件以打断正在进行的 wait
if len(recent_messages_list) >= 1:
self.last_read_time = time.time()
+ self._new_message_event.set() # 触发新消息事件,打断 wait
# 总是执行一次思考迭代(不管有没有新消息)
# wait 动作会在其内部等待,不需要在这里处理
@@ -434,6 +436,9 @@ class BrainChatting:
last_check_time = self.last_read_time
check_interval = 1.0 # 每秒检查一次
+ # 清除事件状态,准备等待新消息
+ self._new_message_event.clear()
+
while self.running:
# 检查是否有新消息
recent_messages_list = message_api.get_messages_by_time_in_chat(
@@ -453,8 +458,15 @@ class BrainChatting:
logger.info(f"{self.log_prefix} 检测到新消息,恢复循环")
return
- # 等待一段时间后再次检查
- await asyncio.sleep(check_interval)
+ # 等待新消息事件或超时后再次检查
+ try:
+ await asyncio.wait_for(self._new_message_event.wait(), timeout=check_interval)
+ # 事件被触发,说明有新消息
+ logger.info(f"{self.log_prefix} 检测到新消息事件,恢复循环")
+ return
+ except asyncio.TimeoutError:
+ # 超时后继续检查
+ continue
async def _handle_action(
self,
@@ -674,7 +686,10 @@ class BrainChatting:
logger.warning(f"{self.log_prefix} wait_seconds 参数格式错误,使用默认值 5 秒")
wait_seconds = 5
- logger.info(f"{self.log_prefix} 执行 wait 动作,等待 {wait_seconds} 秒")
+ logger.info(f"{self.log_prefix} 执行 wait 动作,等待 {wait_seconds} 秒(可被新消息打断)")
+
+ # 清除事件状态,准备等待新消息
+ self._new_message_event.clear()
# 记录动作信息
await database_api.store_action_info(
@@ -687,8 +702,17 @@ class BrainChatting:
action_name="wait",
)
- # 等待指定时间
- await asyncio.sleep(wait_seconds)
+ # 等待指定时间,但可被新消息打断
+ try:
+ await asyncio.wait_for(
+ self._new_message_event.wait(),
+ timeout=wait_seconds
+ )
+ # 如果事件被触发,说明有新消息到达
+ logger.info(f"{self.log_prefix} wait 动作被新消息打断,提前结束等待")
+ except asyncio.TimeoutError:
+ # 超时正常完成
+ pass
logger.info(f"{self.log_prefix} wait 动作完成,继续下一次思考")
@@ -707,7 +731,10 @@ class BrainChatting:
# 使用默认等待时间
wait_seconds = 3
- logger.info(f"{self.log_prefix} 执行 listening(转换为 wait)动作,等待 {wait_seconds} 秒")
+ logger.info(f"{self.log_prefix} 执行 listening(转换为 wait)动作,等待 {wait_seconds} 秒(可被新消息打断)")
+
+ # 清除事件状态,准备等待新消息
+ self._new_message_event.clear()
# 记录动作信息
await database_api.store_action_info(
@@ -720,8 +747,17 @@ class BrainChatting:
action_name="listening",
)
- # 等待指定时间
- await asyncio.sleep(wait_seconds)
+ # 等待指定时间,但可被新消息打断
+ try:
+ await asyncio.wait_for(
+ self._new_message_event.wait(),
+ timeout=wait_seconds
+ )
+ # 如果事件被触发,说明有新消息到达
+ logger.info(f"{self.log_prefix} listening 动作被新消息打断,提前结束等待")
+ except asyncio.TimeoutError:
+ # 超时正常完成
+ pass
logger.info(f"{self.log_prefix} listening 动作完成,继续下一次思考")
diff --git a/src/chat/brain_chat/brain_planner.py b/src/chat/brain_chat/brain_planner.py
index 4245549d..7d5990e4 100644
--- a/src/chat/brain_chat/brain_planner.py
+++ b/src/chat/brain_chat/brain_planner.py
@@ -402,7 +402,7 @@ class BrainPlanner:
moderation_prompt=moderation_prompt_block,
name_block=name_block,
interest=interest,
- plan_style=global_config.personality.private_plan_style,
+ plan_style=global_config.experimental.private_plan_style,
)
return prompt, message_id_list
diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py
index 2f58f704..ba9bf388 100644
--- a/src/chat/heart_flow/heartFC_chat.py
+++ b/src/chat/heart_flow/heartFC_chat.py
@@ -545,9 +545,9 @@ class HeartFChatting:
new_message_count = message_api.count_new_messages(
chat_id=self.chat_stream.stream_id, start_time=self.last_read_time, end_time=time.time()
)
- need_reply = new_message_count >= random.randint(2, 3)
+ need_reply = new_message_count >= random.randint(2, 3) or time.time() - self.last_read_time > 90
if need_reply:
- logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复")
+ logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复,或者上次回复时间超过90秒")
reply_text = ""
first_replied = False
diff --git a/src/chat/message_receive/message.py b/src/chat/message_receive/message.py
index d093e07e..7769b022 100644
--- a/src/chat/message_receive/message.py
+++ b/src/chat/message_receive/message.py
@@ -1,4 +1,5 @@
import time
+import asyncio
import urllib3
from abc import abstractmethod
@@ -20,6 +21,9 @@ logger = get_logger("chat_message")
# 禁用SSL警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+# VLM 处理并发限制(避免同时处理太多图片导致卡死)
+_vlm_semaphore = asyncio.Semaphore(3)
+
# 这个类是消息数据类,用于存储和管理消息数据。
# 它定义了消息的属性,包括群组ID、用户ID、消息ID、原始消息内容、纯文本内容和时间戳。
# 它还定义了两个辅助属性:keywords用于提取消息的关键词,is_plain_text用于判断消息是否为纯文本。
@@ -73,20 +77,35 @@ class Message(MessageBase):
str: 处理后的文本
"""
if segment.type == "seglist":
- # 处理消息段列表
+ # 处理消息段列表 - 使用并行处理提升性能
+ tasks = [self._process_message_segments(seg) for seg in segment.data] # type: ignore
+ results = await asyncio.gather(*tasks, return_exceptions=True)
segments_text = []
- for seg in segment.data:
- processed = await self._process_message_segments(seg) # type: ignore
- if processed:
- segments_text.append(processed)
+ for result in results:
+ if isinstance(result, Exception):
+ logger.error(f"处理消息段时出错: {result}")
+ continue
+ if result:
+ segments_text.append(result)
return " ".join(segments_text)
elif segment.type == "forward":
- segments_text = []
- for node_dict in segment.data:
+ # 处理转发消息 - 使用并行处理
+ async def process_forward_node(node_dict):
message = MessageBase.from_dict(node_dict) # type: ignore
processed_text = await self._process_message_segments(message.message_segment)
if processed_text:
- segments_text.append(f"{global_config.bot.nickname}: {processed_text}")
+ return f"{global_config.bot.nickname}: {processed_text}"
+ return None
+
+ tasks = [process_forward_node(node_dict) for node_dict in segment.data]
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+ segments_text = []
+ for result in results:
+ if isinstance(result, Exception):
+ logger.error(f"处理转发节点时出错: {result}")
+ continue
+ if result:
+ segments_text.append(result)
return "[合并消息]: " + "\n-- ".join(segments_text)
else:
# 处理单个消息段
@@ -173,8 +192,9 @@ class MessageRecv(Message):
self.is_picid = True
self.is_emoji = False
image_manager = get_image_manager()
- # print(f"segment.data: {segment.data}")
- _, processed_text = await image_manager.process_image(segment.data)
+ # 使用 semaphore 限制 VLM 并发,避免同时处理太多图片
+ async with _vlm_semaphore:
+ _, processed_text = await image_manager.process_image(segment.data)
return processed_text
return "[发了一张图片,网卡了加载不出来]"
elif segment.type == "emoji":
@@ -183,7 +203,9 @@ class MessageRecv(Message):
self.is_picid = False
self.is_voice = False
if isinstance(segment.data, str):
- return await get_image_manager().get_emoji_description(segment.data)
+ # 使用 semaphore 限制 VLM 并发
+ async with _vlm_semaphore:
+ return await get_image_manager().get_emoji_description(segment.data)
return "[发了一个表情包,网卡了加载不出来]"
elif segment.type == "voice":
self.is_picid = False
diff --git a/src/common/server.py b/src/common/server.py
index 88608677..e51eb6cd 100644
--- a/src/common/server.py
+++ b/src/common/server.py
@@ -50,7 +50,15 @@ class Server:
async def run(self):
"""启动服务器"""
# 禁用 uvicorn 默认日志和访问日志
- config = Config(app=self.app, host=self._host, port=self._port, log_config=None, access_log=False)
+ # 设置 ws_max_size 为 100MB,支持大消息(如包含多张图片的转发消息)
+ config = Config(
+ app=self.app,
+ host=self._host,
+ port=self._port,
+ log_config=None,
+ access_log=False,
+ ws_max_size=104_857_600, # 100MB
+ )
self._server = UvicornServer(config=config)
try:
await self._server.serve()
diff --git a/src/config/config.py b/src/config/config.py
index 69f1013e..cafde1b6 100644
--- a/src/config/config.py
+++ b/src/config/config.py
@@ -57,7 +57,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
-MMC_VERSION = "0.12.1"
+MMC_VERSION = "0.12.2"
def get_key_comment(toml_table, key):
diff --git a/src/config/official_configs.py b/src/config/official_configs.py
index 9816fd1e..c0e1950f 100644
--- a/src/config/official_configs.py
+++ b/src/config/official_configs.py
@@ -57,9 +57,6 @@ class PersonalityConfig(ConfigBase):
visual_style: str = ""
"""图片提示词"""
- private_plan_style: str = ""
- """私聊说话规则,行为风格"""
-
states: list[str] = field(default_factory=lambda: [])
"""状态列表,用于随机替换personality"""
@@ -713,8 +710,8 @@ class DebugConfig(ConfigBase):
class ExperimentalConfig(ConfigBase):
"""实验功能配置类"""
- enable_friend_chat: bool = False
- """是否启用好友聊天"""
+ private_plan_style: str = ""
+ """私聊说话规则,行为风格(实验性功能)"""
chat_prompts: list[str] = field(default_factory=lambda: [])
"""
@@ -904,6 +901,13 @@ class DreamConfig(ConfigBase):
return False
+ dream_visible: bool = False
+ """
+ 做梦结果是否存储到上下文
+ - True: 将梦境发送给配置的用户后,也会存储到聊天上下文中,在后续对话中可见
+ - False: 仅发送梦境但不存储,不在后续对话上下文中出现
+ """
+
def __post_init__(self):
"""验证配置值"""
if self.interval_minutes < 1:
diff --git a/src/dream/dream_generator.py b/src/dream/dream_generator.py
index edbc3160..c17c9b12 100644
--- a/src/dream/dream_generator.py
+++ b/src/dream/dream_generator.py
@@ -215,11 +215,12 @@ async def generate_dream_summary(
f"platform={platform!r}, user_id={user_id!r}"
)
else:
+ dream_visible = global_config.dream.dream_visible
ok = await send_api.text_to_stream(
dream_content,
stream_id=stream_id,
typing=False,
- storage_message=True,
+ storage_message=dream_visible,
)
if ok:
logger.info(
diff --git a/src/memory_system/memory_retrieval.py b/src/memory_system/memory_retrieval.py
index a893b3cc..4dd613e8 100644
--- a/src/memory_system/memory_retrieval.py
+++ b/src/memory_system/memory_retrieval.py
@@ -1,7 +1,6 @@
import time
import json
import asyncio
-import re
from typing import List, Dict, Any, Optional, Tuple
from src.common.logger import get_logger
from src.config.config import global_config, model_config
@@ -108,7 +107,7 @@ def init_memory_retrieval_prompt():
- 你可以对查询思路给出简短的思考:思考要简短,直接切入要点
- 先思考当前信息是否足够回答问题
- 如果信息不足,则需要使用tool查询信息,你必须给出使用什么工具进行查询
-- 如果当前已收集的信息足够或信息不足确定无法找到答案,你必须调用finish_search工具结束查询
+- 如果当前已收集的信息足够或信息不足确定无法找到答案,你必须调用found_answer工具结束查询
""",
name="memory_retrieval_react_prompt_head",
)
@@ -312,7 +311,7 @@ async def _react_agent_solve_question(
return None
- # 正常迭代:使用head_prompt决定调用哪些工具(包含finish_search工具)
+ # 正常迭代:使用head_prompt决定调用哪些工具(包含found_answer工具)
tool_definitions = tool_registry.get_tool_definitions()
# tool_names = [tool_def["name"] for tool_def in tool_definitions]
# logger.debug(f"ReAct Agent 第 {iteration + 1} 次迭代,问题: {question}|可用工具: {', '.join(tool_names)} (共{len(tool_definitions)}个)")
@@ -373,7 +372,7 @@ async def _react_agent_solve_question(
logger.error(f"ReAct Agent LLM调用失败: {response}")
break
- # 注意:这里会检查finish_search工具调用,如果检测到finish_search工具,会根据found_answer参数决定返回答案或退出查询
+ # 注意:这里会检查found_answer工具调用,如果检测到found_answer工具,会根据answer参数决定返回答案或退出查询
assistant_message: Optional[Message] = None
if tool_calls:
@@ -403,115 +402,146 @@ async def _react_agent_solve_question(
# 处理工具调用
if not tool_calls:
- # 如果没有工具调用,检查响应文本中是否包含finish_search函数调用格式
+ # 如果没有工具调用,检查响应文本中是否包含found_answer函数调用格式或JSON格式
if response and response.strip():
- # 尝试从文本中解析finish_search函数调用
- def parse_finish_search_from_text(text: str):
- """从文本中解析finish_search函数调用,返回(found_answer, answer)元组,如果未找到则返回(None, None)"""
+ # 首先尝试解析JSON格式的found_answer
+ def parse_json_found_answer(text: str):
+ """从文本中解析JSON格式的found_answer,返回(found_answer, answer)元组,如果未找到则返回(None, None)"""
if not text:
return None, None
+
+ try:
+ # 尝试提取JSON对象(可能包含在代码块中或直接是JSON)
+ json_text = text.strip()
+
+ # 如果包含代码块标记,提取JSON部分
+ if "```json" in json_text:
+ start = json_text.find("```json") + 7
+ end = json_text.find("```", start)
+ if end != -1:
+ json_text = json_text[start:end].strip()
+ elif "```" in json_text:
+ start = json_text.find("```") + 3
+ end = json_text.find("```", start)
+ if end != -1:
+ json_text = json_text[start:end].strip()
+
+ # 尝试解析JSON
+ data = json.loads(json_text)
+
+ # 检查是否包含found_answer字段
+ if isinstance(data, dict) and "found_answer" in data:
+ found_answer = bool(data.get("found_answer", False))
+ answer = data.get("answer", "")
+ return found_answer, answer
+ except (json.JSONDecodeError, ValueError, TypeError):
+ # 如果JSON解析失败,尝试在文本中查找JSON对象
+ try:
+ # 查找第一个 { 和最后一个 } 之间的内容(更健壮的JSON提取)
+ first_brace = text.find('{')
+ if first_brace != -1:
+ # 从第一个 { 开始,找到匹配的 }
+ brace_count = 0
+ json_end = -1
+ for i in range(first_brace, len(text)):
+ if text[i] == '{':
+ brace_count += 1
+ elif text[i] == '}':
+ brace_count -= 1
+ if brace_count == 0:
+ json_end = i + 1
+ break
+
+ if json_end != -1:
+ json_text = text[first_brace:json_end]
+ data = json.loads(json_text)
+ if isinstance(data, dict) and "found_answer" in data:
+ found_answer = bool(data.get("found_answer", False))
+ answer = data.get("answer", "")
+ return found_answer, answer
+ except (json.JSONDecodeError, ValueError, TypeError):
+ pass
+
+ return None, None
+
+ # 尝试从文本中解析found_answer函数调用
+ def parse_found_answer_from_text(text: str):
+ """从文本中解析found_answer函数调用,返回answer字符串,如果未找到则返回None
+ 如果answer存在且非空,表示找到答案;如果answer为空或不存在,表示未找到答案"""
+ if not text:
+ return None
- # 查找finish_search函数调用位置(不区分大小写)
- func_pattern = "finish_search"
+ # 查找found_answer函数调用位置(不区分大小写)
+ func_pattern = "found_answer"
text_lower = text.lower()
func_pos = text_lower.find(func_pattern)
if func_pos == -1:
- return None, None
-
- # 查找函数调用的开始和结束位置
- # 从func_pos开始向后查找左括号
- start_pos = text.find("(", func_pos)
- if start_pos == -1:
- return None, None
-
- # 查找匹配的右括号(考虑嵌套)
- paren_count = 0
- end_pos = start_pos
- for i in range(start_pos, len(text)):
- if text[i] == "(":
- paren_count += 1
- elif text[i] == ")":
- paren_count -= 1
- if paren_count == 0:
- end_pos = i
- break
- else:
- # 没有找到匹配的右括号
- return None, None
-
- # 提取函数参数部分
- params_text = text[start_pos + 1 : end_pos]
-
- # 解析found_answer参数(布尔值,可能是true/false/True/False)
- found_answer = None
- found_answer_patterns = [
- r"found_answer\s*=\s*true",
- r"found_answer\s*=\s*True",
- r"found_answer\s*=\s*false",
- r"found_answer\s*=\s*False",
- ]
- for pattern in found_answer_patterns:
- match = re.search(pattern, params_text, re.IGNORECASE)
- if match:
- found_answer = "true" in match.group(0).lower()
- break
+ return None
# 解析answer参数(字符串,使用extract_quoted_content)
- answer = extract_quoted_content(text, "finish_search", "answer")
+ answer = extract_quoted_content(text, "found_answer", "answer")
+
+ # 如果answer存在(即使是空字符串),也返回它(空字符串表示未找到答案)
+ return answer
- return found_answer, answer
+ # 首先尝试解析JSON格式
+ parsed_found_answer_json, parsed_answer_json = parse_json_found_answer(response)
+ is_json_format = parsed_found_answer_json is not None
+
+ # 如果JSON解析成功,使用JSON结果
+ if is_json_format:
+ parsed_answer = parsed_answer_json
+ has_answer = parsed_found_answer_json and parsed_answer and parsed_answer.strip()
+ else:
+ # 如果JSON解析失败,尝试解析函数调用格式
+ parsed_answer = parse_found_answer_from_text(response)
+ # 如果answer存在且非空,表示找到答案;否则表示未找到答案
+ has_answer = parsed_answer is not None and parsed_answer.strip() != ""
- parsed_found_answer, parsed_answer = parse_finish_search_from_text(response)
-
- if parsed_found_answer is not None:
- # 检测到finish_search函数调用格式
- if parsed_found_answer:
+ if parsed_answer is not None or is_json_format:
+ # 检测到found_answer格式(可能是JSON格式或函数调用格式)
+ format_type = "JSON格式" if is_json_format else "函数调用格式"
+ if has_answer:
# 找到了答案
- if parsed_answer:
- step["actions"].append(
- {
- "action_type": "finish_search",
- "action_params": {"found_answer": True, "answer": parsed_answer},
- }
- )
- step["observations"] = ["检测到finish_search文本格式调用,找到答案"]
- thinking_steps.append(step)
- logger.info(
- f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search文本格式找到关于问题{question}的答案: {parsed_answer}"
- )
-
- _log_conversation_messages(
- conversation_messages,
- head_prompt=first_head_prompt,
- final_status=f"找到答案:{parsed_answer}",
- )
-
- return True, parsed_answer, thinking_steps, False
- else:
- # found_answer为True但没有提供answer,视为错误,继续迭代
- logger.warning(
- f"{react_log_prefix}第 {iteration + 1} 次迭代 finish_search文本格式found_answer为True但未提供answer"
- )
- else:
- # 未找到答案,直接退出查询
step["actions"].append(
- {"action_type": "finish_search", "action_params": {"found_answer": False}}
+ {
+ "action_type": "found_answer",
+ "action_params": {"answer": parsed_answer},
+ }
)
- step["observations"] = ["检测到finish_search文本格式调用,未找到答案"]
+ step["observations"] = [f"检测到found_answer{format_type}调用,找到答案"]
thinking_steps.append(step)
logger.info(
- f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search文本格式判断未找到答案"
+ f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer{format_type}找到关于问题{question}的答案: {parsed_answer[:100]}..."
)
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
- final_status="未找到答案:通过finish_search文本格式判断未找到答案",
+ final_status=f"找到答案:{parsed_answer}",
+ )
+
+ return True, parsed_answer, thinking_steps, False
+ else:
+ # 未找到答案,直接退出查询
+ step["actions"].append(
+ {"action_type": "found_answer", "action_params": {"answer": ""}}
+ )
+ step["observations"] = [f"检测到found_answer{format_type}调用,未找到答案"]
+ thinking_steps.append(step)
+ logger.info(
+ f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer{format_type}判断未找到答案"
+ )
+
+ _log_conversation_messages(
+ conversation_messages,
+ head_prompt=first_head_prompt,
+ final_status="未找到答案:通过found_answer文本格式判断未找到答案",
)
return False, "", thinking_steps, False
- # 如果没有检测到finish_search格式,记录思考过程,继续下一轮迭代
+ # 如果没有检测到found_answer格式,记录思考过程,继续下一轮迭代
step["observations"] = [f"思考完成,但未调用工具。响应: {response}"]
logger.info(
f"{react_log_prefix}第 {iteration + 1} 次迭代 思考完成但未调用工具: {response}"
@@ -525,62 +555,55 @@ async def _react_agent_solve_question(
continue
# 处理工具调用
- # 首先检查是否有finish_search工具调用,如果有则立即返回,不再处理其他工具
- finish_search_found = None
- finish_search_answer = None
+ # 首先检查是否有found_answer工具调用,如果有则立即返回,不再处理其他工具
+ found_answer_answer = None
for tool_call in tool_calls:
tool_name = tool_call.func_name
tool_args = tool_call.args or {}
- if tool_name == "finish_search":
- finish_search_found = tool_args.get("found_answer", False)
- finish_search_answer = tool_args.get("answer", "")
+ if tool_name == "found_answer":
+ found_answer_answer = tool_args.get("answer", "")
- if finish_search_found:
+ # 如果answer存在且非空,表示找到答案;否则表示未找到答案
+ if found_answer_answer and found_answer_answer.strip():
# 找到了答案
- if finish_search_answer:
- step["actions"].append(
- {
- "action_type": "finish_search",
- "action_params": {"found_answer": True, "answer": finish_search_answer},
- }
- )
- step["observations"] = ["检测到finish_search工具调用,找到答案"]
- thinking_steps.append(step)
- logger.info(
- f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search工具找到关于问题{question}的答案: {finish_search_answer}"
- )
-
- _log_conversation_messages(
- conversation_messages,
- head_prompt=first_head_prompt,
- final_status=f"找到答案:{finish_search_answer}",
- )
-
- return True, finish_search_answer, thinking_steps, False
- else:
- # found_answer为True但没有提供answer,视为错误
- logger.warning(
- f"{react_log_prefix}第 {iteration + 1} 次迭代 finish_search工具found_answer为True但未提供answer"
- )
- else:
- # 未找到答案,直接退出查询
- step["actions"].append({"action_type": "finish_search", "action_params": {"found_answer": False}})
- step["observations"] = ["检测到finish_search工具调用,未找到答案"]
+ step["actions"].append(
+ {
+ "action_type": "found_answer",
+ "action_params": {"answer": found_answer_answer},
+ }
+ )
+ step["observations"] = ["检测到found_answer工具调用,找到答案"]
thinking_steps.append(step)
logger.info(
- f"{react_log_prefix}第 {iteration + 1} 次迭代 通过finish_search工具判断未找到答案"
+ f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer工具找到关于问题{question}的答案: {found_answer_answer}"
)
_log_conversation_messages(
conversation_messages,
head_prompt=first_head_prompt,
- final_status="未找到答案:通过finish_search工具判断未找到答案",
+ final_status=f"找到答案:{found_answer_answer}",
+ )
+
+ return True, found_answer_answer, thinking_steps, False
+ else:
+ # 未找到答案,直接退出查询
+ step["actions"].append({"action_type": "found_answer", "action_params": {"answer": ""}})
+ step["observations"] = ["检测到found_answer工具调用,未找到答案"]
+ thinking_steps.append(step)
+ logger.info(
+ f"{react_log_prefix}第 {iteration + 1} 次迭代 通过found_answer工具判断未找到答案"
+ )
+
+ _log_conversation_messages(
+ conversation_messages,
+ head_prompt=first_head_prompt,
+ final_status="未找到答案:通过found_answer工具判断未找到答案",
)
return False, "", thinking_steps, False
- # 如果没有finish_search工具调用,继续处理其他工具
+ # 如果没有found_answer工具调用,继续处理其他工具
tool_tasks = []
for i, tool_call in enumerate(tool_calls):
tool_name = tool_call.func_name
@@ -590,8 +613,8 @@ async def _react_agent_solve_question(
f"{react_log_prefix}第 {iteration + 1} 次迭代 工具调用 {i + 1}/{len(tool_calls)}: {tool_name}({tool_args})"
)
- # 跳过finish_search工具调用(已经在上面处理过了)
- if tool_name == "finish_search":
+ # 跳过found_answer工具调用(已经在上面处理过了)
+ if tool_name == "found_answer":
continue
# 记录最后一次使用的工具名称(用于判断是否需要额外迭代)
diff --git a/src/memory_system/retrieval_tools/__init__.py b/src/memory_system/retrieval_tools/__init__.py
index e058deb9..a617d970 100644
--- a/src/memory_system/retrieval_tools/__init__.py
+++ b/src/memory_system/retrieval_tools/__init__.py
@@ -15,7 +15,7 @@ from .query_chat_history import register_tool as register_query_chat_history
from .query_lpmm_knowledge import register_tool as register_lpmm_knowledge
from .query_person_info import register_tool as register_query_person_info
from .query_words import register_tool as register_query_words
-from .found_answer import register_tool as register_finish_search
+from .found_answer import register_tool as register_found_answer
from src.config.config import global_config
@@ -24,7 +24,7 @@ def init_all_tools():
register_query_chat_history()
register_query_person_info()
register_query_words() # 注册query_words工具
- register_finish_search() # 注册finish_search工具
+ register_found_answer() # 注册found_answer工具
if global_config.lpmm_knowledge.lpmm_mode == "agent":
register_lpmm_knowledge()
diff --git a/src/memory_system/retrieval_tools/found_answer.py b/src/memory_system/retrieval_tools/found_answer.py
index bbed96b3..af233cb9 100644
--- a/src/memory_system/retrieval_tools/found_answer.py
+++ b/src/memory_system/retrieval_tools/found_answer.py
@@ -1,5 +1,5 @@
"""
-finish_search工具 - 用于在记忆检索过程中结束查询
+found_answer工具 - 用于在记忆检索过程中结束查询
"""
from src.common.logger import get_logger
@@ -8,17 +8,16 @@ from .tool_registry import register_memory_retrieval_tool
logger = get_logger("memory_retrieval_tools")
-async def finish_search(found_answer: bool, answer: str = "") -> str:
+async def found_answer(answer: str = "") -> str:
"""结束查询
Args:
- found_answer: 是否找到了答案
- answer: 如果找到了答案,提供答案内容;如果未找到,可以为空
+ answer: 如果找到了答案,提供答案内容;如果未找到答案,可以为空或不提供此参数
Returns:
str: 确认信息
"""
- if found_answer:
+ if answer and answer.strip():
logger.info(f"找到答案: {answer}")
return f"已确认找到答案: {answer}"
else:
@@ -27,23 +26,17 @@ async def finish_search(found_answer: bool, answer: str = "") -> str:
def register_tool():
- """注册finish_search工具"""
+ """注册found_answer工具"""
register_memory_retrieval_tool(
- name="finish_search",
- description="当你决定结束查询时,调用此工具。如果找到了明确答案,设置found_answer为true并在answer中提供答案;如果未找到答案,设置found_answer为false。只有在检索到明确、具体的答案时才设置found_answer为true,不要编造信息。",
+ name="found_answer",
+ description="当你决定结束查询时,调用此工具。如果找到了明确答案,在answer参数中提供答案内容;如果未找到答案,可以不提供answer参数或提供空字符串。只有在检索到明确、具体的答案时才提供answer,不要编造信息。",
parameters=[
- {
- "name": "found_answer",
- "type": "boolean",
- "description": "是否找到了答案",
- "required": True,
- },
{
"name": "answer",
"type": "string",
- "description": "如果found_answer为true,提供找到的答案内容,必须基于已收集的信息,不要编造;如果found_answer为false,可以为空",
+ "description": "如果找到了答案,提供找到的答案内容,必须基于已收集的信息,不要编造;如果未找到答案,可以不提供此参数或提供空字符串",
"required": False,
},
],
- execute_func=finish_search,
+ execute_func=found_answer,
)
diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml
index 3b21a432..bc26ef71 100644
--- a/template/bot_config_template.toml
+++ b/template/bot_config_template.toml
@@ -1,5 +1,5 @@
[inner]
-version = "7.3.3"
+version = "7.3.5"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
# 如果你想要修改配置文件,请递增version的值
@@ -40,20 +40,11 @@ multiple_reply_style = [
multiple_probability = 0.3
# 麦麦的说话规则和行为规则:
-plan_style = """
-1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
-2.如果相同的内容已经被执行,请不要重复执行
-3.你对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题
-4.请控制你的发言频率,不要太过频繁的发言
-5.如果有人对你感到厌烦,请减少回复
-6.如果有人在追问你,或者话题没有说完,请你继续回复"""
-
-
-# 麦麦私聊的说话规则,行为风格:
-private_plan_style = """
-1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
-2.如果相同的内容已经被执行,请不要重复执行
-3.某句话如果已经被回复过,不要重复回复"""
+plan_style = """1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
+2.如果相同的action已经被执行,请不要重复执行该action
+3.如果有人对你感到厌烦,请减少回复
+4.如果有人在追问你,或者话题没有说完,请你继续回复
+5.请分析哪些对话是和你说的,哪些是其他人之间的互动,不要误认为其他人之间的互动是和你说的"""
# 多重人格,根据概率随机选择人格
states = [
@@ -153,6 +144,7 @@ first_delay_seconds = 1800 # 程序启动后首次做梦前的延迟时间(秒
# 为空字符串时不推送。
dream_send = ""
+dream_visible = false # 做梦结果是否存储到上下文,True: 将梦境发送给配置的用户后,也会存储到聊天上下文中,在后续对话中可见;False: 仅发送梦境但不存储,不在后续对话上下文中出现
# 做梦时间段配置,格式:["HH:MM-HH:MM", ...]
# 如果列表为空,则表示全天允许做梦。
# 如果配置了时间段,则只有在这些时间段内才会实际执行做梦流程。
@@ -298,6 +290,12 @@ trust_xff = false # 是否启用X-Forwarded-For代理解析(默认false)
secure_cookie = false # 是否启用安全Cookie(仅通过HTTPS传输,默认false)
[experimental] #实验性功能
+# 麦麦私聊的说话规则,行为风格(实验性功能)
+private_plan_style = """
+1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用
+2.如果相同的内容已经被执行,请不要重复执行
+3.某句话如果已经被回复过,不要重复回复"""
+
# 为指定聊天添加额外的prompt配置
# 格式: ["platform:id:type:prompt内容", ...]
# 示例:
diff --git a/webui/dist/assets/icons-8bdCaZgy.js b/webui/dist/assets/icons-8bdCaZgy.js
new file mode 100644
index 00000000..60c964fa
Binary files /dev/null and b/webui/dist/assets/icons-8bdCaZgy.js differ
diff --git a/webui/dist/assets/icons-9Z4kBNLK.js b/webui/dist/assets/icons-9Z4kBNLK.js
deleted file mode 100644
index 1be32d04..00000000
Binary files a/webui/dist/assets/icons-9Z4kBNLK.js and /dev/null differ
diff --git a/webui/dist/assets/index-DC6giT2e.js b/webui/dist/assets/index-DC6giT2e.js
deleted file mode 100644
index 67f24e49..00000000
Binary files a/webui/dist/assets/index-DC6giT2e.js and /dev/null differ
diff --git a/webui/dist/assets/index-DD4VGX3W.js b/webui/dist/assets/index-DD4VGX3W.js
new file mode 100644
index 00000000..d54d300c
Binary files /dev/null and b/webui/dist/assets/index-DD4VGX3W.js differ
diff --git a/webui/dist/assets/index-DseBW7cm.css b/webui/dist/assets/index-DseBW7cm.css
deleted file mode 100644
index 2b7f649b..00000000
Binary files a/webui/dist/assets/index-DseBW7cm.css and /dev/null differ
diff --git a/webui/dist/assets/index-RB5cYCSR.css b/webui/dist/assets/index-RB5cYCSR.css
new file mode 100644
index 00000000..39537543
Binary files /dev/null and b/webui/dist/assets/index-RB5cYCSR.css differ
diff --git a/webui/dist/fonts/JetBrainsMono-Medium.ttf b/webui/dist/fonts/JetBrainsMono-Medium.ttf
new file mode 100644
index 00000000..97671156
Binary files /dev/null and b/webui/dist/fonts/JetBrainsMono-Medium.ttf differ
diff --git a/webui/dist/index.html b/webui/dist/index.html
index 600539d5..1a5c946b 100644
Binary files a/webui/dist/index.html and b/webui/dist/index.html differ