patristic-be / src /api /routers /tools.py
Mario33333's picture
deploy: feature/audiobook@f1c0d35 (fix batch β€” models/providers, TTS $0, grants, DB admins, delete-hardening)
833ba13 verified
Raw
History Blame Contribute Delete
3.96 kB
"""``/tools/*`` β€” read-only registry view.
Two endpoints:
- ``GET /tools?category=`` β€” list, optionally filtered to one category.
- ``GET /tools/{name}`` β€” single tool detail.
Both wrap :func:`src.lib.tools.repo.load_registry`. No paid calls.
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, Path, Query
from src.api.deps import forbid_readers
from src.api.dto.tools import (
ToolCategoryEnum,
ToolDTO,
ToolPricingDTO,
ToolStatusEnum,
)
from src.api.errors import ApiError
from src.lib.auth.models import User
from src.lib.llm_keys.store import KNOWN_PROVIDERS, resolve_provider_key
from src.lib.tools.models import Tool
from src.lib.tools.repo import get_tool, load_registry
router = APIRouter(prefix="/tools", tags=["tools"])
class ToolNotFound(ApiError):
"""404 for ``GET /tools/{name}`` when the registry has no such tool.
Local to this router β€” every other 404 is on the canonical list in
``src/api/errors.py``. We don't add this to the central module
because the tools registry is a single YAML file and an "unknown
tool" lookup is more of a programming error than a real user-facing
state.
"""
code = "TOOL_NOT_FOUND"
status_code = 404
def _to_dto(tool: Tool) -> ToolDTO:
pricing = None
if tool.pricing is not None:
# tools_registry.yaml uses YAML date literals; PyYAML parses them to
# ``datetime.date`` objects. The wire shape is a string (ISO date) β€”
# the FE renders it as-is in the "last verified" column.
last_updated_raw = tool.pricing.last_updated
last_updated = str(last_updated_raw) if last_updated_raw is not None else None
pricing = ToolPricingDTO(
input_per_million_usd=tool.pricing.input_per_million_usd,
output_per_million_usd=tool.pricing.output_per_million_usd,
image_input_tokens_per_image=tool.pricing.image_input_tokens_per_image,
per_request_usd=tool.pricing.per_request_usd,
last_updated=last_updated,
verified=tool.pricing.verified,
)
# Task A1b: for llm tools, surface whether the provider's key is
# resolvable (store or env) so the FE picker can disable unconfigured
# providers. Only meaningful for the 5 managed LLM providers; None for
# everything else. Never exposes the key value.
active: bool | None = None
if tool.category == "llm" and tool.provider in KNOWN_PROVIDERS:
active = resolve_provider_key(tool.provider) is not None
return ToolDTO(
name=tool.name,
category=ToolCategoryEnum(tool.category),
status=ToolStatusEnum(tool.status),
used_by=list(tool.used_by),
compatible_with=list(tool.compatible_with),
source_file=tool.source_file,
provider=tool.provider,
pricing=pricing,
notes=tool.notes,
active=active,
)
@router.get("", response_model=list[ToolDTO])
def list_tools(
_: User = Depends(forbid_readers),
category: ToolCategoryEnum | None = Query(default=None),
) -> list[ToolDTO]:
"""All tools, optionally filtered to one category.
Active tools sort first, then stubbed, then planned (matches the
Operations Tools page's column order).
"""
rank = {"active": 0, "stubbed": 1, "planned": 2}
tools = list(load_registry())
if category is not None:
tools = [t for t in tools if t.category == category.value]
tools.sort(key=lambda t: (rank[t.status], t.name))
return [_to_dto(t) for t in tools]
@router.get("/{name}", response_model=ToolDTO)
def get_tool_endpoint(
name: str = Path(...),
_: User = Depends(forbid_readers),
) -> ToolDTO:
tool = get_tool(name)
if tool is None:
raise ToolNotFound(f"No tool named {name!r} in tools_registry.yaml.")
return _to_dto(tool)