Strona główna / Artykuły / Wskazówki praktyczne: Przyjmowanie danych do RAG produkcyjnego: tworzenie niezawodnych rozwiązań

Wskazówki praktyczne: Przyjmowanie danych do RAG produkcyjnego: tworzenie niezawodnych rozwiązań

Krok po kroku przewodnik po praktycznych wskazówkach: pobieranie danych do RAG produkcyjnego – tworzenie niezawodnych rozwiązań: umowy, sprawdzania oraz gotowe elementy kodu dla zespołów wdrażających ten wzorzec.

4000 słów

To przewodnik pokazuje, jak odtworzyć proces od surowców do gotowego systemu w przypadku: Przyjmowania danych do produkcji RAG: Budowanie niezawodnych pipeline’ów danych przedsiębiorstwa. Skupiamy się na krokach operacyjnych, wyraźnych sprawdzeniach oraz kodzie, który można bez problemu umieścić w repozytorium, bez konieczności domyślania się intencji. Na etapie przeglądu należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić dany krok na podstawie znanego punktu kontrolnego, bez konieczności domyślania się ukrytego stanu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij artefakty, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadania.

Czym właściwie jest przyjmowanie danych

Gdy przechodzisz przez etap „What Data Ingestion Actually”, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy przechodzi się z środowiska demonstracyjnego do współdzielonych środowisk. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabe możliwości wyszukiwania.

Problem formatu

Gdy przechodzisz przez etap „Problemu formatu”, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Zmierz stopę odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe mechanizmy wyszukiwania.

Ładowacze dokumentów: format po formacie

Gdy przechodzisz krok po kroku przez format Document Loaders, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Dokumentuj zarówno ścieżkę prawidłowego działania, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

Dokumenty PDF

Gdy przechodzisz przez etap dokumentów PDF, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co się dzieje w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolij małe, testowalne jednostki od rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

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: Dokumenty polityki wewnętrznej

Gdy przechodzisz przez etap dokumentacji polityki wewnętrznej w formacie DOCX, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co się dzieje w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć przypadkowe, częściowe ukończenie zadań. Zmierz stopień przywoływania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabe możliwości wyszukiwania.

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: Portale zgodności i wewnętrzne wiki

Gdy pracujesz z portalem i etapem zgodności HTML, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czasy wykonywania oraz koszt tokena lub zapytania obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z wersji demonstracyjnej do środowisk współdzielonych. Zmierz dokładność odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

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 i bazy danych relacyjne

Gdy przechodzisz przez etap JSON i baz danych relacyjnych, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny haseł oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Zmierz stopę odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania.

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 i Google Docs: Enterprise Document Stores

Gdy przechodzisz przez etap SharePoint i Google Docs, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co się dzieje w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno prawidłowy przebieg, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dopinane później. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania. Gdy przechodzisz przez etap SharePoint i Google Docs, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co się dzieje w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadań.

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")

Strumieniowanie vs Przyjmowanie partiami

Etap przesyłania strumieniowego w porównaniu z pobieraniem partiami działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przepis transkrypcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Zapisuj czasy wykonywania operacji oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy przechodzi się z środowiska demonstracyjnego do współdzielonych środowisk. Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

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")

Pobieranie multimodalne

Faza multimodalnego pobierania działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię. Zapisz jeden idealny przepis transkrypcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Przechowuj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej w przypadku zmian wskaźników jakości.

Analiza z uwzględnieniem tabeli

Etap analizy z uwzględnieniem tabel działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przepis transkrypcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań przed rozszerzeniem zakresu. Zdokumentuj zarówno prawidłowy przebieg działania, jak i ścieżkę naprawczą. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości nieodebranych stanowią część produktu, a nie elementy dodawane później. Oddziel zasadę dzielenia na fragmenty od zasady wyszukiwania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej w przypadku zmian wskaźników jakości. Etap analizy z uwzględnieniem tabel działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przepis transkrypcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań przed rozszerzeniem zakresu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań.

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

Przyjmowanie obrazów i wykresów

W fazie przyjmowania obrazów i wykresów należy zdefiniować dane wejściowe, osobę odpowiedzialną za ten etap oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten etap na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy proces przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Należy podawać fragmenty tekstu, które faktycznie stanowiły podstawę odpowiedzi. Bez tych odniesień operatorzy nie mogą odróżnić halucynacji od luki w indeksowaniu.

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

Przyjmowanie dźwięku i wideo

W fazie przyjmowania dźwięku i wideo należy zdefiniować dane wejściowe, osobę odpowiedzialną za daną czynność oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić tę czynność od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Należy podawać konkretne fragmenty tekstu, na których opiera się odpowiedź. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.

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,
    }

Podsumowanie: Proces przyjmowania danych w produkcji

W fazie „Putting It Together A” należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy udokumentować zarówno prawidłowy przebieg procesu, jak i ścieżkę naprawczą. Próby ponownych działań, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później. Należy podawać źródła, na których opiera się odpowiedź. Bez tych odniesień operatorzy nie są w stanie odróżnić halucynacji od braku danych w indeksie. W fazie „Putting It Together A” należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Należy nadać nazwy poszczególnym elementom, zdefiniować kryteria sukcesu oraz odrzucić ciche, częściowe ukończenie zadania.

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

Co się dzieje w produkcji

Gdy pracujesz nad etapem „Co się dzieje w produkcji”, najpierw zapisz specyfikację: wymagane dane wejściowe, sygnał sukcesu oraz to, co się dzieje przy częściowym awarii. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czas wykonywania oraz koszt tokena lub zapytania obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z wersji demonstracyjnej do środowisk współdzielonych. Zmierz dokładność odpowiadania na ustalone zestawy pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabą skuteczność wyszukiwania.

Kompromisy w produkcji

Gdy przechodzisz przez etap kompromisów produkcyjnych, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Zmierz stopę odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe mechanizmy wyszukiwania.

Co następuje dalej

Gdy przechodzisz przez etap „Co dalej”, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zdokumentuj zarówno ścieżkę prawidłowego działania, jak i ścieżkę naprawczą. Próby ponowne, kontrola przez ludzi oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dopinane później. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe możliwości wyszukiwania. Gdy przechodzisz przez etap „Co dalej”, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań.

Etap listy kontrolnej operacyjnej funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres.

Niech lepsze będą małe, testowalne jednostki niż rozbudowane skrypty. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.

Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

Gdy budżet na to pozwala, dodaj test wstępny, który sprawdza kluczową ścieżkę w procesie CI przy użyciu narzędzi testowych, a nie rzeczywistych płatnych API.

Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij tworzone artefakty, zdefiniuj kryteria sukcesu i odrzuć ciche, częściowe ukończenie zadań.

Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.

Zanim wdrożysz tę architekturę, zamroź wersje, utwórz dokładny zapis dla kluczowych etapów realizacji oraz potwierdź kroki odwracania zmian. Środowiska współdzielone wymagają ograniczeń szybkości, weryfikacji przynależności użytkowników oraz wyraźnego właściciela odpowiedzialnego za rotację haseł. Wolimy nudną niezawodność od pomysłowych, jednorazowych demonstracji.

Uwaga dotycząca 771b701b6010: unikaj przechowywania kluczy dostawcy w repozytorium, ustaw ograniczenie liczby tokenów na sesję oraz przechowuj zapisy obok plików przygotowawczych do testowania, aby późniejsze zmiany modeli pozostawały porównywalne.