refactor: Introduce accumulate_usage function for token aggregation, enhance alias map expansion in BatchProcessor, and streamline error handling in PreclassificationService. Update PDF processing utilities for improved image conversion and error logging.
This commit is contained in:
+57
-144
@@ -27,177 +27,90 @@ class InvoiceExtractorStrategy(ABC):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
_LOG_EXTRA_BASE = {"dpi": '-', "pdf": '-', "radicado": '-'}
|
||||
|
||||
|
||||
class OllamaExtractor(InvoiceExtractorStrategy):
|
||||
"""Extractor contra un servidor Ollama/OpenAI-compatible.
|
||||
|
||||
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:
|
||||
# Defaults desde settings si no se proveen
|
||||
self.api_key = api_key or settings.api.api_key
|
||||
self.base_url = base_url or settings.api.base_url
|
||||
self.model = model or settings.api.model
|
||||
self.llm = llm_client or LLMClient(api_key=self.api_key, base_url=self.base_url, default_model=self.model, request_timeout_s=settings.api.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": settings.api.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": settings.api.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": settings.api.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."""
|
||||
content: List[Dict[str, Any]] = []
|
||||
for image_b64 in images_b64:
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/png;base64,{image_b64}"},
|
||||
})
|
||||
content.append({"type": "text", "text": instruction})
|
||||
|
||||
messages = []
|
||||
if history:
|
||||
messages.extend(history)
|
||||
messages.append({"role": "user", "content": content})
|
||||
|
||||
messages = self._build_messages(images_b64, instruction, history)
|
||||
self._log_call_start(images_b64, instruction, history)
|
||||
start = time.time()
|
||||
try:
|
||||
# Trazas DEBUG del request (sin exponer imágenes ni texto completo)
|
||||
prompt_len = len(instruction) if isinstance(instruction, str) else 0
|
||||
# Log INFO con metadatos efectivos de la llamada
|
||||
self._logger.info(
|
||||
"backend_call_start",
|
||||
extra={
|
||||
"base_url": self.base_url,
|
||||
"model": self.model,
|
||||
"timeout_s": settings.api.request_timeout_s,
|
||||
"images": len(images_b64),
|
||||
"prompt_len": prompt_len,
|
||||
"history_len": len(history) if history else 0,
|
||||
"dpi": '-', "pdf": '-', "radicado": '-',
|
||||
},
|
||||
)
|
||||
self._logger.debug(
|
||||
f"Llamada backend: images={len(images_b64)}, prompt_len={prompt_len}, history_len={len(history) if history else 0}",
|
||||
extra={"dpi": '-', "pdf": '-', "radicado": '-'},
|
||||
)
|
||||
text, usage_dict = self.llm.chat_text(
|
||||
messages=messages,
|
||||
model=self.model,
|
||||
timeout_s=settings.api.request_timeout_s,
|
||||
component="extractor",
|
||||
messages=messages, model=self.model,
|
||||
timeout_s=settings.api.request_timeout_s, component="extractor",
|
||||
)
|
||||
elapsed = time.time() - start
|
||||
self._logger.info(
|
||||
"Extracción completada",
|
||||
extra={
|
||||
"dpi": '-', "pdf": '-', "radicado": '-',
|
||||
"base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": settings.api.request_timeout_s,
|
||||
"elapsed_ms": int(elapsed * 1000),
|
||||
},
|
||||
)
|
||||
self._logger.debug(
|
||||
f"Backend OK (elapsed_ms={int(elapsed*1000)})",
|
||||
extra={"dpi": '-', "pdf": '-', "radicado": '-'},
|
||||
)
|
||||
return {
|
||||
"raw": text,
|
||||
"usage": usage_dict,
|
||||
}
|
||||
self._log_call_ok(time.time() - start)
|
||||
return {"raw": text, "usage": usage_dict}
|
||||
except Exception:
|
||||
elapsed = time.time() - start
|
||||
self._logger.exception(
|
||||
"Error en llamada al backend",
|
||||
extra={
|
||||
"dpi": '-', "pdf": '-', "radicado": '-',
|
||||
"base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": settings.api.request_timeout_s,
|
||||
"elapsed_ms": int(elapsed * 1000),
|
||||
},
|
||||
)
|
||||
try:
|
||||
self._logger.debug(
|
||||
f"Backend error tras {int(elapsed*1000)} ms",
|
||||
extra={"dpi": '-', "pdf": '-', "radicado": '-'},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
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]:
|
||||
"""Construye el prompt multimodal y devuelve el texto crudo del modelo de forma asíncrona."""
|
||||
content: List[Dict[str, Any]] = []
|
||||
for image_b64 in images_b64:
|
||||
content.append({
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/png;base64,{image_b64}"},
|
||||
})
|
||||
content.append({"type": "text", "text": instruction})
|
||||
|
||||
messages = []
|
||||
if history:
|
||||
messages.extend(history)
|
||||
messages.append({"role": "user", "content": content})
|
||||
|
||||
"""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:
|
||||
# Trazas DEBUG del request (sin exponer imágenes ni texto completo)
|
||||
prompt_len = len(instruction) if isinstance(instruction, str) else 0
|
||||
# Log INFO con metadatos efectivos de la llamada
|
||||
self._logger.info(
|
||||
"backend_call_start",
|
||||
extra={
|
||||
"base_url": self.base_url,
|
||||
"model": self.model,
|
||||
"timeout_s": settings.api.request_timeout_s,
|
||||
"images": len(images_b64),
|
||||
"prompt_len": prompt_len,
|
||||
"history_len": len(history) if history else 0,
|
||||
"dpi": '-', "pdf": '-', "radicado": '-',
|
||||
},
|
||||
)
|
||||
self._logger.debug(
|
||||
f"Llamada backend: images={len(images_b64)}, prompt_len={prompt_len}, history_len={len(history) if history else 0}",
|
||||
extra={"dpi": '-', "pdf": '-', "radicado": '-'},
|
||||
)
|
||||
text, usage_dict = await self.llm.chat_text_async(
|
||||
messages=messages,
|
||||
model=self.model,
|
||||
timeout_s=settings.api.request_timeout_s,
|
||||
component="extractor",
|
||||
messages=messages, model=self.model,
|
||||
timeout_s=settings.api.request_timeout_s, component="extractor",
|
||||
)
|
||||
elapsed = time.time() - start
|
||||
self._logger.info(
|
||||
"Extracción completada",
|
||||
extra={
|
||||
"dpi": '-', "pdf": '-', "radicado": '-',
|
||||
"base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": settings.api.request_timeout_s,
|
||||
"elapsed_ms": int(elapsed * 1000),
|
||||
},
|
||||
)
|
||||
self._logger.debug(
|
||||
f"Backend OK (elapsed_ms={int(elapsed*1000)})",
|
||||
extra={"dpi": '-', "pdf": '-', "radicado": '-'},
|
||||
)
|
||||
return {
|
||||
"raw": text,
|
||||
"usage": usage_dict,
|
||||
}
|
||||
self._log_call_ok(time.time() - start)
|
||||
return {"raw": text, "usage": usage_dict}
|
||||
except Exception:
|
||||
elapsed = time.time() - start
|
||||
self._logger.exception(
|
||||
"Error en llamada al backend",
|
||||
extra={
|
||||
"dpi": '-', "pdf": '-', "radicado": '-',
|
||||
"base_url": self.base_url, "model": self.model,
|
||||
"timeout_s": settings.api.request_timeout_s,
|
||||
"elapsed_ms": int(elapsed * 1000),
|
||||
},
|
||||
)
|
||||
try:
|
||||
self._logger.debug(
|
||||
f"Backend error tras {int(elapsed*1000)} ms",
|
||||
extra={"dpi": '-', "pdf": '-', "radicado": '-'},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user