How to Build an AI Agent in Python

Build a working AI agent in plain Python — no framework — so you understand exactly what an agent is: a model, a few tools and a loop. By the end you will have an agent that converts currencies between PKR, INR and USD, remembers the conversation, stops safely, and is ready to move into LangGraph or behind an API.

By Abdul Rahman Azam, founder of AI Season · Updated

Build agents live with us →

Key facts

Level Beginner — you need basic Python (functions, lists, dictionaries)
Time About 60–90 minutes
You need Python 3.10+, the openai package and an API key from any OpenAI-compatible provider (or Ollama locally)
You build A tool-calling agent with memory, a step limit and input checks
Concepts Tool calling, the agent loop (ReAct), conversation memory, guardrails
Next step Rebuild it in a framework such as LangGraph, then deploy it

What you are going to build

A small but real AI agent: you ask it things like "How many Indian rupees is 5,000 Pakistani rupees, and what's that in dollars?", and instead of guessing, it decides to call a currency tool — twice — and then answers from the results. That is the whole idea of an agent: the model chooses the actions, your code carries them out. (New to the concept? Read what AI agents are first.)

The exchange rates in this tutorial are fixed demo numbers so the code runs offline. In a real agent the tool would call a live rates API.

Step 1: Set up the project

Create a folder and a virtual environment, then install the OpenAI Python SDK. It talks to OpenAI and to any OpenAI-compatible provider — including free-tier services and a local Ollama server — so you can swap models by changing two environment variables.

mkdir first-agent && cd first-agent
python -m venv .venv
# Windows: .venv\Scripts\activate   ·   macOS/Linux: source .venv/bin/activate
pip install openai python-dotenv

Create a .env file for your key. Never commit it to GitHub.

OPENAI_API_KEY=your-key-here
# Optional, for other OpenAI-compatible providers or Ollama:
# OPENAI_BASE_URL=http://localhost:11434/v1
MODEL=gpt-4o-mini
.env — use any chat model that supports tool calling.

Step 2: Make your first model call

Before any agent, check that a plain call works. Every message has a role: system sets the rules, user is the person, assistant is the model.

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI()  # reads OPENAI_API_KEY and, if set, OPENAI_BASE_URL
MODEL = os.getenv("MODEL", "gpt-4o-mini")

reply = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "What is an AI agent, in one sentence?"},
    ],
)
print(reply.choices[0].message.content)
hello.py

Step 3: Give the agent a tool

A tool is an ordinary Python function plus a description the model can read. The description — name, purpose and a JSON schema for the arguments — is what the model uses to decide when and how to call it, so write it as carefully as you write the code.

# Demo rates: units of each currency per 1 US dollar. Replace with a live API in real use.
RATES_PER_USD = {"USD": 1.0, "PKR": 280.0, "INR": 97.0}

def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
    src, dst = from_currency.upper(), to_currency.upper()
    if src not in RATES_PER_USD or dst not in RATES_PER_USD:
        return f"Unsupported currency. Use one of: {', '.join(RATES_PER_USD)}"
    if amount < 0:
        return "Amount must be positive."
    result = amount / RATES_PER_USD[src] * RATES_PER_USD[dst]
    return f"{amount:,.2f} {src} = {result:,.2f} {dst} (demo rate)"

TOOLS = [{
    "type": "function",
    "function": {
        "name": "convert_currency",
        "description": "Convert an amount between PKR, INR and USD.",
        "parameters": {
            "type": "object",
            "properties": {
                "amount": {"type": "number", "description": "The amount to convert"},
                "from_currency": {"type": "string", "enum": ["PKR", "INR", "USD"]},
                "to_currency": {"type": "string", "enum": ["PKR", "INR", "USD"]},
            },
            "required": ["amount", "from_currency", "to_currency"],
        },
    },
}]

# The only functions the agent is allowed to run.
TOOL_FUNCTIONS = {"convert_currency": convert_currency}
tools.py — the enum limits the model to currencies the function supports.

Step 4: Write the agent loop

This is the heart of every agent. Send the conversation and the tool list to the model. If it answers in text, you are done. If it asks for tools, run each one, append the results to the conversation, and ask again. Repeat — but never forever.

import json

SYSTEM_PROMPT = (
    "You are a helpful currency assistant for students in Pakistan and India. "
    "Always use the convert_currency tool for conversions — never guess numbers. "
    "Answer briefly and mention that rates are demo values."
)

def run_tool(name: str, arguments: str) -> str:
    func = TOOL_FUNCTIONS.get(name)
    if func is None:
        return f"Error: unknown tool '{name}'."
    try:
        return str(func(**json.loads(arguments)))
    except (json.JSONDecodeError, TypeError, ValueError) as error:
        return f"Error: bad arguments ({error})."

def run_agent(messages: list, max_steps: int = 6) -> str:
    for _ in range(max_steps):
        response = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
        message = response.choices[0].message

        # Keep the model's turn in the history, as a plain dict so any
        # OpenAI-compatible provider accepts it on the next call.
        turn = {"role": "assistant", "content": message.content}
        if message.tool_calls:
            turn["tool_calls"] = [
                {"id": c.id, "type": "function",
                 "function": {"name": c.function.name, "arguments": c.function.arguments}}
                for c in message.tool_calls
            ]
        messages.append(turn)

        if not message.tool_calls:  # no tool requested: this is the final answer
            return message.content

        for call in message.tool_calls:  # the model may request several tools at once
            result = run_tool(call.function.name, call.function.arguments)
            messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

    return "I stopped because the task needed too many steps. Please try a simpler request."
agent.py — think, act, observe, repeat (the ReAct loop).

