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
- CAI that uses tools: the calculatorrequired
- DAPIs and HTTPrequired
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:
max_rounds— without a cap the model can get stuck in a loop calling the same tool over and over.- Errors as results, not exceptions —
{"error": "unknown city ..."}is sent back so that the model can correct itself in the next round. enumin the schema — then the model cannot invent cities.- 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
- Anthropic — Tool use — documentation, free to read
- The Python documentation (PSF licence) — PSF
- JSON Schema — free to read