EN
Comentarios Automáticos en Reportes PDF — banner
← Volver a proyectos

Comentarios Automáticos en Reportes PDF

· 8 min de lectura

Aplicación web interna de una empresa chilena enfocada a la seguridad vial que genera comentarios automáticos en reportes PDF generados en Power BI.

Vite React TypeScript TailwindCSS FastAPI Python GPT-4o-mini

Stack Tecnológico

CapaTecnologías
FrontendReact 18, TypeScript (strict), Vite 6, TailwindCSS v4
UI ComponentsMUI Material, Lucide React, React Dropzone
BackendFastAPI, Uvicorn, ThreadPoolExecutor
IA / LLMOpenAI Assistants API (gpt-4o-mini + code_interpreter)
Procesamiento de datosPandas, OpenPyXL
Manipulación PDFPyMuPDF (lectura, renderizado PNG, inserción HTML)
Estados / RoutingReact Context API, React Router DOM v7

Arquitectura General

┌──────────────┐     POST /upload          ┌──────────────────────────────────┐
│   Frontend   │ ──────────────────────────▶│          Backend (FastAPI)        │
│  React + TS  │                            │                                  │
│  TailwindCSS │ ◀──────────────────────────│  ┌──────────┐  ┌──────────────┐ │
└──────────────┘     JSON + PNGs + PDF      │  │  upload   │  │  generate    │ │
                                             │  └──────────┘  └──────┬───────┘ │
                                             │                       │         │
                                             │  ┌──────────┐  ┌──────▼───────┐ │
                                             │  │  apply   │  │  regenerate  │ │
                                             │  └────┬─────┘  └──────────────┘ │
                                             │       │                         │
                                             └───────┼─────────────────────────┘


                                            ┌─────────────────┐
                                            │   OpenAI API    │
                                            │  (Assistants)   │
                                            └─────────────────┘

Flujo de datos

  1. Subida: El usuario arrastra/suelta un PDF nombrado siguiendo la convención Empresa - Semana N AAAA.pdf.
  2. Parseo: El backend extrae el nombre del cliente y la semana, descarga datos desde una API REST externa.
  3. Extracción: PyMuPDF extrae títulos de cada página del PDF y clasifica el tipo de gráfico (ranking, evolución, vehículos).
  4. Filtrado: Cada página genera un CSV individual aplicando filtros específicos del cliente según el tipo de gráfico detectado.
  5. Renderizado: Se exporta una imagen PNG por cada página para previsualización en el frontend.
  6. IA: Cada CSV se convierte a texto, se construye un prompt con el título, el contexto del cliente y los datos, y se envía al asistente de OpenAI.
  7. Revisión: El usuario ve cada página con su observación generada, puede aprobar, editar, eliminar o regenerar cualquiera.
  8. Exportación: Las observaciones finales se insertan como HTML en posiciones fijas del PDF usando PyMuPDF.

Características Principales

  • Generación automática de observaciones mediante IA contextualizada con datos reales del reporte y metadatos del cliente.
  • Multi-cliente extensible con sistema de plugins: añadir un nuevo cliente requiere solo dos archivos sin modificar el núcleo.
  • Procesamiento en paralelo de hasta 8 páginas simultáneas con ThreadPoolExecutor para minimizar latencia.
  • Previsualización completa de cada página del PDF como imagen PNG junto a su observación generada.
  • Flujo de revisión interactivo: aprobar, editar inline, regenerar con IA o eliminar observaciones página por página.
  • Inserción tipográfica profesional en PDF con fuente Montserrat-Regular 24px vía insert_htmlbox de PyMuPDF.
  • Arquitectura desacoplada: frontend y backend independientes, comunicación vía REST/JSON.

Flujo de Usuario

Pantalla 1: Inicio

El usuario ve ejemplos del formato esperado de nombre de archivo y arrastra/suelta su PDF. El frontend llama a POST /upload y redirige a la vista de reportes.

