Documentación técnica · v1

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.

EnfoqueRAG cerrado + honesto
NaturalezaApp independiente
BackendAndreu (como Atenea)
ServidorInfra propia de OSG

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.

Solo conocimiento de OSG

Nunca responde con conocimiento genérico de internet. La base es propiedad de OSG, curada y mantenida centralmente.

Nunca se inventa

Cuando no sabe, lo dice honestamente y activa el circuito de escalado. El guardarraíl es de arquitectura, no una instrucción al modelo.

Cita siempre la fuente

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.

Vault (Gitea · .md)
autoalojado en OSG
Claude API
generación con citas
Docker Compose · servidor OSG
API (FastAPI)
chat · auth · escalado
Worker (Celery)
reindexado · emails
PostgreSQL + pgvector
chunks · embeddings · conversaciones
Redis
broker de Celery
Chat (React)
propio + integrado en Atenea
Experto (email)
Resend · formulario
La API es el único punto de entrada para el frontend y, en el futuro, para Atenea.
El worker ejecuta todo lo que no debe bloquear una petición HTTP: reindexar la vault, enviar emails, generar embeddings en batch.
Una sola base de datos para todo: datos relacionales y búsqueda vectorial, gracias a pgvector.
Desacoplado por debajo, integrado por arriba: la integración con Atenea no es una fase de desarrollo de MIA.

Por qué MIA es una app aparte, no un módulo de Atenea

Principio de diseño
Desacoplado por debajo,
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á:

✗ Como módulo dentro de Atenea
  • · 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.
✓ Como app independiente
  • · 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

El precedente de Sage

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.

El chat es streaming

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

PiezaElecciónNotas
BackendFastAPI · Python 3.12async nativo, encaja con streaming y llamadas a Claude
Base de datosPostgreSQL 16 + pgvectoruna pieza para datos y vectores
Vault (almacén)Gitea autoalojadorepo Git de los .md dentro del servidor de OSG
ORM / migracionesSQLAlchemy 2.x · Alembicasync
GeneraciónClaude API (Sonnet)SDK oficial anthropic
EmbeddingsAPI multilingüe asumidoAPI-first para MVP; migrable a local
Cola / tareasCelery + Redisreindexado, emails
EmailResendavisos de escalado
Frontend chatReact + TypeScript + Viteinterfaz mínima, reutilizable en Atenea
DespliegueDocker Composetodo en el servidor de OSG
AuthMagic link (Resend) asumidosin SSO mencionado; cambiar a OAuth si existe

Dos decisiones de partida a confirmar

asumidoEmbeddings: API-first para el MVP
Para arrancar rápido y calibrar el guardarraíl con datos reales antes de invertir en inferencia local. El pipeline abstrae el proveedor detrás de una interfaz 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.
asumidoAuth: magic link por email
No hay SSO corporativo mencionado en el brief. Para 50 empleados, lo más simple y suficientemente seguro: el empleado mete su email corporativo, recibe un enlace de un solo uso vía Resend, entra. Sin contraseñas que gestionar, reutiliza la infraestructura de Resend ya presente. Si OSG usa Google Workspace o Microsoft 365, cambiamos a OAuth — mejor UX si existe.

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.

tabladocuments

Un registro por archivo .md de la vault.

id · uuid PKidentificador
pathruta del archivo en la vault
area_id · FKdel frontmatter — ata este doc al routing de escalado
titleusado como referencia de cita
git_commit_hashpara detectar qué archivos cambiaron (reindexado incremental)
indexed_attimestamp de última indexación
tablachunks unidad de recuperación

La unidad real de recuperación. Cada documento se trocea en varios chunks.

id · uuid PKidentificador
document_id · FKdocumento de origen
content · textel fragmento de texto
embedding · vector(N)N según modelo. Índice hnsw o ivfflat
content_tsv · tsvectorcolumna GENERATED STORED para búsqueda keyword. Índice GIN
token_count · inttamaño del chunk
metadata · jsonbárea denormalizada (filtrar sin JOIN en hot path) + extras del frontmatter
tablaareas gobernanza + escalado

