openai SDK pointing at a local Ollama server (no Anthropic API key required). View Claude version → · OS Track Index →
M05: Function Calling Fundamentals
This is the pivotal moment in the course. The model goes from chatbot to agent. For the first time, the model will do things — not just generate text. You'll build your first tool-using agent.
Learning Objectives
- Explain how function calling works: The model proposes, your code executes
- Define tools with JSON Schema input parameters and clear descriptions
- Implement the complete tool use loop: send → detect tool_calls → execute → return result
- Handle tool errors gracefully without crashing the agent loop
- Build a multi-tool agent that chooses the right tool based on the user's question
What Is Tool Use / Function Calling?
BEFORE: Imagine a super-smart assistant who can write beautifully, reason through complex problems, and hold a conversation — but is locked in a windowless room with no phone, no internet, and can't see the clock. Every answer about the outside world is a guess based on what they already know.
PAIN: When someone asks "What's the weather in Tokyo right now?", the assistant has no choice but to make something up or say "I don't know." They can't check a weather API, query a database, or look anything up. Their intelligence is trapped — brilliant but blind to the real world.
MAPPING: Function calling hands the assistant a set of labeled buttons — get_weather(), calculate(), ask_time() — each one connected to the outside world. Now when someone asks about Tokyo's weather, the assistant presses the get_weather button with "Tokyo" as input, reads the result that comes back, and gives a grounded, accurate answer. That's function callingA mechanism where the model requests to call a function you've defined. The model doesn't execute the function itself — it returns a structured request specifying which function to call and with what arguments. Your code executes it and sends back the result.: The model gains the ability to reach outside its context window and interact with real systems, while you stay in control of what those buttons actually do.
When you call the Chat Completions APIThe OpenAI-compatible API for interacting with the model. You send a list of messages (user/assistant turns) and receive the model's response. It supports text generation, tool use, vision, and streaming. Ollama exposes this API at http://localhost:11434/v1., you can include a tools parameter — an array of tool definitionsA JSON object describing a function the model can call. Each tool has a name, description (critical for the model's selection), and a parameters object (JSON Schema) specifying the expected arguments with types and descriptions — all wrapped as {"type": "function", "function": {...}}. that describe what functions the model is allowed to request.
Each definition has three parts. First, a name — this is how your code identifies which function was called. Second, a description — the plain-English explanation the model reads to decide when this tool is useful. Third, a parameters object — a JSON Schema that specifies what arguments the function accepts, their types, and which ones are required. (In the OpenAI-compatible format these three live inside a function wrapper, as the code below shows.) Of these three, the description matters most — it's what the model uses to decide which tool fits the user's request.
Here's the key idea: the model never runs your code. When the model decides it needs a tool, it stops generating text and instead returns one or more tool callsEntries in response.choices[0].message.tool_calls. Each has a unique id, a function.name, and a function.arguments string (JSON-encoded). Your code reads it, executes the function, and sends back the result.. In plain terms, it's a structured request that says "I want to call get_weather with {city: "Tokyo"}." Your application code reads that request, runs the actual function on your server, and sends the output back as a tool messageA message with role: "tool" that carries the output of the tool execution. It must reference the tool_call_id from the model's request so the model knows which call it answers.. the model then uses that result to write its final answer.
Why this design? Because it keeps you in control. the model can suggest calling any tool you've defined, but your code is the gatekeeper — you validate inputs, enforce rate limits, check permissions, and decide whether to actually execute. This separation of "deciding what to do" (the model) from "actually doing it" (your code) is what makes tool use safe for production systems.
Without tool use, the model is limited to what it learned during training — which has a knowledge cutoff date and zero access to your private data. With tool use, the model can query your production database, check a live inventory API, send an email, or trigger a deployment. A customer support agent with 5 tools (order lookup, refund processor, FAQ search, ticket creator, escalation handler) can resolve 70-80% of tickets autonomously. That's the difference between a chatbot that says "I'd recommend checking your order status" and an agent that says "Your order #4829 shipped yesterday via FedEx and arrives Thursday."
So what does this look like in practice? Here's the actual JSON that the model returns when it wants to call a tool:
That's it — one entry from the message.tool_calls array in the model's response. Let's break it down field by field. The id is a unique identifier you'll need to reference when sending the result back (think of it as a receipt number for this specific tool call). function.name tells you which function to call. function.arguments is a string of JSON — you call json.loads() on it to get the arguments dict, matching the schema you defined.
And here's the message you send back to the model with the tool's output:
Notice the tool_call_id matches the id from the model's request above — that's how the model knows which tool call this result belongs to. The result goes in a message with role: "tool" — a dedicated role for feeding tool output back to the model. No magic — just structured data flowing back and forth.
"the model executes the tool, right?" — No. This is the single most important misconception to clear up. the model never executes anything. It returns a JSON object that says "I'd like to call this function with these arguments." Your code reads that request, decides whether to honor it, runs the actual function, and sends back the result. the model is the decision-maker; your application is the executor.
"Tools are like ChatGPT plugins?" — Not quite. Plugins run on someone else's infrastructure with a fixed interface you can't customize. With the model's function calling, YOU define the tools, YOU write the implementation, and YOU host the execution. There's no plugin marketplace or approval process — any function you can write is a potential tool.
"More tools = more capable agent?" — Actually, the opposite is often true. the model's tool selection accuracy starts to degrade when you offer more than 5-6 tools at once. Each additional tool means more descriptions for the model to evaluate, more chances for ambiguity, and slower response times. Module 6 covers strategies for managing many tools effectively.
"The tool description is just a label — the model understands from the name" — Names help, but the model heavily relies on the description to decide when and how to use a tool. A tool named search with description "Search" gives the model almost nothing to work with. Does it search the web? A database? Files? The description is where you tell the model what data source the tool hits, what format the output comes in, and when this tool is appropriate versus other options.
"If the tool fails, the whole agent crashes" — Only if you let it. Best practice is to catch exceptions and return the error as the tool's result content. the model can then tell the user what went wrong or try an alternative approach. The agent loop keeps running — only an unhandled exception in YOUR code kills it.
Defining Tools
A tool definition is a JSON object you pass to the API that tells the model "here's a function you can ask me to call." It has three parts: name, description, and parametersA JSON Schema object that describes the arguments a tool accepts. JSON Schema is a standard for describing the structure of JSON data — including property names, types, descriptions, required fields, and enums. the model uses this schema to generate valid arguments. In the OpenAI-compatible format, name, description, and parameters all sit inside a function wrapper..
Under the hood, the model processes tool definitions as part of the system context. When a user sends a message, the model reads all available tool descriptions, evaluates which one (if any) matches the user's intent, and either calls the best match or responds directly without tools. This is fundamentally different from keyword matching — the model understands semantics. A user saying "how hot is it in Tokyo?" will match a tool described as "Get current weather for a city" even though the word "weather" never appears in the query.
If you've used APIs before, think of tool definitions as similar to API documentation — but instead of a human developer reading the docs, the model reads them. The parameters field uses JSON Schema — a standard for describing the shape of JSON data. You may have seen it in OpenAPI or Swagger specs. Here's one key difference from M04: in structured output, the schema described what came OUT of the model. Here, the schema describes what goes IN to your function. And of the three parts (name, description, schema), the description is by far the most important — the model picks tools based on descriptions, not names.
Here's a trap almost every beginner falls into: you name your tool search, set the description to "Search", and wonder why the model picks the wrong tool. The problem is that the model relies heavily on the description to decide when and how to use a tool. A one-word description gives the model almost nothing to work with. Does it search the web? A database? Files on disk?
A great description includes four things: (1) what the tool does, (2) what data source it queries, (3) what format the output comes in, and (4) when this tool is appropriate versus other options. Spend more time writing descriptions than writing the tool implementation — descriptions are the #1 lever for tool selection accuracy.
Tool descriptions are critical for selection accuracy. Include: what the tool does, input format with examples, expected output format, and edge cases. Poor descriptions = the model picks the wrong tool.
In the UCC filing pipeline (our capstone domain), you'd define tools like: search_filings(debtor_name) to query the Gold layer in BigQuery, check_entity_risk(entity_id) to pull risk profiles, and get_amendment_history(filing_id) to trace changes over time. Each tool maps to a specific data source, and the descriptions would tell the model exactly when to use each one — e.g., "Search UCC filings by debtor name. Returns filing number, secured party, collateral description, and filing date. Use when the user asks about a company's liens or secured debts."
Example Tool Definition
The diagram above shows the function object. In the OpenAI-compatible format the API actually expects, that object is wrapped in {"type": "function", "function": {…}} — here's the complete tool definition you pass to Ollama:
The Tool Use Loop in Code
The tool use loop is the core pattern that turns the model from a chatbot into an agent. Here's the idea in plain English: you send the model a message, the model thinks about it, and sometimes the model decides it needs more information before answering. When that happens, the model pauses and says "I'd like to call this tool." Your code runs the tool, sends back the result, and the model picks up where it left off. This back-and-forth continues until the model has everything it needs to give a final answer.
In code, this translates to a while loop. Each time the model responds, you check a field called finish_reasonA field in the model's response indicating why generation stopped. "stop" means the model finished normally. "tool_calls" means the model wants to call a tool and is waiting for the result before continuing.. If finish_reason is "tool_calls", the model is waiting for a tool result — so you execute the tool, append the result to the conversation, and call the API again. If finish_reason is "stop", the model is finished — you extract the text and return it to the user.
This is different from a simple request/response pattern. In a normal API call, you send one message and get one reply. With the tool use loop, a single user question might trigger 2, 3, or even 10 API round-trips as the model calls different tools to gather information. Each round-trip adds to the conversation history, so the model always has the full context of what it's already tried. The loop only exits when the model decides it has enough information — signaled by finish_reason: "stop".
"The loop runs forever if the model keeps calling tools" — In theory, yes. In practice, the model almost always converges within 2-5 iterations. But you should always add a safety cap (e.g., max_iterations = 10) so a misbehaving prompt can't burn through your API budget. Module 12 covers this in depth.
"I need to parse the model's text to know when it's done" — Never do this. The finish_reason field is the only reliable signal. Don't scan the model's response for phrases like "I'm finished" or "here's your answer" — those are unreliable and fragile.
"Each loop iteration is a new conversation" — No. Each iteration adds to the SAME messages array. the model sees the full history — including every tool call and result from previous iterations — which is how it knows what it's already tried and what information it still needs.
Never silently return empty results for access failures. {isError: false, results: []} means "nothing found." {isError: true, errorCategory: "access_denied"} means "couldn't even check." the model makes catastrophically different decisions based on which one you return.
Everything in this module covered custom tools — functions you define and execute. Some frameworks ship pre-built tools for common operations, with no schema for you to write:
- Web search — built into some agent frameworks (LangChain's SerpAPI tool, CrewAI's search tool). Same tool call / tool result loop, but the schema and execution are provided for you.
- Code execution — sandboxed Python runners (E2B, Modal) that let the model run code safely. You provide the sandbox; the framework provides the tool definition.
- File operations — read, write, edit files. Often available as prebuilt tools in LangChain and similar frameworks for coding agents.
These work like the custom tools you just wrote — same finish_reason: "tool_calls" loop — but the schema and routing are handled by the framework. You provide the execution environment. M24 covers advanced tool patterns including computer use and sandboxing.
Error Handling & Edge Cases
When tools fail — and they will — the worst thing you can do is crash the agent loop. Instead, the pattern is straightforward: catch the error, describe it in plain language, and send that description back as the tool's result. the model then has enough information to inform the user, try a different approach, or gracefully give up.
Here's how this works internally. When your code catches an exception inside a tool function, you have two choices. You could let the exception propagate, which kills the while loop and likely returns an ugly stack trace to the user. Or you could wrap the call in a try/except, format the error into a JSON object like {"error": "City not found: Tokyoo"}, and return it as the tool result. the model reads that result, realizes something went wrong, and responds accordingly — often saying "I couldn't find weather data for 'Tokyoo'. Did you mean Tokyo?" That's a much better user experience.
This approach differs from traditional error handling because the "caller" (the model) isn't your code — it's a language model that can reason about failures. In normal programming, you handle errors with if/else branches. With tool use, you report errors as data, and the model decides what to do. That means your error messages need to be descriptive enough for the model to make a good decision.
There are four main failure modes to handle:
- Tool timeout: Set a time limit (e.g., 10 seconds) for each tool call. If it exceeds the limit, return a timeout message as the tool result so the model can tell the user to try again.
- Tool failure: Return a human-readable error description, not a raw stack trace. the model needs to understand what went wrong, not debug your Python code.
- Invalid arguments: Validate inputs before executing. If the model sends
"city": 123instead of a string, return a helpful error message explaining what format you expected. - Non-existent tool: This is rare (the model usually sticks to defined tools), but handle it with a fallback that returns
{"error": "Unknown tool: tool_name"}. This prevents a crash if the model hallucinates a tool name.
Return structured errors from tools: {isError: true, errorCategory: "auth_failure", isRetryable: true, context: "Token expired"}. Anti-pattern: generic "Operation failed" — the model can't decide to retry, try alternatives, or escalate.
Code Walkthrough: Multi-Tool Agent
while loop that keeps running until finish_reason is "stop", a dispatcher that routes tool requests to actual functions, and error handling at every step. Read through the chunk annotations before each section to understand what each part does and why.
Complete Tool-Using Agent
This agent has 5 logical sections. Read the annotations to understand each chunk before looking at the code.
Let's start with the foundation. The first thing the code does is initialize the OpenAI client pointed at Ollama and define two tools:
get_weather and calculate. Each tool gets a name, a description, and a JSON Schema specifying what arguments it accepts. The interesting part is the description field — this is literally what the model reads to decide which tool fits the user's request. Think of it as a menu item description at a restaurant: vague descriptions like "Search" are like labeling a dish "Food" — nobody knows what they're getting, and the model will pick the wrong tool.
Next up, the actual functions that run when the model requests a tool. Right now these are mocks —
get_weather returns hardcoded data for a few cities, and calculate evaluates math expressions. In production, you'd replace these with real API calls or database queries. The point of using mocks is that they let you develop and test the entire agent loop without worrying about API keys, network errors, or rate limits. Here's the dilemma with calculate: we need to evaluate a math expression, but Python's eval() can execute arbitrary code if you're not careful. That's why the function validates that the input contains only math characters (0-9 + - * / . ( )) before evaluating. Never skip this step — unsanitized eval is a textbook security vulnerability.
This is the heart of the agent — the
while True loop that keeps the conversation going until the model is done. Here's how each iteration works: send the current messages to the model, check finish_reason in the response. If it's "tool_calls", the model wants to call a tool, so you execute it and loop again. If it's "stop", the model is finished and you return the final text. The key gotcha that trips up most beginners: you must append both the assistant message (with tool_calls) AND the tool role messages to the conversation history before the next API call. Forget either one and the API returns an error — the model needs to see the full conversation to know where it left off.
Inside the loop, the code needs to figure out which function to actually call. That's what the dispatcher dictionary (
tool_functions) does — it maps tool names to Python functions, so "get_weather" routes to get_weather(). The clever bit is what happens when the model requests a tool that doesn't exist in the dictionary: instead of crashing with a KeyError, the code returns an error message as the tool result. the model reads that error and can inform the user or try something else. This is a recurring pattern you'll see in every agent module going forward: errors are data, not crashes.
Finally, the code runs three test queries that exercise every path through the agent. The first ("What's the weather in Tokyo?") requires the weather tool. The second ("What's 15% of 850?") requires the calculator. The third ("What's the capital of France?") needs no tool at all — the model knows the answer from training data and responds directly. Testing all three paths is important because it proves your agent handles tool selection, tool execution, AND the no-tool case correctly. If any of these three fails, you've got a bug in your loop logic.
# pip install openai
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# Define tools (OpenAI format: wrap in {"type":"function","function":{...}})
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city. Returns temp (Celsius), condition, humidity.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Tokyo'"}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Use for any math computation.",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression, e.g. '15 * 3 + 7'"}
},
"required": ["expression"]
}
}
}
]
# Mock tool implementations
def get_weather(city: str) -> dict:
"""Mock weather API."""
data = {"Tokyo": {"temp": 22, "condition": "sunny", "humidity": 65},
"London": {"temp": 14, "condition": "cloudy", "humidity": 80},
"New York": {"temp": 28, "condition": "partly cloudy", "humidity": 70}}
return data.get(city, {"error": f"No data for {city}"})
def calculate(expression: str) -> dict:
"""Safe math evaluator."""
try:
# Only allow safe math operations
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return {"error": "Invalid characters in expression"}
result = eval(expression) # Safe: only math chars allowed
return {"result": result}
except Exception as e:
return {"error": str(e)}
# Tool dispatcher
tool_functions = {"get_weather": get_weather, "calculate": calculate}
def run_agent(user_message: str) -> str:
"""Run the agent loop until the model produces a final response."""
messages = [{"role": "user", "content": user_message}]
while True:
try:
response = client.chat.completions.create(
model="mistral",
tools=tools,
messages=messages,
)
except Exception as e:
return f"API error: {e}"
# Check if the model wants to use a tool
if response.choices[0].finish_reason == "tool_calls":
# Process each tool call
tool_results = []
for tool_call in response.choices[0].message.tool_calls:
func = tool_functions.get(tool_call.function.name)
if func:
result = func(**json.loads(tool_call.function.arguments))
else:
result = {"error": f"Unknown tool: {tool_call.function.name}"}
tool_results.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
# Add assistant response and tool results to conversation
messages.append({"role": "assistant", "content": None,
"tool_calls": response.choices[0].message.tool_calls})
messages.extend(tool_results)
else:
# finish_reason == "stop" — model is done
return response.choices[0].message.content or "No text response."
# Test it!
print(run_agent("What's the weather in Tokyo?"))
print()
print(run_agent("What's 15% of 850?"))
print()
print(run_agent("What's the capital of France?")) # No tool needed
// npm install openai
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather for a city. Returns temp (Celsius), condition, humidity.',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: "City name, e.g. 'Tokyo'" }
},
required: ['city']
}
}
},
{
type: 'function',
function: {
name: 'calculate',
description: 'Evaluate a mathematical expression. Use for any math computation.',
parameters: {
type: 'object',
properties: {
expression: { type: 'string', description: "Math expression, e.g. '15 * 3 + 7'" }
},
required: ['expression']
}
}
}
];
// Mock tool implementations
function getWeather(city) {
const data = {
Tokyo: { temp: 22, condition: 'sunny', humidity: 65 },
London: { temp: 14, condition: 'cloudy', humidity: 80 },
'New York': { temp: 28, condition: 'partly cloudy', humidity: 70 },
};
return data[city] || { error: `No data for ${city}` };
}
function calculate(expression) {
try {
const allowed = /^[0-9+\-*/.() ]+$/;
if (!allowed.test(expression)) return { error: 'Invalid characters' };
const result = Function(`"use strict"; return (${expression})`)();
return { result };
} catch (e) { return { error: e.message }; }
}
const toolFunctions = { get_weather: getWeather, calculate };
async function runAgent(userMessage) {
const messages = [{ role: 'user', content: userMessage }];
while (true) {
let response;
try {
response = await client.chat.completions.create({
model: 'mistral',
tools,
messages,
});
} catch (error) {
return `API error: ${error.message}`;
}
if (response.choices[0].finish_reason === 'tool_calls') {
const toolResults = [];
for (const toolCall of response.choices[0].message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
const func = toolFunctions[toolCall.function.name];
const result = func
? func(...Object.values(args))
: { error: `Unknown tool: ${toolCall.function.name}` };
toolResults.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
}
messages.push({ role: 'assistant', content: null,
tool_calls: response.choices[0].message.tool_calls });
messages.push(...toolResults);
} else {
return response.choices[0].message.content || 'No text response.';
}
}
}
console.log(await runAgent("What's the weather in Tokyo?"));
console.log();
console.log(await runAgent("What's 15% of 850?"));
console.log();
console.log(await runAgent("What's the capital of France?"));
while True loop that keeps calling the Chat Completions API until the model stops requesting tools. For the first query ("weather in Tokyo"), the model recognized it needed get_weather, returned a tool call, your code executed the mock function, sent back the result, and the model composed a natural-language answer from the data. For the second query, the model picked the calculate tool instead. For the third query ("capital of France"), the model knew the answer from its training data and responded directly without calling any tool — finish_reason was "stop" on the first response, so the loop exited immediately. This is the foundational agent pattern that every module from here forward builds upon.
Hands-On Exercise
get_time) so you practice the full flow with more variety.
What You'll Build
A 3-tool agent with get_weather, calculate, and get_time tools that handles tool calls, no-tool questions, and error cases — all with the while finish_reason == "tool_calls" loop pattern.
Time estimate: 25–35 minutes • Prerequisites: M01-M04 labs complete • Files you'll create: tool_agent.py (or tool_agent.mjs)
Environment Setup
mkdir m05-lab && cd m05-lab
python -m venv venv && source venv/bin/activate # Windows: venv\Scripts\activate
pip install openai
# No API key needed for Ollama!
mkdir m05-lab && cd m05-lab
npm init -y && npm install openai
# Start Ollama: ollama pull mistral && ollama serve
Step 1: Define Three Tools with Mock Implementations
This step creates the tool definitions (what the model sees) and mock implementations (what your code runs). Using mock data means you can test the full loop pattern without needing real API keys for weather services or time APIs. The mock implementations return realistic but fixed data.
Create a new file called tool_agent.py (or tool_agent.mjs):
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# --- Tool definitions (OpenAI format) ---
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city. Returns temperature (Celsius), condition, and humidity.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Tokyo' or 'New York'"}
},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports +, -, *, /, **, parentheses.",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression, e.g. '(15 * 7) + 23'"}
},
"required": ["expression"],
},
},
},
{
"type": "function",
"function": {
"name": "get_time",
"description": "Get the current time in a specific timezone. Returns time in HH:MM format.",
"parameters": {
"type": "object",
"properties": {
"timezone": {"type": "string", "description": "Timezone, e.g. 'US/Eastern', 'Asia/Tokyo', 'Europe/London'"}
},
"required": ["timezone"],
},
},
},
]
# --- Mock implementations (what your code runs) ---
MOCK_WEATHER = {
"tokyo": {"temp": 22, "condition": "sunny", "humidity": 45},
"london": {"temp": 14, "condition": "cloudy", "humidity": 72},
"new york": {"temp": 28, "condition": "partly cloudy", "humidity": 60},
}
def run_tool(name: str, args: dict) -> str:
"""Execute a tool and return the result as a JSON string."""
if name == "get_weather":
city = args.get("city", "").lower()
data = MOCK_WEATHER.get(city)
if data:
return json.dumps(data)
return json.dumps({"error": f"City '{args.get('city')}' not found. Available: Tokyo, London, New York"})
elif name == "calculate":
expr = args.get("expression", "")
try:
# Safe eval: only allow math operations
allowed = set("0123456789+-*/.()**% ")
if not all(c in allowed for c in expr):
return json.dumps({"error": f"Invalid characters in expression: {expr}"})
result = eval(expr) # Safe because we validated characters
return json.dumps({"result": result})
except Exception as e:
return json.dumps({"error": f"Calculation failed: {str(e)}"})
elif name == "get_time":
tz = args.get("timezone", "")
# Mock: return a fixed time
times = {"us/eastern": "14:30", "asia/tokyo": "03:30", "europe/london": "19:30"}
time_str = times.get(tz.lower(), "12:00")
return json.dumps({"timezone": tz, "time": time_str})
return json.dumps({"error": f"Unknown tool: {name}"})
print(f"Defined {len(TOOLS)} tools: {[t['name'] for t in TOOLS]}")
print(f"Test: {run_tool('get_weather', {'city': 'Tokyo'})}")
import OpenAI from 'openai';
const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
const TOOLS = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Get current weather for a city. Returns temperature (Celsius), condition, and humidity.',
parameters: {
type: 'object',
properties: { city: { type: 'string', description: "City name, e.g. 'Tokyo'" } },
required: ['city'],
},
},
},
{
type: 'function',
function: {
name: 'calculate',
description: 'Evaluate a mathematical expression. Supports +, -, *, /, **, parentheses.',
parameters: {
type: 'object',
properties: { expression: { type: 'string', description: "Math expression, e.g. '(15 * 7) + 23'" } },
required: ['expression'],
},
},
},
{
type: 'function',
function: {
name: 'get_time',
description: 'Get the current time in a specific timezone.',
parameters: {
type: 'object',
properties: { timezone: { type: 'string', description: "e.g. 'US/Eastern', 'Asia/Tokyo'" } },
required: ['timezone'],
},
},
},
];
const MOCK_WEATHER = {
tokyo: { temp: 22, condition: 'sunny', humidity: 45 },
london: { temp: 14, condition: 'cloudy', humidity: 72 },
'new york': { temp: 28, condition: 'partly cloudy', humidity: 60 },
};
function runTool(name, args) {
if (name === 'get_weather') {
const data = MOCK_WEATHER[args.city?.toLowerCase()];
return data ? JSON.stringify(data) : JSON.stringify({ error: `City '${args.city}' not found` });
}
if (name === 'calculate') {
try {
const result = Function(`"use strict"; return (${args.expression})`)();
return JSON.stringify({ result });
} catch (e) { return JSON.stringify({ error: `Calculation failed: ${e.message}` }); }
}
if (name === 'get_time') {
const times = { 'us/eastern': '14:30', 'asia/tokyo': '03:30', 'europe/london': '19:30' };
return JSON.stringify({ timezone: args.timezone, time: times[args.timezone?.toLowerCase()] || '12:00' });
}
return JSON.stringify({ error: `Unknown tool: ${name}` });
}
console.log(`Defined ${TOOLS.length} tools: ${TOOLS.map(t => t.name).join(', ')}`);
console.log(`Test: ${runTool('get_weather', { city: 'Tokyo' })}`);
Run it: python tool_agent.py (or node tool_agent.mjs)
Troubleshooting Step 1
ModuleNotFoundError: No module named 'openai'— Runpip install openai. Make sure your virtual environment is activated.SyntaxErrorin the TOOLS list — Check for missing commas between tool definitions. Each tool dictionary in the list needs a trailing comma.- Output shows
{"error": "City 'Tokyo' not found"}— The mock lookup uses.lower(). Make sure the MOCK_WEATHER keys are lowercase:"tokyo", not"Tokyo".
Step 2: Build the Agent Loop and Test Single-Tool Calls
Now let's wire up the while finish_reason == "tool_calls" loop and test it with questions that each require exactly one tool. This is the core agent pattern you'll use for the rest of the course. This uses the TOOLS and run_tool() from Step 1.
Add the following to tool_agent.py:
def agent_chat(user_message: str) -> str:
"""Run the full agent loop: send message, handle tool calls, return final answer."""
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="mistral",
tools=TOOLS,
messages=messages,
)
finish_reason = response.choices[0].finish_reason
# If the model is done, return the text
if finish_reason == "stop":
return response.choices[0].message.content or "(no text response)"
# If the model wants to use a tool, execute it
if finish_reason == "tool_calls":
# Append assistant's message (with tool_calls)
messages.append({"role": "assistant", "content": None,
"tool_calls": response.choices[0].message.tool_calls})
# Process each tool call
for tool_call in response.choices[0].message.tool_calls:
name = tool_call.function.name
args = json.loads(tool_call.function.arguments)
print(f" 🔧 Tool call: {name}({json.dumps(args)})")
result = run_tool(name, args)
print(f" 📦 Result: {result[:80]}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
else:
return f"(unexpected finish_reason: {finish_reason})"
# Test with single-tool questions
test_questions = [
"What's the weather like in Tokyo?",
"What is (15 * 7) + 23?",
"What time is it in London?",
"What's the capital of France?", # No tool needed!
]
for q in test_questions:
print(f"\n{'='*50}")
print(f"User: {q}")
answer = agent_chat(q)
print(f"Agent: {answer[:150]}")
async function agentChat(userMessage) {
const messages = [{ role: 'user', content: userMessage }];
while (true) {
const response = await client.chat.completions.create({
model: 'mistral',
tools: TOOLS,
messages,
});
const finishReason = response.choices[0].finish_reason;
if (finishReason === 'stop') {
return response.choices[0].message.content || '(no text response)';
}
if (finishReason === 'tool_calls') {
messages.push({ role: 'assistant', content: null,
tool_calls: response.choices[0].message.tool_calls });
for (const toolCall of response.choices[0].message.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
console.log(` 🔧 Tool call: ${toolCall.function.name}(${JSON.stringify(args)})`);
const result = runTool(toolCall.function.name, args);
console.log(` 📦 Result: ${result.slice(0, 80)}`);
messages.push({ role: 'tool', tool_call_id: toolCall.id, content: result });
}
} else {
return `(unexpected finish_reason: ${finishReason})`;
}
}
}
const testQuestions = [
"What's the weather like in Tokyo?",
"What is (15 * 7) + 23?",
"What time is it in London?",
"What's the capital of France?",
];
for (const q of testQuestions) {
console.log(`\n${'='.repeat(50)}`);
console.log(`User: ${q}`);
const answer = await agentChat(q);
console.log(`Agent: ${answer.slice(0, 150)}`);
}
Run it: python tool_agent.py (or node tool_agent.mjs)
Troubleshooting
- the model uses a tool for the France question — Occasionally the model may try
search_dbor another tool. If this happens consistently, check that your tool descriptions don't overlap with general knowledge questions. The descriptions should be specific about what each tool does. TypeError: 'NoneType' object is not iterable— You looped overmessage.tool_callswhenfinish_reasonwas"stop".tool_callsisNoneunless the model actually requested a tool — guard onfinish_reason == "tool_calls"first.- Infinite loop — Check that you're appending the model's response AND the tool results to
messages. Both are needed for the next iteration.
Verify Everything Works
Run the complete file to execute both steps:
python tool_agent.py # or: node tool_agent.mjs
while finish_reason == "tool_calls" loop. This is the foundational pattern for every agent in this course. In M06, you'll extend this to handle multiple tools in a single turn and manage tool selection for complex queries.
Stretch Goals (Optional)
- Test error handling: ask about a city not in the mock data (e.g., "What's the weather in Mars?") and verify the error flows back through the model gracefully
- Add a
search_database(query)tool with mock data and test a question that requires chaining two tools
You just wrote your first tool call using client.chat.completions.create. This is the RAW approach. As the course progresses this pattern evolves:
- In M12 you wrap it in a ReAct loop.
- In M15B you build a complete multi-agent system.
- In M26 you rebuild it using the Agent SDK in 15 lines instead of 60.
- In M25 you write a spec and Claude Code generates everything.
- In CAPSTONE-7 you build the SAME agent all three ways and compare.
Every approach uses the tool pattern you just learned.
Knowledge Check
Test your understanding of function calling and the tool use loop.
Q1: Who executes the tool — the model or your code?
Q2: What field in the model's response tells you it wants to use a tool?
response.action == "call_function"response.tool_request being non-nullresponse.choices[0].finish_reason == "tool_calls"response.content[0].type == "function_call"finish_reason is "tool_calls", the model is pausing to wait for a tool result. You'll find the tool details in response.choices[0].message.tool_calls.Q3: What's wrong with this tool definition? (Recall from M04: a tool's parameters use JSON Schema — the same format you used for structured output.)
Q4: What should you do when a tool call raises an exception?
Q5: Which message correctly sends a tool's result back to the model (OpenAI-compatible / Ollama)?
{ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "..." }] }{ "role": "tool", "tool_call_id": "call_abc123", "content": "{\"temp\": 22}" }{ "role": "assistant", "tool_call_id": "call_abc123", "content": "..." }{ "role": "function", "name": "get_weather", "content": "..." }role: "tool" carrying the matching tool_call_id and the output as a content string. (Option A is Anthropic's Messages API shape, not the OpenAI-compatible one Ollama uses.)Q6: Which of these is a security risk when implementing tool use?
Module Summary
Key Takeaways
- The model proposes, you execute — the model returns tool calls with arguments; your code runs the actual function. This is the security model.
- Descriptions matter most — the model picks tools based on descriptions. Invest time in making them clear, specific, and complete.
- The while loop pattern — keep sending messages until
finish_reasonis "stop". the model may call multiple tools before finishing. - Return errors as results — when tools fail, send the error as the tool result so the model can adapt. Don't crash.
- This is the chatbot-to-agent moment — the model can now interact with external systems. Every module from here builds on this foundation.
Next Module Preview: M06 — Multi-Tool Orchestration
Now that the model can call one tool, what happens when it needs multiple tools working together? In Module 6, you'll build agents that chain tools, run them in parallel, and dynamically register new tools at runtime.