Skip to main content

Ingesta de manuales

Cómo se cargan los manuales en la base vectorial knowledge_base.

El script pnpm ingest:manuals

scripts/ingest-manuals.ts es offline, idempotente y se corre a mano.

  1. Recorre recursivamente Aura-reloaded/documentation/manuals/ (resuelto como ../documentation/manuals desde scripts/).
  2. Parsea cada .md en chunks (ver convención abajo).
  3. Embeddea título + sección + contenido con OpenAI.
  4. Upsert en knowledge_base con ON CONFLICT (id) DO UPDATE.
cd Aura-reloaded
pnpm ingest:manuals # requiere DATABASE_URL y OPENAI_API_KEY

Es seguro re-correrlo: al ser idempotente por hash, actualiza solo los chunks que cambiaron. Los chunks huérfanos no se borran solos (si renombrás/borrás una sección, el id cambia y la fila vieja queda). Para una limpieza total: TRUNCATE knowledge_base; + re-ingestar.

Convención de formato Markdown (CRÍTICO)

El parser es estricto. Si el .md no la respeta, se pierden chunks en silencio:

  • # (H1) → título del manual. Debe haber uno solo por archivo. Varios pisan el título y rompen la última sección.
  • ## (H2) → una sección = un chunk.
  • Todo lo que está fuera de un ## se descarta (incluido lo que va entre el # y el primer ## ). No pongas info importante antes del primer ## .
  • ### (H3) NO es sección → su contenido se absorbe en el chunk anterior. Usar ## .
  • Secciones con tabla markdownun chunk por fila (Columna: Valor). Ideal para tablas de errores de equipos.
  • Evitar --- (regla horizontal): dentro de una sección de solo-tabla genera un chunk basura "---". Las secciones ya se delimitan por ## .

Ejemplo bien formado: un # de título + varios ## (specs, tabla de errores, notas).

Recuperación

knowledge_base se consulta top-1 por coseno y sin filtro de category. Como devuelve un solo chunk, la granularidad importa: contenido interno/administrativo que no responde consultas de pacientes conviene no ingestarlo (compite por el único chunk). Ver Pipeline.

Footguns

  1. Modelo de embeddings distinto entre ingesta y runtime. El script lee la env var OPENAI_EMBEDDING_MODEL; el runtime lee EMBEDDING_MODEL. Ambos defaultean a text-embedding-3-small, así que sin setear nada coinciden. Pero si cambiás el modelo en uno y no en el otro, los embeddings quedan en espacios distintos y el coseno da basura. Si tocás el modelo, seteá ambas vars igual y re-ingestá todo.
  2. Colisión de filas de tabla. El id de cada fila usa el contenido completo de la fila (rowContent::idx). Si en el pasado usaba solo la primera columna, filas que compartían ese valor (ej. "Luz roja fija" para varios errores) colisionaban y se pisaban. Si ves conteos por source más bajos de lo esperado, sospechá de esto: SELECT source, count(*) FROM knowledge_base GROUP BY source.
  3. Contenido fuera de ## se descarta. Un solo # por archivo.
  4. Re-ingestar tras editar un .md.

Receta: agregar/actualizar un manual

  1. Crear/editar el .md en documentation/manuals/<categoria>/<archivo>.md respetando la convención (un # de título, ## por chunk, tablas para datos tabulares).
  2. cd Aura-reloaded && pnpm ingest:manuals.
  3. Verificar: probar una consulta que debería matchear y mirar el log [Knowledge] HIT/MISS con el score, o el panel /operations (cobertura/hit del RAG).
  4. Si no matchea: revisar el chunking (¿quedó bajo un ## ?), el score vs KNOWLEDGE_THRESHOLD, y que el modelo de embeddings sea el mismo en ingesta y runtime.

Check de sanidad post-ingesta

-- Conteo por archivo (esperá ver todas las categorías)
SELECT source, count(*) FROM knowledge_base GROUP BY source ORDER BY source;

-- Basura que NO debería existir → esperado: 0 filas
SELECT id, source, section, left(content, 50)
FROM knowledge_base
WHERE content LIKE '|%' -- tablas crudas mal parseadas
OR btrim(content) = '---' -- separadores
OR length(btrim(content)) < 5; -- chunks vacíos/triviales