Getting Started with AI Agents in Python: A Beginner's Guide

J
jai
27 Oct, 2026 6 min read 50 views

AI agents are programmatically driven systems that use large language models (LLMs) to reason through tasks, decide which tools to execute, and autonomously take actions until a goal is achieved. Unlike simple prompt-and-response applications, an AI agent operates in a dynamic feedback loop: it evaluates context, invokes tools like external APIs or database queries, processes the tool output, and determines the next step.

In this beginner-friendly guide, you will learn how AI agents work under the hood by building a clean, lightweight agent loop in Python from scratch using the OpenAI Python SDK. We will also explore how tool calling works and briefly look at when you should adopt frameworks like Pydantic AI, LangGraph, or CrewAI.

The Core Components of an AI Agent

Before writing code, it helps to understand the four primary building blocks of any AI agent:

  1. The Model (Brain): An LLM that understands user instructions, reasons through problem steps, and formulates action plans.
  2. Tools (Capabilities): Python functions that give the agent abilities beyond text generation—such as searching the web, executing code, or querying database tables.
  3. The Loop (Execution Cycle): The control structure that continuously sends prompt context to the model, inspects whether the model requested a tool execution, calls the required function, feeds the tool output back to the model, and repeats until a final answer is returned.
  4. Memory / State (Context): The history of messages and tool responses that informs subsequent decisions across conversation turns.

Prerequisites

To follow along with the tutorial, ensure you have Python 3.10+ installed along with the official OpenAI library:

pip install openai pydantic

Set your OpenAI API key as an environment variable in your terminal:

export OPENAI_API_KEY="your-api-key-here"

Step 1: Defining Tools for the Agent

Tools are standard Python functions decorated or described using JSON schema definitions. Modern LLM APIs support native function calling, where you provide function definitions to the model and it returns structured tool call requests when needed.

Let us define two simple tools: a mock weather finder and a basic calculator.

import json
from openai import OpenAI

client = OpenAI()

# Tool 1: Mock weather tool
def get_weather(location: str) -> str:
    """Returns fake weather data for a given location."""
    data = {
        "london": "15°C, Partly Cloudy",
        "tokyo": "22°C, Sunny",
        "new york": "18°C, Rain"
    }
    loc_clean = location.lower().strip()
    return data.get(loc_clean, f"20°C, Clear (default for {location})")

# Tool 2: Basic calculator
def calculate(expression: str) -> str:
    """Evaluates a basic math expression securely."""
    try:
        # Keep evaluation simple for demonstration
        result = eval(expression, {"__builtins__": None}, {})
        return str(result)
    except Exception as err:
        return f"Error evaluating expression: {err}"

# Map tool names to actual Python functions
AVAILABLE_TOOLS = {
    "get_weather": get_weather,
    "calculate": calculate
}

Next, define the tools schema so the LLM understands how and when to invoke these functions:

tools_schema = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather information for a specific city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "The city name, e.g., London or Tokyo"
                    }
                },
                "required": ["location"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Perform basic arithmetic calculations.",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Mathematical expression, e.g., '15 * 4 + 10'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

Step 2: Building the Core Agent Loop

The heart of an agent is an iterative loop. In each turn:

  • We pass the conversation history to the model.
  • If the model returns standard text, the agent is finished and returns the answer.
  • If the model requests one or more tool calls, we execute those functions, append the results to the message history, and repeat the loop.

Here is a complete function implementing the agent loop with turn limits to prevent infinite loops:

def run_agent(user_prompt: str, max_turns: int = 5) -> str:
    messages = [
        {
            "role": "system",
            "content": "You are a helpful AI assistant equipped with tools. Always use available tools to retrieve factual answers when required."
        },
        {
            "role": "user",
            "content": user_prompt
        }
    ]

    for turn in range(max_turns):
        print(f"\n--- Agent Turn {turn + 1} ---")

        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=tools_schema,
            tool_choice="auto"
        )

        response_message = response.choices[0].message
        messages.append(response_message)

        # Check if the model wants to call tools
        tool_calls = response_message.tool_calls

        if not tool_calls:
            # Model didn't request any tool, final answer reached
            print("Agent finished reasoning.")
            return response_message.content

        # Process each requested tool call
        for tool_call in tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)
            
            print(f"Tool Requested: {func_name}({func_args})")

            tool_func = AVAILABLE_TOOLS.get(func_name)
            if tool_func:
                if func_name == "get_weather":
                    result = tool_func(location=func_args.get("location"))
                elif func_name == "calculate":
                    result = tool_func(expression=func_args.get("expression"))
                else:
                    result = "Unknown tool requested."
            else:
                result = f"Error: Tool {func_name} not implemented."

            print(f"Tool Output: {result}")

            # Append tool result back to the context
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

    return "Agent reached maximum execution turns without completing the request."

Step 3: Running the Agent

Now you can test your Python agent with a complex query that requires multiple tool executions:

if __name__ == "__main__":
    prompt = "What is the weather in Tokyo right now? Also, calculate what 25 multiplied by 18 is."
    print(f"User Request: {prompt}")
    
    final_response = run_agent(prompt)
    print("\n=== Final Answer ===")
    print(final_response)

When you run this script, the agent:

  1. Recognizes that it needs weather for Tokyo and a math calculation.
  2. Calls get_weather(location="Tokyo") and calculate(expression="25 * 18").
  3. Receives 22°C, Sunny and 450.
  4. Synthesizes both results into a friendly final response.

When to Move from Raw Loops to Agent Frameworks

Building a raw loop from scratch gives you full control and helps you understand how agent state and tool calling work. However, as agent applications grow in complexity, handling custom edge cases, state persistence, structured input validation, and multi-agent delegation becomes challenging.

Here is when you should consider using high-level Python agent frameworks:

  • Pydantic AI: Excellent for type-safe structured data extraction, tool outputs validated via Pydantic models, and strict output typing.
  • LangGraph: Best when you need explicit control over workflows, state machines, cyclic execution graphs, long-term memory, and human-in-the-loop approval steps.
  • CrewAI: Ideal for role-based multi-agent teams where specialized agents collaborate to complete complex project workflows.
  • OpenAI Agents SDK: Useful when building native workflows heavily aligned with OpenAI assistant platforms and standardized tool handoffs.

Summary

Getting started with AI agents in Python does not require learning complex dependencies right away. By understanding the loop—sending context, parsing tool calls, executing functions, and returning output—you can construct custom, lightweight agents tailored to your exact needs.

As your requirements expand to multi-agent collaboration, memory persistence, or human intervention, higher-level frameworks like Pydantic AI and LangGraph offer robust abstractions built on these exact foundation principles.

Further reading

Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.