LearnFrenzy
Sign in Take a free test

Building & Shipping Agents · Lesson 5 of 6

Build Your First AI Agent in Python (No Framework Needed)

Write a complete agent loop in about 60 lines of plain Python: tools, a step limit, error handling and a final answer. You can run it first with a scripted "fake model", then plug in any real LLM API.

14 min read Intermediate Updated Nov 9, 2026 Reviewed by Saurabh Samir

Frameworks are useful, but they hide the one thing you most need to understand. In this lesson you write the whole agent yourself. It is shorter than you think, and once it works, every framework will make sense.

What we are building

A study-planner agent with two tools: one that reads your exam timetable and one that does date arithmetic. Ask it "How many days do I have before my DBMS exam, and what should I revise each day?" and it will look up the date, calculate the days and write a plan.

Step 1 — The tools

from datetime import date

TIMETABLE = {"DBMS": "2026-11-20", "Operating Systems": "2026-11-24", "Computer Networks": "2026-11-27"}

def get_exam_date(subject: str) -> dict:
    """Look up the exam date for a subject."""
    for name, day in TIMETABLE.items():
        if subject.lower() in name.lower():
            return {"subject": name, "date": day}
    return {"error": f"No exam found for '{subject}'. Known subjects: {list(TIMETABLE)}"}

def days_until(target_date: str) -> dict:
    """Days from today until target_date (YYYY-MM-DD)."""
    try:
        delta = date.fromisoformat(target_date) - date.today()
    except ValueError:
        return {"error": "target_date must be YYYY-MM-DD"}
    return {"days": delta.days}

TOOLS = {"get_exam_date": get_exam_date, "days_until": days_until}

Each tool returns a small dictionary, and problems come back as {"error": ...} — data the model can read — instead of exceptions that crash the loop.

Step 2 — The loop

import json

SYSTEM = ("You are a study-planning assistant for an engineering student. "
          "Use tools for dates and numbers; never guess them. "
          "When you have enough information, give a short day-by-day plan.")

def run_agent(goal: str, call_model, max_steps: int = 6) -> str:
    messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": goal}]
    seen = set()
    for step in range(1, max_steps + 1):
        reply = call_model(messages)                      # one model call per step
        if reply["type"] == "final":
            return reply["text"]
        name, args = reply["tool"], reply["arguments"]
        key = (name, json.dumps(args, sort_keys=True))
        if key in seen:                                   # same call again = stuck
            return "Stopped: the agent repeated the same action. Here is what it found so far."
        seen.add(key)
        tool = TOOLS.get(name)
        result = tool(**args) if tool else {"error": f"Unknown tool {name}"}
        print(f"step {step}: {name}({args}) -> {result}")  # log every action
        messages.append({"role": "assistant", "content": json.dumps(reply)})
        messages.append({"role": "tool", "content": json.dumps(result)})
    return f"Stopped after {max_steps} steps without finishing."

That is a real agent: a goal, tools, a loop, a step limit, a repeated-call check and a log. Everything else — frameworks, multi-agent systems, memory — builds on this skeleton.

Step 3 — Test the loop with a scripted model

Before spending a single API call, test the plumbing with a fake model that replies in a fixed order. This is a habit professional teams use too: test your loop separately from the model.

def scripted_model():
    script = iter([
        {"type": "tool", "tool": "get_exam_date", "arguments": {"subject": "dbms"}},
        {"type": "tool", "tool": "days_until", "arguments": {"target_date": "2026-11-20"}},
        {"type": "final", "text": "You have the days shown above. Day 1: ER diagrams and keys. "
                                   "Day 2: normalisation. Day 3: SQL joins. Last day: past papers."},
    ])
    return lambda messages: next(script)

print(run_agent("How many days until my DBMS exam? Make a plan.", scripted_model()))

Step 4 — Plug in a real model

To go live, write a call_model(messages) function for the provider you use. It must do two things:

  1. Send the messages and the tool definitions (JSON schemas, as in the tool-calling lesson) to the API.
  2. Convert the response into your simple format: {"type": "tool", ...} if the model asked for a tool, or {"type": "final", "text": ...} if it answered.

Every major SDK returns tool requests in a structured field (for example a list of tool calls or "tool use" blocks), so this adapter is usually 15–25 lines. Keeping it separate means you can switch providers — or test with the scripted model — without touching the loop.

Design rule: keep three things separate — the tools, the loop and the model adapter. You will change each of them for different reasons, and separation makes every change small.

Step 5 — Make it sturdier

  • Retries with backoff for network errors and rate limits on the model call.
  • A token or cost budget alongside the step limit.
  • Structured logs — save each step (tool, arguments, result, time) to a file. When something goes wrong, the log tells you exactly where.
  • Timeouts on every tool, so one slow API cannot freeze the agent.

Never put your API key in code. Read it from an environment variable (os.environ["..."]) and add your .env file to .gitignore. Keys pushed to public GitHub repositories are found by bots within minutes.

Try it yourself: Add a third tool, list_topics(subject), that returns the syllabus units for a subject from a dictionary. Update the scripted model to call it, and make the final plan use the real topics. Then lower max_steps to 2 and watch the stopping message appear.

Common interview questions

Try answering each one aloud before you open it.

1Walk me through the core loop of an AI agent.

Send the goal, history and tool definitions to the model; if it returns a tool call, validate it, execute the tool, append the call and result to the history and call the model again; if it returns a final answer, stop. Enforce limits like maximum steps, budget and repeated-call detection.

2How would you test an agent without spending on API calls?

Replace the model with a scripted or mock function that returns predefined tool calls and answers. This tests the loop, tools, error handling and limits deterministically, separately from model behaviour.

3How do you handle a tool that fails?

Catch the error in the tool or loop, return a short structured error message as the tool result so the model can adjust, apply timeouts and retries for transient failures, and count failures towards the step limit.

Practise this

Build something with it

FAQs

Should I use LangChain or another framework instead?

Use one when it saves you real work — ready-made integrations, tracing or multi-agent orchestration. Many production teams still write the core loop themselves because it is short and easy to control.

Can I run an agent with a free or local model?

Yes. Several open models support tool calling and can run locally with tools like Ollama. They are less capable than top hosted models, but perfect for learning and small tasks.

References & further reading

Finished this lesson?

Mark it complete. Create a free account to save it on every device.

Next lesson

Search LearnFrenzy

Esc

Quick links