Replace AI tool dispatch switch with registry-backed handlers
Problem
Refactor dataface/ai/tools.py so canonical tool schemas and handlers are registered in one place instead of maintained through a growing string-based if/elif dispatch chain. The result should reduce drift between schemas, handlers, and client surfaces and make new tool additions additive rather than branching edits.
Context
dataface/ai/tools.py:dispatch_tool_call routes tool names to implementations via
a 7-branch if/elif chain. Each branch manually extracts args from the dict and injects
context values. Adding a tool means editing the middle of this function — branching
rather than additive. The same tool names are already defined in tool_schemas.py
(canonical schemas) and tool_contracts.py (response contracts), so there are three
places that must stay in sync.
Key consumers: MCP server (mcp/server.py), playground (apps/playground/ai_service.py),
cloud (apps/cloud/apps/ai/views.py), terminal agent (agent.py). All call
dispatch_tool_call or handle_tool_call.
Possible Solutions
-
Dict registry of handler functions — map tool name → callable
(args, context) -> dictat module level.dispatch_tool_callbecomes a dict lookup. Adding a tool = define one handler function + one dict entry. Minimal abstraction, no decorators, no classes. Recommended — simplest change that solves the problem. -
Decorator-based registry —
@register_tool("name")decorator auto-populates the dict. Slightly more magic; not warranted for 7 tools. -
Dataclass/TypedDict tool descriptors — unify schema + handler + contract into one object. Over-engineered for this task; larger blast radius.
Plan
- Write a test that asserts
TOOL_HANDLERSdict exists and covers all tool names. - In
tools.py, define one handler function per tool (_handle_render_dashboard, etc.) that takes(args, context)and returns a dict. - Build
TOOL_HANDLERS: dict[str, Callable]mapping names to handlers. - Replace the if/elif chain in
dispatch_tool_callwith a dict lookup + call. - Keep
handle_tool_call,get_tools, and all TOOL_* constants unchanged — public API stable. - Run
just test-file tests/core/test_ai_tools.py+just fix.
Implementation Progress
- Replaced 7-branch if/elif chain in
dispatch_tool_callwithTOOL_HANDLERSdict mapping tool names to handler functions. - Each handler is a standalone function
(args, context) -> dictwith lazy imports. - Shared
_resolve_base_dirhelper for render/review tools. information_schemaalias preserved as a dict entry pointing to_handle_catalog.dispatch_tool_callreduced to 3 lines: lookup, fallback error, call.- Public API (
handle_tool_call,dispatch_tool_call,get_tools, TOOL_* constants) unchanged. - Added
TestToolHandlersRegistrytest class: coverage check, no-extras check, callable check, unknown-tool check. - All 45 AI tests pass, mypy clean, ruff clean, black formatted.
QA Exploration
- [x] QA exploration completed (or N/A for non-UI tasks)
Review Feedback
- [ ] Review cleared