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.
Quick path
Section titled “Quick path”In a hurry? These three steps are the whole challenge. Everything below is the why and the how.
- Run
make f4. No[tool fired] get_user_timeline; the greeting is not tied to your clock. - Edit
start/agent.py: TODO 1 (writeget_user_time’s body), TODO 2 (add@agent.tool_plainto register it). - Done when
get_user_timefires, the log shows your real time, and the greeting matches that hour band.
Mental model
Section titled “Mental model”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.
The mechanic, on a throwaway tool
Section titled “The mechanic, on a throwaway tool”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:
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:
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.
Build it
Section titled “Build it”-
Spot the gap. Run the starter. No
get_user_timein the output. The model had to guess the time of day. -
Write
get_user_time’s body (TODO 1). Same move asget_server_uptimeabove, but reading the clock instead of uptime. Read the current local time withdatetime.now().astimezone()and return it as a string (e.g."14:30") withnow.strftime("%H:%M"), so the model can pick the greeting. Log[tool fired] get_user_time() -> ...so you can see it run. -
Register it with the decorator (TODO 2). An agent can only call tools it was given. Add
@agent.tool_plainabove the function, the same decoratorget_server_uptimecarries. Run again. You should see the tool fire, then an answer that cites the time (buenos días / buenas tardes / buenas noches), withrequestsat 2. -
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.
-
Check you’ve got it. You can say why the model could not know the hour without the tool, you saw
get_user_timefire, 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. NotImplementedErrorraised. You registered the tool before writing the body; replace theraisewith 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.
A couple of things worth knowing
Section titled “A couple of things worth knowing”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.