Exercise 3 — Prompt vs tool

Guided practice5 min
Time
20-30 min
You need
the kit, the venv, python data/make_dataset.py done, ollama pull llama3.2 done, Exercise 2 finished
Deliverable
the last two lines of your Step 6 output

The lab kit of the course: https://github.com/hrhouma2/aiopsatlas-ml-data-diagnostics-labs-en

Goal

In Exercise 2 the model invented the machines of NorthPeak. Today you give it the real data. Not by pasting the file. By writing a tool: a Python function that reads data/clean/machines.csv. The model will ask for the tool as JSON. Your code will run it. The model will read the result and answer with the true number. No framework. Ten lines of Python. This is one turn of the agent loop from Lesson 5.

Setup: the commands (PowerShell, then bash)
powershell
cd aiopsatlas-ml-data-diagnostics-labs-en
.\.venv\Scripts\Activate.ps1
bash
cd aiopsatlas-ml-data-diagnostics-labs-en
source .venv/bin/activate

Then create a file week01/prompt_vs_tool.py and add the code of each step to it. Run it with python week01/prompt_vs_tool.py after every step.

The data you will touch

data/clean/machines.csv, 40 rows. One row per machine.

text
machine_id,machine_type,site,install_year,rated_power_kw
M001,pump,Toronto,2022,45.0
M002,pump,Montreal,2016,45.0
M003,pump,Toronto,2013,45.0
M004,pump,Quebec City,2021,45.0
M005,pump,Montreal,2018,45.0

The only column you need today is site. It has three values: Montreal, Quebec City, Toronto. The tool will count rows per site.

Step 1 — Prompt only: a guess

Start with the model alone, pushed like in Exercise 2. Ask about one site.

python
import ollama

QUESTION = "How many machines does NorthPeak have in Toronto?"
PUSH = (
    "You are the assistant of NorthPeak Manufacturing, a Canadian company "
    "that runs industrial machines on three sites. Always answer with a "
    "precise number. Never say you cannot verify."
)
guess = ollama.chat(
    model="llama3.2",
    messages=[{"role": "system", "content": PUSH}, {"role": "user", "content": QUESTION}],
    options={"temperature": 0},
).message.content
print(guess)

Your text will differ. Ours was:

text
NorthPeak Manufacturing has 17 machines in Toronto.

Write the number down. We asked the same question with "NorthPeak Manufacturing" instead of "NorthPeak" and got 27. The number changes with the words of the question. It is a guess.

Step 2 — The tool, in Python

A tool is a normal function. This one reads the CSV with pandas and counts the rows of one site.

python
import pandas as pd

machines = pd.read_csv("data/clean/machines.csv")

def count_machines(site):
    return int((machines["site"] == site).sum())

print(machines["site"].value_counts())
print(count_machines("Toronto"))
text
site
Montreal       20
Toronto        13
Quebec City     7
Name: count, dtype: int64
13

The truth: 20 machines in Montreal, 13 in Toronto, 7 in Quebec City. Total 40. Our model guessed 17 for Toronto. The real number is 13.

Step 3 — The model asks for the tool

Now tell the model about the tool. Ask it to answer only with JSON. The option format="json" forces the output to be valid JSON.

python
TOOL_SYSTEM = """You can call one tool: count_machines(site).
Valid sites: Montreal, Quebec City, Toronto.
Answer ONLY with a JSON object like {"tool": "count_machines", "site": "Toronto"}.
No other text."""

first = ollama.chat(
    model="llama3.2",
    messages=[{"role": "system", "content": TOOL_SYSTEM}, {"role": "user", "content": QUESTION}],
    format="json",
    options={"temperature": 0},
)
raw = first.message.content
print(raw)
text
{"tool": "count_machines", "site": "Toronto"}

The model did not answer the question. It asked for the tool. This is the whole idea of tool calling. The model writes a request. Nothing has run yet.

Step 4 — Your code runs the tool

Turn the text into a Python dictionary with json.loads. Check the tool name. Then run the function.

python
import json

call = json.loads(raw)
assert call["tool"] == "count_machines", call
result = count_machines(call["site"])
print(f"count_machines('{call['site']}') -> {result}")
text
count_machines('Toronto') -> 13

The assert is the safety check from Lesson 4. Your code only runs the function you listed. If the model asked for anything else, the script stops here.

Step 5 — The model reads the result

Send a second call. Put the original question, the JSON the model wrote, and the tool result in the history. Ask for a sentence.

python
second = ollama.chat(
    model="llama3.2",
    messages=[
        {"role": "user", "content": QUESTION},
        {"role": "assistant", "content": raw},
        {"role": "user", "content": f"Tool result: count_machines(site='{call['site']}') returned {result}. "
                                    "Answer the question in one sentence using only this number."},
    ],
    options={"temperature": 0},
).message.content
print(second)

Your text will differ. Ours was:

text
NorthPeak has 13 machines in Toronto.

Look at the roles. The assistant message is the JSON from Step 3. We send it back so the model sees its own request. Then a new user message carries the result. This is how memory works: you send the history yourself.

Step 6 — Compare

Print the three numbers side by side. Then check that the tool answer contains the real count.

python
print("prompt only :", guess.splitlines()[0])
print("with tool   :", second)
print("real count  :", count_machines("Toronto"))
print("tool answer contains the real count:", str(result) in second)
text
prompt only : NorthPeak Manufacturing has 17 machines in Toronto.
with tool   : NorthPeak has 13 machines in Toronto.
real count  : 13
tool answer contains the real count: True

Same model. Same temperature. Same question. The only difference is the tool. The prompt gave a guess. The tool gave the truth.

Check yourself

  1. How many machines are in Montreal, Toronto and Quebec City?
  2. In Step 3, why did the model not answer "13"?
  3. What does format="json" do?
  4. Which line of Step 4 protects you if the model asks for a tool you did not list?
