Accueil / Articles / Notes pratiques : Ingestion de données pour RAG en production : Créer des systèmes fiables

Notes pratiques : Ingestion de données pour RAG en production : Créer des systèmes fiables

Guide opérationnel des notes pratiques : ingestion de données pour RAG en production – Création de solutions fiables : contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes qui mettent en œuvre ce modèle.

4000 mots

Ce guide reconstitue le parcours allant des matières premières à un système opérationnel pour : l’ingestion de données destinée aux systèmes RAG en environnement de production : création de pipelines de données d’entreprise fiables. L’accent est mis sur des étapes exécutables, des vérifications explicites et du code que vous pouvez intégrer directement dans un dépôt sans devoir deviner son intention. Pour l’étape d’aperçu, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans avoir à deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définissez des vérifications de succès et refusez toute exécution partielle silencieuse.

Qu’est-ce que l’ingestion de données, en réalité ?

Lors de la phase « What Data Ingestion Actually », notez d’abord les exigences : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe de l’environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Changer fréquemment les prompts ne résout que rarement un système de récupération insuffisant.

Le problème du format

Lors de la phase « The Format Problem », notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les flags fonctionnels doivent être regroupés en un seul endroit que les administrateurs peuvent auditer sans devoir lire l’ensemble du système. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Chargeurs de documents : par format

Lorsque vous travaillez sur le format des chargeurs de documents étape par étape, notez d’abord les exigences : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez à la fois le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Documents PDF

Lors de l’étape des documents PDF, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Évaluez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

import fitz  # PyMuPDF
from pathlib import Path

def classify_pdf(pdf_path: str) -> str:
    """
    Classify a PDF as text-native, scanned, or hybrid.
    Returns: 'text', 'scanned', or 'hybrid'
    """
    doc = fitz.open(pdf_path)
    total_pages = len(doc)
    pages_with_text = 0
    total_text_length = 0
    for page in doc:
        text = page.get_text("text").strip()
        if text:
            pages_with_text += 1
            total_text_length += len(text)
    doc.close()
    # No text layer at all: scanned PDF
    if pages_with_text == 0:
        return "scanned"
    # Text exists but average is suspiciously short: likely hybrid with poor OCR
    avg_text_per_page = total_text_length / total_pages
    if avg_text_per_page < 100 and pages_with_text < total_pages * 0.5:
        return "hybrid"
    return "text"

def load_text_pdf(pdf_path: str) -> list[dict]:
    """
    Extract text from a text-native PDF using PyMuPDF.
    Returns a list of page dicts with text and metadata.
    """
    doc = fitz.open(pdf_path)
    pages = []
    for page_num, page in enumerate(doc, start=1):
        text = page.get_text("text")
        pages.append({
            "page_number": page_num,
            "text": text.strip(),
            "source": pdf_path,
            "total_pages": len(doc),
        })
    doc.close()
    return pages
from azure.ai.formrecognizer import DocumentAnalysisClient
from azure.core.credentials import AzureKeyCredential

def load_scanned_pdf_azure(
    pdf_path: str,
    endpoint: str,
    api_key: str,
) -> list[dict]:
    """
    Extract text from a scanned PDF using Azure Document Intelligence.
    Returns pages with text, confidence scores, and table data.
    """
    client = DocumentAnalysisClient(
        endpoint=endpoint,
        credential=AzureKeyCredential(api_key),
    )
    with open(pdf_path, "rb") as f:
        poller = client.begin_analyze_document("prebuilt-read", f)
    result = poller.result()
    pages = []
    for page in result.pages:
        page_text = ""
        low_confidence_words = []
        for line in page.lines:
            page_text += line.content + "\n"
        # Flag words with confidence below threshold
        for word in page.words:
            if word.confidence < 0.85:
                low_confidence_words.append({
                    "word": word.content,
                    "confidence": word.confidence,
                })
        pages.append({
            "page_number": page.page_number,
            "text": page_text.strip(),
            "low_confidence_words": low_confidence_words,
            "needs_review": len(low_confidence_words) > 5,
            "source": pdf_path,
        })
    return pages

