Skip to main content
Lesson 6 of 14 · Function Calling and Tool Use

Execute and Return the Result

Lesson 6 of 14 Updated November 2, 2026 Developer
On this page
  1. The Step You Fully Control
  2. Execute the Function on the Client Side
  3. Build the Tool Message
  4. Send the Result and Get the Final Response
  5. The Complete Flow in One Block
  6. Message Structure at Each Step
  7. Watch the Content Format
  8. Key Takeaways

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
}
FieldRole
role"tool"—identifies the message as a function result
nameThe name of the executed function
contentThe result as a JSON string
tool_call_idThe 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 matching tool_call_id
  • The content of 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