Skip to main content

Build a Minimal Tool-Calling AI Agent in Python

A small tool-calling agent is easiest to understand when the handoff is visible: the model proposes a call, your Python code validates and executes it, and the result returns to the conversation. This tutorial follows that loop with a bounded example, explicit inputs, and a stopping condition. It also distinguishes a demonstration tool from an action that needs real permissions or approval.

The Minimal Tool-Calling Agent Execution Loop Application-controlled execution: The model decides intent; Python executes the side effect 1. User Request • Goal definition • Tool JSON schemas • Initial prompt 2. LLM Propose • Evaluates intent • Emits function_call • Emits arguments 3. App Validate • Verify allowlist • JSON schema parse • Safety check 4. Run Function • Python execution • get_weather(city) • Deterministic data 5. Answer • Append output • Final synthesis • Return to user Multi-turn Tool Loop (if further tools required)
Figure 1: Complete lifecycle of an application-controlled tool calling agent loop in Python.

Most AI applications generate text. An agent goes one step further: it interprets a goal, decides whether it needs an external tool, uses that tool, observes the result, and then produces an answer.

This tutorial builds a minimal tool-calling agent in Python. The agent answers a weather-related question by calling a local get_weather function before making a recommendation.

The implementation is intentionally small. It demonstrates the core agent loop without introducing unnecessary frameworks, memory layers, or multi-agent orchestration.

What Makes a System an Agent?

A chatbot can answer a question directly, but that answer may not be based on current or external data. A tool-using agent follows a different process:

1. Goal
Define the objective
→2. Decide
Select the tool
→3. Run safely
Run safely
→4. Observe
Read the result
→5. Answer
Write the answer

Figure 1. The model proposes a function call; the application controls execution.

Key Takeaways

  • An agent is a loop around a model, tools, state, and a stopping rule.
  • Start with one deterministic tool and validate its output.
  • Keep tool access allowlisted and observable.
  • Require human approval before state-changing actions.

The Minimal Agent Loop

The smallest useful agent loop contains four stages:

  1. Send the user request and available tool definitions to the model.
  2. Check whether the model requested a function call.
  3. Execute the requested function in the application.
  4. Send the function result back to the model.

If the model does not request a tool, the loop ends and the response is returned directly.

Project Setup

pip install openai

setx OPENAI_API_KEY "your_api_key_here"
setx OPENAI_MODEL "gpt-5"

The Responses API supports custom functions as tools, allowing the application to connect model decisions to its own code.

Define a Deterministic Tool

Our agent will use a local weather function. In production, this function could call a weather provider or an internal data service.

from typing import Dict

WEATHER_DATA: Dict[str, Dict[str, object]] = {
    "Seoul": {
        "condition": "light rain",
        "temperature_c": 18,
        "rain_probability": 80,
    },
    "Tokyo": {
        "condition": "clear",
        "temperature_c": 24,
        "rain_probability": 10,
    },
}


def get_weather(city: str) -> Dict[str, object]:
    """Return weather information for a supported city."""
    if city not in WEATHER_DATA:
        return {
            "city": city,
            "condition": "unknown",
            "temperature_c": None,
            "rain_probability": None,
            "error": "Weather data is not available for this city.",
        }

    return {"city": city, **WEATHER_DATA[city]}

The function is deterministic, so the example is reproducible. That is useful when testing the control loop before connecting a live service.

Describe the Tool to the Model

WEATHER_TOOL = {
    "type": "function",
    "name": "get_weather",
    "description": "Get the current weather information for a city.",
    "parameters": {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "The city name, such as Seoul or Tokyo.",
            }
        },
        "required": ["city"],
        "additionalProperties": False,
    },
    "strict": True,
}

The description tells the model when the function is relevant and what arguments it should provide. The schema is a first validation layer; it does not replace validation inside the application.

Implement the Agent

The model does not execute Python code by itself. It requests a function call. The application validates the request, executes the registered function, and sends the result back.

import json
import os
from typing import Any, Dict

from openai import OpenAI

client = OpenAI()
MODEL = os.getenv("OPENAI_MODEL", "gpt-5")
TOOLS = [WEATHER_TOOL]


