Lesson 6 / 25

A Tool Registry in Code

Register Python functions as tools, generate their schemas and dispatch calls safely.

One source of truth

Writing a schema by hand next to each function invites mismatches. A small registry reads each function's signature, builds the schema, and stores the function by name. Then adding a tool means writing one function.

A decorator that builds the schema

This ran as shown. A parameter with no default becomes required; rate has a default so it is optional. The decorator reads the docstring as the description.

import inspect, json
TOOLS = {}
PY2JSON = {int: "integer", float: "number", str: "string", bool: "boolean"}

def tool(fn):
    sig = inspect.signature(fn)
    props = {n: {"type": PY2JSON[p.annotation]} for n, p in sig.parameters.items()}
    required = [n for n, p in sig.parameters.items() if p.default is inspect._empty]
    TOOLS[fn.__name__] = {"fn": fn, "schema": {
        "name": fn.__name__,
        "description": (fn.__doc__ or "").strip(),
        "input_schema": {"type": "object", "properties": props, "required": required}}}
    return fn

@tool
def get_balance(account_id: int) -> str:
    """Return the current balance of one account."""
    return f"account {account_id}: 1500"

@tool
def convert(amount: float, rate: float = 1.0) -> str:
    """Convert an amount using a rate."""
    return str(round(amount * rate, 2))

print(json.dumps(TOOLS["convert"]["schema"]["input_schema"]))
print(sorted(TOOLS))

Output:

{"type": "object", "properties": {"amount": {"type": "number"}, "rate": {"type": "number"}}, "required": ["amount"]}
['convert', 'get_balance']

A safe dispatcher

Validation errors become tool results the model can read and fix. These four calls ran as shown, in order.

def call_tool(name, args):
    if name not in TOOLS:
        return {"is_error": True, "content": f"Unknown tool '{name}'. Available: {sorted(TOOLS)}"}
    schema = TOOLS[name]["schema"]["input_schema"]
    missing = [r for r in schema["required"] if r not in args]
    if missing:
        return {"is_error": True, "content": f"Missing required argument(s): {missing}"}
    extra = [a for a in args if a not in schema["properties"]]
    if extra:
        return {"is_error": True, "content": f"Unexpected argument(s): {extra}"}
    try:
        return {"is_error": False, "content": TOOLS[name]["fn"](**args)}
    except Exception as exc:
        return {"is_error": True, "content": f"{type(exc).__name__}: {exc}"}

print(call_tool("get_balance", {"account_id": 7}))
print(call_tool("get_balance", {}))
print(call_tool("get_balanse", {"account_id": 7}))
print(call_tool("convert", {"amount": 10, "rate": 0.5}))

Output:

{'is_error': False, 'content': 'account 7: 1500'}
{'is_error': True, 'content': "Missing required argument(s): ['account_id']"}
{'is_error': True, 'content': "Unknown tool 'get_balanse'. Available: ['convert', 'get_balance']"}
{'is_error': False, 'content': '5.0'}

Quick check: A call omits a required argument. What should the dispatcher do?

  • Crash the whole program
  • Delete the tool
  • Guess a value silently
  • Return an error result naming the missing argument
Answer

Return an error result naming the missing argument — A clear error lets the model correct the call on the next turn.