Aller au contenu principal

Extraction de métadonnées structurées

Mis à jour le 29 juillet 2026

Extraire exactement ce dont vous avez besoin

L’annotation de document ne se limite pas à récupérer du texte brut. Elle produit des données structurées, typées et validées, directement exploitables dans vos systèmes : une ligne insérée en base, un champ comparé à un référentiel, un montant contrôlé avant paiement. Toute la difficulté se déplace alors vers la conception du schéma, et c’est le sujet de cette leçon.

Structurer plutôt qu’aplatir

Un schéma d’extraction gagne à refléter la réalité du document. Une facture émane d’une entité juridique qui possède une raison sociale, un SIRET et une adresse : plutôt que d’aligner huit champs emetteur_rue, emetteur_ville, emetteur_siret, vous décrivez une Adresse, puis une EntiteJuridique qui la contient, et vous réutilisez ces briques pour l’émetteur comme pour le destinataire. L’énumération TypeDocument joue un rôle voisin : elle interdit au modèle d’inventer une catégorie et vous garantit une valeur exploitable dans un if ou un routage. Les champs Optional disent explicitement ce qui a le droit d’être absent, et les valeurs par défaut — default="France" pour le pays — évitent de faire deviner l’évident.

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

class TypeDocument(str, Enum):
    FACTURE = "facture"
    CONTRAT = "contrat"
    RAPPORT = "rapport"
    FORMULAIRE = "formulaire"
    AUTRE = "autre"

class Adresse(BaseModel):
    rue: Optional[str] = Field(None, description="Numéro et rue")
    code_postal: Optional[str] = Field(None, description="Code postal")
    ville: Optional[str] = Field(None, description="Ville")
    pays: str = Field(default="France", description="Pays")

class EntiteJuridique(BaseModel):
    nom: str = Field(..., description="Raison sociale")
    siret: Optional[str] = Field(None, description="Numéro SIRET")
    adresse: Optional[Adresse] = Field(None, description="Adresse postale")
    email: Optional[str] = Field(None, description="Adresse email")

class ExtractionDocument(BaseModel):
    type_document: TypeDocument = Field(
        ..., description="Type du document"
    )
    date: Optional[str] = Field(
        None, description="Date principale (YYYY-MM-DD)"
    )
    emetteur: EntiteJuridique = Field(
        ..., description="Entité qui émet le document"
    )
    destinataire: Optional[EntiteJuridique] = Field(
        None, description="Entité destinataire"
    )
    resume: str = Field(
        ..., description="Résumé en 2-3 phrases du contenu"
    )
    langue: str = Field(
        ..., description="Langue du document (code ISO 639-1)"
    )

Les données qui se répètent

Beaucoup de documents contiennent des séries : lignes de facturation, clauses, postes de dépense. Vous les modélisez par une list[...] d’un sous-modèle dédié. Le modèle parcourt alors le tableau du document et produit autant d’objets LigneFacture qu’il trouve de lignes, avec la description, la quantité, le prix unitaire et le montant de chacune. Vous récupérez de quoi recalculer le sous-total vous-même et vérifier que le document est cohérent avant de l’accepter.

from pydantic import BaseModel, Field

class LigneFacture(BaseModel):
    description: str = Field(..., description="Description du produit/service")
    quantite: float = Field(..., description="Quantité")
    prix_unitaire: float = Field(..., description="Prix unitaire HT")
    montant: float = Field(..., description="Montant total HT de la ligne")

class FactureDetaillee(BaseModel):
    fournisseur: str = Field(..., description="Nom du fournisseur")
    numero: str = Field(..., description="Numéro de facture")
    date: str = Field(..., description="Date (YYYY-MM-DD)")
    lignes: list[LigneFacture] = Field(
        ..., description="Lignes de la facture"
    )
    sous_total_ht: float = Field(..., description="Sous-total HT")
    tva: float = Field(..., description="Montant TVA")
    total_ttc: float = Field(..., description="Total TTC")
    conditions_paiement: str = Field(
        ..., description="Conditions de paiement mentionnées"
    )

Cases à cocher et formulaires

Les formulaires papier scannés sont le terrain sur lequel le modèle se montre le plus convaincant. Un champ bool associé à l’intitulé de la question suffit : le modèle reconnaît si une case est cochée ou non, extrait la question qui lui correspond, et — point décisif pour un usage médical ou administratif — infère qu’un champ est absent plutôt que d’inventer une valeur, comme l’a démontré l’équipe Mistral. La mention « null si non fourni » dans la description du téléphone n’est pas décorative : elle autorise explicitement le vide.

from pydantic import BaseModel, Field

class ReponseFormulaire(BaseModel):
    question: str = Field(..., description="Intitulé de la question")
    reponse: bool = Field(..., description="True si coché, False sinon")

class FormulaireMedical(BaseModel):
    nom_patient: str = Field(..., description="Nom complet du patient")
    age: Optional[int] = Field(None, description="Âge du patient")
    sexe: Optional[str] = Field(None, description="Sexe (M/F)")
    numero_patient: Optional[str] = Field(None, description="ID patient")
    telephone: Optional[str] = Field(
        None, description="Numéro de téléphone (null si non fourni)"
    )
    reponses: list[ReponseFormulaire] = Field(
        ..., description="Réponses aux questions du formulaire"
    )

Mise en œuvre de bout en bout

Reprenez la facture détaillée et branchez-la sur un document encodé en base64. Le prompt d’annotation complète le schéma en rappelant la nature du fichier et l’attention à porter aux lignes de facturation ; la validation Pydantic au retour transforme la chaîne JSON en objet que vous pouvez parcourir sans crainte.

import os
import json
from mistralai import Mistral
from mistralai.extra import response_format_from_pydantic_model

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

ocr_response = client.ocr.process(
    model="mistral-ocr-latest",
    document={
        "type": "document_base64",
        "document_base64": document_base64
    },
    document_annotation_format=response_format_from_pydantic_model(
        FactureDetaillee
    ),
    document_annotation_prompt=(
        "Ce document est une facture. Extrais toutes les lignes "
        "de facturation avec leurs montants."
    )
)

# Extraction et validation
for page in ocr_response.pages:
    if page.document_annotation:
        facture = FactureDetaillee.model_validate_json(
            page.document_annotation
        )

        print(f"Facture {facture.numero} - {facture.fournisseur}")
        print(f"Date : {facture.date}")
        print(f"\nLignes :")
        for ligne in facture.lignes:
            print(f"  - {ligne.description}: {ligne.quantite} x "
                  f"{ligne.prix_unitaire}€ = {ligne.montant}€")
        print(f"\nTotal TTC : {facture.total_ttc}€")

Une extraction décevante vient presque toujours du schéma, rarement du modèle. Reprenez alors vos descriptions une par une : elles doivent être précises et sans ambiguïté, puisque le modèle n’a qu’elles pour comprendre ce que vous attendez. Rédigez-les dans la langue de vos documents — le français convient parfaitement pour des factures françaises — et réservez les enums aux champs à valeurs limitées, les sous-modèles aux structures que vous réutiliserez d’un schéma à l’autre.

Points clés à retenir

  • Les schémas Pydantic permettent d’extraire des données typées et validées
  • Utilisez des types complexes (listes, enums, modèles imbriqués) pour des extractions précises
  • Le modèle gère les champs optionnels et les cases à cocher avec fiabilité
  • Les descriptions des champs guident l’extraction : soyez précis et explicite
  • Combinez le document_annotation_prompt avec le schéma pour un contexte maximal