Step 3 · The model decides
What this step adds: tool calling. The model reads a description of your function and requests a call — with the right argument, extracted from plain English.
The new idea
Section titled “The new idea”Tool calling: you send the model a description of your function alongside the conversation. Instead of replying with text, the model can reply with a structured request: “call this function, with these arguments.”
The critical detail — and it’s a security fact, not trivia: the model cannot run anything. It has no hands. It emits a request. Your code decides whether to honour it.
The code
Section titled “The code”Create step3.py:
import ollama
def get_transaction(txn_id): records = { "T-1002": {"amount": 4200, "country": "Nigeria", "card_present": False}, } return records[txn_id]
schema = { "type": "function", "function": { "name": "get_transaction", "description": "Look up a transaction's amount, country and card_present flag by its id.", "parameters": { "type": "object", "properties": {"txn_id": {"type": "string"}}, "required": ["txn_id"], }, },}
response = ollama.chat( model="qwen2.5:3b", messages=[{"role": "user", "content": "Look up transaction T-1002 for me."}], tools=[schema],)
print(response.message.tool_calls)The schema dictionary is the interesting part. It’s everything the model will ever know about your function: a name, a one-sentence description, and the shape of the arguments (one required string called txn_id). The model never sees your function’s body — not the records dict, not the data. The description is the interface; the body is private.
Run it
Section titled “Run it”uv run step3.pyExpect something like:
[ToolCall(function=Function(name='get_transaction', arguments={'txn_id': 'T-1002'}))]Why this is the important moment
Section titled “Why this is the important moment”Read that output slowly. You never told the model which function to use. You never told it what argument to pass. It read “Look up transaction T-1002 for me”, matched it against your description, and produced a well-formed request with T-1002 extracted from the sentence.
No if statement of yours made that choice. That’s the handover — a decision made at runtime by something you didn’t write.
Two things to notice before moving on:
- The function still hasn’t run. The model asked; nothing answered. Your
recordsdict was never touched. There is a gap between requesting and executing, and your code owns that gap. Every permission check, every approval, every audit entry in a production agent lives inside it. - The description did the routing. If your description had been vague — “gets data” — the model would have had nothing to match against. When an agent picks the wrong tool, the bug is usually in the English, not the Python.
What’s still wrong
Section titled “What’s still wrong”The request is sitting there unanswered. Closing the loop — running the tool, feeding the result back, letting the model finish its thought — turns this into an agent. That’s step 4.