Pantalla 2: Generación

Al cargar la vista de reportes, se llama automáticamente a POST /generate-observations. El backend:

  • Copia el PDF al directorio de trabajo del cliente/semana.
  • Exporta CSVs por página filtrando datos según el tipo de gráfico detectado.
  • Renderiza PNGs de cada página.
  • Envía cada página (vía CSV + prompt) al asistente de OpenAI en paralelo.
  • Retorna la lista de observaciones, URLs de PNGs y páginas excluidas.

Mientras se procesa, el frontend muestra una animación de carga con Lottie.

Pantalla 3: Revisión

Se renderiza un grid de tarjetas, cada una con:

  • Imagen PNG de la página del PDF.
  • Tarjeta de observación (editable inline al hacer clic).
  • Botones: aprobar (marca borde verde), regenerar con IA (POST /regenerate-observation), eliminar.

Pantalla 4: Exportación

Al presionar “Exportar”, se llama a POST /apply-observations con el JSON de observaciones finales. El backend inserta cada texto en el PDF y devuelve la URL de descarga, que se abre en una nueva pestaña.


Sistema Multi-Cliente (Plugin Pattern)

Cada cliente reside en su propio directorio bajo backend/src/clients/<nombre>/ con exactamente dos archivos:

clients/
├── cliente_a/
│   ├── config.py      # Constructor de metadatos + parser de títulos
│   └── filters.py     # Funciones de filtrado por tipo de gráfico
├── cliente_b/
│   ├── config.py
│   └── filters.py
└── ...
  • config.py: Define la metadata del cliente (niveles de riesgo, reemplazos de texto, etc.) y un parser de títulos que extrae parámetros del título de cada página (fechas, flotas, tipos de vehículo) para pasarlos como argumentos a las funciones de filtrado.
  • filters.py: Contiene funciones puras que reciben un DataFrame y parámetros extraídos del título, y retornan el subconjunto filtrado de datos. Cada tipo de gráfico (ranking, evolución, vehículos) tiene su propia función.

Para registrar un nuevo cliente, se añade una entrada en tres ubicaciones: get_filters.py, json_utils.py y el diccionario CLIENTS en setup.py.

Este diseño permite que el núcleo de la aplicación (build_csv.py, run_observations.py, report_generator.py) opere de forma genérica sin conocer los detalles de cada cliente.


Integración con OpenAI

Assistants API (no Chat Completions)

Se utiliza la Assistants API de OpenAI con el modelo gpt-4o-mini y la herramienta code_interpreter habilitada. Esto le da al asistente un entorno de ejecución de código para analizar los datos tabulares del CSV antes de redactar la observación.

Flujo por cada página:

  1. Se crea un thread nuevo por página (aislamiento total entre observaciones).
  2. Se envía un mensaje con el prompt que incluye: título de la página, datos del CSV en texto plano, contexto del cliente en JSON, y la semana de referencia.
  3. Se ejecuta el run con create_and_poll (espera síncrona hasta completar).
  4. Se extrae la respuesta del asistente del historial del thread.

Paralelismo

Las llamadas a OpenAI se ejecutan en paralelo usando ThreadPoolExecutor(max_workers=8). Cada worker procesa una página distinta, lo que reduce el tiempo total de generación de ~N segundos secuenciales a ~N/8 segundos.

Conteo de tokens

Se utiliza tiktoken para contar tokens antes de enviar prompts, asegurando que no se exceda la ventana de contexto del modelo.


Procesamiento de PDF

Toda la manipulación de PDF se realiza con PyMuPDF en tres etapas:

1. Lectura y extracción

  • extract_titles(): Recorta un área definida del 12% superior de cada página (y0=0, y1=0.12) y extrae el texto para identificar el título del gráfico.
  • function_title(): Clasifica el título en categorías (ranking, evolución, vehículos, generic) mediante análisis de palabras clave.
  • Las páginas sin título reconocible se marcan como excluidas y no pasan por IA.

