Cas Concret : Tracker de Paiements
Mis à jour le 29 juillet 2026
Un projet complet de A à Z
Les leçons précédentes ont isolé chaque mécanisme : la définition des tools, le cycle d’appel, les erreurs, la sécurité. Il est temps de les assembler. Vous allez construire un tracker de paiements conversationnel complet, dans lequel l’utilisateur pose ses questions en langage naturel et le système interroge un DataFrame Pandas via le function calling. Ce cas d’usage est directement inspiré de la documentation officielle Mistral, et il a l’avantage de tenir en une centaine de lignes tout en exposant les cinq briques d’une application réelle : les données, les fonctions métier, les spécifications JSON, le dispatch, la boucle.
Les données
Un DataFrame de cinq transactions suffit à couvrir les situations intéressantes : un client possédant plusieurs paiements, un paiement en attente, un paiement en échec.
import pandas as pd
import json
import os
from mistralai import Mistral
from functools import partial
# DataFrame de transactions
data = {
"transaction_id": ["T1001", "T1002", "T1003", "T1004", "T1005"],
"customer_id": ["C001", "C002", "C003", "C002", "C001"],
"payment_amount": [125.50, 89.99, 350.00, 45.00, 210.75],
"payment_date": ["2026-03-15", "2026-03-16", "2026-03-17", "2026-03-17", "2026-03-18"],
"payment_status": ["payé", "payé", "en_attente", "payé", "échoué"]
}
df = pd.DataFrame(data)
Les fonctions métier
Quatre fonctions couvrent les besoins typiques d’un suivi de paiements. Les trois premières travaillent sur une transaction et se distinguent par leur granularité : le statut seul, la date seule, ou le détail complet. Cette redondance apparente est délibérée — elle permet au modèle de ne demander que ce dont il a besoin, et donc de renvoyer moins de données inutiles dans le contexte. La quatrième change d’axe et raisonne par client.
Notez le contrat commun : toutes retournent une chaîne JSON, y compris en cas d’échec, et toutes gèrent l’absence de l’identifiant demandé plutôt que de laisser Pandas lever une exception sur un index vide.
def retrieve_payment_status(df: pd.DataFrame, transaction_id: str) -> str:
"""Récupère le statut d'un paiement."""
if transaction_id in df["transaction_id"].values:
row = df[df["transaction_id"] == transaction_id].iloc[0]
return json.dumps({
"transaction_id": transaction_id,
"status": row["payment_status"]
})
return json.dumps({"error": f"Transaction {transaction_id} introuvable"})
def retrieve_payment_date(df: pd.DataFrame, transaction_id: str) -> str:
"""Récupère la date d'un paiement."""
if transaction_id in df["transaction_id"].values:
row = df[df["transaction_id"] == transaction_id].iloc[0]
return json.dumps({
"transaction_id": transaction_id,
"date": row["payment_date"]
})
return json.dumps({"error": f"Transaction {transaction_id} introuvable"})
def retrieve_payment_details(df: pd.DataFrame, transaction_id: str) -> str:
"""Récupère tous les détails d'un paiement."""
if transaction_id in df["transaction_id"].values:
row = df[df["transaction_id"] == transaction_id].iloc[0]
return json.dumps({
"transaction_id": transaction_id,
"customer_id": row["customer_id"],
"amount": float(row["payment_amount"]),
"date": row["payment_date"],
"status": row["payment_status"]
})
return json.dumps({"error": f"Transaction {transaction_id} introuvable"})
def list_customer_transactions(df: pd.DataFrame, customer_id: str) -> str:
"""Liste toutes les transactions d'un client."""
customer_df = df[df["customer_id"] == customer_id]
if customer_df.empty:
return json.dumps({"error": f"Client {customer_id} introuvable"})
transactions = []
for _, row in customer_df.iterrows():
transactions.append({
"transaction_id": row["transaction_id"],
"amount": float(row["payment_amount"]),
"date": row["payment_date"],
"status": row["payment_status"]
})
return json.dumps({"customer_id": customer_id, "transactions": transactions})
Le float(row["payment_amount"]) mérite une seconde d’attention : sans cette conversion, json.dumps échoue sur les types NumPy. C’est le genre de détail qui ne se voit qu’à l’exécution.
Les spécifications JSON
Le modèle ne voit rien du code ci-dessus. Il ne connaît que ces descriptions, et la mention explicite des statuts possibles dans retrieve_payment_status comme le rappel du format T1001 dans chaque paramètre sont ce qui lui permet de choisir la bonne fonction et d’extraire correctement l’identifiant.
tools = [
{
"type": "function",
"function": {
"name": "retrieve_payment_status",
"description": "Récupère le statut d'un paiement (payé, en_attente, échoué)",
"parameters": {
"type": "object",
"properties": {
"transaction_id": {
"type": "string",
"description": "Identifiant de la transaction (ex: T1001)"
}
},
"required": ["transaction_id"]
}
}
},
{
"type": "function",
"function": {
"name": "retrieve_payment_date",
"description": "Récupère la date d'un paiement",
"parameters": {
"type": "object",
"properties": {
"transaction_id": {
"type": "string",
"description": "Identifiant de la transaction (ex: T1001)"
}
},
"required": ["transaction_id"]
}
}
},
{
"type": "function",
"function": {
"name": "retrieve_payment_details",
"description": "Récupère tous les détails d'un paiement : montant, date, statut, client",
"parameters": {
"type": "object",
"properties": {
"transaction_id": {
"type": "string",
"description": "Identifiant de la transaction (ex: T1001)"
}
},
"required": ["transaction_id"]
}
}
},
{
"type": "function",
"function": {
"name": "list_customer_transactions",
"description": "Liste toutes les transactions d'un client donné",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "Identifiant du client (ex: C001)"
}
},
"required": ["customer_id"]
}
}
}
]
Le dictionnaire de dispatch
Un problème se pose ici : vos fonctions attendent df en premier argument, mais le modèle ne le fournira jamais — il ignore son existence, et c’est très bien ainsi. functools.partial résout élégamment le cas en pré-liant le DataFrame, de sorte que chaque entrée du dictionnaire devient appelable avec les seuls arguments que le modèle produit.
available_functions = {
"retrieve_payment_status": partial(retrieve_payment_status, df),
"retrieve_payment_date": partial(retrieve_payment_date, df),
"retrieve_payment_details": partial(retrieve_payment_details, df),
"list_customer_transactions": partial(list_customer_transactions, df),
}
La boucle de conversation
Le message système fixe le rôle, la langue et le formatage attendu des montants. La fonction chat orchestre le reste, et sa structure mérite d’être lue avec attention : le while message.tool_calls traite indifféremment un appel unique, plusieurs appels parallèles, ou un enchaînement où le modèle demande une nouvelle fonction après avoir vu le premier résultat. La boucle interne exécute tous les appels du tour courant, la boucle externe relance le modèle tant qu’il en réclame.
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
model = "mistral-large-latest"
messages = [
{
"role": "system",
"content": "Vous êtes un assistant de suivi de paiements. "
"Répondez en français. Utilisez les outils disponibles "
"pour répondre aux questions sur les transactions. "
"Formatez les montants en euros avec 2 décimales."
}
]
def chat(user_input: str) -> str:
"""Tour de conversation complet avec gestion des tool_calls."""
messages.append({"role": "user", "content": user_input})
response = client.chat.complete(
model=model, messages=messages, tools=tools
)
message = response.choices[0].message
# Boucle tant que le modèle veut appeler des fonctions
while message.tool_calls:
messages.append(message)
for tool_call in message.tool_calls:
fn_name = tool_call.function.name
fn_params = json.loads(tool_call.function.arguments)
if fn_name in available_functions:
result = available_functions[fn_name](**fn_params)
else:
result = json.dumps({"error": f"Fonction {fn_name} inconnue"})
messages.append({
"role": "tool",
"name": fn_name,
"content": result,
"tool_call_id": tool_call.id
})
response = client.chat.complete(
model=model, messages=messages, tools=tools
)
message = response.choices[0].message
messages.append(message)
return message.content
Ce que la conversation révèle
Testez maintenant sur quatre tours consécutifs. Le deuxième est le plus instructif : « cette transaction » ne contient aucun identifiant, et pourtant le modèle appelle retrieve_payment_details avec T1001. Il a résolu l’anaphore grâce à l’historique conservé dans messages, ce qui serait impossible si vous réinitialisiez la liste à chaque appel. Le quatrième tour montre l’autre facette : aucune fonction ne cherche les paiements en échec, mais le modèle interroge le client concerné et filtre lui-même le résultat.
# Tour 1
print(chat("Quel est le statut du paiement T1001 ?"))
# -> "Le paiement T1001 a le statut : payé."
# Tour 2
print(chat("Donne-moi tous les détails de cette transaction."))
# -> "Voici les détails du paiement T1001 :
# - Client : C001
# - Montant : 125,50 euros
# - Date : 15 mars 2026
# - Statut : payé"
# Tour 3
print(chat("Quelles sont toutes les transactions du client C002 ?"))
# -> "Le client C002 a 2 transactions :
# - T1002 : 89,99 euros (payé, 16 mars 2026)
# - T1004 : 45,00 euros (payé, 17 mars 2026)"
# Tour 4
print(chat("Y a-t-il des paiements en échec ?"))
# -> "Oui, le paiement T1005 du client C001 (210,75 euros) a échoué."
Le formatage des montants avec virgule décimale et des dates en toutes lettres ne vient d’aucune ligne de code : il découle de la consigne système. C’est le rappel le plus utile de ce chapitre — la moitié de la qualité perçue se joue dans les descriptions et le message système, pas dans la mécanique d’appel.
Points clés à retenir
- Un tracker de paiements complet nécessite 4 fonctions couvrant les cas d’usage principaux
- Le
functools.partiallie le DataFrame aux fonctions pour un dispatch propre - La boucle
while message.tool_callsgère automatiquement les appels séquentiels et parallèles - Le message système guide le modèle sur le ton, la langue et le formatage
- Testez avec des conversations multi-tour pour vérifier que le modèle maintient le contexte