Skip to content

03: Tool Calling

Structured output makes the shape stable. The facts can still be invented.

The user asks:

Will it rain tomorrow afternoon? If yes, make the Hangzhou trip easier.

The model does not know live weather. It may guess, and the guess may sound convincing. We do not want the travel assistant to rely on luck.

This chapter adds tool calling: make "check weather" a Python function.

Code First

from __future__ import annotations

import json
from pathlib import Path

from agent_patterns_lab.runtime import Tool, ToolRegistry, Tracer


def main() -> None:
    tracer = Tracer()

    def get_weather(args: dict) -> str:
        return json.dumps(
            {
                "city": args["city"],
                "date": args["date"],
                "forecast": "light rain after 15:00",
                "temperature_c": "18-23",
            },
            ensure_ascii=False,
        )

    tools = ToolRegistry(
        [
            Tool(
                name="get_weather",
                description="Get a simple weather forecast for a city and date",
                handler=get_weather,
            )
        ]
    )

    out = tools.call("get_weather", {"city": "Hangzhou", "date": "tomorrow"}, tracer=tracer)
    print(out)

    trace_path = tracer.export_jsonl(Path(".traces") / "20_tool_calling.jsonl")
    print(f"[trace] {trace_path}")


if __name__ == "__main__":
    main()

Run:

uv run python examples/20_tool_calling.py

Expected output:

{"city": "Hangzhou", "date": "tomorrow", "forecast": "light rain after 15:00", "temperature_c": "18-23"}

A Tool Is Just A Function

The smallest tool call is:

tool name + args -> Python function -> tool result

This example has one tool:

def get_weather(args: dict) -> str:
    return json.dumps(
        {
            "city": args["city"],
            "date": args["date"],
            "forecast": "light rain after 15:00",
            "temperature_c": "18-23",
        },
        ensure_ascii=False,
    )

It returns a JSON string because later code or models can read fields from it.

ToolRegistry Is The Allowlist

Register the tool:

tools = ToolRegistry(
    [
        Tool(
            name="get_weather",
            description="Get a simple weather forecast for a city and date",
            handler=get_weather,
        )
    ]
)

This boundary matters.

Later, the model may request a tool. Python still decides what can actually run. If a tool is not registered, it cannot be called.

This chapter calls the tool directly so the tool boundary is visible:

out = tools.call("get_weather", {"city": "Hangzhou", "date": "tomorrow"}, tracer=tracer)

In the agent loop chapter, the model will output a tool action and Python will execute it.

What It Fixes

Tool calling gives some facts back to code:

Before After
Model guesses weather Python tool returns weather
Model casually states temperature Tool returns temperature_c
Source is hard to inspect Trace contains the tool call and result

Now the travel assistant can say "light rain after 15:00" because a tool returned it.

What It Does Not Fix

Tool calling gives the program a capability. It does not decide the next step.

There are two different shapes now:

  1. Known path: check weather, draft route, format answer.
  2. Unknown path: if weather is rainy, search indoor places; if a place is closed, search again.

The first does not need an agent. Code can control the path.

Next: use the more predictable option first in 04: Workflow.