Documentación técnica

Motor de Evaluación Normativa

Motor autónomo RAG que evalúa documentos contra normativas UNE, RGPD, DGA y Data Act. Devuelve informes de auditoría estructurados con score, gaps y acciones correctivas vía API HTTP/SSE.

v0.3.0 FastAPI · PostgreSQL · pgvector Python 3.12

Quickstart

Levanta el motor completo desde cero en cuatro pasos.

Requisitos previos

  • Docker + Docker Compose
  • Python 3.12 (para indexador y parser)
  • API key del proveedor de embeddings (configurable)
  • API key del proveedor LLM elegido
  • Tesseract + poppler (solo si hay que reprocesar PDFs)

1 — Arranque del motor RAG

cd IA-data-x
cp .env.example .env        # rellenar DATABASE_URL y las API keys del proveedor elegido
bash start.sh               # levanta postgres + ia-service con healthchecks
curl http://localhost:8001/health    # o el HOST:PORT configurado

Respuesta esperada:

{
  "status": "ok",
  "db": "connected",
  "chunks_indexados": 0,
  "models": { "embed": "<id>", "llm": "<id>" },
  "version": "0.3.0"
}

2 — Indexar normativas

cd IA-data-x
python3 indexar_criterios.py        # interactivo: confirma y empieza
# o sin confirmación:
python3 indexar_criterios.py --yes

Lee normativas-parser/output/eval_criteria.json y publica chunks vía POST /indexar.

3 — (Opcional) Reprocesar normativas desde PDFs

Solo si cambia el corpus normativo. Pipeline completo en normativas-parser/:

cd normativas-parser
bash setup.sh                              # venv + dependencias
python3 1.extract_pdfs.py
python3 2.clean_extracted.py
python3 3.pdf_parser.py
python3 4.llm_cleaner.py --model  --vision-model 
python3 5.generate_eval_json.py --model 
cd ../IA-data-x && python3 indexar_criterios.py --yes

4 — Primera evaluación

curl -N -X POST $BASE_URL/validar-archivo \
  -F "file=@mi_politica.pdf" \
  -F "embed_api_key=$EMBED_API_KEY" \
  -F "llm_api_key=$LLM_API_KEY" \
  -F "proveedor="
Devuelve stream SSE. El último evento resultado contiene el informe JSON completo.

Visión general

Dos módulos autocontenidos que cooperan vía API HTTP.

Offline · una vez por norma

normativas-parser/

Pipeline de ingestión y enriquecimiento de normativas UNE desde PDFs. Produce eval_criteria.json.

Online · cada evaluación

IA-data-x/

Microservicio RAG (FastAPI :8001). Indexa criterios y evalúa documentos del usuario via PostgreSQL + pgvector.

Cadena de valor

FASE OFFLINE

PDF UNE
TXT crudo
TXT saneado
JSON estructurado
eval_criteria.json
indexar_criterios.py → POST /indexar → pgvector

FASE ONLINE

Fichero usuario
Extracción + dedup
Embedding + norma
Clasificación
RAG bidireccional
LLM × batches
Informe SSE
Ambos módulos son agnósticos al proveedor LLM. Cualquier modelo con interfaz tipo Chat Completions sirve; se selecciona por variable de entorno o parámetro por petición.

Casos de uso

  • Auditor sube un SLA — informe evaluado contra UNE-0087 y DGA, cláusulas citadas textualmente, gaps tipificados y acciones correctivas.
  • Responsable de gobierno del dato sube su Política de Datos — evaluación contra UNE-0077 y UNE-0085, score por dimensión, dictamen global.
  • Equipo legal sube un contrato de tratamiento — detección automática de tipo (encargado RGPD), criterios filtrados al rol del firmante, recomendaciones priorizadas.
  • Consultor sube diagnóstico de madurez — evaluación arquetipo "evidencia", verificación de que cada hallazgo es concreto, referenciado y verificable.
  • Integración programáticaPOST /validar-archivo con fichero → stream SSE + informe JSON consumible desde cualquier plataforma.

Módulo normativas-parser

Pipeline determinístico que convierte PDFs de normas UNE en el catálogo de criterios RAG.

Corpus de PDFs (ejemplo)

El parser acepta cualquier normativa en PDF con estructura de secciones. El siguiente corpus es un ejemplo de uso con normas UNE; cualquier otro conjunto de PDFs sigue el mismo pipeline.

