Solving tips
- A registry is just a dict from tool name to callable; register writes to it and call reads from it.
- An unknown tool is the common failure — return a structured error the agent can read, don't crash the loop.
- Return the same shape on success and failure so the caller has one thing to parse.
Every agent needs a way to go from a tool name the model produced ("add", "search") to the actual Python function that runs it. That indirection layer is the tool registry. It is small, but it is where unknown-tool errors and tool crashes get turned into something the agent loop can handle instead of an exception that kills the run.
Task
Implement the ToolRegistry class:
register(name, fn) stores the callable fn under the string name. Registering the same name again overwrites the previous function.
call(name, args) looks up name and invokes fn(args), returning a structured dict:
- success:
{"ok": True, "tool": name, "result": <return value>}
- unknown tool:
{"ok": False, "tool": name, "error": "unknown tool: <name>"}
- tool raised:
{"ok": False, "tool": name, "error": "<exception message>"}
list_tools() returns the registered names as a sorted list.
The point is dispatch plus clean error handling. Both an unknown tool and a tool that raises must come back as a structured ok: False result, never an uncaught exception.
Example
reg = ToolRegistry()
reg.register("add", lambda args: args["a"] + args["b"])
reg.register("div", lambda args: args["a"] / args["b"])
reg.call("add", {"a": 2, "b": 3}) # -> {"ok": True, "tool": "add", "result": 5}
reg.call("mul", {"a": 2, "b": 3}) # -> {"ok": False, "tool": "mul", "error": "unknown tool: mul"}
reg.call("div", {"a": 1, "b": 0}) # -> {"ok": False, "tool": "div", "error": "division by zero"}
reg.list_tools() # -> ["add", "div"]
Constraints
- Do not call any real LLM, network, or external service — tools are plain Python callables passed in.
- A tool raising an exception must be caught and reported, not propagated.
list_tools() output is sorted so it is deterministic.
Approach
Hold a single dict from name to callable. register writes to it, call reads from it: a missing key becomes a structured unknown-tool error, and a try/except around the invocation turns any tool exception into the same ok: False shape. Returning one consistent result dict on every path is what lets the agent loop treat success and failure uniformly.
Solution
from typing import Any, Callable
class ToolRegistry:
def __init__(self) -> None:
self._tools: dict[str, Callable[[dict], Any]] = {}
def register(self, name: str, fn: Callable[[dict], Any]) -> None:
self._tools[name] = fn # overwrites any prior fn for this name
def call(self, name: str, args: dict) -> dict:
fn = self._tools.get(name)
if fn is None:
return {"ok": False, "tool": name, "error": f"unknown tool: {name}"}
try:
result = fn(args)
except Exception as e: # a broken tool must not kill the agent
return {"ok": False, "tool": name, "error": str(e)}
return {"ok": True, "tool": name, "result": result}
def list_tools(self) -> list[str]:
return sorted(self._tools)
Walkthrough
On the example:
register("add", ...) and register("div", ...) populate self._tools with two entries.
call("add", {"a": 2, "b": 3}): get finds the lambda, fn(args) returns 5, and we wrap it as {"ok": True, "tool": "add", "result": 5}.
call("mul", ...): get returns None, so we short-circuit to {"ok": False, "tool": "mul", "error": "unknown tool: mul"} without ever calling anything.
call("div", {"a": 1, "b": 0}): the lambda runs and raises ZeroDivisionError; the except catches it and returns {"ok": False, "tool": "div", "error": "division by zero"}.
list_tools() returns sorted(self._tools) -> ["add", "div"].
Complexity & notes
- Time:
register, call, and dict lookup are O(1); list_tools is O(n log n) for the sort over n tools. Space is O(n) for the registry.
- Catching broad
Exception is deliberate here: tool code is arbitrary and untrusted by the loop, so any failure should surface as data the model can read and retry on, not an uncaught crash. In production you would also log the traceback.
- The uniform result shape (
ok plus tool, then either result or error) is the real design point — the agent loop can branch on ok alone and never needs a try/except of its own.
- Natural extensions an interviewer may probe: storing a description/JSON-schema alongside each
fn to build the model’s tool spec, argument validation before dispatch, and guarding against duplicate registration instead of silently overwriting.