- 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
86 lines
2.6 KiB
Python
86 lines
2.6 KiB
Python
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"
|