Introduce a centralized tool registry system for the assistant to manage executable operations, permissions, and tool catalogs. This refactors the assistant dispatch logic from a monolithic approach to a modular, decorator-based registry. Key changes: - Implement `AssistantToolRegistry` to handle tool registration, permission checks, and safe operation identification. - Add a suite of new assistant tools: backup, commit, branch creation, deployment, health summary, environment listing, LLM operations, maintenance, migration, and dashboard searching. - Refactor `_dispatch.py` and `_llm_planner.py` to utilize the new registry for intent resolution and tool catalog building. - Enhance the LLM planner to support more descriptive clarification responses in Russian when intents are ambiguous. - Update the frontend to support the new tool-based workflow, including improved i18n labels for assistant operations and a new step-by-step wizard for the migration page. - Add ADR-0008 to document the architectural decision for the tool registry.
6.7 KiB
[DEF:ADR-0008:ADR]
@STATUS ACTIVE
@PURPOSE Replace the manual five-file tool registration (tool catalog, dispatch, permissions, safe-ops, command parser) with a single @assistant_tool decorator-based registry that automatically generates the LLM catalog, dispatches operations, and enforces permissions — eliminating cross-file desynchronisation when adding new assistant commands.
@RELATION DEPENDS_ON -> [ADR-0001:ADR]
@RELATION DEPENDS_ON -> [AssistantSchemas]
@RELATION CALLS -> [AssistantToolRegistry]
@RATIONALE The current architecture requires editing 4–5 separate files per new tool: _build_tool_catalog() (tool metadata), _dispatch_intent() (routing), INTENT_PERMISSION_CHECKS (permissions), _SAFE_OPS (safety classification), and optionally _parse_command() (keyword fallback). In practice, developers (both human and AI) frequently miss one of these, resulting in tools that appear in the LLM catalog but fail at dispatch, or dispatch successfully but bypass permission checks. A decorator-based registry collocates all tool metadata with its handler, making it impossible to register an incomplete tool. The registry also enables automatic catalog generation, eliminating the hand-maintained _build_tool_catalog().
@REJECTED Centralised YAML/JSON config file (e.g. tools.yaml) — rejected because it separates tool metadata from implementation, requiring developers to jump between config and code. It also cannot express permission-check logic or handler imports, so it would still need a parallel registration step, reducing the benefit.
@REJECTED Automatic discovery via filesystem scanning (e.g. importlib scanning _tool_*.py) — rejected because it makes the tool set implicit; adding a _tool_*.py file silently registers a tool, which is dangerous for guarded or destructive operations. Explicit decorator registration in a known _tool_registry.py module keeps the tool set fully visible and auditable.
@REJECTED Incremental improvement of existing pattern (add one more dict, add one more if) — rejected because it does not solve the root problem: the five-file scatter. Any new file or dict that must be kept in sync with the handler will eventually desynchronise.
@DATA_CONTRACT Handler[Intent, User, TaskManager, ConfigManager, Session] -> [text: str, task_id: str | None, actions: list[AssistantAction]]
Decision
1. New file: _tool_registry.py
A central registry module that provides:
@assistant_tool(
operation="search_dashboards",
domain="dashboards",
description="Search and list dashboards by name, id, or get all dashboards",
optional_entities=["search", "environment", "page", "page_size"],
risk_level="safe",
requires_confirmation=False,
permission_checks=[("dashboards", "READ")],
)
async def handle_search_dashboards(intent, user, task_manager, config_manager, db):
...
The decorator stores the tool definition in a module-level dict[str, AssistantTool] and attaches it as func.__assistant_tool__ for optional introspection.
2. Auto-generated catalog
_build_tool_catalog() is replaced by get_catalog(user) which:
- Iterates all registered tools
- Filters by user permissions via
_has_any_permission() - Returns LLM-ready dict list
3. Auto-dispatched routing
_dispatch_intent() is replaced by dispatch(operation, ...) which:
- Looks up
operationin the registry - Calls
tool.handler(intent, user, task_manager, config_manager, db) - Returns
(text, task_id, actions)
4. Derived lists
_SAFE_OPS is derived from risk_level == "safe" in the registry. INTENT_PERMISSION_CHECKS is derived from tool.permission_checks. Both hardcoded dicts are removed.
5. Migration strategy
Each existing tool handler is moved from _dispatch.py:_dispatch_intent() into its own _tool_*.py file with an @assistant_tool decorator. Tools are migrated one at a time, in this order:
| Step | Tool | File |
|---|---|---|
| 1 | show_capabilities |
_tool_capabilities.py |
| 2 | get_task_status |
_tool_task_status.py |
| 3 | create_branch |
_tool_create_branch.py |
| 4 | commit_changes |
_tool_commit.py |
| 5 | deploy_dashboard |
_tool_deploy.py |
| 6 | execute_migration |
_tool_migration.py |
| 7 | run_backup |
_tool_backup.py |
| 8 | run_llm_validation |
_tool_llm_validation.py |
| 9 | run_llm_documentation |
_tool_llm_documentation.py |
| 10 | get_health_summary |
_tool_health_summary.py |
After migration, _dispatch.py is reduced to a thin dispatch() wrapper. The _parse_command() function is updated to also use the registry for keyword-to-operation mapping.
6. File layout after migration
backend/src/api/routes/assistant/
├── __init__.py
├── _admin_routes.py # admin endpoints (list_conversations, delete, history, audit)
├── _routes.py # main router (send_message, confirm, cancel)
├── _tool_registry.py # @assistant_tool decorator, get_catalog(), dispatch()
├── _tool_capabilities.py
├── _tool_task_status.py
├── _tool_create_branch.py
├── _tool_commit.py
├── _tool_deploy.py
├── _tool_migration.py
├── _tool_backup.py
├── _tool_llm_validation.py
├── _tool_llm_documentation.py
├── _tool_health_summary.py
├── _schemas.py # Pydantic models, in-memory stores
├── _history.py # Conversation history helpers
├── _dispatch.py # removed or thin wrapper
├── _llm_planner.py # removed _build_tool_catalog, uses get_catalog()
├── _llm_planner_intent.py
├── _command_parser.py
├── _resolvers.py
├── _dataset_review.py
└── _dataset_review_dispatch.py
Consequences
Positive
- Single point of registration — adding a tool requires editing exactly one file: the tool handler with its
@assistant_tooldecorator - No desync — catalog, dispatch, permissions, and safety classification all derive from the same decorator metadata
- Testable —
_tool_registry.pycan be unit-tested without DB or HTTP fixtures - Discoverable —
from _tool_registry import _toolsgives a complete inventory of all registered tools - Safe-by-default — a tool without
@assistant_toolis not registered and cannot be invoked
Negative
- One-time migration cost — 10 existing handlers must be moved to new files (~200 LOC total)
- Import overhead — each
_tool_*.pymust be imported somewhere to trigger decorator registration (solved by importing them in__init__.pyor_tool_registry.py) - Dynamic dispatch — replaces static
if/elifwith dict lookup, which is marginally less explicit (mitigated by strong typing ofAssistantTooldataclass)