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
223 lines
7.2 KiB
Python
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),
|
|
)
|