Donde se cruzan gobernanza de lectura y routing de escalado.

id · uuid PKidentificador
nameRRHH, calidad, técnico, comercial…
responsable_emailquien recibe el aviso cuando MIA no sabe algo de esa área
tablausers
id · uuid PKidentificador
email · nameidentidad del empleado
area_id · FKárea del empleado (para gobernanza de lectura) — no confundir con el área del contenido en documents
roleempleado / experto / curador
tablaconversations · messages · message_citations

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.

tablaescalations ciclo de vida
id · uuid PKidentificador
message_id · FKmensaje que disparó el escalado
area_id · FKárea deducida → a qué responsable avisar
statuspending → answered → validated / rejected
question_text · answer_textpregunta original y respuesta del experto
resulting_document_id · FKdoc creado/actualizado si se indexó
created / answered / validated_attimestamps del ciclo

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

Ingesta
Recuperación
Generación

En el worker, disparada por push a la vault. No se ejecuta en cada pregunta.

1
Webhook de push

Gitea (push) → POST /webhooks/vault → encola tarea Celery reindex_vault.

2
Diff de commits

El worker hace git pull, compara el hash guardado vs el actual y calcula qué archivos cambiaron.

3
Parseo + troceo

Por archivo nuevo/modificado: frontmatter (área, responsable) + troceo por encabezados (máx ~500 tokens, solape ~50 para no cortar ideas).

4
Embeddings

Genera embedding de cada chunk vía EmbeddingProvider.

5
Upsert

Upsert en documents/chunks. Archivos borrados en la vault → borrado en cascada.

Búsqueda híbrida: semántica + palabra clave. Por cada pregunta.

1
Embedding de la pregunta
2
Query semántica

ORDER BY embedding <=> :q LIMIT k — distancia coseno con índice hnsw. Entiende "vacaciones" aunque el doc diga "solicitud de ausencias".

3
Query keyword

content_tsv @@ plainto_tsquery(:q) — acierta con referencias exactas (códigos de tornillo, de modelo).

4
Fusión

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étodoRutaQué hace
POST/auth/request-linkenvía magic link al email
POST/auth/verifycanjea el token del link por una sesión
POST/chat/messagesenvía pregunta, respuesta en streaming (SSE)
GET/chat/conversationslista conversaciones del usuario
GET/chat/conversations/{id}historial de una conversación
POST/escalations/{id}/respondformulario donde el experto responde
GET/escalationspanel de pendientes por área/estado
POST/escalations/{id}/validateaprueba (e indexa) o rechaza
POST/webhooks/vaultdisparado por Gitea, encola reindexado
GET/healthliveness/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

1
Se crea el escalado

La recuperación decide que no hay contexto suficiente → registro escalations con status pending, ligado al mensaje y al área deducida.

2
Aviso por email

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.

3
El experto responde

Abre el enlace, ve la pregunta, responde en un formulario en la propia herramienta. Status → answered.

4
Llega al empleado

La respuesta vuelve a quien preguntó.

5
Validación curada

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.

Contenedores

api · worker · postgres (pgvector) · redis · gitea. Orquestados por un único docker-compose.yml.

Convivencia con Atenea

Reverse proxy compartido enrutando por subdominio o path. MIA y Atenea son servicios independientes en la misma máquina.

Persistencia

Volumen Docker para PostgreSQL + el volumen de Gitea (que guarda el vault) + una copia de trabajo local que el worker mantiene actualizada.

Backup del vault

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.

Salidas a internet

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.

Specs del servidor OSGAndreuOSG

CPU, RAM, disco y confirmación de que Docker/Compose están disponibles. Bloqueante nº 1.

Acceso al servidorAndreu

SSH / credenciales de despliegue. Como el backend lo lleva Andreu (igual que Atenea), el acceso a la máquina lo gestiona él.

Subdominio / rutaAndreuOSG

Un subdominio (ej. mia.grupoosg…) o path bajo el reverse proxy, y su certificado TLS.

Vault en GiteaAndreuSIRNEXA

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.

Claves de APISIRNEXA

