Back to course overview
Module 5From question to metric: the agent interface 25 min

Lab: Wire the tools to the layer

Trace and verify the tool dispatch that connects Claude's tool calls to your semantic layer, and test it without any API calls.

Outcome of this lab

A tested tools.py that turns a tool name + input into a real semantic-layer result — the bridge the agent will drive. No API key needed for this lab.

Step 1 — Read and trace the dispatcher

The dispatcher ships complete in agent/tools.py — your job here is to read it, trace what happens to a tool call, and then prove to yourself it behaves. (If you learn best by typing, delete the body of run_tool and rebuild it from memory — optional, but effective.) It takes a tool name and the model's input dict, runs the right semantic-layer call, and returns a JSON string (what a tool result must be). Crucially, it catches SemanticError and returns it as data so the model can read the error and fix its request — and it answers an unknown tool name with error JSON too, never a crash.

agent/tools.pypython
def run_tool(name: str, tool_input: dict) -> str:
    try:
        if name == "list_metrics":
            return json.dumps(_layer.catalog())
        if name == "query_metrics":
            result = _layer.query(
                metrics=tool_input.get("metrics", []),
                dimensions=tool_input.get("dimensions", []),
                filters=tool_input.get("filters", []),
                time_grain=tool_input.get("time_grain"),
                order_by=tool_input.get("order_by"),
                descending=tool_input.get("descending", True),
                limit=tool_input.get("limit"),
            )
            return json.dumps(result)
        return json.dumps({"error": f"Unknown tool '{name}'"})
    except SemanticError as e:
        return json.dumps({"error": str(e)})   # model reads this and retries

Step 2 — Test the bridge (no LLM required)

Run this from inside the agent/ folder (cd agent, venv active, then python3) — from tools import run_tool only resolves there:

pythonpython
from tools import run_tool
import json

print(json.loads(run_tool("list_metrics", {}))["metrics"].keys())
print(run_tool("query_metrics", {"metrics": ["revenue", "aov"], "dimensions": ["channel"]}))
print(run_tool("query_metrics", {"metrics": ["not_a_metric"]}))  # → clean error JSON
  1. 1Confirm list_metrics returns the full catalog.
  2. 2Confirm query_metrics returns rows for a valid request.
  3. 3Confirm an invalid metric name returns {"error": "..."} — not a crash. This graceful-error behavior is what lets the model self-correct next module.
Why errors-as-data matters

When the model picks a wrong name, it receives the error text as the tool result, reads "Unknown metric 'profit'; known: [...]", and retries with a valid name — often in the same turn. Returning errors as readable data (not exceptions) turns the model into a self-correcting user of your layer.