DOCX : Documents de politique interne

Lors de l’étape des documents de politique interne DOCX, notez d’abord les conditions requises, le signal de succès ainsi que les conséquences en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Considérez cette étape comme un contrat entre les données d’entrée et les résultats validés. Donnez des noms aux éléments produits, définez des critères de succès et refusez tout achèvement partiel silencieux. Évaluez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

from docx import Document as DocxDocument
from docx.oxml.ns import qn

def load_docx(docx_path: str) -> list[dict]:
    """
    Load a DOCX file preserving heading structure and tables.
    Returns a list of content blocks with type and text.
    """
    doc = DocxDocument(docx_path)
    blocks = []
    current_section = "Introduction"
    for element in doc.element.body:
        # Paragraphs
        if element.tag == qn("w:p"):
            para = element
            style = para.style.name if hasattr(para, "style") else ""
            text = para.text.strip()
            if not text:
                continue
            if style.startswith("Heading"):
                level = int(style.replace("Heading ", "")) if style != "Heading" else 1
                current_section = text
                blocks.append({
                    "type": "heading",
                    "level": level,
                    "text": text,
                    "section": current_section,
                    "source": docx_path,
                })
            else:
                blocks.append({
                    "type": "paragraph",
                    "text": text,
                    "section": current_section,
                    "source": docx_path,
                })
        # Tables
        elif element.tag == qn("w:tbl"):
            table_data = []
            for row in element.findall(f".//{qn('w:tr')}"):
                row_data = []
                for cell in row.findall(f".//{qn('w:tc')}"):
                    cell_text = " ".join(
                        p.text for p in cell.findall(f".//{qn('w:t')}")
                        if p.text
                    )
                    row_data.append(cell_text.strip())
                table_data.append(row_data)
            blocks.append({
                "type": "table",
                "data": table_data,
                "section": current_section,
                "source": docx_path,
            })
    return blocks

HTML : Portails de conformité et wikis internes

Lorsque vous travaillez avec les portails et les étapes de conformité HTML, notez d’abord le contrat : les données requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de l’environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

import httpx
from bs4 import BeautifulSoup

def load_html_page(url: str, headers: dict | None = None) -> dict:
    """
    Load an HTML page and extract main content, stripping boilerplate.
    """
    response = httpx.get(url, headers=headers or {}, follow_redirects=True)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")
    # Remove boilerplate elements common in compliance portals
    for tag in soup(["nav", "header", "footer", "script", "style",
                     "aside", "advertisement", ".cookie-banner",
                     ".sidebar", ".breadcrumb"]):
        tag.decompose()
    # Find main content area (adjust selectors for your portal)
    main_content = (
        soup.find("main")
        or soup.find("article")
        or soup.find(id="content")
        or soup.find(class_="content")
        or soup.find("body")
    )
    text = main_content.get_text(separator="\n", strip=True) if main_content else ""
    return {
        "url": url,
        "title": soup.title.string if soup.title else "",
        "text": text,
        "source": url,
    }

JSON et bases de données relationnelles

Lors de l’étape consacrée aux bases de données JSON et relationnelles, notez d’abord les exigences : entrées requises, signal de succès, et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les administrateurs peuvent auditer sans devoir lire l’ensemble du système. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

import json
import psycopg2
from typing import Any

def load_json_policies(json_path: str, text_fields: list[str]) -> list[dict]:
    """
    Load a JSON array of policy documents, extracting specified text fields.
    Banking example: a JSON export from a policy management system.
    """
    with open(json_path, "r", encoding="utf-8") as f:
        policies = json.load(f)
    documents = []
    for policy in policies:
        # Combine relevant text fields into a single document
        text_parts = []
        for field in text_fields:
            if field in policy and policy[field]:
                text_parts.append(f"{field.replace('_', ' ').title()}: {policy[field]}")
        documents.append({
            "text": "\n\n".join(text_parts),
            "policy_id": policy.get("id"),
            "policy_type": policy.get("type"),
            "effective_date": policy.get("effective_date"),
            "jurisdiction": policy.get("jurisdiction"),
            "source": json_path,
        })
    return documents

