Skip to content
AI Agent How-To

Home › Build an agent that does X

Build Your First AI Agent in Python (Step by Step, No Framework)

Intermediate ~7 min read Last tested October 2026

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