MIA — el cerebro de
conocimiento de Grupo OSG
Un chat que responde a los 50 empleados solo con la información real de la empresa, cita de qué documento sale cada cosa, y cuando no sabe, escala a un humano en vez de inventar.
Qué es MIA, en concreto
La interfaz es literalmente una caja de chat: escribes, responde, y cita de qué documento sale cada cosa. Casi todo el producto vive por debajo. Busca exclusivamente en la base de conocimiento de OSG; si la respuesta no está ahí, no la improvisa.
Nunca responde con conocimiento genérico de internet. La base es propiedad de OSG, curada y mantenida centralmente.
Cuando no sabe, lo dice honestamente y activa el circuito de escalado. El guardarraíl es de arquitectura, no una instrucción al modelo.
Cada afirmación enlaza al documento de origen, para que el empleado pueda verificar y profundizar.
Tipos de preguntas que resuelve
Operativas y de procedimiento — "cómo pido las vacaciones"
Técnicas — "qué tornillos pongo en este modelo"
De orientación — "a quién me dirijo si..."
De protocolo y valores — normas de calidad, trato al cliente, cultura
La clave de arquitecturaMIA es una app independiente y autosuficiente: su propio backend, base de datos, chat y vault. Funciona sola de principio a fin. Que luego se integre en Atenea es solo dónde se muestra, no cómo está construida — igual que Atenea consume Sage.
Cómo encaja todo
Un backend autocontenido en Docker Compose sobre el servidor de OSG, que habla con la vault (un Gitea propio en el mismo servidor), Claude API, el chat y el circuito de escalado. Atenea no aparece todavía: se enchufa "por arriba" cuando esté lista.
Por qué MIA es una app aparte, no un módulo de Atenea
integrado por arriba
MIA se construye como servicio autónomo y solo se muestra dentro de Atenea. La integración no es una fase de desarrollo de MIA: es enchufar algo que ya funciona, igual que Atenea consume Sage.
La tentación sería meter MIA como un módulo más dentro del backend de Atenea. Sería un error, y estas son las razones por las que se construye como aplicación independiente que Atenea consumirá:
- · No puede salir a producción hasta que Atenea esté lista. MIA queda bloqueada por un proyecto mucho más grande.
- · Obliga a meter el mundo RAG (Python, pgvector, workers) dentro de un backend Java/Spring que no es su sitio natural.
- · Reindexar la vault o tocar el pipeline implica desplegar Atenea entera.
- · Un fallo de MIA puede tumbar Atenea, y al revés. Ciclos de vida atados.
- · MIA queda atrapada ahí: no se puede reutilizar en otro sitio ni ofrecer como producto propio.
- · Da valor a OSG desde el día uno, sin esperar a Atenea. No hay bloqueo.
- · Cada mundo con su stack: RAG en Python/FastAPI, Atenea en Java/Spring. Cada uno usa la herramienta correcta.
- · Ciclo de vida propio: reindexar, actualizar prompts o desplegar MIA no toca Atenea.
- · Aislamiento de fallos: cada servicio cae y se levanta por su cuenta.
- · Reutilizable: su chat es una superficie mínima que se puede embeber en Atenea, usar sola, o llevar a otro cliente.
Los dos motivos que lo cierran
Atenea ya consume Sage como sistema externo en vez de reimplementarlo dentro. MIA sigue el mismo patrón probado: un servicio que se consume por API, no código que se reescribe dentro de otro. Atenea le pasa la identidad del usuario y muestra el chat como un módulo más.
Como la respuesta llega en streaming (SSE), lo natural es que el frontend hable directo con la API de MIA, sin proxear el stream por el backend de Atenea. Eso solo tiene sentido si MIA expone su propia API — es decir, si es una app aparte.
En una fraseLa integración con Atenea es dónde se muestra MIA, no cómo está construida. Separarla no añade trabajo: lo quita, porque desacopla dos proyectos que avanzan a ritmos distintos.
Stack, con versiones concretas
| Pieza | Elección | Notas |
|---|---|---|
| Backend | FastAPI · Python 3.12 | async nativo, encaja con streaming y llamadas a Claude |
| Base de datos | PostgreSQL 16 + pgvector | una pieza para datos y vectores |
| Vault (almacén) | Gitea autoalojado | repo Git de los .md dentro del servidor de OSG |
| ORM / migraciones | SQLAlchemy 2.x · Alembic | async |
| Generación | Claude API (Sonnet) | SDK oficial anthropic |
| Embeddings | API multilingüe asumido | API-first para MVP; migrable a local |
| Cola / tareas | Celery + Redis | reindexado, emails |
| Resend | avisos de escalado | |
| Frontend chat | React + TypeScript + Vite | interfaz mínima, reutilizable en Atenea |
| Despliegue | Docker Compose | todo en el servidor de OSG |
| Auth | Magic link (Resend) asumido | sin SSO mencionado; cambiar a OAuth si existe |
Dos decisiones de partida a confirmar
EmbeddingProvider, así que migrar a local (ej. bge-m3 self-hosted) es cambiar una implementación, no rediseñar. Si la privacidad es requisito duro desde el día 1 (nada del conocimiento interno sale del servidor), se invierte el orden. Nota: aunque los embeddings sean locales, la generación sí pasa por Claude API.PostgreSQL + pgvector
Nueve tablas. Haz clic en cada una para ver sus campos. El punto donde se cruzan gobernanza y escalado es la tabla areas.
Un registro por archivo .md de la vault.
La unidad real de recuperación. Cada documento se trocea en varios chunks.
Donde se cruzan gobernanza de lectura y routing de escalado.
conversations: id, user_id, language, created_at.
messages: id, conversation_id, role (user/assistant), content, escalated (bool), created_at.
message_citations: message_id ↔ chunk_id — mapea cada respuesta a los fragmentos que citó, para enlazar al archivo origen.
Existen porque asumido el chat es multi-turno con contexto acotado. Si cada pregunta debe ser independiente, se simplifica eliminando el concepto de sesión.
Gobernanza · asumido abierto por defectoEl schema ya soporta restringir por área, pero el filtrado en recuperación empieza desactivado: cualquier empleado autenticado recupera de cualquier área. Si hay restricciones (ej. RRHH/nóminas), se activa un filtro WHERE area_id IN (...) sin tocar el schema.
Tres piezas + el guardarraíl
En el worker, disparada por push a la vault. No se ejecuta en cada pregunta.
Gitea (push) → POST /webhooks/vault → encola tarea Celery reindex_vault.
El worker hace git pull, compara el hash guardado vs el actual y calcula qué archivos cambiaron.
Por archivo nuevo/modificado: frontmatter (área, responsable) + troceo por encabezados (máx ~500 tokens, solape ~50 para no cortar ideas).
Genera embedding de cada chunk vía EmbeddingProvider.
Upsert en documents/chunks. Archivos borrados en la vault → borrado en cascada.
Búsqueda híbrida: semántica + palabra clave. Por cada pregunta.
ORDER BY embedding <=> :q LIMIT k — distancia coseno con índice hnsw. Entiende "vacaciones" aunque el doc diga "solicitud de ausencias".
content_tsv @@ plainto_tsquery(:q) — acierta con referencias exactas (códigos de tornillo, de modelo).
Combina ambos rankings (Reciprocal Rank Fusion) en un único top-k.
El guardarraíl · aquí, no en el promptSi el mejor score semántico está bajo umbral o no hay suficientes fragmentos, no se llama a Claude — se dispara el escalado directamente. Umbral calibrado empíricamente con la vault real en el MVP (empezar conservador, ej. coseno > 0.35).
Solo cuando el guardarraíl da luz verde.
# Prompt de generación system = tono OSG + instrucción de citar + no usar conocimiento externo context = chunks recuperados (con document.title como referencia) history = últimos N turnos de la conversación # si multi-turno query = pregunta del empleado claude.stream(system, context, history, query) # → respuesta en streaming, citas mapeadas a document_id/chunk_id
El frontend pinta las citas mapeando cada referencia a su document_id/chunk_id para enlazar al archivo origen.
asumido Responde en el idioma de la pregunta (es/ca) por instrucción de prompt, sin lógica propia de detección.
Endpoints que expone
| Método | Ruta | Qué hace |
|---|---|---|
| POST | /auth/request-link | envía magic link al email |
| POST | /auth/verify | canjea el token del link por una sesión |
| POST | /chat/messages | envía pregunta, respuesta en streaming (SSE) |
| GET | /chat/conversations | lista conversaciones del usuario |
| GET | /chat/conversations/{id} | historial de una conversación |
| POST | /escalations/{id}/respond | formulario donde el experto responde |
| GET | /escalations | panel de pendientes por área/estado |
| POST | /escalations/{id}/validate | aprueba (e indexa) o rechaza |
| POST | /webhooks/vault | disparado por Gitea, encola reindexado |
| GET | /health | liveness/readiness para Docker Compose |
La superficie que le importa a Atenea/chat/messages es el único endpoint que necesita la integración: recibe la identidad del usuario (pasada por Atenea) y el texto, devuelve streaming con citas. Exactamente la superficie mínima que pide el brief para integrar "por arriba".
Cuando MIA no sabe, aprende
La recuperación decide que no hay contexto suficiente → registro escalations con status pending, ligado al mensaje y al área deducida.
El worker envía email vía Resend al responsable del área con un enlace a /escalations/{id}/respond. El email es solo el aviso, no se parsea la respuesta del correo.
Abre el enlace, ve la pregunta, responde en un formulario en la propia herramienta. Status → answered.
La respuesta vuelve a quien preguntó.
Se decide si entra en la vault. Si sí: se crea/actualiza un .md (commit + push), lo que dispara el reindexado normal. Status → validated. La base aprende, pero curada — nunca se inyecta cruda.
Organización del código
mia/ ├── api/ # FastAPI │ ├── app/ │ │ ├── main.py │ │ ├── core/ # config, seguridad, dependencias │ │ ├── models/ # SQLAlchemy (1:1 con el modelo de datos) │ │ ├── schemas/ # Pydantic (request/response) │ │ ├── routers/ # auth · chat · escalations · webhooks │ │ └── services/ │ │ ├── retrieval.py # búsqueda híbrida + guardarraíl │ │ ├── generation.py # prompt + llamada a Claude │ │ ├── ingestion.py # parseo, troceo, embeddings │ │ └── escalation.py # ciclo de vida del escalado │ ├── alembic/ │ └── pyproject.toml ├── worker/ # Celery │ └── tasks/ # reindex_vault · send_escalation_email ├── frontend/ # React + TS │ └── src/ │ ├── chat/ # lógica reutilizable (streaming, citas) │ └── App.tsx ├── docker-compose.yml ├── .env.example └── docs/ # esta documentación
Vive en el servidor de OSG
MIA se despliega vía Docker Compose en la infraestructura propia de OSG, conviviendo con Atenea en el mismo servidor. No hay hosting externo: el conocimiento interno y los datos se quedan en casa de OSG.
api · worker · postgres (pgvector) · redis · gitea. Orquestados por un único docker-compose.yml.
Reverse proxy compartido enrutando por subdominio o path. MIA y Atenea son servicios independientes en la misma máquina.
Volumen Docker para PostgreSQL + el volumen de Gitea (que guarda el vault) + una copia de trabajo local que el worker mantiene actualizada.
Al vivir en un Gitea propio (sin copia externa como daría GitHub), el volumen de Gitea debe entrar en la política de backups de OSG. Sin backup, un fallo del servidor se lleva el activo más valioso de MIA.
Solo Claude API (generación) y Resend (email). Con embeddings locales, también estos se quedarían dentro.
Bloqueante pendienteConfirmar los recursos del servidor de OSG (CPU, RAM, disco, Docker disponible). Tiene que sostener Atenea y MIA a la vez. Es la dependencia que hay que cerrar con Andreu antes de nada.
Para acceder y desplegar
Checklist de lo que hace falta reunir. La etiqueta indica quién lo aporta o gestiona.
CPU, RAM, disco y confirmación de que Docker/Compose están disponibles. Bloqueante nº 1.
SSH / credenciales de despliegue. Como el backend lo lleva Andreu (igual que Atenea), el acceso a la máquina lo gestiona él.
Un subdominio (ej. mia.grupoosg…) o path bajo el reverse proxy, y su certificado TLS.
Gitea desplegado en el servidor (Andreu) + el repo del vault con su estructura y contenido (SIRNEXA) + webhook de push configurado hacia MIA. El backup del volumen entra en la política de OSG.
Claude API key, Resend API key, y (si aplica) key del proveedor de embeddings. Van en variables de entorno del servidor.
Dominio verificado en Resend para enviar los avisos de escalado desde una dirección de OSG (SPF/DKIM).
Los 50 emails corporativos + las áreas/departamentos reales con su responsable (alimenta auth, gobernanza y routing de escalado).
¿Google Workspace o Microsoft 365 en uso? Determina si auth es magic link u OAuth.
Quién hace qué
Mismo modelo que Atenea: Andreu se encarga de todo el backend e infraestructura. SIRNEXA define el producto, la capa de conocimiento, el diseño del RAG y el frontend.
Andreu — backend + infra
- API FastAPI y todos los endpoints
- Worker Celery + Redis
- PostgreSQL + pgvector, migraciones
- Implementación del pipeline RAG
- Despliegue Docker Compose en servidor OSG
- Reverse proxy y convivencia con Atenea
- Gestión de variables de entorno y secretos
- Administración de Gitea y backups del vault
- Integración futura con Atenea (lado servidor)
SIRNEXA — producto + conocimiento
- Definición de producto y esta documentación
- Estructura y curación de la vault (.md)
- Diseño del RAG: troceo, prompts, guardarraíl
- Frontend del chat (React)
- Diseño de la integración con Atenea (lado UI)
- Calibración del umbral "no lo sé"
- Prompt de sistema: tono y valores de OSG
/chat/messages respete su forma, frontend y backend avanzan en paralelo.Costes mensuales
El coste de MIA lo domina una sola partida — la generación con Claude — y aun así se mide en decenas de dólares al mes, no cientos. Las otras dos partidas (embeddings y email) son calderilla a este volumen. El servidor es de OSG y ya está pagado.
Calculadora
Los tres escenarios de un vistazo
Total mensual ≈ coste de generación (email y embeddings quedan en la franja gratuita a estos volúmenes).
| Volumen | Sonnet 5 intro | Sonnet 5 estándar | Haiku 4.5 |
|---|---|---|---|
| Bajo · ~1.100/mes 1 pregunta/empleado/día | ~$11 | ~$17 | ~$6 |
| Medio · ~3.300/mes 3 preguntas/empleado/día | ~$34 | ~$50 | ~$17 |
| Alto · ~8.800/mes 8 preguntas/empleado/día | ~$90 | ~$135 | ~$45 |
Lectura honestaNi en el peor caso (uso alto + Sonnet a precio estándar) MIA pasa de ~135 $/mes. El coste de la API no es el problema a vigilar: el que importa es si el servidor de OSG aguanta MIA junto a Atenea — eso lo confirma Andreu con las specs. Quién asume el gasto mensual (OSG o SIRNEXA) es una decisión comercial, no técnica. Sonnet 5 está en precio introductorio hasta el 31 de agosto; a partir de septiembre sube ~50%, y ahí Haiku 4.5 es la palanca de ahorro si hiciera falta.
Fases de construcción
Preguntas abiertas
rosa cambia modelo de datos o arquitectura · azul afecta a implementación
Bloqueante nº 1. Define si todo cabe junto a Atenea. → Andreu.
¿La privacidad es requisito duro desde el día 1? La generación pasa por Claude API en cualquier caso — ¿aceptado?
¿OSG usa Google Workspace / Microsoft 365?
¿O hay áreas restringidas por rol (RRHH, dirección)?
¿El chat recuerda el contexto anterior o cada pregunta es aislada?
¿Siempre en el idioma de la pregunta? ¿La vault estará en un solo idioma o mezclada?
¿Intuición de partida o calibración empírica en el MVP? (plan por defecto: lo segundo).
¿El experto valida o un curador central? ¿Clasifica MIA el área o la elige el empleado?
¿Qué se consulta exactamente? ¿Solo lectura? (asumido: sí).
¿Cuánto contenido real hay ya? ¿Lista de áreas/departamentos de OSG?