def load_from_postgres(
    connection_string: str,
    query: str,
    text_column: str,
    metadata_columns: list[str],
) -> list[dict]:
    """
    Load documents from a PostgreSQL database.
    Banking example: regulatory requirement records from a compliance database.
    """
    conn = psycopg2.connect(connection_string)
    cursor = conn.cursor()
    cursor.execute(query)
    columns = [desc[0] for desc in cursor.description]
    rows = cursor.fetchall()
    cursor.close()
    conn.close()
    documents = []
    for row in rows:
        row_dict = dict(zip(columns, row))
        documents.append({
            "text": str(row_dict.get(text_column, "")),
            **{col: row_dict.get(col) for col in metadata_columns},
            "source": "postgresql",
        })
    return documents

SharePoint et Google Docs : entrepôts de documents d’entreprise

Lors de l’étape SharePoint et Google Docs, notez d’abord les conditions du contrat : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez à la fois le parcours normal et celui de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations ultérieures. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts résout rarement un système de récupération insuffisant. Lors de l’étape SharePoint et Google Docs, notez d’abord les conditions du contrat : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des critères de succès et refusez les terminations partielles silencieuses.

import httpx
from typing import Generator

def load_sharepoint_library(
    tenant_id: str,
    client_id: str,
    client_secret: str,
    site_id: str,
    drive_id: str,
) -> Generator[dict, None, None]:
    """
    Iterate over all DOCX and PDF files in a SharePoint document library.
    Yields file metadata dicts; caller is responsible for downloading and parsing.
    """
    # Get access token
    token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    token_response = httpx.post(token_url, data={
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": "https://graph.microsoft.com/.default",
    })
    access_token = token_response.json()["access_token"]
    headers = {"Authorization": f"Bearer {access_token}"}
    base_url = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}"
    # List files recursively
    next_link = f"{base_url}/root/children"
    while next_link:
        response = httpx.get(next_link, headers=headers)
        data = response.json()
        for item in data.get("value", []):
            if item.get("file") and item["name"].endswith((".pdf", ".docx")):
                yield {
                    "name": item["name"],
                    "download_url": item.get("@microsoft.graph.downloadUrl"),
                    "created": item.get("createdDateTime"),
                    "modified": item.get("lastModifiedDateTime"),
                    "size": item.get("size"),
                    "web_url": item.get("webUrl"),
                }
        next_link = data.get("@odata.nextLink")

Ingestion en flux continu vs par lots

La phase de streaming contre l’ingestion par lots fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

import time
import httpx
from pathlib import Path

class RateLimitedConnector:
    """
    A base connector with retry logic and rate limiting.
    Use this pattern for any external API that imposes rate limits.
    Banking example: connecting to an RBI document portal or a Bloomberg data feed.
    """
    def __init__(
        self,
        base_url: str,
        api_key: str,
        requests_per_minute: int = 60,
        max_retries: int = 3,
        backoff_factor: float = 2.0,
    ):
        self.base_url = base_url
        self.api_key = api_key
        self.min_interval = 60.0 / requests_per_minute
        self.last_request_time = 0.0
        self.max_retries = max_retries
        self.backoff_factor = backoff_factor
    def _wait_for_rate_limit(self):
        elapsed = time.monotonic() - self.last_request_time
        if elapsed < self.min_interval:
            time.sleep(self.min_interval - elapsed)
        self.last_request_time = time.monotonic()
    def fetch(self, endpoint: str, params: dict | None = None) -> dict:
        url = f"{self.base_url}/{endpoint}"
        headers = {"Authorization": f"Bearer {self.api_key}"}
        for attempt in range(self.max_retries):
            self._wait_for_rate_limit()
            try:
                response = httpx.get(url, headers=headers, params=params or {})
                if response.status_code == 429:
                    # Rate limited: back off and retry
                    wait_time = self.backoff_factor ** attempt
                    time.sleep(wait_time)
                    continue
                response.raise_for_status()
                return response.json()
            except httpx.HTTPError as e:
                if attempt == self.max_retries - 1:
                    raise
                time.sleep(self.backoff_factor ** attempt)
        raise RuntimeError(f"Failed to fetch {url} after {self.max_retries} attempts")

