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:
-270
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
|
||||
@@ -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}"),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
|
||||
@@ -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"})
|
||||
```
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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"]
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
|
||||
@@ -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))
|
||||
```
|
||||
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user