Files
JARVIS/backend/app/agents/tools/migration.py
WIN-JHFT4D3SIVT\caoxiaozhu e5bd492d74 feat(agents): Phase 6 tool system refactoring
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
2026-04-04 22:47:48 +08:00

252 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""工具迁移和向后兼容层 - 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