"""将工具 schema 转为 LangChain StructuredTool。"""
from __future__ import annotations
from typing import Any
from langchain_core.tools import StructuredTool
from pydantic import Field, create_model
from app.services.agent_registry import normalize_tool_ids
from app.services.agent_tool_schemas import _SCHEMAS
from app.services.chat_tools import execute_tool
def _json_type_to_python(spec: dict[str, Any]) -> type[Any]:
t = spec.get("type")
if t == "string":
return str
if t == "integer":
return int
if t == "number":
return float
if t == "boolean":
return bool
return Any
def _params_to_model(tool_id: str, parameters: dict[str, Any]) -> type[Any]:
props = parameters.get("properties") or {}
required = set(parameters.get("required") or [])
fields: dict[str, Any] = {}
for name, spec in props.items():
if not isinstance(spec, dict):
spec = {}
py_type = _json_type_to_python(spec)
desc = str(spec.get("description") or "")
if name in required:
fields[name] = (py_type, Field(..., description=desc))
else:
fields[name] = (py_type | None, Field(default=None, description=desc))
model_name = f"Tool_{tool_id}_Args"
if not fields:
return create_model(model_name)
return create_model(model_name, **fields)
def build_langchain_tools(tool_ids: list[str] | None) -> list[Any]:
tools: list[Any] = []
for tid in normalize_tool_ids(tool_ids):
schema = _SCHEMAS.get(tid)
if not schema:
continue
fn = schema.get("function") or {}
name = str(fn.get("name") or tid)
description = str(fn.get("description") or tid)
parameters = fn.get("parameters") if isinstance(fn.get("parameters"), dict) else {}
args_model = _params_to_model(tid, parameters)
def _make_runner(tool_id: str):
def _run(**kwargs: Any) -> str:
clean = {k: v for k, v in kwargs.items() if v is not None}
return execute_tool(tool_id, clean)
return _run
tools.append(
StructuredTool.from_function(
func=_make_runner(tid),
name=name,
description=description,
args_schema=args_model,
)
)
return tools