Ingestion multimodale

La phase d’ingestion multimodale fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Conservez les configurations en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Séparez la politique de segmentation des données de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent.

Analyse consciente des tableaux

La phase de parsing conscient des tableaux fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours réussi et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure. Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les métriques de qualité évoluent. La phase de parsing conscient des tableaux fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définites des vérifications de succès et refusez toute complétion partielle silencieuse.

import fitz
import pdfplumber

def extract_tables_from_pdf(pdf_path: str) -> list[dict]:
    """
    Extract tables from a PDF using pdfplumber.
    Returns tables as both structured data and serialised text.
    Banking example: extracting capital ratio tables from Basel III compliance reports.
    """
    tables_output = []
    with pdfplumber.open(pdf_path) as pdf:
        for page_num, page in enumerate(pdf.pages, start=1):
            tables = page.extract_tables()
            for table_idx, table in enumerate(tables):
                if not table or len(table) < 2:
                    continue
                # First row as headers
                headers = [str(h).strip() if h else f"col_{i}"
                           for i, h in enumerate(table[0])]
                rows = table[1:]
                # Structured data representation
                structured = []
                for row in rows:
                    row_dict = {}
                    for header, cell in zip(headers, row):
                        row_dict[header] = str(cell).strip() if cell else ""
                    structured.append(row_dict)
                # Text serialisation (markdown table format for LLM consumption)
                header_row = " | ".join(headers)
                separator = " | ".join(["---"] * len(headers))
                data_rows = [
                    " | ".join(str(cell or "").strip() for cell in row)
                    for row in rows
                ]
                serialised = "\n".join([header_row, separator] + data_rows)
                tables_output.append({
                    "page_number": page_num,
                    "table_index": table_idx,
                    "headers": headers,
                    "structured_data": structured,
                    "serialised_text": serialised,
                    "source": pdf_path,
                })
    return tables_output

Ingestion d’images et de graphiques

Pour l’étape d’ingestion d’images et de graphiques, définissez les entrées, le responsable de l’étape ainsi que les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

import fitz
import base64
import httpx
from io import BytesIO

def extract_and_describe_images(
    pdf_path: str,
    openai_api_key: str,
    min_image_size: int = 5000,  # bytes, to skip tiny decorative images
) -> list[dict]:
    """
    Extract images from a PDF and generate text descriptions using GPT-4V.
    Banking example: describing NPA trend charts and regulatory workflow diagrams.
    """
    doc = fitz.open(pdf_path)
    image_docs = []
    for page_num in range(len(doc)):
        page = doc[page_num]
        image_list = page.get_images(full=True)
        for img_idx, img_info in enumerate(image_list):
            xref = img_info[0]
            base_image = doc.extract_image(xref)
            image_bytes = base_image["image"]
            # Skip tiny decorative images
            if len(image_bytes) < min_image_size:
                continue
            # Encode to base64 for the vision API
            image_b64 = base64.b64encode(image_bytes).decode("utf-8")
            image_ext = base_image["ext"]
            # Generate description with GPT-4V
            response = httpx.post(
                "https://api.openai.com/v1/chat/completions",
                headers={"Authorization": f"Bearer {openai_api_key}"},
                json={
                    "model": "gpt-4o",
                    "messages": [
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "text",
                                    "text": (
                                        "This image is from a banking regulatory document. "
                                        "Describe its content precisely, including any numerical "
                                        "values, trends, labels, axes, or process steps shown. "
                                        "If it is a chart, describe what metric is shown, the "
                                        "time period, and the key data points. If it is a "
                                        "diagram, describe the process or relationship shown."
                                    ),
                                },
                                {
                                    "type": "image_url",
                                    "image_url": {
                                        "url": f"data:image/{image_ext};base64,{image_b64}"
                                    },
                                },
                            ],
                        }
                    ],
                    "max_tokens": 400,
                },
            )
            description = response.json()["choices"][0]["message"]["content"]
            image_docs.append({
                "type": "image_description",
                "page_number": page_num + 1,
                "image_index": img_idx,
                "description": description,
                "source": pdf_path,
            })
    doc.close()
    return image_docs

