OCR Mistral: modelo mistral-ocr-4-0, batch + fallback con reintentos y errores etiquetados
- Modelo por defecto mistral-ocr-4-0 (.env.example, config.py, docker-compose) - MistralClient: OCR en lote via API Batch (/v1/ocr) subiendo JSONL, polling y descarga de resultados - Reintentos con backoff exponencial en llamadas a Mistral (429/5xx/red); sin reintento en 401/400 - PdfProcessor: lote cuando >= umbral de paginas, fallback sincrono pagina a pagina, placeholder rastreable por pagina fallida - Errores etiquetados ([AUTH_MISTRAL], [BATCH], [NETWORK], [OCR_EMPTY], [PDF_PARSE]...) en el mensaje del job - Nuevas variables: MISTRAL_OCR_BATCH_THRESHOLD/POLL_SECONDS/TIMEOUT_SECONDS/RETRY_ATTEMPTS/RETRY_BACKOFF_BASE
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class ProcessingError(Exception):
|
||||
"""Error de procesamiento con un codigo corto y rastreable.
|
||||
|
||||
El atributo ``code`` es un identificador estable en MAYUSCULAS que se
|
||||
antepone al mensaje del job (p.ej. ``[AUTH_MISTRAL] API Key invalida``)
|
||||
para que el usuario pueda buscar y diagnosticar rapidamente el tipo de
|
||||
fallo desde la UI.
|
||||
"""
|
||||
|
||||
def __init__(self, code: str, detail: str) -> None:
|
||||
super().__init__(f"[{code}] {detail}")
|
||||
self.code = code
|
||||
self.detail = detail
|
||||
|
||||
@property
|
||||
def labeled(self) -> str:
|
||||
return f"[{self.code}] {self.detail}"
|
||||
|
||||
|
||||
class MistralAuthError(ProcessingError):
|
||||
def __init__(self, detail: str = "API Key de Mistral invalida o no autorizada (401)") -> None:
|
||||
super().__init__("AUTH_MISTRAL", detail)
|
||||
|
||||
|
||||
class MistralRateLimitError(ProcessingError):
|
||||
def __init__(self, detail: str = "Limite de uso de Mistral alcanzado (429)") -> None:
|
||||
super().__init__("RATE_LIMIT", detail)
|
||||
|
||||
|
||||
class MistralServerError(ProcessingError):
|
||||
def __init__(self, status: int, body: str) -> None:
|
||||
super().__init__("MISTRAL_SERVER", f"Mistral HTTP {status}: {body[:200]}")
|
||||
|
||||
|
||||
class MistralBatchError(ProcessingError):
|
||||
def __init__(self, detail: str) -> None:
|
||||
super().__init__("BATCH", detail)
|
||||
|
||||
|
||||
class MistralOCREmptyError(ProcessingError):
|
||||
def __init__(self, detail: str = "OCR no devolvio contenido") -> None:
|
||||
super().__init__("OCR_EMPTY", detail)
|
||||
|
||||
|
||||
class NetworkError(ProcessingError):
|
||||
def __init__(self, detail: str) -> None:
|
||||
super().__init__("NETWORK", detail)
|
||||
|
||||
|
||||
NON_RETRYABLE_CODES = {
|
||||
"AUTH_MISTRAL",
|
||||
"CONFIG",
|
||||
"OCR_EMPTY",
|
||||
"MISTRAL_CLIENT",
|
||||
"BAD_PDF",
|
||||
"FILE_IO",
|
||||
}
|
||||
|
||||
|
||||
def classify_requests_error(exc: requests.RequestException) -> ProcessingError:
|
||||
if isinstance(exc, requests.Timeout):
|
||||
return NetworkError(f"timeout de red con Mistral: {exc}")
|
||||
if isinstance(exc, requests.ConnectionError):
|
||||
return NetworkError(f"sin conexion a Mistral: {exc}")
|
||||
return NetworkError(f"error de red con Mistral: {exc}")
|
||||
|
||||
|
||||
def classify_unknown_error(exc: Exception) -> str:
|
||||
"""Etiqueta breve para excepciones no envueltas en ProcessingError."""
|
||||
name = type(exc).__name__.lower()
|
||||
message = str(exc).lower()
|
||||
if "fitz" in name or "pdf" in name or "pdf" in message or "muPDF" in message:
|
||||
return "PDF_PARSE"
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
return "FILE_IO"
|
||||
if isinstance(exc, (PermissionError, OSError)):
|
||||
return "FILE_IO"
|
||||
if isinstance(exc, ValueError):
|
||||
return "VALIDATION"
|
||||
return "UNKNOWN"
|
||||
Reference in New Issue
Block a user