def execute_tool(name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
    """Execute only tools explicitly registered by the application."""
    if name == "get_weather":
        return get_weather(**arguments)
    return {"error": f"Unknown tool: {name}"}


def run_agent(user_request: str, max_steps: int = 4) -> str:
    """Run a bounded tool-calling agent."""
    input_items = [{
        "role": "user",
        "content": user_request,
    }]

    for _ in range(max_steps):
        response = client.responses.create(
            model=MODEL,
            tools=TOOLS,
            input=input_items,
        )

        input_items.extend(response.output)
        function_calls = [
            item for item in response.output
            if item.type == "function_call"
        ]

        if not function_calls:
            return response.output_text

        for function_call in function_calls:
            try:
                arguments = json.loads(function_call.arguments)
                tool_result = execute_tool(
                    function_call.name,
                    arguments,
                )
            except (json.JSONDecodeError, TypeError) as error:
                tool_result = {
                    "error": f"Invalid tool arguments: {error}"
                }

            input_items.append({
                "type": "function_call_output",
                "call_id": function_call.call_id,
                "output": json.dumps(tool_result),
            })

    return "The agent stopped because it reached the maximum number of steps."


if __name__ == "__main__":
    print(run_agent(
        "What is the weather in Seoul, and should I carry an umbrella?"
    ))

The loop is bounded by max_steps. This prevents repeated calls, circular behavior, and unbounded API costs.

Example Execution

User:
What is the weather in Seoul, and should I carry an umbrella?

Agent decision:
Call get_weather(city="Seoul")

Tool result:
{
  "city": "Seoul",
  "condition": "light rain",
  "temperature_c": 18,
  "rain_probability": 80
}

Final answer:
Seoul is expected to have light rain with an 80% chance of precipitation.
The temperature is around 18°C, so carrying an umbrella would be a good idea.

The agent is using an external result as part of its decision process rather than producing an unsupported guess.

Validation and Failure Handling

Without validation, a plausible string can be treated as a successful tool result. A production tool boundary should validate:

  • argument types and required fields;
  • authorization and allowed operations;
  • response schema and required values;
  • timeouts and retry limits; and
  • business rules before state changes.
ConditionApplication responseContinue?
Valid resultAppend observation to stateYes, if another step is needed
Invalid argumentsReturn a structured errorOnly within retry budget
Unauthorized actionReject and log the requestNo
Retry budget exhaustedReturn failure with evidenceNo

Human Approval for Risky Actions

Reading weather data is harmless, so the example executes automatically. Sending an email, publishing a blog post, deleting a file, or making a purchase should use an approval boundary.

Agent proposes
action + parameters
→Policy check
scope + permissions
→Human approval
approve or reject
→Execute
record the result

Figure 2. The model can propose an action, but the application and human remain in control.

What This Example Does Not Demonstrate

This is a minimal tool-calling agent, not a complete autonomous system. It does not include long-term memory, a vector database, multi-agent collaboration, background scheduling, web browsing, automatic code execution, or guaranteed factual accuracy.

Those features can be added later, but the basic loop should be observable and testable first.

A Practical Development Path

  1. One tool: build a deterministic function and log every call.
  2. Read-only tools: add search, database lookup, or document retrieval.
  3. Validation: handle invalid arguments, unavailable data, timeouts, and API errors.
  4. Approval: require explicit approval before external side effects.
  5. Persistent state: store task status, tool results, and execution history.
  6. Evaluation: measure tool selection, argument validity, unnecessary calls, and failure explanations.

Conclusion

A useful AI agent does not need a complicated architecture. Its core behavior can be expressed as:

Goal → Decide → Call tool → Observe result → Respond

The important engineering work is controlling the boundary between the model and the tools. Start with one tool, one bounded loop, clear logs, and a human approval step for risky actions. Once that foundation works reliably, additional capabilities can be added without turning the system into an opaque collection of prompts and callbacks.

References

  1. OpenAI Developer Quickstart
  2. OpenAI Responses API Reference

Frequently Asked Questions (FAQ)

1. Why shouldn't the LLM execute Python code directly on the host machine?

Allowing an AI model to generate and execute arbitrary bash or Python code directly on host infrastructure introduces critical remote code execution (RCE) vulnerabilities. By strictly decoupling intent declaration (the model outputting JSON function arguments) from execution (the host Python application executing an allowlisted, validated function), you enforce a zero-trust security perimeter.

2. What happens if the model provides invalid arguments to a function?

Always wrap function execution in robust try-except blocks. If argument decoding fails, append a structured error dictionary (e.g., {"error": "Invalid argument format"}) to the conversation history and let the model observe the error and self-correct on its next iteration.

3. How do I prevent infinite tool-calling loops?

Always enforce a strict max_steps counter (typically 3 to 6 turns) in your primary agent loop. If the loop reaches the ceiling without concluding, force a termination event and return an explicit fallback explanation to the caller.

4. Can this architecture support asynchronous or long-running tools?

Yes. In production systems, you can convert the execution dispatcher into an asynchronous queue (using Celery, Redis, or FastAPI background tasks). The agent emits a task ID, pauses the session state, and resumes once external telemetry fires a completion webhook.

Editorial Verdict: Clean Foundations Before Framework Bloat

Modern agent frameworks like LangChain, CrewAI, or AutoGen offer valuable abstractions, but they frequently obscure the fundamental dynamics of function execution. By building a minimal, pure-Python tool loop from scratch, engineers gain total visibility over state management, token budgets, and security gates—ensuring their production systems remain robust, explainable, and dependable.

Editorial record

Editorial update — September 13, 2026: This revision replaces the abstract discussion with a reproducible tool-calling implementation, explicit validation, bounded retries, and human approval for risky actions.

Update history: September 13, 2026 — technical rewrite with a complete agent loop and new diagrams. Original publication: February 6, 2025.

Popular posts from this blog

Meta’s VideoJAM Explained: Why Motion Coherence Matters in AI Video

VideoJAM is easy to misunderstand because its name sounds like a product. The paper presents something narrower and more useful to analyze: a framework that gives a video generator an explicit motion prior. It is designed to address temporal coherence—the way objects, bodies, and physical interactions should remain plausible as a clip unfolds—not to provide a consumer dashboard with templates or a “generate video” button. Key Takeaways VideoJAM is a research framework for video generators, not a consumer-facing app with templates or a standalone UI. The training objective predicts appearance and motion from one joint representation; Inner-Guidance uses the model’s evolving motion prediction during sampling. On VideoJAM-bench, the authors reported motion-preference results against Sora, Kling 1.5, and Runway Gen3, while visual quality and motion remain separate measures. The experiments used 256×256, 128-frame clips at 24 fps and a motion-focused 128-prompt benchmark, so the results do ...

Grok 3’s 2025 Release: What xAI Announced, What Arrived, and What Changed

“When will Grok 3 launch?” was a moving target in February 2025. Elon Musk and xAI teased the model before the public beta announcement, so headlines often mixed a promised date with a product that was still being staged. The durable story is the timeline: a training claim, an announcement, limited access, and a later evaluation period are four different events. Key Takeaways xAI’s written Grok 3 announcement is dated February 19, 2025. That document date alone does not establish the time of a livestream or first access for every account. The beta included Grok 3, Grok 3 mini, optional Think modes, and the DeepSearch research agent. Early access was tied to Grok.com and X Premium tiers; xAI said API access was coming later at announcement time. A release date is not a quality guarantee. Check availability, rate limits, citations, and tool permissions in the environment you actually use. Open original image at full size ↗ Figure 1: Official NVIDIA–xAI visual from the October 2024 Coloss...

How to Process Apple Mail in Bulk with Claude: A Safer, Review-First Workflow

A crowded Apple Mail inbox is easier to review when message selection, classification, and the final action remain separate. This guide uses Claude to help summarize and draft from deliberately supplied text, while a person checks the original thread before sending, archiving, or deleting anything. The worked example uses fictional messages to make that review boundary concrete. Key Takeaways Claude is useful for triage, summaries, and draft replies, but a chat session is not a native Apple Mail control layer. A safer workflow separates four steps: select, redact, classify, and review. Use a structured output format so every message receives an explicit category, urgency, next action, and an uncertainty note. Measure the workflow on a small labelled sample before expanding it to a real mailbox. Open original diagram at full size ↗ Figure 1: Prompt chaining workflow from Anthropic: break a review task into explicit steps so selection, redaction, classification, and drafting c...