diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 697c4775..9c8cba5d 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,9 +1,26 @@ name: Ruff on: [ push, pull_request ] + +permissions: + contents: write + jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.head_ref || github.ref_name }} - uses: astral-sh/ruff-action@v3 + - run: ruff check --fix + - run: ruff format + - name: Commit changes + if: success() + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add -A + git diff --quiet && git diff --staged --quiet || git commit -m "🤖 自动格式化代码 [skip ci]" + git push diff --git a/.gitignore b/.gitignore index c2fb389e..3e9b9868 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,8 @@ message_queue_window.bat message_queue_window.txt queue_update.txt memory_graph.gml +/src/do_tool/tool_can_use/auto_create_tool.py +/src/do_tool/tool_can_use/execute_python_code_tool.py .env .env.* .cursor @@ -28,6 +30,9 @@ config/bot_config.toml config/bot_config.toml.bak src/plugins/remote/client_uuid.json run_none.bat +(测试版)麦麦生成人格.bat +(临时版)麦麦开始学习.bat +src/plugins/utils/statistic.py # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -237,3 +242,4 @@ logs /config/* run_none.bat config/old/bot_config_20250405_212257.toml + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..02fe9f82 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands +- **Run Bot**: `python bot.py` +- **Lint**: `ruff check --fix .` or `ruff format .` +- **Run Tests**: `python -m unittest discover -v` +- **Run Single Test**: `python -m unittest src/plugins/message/test.py` + +## Code Style +- **Formatting**: Line length 120 chars, use double quotes for strings +- **Imports**: Group standard library, external packages, then internal imports +- **Naming**: snake_case for functions/variables, PascalCase for classes +- **Error Handling**: Use try/except blocks with specific exceptions +- **Types**: Use type hints where possible +- **Docstrings**: Document classes and complex functions +- **Linting**: Follow ruff rules (E, F, B) with ignores E711, E501 + +When making changes, run `ruff check --fix .` to ensure code follows style guidelines. The codebase uses Ruff for linting and formatting. \ No newline at end of file diff --git a/README.md b/README.md index fa97fec1..325e3ad2 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,68 @@ # 麦麦!MaiCore-MaiMBot (编辑中) - -## 新版0.6.0部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a - +
-![Python Version](https://img.shields.io/badge/Python-3.9+-blue) -![License](https://img.shields.io/github/license/SengokuCola/MaiMBot) -![Status](https://img.shields.io/badge/状态-开发中-yellow) + ![Python Version](https://img.shields.io/badge/Python-3.10+-blue) + ![License](https://img.shields.io/github/license/SengokuCola/MaiMBot?label=协议) + ![Status](https://img.shields.io/badge/状态-开发中-yellow) + ![Contributors](https://img.shields.io/github/contributors/MaiM-with-u/MaiBot.svg?style=flat&label=贡献者) + ![forks](https://img.shields.io/github/forks/MaiM-with-u/MaiBot.svg?style=flat&label=分支数) + ![stars](https://img.shields.io/github/stars/MaiM-with-u/MaiBot?style=flat&label=星标数) + ![issues](https://img.shields.io/github/issues/MaiM-with-u/MaiBot) + +
+ +

+ + Logo + +
+ + 画师:略nd + + +

MaiBot(麦麦)

+

+ 一款专注于 群组聊天 的赛博网友 +
+ 探索本项目的文档 » +
+
+ + 报告Bug + · + 提出新特性 +

+ +

+ +## 新版0.6.x部署前先阅读:https://docs.mai-mai.org/manual/usage/mmc_q_a - ## 📝 项目简介 **🍔MaiCore是一个基于大语言模型的可交互智能体** -- LLM 提供对话能力 -- 动态Prompt构建器 -- 实时的思维系统 -- MongoDB 提供数据持久化支持 -- 可扩展,可支持多种平台和多种功能 -**最新版本: v0.6.0** ([查看更新日志](changelogs/changelog.md)) +- 💭 **智能对话系统**:基于LLM的自然语言交互 +- 🤔 **实时思维系统**:模拟人类思考过程 +- 💝 **情感表达系统**:丰富的表情包和情绪表达 +- 🧠 **持久记忆系统**:基于MongoDB的长期记忆存储 +- 🔄 **动态人格系统**:自适应的性格特征 + +
+ + 麦麦演示视频 +
+ 👆 点击观看麦麦演示视频 👆 +
+
+ + +### 📢 版本信息 + +**最新版本: v0.6.2** ([查看更新日志](changelogs/changelog.md)) > [!WARNING] > 请阅读教程后更新!!!!!!! > 请阅读教程后更新!!!!!!! @@ -28,19 +70,12 @@ > 次版本MaiBot将基于MaiCore运行,不再依赖于nonebot相关组件运行。 > MaiBot将通过nonebot的插件与nonebot建立联系,然后nonebot与QQ建立联系,实现MaiBot与QQ的交互 -**分支介绍:** -- main 稳定版本 -- dev 开发版(不知道什么意思就别下) -- classical 0.6.0以前的版本 +**分支说明:** +- `main`: 稳定发布版本 +- `dev`: 开发测试版本(不知道什么意思就别下) +- `classical`: 0.6.0之前的版本 -
- - 麦麦演示视频 -
- 👆 点击观看麦麦演示视频 👆 -
-
> [!WARNING] > - 项目处于活跃开发阶段,代码可能随时更改 @@ -49,6 +84,12 @@ > - 由于持续迭代,可能存在一些已知或未知的bug > - 由于开发中,可能消耗较多token +### ⚠️ 重要提示 + +- 升级到v0.6.x版本前请务必阅读:[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) +- 本版本基于MaiCore重构,通过nonebot插件与QQ平台交互 +- 项目处于活跃开发阶段,功能和API可能随时调整 + ### 💬交流群(开发和建议相关讨论)不一定有空回复,会优先写文档和代码 - [五群](https://qm.qq.com/q/JxvHZnxyec) 1022489779 - [一群](https://qm.qq.com/q/VQ3XZrWgMs) 766798517 【已满】 @@ -67,60 +108,41 @@ - [📚 核心Wiki文档](https://docs.mai-mai.org) - 项目最全面的文档中心,你可以了解麦麦有关的一切 ### 最新版本部署教程(MaiCore版本) -- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy.html) - 基于MaiCore的新版本部署方式(与旧版本不兼容) +- [🚀 最新版本部署教程](https://docs.mai-mai.org/manual/deployment/mmc_deploy_windows.html) - 基于MaiCore的新版本部署方式(与旧版本不兼容) ## 🎯 功能介绍 -### 💬 聊天功能 -- 提供思维流(心流)聊天和推理聊天两种对话逻辑 -- 支持关键词检索主动发言:对消息的话题topic进行识别,如果检测到麦麦存储过的话题就会主动进行发言 -- 支持bot名字呼唤发言:检测到"麦麦"会主动发言,可配置 -- 支持多模型,多厂商自定义配置 -- 动态的prompt构建器,更拟人 -- 支持图片,转发消息,回复消息的识别 -- 支持私聊功能,可使用PFC模式的有目的多轮对话(实验性) +| 模块 | 主要功能 | 特点 | +|------|---------|------| +| 💬 聊天系统 | • 心流/推理聊天
• 关键词主动发言
• 多模型支持
• 动态prompt构建
• 私聊功能(PFC) | 拟人化交互 | +| 🧠 心流系统 | • 实时思考生成
• 自动启停机制
• 日程系统联动
• 工具调用能力 | 智能化决策 | +| 🧠 记忆系统 | • 优化记忆抽取
• 海马体记忆机制
• 聊天记录概括 | 持久化记忆 | +| 😊 表情系统 | • 情绪匹配发送
• GIF支持
• 自动收集与审查 | 丰富表达 | +| 📅 日程系统 | • 动态日程生成
• 自定义想象力
• 思维流联动 | 智能规划 | +| 👥 关系系统 | • 关系管理优化
• 丰富接口支持
• 个性化交互 | 深度社交 | +| 📊 统计系统 | • 使用数据统计
• LLM调用记录
• 实时控制台显示 | 数据可视 | +| 🔧 系统功能 | • 优雅关闭机制
• 自动数据保存
• 异常处理完善 | 稳定可靠 | +| 🛠️ 工具系统 | • 知识获取工具
• 自动注册机制
• 多工具支持 | 扩展功能 | -### 🧠 思维流系统 -- 思维流能够在回复前后进行思考,生成实时想法 -- 思维流自动启停机制,提升资源利用效率 -- 思维流与日程系统联动,实现动态日程生成 +## 📐 项目架构 -### 🧠 记忆系统 2.0 -- 优化记忆抽取策略和prompt结构 -- 改进海马体记忆提取机制,提升自然度 -- 对聊天记录进行概括存储,在需要时调用 +```mermaid +graph TD + A[MaiCore] --> B[对话系统] + A --> C[心流系统] + A --> D[记忆系统] + A --> E[情感系统] + B --> F[多模型支持] + B --> G[动态Prompt] + C --> H[实时思考] + C --> I[日程联动] + D --> J[记忆存储] + D --> K[记忆检索] + E --> L[表情管理] + E --> M[情绪识别] +``` -### 😊 表情包系统 -- 支持根据发言内容发送对应情绪的表情包 -- 支持识别和处理gif表情包 -- 会自动偷群友的表情包 -- 表情包审查功能 -- 表情包文件完整性自动检查 -- 自动清理缓存图片 - -### 📅 日程系统 -- 动态更新的日程生成 -- 可自定义想象力程度 -- 与聊天情况交互(思维流模式下) - -### 👥 关系系统 2.0 -- 优化关系管理系统,适用于新版本 -- 提供更丰富的关系接口 -- 针对每个用户创建"关系",实现个性化回复 - -### 📊 统计系统 -- 详细的使用数据统计 -- LLM调用统计 -- 在控制台显示统计信息 - -### 🔧 系统功能 -- 支持优雅的shutdown机制 -- 自动保存功能,定期保存聊天记录和关系数据 -- 完善的异常处理机制 -- 可自定义时区设置 -- 优化的日志输出格式 -- 配置自动更新功能 ## 开发计划TODO:LIST @@ -157,7 +179,6 @@ MaiCore是一个开源项目,我们非常欢迎你的参与。你的贡献, ## 致谢 -- [nonebot2](https://github.com/nonebot/nonebot2): 跨平台 Python 异步聊天机器人框架 - [NapCat](https://github.com/NapNeko/NapCatQQ): 现代化的基于 NTQQ 的 Bot 协议端实现 ### 贡献者 diff --git a/bot.py b/bot.py index a0bf3a3c..653efd45 100644 --- a/bot.py +++ b/bot.py @@ -7,11 +7,16 @@ from pathlib import Path import time import platform from dotenv import load_dotenv -from src.common.logger import get_module_logger +from src.common.logger import get_module_logger, LogConfig, CONFIRM_STYLE_CONFIG +from src.common.crash_logger import install_crash_handler from src.main import MainSystem logger = get_module_logger("main_bot") - +confirm_logger_config = LogConfig( + console_format=CONFIRM_STYLE_CONFIG["console_format"], + file_format=CONFIRM_STYLE_CONFIG["file_format"], +) +confirm_logger = get_module_logger("confirm", config=confirm_logger_config) # 获取没有加载env时的环境变量 env_mask = {key: os.getenv(key) for key in os.environ} @@ -165,8 +170,8 @@ def check_eula(): # 如果EULA或隐私条款有更新,提示用户重新确认 if eula_updated or privacy_updated: - print("EULA或隐私条款内容已更新,请在阅读后重新确认,继续运行视为同意更新后的以上两款协议") - print( + confirm_logger.critical("EULA或隐私条款内容已更新,请在阅读后重新确认,继续运行视为同意更新后的以上两款协议") + confirm_logger.critical( f'输入"同意"或"confirmed"或设置环境变量"EULA_AGREE={eula_new_hash}"和"PRIVACY_AGREE={privacy_new_hash}"继续运行' ) while True: @@ -175,14 +180,14 @@ def check_eula(): # print("确认成功,继续运行") # print(f"确认成功,继续运行{eula_updated} {privacy_updated}") if eula_updated: - print(f"更新EULA确认文件{eula_new_hash}") + logger.info(f"更新EULA确认文件{eula_new_hash}") eula_confirm_file.write_text(eula_new_hash, encoding="utf-8") if privacy_updated: - print(f"更新隐私条款确认文件{privacy_new_hash}") + logger.info(f"更新隐私条款确认文件{privacy_new_hash}") privacy_confirm_file.write_text(privacy_new_hash, encoding="utf-8") break else: - print('请输入"同意"或"confirmed"以继续运行') + confirm_logger.critical('请输入"同意"或"confirmed"以继续运行') return elif eula_confirmed and privacy_confirmed: return @@ -193,6 +198,9 @@ def raw_main(): if platform.system().lower() != "windows": time.tzset() + # 安装崩溃日志处理器 + install_crash_handler() + check_eula() print("检查EULA和隐私条款完成") easter_egg() diff --git a/changelogs/changelog.md b/changelogs/changelog.md index 6b9898b5..0ddb486b 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,5 +1,58 @@ # Changelog +## [0.6.2] - 2025-4-14 + +### 摘要 +- MaiBot 0.6.2 版本发布! +- 优化了心流的观察系统,优化提示词和表现,现在心流表现更好! +- 新增工具调用能力,可以更好地获取信息 +- 本次更新主要围绕工具系统、心流系统、消息处理和代码优化展开,新增多个工具类,优化了心流系统的逻辑,改进了消息处理流程,并修复了多个问题。 + +### 🌟 核心功能增强 +#### 工具系统 +- 新增了知识获取工具系统,支持通过心流调用获取多种知识 +- 新增了工具系统使用指南,详细说明工具结构、自动注册机制和添加步骤 +- 新增了多个实用工具类,包括心情调整工具`ChangeMoodTool`、关系查询工具`RelationshipTool`、数值比较工具`CompareNumbersTool`、日程获取工具`GetCurrentTaskTool`、上下文压缩工具`CompressContextTool`和知识获取工具`GetKnowledgeTool` +- 更新了`ToolUser`类,支持自动获取已注册工具定义并调用`execute`方法 +- 需要配置支持工具调用的模型才能使用完整功能 + +#### 心流系统 +- 新增了上下文压缩缓存功能,可以有更持久的记忆 +- 新增了心流系统的README.md文件,详细介绍了系统架构、主要功能和工作流程。 +- 优化了心流系统的逻辑,包括子心流自动清理和合理配置更新间隔。 +- 改进了心流观察系统,优化了提示词设计和系统表现,使心流运行更加稳定高效。 +- 更新了`Heartflow`类的方法和属性,支持异步生成提示词并提升生成质量。 + +#### 消息处理 +- 改进了消息处理流程,包括回复检查、消息生成和发送逻辑。 +- 新增了`ReplyGenerator`类,用于根据观察信息和对话信息生成回复。 +- 优化了消息队列管理系统,支持按时间顺序处理消息。 + +#### 现在可以启用更好的表情包发送系统 + +### 💻 系统架构优化 + +#### 部署支持 +- 更新了Docker部署文档,优化了服务配置和挂载路径。 +- 完善了Linux和Windows脚本支持。 + +### 🐛 问题修复 +- 修复了消息处理器中的正则表达式匹配问题。 +- 修复了图像处理中的帧大小和拼接问题。 +- 修复了私聊时产生`reply`消息的bug。 +- 修复了配置文件加载时的版本兼容性问题。 + +### 📚 文档更新 +- 更新了`README.md`文件,包括Python版本要求和协议信息。 +- 新增了工具系统和心流系统的详细文档。 +- 优化了部署相关文档的完整性。 + +### 🔧 其他改进 +- 新增了崩溃日志记录器,记录崩溃信息到日志文件。 +- 优化了统计信息输出,在控制台显示详细统计信息。 +- 改进了异常处理机制,提升系统稳定性。 +- 现可配置部分模型的temp参数 + ## [0.6.0] - 2025-4-4 ### 摘要 diff --git a/changelogs/changelog_config.md b/changelogs/changelog_config.md index 32912f69..e438ea31 100644 --- a/changelogs/changelog_config.md +++ b/changelogs/changelog_config.md @@ -22,7 +22,7 @@ ## [0.0.11] - 2025-3-12 ### Added - 新增了 `schedule` 配置项,用于配置日程表生成功能 -- 新增了 `response_spliter` 配置项,用于控制回复分割 +- 新增了 `response_splitter` 配置项,用于控制回复分割 - 新增了 `experimental` 配置项,用于实验性功能开关 - 新增了 `llm_observation` 和 `llm_sub_heartflow` 模型配置 - 新增了 `llm_heartflow` 模型配置 diff --git a/depends-data/maimai.png b/depends-data/maimai.png new file mode 100644 index 00000000..faccb856 Binary files /dev/null and b/depends-data/maimai.png differ diff --git a/depends-data/video.png b/depends-data/video.png new file mode 100644 index 00000000..84176b2d Binary files /dev/null and b/depends-data/video.png differ diff --git a/docker-compose.yml b/docker-compose.yml index bde382e5..000d00c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,14 @@ services: adapters: container_name: maim-bot-adapters - image: maple127667/maimbot-adapter:latest + image: unclas/maimbot-adapter:latest # image: infinitycat/maimbot-adapter:latest environment: - TZ=Asia/Shanghai - ports: - - "18002:18002" +# ports: +# - "8095:8095" volumes: - - ./docker-config/adapters/config.py:/adapters/src/plugins/nonebot_plugin_maibot_adapters/config.py # 持久化adapters配置文件 - - ./docker-config/adapters/.env:/adapters/.env # 持久化adapters配置文件 - - ./data/qq:/app/.config/QQ # 持久化QQ本体并同步qq表情和图片到adapters - - ./data/MaiMBot:/adapters/data + - ./docker-config/adapters/config.toml:/adapters/config.toml restart: always depends_on: - mongodb @@ -25,8 +22,8 @@ services: - TZ=Asia/Shanghai # - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA # - PRIVACY_AGREE=2402af06e133d2d10d9c6c643fdc9333 # 同意EULA - ports: - - "8000:8000" +# ports: +# - "8000:8000" volumes: - ./docker-config/mmc/.env:/MaiMBot/.env # 持久化env配置文件 - ./docker-config/mmc:/MaiMBot/config # 持久化bot配置文件 @@ -42,8 +39,8 @@ services: - TZ=Asia/Shanghai # - MONGO_INITDB_ROOT_USERNAME=your_username # 此处配置mongo用户 # - MONGO_INITDB_ROOT_PASSWORD=your_password # 此处配置mongo密码 - ports: - - "27017:27017" +# ports: +# - "27017:27017" restart: always volumes: - mongodb:/data/db # 持久化mongodb数据 @@ -58,11 +55,10 @@ services: - TZ=Asia/Shanghai ports: - "6099:6099" - - "8095:8095" volumes: - ./docker-config/napcat:/app/napcat/config # 持久化napcat配置文件 - ./data/qq:/app/.config/QQ # 持久化QQ本体并同步qq表情和图片到adapters - - ./data/MaiMBot:/adapters/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 + - ./data/MaiMBot:/MaiMBot/data # NapCat 和 NoneBot 共享此卷,否则发送图片会有问题 container_name: maim-bot-napcat restart: always image: mlikiowa/napcat-docker:latest diff --git a/requirements.txt b/requirements.txt index ada41d29..0fcb31f8 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/scripts/run.sh b/scripts/run.sh index 342a23fe..b7ecbc84 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,10 +1,10 @@ #!/bin/bash -# MaiCore & Nonebot adapter一键安装脚本 by Cookie_987 +# MaiCore & NapCat Adapter一键安装脚本 by Cookie_987 # 适用于Arch/Ubuntu 24.10/Debian 12/CentOS 9 # 请小心使用任何一键脚本! -INSTALLER_VERSION="0.0.2-refactor" +INSTALLER_VERSION="0.0.3-refactor" LANG=C.UTF-8 # 如无法访问GitHub请修改此处镜像地址 @@ -31,7 +31,7 @@ DEFAULT_INSTALL_DIR="/opt/maicore" # 服务名称 SERVICE_NAME="maicore" SERVICE_NAME_WEB="maicore-web" -SERVICE_NAME_NBADAPTER="maicore-nonebot-adapter" +SERVICE_NAME_NBADAPTER="maibot-napcat-adapter" IS_INSTALL_MONGODB=false IS_INSTALL_NAPCAT=false @@ -59,9 +59,9 @@ show_menu() { "1" "启动MaiCore" \ "2" "停止MaiCore" \ "3" "重启MaiCore" \ - "4" "启动Nonebot adapter" \ - "5" "停止Nonebot adapter" \ - "6" "重启Nonebot adapter" \ + "4" "启动NapCat Adapter" \ + "5" "停止NapCat Adapter" \ + "6" "重启NapCat Adapter" \ "7" "拉取最新MaiCore仓库" \ "8" "切换分支" \ "9" "退出" 3>&1 1>&2 2>&3) @@ -83,15 +83,15 @@ show_menu() { ;; 4) systemctl start ${SERVICE_NAME_NBADAPTER} - whiptail --msgbox "✅Nonebot adapter已启动" 10 60 + whiptail --msgbox "✅NapCat Adapter已启动" 10 60 ;; 5) systemctl stop ${SERVICE_NAME_NBADAPTER} - whiptail --msgbox "🛑Nonebot adapter已停止" 10 60 + whiptail --msgbox "🛑NapCat Adapter已停止" 10 60 ;; 6) systemctl restart ${SERVICE_NAME_NBADAPTER} - whiptail --msgbox "🔄Nonebot adapter已重启" 10 60 + whiptail --msgbox "🔄NapCat Adapter已重启" 10 60 ;; 7) update_dependencies @@ -357,8 +357,8 @@ run_installation() { # Python版本检查 check_python() { PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') - if ! python3 -c "import sys; exit(0) if sys.version_info >= (3,9) else exit(1)"; then - whiptail --title "⚠️ [4/6] Python 版本过低" --msgbox "检测到 Python 版本为 $PYTHON_VERSION,需要 3.9 或以上!\n请升级 Python 后重新运行本脚本。" 10 60 + if ! python3 -c "import sys; exit(0) if sys.version_info >= (3,10) else exit(1)"; then + whiptail --title "⚠️ [4/6] Python 版本过低" --msgbox "检测到 Python 版本为 $PYTHON_VERSION,需要 3.10 或以上!\n请升级 Python 后重新运行本脚本。" 10 60 exit 1 fi } @@ -410,7 +410,7 @@ run_installation() { # 确认安装 confirm_install() { local confirm_msg="请确认以下更改:\n\n" - confirm_msg+="📂 安装MaiCore、Nonebot Adapter到: $INSTALL_DIR\n" + confirm_msg+="📂 安装MaiCore、NapCat Adapter到: $INSTALL_DIR\n" confirm_msg+="🔀 分支: $BRANCH\n" [[ $IS_INSTALL_DEPENDENCIES == true ]] && confirm_msg+="📦 安装依赖:${missing_packages[@]}\n" [[ $IS_INSTALL_MONGODB == true || $IS_INSTALL_NAPCAT == true ]] && confirm_msg+="📦 安装额外组件:\n" @@ -499,50 +499,28 @@ EOF } echo -e "${GREEN}克隆 nonebot-plugin-maibot-adapters 仓库...${RESET}" - git clone $GITHUB_REPO/MaiM-with-u/nonebot-plugin-maibot-adapters.git || { - echo -e "${RED}克隆 nonebot-plugin-maibot-adapters 仓库失败!${RESET}" + git clone $GITHUB_REPO/MaiM-with-u/MaiBot-Napcat-Adapter.git || { + echo -e "${RED}克隆 MaiBot-Napcat-Adapter.git 仓库失败!${RESET}" exit 1 } echo -e "${GREEN}安装Python依赖...${RESET}" pip install -r MaiBot/requirements.txt - pip install nb-cli - pip install nonebot-adapter-onebot - pip install 'nonebot2[fastapi]' + cd MaiBot + pip install uv + uv pip install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt + cd .. echo -e "${GREEN}安装maim_message依赖...${RESET}" cd maim_message - pip install -e . + uv pip install -i https://mirrors.aliyun.com/pypi/simple -e . cd .. - echo -e "${GREEN}部署Nonebot adapter...${RESET}" - cd MaiBot - mkdir nonebot-maibot-adapter - cd nonebot-maibot-adapter - cat > pyproject.toml < README.md - mkdir src - cp -r ../../nonebot-plugin-maibot-adapters/nonebot_plugin_maibot_adapters src/plugins/nonebot_plugin_maibot_adapters + echo -e "${GREEN}部署MaiBot Napcat Adapter...${RESET}" + cd MaiBot-Napcat-Adapter + uv pip install -i https://mirrors.aliyun.com/pypi/simple -r requirements.txt cd .. - cd .. - echo -e "${GREEN}同意协议...${RESET}" @@ -590,13 +568,13 @@ EOF cat > /etc/systemd/system/${SERVICE_NAME_NBADAPTER}.service < /etc/maicore_install.conf diff --git a/src/common/crash_logger.py b/src/common/crash_logger.py new file mode 100644 index 00000000..d1e4fb51 --- /dev/null +++ b/src/common/crash_logger.py @@ -0,0 +1,69 @@ +import sys +import traceback +import logging +from pathlib import Path +from logging.handlers import RotatingFileHandler + + +def setup_crash_logger(): + """设置崩溃日志记录器""" + # 创建logs/crash目录(如果不存在) + crash_log_dir = Path("logs/crash") + crash_log_dir.mkdir(parents=True, exist_ok=True) + + # 创建日志记录器 + crash_logger = logging.getLogger("crash_logger") + crash_logger.setLevel(logging.ERROR) + + # 设置日志格式 + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s\n异常类型: %(exc_info)s\n详细信息:\n%(message)s\n-------------------\n" + ) + + # 创建按大小轮转的文件处理器(最大10MB,保留5个备份) + log_file = crash_log_dir / "crash.log" + file_handler = RotatingFileHandler( + log_file, + maxBytes=10 * 1024 * 1024, # 10MB + backupCount=5, + encoding="utf-8", + ) + file_handler.setFormatter(formatter) + crash_logger.addHandler(file_handler) + + return crash_logger + + +def log_crash(exc_type, exc_value, exc_traceback): + """记录崩溃信息到日志文件""" + if exc_type is None: + return + + # 获取崩溃日志记录器 + crash_logger = logging.getLogger("crash_logger") + + # 获取完整的异常堆栈信息 + stack_trace = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) + + # 记录崩溃信息 + crash_logger.error(stack_trace, exc_info=(exc_type, exc_value, exc_traceback)) + + +def install_crash_handler(): + """安装全局异常处理器""" + # 设置崩溃日志记录器 + setup_crash_logger() + + # 保存原始的异常处理器 + original_hook = sys.excepthook + + def exception_handler(exc_type, exc_value, exc_traceback): + """全局异常处理器""" + # 记录崩溃信息 + log_crash(exc_type, exc_value, exc_traceback) + + # 调用原始的异常处理器 + original_hook(exc_type, exc_value, exc_traceback) + + # 设置全局异常处理器 + sys.excepthook = exception_handler diff --git a/src/common/logger.py b/src/common/logger.py index 9e118622..7365e34a 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -102,10 +102,28 @@ MOOD_STYLE_CONFIG = { "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}"), }, "simple": { - "console_format": ("{time:MM-DD HH:mm} | 心情 | {message}"), + "console_format": ("{time:MM-DD HH:mm} | 心情 | {message}"), "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 心情 | {message}"), }, } +# tool use +TOOL_USE_STYLE_CONFIG = { + "advanced": { + "console_format": ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{extra[module]: <12} | " + "工具使用 | " + "{message}" + ), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 工具使用 | {message}"), + }, + "simple": { + "console_format": ("{time:MM-DD HH:mm} | 工具使用 | {message}"), + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 工具使用 | {message}"), + }, +} + # relationship RELATION_STYLE_CONFIG = { @@ -283,13 +301,15 @@ WILLING_STYLE_CONFIG = { "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 意愿 | {message}"), }, "simple": { - "console_format": ( - "{time:MM-DD HH:mm} | 意愿 | {message}" - ), # noqa: E501 + "console_format": ("{time:MM-DD HH:mm} | 意愿 | {message}"), # noqa: E501 "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | 意愿 | {message}"), }, } +CONFIRM_STYLE_CONFIG = { + "console_format": ("{message}"), # noqa: E501 + "file_format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[module]: <15} | EULA与PRIVACY确认 | {message}"), +} # 根据SIMPLE_OUTPUT选择配置 MEMORY_STYLE_CONFIG = MEMORY_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else MEMORY_STYLE_CONFIG["advanced"] @@ -306,6 +326,7 @@ SUB_HEARTFLOW_STYLE_CONFIG = ( ) # noqa: E501 WILLING_STYLE_CONFIG = WILLING_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else WILLING_STYLE_CONFIG["advanced"] CONFIG_STYLE_CONFIG = CONFIG_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else CONFIG_STYLE_CONFIG["advanced"] +TOOL_USE_STYLE_CONFIG = TOOL_USE_STYLE_CONFIG["simple"] if SIMPLE_OUTPUT else TOOL_USE_STYLE_CONFIG["advanced"] def is_registered_module(record: dict) -> bool: diff --git a/src/common/server.py b/src/common/server.py new file mode 100644 index 00000000..a4998a30 --- /dev/null +++ b/src/common/server.py @@ -0,0 +1,73 @@ +from fastapi import FastAPI, APIRouter +from typing import Optional +from uvicorn import Config, Server as UvicornServer +import os + + +class Server: + def __init__(self, host: Optional[str] = None, port: Optional[int] = None, app_name: str = "MaiMCore"): + self.app = FastAPI(title=app_name) + self._host: str = "127.0.0.1" + self._port: int = 8080 + self._server: Optional[UvicornServer] = None + self.set_address(host, port) + + def register_router(self, router: APIRouter, prefix: str = ""): + """注册路由 + + APIRouter 用于对相关的路由端点进行分组和模块化管理: + 1. 可以将相关的端点组织在一起,便于管理 + 2. 支持添加统一的路由前缀 + 3. 可以为一组路由添加共同的依赖项、标签等 + + 示例: + router = APIRouter() + + @router.get("/users") + def get_users(): + return {"users": [...]} + + @router.post("/users") + def create_user(): + return {"msg": "user created"} + + # 注册路由,添加前缀 "/api/v1" + server.register_router(router, prefix="/api/v1") + """ + self.app.include_router(router, prefix=prefix) + + def set_address(self, host: Optional[str] = None, port: Optional[int] = None): + """设置服务器地址和端口""" + if host: + self._host = host + if port: + self._port = port + + async def run(self): + """启动服务器""" + config = Config(app=self.app, host=self._host, port=self._port) + self._server = UvicornServer(config=config) + try: + await self._server.serve() + except KeyboardInterrupt: + await self.shutdown() + raise + except Exception as e: + await self.shutdown() + raise RuntimeError(f"服务器运行错误: {str(e)}") from e + finally: + await self.shutdown() + + async def shutdown(self): + """安全关闭服务器""" + if self._server: + self._server.should_exit = True + await self._server.shutdown() + self._server = None + + def get_app(self) -> FastAPI: + """获取 FastAPI 实例""" + return self.app + + +global_server = Server(host=os.environ["HOST"], port=int(os.environ["PORT"])) diff --git a/src/do_tool/tool_can_use/README.md b/src/do_tool/tool_can_use/README.md new file mode 100644 index 00000000..15c77188 --- /dev/null +++ b/src/do_tool/tool_can_use/README.md @@ -0,0 +1,102 @@ +# 工具系统使用指南 + +## 概述 + +`tool_can_use` 是一个插件式工具系统,允许轻松扩展和注册新工具。每个工具作为独立的文件存在于该目录下,系统会自动发现和注册这些工具。 + +## 工具结构 + +每个工具应该继承 `BaseTool` 基类并实现必要的属性和方法: + +```python +from src.do_tool.tool_can_use.base_tool import BaseTool, register_tool + +class MyNewTool(BaseTool): + # 工具名称,必须唯一 + name = "my_new_tool" + + # 工具描述,告诉LLM这个工具的用途 + description = "这是一个新工具,用于..." + + # 工具参数定义,遵循JSONSchema格式 + parameters = { + "type": "object", + "properties": { + "param1": { + "type": "string", + "description": "参数1的描述" + }, + "param2": { + "type": "integer", + "description": "参数2的描述" + } + }, + "required": ["param1"] # 必需的参数列表 + } + + async def execute(self, function_args, message_txt=""): + """执行工具逻辑 + + Args: + function_args: 工具调用参数 + message_txt: 原始消息文本 + + Returns: + Dict: 包含执行结果的字典,必须包含name和content字段 + """ + # 实现工具逻辑 + result = f"工具执行结果: {function_args.get('param1')}" + + return { + "name": self.name, + "content": result + } + +# 注册工具 +register_tool(MyNewTool) +``` + +## 自动注册机制 + +工具系统通过以下步骤自动注册工具: + +1. 在`__init__.py`中,`discover_tools()`函数会自动遍历当前目录中的所有Python文件 +2. 对于每个文件,系统会寻找继承自`BaseTool`的类 +3. 这些类会被自动注册到工具注册表中 + +只要确保在每个工具文件的末尾调用`register_tool(YourToolClass)`,工具就会被自动注册。 + +## 添加新工具步骤 + +1. 在`tool_can_use`目录下创建新的Python文件(如`my_new_tool.py`) +2. 导入`BaseTool`和`register_tool` +3. 创建继承自`BaseTool`的工具类 +4. 实现必要的属性(`name`, `description`, `parameters`) +5. 实现`execute`方法 +6. 使用`register_tool`注册工具 + +## 与ToolUser整合 + +`ToolUser`类已经更新为使用这个新的工具系统,它会: + +1. 自动获取所有已注册工具的定义 +2. 基于工具名称找到对应的工具实例 +3. 调用工具的`execute`方法 + +## 使用示例 + +```python +from src.do_tool.tool_use import ToolUser + +# 创建工具用户 +tool_user = ToolUser() + +# 使用工具 +result = await tool_user.use_tool(message_txt="查询关于Python的知识", sender_name="用户", chat_stream=chat_stream) + +# 处理结果 +if result["used_tools"]: + print("工具使用结果:", result["collected_info"]) +else: + print("未使用工具") +``` \ No newline at end of file diff --git a/src/do_tool/tool_can_use/__init__.py b/src/do_tool/tool_can_use/__init__.py new file mode 100644 index 00000000..a7ea17ab --- /dev/null +++ b/src/do_tool/tool_can_use/__init__.py @@ -0,0 +1,20 @@ +from src.do_tool.tool_can_use.base_tool import ( + BaseTool, + register_tool, + discover_tools, + get_all_tool_definitions, + get_tool_instance, + TOOL_REGISTRY, +) + +__all__ = [ + "BaseTool", + "register_tool", + "discover_tools", + "get_all_tool_definitions", + "get_tool_instance", + "TOOL_REGISTRY", +] + +# 自动发现并注册工具 +discover_tools() diff --git a/src/do_tool/tool_can_use/base_tool.py b/src/do_tool/tool_can_use/base_tool.py new file mode 100644 index 00000000..b1edf805 --- /dev/null +++ b/src/do_tool/tool_can_use/base_tool.py @@ -0,0 +1,113 @@ +from typing import Dict, List, Any, Optional, Type +import inspect +import importlib +import pkgutil +import os +from src.common.logger import get_module_logger + +logger = get_module_logger("base_tool") + +# 工具注册表 +TOOL_REGISTRY = {} + + +class BaseTool: + """所有工具的基类""" + + # 工具名称,子类必须重写 + name = None + # 工具描述,子类必须重写 + description = None + # 工具参数定义,子类必须重写 + parameters = None + + @classmethod + def get_tool_definition(cls) -> Dict[str, Any]: + """获取工具定义,用于LLM工具调用 + + Returns: + Dict: 工具定义字典 + """ + if not cls.name or not cls.description or not cls.parameters: + raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性") + + return { + "type": "function", + "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters}, + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行工具函数 + + Args: + function_args: 工具调用参数 + message_txt: 原始消息文本 + + Returns: + Dict: 工具执行结果 + """ + raise NotImplementedError("子类必须实现execute方法") + + +def register_tool(tool_class: Type[BaseTool]): + """注册工具到全局注册表 + + Args: + tool_class: 工具类 + """ + if not issubclass(tool_class, BaseTool): + raise TypeError(f"{tool_class.__name__} 不是 BaseTool 的子类") + + tool_name = tool_class.name + if not tool_name: + raise ValueError(f"工具类 {tool_class.__name__} 没有定义 name 属性") + + TOOL_REGISTRY[tool_name] = tool_class + logger.info(f"已注册工具: {tool_name}") + + +def discover_tools(): + """自动发现并注册tool_can_use目录下的所有工具""" + # 获取当前目录路径 + current_dir = os.path.dirname(os.path.abspath(__file__)) + package_name = os.path.basename(current_dir) + + # 遍历包中的所有模块 + for _, module_name, _ in pkgutil.iter_modules([current_dir]): + # 跳过当前模块和__pycache__ + if module_name == "base_tool" or module_name.startswith("__"): + continue + + # 导入模块 + module = importlib.import_module(f"src.do_tool.{package_name}.{module_name}") + + # 查找模块中的工具类 + for _, obj in inspect.getmembers(module): + if inspect.isclass(obj) and issubclass(obj, BaseTool) and obj != BaseTool: + register_tool(obj) + + logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具") + + +def get_all_tool_definitions() -> List[Dict[str, Any]]: + """获取所有已注册工具的定义 + + Returns: + List[Dict]: 工具定义列表 + """ + return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()] + + +def get_tool_instance(tool_name: str) -> Optional[BaseTool]: + """获取指定名称的工具实例 + + Args: + tool_name: 工具名称 + + Returns: + Optional[BaseTool]: 工具实例,如果找不到则返回None + """ + tool_class = TOOL_REGISTRY.get(tool_name) + if not tool_class: + return None + return tool_class() diff --git a/src/do_tool/tool_can_use/change_mood.py b/src/do_tool/tool_can_use/change_mood.py new file mode 100644 index 00000000..53410068 --- /dev/null +++ b/src/do_tool/tool_can_use/change_mood.py @@ -0,0 +1,57 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.plugins.config.config import global_config +from src.common.logger import get_module_logger +from src.plugins.moods.moods import MoodManager +from src.plugins.chat_module.think_flow_chat.think_flow_generator import ResponseGenerator + +from typing import Dict, Any + +logger = get_module_logger("change_mood_tool") + + +class ChangeMoodTool(BaseTool): + """改变心情的工具""" + + name = "change_mood" + description = "根据收到的内容和自身回复的内容,改变心情,当你回复了别人的消息,你可以使用这个工具" + parameters = { + "type": "object", + "properties": { + "text": {"type": "string", "description": "引起你改变心情的文本"}, + "response_set": {"type": "list", "description": "你对文本的回复"}, + }, + "required": ["text", "response_set"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str) -> Dict[str, Any]: + """执行心情改变 + + Args: + function_args: 工具参数 + message_processed_plain_text: 原始消息文本 + response_set: 原始消息文本 + + Returns: + Dict: 工具执行结果 + """ + try: + response_set = function_args.get("response_set") + message_processed_plain_text = function_args.get("text") + + mood_manager = MoodManager.get_instance() + gpt = ResponseGenerator() + + if response_set is None: + response_set = ["你还没有回复"] + + ori_response = ",".join(response_set) + _stance, emotion = await gpt._get_emotion_tags(ori_response, message_processed_plain_text) + mood_manager.update_mood_from_emotion(emotion, global_config.mood_intensity_factor) + return {"name": "change_mood", "content": f"你的心情刚刚变化了,现在的心情是: {emotion}"} + except Exception as e: + logger.error(f"心情改变工具执行失败: {str(e)}") + return {"name": "change_mood", "content": f"心情改变失败: {str(e)}"} + + +# 注册工具 +# register_tool(ChangeMoodTool) diff --git a/src/do_tool/tool_can_use/change_relationship.py b/src/do_tool/tool_can_use/change_relationship.py new file mode 100644 index 00000000..9aa08485 --- /dev/null +++ b/src/do_tool/tool_can_use/change_relationship.py @@ -0,0 +1,44 @@ +# from src.plugins.person_info.relationship_manager import relationship_manager +from src.common.logger import get_module_logger +from src.do_tool.tool_can_use.base_tool import BaseTool +# from src.plugins.chat_module.think_flow_chat.think_flow_generator import ResponseGenerator + + +logger = get_module_logger("relationship_tool") + + +class RelationshipTool(BaseTool): + name = "change_relationship" + description = "根据收到的文本和回复内容,修改与特定用户的关系值,当你回复了别人的消息,你可以使用这个工具" + parameters = { + "type": "object", + "properties": { + "text": {"type": "string", "description": "收到的文本"}, + "changed_value": {"type": "number", "description": "变更值"}, + "reason": {"type": "string", "description": "变更原因"}, + }, + "required": ["text", "changed_value", "reason"], + } + + async def execute(self, args: dict, message_txt: str) -> dict: + """执行工具功能 + + Args: + args: 包含工具参数的字典 + text: 原始消息文本 + changed_value: 变更值 + reason: 变更原因 + + Returns: + dict: 包含执行结果的字典 + """ + try: + text = args.get("text") + changed_value = args.get("changed_value") + reason = args.get("reason") + + return {"content": f"因为你刚刚因为{reason},所以你和发[{text}]这条消息的人的关系值变化为{changed_value}"} + + except Exception as e: + logger.error(f"修改关系值时发生错误: {str(e)}") + return {"content": f"修改关系值失败: {str(e)}"} diff --git a/src/do_tool/tool_can_use/compare_numbers_tool.py b/src/do_tool/tool_can_use/compare_numbers_tool.py new file mode 100644 index 00000000..48cee515 --- /dev/null +++ b/src/do_tool/tool_can_use/compare_numbers_tool.py @@ -0,0 +1,50 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.common.logger import get_module_logger +from typing import Dict, Any + +logger = get_module_logger("compare_numbers_tool") + + +class CompareNumbersTool(BaseTool): + """比较两个数大小的工具""" + + name = "compare_numbers" + description = "比较两个数的大小,返回较大的数" + parameters = { + "type": "object", + "properties": { + "num1": {"type": "number", "description": "第一个数字"}, + "num2": {"type": "number", "description": "第二个数字"}, + }, + "required": ["num1", "num2"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行比较两个数的大小 + + Args: + function_args: 工具参数 + message_txt: 原始消息文本 + + Returns: + Dict: 工具执行结果 + """ + try: + num1 = function_args.get("num1") + num2 = function_args.get("num2") + + if num1 > num2: + result = f"{num1} 大于 {num2}" + elif num1 < num2: + result = f"{num1} 小于 {num2}" + else: + result = f"{num1} 等于 {num2}" + + return {"name": self.name, "content": result} + except Exception as e: + logger.error(f"比较数字失败: {str(e)}") + return {"name": self.name, "content": f"比较数字失败: {str(e)}"} + + +# 注册工具 +# register_tool(CompareNumbersTool) diff --git a/src/do_tool/tool_can_use/get_current_task.py b/src/do_tool/tool_can_use/get_current_task.py new file mode 100644 index 00000000..d5660f6a --- /dev/null +++ b/src/do_tool/tool_can_use/get_current_task.py @@ -0,0 +1,59 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.plugins.schedule.schedule_generator import bot_schedule +from src.common.logger import get_module_logger +from typing import Dict, Any +from datetime import datetime + +logger = get_module_logger("get_current_task_tool") + + +class GetCurrentTaskTool(BaseTool): + """获取当前正在做的事情/最近的任务工具""" + + name = "get_schedule" + description = "获取当前正在做的事情,或者某个时间点/时间段的日程信息" + parameters = { + "type": "object", + "properties": { + "start_time": {"type": "string", "description": "开始时间,格式为'HH:MM',填写current则获取当前任务"}, + "end_time": {"type": "string", "description": "结束时间,格式为'HH:MM',填写current则获取当前任务"}, + }, + "required": ["start_time", "end_time"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行获取当前任务或指定时间段的日程信息 + + Args: + function_args: 工具参数 + message_txt: 原始消息文本,此工具不使用 + + Returns: + Dict: 工具执行结果 + """ + start_time = function_args.get("start_time") + end_time = function_args.get("end_time") + + # 如果 start_time 或 end_time 为 "current",则获取当前任务 + if start_time == "current" or end_time == "current": + current_task = bot_schedule.get_current_num_task(num=1, time_info=True) + current_time = datetime.now().strftime("%H:%M:%S") + current_date = datetime.now().strftime("%Y-%m-%d") + if current_task: + task_info = f"{current_date} {current_time},你在{current_task}" + else: + task_info = f"{current_time} {current_date},没在做任何事情" + # 如果提供了时间范围,则获取该时间段的日程信息 + elif start_time and end_time: + tasks = await bot_schedule.get_task_from_time_to_time(start_time, end_time) + if tasks: + task_list = [] + for task in tasks: + task_time = task[0].strftime("%H:%M") + task_content = task[1] + task_list.append(f"{task_time}时,{task_content}") + task_info = "\n".join(task_list) + else: + task_info = f"在 {start_time} 到 {end_time} 之间没有找到日程信息" + + return {"name": "get_current_task", "content": f"日程信息: {task_info}"} diff --git a/src/do_tool/tool_can_use/get_knowledge.py b/src/do_tool/tool_can_use/get_knowledge.py new file mode 100644 index 00000000..b78c0775 --- /dev/null +++ b/src/do_tool/tool_can_use/get_knowledge.py @@ -0,0 +1,135 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.plugins.chat.utils import get_embedding +from src.common.database import db +from src.common.logger import get_module_logger +from typing import Dict, Any, Union + +logger = get_module_logger("get_knowledge_tool") + + +class SearchKnowledgeTool(BaseTool): + """从知识库中搜索相关信息的工具""" + + name = "search_knowledge" + description = "从知识库中搜索相关信息" + parameters = { + "type": "object", + "properties": { + "query": {"type": "string", "description": "搜索查询关键词"}, + "threshold": {"type": "number", "description": "相似度阈值,0.0到1.0之间"}, + }, + "required": ["query"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行知识库搜索 + + Args: + function_args: 工具参数 + message_txt: 原始消息文本 + + Returns: + Dict: 工具执行结果 + """ + try: + query = function_args.get("query", message_txt) + threshold = function_args.get("threshold", 0.4) + + # 调用知识库搜索 + embedding = await get_embedding(query, request_type="info_retrieval") + if embedding: + knowledge_info = self.get_info_from_db(embedding, limit=3, threshold=threshold) + if knowledge_info: + content = f"你知道这些知识: {knowledge_info}" + else: + content = f"你不太了解有关{query}的知识" + return {"name": "search_knowledge", "content": content} + return {"name": "search_knowledge", "content": f"无法获取关于'{query}'的嵌入向量"} + except Exception as e: + logger.error(f"知识库搜索工具执行失败: {str(e)}") + return {"name": "search_knowledge", "content": f"知识库搜索失败: {str(e)}"} + + def get_info_from_db( + self, query_embedding: list, limit: int = 1, threshold: float = 0.5, return_raw: bool = False + ) -> Union[str, list]: + """从数据库中获取相关信息 + + Args: + query_embedding: 查询的嵌入向量 + limit: 最大返回结果数 + threshold: 相似度阈值 + return_raw: 是否返回原始结果 + + Returns: + Union[str, list]: 格式化的信息字符串或原始结果列表 + """ + if not query_embedding: + return "" if not return_raw else [] + + # 使用余弦相似度计算 + pipeline = [ + { + "$addFields": { + "dotProduct": { + "$reduce": { + "input": {"$range": [0, {"$size": "$embedding"}]}, + "initialValue": 0, + "in": { + "$add": [ + "$$value", + { + "$multiply": [ + {"$arrayElemAt": ["$embedding", "$$this"]}, + {"$arrayElemAt": [query_embedding, "$$this"]}, + ] + }, + ] + }, + } + }, + "magnitude1": { + "$sqrt": { + "$reduce": { + "input": "$embedding", + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + "magnitude2": { + "$sqrt": { + "$reduce": { + "input": query_embedding, + "initialValue": 0, + "in": {"$add": ["$$value", {"$multiply": ["$$this", "$$this"]}]}, + } + } + }, + } + }, + {"$addFields": {"similarity": {"$divide": ["$dotProduct", {"$multiply": ["$magnitude1", "$magnitude2"]}]}}}, + { + "$match": { + "similarity": {"$gte": threshold} # 只保留相似度大于等于阈值的结果 + } + }, + {"$sort": {"similarity": -1}}, + {"$limit": limit}, + {"$project": {"content": 1, "similarity": 1}}, + ] + + results = list(db.knowledges.aggregate(pipeline)) + logger.debug(f"知识库查询结果数量: {len(results)}") + + if not results: + return "" if not return_raw else [] + + if return_raw: + return results + else: + # 返回所有找到的内容,用换行分隔 + return "\n".join(str(result["content"]) for result in results) + + +# 注册工具 +# register_tool(SearchKnowledgeTool) diff --git a/src/do_tool/tool_can_use/get_memory.py b/src/do_tool/tool_can_use/get_memory.py new file mode 100644 index 00000000..6a3c1c39 --- /dev/null +++ b/src/do_tool/tool_can_use/get_memory.py @@ -0,0 +1,59 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.plugins.memory_system.Hippocampus import HippocampusManager +from src.common.logger import get_module_logger +from typing import Dict, Any + +logger = get_module_logger("mid_chat_mem_tool") + + +class GetMemoryTool(BaseTool): + """从记忆系统中获取相关记忆的工具""" + + name = "mid_chat_mem" + description = "从记忆系统中获取相关记忆" + parameters = { + "type": "object", + "properties": { + "text": {"type": "string", "description": "要查询的相关文本"}, + "max_memory_num": {"type": "integer", "description": "最大返回记忆数量"}, + }, + "required": ["text"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行记忆获取 + + Args: + function_args: 工具参数 + message_txt: 原始消息文本 + + Returns: + Dict: 工具执行结果 + """ + try: + text = function_args.get("text", message_txt) + max_memory_num = function_args.get("max_memory_num", 2) + + # 调用记忆系统 + related_memory = await HippocampusManager.get_instance().get_memory_from_text( + text=text, max_memory_num=max_memory_num, max_memory_length=2, max_depth=3, fast_retrieval=False + ) + + memory_info = "" + if related_memory: + for memory in related_memory: + memory_info += memory[1] + "\n" + + if memory_info: + content = f"你记得这些事情: {memory_info}" + else: + content = f"你不太记得有关{text}的记忆,你对此不太了解" + + return {"name": "mid_chat_mem", "content": content} + except Exception as e: + logger.error(f"记忆获取工具执行失败: {str(e)}") + return {"name": "mid_chat_mem", "content": f"记忆获取失败: {str(e)}"} + + +# 注册工具 +# register_tool(GetMemoryTool) diff --git a/src/do_tool/tool_can_use/get_time_date.py b/src/do_tool/tool_can_use/get_time_date.py new file mode 100644 index 00000000..c3c9c837 --- /dev/null +++ b/src/do_tool/tool_can_use/get_time_date.py @@ -0,0 +1,38 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.common.logger import get_module_logger +from typing import Dict, Any +from datetime import datetime + +logger = get_module_logger("get_time_date") + + +class GetCurrentDateTimeTool(BaseTool): + """获取当前时间、日期、年份和星期的工具""" + + name = "get_current_date_time" + description = "当有人询问或者涉及到具体时间或者日期的时候,必须使用这个工具" + parameters = { + "type": "object", + "properties": {}, + "required": [], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行获取当前时间、日期、年份和星期 + + Args: + function_args: 工具参数(此工具不使用) + message_txt: 原始消息文本(此工具不使用) + + Returns: + Dict: 工具执行结果 + """ + current_time = datetime.now().strftime("%H:%M:%S") + current_date = datetime.now().strftime("%Y-%m-%d") + current_year = datetime.now().strftime("%Y") + current_weekday = datetime.now().strftime("%A") + + return { + "name": "get_current_date_time", + "content": f"当前时间: {current_time}, 日期: {current_date}, 年份: {current_year}, 星期: {current_weekday}", + } diff --git a/src/do_tool/tool_can_use/mid_chat_mem.py b/src/do_tool/tool_can_use/mid_chat_mem.py new file mode 100644 index 00000000..26d26704 --- /dev/null +++ b/src/do_tool/tool_can_use/mid_chat_mem.py @@ -0,0 +1,40 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.common.logger import get_module_logger +from typing import Dict, Any + +logger = get_module_logger("get_mid_memory_tool") + + +class GetMidMemoryTool(BaseTool): + """从记忆系统中获取相关记忆的工具""" + + name = "mid_chat_mem" + description = "之前的聊天内容中获取具体信息,当最新消息提到,或者你需要回复的消息中提到,你可以使用这个工具" + parameters = { + "type": "object", + "properties": { + "id": {"type": "integer", "description": "要查询的聊天记录id"}, + }, + "required": ["id"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str = "") -> Dict[str, Any]: + """执行记忆获取 + + Args: + function_args: 工具参数 + message_txt: 原始消息文本 + + Returns: + Dict: 工具执行结果 + """ + try: + id = function_args.get("id") + return {"name": "mid_chat_mem", "content": str(id)} + except Exception as e: + logger.error(f"聊天记录获取工具执行失败: {str(e)}") + return {"name": "mid_chat_mem", "content": f"聊天记录获取失败: {str(e)}"} + + +# 注册工具 +# register_tool(GetMemoryTool) diff --git a/src/do_tool/tool_can_use/send_emoji.py b/src/do_tool/tool_can_use/send_emoji.py new file mode 100644 index 00000000..9cd48f0e --- /dev/null +++ b/src/do_tool/tool_can_use/send_emoji.py @@ -0,0 +1,25 @@ +from src.do_tool.tool_can_use.base_tool import BaseTool +from src.common.logger import get_module_logger + +from typing import Dict, Any + +logger = get_module_logger("send_emoji_tool") + + +class SendEmojiTool(BaseTool): + """发送表情包的工具""" + + name = "send_emoji" + description = "当你觉得需要表达情感,或者帮助表达,可以使用这个工具发送表情包" + parameters = { + "type": "object", + "properties": {"text": {"type": "string", "description": "要发送的表情包描述"}}, + "required": ["text"], + } + + async def execute(self, function_args: Dict[str, Any], message_txt: str) -> Dict[str, Any]: + text = function_args.get("text", message_txt) + return { + "name": "send_emoji", + "content": text, + } diff --git a/src/do_tool/tool_use.py b/src/do_tool/tool_use.py new file mode 100644 index 00000000..b14927be --- /dev/null +++ b/src/do_tool/tool_use.py @@ -0,0 +1,193 @@ +from src.plugins.models.utils_model import LLM_request +from src.plugins.config.config import global_config +from src.plugins.chat.chat_stream import ChatStream +from src.common.database import db +import time +import json +from src.common.logger import get_module_logger, TOOL_USE_STYLE_CONFIG, LogConfig +from src.do_tool.tool_can_use import get_all_tool_definitions, get_tool_instance +from src.heart_flow.sub_heartflow import SubHeartflow + +tool_use_config = LogConfig( + # 使用消息发送专用样式 + console_format=TOOL_USE_STYLE_CONFIG["console_format"], + file_format=TOOL_USE_STYLE_CONFIG["file_format"], +) +logger = get_module_logger("tool_use", config=tool_use_config) + + +class ToolUser: + def __init__(self): + self.llm_model_tool = LLM_request( + model=global_config.llm_tool_use, temperature=0.2, max_tokens=1000, request_type="tool_use" + ) + + async def _build_tool_prompt( + self, message_txt: str, sender_name: str, chat_stream: ChatStream, subheartflow: SubHeartflow = None + ): + """构建工具使用的提示词 + + Args: + message_txt: 用户消息文本 + sender_name: 发送者名称 + chat_stream: 聊天流对象 + + Returns: + str: 构建好的提示词 + """ + if subheartflow: + mid_memory_info = subheartflow.observations[0].mid_memory_info + # print(f"intol111111111111111111111111111111111222222222222mid_memory_info:{mid_memory_info}") + else: + mid_memory_info = "" + + new_messages = list( + db.messages.find({"chat_id": chat_stream.stream_id, "time": {"$gt": time.time()}}).sort("time", 1).limit(15) + ) + new_messages_str = "" + for msg in new_messages: + if "detailed_plain_text" in msg: + new_messages_str += f"{msg['detailed_plain_text']}" + + # 这些信息应该从调用者传入,而不是从self获取 + bot_name = global_config.BOT_NICKNAME + prompt = "" + prompt += mid_memory_info + prompt += "你正在思考如何回复群里的消息。\n" + prompt += f"你注意到{sender_name}刚刚说:{message_txt}\n" + prompt += f"注意你就是{bot_name},{bot_name}指的就是你。" + + prompt += "你现在需要对群里的聊天内容进行回复,现在选择工具来对消息和你的回复进行处理,你是否需要额外的信息,比如回忆或者搜寻已有的知识,改变关系和情感,或者了解你现在正在做什么。" + return prompt + + def _define_tools(self): + """获取所有已注册工具的定义 + + Returns: + list: 工具定义列表 + """ + return get_all_tool_definitions() + + async def _execute_tool_call(self, tool_call, message_txt: str): + """执行特定的工具调用 + + Args: + tool_call: 工具调用对象 + message_txt: 原始消息文本 + + Returns: + dict: 工具调用结果 + """ + try: + function_name = tool_call["function"]["name"] + function_args = json.loads(tool_call["function"]["arguments"]) + + # 获取对应工具实例 + tool_instance = get_tool_instance(function_name) + if not tool_instance: + logger.warning(f"未知工具名称: {function_name}") + return None + + # 执行工具 + result = await tool_instance.execute(function_args, message_txt) + if result: + # 直接使用 function_name 作为 tool_type + tool_type = function_name + + return { + "tool_call_id": tool_call["id"], + "role": "tool", + "name": function_name, + "type": tool_type, + "content": result["content"], + } + return None + except Exception as e: + logger.error(f"执行工具调用时发生错误: {str(e)}") + return None + + async def use_tool( + self, message_txt: str, sender_name: str, chat_stream: ChatStream, subheartflow: SubHeartflow = None + ): + """使用工具辅助思考,判断是否需要额外信息 + + Args: + message_txt: 用户消息文本 + sender_name: 发送者名称 + chat_stream: 聊天流对象 + + Returns: + dict: 工具使用结果,包含结构化的信息 + """ + try: + # 构建提示词 + prompt = await self._build_tool_prompt(message_txt, sender_name, chat_stream, subheartflow) + + # 定义可用工具 + tools = self._define_tools() + logger.trace(f"工具定义: {tools}") + + # 使用llm_model_tool发送带工具定义的请求 + payload = { + "model": self.llm_model_tool.model_name, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": global_config.max_response_length, + "tools": tools, + "temperature": 0.2, + } + + logger.trace(f"发送工具调用请求,模型: {self.llm_model_tool.model_name}") + # 发送请求获取模型是否需要调用工具 + response = await self.llm_model_tool._execute_request( + endpoint="/chat/completions", payload=payload, prompt=prompt + ) + + # 根据返回值数量判断是否有工具调用 + if len(response) == 3: + content, reasoning_content, tool_calls = response + # logger.info(f"工具思考: {tool_calls}") + # logger.debug(f"工具思考: {content}") + + # 检查响应中工具调用是否有效 + if not tool_calls: + logger.debug("模型返回了空的tool_calls列表") + return {"used_tools": False} + + tool_calls_str = "" + for tool_call in tool_calls: + tool_calls_str += f"{tool_call['function']['name']}\n" + logger.info(f"根据:\n{prompt}\n模型请求调用{len(tool_calls)}个工具: {tool_calls_str}") + tool_results = [] + structured_info = {} # 动态生成键 + + # 执行所有工具调用 + for tool_call in tool_calls: + result = await self._execute_tool_call(tool_call, message_txt) + if result: + tool_results.append(result) + # 使用工具名称作为键 + tool_name = result["name"] + if tool_name not in structured_info: + structured_info[tool_name] = [] + structured_info[tool_name].append({"name": result["name"], "content": result["content"]}) + + # 如果有工具结果,返回结构化的信息 + if structured_info: + logger.info(f"工具调用收集到结构化信息: {json.dumps(structured_info, ensure_ascii=False)}") + return {"used_tools": True, "structured_info": structured_info} + else: + # 没有工具调用 + content, reasoning_content = response + logger.debug("模型没有请求调用任何工具") + + # 如果没有工具调用或处理失败,直接返回原始思考 + return { + "used_tools": False, + } + + except Exception as e: + logger.error(f"工具调用过程中出错: {str(e)}") + return { + "used_tools": False, + "error": str(e), + } diff --git a/src/gui/logger_gui.py b/src/gui/logger_gui.py index 9488446c..ad6edafb 100644 --- a/src/gui/logger_gui.py +++ b/src/gui/logger_gui.py @@ -24,10 +24,10 @@ # # 标记GUI是否运行中 # self.is_running = True - + # # 程序关闭时的清理操作 # self.protocol("WM_DELETE_WINDOW", self._on_closing) - + # # 初始化进程、日志队列、日志数据等变量 # self.process = None # self.log_queue = queue.Queue() @@ -236,7 +236,7 @@ # while not self.log_queue.empty(): # line = self.log_queue.get() # self.process_log_line(line) - + # # 仅在GUI仍在运行时继续处理队列 # if self.is_running: # self.after(100, self.process_log_queue) @@ -245,11 +245,11 @@ # """解析单行日志并更新日志数据和筛选器""" # match = re.match( # r"""^ -# (?:(?P