Execute and Return the Result
On this page
The Step You Fully Control
You’ve received a tool_call from the model. It tells you which function to call and with what arguments—nothing more. Now your code takes over: it executes the function, retrieves the result, and sends it back to the model as a message with the role tool. This lesson covers that entire round trip. This is the only moment in the cycle where you make all the decisions.
Execute the Function on the Client Side
Let’s revisit the dispatch dictionary from Lesson 4 and connect it to the model’s response.
import json
from functools import partial
# Functions already defined
available_functions = {
"retrieve_payment_status": partial(retrieve_payment_status, df),
"retrieve_payment_date": partial(retrieve_payment_date, df),
}
# The model returned a tool_call
tool_call = response.choices[0].message.tool_calls[0]
function_name = tool_call.function.name
function_params = json.loads(tool_call.function.arguments)
# Execute the function
if function_name in available_functions:
result = available_functions[function_name](**function_params)
print(f"Result: {result}")
# → '{"status": "paid"}'
else:
result = json.dumps({"error": f"Function {function_name} not available"})
Two mechanisms are at work here. The first is **function_params, which unpacks the dictionary from the JSON into named arguments: if the model generated {"transaction_id": "T1001"}, the call is exactly equivalent to retrieve_payment_status(transaction_id="T1001"). The second is the else block. It’s less flashy but essential. A model might hallucinate a function name that doesn’t exist in your dispatch; without this check, your app raises a KeyError and the conversation stops dead. With it, the error becomes data the model can handle gracefully.
Build the Tool Message
The result must go back to the model in a specific format—a message with the role "tool":
tool_message = {
"role": "tool",
"name": function_name,
"content": result, # Must be a string
"tool_call_id": tool_call.id
}
| Field | Role |
|---|---|
role | "tool"—identifies the message as a function result |
name | The name of the executed function |
content | The result as a JSON string |
tool_call_id | The ID of the original tool_call—lets the model match the result to the right call |
Send the Result and Get the Final Response
The model has no memory from one call to the next: it doesn’t remember asking you for anything. So you must rebuild the entire conversation for it, including its own request for a tool call, before presenting the result.
# 1. Add the assistant message (with the tool_call)
messages.append(response.choices[0].message)
# 2. Add the function result
messages.append({
"role": "tool",
"name": function_name,
"content": result,
"tool_call_id": tool_call.id
})
# 3. Call the model again with the full context
final_response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
tools=tools
)
# 4. Display the final response
print(final_response.choices[0].message.content)
# → "Payment T1001 status is: paid."
The order of the two append operations is non-negotiable: the assistant message carrying the tool_call must come before the tool message that answers it. Swapping them creates an inconsistent history that the API rejects.
The Complete Flow in One Block
Here’s the entire process, from the user’s question to the final answer:
import os
import json
from mistralai.client import Mistral
from functools import partial
# Configuration
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
model = "mistral-large-latest"
# Functions and tools (defined earlier)
available_functions = {
"retrieve_payment_status": partial(retrieve_payment_status, df),
"retrieve_payment_date": partial(retrieve_payment_date, df),
}
# Steps 1-2: User question + API call
messages = [
{"role": "user", "content": "What is the status of payment T1001?"}
]
response = client.chat.complete(
model=model, messages=messages, tools=tools
)
# Steps 3-4: Extract the tool_call and execute
message = response.choices[0].message
if message.tool_calls:
tool_call = message.tool_calls[0]
function_name = tool_call.function.name
function_params = json.loads(tool_call.function.arguments)
# Client-side execution
result = available_functions[function_name](**function_params)
# Step 5: Send the result back to the model
messages.append(message)
messages.append({
"role": "tool",
"name": function_name,
"content": result,
"tool_call_id": tool_call.id
})
final_response = client.chat.complete(
model=model, messages=messages, tools=tools
)
print(final_response.choices[0].message.content)
else:
print(message.content)
Message Structure at Each Step
It’s worth inspecting the messages list once the cycle finishes, because it’s the best way to debug misbehaving function calling. It contains four messages.
# Message 1: The user's question
{"role": "user", "content": "What is the status of payment T1001?"}
# Message 2: The model's response with the tool_call
{"role": "assistant", "tool_calls": [{"id": "call_abc123", ...}]}
# Message 3: The function result
{"role": "tool", "name": "retrieve_payment_status",
"content": '{"status": "paid"}', "tool_call_id": "call_abc123"}
# Message 4 (implicit): The model's final response
# → "Payment T1001 status is: paid."
Follow the ID call_abc123: it’s born in Message 2 and reappears in Message 3. That thread links the request to its answer. This structure is the fundamental pattern of function calling; every variation you encounter later, whether multi-turn or parallel, just repeats or duplicates it.
Watch the Content Format
One error shows up more often than all the others, and it comes down to the type of the content field: it must always be a string.
# Correct — JSON string
{"content": '{"status": "paid"}'}
# Correct — plain text
{"content": "The status is paid"}
# Incorrect — Python dictionary (will cause an error)
{"content": {"status": "paid"}}
The difference between the first and third examples lies in the quotes wrapping the JSON. If your function returns a dictionary, convert it with json.dumps() before sending—that’s precisely why the functions in Lesson 4 all ended with a json.dumps().
Key Takeaways
- Function execution happens on the client side, in your code—never by the model
- The result is sent back in a message with role
"tool"and the matchingtool_call_id - The
contentof the tool message must be a string (a JSON string) - After sending the result, you call
chat.complete()again so the model can formulate the final response - Always verify that the requested function exists in your dispatch dictionary