Skip to content
Naveen Raj

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.