ArchivoNormaAño
Especificacion_UNE_0077.pdfGobierno del Dato2023
Especificacion_UNE_0078.pdfGestión del Dato2023
Especificacion_UNE_0079.pdfGestión de la Calidad del Dato2023
Especificacion_UNE_0080.pdfGuía de evaluación del Gobierno, Gestión y Calidad2023
Especificacion_UNE_0081.pdfGuía de evaluación de la Calidad de un Conjunto de Datos2023
Especificacion_UNE_0085.pdfGuía de implantación del Gobierno del Dato2024
Especificacion_UNE_0087.pdfCompartición y espacios de datos2025

Pipeline de 5 etapas

Cada etapa es reentrante: si se interrumpe, reanuda donde quedó detectando qué documentos ya tienen salida válida.

01
1.extract_pdfs.py Sin LLM

Extracción PDF → TXT

Abre PDFs con pdfplumber. Detecta páginas escaneadas y aplica OCR (Tesseract, español). Extrae tablas como bloques [TABLA:CSV] e imágenes con placeholder [IMG:NNN]. Etiqueta ruido con [RUIDO:...].

output/*.txt

02
2.clean_extracted.py Sin LLM

Saneamiento heurístico

Elimina líneas [RUIDO], normaliza codificación (NFC), elimina duplicados y páginas en blanco. Convierte tablas CSV a Markdown para mejorar procesamiento LLM posterior.

output/cleaned/*.txt

03
3.pdf_parser.py Sin LLM

Segmentación a JSON estructurado

Convierte el TXT saneado en JSON con esquema fijo: id, numero, año, titulo y una lista plana de secciones con codigo, nivel, parent_codigo, contenido, es_anexo.

output/json/*.json

04
4.llm_cleaner.py LLM texto + visión

Limpieza con LLM y visión

Cuatro sub-tareas: A) limpieza de prosa (sin reescribir), B) reconstrucción de tablas Markdown deterioradas, C) clasificación de imágenes (relevante/decorativa), D) extracción de tablas desde imágenes. Flags: --dry-run, --skip-images, --doc <id>.

output/json/cleaned/*.json

05
5.generate_eval_json.py LLM + embeddings

Generación de criterios RAG

Etapa crítica. Pre-computa embeddings de todas las secciones, extrae criterios con razonamiento LLM (detección de tablas TAREA/ENTRADAS/SALIDAS + lenguaje normativo prescriptivo), calcula referencias cruzadas por similitud coseno entre normas (sin LLM), y valida que cada criterio tenga ≥15 palabras y un verbo normativo.

output/eval_criteria.json

Modelos IA

El motor es agnóstico al proveedor: acepta cualquier modelo con interfaz Chat Completions. El modelo se pasa por parámetro en cada llamada — no hay proveedor fijo.

El proveedor de embeddings también es configurable. Se requiere un modelo que genere vectores de dimensión fija (el sistema usa 1024 por defecto, ajustable).

Criterios de selección

AspectoQué buscar
ContextoCuanto mayor, mejor: documentos normativos pueden ser extensos. Modelos con 100K+ tokens evitan truncaciones.
Visión (script 4)Necesaria si el PDF contiene tablas como imagen. Cualquier modelo multimodal es compatible.
Calidad de seguimientoEl prompt exige JSON estructurado estricto. Modelos con bajo seguimiento de instrucciones generan JSON roto.
CosteLa evaluación puede suponer cientos de miles de tokens por documento. Considerar precio input/output según volumen.

Recetas rápidas

# Pasar el modelo deseado como argumento
python3 4.llm_cleaner.py --model  --vision-model 
python3 5.generate_eval_json.py --model 

Formato eval_criteria.json

{
  "version": "2.0",
  "total_criterios": 42,
  "criterios": [
    {
      "id":                    "77.1",
      "une_fuente":            "une-0077-2023-gobierno-del-dato",
      "dimension_codigo":      "5.1",
      "dimension_nombre":      "Política de gobierno del dato",
      "criterio":              "Aprobar y publicar una política formal de gobierno del dato",
      "tipo_documento_afectado": "Política de Gobierno del Dato",
      "descripcion":           "La norma establece que la organización debe disponer…",
      "evidencias_requeridas": "Política aprobada, acta de aprobación por dirección",
      "resultado_esperado":    "Documento de política publicado y accesible",
      "posible_evidencia":     "Política firmada, publicación en intranet, acta del comité",
      "medidas_correctivas":   "Redactar y aprobar la política con patrocinio directivo",
      "origen":                "LENGUAJE_NORMATIVO",
      "referencias_normativas": [...],
      "referencias": [...]
    }
  ]
}
Campos clave para RAG: descripcion, evidencias_requeridas y medidas_correctivas alimentan el prompt LLM. tipo_documento_afectado es la clave de filtrado contextual.

Puente parser → RAG: indexar_criterios.py

Único vínculo entre la fase offline y la online. Produce chunks RAG y los envía a POST /indexar.

Por cada criterio se construyen tres tipos de chunks:

01

Intro de la norma

Una sola vez por fuente. Incluye motivación, objeto, alcance y concepto base. Etiquetado con normative_force: "CONTEXT".

02

Chunk principal del criterio

Contiene norma, dimensión, criterio, evidencias, medidas correctivas. Se enriquece con keywords semánticos (LLM ligero) para potenciar BM25. La fuerza normativa se infiere por regex: MUST vs. SHOULD.

03

Chunks de referencia normativa

Uno por cada referencia con extracto no vacío. Permiten que el LLM cite el texto original de la norma ("Contextual Retrieval").

El script aplica un rate limit de 1.1 s entre peticiones y reintentos con backoff exponencial (MAX_REINTENTOS=3, BACKOFF_BASE=5.0). Tras el primer pase, hace una pasada de recuperación para chunks fallidos.

POST /indexar es idempotente: cada chunk se identifica por SHA-256 del contenido. Re-indexar produce action: "updated" sin duplicar.

Módulo IA-data-x

Stack y arquitectura

ComponenteTecnología
RuntimePython 3.12
Framework HTTPFastAPI 0.115.x + Uvicorn
Base de datosPostgreSQL 16 + extensión pgvector
Pool de conexionesasyncpg (min 2, max 10)
EmbeddingsVectores de 1024 dimensiones (modelo configurable)
Índice vectorialHNSW coseno (m=16, ef_construction=64)
Índice léxicoGIN sobre tsvector español (BM25)
Modelos LLMConfigurables por variable de entorno o por petición
ContenedoresDocker Compose (postgres + ia-service)
StreamingServer-Sent Events (SSE)

El servicio escucha en el puerto 8001. La base de datos en el puerto 5432 del contenedor (mapeado a 5434 en algunos entornos host).

Modelo de datos

Base de datos: ia_normativas.

Tabla normativas — criterios indexados:

CampoTipoDescripción
idSERIAL PK
contenidoTEXTTexto completo del chunk
fuenteVARCHAR(200)Norma (ej. UNE-0085-2024)
seccionVARCHAR(100)Código de sección o id de criterio
dimensionTEXTDimensión normativa
hashVARCHAR(64) UNIQUESHA-256 — clave de deduplicación
embeddingVECTOR(1024)Embedding semántico
tsvTSVECTOR GENERATEDFull-text search español
tipo_documento_afectadoTEXTTipo de documento que evalúa este criterio
keywordsTEXTKeywords semánticos para BM25 mejorado
normative_forceVARCHAR(10)MUST / SHOULD / MAY / CONTEXT

Tabla validaciones — log de auditoría:

CampoTipo
documento_hashVARCHAR(64) — SHA-256 del documento evaluado
fuente_normaVARCHAR(200)
resultadoJSONB — informe completo
chunks_recuperadosJSONB — chunks RAG + scores (trazabilidad)
tokens_usadosINT
ts_validacionTIMESTAMP

Mapa de módulos core/

config.pyConstantes de entorno: DATABASE_URL, EMBED_MODEL, LLM_MODEL, DOC_RAG_THRESHOLD, MAX_UPLOAD_CHARS, MODO_VALIDACION, RAG_MODE, RAG_K_POR_FUENTE…
database.pyPool asyncpg: init_pool, close_pool, check_pool
embeddings.pyget_embedding, get_embeddings_batch con caché LRU MD5, coseno, coseno_matrix (numpy vectorizado)
llm_client.pyllamar_llm (interfaz única), _limpiar_json (reparador robusto de JSON truncado), _normalizar_resultado
classification.py_detectar_tipo_documento (LLM, 21 tipos), _clasificar_documento (arquetipo por overlap keywords), _clasificar_modalidad
filtering.pyFiltros de criterios por norma, tipo, arquetipo, modalidad — todos con fallback a conjunto completo
rag.pychunk_documento, pasajes_por_criterio, construir_texto_para_embedding (Contextual Retrieval)
pipeline.pyOrquestador: _pipeline_single, _pipeline_stream, run_validar_sync, _buscar_chunks_criterio, _sse
prompt_builders.pyConstrucción de prompts del LLM y cálculo determinístico de campos derivados (score, dictamen)
prompts.py8 arquetipos × 3-5 modalidades con keywords, excluir_dims y contexto_prompt por modalidad
dynamic_config.pyHot-reload de prompt_config.json sin reiniciar el servicio
logging_config.pylog_embedding, log_search, log_prompt, log_llm_response, log_rag_result (trazabilidad completa)

Variables de configuración clave

VariableDefaultSignificado
DATABASE_URLobligatoriaConexión PostgreSQL
EMBED_MODELconfigurableModelo de embeddings (debe generar vectores de 1024 dims por defecto)
DOC_RAG_THRESHOLD15000Chars mínimos para activar RAG bidireccional
MAX_UPLOAD_CHARS200000Límite de texto procesado (truncación inteligente)
MODO_VALIDACIONragrag (pgvector) | checklist (XLSX)
RAG_MODEper_normafull (todos los criterios) | per_norma (top-K)
RAG_K_POR_FUENTE50Top-K cuando RAG_MODE=per_norma

Sistema de arquetipos

El motor clasifica cada documento en un arquetipo. El arquetipo determina la regla fundamental que el LLM aplica para dictaminar cumplimiento — no es lo mismo evaluar un contrato que una política o una evidencia técnica.

ArquetipoModalidadesRegla fundamental
contractual participante, operador, proveedor_servicio, sla, nda CUMPLE si establece obligación jurídica (no se exige operatividad)
tecnico log_accesos, log_auditoria, config_infraestructura, captura_monitor CUMPLE si evidencia técnica demuestra control activo con resultados reales
politica politica_datos, procedimiento, manual MENCIÓN ≠ ACREDITACIÓN: solo CUMPLE si implantado operativamente
estrategico plan_estrategico, acta_comite, programa_gobierno CUMPLE si establece qué + quién + cómo (requiere accionabilidad)
evidencia informe_auditoria, certificacion, diagnostico_madurez CUMPLE si evidencia concreta, referenciada y verificable
catalogo catalogo_activos, diccionario_datos, inventario_sistemas CUMPLE si elemento documentado con detalle operativamente útil
arquitectura arquitectura_tecnica, diseno_api, flujo_datos CUMPLE si especifica componente con suficiente detalle implementable
requisitos dmp, especificacion, historia_usuario CUMPLE si existe requisito + criterio de aceptación verificable
La clasificación ocurre en tres niveles en cascada: tipo de documento (LLM, catálogo ampliable) → arquetipo (heurístico por keywords) → modalidad (filtrado fino). Cada nivel tiene fallback automático al conjunto anterior si el resultado queda vacío.

Flujo completo de una evaluación

Lo que ocurre cuando un usuario sube un fichero a POST /validar-archivo.

5.1 — Carga y normalización del documento

Petición entrante: multipart/form-data.

CampoObligatorioDescripción
filePDF, DOCX o TXT — máx. 50 MB
fuente_normaNoNorma concreta (si se omite, se auto-detecta)
fuentes_normaNoArray JSON o CSV de fuentes — evalúa contra varias normas
top_kNoTop-K de pasajes RAG por criterio (default 10, clamp 1–50)
contextoNoTexto adicional del analista, se antepone al documento
proveedorNoIdentificador del modelo LLM a usar
embed_api_keyAPI key del proveedor de embeddings configurado
llm_api_keyAPI key del proveedor LLM elegido
modo_validacionNorag | checklist
user_idNoID de usuario para logging por carpeta

Pasos de preparación en main.py: validar_archivo:

  1. Control de tamaño → HTTP 413 si > 50 MB
  2. Extracción de texto (PDF: pypdf; DOCX: python-docx; TXT: UTF-8 con fallback latin-1)
  3. Deduplicación de párrafos (_deduplicar_parrafos)
  4. Detección de subdocumentos con inyección de marcadores ═══ SUBDOCUMENTO ═══
  5. Concatenación del contexto del analista
  6. Truncación inteligente si > 200 000 chars: 50% inicio + 30% centro + 20% final
  7. Validación mínima: si len < 50 → HTTP 422
  8. Logging completo del texto que entrará al pipeline

5.2 — Embedding y detección de norma

Se computa el embedding del documento con caché LRU (2048 entradas, clave MD5). Con ese vector se ejecuta la detección de norma aplicable si el usuario no especificó fuente_norma:

ORDER BY
  0.7 * (1 - (embedding <=> $1::vector)) +
  0.3 * ts_rank(tsv, plainto_tsquery('spanish', $2))
DESC LIMIT 40

Los scores se agregan por fuente y se normalizan a porcentaje de relevancia, devolviendo las N normativas más relevantes.

5.3 — Clasificación documental jerárquica

Tres niveles en cascada:

01

Nivel 1

Tipo de documento (LLM)

El LLM recibe un preview de 4-6 K chars y elige entre 21 tipos conocidos: Política de Gobierno del Dato, SLA, NDA, Catálogo de activos, Informe de auditoría, Diagnóstico de madurez, Arquitectura técnica…

02

Nivel 2

Arquetipo (heurístico)

Mapeo por overlap de keywords a uno de 8 arquetipos: contractual, tecnico, politica, estrategico, evidencia, catalogo, arquitectura, requisitos. Cada arquetipo define su regla fundamental de evaluación.

03

Nivel 3

Modalidad

Afina dentro del arquetipo (ej. dentro de contractual: sla, nda, participante). Ajusta excluir_dims y añade contexto específico al system prompt.

5.4 — Selección de criterios

Filtros en cascada, todos con fallback automático al conjunto previo si el resultado queda vacío:

  1. _filtrar_criterios_por_norma — pre-filtra por prefijo de fuente UNE según tipo
  2. _filtrar_criterios_por_tipo — filtra por tipo_documento_afectado
  3. _filtrar_dimensiones_por_arquetipo — elimina criterios en excluir_dims
  4. _filtrar_dimensiones_por_modalidad — filtrado fino adicional

5.5 — RAG bidireccional

Activo si el documento supera DOC_RAG_THRESHOLD (15 000 chars). La clave: el score no es solo query→corpus, sino criterio↔chunks del documento:

  1. Chunking del documento: 1 500 chars, overlap 200, corte en saltos de línea
  2. Embeddings en batch (caché LRU compartida)
  3. Ajuste dinámico de top-K: >40 criterios → K=1; >20 → K=2
  4. BM25-lite en memoria (k1=1.5, b=0.75) con keywords del criterio
  5. Score híbrido: coseno ponderado + BM25-lite
  6. Top-K pasajes por criterio, recortados a 1 200 chars
  7. Fallback robusto: si fallan embeddings → primeros 15 chunks

5.6 — Evaluación LLM por batches

Batches de EVAL_BATCH_SIZE = 25 criterios por llamada LLM. Por cada batch:

  1. Construcción del prompt con regla del arquetipo + contexto de modalidad + criterios + pasajes RAG
  2. Llamada al LLM: max_tokens 32 K, timeout 180 s, temperatura 0.1–0.6 según modelo
  3. Reparación del JSON: extrae bloques markdown, corrige comas finales, cierra strings truncados, recupera objeto a objeto como último recurso
  4. Normalización de resultado a valores canónicos: Cumple / Parcial / No cumple / Ausencia
  5. Segunda pasada de completion si quedan criterios sin respuesta válida
  6. Deduplicación por criterio_codigo

Esquema JSON de respuesta por criterio:

{
  "criterio_codigo": "77.1",
  "criterio_nombre": "...",
  "dimension": "...",
  "fuente": "UNE-0077:2023",
  "resultado": "Cumple" | "Parcial" | "No cumple" | "Ausencia",
  "justificacion": "...",
  "razonamiento": "...",
  "evidencias": [{ "cita": "...", "ubicacion": "...", "comentario": "..." }],
  "gap_type": "PARTIAL_COVERAGE" | "MISSING" | "MISALIGNED" | null,
  "gap_detectado": "...",
  "accion_requerida": "...",
  "confianza": "alta" | "media" | "baja",
  "lecturas_alternativas_descartadas": [...]
}

5.7 — Cálculo determinístico de campos derivados

Sin LLM. Pura aritmética sobre los resultados agregados:

Cumple+1.0
Parcial+0.5
No cumple0.0
Ausencia−0.2

Score ponderado = (suma de scores / nº criterios) × 100. Dictamen global:

DictamenCondición
ConformeScore ≥ 80 y cero dimensiones "No superada"
Parcialmente conformeScore ≥ 50
No conformeScore < 50

5.8 — Persistencia, trazabilidad y SSE

Eventos SSE durante una evaluación típica:

event: paso         data: {"paso":"embed","msg":"Generando embedding..."}
event: norma_inicio data: {"fuente":"UNE-0085-2024","idx":1,"total":1}
event: paso         data: {"paso":"clasificar","msg":"Clasificando documento..."}
event: paso         data: {"paso":"filtrar","msg":"Filtrando criterios..."}
event: paso         data: {"paso":"rag","msg":"Recuperando pasajes RAG..."}
event: paso         data: {"paso":"llm","msg":"Evaluando criterios 1–25 de 87..."}
event: paso         data: {"paso":"llm","msg":"Evaluando criterios 26–50 de 87..."}
...
event: resultado    data: { ...informe JSON completo... }
event: todo_fin     data: {"total":1}

Trazabilidad en tres niveles:

  1. BD (validaciones.chunks_recuperados): IDs y scores de chunks RAG usados
  2. Log de servicio (ia-service.log): líneas [TEXTOCLIENTE], [EMBED], [SEARCH], [PROMPT], [LLM-RESPONSE]
  3. Log por evaluación (/app/logs/ia/evaluations/<user_id>/): JSON íntegro del informe

API Reference

Todos los endpoints del servicio. El host y puerto dependen de tu configuración de despliegue (por defecto localhost:8001 en desarrollo local).

MétodoRutaFunción
GET/healthEstado del servicio, conectividad DB, conteo de chunks, modelos en uso
GET/proveedoresLista de modelos LLM disponibles para la UI
GET/normasNormativas indexadas, totales por fuente y dimensiones
GET/detectar-norma?texto=…Ranking de normas más relevantes para un fragmento
GET/explorarExplorador paginado de criterios (filtros por fuente, dimensión, keyword)
POST/indexarUpsert de un chunk por hash SHA-256
POST/buscarBúsqueda híbrida 70% coseno + 30% BM25 con filtros
POST/consultarQ&A conversacional sobre normativas (RAG sobre chunks)
POST/validarPipeline RAG completo síncrono
POST/validar-streamPipeline con streaming SSE (texto plano en body)
POST/extraer-textoExtrae texto plano de PDF/DOCX/TXT sin evaluar
POST/validar-archivoEndpoint principal. multipart/form-data con fichero + SSE
POST/recomendarGenera recomendaciones de mejora a partir de criterios fallados

Ejemplos de uso (curl)

En los ejemplos siguientes, sustituye $BASE_URL por la dirección de tu instancia.

Health check

curl -s $BASE_URL/health | jq

Listar normativas indexadas

curl -s $BASE_URL/normas | jq

Buscar criterios por keyword

curl -s -X POST "$BASE_URL/buscar?query=catalogación+activos&top_k=5" | jq

Detectar norma aplicable

curl -s "$BASE_URL/detectar-norma?texto=la%20organización%20designará%20un%20Data%20Owner" | jq

Validación síncrona (texto plano)

curl -X POST $BASE_URL/validar \
  -H 'Content-Type: application/json' \
  -d '{
    "texto": "La organización designa…",
    "fuente_norma": "nombre-de-la-norma",
    "proveedor": "tu-modelo",
    "embed_api_key": "sk-…",
    "llm_api_key": "sk-…"
  }' | jq

Validación de fichero con streaming SSE

curl -N -X POST $BASE_URL/validar-archivo \
  -F "file=@politica_datos.pdf" \
  -F 'fuentes_norma=["norma-1","norma-2"]' \
  -F "proveedor=tu-modelo" \
  -F "top_k=10" \
  -F "contexto=Empresa del sector financiero, 2000 empleados" \
  -F "embed_api_key=$EMBED_API_KEY" \
  -F "llm_api_key=$LLM_API_KEY" \
  -F "user_id=auditor-42"

Generar recomendaciones

curl -X POST $BASE_URL/recomendar \
  -H 'Content-Type: application/json' \
  -d '{
    "eval_name": "Evaluación Q1",
    "criterios": [
      {"codigo":"Gob.1","descripcion":"OGD no constituida","dimension":"Gobernanza"}
    ],
    "top_k": 5
  }' | jq

Códigos de error HTTP

EndpointCódigoCuándo
/validar-archivo413Fichero > 50 MB
/validar-archivo422No se pudo extraer texto / texto < 50 chars
/validar-archivo401API key de embeddings o LLM ausente o inválida
/indexar400contenido vacío
Cualquier endpoint LLM502Timeout o respuesta no parseable tras reintentos
Cualquier endpoint BD503Pool de conexiones caído (auto-reintenta)
Global500Error inesperado — revisar ia-service.log
Reintentos automáticos internos para 429 (rate limit) y 5xx transitorios del proveedor LLM/embeddings.

Glosario

Norma / fuente
Especificación normativa indexada (ej. UNE-0085-2024)
Criterio
Obligación evaluable extraída de la norma — unidad atómica de evaluación
Dimensión
Agrupación temática de criterios dentro de una norma (Política, Roles, Calidad…)
Arquetipo
Categoría documental amplia (8 valores: contractual, técnico, política, estratégico, evidencia, catálogo, arquitectura, requisitos) — determina la regla fundamental de evaluación
Modalidad
Sub-categoría dentro de un arquetipo (ej. dentro de contractual: SLA, NDA, encargo)
MUST / SHOULD / MAY / CONTEXT
Fuerza normativa del criterio (ISO/IEC Directives Part 2). MUST = obligatorio, SHOULD = recomendable, MAY = opcional, CONTEXT = pieza de contexto sin evaluación
Resultado
Veredicto por criterio: Cumple / Parcial / No cumple / Ausencia
gap_type
Tipificación del fallo: PARTIAL_COVERAGE (cobertura incompleta) / MISSING (ausencia total) / MISALIGNED (presente pero erróneo)
Score ponderado
Métrica 0-100. Cumple=+1.0, Parcial=+0.5, No cumple=0, Ausencia=−0.2
RAG bidireccional
Estrategia de recuperación: chunks del documento × embedding del criterio (no solo query → corpus)
Contextual Retrieval
Anteposición de metadatos (norma, dimensión, código) al contenido antes de embebir, para mejorar recuperación
Chunk
Fragmento textual indexado o segmento del documento (típicamente 1 500 chars con overlap 200)
SSE
Server-Sent Events — transporte HTTP unidireccional para streaming de progreso en tiempo real

Rendimiento y costes

Latencias típicas

OperaciónTiempo orientativo
/indexar un chunk200–400 ms
Indexar eval_criteria.json completo (~300 chunks)~6–8 min (rate-limit 1.1 s)
/health< 50 ms
/buscar200–500 ms
Embedding de documento 50K chars1–2 s
Detección de norma300–700 ms
RAG bidireccional (M chunks × N criterios)2–5 s
LLM por batch de 25 criterios15–40 s (según modelo)
Evaluación completa (1 norma, corpus estándar)60–180 s
Evaluación contra 3 normas3–7 min

Caché

LRU de embeddings en proceso (max=2048). Hit-ratio típico tras precalentar: 60–80 % en cargas similares. cache_stats() expuesta en /health.

Costes orientativos

  • Embedding de 1 documento de 50K chars → ~40K tokens (despreciable)
  • Evaluación completa con LLM medio → ~120K–200K tokens input + ~30K output (varía con el número de criterios)
  • Indexar el corpus completo de normativas → ~600K tokens (parser etapa 5, una sola vez)

Tuning

VariableEfecto
RAG_MODE=per_norma + RAG_K_POR_FUENTE=25Prompts más cortos, evaluación más rápida y barata, menos cobertura
EVAL_BATCH_SIZE (en pipeline.py)Más grande = menos llamadas LLM pero más riesgo de truncación
DOC_RAG_THRESHOLD bajoActiva RAG incluso para docs cortos (más preciso pero más latencia)
Pool asyncpg min/maxAmpliar para concurrencia alta

Seguridad y privacidad

AspectoImplementación
API keys del usuarioRecibidas por petición, nunca persistidas en BD. El servicio no almacena claves entre peticiones.
Documento subidoHasheado con SHA-256. El texto completo no se almacena en BD — solo el informe como JSONB.
Logs a discoCarpeta por user_id en /app/logs/ia/evaluations/. Contiene el informe pero no la API key.
Logs de servicio[TEXTOCLIENTE] registra el texto enviado al LLM. En producción: LOG_LEVEL=warning.
Validación de inputsTamaño 50 MB (anti-DoS), tipos MIME validados, JSON parseado defensivamente.
Inyección SQLTodas las consultas usan parámetros asyncpg — sin concatenación.
CORSConfigurable vía CORS_ORIGINS. Default * — cambiar en producción.
TLSEl servicio escucha HTTP plano. TLS debe terminarse en reverse proxy (Caddy, nginx).
Datos personales: si el documento contiene PII, esta viaja al proveedor LLM elegido por el cliente. El motor no aplica anonimización — responsabilidad del usuario y del proveedor LLM.

Limitaciones conocidas

  • Tamaño máximo de fichero: 50 MB (HTTP 413)
  • Texto procesado por evaluación: 200 000 chars (configurable). Por encima → truncación inteligente 50/30/20 %
  • Ventana de contexto LLM: depende del modelo elegido. El motor parte criterios en batches de 25 pero el documento + prompt deben caber
  • Idioma: optimizado para español (tsvector('spanish'), prompts en castellano). Documentos en otros idiomas funcionan pero la calidad de BM25 baja
  • PDFs escaneados: /validar-archivo usa pypdf que requiere texto extraíble. Un PDF escaneado producirá texto vacío y HTTP 422 (el OCR solo está en el pipeline parser)
  • Dependencia de APIs externas: requiere conectividad al servicio de embeddings y al LLM. Sin red → sin evaluaciones
  • Determinismo: la evaluación no es 100% reproducible entre ejecuciones (LLM con temperature > 0). Para auditoría → guardar chunks_recuperados + documento_hash
  • Evaluación incremental: cada /validar-archivo es independiente. Se puede simular pasando ambos ficheros concatenados o usando contexto para anexos cortos

Troubleshooting

SíntomaCausa probableSolución
chunks_indexados: 0BD vacíapython3 indexar_criterios.py (tras generar eval_criteria.json)
KeyError: DATABASE_URL.env no cargadoVerificar .env en raíz del proyecto
401 UnauthorizedAPI key inválidaRegenerar la clave del proveedor correspondiente
Connection refused 5434PostgreSQL no arrancadobash start.sh
BD muestra criterios pobreseval_criteria.json no generadoEjecutar scripts 1-5 en normativas-parser/
Respuesta LLM cortadaTimeout o cuota agotadaCambiar proveedor en selector de modelo
LLM devuelve JSON rotoModelo débil o prompt demasiado largoCambiar a un modelo con mejor seguimiento de instrucciones; reducir EVAL_BATCH_SIZE
RAG no recupera pasajes relevantesCriterios indexados sin contexto ricoRe-indexar con indexar_criterios.py (no indexar.py legacy)
Evaluación muy lentaModelo LLM lento o batches grandesReducir EVAL_BATCH_SIZE o cambiar a un modelo más rápido
429 en embeddings al indexarRate limit del proveedorNormal — el script gestiona automáticamente con backoff. Esperar o pasar a un tier superior.
Pocos criterios extraídos (script 5)Tablas TAREA/ENTRADAS/SALIDAS mal formateadasReprocesar con un modelo de visión de mayor calidad: python3 4.llm_cleaner.py --vision-model <modelo> --doc nombre-*
Score inesperadamente bajoDocumento fuera de alcance normativoVerificar arquetipo detectado en logs; ajustar fuente_norma

Diagnóstico rápido

# Health completo  (reemplaza HOST:PORT por tu endpoint)
curl -s http://HOST:PORT/health | python3 -m json.tool

# Logs en tiempo real
docker logs -f ia-service

# Chunks por fuente
curl -s http://HOST:PORT/normas | python3 -c "
import json,sys; d=json.load(sys.stdin)
[print(f'{x[\"fuente\"]}: {x[\"total_criterios\"]} criterios') for x in d]
"

# Re-indexar desde cero
cd IA-data-x && ./start-all.sh --reset && python3 indexar_criterios.py --yes
Vector DB desincronizada tras cambios en criterios: ./start-all.sh --reset borra la BD y la recrea. Luego re-indexar con python3 indexar_criterios.py --yes.

FAQ