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
+6 -25
View File
@@ -18,7 +18,7 @@ from application.preclassification import PreclassificationService
from application.strategies import OllamaExtractor
from application.database import DatabaseManager
from application.config import settings
from application.costs import compute_cost_from_usage, MODEL_PRICING, sum_costs
from application.costs import MODEL_PRICING
import pandas as pd
@@ -168,8 +168,7 @@ class AppController:
selected = fd.askdirectory()
if not selected:
return
self.view.root_entry.delete(0, 'end')
self.view.root_entry.insert(0, selected)
self.view.set_root_path(selected)
# Detectar si el primer documento a procesar es TIFF para esta carpeta
try:
@@ -195,26 +194,15 @@ class AppController:
selected = fd.askdirectory()
if not selected:
return
try:
self.view.preclass_root_entry.delete(0, 'end')
self.view.preclass_root_entry.insert(0, selected)
except Exception:
# fallback si cambia el nombre del widget
try:
setter = getattr(self.view, "set_preclass_root_path", None)
if callable(setter):
setter(selected)
except Exception:
pass
self.view.set_preclass_root_path(selected)
def on_run_preclassification(self):
"""Ejecuta la preclasificación en un hilo separado."""
# 1. Leer inputs de la UI
root = self.view.get_preclass_root_path()
mode = getattr(self.view, "get_preclass_mode", lambda: "radicados")()
patterns = self.view.get_preclass_patterns()
target_info = self.view.get_preclass_target_info()
page_range = self.view.get_preclass_range() # (enabled, start, end)
page_range = self.view.get_preclass_range()
mode = self.view.get_preclass_mode()
# Validaciones básicas
@@ -359,15 +347,9 @@ class AppController:
# --- Pestaña Resultados ---
def on_res_select(self, event):
"""Muestra detalles del resultado seleccionado."""
selection = self.view.tree_results.selection()
if not selection:
return
# values: (ID, Run, Fecha, Radicado, Estado, Archivo)
item_vals = self.view.tree_results.item(selection[0], "values")
item_vals = self.view.get_selected_result_values()
if not item_vals:
return
try:
res_id = int(item_vals[0])
matches = self.db_manager.get_matches_for_result(res_id)
@@ -464,8 +446,7 @@ class AppController:
"""Abre un diálogo de guardado y establece la ruta del CSV de salida."""
selected = fd.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV", "*.csv"), ("Todos", "*.*")])
if selected:
self.view.csv_entry.delete(0, 'end')
self.view.csv_entry.insert(0, selected)
self.view.set_csv_path(selected)
def on_process(self):
"""Inicia el flujo de procesamiento por radicado (solo facturas) en un hilo de fondo.
+15 -1
View File
@@ -25,6 +25,8 @@ class MainProcessingTab:
self._create_config_section()
self._create_process_button()
self._create_output_section()
# Conectar el trace después de que todos los widgets existen
self.mode_var.trace_add("write", lambda *_: self.update_ui_for_mode())
def _setup_layout(self):
"""Configura el layout base de la pestaña."""
@@ -297,10 +299,22 @@ class MainProcessingTab:
def get_root_path(self) -> str:
"""Devuelve la ruta raíz seleccionada."""
return self.root_entry.get().strip()
def set_root_path(self, path: str) -> None:
"""Establece la ruta raíz."""
self.root_entry.configure(state="normal")
self.root_entry.delete(0, "end")
self.root_entry.insert(0, path)
def get_csv_path(self) -> str:
"""Devuelve la ruta del CSV."""
return self.csv_entry.get().strip()
def set_csv_path(self, path: str) -> None:
"""Establece la ruta del CSV."""
self.csv_entry.configure(state="normal")
self.csv_entry.delete(0, "end")
self.csv_entry.insert(0, path)
def get_process_mode(self) -> str:
"""Devuelve el modo de procesamiento: 'radicados' o 'general'."""
+4 -2
View File
@@ -3,8 +3,11 @@ import customtkinter as ctk
import tkinter.ttk as ttk
from gui.styles import AppStyles
# matplotlib es opcional
# matplotlib y numpy son opcionales
try:
import matplotlib
matplotlib.use("Agg") # backend no interactivo para render en canvas Tk
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
HAS_MPL = True
@@ -226,7 +229,6 @@ class MetricsTab:
extract_vals = [input_extract, cached_extract, output_extract]
norm_vals = [input_norm, cached_norm, output_norm]
import numpy as np
x = np.arange(len(labels))
width = 0.35
+7 -1
View File
@@ -388,7 +388,13 @@ class PreclassificationTab:
def get_root_path(self) -> str:
"""Devuelve la ruta raíz de preclasificación."""
return self.preclass_root_entry.get().strip()
def set_root_path(self, path: str) -> None:
"""Establece la ruta raíz de preclasificación."""
self.preclass_root_entry.configure(state="normal")
self.preclass_root_entry.delete(0, "end")
self.preclass_root_entry.insert(0, path)
def get_mode(self) -> str:
"""Devuelve el modo de preclasificación: 'radicados' o 'general'."""
try:
+7
View File
@@ -202,6 +202,13 @@ class ResultsTab:
def set_on_select_result(self, callback):
"""Vincula el callback al evento de selección de fila."""
self.tree_results.bind("<<TreeviewSelect>>", callback)
def get_selected_result_values(self) -> tuple | None:
"""Devuelve los valores de la fila seleccionada o None si no hay selección."""
selection = self.tree_results.selection()
if not selection:
return None
return self.tree_results.item(selection[0], "values") or None
def update_matches_table(self, rows):
"""Actualiza la tabla de matches con los datos proporcionados."""
+13 -13
View File
@@ -3,19 +3,11 @@
Expone métodos para enlazar callbacks, leer entradas y escribir salida en un
área de texto, además de habilitar/deshabilitar controles durante el proceso.
"""
import os # Necesario para basename en update_res_table
import os
import customtkinter as ctk
from gui.styles import AppStyles
from gui.tabs import SettingsTab, ResultsTab, MainProcessingTab, PreclassificationTab, MetricsTab
# matplotlib es opcional
try:
import matplotlib
matplotlib.use("Agg") # backend no interactivo para render en canvas
HAS_MPL = True
except Exception:
HAS_MPL = False
class InvoiceAppView(ctk.CTk):
"""Ventana principal con pestañas de Procesar y Configuración."""
@@ -160,13 +152,21 @@ class InvoiceAppView(ctk.CTk):
def append_preclass_output_safe(self, text: str) -> None:
self.after(0, lambda: self.preclass_tab.append_output_safe(text))
def _update_ui_for_mode(self):
"""Actualiza la visibilidad de componentes según el modo seleccionado."""
self.main_tab.update_ui_for_mode()
def get_root_path(self) -> str:
return self.main_tab.get_root_path()
def set_root_path(self, path: str) -> None:
self.main_tab.set_root_path(path)
def set_csv_path(self, path: str) -> None:
self.main_tab.set_csv_path(path)
def set_preclass_root_path(self, path: str) -> None:
self.preclass_tab.set_root_path(path)
def get_selected_result_values(self) -> tuple | None:
return self.results_tab.get_selected_result_values()
def get_process_mode(self) -> str:
"""Devuelve el modo de procesamiento: 'radicados' o 'general'."""
return self.main_tab.get_process_mode()