feat: Implement the initial IPSExtractor project structure, including application logic, GUI, core utilities, and documentation.
This commit is contained in:
+270
@@ -0,0 +1,270 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# `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()
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# `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}"),
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# `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")
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# `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")
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# `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"})
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# `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",
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# `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"]
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# 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.
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# `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()
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# `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))
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# `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