Files
2026-08-21 16:00:13 +08:00

138 lines
5.3 KiB
Python
Raw Permalink 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.
"""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)