Lab 28 — Tool Calling & AI Agents¶
Session: Week 14 · Session 28 · 90 min CLO: CLO-3 Difficulty: Advanced
Learning objectives¶
By the end of this lab you can:
- Describe functions to an LLM with OpenAI-style tool schemas.
- Implement the model→tool→result round trip.
- Run the loop with a real model or a deterministic mock.
- Explain why the model proposes, your code executes split is a safety property.
Problem statement¶
You must give an LLM the ability to compute instead of guess: three pandas tools (mean tip by day, missing-value count, numeric summary) plus a loop that executes whatever tool call the model proposes. The twist: the deliverable must work with a real local model and with a mock — because in production the loop is the same either way.
Dataset requirements¶
Seaborn built-in tips (244 rows).
Step-by-step tasks¶
- Tools: write three plain functions:
tip_by(df, by)→df.groupby(by)["tip"].mean().round(2).to_string()missing_summary(df)→df.isna().sum().to_string()describe_numeric(df)→df.describe().round(2).to_string()Build aREGISTRY = {name: func}dict.- Schemas: for each tool, write the OpenAI-style schema dict
(
type: "function",name,description,parameterswithpropertiesandrequired).tip_byneeds abystring property. - Executor:
execute_tool(name, arguments, df)that looks the function up inREGISTRYand calls it with the arguments. Add a guard: if the name is not inREGISTRY, return"unknown tool: <name>"instead of raising. - Mock model: write
mock_model(messages, tools)that: - returns a tool call for
tip_by(by="day")on the first user message, - and returns a final answer ("The mean tip is highest on Sunday.") once a tool result is present in the messages.
- Loop: implement
run_loop(question, use_real_llm)(max 4 steps): append user message → get model message → if it hastool_calls, execute each and append the tool result as role"tool"→ else return the answer. Print the transcript (each step) so the loop is auditable. - Run both paths:
run_loop("Which day has the highest mean tip?", use_real_llm=False)(mock — must print the transcript) and, guarded, the same question withuse_real_llm=True(Ollama; degrades to a message if unavailable). - Verify the mock's final answer against
tip_by(tips, "day")— the real max is Sun ≈ 3.26, so the mock is correct; state this in a comment. - Agent boundaries (session topic): in one markdown paragraph, list
the three boundaries your loop enforces (registered tools only, max
steps, your code executes the tools) and why each one is a safety
property. Then automate a small project subtask: run the loop on your
own project DataFrame with
missing_summaryanddescribe_numeric(mock path) and save the transcript to a text file as an agent log.
Starter code¶
import pandas as pd
import seaborn as sns
tips = sns.load_dataset("tips")
# 1. tools + registry
def tip_by(df, by):
return df.groupby(by)["tip"].mean().round(2).to_string()
def missing_summary(df):
return df.isna().sum().to_string()
def describe_numeric(df):
return df.describe().round(2).to_string()
REGISTRY = {"tip_by": tip_by, "missing_summary": missing_summary,
"describe_numeric": describe_numeric}
# 2. schemas (write these yourself — one per tool)
# tip_by_schema = {"type": "function", "function": {...}}
# your code here
# 3-6. executor, mock, loop, runs
# your code here
Expected output¶
REGISTRYhas 3 entries; schemas printed for inspection.execute_tool("tip_by", {"by": "day"}, tips)returns the day table;execute_tool("hack", {}, tips)returns"unknown tool: hack".- Mock path prints a transcript showing: user → tool_call → tool result → final answer.
- Real path (if Ollama up) prints a transcript too; otherwise a clear skip message.
- Final answer verified: Sun has the highest mean tip.
Questions¶
- Why does the model send a proposal (tool call) instead of running code itself?
- What is the purpose of the
descriptionfield in a schema? - Why must tool results come back to the model as messages? What would the model miss without them?
- What does the
REGISTRYguard protect against? - When would you set
max_stepsto 1? When would you raise it?
Challenge task¶
Add a fourth tool of your own (e.g., top_n(df, column, n) returning
the largest rows) with a correct schema, and extend the mock to call it in
a two-step plan: first top_n on total_bill, then answer. Run the mock
path and show the transcript proves the answer came from the tool result,
not from the model's memory.
Marking rubric¶
| Criteria | Max | Notes |
|---|---|---|
| Three tools + registry | 4 | functions correct |
| Schemas with required fields | 5 | all three complete |
| Executor + unknown-tool guard | 4 | tested both ways |
| Mock model (tool call → answer) | 5 | correct flow |
| Loop with transcript | 5 | auditable steps |
| Both paths run | 3 | mock always, real guarded |
| Verification comment | 3 | Sun ≈ 3.26 |
| Answers to questions | 3 | Q1, Q3, Q4 correct |
| Challenge: 4th tool + 2-step plan | 4 | works in mock |
| Total | 36 |