Build a Simple Agent in Python
In this lesson you will put tools, the loop and memory together into a small agent that answers shop questions using real Python functions.
What We Are Building
A shop assistant for a stationery shop in Karachi. A customer asks: "How much for 3 notebooks and 2 pens?" The agent must:
- Look up the price of each item with a
get_pricetool - Add up the total with a
calculatortool - Reply with the answer, without guessing any price
We will build it in four parts: the tools, the tool descriptions, the loop, and the model connection. First we test with a fake model so you can run everything without an API key.
Part 1: The Tools
Tools are ordinary Python functions. Each one does one job and returns short text:
Example
PRICES = {"notebook": 120, "pen": 40, "school bag": 2950}
def get_price(item):
price = PRICES.get(item.lower())
if price is None:
return f"No price found for {item}"
return f"{item} costs Rs {price}"
def calculator(expression):
allowed = set("0123456789+-*/(). ")
if not set(expression) <= allowed or "**" in expression:
return "Error: only numbers and + - * / ( ) are allowed"
return str(eval(expression))
TOOLS = {"get_price": get_price, "calculator": calculator}
print(get_price("Pen"))
print(calculator("3*120 + 2*40"))
print(calculator("__import__('os')"))Pen costs Rs 40 440 Error: only numbers and + - * / ( ) are allowed
The calculator checks every character before it runs eval(). Without that check, the model (or a trick hidden in a web page) could make your program run any Python code.
Return errors as normal text, like "No price found". The model reads the error and can try again, for example with a different spelling.
Part 2: Describe the Tools
The model only knows about a tool from its description. We use the JSON Schema format from the function calling lesson:
Example
TOOL_SPECS = [
{"type": "function", "function": {
"name": "get_price",
"description": "Get the shop price of one item in rupees.",
"parameters": {"type": "object",
"properties": {"item": {"type": "string"}},
"required": ["item"]}}},
{"type": "function", "function": {
"name": "calculator",
"description": "Calculate a maths expression, like 3*120+2*40.",
"parameters": {"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"]}}},
]Part 3: The Agent Loop
This is the heart of the agent. It sends the conversation to the model, runs any tool calls, adds the results to the messages (the agent's short-term memory), and repeats until the model answers in plain text:
Example
import json
def run_tool_call(call):
name = call["function"]["name"]
if name not in TOOLS:
return f"Error: unknown tool {name}"
try:
args = json.loads(call["function"]["arguments"])
return TOOLS[name](**args)
except (json.JSONDecodeError, TypeError) as err:
return f"Error: bad arguments ({err})"
def run_agent(goal, ask_model, max_steps=6):
messages = [
{"role": "system", "content": "You are a helpful shop assistant. "
"Use the tools to find prices and do maths. Never guess a price."},
{"role": "user", "content": goal},
]
for step in range(1, max_steps + 1):
reply = ask_model(messages, TOOL_SPECS)
messages.append(reply)
if not reply.get("tool_calls"):
return reply["content"] # plain answer: we are done
for call in reply["tool_calls"]:
result = run_tool_call(call)
print(f"Step {step}: {call['function']['name']} -> {result}")
messages.append({"role": "tool", "tool_call_id": call["id"],
"content": result})
return "Stopped: step limit reached."Notice the safety points:
| Line | Why it matters |
|---|---|
if name not in TOOLS | The model cannot call a function you did not offer |
try ... except | Broken arguments become an error message, not a crash |
max_steps=6 | The loop can never run forever |
Part 4: Test with a Fake Model
A fake model returns replies in the same shape a real API does. It lets you test the loop for free:
Example
def tool_call(call_id, name, args):
return {"id": call_id, "type": "function",
"function": {"name": name, "arguments": json.dumps(args)}}
def fake_model(messages, tools):
results = [m["content"] for m in messages if m["role"] == "tool"]
if not results:
return {"role": "assistant", "content": None, "tool_calls": [
tool_call("c1", "get_price", {"item": "notebook"}),
tool_call("c2", "get_price", {"item": "pen"})]}
if len(results) == 2:
return {"role": "assistant", "content": None, "tool_calls": [
tool_call("c3", "calculator", {"expression": "3*120 + 2*40"})]}
return {"role": "assistant",
"content": f"3 notebooks and 2 pens cost Rs {results[-1]} in total."}
answer = run_agent("How much for 3 notebooks and 2 pens?", fake_model)
print("Answer:", answer)Step 1: get_price -> notebook costs Rs 120 Step 1: get_price -> pen costs Rs 40 Step 2: calculator -> 440 Answer: 3 notebooks and 2 pens cost Rs 440 in total.
Put the code from Parts 1 to 4 in one file, for example agent.py, and run it with python agent.py.
Connecting a Real Model
Now replace the fake model with a real one. This function calls any OpenAI-compatible chat completions API. Many providers offer one, including the services behind models like ChatGPT, Gemini and Claude (check each provider's documentation for its base URL and model names). Install the library with pip install requests and keep the key in environment variables:
Example
import os
import requests
def ask_model(messages, tools):
response = requests.post(
f"{os.environ['AI_BASE_URL']}/chat/completions",
headers={"Authorization": f"Bearer {os.environ['AI_API_KEY']}"},
json={"model": os.environ["AI_MODEL"], "messages": messages,
"tools": tools},
timeout=60,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]
print(run_agent("How much for 3 notebooks and 2 pens?", ask_model))The real model now decides by itself which tools to call. Because the loop does not care which "brain" it uses, the same run_agent works with the fake model and the real one.
Many providers also have their own SDKs and agent frameworks that hide this loop. Building it once by hand, as here, helps you understand and debug any framework later.
Ideas to Improve It
- Add a
check_stock(item)tool and tell the agent to warn when an item is out of stock - Save each finished conversation to a log file so you can review what the agent did
- Ask the model to reply in Urdu or Roman Urdu when the customer writes in it
Exercise
Exercise
In the run_agent loop, how does the agent know it is finished?
