477 lines
12 KiB
Markdown
477 lines
12 KiB
Markdown
# Phase T.2:工具注册中心
|
||
|
||
日期:2026-04-04
|
||
状态:待开始
|
||
依赖:T.1(待完成)
|
||
|
||
---
|
||
|
||
## 1. 本阶段目的
|
||
|
||
建立 Jarvis 的工具注册中心:
|
||
|
||
- 工具动态发现
|
||
- 工具注册与管理
|
||
- 工具描述生成
|
||
- 调用统计与监控
|
||
|
||
---
|
||
|
||
## 2. 工具注册中心架构
|
||
|
||
### 2.1 核心类
|
||
|
||
```python
|
||
# tools/registry.py
|
||
from typing import Dict, List, Optional, Callable
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
import asyncio
|
||
|
||
|
||
@dataclass
|
||
class ToolMetadata:
|
||
"""工具元数据"""
|
||
name: str
|
||
display_name: str
|
||
description: str
|
||
version: str
|
||
author: Optional[str] = None
|
||
tags: List[str] = field(default_factory=list)
|
||
dependencies: List[str] = field(default_factory=list)
|
||
enabled: bool = True
|
||
registered_at: datetime = field(default_factory=datetime.utcnow)
|
||
|
||
# 统计
|
||
call_count: int = 0
|
||
error_count: int = 0
|
||
total_duration_ms: int = 0
|
||
|
||
@property
|
||
def avg_duration_ms(self) -> int:
|
||
if self.call_count == 0:
|
||
return 0
|
||
return self.total_duration_ms // self.call_count
|
||
|
||
@property
|
||
def error_rate(self) -> float:
|
||
if self.call_count == 0:
|
||
return 0.0
|
||
return self.error_count / self.call_count
|
||
|
||
|
||
class ToolRegistry:
|
||
"""工具注册中心"""
|
||
|
||
def __init__(self):
|
||
self._tools: Dict[str, ToolMetadata] = {}
|
||
self._executors: Dict[str, Callable] = {}
|
||
self._configs: Dict[str, dict] = {}
|
||
self._lock = asyncio.Lock()
|
||
|
||
# === 注册方法 ===
|
||
|
||
async def register(
|
||
self,
|
||
manifest_path: str,
|
||
executor: Callable,
|
||
config: Optional[dict] = None,
|
||
) -> ToolMetadata:
|
||
"""注册工具"""
|
||
from tools.schemas.validator import validate_manifest
|
||
import yaml
|
||
|
||
with open(manifest_path) as f:
|
||
data = yaml.safe_load(f)
|
||
|
||
manifest = validate_manifest(data)
|
||
|
||
metadata = ToolMetadata(
|
||
name=manifest.name,
|
||
display_name=manifest.display_name,
|
||
description=manifest.description,
|
||
version=manifest.version,
|
||
author=manifest.author,
|
||
tags=manifest.tags or [],
|
||
dependencies=manifest.dependencies or [],
|
||
enabled=manifest.enabled,
|
||
)
|
||
|
||
async with self._lock:
|
||
self._tools[manifest.name] = metadata
|
||
self._executors[manifest.name] = executor
|
||
if config:
|
||
self._configs[manifest.name] = config
|
||
|
||
return metadata
|
||
|
||
async def unregister(self, name: str) -> bool:
|
||
"""注销工具"""
|
||
async with self._lock:
|
||
if name in self._tools:
|
||
del self._tools[name]
|
||
del self._executors[name]
|
||
self._configs.pop(name, None)
|
||
return True
|
||
return False
|
||
|
||
async def enable(self, name: str) -> None:
|
||
"""启用工具"""
|
||
async with self._lock:
|
||
if name in self._tools:
|
||
self._tools[name].enabled = True
|
||
|
||
async def disable(self, name: str) -> None:
|
||
"""禁用工具"""
|
||
async with self._lock:
|
||
if name in self._tools:
|
||
self._tools[name].enabled = False
|
||
|
||
# === 查询方法 ===
|
||
|
||
async def get(self, name: str) -> Optional[ToolMetadata]:
|
||
"""获取工具元数据"""
|
||
return self._tools.get(name)
|
||
|
||
async def get_executor(self, name: str) -> Optional[Callable]:
|
||
"""获取工具执行器"""
|
||
return self._executors.get(name)
|
||
|
||
async def get_config(self, name: str) -> dict:
|
||
"""获取工具配置"""
|
||
return self._configs.get(name, {})
|
||
|
||
async def list_all(self) -> List[ToolMetadata]:
|
||
"""列出所有工具"""
|
||
return list(self._tools.values())
|
||
|
||
async def list_enabled(self) -> List[ToolMetadata]:
|
||
"""列出已启用的工具"""
|
||
return [t for t in self._tools.values() if t.enabled]
|
||
|
||
async def list_by_tag(self, tag: str) -> List[ToolMetadata]:
|
||
"""按标签筛选工具"""
|
||
return [t for t in self._tools.values() if tag in t.tags]
|
||
|
||
async def search(self, query: str) -> List[ToolMetadata]:
|
||
"""搜索工具"""
|
||
query = query.lower()
|
||
return [
|
||
t for t in self._tools.values()
|
||
if query in t.name.lower()
|
||
or query in t.description.lower()
|
||
or query in t.display_name.lower()
|
||
]
|
||
|
||
# === 统计方法 ===
|
||
|
||
async def record_call(
|
||
self,
|
||
name: str,
|
||
duration_ms: int,
|
||
error: bool = False,
|
||
) -> None:
|
||
"""记录调用"""
|
||
async with self._lock:
|
||
if name in self._tools:
|
||
tool = self._tools[name]
|
||
tool.call_count += 1
|
||
tool.total_duration_ms += duration_ms
|
||
if error:
|
||
tool.error_count += 1
|
||
|
||
async def get_stats(self) -> dict:
|
||
"""获取统计信息"""
|
||
tools = list(self._tools.values())
|
||
return {
|
||
"total_tools": len(tools),
|
||
"enabled_tools": sum(1 for t in tools if t.enabled),
|
||
"total_calls": sum(t.call_count for t in tools),
|
||
"total_errors": sum(t.error_count for t in tools),
|
||
"avg_error_rate": sum(t.error_rate for t in tools) / len(tools) if tools else 0,
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 工具发现机制
|
||
|
||
### 3.1 自动发现
|
||
|
||
```python
|
||
# tools/discovery.py
|
||
from pathlib import Path
|
||
from typing import List
|
||
|
||
|
||
class ToolDiscovery:
|
||
"""工具自动发现"""
|
||
|
||
def __init__(self, manifest_dir: Path):
|
||
self.manifest_dir = manifest_dir
|
||
|
||
def discover(self) -> List[Path]:
|
||
"""发现所有 Manifest 文件"""
|
||
manifests = list(self.manifest_dir.glob("**/*.yaml"))
|
||
manifests.extend(self.manifest_dir.glob("**/*.yml"))
|
||
manifests.extend(self.manifest_dir.glob("**/*.json"))
|
||
return manifests
|
||
|
||
def discover_by_tag(self, tag: str) -> List[Path]:
|
||
"""按标签发现"""
|
||
# 读取所有 manifest,筛选标签
|
||
pass
|
||
|
||
async def hot_reload(self, registry: ToolRegistry) -> None:
|
||
"""热重载工具"""
|
||
for manifest_path in self.discover():
|
||
# 重新注册
|
||
pass
|
||
```
|
||
|
||
### 3.2 启动时注册
|
||
|
||
```python
|
||
# tools/loader.py
|
||
from pathlib import Path
|
||
|
||
|
||
async def load_all_tools(registry: ToolRegistry) -> None:
|
||
"""加载所有工具"""
|
||
manifest_dir = Path(__file__).parent / "manifests"
|
||
|
||
discovery = ToolDiscovery(manifest_dir)
|
||
|
||
for manifest_path in discovery.discover():
|
||
tool_name = manifest_path.stem
|
||
|
||
# 加载 executor
|
||
executor = load_executor(manifest_path)
|
||
|
||
# 加载配置
|
||
config = load_config(tool_name)
|
||
|
||
# 注册
|
||
await registry.register(manifest_path, executor, config)
|
||
|
||
|
||
def load_executor(manifest_path: Path) -> Callable:
|
||
"""加载工具执行器"""
|
||
import yaml
|
||
|
||
with open(manifest_path) as f:
|
||
manifest = yaml.safe_load(f)
|
||
|
||
# 根据运行时类型加载
|
||
runtime = manifest.get("runtime", "python")
|
||
|
||
if runtime == "python":
|
||
return load_python_executor(manifest)
|
||
elif runtime == "javascript":
|
||
return load_js_executor(manifest)
|
||
else:
|
||
return load_native_executor(manifest)
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 工具描述生成
|
||
|
||
### 4.1 AI 友好的工具描述
|
||
|
||
```python
|
||
# tools/description.py
|
||
from typing import Dict, List
|
||
|
||
|
||
def generate_tool_description(manifest: dict) -> str:
|
||
"""生成 AI 友好的工具描述"""
|
||
lines = [
|
||
f"## {manifest['display_name']}",
|
||
f"{manifest['description']}",
|
||
"",
|
||
"### 可用命令:",
|
||
]
|
||
|
||
for cmd in manifest.get("commands", []):
|
||
lines.append(f"#### {cmd['name']}")
|
||
lines.append(cmd["description"])
|
||
lines.append("")
|
||
|
||
if cmd.get("example"):
|
||
lines.append("**示例:**")
|
||
lines.append(f"```\n{cmd['example']}\n```")
|
||
lines.append("")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def generate_tools_for_llm(registry: ToolRegistry) -> str:
|
||
"""生成给 LLM 的工具列表"""
|
||
tools = registry.list_enabled()
|
||
|
||
sections = ["## 可用工具\n"]
|
||
|
||
for tool in tools:
|
||
manifest = load_manifest(tool.name)
|
||
sections.append(generate_tool_description(manifest))
|
||
sections.append("\n---\n")
|
||
|
||
return "\n".join(sections)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 权限控制
|
||
|
||
### 5.1 工具权限
|
||
|
||
```python
|
||
# tools/permissions.py
|
||
from enum import Enum
|
||
from typing import Set
|
||
|
||
|
||
class ToolPermission(str, Enum):
|
||
"""工具权限"""
|
||
EXECUTE = "tool:execute"
|
||
CONFIGURE = "tool:configure"
|
||
ENABLE = "tool:enable"
|
||
DISABLE = "tool:disable"
|
||
|
||
|
||
class ToolPermissionChecker:
|
||
"""工具权限检查"""
|
||
|
||
def __init__(self):
|
||
self._user_permissions: Dict[str, Set[ToolPermission]] = {}
|
||
self._tool_roles: Dict[str, Set[str]] = {} # tool_name -> required_roles
|
||
|
||
def set_user_permissions(
|
||
self,
|
||
user_id: str,
|
||
permissions: Set[ToolPermission],
|
||
) -> None:
|
||
"""设置用户权限"""
|
||
self._user_permissions[user_id] = permissions
|
||
|
||
def set_tool_roles(
|
||
self,
|
||
tool_name: str,
|
||
required_roles: Set[str],
|
||
) -> None:
|
||
"""设置工具所需角色"""
|
||
self._tool_roles[tool_name] = required_roles
|
||
|
||
def can_execute(self, user_id: str, tool_name: str) -> bool:
|
||
"""检查用户是否可以执行工具"""
|
||
# 检查全局权限
|
||
if ToolPermission.EXECUTE in self._user_permissions.get(user_id, set()):
|
||
return True
|
||
|
||
# 检查工具特定角色
|
||
required_roles = self._tool_roles.get(tool_name, set())
|
||
if not required_roles:
|
||
return True
|
||
|
||
# TODO: 检查用户是否有所需角色
|
||
return False
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 集成到 Agent
|
||
|
||
### 6.1 LangChain 集成
|
||
|
||
```python
|
||
# tools/langchain_adapter.py
|
||
from typing import List, Optional
|
||
from langchain.tools import BaseTool
|
||
from pydantic import BaseModel
|
||
|
||
|
||
class LangChainToolAdapter:
|
||
"""LangChain 工具适配器"""
|
||
|
||
def __init__(self, registry: ToolRegistry):
|
||
self.registry = registry
|
||
|
||
def to_langchain_tools(self) -> List[BaseTool]:
|
||
"""转换为 LangChain 工具"""
|
||
tools = []
|
||
|
||
for metadata in self.registry.list_enabled():
|
||
executor = self.registry.get_executor(metadata.name)
|
||
config = self.registry.get_config(metadata.name)
|
||
|
||
tool = self._create_tool(metadata, executor, config)
|
||
tools.append(tool)
|
||
|
||
return tools
|
||
|
||
def _create_tool(
|
||
self,
|
||
metadata: ToolMetadata,
|
||
executor: Callable,
|
||
config: dict,
|
||
) -> BaseTool:
|
||
"""创建单个 LangChain 工具"""
|
||
# 根据 manifest 创建工具
|
||
pass
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 实现步骤
|
||
|
||
| 步骤 | 任务 | 优先级 |
|
||
|------|------|--------|
|
||
| 1 | 实现 ToolRegistry 类 | 🟢 高 |
|
||
| 2 | 实现 ToolDiscovery | 🟢 高 |
|
||
| 3 | 实现工具描述生成 | 🟡 中 |
|
||
| 4 | 实现权限检查 | 🟡 中 |
|
||
| 5 | 实现 LangChain 适配器 | 🟡 中 |
|
||
| 6 | 集成到 Agent | 🟢 高 |
|
||
| 7 | 单元测试 | 🟡 中 |
|
||
|
||
---
|
||
|
||
## 8. 核心文件变更
|
||
|
||
| 文件 | 变更 |
|
||
|------|------|
|
||
| `tools/registry.py` | 新增 |
|
||
| `tools/discovery.py` | 新增 |
|
||
| `tools/description.py` | 新增 |
|
||
| `tools/permissions.py` | 新增 |
|
||
| `tools/langchain_adapter.py` | 新增 |
|
||
| `tools/__init__.py` | 更新导出 |
|
||
|
||
---
|
||
|
||
## 9. 工作量估算
|
||
|
||
| 任务 | 工作量 |
|
||
|------|--------|
|
||
| ToolRegistry | 0.5 天 |
|
||
| ToolDiscovery | 0.5 天 |
|
||
| 描述生成 | 0.3 天 |
|
||
| 权限检查 | 0.3 天 |
|
||
| LangChain 适配 | 0.3 天 |
|
||
| 集成 | 0.5 天 |
|
||
| 单元测试 | 0.5 天 |
|
||
| **总计** | **2.5 天** |
|
||
|
||
---
|
||
|
||
## 10. 验收标准
|
||
|
||
- [ ] ToolRegistry 可正确注册/注销工具
|
||
- [ ] ToolDiscovery 可发现所有 Manifest
|
||
- [ ] 工具描述生成正确
|
||
- [ ] 权限检查正常工作
|
||
- [ ] LangChain 工具可正常转换
|
||
- [ ] Agent 可使用注册的工具
|
||
- [ ] 单元测试通过
|