Add parameter schema and defaults to ToolResource model and schemas. Implement runtime tool resolution in assistants and tools routers, ensuring proper handling of tool parameters. Update tests to validate new functionality and ensure correct integration of parameter handling in the API.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import inspect, text
|
||||
from typing import Optional, Dict, Any, List
|
||||
import time
|
||||
import uuid
|
||||
@@ -111,6 +112,61 @@ TOOL_ICON_MAP = {
|
||||
TOOL_HTTP_DEFAULTS = {
|
||||
}
|
||||
|
||||
TOOL_PARAMETER_DEFAULTS = {
|
||||
"increase_volume": {"step": 1},
|
||||
"decrease_volume": {"step": 1},
|
||||
}
|
||||
|
||||
|
||||
def _normalize_parameter_schema(value: Any, *, tool_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
value = {}
|
||||
normalized = dict(value)
|
||||
if not normalized:
|
||||
fallback = TOOL_REGISTRY.get(str(tool_id or "").strip(), {}).get("parameters")
|
||||
if isinstance(fallback, dict):
|
||||
normalized = dict(fallback)
|
||||
normalized.setdefault("type", "object")
|
||||
if normalized.get("type") != "object":
|
||||
raise HTTPException(status_code=400, detail="parameter_schema.type must be 'object'")
|
||||
properties = normalized.get("properties")
|
||||
if not isinstance(properties, dict):
|
||||
normalized["properties"] = {}
|
||||
required = normalized.get("required")
|
||||
if required is None:
|
||||
normalized["required"] = []
|
||||
elif not isinstance(required, list):
|
||||
raise HTTPException(status_code=400, detail="parameter_schema.required must be an array")
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_parameter_defaults(value: Any) -> Dict[str, Any]:
|
||||
if value is None:
|
||||
return {}
|
||||
if not isinstance(value, dict):
|
||||
raise HTTPException(status_code=400, detail="parameter_defaults must be an object")
|
||||
return dict(value)
|
||||
|
||||
|
||||
def _ensure_tool_resource_schema(db: Session) -> None:
|
||||
"""Apply lightweight SQLite migrations for newly added tool_resources columns."""
|
||||
bind = db.get_bind()
|
||||
inspector = inspect(bind)
|
||||
try:
|
||||
columns = {col["name"] for col in inspector.get_columns("tool_resources")}
|
||||
except Exception:
|
||||
return
|
||||
|
||||
altered = False
|
||||
if "parameter_schema" not in columns:
|
||||
db.execute(text("ALTER TABLE tool_resources ADD COLUMN parameter_schema JSON"))
|
||||
altered = True
|
||||
if "parameter_defaults" not in columns:
|
||||
db.execute(text("ALTER TABLE tool_resources ADD COLUMN parameter_defaults JSON"))
|
||||
altered = True
|
||||
if altered:
|
||||
db.commit()
|
||||
|
||||
|
||||
def _normalize_http_method(method: Optional[str]) -> str:
|
||||
normalized = str(method or "GET").strip().upper()
|
||||
@@ -127,8 +183,10 @@ def _validate_query_http_config(*, category: str, tool_id: Optional[str], http_u
|
||||
if _requires_http_request(category, tool_id) and not str(http_url or "").strip():
|
||||
raise HTTPException(status_code=400, detail="http_url is required for query tools (except calculator/code_interpreter)")
|
||||
|
||||
|
||||
def _seed_default_tools_if_empty(db: Session) -> None:
|
||||
"""Seed built-in tools only when tool_resources is empty."""
|
||||
_ensure_tool_resource_schema(db)
|
||||
if db.query(ToolResource).count() > 0:
|
||||
return
|
||||
for tool_id, payload in TOOL_REGISTRY.items():
|
||||
@@ -144,6 +202,8 @@ def _seed_default_tools_if_empty(db: Session) -> None:
|
||||
http_url=http_defaults.get("http_url"),
|
||||
http_headers=http_defaults.get("http_headers") or {},
|
||||
http_timeout_ms=int(http_defaults.get("http_timeout_ms") or 10000),
|
||||
parameter_schema=_normalize_parameter_schema(payload.get("parameters"), tool_id=tool_id),
|
||||
parameter_defaults=_normalize_parameter_defaults(TOOL_PARAMETER_DEFAULTS.get(tool_id)),
|
||||
enabled=True,
|
||||
is_system=True,
|
||||
))
|
||||
@@ -215,6 +275,8 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)
|
||||
raise HTTPException(status_code=400, detail="Tool ID already exists")
|
||||
|
||||
_validate_query_http_config(category=data.category, tool_id=candidate_id, http_url=data.http_url)
|
||||
parameter_schema = _normalize_parameter_schema(data.parameter_schema, tool_id=candidate_id)
|
||||
parameter_defaults = _normalize_parameter_defaults(data.parameter_defaults)
|
||||
|
||||
item = ToolResource(
|
||||
id=candidate_id or f"tool_{str(uuid.uuid4())[:8]}",
|
||||
@@ -227,6 +289,8 @@ def create_tool_resource(data: ToolResourceCreate, db: Session = Depends(get_db)
|
||||
http_url=(data.http_url or "").strip() or None,
|
||||
http_headers=data.http_headers or {},
|
||||
http_timeout_ms=max(1000, int(data.http_timeout_ms or 10000)),
|
||||
parameter_schema=parameter_schema,
|
||||
parameter_defaults=parameter_defaults,
|
||||
enabled=data.enabled,
|
||||
is_system=False,
|
||||
)
|
||||
@@ -254,6 +318,10 @@ def update_tool_resource(id: str, data: ToolResourceUpdate, db: Session = Depend
|
||||
update_data["http_method"] = _normalize_http_method(update_data.get("http_method"))
|
||||
if "http_timeout_ms" in update_data and update_data.get("http_timeout_ms") is not None:
|
||||
update_data["http_timeout_ms"] = max(1000, int(update_data["http_timeout_ms"]))
|
||||
if "parameter_schema" in update_data:
|
||||
update_data["parameter_schema"] = _normalize_parameter_schema(update_data.get("parameter_schema"), tool_id=id)
|
||||
if "parameter_defaults" in update_data:
|
||||
update_data["parameter_defaults"] = _normalize_parameter_defaults(update_data.get("parameter_defaults"))
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(item, field, value)
|
||||
|
||||
Reference in New Issue
Block a user