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:
erik
2026-03-01 20:55:52 -05:00
parent 3c258ebf12
commit e7fde25227
29 changed files with 321 additions and 1206 deletions
-270
View File
@@ -1,270 +0,0 @@
# IPSExtractor — Documentación técnica (Desarrolladores)
Este documento explica la arquitectura, módulos, flujos, modelo de datos/CSV, estados de la GUI, configuración y extensibilidad del proyecto. Contiene diagramas Mermaid incrustados.
## Índice
- [Visión general](#visión-general)
- [Arquitectura de componentes](#arquitectura-de-componentes)
- [Módulos y responsabilidades](#módulos-y-responsabilidades)
- [Flujos de ejecución](#flujos-de-ejecución)
- [Modelo de datos y CSV](#modelo-de-datos-y-csv)
- [Estados de la GUI y manejo de errores](#estados-de-la-gui-y-manejo-de-errores)
- [Configuración y ejecución](#configuración-y-ejecución)
- [Extensibilidad](#extensibilidad)
- [Registro y métricas](#registro-y-métricas)
## Visión general
La aplicación procesa carpetas con “radicados” que contienen PDFs/TIFFs de facturas. Orquesta la conversión de documentos a imágenes Base64, llama a una estrategia de extracción para obtener un JSON, normaliza el resultado a un esquema seleccionado por el usuario y exporta CSV incremental. Incluye GUI con selección interactiva de campos y estimación de costos por tokens.
## Arquitectura de componentes
```mermaid
flowchart LR
subgraph GUI
V[gui/view.py]
C[gui/controller.py]
FS[gui/field_selector.py]
end
subgraph APP[application/*]
S[service.py]
STG[strategies.py<br/>(InvoiceExtractorStrategy)]
NRM[normalizer.py]
PR[profiles.py]
PB[prompts.py]
CFG[config.py]
end
subgraph CORE
PDF[core/pdf_utils.py]
TXT[core/text_utils.py]
CSV[core/csv_utils.py]
UT[core/utils.py]
end
V --> C
C --> S
C --> FS
S --> STG
S --> NRM
S --> PR
S --> PB
S --> CFG
S --> PDF
S --> TXT
S --> CSV
S --> UT
S --> LOG[(logs/*)]
```
## Módulos y responsabilidades
- `application/service.py`: orquestación principal (extracción con fallback, schema inicial, normalización, CSV incremental, estimación de costos/tokens).
- Funciones clave:
- `extract_full_from_pdf(pdf_path, instruction?)`: conversión a imágenes, llamada a extractor, parseo JSON, fallback DPI/páginas.
- `build_schema_from_first(pdf_path, instruction?)`: genera `schema` a partir del primer documento.
- `build_schema_from_first_for_types(root_path, doc_types)`: genera `schema_by_type` usando el primer radicado y la primera coincidencia por keyword de cada tipo.
- `process_root_with_field_selection(root_path, keywords|doc_types, fields?, ...)`: flujo por radicado; soporta multi-documento; selección de campos; normalización y CSV incremental.
- `estimate_tokens_and_cost(root_path, keywords|doc_types, ...)`: simulación con primer radicado; en multi suma por tipo.
- `application/profiles.py`: perfiles por tipo de documento e instrucciones por defecto.
- `application/strategies.py`: interfaz/estrategia de extracción (`InvoiceExtractorStrategy`).
- `application/normalizer.py`: `normalize_to_schema(...)` para ajustar el JSON al esquema elegido.
- `application/prompts.py`: `PromptBuilder.build(...)` para prompts reducidos según campos seleccionados.
- `application/config.py`: `settings` (imaging, hilos, DPI, etc.).
- `core/pdf_utils.py`: conversión PDF/TIFF → imágenes Base64.
- `core/text_utils.py`: `extract_json_from_text`, `build_schema`.
- `core/csv_utils.py`: aplanado de campos no-lista, detección de la primera lista de ítems, agregaciones y encabezados.
- `core/utils.py`: utilidades (p. ej., `collect_radicado_docs`).
- `gui/controller.py`: coordina GUI ↔ servicio, selector de campos y callbacks de progreso/uso.
- `gui/field_selector.py`: diálogo de selección de paths y alias (valida duplicados y permite reproceso). En modo multi, agrupa visualmente por `Tipo` (p. ej., `Factura/...`, `Remision/...`).
- `gui/view.py`: añade lista dinámica de Tipos de Documento (Nombre Lógico + Keyword).
- `gui/view.py`: componentes visuales y actualizaciones (KPIs, barras, líneas de costo, etc.).
## Flujos de ejecución
Incluye: disparo desde GUI, descubrimiento de radicados, creación de schema, selección de campos/alias, estimación y confirmación, extracción por documento con fallback, normalización y exportación incremental a CSV.
### Secuencia general (GUI → Service → Normalización → CSV)
```mermaid
sequenceDiagram
autonumber
actor U as Usuario
participant V as gui/view.py
participant C as gui/controller.py
participant S as application/service.py
participant FS as gui/field_selector.py
participant ST as application/strategies.py
participant N as application/normalizer.py
participant CSV as core/csv_utils.py
participant TXT as core/text_utils.py
participant UT as core/utils.py
U->>V: Click "Procesar"
V->>C: on_process()
note over C: Lee root, keywords, fields opcionales
C->>S: process_root_with_field_selection(fields=None, doc_types?)
S->>UT: collect_radicado_docs()
S->>S: build_schema_from_first(first.pdf)
S->>TXT: extract_json_from_text(raw)
S-->>C: [{ _needs_fields: true, schema, first_full }]
C->>FS: Abrir diálogo de selección (schema + sample)
FS-->>C: on_submit(selected_paths, alias_map) Note: paths con prefijo de tipo `Tipo/Subruta`
C->>S: estimate_tokens_and_cost(..., doc_types?, schema_by_type?, selected_paths?)
C->>U: Confirmación (costos estimados)
U-->>C: Aceptar
C->>S: process_root_with_field_selection(fields=selected_fields, alias_map)
loop Por cada radicado
alt Multi-documento
loop Por cada Tipo
S->>ST: extract(images, prompt reducido por Tipo)
S->>N: normalize_to_schema(data, paths del Tipo)
end
S-->>S: fusiona por Tipo => fused_data = {'radicado': R, 'Factura': {...}, 'Remision': {...}}
S->>CSV: rows_from_data(fused_data, allowed_paths con prefijo)
else Single-documento
S->>ST: extract(images, prompt reducido)
S->>N: normalize_to_schema(data, paths)
S->>CSV: rows_from_data(data, allowed_paths, aggregate_list_paths?)
end
S->>S: _append_rows_to_csv(...)
S-->>C: on_usage(phase, radicado, usage, cost)
C->>V: Actualiza KPIs/barras/líneas
end
S-->>C: resultados finales (JSON)
C->>V: Imprime resultados en Output
```
### Secuencia de extracción con fallback
```mermaid
sequenceDiagram
autonumber
participant S as application/service.py
participant PDF as core/pdf_utils.py
participant ST as application/strategies.py
participant TXT as core/text_utils.py
S->>PDF: document_to_images_bs64(pdf, dpi=config, threads)
S->>ST: extract(images_b64, instruction)
alt Éxito
ST-->>S: {raw, usage}
else Error/Timeout/Payload
S->>PDF: document_to_images_bs64(pdf, dpi=fallback, pages<=5)
S->>ST: extract(images_b64_fb, instruction)
ST-->>S: {raw, usage}
end
S->>TXT: extract_json_from_text(raw)
TXT-->>S: json_text
S-->>S: json.loads(json_text) => data
```
## Modelo de datos y CSV
Describe cómo se aplanan campos no-lista, cómo se detecta la primera lista de ítems, cómo se agregan listas a columnas y cómo `allowed_paths` y los alias determinan el encabezado y las filas del CSV.
### Diagrama de datos
```mermaid
classDiagram
class ExtractedData {
+string radicado
+dict ...campos_no_lista
+Item[] items
+string _source_pdf
}
class Item {
+string Codigo
+int Cantidad
+float V/Unitario
+... otros subcampos anidados
}
ExtractedData "1" --> "0..*" Item : items
```
Notas:
- Los “campos no lista” se aplanan con separador `/`, por ejemplo: `Paciente/Nombre`, `Factura/Fecha`.
- La primera lista de objetos detectada se considera “lista de ítems principal” y se expande en columnas.
### Mapeo a CSV (`core/csv_utils.py`)
- Aplanado no-lista: `flatten_non_list_fields(data)` produce columnas para todas las hojas escalar (excluye listas).
- Detección de lista principal: `_find_first_items_list(data)` devuelve `items_key` y `items`.
- Subcolumnas de ítems: se aplanan como `items_key/*/subruta`, por ejemplo: `items/*/V/Unitario`.
- Filtrado por `allowed_paths` (selección del usuario): solo se mantienen rutas de esa lista (más `radicado` siempre).
- Agregación opcional: `aggregate_list_paths=[base_path]` genera columnas `base_path/*/subclave` con valores unidos por coma (para listas no principales).
- Encabezado final: `['radicado', ...campos_no_lista, ...campos_items]` respetando `allowed_paths` y orden alfabético de subrutas.
- Alias: el `alias_map` de la GUI renombra columnas en el CSV final (encabezado y filas).
Ejemplos de columnas resultantes:
- `radicado`
- `Factura/Número`
- `Paciente/Nombre`
- `items/*/Codigo`
- `items/*/V/Unitario`
## Estados de la GUI y manejo de errores
Estados principales: Idle → Processing → (NeedsFields) → Estimating → Confirming → Processing2 → Done/Error. La extracción tiene fallback controlado y los errores se registran y opcionalmente se reflejan como filas de error en el CSV.
```mermaid
stateDiagram-v2
[*] --> Idle
Idle --> Processing: Click "Procesar"
Processing --> NeedsFields: Service devuelve _needs_fields
NeedsFields --> Estimating: on_submit(selected_fields, alias_map)
Estimating --> Confirming: Mostrar costos estimados
Confirming --> Processing2: Usuario acepta
Confirming --> Idle: Usuario cancela
Processing2 --> Done: Todos los radicados OK
Processing2 --> Error: Excepción por radicado
Done --> Idle
Error --> Idle
```
Manejo de errores/fallback:
- Extracción: si falla la primera llamada (timeout/payload), se reprocesa con DPI reducido y límite de páginas (≤5).
- Escritura CSV: filas de error con `radicado`, `_source_pdf`, `error` cuando hay excepción.
- Logs: se emiten `logger.info/debug/exception` con contexto (`pdf`, `radicado`, métricas de tiempo y tokens).
- GUI: mantiene KPIs y barreras por fase; errores no bloquean el procesamiento de los siguientes radicados.
## Configuración y ejecución
Parámetros clave en `application/config.py`:
- `settings.imaging.pdf_dpi`: DPI para rasterizar PDF/TIFF (impacta calidad/tamaño).
- `settings.imaging.pdf_thread_count`: paralelismo al generar imágenes.
- `settings.imaging.tiff_pages`: páginas a considerar por defecto en TIFF multi-página.
Ejecución (GUI):
1) Inicie la aplicación GUI.
2) Seleccione la carpeta raíz de radicados (cada radicado contiene PDFs/TIFFs).
3) Opcional: defina palabras clave de búsqueda (por defecto: `Factura_O_Documento_Equivalente, factura`).
4) Opcional: defina campos a extraer (si se deja vacío se abrirá el selector).
5) Opcional: elija ruta de salida para el CSV.
6) Presione “Procesar”.
Exportación CSV:
- Si se especifica `csv_output_path`, el servicio añadirá filas incrementalmente conforme procesa cada documento.
- Los encabezados se generan en la primera escritura; si existe un CSV previo con contenido, se preserva su encabezado.
## Extensibilidad
Nuevos perfiles, estrategias de extracción o ajustes en normalización y costos se pueden incorporar con cambios locales y mínimos acoplamientos.
- Perfiles (`application/profiles.py`): agregue una entrada en `PROFILES` definiendo `default_instruction`, `aggregate_list_paths`, etc.
- Estrategias de extracción (`application/strategies.py`): implemente `InvoiceExtractorStrategy.extract(images_b64, instruction)` para soportar nuevos backends/LLMs.
- Normalización (`application/normalizer.py`): adapte `normalize_to_schema` para modelos y políticas de mapeo; los costos por fase se calculan en el servicio.
- Prompts (`application/prompts.py`): `PromptBuilder.build(selected_fields, schema)` puede ajustarse para controlar la precisión y tamaño del prompt.
- Estimación de costos (`application/service.py::estimate_tokens_and_cost`): modifique tarifas/modelos según su despliegue.
Preferencia de diseño:
- Siempre que sea posible, se recomienda ajustar el prompt para conseguir un JSON estable en vez de depender de mapeos/alias complejos en código [[memory:10659104]].
## Registro y métricas
La app registra progreso, tiempos y costos por fase y por documento. Los logs se escriben en `logs/*` y la GUI muestra KPIs agregados.
-15
View File
@@ -1,15 +0,0 @@
# Documentación de `application/`
## Índice
- [`service.py`](./service.md): fachada de procesamiento de facturas.
- [`normalizer.py`](./normalizer.md): normalización LLM a esquema canónico.
- [`strategies.py`](./strategies.md): estrategias de extracción (OpenAI/Ollama).
- [`profiles.py`](./profiles.md): perfiles e instrucciones por defecto.
- [`prompts.py`](./prompts.md): construcción de prompts.
- [`config.py` y `logging_config.py`](./config.md): configuración y logging (unificado).
## Relaciones entre módulos
- `service` usa `strategies` para extraer, `profiles` para instrucciones por defecto, `prompts` para prompts reducidos y `normalizer` cuando se requiere adaptar al esquema canónico.
- `config` provee `settings` consumido por varios módulos; `logging_config` inicializa logging consistente.
-36
View File
@@ -1,36 +0,0 @@
# `application/config.py` y `application/logging_config.py`
## Propósito
Centralizan configuración (Pydantic Settings) y logging en formato JSON.
## Configuración (`application/config.py`)
### Clases
- `ApiSettings`: `base_url`, `api_key`, `model`, `request_timeout_s`.
- `ImagingSettings`: `pdf_dpi`, `pdf_thread_count`, `tiff_pages`, y parámetros judiciales.
- `RetrySettings`: reintentos y backoff.
- `AppSettings`: agrega las anteriores y `csv_delimiter`.
Se expone `settings = AppSettings()`. Lee variables desde `.env` (si existe).
### Variables de entorno relevantes
- `EXTRACTOR_BASE_URL`, `EXTRACTOR_API_KEY`, `EXTRACTOR_MODEL`, `EXTRACTOR_TIMEOUT_S`
- `PDF_DPI`, `PDF_THREAD_COUNT`, `TIFF_PAGES`
- `JUDICIAL_FAST_FIND_DPI`, `JUDICIAL_MAIN_DPI`, `JUDICIAL_GRAYSCALE`, `JUDICIAL_VALIDATE_IMAGES`
- `RETRY_MAX_ATTEMPTS`, `RETRY_WAIT_MIN_S`, `RETRY_WAIT_MAX_S`
- `CSV_DELIMITER`
## Logging (`application/logging_config.py`)
Configura loggers rotativos unificados:
- `logs/ips_extractor.log` (aplicación),
- `logs/llm.log` (normalización),
- `logs/api_calls.log` (llamadas a APIs),
y consola para WARNING+.
### Uso
```python
from application.logging_config import configure_logging
configure_logging()
```
-89
View File
@@ -1,89 +0,0 @@
# `application/flows.py`
## Propósito
Orquesta el procesamiento por radicado para:
- Calcular encabezados CSV unificados a partir de muestras y selección de campos.
- Extraer información de factura y, opcionalmente, preformato.
- Escribir filas en CSV y emitir JSON a la interfaz gráfica mediante callbacks.
Incluye una heurística para inferir qué listas deben expandirse por filas y cuáles agregarse como columnas (en preformato).
## API pública
### `compute_csv_header_union(...) -> tuple[list[str] | None, dict[str, str]]`
Calcula el encabezado CSV común (unión) desde datos de muestra y paths seleccionados, aplicando alias y resolviendo duplicados.
Parámetros clave:
- `sample_fact`, `sample_pre`: ejemplos (dict o compatible) para derivar columnas.
- `selected_fields_fact`, `selected_fields_pre`: paths seleccionados por documento.
- `pre_enabled`: habilita el cálculo para preformato.
- `alias_map_fact`, `alias_map_pre`: alias opcionales path->nombre.
Devuelve:
- Lista de encabezados finales (o `None` si no hay columnas).
- Mapa `path -> alias final` aplicado.
Notas:
- `radicado` siempre se incluye como columna.
- Duplicados de alias se desambiguarán con sufijos `(n)`.
### `process_radicados_parallel(...) -> None`
Procesa en paralelo los radicados, realizando:
1) Extracción de datos de factura y preformato (si está habilitado).
2) Emisión de JSON via `on_output`.
3) Escritura incremental en CSV con encabezado dado (`csv_header_union`) o derivado.
Parámetros clave:
- `service`: Debe exponer `extract_full_from_pdf(...)` y `_append_rows_to_csv(...)`.
- `root`, `keywords`, `pre_keywords`: Búsqueda de documentos por radicado.
- `selected_fields_fact`, `selected_fields_pre`: paths de salida.
- `csv_path`: Archivo CSV destino (si `None`, no escribe).
- `on_output`: Callback que recibe texto JSON por radicado.
- `on_progress`: Callback `(radicado, done, total)` para progreso.
- `instruction_invoice`, `instruction_pre`: instrucciones LLM.
- `alias_final_map`: alias finales a aplicar a encabezados/filas.
Concurrencia:
- Usa `ThreadPoolExecutor` con `max_workers=4` para balancear IO/CPU.
- Los errores por documento se capturan y se registran sin detener el flujo.
## Consideraciones
- Si se define `csv_header_union`, éste manda sobre encabezados derivados por radicado.
- Para preformato, la heurística `_infer_aggregate_list_paths_from_selected` selecciona la lista principal y las listas a agregar; si no hay inferencia, se consulta `profiles`.
- `on_output` y `on_progress` son opcionales y tolerantes a errores (capturados).
## Ejemplos
### Cálculo de encabezado
```python
from application.flows import compute_csv_header_union
header, alias_map = compute_csv_header_union(
sample_fact={'radicado': 'x', 'items': [{'codigo': 'A1', 'cantidad': 2}]},
sample_pre=None,
selected_fields_fact=['radicado', 'items/*/codigo', 'items/*/cantidad'],
selected_fields_pre=None,
pre_enabled=False,
)
# header ~= ['radicado', 'items/*/codigo', 'items/*/cantidad']
```
### Proceso por radicado (escritura CSV)
```python
from application.flows import process_radicados_parallel
process_radicados_parallel(
service=my_service, # Debe implementar extract_full_from_pdf y _append_rows_to_csv
root="C:/carpeta/raiz",
keywords=["FACTURA", "RADICADO"],
pre_enabled=True,
pre_keywords=["PREFORMATO"],
selected_fields_fact=['radicado', 'items/*/codigo', 'items/*/cantidad'],
selected_fields_pre=['radicado', 'dx/*/descripcion'],
csv_path="salida.csv",
on_output=lambda txt: print(txt),
on_progress=lambda r,d,t: print(f"{d}/{t} -> {r}"),
)
```
-28
View File
@@ -1,28 +0,0 @@
# `application/normalizer.py`
## Propósito
Normaliza un JSON de entrada al esquema canónico basado en rutas (paths) seleccionado por el usuario, utilizando un LLM.
## API pública
### `build_canonical_template(paths: list[str]) -> dict`
Convierte rutas del tipo `a/b/*/c` en un objeto anidado con listas y `null`, siempre incluyendo `radicado` en la raíz.
### `normalize_to_schema(data: dict, paths: list[str], profile_name: str, model: str | None = None) -> dict | None`
Usa un LLM para mapear `data` al template canónico generado por `build_canonical_template`. Retorna `None` si no hay API Key o si el LLM falla.
## Consideraciones
- Control de reintentos y timeouts vía `settings.retry` y `settings.api`.
- Mantiene `radicado` si estaba presente en el `data` original.
- Registra métricas (duración, modelo, base_url) sin exponer contenido sensible.
## Ejemplo
```python
from application.normalizer import build_canonical_template, normalize_to_schema
paths = ["radicado", "items/*/codigo", "items/*/cantidad"]
template = build_canonical_template(paths)
normalized = normalize_to_schema({"radicado": "R-1", "items": [{"codigo": "A", "cantidad": 2}]}, paths, profile_name="invoice")
```
-26
View File
@@ -1,26 +0,0 @@
# `application/profiles.py`
## Propósito
Define perfiles por tipo de documento con:
- `wrap_root`: clave raíz para envolver salidas.
- `default_instruction`: instrucción por defecto para el LLM.
- `aggregate_list_paths`: rutas de listas a agregar en CSV cuando corresponda.
## API pública
### `get_profile(name: str) -> Profile`
Devuelve un perfil por nombre. Si no existe, usa `invoice`.
## Perfiles
- `invoice`: sin `wrap_root`, instrucción para tablas y etiquetas.
## Ejemplo
```python
from application.profiles import get_profile
prof = get_profile("invoice")
inst = prof["default_instruction"]
agg = prof.get("aggregate_list_paths")
```
-28
View File
@@ -1,28 +0,0 @@
# `application/prompts.py`
## Propósito
Centraliza la construcción de prompts para:
- Extracción reducida limitada a los campos seleccionados.
- Normalización de JSON al esquema canónico.
## API pública
### `PromptBuilder.build(selected_fields, schema, profile_name=None) -> str`
Devuelve un JSON de instrucciones para extracción, respetando exactamente las claves solicitadas.
### `PromptBuilder.build_normalization_messages(profile_name, template, source_json) -> list[dict[str, str]]`
Construye los mensajes `system` y `user` para solicitar al LLM la normalización al `template`.
## Ejemplo
```python
from application.prompts import PromptBuilder
prompt = PromptBuilder.build(
["radicado", "items/*/codigo"],
{"radicado": "str", "items/*/codigo": "str"},
profile_name="invoice"
)
msgs = PromptBuilder.build_normalization_messages("invoice", {"radicado": None}, {"radicado": "R-1"})
```
-53
View File
@@ -1,53 +0,0 @@
# `application/service.py`
## Propósito
Fachada de alto nivel para procesar facturas:
- Convierte documentos (PDF/TIFF) a imágenes base64 según configuración.
- Llama a una estrategia de extracción multimodal (OpenAI/Ollama compatible).
- Construye un esquema a partir del primer documento y permite seleccionar campos.
- Escribe CSV incrementalmente respetando la selección de paths.
## API pública
### `InvoiceProcessingService.extract_full_from_pdf(pdf_path, instruction=None) -> dict`
Extrae JSON completo desde un documento. Selecciona instrucción por defecto del perfil `invoice` si no se provee y aplica fallback (DPI reducido, límite de páginas) ante fallos del backend.
Parámetros:
- `pdf_path`: ruta al documento.
- `instruction`: instrucción (prompt) opcional.
### `InvoiceProcessingService.build_schema_from_first(pdf_path, ...) -> (schema, data)`
Construye un esquema de rutas a partir del primer documento y devuelve también los datos extraídos.
### `InvoiceProcessingService.process_root_with_field_selection(root_path, keywords=None, *, doc_types=None, fields=None, on_progress=None, csv_output_path=None, csv_header_override=None, instruction_override=None, on_usage=None, alias_map=None) -> list[dict]`
Procesa radicados en un directorio aplicando una selección de campos. Si `fields` es `None`, devuelve un objeto con `_needs_fields` para que el llamador abra el selector de campos.
#### Selección de archivos por keywords/patrones
- En modo sencillo (`keywords`), las palabras se interpretan como:
- Texto sin `*`/`?`: coincide si el nombre de archivo **contiene** ese texto (case-insensitive).
- Patrón con `*`/`?`: se evalúa usando semántica tipo Windows (`*_1`, `*_OK?.pdf`, etc.).
- En modo multi-documento (`doc_types`), cada tipo tiene su propio keyword/patrón con la misma semántica.
- Si el patrón no incluye extensión, se aplica sobre el nombre sin extensión y solo se consideran `.pdf`, `.tif`, `.tiff`.
- Si incluye una extensión válida (por ejemplo `.pdf` en `*_OK?.pdf`), se respeta exactamente esa extensión.
## Consideraciones
- La escritura CSV respeta el orden de `csv_header_override` si se proporciona.
- Se incluyen logs detallados de tiempos y tamaños para diagnosticar problemas.
## Ejemplos
```python
from application.service import InvoiceProcessingService
from application.strategies import OllamaExtractor
svc = InvoiceProcessingService(OllamaExtractor())
schema, first = svc.build_schema_from_first("ejemplo.pdf")
resultados = svc.process_root_with_field_selection(
root_path="C:/facturas",
keywords=["FACTURA"],
fields=["radicado", "items/*/codigo", "items/*/cantidad"],
csv_output_path="salida.csv",
)
```
-31
View File
@@ -1,31 +0,0 @@
# `application/strategies.py`
## Propósito
Define la interfaz `InvoiceExtractorStrategy` y una implementación `OllamaExtractor` para realizar llamadas a un backend compatible con OpenAI/Ollama que procese imágenes + texto.
## API pública
### `class InvoiceExtractorStrategy(ABC)`
Interfaz para extraer datos a partir de páginas en base64 e instrucción de texto.
### `class OllamaExtractor(InvoiceExtractorStrategy)`
Extractor que construye prompts multimodales y llama a `chat.completions.create`.
Métodos clave:
- `extract(images_b64: list[str], instruction: str) -> dict`: devuelve al menos `{"raw": ...}` con el texto del modelo.
- `update_config(api_key=None, base_url=None, model=None)`: actualiza y recrea el cliente interno.
## Consideraciones
- Reintentos con `tenacity` usando la configuración de `settings.retry`.
- Registro de métricas (tokens, tiempos) en `application.api_calls` y logs propios.
## Ejemplo
```python
from application.strategies import OllamaExtractor
ext = OllamaExtractor()
res = ext.extract([img_b64_1, img_b64_2], "Instrucción...")
text = res["raw"]
```
-12
View File
@@ -1,12 +0,0 @@
# Documentación de `gui/`
## Índice
- [`controller.py`](./controller.md): controlador principal de la GUI.
- [`field_selector.py`](./field_selector.md): diálogo de selección de campos.
- [`view.py`](./view.md): vista principal `customtkinter`.
## Relaciones entre módulos
- `controller` coordina `view` y llama a servicios de `application`; abre `field_selector` cuando se requieren campos.
- `view` expone callbacks, getters y helpers seguros para escribir salida desde hilos.
-32
View File
@@ -1,32 +0,0 @@
# `gui/controller.py`
## Propósito
Controlador principal de la GUI: enlaza la vista con los servicios de aplicación, maneja selección de carpetas/CSV, abre el selector de campos y ejecuta el procesamiento por radicado.
## API pública
### `class AppController`
- `__init__(view, service)`: inicializa y vincula callbacks.
- `on_select_root()`: abre diálogo de carpeta para la raíz.
- `on_select_csv()`: abre diálogo para guardar CSV.
- `on_process()`: coordina selección de campos y el procesamiento por radicado.
## Flujo
1) Construye schema/muestra de factura cuando es necesario.
2) Abre `FieldSelectorDialog` para elegir campos (y alias).
3) Relanza el procesamiento por radicado, emite JSON y escribe CSV incremental.
## Ejemplo
```python
from gui.view import InvoiceAppView
from gui.controller import AppController
from application.service import InvoiceProcessingService
from application.strategies import OllamaExtractor
view = InvoiceAppView()
svc = InvoiceProcessingService(OllamaExtractor())
controller = AppController(view, svc)
view.mainloop()
```
-24
View File
@@ -1,24 +0,0 @@
# `gui/field_selector.py`
## Propósito
Diálogo modal para buscar y seleccionar paths del schema, con la posibilidad de asignar alias para los encabezados del CSV.
## API pública
### `class FieldSelectorDialog`
- `__init__(master, schema, sample=None, initial_selected=None)`: construye UI.
- `set_on_submit(callback)`: registra callback `(selected_fields, alias_map)`.
## Interacciones
- Búsqueda en vivo por texto.
- Seleccionar todo / limpiar.
- Vista de muestra JSON del primer documento.
- Validación de alias duplicados.
## Ejemplo
```python
dlg = FieldSelectorDialog(parent, schema, sample)
dlg.set_on_submit(lambda fields, aliases: print(fields, aliases))
```
-30
View File
@@ -1,30 +0,0 @@
# `gui/view.py`
## Propósito
Vista principal basada en `customtkinter` que define la interfaz de procesamiento y expone métodos para enlazar callbacks y comunicar estado.
## API pública
### `class InvoiceAppView`
- `set_on_select_root(cb)`, `set_on_process(cb)`, `set_on_select_csv(cb)`
- Getters: `get_root_path()`, `get_keywords()`, `get_fields()`, `get_csv_path()`
- Output: `append_output(text)`, `append_output_safe(text)`
- Estado: `set_processing(bool)`
### Keywords / patrones de archivos
- En la pestaña principal, cada Tipo de Documento tiene un campo de **Keyword/Patrón**.
- Se aceptan tanto palabras simples (sin `*` ni `?`) como patrones tipo Windows con `*` y `?`.
- Si el patrón **no incluye extensión**, se aplica sobre el nombre sin extensión y solo se consideran archivos `.pdf`, `.tif`, `.tiff`.
- Si el patrón **incluye extensión** válida (p. ej. `*_OK?.pdf`), se aplica sobre el nombre completo respetando esa extensión.
## Ejemplo
```python
from gui.view import InvoiceAppView
view = InvoiceAppView()
view.set_on_process(lambda: print("Procesar!"))
view.mainloop()
```