"""Procesamiento de ingestas individuales desde la app desktop.""" from __future__ import annotations import logging from typing import Any from domain.normalizer import normalize_to_schema from domain.profiles import get_profile from domain.prompts import PromptBuilder from domain.service import InvoiceProcessingService from infrastructure.database import ExtractionRow, Task, utcnow from infrastructure.serialization import flatten_extraction_result from sqlalchemy.orm import Session logger = logging.getLogger(__name__) def _paths_for_type(fields: list[str], doc_type: str | None) -> list[str]: if not doc_type: return list(fields or []) prefix = f"{doc_type}/" return [path.split("/", 1)[1] for path in fields if path.startswith(prefix)] def _expected_doc_types_for_radicado(request: dict[str, Any], radicado: str) -> set[str]: expected: set[str] = set() for item in request.get("documents") or []: if str(item.get("radicado") or "") != radicado: continue doc_type = str(item.get("doc_type") or item.get("docType") or "").strip() if doc_type: expected.add(doc_type) return expected def _finalize_radicado_if_ready( session: Session, *, task: Task, batch_id: str, radicado: str, request: dict[str, Any], fields: list[str], alias_map: dict[str, str], ) -> bool: partials = dict((task.result or {}).get("partials") or {}) radicado_partials = dict(partials.get(radicado) or {}) expected = _expected_doc_types_for_radicado(request, radicado) if expected and not expected.issubset(set(radicado_partials.keys())): return False fused: dict[str, Any] = {"radicado": radicado, **radicado_partials} source_pdf = None for item in request.get("documents") or []: if str(item.get("radicado") or "") == radicado: source_pdf = item.get("filename") break if source_pdf: fused["_source_pdf"] = source_pdf row_index = int((task.result or {}).get("row_count") or 0) for row in flatten_extraction_result(fused, fields, alias_map): row_index += 1 session.add( ExtractionRow( batch_id=batch_id, radicado=radicado, source_pdf=source_pdf, row_index=row_index, row_json=row, raw_json=fused, ) ) partials.pop(radicado, None) result = dict(task.result or {}) result["partials"] = partials result["row_count"] = row_index result["processed"] = int(result.get("processed") or 0) + 1 task.result = result task.updated_at = utcnow() return True def _service_for_model(base_service: InvoiceProcessingService, model: str | None) -> InvoiceProcessingService: """Devuelve un servicio con el modelo solicitado; reutiliza base_service si coincide.""" if not model: return base_service current_model = getattr(base_service.extractor, "model", None) if current_model == model: return base_service from domain.strategies import OpenAICompatibleExtractor from domain.batch_processor import BatchProcessor from domain.estimator import Estimator extractor = OpenAICompatibleExtractor(model=model) return InvoiceProcessingService( extractor, batch_processor_factory=lambda s: BatchProcessor(s), estimator_factory=lambda s: Estimator(s), ) def process_desktop_ingest( service: InvoiceProcessingService, session: Session, *, task: Task, batch_id: str, temp_path: str, radicado: str, filename: str, doc_type: str | None, instruction: str | None, tiff_page_range: tuple[int, int] | None, model: str | None = None, ) -> dict[str, Any]: service = _service_for_model(service, model) request = dict(task.request or {}) fields = list(request.get("fields") or []) alias_map = dict(request.get("alias_map") or {}) structure_mode = str(request.get("structure_mode") or "radicados") doc_types = list(request.get("doc_types") or []) prof = get_profile("invoice") selected_paths = _paths_for_type(fields, doc_type if structure_mode == "radicados" and doc_types else None) if doc_type and structure_mode == "radicados" and doc_types: try: prompt = PromptBuilder.build_for_type(doc_type, selected_paths, {}, profile_name=prof.get("name", "invoice")) except Exception: prompt = PromptBuilder.build(selected_paths, {}, profile_name=prof.get("name", "invoice")) else: prompt = instruction or PromptBuilder.build(selected_paths, {}, profile_name=prof.get("name", "invoice")) data, _usage = service.extract_full_from_pdf( temp_path, instruction=prompt, tiff_page_range=tiff_page_range, ) raw_data = data if isinstance(data, dict) else {"data": data} if isinstance(data, dict): data["radicado"] = radicado else: data = {"radicado": radicado, "data": data} norm = normalize_to_schema( data, paths=selected_paths, profile_name=prof.get("name", "invoice"), ) normalized_obj = data if isinstance(norm, dict): normalized_obj = norm.get("normalized") or data result = dict(task.result or {}) errors = list(result.get("errors") or []) if structure_mode == "radicados" and doc_types and doc_type: partials = dict(result.get("partials") or {}) radicado_partials = dict(partials.get(radicado) or {}) radicado_partials[doc_type] = normalized_obj or {} partials[radicado] = radicado_partials result["partials"] = partials task.result = result session.flush() finalized = _finalize_radicado_if_ready( session, task=task, batch_id=batch_id, radicado=radicado, request=request, fields=fields, alias_map=alias_map, ) return { "radicado": radicado, "filename": filename, "doc_type": doc_type, "finalized": finalized, "processed": int((task.result or {}).get("processed") or 0), "total": int((task.result or {}).get("total") or 0), } fused = {"radicado": radicado, "_source_pdf": filename} if doc_type: fused[doc_type] = normalized_obj or {} else: fused.update(normalized_obj if isinstance(normalized_obj, dict) else {"data": normalized_obj}) row_index = int(result.get("row_count") or 0) for row in flatten_extraction_result(fused, fields, alias_map): row_index += 1 session.add( ExtractionRow( batch_id=batch_id, radicado=radicado, source_pdf=filename, row_index=row_index, row_json=row, raw_json=fused, ) ) result["row_count"] = row_index result["processed"] = int(result.get("processed") or 0) + 1 task.result = result task.updated_at = utcnow() session.flush() return { "radicado": radicado, "filename": filename, "doc_type": doc_type, "finalized": True, "processed": int(result.get("processed") or 0), "total": int(result.get("total") or 0), } def mark_task_completed_if_done(session: Session, task: Task, batch_id: str) -> None: result = dict(task.result or {}) processed = int(result.get("processed") or 0) total = int(result.get("total") or 0) partials = dict(result.get("partials") or {}) if processed < total or partials: return from infrastructure.database import ExtractionBatch task.status = "completed" task.finished_at = utcnow() task.updated_at = utcnow() batch = session.get(ExtractionBatch, batch_id) if batch is not None: batch.status = "completed" batch.updated_at = utcnow()