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
164 lines
5.6 KiB
Python
164 lines
5.6 KiB
Python
"""
|
|
Tool Registry
|
|
=============
|
|
|
|
Central registry that discovers and manages all tools (built-in and plugin).
|
|
Provides lookup, listing, and OpenAI schema generation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from deeptutor.core.tool_protocol import BaseTool, ToolDefinition, ToolPromptHints
|
|
from deeptutor.tools.builtin import BUILTIN_TOOL_TYPES, TOOL_ALIASES
|
|
from deeptutor.tools.prompting import ToolPromptComposer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ToolRegistry:
|
|
"""
|
|
Singleton-ish registry of all available tools.
|
|
|
|
Usage::
|
|
|
|
registry = ToolRegistry()
|
|
registry.load_builtins()
|
|
rag = registry.get("rag")
|
|
result = await rag.execute(query="hello")
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._tools: dict[str, BaseTool] = {}
|
|
|
|
def register(self, tool: BaseTool) -> None:
|
|
name = tool.name
|
|
self._tools[name] = tool
|
|
logger.debug("Registered tool: %s", name)
|
|
|
|
def unregister(self, name: str) -> None:
|
|
"""Remove a tool (no-op when absent). Used by MCP reloads."""
|
|
self._tools.pop(name, None)
|
|
|
|
def deferred_tools(self) -> list[BaseTool]:
|
|
"""Tools flagged for progressive disclosure (see ``BaseTool.deferred``)."""
|
|
return [t for t in self._tools.values() if getattr(t, "deferred", False)]
|
|
|
|
def load_builtins(self) -> None:
|
|
"""Instantiate and register all built-in tools."""
|
|
for tool_type in BUILTIN_TOOL_TYPES:
|
|
try:
|
|
tool = tool_type()
|
|
except Exception:
|
|
logger.warning("Failed to instantiate built-in tool %s", tool_type, exc_info=True)
|
|
continue
|
|
if tool.name in self._tools:
|
|
continue
|
|
self.register(tool)
|
|
|
|
def _resolve_request(
|
|
self,
|
|
name: str,
|
|
kwargs: dict[str, Any] | None = None,
|
|
) -> tuple[str, dict[str, Any]]:
|
|
if name in self._tools:
|
|
return name, dict(kwargs or {})
|
|
|
|
resolved_name, default_kwargs = TOOL_ALIASES.get(name, (name, {}))
|
|
merged_kwargs = {**default_kwargs, **(kwargs or {})}
|
|
|
|
return resolved_name, merged_kwargs
|
|
|
|
def get(self, name: str) -> BaseTool | None:
|
|
resolved_name, _ = self._resolve_request(name)
|
|
return self._tools.get(resolved_name)
|
|
|
|
def list_tools(self) -> list[str]:
|
|
return list(self._tools.keys())
|
|
|
|
def get_enabled(self, names: list[str]) -> list[BaseTool]:
|
|
"""Return tool instances for the given names (skipping unknown)."""
|
|
enabled: list[BaseTool] = []
|
|
seen: set[str] = set()
|
|
for name in names:
|
|
tool = self.get(name)
|
|
if tool is None or tool.name in seen:
|
|
continue
|
|
enabled.append(tool)
|
|
seen.add(tool.name)
|
|
return enabled
|
|
|
|
def get_definitions(self, names: list[str] | None = None) -> list[ToolDefinition]:
|
|
"""Return definitions for *names* (or all if None)."""
|
|
tools = self._tools.values() if names is None else self.get_enabled(names)
|
|
return [t.get_definition() for t in tools]
|
|
|
|
def get_prompt_hints(
|
|
self,
|
|
names: list[str],
|
|
language: str = "en",
|
|
) -> list[tuple[str, ToolPromptHints]]:
|
|
"""Return prompt hints for the given tool names."""
|
|
entries: list[tuple[str, ToolPromptHints]] = []
|
|
for tool in self.get_enabled(names):
|
|
entries.append((tool.name, tool.get_prompt_hints(language=language)))
|
|
return entries
|
|
|
|
def build_prompt_text(
|
|
self,
|
|
names: list[str],
|
|
format: str = "list",
|
|
language: str = "en",
|
|
**opts: Any,
|
|
) -> str:
|
|
"""Compose prompt text for the given tools."""
|
|
composer = ToolPromptComposer(language=language)
|
|
hints = self.get_prompt_hints(names, language=language)
|
|
if format == "list":
|
|
return composer.format_list(hints)
|
|
if format == "list_with_usage":
|
|
return composer.format_list_with_usage(hints)
|
|
if format == "table":
|
|
return composer.format_table(
|
|
hints,
|
|
control_actions=opts.get("control_actions"),
|
|
)
|
|
if format == "aliases":
|
|
return composer.format_aliases(hints)
|
|
if format == "phased":
|
|
return composer.format_phased(hints)
|
|
raise ValueError(f"Unsupported prompt format: {format}")
|
|
|
|
def build_openai_schemas(self, names: list[str] | None = None) -> list[dict[str, Any]]:
|
|
"""Build OpenAI function-calling tool schemas."""
|
|
return [d.to_openai_schema() for d in self.get_definitions(names)]
|
|
|
|
async def execute(self, name: str, /, **kwargs: Any):
|
|
"""Resolve aliases, execute the tool, and return its ToolResult.
|
|
|
|
``name`` (the tool to run) is positional-only so it never collides
|
|
with a tool argument that happens to also be called ``name`` — e.g.
|
|
``read_skill(name=...)`` or an MCP tool whose schema declares a
|
|
``name`` parameter. All callers already pass the tool name
|
|
positionally.
|
|
"""
|
|
resolved_name, resolved_kwargs = self._resolve_request(name, kwargs)
|
|
tool = self._tools.get(resolved_name)
|
|
if tool is None:
|
|
raise KeyError(f"Unknown tool: {name}")
|
|
return await tool.execute(**resolved_kwargs)
|
|
|
|
|
|
_default_registry: ToolRegistry | None = None
|
|
|
|
|
|
def get_tool_registry() -> ToolRegistry:
|
|
"""Return the global ToolRegistry (creating & loading builtins on first call)."""
|
|
global _default_registry
|
|
if _default_registry is None:
|
|
_default_registry = ToolRegistry()
|
|
_default_registry.load_builtins()
|
|
return _default_registry
|