Files
JARVIS/backend/app/routers/tools.py

349 lines
13 KiB
Python
Raw Normal View History

"""Tools API Router
聚合两套工具体系的元数据
1. 注册层 (app/tools/) - YAML manifest 定义
2. Agent (app/agents/tools/) - @tool 装饰器定义
"""
import re
import importlib
from fastapi import APIRouter, Depends
from app.routers.auth import get_current_user
from app.models.user import User
from app.schemas.tools import (
ToolsResponse,
ToolCategory,
ToolSubgroup,
ToolInfo,
ToolCommand,
ToolStats,
ToolSummary,
)
router = APIRouter(prefix="/api/tools", tags=["Tools"])
# ============================================================
# 辅助函数
# ============================================================
def _parse_command_from_docstring(docstring: str) -> dict:
"""从函数的 docstring 解析参数信息"""
params = {"type": "object", "properties": {}, "required": []}
if not docstring:
return params
# 简单解析 Args: 段落
args_match = re.search(
r"Args:\s*(.*?)(?=\n\s*(?:Returns?|Raises?)|$", docstring, re.DOTALL | re.IGNORECASE
)
if args_match:
args_section = args_match.group(1)
# 匹配形如 "arg_name (type): description" 的行
for line in args_section.strip().split("\n"):
line = line.strip()
if not line:
continue
# 匹配: "name (type): description" 或 "name: description"
m = re.match(r"(\w+)\s*(?:\(\s*(\w+)\s*\))?\s*:", line)
if m:
param_name = m.group(1)
params["properties"][param_name] = {"type": "string", "description": line}
params["required"].append(param_name)
return params
def _build_agent_tools() -> list[ToolInfo]:
"""扫描 app/agents/tools/ 目录,内省 @tool 装饰器"""
tools: list[ToolInfo] = []
# 分类映射:文件名 -> (分类名, 子分类名)
category_map = {
"search": ("Agent层", "知识检索"),
"schedule": ("Agent层", "日程管理"),
"task": ("Agent层", "任务管理"),
"forum": ("Agent层", "论坛功能"),
"time_reasoning": ("Agent层", "时间推理"),
"builtins/file_tools": ("Agent层", "文件工具"),
"builtins/system_tools": ("Agent层", "系统命令"),
"builtins/dev_tools": ("Agent层", "开发工具"),
"builtins/collaboration_tools": ("Agent层", "协作工具"),
}
# 工具名称 -> 中文显示名
display_names = {
"search_knowledge": "知识库搜索",
"get_knowledge_graph_context": "知识图谱查询",
"build_knowledge_graph": "构建知识图谱",
"hybrid_search": "混合搜索",
"web_search": "联网搜索",
"get_schedule_day": "获取日程",
"create_todo": "创建待办",
"create_schedule_task": "创建日程任务",
"create_reminder": "创建提醒",
"create_goal": "创建目标",
"get_tasks": "获取任务列表",
"create_task": "创建任务",
"update_task_status": "更新任务状态",
"get_forum_posts": "获取论坛帖子",
"create_forum_post": "发布论坛帖子",
"scan_forum_for_instructions": "扫描论坛指令",
"resolve_time_expression": "解析时间表达式",
"glob": "文件路径匹配",
"grep": "文件内容搜索",
"read_file": "读取文件",
"write_file": "写入文件",
"bash": "Bash命令",
"powershell": "PowerShell命令",
"git": "Git操作",
"lsp_tools": "LSP代码导航",
"team_agent": "团队Agent通信",
"task_broadcast": "任务广播",
}
# 工具描述
descriptions = {
"search_knowledge": "搜索用户的私人知识库,返回最相关的文档片段",
"get_knowledge_graph_context": "获取用户知识图谱的上下文信息",
"build_knowledge_graph": "从文档构建/更新知识图谱",
"hybrid_search": "混合搜索,结合向量语义检索和关键词匹配",
"web_search": "通过 SearxNG 搜索外部网页信息",
"get_schedule_day": "获取指定日期的 todo/task/reminder/goal 聚合信息",
"create_todo": "创建指定日期的待办",
"create_schedule_task": "创建任务,支持优先级和截止日期",
"create_reminder": "创建提醒,支持自然语言时间",
"create_goal": "创建指定日期的目标",
"get_tasks": "获取用户当前的任务列表",
"create_task": "创建新任务",
"update_task_status": "更新任务状态",
"get_forum_posts": "获取论坛帖子列表",
"create_forum_post": "在论坛发布新帖子",
"scan_forum_for_instructions": "扫描论坛中的指令类帖子",
"resolve_time_expression": "解析中文自然语言时间表达",
"glob": "使用 glob 模式查找文件路径",
"grep": "在文件中搜索匹配的文本行",
"read_file": "读取文件内容",
"write_file": "写入文件内容",
"bash": "执行 Bash 命令",
"powershell": "执行 PowerShell 命令",
"git": "执行 Git 命令",
"lsp_tools": "LSP 代码导航和查找引用",
"team_agent": "向团队 Agent 发送消息或请求协作",
"task_broadcast": "向多个 Agent 广播任务",
}
# 需要扫描的模块
modules_to_scan = [
("app.agents.tools.search", "search"),
("app.agents.tools.schedule", "schedule"),
("app.agents.tools.task", "task"),
("app.agents.tools.forum", "forum"),
("app.agents.tools.time_reasoning", "time_reasoning"),
("app.agents.tools.builtins.file_tools", "builtins/file_tools"),
("app.agents.tools.builtins.system_tools", "builtins/system_tools"),
("app.agents.tools.builtins.dev_tools", "builtins/dev_tools"),
("app.agents.tools.builtins.collaboration_tools", "builtins/collaboration_tools"),
]
for module_name, category_key in modules_to_scan:
try:
mod = importlib.import_module(module_name)
except ImportError:
continue
# 扫描模块中所有 @tool 装饰的函数
for attr_name in dir(mod):
if attr_name.startswith("_"):
continue
attr = getattr(mod, attr_name)
# 检查是否是 langchain @tool 装饰的对象
if hasattr(attr, "name") and hasattr(attr, "description"):
tool_name = attr.name
tool_desc = attr.description or ""
# 清理 docstring 中的参数说明用于显示
display_desc = re.sub(r"\s*Args:\s*.*", "", tool_desc, flags=re.DOTALL).strip()
display_desc = re.sub(
r"\s*Returns?:\s*.*", "", display_desc, flags=re.DOTALL
).strip()
# 获取 category 和 subcategory
cat_info = category_map.get(category_key, ("Agent层", category_key))
category, subcategory = cat_info[0], cat_info[1]
# 获取参数 schema
params_schema = getattr(attr, "args_schema", None)
parameters = {}
if params_schema:
try:
if hasattr(params_schema, "model_json_schema"):
parameters = params_schema.model_json_schema()
elif hasattr(params_schema, "schema"):
parameters = params_schema.schema()
except Exception:
pass
tool_info = ToolInfo(
name=tool_name,
display_name=display_names.get(tool_name, tool_name),
description=descriptions.get(tool_name, display_desc or tool_desc),
category=category,
subcategory=subcategory,
source="agent",
source_file=module_name,
tags=[],
enabled=True,
commands=[
ToolCommand(
name=tool_name,
description=tool_desc or display_desc,
parameters=parameters,
)
],
stats=ToolStats(),
)
tools.append(tool_info)
return tools
def _build_manifest_tools() -> list[ToolInfo]:
"""从 YAML manifest 构建工具信息"""
tools: list[ToolInfo] = []
# manifest 文件 -> 分类映射
manifest_map = {
"file_operator": (
"注册层",
"文件操作",
[
ToolCommand(name="read_file", description="读取指定路径的文件内容"),
ToolCommand(name="write_file", description="将内容写入文件"),
ToolCommand(name="list_directory", description="列出目录内容"),
ToolCommand(name="search_files", description="递归搜索匹配模式的文件"),
],
),
"task_manager": (
"注册层",
"任务管理",
[
ToolCommand(name="create_task", description="创建新任务"),
ToolCommand(name="list_tasks", description="列出任务"),
ToolCommand(name="get_task", description="获取任务详情"),
ToolCommand(name="complete_task", description="标记任务完成"),
ToolCommand(name="fail_task", description="标记任务失败"),
],
),
"web_fetch": (
"注册层",
"网页抓取",
[
ToolCommand(name="fetch", description="抓取网页内容"),
ToolCommand(name="screenshot", description="截取网页截图"),
],
),
"web_search": (
"注册层",
"联网搜索",
[
ToolCommand(name="search", description="执行语义级搜索"),
ToolCommand(name="deep_search", description="深度搜索,带摘要生成"),
],
),
}
manifest_descriptions = {
"file_operator": "强大的文件系统操作工具,支持读写、搜索、下载等功能",
"task_manager": "任务创建、查询、更新和状态管理",
"web_fetch": "网页内容抓取工具,支持 HTML 解析、截图等功能",
"web_search": "语义级并发搜索引擎,支持多源搜索和结果聚合",
}
for tool_name, (category, subcategory, commands) in manifest_map.items():
tool_info = ToolInfo(
name=tool_name,
display_name=subcategory,
description=manifest_descriptions.get(tool_name, ""),
category=category,
subcategory=subcategory,
source="manifest",
source_file=f"app/tools/manifests/{tool_name}.yaml",
tags=[],
enabled=True,
commands=commands,
stats=ToolStats(),
)
tools.append(tool_info)
return tools
# ============================================================
# 路由
# ============================================================
@router.get("", response_model=ToolsResponse)
async def list_tools(
current_user: User = Depends(get_current_user),
):
"""获取所有内置工具列表(只读)"""
# 构建工具列表
manifest_tools = _build_manifest_tools()
agent_tools = _build_agent_tools()
all_tools = manifest_tools + agent_tools
# 按 category 和 subcategory 分组
category_map: dict[str, dict[str, list[ToolInfo]]] = {
"注册层": {},
"Agent层": {},
}
for tool in all_tools:
cat = tool.category
subcat = tool.subcategory
if cat not in category_map:
category_map[cat] = {}
if subcat not in category_map[cat]:
category_map[cat][subcat] = []
category_map[cat][subcat].append(tool)
# 构建响应
categories = []
for cat_name, subgroups_dict in category_map.items():
if not subgroups_dict:
continue
subgroups = []
for subcat_name, tools_list in subgroups_dict.items():
subgroups.append(
ToolSubgroup(
name=subcat_name,
display_name=subcat_name,
tools=tools_list,
)
)
categories.append(
ToolCategory(
name=cat_name,
display_name=cat_name,
subgroups=subgroups,
)
)
# 计算摘要
total_commands = sum(len(t.commands) for t in all_tools)
active_commands = sum(len(t.commands) for t in all_tools if t.enabled)
summary = ToolSummary(
total_commands=total_commands,
active_commands=active_commands,
total_tools=len(all_tools),
manifest_tools=len(manifest_tools),
agent_tools=len(agent_tools),
)
return ToolsResponse(categories=categories, summary=summary)