Projects/Weather Tool Agent/Part 2
Tools and Simulated Environment
Register geocode, weather, and finish as functions with stable JSON-shaped results, unknown-city errors, and no real network.
Tools are how the agent touches a world. In production the world is HTTP. Here the world is two dictionaries and a finish function. That is not a toy limitation — it is how you write tests. If a tool's contract is stable, you can swap the dict for httpx.get later without rewriting the loop.
A tool is a function with a name, a JSON-serializable argument object, and a JSON-serializable result. Side effects belong inside the function, not in the model. The model never mutates the city table. The model only chooses names and args.
Design tools like public APIs
Narrow arguments. Return dicts, not exceptions that kill the loop (catch internally, return {"error": ...}). Make errors machine-readable: a string code plus an optional message. Models pattern-match on codes better than on essays.
| Tool | Args | Success | Error codes |
|---|---|---|---|
geocode | city: str | lat, lon, name | unknown_city, bad_args |
weather | lat: float, lon: float | temp_c, conditions, place | no_forecast, bad_args |
finish | answer: str | final: str | bad_args |
finish is a tool, not a magic side channel. That keeps the protocol uniform: every policy output is one tool call. The loop treats finish as the success/refusal stop.
The simulated planet
You need a handful of cities — enough for tests, not a gazetteer. Round coordinates to 2 decimals when you key the weather table so float noise does not miss. Unknown cities must fail. A missing forecast (coords that were never indexed) must fail. Do not "helpfully" pick the nearest city. Helpful geocoding is how you report Lisbon weather for a typo of London.
Run to execute this in your browser. Nothing is sent to a server.
Step-by-step: what you just registered
- Normalize city strings. Case, extra spaces, and
PARISvsParismust not be different places. Collapsing whitespace avoids a class of "the model added a newline" bugs. - Return errors as data.
unknown_cityis an observation the policy can read. If youraise, the loop dies and you cannot retry. - Round coordinates. The model might echo
48.8600001. Rounding at the tool boundary is cheaper than teaching floats to an LLM. - Keep
finishpicky. Empty answers are not done. Whitespace-only is not done. - Unknown tools return
unknown_tool. The registry is a firewall.
Why three tools, not one `get_weather(city)`
You could wrap geocode+weather in a single function. Then the model has nothing to sequence, and you have a workflow. The pedagogical point of this project is multi-step tool use: the policy must notice that it does not have coordinates yet. In production you might fuse them — after you can sequence.
A second reason: geocoding and weather fail independently. Fused tools hide which hop broke. Split tools make evals honest.
Argument boundaries
Never let the model pass the whole transcript into a tool. Each tool gets only its args. That is how you sandbox. geocode cannot read FORECASTS. weather cannot rename cities. finish cannot geocode. If you later add a debug_dump tool for yourself, do not put it in the registry the model sees.
Watch out:A tool that acceptssql: strorcode: stris a different product. Keep this agent's hands small.
Simulated latency and flaky APIs (optional stub)
Real weather APIs timeout. You can simulate flakiness with a counter: the first weather call for Oslo returns {"error": "timeout"}, the second succeeds. The loop in part 3 will retry. You do not need threads. A module-level integer is an environment.
Do not add random failure in the default catalog or your tests will flake. Flakiness should be opt-in for a named test.
Exercise
Add london to CITIES and FORECASTS. Add a timeout error path: if city == "oslo" and a global OSLO_FAILS is True, geocode returns timeout once then works. Print both calls. You will use this in part 3 retries.
Check your understanding