feat: Introduce unified tooling system for plugins and MCP

- Added a new `tooling` module to define a unified model for tool declarations, invocations, and execution results, facilitating compatibility between plugins, legacy actions, and MCP tools.
- Implemented `ToolProvider` interface for various tool providers including built-in tools, MCP tools, and plugin runtime tools.
- Enhanced `MCPManager` and `MCPConnection` to support unified tool invocation and execution results.
- Updated `ComponentRegistry` and related classes to accommodate the new tool specifications and descriptions.
- Refactored existing components to utilize the new tooling system, ensuring backward compatibility with legacy actions.
- Improved error handling and logging for tool invocations across different providers.
This commit is contained in:
DrSmoothl
2026-03-30 23:11:56 +08:00
parent 898b693fe0
commit dc2bf02a42
35 changed files with 1663 additions and 6756 deletions

View File

@@ -1,4 +1,4 @@
"""Host-side ComponentRegistry
"""Host 侧组件注册表。
对齐旧系统 component_registry.py 的核心能力:
- 按类型注册组件action / command / tool / event_handler / hook_handler / message_gateway
@@ -16,6 +16,7 @@ import contextlib
import re
from src.common.logger import get_logger
from src.core.tooling import build_tool_detailed_description
logger = get_logger("plugin_runtime.host.component_registry")
@@ -89,11 +90,81 @@ class ToolEntry(ComponentEntry):
"""Tool 组件条目"""
def __init__(self, name: str, component_type: str, plugin_id: str, metadata: Dict[str, Any]) -> None:
self.description: str = metadata.get("description", "")
self.description: str = str(metadata.get("description", "") or "").strip()
self.brief_description: str = str(
metadata.get("brief_description", self.description) or self.description or f"工具 {name}"
).strip()
self.parameters: List[Dict[str, Any]] = metadata.get("parameters", [])
self.parameters_raw: Dict[str, Any] | List[Dict[str, Any]] = metadata.get("parameters_raw", {})
detailed_description = str(metadata.get("detailed_description", "") or "").strip()
self.detailed_description: str = detailed_description
self.invoke_method: str = str(metadata.get("invoke_method", "plugin.invoke_tool") or "plugin.invoke_tool").strip()
self.legacy_component_type: str = str(metadata.get("legacy_component_type", "") or "").strip()
super().__init__(name, component_type, plugin_id, metadata)
if not self.detailed_description:
parameters_schema = self._get_parameters_schema()
self.detailed_description = build_tool_detailed_description(parameters_schema)
def _get_parameters_schema(self) -> Dict[str, Any] | None:
"""获取当前工具条目的对象级参数 Schema。
Returns:
Dict[str, Any] | None: 归一化后的参数 Schema。
"""
if isinstance(self.parameters_raw, dict) and self.parameters_raw:
if self.parameters_raw.get("type") == "object" or "properties" in self.parameters_raw:
return dict(self.parameters_raw)
required_names: List[str] = []
normalized_properties: Dict[str, Any] = {}
for property_name, property_schema in self.parameters_raw.items():
if not isinstance(property_schema, dict):
continue
property_schema_copy = dict(property_schema)
if bool(property_schema_copy.pop("required", False)):
required_names.append(str(property_name))
normalized_properties[str(property_name)] = property_schema_copy
schema: Dict[str, Any] = {
"type": "object",
"properties": normalized_properties,
}
if required_names:
schema["required"] = required_names
return schema
if isinstance(self.parameters, list) and self.parameters:
properties: Dict[str, Any] = {}
required_names: List[str] = []
for parameter in self.parameters:
if not isinstance(parameter, dict):
continue
parameter_name = str(parameter.get("name", "") or "").strip()
if not parameter_name:
continue
if bool(parameter.get("required", False)):
required_names.append(parameter_name)
properties[parameter_name] = {
key: value
for key, value in parameter.items()
if key not in {"name", "required", "param_type"}
}
properties[parameter_name]["type"] = str(
parameter.get("type", parameter.get("param_type", "string")) or "string"
)
schema = {
"type": "object",
"properties": properties,
}
if required_names:
schema["required"] = required_names
return schema
return None
class EventHandlerEntry(ComponentEntry):
"""EventHandler 组件条目"""
@@ -282,7 +353,7 @@ class MessageGatewayEntry(ComponentEntry):
class ComponentRegistry:
"""Host-side 组件注册表
"""Host 组件注册表
由 Supervisor 在收到 plugin.register_components 时调用。
供业务层查询可用组件、匹配命令、调度 action/event 等。
@@ -300,6 +371,86 @@ class ComponentRegistry:
# 按插件索引
self._by_plugin: Dict[str, List[ComponentEntry]] = {}
@staticmethod
def _convert_action_metadata_to_tool_metadata(
name: str,
metadata: Dict[str, Any],
) -> Dict[str, Any]:
"""将旧 Action 元数据转换为统一 Tool 元数据。
Args:
name: 组件名称。
metadata: Action 原始元数据。
Returns:
Dict[str, Any]: 转换后的 Tool 元数据。
"""
action_parameters = metadata.get("action_parameters")
parameters_schema: Dict[str, Any] | None = None
if isinstance(action_parameters, dict) and action_parameters:
properties: Dict[str, Any] = {}
for parameter_name, parameter_description in action_parameters.items():
normalized_name = str(parameter_name or "").strip()
if not normalized_name:
continue
properties[normalized_name] = {
"type": "string",
"description": str(parameter_description or "").strip() or "兼容旧 Action 参数",
}
if properties:
parameters_schema = {
"type": "object",
"properties": properties,
}
detailed_parts: List[str] = []
if parameters_schema is not None:
parameter_description = build_tool_detailed_description(parameters_schema)
if parameter_description:
detailed_parts.append(parameter_description)
action_require = [
str(item).strip()
for item in (metadata.get("action_require") or [])
if str(item).strip()
]
if action_require:
detailed_parts.append("使用建议:\n" + "\n".join(f"- {item}" for item in action_require))
associated_types = [
str(item).strip()
for item in (metadata.get("associated_types") or [])
if str(item).strip()
]
if associated_types:
detailed_parts.append(f"适用消息类型:{''.join(associated_types)}")
activation_type = str(metadata.get("activation_type", "always") or "always").strip()
activation_keywords = [
str(item).strip()
for item in (metadata.get("activation_keywords") or [])
if str(item).strip()
]
activation_lines = [f"兼容旧 Action 激活方式:{activation_type}"]
if activation_keywords:
activation_lines.append(f"激活关键词:{''.join(activation_keywords)}")
if str(metadata.get("action_prompt", "") or "").strip():
activation_lines.append(f"原始 Action 提示语:{str(metadata['action_prompt']).strip()}")
detailed_parts.append("\n".join(activation_lines))
brief_description = str(metadata.get("brief_description", metadata.get("description", "") or f"工具 {name}")).strip()
return {
**metadata,
"description": brief_description,
"brief_description": brief_description,
"detailed_description": "\n\n".join(part for part in detailed_parts if part).strip(),
"parameters_raw": parameters_schema or {},
"invoke_method": "plugin.invoke_action",
"legacy_action": True,
"legacy_component_type": "ACTION",
}
@staticmethod
def _normalize_component_type(component_type: str) -> ComponentTypes:
"""规范化组件类型输入。
@@ -338,18 +489,20 @@ class ComponentRegistry:
"""
try:
normalized_type = self._normalize_component_type(component_type)
normalized_metadata = dict(metadata)
if normalized_type == ComponentTypes.ACTION:
comp = ActionEntry(name, normalized_type.value, plugin_id, metadata)
normalized_metadata = self._convert_action_metadata_to_tool_metadata(name, normalized_metadata)
comp = ToolEntry(name, ComponentTypes.TOOL.value, plugin_id, normalized_metadata)
elif normalized_type == ComponentTypes.COMMAND:
comp = CommandEntry(name, normalized_type.value, plugin_id, metadata)
comp = CommandEntry(name, normalized_type.value, plugin_id, normalized_metadata)
elif normalized_type == ComponentTypes.TOOL:
comp = ToolEntry(name, normalized_type.value, plugin_id, metadata)
comp = ToolEntry(name, normalized_type.value, plugin_id, normalized_metadata)
elif normalized_type == ComponentTypes.EVENT_HANDLER:
comp = EventHandlerEntry(name, normalized_type.value, plugin_id, metadata)
comp = EventHandlerEntry(name, normalized_type.value, plugin_id, normalized_metadata)
elif normalized_type == ComponentTypes.HOOK_HANDLER:
comp = HookHandlerEntry(name, normalized_type.value, plugin_id, metadata)
comp = HookHandlerEntry(name, normalized_type.value, plugin_id, normalized_metadata)
elif normalized_type == ComponentTypes.MESSAGE_GATEWAY:
comp = MessageGatewayEntry(name, normalized_type.value, plugin_id, metadata)
comp = MessageGatewayEntry(name, normalized_type.value, plugin_id, normalized_metadata)
else:
raise ValueError(f"组件类型 {component_type} 不存在")
except ValueError: