# Parameters
BATCH_MODE = "true"Navigation : Index | << Précédent | Suivant >>
Function Calling : Connecter les LLMs au Monde Réel
Dans ce notebook, nous explorons le Function Calling (appel de fonctions), qui permet aux modèles d’interagir avec des systèmes externes comme des APIs, bases de données, ou services web.
Objectifs : - Comprendre la structure des Tools dans l’API OpenAI - Maîtriser tool_choice (auto, required, none) - Exécuter des appels groupés en un seul tour - Construire des boucles agentiques
Prérequis : Notebook 1 (OpenAI Intro), Notebook 3 (Structured Outputs)
Durée estimée : 60 minutes
# Verification des dependances (import guards)
try:
from openai import OpenAI
openai_AVAILABLE = True
except ImportError:
openai_AVAILABLE = False
print(f'WARNING: openai non installe. Installez avec: pip install openai')
try:
from dotenv import load_dotenv
dotenv_AVAILABLE = True
except ImportError:
dotenv_AVAILABLE = False
print(f'WARNING: python-dotenv non installe. Installez avec: pip install python-dotenv')
# Installation et configuration
import os
from pathlib import Path
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
%pip install -q openai python-dotenv
import json
from openai import OpenAI
from dotenv import load_dotenv
from datetime import datetime
import random
# Chargement robuste de la configuration .env
from dotenv import load_dotenv
# Recherche du .env dans tous les parents (pour Papermill qui change le cwd)
current_path = Path.cwd()
env_loaded = False
for _ in range(10):
env_path = current_path / ".env"
if env_path.exists():
load_dotenv(env_path)
print(f".env charge depuis: {env_path.name}")
env_loaded = True
break
if current_path.name == "GenAI" or len(current_path.parts) <= 1:
break
current_path = current_path.parent
if not env_loaded:
print("WARNING: .env non trouve, utilisation variables environnement")
client = OpenAI()
# Charger le modèle depuis .env ou utiliser gpt-5-mini par défaut
DEFAULT_MODEL = os.getenv("OPENAI_MODEL", "gpt-5-mini")
BATCH_MODE = os.getenv("BATCH_MODE", "false").lower() == "true"
print("Client OpenAI initialisé !")
print(f"Modèle par défaut: {DEFAULT_MODEL}")
print(f"Mode batch: {BATCH_MODE}")Note: you may need to restart the kernel to use updated packages.
.env charge depuis: .env
Client OpenAI initialisé !
Modèle par défaut: gpt-5-mini
Mode batch: False
La configuration de l’environnement est terminée. La prochaine cellule définit les variables globales et initialise le client OpenAI qui sera utilisé dans toutes les démonstrations suivantes.
1. Architecture des Tools
Les Tools permettent au modèle d’appeler des fonctions définies par l’utilisateur. La structure d’un tool suit le format :
{
"type": "function",
"function": {
"name": "nom_fonction",
"description": "Description claire pour aider le modèle à choisir",
"parameters": {
"type": "object",
"properties": { ... }, // JSON Schema
"required": [...] // Paramètres obligatoires
}
}
}Points clés : - La description est cruciale : elle guide le modèle pour choisir le bon outil - Les parameters utilisent JSON Schema pour validation - Le modèle génère les arguments, l’utilisateur exécute la fonction
# Définition d'un outil météo
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Obtenir la météo actuelle d'une ville",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "Nom de la ville (ex: Paris, Lyon)"
},
"unit": {
"type": "string",
"enum": ["fahrenheit", "celsius"],
"description": "Unité de température"
}
},
"required": ["location"]
}
}
}]
print("Tool défini:", tools[0]["function"]["name"])
print("\nStructure du tool:")
print(json.dumps(tools[0], indent=2, ensure_ascii=False))Tool défini: get_weather
Structure du tool:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Obtenir la météo actuelle d'une ville",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "Nom de la ville (ex: Paris, Lyon)"
},
"unit": {
"type": "string",
"enum": [
"fahrenheit",
"celsius"
],
"description": "Unité de température"
}
},
"required": [
"location"
]
}
}
}
Interprétation de la structure du tool
L’output affiche la structure complète du tool get_weather au format JSON. Observons les éléments clés :
| Champ | Valeur | Rôle |
|---|---|---|
type |
"function" |
Indique qu’il s’agit d’un appel de fonction |
function.name |
"get_weather" |
Identifiant unique de la fonction |
function.description |
“Obtenir la météo…” | Utilisée par le modèle pour décider quand appeler cette fonction |
parameters.properties |
location, unit |
Définit les arguments que le modèle doit générer |
parameters.required |
["location"] |
location est obligatoire, unit est optionnel |
Point critique : La description est ce qui permet au modèle de choisir le bon outil parmi plusieurs. Plus elle est précise, meilleures seront les décisions du modèle.
Analogie : C’est comme donner une boîte à outils à un assistant - la description de chaque outil (marteau, tournevis) l’aide à choisir le bon pour chaque tâche.
# Implémentation de la fonction météo
def get_weather(location: str, unit: str = "celsius") -> str:
"""Simule une API météo (en production, appeler une vraie API comme OpenWeatherMap)"""
# Données simulées pour démonstration
weather_data = {
"Paris": {"temp_c": 18, "condition": "nuageux", "humidity": 65},
"Lyon": {"temp_c": 22, "condition": "ensoleillé", "humidity": 45},
"Marseille": {"temp_c": 26, "condition": "ensoleillé", "humidity": 55},
}
# Fallback pour villes non répertoriées
data = weather_data.get(location, {"temp_c": 20, "condition": "variable", "humidity": 50})
# Conversion température
temp = data["temp_c"] if unit == "celsius" else data["temp_c"] * 9/5 + 32
unit_symbol = "°C" if unit == "celsius" else "°F"
return json.dumps({
"location": location,
"temperature": f"{temp}{unit_symbol}",
"condition": data["condition"],
"humidity": f"{data['humidity']}%"
}, ensure_ascii=False)
# Test de la fonction
print("Test de la fonction get_weather:")
print(get_weather("Paris", "celsius"))
print("\nTest avec Fahrenheit:")
print(get_weather("Lyon", "fahrenheit"))
print("\nTest ville inconnue (fallback):")
print(get_weather("Bordeaux"))Test de la fonction get_weather:
{"location": "Paris", "temperature": "18°C", "condition": "nuageux", "humidity": "65%"}
Test avec Fahrenheit:
{"location": "Lyon", "temperature": "71.6°F", "condition": "ensoleillé", "humidity": "45%"}
Test ville inconnue (fallback):
{"location": "Bordeaux", "temperature": "20°C", "condition": "variable", "humidity": "50%"}
Interprétation des tests
Test 1 - Paris en Celsius : Retourne des données simulées pour Paris (18°C, nuageux, 65% humidité). En production, cette fonction appellerait une API réelle comme OpenWeatherMap.
Test 2 - Lyon en Fahrenheit : Conversion automatique : 22°C × 9/5 + 32 = 71.6°F. La fonction gère l’unité de température de manière flexible.
Test 3 - Bordeaux (fallback) : La ville n’existe pas dans weather_data, donc la fonction retourne des valeurs par défaut (20°C, variable). Ce mécanisme de fallback évite les erreurs et garantit une réponse.
Points clés : - Retour en JSON structuré (facilite le parsing par le modèle) - Gestion des cas limites (villes inconnues) - Paramètres optionnels avec valeurs par défaut
Production : Remplacer la simulation par une vraie API et ajouter un cache pour réduire les appels.
2. Premier Appel avec Tool
Lorsqu’on passe des tools à l’API, le modèle peut décider d’appeler une fonction au lieu de répondre directement. Le paramètre tool_choice contrôle ce comportement :
auto(défaut) : Le modèle décide s’il doit appeler un outilrequired: Le modèle DOIT appeler au moins un outilnone: Le modèle ne peut PAS appeler d’outils{"type": "function", "function": {"name": "..."}}: Forcer un outil spécifique
# Premier appel avec tool
# messages = [{"role": "user", "content": "Quelle est la météo à Paris aujourd'hui?"}]
messages = [{"role": "user", "content": "What's the weather forecast for tomorrow in NYC?"}]
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=messages,
tools=tools,
tool_choice="auto" # Le modèle décide s'il doit appeler un outil
)
assistant_message = response.choices[0].message
print("Finish reason:", response.choices[0].finish_reason)
# Examiner les tool_calls ou afficher la réponse directe
if assistant_message.tool_calls:
print("\nLe modèle a décidé d'appeler un outil !")
print("\nContenu de la réponse du modèle:")
print(assistant_message.content)
tool_call = assistant_message.tool_calls[0]
print("\nTool call détaillé:")
print(f" ID: {tool_call.id}")
print(f" Fonction: {tool_call.function.name}")
print(f" Arguments: {tool_call.function.arguments}")
else:
print("\nLe modèle a répondu directement sans appeler d'outil.")
print("\nContenu de la réponse du modèle:")
print(assistant_message.content)Finish reason: stop
Le modèle a répondu directement sans appeler d'outil.
Contenu de la réponse du modèle:
Do you mean New York City, NY (USA)? I can fetch current conditions right away, but my available weather tool returns current weather, not multi-day forecasts. If you want tomorrow’s forecast specifically I can either:
- try to pull a forecast from an online source (I’ll need your permission to fetch it), or
- give a quick general expectation based on typical patterns and recent trends.
Which would you prefer? Also which temperature unit do you want: Fahrenheit (°F) or Celsius (°C)?
Interprétation de la réponse
Avec tool_choice: "auto", le modèle décide lui-même d’appeler un outil ou de répondre directement. La sortie fraîche de la cellule précédente rend cette décision observable sans la supposer :
finish_reason: tool_callset une listetool_callsnon vide : le modèle demande l’exécution d’un outil ;finish_reason: stopet aucuntool_calls: le modèle répond directement, et son texte est affiché par la brancheelse.
Leçon :
tool_choice: "auto"ne garantit pas un appel d’outil. Le diagnostic imprimé doit donc dépendre demessage.tool_calls, jamais d’une hypothèse sur la requête. Pour garantir un appel, utiliseztool_choice: "required".
Structure d’un tool_call (lorsque le modèle décide d’appeler un outil) :
| Champ | Valeur | Signification |
|---|---|---|
id |
call_XXX |
Identifiant unique pour lier la réponse de l’outil |
function.name |
get_weather |
Fonction choisie par le modèle |
function.arguments |
{"location": "...", "unit": "..."} |
Arguments générés (JSON) |
Important : le modèle n’exécute pas la fonction lui-même. Il retourne uniquement les instructions (nom + arguments) ; c’est l’application qui exécute la fonction et réinjecte le résultat.
3. Flux Complet : Boucle Agentique
Le flux typique d’une conversation avec function calling :
- Utilisateur envoie un message
- Modèle retourne
tool_calls(ou répond directement si pas besoin d’outils) - Application exécute les fonctions demandées
- Application injecte les résultats avec
rôle="tool" - Modèle utilise les résultats pour formuler une réponse finale
- Retour à l’étape 2 si le modèle veut appeler d’autres fonctions
Cette boucle est appelée boucle agentique car le modèle agit comme un agent autonome qui décide quand et quels outils utiliser.
def run_conversation(user_message: str, tools: list, available_functions: dict):
"""Exécute une conversation complète avec appels de fonctions"""
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=messages,
tools=tools,
tool_choice="auto"
)
assistant_message = response.choices[0].message
messages.append(assistant_message)
# Si pas d'appel de fonction, on a terminé
if not assistant_message.tool_calls:
return assistant_message.content
# Exécuter chaque fonction appelée
for tool_call in assistant_message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f" Appel: {function_name}({function_args})")
if function_name in available_functions:
result = available_functions[function_name](**function_args)
else:
result = json.dumps({"error": f"Fonction {function_name} non trouvée"})
# Injecter le résultat dans la conversation
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
# Test de la boucle agentique
available_functions = {"get_weather": get_weather}
question = "Quel temps fait-il à Lyon?"
print(f"Question: {question}\n")
result = run_conversation(question, tools, available_functions)
print(f"\nRéponse finale: {result}")Question: Quel temps fait-il à Lyon?
Appel: get_weather({'location': 'Lyon', 'unit': 'celsius'})
Réponse finale: Il fait 22°C à Lyon, ensoleillé, humidité 45%. Voulez-vous la prévision pour les prochains jours ou d'autres services (alerte météo, conseils vestimentaires) ?
Analyse du flux de la boucle agentique
Étapes exécutées :
- Tour 1 : Modèle reçoit “Quel temps fait-il à Lyon?”
- Décision : Appeler
get_weather(location="Lyon") - Retour fonction :
{"location": "Lyon", "temperature": "22°C", "condition": "ensoleillé", ...}
- Décision : Appeler
- Tour 2 : Modèle reçoit le résultat de la fonction
- Décision : Formuler une réponse en langage naturel
- Pas de
tool_calls→ boucle terminée
Points clés :
- Le modèle décide automatiquement quand arrêter (pas de
tool_calls→ sortie) - L’ajout de
rôle="tool"dans les messages permet au modèle de “voir” les résultats - Ce pattern est à la base des agents autonomes (AutoGPT, BabyAGI, etc.)
Note : Une boucle mal conçue peut tourner indéfiniment. Toujours prévoir une limite
max_iterations.
4. Appels de Fonctions Multiples et Groupés en un seul tour
Le modèle peut appeler plusieurs fonctions dans une seule réponse. Ceci est utile pour : - Répondre à des questions complexes nécessitant plusieurs sources de données - Exécuter plusieurs actions groupées en un seul tour - Optimiser le nombre d’aller-retours API
Ajoutons plus de fonctions pour démontrer ce comportement.
# Ajouter plus de fonctions
def get_time(timezone: str = "Europe/Paris") -> str:
"""Retourne l'heure actuelle (simulation)"""
return json.dumps({
"timezone": timezone,
"time": datetime.now().strftime("%H:%M:%S"),
"date": datetime.now().strftime("%Y-%m-%d")
})
def create_reminder(title: str, time: str, priority: str = "normal") -> str:
"""Crée un rappel (simulation)"""
return json.dumps({
"status": "created",
"reminder": {"title": title, "time": time, "priority": priority}
}, ensure_ascii=False)
# Définir les nouveaux tools
extended_tools = tools + [
{
"type": "function",
"function": {
"name": "get_time",
"description": "Obtenir l'heure et la date actuelles pour un fuseau horaire donné",
"parameters": {
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "Fuseau horaire (ex: Europe/Paris, America/New_York)"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "create_reminder",
"description": "Créer un rappel pour une tâche future",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Titre du rappel"
},
"time": {
"type": "string",
"description": "Heure du rappel (ex: '09:00', 'demain 14h')"
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high"],
"description": "Priorité du rappel"
}
},
"required": ["title", "time"]
}
}
}
]
all_functions = {
"get_weather": get_weather,
"get_time": get_time,
"create_reminder": create_reminder
}
print("Fonctions disponibles:", ", ".join(all_functions.keys()))Fonctions disponibles: get_weather, get_time, create_reminder
Analyse des nouvelles fonctions
Nous avons ajouté deux nouvelles fonctions pour démontrer les appels multiples :
| Fonction | Objectif | Paramètres | Cas d’usage |
|---|---|---|---|
get_time() |
Obtenir l’heure actuelle | timezone (optionnel) |
Planification, rappels temporels |
create_reminder() |
Créer un rappel | title, time, priority |
Gestion de tâches, productivité |
Points techniques : - Les deux fonctions retournent du JSON structuré (facilite le parsing par le modèle) - priority utilise un enum pour contraindre les valeurs possibles (low/normal/high) - Les paramètres optionnels ont des valeurs par défaut sensibles
Avec ces 3 fonctions (météo, heure, rappel), le modèle peut maintenant orchestrer des scénarios complexes combinant plusieurs sources de données et actions.
# Test avec requête complexe nécessitant plusieurs appels
complex_question = (
"Quelle heure est-il à Paris et quel temps fait-il? "
"Crée aussi un rappel pour demain 9h: 'Réunion équipe'"
)
print("Question complexe:")
print(complex_question)
print("\nAppels de fonctions (groupés en un seul tour):")
result = run_conversation(complex_question, extended_tools, all_functions)
print("\nRéponse finale:")
print(result)Question complexe:
Quelle heure est-il à Paris et quel temps fait-il? Crée aussi un rappel pour demain 9h: 'Réunion équipe'
Appels de fonctions (groupés en un seul tour):
Appel: get_time({'timezone': 'Europe/Paris'})
Appel: get_weather({'location': 'Paris', 'unit': 'celsius'})
Appel: create_reminder({'title': 'Réunion équipe', 'time': 'demain 09:00', 'priority': 'normal'})
Réponse finale:
À Paris il est actuellement 00:05:16 (16 septembre 2026, fuseau Europe/Paris).
Météo à Paris : 18°C, nuageux — humidité ~65%.
Le rappel "Réunion équipe" a été créé pour demain à 09:00 (17 septembre 2026) — priorité : normale.
Souhaitez-vous modifier l'heure, la priorité ou ajouter une alerte sonore/lieu ?
Analyse de l’exécution groupée en un seul tour
Observation clé : Le modèle a effectué 3 appels de fonction groupés en un seul tour dans une seule réponse :
get_time(timezone='Europe/Paris')get_weather(location='Paris')create_reminder(title='Réunion équipe', time='demain 09h')
Avantages des appels groupés en un seul tour :
| Aspect | Sans groupement | Groupées en un seul tour |
|---|---|---|
| Nombre d’aller-retours API | 3 tours (1 appel par tour) | 1 tour (3 appels groupés) |
| Latence totale | (non mesurée) | (test du flux Python, pas un benchmark fournisseur) |
| Tokens consommés | Non mesurés | Non mesurés |
Le notebook démontre uniquement la réduction du nombre de tours LLM pour ce scénario. Il ne compare ni les usages de tokens ni la latence de deux exécutions équivalentes ; aucune économie de tokens ou de temps n’est donc conclue ici.
Comment le modèle décide-t-il d’exécuter des appels groupés en un seul tour ?
Le modèle détecte que les 3 fonctions sont indépendantes (pas de dépendance de données entre elles). Si une fonction dépend du résultat d’une autre, le modèle demande les appels en plusieurs tours et l’application les exécute dans l’ordre requis.
Best practice : Concevez vos fonctions pour maximiser l’indépendance et permettre le groupement en un seul tour.
Exercice 1 : Définir et appeler un nouvel outil simple
Avant de passer à l’exercice suivant (Exercice 2), vérifiez votre maîtrise du flux de base des Sections 1 à 3 en créant un nouvel outil de zéro et en l’exécutant via run_conversation.
Objectif : définir un outil de calcul de pourboire (calculate_tip) prenant un montant et un pourcentage, implémenter la fonction Python correspondante, puis appeler run_conversation avec une requête utilisateur naturelle (ex. « Ajoute 18 % de pourboire à 42,50 € ») afin de constater que le modèle sélectionne l’outil et renvoie le bon résultat.
Indices : - # Indice : un outil est un dictionnaire {"type": "function", "function": {"name": ..., "description": ..., "parameters": {...}}} (voir Section 1). - # Indice : run_conversation renvoie le texte final (str) ; pour inspecter la structure API, utiliser l’objet response de la cellule de démonstration. - # Étape 1 : écrire le JSON Schema de l’outil calculate_tip (paramètres amount et tip_percent). - # Étape 2 : implémenter def calculate_tip(amount: float, tip_percent: float) -> str: qui renvoie une chaîne descriptive. - # Étape 3 : alimenter tools et available_functions, appeler run_conversation et afficher la réponse.
# Exercice 1 : Définir et appeler un nouvel outil simple
# TODO etudiant : Creez un outil calculate_tip et executez-le via run_conversation
print("Exercice 1 a completer : decommentez et adaptez les etapes ci-dessus (schema, fonction, appel).")
# Etape 1 : Definir le schema de l'outil calculate_tip
# tip_tool = [
# {
# "type": "function",
# "function": {
# "name": "calculate_tip",
# "description": "Calcule le pourboire a laisser pour un montant donne",
# "parameters": {
# "type": "object",
# "properties": {
# "amount": {"type": "number", "description": "Montant de l'addition en euros"},
# "tip_percent": {"type": "number", "description": "Pourcentage de pourboire (ex. 18 pour 18%)"}
# },
# "required": ["amount", "tip_percent"]
# }
# }
# }
# ]
# Etape 2 : Implementer la fonction Python
# def calculate_tip(amount: float, tip_percent: float) -> str:
# tip = amount * tip_percent / 100
# return f"Pourboire de {tip:.2f} EUR pour une addition de {amount} EUR (total : {amount + tip:.2f} EUR)"
# Etape 3 : Executer via run_conversation et afficher la reponse
# available_functions = {"calculate_tip": calculate_tip}
# result = run_conversation(
# user_message="Ajoute 18% de pourboire a 42,50 EUR",
# tools=tip_tool,
# available_functions=available_functions,
# )
# print(result)Exercice 1 a completer : decommentez et adaptez les etapes ci-dessus (schema, fonction, appel).
Exercice 2 : Orchestrateur d’appels groupés en un seul tour
En vous appuyant sur la Section 4, concevez un scénario où le modèle doit appeler plusieurs outils groupés en un seul tour dans une seule réponse, puis vérifiez en inspectant directement la première réponse API.
Objectif : définir un jeu de 3 outils complémentaires (par exemple recherche d’actualités, conversion de devise et calcul d’itinéraire), construire une requête utilisateur qui mobilise ces trois sources simultanément, et confirmer que la réponse contient bien plusieurs tool_calls — len(response.choices[0].message.tool_calls), noms de fonctions, arguments.
Indices : - # Indice : un outil est un dictionnaire {"type": "function", "function": {...}} dans la liste tools. - # Indice : l’exercice inspecte directement la première réponse API : len(tool_calls), noms de fonctions, arguments. - # Étape 1 : définir les 3 outils (JSON Schema). - # Étape 2 : implémenter les fonctions simulées. - # Étape 3 : construire l’appel API (client.chat.completions.create) et inspecter response.choices[0].message.tool_calls.
# Exercice 2 : Orchestrateur d'appels groupés en un seul tour
# TODO etudiant : Creez un scenario declenchant plusieurs appels d'outils groupes en un seul tour
print("Exercice 2 a completer : decommentez et adaptez les etapes ci-dessus (3 outils, requete, inspection).")
# Etape 1 : Definir 3 tools complementaires avec JSON Schema
# grouped_tools = [
# {
# "type": "function",
# "function": {
# "name": "search_news",
# "description": "Recherche les actualites recentes",
# "parameters": {
# "type": "object",
# "properties": {
# "query": {"type": "string", "description": "Sujet de la recherche"}
# },
# "required": ["query"]
# }
# }
# },
# {
# "type": "function",
# "function": {
# "name": "convert_currency",
# "description": "Convertit un montant entre deux devises",
# "parameters": {
# "type": "object",
# "properties": {
# "amount": {"type": "number"},
# "from_currency": {"type": "string"},
# "to_currency": {"type": "string"}
# },
# "required": ["amount", "from_currency", "to_currency"]
# }
# }
# },
# {
# "type": "function",
# "function": {
# "name": "get_route",
# "description": "Calcule un itineraire",
# "parameters": {
# "type": "object",
# "properties": {
# "origin": {"type": "string"},
# "destination": {"type": "string"}
# },
# "required": ["origin", "destination"]
# }
# }
# }
# ]
# Etape 2 : Implementer les fonctions simulees
# def search_news(query: str) -> str: ...
# def convert_currency(amount: float, from_currency: str, to_currency: str) -> str: ...
# def get_route(origin: str, destination: str) -> str: ...
# Etape 3 : Construire une requete mobilisant les 3 sources, puis inspecter la premiere reponse API
# response = client.chat.completions.create(
# model=DEFAULT_MODEL,
# messages=[{"role": "user", "content": "Donne-moi l'actu sur l'IA, convertis 100 EUR en JPY et l'itineraire Paris-Lyon"}],
# tools=grouped_tools,
# tool_choice="auto",
# )
# for tc in response.choices[0].message.tool_calls:
# print(tc.function.name) # doit afficher les 3 outils en un seul tourExercice 2 a completer : decommentez et adaptez les etapes ci-dessus (3 outils, requete, inspection).
5. Contrôle Avancé : tool_choice
Le paramètre tool_choice offre un contrôle fin sur le comportement du modèle :
| Valeur | Comportement |
|---|---|
"auto" |
Le modèle décide (défaut) |
"required" |
Le modèle DOIT appeler au moins un outil |
"none" |
Le modèle ne peut PAS appeler d’outils |
{"type": "function", "function": {"name": "X"}} |
Force l’appel de la fonction X |
Cas d’usage : - "required" : Forcer l’utilisation d’outils pour des données en temps réel - "none" : Désactiver temporairement les outils (mode conversation pure) - Spécifique : Garantir qu’une action précise sera exécutée
# Test 1: Forcer l'appel d'une fonction spécifique
print("=== Test 1: Forcer l'appel d'un outil spécifique ===")
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Bonjour, comment vas-tu?"}],
tools=tools,
tool_choice={"type": "function", "function": {"name": "get_weather"}}
)
print("Question: Bonjour, comment vas-tu?")
print("\nAvec tool_choice forcé à get_weather:")
if response.choices[0].message.tool_calls:
tc = response.choices[0].message.tool_calls[0]
print(f" Le modèle a appelé: {tc.function.name}")
print(f" Arguments: {tc.function.arguments}")
# Test 2: Désactiver les tools
print("\n=== Test 2: Désactiver les tools ===")
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Quelle est la météo à Paris?"}],
tools=tools,
tool_choice="none"
)
print("Question: Quelle est la météo à Paris?")
print("\nAvec tool_choice='none' (pas d'appel de fonction):")
print(f" Réponse directe: {response.choices[0].message.content[:100]}...")
# Test 3: Forcer au moins un appel (required)
print("\n=== Test 3: tool_choice='required' ===")
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Dis-moi bonjour"}],
tools=extended_tools,
tool_choice="required"
)
print("Question: Dis-moi bonjour")
print("\nAvec tool_choice='required':")
if response.choices[0].message.tool_calls:
contenu_reponse = response.choices[0].message.content
print("\nContenu de la réponse du modèle:")
print(contenu_reponse)
tc = response.choices[0].message.tool_calls[0]
print(f" Le modèle est FORCÉ d'appeler un outil: {tc.function.name} avec les paramètres {tc.function.arguments}")=== Test 1: Forcer l'appel d'un outil spécifique ===
Question: Bonjour, comment vas-tu?
Avec tool_choice forcé à get_weather:
Le modèle a appelé: get_weather
Arguments: {"location":"Paris","unit":"celsius"}
=== Test 2: Désactiver les tools ===
Question: Quelle est la météo à Paris?
Avec tool_choice='none' (pas d'appel de fonction):
Réponse directe: Je n’ai pas accès aux données météo en direct en ce moment, donc je ne peux pas donner la météo actu...
=== Test 3: tool_choice='required' ===
Question: Dis-moi bonjour
Avec tool_choice='required':
Contenu de la réponse du modèle:
None
Le modèle est FORCÉ d'appeler un outil: get_time avec les paramètres {"timezone":"Europe/Paris"}
Analyse des comportements de tool_choice
| Mode | Requête | Résultat | Observation |
|---|---|---|---|
| Forcé spécifique | “Bonjour, comment vas-tu?” | Appel forcé à get_weather |
Le modèle DOIT appeler la fonction même si inappropriée |
| none | “Quelle est la météo à Paris?” | Réponse textuelle sans fonction | Le modèle répond directement sans accès aux données réelles |
| required | “Dis-moi bonjour” | Appel forcé à un outil (ex: get_time) |
Le modèle choisit l’outil le moins inapproprié |
Implications pratiques : - tool_choice forcé peut générer des appels non pertinents → utiliser avec parcimonie - tool_choice="none" utile pour économiser des appels API coûteux quand les données ne changent pas - tool_choice="required" garantit l’utilisation de données temps réel (ex: prix en bourse)
Attention : Forcer un outil peut dégrader l’expérience utilisateur si mal utilisé.
6. Gestion Robuste des Erreurs
Dans un système de production, la gestion d’erreurs est cruciale :
Sources d’erreurs possibles : 1. Erreurs API : Timeout, limites de taux, problèmes réseau 2. Arguments invalides : JSON malformé, types incorrects 3. Fonction non disponible : Le modèle appelle une fonction qui n’existe pas 4. Erreur d’exécution : La fonction échoue (ex: API externe indisponible) 5. Boucles infinies : Le modèle continue d’appeler des fonctions sans fin
Bonnes pratiques : - Limiter le nombre d’itérations (max_iterations) - Valider les arguments JSON - Retourner des messages d’erreur structurés au modèle - Logger les erreurs pour débogage
def run_safe_conversation(user_message: str, tools: list, available_functions: dict, max_iterations: int = 5):
"""Version robuste avec gestion d'erreurs et limite d'itérations"""
messages = [{"role": "user", "content": user_message}]
for iteration in range(max_iterations):
# Gestion erreurs API
try:
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=messages,
tools=tools,
tool_choice="auto"
)
except Exception as e:
return f"Erreur API: {e}"
assistant_message = response.choices[0].message
messages.append(assistant_message)
if not assistant_message.tool_calls:
return assistant_message.content
# Traiter chaque appel de fonction
for tool_call in assistant_message.tool_calls:
function_name = tool_call.function.name
# Gestion erreurs JSON
try:
function_args = json.loads(tool_call.function.arguments)
except json.JSONDecodeError:
result = json.dumps({"error": "Arguments invalides, JSON malformé"})
messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
continue
# Vérifier que la fonction existe
if function_name not in available_functions:
result = json.dumps({"error": f"Fonction '{function_name}' non disponible"})
else:
# Gestion erreurs d'exécution
try:
result = available_functions[function_name](**function_args)
except Exception as e:
result = json.dumps({"error": f"Erreur d'exécution: {str(e)}"})
messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
return f"Limite d'itérations atteinte ({max_iterations}). Conversation trop longue."
# Test de gestion d'erreurs
print("=== Test 1: Ville non répertoriée (fallback) ===")
print(run_safe_conversation("Météo à Berlin?", tools, all_functions))
print("\n=== Test 2: Fonction inexistante ===")
print(run_safe_conversation("Achète-moi un billet d'avion", extended_tools, all_functions))
print("\n=== Test 3: Arguments invalides ===")
# Simuler un cas où le modèle pourrait générer des arguments incorrects
print(run_safe_conversation("Météo à ????? température en @@@@", tools, all_functions))=== Test 1: Ville non répertoriée (fallback) ===
À Berlin il fait actuellement 20 °C, temps variable, humidité 50 %. Voulez-vous la prévision pour les prochaines heures ou les prochains jours ?
=== Test 2: Fonction inexistante ===
Je ne peux pas acheter directement un billet (pas d’accès aux paiements ou aux comptes), mais je peux tout faire pour te faciliter la réservation : chercher des vols, comparer prix/horaires, te donner des liens pour réserver, préparer les infos à entrer et te guider pas à pas.
Pour commencer, donne-moi ces informations :
- Ville/aéroport de départ (ex. Paris CDG)
- Ville/aéroport d’arrivée
- Dates (aller / retour ou aller simple / dates flexibles ?)
- Nombre de passagers et âges (adulte/enfant/bébé)
- Classe souhaitée (économique / premium / affaires / première)
- Budget approximatif ou priorité (prix, rapidité, escales, compagnie préférée)
- Préférences d’horaires (matin / après-midi / nuit) et tolérance aux escales
- Pays de résidence / passeport (utile pour contrôler visa et tarifs)
- Besoin d’assistance spéciale (bagages supplémentaires, animaux, fauteuil roulant…)
Souhaites-tu aussi que je te crée un rappel pour réserver (date/heure) ? Si oui indique quand et je le programme.
=== Test 3: Arguments invalides ===
Vous avez laissé des marqueurs (????? et @@@@). Pour que je récupère la météo il me faut :
- une ville (ex. Paris, Lyon) à la place de "?????" ;
- l’unité de température (celsius ou fahrenheit) à la place de "@@@@".
Exemples de message que vous pouvez envoyer :
- Météo à Paris température en celsius
- Météo à New York température en fahrenheit
Voulez-vous la météo actuelle ou une prévision (heures/jours) ? Indiquez la ville et l’unité et je vous donne la météo.
Analyse des résultats de gestion d’erreurs
Les trois tests démontrent la robustesse du système :
| Test | Scénario | Comportement attendu |
|---|---|---|
| Ville non répertoriée | Berlin n’est pas dans weather_data |
Fallback vers données par défaut (20°C, variable) |
| Fonction inexistante | “Achète-moi un billet d’avion” | Retour JSON avec error: "Fonction 'X' non disponible" |
| Arguments invalides | Caractères spéciaux (??, @@) | Le modèle peut soit échouer à générer des arguments valides, soit utiliser un fallback |
Points clés : - La fonction run_safe_conversation() ne plante jamais - Tous les cas d’erreur sont capturés et retournés de manière structurée - Le modèle reçoit les messages d’erreur et peut adapter sa stratégie
Production : En environnement réel, logguer toutes les erreurs pour analyse et amélioration continue.
Exercice 3 : Analyseur de logs avec tool_choice
Créez un outil d’analyse de logs qui utilise les différents modes de tool_choice pour contrôler le comportement du modèle.
Objectif : Définir un tool analyze_log qui detecte le niveau de severite d’un message de log (ERROR, WARNING, INFO), et tester les 3 modes de tool_choice (auto, required, none) sur différentes requêtes pour observer comment le modèle decide d’appeler ou non l’outil.
Indices : - Inspirez-vous de la section 5 sur tool_choice pour les 3 modes - Le tool doit avoir un paramètre log_message de type string - Implementez une fonction qui retourne un JSON avec le niveau detecte et un resume - Testez avec : un message d’erreur clair (mode auto), une question sans rapport (mode auto), et forcez l’appel (mode required)
# Exercice 3 : Analyseur de logs avec tool_choice
# TODO etudiant : Creez un outil d'analyse de logs
# Etape 1 : Definir le tool analyze_log avec JSON Schema
# log_tool = [{
# "type": "function",
# "function": {
# "name": "analyze_log",
# "description": "...",
# "parameters": {
# "type": "object",
# "properties": {
# "log_message": {"type": "string", "description": "..."}
# },
# "required": ["log_message"]
# }
# }
# }]
# Etape 2 : Implementer la fonction
# def analyze_log(log_message: str) -> str:
# ...
# return json.dumps({"severity": ..., "summary": ...})
# Etape 3 : Tester les 3 modes tool_choice
# Test auto avec message d'erreur, auto avec question sans rapport, required
print("Exercice a completer")Exercice a completer
7. Cas d’Usage Avancés
Voyons quelques patterns avancés de function calling :
A. Recherche de base de données
Simuler une recherche dans une base de données avec filtres.
# Simuler une base de données de cours
COURSE_DB = [
{"id": 1, "title": "Python pour débutants", "category": "Programming", "duration_hours": 10, "level": "Débutant", "price": 49},
{"id": 2, "title": "Machine Learning Intro", "category": "ML", "duration_hours": 15, "level": "Intermédiaire", "price": 99},
{"id": 3, "title": "Deep Learning Avancé", "category": "ML", "duration_hours": 30, "level": "Avancé", "price": 299},
{"id": 4, "title": "Data Science avec R", "category": "Data", "duration_hours": 20, "level": "Intermédiaire", "price": 149},
{"id": 5, "title": "Computer Vision", "category": "ML", "duration_hours": 25, "level": "Avancé", "price": 349},
]
def search_courses(category: str = None, min_duration: int = None, max_price: int = None, level: str = None) -> str:
"""Recherche de cours avec filtres multiples"""
results = COURSE_DB.copy()
if category:
results = [c for c in results if c["category"] == category]
if min_duration:
results = [c for c in results if c["duration_hours"] >= min_duration]
if max_price:
results = [c for c in results if c["price"] <= max_price]
if level:
results = [c for c in results if c["level"] == level]
return json.dumps({"count": len(results), "courses": results}, ensure_ascii=False)
# Définir le tool
search_tool = [{
"type": "function",
"function": {
"name": "search_courses",
"description": "Rechercher des cours dans la base de données avec filtres optionnels",
"parameters": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["Programming", "ML", "Data"], "description": "Catégorie de cours"},
"min_duration": {"type": "integer", "description": "Durée minimale en heures"},
"max_price": {"type": "integer", "description": "Prix maximum en euros"},
"level": {"type": "string", "enum": ["Débutant", "Intermédiaire", "Avancé"], "description": "Niveau"}
}
}
}
}]
search_functions = {"search_courses": search_courses}
# Test
question = "Trouve-moi tous les cours de Machine Learning de plus de 20 heures"
print(f"Question: {question}\n")
print(run_safe_conversation(question, search_tool, search_functions))Question: Trouve-moi tous les cours de Machine Learning de plus de 20 heures
J’ai trouvé 2 cours de Machine Learning de plus de 20 heures :
1. Deep Learning Avancé
- ID : 3
- Durée : 30 heures
- Niveau : Avancé
- Prix : 299 €
2. Computer Vision
- ID : 5
- Durée : 25 heures
- Niveau : Avancé
- Prix : 349 €
Souhaitez-vous les détails (programme, prérequis), comparer les contenus, ou vous inscrire à l’un d’eux ?
Interprétation des résultats de recherche
Requête analysée par le modèle : - Catégorie : Machine Learning - Durée minimale : 20 heures
Résultats retournés : Le modèle a correctement extrait les critères et construit les arguments de la fonction search_courses(). Notez que :
- Traitement du langage naturel : “de plus de 20 heures” →
min_duration=20 - Mapping de catégorie : “Machine Learning” →
category="ML" - Filtres combinés : Les deux critères sont appliqués en ET logique
Ce pattern de recherche s’applique à de nombreux cas d’usage réels (e-commerce, CRM, documentation, etc.).
B. Orchestration Multi-Étapes
Le modèle peut orchestrer plusieurs étapes complexes automatiquement.
# Le modèle peut décomposer une tâche complexe en plusieurs appels
scenario = "Planifie ma journée demain - réveil à 7h, sport à 8h, travail à 9h"
print(f"Scénario: {scenario}\n")
print("Étapes exécutées:")
result = run_conversation(scenario, extended_tools, all_functions)
print("\nRésultat:")
print(result)Scénario: Planifie ma journée demain - réveil à 7h, sport à 8h, travail à 9h
Étapes exécutées:
Résultat:
Voici une proposition de planning clair et adaptable pour demain, en partant du réveil à 7h, sport à 8h et début du travail à 9h.
Planning proposé
- 06:50 — réveil doux (alarme de backup) / 10 min d’étirement au lit ou respiration
- 07:00 — réveil officiel : toilette rapide (lavage visage, brossage dents)
- 07:10 — préparation / habillage (préparer tenue de sport si pas prêt la veille)
- 07:20 — petit-déjeuner énergétique (ex. flocons d’avoine, yaourt + fruit, café/thé)
- 07:40 — check rapide : sac / clés / lunch / documents pour le travail
- 07:50 — déplacement vers le lieu du sport (ou mise en place si à la maison)
- 08:00–08:45 — séance de sport (45 min) — échauffement + entraînement principal
- 08:45–09:00 — douche rapide et préparation finale pour le travail
- 09:00 — début du travail
Suggestions pour la journée de travail
- Début (09:00–11:00) : bloc de travail concentré — prioriser 1 tâche importante (pas d’e-mails)
- Pause (11:00–11:10) : marche courte, hydratation
- Milieu de matinée (11:10–12:30) : tâches secondaires / réunions
- Pause déjeuner (12:30–13:15) : repas + 10 min de marche si possible
- Après-midi (13:15–16:30) : 2 blocs Pomodoro (25/5) pour avancer sur projets
- Pause (16:30–16:40) : étirements, changer d’air
- Fin de journée (16:40–17:30) : finir tâches rapides, planifier lendemain, synthèse
- 17:30 : fin du travail (ou adapter selon ton horaire réel)
Soirée (suggestion)
- 18:00–19:00 : temps libre / courses / préparation dîner
- 19:00–20:00 : dîner tranquille
- 20:00–21:30 : activité détente productive (lecture, hobby, social)
- 21:30–22:00 : préparation au coucher (écran réduit, douche si nécessaire)
- 22:30–23:00 : coucher recommandé (si tu veux ~8 h de sommeil, coucher vers 23:00)
Conseils pratiques
- Prépare la tenue de sport, sac et lunch la veille pour gagner du temps le matin.
- Si tu as un trajet pour aller au travail, ajuste la fenêtre 08:45–09:00 pour inclure le temps de trajet.
- Si tu préfères une séance de 1 h, commence à 08:00 et réduis la pause/douche ou réajuste le réveil à 06:50.
- Pour rester productif, choisis 3 priorités top pour la journée et note-les la veille.
- Veux-tu que je crée des rappels (réveil, sport, début du travail) dans ton agenda ? Je peux les ajouter si tu me dis quand et quel niveau d’alerte.
Interprétation de l’orchestration
Le modèle illustre un comportement caractéristique des agents conversationnels face à une intention complexe : il privilégie la proposition textuelle et la confirmation avant l’action.
Analyse du comportement : - Entrée : “Planifie ma journée demain - réveil à 7h, sport à 8h, travail à 9h” - Réponse en langage naturel : plutôt que d’appeler directement create_reminder(), le modèle a produit une proposition de planning détaillée et demandé confirmation avant de créer les rappels (comportement prudent typique de gpt-5-mini) - Pattern observé : le modèle distingue « planifier » (générer un plan textuel) de « créer un rappel » (action nécessitant un appel d’outil) — il sollicite l’utilisateur pour la priority de chaque rappel avant d’agir
Ce pattern est fondamental pour : - Agents de productivité : Transformation de langage naturel en actions multiples - Workflows complexes : Orchestration automatique de tâches interdépendantes - Interfaces conversationnelles : Réduire le nombre d’interactions utilisateur
Pattern clé : face à une intention ouverte (« planifie ma journée »), le modèle a produit une proposition textuelle et sollicité la confirmation de l’utilisateur plutôt que d’exécuter des actions atomiques (la sortie
Étapes exécutées:est vide) — comportement prudent typique avant une action à effet de bord (création de rappels). L’orchestration pleinement autonome (actions exécutées sans intervention) s’obtient en forçant les appels d’outils (tool_choice: "required") ou en précisant l’intention.
Exercice 4 : Outil de conversion de devises
Créez un outil de conversion de devises et integrez-le dans la boucle agentique. Le modèle doit pouvoir convertir des montants entre différentes devises en utilisant un outil que vous definissez.
Objectif : Définir un tool convert_currency avec les paramètres amount (montant), from_currency (devise source) et to_currency (devise cible), implementer la fonction simulee, et tester avec la question : “Combien font 100 euros en dollars et en yen ?”
Indices : - Definissez le tool avec JSON Schema en vous inspirant de get_weather - Pour la fonction simulee, utilisez des taux fixes (EUR/USD = 1.10, EUR/JPY = 160, USD/JPY = 145) - Le modèle peut appeler la fonction plusieurs fois si la question implique plusieurs conversions - Utilisez run_safe_conversation() pour gerer les erreurs
# Exercice 4 : Outil de conversion de devises
# TODO etudiant : Creez un outil de conversion de devises
# Etape 1 : Definir le tool avec JSON Schema
# currency_tool = [{
# "type": "function",
# "function": {
# "name": "convert_currency",
# ...
# }
# }]
# Etape 2 : Implementer la fonction simulee avec des taux fixes
# def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
# rates = {"EUR": 1.0, "USD": 1.10, "JPY": 160.0, "GBP": 0.85}
# ...
# return json.dumps({"amount": ..., "from": ..., "to": ..., "result": ...})
# Etape 3 : Tester avec une question multi-conversion
# all_tools_with_currency = extended_tools + currency_tool
# all_functions["convert_currency"] = convert_currency
# result = run_safe_conversation(
# "Combien font 100 euros en dollars et en yen ?",
# all_tools_with_currency,
# all_functions
# )
# print(result)
print("Exercice a completer")Exercice a completer
8. Conclusion et Bonnes Pratiques
Points Clés
- Descriptions précises : Les descriptions des fonctions guident le modèle. Soyez explicite !
- JSON Schema rigoureux : Définissez bien les types et contraintes des paramètres
- Gestion d’erreurs : Toujours prévoir des fallbacks et retourner des erreurs structurées
- Limite d’itérations : Évitez les boucles infinies avec un max_iterations
- Sécurité : Validez TOUJOURS les arguments avant exécution (injections, chemins dangereux, etc.)
Cas d’Usage Réels
| Domaine | Exemple d’Application |
|---|---|
| E-commerce | Recherche produits, ajout panier, suivi commande |
| Productivité | Gestion calendrier, emails, rappels |
| Finance | Consultation soldes, virements, alertes |
| Data Science | Requêtes SQL, visualisations, analyses |
| DevOps | Déploiements, logs, monitoring |
Exercices Suggérés
- Système de réservation : Créez des fonctions pour chercher/réserver des restaurants
- Calculatrice avancée : Fonctions mathématiques (factorielle, fibonacci, primalité)
- API réelle : Intégrez une vraie API météo (OpenWeatherMap) au lieu de la simulation
- Multi-agents : Créez plusieurs “agents” avec des sets de fonctions différents
Prochaine Étape
Dans le notebook suivant (05_RAG_Modern.ipynb), nous verrons comment combiner function calling avec le RAG (Retrieval-Augmented Generation) pour créer des assistants qui peuvent chercher dans des bases de connaissances.
Ressources : - OpenAI Function Calling Guide - JSON Schema Documentation - Documentation Notebook 1 (OpenAI Intro) - Documentation Notebook 3 (Structured Outputs)
# Cellule de validation finale
print("✓ Notebook Function Calling terminé !")
print("✓ Concepts maîtrisés: Tools, tool_choice, boucles agentiques, gestion d'erreurs")
print("✓ Prochaine étape: RAG (Retrieval-Augmented Generation)")✓ Notebook Function Calling terminé !
✓ Concepts maîtrisés: Tools, tool_choice, boucles agentiques, gestion d'erreurs
✓ Prochaine étape: RAG (Retrieval-Augmented Generation)
Validation de la progression
Cette cellule confirme la complétion du notebook et résume les acquis :
Concepts maîtrisés : 1. Tools : Définition de fonctions avec JSON Schema 2. tool_choice : Contrôle fin du comportement du modèle (auto, required, none, spécifique) 3. Boucles agentiques : Pattern fondamental pour les agents autonomes 4. Gestion d’erreurs : Robustesse en production (timeouts, validation, limites) 5. Orchestration : Appels groupés en un seul tour et décomposition automatique de tâches complexes
Prochaine étape : Le notebook RAG (Retrieval-Augmented Generation) combinera ces techniques avec des bases de connaissances pour créer des assistants capables de raisonner sur des documents.
EXEMPLE CORRIGÉ - Assistant de Planification Multi-Outils
Solution par Nathan JULOU et Robin VAZ
Objectif
Créer un assistant de planification qui combine plusieurs outils pour orchestrer une soirée complète.
Outils implémentés
| Outil | Description | Paramètres |
|---|---|---|
search_events(city, date, category) |
Rechercher des événements culturels | city, date, category (optionnel) |
get_travel_time(origin, destination, transport) |
Calculer le temps de trajet | origin, destination, transport (optionnel) |
book_table(restaurant, time, guests, special_requests) |
Réserver une table | restaurant, time, guests, special_requests (optionnel) |
Techniques utilisées
- JSON Schema : Définition rigoureuse des paramètres
- Boucle agentique :
run_conversation()orchestre les appels - Appels groupés en un seul tour : Le modèle peut appeler plusieurs outils simultanément
Pattern clé : Le modèle décide automatiquement quels outils appeler et dans quel ordre.
# EXEMPLE CORRIGÉ: Assistant de Planification Multi-Outils
# Solution par Nathan JULOU et Robin VAZ
# 1. Définition des 3 outils
# Tool search_events
search_events_tool = {
"type": "function",
"function": {
"name": "search_events",
"description": "Rechercher des événements culturels (ex: concerts, spectacles, expos) dans une ville pour une date donnée",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Nom de la ville (ex: Lyon, Paris)"},
"date": {"type": "string", "description": "Date de l'événement (ex: 'ce soir', 'demain')"},
"category": {"type": "string", "enum": ["culturel", "concert", "theatre", "exposition"], "description": "Catégorie d'événement"}
},
"required": ["city", "date"]
}
}
}
# Tool get_travel_time
get_travel_time_tool = {
"type": "function",
"function": {
"name": "get_travel_time",
"description": "Obtenir le temps de trajet estimé entre deux lieux à Lyon",
"parameters": {
"type": "object",
"properties": {
"origin": {"type": "string", "description": "Point de départ (ex: 'Part-Dieu', 'Villeurbanne')"},
"destination": {"type": "string", "description": "Point d'arrivée (ex: 'Croix-Rousse', 'Tête d'Or')"},
"transport": {"type": "string", "enum": ["transports", "voiture", "velo", "pieds"], "description": "Mode de transport"}
},
"required": ["origin", "destination"]
}
}
}
# Tool book_table
book_table_tool = {
"type": "function",
"function": {
"name": "book_table",
"description": "Réserver une table dans un restaurant pour un nombre de personnes à une heure donnée",
"parameters": {
"type": "object",
"properties": {
"restaurant": {"type": "string", "description": "Nom du restaurant"},
"time": {"type": "string", "description": "Heure de réservation (ex: '19:00', '20:30')"},
"guests": {"type": "integer", "description": "Nombre de personnes"},
"special_requests": {"type": "string", "description": "Requêtes spéciales (ex: 'vue', 'calme')"}
},
"required": ["restaurant", "time", "guests"]
}
}
}
print("3 outils définis: search_events, get_travel_time, book_table")3 outils définis: search_events, get_travel_time, book_table
# 2. Implémentation des fonctions simulées
def search_events(city: str, date: str, category: str = "culturel") -> str:
"""Simule une recherche d'événements"""
events_db = {
"Lyon": [
{"title": "Nuits de la Peur", "venue": "Théâtre de la Croix-Rousse", "time": "19:00", "price": "15€"},
{"title": "Concert Jazz", "venue": "Salle 300", "time": "20:30", "price": "25€"},
{"title": "Expo Impressionnistes", "venue": "Musée des Beaux-Arts", "time": "10:00-18:00", "price": "12€"},
],
"Paris": [
{"title": "Hamlet", "venue": "Comédie-Française", "time": "19:30", "price": "35€"},
]
}
city_events = events_db.get(city, [])
return json.dumps({"city": city, "date": date, "events": city_events}, ensure_ascii=False)
def get_travel_time(origin: str, destination: str, transport: str = "transports") -> str:
"""Simule un calcul de temps de trajet"""
travel_times = {
("Part-Dieu", "Croix-Rousse"): {"transports": "12 min", "voiture": "8 min", "velo": "15 min", "pieds": "25 min"},
("Part-Dieu", "Villeurbanne"): {"transports": "10 min", "voiture": "12 min", "velo": "18 min", "pieds": "35 min"},
("Croix-Rousse", "Villeurbanne"): {"transports": "20 min", "voiture": "15 min", "velo": "22 min", "pieds": "40 min"},
}
key = (origin, destination)
time = travel_times.get(key, {}).get(transport, "15 min (estimé)")
return json.dumps({"origin": origin, "destination": destination, "transport": transport, "time": time}, ensure_ascii=False)
def book_table(restaurant: str, time: str, guests: int, special_requests: str = "") -> str:
"""Simule une réservation de restaurant"""
restaurants = {
"Le Restaurant Italien": {"cuisine": "italien", "price_range": "15-25€"},
"Bistrot du Coin": {"cuisine": "français", "price_range": "20-35€"},
"Sushi Express": {"cuisine": "japonais", "price_range": "12-20€"},
}
info = restaurants.get(restaurant, {"cuisine": "inconnu", "price_range": "N/A"})
return json.dumps({
"status": "confirmed",
"restaurant": restaurant,
"time": time,
"guests": guests,
"cuisine": info["cuisine"],
"price_range": info["price_range"],
"special_requests": special_requests
}, ensure_ascii=False)
print("Fonctions simulées implémentées")Fonctions simulées implémentées
Les fonctions simulées étant définies, le test de l’assistant de planification peut être lancé. Cette cellule orchestre un appel complet au modèle avec tous les outils disponibles pour répondre à une requête utilisateur complexe.
# 3. Test de l'assistant de planification
all_tools = [search_events_tool, get_travel_time_tool, book_table_tool]
all_functions = {
"search_events": search_events,
"get_travel_time": get_travel_time,
"book_table": book_table
}
print("Outils disponibles:", ", ".join(all_functions.keys()))
# Test avec la requête du challenge
test_request = "Je veux un événement culturel à Lyon ce soir vers 19h, puis un restaurant italien pas cher, en partant de la Part-Dieu"
print(f"\nQuestion: {test_request}")
print("\nAppels de fonctions:")
result = run_conversation(test_request, all_tools, all_functions)
print("\nRésultat final:")
print(result)Outils disponibles: search_events, get_travel_time, book_table
Question: Je veux un événement culturel à Lyon ce soir vers 19h, puis un restaurant italien pas cher, en partant de la Part-Dieu
Appels de fonctions:
Appel: search_events({'city': 'Lyon', 'date': 'ce soir', 'category': 'culturel'})
Appel: get_travel_time({'origin': 'Part-Dieu', 'destination': 'Croix-Rousse', 'transport': 'transports'})
Résultat final:
Parfait — j’ai trouvé une option pour ce soir :
- Événement : "Nuits de la Peur"
Lieu : Théâtre de la Croix-Rousse
Horaire : 19:00
Tarif : 15€
- Trajet Part-Dieu → Croix-Rousse (transports en commun) : environ 12 minutes
Avant de chercher un resto italien pas cher, deux petites précisions pour que je cible au mieux :
1) Préférez‑vous un restaurant proche du Théâtre de la Croix‑Rousse (pratique après le spectacle) ou plutôt proche de la Part‑Dieu (pour un retour plus direct) ?
2) Combien de personnes ? Voulez‑vous que je réserve la table ?
3) Préférence précise (pizzeria, trattoria, plats à emporter, végétarien, etc.) et fourchette de prix souhaitée ?
Dites‑moi ça et je vous propose 2–3 adresses bon marché et peux aussi réserver si vous le souhaitez.
Analyse de l’assistant de planification
Points forts de l’implémentation :
- Tools bien documentés : Les descriptions guident le modèle dans ses choix
- Fonctions simulées réalistes : Retours JSON structurés avec données cohérentes
- Réutilisation de
run_conversation(): Pattern agentique standard
Observations sur le test :
Le modèle a correctement : - Identifié le besoin d’appeler search_events pour Lyon - Extrait les paramètres (city='Lyon', date='ce soir', category='culturel') - Proposé une stratégie pour la suite (restaurant + trajet)
Pattern d’orchestration :
Question utilisateur → Analyse → Appel tool → Résultat → Synthèse
Le modèle décide automatiquement de l’ordre des opérations sans règle explicite.
CHALLENGE BONUS - Agent de Résolution d’Équations Mathématiques
Points : 0.75 pts
Objectif
Créer un agent mathématique capable de décomposer et résoudre des expressions complexes en utilisant des outils de calcul.
Ce que vous avez appris
L’exemple précédent vous a montré: - Définition de tools avec JSON Schema - Fonctions simulées retournant du JSON - Boucle agentique avec run_conversation()
Nouveau défi : Arbre d’opérations
Contrairement à l’assistant de planification (outils indépendants), vous devez créer un agent qui décompose une expression en opérations atomiques :
Expression: "calcule (3 + 5) * 2**3"
↓
1. add(3, 5) → 8
2. power(2, 3) → 8
3. multiply(8, 8) → 64
Critères de succès
-
- Passe l’expression au modèle
- Laisse le modèle appeler les outils dans l’ordre correct
- Retourne
{"result": int, "steps": [...]}
Contraintes techniques
- Les outils ne prennent que 2 arguments (opérations binaires)
- Le modèle doit gérer la précédence des opérateurs
- Utiliser
run_conversation()de la Section 3 - Logger chaque appel avec
print()
Différence avec l’exemple corrigé
| Exemple corrigé (Planification) | Nouveau challenge (Maths) |
|---|---|
| Outils indépendants | Outils avec dépendances |
| Ordre libre | Ordre imposé par les maths |
| Simulation de données | Vrais calculs |
| 3 outils | 4 outils |
Indices :
- Le modèle doit comprendre qu’il faut résoudre
(10 - 3)avant de multiplier power(5, 2)doit être appelé avant l’addition finale- Le retour JSON peut inclure
"steps": [{"op": "add", "args": [3, 5], "result": 8}, ...]
Soumission : PR avec titre “Challenge #2 - [Votre Nom]” et les 4 tools + fonction solve_math_expression
# CHALLENGE: Agent mathématique multi-outils
# TODO: Implémentez votre agent de calcul
# 1. Définir les outils (tools)
# math_tools = [...]
# 2. Implémenter les fonctions
# def add(a, b): ...
# def subtract(a, b): ...
# def multiply(a, b): ...
# def power(base, exponent): ...
# 3. Fonction de résolution avec suivi des étapes
# def solve_math_expression(expr: str) -> dict:
# steps = []
# ...utiliser run_conversation()...
# return {"result": final_result, "steps": steps}
# 4. Tester
# result = solve_math_expression("(10 - 3) * 2 + 5**2")
# print(f"Résultat: {result['result']}")
# print(f"Étapes: {result['steps']}")
print("Challenge a completer")Challenge a completer