Tool registry

The tools layer is where most agent code goes off the rails. People wire tools with long if-else chains in the handler, then every new tool means editing three places. The registry pattern fixes that: every tool registers its name and handler in one place, and the dispatch code never grows.

layers/tools.py
python
def get_time() -> str:
    return datetime.now(timezone.utc).isoformat()


def calculator(expression: str) -> str:
    expr = expression.replace("x", "*").replace("X", "*")
    expr = expr.strip()
    if not _SAFE_EXPR.match(expr):
        raise ValueError("unsupported characters in expression")
    value = eval(expr, {"__builtins__": {}}, {})
    return str(value)


class ToolsLayer:
    def __init__(self) -> None:
        self._registry: Dict[str, Callable] = {
            "get_time": lambda **_: get_time(),
            "calculator": lambda expression, **_: calculator(expression),
        }

    def list_tools(self) -> list[str]:
        return list(self._registry.keys())

    def run(self, tool_name: str, message: str) -> str:
        fn = self._registry.get(tool_name)
        if not fn:
            raise ValueError(f"unknown tool: {tool_name}")
        if tool_name == "calculator":
            match = re.search(r"[\d\s\+\-\*/xX\.\(\)]+", message)
            expression = match.group(0) if match else message
            return fn(expression=expression)
        return fn()

Two tools, one registry, one dispatch function. Add a new tool by adding one line to _registry. The orchestrator and the router never change.

Registry dispatch vs if-else chains

Registry keeps the dispatch site size constant as tools grow.

Quiz: Quiz

Loading practice…