Build an AI Agent in Python: Complete Tutorial with Code

Build an AI agent in Python with COMPLETE code examples. From scratch or with frameworks — tool calling, memory, agent loop. Start building now.

Quick Answer: You can build a working AI agent in Python in under 80 lines of code — an LLM connection, a tool registry, and a loop that decides when to act and when to stop.


Building an AI agent in Python is easier than most tutorials make it seem. At its core, an agent is just a loop: the LLM thinks, decides whether to call a tool, reads the result, and repeats until the task is done. No PhD required, no massive framework dependency — just Python and an API key. Tools like cowork.ink make it even simpler when you're ready to orchestrate agents across a team.

This tutorial walks you through building an AI agent from scratch, then shows you when and how to use a framework instead. You'll get complete, runnable code at every step.

What You'll Build

A Python AI agent that can answer questions, call external tools (like a calculator and web search), maintain conversation memory, and decide autonomously when to stop. All in pure Python — no frameworks.

Prerequisites

Before you start, make sure you have:

  • Python 3.10+ installed
  • An API key from OpenAI, Anthropic, or Google (we'll use OpenAI in this tutorial)
  • Basic Python knowledge (functions, classes, dictionaries)

Set up your project:

mkdir my-ai-agent && cd my-ai-agent
python -m venv venv && source venv/bin/activate
pip install openai python-dotenv

Create a .env file with your API key:

OPENAI_API_KEY=sk-your-key-here

Step 1 — Connect to the LLM

Every agent starts with an LLM connection. This is the "brain" that reasons about what to do next.

import os
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

def chat(messages: list[dict]) -> dict:
    """Send messages to the LLM and return the response."""
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
    )
    return response.choices[0].message

This function sends a conversation history to the LLM and returns its response. Simple — but not yet an agent. An agent needs tools to interact with the world.


Step 2 — Define Tools

Tools are functions the agent can call. You define both the function itself and a schema that tells the LLM what the tool does, what parameters it expects, and when to use it.

import json

# The actual tool functions
def calculate(expression: str) -> str:
    """Evaluate a math expression safely."""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "Error: invalid characters in expression"
    try:
        return str(eval(expression))  # safe with character allowlist
    except Exception as e:
        return f"Error: {e}"

def get_weather(city: str) -> str:
    """Get current weather for a city (mock)."""
    # Replace with a real API call in production
    weather_data = {
        "london": "15°C, cloudy",
        "new york": "22°C, sunny",
        "tokyo": "18°C, rainy",
    }
    return weather_data.get(city.lower(), f"No data for {city}")

# Tool schemas for the LLM
tools = [
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Evaluate a mathematical expression",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Math expression, e.g. '2 + 2 * 3'"
                    }
                },
                "required": ["expression"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name, e.g. 'London'"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

# Registry maps tool names to functions
tool_registry = {
    "calculate": calculate,
    "get_weather": get_weather,
}

The schema format follows the OpenAI function calling spec, which Anthropic and Google also support with minor variations.

Why Define Schemas?

The LLM never executes code directly. It returns a JSON object saying which tool to call and with what arguments. Your code then executes the function and feeds the result back. This is the foundation of how AI agents work.


Step 3 — Build the Agent Loop

The agent loop is the core pattern that makes an agent an agent. It's what separates autonomous AI from a simple chatbot. The loop follows the ReAct pattern — Reason, Act, Observe, Repeat.

def run_agent(user_message: str, system_prompt: str = None):
    """Run the agent loop until the task is complete."""
    messages = []

    if system_prompt:
        messages.append({"role": "system", "content": system_prompt})

    messages.append({"role": "user", "content": user_message})

    max_iterations = 10  # safety limit

    for i in range(max_iterations):
        # 1. Think — ask the LLM what to do
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
        )
        assistant_message = response.choices[0].message
        messages.append(assistant_message)

        # 2. Check — did the LLM want to call a tool?
        if not assistant_message.tool_calls:
            # No tool calls = the agent is done
            return assistant_message.content

        # 3. Act — execute each tool call
        for tool_call in assistant_message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)

            # Execute the tool
            func = tool_registry.get(func_name)
            if func:
                result = func(**func_args)
            else:
                result = f"Error: unknown tool '{func_name}'"

            # 4. Observe — feed the result back to the LLM
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result),
            })

        # Loop continues — LLM will see the tool results
        # and decide whether to call more tools or respond

    return "Agent reached maximum iterations."

Here's what happens at each step:

  1. Think — the LLM receives the full conversation and decides what to do
  2. Check — if no tool calls are returned, the agent has its final answer
  3. Act — execute the requested tool(s) and capture results
  4. Observe — append results to the conversation so the LLM can see them

The loop repeats until the LLM responds without requesting any tools — that's the natural stop condition.


Step 4 — Add Conversation Memory

Right now our agent handles one request and exits. To make it conversational, add memory — a persistent message list across turns.

class Agent:
    def __init__(self, system_prompt: str = "You are a helpful assistant."):
        self.client = OpenAI()
        self.messages = [{"role": "system", "content": system_prompt}]

    def chat(self, user_message: str) -> str:
        """Send a message and get a response, maintaining history."""
        self.messages.append({"role": "user", "content": user_message})

        for _ in range(10):  # max iterations
            response = self.client.chat.completions.create(
                model="gpt-4o",
                messages=self.messages,
                tools=tools,
            )
            msg = response.choices[0].message
            self.messages.append(msg)

            if not msg.tool_calls:
                return msg.content

            for tc in msg.tool_calls:
                func = tool_registry.get(tc.function.name)
                result = func(**json.loads(tc.function.arguments)) if func else "Unknown tool"
                self.messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": str(result),
                })

        return "Max iterations reached."

