Phase 6.1: ToolRegistry infrastructure - Add ToolManifest with ToolCategory, PermissionClass, SideEffectScope - Add ToolRegistry singleton with register/get/unregister/list/search - Add BaseTool abstract class with ReadTool/WriteTool/DBWriteTool/ExternalTool/NetworkTool subclasses - Add migration layer for backward compatibility Phase 6.2: Hook interception system - Add HookType (PRE_TOOL_USE, POST_TOOL_USE, TOOL_ERROR, TOOL_SKIP) - Add HookManager with singleton for hook registration - Add HookExecutor for pre/post/error hook execution Phase 6.3: Streaming execution - Add StreamingToolExecutor with batch execution support Phase 6.4: New builtin tools - Add file_tools: GlobTool, GrepTool, ReadFileTool, WriteFileTool - Add system_tools: BashTool, PowerShellTool - Add dev_tools: LSPTools, GitTool - Add collaboration_tools: TeamAgentTool, TaskBroadcastTool Tests: 29 passed
252 lines
7.6 KiB
Python
252 lines
7.6 KiB
Python
"""工具迁移和向后兼容层 - Phase 6.1
|
||
|
||
将现有 @tool 装饰的工具迁移到 ToolRegistry,同时保持向后兼容。
|
||
"""
|
||
|
||
from functools import wraps
|
||
from typing import Any, Callable
|
||
|
||
from app.agents.tools.manifest import (
|
||
PermissionClass,
|
||
SideEffectScope,
|
||
ToolCategory,
|
||
ToolManifest,
|
||
)
|
||
from app.agents.tools.registry import get_tool_registry
|
||
|
||
|
||
# 现有工具的类别映射
|
||
_TOOL_CATEGORY_MAP: dict[str, tuple[ToolCategory, PermissionClass, SideEffectScope]] = {
|
||
# 知识检索 - 只读
|
||
"search_knowledge": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
"get_knowledge_graph_context": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
"hybrid_search": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
"web_search": (ToolCategory.NETWORK, PermissionClass.EXTERNAL, SideEffectScope.NETWORK),
|
||
# 知识构建 - 写入
|
||
"build_knowledge_graph": (
|
||
ToolCategory.WRITE,
|
||
PermissionClass.WRITE,
|
||
SideEffectScope.LOCAL_STATE,
|
||
),
|
||
# 任务工具
|
||
"get_tasks": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
"create_task": (ToolCategory.WRITE, PermissionClass.WRITE, SideEffectScope.LOCAL_STATE),
|
||
"update_task_status": (ToolCategory.WRITE, PermissionClass.WRITE, SideEffectScope.LOCAL_STATE),
|
||
# 日程工具
|
||
"get_schedule_day": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
"create_todo": (ToolCategory.WRITE, PermissionClass.WRITE, SideEffectScope.LOCAL_STATE),
|
||
"create_schedule_task": (
|
||
ToolCategory.WRITE,
|
||
PermissionClass.WRITE,
|
||
SideEffectScope.LOCAL_STATE,
|
||
),
|
||
"create_reminder": (ToolCategory.WRITE, PermissionClass.WRITE, SideEffectScope.LOCAL_STATE),
|
||
"create_goal": (ToolCategory.WRITE, PermissionClass.WRITE, SideEffectScope.LOCAL_STATE),
|
||
"resolve_time_expression": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
# 论坛工具
|
||
"get_forum_posts": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
"create_forum_post": (ToolCategory.WRITE, PermissionClass.WRITE, SideEffectScope.LOCAL_STATE),
|
||
"scan_forum_for_instructions": (ToolCategory.READ, PermissionClass.READ, SideEffectScope.NONE),
|
||
}
|
||
|
||
|
||
def get_tool_category(name: str) -> tuple[ToolCategory, PermissionClass, SideEffectScope]:
|
||
"""获取工具的类别信息"""
|
||
return _TOOL_CATEGORY_MAP.get(
|
||
name,
|
||
(ToolCategory.EXTERNAL, PermissionClass.EXTERNAL, SideEffectScope.NETWORK),
|
||
)
|
||
|
||
|
||
def infer_tags_from_docstring(docstring: str | None) -> list[str]:
|
||
"""从 docstring 推断工具标签"""
|
||
if not docstring:
|
||
return []
|
||
tags = []
|
||
doc_lower = docstring.lower()
|
||
if "搜索" in docstring or "查询" in docstring or "search" in doc_lower:
|
||
tags.append("search")
|
||
if "创建" in docstring or "新建" in docstring or "create" in doc_lower:
|
||
tags.append("create")
|
||
if "获取" in docstring or "读取" in docstring or "get" in doc_lower:
|
||
tags.append("read")
|
||
if "更新" in docstring or "修改" in docstring or "update" in doc_lower:
|
||
tags.append("update")
|
||
return tags
|
||
|
||
|
||
def migrate_tool(tool_func: Callable) -> Callable:
|
||
"""将现有 @tool 装饰的函数迁移到 ToolRegistry
|
||
|
||
Args:
|
||
tool_func: LangChain @tool 装饰的函数
|
||
|
||
Returns:
|
||
原函数(已注册到 registry)
|
||
"""
|
||
registry = get_tool_registry()
|
||
|
||
# 如果已经注册,跳过
|
||
if registry.get(tool_func.name):
|
||
return tool_func
|
||
|
||
# 获取类别信息
|
||
category, permission, side_effect = get_tool_category(tool_func.name)
|
||
|
||
# 从 docstring 提取 description
|
||
description = tool_func.description if hasattr(tool_func, "description") else ""
|
||
|
||
# 推断 tags
|
||
tags = infer_tags_from_docstring(description)
|
||
tags.append("migrated")
|
||
|
||
# 创建 manifest
|
||
manifest = ToolManifest(
|
||
name=tool_func.name,
|
||
description=description,
|
||
category=category,
|
||
parameters={}, # LangChain @tool 动态处理参数
|
||
return_schema={},
|
||
permission_class=permission,
|
||
side_effect_scope=side_effect,
|
||
requires_confirmation=side_effect != SideEffectScope.NONE,
|
||
is_streaming=False,
|
||
tags=tags,
|
||
)
|
||
|
||
# 注册到 registry
|
||
registry.register(manifest, tool_func)
|
||
|
||
return tool_func
|
||
|
||
|
||
def migrate_all_tools() -> int:
|
||
"""迁移所有现有工具到 ToolRegistry
|
||
|
||
Returns:
|
||
迁移的工具数量
|
||
"""
|
||
from app.agents.tools import (
|
||
ALL_TOOLS,
|
||
KNOWLEDGE_GRAPH_TOOLS,
|
||
KNOWLEDGE_RETRIEVAL_TOOLS,
|
||
SCHEDULE_READ_TOOLS,
|
||
SCHEDULE_WRITE_TOOLS,
|
||
TASK_TOOLS,
|
||
FORUM_TOOLS,
|
||
)
|
||
|
||
all_tools = (
|
||
KNOWLEDGE_RETRIEVAL_TOOLS
|
||
+ KNOWLEDGE_GRAPH_TOOLS
|
||
+ TASK_TOOLS
|
||
+ SCHEDULE_READ_TOOLS
|
||
+ SCHEDULE_WRITE_TOOLS
|
||
+ FORUM_TOOLS
|
||
)
|
||
|
||
count = 0
|
||
for tool in all_tools:
|
||
try:
|
||
migrate_tool(tool)
|
||
count += 1
|
||
except Exception as e:
|
||
print(f"Failed to migrate tool {getattr(tool, 'name', 'unknown')}: {e}")
|
||
|
||
return count
|
||
|
||
|
||
class BackwardCompatTool:
|
||
"""向后兼容工具包装器
|
||
|
||
确保现有代码通过 registry.get_executor() 仍能正常调用工具。
|
||
"""
|
||
|
||
def __init__(self, name: str):
|
||
self.name = name
|
||
self._registry = get_tool_registry()
|
||
|
||
def __call__(self, *args, **kwargs) -> Any:
|
||
executor = self._registry.get_executor(self.name)
|
||
if executor is None:
|
||
raise ValueError(f"Tool not found in registry: {self.name}")
|
||
return executor(*args, **kwargs)
|
||
|
||
def invoke(self, tool_input: dict[str, Any]) -> Any:
|
||
"""LangChain 风格的 invoke 调用"""
|
||
executor = self._registry.get_executor(self.name)
|
||
if executor is None:
|
||
raise ValueError(f"Tool not found in registry: {self.name}")
|
||
|
||
# 处理位置参数
|
||
if isinstance(tool_input, dict):
|
||
return executor(**tool_input)
|
||
return executor(tool_input)
|
||
|
||
|
||
def create_compat_layer() -> dict[str, BackwardCompatTool]:
|
||
"""创建向后兼容层
|
||
|
||
返回一个字典,允许通过名称访问兼容的工具包装器。
|
||
"""
|
||
registry = get_tool_registry()
|
||
tools = registry.list_all()
|
||
|
||
return {tool.name: BackwardCompatTool(tool.name) for tool in tools}
|
||
|
||
|
||
# 自动迁移装饰器
|
||
def auto_migrate(func: Callable) -> Callable:
|
||
"""自动迁移装饰器
|
||
|
||
用于装饰新的 @tool 函数,自动注册到 registry。
|
||
"""
|
||
|
||
@wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
return func(*args, **kwargs)
|
||
|
||
# 迁移到 registry
|
||
migrate_tool(wrapper)
|
||
|
||
return wrapper
|
||
|
||
|
||
# 便捷函数:获取兼容的工具执行器
|
||
def get_tool_executor(name: str) -> Callable | None:
|
||
"""获取工具执行器(兼容层)
|
||
|
||
优先从 registry 获取,fallback 到直接导入。
|
||
"""
|
||
registry = get_tool_registry()
|
||
executor = registry.get_executor(name)
|
||
|
||
if executor is not None:
|
||
return executor
|
||
|
||
# Fallback: 直接从模块导入(仅用于迁移期间)
|
||
try:
|
||
from app.agents.tools import (
|
||
TASK_TOOLS,
|
||
SCHEDULE_READ_TOOLS,
|
||
SCHEDULE_WRITE_TOOLS,
|
||
FORUM_TOOLS,
|
||
KNOWLEDGE_RETRIEVAL_TOOLS,
|
||
)
|
||
|
||
all_tools = (
|
||
KNOWLEDGE_RETRIEVAL_TOOLS
|
||
+ TASK_TOOLS
|
||
+ SCHEDULE_READ_TOOLS
|
||
+ SCHEDULE_WRITE_TOOLS
|
||
+ FORUM_TOOLS
|
||
)
|
||
|
||
for tool in all_tools:
|
||
if hasattr(tool, "name") and tool.name == name:
|
||
return tool
|
||
except ImportError:
|
||
pass
|
||
|
||
return None
|