Tutorial
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
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-dotenvCreate 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-miniStep 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)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}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."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_notesStep 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
evalor 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 --reloadCommon mistakes (and fixes)
| Mistake | What happens | Fix |
|---|---|---|
| Vague tool descriptions | The model calls the wrong tool or none | Describe when to use each tool and constrain arguments with enums |
| No step limit | Endless loops and surprise bills | Cap iterations, tokens and spend |
| Not appending the assistant message | The API rejects the tool results | Append the model's message before the tool messages |
| Trusting tool output | Prompt injection from documents or web pages | Treat tool results as data; restrict what tools can do |
| Testing with one question | It works in the demo, fails for users | Keep 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.