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.