Files
JARVIS/backend/app/agents/tools/migration.py

252 lines
7.6 KiB
Python
Raw Normal View History

"""工具迁移和向后兼容层 - 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