# Migración a microservicios y arquitectura IPSExtractor Documento de referencia para la rama `migration-services` respecto a `main`. Describe qué hay de nuevo, cómo funciona la infraestructura, los patrones de diseño, cómo escalar el sistema y cómo incorporar servicios futuros. > **Nota:** el repositorio no tiene rama `master`; la línea base de comparación es **`main`**. --- ## 1. Resumen ejecutivo IPSExtractor evolucionó de un **monolito Python** (interfaz gráfica de escritorio + API embebida) a una **arquitectura de microservicios** orquestada con Docker Compose, con una **aplicación de escritorio Electron + React** como cliente principal. | Aspecto | Estado actual | |---------|---------------| | Despliegue | `docker compose up` — stack completo en contenedores | | Puerta de entrada | **API Gateway** en `0.0.0.0:8189` (único puerto expuesto al host) | | Persistencia | PostgreSQL 16 + migraciones Alembic | | Cola async | Redis 7 + workers ARQ para extracción LLM | | Conteo | `count-api` (Go, Tesseract + ONNX), stateless | | Cliente | IPSExtractor Desktop **v1.0.0** (2026-07-06), instalador NSIS Windows | **Alcance de la migración (rama `migration-services`):** ~170 archivos modificados, +27 000 / −8 500 líneas, 5 commits sobre `main`. --- ## 2. Qué cambió respecto a `main` ### Antes y después | Antes (`main`) | Ahora (`migration-services`) | |----------------|------------------------------| | `gui/` + `gui_main.py` — UI desktop en Python | `services/desktop_app/` — Electron + React + TypeScript | | `server/app/` — API monolítica | `api_gateway` + `extraction` + `cleaning` + `count-api` | | `application/` — lógica de dominio acoplada al monolito | Dominio repartido bajo `services//` | | `installer.iss` / `build_installer.bat` — Inno Setup | `electron-builder` → `IPSExtractor-Desktop-1.0.0-Setup.exe` | | Sin orquestación unificada | [`docker-compose.yml`](../docker-compose.yml) — stack completo | | Sin modelo de trabajos en BD | PostgreSQL: `jobs`, `tasks`, conteo, extracción, limpieza | | Procesamiento síncrono en el mismo proceso | Redis + workers ARQ (`WORKER_REPLICAS`) | ### Eliminado - `gui/` — pestañas de extracción, limpieza, métricas, preclasificación, etc. - `application/` en la raíz del repo - `server/` — API FastAPI monolítica - `gui_main.py`, `requirements.txt` raíz, `installer.iss` - Tests legacy en `tests/` (sustituidos por tests por servicio / desktop) ### Añadido (highlights) | Ruta | Rol | |------|-----| | `services/api_gateway/` | Proxy HTTP único hacia el resto de servicios | | `services/extraction/` | Jobs, tasks, conteo en BD, extracción LLM, API REST | | `services/extraction/worker/` | Workers ARQ para procesamiento asíncrono | | `services/cleaning/` | Limpieza y transformación de datos CSV | | `services/services-count-facturas-medicas-serving/` | `count-api` — OCR + clasificación ONNX (Go) | | `services/db/` | Migraciones Alembic (PostgreSQL) | | `services/desktop_app/` | App Electron + React | | `docs/database.dbml` | Esquema completo de la base de datos | | `.env.example` | Variables de entorno de referencia | ### Commits en `migration-services` (sobre `main`) 1. Eliminación del monolito legacy + introducción de Docker Compose microservicios 2. App desktop Electron + wizards de extracción 3. Redis, `.env.example`, indexación de carpetas 4. Servicio `db-migrations`, jobs y conteo en PostgreSQL 5. Healthchecks, puerto 8189, OCR tuning, instalador desktop v1.0.0 --- ## 3. Infraestructura nueva ### 3.1 Despliegue Docker Solo el **API Gateway** publica un puerto al host. El resto de servicios se comunican por la red interna de Docker. | Servicio | Puerto host | Puerto interno | Tecnología | |----------|-------------|----------------|------------| | `api_gateway` | **8189** | 8000 | Python / FastAPI | | `extraction` | — | 8001 | Python / FastAPI | | `cleaning` | — | 8002 | Python / FastAPI | | `count-api` | — | 8080 | Go | | `database` | — | 5432 | PostgreSQL 16 | | `redis` | — | 6379 | Redis 7 | | `worker` | — | — | Python ARQ (N réplicas) | | `db-migrations` | — | — | Alembic (one-shot) | ```mermaid flowchart TB subgraph host [Host] Desktop[DesktopApp Electron] Port8189["0.0.0.0:8189"] end subgraph docker [Red Docker interna] GW[api_gateway :8000] EXT[extraction :8001] CLN[cleaning :8002] CNT[count-api :8080] WRK[worker ARQ x N] PG[(PostgreSQL)] RD[(Redis)] end Desktop -->|"HTTPS prod o 127.0.0.1:8189 dev"| Port8189 Port8189 --> GW GW --> EXT GW --> CLN GW --> CNT EXT --> PG EXT --> RD WRK --> PG WRK --> RD CLN --> PG ``` ### 3.2 Arranque y dependencias El orden de arranque está controlado por healthchecks y `depends_on`: 1. `database` y `redis` deben estar **healthy** 2. `db-migrations` ejecuta Alembic y termina con éxito 3. `extraction`, `cleaning`, `count-api` y `worker` arrancan 4. `api_gateway` espera a `extraction` healthy y `count-api` healthy ```bash cp .env.example .env docker compose up --build curl http://127.0.0.1:8189/health/live ``` > En Windows use `127.0.0.1` en lugar de `localhost` para evitar timeouts por resolución IPv6 (`::1`). ### 3.3 Flujo de negocio (centrado en jobs) Un **job** agrupa el trabajo del usuario (carpeta de documentos). Dentro del job se ejecutan **tasks** por módulo (`counting`, `extraction`, `cleaning`). ```mermaid sequenceDiagram participant App as DesktopApp participant GW as ApiGateway participant CNT as CountApi participant EXT as Extraction participant DB as PostgreSQL App->>GW: POST /count/v1/count GW->>CNT: proxy OCR+ONNX CNT-->>App: resultados por PDF App->>GW: POST /count-jobs GW->>EXT: persistir conteo EXT->>DB: count_job_files App->>GW: POST /tasks + ingest-many GW->>EXT: encolar extracción EXT->>DB: extraction_rows ``` ### 3.4 Rutas del API Gateway El gateway en [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py) actúa como reverse proxy: | Prefijo público | Servicio upstream | |-----------------|-------------------| | `/jobs`, `/tasks`, `/count-jobs` | `extraction:8001` | | `/extraction/*` | `extraction:8001` | | `/cleaning/*` | `cleaning:8002` | | `/count/*` | `count-api:8080` | | `/health`, `/health/live`, `/config`, `/models` | Agregación / proxy | Timeouts diferenciados: peticiones JSON (~30 s) vs uploads OCR/conteo (hasta 600 s). --- ## 4. Patrones de diseño | Patrón | Ubicación | Propósito | |--------|-----------|-----------| | **API Gateway / Reverse Proxy** | [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py) | Un solo endpoint público; enrutamiento por prefijo | | **Microservicios sin paquetes compartidos** | `services/*` | Cada servicio con Dockerfile, dominio y dependencias propias | | **Cola de trabajos async** | [`services/extraction/worker/`](../services/extraction/worker/), Redis | Extracción LLM fuera del ciclo request/response HTTP | | **Registry / Plugin (desktop)** | [`services/desktop_app/src/renderer/modules/registry.ts`](../services/desktop_app/src/renderer/modules/registry.ts) | Módulos `counting` / `extraction` se registran sin acoplar `App.tsx` | | **Job + Task (agregado)** | Tablas `jobs` → `tasks` en PostgreSQL | Un trabajo del usuario agrupa módulos ejecutados | | **Healthchecks en cadena** | [`docker-compose.yml`](../docker-compose.yml) | `depends_on: service_healthy` evita arranques prematuros | | **BFF con timeouts por ruta** | Gateway `_is_upload_path()` | JSON ligero vs uploads pesados | | **Stateless vs stateful** | `count-api` (stateless) vs `extraction` (stateful + BD) | Conteo horizontalmente escalable; extracción con persistencia | ### Registry de módulos (desktop) Cada módulo implementa `ModuleDescriptor` y se auto-registra al importarse: ```typescript // services/desktop_app/src/renderer/modules/counting/CountingModule.ts registerModule({ type: "counting", label: "Conteo", Wizard: CountingWizardModule, Results: CountingResults, hasTask: (tasks) => tasks.some((t) => t.type === "counting"), }); ``` `App.tsx` solo importa los módulos; no necesita conocer su implementación interna. --- ## 5. Componentes principales ### 5.1 Backend | Servicio | Responsabilidad | |----------|-----------------| | **api_gateway** | Proxy HTTP, `/health/live`, CORS, logging, errores 503/504 hacia upstream caído | | **extraction** | CRUD de jobs/tasks, count-jobs, ingest de PDFs, estimación de costos, API LLM | | **worker** | Procesamiento ARQ en background; escala con `WORKER_REPLICAS` | | **count-api** | OCR (Tesseract) + inferencia ONNX por lote; sin estado ni BD | | **cleaning** | Reglas de limpieza CSV; expuesto vía `/cleaning/*` | | **db-migrations** | Alembic one-shot antes de que arranquen los servicios de aplicación | ### 5.2 Base de datos (resumen) ``` jobs — contenedor de un trabajo del usuario tasks — módulo ejecutado (counting / extraction / cleaning) count_job_files — resultado de conteo por PDF extraction_batches — lote de extracción extraction_rows — fila extraída por documento cleaning_jobs — job de limpieza cleaned_rows — fila transformada ``` Esquema completo: [`database.dbml`](database.dbml) ### 5.3 Desktop (`services/desktop_app/`) | Componente | Función | |------------|---------| | `App.tsx` | Navegación Home / Job / Settings; sidebar con submenú por job | | `HomeView.tsx` | Listado y creación de jobs | | `JobView.tsx` | Paneles de conteo y extracción (progreso en segundo plano) | | `JobResultsView.tsx` | Resultados unificados, export CSV, lanzar extracción desde válidos | | `CountingWizard.tsx` | Lotes paralelos hacia `POST /count/v1/count` | | `JobExtractionWizard.tsx` | Extracción con upload de PDFs desde el escritorio | | `apiClient.ts` | Cliente HTTP único hacia el gateway (timeouts, mensajes de error) | | `electron/folderIndex.cjs` | Indexación local: modo `general` o `radicados` | | `electron/preload.cjs` | Bridge seguro main ↔ renderer (`contextBridge`) | --- ## 6. Cómo escalar para mayor velocidad ### 6.1 Conteo (`count-api`) El conteo es el cuello de botella más frecuente en lotes grandes de PDFs. | Variable / ajuste | Efecto | Dónde | |-------------------|--------|-------| | `OCR_DPI=150` | ~4× menos RAM que 300 DPI | `docker-compose.yml` | | `OCR_POOL_SIZE` | Goroutines OCR por request | `docker-compose.yml` | | `SYNC_PROCESS_CONCURRENCY` | Paralelismo interno por request | `docker-compose.yml` | | `COUNT_API_MEMORY` | Límite RAM del contenedor (evitar OOM) | `docker-compose.yml` | | `restart: unless-stopped` | Recuperación automática tras OOM | `docker-compose.yml` | | `BATCH_SIZE=10` | PDFs por petición HTTP | `CountingWizard.tsx` | | `BATCH_CONCURRENCY=2` | Lotes HTTP simultáneos desde desktop | `CountingWizard.tsx` | **Futuro:** réplicas de `count-api` detrás del gateway con balanceo round-robin en el proxy. ### 6.2 Extracción LLM | Variable / comando | Efecto | |--------------------|--------| | `WORKER_REPLICAS=4` | Más workers ARQ procesando en paralelo | | `docker compose up --scale worker=4` | Escala horizontal de workers | | `WORKER_CONCURRENCY` | Tareas concurrentes por worker | | `CONCURRENCY_LIMIT` | Límite global en el servicio extraction | | `PDF_THREAD_COUNT` | Hilos para renderizado de PDF | | `PDF_DPI` | Resolución de páginas enviadas al LLM | ### 6.3 Infraestructura y build | Problema | Solución | |----------|----------| | BuildKit satura al construir `count-api` | `docker compose build count-api` antes del stack completo | | Build paralelo falla en Docker Desktop | `COMPOSE_BAKE=false COMPOSE_PARALLEL_LIMIT=2` (ver `.env.example`) | | Conexiones BD colgadas | Pool SQLAlchemy + `connect_timeout` en [`database.py`](../services/extraction/infrastructure/database.py) | | Gateway sin respuesta en Windows | Usar `127.0.0.1:8189`, no `localhost` | --- ## 7. Cómo añadir un servicio futuro Checklist para incorporar un nuevo microservicio sin romper la arquitectura: ### Paso 1 — Crear el servicio ``` services/mi-servicio/ Dockerfile api/main.py # API propia (FastAPI, Go, etc.) domain/ # lógica de negocio requirements.txt README.md ``` ### Paso 2 — Registrar en Docker Compose En [`docker-compose.yml`](../docker-compose.yml): - Añadir el servicio **sin** `ports` al host (solo red interna) - Definir `healthcheck` si expone `/health` - Configurar `depends_on` según dependencias (BD, migraciones, etc.) ### Paso 3 — Exponer vía API Gateway En [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py): ```python MI_SERVICIO_URL = os.getenv("MI_SERVICIO_URL", "http://mi-servicio:8003").rstrip("/") @app.api_route("/mi-servicio/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) async def mi_servicio_proxy(path: str, request: Request) -> Response: return await _proxy_request(request, f"{MI_SERVICIO_URL}/{path}") ``` Añadir `MI_SERVICIO_URL` en el bloque `environment` de `api_gateway` en compose. ### Paso 4 — Base de datos (si aplica) 1. Crear migración Alembic en `services/db/alembic/versions/` 2. El servicio `db-migrations` la aplicará en el próximo `docker compose up` ### Paso 5 — Módulo en la app desktop (si aplica) 1. Crear `services/desktop_app/src/renderer/modules/mi-modulo/` 2. Implementar `ModuleDescriptor` y llamar `registerModule()` 3. Importar el módulo en `App.tsx` (una línea, igual que counting/extraction) 4. Añadir entrada en el sidebar de `JobView` si requiere sección propia ### Paso 6 — Documentación - README del servicio - Actualizar tabla de servicios en [`README.md`](../README.md) - Actualizar este documento si el patrón de integración cambia ```mermaid flowchart LR NewSvc[nuevo_servicio] Compose[docker-compose] GW[api_gateway nueva ruta] Reg[registerModule desktop] NewSvc --> Compose Compose --> GW GW --> Desktop Reg --> Desktop ``` --- ## 8. App de escritorio — versión y funcionalidades ### 8.1 Versión e instalador | Campo | Valor | |-------|-------| | **Versión** | 1.0.0 | | **Fecha de release** | 2026-07-06 | | **Instalador** | `services/desktop_app/release/IPSExtractor-Desktop-1.0.0-Setup.exe` | | **Formato** | NSIS, Windows x64 | | **Fuente de versión** | `services/desktop_app/package.json` + `src/shared/version.json` | Generar instalador: ```bash cd services/desktop_app npm install npm run dist ``` ### 8.2 URL del API Gateway | Entorno | URL por defecto | |---------|-----------------| | Desarrollo (`npm run dev`) | `http://127.0.0.1:8189` | | Instalador (build producción) | `https://pys1.builker.xyz` | La URL es configurable en **Configuración** de la app y se persiste en `localStorage`. ### 8.3 Funcionalidades incluidas - **Gestión de trabajos (jobs):** crear, abrir y organizar trabajos con carpeta local de documentos - **Conteo:** OCR + clasificación ML en lotes con barra de progreso en segundo plano - **Validación humana:** marcar archivos del conteo como válidos/inválidos - **Extracción LLM:** wizard con descubrimiento de campos, estimación de costo y ejecución - **Extracción desde conteo:** lanzar extracción solo sobre PDFs válidos del conteo - **Resultados unificados:** vista de conteo y extracción con exportación CSV - **Configuración:** URL del gateway, verificación de conexión, vista de config/modelos del backend - **Versión visible:** sidebar (menú) y esquina superior derecha (`v1.0.0 · 6 jul 2026`) ### 8.4 Arquitectura de la UI ``` Home (lista de jobs) └── Job activo ├── Conteo → CountingWizardModule ├── Extracción → JobExtractionWizard └── Resultados → JobResultsView Configuración (global) ``` La navegación usa sidebar lateral con submenú contextual por job (sin tabs en el área principal). --- ## 9. Enlaces relacionados | Documento | Descripción | |-----------|-------------| | [`README.md`](../README.md) | Guía rápida del stack completo | | [`services/api_gateway/README.md`](../services/api_gateway/README.md) | API Gateway | | [`services/extraction/README.md`](../services/extraction/README.md) | Extracción LLM y jobs | | [`services/cleaning/README.md`](../services/cleaning/README.md) | Limpieza de datos | | [`services/services-count-facturas-medicas-serving/README.md`](../services/services-count-facturas-medicas-serving/README.md) | Count API (Go) | | [`services/db/README.md`](../services/db/README.md) | Migraciones Alembic | | [`services/desktop_app/README.md`](../services/desktop_app/README.md) | App Electron | | [`database.dbml`](database.dbml) | Esquema completo de PostgreSQL | | [`.env.example`](../.env.example) | Variables de entorno | --- *Última actualización: 2026-07-06 — rama `migration-services`.*