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:
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)
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
llama3.2 pulled — see the local AI tutorialpip install requestsStep 1: Define the Tools
Tools are ordinary Python functions with a description the LLM can read:
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:
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
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:
range(5) cap prevents runaway loops — every real agent has oneStep 4: Run It
while True:
user_input = input("\nYou: ")
if user_input.lower() in ("quit", "exit"):
break
print("Agent:", run_agent(user_input))Try:
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 knowsThe 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
range(5) until you have a reason otherwise.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
What to Try Next
ddgs package) — an agent that researches.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.