"""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)