Notice three safety details that beginners skip: the model can only run functions in TOOL_FUNCTIONS; bad arguments come back to the model as an error message it can recover from instead of crashing your program; and max_steps guarantees the loop ends.

Step 5: Add memory and chat with it

Models are stateless — they only know what you send each time. "Memory" in its simplest form is keeping the message list between turns, so a follow-up like "and in dollars?" makes sense.

def chat():
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    print("Currency agent ready. Type 'quit' to exit.")
    while True:
        user_text = input("You: ").strip()
        if user_text.lower() in {"quit", "exit"}:
            break
        if not user_text or len(user_text) > 2000:  # a cheap input guardrail
            print("Agent: Please type a question under 2,000 characters.")
            continue
        messages.append({"role": "user", "content": user_text})
        print("Agent:", run_agent(messages))

if __name__ == "__main__":
    chat()

Try: "Convert 5000 PKR to INR." Then: "And what is that in dollars?" The second question only works because the history — including the first tool result — is still in messages.

Long conversations eventually overflow the model's context window and cost more on every call. Real agents trim or summarise old turns and store long-term facts separately, for example in a database or vector store.

Step 6: Add a second tool and watch it choose

Agents get interesting when there is a choice. Add a tool that answers from your own notes — here a tiny dictionary, in real life a RAG search over documents — and the model will pick the right tool for each question, or use both.

COURSE_NOTES = {
    "refund": "Top 3 scorers across the mid and final test get a full refund.",
    "schedule": "Two live sessions a week for six weeks, all recorded.",
}

def search_notes(topic: str) -> str:
    return COURSE_NOTES.get(topic.lower(), "No note on that topic.")

TOOLS.append({
    "type": "function",
    "function": {
        "name": "search_notes",
        "description": "Look up course policy notes by topic: refund or schedule.",
        "parameters": {
            "type": "object",
            "properties": {"topic": {"type": "string", "enum": list(COURSE_NOTES)}},
            "required": ["topic"],
        },
    },
})
TOOL_FUNCTIONS["search_notes"] = search_notes

Step 7: Guardrails you should always add

  • Step and cost limits — a maximum number of loop iterations and tokens per request.
  • An allow-list of tools — the model can only request functions you registered; never eval or run code it writes without a sandbox.
  • Validated arguments — schemas, enums and type checks before a tool touches anything real.
  • Tool output is data, not instructions — text from web pages or documents may contain prompt injection; the system prompt should say so.
  • Human approval for risky actions — payments, deleting records and sending messages should wait for a person to confirm.
  • Logs — record every tool call and result, so you can see why the agent did what it did.

Step 8: When to move to a framework

The hand-written loop is perfect for learning and for small agents. Reach for a framework when you need things like saved state across sessions, branching workflows, retries, several cooperating agents, streaming progress to a UI, or pausing for human approval. LangGraph models all of that as a graph of steps and is a common next step; LangChain provides ready-made model, tool and retriever components on top. Compare the options in AI agent frameworks compared and LangChain vs LangGraph.

Step 9: Put it behind an API

To let a website, WhatsApp bot or mobile app use your agent, wrap it in a small web API. With FastAPI it takes a few lines; in production you would add authentication, per-user conversation storage and rate limits.

# pip install fastapi uvicorn
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Question(BaseModel):
    text: str

@app.post("/ask")
def ask(question: Question):
    messages = [{"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": question.text[:2000]}]
    return {"answer": run_agent(messages)}

# run with: uvicorn server:app --reload
server.py — a stateless endpoint; store messages per user to keep memory.

Common mistakes (and fixes)

MistakeWhat happensFix
Vague tool descriptionsThe model calls the wrong tool or noneDescribe when to use each tool and constrain arguments with enums
No step limitEndless loops and surprise billsCap iterations, tokens and spend
Not appending the assistant messageThe API rejects the tool resultsAppend the model's message before the tool messages
Trusting tool outputPrompt injection from documents or web pagesTreat tool results as data; restrict what tools can do
Testing with one questionIt works in the demo, fails for usersKeep a set of test questions and rerun them after every change

Where to go from here

You now understand the core of every agent. The next skills are retrieval over real documents, stateful workflows in LangGraph, connecting tools through MCP, evaluation, and deployment. The AI agents roadmap lays out the order, and the free Cohort 01 course material has runnable code for each.

Prefer to learn it live? The AI Season bootcamp builds on exactly this loop over 12 sessions — explained in Urdu, coded in English — ending with deployed, guardrailed agents. Cohort 02 starts 1st January 2027; the early-bird fee is PKR 3,000.

Frequently asked questions

Can I build an AI agent for free?

Yes. Several model providers offer free tiers, and you can run open models locally with Ollama through the same OpenAI-compatible code. Python and the libraries used here are free.

Do I need LangChain or LangGraph to build an AI agent?

No. As this tutorial shows, an agent is a model, some tools and a loop. Frameworks earn their place when you need persistent state, branching, retries, human approval or several agents.

Which model is best for AI agents?

Any current model that supports tool calling works for learning. For production, test a few on your own tasks — accuracy on tool choice, speed and cost matter more than leaderboard rankings.

What is the difference between tool calling and function calling?

They are the same idea: the model returns a structured request to run a named function with arguments, and your code runs it. 'Function calling' was the earlier name; 'tool calling' is now more common.

How do I give my agent long-term memory?

Store important facts outside the conversation — in a database or a vector store — and add a tool or a retrieval step that fetches them when relevant. Keep the chat history short by trimming or summarising old turns.

Can this agent run on WhatsApp?

Yes. Put the agent behind an API (Step 9) and connect it to a WhatsApp integration. AI Season's open course material includes a WhatsApp bot built on the same ideas.