Now you can have multi-turn conversations:

agent = Agent("You are a helpful math and weather assistant.")
print(agent.chat("What's 234 * 567?"))
print(agent.chat("Now divide that result by 3"))
print(agent.chat("What's the weather in Tokyo?"))

The agent remembers previous answers because self.messages persists between calls. For deeper memory strategies — including long-term and semantic memory — see our guide to AI agent memory.


Step 5 — Test Your Agent

Run a few test cases to verify everything works:

if __name__ == "__main__":
    agent = Agent(
        "You are a helpful assistant with access to a calculator and weather data. "
        "Use tools when needed. Be concise."
    )

    # Test 1: Direct question (no tools needed)
    print(agent.chat("What is an AI agent?"))

    # Test 2: Single tool call
    print(agent.chat("What's 15% of 2499.99?"))

    # Test 3: Multiple tool calls
    print(agent.chat("What's the weather in London and New York?"))

    # Test 4: Multi-step reasoning
    print(agent.chat("If London is 15°C, convert that to Fahrenheit"))

You should see the agent automatically deciding when to use tools and when to answer directly.


When to Use a Framework Instead

Building from scratch teaches you the fundamentals, but frameworks save time in production. Here's when to reach for one:

ScenarioRecommendation
Learning / prototypingBuild from scratch (as above)
Complex stateful workflowsLangGraph — graph-based control flow, checkpointing
Multi-agent collaborationCrewAI — role-based agents, built-in MCP support
Fast GPT-only prototypeOpenAI Agents SDK — working agent in ~20 lines
Team orchestrationcowork.ink — shared workspace, no code required

For a detailed framework breakdown, see our LangGraph vs CrewAI vs OpenAI Agents SDK comparison. If you want to build on top of the LangChain ecosystem specifically, our LangChain tutorial walks through the fundamentals.

Quick Example: LangGraph Agent

Here's the same agent built with LangGraph for comparison:

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def calculate(expression: str) -> str:
    """Evaluate a math expression."""
    allowed = set("0123456789+-*/.() ")
    if not all(c in allowed for c in expression):
        return "Error: invalid characters"
    return str(eval(expression))

llm = ChatOpenAI(model="gpt-4o")
agent = create_react_agent(llm, [calculate])

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's 234 * 567?"}]}
)
print(result["messages"][-1].content)

LangGraph reduces boilerplate but hides the loop. Start from scratch first, then adopt a framework when you need features like agent orchestration or persistence. For a deeper LangGraph walkthrough, see our LangGraph tutorial.


Production Best Practices

Before deploying your agent, address these concerns:

  1. Set iteration limits. Always cap the agent loop (we used max_iterations = 10). Runaway agents burn tokens and money.
  2. Validate tool inputs. Never pass unsanitized input to eval(), exec(), or shell commands. Use allowlists like the calculator example above.
  3. Handle errors gracefully. Wrap tool calls in try/except and return error messages the LLM can understand and recover from.
  4. Monitor costs. Each loop iteration is an API call. Track token usage and set budget alerts. See our AI agent cost optimization guide for strategies.
  5. Add logging. Log every tool call, its arguments, and results. You'll need this for debugging and agent observability.
Security Reminder

Never give agents unrestricted shell access or database write permissions in production. Start with read-only tools and expand access carefully. Read our AI agent security guide for a complete checklist.


Get Started

You've just built a fully functional AI agent in Python — from a raw LLM call to tool-calling, memory, and an autonomous agent loop. The complete code is under 80 lines.

When you're ready to scale from a single agent to a team of collaborating agents, cowork.ink gives your entire engineering team a shared workspace for AI agent orchestration — no prompt gymnastics, no framework lock-in.

Try cowork.ink free — set up your first AI agent in minutes.

Frequently Asked Questions

How many lines of code does a basic AI agent need?
A minimal AI agent in Python needs about 50–80 lines of code. You need an LLM connection, a tool registry, and an agent loop. See our [full tutorial](/blog/build-ai-agent-python/) for working code.
What is the best Python framework for building AI agents?
It depends on your use case. LangGraph is best for complex stateful workflows, CrewAI excels at multi-agent collaboration, and the OpenAI Agents SDK offers the fastest prototyping. For learning, start without a framework to understand the fundamentals. Compare options in our [framework comparison](/blog/ag2-vs-crewai-vs-langgraph-openai-agents-sdk/).
Can I build an AI agent without using LangChain?
Yes. Most LLM providers (OpenAI, Anthropic, Google) offer tool-calling APIs that let you build agents with just their Python SDK. Frameworks add convenience but are not required. Learn about [tool calling](/blog/ai-agent-tool-calling/) fundamentals first.
What is the agent loop in AI agents?
The agent loop is the core execution cycle where an AI agent thinks, decides whether to call a tool, observes the result, and repeats until the task is complete. It is the pattern that separates agents from simple chatbots. Read more about [how AI agents work](/blog/ai-agents-explained/).
How much does it cost to run a Python AI agent?
Costs depend on the LLM provider. A single agent session costs roughly $0.01–0.05 with GPT-4o or Claude Sonnet, and as low as $0.002 with DeepSeek. Check our [AI API pricing comparison](/blog/ai-api-pricing-comparison/) for current rates.
Home Blog Company