AIFebruary 22, 20265 min read

Build Your First AI Agent with Python

Build a real AI agent from scratch with Python — an LLM that thinks, calls tools like a calculator and API, and loops until the job is done.

Galvan

Galvan

Founder & Creator

Introduction

2025 was the year of chatbots. 2026 is the year of agents — AI systems that don't just talk, but *act*: calling calculators, searching the web, reading files, and chaining steps until a task is done.

The word sounds futuristic, but an agent is just three parts in a loop:

  • A brain — an LLM that decides what to do
  • Tools — functions it can call (calculator, weather API, file reader)
  • A loop — think → act → observe → repeat until done
  • You already have all three skills: the LLM calls from the chatbot tutorial, functions from Module 7 of the Python course, and loops from Module 6. Today we assemble them into an agent — running on free local AI via [Ollama](/posts/run-ai-models-locally-with-ollama), so it costs nothing to experiment.

    The Agent Loop (the entire concept on one screen)

    code
    User: "What is 247 times 38?"
       ↓
    LLM thinks: "I shouldn't do math myself — I have a calculator tool"
       ↓
    Agent: CALL calculator(247, 38, "*")
       ↓
    Tool returns: 9386
       ↓
    LLM thinks: "I have the answer now"
       ↓
    Agent: "247 × 38 = 9,386"

    A chatbot would guess the math (and sometimes get it wrong). An agent uses tools for facts and computation, reserving the LLM's intelligence for decisions and language. That reliability difference is why agents matter.

    Prerequisites

  • Ollama installed with llama3.2 pulled — see the local AI tutorial
  • Python packages: pip install requests
  • Everything else is standard library
  • Step 1: Define the Tools

    Tools are ordinary Python functions with a description the LLM can read:

    code
    import json
    import requests
    
    
    def calculator(a, b, operation):
        a, b = float(a), float(b)
        if operation == "+": return a + b
        if operation == "-": return a - b
        if operation == "*": return a * b
        if operation == "/":
            if b == 0:
                return "Error: division by zero"
            return a / b
        return f"Unknown operation: {operation}"
    
    
    def get_weather(city):
        # using a free API — no key needed
        try:
            r = requests.get(
                f"https://wttr.in/{city}?format=j1", timeout=10
            )
            current = r.json()["current_condition"][0]
            return f"{current['temp_C']}°C, {current['weatherDesc'][0]['value']}"
        except Exception as e:
            return f"Weather lookup failed: {e}"
    
    
    TOOLS = {
        "calculator": calculator,
        "get_weather": get_weather,
    }

    The TOOLS dictionary maps names the LLM will use to the real functions. Adding a new capability later is one function plus one dictionary entry.

    Step 2: Tell the LLM About Its Tools

    The agent's system prompt is the instruction manual. Small local models need it spelled out precisely:

    code
    SYSTEM_PROMPT = """You are a helpful agent with access to these tools:
    
    1. calculator(a, b, operation) - does math. operation is +, -, *, or /
    2. get_weather(city) - current weather for a city
    
    To use a tool, reply with ONLY a JSON object:
    {"tool": "tool_name", "args": {"a": 1, "b": 2, "operation": "+"}}
    
    After you receive a tool result, answer the user in plain text.
    If no tool is needed, just answer normally."""

    This is the ReAct pattern (Reason + Act) in its simplest form: the model either answers or requests a tool — nothing else. Strict output formats keep small models on rails.

    Step 3: The Loop — where the agent lives

    code
    def run_agent(user_input):
        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_input},
        ]
    
        for step in range(5):                      # safety limit!
            response = requests.post(
                "http://localhost:11434/api/chat",
                json={"model": "llama3.2", "messages": messages, "stream": False},
                timeout=120,
            )
            reply = response.json()["message"]["content"].strip()
    
            # Is the reply a tool call? (starts with { and names a tool)
            if reply.startswith("{") and "tool" in reply:
                try:
                    call = json.loads(reply)
                    tool_name = call["tool"]
                    args = call.get("args", {})
                    print(f"🔧 Using tool: {tool_name}({args})")
    
                    result = TOOLS[tool_name](**args)
                    print(f"🔧 Result: {result}")
    
                    messages.append({"role": "assistant", "content": reply})
                    messages.append({"role": "user",
                                     "content": f"Tool result: {result}"})
                    continue                        # back to the LLM
                except (json.JSONDecodeError, KeyError, TypeError):
                    pass                            # not a valid call — treat as answer
    
            return reply                            # plain answer — done!
    
        return "I couldn't finish within 5 steps."

    Read the loop slowly — it is the agent:

  • Send the conversation to the LLM
  • If the reply is a tool call → run the real function, append the result, loop again
  • If it's a plain answer → return it
  • The range(5) cap prevents runaway loops — every real agent has one
  • Step 4: Run It

    code
    while True:
        user_input = input("\nYou: ")
        if user_input.lower() in ("quit", "exit"):
            break
        print("Agent:", run_agent(user_input))

    Try:

    code
    You: What is 144 divided by 12?
    🔧 Using tool: calculator({'a': 144, 'b': 12, 'operation': '/'})
    🔧 Result: 12.0
    Agent: 144 divided by 12 is 12.
    
    You: Weather in Delhi?
    🔧 Using tool: get_weather({'city': 'Delhi'})
    🔧 Result: 31°C, Sunny
    Agent: It's currently 31°C and sunny in Delhi.
    
    You: What's the capital of France?
    Agent: The capital of France is Paris.      ← no tool needed — it just knows

    The model *chose* when to use tools and when to answer directly. That decision-making is what makes it an agent rather than a script.

    Common Errors & Fixes

  • Model returns broken JSON — small models are imperfect JSON writers; the try/except falls through to a plain answer. Improve reliability by tightening the system prompt's format example.
  • Agent loops forever on one task — your step cap is missing or too high. Keep range(5) until you have a reason otherwise.
  • `Connection refused` from Ollama — the Ollama app isn't running; launch it first.
  • Tool name hallucinated — the model invented multiply() instead of calculator. List the exact tool names in the prompt and return a friendly error from TOOLS.get(tool_name) so the model can self-correct.
  • Key Concepts

  • Agent = LLM + tools + loop — nothing more mysterious than that
  • Tool descriptions are prompts — the model only knows what you declare
  • The step cap — every real agent bounds its own looping
  • Observe → decide → act — the loop that scales from calculator agents to web-browsing ones
  • What to Try Next

  • Add a read_file(path) tool — the agent can now answer questions about your own notes.
  • Add a web search tool (DuckDuckGo's ddgs package) — an agent that researches.
  • Chain tools: "What's 3× the current temperature in Mumbai?" needs weather, then calculator — watch the multi-step loop shine.
  • Wrap it in Streamlit with the chat UI from the Mistral chatbot and show the tool calls in an expander.
  • FAQ

    Why build the loop manually instead of using a framework?

    Frameworks (LangChain, CrewAI) are excellent — but they hide the loop you're learning here. Once you've written it in 40 lines, every framework becomes an optimization you *understand* rather than magic you *hope* works.

    Why is my local model worse at tool calls than ChatGPT?

    Small models (3B) are learning to drive. Larger Ollama models (8B+) follow tool formats more reliably — the loop stays identical. Cloud APIs also offer native function-calling modes; the concept transfers one-to-one.

    Is this how real production agents work?

    The loop, the tools, the cap, the observation feeding — yes, this is the skeleton. Production adds memory, better parsing, parallel tools, and guardrails. You've built the part that matters.