Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

223 lines
7.2 KiB
Python

"""
Tools API Router
================
Read-only listing of the chat agent's built-in tools, used by the Settings UI
to render the "Tools" sub-page. Returns each tool's definition (name,
description, parameters) alongside its bilingual prompt hints, so the frontend
can show authoritative copy without duplicating the catalog.
"""
from __future__ import annotations
import logging
from typing import Any, Literal
from fastapi import APIRouter
from pydantic import BaseModel
from deeptutor.api.routers.settings import get_enabled_optional_tools
from deeptutor.core.tool_protocol import BaseTool, ToolDefinition, ToolPromptHints
from deeptutor.i18n.metadata_i18n import tool_description_i18n
from deeptutor.tools.builtin import (
BUILTIN_TOOL_TYPES,
COMING_SOON_TOOL_TYPES,
TOOL_ALIASES,
USER_TOGGLEABLE_TOOL_NAMES,
)
logger = logging.getLogger(__name__)
router = APIRouter()
class ToolParameterPayload(BaseModel):
name: str
type: str
description: str = ""
required: bool = True
default: Any = None
enum: list[str] | None = None
class ToolAliasPayload(BaseModel):
name: str
description: str = ""
input_format: str = ""
when_to_use: str = ""
phase: str = ""
class ToolHintsPayload(BaseModel):
short_description: str = ""
when_to_use: str = ""
input_format: str = ""
guideline: str = ""
note: str = ""
phase: str = ""
aliases: list[ToolAliasPayload] = []
class BuiltinToolPayload(BaseModel):
name: str
description: str
description_i18n: dict[Literal["en", "zh"], str] = {}
parameters: list[ToolParameterPayload]
hints: dict[Literal["en", "zh"], ToolHintsPayload]
aliases: list[str] = []
# True iff the user is allowed to switch this tool on/off from the
# /settings/tools UI. Locked-on tools (auto-mounted by the chat
# pipeline under context gates) report ``False`` and the UI renders
# them as informational entries only.
toggleable: bool = False
# Whether the tool is currently on. For toggleable tools this
# reflects the user's saved preference; for locked-on tools this is
# always ``True``.
enabled: bool = True
# ``coming_soon`` tools are NOT registered with the runtime — the chat
# agent cannot invoke them. They are listed here only so the settings
# page can render a placeholder card explaining the capability is on
# the roadmap. The frontend should lock the toggle and show a badge.
coming_soon: bool = False
# The capability that owns this tool (e.g. ``solve`` / ``mastery``), or
# ``None`` for a plain system built-in. Owned tools are reused by their
# capability on top of the shared built-in surface; the settings UI groups
# them under their owner, below the built-in section.
capability: str | None = None
class ToolsListResponse(BaseModel):
tools: list[BuiltinToolPayload]
enabled_optional_tools: list[str]
def _serialise_definition(
definition: ToolDefinition,
) -> tuple[str, str, list[ToolParameterPayload]]:
params = [
ToolParameterPayload(
name=p.name,
type=p.type,
description=p.description,
required=p.required,
default=p.default,
enum=p.enum,
)
for p in definition.parameters
]
return definition.name, definition.description, params
def _serialise_hints(hints: ToolPromptHints) -> ToolHintsPayload:
return ToolHintsPayload(
short_description=hints.short_description,
when_to_use=hints.when_to_use,
input_format=hints.input_format,
guideline=hints.guideline,
note=hints.note,
phase=hints.phase,
aliases=[
ToolAliasPayload(
name=alias.name,
description=alias.description,
input_format=alias.input_format,
when_to_use=alias.when_to_use,
phase=alias.phase,
)
for alias in hints.aliases
],
)
def _collect_aliases_for(tool_name: str) -> list[str]:
return sorted(alias for alias, (target, _) in TOOL_ALIASES.items() if target == tool_name)
def _build_tool_payload(
tool: BaseTool,
*,
enabled_optional: set[str],
coming_soon: bool = False,
capability: str | None = None,
) -> BuiltinToolPayload:
name, description, parameters = _serialise_definition(tool.get_definition())
descriptions = tool_description_i18n(name, description)
toggleable = (not coming_soon) and (name in USER_TOGGLEABLE_TOOL_NAMES)
if coming_soon:
enabled = False
elif toggleable:
enabled = name in enabled_optional
else:
enabled = True
return BuiltinToolPayload(
name=name,
description=descriptions.get("en") or description,
description_i18n=descriptions,
parameters=parameters,
hints={
"en": _serialise_hints(tool.get_prompt_hints(language="en")),
"zh": _serialise_hints(tool.get_prompt_hints(language="zh")),
},
aliases=_collect_aliases_for(name),
toggleable=toggleable,
enabled=enabled,
coming_soon=coming_soon,
capability=capability,
)
@router.get("", response_model=ToolsListResponse)
async def list_builtin_tools() -> ToolsListResponse:
"""Return all built-in tools the chat agent can invoke, plus any
coming-soon placeholders for the settings page."""
from deeptutor.capabilities import capability_tool_owners
enabled_optional = set(get_enabled_optional_tools())
owners = capability_tool_owners()
payloads: list[BuiltinToolPayload] = []
for tool_type in BUILTIN_TOOL_TYPES:
try:
instance = tool_type()
payloads.append(
_build_tool_payload(
instance,
enabled_optional=enabled_optional,
capability=owners.get(instance.name),
)
)
except Exception:
logger.exception("Failed to serialise tool %s", tool_type.__name__)
for tool_type in COMING_SOON_TOOL_TYPES:
try:
instance = tool_type()
payloads.append(
_build_tool_payload(
instance,
enabled_optional=enabled_optional,
coming_soon=True,
)
)
except Exception:
logger.exception("Failed to serialise coming-soon tool %s", tool_type.__name__)
# Guard against the unlikely case of name collision (e.g. someone
# accidentally registers the same tool both as built-in and coming-soon).
seen: set[str] = set()
deduped: list[BuiltinToolPayload] = []
for payload in payloads:
if payload.name in seen:
continue
seen.add(payload.name)
deduped.append(payload)
# Toggleable tools outside the user's admin grant don't exist for them:
# hidden here so the settings page and composer match what turn_runtime
# will actually allow.
from deeptutor.multi_user.tool_access import allowed_optional_tools
allowed = allowed_optional_tools()
if allowed is not None:
deduped = [p for p in deduped if not p.toggleable or p.name in allowed]
return ToolsListResponse(
tools=deduped,
enabled_optional_tools=sorted(enabled_optional),
)