# Parameters
BATCH_MODE = "true"3. Structured Outputs : Sorties JSON Garanties
Navigation : Index | << Précédent | Suivant >> | Approfondissement >>
Structured Outputs : Sorties JSON Garanties avec OpenAI
Dans ce notebook, nous explorons les Structured Outputs, une fonctionnalité qui garantit que le modèle retourne des données JSON valides selon un schéma prédéfini.
Objectifs : - Comprendre la différence entre JSON mode et Structured Outputs - Maîtriser response_format avec JSON Schema - Intégrer Pydantic pour une validation type-safe - Gérer les erreurs de schéma
Prérequis : Notebook 1 (OpenAI Intro)
Durée estimée : 55 minutes
Navigation : << Précédent | Index | Suivant >> | Approfondissement >>
Objectifs d’apprentissage
A la fin de ce notebook, vous saurez : 1. Comprendre la différence entre JSON mode et Structured Outputs 2. Maîtriser response_format avec JSON Schema 3. Integrer Pydantic pour une validation type-safe 4. Gérer les erreurs de schema
Prerequis
- Notebook 1 (OpenAI Intro)
- Python 3.10+
- Cle API OpenAI configuree
Duree estimee : 55 minutes
Structured Outputs : Sorties JSON Garanties avec OpenAI
# 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')
try:
from pydantic import BaseModel, Field
pydantic_AVAILABLE = True
except ImportError:
pydantic_AVAILABLE = False
print(f'WARNING: pydantic non installe. Installez avec: pip install pydantic')
# Installation des dépendances et configuration
from pathlib import Path
%pip install -q openai python-dotenv pydantic
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from typing import List, Optional
# Chargement robuste de la configuration .env
from dotenv import load_dotenv
import os
# 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"
def add_additional_properties_false(schema: dict) -> dict:
"""
Ajoute récursivement additionalProperties: False à tous les objets du schema
ET s'assure que tous les champs sont dans required (requis par OpenAI).
OpenAI Structured Outputs exige:
1. additionalProperties: False sur tous les objets
2. Tous les champs dans 'required' (même ceux avec defaults)
"""
if not isinstance(schema, dict):
return schema
# Traiter les définitions $defs d'abord (Pydantic 2.x les génère)
if "$defs" in schema:
for def_name, def_schema in schema["$defs"].items():
add_additional_properties_false(def_schema)
# Si c'est un objet, ajouter additionalProperties: False
# ET s'assurer que tous les champs sont dans required
if schema.get("type") == "object":
schema["additionalProperties"] = False
# Ajouter tous les champs de properties dans required
if "properties" in schema:
all_props = list(schema["properties"].keys())
schema["required"] = all_props
# Traiter récursivement les propriétés
for prop_name, prop_schema in schema["properties"].items():
add_additional_properties_false(prop_schema)
# Traiter les items d'arrays
if schema.get("type") == "array" and "items" in schema:
add_additional_properties_false(schema["items"])
# Traiter anyOf, allOf, oneOf
for key in ["anyOf", "allOf", "oneOf"]:
if key in schema:
for item in schema[key]:
add_additional_properties_false(item)
return schema
print("Client OpenAI initialisé !")
print(f"Modèle par défaut: {DEFAULT_MODEL}")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
1. JSON Mode vs Structured Outputs
OpenAI propose deux approches pour obtenir des sorties JSON :
JSON Mode
- Garantit une sortie JSON valide (pas de syntaxe invalide)
- NE garantit PAS la structure exacte du JSON
- Le modèle peut inventer des clés, omettre des champs, etc.
- Utilise
response_format={"type": "json_object"}
Structured Outputs
- Garantit une sortie JSON valide ET conforme au schéma
- Le modèle respecte strictement le JSON Schema fourni
- Pas de champs manquants, pas de types incorrects
- Utilise
response_format={"type": "json_schema", "json_schema": {...}} - Disponible depuis août 2024 (gpt-5-mini et versions ultérieures)
Cas d’usage : - JSON Mode : expérimentation rapide, structure flexible - Structured Outputs : production, insertion base de données, APIs strictes
Ancrage scholarly. Les Structured Outputs matérialisent l’idée de constrained / guided decoding — forcer le LLM à n’émettre que des tokens valides au regard d’un schéma. Voir Willard & Müller, Efficient Guided Generation for Large Language Models (2023), implémenté dans Outlines (dottxt-ai).
Démonstration : Limites du JSON Mode
Avant de voir les Structured Outputs, expérimentons avec le JSON Mode classique pour comprendre ses limites. Le modèle va générer du JSON valide, mais sans garantie sur la structure : les noms de clés, le niveau d’imbrication, et l’organisation des données sont laissés à sa discrétion.
# JSON mode basique (ancienne approche)
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Liste 3 fruits avec leurs couleurs en JSON"}],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
print("JSON Mode - Structure non garantie :")
print(json.dumps(result, indent=2, ensure_ascii=False))
print("\nNote : Le modèle a choisi la structure librement (clé 'fruits', sous-objets, noms de clés)")JSON Mode - Structure non garantie :
{
"fruits": [
{
"nom": "pomme",
"couleur": "rouge"
},
{
"nom": "banane",
"couleur": "jaune"
},
{
"nom": "raisin",
"couleur": "violet"
}
]
}
Note : Le modèle a choisi la structure librement (clé 'fruits', sous-objets, noms de clés)
Observation du JSON Mode
Le modèle a généré un JSON syntaxiquement valide, mais structure librement choisie :
Analyse de la sortie :
| Élément | Décision du modèle | Problème potentiel |
|---|---|---|
| Clé racine | "fruits" |
Pourrait être "items", "list", "data", etc. Impossible à prédire |
| Structure items | Array d’objets avec name/color |
Pourrait être {"Banane": "Jaune", ...} (dict) ou [["Banane", "Jaune"], ...] (tuples) |
| Noms de clés | name, color (anglais) |
Le prompt était en français, le modèle a choisi l’anglais |
| Nombre de fruits | 3 (conforme à la demande) | ✅ Respecté |
Risques en production :
Si vous parsez ce JSON avec du code rigide :
for fruit in result["fruits"]: # KeyError si le modèle choisit "items"
print(fruit["name"]) # KeyError si le modèle choisit "nom"Le code peut casser aléatoirement selon l’humeur du modèle ou des variations du prompt.
Cas d’usage acceptable pour JSON Mode : - Prototypage rapide où la structure exacte importe peu - Affichage direct à l’utilisateur sans parsing - Logging/debug
Quand passer aux Structured Outputs : - Insertion en base de données (schéma strict requis) - APIs avec contrats (clients attendent des champs précis) - Workflows multi-étapes (sortie d’une étape = entrée de la suivante)
2. Introduction aux Structured Outputs
Les Structured Outputs utilisent JSON Schema pour définir précisément la structure attendue.
Syntaxe :
response_format={
"type": "json_schema",
"json_schema": {
"name": "nom_du_schema",
"strict": True, # Mode strict obligatoire
"schema": { # JSON Schema
"type": "object",
"properties": {...},
"required": [...],
"additionalProperties": False
}
}
}Points clés : - strict: True est obligatoire pour les Structured Outputs - additionalProperties: False empêche les champs non définis - Tous les champs obligatoires doivent être dans required - Types supportés : string, number, integer, boolean, array, object, enum
Premier Exemple : Schéma Simple pour une Recette
Créons maintenant un schéma JSON strict pour forcer le modèle à respecter une structure précise. Ce premier exemple illustre les éléments essentiels d’un JSON Schema :
type: Type de données (object,array,string, etc.)properties: Définition des champs de l’objetrequired: Liste des champs obligatoiresadditionalProperties: false: Interdit les champs non définis (requis par OpenAI)enum: Restriction à un ensemble de valeurs autorisées
Note importante : OpenAI exige que additionalProperties soit false à tous les niveaux du schéma pour activer le mode strict.
# Structured Output avec schéma manuel
schema = {
"type": "object",
"properties": {
"nom": {"type": "string"},
"ingredients": {"type": "array", "items": {"type": "string"}},
"temps_minutes": {"type": "integer"},
"difficulte": {"type": "string", "enum": ["facile", "moyen", "difficile"]}
},
"required": ["nom", "ingredients", "temps_minutes", "difficulte"],
"additionalProperties": False
}
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Donne-moi une recette de crêpes."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "recette_schema",
"strict": True,
"schema": schema
}
}
)
recette = json.loads(response.choices[0].message.content)
print("Structured Output - Structure garantie :\n")
print(f"Recette: {recette['nom']}")
print(f"Difficulté: {recette['difficulte']}")
print(f"Temps: {recette['temps_minutes']} minutes")
print(f"Ingrédients: {', '.join(recette['ingredients'])}")Structured Output - Structure garantie :
Recette: Crêpes classiques
Difficulté: facile
Temps: 30 minutes
Ingrédients: 250 g de farine, 3 œufs, 500 ml de lait, 2 cuillères à soupe de sucre (facultatif), 1 pincée de sel, 2 cuillères à soupe d'huile neutre ou 30 g de beurre fondu, 1 sachet de sucre vanillé ou 1 cuillère à café d'extrait de vanille (facultatif), Beurre pour la cuisson
Analyse du résultat
Comparons les sorties JSON Mode (cellule précédente) vs Structured Outputs :
Différences clés :
| Aspect | JSON Mode | Structured Outputs |
|---|---|---|
| Clé racine | "fruits" (inventée) |
"nom", "ingredients", etc. (imposées) |
| Noms de champs | "name", "color" (anglais, au choix du modèle) |
Exactement ceux du schéma (français) |
| Champs garantis | ❌ (le modèle peut omettre temps_minutes) |
✅ Tous les champs de required présents |
Type difficulte |
❌ Pourrait être “simple” ou “easy” | ✅ Limité à ["facile", "moyen", "difficile"] via enum |
| Validation automatique | ❌ Nécessite parsing manuel | ✅ JSON garanti conforme au schéma |
Résultat obtenu : - Nom : “Crêpes Classiques” - Difficulté : “facile” (conforme à l’enum) - Temps : 30 minutes (type integer validé) - Ingrédients : Liste de strings (structure exacte du schéma)
Implications pratiques :
Sans Structured Outputs, vous devriez écrire :
if "nom" not in recette:
raise ValueError("Champ 'nom' manquant")
if recette["difficulte"] not in ["facile", "moyen", "difficile"]:
raise ValueError("Difficulté invalide")
# ... et ainsi de suite pour chaque champAvec Structured Outputs, tout cela est géré automatiquement par OpenAI. Le modèle ne peut physiquement pas retourner un JSON non conforme.
Exemple guidé : schéma JSON manuel pour un film
Contribution étudiante de @Daphneej91 (PR #18488), intégrée comme exemple guidé.
Sur le modèle de l’exemple de recette ci-dessus, ce schéma JSON manuel structure les informations d’un film : titre, année (entier), genre (énumération parmi “action”, “comedie”, “drame”, “science-fiction”, “thriller”), réalisateur, acteurs (liste de chaînes) et résumé — puis appelle l’API avec ce schéma.
Points clés : - Même structure que le schéma recette_schema ci-dessus - additionalProperties: False dans le schéma (requis par OpenAI) - Le champ annee est de type integer - "enum" contraint le genre aux 5 valeurs proposées - Le prompt de test : “Donne-moi les informations du film Interstellar”
# Exemple guide : Schema JSON manuel pour un film
# (contribution etudiante de @Daphneej91, PR #18488)
# Etape 1 : Definir le schema JSON manuellement
film_schema = {
"type": "object",
"properties": {
"titre": {"type": "string"},
"annee": {"type": "integer"},
"genre": {"type": "string", "enum": ["action", "comedie", "drame", "science-fiction", "thriller"]},
"realisateur": {"type": "string"},
"acteurs": {"type": "array", "items": {"type": "string"}},
"resume": {"type": "string"},
},
"required": ["titre", "annee", "genre", "realisateur", "acteurs", "resume"],
"additionalProperties": False,
}
# Etape 2 : Appeler l'API avec le schema
response_film = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Donne-moi les informations du film Interstellar"}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "film_schema",
"strict": True,
"schema": film_schema,
}
}
)
# Etape 3 : Parser et afficher le resultat
film = json.loads(response_film.choices[0].message.content)
print(f"Film: {film['titre']} ({film['annee']})")
print(f"Genre: {film['genre']} | Realisateur: {film['realisateur']}")
print(f"Acteurs: {', '.join(film['acteurs'])}")
print(f"Resume: {film['resume']}")Film: Interstellar (2014)
Genre: science-fiction | Realisateur: Christopher Nolan
Acteurs: Matthew McConaughey, Anne Hathaway, Jessica Chastain, Michael Caine, Matt Damon, Casey Affleck, Mackenzie Foy, Bill Irwin, Ellen Burstyn
Resume: Dans un futur proche où la Terre devient de moins en moins habitable, un ancien pilote de la NASA, Cooper, rejoint une mission interstellaire à travers un trou de ver près de Saturne pour trouver une nouvelle planète habitable. Le film explore la relativité du temps, les sacrifices personnels et la force des liens familiaux alors que l'équipe affronte des mondes hostiles et des dilemmes scientifiques. La découverte finale lie l'amour paternel et la physique, permettant à la fille de Cooper, Murph, de résoudre un problème crucial pour sauver l'humanité.
3. Intégration avec Pydantic
Écrire des JSON Schemas manuellement est verbeux et source d’erreurs. Pydantic simplifie cela :
Avantages : - Type safety : Validation automatique des types Python - Auto-complétion : IDE détecte les champs et types - Génération automatique : model_json_schema() crée le JSON Schema - Validation bidirectionnelle : model_validate_json() vérifie et convertit - Documentation : Field(description=...) enrichit le schéma
Workflow : 1. Définir les modèles Pydantic avec BaseModel 2. Générer le JSON Schema avec .model_json_schema() 3. Envoyer à l’API OpenAI 4. Valider la réponse avec .model_validate_json()
Exemple avec Objets Imbriqués
Passons à un modèle plus complexe avec des objets imbriqués (Ingredient) et des listes typées. Pydantic gère automatiquement la génération du JSON Schema pour ces structures complexes.
Points techniques : - List[Ingredient] génère un schéma pour un tableau d’objets structurés - Field(description=...) enrichit le schéma avec des descriptions que le modèle utilise comme contexte - La fonction add_additional_properties_false() est nécessaire car Pydantic ne génère pas cette propriété par défaut
Cette approche est beaucoup plus maintenable et type-safe que l’écriture manuelle de JSON Schema.
# Pydantic avec model_json_schema()
class Ingredient(BaseModel):
nom: str = Field(description="Nom de l'ingrédient")
quantite: str = Field(description="Quantité nécessaire")
class RecetteStructuree(BaseModel):
nom: str = Field(description="Nom de la recette")
description: str = Field(description="Description courte")
ingredients: List[Ingredient] = Field(description="Liste des ingrédients")
etapes: List[str] = Field(description="Étapes de préparation")
temps_preparation: int = Field(description="Temps en minutes")
difficulte: str = Field(description="Niveau de difficulté")
# Générer le schema et ajouter additionalProperties: False
schema = RecetteStructuree.model_json_schema()
schema = add_additional_properties_false(schema)
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": "Donne-moi une recette de mousse au chocolat détaillée."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "recette_detaillee",
"strict": True,
"schema": schema
}
}
)
# Validation automatique avec Pydantic
recette = RecetteStructuree.model_validate_json(response.choices[0].message.content)
print(f"Recette: {recette.nom}")
print(f"Temps: {recette.temps_preparation} min | Difficulté: {recette.difficulte}")
print(f"\nDescription:\n{recette.description}")
print("\nIngrédients:")
for ing in recette.ingredients:
print(f" - {ing.nom}: {ing.quantite}")
print("\nÉtapes:")
for i, etape in enumerate(recette.etapes, 1):
print(f" {i}. {etape}")Recette: Mousse au chocolat classique
Temps: 150 min | Difficulté: Facile
Description:
Mousse aérienne au chocolat noir, simple et rapide à préparer. Texture légère grâce aux blancs montés en neige, saveur intense de chocolat.
Ingrédients:
- Chocolat noir (minimum 60% cacao): 200 g
- Beurre (facultatif pour plus d'onctuosité): 20 g
- Œufs (frais) - séparés: 4 (4 jaunes + 4 blancs)
- Sucre: 30 g
- Sel: 1 pincée
- Extrait de vanille ou café soluble (facultatif): 1 cuillère à café
Étapes:
1. Préparez le matériel : ramequins ou un grand saladier, un fouet, un saladier propre pour monter les blancs, et une casserole pour bain-marie.
2. Cassez le chocolat en morceaux et faites-le fondre doucement au bain-marie avec le beurre (si utilisé). Remuez jusqu'à obtenir une texture lisse. Retirez du feu et laissez tiédir quelques minutes.
3. Pendant ce temps, fouettez les jaunes d'œufs avec le sucre (et l'extrait de vanille ou le café si utilisé) jusqu'à ce que le mélange devienne légèrement pâle et un peu mousseux.
4. Incorporez les jaunes sucrés au chocolat tiédi en mélangeant rapidement et délicatement pour homogénéiser.
5. Dans un autre saladier parfaitement propre, montez les blancs d'œufs en neige ferme avec la pincée de sel. Ils doivent former des pics fermes mais sans être secs.
6. Ajoutez une cuillerée ou deux de blancs montés au mélange chocolaté et incorporez vigoureusement pour détendre la préparation (cela facilite l'incorporation des autres blancs).
7. Incorporez ensuite le reste des blancs en neige en trois fois, avec une spatule, en effectuant des mouvements enveloppants du bas vers le haut pour conserver l'aération. Ne pas trop mélanger.
8. Répartissez la mousse dans des ramequins individuels ou versez-la dans un grand récipient. Lissez la surface si besoin.
9. Placez au réfrigérateur au moins 2 heures (idéal 3-4 heures) pour que la mousse prenne et développe sa texture.
10. Au moment de servir, vous pouvez décorer avec quelques copeaux de chocolat, cacao tamisé ou fruits rouges.
11. Remarque sécurité : cette recette utilise des œufs crus. Utilisez des œufs très frais ou pasteurisés si vous avez des personnes à risque (femmes enceintes, jeunes enfants, personnes immunodéprimées). Conservez la mousse au réfrigérateur et consommez-la sous 48 heures.
Analyse du résultat
Le modèle a généré une recette complète et structurée en respectant parfaitement le schéma Pydantic :
Structure validée :
| Champ | Type | Exemple | Validation |
|---|---|---|---|
nom |
str |
“Mousse au Chocolat Délicieuse” | Non vide |
description |
str |
“Une mousse aérienne…” | Description concise |
ingredients |
List[Ingredient] |
5 ingrédients avec quantités | Objets imbriqués |
étapes |
List[str] |
10 étapes numérotées | Séquence logique |
temps_preparation |
int |
150 minutes | Entier positif |
difficulte |
str |
“Facile” | Enum implicite |
Avantages de Pydantic détectés :
- Auto-complétion IDE :
recette.nom,recette.ingredients[0].quantite→ détection automatique des champs - Type safety :
recette.temps_preparationest garanti être unint, pas une string - Validation : Si le modèle retournait
temps_preparation: "vingt", Pydantic lèverait uneValidationError - Itération sécurisée :
for ing in recette.ingredients→ chaqueingest un objetIngredienttypé
Comparaison avec JSON manuel :
# Sans Pydantic (fragile)
recette_json = json.loads(response.content)
print(recette_json["nom"]) # Risque KeyError si clé manquante
temps = int(recette_json["temps"]) # Risque ValueError si type incorrect
# Avec Pydantic (robuste)
recette = RecetteStructuree.model_validate_json(response.content)
print(recette.nom) # Garanti exister et être une string
temps = recette.temps_preparation # Garanti être un intNote : Le schéma Pydantic sert de contrat entre l’API et le code. Toute modification du modèle sera détectée par le type checker (mypy, Pylance).
Exemple guidé : extracteur d’événements avec contraintes réelles et validation croisée
Contribution étudiante de @Daphneej91 (PR #18488), intégrée comme exemple guidé.
Un Field(description=...) sur un champ str aide le modèle mais n’impose aucune valeur : sans contrainte de type, le validateur accepterait priorite="banane" et date="pas-une-date". Cet exemple définit un modèle EvenementExtrait qui contraint réellement ses valeurs :
Literal[...](ensemble fermé) pour la priorité et le type d’événement ;- le type
datepourdate_evenementetdate_reference; - un
field_validatorqui rejette les champs texte vides ; - un
model_validatorqui vérifie la cohérence croisée des champs : un événement à venir ne peut pas être antérieur à la date de référence.
L’exemple instancie le modèle directement (sans appel LLM) : la validation croisée est ici purement Pydantic. La date de référence matérialise « aujourd’hui » fourni par l’appelant, comme recommandé lorsqu’un texte contient des expressions relatives (« lundi prochain »).
👉 L’exemple guidé qui suit redéfinit EvenementExtrait et complète avec trois observables supplémentaires : l’exposition des enums dans le JSON Schema, le rejet effectif des valeurs invalides, et la résolution des dates relatives contre une date_reference.
# Exemple guide : extracteur d'evenements avec contraintes reelles (contribution @Daphneej91, PR #18488)
from typing import List, Literal
from datetime import date
from pydantic import BaseModel, Field, field_validator, model_validator
# Modele Pydantic qui CONTRAINT reellement priorite, type et date
class EvenementExtrait(BaseModel):
titre: str = Field(description="Titre de l'evenement")
type_evenement: Literal["reunion", "deadline", "livraison", "presentation"]
date_evenement: date = Field(description="Date absolue au format YYYY-MM-DD, resolue contre date_reference")
priorite: Literal["haute", "moyenne", "basse"]
date_reference: date = Field(description="Date du jour fournie pour resoudre les expressions relatives")
participants: List[str] = Field(description="Personnes ou groupes impliques")
description: str = Field(description="Description breve")
@field_validator("titre", "description")
@classmethod
def non_vide(cls, v: str) -> str:
if not v.strip():
raise ValueError("champ texte vide")
return v.strip()
@model_validator(mode="after")
def date_coherente(self):
# Un evenement a venir ne peut pas etre anterieur a la date du jour
if self.date_evenement < self.date_reference:
raise ValueError(
f"date_evenement {self.date_evenement} anterieure a date_reference {self.date_reference}")
return self
# Test avec un exemple : "Reunion de sprint lundi prochain a 14h avec l'equipe dev."
# date_reference = lundi 2026-03-30, donc "lundi prochain" = 2026-04-06
evenement = EvenementExtrait(
titre="Reunion de sprint",
type_evenement="reunion",
date_evenement="2026-04-06",
priorite="moyenne",
date_reference="2026-03-30",
participants=["equipe dev"],
description="Reunion de sprint a 14h",
)
print(f"{evenement.date_evenement} | {evenement.type_evenement} | {evenement.priorite} | {evenement.titre}")
print(f"Participants: {', '.join(evenement.participants)}")2026-04-06 | reunion | moyenne | Reunion de sprint
Participants: equipe dev
Exemple guidé : valider réellement la priorité, le type et la date
L’exemple guidé ci-dessus contraint déjà les valeurs (Literal, date) et vérifie la cohérence croisée des champs via un model_validator. Celui-ci redéfinit le même modèle (sans validateurs croisés, participants par défaut) pour montrer trois autres observables :
- Le JSON Schema exposer les enums (
Literal) — contraintes réelles transmises au modèle ; - Le valideur rejeter une priorité inconnue et une date non parsable (ce que
Field(description=...)laissait passer) ; - Comment résoudre une expression relative (« lundi prochain », « 30 mars ») contre une
date_referencefournie explicitement, et signaler une expression ambiguë.
Extraction et résolution sont ici volontairement séparées : la résolution d’une date relative est un ajout pédagogique (déterministe, sans appel LLM), présenté avant la section 5 où extract_structured() fait l’appel réseau.
# Exemple guide : contraintes reelles (Literal + date), resolution des dates relatives
# Note : ce second exemple REDEFINIT EvenementExtrait (sans validateurs croises,
# participants par defaut) pour se concentrer sur le schema expose et les dates relatives.
from typing import List, Literal
from datetime import date, timedelta
from pydantic import BaseModel, Field, ValidationError
class EvenementExtrait(BaseModel):
titre: str = Field(description="Titre de l'evenement")
type_evenement: Literal["reunion", "deadline", "livraison", "presentation"]
date_evenement: date = Field(description="Date au format YYYY-MM-DD, resolue contre date_reference")
priorite: Literal["haute", "moyenne", "basse"]
date_reference: date = Field(description="Date du jour fournie pour resoudre les expressions relatives")
participants: List[str] = Field(default_factory=list, description="Personnes impliquees")
description: str = Field(description="Description breve")
print("1. Le JSON schema expose les contraintes (enums) :")
schema = EvenementExtrait.model_json_schema()
print(" priorite :", schema["properties"]["priorite"]["enum"])
print(" type :", schema["properties"]["type_evenement"]["enum"])
print(" date :", schema["properties"]["date_evenement"]["type"], "/", schema["properties"]["date_evenement"]["format"])
print()
print("2. Le validateur REJETTE ce que Field(description=...) laissait passer :")
base = dict(titre="Reunion", type_evenement="reunion", date_evenement="2026-04-06",
priorite="haute", date_reference="2026-03-30", description="Sprint planning")
try:
EvenementExtrait(**{**base, "priorite": "banane"})
print(" priorite 'banane' : ACCEPTE (probleme, aurait du rejeter)")
except ValidationError as e:
print(" priorite 'banane' : REJETE ->", e.errors()[0]["loc"], e.errors()[0]["type"])
try:
EvenementExtrait(**{**base, "date_evenement": "pas-une-date"})
print(" date invalide : ACCEPTE (probleme, aurait du rejeter)")
except ValidationError as e:
print(" date invalide : REJETE ->", e.errors()[0]["loc"], e.errors()[0]["type"])
print()
print("3. Resolution d'une expression relative contre une date_reference :")
JOURS = {"lundi": 0, "mardi": 1, "mercredi": 2, "jeudi": 3, "vendredi": 4, "samedi": 5, "dimanche": 6}
MOIS = {"janvier": 1, "fevrier": 2, "mars": 3, "avril": 4, "mai": 5, "juin": 6,
"juillet": 7, "aout": 8, "septembre": 9, "octobre": 10, "novembre": 11, "decembre": 12}
def resoudre_date_relative(expr, ref):
expr = expr.lower().strip()
if "aujourd" in expr:
return ref
mots = expr.split()
if len(mots) >= 2 and mots[1] == "prochain" and mots[0] in JOURS:
jours = (JOURS[mots[0]] - ref.weekday()) % 7
jours = 7 if jours == 0 else jours
return ref + timedelta(days=jours)
if mots and mots[0] in JOURS:
return ref + timedelta(days=(JOURS[mots[0]] - ref.weekday()) % 7)
if len(mots) >= 2 and mots[0].isdigit() and mots[1] in MOIS:
try:
return date(ref.year, MOIS[mots[1]], int(mots[0]))
except ValueError:
raise ValueError("date impossible : %s" % expr)
raise ValueError("expression relative non resolvable (ambigu) : %s" % expr)
ref = date(2026, 3, 30) # un lundi
print(" date_reference =", ref.isoformat())
for expr in ["lundi prochain", "vendredi", "30 mars", "le 30 mars"]:
try:
d = resoudre_date_relative(expr, ref)
print(" %r -> %s" % (expr, d.isoformat()))
except ValueError as err:
print(" %r -> %s" % (expr, err))
print()
print("4. Modele valide avec la date resolue (champ date_evenement en type date) :")
ev = EvenementExtrait(titre="Reunion de sprint", type_evenement="reunion",
date_evenement=resoudre_date_relative("lundi prochain", ref),
priorite="haute", date_reference=ref,
participants=["Alice", "Bob"], description="Sprint planning")
print(" evenement valide :", ev.titre, "|", ev.date_evenement.isoformat(), "|", ev.priorite)1. Le JSON schema expose les contraintes (enums) :
priorite : ['haute', 'moyenne', 'basse']
type : ['reunion', 'deadline', 'livraison', 'presentation']
date : string / date
2. Le validateur REJETTE ce que Field(description=...) laissait passer :
priorite 'banane' : REJETE -> ('priorite',) literal_error
date invalide : REJETE -> ('date_evenement',) date_from_datetime_parsing
3. Resolution d'une expression relative contre une date_reference :
date_reference = 2026-03-30
'lundi prochain' -> 2026-04-06
'vendredi' -> 2026-04-03
'30 mars' -> 2026-03-30
'le 30 mars' -> expression relative non resolvable (ambigu) : le 30 mars
4. Modele valide avec la date resolue (champ date_evenement en type date) :
evenement valide : Reunion de sprint | 2026-04-06 | haute
4. Cas d’Usage Avancés
Les Structured Outputs excellent dans les scénarios nécessitant une structure de données fiable :
Extraction de Données
- Documents : CVs, factures, contrats
- Emails : Extraction expéditeur, sujet, actions requises
- Articles : Métadonnées (titre, auteur, date, mots-clés)
Génération de Formulaires
- Questionnaires dynamiques : Générer des questions avec types de réponse
- Configurations : Paramètres applicatifs structurés
Intégrations API
- Réponses normalisées : Format uniforme pour frontend
- Insertion base de données : Validation avant INSERT/UPDATE
- Webhooks : Payload structuré pour intégrations tierces
Workflows Multi-Étapes
- Agents : Chaque étape retourne une structure validée
- Pipelines : Chaînage de transformations type-safe
Exemple Réaliste : Extraction de CV
L’extraction de données structurées depuis du texte non structuré est l’un des cas d’usage les plus puissants des Structured Outputs.
Défi technique : Le mode strict d’OpenAI impose que tous les champs soient dans required. Pour simuler des champs optionnels (comme email ou telephone qui peuvent être absents d’un CV), nous utilisons des valeurs par défaut ("" pour les strings) au lieu de Optional[str].
Cette approche permet de : - Garantir que le schéma est conforme aux exigences OpenAI - Gérer l’absence de certaines informations avec des valeurs par défaut - Maintenir la type safety Pydantic
Résultat attendu : Le modèle va parser le texte du CV et remplir automatiquement toutes les structures (expériences, formations, compétences) en respectant strictement le schéma Pydantic.
# Extraction structurée de CV
# Note: Pour Structured Outputs strict, tous les champs doivent être dans required.
# Les champs peuvent avoir des valeurs par défaut pour être "optionnels" en pratique.
class Experience(BaseModel):
entreprise: str
poste: str
dates: str
description: str = "" # Valeur par défaut au lieu d'Optional
class Formation(BaseModel):
etablissement: str
diplome: str
annee: int
class CVStructure(BaseModel):
nom: str
email: str = "" # Valeur par défaut
telephone: str = "" # Valeur par défaut
experiences: List[Experience]
formations: List[Formation]
competences: List[str]
texte_cv = """
Jean Dupont - jean.dupont@email.com - 06 12 34 56 78
EXPÉRIENCES:
- Développeur Senior chez TechCorp (2020-2023): Développement d'applications Python
- Développeur Junior chez StartupXYZ (2018-2020): Maintenance de bases de données
FORMATION:
- Master Informatique, Université Paris-Saclay, 2018
- Licence Informatique, Université Lyon 1, 2016
COMPÉTENCES: Python, SQL, Docker, Git, Machine Learning
"""
# Générer le schema et ajouter additionalProperties: False
schema = CVStructure.model_json_schema()
schema = add_additional_properties_false(schema)
response = client.chat.completions.create(
model=DEFAULT_MODEL,
messages=[{"role": "user", "content": f"Extrais les informations de ce CV:\n\n{texte_cv}"}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "cv_extraction",
"strict": True,
"schema": schema
}
}
)
cv = CVStructure.model_validate_json(response.choices[0].message.content)
print("=== CV STRUCTURÉ ===\n")
print(f"Candidat: {cv.nom}")
print(f"Contact: {cv.email} | {cv.telephone}")
print(f"\nExpériences ({len(cv.experiences)}):")
for exp in cv.experiences:
print(f" - {exp.poste} @ {exp.entreprise} ({exp.dates})")
if exp.description:
print(f" -> {exp.description}")
print(f"\nFormations ({len(cv.formations)}):")
for form in cv.formations:
print(f" - {form.diplome} ({form.etablissement}, {form.annee})")
print(f"\nCompétences: {', '.join(cv.competences)}")=== CV STRUCTURÉ ===
Candidat: Jean Dupont
Contact: jean.dupont@email.com | 06 12 34 56 78
Expériences (2):
- Développeur Senior @ TechCorp (2020-2023)
-> Développement d'applications Python
- Développeur Junior @ StartupXYZ (2018-2020)
-> Maintenance de bases de données
Formations (2):
- Master Informatique (Université Paris-Saclay, 2018)
- Licence Informatique (Université Lyon 1, 2016)
Compétences: Python, SQL, Docker, Git, Machine Learning
Analyse de l’extraction structurée
Le modèle a parfaitement parsé le CV non structuré en objets Pydantic typés :
Données extraites avec succès :
| Catégorie | Résultat | Précision |
|---|---|---|
| Identité | Nom, email, téléphone | 100% |
| Expériences | 2 postes avec dates et descriptions | Chronologie respectée |
| Formations | 2 diplômes avec établissements et années | Ordre anti-chronologique |
| Compétences | 5 compétences techniques | Liste complète |
Points techniques importants :
Gestion des champs “optionnels” : Les champs
email,telephone,descriptionont des valeurs par défaut""au lieu d’êtreOptional[str]. Cela respecte l’exigence OpenAI que tous les champs soient dansrequireden mode strict.Objets imbriqués :
List[Expérience]etList[Formation]sont correctement gérés. Le JSON Schema généré par Pydantic inclut des définitions d’objets imbriqués ($defs).Validation automatique :
CVStructure.model_validate_json()garantit que chaque expérience a tous les champs requis et que les années sont bien des entiers.
Cas d’usage production : - ATS (Applicant Tracking System) : Parsing automatique de milliers de CVs - Matching candidat-poste : Extraction des compétences pour scoring - Anonymisation : Retrait automatique des données personnelles avant analyse RH - Base de données talents : Insertion directe en base avec schéma validé
5. Gestion des Erreurs
Même avec Structured Outputs, plusieurs types d’erreurs peuvent survenir :
Erreurs API
- Quota dépassé : Limite de tokens ou requêtes
- Modèle non supporté : Structured Outputs requiert gpt-5-mini+
- Schéma invalide : JSON Schema mal formé ou trop complexe
Erreurs de Validation Pydantic
- Type mismatch : Le modèle retourne un type incorrect (rare avec strict mode)
- Contraintes non respectées :
Field(ge=0, le=100)violé - Champs manquants : Champ requis absent (ne devrait pas arriver)
Bonnes Pratiques
- Try/except : Toujours wrapper les appels API
- Validation explicite : Utiliser
model_validate_json()plutôt quejson.loads() - Logging : Logger les erreurs pour debug
- Fallback : Prévoir une stratégie de secours (retry, valeur par défaut)
Fonction Utilitaire Réutilisable
Pour éviter de répéter le même code de gestion d’erreurs et de génération de schéma, créons une fonction générique qui :
- Accepte n’importe quel modèle Pydantic
- Génère le JSON Schema adapté
- Gère les erreurs API et de validation
- Retourne une instance validée ou
None
Cette fonction peut ensuite être réutilisée pour tous les cas d’extraction structurée du projet.
Test de la fonction : Évaluons un texte simple avec un modèle incluant des contraintes de validation (conint(ge=0, le=100) pour forcer un score entre 0 et 100).
# Gestion robuste des erreurs
from pydantic import ValidationError, conint
def extract_structured(prompt: str, model_class: type[BaseModel], model: str = None):
"""
Fonction utilitaire pour extraction structurée avec gestion d'erreurs.
Args:
prompt: Prompt utilisateur
model_class: Classe Pydantic pour validation
model: Modèle OpenAI à utiliser (défaut: DEFAULT_MODEL)
Returns:
Instance validée de model_class ou None en cas d'erreur
"""
if model is None:
model = DEFAULT_MODEL
try:
# Générer le schema et ajouter additionalProperties: False (requis par OpenAI)
schema = model_class.model_json_schema()
schema = add_additional_properties_false(schema)
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": prompt}],
response_format={
"type": "json_schema",
"json_schema": {
"name": model_class.__name__.lower(),
"strict": True,
"schema": schema
}
}
)
return model_class.model_validate_json(response.choices[0].message.content)
except ValidationError as e:
print(f"Erreur de validation Pydantic: {e}")
return None
except Exception as e:
print(f"Erreur API: {e}")
return None
# Test de la fonction utilitaire
class Evaluation(BaseModel):
score: conint(ge=0, le=100) = Field(description="Score de 0 à 100")
commentaire: str = Field(description="Commentaire détaillé")
points_forts: List[str] = Field(description="Points forts identifiés")
points_amelioration: List[str] = Field(description="Points à améliorer")
eval_result = extract_structured(
"Évalue ce texte: 'Python est un langage de programmation populaire.'",
Evaluation
)
if eval_result:
print("=== ÉVALUATION STRUCTURÉE ===\n")
print(f"Score: {eval_result.score}/100")
print(f"Commentaire: {eval_result.commentaire}")
print("\nPoints forts:")
for pf in eval_result.points_forts:
print(f" + {pf}")
print("\nPoints d'amélioration:")
for pa in eval_result.points_amelioration:
print(f" - {pa}")
else:
print("Échec de l'extraction structurée")=== ÉVALUATION STRUCTURÉE ===
Score: 85/100
Commentaire: Phrase correcte, claire et factuelle : elle affirme simplement que Python est un langage de programmation populaire. La formulation est grammaticalement juste et concise. Pour être plus informative et précise, on pourrait indiquer en quoi ou auprès de qui il est populaire (par ex. développeurs web, data science) ou apporter une source/mesure (classements, statistiques).
Points forts:
+ Grammaticalement correcte et bien ponctuée
+ Clarté et concision — le message est immédiatement compréhensible
+ Ton neutre et factuel
Points d'amélioration:
- Trop générale — manque de contexte (pour quel public ou quel usage ?)
- Aucune preuve ou précision (classement, domaines d'utilisation, exemples)
- Formulation peu nuancée : «populaire» est vague ; on pourrait préciser le degré («très populaire») ou citer des sources
Interprétation des résultats
L’évaluation structurée démontre la puissance de la validation Pydantic combinée aux Structured Outputs :
Résultats obtenus : - Score : 88/100 (validé automatiquement dans la plage 0-100 grâce à conint) - Analyse qualitative : Le modèle a identifié que le texte est factuel mais manque de détails - Points forts : Concision, clarté de l’affirmation - Axes d’amélioration : Enrichissement avec exemples et justifications
Validation robuste :
La fonction extract_structured() encapsule les bonnes pratiques :
- Génération automatique du schéma :
model_json_schema()évite l’écriture manuelle - Ajout de
additionalProperties: false: Requis par OpenAI strict mode - Gestion des erreurs API : Timeouts, quotas, modèles invalides
- Validation Pydantic :
model_validate_json()garantit type safety - Retour gracieux :
Noneen cas d’erreur au lieu d’exception non gérée
Avantage production : Cette fonction peut être réutilisée pour tous les cas d’extraction structurée du projet. Créer une librairie partagée avec des modèles Pydantic communs (feedback, facture, événement, etc.) accélère le développement.
6. Exemple Complet : Analyseur de Feedback Produit
Cas d’usage réaliste : analyser des feedbacks clients et les structurer pour analyse.
Objectifs : - Extraire sentiment, catégorie, priorité - Identifier les fonctionnalités mentionnées - Détecter les bugs signalés - Structurer pour insertion en base de données
Simulation de feedback client : Le modèle va analyser le texte, identifier automatiquement : - Le sentiment (ici mixte : positif sur l’appli, négatif sur le bug) - La catégorie (probablement bug_report) - La priorité (haute car bloquant pour le travail) - Les bugs et suggestions implicites
# Analyseur de feedback produit
class Bug(BaseModel):
description: str = Field(description="Description du bug")
severite: str = Field(description="Sévérité: critique, haute, moyenne, basse")
class FeedbackAnalyse(BaseModel):
sentiment: str = Field(description="Sentiment général: positif, négatif, neutre, mixte")
categorie: str = Field(description="Catégorie: feature_request, bug_report, question, praise, complaint")
priorite: str = Field(description="Priorité: haute, moyenne, basse")
resume: str = Field(description="Résumé en une phrase")
fonctionnalites_mentionnees: List[str] = Field(description="Fonctionnalités citées")
bugs_detectes: List[str] = Field(description="Bugs identifiés")
suggestions: List[str] = Field(description="Suggestions d'amélioration")
actions_recommandees: List[str] = Field(description="Actions à entreprendre")
feedback_client = """
J'adore l'application en général, mais le bouton d'export ne fonctionne pas depuis la dernière mise à jour.
J'ai essayé plusieurs fois et rien ne se passe. C'est vraiment frustrant car j'en ai besoin pour mon travail.
Sinon l'interface mobile pourrait être améliorée, elle est un peu confuse.
"""
analyse = extract_structured(
f"Analyse ce feedback client et structure-le:\n\n{feedback_client}",
FeedbackAnalyse
)
if analyse:
print("=== ANALYSE DE FEEDBACK ===\n")
print(f"Sentiment: {analyse.sentiment}")
print(f"Catégorie: {analyse.categorie}")
print(f"Priorité: {analyse.priorite}")
print(f"\nRésumé:\n{analyse.resume}")
print(f"\nFonctionnalités mentionnées:")
for feat in analyse.fonctionnalites_mentionnees:
print(f" • {feat}")
if analyse.bugs_detectes:
print(f"\nBugs détectés:")
for bug in analyse.bugs_detectes:
print(f" 🐛 {bug}")
if analyse.suggestions:
print(f"\nSuggestions:")
for sug in analyse.suggestions:
print(f" 💡 {sug}")
print(f"\nActions recommandées:")
for action in analyse.actions_recommandees:
print(f" → {action}")=== ANALYSE DE FEEDBACK ===
Sentiment: mixte
Catégorie: bug_report
Priorité: haute
Résumé:
Utilisateur apprécie l'application mais le bouton d'export ne fonctionne plus depuis la dernière mise à jour et l'interface mobile est jugée confuse.
Fonctionnalités mentionnées:
• Bouton d'export
• Interface mobile
• Application (usage général)
Bugs détectés:
🐛 Le bouton d'export est inopérant depuis la dernière mise à jour — l'utilisateur clique plusieurs fois et rien ne se passe
Suggestions:
💡 Corriger le dysfonctionnement du bouton d'export en priorité
💡 Améliorer la clarté et l'ergonomie de l'interface mobile
Actions recommandées:
→ Reproduire le bug sur les environnements de test avec la version de la dernière mise à jour
→ Récupérer les informations complémentaires auprès de l'utilisateur (version de l'app, OS, modèle de téléphone, étapes exactes, éventuels messages d'erreur)
→ Consulter les logs et ajouter du logging côté export pour identifier la cause
→ Déployer un correctif / hotfix pour le bouton d'export si la cause est critique
→ Prévoir une revue UX de l'interface mobile et prioriser des améliorations ergonomiques
→ Communiquer un message clair à l'utilisateur (accusé de réception, délai estimé de correction)
Interprétation de l’analyse de feedback
Le modèle a correctement identifié :
| Dimension | Valeur détectée | Justification |
|---|---|---|
| Sentiment | Mixte | “J’adore” (positif) vs “frustrant” (négatif) |
| Catégorie | bug_report |
Dysfonctionnement signalé explicitement |
| Priorité | Haute | Impact métier direct (“besoin pour mon travail”) |
| Bug principal | Bouton export non fonctionnel | Depuis dernière mise à jour |
| Suggestion | Amélioration interface mobile | Point secondaire mais identifié |
Actions extraites automatiquement : 1. Corriger le bug d’export (prioritaire) 2. Planifier amélioration UX mobile
Ce type de structuration permet : - Tri automatique par priorité dans un système de ticketing - Calcul de métriques (distribution sentiments, catégories) - Routing intelligent vers les équipes (dev vs UX vs support) - Alimentation d’un dashboard produit en temps réel
Note production : En contexte réel, ajouter une validation humaine pour les feedbacks haute priorité avant création automatique de tickets.
Exemple guidé : analyseur de produits e-commerce
Contribution étudiante de @Daphneej91 (PR #18488), intégrée comme exemple guidé.
Cet exemple extrait et structure les informations d’une fiche produit e-commerce, avec des objets imbriqués (caractéristiques techniques, avis clients) et des contraintes de validation. Le schéma ProduitAnalyse est défini avec Pydantic, puis les données sont extraites d’une description produit non structurée.
Points clés : - La fonction extract_structured() déjà définie dans ce notebook fait l’appel API et la validation - Le modèle inclut : nom, prix (confloat(gt=0)), catégorie, liste de caractéristiques (nom + valeur), note moyenne (0-5 avec confloat), et liste d’avis (auteur, commentaire, note conint(ge=1, le=5)) - Texte de test (légèrement enrichi d’un prix par rapport à l’énoncé d’origine) : “MacBook Pro M4 - 1999 EUR. Ordinateur portable Apple avec puce M4 Pro, 18Go RAM, 512Go SSD. Ecran Liquid Retina XDR 14 pouces. Note : 4.7/5. Avis : ‘Excellent pour le dev’ par Alice (5/5), ‘Un peu cher’ par Bob (4/5).”
# Exemple guide : Analyseur de produits e-commerce
# (contribution etudiante de @Daphneej91, PR #18488)
# Etape 1 : Definir les modeles Pydantiques imbriques
from pydantic import confloat
from pydantic import BaseModel, Field, confloat, conint
class Caracteristique(BaseModel):
nom: str = Field(description="Nom de la caracteristique")
valeur: str = Field(description="Valeur de la caracteristique")
class Avis(BaseModel):
auteur: str = Field(description="Nom de l'auteur de l'avis")
commentaire: str = Field(description="Contenu de l'avis")
note: conint(ge=1, le=5) = Field(description="Note sur 5")
class ProduitAnalyse(BaseModel):
nom: str = Field(description="Nom du produit")
prix: confloat(gt=0) = Field(description="Prix en euros")
categorie: str = Field(description="Catégorie du produit")
caracteristiques: list[Caracteristique] = Field(description="Caractéristiques")
note_moyenne: confloat(ge=0, le=5) = Field(description="Note moyenne sur 5")
avis: list[Avis] = Field(description="Avis clients")
# Etape 2 : Tester avec un texte de produit
texte_produit = (
"MacBook Pro M4 - 1999 EUR. Ordinateur portable Apple avec puce M4 Pro, "
"18Go RAM, 512Go SSD. Ecran Liquid Retina XDR 14 pouces. Note : 4.7/5. "
"Avis : 'Excellent pour le dev' par Alice (5/5), 'Un peu cher' par Bob (4/5)."
)
produit = extract_structured(f"Extrais les infos de ce produit:\n{texte_produit}", ProduitAnalyse)
# Etape 3 : Afficher le resultat structure
if produit:
print(f"Produit: {produit.nom} ({produit.prix} EUR) note : {produit.note_moyenne}")
print("Caractéristiques :")
for carac in produit.caracteristiques:
print(f" {carac.nom}: {carac.valeur}")
print("Avis :")
for a in produit.avis:
print(f" {a.auteur}: {a.note}/5 : {a.commentaire}")Produit: MacBook Pro M4 (1999.0 EUR) note : 4.7
Caractéristiques :
Marque: Apple
Puce: M4 Pro
RAM: 18 Go
Stockage: 512 Go SSD
Écran: Liquid Retina XDR 14 pouces
Avis :
Alice: 5/5 : Excellent pour le dev
Bob: 4/5 : Un peu cher
7. Comparaison des Approches
Récapitulatif des différentes méthodes pour obtenir des sorties structurées :
# Tableau comparatif
print("╔════════════════════════════════════════════════════════════════════════════════════╗")
print("║ COMPARAISON DES APPROCHES JSON ║")
print("╠════════════════════════════════════════════════════════════════════════════════════╣")
print("║ Approche │ JSON valide │ Structure garantie │ Type-safe │ Complexité ║")
print("╠════════════════════════════════════════════════════════════════════════════════════╣")
print("║ Prompt classique │ ❌ │ ❌ │ ❌ │ Faible ║")
print("║ JSON mode │ ✅ │ ❌ │ ❌ │ Faible ║")
print("║ JSON Schema manuel │ ✅ │ ✅ │ ❌ │ Élevée ║")
print("║ Pydantic + Structured │ ✅ │ ✅ │ ✅ │ Moyenne ║")
print("╚════════════════════════════════════════════════════════════════════════════════════╝")
print("\nRecommandations :")
print(" • Prototypage rapide → JSON mode")
print(" • Production / APIs → Pydantic + Structured Outputs")
print(" • Insertion BDD → Pydantic + Structured Outputs")
print(" • Workflows complexes → Pydantic + Structured Outputs")╔════════════════════════════════════════════════════════════════════════════════════╗
║ COMPARAISON DES APPROCHES JSON ║
╠════════════════════════════════════════════════════════════════════════════════════╣
║ Approche │ JSON valide │ Structure garantie │ Type-safe │ Complexité ║
╠════════════════════════════════════════════════════════════════════════════════════╣
║ Prompt classique │ ❌ │ ❌ │ ❌ │ Faible ║
║ JSON mode │ ✅ │ ❌ │ ❌ │ Faible ║
║ JSON Schema manuel │ ✅ │ ✅ │ ❌ │ Élevée ║
║ Pydantic + Structured │ ✅ │ ✅ │ ✅ │ Moyenne ║
╚════════════════════════════════════════════════════════════════════════════════════╝
Recommandations :
• Prototypage rapide → JSON mode
• Production / APIs → Pydantic + Structured Outputs
• Insertion BDD → Pydantic + Structured Outputs
• Workflows complexes → Pydantic + Structured Outputs
Exercices à compléter
Les trois exercices suivants mesurent des comportements qu’aucun exemple guidé de ce notebook ne couvre : champs absents ou inventés, natures d’objets mélangées dans une même liste, et taux de réussite sur des entrées variées. Ils réutilisent le client, la fonction extract_structured() et les contraintes Pydantic vus plus haut.
Exercice 1 : Champs optionnels en mode strict — information absente ou inventée ?
Tous les schémas de ce notebook utilisent des champs tous obligatoires. En mode strict, OpenAI impose que chaque champ figure dans required, mais un champ peut rester nullable : son type devient anyOf: [{"type": "string"}, {"type": "null"}]. La question que cet exercice mesure : quand l’information est absente du texte, le modèle renvoie-t-il null ou invente-t-il une valeur plausible ?
Objectif : définir un modèle ContactExtrait avec des champs obligatoires (nom, telephone) et des champs optionnels (email, entreprise, ville en Optional[str] = None), l’appliquer à deux textes — l’un complet, l’autre où certaines informations manquent — et vérifier champ par champ que les informations absentes valent None, pas une valeur hallucinée.
Indices : - Optional[str] = None produit exactement le anyOf string/null accepté par le mode strict - Précisez dans chaque Field(description=...) : « null si l’information n’apparaît pas dans le texte » — sans cette consigne, le modèle n’a aucune raison de choisir null - extract_structured() renvoie l’instance validée ou None en cas d’échec - Comparez le nombre de champs renseignés entre le texte complet et le texte lacunaire, et signalez toute valeur qui n’apparaît nulle part dans le texte source
# Exercice 1 : Champs optionnels en mode strict -- information absente ou inventee ?
# TODO etudiant : definir ContactExtrait et mesurer champs absents vs inventes
# Etape 1 : Definir le modele (champs requis ET optionnels)
# TODO etudiant : class ContactExtrait(BaseModel) avec nom et telephone requis,
# puis email, entreprise et ville en Optional[str] = None
# (description de chaque champ optionnel : "null si l'information n'apparait pas dans le texte")
# Etape 2 : Extraire depuis un texte complet puis un texte lacunaire
texte_complet = ("Bonjour, je m'appelle Ines Benali, joignable au 06 98 76 54 32. "
"Email : ines.benali@exemple.fr, societe Atelier Numerique, Lyon. "
"Je souhaite un devis.")
texte_lacunaire = ("Bonjour, je m'appelle Ines Benali, joignable au 06 98 76 54 32. "
"Je souhaite un devis.")
# TODO etudiant : appeler extract_structured(...) sur les deux textes avec ContactExtrait
# Etape 3 : Comparer champ par champ
# TODO etudiant : pour chaque champ optionnel, afficher sa valeur (ou None) pour les deux textes,
# compter les champs renseignes dans chaque cas, et signaler toute valeur inventee
# (une valeur qui n'apparait nulle part dans le texte source est une hallucination)
resultat_complet = None # TODO etudiant
resultat_lacunaire = None # TODO etudiant
print("Exercice a completer")Exercice a completer
Exercice 2 : Union de types — des objets de natures différentes dans une même liste
Chaque liste de ce notebook contient des objets homogènes (des Ingredient, des Avis, des Caracteristique). Un journal de modifications mélange des entrées de natures différentes : fonctionnalités, correctifs, dépréciations. Structurer cette hétérogénéité demande une union de types : List[Union[A, B, C]] produit dans le schéma un items avec anyOf — une branche par nature d’objet.
Objectif : définir trois modèles Pydantic (NouvelleFonctionnalite, CorrectionBug, Deprecation), chacun portant un champ type: Literal[...] distinct (“feature”, “bugfix”, “deprecation”), les réunir dans un modèle Changelog dont le champ entrees est une liste de cette union, extraire le changelog ci-dessous, puis vérifier que chaque élément est validé dans la bonne classe (isinstance) et compter chaque nature.
Indices : - Chaque branche doit porter un champ type avec un Literal différent : c’est lui qui garantit qu’une seule branche peut valider un objet donné - Le anyOf généré est accepté par le mode strict ; la fonction add_additional_properties_false() (utilisée par extract_structured) ajoute additionalProperties: False dans chaque branche - Après model_validate_json, Pydantic rend pour chaque élément l’instance de la bonne classe — vérifiez avec isinstance(entree, NouvelleFonctionnalite) etc. - Comptez les natures (par exemple avec collections.Counter) et confrontez au texte source : 2 fonctionnalités, 2 correctifs, 1 dépréciation
Changelog de test : “Version 2.3.0 — Nouvelle fonctionnalité : export PDF (module Rapports). Correction : crash au démarrage sur Windows, sévérité critique. Dépréciation : l’API v1, remplacée par l’API v2. Nouvelle fonctionnalité : thème sombre (module Interface). Correction : faute de frappe dans les paramètres, sévérité mineure.”
# Exercice 2 : Union de types -- des objets de natures differentes dans une meme liste
# TODO etudiant : definir les trois modeles + l'union, extraire et verifier les classes
# Etape 1 : Definir les trois modeles, chacun avec un champ type: Literal[...] distinct
# TODO etudiant : NouvelleFonctionnalite (type "feature", titre, module),
# CorrectionBug (type "bugfix", titre, severite Literal["critique", "majeure", "mineure"]),
# Deprecation (type "deprecation", element, remplace_par)
# Etape 2 : Definir Changelog (version + entrees en List[Union[...]]) et extraire
changelog_texte = ("Version 2.3.0 - Nouvelle fonctionnalite : export PDF (module Rapports). "
"Correction : crash au demarrage sur Windows, severite critique. "
"Deprecation : l'API v1, remplacee par l'API v2. "
"Nouvelle fonctionnalite : theme sombre (module Interface). "
"Correction : faute de frappe dans les parametres, severite mineure.")
# TODO etudiant : appeler extract_structured(...) avec le modele Changelog
# Etape 3 : Verifier la classe de chaque entree et compter les natures
# TODO etudiant : pour chaque entree, verifier isinstance(...) puis compter les natures
# (collections.Counter) et confronter le decompte au texte source
changelog = None # TODO etudiant
print("Exercice a completer")Exercice a completer
Exercice 3 : Robustesse — taux de réussite d’une extraction sur N textes variés
Les exemples de ce notebook montrent des extractions réussies. En production, la vraie question est mesurable : sur N entrées variées, combien respectent le schéma et les contraintes ? extract_structured() rend déjà None en cas d’échec (erreur API ou ValidationError Pydantic) — il suffit de compter.
Objectif : définir un modèle CritiqueExtraite (sentiment contraint par Literal["positif", "negatif", "neutre", "mixte"], note contrainte par conint(ge=0, le=5)), l’appliquer à une liste de 6 à 8 textes volontairement hétérogènes (très court, familier, ambivalent, hors sujet, très long), compter les succès et les échecs, et afficher le taux de réussite.
Indices : - Réutilisez extract_structured() tel quel : chaque appel renvoie l’instance validée ou None - C’est la variété des textes qui rend la mesure intéressante : incluez au moins un texte sans aucune opinion (que renvoie le modèle pour le sentiment ?) et un texte très long - succes = sum(1 for r in resultats if r is not None), puis affichez le pourcentage - Pour chaque échec, notez la cause visible dans la sortie de extract_structured (erreur de validation vs erreur API)
# Exercice 3 : Robustesse -- taux de reussite d'une extraction sur N textes varies
# TODO etudiant : mesurer le taux de reussite d'une extraction contrainte sur N textes
# Etape 1 : Definir le modele contraint
# TODO etudiant : class CritiqueExtraite(BaseModel) avec
# sentiment: Literal["positif", "negatif", "neutre", "mixte"] et note: conint(ge=0, le=5)
# Etape 2 : Preparer une liste de 6 a 8 textes heterogenes
# TODO etudiant : textes = [...] avec au minimum un texte tres court, un texte familier,
# un texte ambivalent, un texte HORS SUJET (aucune opinion) et un texte tres long
# Etape 3 : Extraire, compter succes et echecs, afficher le taux
# TODO etudiant : boucler avec extract_structured(...), compter les resultats None,
# afficher le taux de reussite en pourcentage et la cause de chaque echec
resultats = None # TODO etudiant
print("Exercice a completer")Exercice a completer
Conclusion et Exercices
Résumé
Les Structured Outputs d’OpenAI révolutionnent l’intégration LLM en production :
✅ Fiabilité : Garantie de conformité au schéma (plus de parsing fragile)
✅ Type Safety : Pydantic apporte validation et auto-complétion
✅ Productivité : Moins de code de validation manuelle
✅ Maintenabilité : Schémas centralisés et réutilisables
Points clés à retenir : - strict: True est obligatoire pour Structured Outputs - Pydantic simplifie drastiquement la définition de schémas - Toujours valider avec .model_validate_json() pour type safety - Gérer les erreurs API et de validation explicitement
Pour aller plus loin
Pour aller plus loin A : Extracteur d’Événements (Débutant)
Créer un modèle Pydantic pour extraire des événements d’un texte : - Titre de l’événement - Date et heure - Lieu - Participants (liste)
Tester avec : “Réunion d’équipe le 15 mars 2025 à 14h30 dans la salle A. Participants : Alice, Bob, Charlie.”
Pour aller plus loin B : Analyseur de Factures (Intermédiaire)
Définir un schéma pour extraire : - Numéro de facture - Date d’émission - Fournisseur (nom, adresse) - Articles (nom, quantité, prix unitaire, total) - Montant total TTC
Générer une fausse facture avec GPT et extraire les données.
Pour aller plus loin C : Générateur de Quiz (Avancé)
Créer un système qui : 1. Génère un quiz sur un sujet donné (5 questions) 2. Chaque question a 4 choix et une réponse correcte 3. Inclut une explication pour chaque réponse 4. Valide que la réponse correcte est bien dans les choix
Bonus : Ajouter des contraintes Pydantic (ex: @field_validator)
Ressources
- Documentation OpenAI Structured Outputs
- Pydantic Documentation
- JSON Schema Reference
- OpenAI Cookbook - Structured Outputs
Prochaine étape : Notebook 4 - Function Calling et Agents