Aller au contenu principal

Cas d'usage avancés

Au-delà des exemples simples

Maintenant que vous maîtrisez Pydantic, Zod et client.chat.parse(), passons à des cas d’usage réels qui illustrent la puissance des Custom Structured Outputs dans des scénarios professionnels.

Cas 1 : Extraction de factures

L’extraction automatique de factures est un classique du traitement documentaire. Voici un schéma complet :

from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum

class Currency(str, Enum):
    eur = "EUR"
    usd = "USD"
    gbp = "GBP"

class LineItem(BaseModel):
    description: str = Field(description="Description de la ligne")
    quantity: float = Field(description="Quantité")
    unit_price: float = Field(description="Prix unitaire HT")
    vat_rate: float = Field(description="Taux de TVA en pourcentage (ex: 20.0)")
    total_ht: float = Field(description="Total HT de la ligne")

class InvoiceData(BaseModel):
    invoice_number: str = Field(description="Numéro de facture")
    date: str = Field(description="Date de facturation au format YYYY-MM-DD")
    due_date: Optional[str] = Field(default=None, description="Date d'échéance")
    vendor_name: str = Field(description="Nom du fournisseur")
    vendor_address: Optional[str] = Field(default=None)
    client_name: str = Field(description="Nom du client")
    line_items: list[LineItem] = Field(description="Lignes de la facture")
    subtotal_ht: float = Field(description="Sous-total HT")
    total_vat: float = Field(description="Montant total de TVA")
    total_ttc: float = Field(description="Total TTC")
    currency: Currency = Field(description="Devise")
    payment_method: Optional[str] = Field(default=None)

