Part one: four tools
Add three tools to Rover, alongside read_file. Write the descriptions yourself before
looking at mine.
import json
import os
TOOLS = [
{
"name": "read_file",
"description": (
"Return the full text contents of a file. "
"Use this when you need to know what is written inside a file."
),
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string", "description": "File to read."}},
"required": ["path"],
},
},
{
"name": "list_files",
"description": (
"List the file names in the current folder. "
"Use this when you do not know what files exist, or the user names a file "
"you cannot find."
),
"input_schema": {"type": "object", "properties": {}},
},
{
"name": "search_files",
"description": (
"Search every text file in the current folder for a pattern, and return "
"matching lines with their file names. "
"Use this to find which file mentions something, before reading whole files."
),
"input_schema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Text to look for."}
},
"required": ["pattern"],
},
},
{
"name": "write_file",
"description": (
"Write text to a file, replacing anything already there. "
"Use this only when the user asks for something to be saved."
),
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "File to write."},
"content": {"type": "string", "description": "Text to write into it."},
},
"required": ["path", "content"],
},
},
]
def list_files():
return "\n".join(sorted(os.listdir(".")))
def search_files(pattern):
hits = []
for name in sorted(os.listdir(".")):
if not os.path.isfile(name):
continue
try:
with open(name) as f:
for i, line in enumerate(f, 1):
if pattern.lower() in line.lower():
hits.append(f"{name}:{i}: {line.strip()}")
except (UnicodeDecodeError, PermissionError):
continue
return "\n".join(hits) if hits else f"No matches for {pattern!r} in this folder."
def write_file(path, content):
with open(path, "w") as f:
f.write(content)
return f"Wrote {len(content)} characters to {path}."
def run_tool(name, tool_input):
"""Returns (output_text, is_error)."""
try:
if name == "read_file":
with open(tool_input["path"]) as f:
return f.read(), False
if name == "list_files":
return list_files(), False
if name == "search_files":
return search_files(tool_input["pattern"]), False
if name == "write_file":
return write_file(tool_input["path"], tool_input["content"]), False
return f"No tool named {name}.", True
except FileNotFoundError:
return (
f"No file called {tool_input.get('path')} here. "
f"These exist: {list_files()}"
), True
except Exception as e:
return f"The tool failed: {e}", True
Now run the loop from Lesson 1.6 with these four tools and try each of these:
| Ask | What it should do |
|---|---|
| “What files are here?” | list_files only |
| “Which file mentions the plumber?” | search_files, not four read_file calls |
| “Read notes.txt and summary.txt” | Two read_file calls in one turn |
| “Save a summary of notes.txt to out.txt” | read_file, then write_file — two turns |
| “What is 12 times 12?” | No tools at all |
The fourth one is the interesting case. It needs two turns because the second call depends on
the first. The model works that out from the task, not from anything you wrote.
If you are running a small local model
Run that table anyway, and expect one row to fail. Small models are good at choosing between
tools and bad at choosing no tool. A local model tested for this course got the first four
rows right and then called read_file for “what is 12 times 12?”.
That is worth knowing precisely, because it tells you which lessons in this module still apply
to you:
| Claim | Holds on a small local model? |
|---|---|
| Clear descriptions beat vague ones | Yes — this reproduces |
| A stated boundary fixes confusion between two similar tools | Yes |
| Trigger sentences change how often a tool is used | Yes |
| A good description stops it using a tool it does not need | Weakly — expect over-triggering |
So do the whole module locally. Just do not conclude that your description is broken when the
model reaches for a tool on a question that needed none. That one is the model, and the fix is
a better model rather than better words — which is the only place in this course where that is
the honest answer.
Part two: your provider may ship a loop
Most providers offer a helper that runs the loop for you. The names differ — Anthropic calls
it a tool runner, others call it an agent or an assistant — but the idea is the same, and it
usually looks something like this:
# Sketch of the shape these helpers take. Check your provider's own documentation
# for the exact names; this is here so you recognise one when you meet it.
@tool # a decorator that registers the function
def read_file(path: str) -> str:
"""Return the full text contents of a file.
Use this when you need to know what is written inside a file.
Args:
path: The file to read, for example notes.txt.
"""
with open(path) as f:
return f.read()
runner = some_provider.tool_runner(tools=[read_file], messages=[...])
for message in runner:
...
Look at what disappeared. No while loop. No stop check. No result assembly. No schema —
it is generated from the type hints, and the description comes from the docstring.
Now look at what did not disappear. That docstring is a tool description. The trigger
sentence — “Use this when you need to know what is written inside a file” — is doing exactly
the job Lesson 2.2 described, and if you leave it out the helper cannot put it back.
Which one should you use
For real work on a single provider, use their helper. It is less code, and the loop it runs is
the one you just wrote by hand.
Two things to know before you reach for one:
It ties you to that provider. These helpers are not standardised. Writing one loop yourself
and swapping connect(...) is the reason this course can offer you four options in Lesson 1.1.
It does not write your descriptions. The helper removes the loop, which was the easy part.
Everything in this module still applies, unchanged.
We keep the manual loop for the rest of this course. Modules 3 and 4 add permission checks
and recovery logic in the middle of it, and those changes are much clearer in a loop you can
read than inside somebody else’s callback.
The helper removes the loop. It does not remove the thinking. The part you had to learn is
the part it cannot do for you.
Try this before the next module
Take the four-tool Rover and deliberately break one description — remove the trigger sentence
from search_files.
Ask “which file mentions the plumber?” five times. Count how often it searches versus reading
every file one by one.
That number is your evidence for the claim this module opened with. Keep it. In Module 7 you
will turn exactly this kind of manual counting into an eval that runs on its own.