chore: Remove obsolete build and GUI files, and introduce a new Docker Compose setup for microservices architecture. This change streamlines the project structure by eliminating legacy scripts and integrating a PostgreSQL database service alongside extraction and cleaning services.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Estrategias de extracción de información desde imágenes de documentos.
|
||||
|
||||
Define la interfaz `InvoiceExtractorStrategy` y una implementación basada en
|
||||
un endpoint compatible con la API de OpenAI para modelos multimodales.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from .config import extractor_llm_config
|
||||
from .llm_client import LLMClient
|
||||
import logging
|
||||
import time
|
||||
|
||||
|
||||
class InvoiceExtractorStrategy(ABC):
|
||||
"""Interfaz para extraer datos a partir de páginas en base64 e instrucción."""
|
||||
@abstractmethod
|
||||
def extract(self, images_b64: List[str], instruction: str, history: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
"""Devuelve un dict con al menos la clave "raw" (texto de salida del modelo)."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def extract_async(self, images_b64: List[str], instruction: str, history: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
"""Versión asíncrona de extract. Por defecto lanza NotImplementedError."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
_LOG_EXTRA_BASE = {"dpi": '-', "pdf": '-', "radicado": '-'}
|
||||
|
||||
|
||||
class OpenAICompatibleExtractor(InvoiceExtractorStrategy):
|
||||
"""Extractor contra un backend compatible con Chat Completions.
|
||||
|
||||
Parámetros configurables: `api_key`, `base_url` y `model`.
|
||||
"""
|
||||
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, model: Optional[str] = None, llm_client: Optional[LLMClient] = None) -> None:
|
||||
config = extractor_llm_config()
|
||||
self.api_key = api_key if api_key is not None else str(config["api_key"])
|
||||
self.base_url = base_url if base_url is not None else str(config["base_url"])
|
||||
self.model = model if model is not None else str(config["model"])
|
||||
self.request_timeout_s = float(config["timeout_s"])
|
||||
self.llm = llm_client or LLMClient(
|
||||
api_key=self.api_key,
|
||||
base_url=self.base_url,
|
||||
default_model=self.model,
|
||||
request_timeout_s=self.request_timeout_s,
|
||||
)
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
def _build_messages(self, images_b64: List[str], instruction: str, history: Optional[List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
|
||||
content: List[Dict[str, Any]] = [
|
||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img}"}}
|
||||
for img in images_b64
|
||||
]
|
||||
content.append({"type": "text", "text": instruction})
|
||||
messages = list(history) if history else []
|
||||
messages.append({"role": "user", "content": content})
|
||||
return messages
|
||||
|
||||
def _log_call_start(self, images_b64: List[str], instruction: str, history: Optional[List[Dict[str, Any]]]) -> None:
|
||||
prompt_len = len(instruction) if isinstance(instruction, str) else 0
|
||||
self._logger.info(
|
||||
"backend_call_start",
|
||||
extra={**_LOG_EXTRA_BASE, "base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": self.request_timeout_s, "images": len(images_b64),
|
||||
"prompt_len": prompt_len, "history_len": len(history) if history else 0},
|
||||
)
|
||||
self._logger.debug(
|
||||
f"Llamada backend: images={len(images_b64)}, prompt_len={prompt_len}, history_len={len(history) if history else 0}",
|
||||
extra=_LOG_EXTRA_BASE,
|
||||
)
|
||||
|
||||
def _log_call_ok(self, elapsed: float) -> None:
|
||||
self._logger.info(
|
||||
"Extracción completada",
|
||||
extra={**_LOG_EXTRA_BASE, "base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": self.request_timeout_s, "elapsed_ms": int(elapsed * 1000)},
|
||||
)
|
||||
self._logger.debug(f"Backend OK (elapsed_ms={int(elapsed*1000)})", extra=_LOG_EXTRA_BASE)
|
||||
|
||||
def _log_call_error(self, elapsed: float) -> None:
|
||||
self._logger.exception(
|
||||
"Error en llamada al backend",
|
||||
extra={**_LOG_EXTRA_BASE, "base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": self.request_timeout_s, "elapsed_ms": int(elapsed * 1000)},
|
||||
)
|
||||
self._logger.debug(f"Backend error tras {int(elapsed*1000)} ms", extra=_LOG_EXTRA_BASE)
|
||||
|
||||
def extract(self, images_b64: List[str], instruction: str, history: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
"""Construye el prompt multimodal y devuelve el texto crudo del modelo."""
|
||||
messages = self._build_messages(images_b64, instruction, history)
|
||||
self._log_call_start(images_b64, instruction, history)
|
||||
start = time.time()
|
||||
try:
|
||||
text, usage_dict = self.llm.chat_text(
|
||||
messages=messages, model=self.model,
|
||||
timeout_s=self.request_timeout_s, component="extractor",
|
||||
)
|
||||
self._log_call_ok(time.time() - start)
|
||||
return {"raw": text, "usage": usage_dict}
|
||||
except Exception:
|
||||
self._log_call_error(time.time() - start)
|
||||
raise
|
||||
|
||||
async def extract_async(self, images_b64: List[str], instruction: str, history: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
||||
"""Versión asíncrona de extract."""
|
||||
messages = self._build_messages(images_b64, instruction, history)
|
||||
self._log_call_start(images_b64, instruction, history)
|
||||
start = time.time()
|
||||
try:
|
||||
text, usage_dict = await self.llm.chat_text_async(
|
||||
messages=messages, model=self.model,
|
||||
timeout_s=self.request_timeout_s, component="extractor",
|
||||
)
|
||||
self._log_call_ok(time.time() - start)
|
||||
return {"raw": text, "usage": usage_dict}
|
||||
except Exception:
|
||||
self._log_call_error(time.time() - start)
|
||||
raise
|
||||
|
||||
def update_config(self, *, api_key: str | None = None, base_url: str | None = None, model: str | None = None) -> None:
|
||||
"""Actualiza la configuración del cliente y recrea la instancia interna."""
|
||||
if api_key is not None:
|
||||
self.api_key = api_key
|
||||
if base_url is not None:
|
||||
self.base_url = base_url
|
||||
if model is not None:
|
||||
self.model = model
|
||||
# actualizar cliente LLM con config actual
|
||||
self.llm.update_config(api_key=self.api_key, base_url=self.base_url, default_model=self.model, request_timeout_s=self.request_timeout_s)
|
||||
Reference in New Issue
Block a user