"""ToolSpec → OpenAI 函数 schema 的转换与默认工具集选取。 直接产出 OpenAI function schema dict(``{"type": "function", "function": {...}}``), 与 ChatResult.tool_calls 里的 name/args 回填闭环天然对齐。 ToolSpec 的 name/description 直接映射;参数 schema 因 ToolSpec 未内嵌 JSON Schema, 这里按工具 name 查表给出最小参数 schema(builtins 内工具参数固定,可选参数也显式列出)。 未知工具默认空参数 schema(``{"type": "object", "properties": {}}``),模型按需传参。 """ from __future__ import annotations from typing import Any from app.modules.tool_runtime.definition import ToolSpec # 内置工具的参数 schema(name → JSON Schema)。新增工具时扩展此表即可。 # 保持最小集合:只声明 known 工具的参数;调用方传入未登记工具时回退空对象 schema。 _PARAM_SCHEMAS: dict[str, dict[str, Any]] = { "datetime": {"type": "object", "properties": {}, "required": []}, "read_file": { "type": "object", "properties": { "path": {"type": "string", "description": "workspace 内的文件相对路径。"}, "offset": { "type": "integer", "description": "起始行号(1-based),默认 1。大文件分段读取后续内容时使用。", }, "limit": { "type": "integer", "description": "读取的行数上限;不传则读到末尾。大文件分段读取时使用。", }, }, "required": ["path"], }, "list_dir": { "type": "object", "properties": { "path": { "type": "string", "description": "workspace 内的目录相对路径,缺省为 '.'。", }, }, "required": [], }, "write_file": { "type": "object", "properties": { "path": {"type": "string", "description": "workspace 内文件相对路径。"}, "content": {"type": "string", "description": "要写入的文本内容。"}, }, "required": ["path"], }, "shell": { "type": "object", "properties": { "command": { "type": "string", "description": "要在沙箱中执行的 shell 命令。工作目录为沙箱隔离的 workspace 目录(并非 /home/user 或 /tmp 等 Linux 路径,这些路径不存在,不要 cd 到它们)。read_file/write_file 操作的也是这个 workspace 目录,shell 里 cd 到别的目录将看不到这些文件。", }, "timeout_ms": {"type": "integer", "description": "可选超时(毫秒),默认 30000。"}, "stdin": {"type": "string", "description": "可选标准输入。"}, }, "required": ["command"], }, "fetch_url": { "type": "object", "properties": { "url": {"type": "string", "description": "要抓取的网页 URL。"}, "max_chars": { "type": "integer", "description": "正文最大字符数,默认 8000。", }, }, "required": ["url"], }, } _EMPTY_SCHEMA: dict[str, Any] = {"type": "object", "properties": {}, "required": []} def to_ai_provider_tools(tool_specs: list[ToolSpec]) -> list[dict[str, Any]]: """把 ToolSpec 列表转成 OpenAI function schema dict 列表。 先查 ``_PARAM_SCHEMAS``(内置工具),未命中则用 ``spec.input_schema``(MCP 工具), 仍为空则回退无参占位 ``_EMPTY_SCHEMA``。 """ tools: list[dict[str, Any]] = [] for spec in tool_specs: params = _PARAM_SCHEMAS.get(spec.name) if not params and spec.name.startswith("skill:"): params = _PARAM_SCHEMAS.get(spec.name.rsplit("/", 1)[-1]) if not params: params = spec.input_schema or _EMPTY_SCHEMA tools.append( { "type": "function", "function": { "name": spec.name, "description": spec.description, "parameters": params, }, } ) return tools # 核心工具恒绑(沙箱类,始终可用)。 _CORE_TOOL_NAMES = ["datetime", "read_file", "list_dir", "write_file", "shell"] def default_tools( tool_specs_by_name: dict[str, ToolSpec] | None = None, *, allowed_tools: tuple[str, ...] = (), ) -> list[dict[str, Any]]: """返回工具对应的 OpenAI function schema 列表。 - 未传 ``tool_specs_by_name`` 时,从 ``builtin_specs()`` 加载仅返回核心工具 (datetime/read_file/list_dir/write_file/shell),共 5 个。 - 传入自定义工具池时(如 ``tool_runtime._resolve_specs()``),返回池中所有工具, 含 MCP 注册的工具,不限于核心工具。 """ if tool_specs_by_name is None: from app.application.tool_runtime.builtins import builtin_specs tool_specs_by_name = builtin_specs() chosen = [ tool_specs_by_name[name] for name in _CORE_TOOL_NAMES if name in tool_specs_by_name ] else: chosen = list(tool_specs_by_name.values()) if allowed_tools: allowed = set(allowed_tools) chosen = [spec for spec in chosen if spec.name in allowed] return to_ai_provider_tools(chosen)