# Extraction
response = client.chat.parse(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": """Tu es un expert en comptabilité. Extrais les données de la facture.
Sois précis sur les montants. Calcule les totaux si nécessaire.
Les dates doivent être au format YYYY-MM-DD."""
        },
        {"role": "user", "content": texte_facture}
    ],
    response_format=InvoiceData,
    temperature=0
)

facture = response.choices[0].message.parsed
print(f"Facture {facture.invoice_number}{facture.vendor_name}")
print(f"Total TTC : {facture.total_ttc} {facture.currency.value}")
for item in facture.line_items:
    print(f"  - {item.description}: {item.quantity} x {item.unit_price}€ = {item.total_ht}€ HT")

L’équivalent TypeScript :

import { z } from "zod";

const LineItemSchema = z.object({
  description: z.string(),
  quantity: z.number(),
  unitPrice: z.number().describe("Prix unitaire HT"),
  vatRate: z.number().describe("Taux de TVA en %"),
  totalHt: z.number(),
});

const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  date: z.string().describe("Format YYYY-MM-DD"),
  dueDate: z.string().nullable(),
  vendorName: z.string(),
  clientName: z.string(),
  lineItems: z.array(LineItemSchema),
  subtotalHt: z.number(),
  totalVat: z.number(),
  totalTtc: z.number(),
  currency: z.enum(["EUR", "USD", "GBP"]),
});

const response = await client.chat.parse({
  model: "mistral-large-latest",
  messages: [
    { role: "system", content: "Extrais les données de la facture avec précision." },
    { role: "user", content: invoiceText },
  ],
  responseFormat: InvoiceSchema,
  temperature: 0,
});

const invoice = response.choices[0].message.parsed!;
console.log(`Facture ${invoice.invoiceNumber} — Total: ${invoice.totalTtc} ${invoice.currency}`);

Cas 2 : Analyse de sentiments multi-aspects

Au-delà du simple positif/négatif, une analyse de sentiments professionnelle décompose le sentiment par aspect :

from pydantic import BaseModel, Field

class AspectSentiment(BaseModel):
    aspect: str = Field(description="L'aspect analysé (qualité, prix, service, livraison, etc.)")
    sentiment: str = Field(description="positif, négatif ou neutre")
    score: float = Field(description="Score entre -1.0 (très négatif) et 1.0 (très positif)")
    evidence: str = Field(description="Citation ou extrait du texte qui justifie le sentiment")

class ReviewAnalysis(BaseModel):
    overall_sentiment: str = Field(description="Sentiment global : positif, négatif, mitigé ou neutre")
    overall_score: float = Field(description="Score global entre -1.0 et 1.0")
    aspects: list[AspectSentiment] = Field(description="Analyse par aspect")
    recommendation: bool = Field(description="L'auteur recommande-t-il le produit/service ?")
    key_strengths: list[str] = Field(description="Points forts identifiés")
    key_weaknesses: list[str] = Field(description="Points faibles identifiés")

avis = """
L'hôtel est magnifique, la chambre était spacieuse et propre. Le petit-déjeuner est excellent
avec un large choix. Par contre, le parking est hors de prix (35€/nuit) et le wifi était
instable tout au long du séjour. Le personnel à la réception était très accueillant,
mais le room service a mis plus d'une heure à livrer notre commande.
"""

response = client.chat.parse(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": "Analyse cet avis client en détaillant chaque aspect mentionné."
        },
        {"role": "user", "content": avis}
    ],
    response_format=ReviewAnalysis,
    temperature=0
)

review = response.choices[0].message.parsed
print(f"Sentiment global : {review.overall_sentiment} ({review.overall_score:+.1f})")
for aspect in review.aspects:
    print(f"  [{aspect.score:+.1f}] {aspect.aspect}: {aspect.sentiment}")

Cas 3 : Classification multi-label

Contrairement à la classification simple (un seul label), la classification multi-label attribue plusieurs catégories à un texte :

from pydantic import BaseModel, Field

class LabelScore(BaseModel):
    label: str = Field(description="Nom de la catégorie")
    confidence: float = Field(description="Score de confiance entre 0.0 et 1.0")
    reasoning: str = Field(description="Justification en une phrase")

class MultiLabelClassification(BaseModel):
    primary_label: str = Field(description="Catégorie principale")
    all_labels: list[LabelScore] = Field(
        description="Toutes les catégories applicables avec leur score de confiance"
    )
    requires_human_review: bool = Field(
        description="True si la classification est incertaine (confiance < 0.7)"
    )

text = """
Notre application de paiement mobile présente une faille de sécurité permettant
l'accès aux données bancaires. Plusieurs clients ont signalé des transactions
non autorisées. L'équipe technique est en cours d'investigation.
"""

response = client.chat.parse(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": """Classifie ce texte parmi les catégories suivantes (plusieurs possibles) :
security, finance, technical, legal, customer_support, compliance, fraud, data_privacy.
Attribue un score de confiance à chaque catégorie applicable."""
        },
        {"role": "user", "content": text}
    ],
    response_format=MultiLabelClassification,
    temperature=0
)

result = response.choices[0].message.parsed
print(f"Catégorie principale : {result.primary_label}")
print(f"Revue humaine nécessaire : {'Oui' if result.requires_human_review else 'Non'}")
for label in result.all_labels:
    print(f"  [{label.confidence:.0%}] {label.label}{label.reasoning}")

Pattern commun : wrapper réutilisable

Pour éviter la répétition, créez un wrapper :

from typing import TypeVar, Type
from pydantic import BaseModel

T = TypeVar("T", bound=BaseModel)

def extract(client, text: str, schema: Type[T], system_prompt: str = "") -> T:
    """Extraction structurée réutilisable."""
    messages = []
    if system_prompt:
        messages.append({"role": "system", "content": system_prompt})
    messages.append({"role": "user", "content": text})

    response = client.chat.parse(
        model="mistral-large-latest",
        messages=messages,
        response_format=schema,
        temperature=0
    )
    return response.choices[0].message.parsed

# Utilisation
facture = extract(client, texte_facture, InvoiceData, "Extrais les données de la facture.")
review = extract(client, avis_client, ReviewAnalysis, "Analyse cet avis client.")
classification = extract(client, ticket, MultiLabelClassification, "Classifie ce texte.")

Points clés à retenir

  • Les schémas imbriqués (list[LineItem], list[AspectSentiment]) permettent des analyses complexes
  • Les enums (Currency, catégories) contraignent les valeurs possibles
  • Le champ evidence / reasoning force le modèle à justifier ses choix
  • Un wrapper générique réduit la duplication de code
  • temperature=0 est systématique pour ces cas d’usage en production