From 44d86c88477d0ffe7c8dbff7daf04d335e29bdfc Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 26 Jul 2025 18:37:29 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E5=B0=9D=E8=AF=95=E6=95=B4=E5=90=88?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=92=8C=E6=8F=92=E4=BB=B6=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/hello_world_plugin/_manifest.json | 53 ------ plugins/hello_world_plugin/plugin.py | 170 ------------------ src/chat/replyer/default_generator.py | 2 +- src/plugin_system/__init__.py | 4 + src/plugin_system/apis/tool_api.py | 25 +++ src/plugin_system/base/__init__.py | 4 + src/plugin_system/base/base_tool.py | 63 +++++++ src/plugin_system/base/component_types.py | 13 ++ src/plugin_system/core/__init__.py | 2 + src/plugin_system/core/component_registry.py | 48 ++++- .../core}/tool_executor.py | 4 +- src/{tools => plugin_system/core}/tool_use.py | 7 +- src/tools/not_using/get_knowledge.py | 133 -------------- src/tools/not_using/lpmm_get_knowledge.py | 60 ------- src/tools/tool_can_use/__init__.py | 20 --- src/tools/tool_can_use/base_tool.py | 115 ------------ .../tool_can_use/compare_numbers_tool.py | 45 ----- src/tools/tool_can_use/rename_person_tool.py | 103 ----------- 18 files changed, 165 insertions(+), 706 deletions(-) delete mode 100644 plugins/hello_world_plugin/_manifest.json delete mode 100644 plugins/hello_world_plugin/plugin.py create mode 100644 src/plugin_system/apis/tool_api.py create mode 100644 src/plugin_system/base/base_tool.py rename src/{tools => plugin_system/core}/tool_executor.py (99%) rename src/{tools => plugin_system/core}/tool_use.py (86%) delete mode 100644 src/tools/not_using/get_knowledge.py delete mode 100644 src/tools/not_using/lpmm_get_knowledge.py delete mode 100644 src/tools/tool_can_use/__init__.py delete mode 100644 src/tools/tool_can_use/base_tool.py delete mode 100644 src/tools/tool_can_use/compare_numbers_tool.py delete mode 100644 src/tools/tool_can_use/rename_person_tool.py diff --git a/plugins/hello_world_plugin/_manifest.json b/plugins/hello_world_plugin/_manifest.json deleted file mode 100644 index b1a4c4eb8..000000000 --- a/plugins/hello_world_plugin/_manifest.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "manifest_version": 1, - "name": "Hello World 示例插件 (Hello World Plugin)", - "version": "1.0.0", - "description": "我的第一个MaiCore插件,包含问候功能和时间查询等基础示例", - "author": { - "name": "MaiBot开发团队", - "url": "https://github.com/MaiM-with-u" - }, - "license": "GPL-v3.0-or-later", - - "host_application": { - "min_version": "0.8.0" - }, - "homepage_url": "https://github.com/MaiM-with-u/maibot", - "repository_url": "https://github.com/MaiM-with-u/maibot", - "keywords": ["demo", "example", "hello", "greeting", "tutorial"], - "categories": ["Examples", "Tutorial"], - - "default_locale": "zh-CN", - "locales_path": "_locales", - - "plugin_info": { - "is_built_in": false, - "plugin_type": "example", - "components": [ - { - "type": "action", - "name": "hello_greeting", - "description": "向用户发送问候消息" - }, - { - "type": "action", - "name": "bye_greeting", - "description": "向用户发送告别消息", - "activation_modes": ["keyword"], - "keywords": ["再见", "bye", "88", "拜拜"] - }, - { - "type": "command", - "name": "time", - "description": "查询当前时间", - "pattern": "/time" - } - ], - "features": [ - "问候和告别功能", - "时间查询命令", - "配置文件示例", - "新手教程代码" - ] - } -} \ No newline at end of file diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py deleted file mode 100644 index 8ede9616a..000000000 --- a/plugins/hello_world_plugin/plugin.py +++ /dev/null @@ -1,170 +0,0 @@ -from typing import List, Tuple, Type -from src.plugin_system import ( - BasePlugin, - register_plugin, - BaseAction, - BaseCommand, - ComponentInfo, - ActionActivationType, - ConfigField, - BaseEventHandler, - EventType, - MaiMessages, -) - - -# ===== Action组件 ===== -class HelloAction(BaseAction): - """问候Action - 简单的问候动作""" - - # === 基本信息(必须填写)=== - action_name = "hello_greeting" - action_description = "向用户发送问候消息" - activation_type = ActionActivationType.ALWAYS # 始终激活 - - # === 功能描述(必须填写)=== - action_parameters = {"greeting_message": "要发送的问候消息"} - action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"] - associated_types = ["text"] - - async def execute(self) -> Tuple[bool, str]: - """执行问候动作 - 这是核心功能""" - # 发送问候消息 - greeting_message = self.action_data.get("greeting_message", "") - base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊") - message = base_message + greeting_message - await self.send_text(message) - - return True, "发送了问候消息" - - -class ByeAction(BaseAction): - """告别Action - 只在用户说再见时激活""" - - action_name = "bye_greeting" - action_description = "向用户发送告别消息" - - # 使用关键词激活 - activation_type = ActionActivationType.KEYWORD - - # 关键词设置 - activation_keywords = ["再见", "bye", "88", "拜拜"] - keyword_case_sensitive = False - - action_parameters = {"bye_message": "要发送的告别消息"} - action_require = [ - "用户要告别时使用", - "当有人要离开时使用", - "当有人和你说再见时使用", - ] - associated_types = ["text"] - - async def execute(self) -> Tuple[bool, str]: - bye_message = self.action_data.get("bye_message", "") - - message = f"再见!期待下次聊天!👋{bye_message}" - await self.send_text(message) - return True, "发送了告别消息" - - -class TimeCommand(BaseCommand): - """时间查询Command - 响应/time命令""" - - command_name = "time" - command_description = "查询当前时间" - - # === 命令设置(必须填写)=== - command_pattern = r"^/time$" # 精确匹配 "/time" 命令 - - async def execute(self) -> Tuple[bool, str, bool]: - """执行时间查询""" - import datetime - - # 获取当前时间 - time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore - now = datetime.datetime.now() - time_str = now.strftime(time_format) - - # 发送时间信息 - message = f"⏰ 当前时间:{time_str}" - await self.send_text(message) - - return True, f"显示了当前时间: {time_str}", True - - -class PrintMessage(BaseEventHandler): - """打印消息事件处理器 - 处理打印消息事件""" - - event_type = EventType.ON_MESSAGE - handler_name = "print_message_handler" - handler_description = "打印接收到的消息" - - async def execute(self, message: MaiMessages) -> Tuple[bool, bool, str | None]: - """执行打印消息事件处理""" - # 打印接收到的消息 - if self.get_config("print_message.enabled", False): - print(f"接收到消息: {message.raw_message}") - return True, True, "消息已打印" - - -# ===== 插件注册 ===== - - -@register_plugin -class HelloWorldPlugin(BasePlugin): - """Hello World插件 - 你的第一个MaiCore插件""" - - # 插件基本信息 - plugin_name: str = "hello_world_plugin" # 内部标识符 - enable_plugin: bool = True - dependencies: List[str] = [] # 插件依赖列表 - python_dependencies: List[str] = [] # Python包依赖列表 - config_file_name: str = "config.toml" # 配置文件名 - - # 配置节描述 - config_section_descriptions = {"plugin": "插件基本信息", "greeting": "问候功能配置", "time": "时间查询配置"} - - # 配置Schema定义 - config_schema: dict = { - "plugin": { - "name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"), - "version": ConfigField(type=str, default="1.0.0", description="插件版本"), - "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), - }, - "greeting": { - "message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"), - "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"), - }, - "time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")}, - "print_message": {"enabled": ConfigField(type=bool, default=True, description="是否启用打印")}, - } - - def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - return [ - (HelloAction.get_action_info(), HelloAction), - (ByeAction.get_action_info(), ByeAction), # 添加告别Action - (TimeCommand.get_command_info(), TimeCommand), - (PrintMessage.get_handler_info(), PrintMessage), - ] - - -# @register_plugin -# class HelloWorldEventPlugin(BaseEPlugin): -# """Hello World事件插件 - 处理问候和告别事件""" - -# plugin_name = "hello_world_event_plugin" -# enable_plugin = False -# dependencies = [] -# python_dependencies = [] -# config_file_name = "event_config.toml" - -# config_schema = { -# "plugin": { -# "name": ConfigField(type=str, default="hello_world_event_plugin", description="插件名称"), -# "version": ConfigField(type=str, default="1.0.0", description="插件版本"), -# "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), -# }, -# } - -# def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: -# return [(PrintMessage.get_handler_info(), PrintMessage)] diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 9d75671c6..6b1475ee7 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -25,7 +25,7 @@ from src.chat.memory_system.instant_memory import InstantMemory from src.mood.mood_manager import mood_manager from src.person_info.relationship_fetcher import relationship_fetcher_manager from src.person_info.person_info import get_person_info_manager -from src.tools.tool_executor import ToolExecutor +from src.plugin_system.core.tool_executor import ToolExecutor from src.plugin_system.base.component_types import ActionInfo logger = get_logger("replyer") diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index eb07dbc92..3e692bb2f 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -9,6 +9,7 @@ from .base import ( BasePlugin, BaseAction, BaseCommand, + BaseTool, ConfigField, ComponentType, ActionActivationType, @@ -34,6 +35,7 @@ from .utils import ( from .apis import ( chat_api, + tool_api, component_manage_api, config_api, database_api, @@ -55,6 +57,7 @@ __version__ = "1.0.0" __all__ = [ # API 模块 "chat_api", + "tool_api", "component_manage_api", "config_api", "database_api", @@ -72,6 +75,7 @@ __all__ = [ "BasePlugin", "BaseAction", "BaseCommand", + "BaseTool", "BaseEventHandler", # 类型定义 "ComponentType", diff --git a/src/plugin_system/apis/tool_api.py b/src/plugin_system/apis/tool_api.py new file mode 100644 index 000000000..09fee548e --- /dev/null +++ b/src/plugin_system/apis/tool_api.py @@ -0,0 +1,25 @@ +from typing import Optional +from src.plugin_system.base.base_tool import BaseTool +from src.plugin_system.base.component_types import ComponentType + +from src.common.logger import get_logger + +logger = get_logger("tool_api") + +def get_tool_instance(tool_name: str) -> Optional[BaseTool]: + """获取公开工具实例""" + from src.plugin_system.core import component_registry + + tool_class = component_registry.get_component_class(tool_name, ComponentType.TOOL) + if not tool_class: + return None + + return tool_class() + +def get_llm_available_tool_definitions(): + from src.plugin_system.core import component_registry + + llm_available_tools = component_registry.get_llm_available_tools() + return [tool_class().get_tool_definition() for tool_class in llm_available_tools.values()] + + diff --git a/src/plugin_system/base/__init__.py b/src/plugin_system/base/__init__.py index a95e05aed..b9a2893e4 100644 --- a/src/plugin_system/base/__init__.py +++ b/src/plugin_system/base/__init__.py @@ -6,6 +6,7 @@ from .base_plugin import BasePlugin from .base_action import BaseAction +from .base_tool import BaseTool from .base_command import BaseCommand from .base_events_handler import BaseEventHandler from .component_types import ( @@ -15,6 +16,7 @@ from .component_types import ( ComponentInfo, ActionInfo, CommandInfo, + ToolInfo, PluginInfo, PythonDependency, EventHandlerInfo, @@ -27,12 +29,14 @@ __all__ = [ "BasePlugin", "BaseAction", "BaseCommand", + "BaseTool", "ComponentType", "ActionActivationType", "ChatMode", "ComponentInfo", "ActionInfo", "CommandInfo", + "ToolInfo", "PluginInfo", "PythonDependency", "ConfigField", diff --git a/src/plugin_system/base/base_tool.py b/src/plugin_system/base/base_tool.py new file mode 100644 index 000000000..e73562f18 --- /dev/null +++ b/src/plugin_system/base/base_tool.py @@ -0,0 +1,63 @@ +from typing import List, Any, Optional, Type +from src.common.logger import get_logger +from rich.traceback import install +from src.plugin_system.base.component_types import ToolInfo +install(extra_lines=3) + +logger = get_logger("base_tool") + +# 工具注册表 +TOOL_REGISTRY = {} + + +class BaseTool: + """所有工具的基类""" + + # 工具名称,子类必须重写 + name = None + # 工具描述,子类必须重写 + description = None + # 工具参数定义,子类必须重写 + parameters = None + # 是否可供LLM使用,默认为False + available_for_llm = False + + @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}, + } + + @classmethod + def get_tool_info(cls) -> ToolInfo: + """获取工具信息""" + if not cls.name or not cls.description: + raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name 和 description 属性") + + return ToolInfo( + tool_name=cls.name, + tool_description=cls.description, + available_for_llm=cls.available_for_llm, + tool_parameters=cls.parameters + ) + + # 工具参数定义,子类必须重写 + async def execute(self, **function_args: dict[str, Any]) -> dict[str, Any]: + """执行工具函数 + + Args: + function_args: 工具调用参数 + + Returns: + dict: 工具执行结果 + """ + raise NotImplementedError("子类必须实现execute方法") diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index eeb2a5a08..e8cd109b7 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -10,6 +10,7 @@ class ComponentType(Enum): ACTION = "action" # 动作组件 COMMAND = "command" # 命令组件 + TOOL = "tool" # 服务组件(预留) SCHEDULER = "scheduler" # 定时任务组件(预留) EVENT_HANDLER = "event_handler" # 事件处理组件(预留) @@ -144,7 +145,19 @@ class CommandInfo(ComponentInfo): def __post_init__(self): super().__post_init__() self.component_type = ComponentType.COMMAND + +@dataclass +class ToolInfo(ComponentInfo): + """工具组件信息""" + tool_name: str = "" # 工具名称 + tool_parameters: Dict[str, Any] = field(default_factory=dict) # 工具参数定义 + available_for_llm: bool = True # 是否可供LLM使用 + tool_description: str = "" # 工具描述 + + def __post_init__(self): + super().__post_init__() + self.component_type = ComponentType.TOOL @dataclass class EventHandlerInfo(ComponentInfo): diff --git a/src/plugin_system/core/__init__.py b/src/plugin_system/core/__init__.py index 3193828bf..b40fa51fe 100644 --- a/src/plugin_system/core/__init__.py +++ b/src/plugin_system/core/__init__.py @@ -9,6 +9,7 @@ from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.dependency_manager import dependency_manager from src.plugin_system.core.events_manager import events_manager from src.plugin_system.core.global_announcement_manager import global_announcement_manager +from src.plugin_system.core.tool_use import tool_user __all__ = [ "plugin_manager", @@ -16,4 +17,5 @@ __all__ = [ "dependency_manager", "events_manager", "global_announcement_manager", + "tool_user", ] diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 2ea89b880..7d7ab34ad 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -6,6 +6,7 @@ from src.common.logger import get_logger from src.plugin_system.base.component_types import ( ComponentInfo, ActionInfo, + ToolInfo, CommandInfo, EventHandlerInfo, PluginInfo, @@ -13,6 +14,7 @@ from src.plugin_system.base.component_types import ( ) from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_action import BaseAction +from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.base_events_handler import BaseEventHandler logger = get_logger("component_registry") @@ -30,7 +32,7 @@ class ComponentRegistry: """组件注册表 命名空间式组件名 -> 组件信息""" self._components_by_type: Dict[ComponentType, Dict[str, ComponentInfo]] = {types: {} for types in ComponentType} """类型 -> 组件原名称 -> 组件信息""" - self._components_classes: Dict[str, Type[Union[BaseCommand, BaseAction, BaseEventHandler]]] = {} + self._components_classes: Dict[str, Type[Union[BaseCommand, BaseAction, BaseTool, BaseEventHandler]]] = {} """命名空间式组件名 -> 组件类""" # 插件注册表 @@ -49,6 +51,10 @@ class ComponentRegistry: self._command_patterns: Dict[Pattern, str] = {} """编译后的正则 -> command名""" + # 工具特定注册表 + self._tool_registry: Dict[str, BaseTool] = {} # 工具名 -> 工具类 + self._llm_available_tools: Dict[str, str] = {} # 公开的工具名 -> 描述 + # EventHandler特定注册表 self._event_handler_registry: Dict[str, Type[BaseEventHandler]] = {} """event_handler名 -> event_handler类""" @@ -125,6 +131,10 @@ class ComponentRegistry: assert isinstance(component_info, CommandInfo) assert issubclass(component_class, BaseCommand) ret = self._register_command_component(component_info, component_class) + case ComponentType.TOOL: + assert isinstance(component_info, ToolInfo) + assert issubclass(component_class, BaseTool) + ret = self._register_tool_component(component_info, component_class) case ComponentType.EVENT_HANDLER: assert isinstance(component_info, EventHandlerInfo) assert issubclass(component_class, BaseEventHandler) @@ -180,6 +190,15 @@ class ComponentRegistry: return True + def _register_tool_component(self, tool_info: ToolInfo, tool_class: BaseTool): + """注册Tool组件到Tool特定注册表""" + tool_name = tool_info.name + self._tool_registry[tool_name] = tool_class + + # 如果是llm可用的且启用的工具,添加到 llm可用工具列表 + if tool_info.available_for_llm and tool_info.enabled: + self._llm_available_tools[tool_name] = tool_class + def _register_event_handler_component( self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler] ) -> bool: @@ -475,7 +494,28 @@ class ComponentRegistry: candidates[0].match(text).groupdict(), # type: ignore command_info, ) + + # === Tool 特定查询方法 === + def get_tool_registry(self) -> Dict[str, Type[BaseTool]]: + """获取Tool注册表""" + return self._tool_registry.copy() + + def get_llm_available_tools(self) -> Dict[str, str]: + """获取LLM可用的Tool列表""" + return self._llm_available_tools.copy() + def get_registered_tool_info(self, tool_name: str) -> Optional[ToolInfo]: + """获取Tool信息 + + Args: + tool_name: 工具名称 + + Returns: + ToolInfo: 工具信息对象,如果工具不存在则返回 None + """ + info = self.get_component_info(tool_name, ComponentType.TOOL) + return info if isinstance(info, ToolInfo) else None + # === EventHandler 特定查询方法 === def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]: @@ -529,17 +569,21 @@ class ComponentRegistry: """获取注册中心统计信息""" action_components: int = 0 command_components: int = 0 - events_handlers: int = 0 + tool_components: int = 0 + events_handlers: int = 0 for component in self._components.values(): if component.component_type == ComponentType.ACTION: action_components += 1 elif component.component_type == ComponentType.COMMAND: command_components += 1 + elif component.component_type == ComponentType.TOOL: + tool_components += 1 elif component.component_type == ComponentType.EVENT_HANDLER: events_handlers += 1 return { "action_components": action_components, "command_components": command_components, + "tool_components": tool_components, "event_handlers": events_handlers, "total_components": len(self._components), "total_plugins": len(self._plugins), diff --git a/src/tools/tool_executor.py b/src/plugin_system/core/tool_executor.py similarity index 99% rename from src/tools/tool_executor.py rename to src/plugin_system/core/tool_executor.py index 0f50ca2ab..45fe2a5fb 100644 --- a/src/tools/tool_executor.py +++ b/src/plugin_system/core/tool_executor.py @@ -3,7 +3,7 @@ from src.config.config import global_config import time from src.common.logger import get_logger from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.tools.tool_use import ToolUser +from .tool_use import tool_user from src.chat.utils.json_utils import process_llm_tool_calls from typing import List, Dict, Tuple, Optional from src.chat.message_receive.chat_stream import get_chat_manager @@ -52,7 +52,7 @@ class ToolExecutor: ) # 初始化工具实例 - self.tool_instance = ToolUser() + self.tool_instance = tool_user # 缓存配置 self.enable_cache = enable_cache diff --git a/src/tools/tool_use.py b/src/plugin_system/core/tool_use.py similarity index 86% rename from src/tools/tool_use.py rename to src/plugin_system/core/tool_use.py index 6a8cd48a6..9dd456ae3 100644 --- a/src/tools/tool_use.py +++ b/src/plugin_system/core/tool_use.py @@ -1,6 +1,6 @@ import json from src.common.logger import get_logger -from src.tools.tool_can_use import get_all_tool_definitions, get_tool_instance +from src.plugin_system.apis.tool_api import get_llm_available_tool_definitions,get_tool_instance logger = get_logger("tool_use") @@ -13,7 +13,7 @@ class ToolUser: Returns: list: 工具定义列表 """ - return get_all_tool_definitions() + return get_llm_available_tool_definitions() @staticmethod async def execute_tool_call(tool_call): @@ -30,6 +30,7 @@ class ToolUser: try: function_name = tool_call["function"]["name"] function_args = json.loads(tool_call["function"]["arguments"]) + function_args["llm_called"] = True # 标记为LLM调用 # 获取对应工具实例 tool_instance = get_tool_instance(function_name) @@ -54,3 +55,5 @@ class ToolUser: except Exception as e: logger.error(f"执行工具调用时发生错误: {str(e)}") return None + +tool_user = ToolUser() \ No newline at end of file diff --git a/src/tools/not_using/get_knowledge.py b/src/tools/not_using/get_knowledge.py deleted file mode 100644 index c436d7742..000000000 --- a/src/tools/not_using/get_knowledge.py +++ /dev/null @@ -1,133 +0,0 @@ -from src.tools.tool_can_use.base_tool import BaseTool -from src.chat.utils.utils import get_embedding -from src.common.database.database_model import Knowledges # Updated import -from src.common.logger import get_logger -from typing import Any, Union, List # Added List -import json # Added for parsing embedding -import math # Added for cosine similarity - -logger = get_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]) -> dict[str, Any]: - """执行知识库搜索 - - Args: - function_args: 工具参数 - - Returns: - dict: 工具执行结果 - """ - query = "" # Initialize query to ensure it's defined in except block - try: - query = function_args.get("query") - 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 {"type": "knowledge", "id": query, "content": content} - return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你知识库炸了"} - except Exception as e: - logger.error(f"知识库搜索工具执行失败: {str(e)}") - return {"type": "info", "id": query, "content": f"知识库搜索失败,炸了: {str(e)}"} - - @staticmethod - def _cosine_similarity(vec1: List[float], vec2: List[float]) -> float: - """计算两个向量之间的余弦相似度""" - dot_product = sum(p * q for p, q in zip(vec1, vec2, strict=False)) - magnitude1 = math.sqrt(sum(p * p for p in vec1)) - magnitude2 = math.sqrt(sum(q * q for q in vec2)) - if magnitude1 == 0 or magnitude2 == 0: - return 0.0 - return dot_product / (magnitude1 * magnitude2) - - @staticmethod - def get_info_from_db( - query_embedding: list[float], 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 [] - - similar_items = [] - try: - all_knowledges = Knowledges.select() - for item in all_knowledges: - try: - item_embedding_str = item.embedding - if not item_embedding_str: - logger.warning(f"Knowledge item ID {item.id} has empty embedding string.") - continue - item_embedding = json.loads(item_embedding_str) - if not isinstance(item_embedding, list) or not all( - isinstance(x, (int, float)) for x in item_embedding - ): - logger.warning(f"Knowledge item ID {item.id} has invalid embedding format after JSON parsing.") - continue - except json.JSONDecodeError: - logger.warning(f"Failed to parse embedding for knowledge item ID {item.id}") - continue - except AttributeError: - logger.warning(f"Knowledge item ID {item.id} missing 'embedding' attribute or it's not a string.") - continue - - similarity = SearchKnowledgeTool._cosine_similarity(query_embedding, item_embedding) - - if similarity >= threshold: - similar_items.append({"content": item.content, "similarity": similarity, "raw_item": item}) - - # 按相似度降序排序 - similar_items.sort(key=lambda x: x["similarity"], reverse=True) - - # 应用限制 - results = similar_items[:limit] - logger.debug(f"知识库查询后,符合条件的结果数量: {len(results)}") - - except Exception as e: - logger.error(f"从 Peewee 数据库获取知识信息失败: {str(e)}") - return "" if not return_raw else [] - - if not results: - return "" if not return_raw else [] - - if return_raw: - # Peewee 模型实例不能直接序列化为 JSON,如果需要原始模型,调用者需要处理 - # 这里返回包含内容和相似度的字典列表 - return [{"content": r["content"], "similarity": r["similarity"]} for r in results] - else: - # 返回所有找到的内容,用换行分隔 - return "\n".join(str(result["content"]) for result in results) - - -# 注册工具 -# register_tool(SearchKnowledgeTool) diff --git a/src/tools/not_using/lpmm_get_knowledge.py b/src/tools/not_using/lpmm_get_knowledge.py deleted file mode 100644 index 467db6ed1..000000000 --- a/src/tools/not_using/lpmm_get_knowledge.py +++ /dev/null @@ -1,60 +0,0 @@ -from src.tools.tool_can_use.base_tool import BaseTool - -# from src.common.database import db -from src.common.logger import get_logger -from typing import Dict, Any -from src.chat.knowledge.knowledge_lib import qa_manager - - -logger = get_logger("lpmm_get_knowledge_tool") - - -class SearchKnowledgeFromLPMMTool(BaseTool): - """从LPMM知识库中搜索相关信息的工具""" - - name = "lpmm_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]) -> Dict[str, Any]: - """执行知识库搜索 - - Args: - function_args: 工具参数 - - Returns: - Dict: 工具执行结果 - """ - try: - query: str = function_args.get("query") # type: ignore - # threshold = function_args.get("threshold", 0.4) - - # 检查LPMM知识库是否启用 - if qa_manager is None: - logger.debug("LPMM知识库已禁用,跳过知识获取") - return {"type": "info", "id": query, "content": "LPMM知识库已禁用"} - - # 调用知识库搜索 - - knowledge_info = await qa_manager.get_knowledge(query) - - logger.debug(f"知识库查询结果: {knowledge_info}") - - if knowledge_info: - content = f"你知道这些知识: {knowledge_info}" - else: - content = f"你不太了解有关{query}的知识" - return {"type": "lpmm_knowledge", "id": query, "content": content} - except Exception as e: - # 捕获异常并记录错误 - logger.error(f"知识库搜索工具执行失败: {str(e)}") - # 在其他异常情况下,确保 id 仍然是 query (如果它被定义了) - query_id = query if "query" in locals() else "unknown_query" - return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {str(e)}"} diff --git a/src/tools/tool_can_use/__init__.py b/src/tools/tool_can_use/__init__.py deleted file mode 100644 index 14bae04c0..000000000 --- a/src/tools/tool_can_use/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from src.tools.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/tools/tool_can_use/base_tool.py b/src/tools/tool_can_use/base_tool.py deleted file mode 100644 index 89d051dc5..000000000 --- a/src/tools/tool_can_use/base_tool.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import List, Any, Optional, Type -import inspect -import importlib -import pkgutil -import os -from src.common.logger import get_logger -from rich.traceback import install - -install(extra_lines=3) - -logger = get_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]) -> dict[str, Any]: - """执行工具函数 - - Args: - function_args: 工具调用参数 - - 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.tools.{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/tools/tool_can_use/compare_numbers_tool.py b/src/tools/tool_can_use/compare_numbers_tool.py deleted file mode 100644 index 236a4587d..000000000 --- a/src/tools/tool_can_use/compare_numbers_tool.py +++ /dev/null @@ -1,45 +0,0 @@ -from src.tools.tool_can_use.base_tool import BaseTool -from src.common.logger import get_logger -from typing import Any - -logger = get_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]) -> dict[str, Any]: - """执行比较两个数的大小 - - Args: - function_args: 工具参数 - - Returns: - dict: 工具执行结果 - """ - num1: int | float = function_args.get("num1") # type: ignore - num2: int | float = function_args.get("num2") # type: ignore - - try: - 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)}"} diff --git a/src/tools/tool_can_use/rename_person_tool.py b/src/tools/tool_can_use/rename_person_tool.py deleted file mode 100644 index 17e624686..000000000 --- a/src/tools/tool_can_use/rename_person_tool.py +++ /dev/null @@ -1,103 +0,0 @@ -from src.tools.tool_can_use.base_tool import BaseTool -from src.person_info.person_info import get_person_info_manager -from src.common.logger import get_logger - - -logger = get_logger("rename_person_tool") - - -class RenamePersonTool(BaseTool): - name = "rename_person" - description = ( - "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。你想给人改名,叫别人别的称呼,需要调用这个工具。" - ) - parameters = { - "type": "object", - "properties": { - "person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"}, - "message_content": { - "type": "string", - "description": "当前的聊天内容或特定要求,用于提供取名建议的上下文,尽可能详细。", - }, - }, - "required": ["person_name"], - } - - async def execute(self, function_args: dict): - """ - 执行取名工具逻辑 - - Args: - function_args (dict): 包含 'person_name' 和可选 'message_content' 的字典 - message_txt (str): 原始消息文本 (这里未使用,因为 message_content 更明确) - - Returns: - dict: 包含执行结果的字典 - """ - person_name_to_find = function_args.get("person_name") - request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 - - if not person_name_to_find: - return {"name": self.name, "content": "错误:必须提供需要重命名的用户昵称 (person_name)。"} - person_info_manager = get_person_info_manager() - try: - # 1. 根据昵称查找用户信息 - logger.debug(f"尝试根据昵称 '{person_name_to_find}' 查找用户...") - person_info = await person_info_manager.get_person_info_by_name(person_name_to_find) - - if not person_info: - logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。") - return { - "name": self.name, - "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。", - } - - person_id = person_info.get("person_id") - user_nickname = person_info.get("nickname") # 这是用户原始昵称 - user_cardname = person_info.get("user_cardname") - user_avatar = person_info.get("user_avatar") - - if not person_id: - logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") - return {"name": self.name, "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。"} - - # 2. 调用 qv_person_name 进行取名 - logger.debug( - f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'" - ) - result = await person_info_manager.qv_person_name( - person_id=person_id, - user_nickname=user_nickname, # type: ignore - user_cardname=user_cardname, # type: ignore - user_avatar=user_avatar, # type: ignore - request=request_context, - ) - - # 3. 处理结果 - if result and result.get("nickname"): - new_name = result["nickname"] - # reason = result.get("reason", "未提供理由") - logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") - - content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" - logger.info(content) - return {"name": self.name, "content": content} - else: - logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") - # 尝试从内存中获取可能已经更新的名字 - current_name = await person_info_manager.get_value(person_id, "person_name") - if current_name and current_name != person_name_to_find: - return { - "name": self.name, - "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。", - } - else: - return { - "name": self.name, - "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。", - } - - except Exception as e: - error_msg = f"重命名失败: {str(e)}" - logger.error(error_msg, exc_info=True) - return {"name": self.name, "content": error_msg} From 3ebca2efaa780116d7d89d59fb9493d82c8c2b4e Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 26 Jul 2025 18:55:50 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86import=E6=97=B6?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF=E5=AF=BC=E5=85=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/replyer/default_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 24ee95e35..78534f0ca 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -29,7 +29,6 @@ from src.chat.memory_system.instant_memory import InstantMemory from src.mood.mood_manager import mood_manager from src.person_info.relationship_fetcher import relationship_fetcher_manager from src.person_info.person_info import get_person_info_manager -from src.plugin_system.core.tool_executor import ToolExecutor from src.plugin_system.base.component_types import ActionInfo logger = get_logger("replyer") @@ -164,6 +163,8 @@ class DefaultReplyer: self.heart_fc_sender = HeartFCSender() self.memory_activator = MemoryActivator() self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id) + + from src.plugin_system.core.tool_executor import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=True, cache_ttl=3) def _select_weighted_model_config(self) -> Dict[str, Any]: From 3021acff59f34700f9af47d86a7635756a9d18af Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 26 Jul 2025 20:49:22 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9Bbug=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=BA=86=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=8A=A0=E8=BD=BD=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/base/base_tool.py | 9 +++++---- src/plugin_system/base/component_types.py | 1 - src/plugin_system/core/component_registry.py | 4 +++- src/plugin_system/core/plugin_manager.py | 10 ++++++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/plugin_system/base/base_tool.py b/src/plugin_system/base/base_tool.py index e73562f18..dc6147b9f 100644 --- a/src/plugin_system/base/base_tool.py +++ b/src/plugin_system/base/base_tool.py @@ -1,7 +1,7 @@ from typing import List, Any, Optional, Type from src.common.logger import get_logger from rich.traceback import install -from src.plugin_system.base.component_types import ToolInfo +from src.plugin_system.base.component_types import ComponentType, ToolInfo install(extra_lines=3) logger = get_logger("base_tool") @@ -44,14 +44,15 @@ class BaseTool: raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name 和 description 属性") return ToolInfo( - tool_name=cls.name, + name=cls.name, tool_description=cls.description, available_for_llm=cls.available_for_llm, - tool_parameters=cls.parameters + tool_parameters=cls.parameters, + component_type=ComponentType.TOOL, ) # 工具参数定义,子类必须重写 - async def execute(self, **function_args: dict[str, Any]) -> dict[str, Any]: + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: """执行工具函数 Args: diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index e8cd109b7..cbbe959fa 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -150,7 +150,6 @@ class CommandInfo(ComponentInfo): class ToolInfo(ComponentInfo): """工具组件信息""" - tool_name: str = "" # 工具名称 tool_parameters: Dict[str, Any] = field(default_factory=dict) # 工具参数定义 available_for_llm: bool = True # 是否可供LLM使用 tool_description: str = "" # 工具描述 diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 7d7ab34ad..ef28f7d07 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -198,7 +198,9 @@ class ComponentRegistry: # 如果是llm可用的且启用的工具,添加到 llm可用工具列表 if tool_info.available_for_llm and tool_info.enabled: self._llm_available_tools[tool_name] = tool_class - + + return True + def _register_event_handler_component( self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler] ) -> bool: diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 98bce4bdb..527270ee3 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -345,6 +345,7 @@ class PluginManager: stats = component_registry.get_registry_stats() action_count = stats.get("action_components", 0) command_count = stats.get("command_components", 0) + tool_count = stats.get("tool_components", 0) event_handler_count = stats.get("event_handlers", 0) total_components = stats.get("total_components", 0) @@ -352,7 +353,7 @@ class PluginManager: if total_registered > 0: logger.info("🎉 插件系统加载完成!") logger.info( - f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, EventHandler: {event_handler_count})" + f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, EventHandler: {event_handler_count})" ) # 显示详细的插件列表 @@ -387,6 +388,9 @@ class PluginManager: command_components = [ c for c in plugin_info.components if c.component_type == ComponentType.COMMAND ] + tool_components = [ + c for c in plugin_info.components if c.component_type == ComponentType.TOOL + ] event_handler_components = [ c for c in plugin_info.components if c.component_type == ComponentType.EVENT_HANDLER ] @@ -398,7 +402,9 @@ class PluginManager: if command_components: command_names = [c.name for c in command_components] logger.info(f" ⚡ Command组件: {', '.join(command_names)}") - + if tool_components: + tool_names = [c.name for c in tool_components] + logger.info(f" 🛠️ Tool组件: {', '.join(tool_names)}") if event_handler_components: event_handler_names = [c.name for c in event_handler_components] logger.info(f" 📢 EventHandler组件: {', '.join(event_handler_names)}") From 8aa8f0e6b7a71b452c5aaf95fbd17cdb80d7d32a Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 26 Jul 2025 22:29:44 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86hello=5Fworld=5F?= =?UTF-8?q?plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/hello_world_plugin/_manifest.json | 53 ++++++ plugins/hello_world_plugin/plugin.py | 202 ++++++++++++++++++++++ src/plugin_system/base/base_tool.py | 2 - src/plugin_system/base/component_types.py | 2 +- 4 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 plugins/hello_world_plugin/_manifest.json create mode 100644 plugins/hello_world_plugin/plugin.py diff --git a/plugins/hello_world_plugin/_manifest.json b/plugins/hello_world_plugin/_manifest.json new file mode 100644 index 000000000..b1a4c4eb8 --- /dev/null +++ b/plugins/hello_world_plugin/_manifest.json @@ -0,0 +1,53 @@ +{ + "manifest_version": 1, + "name": "Hello World 示例插件 (Hello World Plugin)", + "version": "1.0.0", + "description": "我的第一个MaiCore插件,包含问候功能和时间查询等基础示例", + "author": { + "name": "MaiBot开发团队", + "url": "https://github.com/MaiM-with-u" + }, + "license": "GPL-v3.0-or-later", + + "host_application": { + "min_version": "0.8.0" + }, + "homepage_url": "https://github.com/MaiM-with-u/maibot", + "repository_url": "https://github.com/MaiM-with-u/maibot", + "keywords": ["demo", "example", "hello", "greeting", "tutorial"], + "categories": ["Examples", "Tutorial"], + + "default_locale": "zh-CN", + "locales_path": "_locales", + + "plugin_info": { + "is_built_in": false, + "plugin_type": "example", + "components": [ + { + "type": "action", + "name": "hello_greeting", + "description": "向用户发送问候消息" + }, + { + "type": "action", + "name": "bye_greeting", + "description": "向用户发送告别消息", + "activation_modes": ["keyword"], + "keywords": ["再见", "bye", "88", "拜拜"] + }, + { + "type": "command", + "name": "time", + "description": "查询当前时间", + "pattern": "/time" + } + ], + "features": [ + "问候和告别功能", + "时间查询命令", + "配置文件示例", + "新手教程代码" + ] + } +} \ No newline at end of file diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py new file mode 100644 index 000000000..8093bc885 --- /dev/null +++ b/plugins/hello_world_plugin/plugin.py @@ -0,0 +1,202 @@ +from typing import List, Tuple, Type +from src.plugin_system.apis import tool_api +from src.plugin_system import ( + BasePlugin, + register_plugin, + BaseAction, + BaseCommand, + BaseTool, + ComponentInfo, + ActionActivationType, + ConfigField, + BaseEventHandler, + EventType, + MaiMessages, +) + +class HelloTool(BaseTool): + """问候工具 - 用于发送问候消息""" + + name = "hello_tool" + description = "发送问候消息" + parameters = { + "type": "object", + "properties": { + "greeting_message": { + "type": "string", + "description": "要发送的问候消息" + }, + }, + "required": ["greeting_message"] + } + available_for_llm = True + + + async def execute(self, function_args): + """执行问候工具""" + import random + greeting_message = random.choice(function_args.get("greeting_message", ["嗨!很高兴见到你!😊"])) + return { + "name": self.name, + "content": greeting_message + } + +# ===== Action组件 ===== +class HelloAction(BaseAction): + """问候Action - 简单的问候动作""" + + # === 基本信息(必须填写)=== + action_name = "hello_greeting" + action_description = "向用户发送问候消息" + activation_type = ActionActivationType.ALWAYS # 始终激活 + + # === 功能描述(必须填写)=== + action_parameters = {"greeting_message": "要发送的问候消息"} + action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"] + associated_types = ["text"] + + async def execute(self) -> Tuple[bool, str]: + """执行问候动作 - 这是核心功能""" + # 发送问候消息 + hello_tool = tool_api.get_tool_instance("hello_tool") + greeting_message = await hello_tool.execute({ + "greeting_message": self.action_data.get("greeting_message", "") + }) + base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊") + message = base_message + greeting_message + await self.send_text(message) + + return True, "发送了问候消息" + + +class ByeAction(BaseAction): + """告别Action - 只在用户说再见时激活""" + + action_name = "bye_greeting" + action_description = "向用户发送告别消息" + + # 使用关键词激活 + activation_type = ActionActivationType.KEYWORD + + # 关键词设置 + activation_keywords = ["再见", "bye", "88", "拜拜"] + keyword_case_sensitive = False + + action_parameters = {"bye_message": "要发送的告别消息"} + action_require = [ + "用户要告别时使用", + "当有人要离开时使用", + "当有人和你说再见时使用", + ] + associated_types = ["text"] + + async def execute(self) -> Tuple[bool, str]: + bye_message = self.action_data.get("bye_message", "") + + message = f"再见!期待下次聊天!👋{bye_message}" + await self.send_text(message) + return True, "发送了告别消息" + + +class TimeCommand(BaseCommand): + """时间查询Command - 响应/time命令""" + + command_name = "time" + command_description = "查询当前时间" + + # === 命令设置(必须填写)=== + command_pattern = r"^/time$" # 精确匹配 "/time" 命令 + + async def execute(self) -> Tuple[bool, str, bool]: + """执行时间查询""" + import datetime + + # 获取当前时间 + time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore + now = datetime.datetime.now() + time_str = now.strftime(time_format) + + # 发送时间信息 + message = f"⏰ 当前时间:{time_str}" + await self.send_text(message) + + return True, f"显示了当前时间: {time_str}", True + + +class PrintMessage(BaseEventHandler): + """打印消息事件处理器 - 处理打印消息事件""" + + event_type = EventType.ON_MESSAGE + handler_name = "print_message_handler" + handler_description = "打印接收到的消息" + + async def execute(self, message: MaiMessages) -> Tuple[bool, bool, str | None]: + """执行打印消息事件处理""" + # 打印接收到的消息 + if self.get_config("print_message.enabled", False): + print(f"接收到消息: {message.raw_message}") + return True, True, "消息已打印" + + +# ===== 插件注册 ===== + + +@register_plugin +class HelloWorldPlugin(BasePlugin): + """Hello World插件 - 你的第一个MaiCore插件""" + + # 插件基本信息 + plugin_name: str = "hello_world_plugin" # 内部标识符 + enable_plugin: bool = True + dependencies: List[str] = [] # 插件依赖列表 + python_dependencies: List[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" # 配置文件名 + + # 配置节描述 + config_section_descriptions = {"plugin": "插件基本信息", "greeting": "问候功能配置", "time": "时间查询配置"} + + # 配置Schema定义 + config_schema: dict = { + "plugin": { + "name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"), + "version": ConfigField(type=str, default="1.0.0", description="插件版本"), + "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), + }, + "greeting": { + "message": ConfigField(type=list, default=["嗨!很开心见到你!😊","Ciallo~(∠・ω< )⌒★"], description="默认问候消息"), + "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"), + }, + "time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")}, + "print_message": {"enabled": ConfigField(type=bool, default=True, description="是否启用打印")}, + } + + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + return [ + (HelloAction.get_action_info(), HelloAction), + (HelloTool.get_tool_info(), HelloTool), # 添加问候工具 + (ByeAction.get_action_info(), ByeAction), # 添加告别Action + (TimeCommand.get_command_info(), TimeCommand), + (PrintMessage.get_handler_info(), PrintMessage), + ] + + +# @register_plugin +# class HelloWorldEventPlugin(BaseEPlugin): +# """Hello World事件插件 - 处理问候和告别事件""" + +# plugin_name = "hello_world_event_plugin" +# enable_plugin = False +# dependencies = [] +# python_dependencies = [] +# config_file_name = "event_config.toml" + +# config_schema = { +# "plugin": { +# "name": ConfigField(type=str, default="hello_world_event_plugin", description="插件名称"), +# "version": ConfigField(type=str, default="1.0.0", description="插件版本"), +# "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), +# }, +# } + +# def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: +# return [(PrintMessage.get_handler_info(), PrintMessage)] diff --git a/src/plugin_system/base/base_tool.py b/src/plugin_system/base/base_tool.py index dc6147b9f..b2f219629 100644 --- a/src/plugin_system/base/base_tool.py +++ b/src/plugin_system/base/base_tool.py @@ -6,8 +6,6 @@ install(extra_lines=3) logger = get_logger("base_tool") -# 工具注册表 -TOOL_REGISTRY = {} class BaseTool: diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index cbbe959fa..3ecb15a0a 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -151,7 +151,7 @@ class ToolInfo(ComponentInfo): """工具组件信息""" tool_parameters: Dict[str, Any] = field(default_factory=dict) # 工具参数定义 - available_for_llm: bool = True # 是否可供LLM使用 + available_for_llm: bool = False # 是否可供LLM使用 tool_description: str = "" # 工具描述 def __post_init__(self): From 8cc6636b20e3ee1ddebb59304e557f097bbd9c85 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sat, 26 Jul 2025 22:37:46 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E5=A4=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugin_system/core/component_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index ef28f7d07..d40d0f624 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -53,7 +53,7 @@ class ComponentRegistry: # 工具特定注册表 self._tool_registry: Dict[str, BaseTool] = {} # 工具名 -> 工具类 - self._llm_available_tools: Dict[str, str] = {} # 公开的工具名 -> 描述 + self._llm_available_tools: Dict[str, str] = {} # llm可用的工具名 -> 描述 # EventHandler特定注册表 self._event_handler_registry: Dict[str, Type[BaseEventHandler]] = {} From fa7b9dd7d8d19a11027938acd57c534c08cedefa Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 27 Jul 2025 00:02:40 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E6=89=BE=E5=9B=9E=E5=8E=9F=E6=9D=A5?= =?UTF-8?q?=E7=9A=84tools=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tools/not_using/get_knowledge.py | 133 ++++++ src/tools/not_using/lpmm_get_knowledge.py | 60 +++ src/tools/tool_can_use/__init__.py | 20 + src/tools/tool_can_use/base_tool.py | 115 +++++ .../tool_can_use/compare_numbers_tool.py | 45 ++ src/tools/tool_can_use/rename_person_tool.py | 103 +++++ src/tools/tool_executor.py | 407 ++++++++++++++++++ src/tools/tool_use.py | 56 +++ 8 files changed, 939 insertions(+) create mode 100644 src/tools/not_using/get_knowledge.py create mode 100644 src/tools/not_using/lpmm_get_knowledge.py create mode 100644 src/tools/tool_can_use/__init__.py create mode 100644 src/tools/tool_can_use/base_tool.py create mode 100644 src/tools/tool_can_use/compare_numbers_tool.py create mode 100644 src/tools/tool_can_use/rename_person_tool.py create mode 100644 src/tools/tool_executor.py create mode 100644 src/tools/tool_use.py diff --git a/src/tools/not_using/get_knowledge.py b/src/tools/not_using/get_knowledge.py new file mode 100644 index 000000000..c436d7742 --- /dev/null +++ b/src/tools/not_using/get_knowledge.py @@ -0,0 +1,133 @@ +from src.tools.tool_can_use.base_tool import BaseTool +from src.chat.utils.utils import get_embedding +from src.common.database.database_model import Knowledges # Updated import +from src.common.logger import get_logger +from typing import Any, Union, List # Added List +import json # Added for parsing embedding +import math # Added for cosine similarity + +logger = get_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]) -> dict[str, Any]: + """执行知识库搜索 + + Args: + function_args: 工具参数 + + Returns: + dict: 工具执行结果 + """ + query = "" # Initialize query to ensure it's defined in except block + try: + query = function_args.get("query") + 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 {"type": "knowledge", "id": query, "content": content} + return {"type": "info", "id": query, "content": f"无法获取关于'{query}'的嵌入向量,你知识库炸了"} + except Exception as e: + logger.error(f"知识库搜索工具执行失败: {str(e)}") + return {"type": "info", "id": query, "content": f"知识库搜索失败,炸了: {str(e)}"} + + @staticmethod + def _cosine_similarity(vec1: List[float], vec2: List[float]) -> float: + """计算两个向量之间的余弦相似度""" + dot_product = sum(p * q for p, q in zip(vec1, vec2, strict=False)) + magnitude1 = math.sqrt(sum(p * p for p in vec1)) + magnitude2 = math.sqrt(sum(q * q for q in vec2)) + if magnitude1 == 0 or magnitude2 == 0: + return 0.0 + return dot_product / (magnitude1 * magnitude2) + + @staticmethod + def get_info_from_db( + query_embedding: list[float], 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 [] + + similar_items = [] + try: + all_knowledges = Knowledges.select() + for item in all_knowledges: + try: + item_embedding_str = item.embedding + if not item_embedding_str: + logger.warning(f"Knowledge item ID {item.id} has empty embedding string.") + continue + item_embedding = json.loads(item_embedding_str) + if not isinstance(item_embedding, list) or not all( + isinstance(x, (int, float)) for x in item_embedding + ): + logger.warning(f"Knowledge item ID {item.id} has invalid embedding format after JSON parsing.") + continue + except json.JSONDecodeError: + logger.warning(f"Failed to parse embedding for knowledge item ID {item.id}") + continue + except AttributeError: + logger.warning(f"Knowledge item ID {item.id} missing 'embedding' attribute or it's not a string.") + continue + + similarity = SearchKnowledgeTool._cosine_similarity(query_embedding, item_embedding) + + if similarity >= threshold: + similar_items.append({"content": item.content, "similarity": similarity, "raw_item": item}) + + # 按相似度降序排序 + similar_items.sort(key=lambda x: x["similarity"], reverse=True) + + # 应用限制 + results = similar_items[:limit] + logger.debug(f"知识库查询后,符合条件的结果数量: {len(results)}") + + except Exception as e: + logger.error(f"从 Peewee 数据库获取知识信息失败: {str(e)}") + return "" if not return_raw else [] + + if not results: + return "" if not return_raw else [] + + if return_raw: + # Peewee 模型实例不能直接序列化为 JSON,如果需要原始模型,调用者需要处理 + # 这里返回包含内容和相似度的字典列表 + return [{"content": r["content"], "similarity": r["similarity"]} for r in results] + else: + # 返回所有找到的内容,用换行分隔 + return "\n".join(str(result["content"]) for result in results) + + +# 注册工具 +# register_tool(SearchKnowledgeTool) diff --git a/src/tools/not_using/lpmm_get_knowledge.py b/src/tools/not_using/lpmm_get_knowledge.py new file mode 100644 index 000000000..467db6ed1 --- /dev/null +++ b/src/tools/not_using/lpmm_get_knowledge.py @@ -0,0 +1,60 @@ +from src.tools.tool_can_use.base_tool import BaseTool + +# from src.common.database import db +from src.common.logger import get_logger +from typing import Dict, Any +from src.chat.knowledge.knowledge_lib import qa_manager + + +logger = get_logger("lpmm_get_knowledge_tool") + + +class SearchKnowledgeFromLPMMTool(BaseTool): + """从LPMM知识库中搜索相关信息的工具""" + + name = "lpmm_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]) -> Dict[str, Any]: + """执行知识库搜索 + + Args: + function_args: 工具参数 + + Returns: + Dict: 工具执行结果 + """ + try: + query: str = function_args.get("query") # type: ignore + # threshold = function_args.get("threshold", 0.4) + + # 检查LPMM知识库是否启用 + if qa_manager is None: + logger.debug("LPMM知识库已禁用,跳过知识获取") + return {"type": "info", "id": query, "content": "LPMM知识库已禁用"} + + # 调用知识库搜索 + + knowledge_info = await qa_manager.get_knowledge(query) + + logger.debug(f"知识库查询结果: {knowledge_info}") + + if knowledge_info: + content = f"你知道这些知识: {knowledge_info}" + else: + content = f"你不太了解有关{query}的知识" + return {"type": "lpmm_knowledge", "id": query, "content": content} + except Exception as e: + # 捕获异常并记录错误 + logger.error(f"知识库搜索工具执行失败: {str(e)}") + # 在其他异常情况下,确保 id 仍然是 query (如果它被定义了) + query_id = query if "query" in locals() else "unknown_query" + return {"type": "info", "id": query_id, "content": f"lpmm知识库搜索失败,炸了: {str(e)}"} diff --git a/src/tools/tool_can_use/__init__.py b/src/tools/tool_can_use/__init__.py new file mode 100644 index 000000000..14bae04c0 --- /dev/null +++ b/src/tools/tool_can_use/__init__.py @@ -0,0 +1,20 @@ +from src.tools.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/tools/tool_can_use/base_tool.py b/src/tools/tool_can_use/base_tool.py new file mode 100644 index 000000000..89d051dc5 --- /dev/null +++ b/src/tools/tool_can_use/base_tool.py @@ -0,0 +1,115 @@ +from typing import List, Any, Optional, Type +import inspect +import importlib +import pkgutil +import os +from src.common.logger import get_logger +from rich.traceback import install + +install(extra_lines=3) + +logger = get_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]) -> dict[str, Any]: + """执行工具函数 + + Args: + function_args: 工具调用参数 + + 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.tools.{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/tools/tool_can_use/compare_numbers_tool.py b/src/tools/tool_can_use/compare_numbers_tool.py new file mode 100644 index 000000000..236a4587d --- /dev/null +++ b/src/tools/tool_can_use/compare_numbers_tool.py @@ -0,0 +1,45 @@ +from src.tools.tool_can_use.base_tool import BaseTool +from src.common.logger import get_logger +from typing import Any + +logger = get_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]) -> dict[str, Any]: + """执行比较两个数的大小 + + Args: + function_args: 工具参数 + + Returns: + dict: 工具执行结果 + """ + num1: int | float = function_args.get("num1") # type: ignore + num2: int | float = function_args.get("num2") # type: ignore + + try: + 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)}"} diff --git a/src/tools/tool_can_use/rename_person_tool.py b/src/tools/tool_can_use/rename_person_tool.py new file mode 100644 index 000000000..17e624686 --- /dev/null +++ b/src/tools/tool_can_use/rename_person_tool.py @@ -0,0 +1,103 @@ +from src.tools.tool_can_use.base_tool import BaseTool +from src.person_info.person_info import get_person_info_manager +from src.common.logger import get_logger + + +logger = get_logger("rename_person_tool") + + +class RenamePersonTool(BaseTool): + name = "rename_person" + description = ( + "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。你想给人改名,叫别人别的称呼,需要调用这个工具。" + ) + parameters = { + "type": "object", + "properties": { + "person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"}, + "message_content": { + "type": "string", + "description": "当前的聊天内容或特定要求,用于提供取名建议的上下文,尽可能详细。", + }, + }, + "required": ["person_name"], + } + + async def execute(self, function_args: dict): + """ + 执行取名工具逻辑 + + Args: + function_args (dict): 包含 'person_name' 和可选 'message_content' 的字典 + message_txt (str): 原始消息文本 (这里未使用,因为 message_content 更明确) + + Returns: + dict: 包含执行结果的字典 + """ + person_name_to_find = function_args.get("person_name") + request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 + + if not person_name_to_find: + return {"name": self.name, "content": "错误:必须提供需要重命名的用户昵称 (person_name)。"} + person_info_manager = get_person_info_manager() + try: + # 1. 根据昵称查找用户信息 + logger.debug(f"尝试根据昵称 '{person_name_to_find}' 查找用户...") + person_info = await person_info_manager.get_person_info_by_name(person_name_to_find) + + if not person_info: + logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。") + return { + "name": self.name, + "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。", + } + + person_id = person_info.get("person_id") + user_nickname = person_info.get("nickname") # 这是用户原始昵称 + user_cardname = person_info.get("user_cardname") + user_avatar = person_info.get("user_avatar") + + if not person_id: + logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") + return {"name": self.name, "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。"} + + # 2. 调用 qv_person_name 进行取名 + logger.debug( + f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'" + ) + result = await person_info_manager.qv_person_name( + person_id=person_id, + user_nickname=user_nickname, # type: ignore + user_cardname=user_cardname, # type: ignore + user_avatar=user_avatar, # type: ignore + request=request_context, + ) + + # 3. 处理结果 + if result and result.get("nickname"): + new_name = result["nickname"] + # reason = result.get("reason", "未提供理由") + logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") + + content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" + logger.info(content) + return {"name": self.name, "content": content} + else: + logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") + # 尝试从内存中获取可能已经更新的名字 + current_name = await person_info_manager.get_value(person_id, "person_name") + if current_name and current_name != person_name_to_find: + return { + "name": self.name, + "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。", + } + else: + return { + "name": self.name, + "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。", + } + + except Exception as e: + error_msg = f"重命名失败: {str(e)}" + logger.error(error_msg, exc_info=True) + return {"name": self.name, "content": error_msg} diff --git a/src/tools/tool_executor.py b/src/tools/tool_executor.py new file mode 100644 index 000000000..0f50ca2ab --- /dev/null +++ b/src/tools/tool_executor.py @@ -0,0 +1,407 @@ +from src.llm_models.utils_model import LLMRequest +from src.config.config import global_config +import time +from src.common.logger import get_logger +from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.tools.tool_use import ToolUser +from src.chat.utils.json_utils import process_llm_tool_calls +from typing import List, Dict, Tuple, Optional +from src.chat.message_receive.chat_stream import get_chat_manager + +logger = get_logger("tool_executor") + + +def init_tool_executor_prompt(): + """初始化工具执行器的提示词""" + tool_executor_prompt = """ +你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}。 +群里正在进行的聊天内容: +{chat_history} + +现在,{sender}发送了内容:{target_message},你想要回复ta。 +请仔细分析聊天内容,考虑以下几点: +1. 内容中是否包含需要查询信息的问题 +2. 是否有明确的工具使用指令 + +If you need to use a tool, please directly call the corresponding tool function. If you do not need to use any tool, simply output "No tool needed". +""" + Prompt(tool_executor_prompt, "tool_executor_prompt") + + +class ToolExecutor: + """独立的工具执行器组件 + + 可以直接输入聊天消息内容,自动判断并执行相应的工具,返回结构化的工具执行结果。 + """ + + def __init__(self, chat_id: str, enable_cache: bool = True, cache_ttl: int = 3): + """初始化工具执行器 + + Args: + executor_id: 执行器标识符,用于日志记录 + enable_cache: 是否启用缓存机制 + cache_ttl: 缓存生存时间(周期数) + """ + self.chat_id = chat_id + self.chat_stream = get_chat_manager().get_stream(self.chat_id) + self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]" + + self.llm_model = LLMRequest( + model=global_config.model.tool_use, + request_type="tool_executor", + ) + + # 初始化工具实例 + self.tool_instance = ToolUser() + + # 缓存配置 + self.enable_cache = enable_cache + self.cache_ttl = cache_ttl + self.tool_cache = {} # 格式: {cache_key: {"result": result, "ttl": ttl, "timestamp": timestamp}} + + logger.info(f"{self.log_prefix}工具执行器初始化完成,缓存{'启用' if enable_cache else '禁用'},TTL={cache_ttl}") + + async def execute_from_chat_message( + self, target_message: str, chat_history: str, sender: str, return_details: bool = False + ) -> Tuple[List[Dict], List[str], str]: + """从聊天消息执行工具 + + Args: + target_message: 目标消息内容 + chat_history: 聊天历史 + sender: 发送者 + return_details: 是否返回详细信息(使用的工具列表和提示词) + + Returns: + 如果return_details为False: List[Dict] - 工具执行结果列表 + 如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词) + """ + + # 首先检查缓存 + cache_key = self._generate_cache_key(target_message, chat_history, sender) + if cached_result := self._get_from_cache(cache_key): + logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行") + if not return_details: + return cached_result, [], "使用缓存结果" + + # 从缓存结果中提取工具名称 + used_tools = [result.get("tool_name", "unknown") for result in cached_result] + return cached_result, used_tools, "使用缓存结果" + + # 缓存未命中,执行工具调用 + # 获取可用工具 + tools = self.tool_instance._define_tools() + + # 获取当前时间 + time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + bot_name = global_config.bot.nickname + + # 构建工具调用提示词 + prompt = await global_prompt_manager.format_prompt( + "tool_executor_prompt", + target_message=target_message, + chat_history=chat_history, + sender=sender, + bot_name=bot_name, + time_now=time_now, + ) + + logger.debug(f"{self.log_prefix}开始LLM工具调用分析") + + # 调用LLM进行工具决策 + response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools) + + # 解析LLM响应 + if len(other_info) == 3: + reasoning_content, model_name, tool_calls = other_info + else: + reasoning_content, model_name = other_info + tool_calls = None + + # 执行工具调用 + tool_results, used_tools = await self._execute_tool_calls(tool_calls) + + # 缓存结果 + if tool_results: + self._set_cache(cache_key, tool_results) + + if used_tools: + logger.info(f"{self.log_prefix}工具执行完成,共执行{len(used_tools)}个工具: {used_tools}") + + if return_details: + return tool_results, used_tools, prompt + else: + return tool_results, [], "" + + async def _execute_tool_calls(self, tool_calls) -> Tuple[List[Dict], List[str]]: + """执行工具调用 + + Args: + tool_calls: LLM返回的工具调用列表 + + Returns: + Tuple[List[Dict], List[str]]: (工具执行结果列表, 使用的工具名称列表) + """ + tool_results = [] + used_tools = [] + + if not tool_calls: + logger.debug(f"{self.log_prefix}无需执行工具") + return tool_results, used_tools + + logger.info(f"{self.log_prefix}开始执行工具调用: {tool_calls}") + + # 处理工具调用 + success, valid_tool_calls, error_msg = process_llm_tool_calls(tool_calls) + + if not success: + logger.error(f"{self.log_prefix}工具调用解析失败: {error_msg}") + return tool_results, used_tools + + if not valid_tool_calls: + logger.debug(f"{self.log_prefix}无有效工具调用") + return tool_results, used_tools + + # 执行每个工具调用 + for tool_call in valid_tool_calls: + try: + tool_name = tool_call.get("name", "unknown_tool") + used_tools.append(tool_name) + + logger.debug(f"{self.log_prefix}执行工具: {tool_name}") + + # 执行工具 + result = await self.tool_instance.execute_tool_call(tool_call) + + if result: + tool_info = { + "type": result.get("type", "unknown_type"), + "id": result.get("id", f"tool_exec_{time.time()}"), + "content": result.get("content", ""), + "tool_name": tool_name, + "timestamp": time.time(), + } + tool_results.append(tool_info) + + logger.info(f"{self.log_prefix}工具{tool_name}执行成功,类型: {tool_info['type']}") + content = tool_info["content"] + if not isinstance(content, (str, list, tuple)): + content = str(content) + preview = content[:200] + logger.debug(f"{self.log_prefix}工具{tool_name}结果内容: {preview}...") + + except Exception as e: + logger.error(f"{self.log_prefix}工具{tool_name}执行失败: {e}") + # 添加错误信息到结果中 + error_info = { + "type": "tool_error", + "id": f"tool_error_{time.time()}", + "content": f"工具{tool_name}执行失败: {str(e)}", + "tool_name": tool_name, + "timestamp": time.time(), + } + tool_results.append(error_info) + + return tool_results, used_tools + + def _generate_cache_key(self, target_message: str, chat_history: str, sender: str) -> str: + """生成缓存键 + + Args: + target_message: 目标消息内容 + chat_history: 聊天历史 + sender: 发送者 + + Returns: + str: 缓存键 + """ + import hashlib + + # 使用消息内容和群聊状态生成唯一缓存键 + content = f"{target_message}_{chat_history}_{sender}" + return hashlib.md5(content.encode()).hexdigest() + + def _get_from_cache(self, cache_key: str) -> Optional[List[Dict]]: + """从缓存获取结果 + + Args: + cache_key: 缓存键 + + Returns: + Optional[List[Dict]]: 缓存的结果,如果不存在或过期则返回None + """ + if not self.enable_cache or cache_key not in self.tool_cache: + return None + + cache_item = self.tool_cache[cache_key] + if cache_item["ttl"] <= 0: + # 缓存过期,删除 + del self.tool_cache[cache_key] + logger.debug(f"{self.log_prefix}缓存过期,删除缓存键: {cache_key}") + return None + + # 减少TTL + cache_item["ttl"] -= 1 + logger.debug(f"{self.log_prefix}使用缓存结果,剩余TTL: {cache_item['ttl']}") + return cache_item["result"] + + def _set_cache(self, cache_key: str, result: List[Dict]): + """设置缓存 + + Args: + cache_key: 缓存键 + result: 要缓存的结果 + """ + if not self.enable_cache: + return + + self.tool_cache[cache_key] = {"result": result, "ttl": self.cache_ttl, "timestamp": time.time()} + logger.debug(f"{self.log_prefix}设置缓存,TTL: {self.cache_ttl}") + + def _cleanup_expired_cache(self): + """清理过期的缓存""" + if not self.enable_cache: + return + + expired_keys = [] + expired_keys.extend(cache_key for cache_key, cache_item in self.tool_cache.items() if cache_item["ttl"] <= 0) + for key in expired_keys: + del self.tool_cache[key] + + if expired_keys: + logger.debug(f"{self.log_prefix}清理了{len(expired_keys)}个过期缓存") + + def get_available_tools(self) -> List[str]: + """获取可用工具列表 + + Returns: + List[str]: 可用工具名称列表 + """ + tools = self.tool_instance._define_tools() + return [tool.get("function", {}).get("name", "unknown") for tool in tools] + + async def execute_specific_tool( + self, tool_name: str, tool_args: Dict, validate_args: bool = True + ) -> Optional[Dict]: + """直接执行指定工具 + + Args: + tool_name: 工具名称 + tool_args: 工具参数 + validate_args: 是否验证参数 + + Returns: + Optional[Dict]: 工具执行结果,失败时返回None + """ + try: + tool_call = {"name": tool_name, "arguments": tool_args} + + logger.info(f"{self.log_prefix}直接执行工具: {tool_name}") + + result = await self.tool_instance.execute_tool_call(tool_call) + + if result: + tool_info = { + "type": result.get("type", "unknown_type"), + "id": result.get("id", f"direct_tool_{time.time()}"), + "content": result.get("content", ""), + "tool_name": tool_name, + "timestamp": time.time(), + } + logger.info(f"{self.log_prefix}直接工具执行成功: {tool_name}") + return tool_info + + except Exception as e: + logger.error(f"{self.log_prefix}直接工具执行失败 {tool_name}: {e}") + + return None + + def clear_cache(self): + """清空所有缓存""" + if self.enable_cache: + cache_count = len(self.tool_cache) + self.tool_cache.clear() + logger.info(f"{self.log_prefix}清空了{cache_count}个缓存项") + + def get_cache_status(self) -> Dict: + """获取缓存状态信息 + + Returns: + Dict: 包含缓存统计信息的字典 + """ + if not self.enable_cache: + return {"enabled": False, "cache_count": 0} + + # 清理过期缓存 + self._cleanup_expired_cache() + + total_count = len(self.tool_cache) + ttl_distribution = {} + + for cache_item in self.tool_cache.values(): + ttl = cache_item["ttl"] + ttl_distribution[ttl] = ttl_distribution.get(ttl, 0) + 1 + + return { + "enabled": True, + "cache_count": total_count, + "cache_ttl": self.cache_ttl, + "ttl_distribution": ttl_distribution, + } + + def set_cache_config(self, enable_cache: Optional[bool] = None, cache_ttl: int = -1): + """动态修改缓存配置 + + Args: + enable_cache: 是否启用缓存 + cache_ttl: 缓存TTL + """ + if enable_cache is not None: + self.enable_cache = enable_cache + logger.info(f"{self.log_prefix}缓存状态修改为: {'启用' if enable_cache else '禁用'}") + + if cache_ttl > 0: + self.cache_ttl = cache_ttl + logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}") + + +# 初始化提示词 +init_tool_executor_prompt() + + +""" +使用示例: + +# 1. 基础使用 - 从聊天消息执行工具(启用缓存,默认TTL=3) +executor = ToolExecutor(executor_id="my_executor") +results, _, _ = await executor.execute_from_chat_message( + talking_message_str="今天天气怎么样?现在几点了?", + is_group_chat=False +) + +# 2. 禁用缓存的执行器 +no_cache_executor = ToolExecutor(executor_id="no_cache", enable_cache=False) + +# 3. 自定义缓存TTL +long_cache_executor = ToolExecutor(executor_id="long_cache", cache_ttl=10) + +# 4. 获取详细信息 +results, used_tools, prompt = await executor.execute_from_chat_message( + talking_message_str="帮我查询Python相关知识", + is_group_chat=False, + return_details=True +) + +# 5. 直接执行特定工具 +result = await executor.execute_specific_tool( + tool_name="get_knowledge", + tool_args={"query": "机器学习"} +) + +# 6. 缓存管理 +available_tools = executor.get_available_tools() +cache_status = executor.get_cache_status() # 查看缓存状态 +executor.clear_cache() # 清空缓存 +executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置 +""" diff --git a/src/tools/tool_use.py b/src/tools/tool_use.py new file mode 100644 index 000000000..6a8cd48a6 --- /dev/null +++ b/src/tools/tool_use.py @@ -0,0 +1,56 @@ +import json +from src.common.logger import get_logger +from src.tools.tool_can_use import get_all_tool_definitions, get_tool_instance + +logger = get_logger("tool_use") + + +class ToolUser: + @staticmethod + def _define_tools(): + """获取所有已注册工具的定义 + + Returns: + list: 工具定义列表 + """ + return get_all_tool_definitions() + + @staticmethod + async def execute_tool_call(tool_call): + # sourcery skip: use-assigned-variable + """执行特定的工具调用 + + 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) + 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 From 4ac487dd141bd79409a6735928933b143aebf679 Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 27 Jul 2025 00:24:40 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E5=B0=86ToolExecutor=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=BF=9Btool=5Fuse=EF=BC=8C=E9=A1=BA=E4=BE=BF=E6=94=B9?= =?UTF-8?q?=E4=BA=86=E4=B8=A4=E5=A4=84typing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/replyer/default_generator.py | 2 +- src/plugin_system/core/component_registry.py | 4 +- src/plugin_system/core/tool_executor.py | 407 ------------------- src/plugin_system/core/tool_use.py | 401 +++++++++++++++++- 4 files changed, 403 insertions(+), 411 deletions(-) delete mode 100644 src/plugin_system/core/tool_executor.py diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 15434d85f..e86b3fd23 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -139,7 +139,7 @@ class DefaultReplyer: self.memory_activator = MemoryActivator() self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id) - from src.plugin_system.core.tool_executor import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 + from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=True, cache_ttl=3) def _select_weighted_model_config(self) -> Dict[str, Any]: diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index d40d0f624..832739f1d 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -52,8 +52,8 @@ class ComponentRegistry: """编译后的正则 -> command名""" # 工具特定注册表 - self._tool_registry: Dict[str, BaseTool] = {} # 工具名 -> 工具类 - self._llm_available_tools: Dict[str, str] = {} # llm可用的工具名 -> 描述 + self._tool_registry: Dict[str, Type[BaseTool]] = {} # 工具名 -> 工具类 + self._llm_available_tools: Dict[str, Type[BaseTool]] = {} # llm可用的工具名 -> 工具类 # EventHandler特定注册表 self._event_handler_registry: Dict[str, Type[BaseEventHandler]] = {} diff --git a/src/plugin_system/core/tool_executor.py b/src/plugin_system/core/tool_executor.py deleted file mode 100644 index 45fe2a5fb..000000000 --- a/src/plugin_system/core/tool_executor.py +++ /dev/null @@ -1,407 +0,0 @@ -from src.llm_models.utils_model import LLMRequest -from src.config.config import global_config -import time -from src.common.logger import get_logger -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from .tool_use import tool_user -from src.chat.utils.json_utils import process_llm_tool_calls -from typing import List, Dict, Tuple, Optional -from src.chat.message_receive.chat_stream import get_chat_manager - -logger = get_logger("tool_executor") - - -def init_tool_executor_prompt(): - """初始化工具执行器的提示词""" - tool_executor_prompt = """ -你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}。 -群里正在进行的聊天内容: -{chat_history} - -现在,{sender}发送了内容:{target_message},你想要回复ta。 -请仔细分析聊天内容,考虑以下几点: -1. 内容中是否包含需要查询信息的问题 -2. 是否有明确的工具使用指令 - -If you need to use a tool, please directly call the corresponding tool function. If you do not need to use any tool, simply output "No tool needed". -""" - Prompt(tool_executor_prompt, "tool_executor_prompt") - - -class ToolExecutor: - """独立的工具执行器组件 - - 可以直接输入聊天消息内容,自动判断并执行相应的工具,返回结构化的工具执行结果。 - """ - - def __init__(self, chat_id: str, enable_cache: bool = True, cache_ttl: int = 3): - """初始化工具执行器 - - Args: - executor_id: 执行器标识符,用于日志记录 - enable_cache: 是否启用缓存机制 - cache_ttl: 缓存生存时间(周期数) - """ - self.chat_id = chat_id - self.chat_stream = get_chat_manager().get_stream(self.chat_id) - self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]" - - self.llm_model = LLMRequest( - model=global_config.model.tool_use, - request_type="tool_executor", - ) - - # 初始化工具实例 - self.tool_instance = tool_user - - # 缓存配置 - self.enable_cache = enable_cache - self.cache_ttl = cache_ttl - self.tool_cache = {} # 格式: {cache_key: {"result": result, "ttl": ttl, "timestamp": timestamp}} - - logger.info(f"{self.log_prefix}工具执行器初始化完成,缓存{'启用' if enable_cache else '禁用'},TTL={cache_ttl}") - - async def execute_from_chat_message( - self, target_message: str, chat_history: str, sender: str, return_details: bool = False - ) -> Tuple[List[Dict], List[str], str]: - """从聊天消息执行工具 - - Args: - target_message: 目标消息内容 - chat_history: 聊天历史 - sender: 发送者 - return_details: 是否返回详细信息(使用的工具列表和提示词) - - Returns: - 如果return_details为False: List[Dict] - 工具执行结果列表 - 如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词) - """ - - # 首先检查缓存 - cache_key = self._generate_cache_key(target_message, chat_history, sender) - if cached_result := self._get_from_cache(cache_key): - logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行") - if not return_details: - return cached_result, [], "使用缓存结果" - - # 从缓存结果中提取工具名称 - used_tools = [result.get("tool_name", "unknown") for result in cached_result] - return cached_result, used_tools, "使用缓存结果" - - # 缓存未命中,执行工具调用 - # 获取可用工具 - tools = self.tool_instance._define_tools() - - # 获取当前时间 - time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - - bot_name = global_config.bot.nickname - - # 构建工具调用提示词 - prompt = await global_prompt_manager.format_prompt( - "tool_executor_prompt", - target_message=target_message, - chat_history=chat_history, - sender=sender, - bot_name=bot_name, - time_now=time_now, - ) - - logger.debug(f"{self.log_prefix}开始LLM工具调用分析") - - # 调用LLM进行工具决策 - response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools) - - # 解析LLM响应 - if len(other_info) == 3: - reasoning_content, model_name, tool_calls = other_info - else: - reasoning_content, model_name = other_info - tool_calls = None - - # 执行工具调用 - tool_results, used_tools = await self._execute_tool_calls(tool_calls) - - # 缓存结果 - if tool_results: - self._set_cache(cache_key, tool_results) - - if used_tools: - logger.info(f"{self.log_prefix}工具执行完成,共执行{len(used_tools)}个工具: {used_tools}") - - if return_details: - return tool_results, used_tools, prompt - else: - return tool_results, [], "" - - async def _execute_tool_calls(self, tool_calls) -> Tuple[List[Dict], List[str]]: - """执行工具调用 - - Args: - tool_calls: LLM返回的工具调用列表 - - Returns: - Tuple[List[Dict], List[str]]: (工具执行结果列表, 使用的工具名称列表) - """ - tool_results = [] - used_tools = [] - - if not tool_calls: - logger.debug(f"{self.log_prefix}无需执行工具") - return tool_results, used_tools - - logger.info(f"{self.log_prefix}开始执行工具调用: {tool_calls}") - - # 处理工具调用 - success, valid_tool_calls, error_msg = process_llm_tool_calls(tool_calls) - - if not success: - logger.error(f"{self.log_prefix}工具调用解析失败: {error_msg}") - return tool_results, used_tools - - if not valid_tool_calls: - logger.debug(f"{self.log_prefix}无有效工具调用") - return tool_results, used_tools - - # 执行每个工具调用 - for tool_call in valid_tool_calls: - try: - tool_name = tool_call.get("name", "unknown_tool") - used_tools.append(tool_name) - - logger.debug(f"{self.log_prefix}执行工具: {tool_name}") - - # 执行工具 - result = await self.tool_instance.execute_tool_call(tool_call) - - if result: - tool_info = { - "type": result.get("type", "unknown_type"), - "id": result.get("id", f"tool_exec_{time.time()}"), - "content": result.get("content", ""), - "tool_name": tool_name, - "timestamp": time.time(), - } - tool_results.append(tool_info) - - logger.info(f"{self.log_prefix}工具{tool_name}执行成功,类型: {tool_info['type']}") - content = tool_info["content"] - if not isinstance(content, (str, list, tuple)): - content = str(content) - preview = content[:200] - logger.debug(f"{self.log_prefix}工具{tool_name}结果内容: {preview}...") - - except Exception as e: - logger.error(f"{self.log_prefix}工具{tool_name}执行失败: {e}") - # 添加错误信息到结果中 - error_info = { - "type": "tool_error", - "id": f"tool_error_{time.time()}", - "content": f"工具{tool_name}执行失败: {str(e)}", - "tool_name": tool_name, - "timestamp": time.time(), - } - tool_results.append(error_info) - - return tool_results, used_tools - - def _generate_cache_key(self, target_message: str, chat_history: str, sender: str) -> str: - """生成缓存键 - - Args: - target_message: 目标消息内容 - chat_history: 聊天历史 - sender: 发送者 - - Returns: - str: 缓存键 - """ - import hashlib - - # 使用消息内容和群聊状态生成唯一缓存键 - content = f"{target_message}_{chat_history}_{sender}" - return hashlib.md5(content.encode()).hexdigest() - - def _get_from_cache(self, cache_key: str) -> Optional[List[Dict]]: - """从缓存获取结果 - - Args: - cache_key: 缓存键 - - Returns: - Optional[List[Dict]]: 缓存的结果,如果不存在或过期则返回None - """ - if not self.enable_cache or cache_key not in self.tool_cache: - return None - - cache_item = self.tool_cache[cache_key] - if cache_item["ttl"] <= 0: - # 缓存过期,删除 - del self.tool_cache[cache_key] - logger.debug(f"{self.log_prefix}缓存过期,删除缓存键: {cache_key}") - return None - - # 减少TTL - cache_item["ttl"] -= 1 - logger.debug(f"{self.log_prefix}使用缓存结果,剩余TTL: {cache_item['ttl']}") - return cache_item["result"] - - def _set_cache(self, cache_key: str, result: List[Dict]): - """设置缓存 - - Args: - cache_key: 缓存键 - result: 要缓存的结果 - """ - if not self.enable_cache: - return - - self.tool_cache[cache_key] = {"result": result, "ttl": self.cache_ttl, "timestamp": time.time()} - logger.debug(f"{self.log_prefix}设置缓存,TTL: {self.cache_ttl}") - - def _cleanup_expired_cache(self): - """清理过期的缓存""" - if not self.enable_cache: - return - - expired_keys = [] - expired_keys.extend(cache_key for cache_key, cache_item in self.tool_cache.items() if cache_item["ttl"] <= 0) - for key in expired_keys: - del self.tool_cache[key] - - if expired_keys: - logger.debug(f"{self.log_prefix}清理了{len(expired_keys)}个过期缓存") - - def get_available_tools(self) -> List[str]: - """获取可用工具列表 - - Returns: - List[str]: 可用工具名称列表 - """ - tools = self.tool_instance._define_tools() - return [tool.get("function", {}).get("name", "unknown") for tool in tools] - - async def execute_specific_tool( - self, tool_name: str, tool_args: Dict, validate_args: bool = True - ) -> Optional[Dict]: - """直接执行指定工具 - - Args: - tool_name: 工具名称 - tool_args: 工具参数 - validate_args: 是否验证参数 - - Returns: - Optional[Dict]: 工具执行结果,失败时返回None - """ - try: - tool_call = {"name": tool_name, "arguments": tool_args} - - logger.info(f"{self.log_prefix}直接执行工具: {tool_name}") - - result = await self.tool_instance.execute_tool_call(tool_call) - - if result: - tool_info = { - "type": result.get("type", "unknown_type"), - "id": result.get("id", f"direct_tool_{time.time()}"), - "content": result.get("content", ""), - "tool_name": tool_name, - "timestamp": time.time(), - } - logger.info(f"{self.log_prefix}直接工具执行成功: {tool_name}") - return tool_info - - except Exception as e: - logger.error(f"{self.log_prefix}直接工具执行失败 {tool_name}: {e}") - - return None - - def clear_cache(self): - """清空所有缓存""" - if self.enable_cache: - cache_count = len(self.tool_cache) - self.tool_cache.clear() - logger.info(f"{self.log_prefix}清空了{cache_count}个缓存项") - - def get_cache_status(self) -> Dict: - """获取缓存状态信息 - - Returns: - Dict: 包含缓存统计信息的字典 - """ - if not self.enable_cache: - return {"enabled": False, "cache_count": 0} - - # 清理过期缓存 - self._cleanup_expired_cache() - - total_count = len(self.tool_cache) - ttl_distribution = {} - - for cache_item in self.tool_cache.values(): - ttl = cache_item["ttl"] - ttl_distribution[ttl] = ttl_distribution.get(ttl, 0) + 1 - - return { - "enabled": True, - "cache_count": total_count, - "cache_ttl": self.cache_ttl, - "ttl_distribution": ttl_distribution, - } - - def set_cache_config(self, enable_cache: Optional[bool] = None, cache_ttl: int = -1): - """动态修改缓存配置 - - Args: - enable_cache: 是否启用缓存 - cache_ttl: 缓存TTL - """ - if enable_cache is not None: - self.enable_cache = enable_cache - logger.info(f"{self.log_prefix}缓存状态修改为: {'启用' if enable_cache else '禁用'}") - - if cache_ttl > 0: - self.cache_ttl = cache_ttl - logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}") - - -# 初始化提示词 -init_tool_executor_prompt() - - -""" -使用示例: - -# 1. 基础使用 - 从聊天消息执行工具(启用缓存,默认TTL=3) -executor = ToolExecutor(executor_id="my_executor") -results, _, _ = await executor.execute_from_chat_message( - talking_message_str="今天天气怎么样?现在几点了?", - is_group_chat=False -) - -# 2. 禁用缓存的执行器 -no_cache_executor = ToolExecutor(executor_id="no_cache", enable_cache=False) - -# 3. 自定义缓存TTL -long_cache_executor = ToolExecutor(executor_id="long_cache", cache_ttl=10) - -# 4. 获取详细信息 -results, used_tools, prompt = await executor.execute_from_chat_message( - talking_message_str="帮我查询Python相关知识", - is_group_chat=False, - return_details=True -) - -# 5. 直接执行特定工具 -result = await executor.execute_specific_tool( - tool_name="get_knowledge", - tool_args={"query": "机器学习"} -) - -# 6. 缓存管理 -available_tools = executor.get_available_tools() -cache_status = executor.get_cache_status() # 查看缓存状态 -executor.clear_cache() # 清空缓存 -executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置 -""" diff --git a/src/plugin_system/core/tool_use.py b/src/plugin_system/core/tool_use.py index 9dd456ae3..bec600190 100644 --- a/src/plugin_system/core/tool_use.py +++ b/src/plugin_system/core/tool_use.py @@ -1,9 +1,408 @@ import json -from src.common.logger import get_logger +import time +from typing import List, Dict, Tuple, Optional from src.plugin_system.apis.tool_api import get_llm_available_tool_definitions,get_tool_instance +from src.llm_models.utils_model import LLMRequest +from src.config.config import global_config +from src.chat.utils.prompt_builder import Prompt, global_prompt_manager +from src.chat.utils.json_utils import process_llm_tool_calls +from src.chat.message_receive.chat_stream import get_chat_manager +from src.common.logger import get_logger logger = get_logger("tool_use") +def init_tool_executor_prompt(): + """初始化工具执行器的提示词""" + tool_executor_prompt = """ +你是一个专门执行工具的助手。你的名字是{bot_name}。现在是{time_now}。 +群里正在进行的聊天内容: +{chat_history} + +现在,{sender}发送了内容:{target_message},你想要回复ta。 +请仔细分析聊天内容,考虑以下几点: +1. 内容中是否包含需要查询信息的问题 +2. 是否有明确的工具使用指令 + +If you need to use a tool, please directly call the corresponding tool function. If you do not need to use any tool, simply output "No tool needed". +""" + Prompt(tool_executor_prompt, "tool_executor_prompt") + +# 初始化提示词 +init_tool_executor_prompt() + +class ToolExecutor: + """独立的工具执行器组件 + + 可以直接输入聊天消息内容,自动判断并执行相应的工具,返回结构化的工具执行结果。 + """ + + def __init__(self, chat_id: str, enable_cache: bool = True, cache_ttl: int = 3): + """初始化工具执行器 + + Args: + executor_id: 执行器标识符,用于日志记录 + enable_cache: 是否启用缓存机制 + cache_ttl: 缓存生存时间(周期数) + """ + self.chat_id = chat_id + self.chat_stream = get_chat_manager().get_stream(self.chat_id) + self.log_prefix = f"[{get_chat_manager().get_stream_name(self.chat_id) or self.chat_id}]" + + self.llm_model = LLMRequest( + model=global_config.model.tool_use, + request_type="tool_executor", + ) + + # 初始化工具实例 + self.tool_instance = ToolUser() + + # 缓存配置 + self.enable_cache = enable_cache + self.cache_ttl = cache_ttl + self.tool_cache = {} # 格式: {cache_key: {"result": result, "ttl": ttl, "timestamp": timestamp}} + + logger.info(f"{self.log_prefix}工具执行器初始化完成,缓存{'启用' if enable_cache else '禁用'},TTL={cache_ttl}") + + async def execute_from_chat_message( + self, target_message: str, chat_history: str, sender: str, return_details: bool = False + ) -> Tuple[List[Dict], List[str], str]: + """从聊天消息执行工具 + + Args: + target_message: 目标消息内容 + chat_history: 聊天历史 + sender: 发送者 + return_details: 是否返回详细信息(使用的工具列表和提示词) + + Returns: + 如果return_details为False: List[Dict] - 工具执行结果列表 + 如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词) + """ + + # 首先检查缓存 + cache_key = self._generate_cache_key(target_message, chat_history, sender) + if cached_result := self._get_from_cache(cache_key): + logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行") + if not return_details: + return cached_result, [], "使用缓存结果" + + # 从缓存结果中提取工具名称 + used_tools = [result.get("tool_name", "unknown") for result in cached_result] + return cached_result, used_tools, "使用缓存结果" + + # 缓存未命中,执行工具调用 + # 获取可用工具 + tools = self.tool_instance._define_tools() + + # 获取当前时间 + time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + + bot_name = global_config.bot.nickname + + # 构建工具调用提示词 + prompt = await global_prompt_manager.format_prompt( + "tool_executor_prompt", + target_message=target_message, + chat_history=chat_history, + sender=sender, + bot_name=bot_name, + time_now=time_now, + ) + + logger.debug(f"{self.log_prefix}开始LLM工具调用分析") + + # 调用LLM进行工具决策 + response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools) + + # 解析LLM响应 + if len(other_info) == 3: + reasoning_content, model_name, tool_calls = other_info + else: + reasoning_content, model_name = other_info + tool_calls = None + + # 执行工具调用 + tool_results, used_tools = await self._execute_tool_calls(tool_calls) + + # 缓存结果 + if tool_results: + self._set_cache(cache_key, tool_results) + + if used_tools: + logger.info(f"{self.log_prefix}工具执行完成,共执行{len(used_tools)}个工具: {used_tools}") + + if return_details: + return tool_results, used_tools, prompt + else: + return tool_results, [], "" + + async def _execute_tool_calls(self, tool_calls) -> Tuple[List[Dict], List[str]]: + """执行工具调用 + + Args: + tool_calls: LLM返回的工具调用列表 + + Returns: + Tuple[List[Dict], List[str]]: (工具执行结果列表, 使用的工具名称列表) + """ + tool_results = [] + used_tools = [] + + if not tool_calls: + logger.debug(f"{self.log_prefix}无需执行工具") + return tool_results, used_tools + + logger.info(f"{self.log_prefix}开始执行工具调用: {tool_calls}") + + # 处理工具调用 + success, valid_tool_calls, error_msg = process_llm_tool_calls(tool_calls) + + if not success: + logger.error(f"{self.log_prefix}工具调用解析失败: {error_msg}") + return tool_results, used_tools + + if not valid_tool_calls: + logger.debug(f"{self.log_prefix}无有效工具调用") + return tool_results, used_tools + + # 执行每个工具调用 + for tool_call in valid_tool_calls: + try: + tool_name = tool_call.get("name", "unknown_tool") + used_tools.append(tool_name) + + logger.debug(f"{self.log_prefix}执行工具: {tool_name}") + + # 执行工具 + result = await self.tool_instance.execute_tool_call(tool_call) + + if result: + tool_info = { + "type": result.get("type", "unknown_type"), + "id": result.get("id", f"tool_exec_{time.time()}"), + "content": result.get("content", ""), + "tool_name": tool_name, + "timestamp": time.time(), + } + tool_results.append(tool_info) + + logger.info(f"{self.log_prefix}工具{tool_name}执行成功,类型: {tool_info['type']}") + content = tool_info["content"] + if not isinstance(content, (str, list, tuple)): + content = str(content) + preview = content[:200] + logger.debug(f"{self.log_prefix}工具{tool_name}结果内容: {preview}...") + + except Exception as e: + logger.error(f"{self.log_prefix}工具{tool_name}执行失败: {e}") + # 添加错误信息到结果中 + error_info = { + "type": "tool_error", + "id": f"tool_error_{time.time()}", + "content": f"工具{tool_name}执行失败: {str(e)}", + "tool_name": tool_name, + "timestamp": time.time(), + } + tool_results.append(error_info) + + return tool_results, used_tools + + def _generate_cache_key(self, target_message: str, chat_history: str, sender: str) -> str: + """生成缓存键 + + Args: + target_message: 目标消息内容 + chat_history: 聊天历史 + sender: 发送者 + + Returns: + str: 缓存键 + """ + import hashlib + + # 使用消息内容和群聊状态生成唯一缓存键 + content = f"{target_message}_{chat_history}_{sender}" + return hashlib.md5(content.encode()).hexdigest() + + def _get_from_cache(self, cache_key: str) -> Optional[List[Dict]]: + """从缓存获取结果 + + Args: + cache_key: 缓存键 + + Returns: + Optional[List[Dict]]: 缓存的结果,如果不存在或过期则返回None + """ + if not self.enable_cache or cache_key not in self.tool_cache: + return None + + cache_item = self.tool_cache[cache_key] + if cache_item["ttl"] <= 0: + # 缓存过期,删除 + del self.tool_cache[cache_key] + logger.debug(f"{self.log_prefix}缓存过期,删除缓存键: {cache_key}") + return None + + # 减少TTL + cache_item["ttl"] -= 1 + logger.debug(f"{self.log_prefix}使用缓存结果,剩余TTL: {cache_item['ttl']}") + return cache_item["result"] + + def _set_cache(self, cache_key: str, result: List[Dict]): + """设置缓存 + + Args: + cache_key: 缓存键 + result: 要缓存的结果 + """ + if not self.enable_cache: + return + + self.tool_cache[cache_key] = {"result": result, "ttl": self.cache_ttl, "timestamp": time.time()} + logger.debug(f"{self.log_prefix}设置缓存,TTL: {self.cache_ttl}") + + def _cleanup_expired_cache(self): + """清理过期的缓存""" + if not self.enable_cache: + return + + expired_keys = [] + expired_keys.extend(cache_key for cache_key, cache_item in self.tool_cache.items() if cache_item["ttl"] <= 0) + for key in expired_keys: + del self.tool_cache[key] + + if expired_keys: + logger.debug(f"{self.log_prefix}清理了{len(expired_keys)}个过期缓存") + + def get_available_tools(self) -> List[str]: + """获取可用工具列表 + + Returns: + List[str]: 可用工具名称列表 + """ + tools = self.tool_instance._define_tools() + return [tool.get("function", {}).get("name", "unknown") for tool in tools] + + async def execute_specific_tool( + self, tool_name: str, tool_args: Dict, validate_args: bool = True + ) -> Optional[Dict]: + """直接执行指定工具 + + Args: + tool_name: 工具名称 + tool_args: 工具参数 + validate_args: 是否验证参数 + + Returns: + Optional[Dict]: 工具执行结果,失败时返回None + """ + try: + tool_call = {"name": tool_name, "arguments": tool_args} + + logger.info(f"{self.log_prefix}直接执行工具: {tool_name}") + + result = await self.tool_instance.execute_tool_call(tool_call) + + if result: + tool_info = { + "type": result.get("type", "unknown_type"), + "id": result.get("id", f"direct_tool_{time.time()}"), + "content": result.get("content", ""), + "tool_name": tool_name, + "timestamp": time.time(), + } + logger.info(f"{self.log_prefix}直接工具执行成功: {tool_name}") + return tool_info + + except Exception as e: + logger.error(f"{self.log_prefix}直接工具执行失败 {tool_name}: {e}") + + return None + + def clear_cache(self): + """清空所有缓存""" + if self.enable_cache: + cache_count = len(self.tool_cache) + self.tool_cache.clear() + logger.info(f"{self.log_prefix}清空了{cache_count}个缓存项") + + def get_cache_status(self) -> Dict: + """获取缓存状态信息 + + Returns: + Dict: 包含缓存统计信息的字典 + """ + if not self.enable_cache: + return {"enabled": False, "cache_count": 0} + + # 清理过期缓存 + self._cleanup_expired_cache() + + total_count = len(self.tool_cache) + ttl_distribution = {} + + for cache_item in self.tool_cache.values(): + ttl = cache_item["ttl"] + ttl_distribution[ttl] = ttl_distribution.get(ttl, 0) + 1 + + return { + "enabled": True, + "cache_count": total_count, + "cache_ttl": self.cache_ttl, + "ttl_distribution": ttl_distribution, + } + + def set_cache_config(self, enable_cache: Optional[bool] = None, cache_ttl: int = -1): + """动态修改缓存配置 + + Args: + enable_cache: 是否启用缓存 + cache_ttl: 缓存TTL + """ + if enable_cache is not None: + self.enable_cache = enable_cache + logger.info(f"{self.log_prefix}缓存状态修改为: {'启用' if enable_cache else '禁用'}") + + if cache_ttl > 0: + self.cache_ttl = cache_ttl + logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}") + +""" +ToolExecutor使用示例: + +# 1. 基础使用 - 从聊天消息执行工具(启用缓存,默认TTL=3) +executor = ToolExecutor(executor_id="my_executor") +results, _, _ = await executor.execute_from_chat_message( + talking_message_str="今天天气怎么样?现在几点了?", + is_group_chat=False +) + +# 2. 禁用缓存的执行器 +no_cache_executor = ToolExecutor(executor_id="no_cache", enable_cache=False) + +# 3. 自定义缓存TTL +long_cache_executor = ToolExecutor(executor_id="long_cache", cache_ttl=10) + +# 4. 获取详细信息 +results, used_tools, prompt = await executor.execute_from_chat_message( + talking_message_str="帮我查询Python相关知识", + is_group_chat=False, + return_details=True +) + +# 5. 直接执行特定工具 +result = await executor.execute_specific_tool( + tool_name="get_knowledge", + tool_args={"query": "机器学习"} +) + +# 6. 缓存管理 +available_tools = executor.get_available_tools() +cache_status = executor.get_cache_status() # 查看缓存状态 +executor.clear_cache() # 清空缓存 +executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置 +""" + class ToolUser: @staticmethod