Skip to content

f4: Tools

Up to now TripMate answered from what the model already knows. For “what greeting should I use right now?” it will guess: a generic hola, or buenos días with no tie to your clock.

A tool closes that gap. The model calls your function when it needs a fact it cannot know on its own; the return value flows back into the answer.

This challenge adds one tool: read the real time from the machine running the script. “Right now” is your desk clock, not a timezone table in the repo.

In a hurry? These three steps are the whole challenge. Everything below is the why and the how.

  1. Run make f4. No [tool fired] get_user_time line; the greeting is not tied to your clock.
  2. Edit start/agent.py: TODO 1 (write get_user_time’s body), TODO 2 (add @agent.tool_plain to register it).
  3. Done when get_user_time fires, the log shows your real time, and the greeting matches that hour band.
prompt  ->  model  ->  needs the clock?  ->  [ get_user_time ]  --.
   answer  <-  model  <-------- tool result -------------------'

One tool, one gap. In f2, requests was 1 with no tools. After you wire this, requests climbs to 2 when the model calls the tool and then answers.

In Pydantic AI a tool is a decorated function: its docstring is the description the model reads to decide whether to call it, and its body is the code we run on its behalf. Here’s the whole shape on something unrelated, server uptime, a value the model cannot know:

import time


@agent.tool_plain
async def get_server_uptime() -> dict:
    """Get how long this server process has been running.

    Call it when asked about uptime; you cannot know it any other way.
    """
    uptime_seconds = round(time.monotonic())          # a real value, read at call time
    print(f"  [tool fired] get_server_uptime() -> {uptime_seconds}s")
    return {"uptime_seconds": uptime_seconds}          # whatever you return flows back

TripMate’s get_user_time uses the same shape: a docstring description, an @agent.tool_plain decorator, a body that reads a real value. Your job is the body and the decorator.

Open start/agent.py. get_user_time is declared with its docstring, but its body raises and the @agent.tool_plain decorator isn’t there yet, so it isn’t registered.

Run it:

make f4

You get an answer, but no clock tool in the run. The model guessed the time of day. Scroll up to the console trace and there is no get_user_time span.

  1. Spot the gap. Run the starter. No get_user_time in the output. The model had to guess the time of day.

  2. Write get_user_time’s body (TODO 1). Same move as get_server_uptime above, but reading the clock instead of uptime. Read the current local time with datetime.now().astimezone() and return it as a string (e.g. "14:30") with now.strftime("%H:%M"), so the model can pick the greeting. Log [tool fired] get_user_time() -> ... so you can see it run.

  3. Register it with the decorator (TODO 2). An agent can only call tools it was given. Add @agent.tool_plain above the function, the same decorator get_server_uptime carries. Run again. You should see the tool fire, then an answer that cites the time (buenos días / buenas tardes / buenas noches), with requests at 2.

  4. Poke. Run at a different time of day, or change the prompt (“I’m heading to France…”) and see whether the model still reaches for the clock.

  5. Check you’ve got it. You can say why the model could not know the hour without the tool, you saw get_user_time fire, and the greeting lines up with the logged time.

Stuck? finish/agent.py is the canonical version. Read it after you’ve had a real go.

  • No [tool fired] line. You added the body but not @agent.tool_plain; an undecorated function is just a function.
  • NotImplementedError raised. You registered the tool before writing the body; replace the raise with the real read.
  • Tool fires, greeting band feels off. Small models wobble on cutoffs (14:00 vs 15:00). Pass when the tool ran and the answer cites the time from the log.
  • Looking up Madrid in a table. That teaches lookup data (closer to r1). This lesson is platform clock truth that changes every run.
Why a tool instead of "reply with the right greeting" in the prompt?

The prompt can ask for buenos días or buenas tardes. It cannot tell the model what hour it is on your laptop. Only code running on your machine can read that clock.

What the model actually sends when it calls a tool

When the model decides to use a tool it does not run your function. It emits a structured tool call: the tool’s name plus its arguments. Pydantic AI reads that, runs your tool function, validates the args, and feeds the return value back into the run as a tool result, which is the next thing the model sees. That round trip is why usage().requests goes from 1 to 2: one request to ask for the tool, one to read its result and answer.

Where do two tools show up?

This challenge uses one tool so the wiring stays clear. Patterns track challenges combine several tools in one run; the same loop, more calls.

Next up is f5, guardrails: a cheap check that runs before the agent and decides whether a request is even safe and on-topic to answer.