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

Call the API with Tools

Lesson 5 of 14 Updated November 2, 2026 Developer
On this page
  1. Send Tools to the Mistral API
  2. The First Request
  3. Understanding the Model Response
  4. Parsing the Response and Extracting tool_calls
  5. When the Model Requests Missing Information
  6. Handling the System Message
  7. Complete Code for Steps 2–3
  8. Key Takeaways

Send Tools to the Mistral API

You have defined your functions and their JSON specifications. Now you need to send them to the Mistral API and, more importantly, understand what the model returns. We are now at steps 2 and 3 of the process: the model receives the user request along with the tools and produces a function call—or declines to make one, which is equally instructive.

The First Request

import os
import json
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

# Tools defined in the previous lesson
tools = [
    {
        "type": "function",
        "function": {
            "name": "retrieve_payment_status",
            "description": "Retrieves the status of a payment based on the transaction ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "transaction_id": {
                        "type": "string",
                        "description": "Unique transaction identifier (e.g., T1001)"
                    }
                },
                "required": ["transaction_id"]
            }
        }
    }
]

# User message
messages = [
    {"role": "user", "content": "What is the status of payment T1001?"}
]

# API call with tools
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages,
    tools=tools
)

Understanding the Model Response

The response contains a message object that can take two forms, and your code must handle both from day one. In the first case, the model responds directly because no function is needed: content holds text, and tool_calls is None.

# response.choices[0].message.content contains the text response
# response.choices[0].message.tool_calls is None
print(response.choices[0].message.content)
# → "Hello! How can I help you?"

In the second case, the model wants to call a function. The situation reverses: content is empty or null, and tool_calls carries the payload.

# response.choices[0].message.content is None or empty
# response.choices[0].message.tool_calls contains the calls
message = response.choices[0].message

if message.tool_calls:
    for tool_call in message.tool_calls:
        print(f"Function: {tool_call.function.name}")
        print(f"Arguments: {tool_call.function.arguments}")
        print(f"ID: {tool_call.id}")

Typical output:

Function: retrieve_payment_status
Arguments: {"transaction_id": "T1001"}
ID: call_abc123xyz

Parsing the Response and Extracting tool_calls

Here is the complete code to cleanly extract information from a tool call:

message = response.choices[0].message

if message.tool_calls and len(message.tool_calls) > 0:
    tool_call = message.tool_calls[0]

    # Extract the function name
    function_name = tool_call.function.name

    # Parse JSON arguments into a Python dictionary
    function_params = json.loads(tool_call.function.arguments)

    # Retrieve the unique call identifier
    tool_call_id = tool_call.id

    print(f"The model wants to call: {function_name}")
    print(f"With parameters: {function_params}")
    print(f"Call ID: {tool_call_id}")
else:
    # The model responded directly
    print(f"Response: {message.content}")

Three pieces of information come out of this block, and the third is the one people forget. The function name is used for dispatching, the parsed arguments are used for the call, but the tool_call_id is what allows the model to associate your result with the correct call when you send it back. Lose it, and the conversation turn becomes unrecoverable.

When the Model Requests Missing Information

Sometimes the user does not provide enough information. The model has an elegant way out: do not call a function at all, and ask a clarifying question instead.

messages = [
    {"role": "user", "content": "What is the status of my payment?"}
]

response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages,
    tools=tools
)

message = response.choices[0].message
print(message.content)
# → "I need the transaction ID to check the status.
#     Could you please provide the transaction number?"

This behavior is exactly what you want. The model knows that transaction_id is listed in required; rather than inventing a plausible ID and triggering a request on non-existent data, it yields the floor to the user. This is also the best demonstration of the utility of the required field covered in the previous lesson.

Handling the System Message

The system message lets you frame this behavior rather than hoping for it.

messages = [
    {
        "role": "system",
        "content": "You are a payment tracking assistant. "
                   "Respond in English. "
                   "Use the available tools to answer questions "
                   "about transactions. If the user does not provide "
                   "the transaction ID, ask for it politely."
    },
    {
        "role": "user",
        "content": "What is the status of payment T1001?"
    }
]

Read it line by line: the first sentence sets the role, the second sets the tone and language of the assistant, the third specifies when to use functions, and the last gives an explicit instruction for an edge case—missing information. These are the three use cases for which a system message is recommended, and they suffice most of the time.

Complete Code for Steps 2–3

Here is the complete, reusable code for sending a request with tools. It encapsulates everything above and normalizes the response into a dictionary with two forms, saving you from scattering if message.tool_calls checks throughout your application.

import os
import json
from mistralai.client import Mistral

def call_mistral_with_tools(
    messages: list,
    tools: list,
    model: str = "mistral-large-latest",
    tool_choice: str = "auto"
) -> dict:
    """Sends a request to Mistral with tools and returns the parsed response."""
    client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

    response = client.chat.complete(
        model=model,
        messages=messages,
        tools=tools,
        tool_choice=tool_choice
    )

    message = response.choices[0].message

    if message.tool_calls and len(message.tool_calls) > 0:
        return {
            "type": "tool_call",
            "message": message,
            "tool_calls": [
                {
                    "id": tc.id,
                    "function_name": tc.function.name,
                    "arguments": json.loads(tc.function.arguments)
                }
                for tc in message.tool_calls
            ]
        }

    return {
        "type": "text",
        "message": message,
        "content": message.content
    }

Note that the list comprehension iterates over all tool_calls, not just the first one. The model can produce several at once; a later lesson will cover that, but your utility function is already ready to handle them.

Key Takeaways

  • Send tools and messages to client.chat.complete() to enable function calling
  • Always check if message.tool_calls exists before attempting to parse
  • The tool_call_id is mandatory for sending the result back to the model
  • Arguments are a JSON string that must be parsed with json.loads()
  • The model may choose to respond directly if it does not need a function or if information is missing