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