Claude API key, Resend API key, y (si aplica) key del proveedor de embeddings. Van en variables de entorno del servidor.

Dominio de email verificadoOSGSIRNEXA

Dominio verificado en Resend para enviar los avisos de escalado desde una dirección de OSG (SPF/DKIM).

Lista de empleados y áreasOSG

Los 50 emails corporativos + las áreas/departamentos reales con su responsable (alimenta auth, gobernanza y routing de escalado).

Decisión SSOOSG

¿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
El contrato entre ambos lados es la API: mientras /chat/messages respete su forma, frontend y backend avanzan en paralelo.
La vault es un activo de SIRNEXA/OSG, no del backend: Andreu la consume, no la define.

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.

Supuesto por pregunta: prompt de sistema ~1.000 tokens (cacheado, −90%), 5 fragmentos de contexto ~2.500 tokens, pregunta + historial ~500 tokens, respuesta ~400 tokens.
Caché de prompt activada — clave en RAG, porque el prompt de sistema se repite en cada pregunta y su coste baja al 10%.
Precios de julio 2026: Sonnet 5 introductorio 2 $/10 $ por millón (hasta el 31 ago), luego 3 $/15 $. Haiku 4.5 en 1 $/5 $. Cifras en USD.

Calculadora

Volumen de uso
3 preguntas por empleado y día · 50 empleados · 22 días laborables
= 3.300 preguntas/mes
Modelo de generación
Sonnet 5 intro
Sonnet 5 estándar
Haiku 4.5
Embeddings
Vía API
Locales
Coste por pregunta$0.0102
Generación (Claude)$34
Embeddings≈ 0 $
Email (Resend)0 $
Servidor OSGincluido
Total mensual $34
USD · ≈ ×0,92 para € (según cambio)

Los tres escenarios de un vistazo

Total mensual ≈ coste de generación (email y embeddings quedan en la franja gratuita a estos volúmenes).

VolumenSonnet 5 introSonnet 5 estándarHaiku 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

0 · prerrequisitoLa vault
No es código. Sin conocimiento bien estructurado no hay nada que recuperar. Ya en marcha en Obsidian con OSG como primer caso real.
1 · MVPEl RAG desnudo
Ingesta → recuperación → generación con citas y el guardarraíl de "no lo sé". Un MIA que responde bien solo con la vault, expuesto por API y con su chat propio. Sin escalado ni HubSpot todavía. Ya demuestra el valor entero.
2 · segunda olaEscalado + HubSpot
Circuito de escalado (Resend + routing por área + reincorporación curada) y conexión con HubSpot para preguntas de cliente.
3 · integraciónDentro de Atenea
Puede ir en paralelo en cuanto la API responda: el chat React contra una API que ya funciona. No bloquea nada del MVP.

Preguntas abiertas

rosa cambia modelo de datos o arquitectura  ·  azul afecta a implementación

Recursos del servidor OSG

Bloqueante nº 1. Define si todo cabe junto a Atenea. → Andreu.

Embeddings: local vs API

¿La privacidad es requisito duro desde el día 1? La generación pasa por Claude API en cualquier caso — ¿aceptado?

Auth: magic link u OAuth

¿OSG usa Google Workspace / Microsoft 365?

Gobernanza: ¿todos ven todo?

¿O hay áreas restringidas por rol (RRHH, dirección)?

Multi-turno o preguntas independientes

¿El chat recuerda el contexto anterior o cada pregunta es aislada?

Idioma de respuesta

¿Siempre en el idioma de la pregunta? ¿La vault estará en un solo idioma o mezclada?

Umbral del guardarraíl

¿Intuición de partida o calibración empírica en el MVP? (plan por defecto: lo segundo).

Escalado: quién valida y cómo se deduce el área

¿El experto valida o un curador central? ¿Clasifica MIA el área o la elige el empleado?

HubSpot: alcance real

¿Qué se consulta exactamente? ¿Solo lectura? (asumido: sí).

Estado y áreas de la vault

¿Cuánto contenido real hay ya? ¿Lista de áreas/departamentos de OSG?