03. Defining Tool Functions

Chapter 3 of 18 · 20 min

Tool definitions must translate into actual executable code. The mapping between JSON Schema definitions and Python functions forms the foundation of your function calling system.

A tool registry maps function names to callable implementations. Each entry contains the JSON Schema definition, the actual function, and any authentication or rate-limiting requirements.

import json
from typing import Callable, Any

class ToolRegistry:
    def __init__(self):
        self.tools: dict[str, dict] = {}
    
    def register(self, name: str, func: Callable, schema: dict) -> None:
        self.tools[name] = {
            "function": func,
            "schema": schema
        }
    
    def get_schema(self, name: str) -> dict | None:
        return self.tools.get(name, {}).get("schema")
    
    def execute(self, name: str, arguments: dict) -> Any:
        if name not in self.tools:
            raise ValueError(f"Unknown tool: {name}")
        return self.tools[name]["function"](**arguments)

Register tools with descriptive schemas:

def get_weather(city: str, units: str = "celsius") -> dict:
    # Implementation placeholder
    return {"city": city, "temp": 22, "units": units}

registry = ToolRegistry()
registry.register("get_weather", get_weather, {
    "type": "object",
    "properties": {
        "city": {
            "type": "string",
            "description": "City name for weather lookup"
        },
        "units": {
            "type": "string",
            "enum": ["celsius", "fahrenheit"]
        }
    },
    "required": ["city"]
})

Execution requires argument validation against the schema before calling:

def execute_tool(name: str, arguments: dict, schema: dict) -> dict:
    try:
        # Validate required fields
        for required_field in schema.get("required", []):
            if required_field not in arguments:
                return {
                    "success": False,
                    "error": f"Missing required field: {required_field}"
                }
        
        # Validate types
        for field, value in arguments.items():
            field_schema = schema.get("properties", {}).get(field, {})
            expected_type = field_schema.get("type")
            
            if expected_type == "number" and not isinstance(value, (int, float)):
                return {"success": False, "error": f"{field} must be a number"}
            elif expected_type == "string" and not isinstance(value, str):
                return {"success": False, "error": f"{field} must be a string"}
        
        result = registry.execute(name, arguments)
        return {"success": True, "result": result}
    except Exception as e:
        return {"success": False, "error": str(e)}

Security considerations matter for tool execution. Validate file paths to prevent directory traversal, sanitize SQL inputs to prevent injection, implement timeouts for long-running operations, and log all tool executions for audit purposes.

EXERCISE

Create a tool registry with three tools: a file reader, a web search simulator, and a calculator. Implement argument validation and handle execution errors gracefully.