Skip to main content
Article
tool-callingfunction-callingpythonai-agentsopenaipydanticautomationscript

Build a Robust Tool-Calling Agent with Python

Use the Tool Calling Setup script to build a modular AI agent. This pack shows you how to define custom tools with typed arguments using Pydantic, register them, and run an agent that can use your new tools securely.

intermediate30 min4 steps
The play
  1. Understand the Core Structure
    Download or clone the 'Tool Calling Setup' script. Familiarize yourself with its modular design: a central agent loop, a pluggable tool registry, and individual tool definitions. This separation makes it easy to add or remove capabilities without rewriting the core logic.
  2. Define a Custom Tool with Pydantic
    Create a new tool by defining a Python function and a Pydantic model for its arguments. This enables automatic validation and provides a clear schema for the LLM. For example, let's create a simple weather tool.
  3. Register Your New Tool
    The 'Tool Calling Setup' uses a central registry to know which tools are available. Import your new tool and add its function and argument schema to the registry. This makes it discoverable by the agent.
  4. Run the Agent and Test the Tool
    Execute the main agent script. Give it a prompt that requires your new tool, like 'What's the weather in Boston?'. The agent will identify the correct tool, validate the 'location' argument, execute the function, and use its output to answer your question.
Starter code
import os
import json
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Dict, Any, Callable

# --- 1. Tool Definition (e.g., tools/weather.py) ---
class GetWeatherArgs(BaseModel):
    location: str = Field(..., description="The city, e.g., San Francisco")

def get_weather(args: GetWeatherArgs) -> str:
    """Gets the current weather for a specified location."""
    print(f"[Tool Executed] Getting weather for {args.location}")
    # In a real app, you'd call a weather API here.
    if "boston" in args.location.lower():
        return json.dumps({"temperature": "52°F", "conditions": "cloudy"})
    return json.dumps({"temperature": "75°F", "conditions": "sunny"})

# --- 2. Tool Registry (e.g., tool_registry.py) ---
# The registry holds the function reference and its Pydantic schema
TOOL_REGISTRY: Dict[str, Dict[str, Any]] = {
    "get_weather": {
        "function": get_weather,
        "pydantic_model": GetWeatherArgs
    }
}

# --- 3. Agent Logic (e.g., agent.py) ---
def run_conversation(user_prompt: str, client: OpenAI):
    print(f"\nUser: {user_prompt}")
    messages = [{"role": "user", "content": user_prompt}]
    
    # Convert Pydantic models to OpenAI tool format
    tools_for_api = [
        {
            "type": "function",
            "function": {
                "name": name,
                "description": details["function"].__doc__,
                "parameters": details["pydantic_model"].model_json_schema()
            }
        }
        for name, details in TOOL_REGISTRY.items()
    ]

    # First API call to see if a tool is needed
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools_for_api,
        tool_choice="auto",
    )
    response_message = response.choices[0].message
    messages.append(response_message)

    # Check for tool calls
    if response_message.tool_calls:
        for tool_call in response_message.tool_calls:
            tool_name = tool_call.function.name
            print(f"[LLM decided to call tool: {tool_name}]")
            
            if tool_name in TOOL_REGISTRY:
                tool_info = TOOL_REGISTRY[tool_name]
                function_to_call = tool_info["function"]
                pydantic_model = tool_info["pydantic_model"]
                
                try:
                    # Validate arguments with Pydantic
                    tool_args = pydantic_model.model_validate_json(tool_call.function.arguments)
                    function_response = function_to_call(tool_args)
                except Exception as e:
                    print(f"[Error] Invalid arguments: {e}")
                    function_response = f"Error: {e}"

                messages.append(
                    {
                        "tool_call_id": tool_call.id,
                        "role": "tool",
                        "name": tool_name,
                        "content": function_response,
                    }
                )
            else:
                 print(f"[Error] Tool '{tool_name}' not found in registry.")

        # Second API call to get a final response based on tool output
        final_response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
        )
        print(f"Agent: {final_response.choices[0].message.content}")
    else:
        print(f"Agent: {response_message.content}")

if __name__ == "__main__":
    # Make sure to set your OPENAI_API_KEY environment variable
    try:
        client = OpenAI()
    except Exception as e:
        print("Error: OpenAI API key not configured.")
        print("Please set the OPENAI_API_KEY environment variable.")
        exit(1)

    run_conversation("What is the weather like in Boston?", client)
Build a Robust Tool-Calling Agent with Python — Action Pack