Skip to content
AI-grafen
DAI developerAgents and tool use· about 45 min· fundamentals that rarely change· verified 2026-09-20· EN

Build a simple agent with one tool

Be able to let a model call a function and use the answer.

Prerequisites

Intuition

A language model cannot look up today's date, compute exactly, or read your calendar. But it can say that it wants to — and then it is your code that does the job.

The loop has four steps:

1. You describe which tools exist
2. The model answers: "call weather(city='Malmö')"
3. YOUR CODE runs the function and gets the answer 14
4. You send the answer back; the model formulates the final answer

Step 3 is the important one: the model runs nothing. It only produces a text answer saying what it wants to happen. Your code decides whether it will.

That is both a limitation and the whole security model — you can always refuse, validate, or ask the user first.

Code

import json

# 1. The tools — ordinary Python functions
def weather(city: str) -> dict:
    data = {"Malmö": 14, "Kiruna": -3, "Göteborg": 11}
    if city not in data:
        return {"error": f"unknown city '{city}'. Choose between: {sorted(data)}"}
    return {"city": city, "degrees": data[city]}

def compute(expression: str) -> dict:
    if not set(expression) <= set("0123456789+-*/(). "):
        return {"error": "the expression may only contain digits and + - * / ( )"}
    try:
        return {"answer": eval(expression, {"__builtins__": {}}, {})}
    except Exception as e:
        return {"error": f"could not compute: {type(e).__name__}"}

TOOLS = {"weather": weather, "compute": compute}

SPEC = [
  {"type": "function", "function": {
     "name": "weather", "description": "The temperature in a Swedish city right now, in degrees Celsius.",
     "parameters": {"type": "object", "required": ["city"], "additionalProperties": False,
        "properties": {"city": {"type": "string", "enum": ["Malmö", "Kiruna", "Göteborg"],
                                "description": "The name of the city"}}}}},
  {"type": "function", "function": {
     "name": "compute", "description": "Computes an arithmetic expression exactly.",
     "parameters": {"type": "object", "required": ["expression"], "additionalProperties": False,
        "properties": {"expression": {"type": "string", "description": "E.g. '17 * 23'"}}}}},
]

def run(llm, question, max_rounds=5):
    messages = [{"role": "user", "content": question}]
    for _ in range(max_rounds):                    # max_rounds protects against loops
        answer = llm(messages, tools=SPEC)
        messages.append(answer)
        if not answer.get("tool_calls"):
            return answer["content"]
        for call in answer["tool_calls"]:
            name = call["function"]["name"]
            try:
                args = json.loads(call["function"]["arguments"])
            except json.JSONDecodeError:
                result = {"error": "the arguments were not valid JSON"}
            else:
                fn = TOOLS.get(name)
                result = fn(**args) if fn else {"error": f"unknown tool '{name}'"}
            messages.append({"role": "tool", "tool_call_id": call["id"],
                             "content": json.dumps(result, ensure_ascii=False)})
    return "Reached the maximum number of steps without finishing."

# print(run(llm, "How warm is it in Malmö, and what is 17 times 23?"))
# "It is 14 degrees in Malmö, and 17 times 23 is 391."

Four details that make the difference between a toy and something that holds:

  1. max_rounds — without a cap the model can get stuck in a loop calling the same tool over and over.
  2. Errors as results, not exceptions — {"error": "unknown city ..."} is sent back so that the model can correct itself in the next round.
  3. enum in the schema — then the model cannot invent cities.
  4. A restricted eval — the character check and the empty __builtins__ mean that the expression cannot run arbitrary code. (In a real system you use a proper expression parser instead.)

Mastery means

  • Lets the model call a function
  • Handles the answer and faulty calls
  • Explains the loop call → result → answer

Sign in to do the exercises and build your mastery up.

Sources

All the sources and licences