Building AI Agents from Scratch with Python · Tools and Real-World Robustness
Implementing a Calculator Tool
Now the loop from Chapter 2 gets the branch it was missing: when the model's response contains a tool call, execute it and loop again instead of returning immediately.
def calculator(expression: str) -> str:
try:
# eval() is fine for a guide; never eval() untrusted input in production —
# use a real expression parser like `numexpr` or `asteval` instead.
return str(eval(expression, {"__builtins__": {}}))
except Exception as e:
return f"error: {e}"
TOOLS = {"calculator": calculator}
def run_agent(user_input: str, messages: list[dict]) -> list[dict]:
messages.append({"role": "user", "content": user_input})
while True:
response = client.messages.create(
model=MODEL,
max_tokens=1024,
tools=tools,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return messages # model gave a final answer — loop ends
# Model wants a tool. Run it and feed the result back in.
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
fn = TOOLS[block.name]
result = fn(**block.input)
tool_results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": result}
)
messages.append({"role": "user", "content": tool_results})
# loop again — the model sees the tool result as the next observation
TOOLS is a plain dict mapping tool name → Python function. Adding a second tool means adding one entry to tools (the schema) and one entry to TOOLS (the implementation) — nothing else in the loop changes. That's the whole extensibility story for tool calling.