Ingestion audio et vidéo

Pour l’étape d’ingestion audio et vidéo, définissez les entrées, le responsable de cette étape ainsi que les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu, sans avoir à deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

import httpx
import time

def transcribe_audio(
    audio_path: str,
    openai_api_key: str,
    language: str = "en",
) -> dict:
    """
    Transcribe an audio file using OpenAI Whisper.
    Banking example: transcribing an RBI monetary policy press conference recording.
    """
    with open(audio_path, "rb") as audio_file:
        response = httpx.post(
            "https://api.openai.com/v1/audio/transcriptions",
            headers={"Authorization": f"Bearer {openai_api_key}"},
            data={
                "model": "whisper-1",
                "language": language,
                "response_format": "verbose_json",  # includes timestamps per segment
                "timestamp_granularities[]": "segment",
            },
            files={"file": (audio_path, audio_file, "audio/mpeg")},
        )
    result = response.json()
    return {
        "transcript": result.get("text", ""),
        "segments": [
            {
                "start": seg["start"],
                "end": seg["end"],
                "text": seg["text"],
            }
            for seg in result.get("segments", [])
        ],
        "language": result.get("language", language),
        "source": audio_path,
    }

Assemblage : un pipeline d’ingestion en production

Pendant l’étape « Putting It Together A », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’une mise en forme ultérieure. Citez les passages qui justifient réellement la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation. Pendant l’étape « Putting It Together A », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminaisons partielles silencieuses.

from pathlib import Path
from enum import Enum
import logging

logger = logging.getLogger(__name__)

class DocumentType(Enum):
    PDF_TEXT = "pdf_text"
    PDF_SCANNED = "pdf_scanned"
    DOCX = "docx"
    HTML = "html"
    JSON = "json"
    AUDIO = "audio"
    UNKNOWN = "unknown"

def detect_document_type(file_path: str) -> DocumentType:
    path = Path(file_path)
    suffix = path.suffix.lower()
    if suffix == ".pdf":
        classification = classify_pdf(file_path)
        return DocumentType.PDF_SCANNED if classification == "scanned" else DocumentType.PDF_TEXT
    elif suffix == ".docx":
        return DocumentType.DOCX
    elif suffix in (".html", ".htm"):
        return DocumentType.HTML
    elif suffix == ".json":
        return DocumentType.JSON
    elif suffix in (".mp3", ".mp4", ".wav", ".m4a"):
        return DocumentType.AUDIO
    return DocumentType.UNKNOWN