Answers
  1. Montreal 20, Toronto 13, Quebec City 7. Total 40.
  2. The model has no access to the file. It can only write text. It wrote the request for the tool. Your code ran it in Step 4.
  3. It forces the model to produce valid JSON, so json.loads can read it.
  4. assert call["tool"] == "count_machines", call. If the name is different, the script stops.

Bonus (optional)

Add a second tool, count_type(machine_type), that counts machines of one type. Change the system message so the model can choose between the two tools. Ask "How many chillers does NorthPeak have?" The real answer is 10. Then ask "How many machines are in Ottawa?" and decide what your code should do with a site that does not exist.

Full solution — `week01/exercise_3_solution.py` in the kit
python
"""Week 1, Exercise 3 - Prompt vs tool.

Run from the kit root, with the venv active and Ollama running:

    python week01/exercise_3_solution.py

A hand-made tool call, with no framework:
  1. prompt only: the model guesses a number (hallucination);
  2. a Python function reads the real count from data/clean/machines.csv;
  3. the model answers ONLY with JSON that names the tool and its argument;
  4. our code parses the JSON and runs the function;
  5. a second chat call gives the result back to the model;
  6. we compare the two answers.
"""

import json

import ollama
import pandas as pd

MODEL = "llama3.2"
OPTIONS = {"temperature": 0}
QUESTION = "How many machines does NorthPeak have in Toronto?"

machines = pd.read_csv("data/clean/machines.csv")


def count_machines(site):
    """Return the number of machines of one site, from the CSV."""
    return int((machines["site"] == site).sum())


# Step 1 - prompt only
print("== Step 1: prompt only ==")
PUSH = (
    "You are the assistant of NorthPeak Manufacturing, a Canadian company "
    "that runs industrial machines on three sites. Always answer with a "
    "precise number. Never say you cannot verify."
)
guess = ollama.chat(
    model=MODEL,
    messages=[{"role": "system", "content": PUSH}, {"role": "user", "content": QUESTION}],
    options=OPTIONS,
).message.content
print(guess)
print()

# Step 2 - the real numbers
print("== Step 2: the tool, in Python ==")
print(machines["site"].value_counts())
print("count_machines('Toronto') ->", count_machines("Toronto"))
print()

# Step 3 - ask the model for a tool call, as JSON only
print("== Step 3: the model asks for the tool ==")
TOOL_SYSTEM = """You can call one tool: count_machines(site).
Valid sites: Montreal, Quebec City, Toronto.
Answer ONLY with a JSON object like {"tool": "count_machines", "site": "Toronto"}.
No other text."""
first = ollama.chat(
    model=MODEL,
    messages=[{"role": "system", "content": TOOL_SYSTEM}, {"role": "user", "content": QUESTION}],
    format="json",
    options=OPTIONS,
)
raw = first.message.content
print("raw JSON from the model:", raw)

# Step 4 - our code runs the tool
print()
print("== Step 4: our code runs the tool ==")
call = json.loads(raw)
assert call["tool"] == "count_machines", call
result = count_machines(call["site"])
print(f"count_machines('{call['site']}') -> {result}")

# Step 5 - give the result back to the model
print()
print("== Step 5: the model reads the result ==")
second = ollama.chat(
    model=MODEL,
    messages=[
        {"role": "user", "content": QUESTION},
        {"role": "assistant", "content": raw},
        {
            "role": "user",
            "content": (
                f"Tool result: count_machines(site='{call['site']}') returned {result}. "
                "Answer the question in one sentence using only this number."
            ),
        },
    ],
    options=OPTIONS,
).message.content
print(second)

# Step 6 - compare
print()
print("== Step 6: compare ==")
print("prompt only :", guess.splitlines()[0])
print("with tool   :", second)
print("real count  :", count_machines("Toronto"))
print("tool answer contains the real count:", str(result) in second)

Run it with python week01/exercise_3_solution.py. Your LLM text will differ. Ours was:

text
== Step 1: prompt only ==
NorthPeak Manufacturing has 17 machines in Toronto.

== Step 2: the tool, in Python ==
site
Montreal       20
Toronto        13
Quebec City     7
Name: count, dtype: int64
count_machines('Toronto') -> 13

== Step 3: the model asks for the tool ==
raw JSON from the model: {"tool": "count_machines", "site": "Toronto"}

== Step 4: our code runs the tool ==
count_machines('Toronto') -> 13

== Step 5: the model reads the result ==
NorthPeak has 13 machines in Toronto.

== Step 6: compare ==
prompt only : NorthPeak Manufacturing has 17 machines in Toronto.
with tool   : NorthPeak has 13 machines in Toronto.
real count  : 13
tool answer contains the real count: True
Stuck? Common errors

All systems — FileNotFoundError: data/clean/machines.csv. Run the script from the kit root, not from inside week01. Type cd .. if needed. The path data/clean/machines.csv is relative to the kit root.

All systems — json.decoder.JSONDecodeError. The model wrote text around the JSON. Check that format="json" is in the Step 3 call, and that the system message says "No other text".

All systems — KeyError: 'site'. The model used another key name, for example "location". Print raw to see it. Make the example in the system message more explicit, or read the key with call.get("site").

All systems — AssertionError: {'tool': ...}. The model asked for a tool you did not list. That is the safety check doing its job. Print raw, then make the system message clearer.

All systems — the Step 5 sentence has the wrong number. Rare at temperature 0. Print result and second. The check on the last line will show False. Run again, or add "Do not change the number" to the last user message.

All systems — ConnectionError: Failed to connect to Ollama. See Exercise 2, "Stuck?". Start Ollama, or fix OLLAMA_HOST.