Syllabus Lesson 225 of 239 · Project: Tool-Using Research Agent
Project: Tool-Using Research Agent

Tool Registry + Dispatcher

Every "AI agent" you have ever read about bottoms out in one unglamorous component: a tool registry. The model emits something like {"tool": "search", "args": {"query": "weather"}}, and your code has to find the right Python function, check the arguments are sane, run it, and hand the result back. Get this layer wrong and the whole agent is one malformed argument away from a crash in production. This first lesson of the project builds exactly that layer, and you will reuse the idea in every lesson after it.

You are building the dispatcher for a research agent. It needs two tools to start: a search(query) that looks a fact up, and a calc(op, a, b) that does arithmetic. The registry maps a tool name to a pair: the function, and an argument schema (the set of argument names the tool accepts). Dispatch is the gate everything passes through.

class Registry:
    def __init__(self):
        self.tools = {}   # name -> {"fn": callable, "args": set_of_arg_names}

    def register(self, name, fn, args):
        self.tools[name] = {"fn": fn, "args": set(args)}

    def dispatch(self, name, args):
        # 1. unknown tool name      -> raise KeyError
        # 2. args keys != schema    -> raise ValueError (missing OR extra)
        # 3. otherwise              -> call fn(**args) and return the result
        ...

The rules dispatch must enforce (this is the whole point of the exercise):

  • An unknown tool name -> raise KeyError. The agent asked for something that does not exist.
  • The args dict's keys must match the registered schema exactly. A missing argument or an extra one -> raise ValueError. This is what stops a hallucinated {"q": ...} (wrong key) from silently doing nothing.
  • A valid call -> run fn(**args) and return whatever it returns.

Why a set for the schema? Because validation is then just set equality: set(args) == self.tools[name]["args"]. If that is false, figure out whether keys are missing or extra and raise ValueError either way. Calling with fn(**args) spreads the dict into keyword arguments, so the function signature does the final binding.

A hiring manager reading "built a tool-using agent" is really asking: did you handle the unhappy path? The agent that ships is the one whose dispatcher refuses bad calls loudly instead of corrupting a run. Press Run to register two tools and watch good calls succeed while bad ones raise.

Your turn

Build a Registry class. register(name, fn, args) stores the function and its argument-name schema (use a set). dispatch(name, args) must: raise KeyError for an unknown tool name; raise ValueError if args' keys do not match the schema exactly (missing OR extra); otherwise call fn(**args) and return the result. Register a search(query) and a calc(op, a, b) tool so the dispatcher has something real to route to.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output