def ingest_document(
    file_path: str,
    azure_endpoint: str | None = None,
    azure_api_key: str | None = None,
    openai_api_key: str | None = None,
) -> list[dict]:
    """
    Route a document to the correct parser and return a list of content blocks
    with a consistent schema regardless of input format.
    Every output block contains:
    - text: the extracted text content
    - source: origin path or URL
    - doc_type: the detected document type
    - page_number: where applicable
    - needs_review: flag for human review queue
    """
    doc_type = detect_document_type(file_path)
    results = []
    try:
        if doc_type == DocumentType.PDF_TEXT:
            pages = load_text_pdf(file_path)
            for page in pages:
                page["doc_type"] = "pdf_text"
                page["needs_review"] = False
                results.append(page)
        elif doc_type == DocumentType.PDF_SCANNED:
            if azure_endpoint and azure_api_key:
                pages = load_scanned_pdf_azure(file_path, azure_endpoint, azure_api_key)
                for page in pages:
                    page["doc_type"] = "pdf_scanned"
                    results.append(page)
            else:
                logger.warning(
                    f"Scanned PDF detected but no OCR credentials provided: {file_path}"
                )
                results.append({
                    "text": "",
                    "source": file_path,
                    "doc_type": "pdf_scanned",
                    "needs_review": True,
                    "error": "No OCR credentials available",
                })
        elif doc_type == DocumentType.DOCX:
            blocks = load_docx(file_path)
            for block in blocks:
                block["doc_type"] = "docx"
                block["needs_review"] = False
                results.append(block)
        elif doc_type == DocumentType.AUDIO:
            if openai_api_key:
                transcript = transcribe_audio(file_path, openai_api_key)
                for segment in transcript["segments"]:
                    results.append({
                        "text": segment["text"],
                        "source": file_path,
                        "doc_type": "audio_transcript",
                        "start_seconds": segment["start"],
                        "end_seconds": segment["end"],
                        "needs_review": False,
                    })
        else:
            logger.warning(f"Unknown document type, skipping: {file_path}")
    except Exception as e:
        logger.error(f"Ingestion failed for {file_path}: {e}")
        results.append({
            "text": "",
            "source": file_path,
            "doc_type": str(doc_type.value),
            "needs_review": True,
            "error": str(e),
        })
    return results

Qu’est-ce qui ne va pas en production ?

Lorsque vous analysez les problèmes qui surviennent en phase de test, notez d’abord les exigences : entrées requises, signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le système passe d’un environnement de démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Compromis en production

Lors de la phase des compromis de production, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent des prompts ne résout que rarement un système de récupération insuffisant.

Que vient ensuite ?

Lors de l’étape « What Comes Next », notez d’abord les conditions du contrat : entrées requises, signal de succès et comportement en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Documentez ensemble le parcours normal et les procédures de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Un changement fréquent de prompts ne résout que rarement un système de récupération insuffisant. Lors de l’étape « What Comes Next », notez d’abord les conditions du contrat : entrées requises, signal de succès et comportement en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des critères de succès et refusez les complétions partielles silencieuses.

Liste de contrôle opérationnelle

La phase de liste de contrôle opérationnel fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre.

Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’échec doit pointer vers une seule responsabilité et non vers un processus embrouillé.

Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les indicateurs de qualité changent.

Ajoutez un test de base qui exerce le chemin critique dans l’environnement CI à l’aide de fichiers de configuration, et non d’API payantes en ligne, chaque fois que le budget le permet.

Considérez cette phase comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définez des vérifications de succès, et refusez toute complétion partielle silencieuse.

Séparez la politique de segmentation de la politique de récupération. Modifier l’une ne doit pas obliger à réécrire l’autre lorsque les indicateurs de qualité changent.

Au préalable de promouvoir l’ensemble technique, figez les versions, conservez une transcription exemplaire pour le chemin critique, et vérifiez les étapes de réversion. Les environnements partagés nécessitent des limites de débit, des contrôles d’attribution, ainsi qu’un responsable clair pour la rotation des secrets. Préférez une fiabilité solide à des démonstrations brillantes mais ponctuelles.

Note de batch pour 771b701b6010 : gardez les clés du fournisseur hors du répertoire, fixez un plafond pour les tokens par session, et stockez les transcriptions à côté des fichiers d’évaluation afin que les remplacements ultérieurs de modèles restent comparables.