Build Your First AI Agent in Python (Step by Step, No Framework)
Updated 2026-10-15
This guide builds a real agent from scratch, without a framework, so you understand exactly what LangGraph, CrewAI and friends do under the hood. The agent can do math and read files in a folder, and decides on its own which tool to use.
Prerequisites
- Python 3.10 or newer
- An OpenAI API key (any provider with an OpenAI-compatible Chat Completions API also works, e.g. a local Ollama server)
- 30 minutes
Steps
1. Create a project and install the SDK
mkdir first-agent && cd first-agent
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install openai
export OPENAI_API_KEY="sk-..."
2. Write the tools as normal Python functions
# agent.py
import json, os, pathlib
from openai import OpenAI
client = OpenAI()
MODEL = os.environ.get("MODEL", "gpt-4.1-mini")
ROOT = pathlib.Path("./notes").resolve()
def calculator(expression: str) -> str:
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed:
return "error: only numbers and + - * / ( ) are allowed"
return str(eval(expression, {"__builtins__": {}}))
def read_note(filename: str) -> str:
path = (ROOT / filename).resolve()
if ROOT not in path.parents: # block ../ escapes
return "error: file outside notes folder"
return path.read_text() if path.exists() else "error: not found"
def list_notes() -> str:
return "\n".join(p.name for p in ROOT.glob("*.txt")) or "(no notes)"
FUNCS = {"calculator": calculator, "read_note": read_note, "list_notes": list_notes}
3. Describe the tools to the model with JSON Schema
TOOLS = [
{"type": "function", "function": {
"name": "calculator", "description": "Evaluate an arithmetic expression.",
"parameters": {"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"]}}},
{"type": "function", "function": {
"name": "read_note", "description": "Read a .txt note by filename.",
"parameters": {"type": "object",
"properties": {"filename": {"type": "string"}},
"required": ["filename"]}}},
{"type": "function", "function": {
"name": "list_notes", "description": "List available note files.",
"parameters": {"type": "object", "properties": {}}}},
]
4. Write the agent loop
def run_agent(goal: str, max_steps: int = 8) -> str:
messages = [
{"role": "system", "content": "You are a helpful agent. Use tools when needed. Be concise."},
{"role": "user", "content": goal},
]
for _ in range(max_steps):
resp = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS)
msg = resp.choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg)
for call in msg.tool_calls:
args = json.loads(call.function.arguments or "{}")
try:
result = FUNCS[call.function.name](**args)
except Exception as e:
result = f"error: {e}"
print(f" -> {call.function.name}({args}) = {result[:80]}")
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
return "Stopped: step limit reached."
if __name__ == "__main__":
import sys
print(run_agent(" ".join(sys.argv[1:]) or "What notes do I have?"))
5. Run it
mkdir notes
echo "Invoice total: 1240 USD, 3 items" > notes/invoice.txt
python agent.py "Read my invoice note and tell me the average cost per item"
You should see the agent call list_notes, then read_note, then calculator("1240/3"), and finally answer with roughly 413.33.
6. Make it safer before adding real tools
- Keep
max_steps. It is your protection against runaway loops and bills. - Validate every argument inside the tool; never trust the model’s input.
- For tools with side effects (sending email, deleting rows), ask for human confirmation first.
Common errors
openai.AuthenticationError: 401: OPENAI_API_KEY is not set in the shell that runs Python. Run echo $OPENAI_API_KEY to check.
Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls': you appended tool results without first appending the assistant message that contained tool_calls. Keep the messages.append(msg) line before the inner loop.
json.decoder.JSONDecodeError on arguments: rare, but models can emit invalid JSON. Wrap json.loads in try/except and return an error string so the model can retry.
The agent never uses a tool: improve the tool description. Models choose tools mostly from that text.
Where to go next
- Swap the loop for the OpenAI Agents SDK once you understand it.
- Give it real apps: Gmail, Slack, a database.