2. Renderizado PNG

  • CSVExporter.exportPNG(): Renderiza cada página del PDF a una imagen PNG para la previsualización en el frontend.

3. Inserción de observaciones

  • insert_observations(): Para cada observación aprobada, inserta un bloque HTML en una posición fija de la página (2%-98% ancho, 87%-98.5% alto).
  • Usa page.insert_font() para cargar Montserrat-Regular como fuente tipográfica.
  • Usa page.insert_htmlbox() con CSS inline (font-size: 24px) para escribir el texto de la observación precedido por la etiqueta <b>Observación:</b>.

Frontend

Tecnologías y decisiones de arquitectura

  • React 18 con TypeScript strict mode (noUnusedLocals, noUnusedParameters).
  • Vite 6 como bundler con el plugin nativo de TailwindCSS v4 (sin PostCSS ni tailwind.config.js).
  • TailwindCSS v4 para estilos utility-first.
  • MUI Material para componentes de UI complementarios (diálogos, loaders).
  • React Context API como gestor de estado global: un PDFContext que comparte el archivo PDF, observaciones, PNGs, empresa y semana entre todos los componentes.
  • AlertContext con provider para notificaciones toast (éxito/error).
  • React Router DOM v7 con dos rutas: Home (subida) y Reports (visualización y exportación).
  • React Dropzone para la zona de arrastrar y soltar archivos PDF.
  • Lucide React para iconografía.
  • LottieFiles dotlottie-react para animaciones de carga.

Componentes principales

ComponenteFunción
DragDropZona de dropzone que sube el PDF vía POST /upload y navega a /reports
PageCardGrid responsivo de páginas, cada una con su PNG y ObservationCard
ObservationCardTexto editable inline + botones de aprobar, regenerar y eliminar
ExportButtonEnvía POST /apply-observations y abre el PDF final en nueva pestaña
LoadingContainerAnimación Lottie con spinner mientras se generan observaciones
ConfirmationDialogDiálogo MUI de confirmación antes de eliminar/regenerar
Approve / Regenerate / DeleteBotones de acción individuales por observación

Desafíos Técnicos y Aprendizajes

Diseño del sistema multi-cliente

El mayor desafío arquitectónico fue diseñar una abstracción para añadir nuevos clientes sin modificar el núcleo de procesamiento. El patrón de plugin resolvió esto: cada cliente expone su lógica de filtrado y parseo mediante una interfaz implícita (dos archivos con funciones esperadas). El router get_filters.py actúa como despachador dinámico.

Paralelización de llamadas a OpenAI

Las llamadas secuenciales a la API de OpenAI para 30+ páginas tomaban más de 2 minutos. La migración a ThreadPoolExecutor con 8 workers redujo el tiempo a ~20-30 segundos. El mayor aprendizaje fue manejar correctamente los threads de OpenAI (cada uno crea su propio thread de conversación) y recolectar resultados con as_completed.

Parseo de títulos desde PDF

Extraer información estructurada de los títulos de cada página requirió combinar OCR implícito de PyMuPDF con recorte de regiones, limpieza de texto y clasificación por palabras clave. Cada cliente tiene además su propio parser de títulos para extraer parámetros específicos (fechas, flotas, tipos) desde formatos de título no estandarizados.

Inserción tipográfica en PDF

La inserción de texto en PDFs generados por Power BI presentó desafíos debido a que insert_htmlbox de PyMuPDF no soporta todas las propiedades CSS. Las fuentes TTF embebidas, el posicionamiento proporcional y el HTML mínimo con CSS inline lograron un resultado profesional.

TypeScript strict desde el inicio

Configurar el proyecto con noUnusedLocals y noUnusedParameters desde el primer día forzó disciplina en el tipado y evitó acumulación de código muerto, aunque requirió más rigor durante el desarrollo.


Referencias