AI Agents Tutorial menu

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:

  1. Look up the price of each item with a get_price tool
  2. Add up the total with a calculator tool
  3. 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

python
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')"))
Output
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

python
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

python
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:

LineWhy it matters
if name not in TOOLSThe model cannot call a function you did not offer
try ... exceptBroken arguments become an error message, not a crash
max_steps=6The 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

python
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)
Output
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

python
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?

Learn AI Agents with a teacher

Join the AI & Prompt Engineering course at Workownix, online or in Orangi Town, Karachi, and get a certificate.

See the course Ask on WhatsApp