Sistema RAG (Retrieval-Augmented Generation) especializado en consultas sobre normativa jurídica española, con trazabilidad completa y citación de fuentes oficiales.
Important
El despliegue operativo actual se realiza con
docker-compose.production.yml. La guía completa y la asignación de puertos
están en DEPLOYMENT.md.
make up
make healthPara ver todos los comandos disponibles: make help
| Servicio | Puerto del host |
|---|---|
| Frontend | 10100 |
| API (solo localhost) | 10101 |
| PostgreSQL compartido | 10110 |
| Qdrant HTTP / gRPC | 10111 / 10112 |
La aplicación reutiliza PostgreSQL y Qdrant mediante la red externa
docker_iajuridica_network; este Compose no crea ni sustituye sus datos. El
proveedor LLM se configura mediante .env y la aplicación no levanta Ollama.
# 1. Arrancar infraestructura (PostgreSQL + Qdrant)
make dev-infra
# 2. Arrancar aplicación con hot-reload
make devAcceso:
- Frontend: http://localhost:10120
- API: http://localhost:10121
- Desde tu red local: http://TU_IP:10120
-
Arranque:
docker compose up -d ollama. -
Descargar modelo (una sola vez):
docker compose run --rm ollama ollama pull gpt-oss-20b -
Verificar salud:
curl -f http://localhost:11434/api/tags
Notas:
- En Docker Compose el backend usa
GPT_OSS_BASE_URL=http://ollama:11434(ya definido endocker-compose.yml). - El puerto 11434 queda publicado en el host para uso local (
http://localhost:11434).
- Responde consultas sobre normativa española con contexto legal preciso
- Citación automática de fuentes (BOE, DOGC, etc.)
- Verificación de vigencia y estado de las normas
- Qdrant con embeddings bge-m3-es-legal-tmp-6 (fine-tuned para derecho español)
- Filtros críticos:
estado_vigencia,ambito_codigo,diario_sigla,fecha_vigencia - Agregación de chunks por documento con scoring configurable (max/avg/sum/count_weighted)
- PostgreSQL batch fetch optimizado (
WHERE id_documento = ANY($1))
- Respuestas contextualizadas usando modelos LLM de cualquier proveedor; se da preferencia a modelos alojados localmente cuando sea posible (también se admiten servicios gestionados en la nube).
- Prompts versionados y optimizados
- Safety filters y validación de respuestas
- Logging estructurado con structlog
- Métricas de latencia (p50, p90, p99) por etapa
- Trazabilidad E2E con request_id
- SLOs y circuit breaker
- Manejo de errores con reintentos
- Caché LRU de documentos
- Pool de conexiones async (PostgreSQL, Qdrant)
- Rate limiting y validación de entrada
┌─────────────┐
│ Usuario │
└──────┬──────┘
│
v
┌─────────────────────────────────────────────────────┐
│ API (FastAPI) │
│ • /api/answer │
│ • /api/docs/{id} │
│ • /health, /metrics │
└─────────────────┬───────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────┐
│ RAG Orchestrator │
│ Coordina el flujo completo de retrieval → LLM │
└─────────────────┬───────────────────────────────────┘
│
┌──────────┴──────────┐
│ │
v v
┌──────────────┐ ┌──────────────┐
│ Retrieval │ │ Context │
│ • Qdrant │ │ Builder │
│ • PostgreSQL│ │ • Whole-doc │
│ • Aggregator│ │ • Budget │
└──────────────┘ └──────────────┘
│ │
└──────────┬──────────┘
│
v
┌────────────────┐
│ Prompt Reg. │
│ • Templates │
│ • Versiones │
└────────┬───────┘
│
v
┌────────────────┐
│ LLM │
│ • Retry │
│ • Timeout │
└────────┬───────┘
│
v
┌────────────────┐
│ LLM │
│ • Streaming │
│ • Markdown │
└────────┬───────┘
│
v
┌────────────────┐
│ Streaming API │
│ • NDJSON │
│ • Eventos │
└────────────────┘
-
Usuario pregunta: "What does the law say about vacations?" (en cualquier idioma)
-
Intervención de Query (LLM con respuesta JSON):
- LLM analiza la query y devuelve JSON estructurado:
{"idiomaQuery": "Inglés", "canonical_es": "¿Cuál es la normativa vigente sobre el período vacacional?"}
- LLM analiza la query y devuelve JSON estructurado:
-
Embeddings: Convierte
canonical_esa vector conbge-m3-es-legal-tmp-6(1024-dim) -
Búsqueda vectorial masiva (Qdrant): Recupera
RETRIEVAL_K_CHUNKS_PRIMARYchunks (default: 200) conscore_threshold(0.70) -
Agregación por documento: Agrupa chunks por
id_documentoy calculascore_doc(max/avg/sum/count_weighted) -
Limitación a candidatos: Selecciona top
RETRIEVAL_K_DOCS_CANDIDATES_MAXdocumentos únicos (default: 50) ordenados porscore_doc -
Recuperación batch (PostgreSQL): Fetch de textos completos + metadatos +
url_html_consolidadasolo para los candidatos. Si un documento excede 500.000 caracteres, se reemplaza su texto por una instrucción con el enlace oficial para evitar saturar el contexto. -
Selección por presupuesto:
WholeDocsBudgetPolicyincluye documentos completos hastaCONTEXT_MAX_CHARS. Si un documento no cabe, se incluye una versión resumida con enlace y relevancia para no perder contexto. -
Construcción de contexto: Formatea artículos completos con encabezados y metadatos. Se preparan las fuentes deterministas (extraídas de DB) antes de llamar al LLM.
-
Generación Streaming (Markdown-First):
- Template incluye contexto + idioma del usuario.
- LLM genera la respuesta en Markdown directamente, permitiendo razonamiento jurídico fluido y visualización instantánea (streaming).
-
Streaming Response (NDJSON): La API emite eventos en tiempo real:
source: Lista de fuentes oficiales recuperadas (antes de empezar el texto).token: Chunks de texto del mensaje.meta: Latencias, modelo usado y estadísticas finales.
-
Respuesta final: El frontend renderiza el Markdown y muestra las tarjetas de fuentes (
SourceCard) en cascada.
- ✅ Sin alucinaciones en fuentes: Las citaciones no las inventa el LLM; vienen directamente de la base de datos (PostgreSQL).
- ✅ Máxima rapidez (Streaming): El usuario ve la respuesta palabra a palabra sin esperar al final del parseo JSON.
- ✅ Mejor razonamiento: El LLM no gasta tokens ni atención en mantener una estructura JSON compleja, centrándose en el análisis legal.
- ✅ Fallback Web: Integración opcional con MCP (Tavily) si no hay normativa local que responda a la consulta.
Por defecto estado_vigencia="VIGENTE" (Filters.solo_vigentes=True), con filtros adicionales validados en retrieval/filters.py.
Aunque el flujo de respuesta final es Markdown, la normalización de la consulta sigue un patrón JSON-First para garantizar que el motor de búsqueda reciba términos limpios:
class QueryInterventionResult(BaseModel):
idiomaQuery: str # Idioma detectado
canonical_es: str # Query normalizada en español
palabrasClave: list[str] # Refuerzo para embeddingsEl frontend renderiza botones verticales con las fuentes citadas:
┌─────────────────────────────────┐
│ Respuesta del RAG │ [📄 BOE-A-2015-11430] ← Click abre BOE
│ According to Spanish labor... │ [📄 BOE-A-2021-5123] ← Click abre BOE
└─────────────────────────────────┘
Cada botón usa citation.url para window.open(citation.url, '_blank').
- Qdrant (≥1.7): Base de datos vectorial
- Colección:
articulos_juridicos - Embeddings: bge-m3-es-legal-tmp-6 (dim=1024, cosine distance)
- HNSW: ef_construct≈200, m≈48, quantization int8
- Colección:
- PostgreSQL (≥14): Base de datos relacional
- Tabla:
articles(26 columnas + JSONBmetadatos_completos) - Pool async con asyncpg
- Tabla:
- LLM (proveedor o local): Credenciales/configuración según el proveedor (por ejemplo, API key, endpoint o configuración local).
- Preferencia por modelos alojados localmente en el servidor cuando sea posible; también se soportan servicios gestionados en la nube.
- Python 3.11+
- pip para gestión de dependencias
Arquitectura Mock-First para desarrollo offline y CI:
- Desarrollo/CI (mocks):
USE_QDRANT_MOCK=trueUSE_POSTGRES_MOCK=trueUSE_LLM_MOCK=true
- Integración/Producción (reales):
USE_QDRANT_MOCK=false,USE_POSTGRES_MOCK=false,USE_LLM_MOCK=false- Configurar
QDRANT_URL,POSTGRES_*/POSTGRES_DSN,LLM_API_KEY(o las credenciales específicas del proveedor que uses)
- Selección automática vía factories:
retrieval/factory.py,llm/factory.py - Fixtures de prueba para mocks:
tests/fixtures/
Para evitar scripts paralelos y facilitar la selección de entorno, inicia con el lanzador interactivo:
python run.pyOpciones disponibles:
-
- Real definitiva (desactiva mocks y arranca en production)
-
- Mock (activa todos los mocks para trabajo offline/CI)
-
- Personalizada (elige qué mocks activar y credenciales/URLs)
El menú aplica las flags USE_* en el proceso sin modificar tu .env.
Puedes usar el mismo menú sin interacción por línea de comandos:
# Modo mock completo
python run.py --mode mock
# Modo real (producción) con más workers
python run.py --mode real --api-workers 4 --log-level INFO --yes
# Modo personalizado: Qdrant real, Postgres mock, LLM real
python run.py --mode custom \
--use-qdrant-mock false \
--use-postgres-mock true \
--use-llm-mock false \
--qdrant-url http://localhost:10111 \
--llm-api-key "$LLM_API_KEY" \
--api-host 0.0.0.0 --api-port 8000 --api-workers 1 --log-level DEBUG --yesSugerencia: exporta credenciales como variables de entorno y pásalas con --yes para evitar prompts.
./scripts/run_dev.sh
# O manualmente:
uvicorn backend.api.server:app --reloadEl servidor estará disponible en: http://localhost:8000
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Abre en el navegador:
frontend/web/index.html
curl -X POST "http://localhost:8000/api/answer" \
-H "Content-Type: application/json" \
-d '{
"query": {
"text": "¿Qué es el Estatuto de los Trabajadores?",
"locale": "es"
},
"filters": {
"solo_vigentes": true
},
"k": 10
}'{
"text": "El Estatuto de los Trabajadores (Real Decreto Legislativo 2/2015) es la norma fundamental que regula las relaciones laborales en España...",
"citations": [
{
"id_documento": "BOE-A-2015-11430_a1",
"titulo": "Real Decreto Legislativo 2/2015, de 23 de octubre...",
"url": "https://www.boe.es/buscar/act.php?id=BOE-A-2015-11430",
"estado_vigencia": "VIGENTE"
}
],
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "OK",
"latency_ms": {
"retrieval": 185,
"total": 2362
}
}
}Nota: Este ejemplo muestra datos ilustrativos. La política WholeDocsBudgetPolicy incluye documentos completos sin truncar (cuando CONTEXT_ALLOW_TRUNCATION=false), por lo que truncated_docs es siempre 0.
📁 RAG Jurídico
.
├── 📄 pyproject.toml # Configuración del proyecto y dependencias
├── 📄 README.md # Documentación principal
├── 📄 ROADMAP.md # Plan de implementación detallado
├── 📄 .env.example # Template de configuración
├── 📄 .gitignore # Exclusiones de git
│
├── 📁 backend/ # 🎯 Código fuente principal
│ │
│ ├── 📁 api/ # Capa API REST (FastAPI)
│ │ ├── __init__.py # Módulo API
│ │ ├── server.py # ✅ FastAPI app + middlewares
│ │ ├── routes_answer.py # ✅ Endpoint POST /api/answer
│ │ ├── routes_docs.py # ✅ Endpoint GET /api/docs/{id}
│ │ ├── routes_health.py # ✅ Endpoints GET /health/*
│ │ ├── routes_suggest.py # 📝 Archivo vacío (pendiente implementar)
│ │ └── 📁 middleware/ # Middlewares de seguridad
│ │ ├── rate_limit.py # ✅ Rate limiting por IP
│ │ └── api_key.py # ✅ Validación de API keys
│ │
│ ├── 📁 core/ # Núcleo del sistema
│ │ ├── __init__.py # Módulo core
│ │ ├── config.py # ✅ Settings con Pydantic (359 líneas)
│ │ ├── types.py # ✅ DTOs (Query, Filters, Hit, Answer...)
│ │ ├── errors.py # ✅ Excepciones personalizadas
│ │ ├── logging.py # ✅ Structlog configurado
│ │ └── metrics.py # ✅ Sistema de métricas p50/p90/p99
│ │
│ ├── 📁 retrieval/ # Búsqueda y recuperación
│ │ ├── __init__.py # Módulo retrieval
│ │ ├── factory.py # ✅ Factory pattern (mock/real)
│ │ ├── qdrant_client.py # ✅ Cliente Qdrant (vector search)
│ │ ├── postgres_store.py # ✅ Store PostgreSQL (batch fetch)
│ │ ├── aggregator.py # ✅ Agregación/deduplicación
│ │ ├── embedding_service.py # ✅ Generación embeddings
│ │ ├── filters.py # ✅ Constructor de filtros Qdrant
│ │ └── 📁 mocks/ # Implementaciones mock
│ │ ├── qdrant_mock.py
│ │ └── postgres_mock.py
│ │
│ ├── 📁 context/ # Construcción de contexto
│ │ ├── __init__.py # Módulo context
│ │ ├── builder.py # ✅ Context builder (Whole-doc)
│ │ └── policies.py # ✅ Políticas de selección
│ │
│ ├── 📁 prompts/ # Gestión de prompts versionados
│ │ ├── __init__.py # Módulo prompts
│ │ ├── registry.py # ✅ Registry de templates
│ │ ├── 📁 templates/ # Plantillas de prompts
│ │ │ └── answer_v1.txt # ✅ Prompt actual (único)
│ │ └── 📁 tests/ # Tests de regresión
│ │ └── test_prompts_regression.yaml
│ │
│ ├── 📁 llm/ # Integración con LLMs
│ │ ├── __init__.py # Módulo LLM
│ │ ├── factory.py # ✅ Factory pattern (mock/real)
│ │ ├── llm_client.py # ✅ Cliente genérico LLM (con retry)
│ │ ├── safety.py # 📝 Archivo vacío (pendiente)
│ │ └── 📁 mocks/ # Implementaciones mock
│ │ └── llm_mock.py
│ │
│ └── 📁 rag/ # Orquestación RAG
│ ├── __init__.py # Módulo RAG
│ ├── orchestrator.py # ✅ Orquestador E2E (Streaming + Determinismo)
│ └── query_processor.py # ✅ Intervención de query (JSON-First)
│
├── 📁 tests/ # 🧪 Suite de tests
│ ├── __init__.py
│ ├── test_api.py # ✅ Tests endpoints básicos
│ ├── test_routes_docs.py # ✅ Tests /api/docs/{id}
│ ├── test_security.py # ✅ Tests rate limiting + API keys + health
│ ├── test_retrieval.py # ✅ Tests Qdrant/PostgreSQL
│ ├── test_context.py # ✅ Tests builder/policies
│ ├── test_rag_flow.py # ✅ Tests integración E2E (mocks)
│ ├── test_smoke_mock.py # ✅ Smoke test rápido
│ └── test_integration_real.py # ✅ Tests con servicios reales
│
├── 📁 scripts/ # Scripts de utilidad
│ ├── run_dev.sh # ✅ Ejecutar servidor en desarrollo
│ ├── load_test.http # ✅ Ejemplos de peticiones HTTP
│ └── smoke.py # ✅ Script de smoke test CLI
│
├── 📁 Informes/ # 📊 Documentación y análisis
│ ├── ROADMAP.md # Plan detallado de implementación
│ └── analisis_variables_flujo.md # Análisis del flujo de variables
│
├── 📄 run.py # ✅ Lanzador interactivo (mock/real/custom)
│
└── 📁 frontend/ # 🎨 UI web (demo)
└── 📁 web/
└── index.html # ✅ Interfaz demo HTML/JS
Leyenda:
- ✅ = Implementado
- 🔜 = Pendiente / por mejorar
- 📁 = Directorio
- 📄 = Archivo
Nota: La sección muestra el estado actual de los módulos principales y ayudará a orientar el trabajo en F1–F5. Para cambios mayores en la arquitectura, actualiza este bloque con las nuevas rutas o estados.
# === Desarrollo ===
# Ejecutar servidor en modo desarrollo
./scripts/run_dev.sh
# O manualmente: uvicorn backend.api.server:app --reload
# === Testing ===
# Todos los tests
pytest
# Tests con coverage
pytest --cov=backend --cov-report=html
# Suite específica
pytest tests/test_retrieval.py -v
# === Calidad de Código ===
# Linting
ruff check backend/
# Formateo (si usas black)
black backend/
# Type checking
mypy backend/
# === Base de Datos ===
# Verificar estructura PostgreSQL
sudo docker exec -it iajuridica_postgres psql -U iajuridica -d legislacion -c "\d+ articles"
# Contar documentos vigentes
sudo docker exec -it iajuridica_postgres psql -U iajuridica -d legislacion -c "SELECT COUNT(*) FROM articles WHERE estado_vigencia = 'VIGENTE';"
# === Qdrant ===
# Verificar colección
curl http://localhost:10111/collections/articulos_juridicos
# Inspeccionar vector específico
curl -X POST http://localhost:10111/collections/articulos_juridicos/points \
-H "Content-Type: application/json" \
-d '{"ids": ["BOA-d-2014-90375_a82_V20140910_c0"], "with_payload": true, "with_vector": false}'# Todos los tests
pytest
# Con coverage (objetivo >80%)
pytest --cov=backend --cov-report=html
# Output HTML en htmlcov/index.html
# Tests por módulo
pytest tests/test_api.py -v # API endpoints
pytest tests/test_retrieval.py -v # Qdrant + PostgreSQL
pytest tests/test_context.py -v # Context builder
pytest tests/test_rag_flow.py -v # Flujo E2E
# Configuración en pyproject.toml
# - asyncio_mode = "auto" (para async tests)
# - Markers, fixtures y coverage configuradosVer scripts/load_test.http para ejemplos de peticiones HTTP.
Tras ejecutar tests con coverage, abre htmlcov/index.html para inspeccionar rutas de ejecución y líneas no cubiertas. La carpeta htmlcov/ se genera automáticamente como informe visual de cobertura.
tests/test_api.py: Endpoints FastAPI con TestClienttests/test_retrieval.py: Qdrant y PostgreSQL (requiere servicios activos)tests/test_context.py: Context builder y políticas de truncadotests/test_rag_flow.py: Integración E2E con mocks
Ver Informes/ROADMAP.md para el plan detallado de implementación (924 líneas con estado actualizado al 17/11/2025).
- ✅ F0: Esqueleto y configuración (COMPLETADO)
- ✅ F1: Conectores Qdrant y PostgreSQL (COMPLETADO)
- ✅ F2: Context builder y prompts (COMPLETADO)
- ✅ F3: LLM y orquestación (COMPLETADO)
- ✅ F4: API y UI (COMPLETADO)
- ⏳ F5: Optimización y benchmarks (PENDIENTE)
Estado actual: Sistema completamente funcional con arquitectura Híbrida (JSON-First para normalización, Markdown-First para respuesta). Streaming de tokens y fuentes deterministas habilitados. MCP / Búsqueda web integrada como fallback. Ready for production.
- Retrieval p90: < 300ms (búsqueda Qdrant + fetch PostgreSQL)
- Context building p90: < 50ms
- LLM generation p90: < 3s
- Total p90: < 5s
- Error rate: < 0.5%
- Uptime: > 99%
- Latencias por etapa: retrieval, context_building, llm_generation, postprocess
- Percentiles: p50, p90, p99 (vía
metrics_collector.timer()) - Counters: total_requests, successful_requests, failed_requests
- Gauges: active_connections, cache_size
- Formato: JSON en producción, colores en desarrollo
- Request ID: UUID único por petición para trazabilidad E2E
- Contexto automático: app, version, environment, request_id
- Redacción: API keys, passwords automáticamente redactados
# Health check
curl http://localhost:8000/health
# Métricas agregadas
curl http://localhost:8000/metrics
# Ver logs estructurados
# En producción: JSON parseables por ELK, Datadog, etc.
# En desarrollo: Logs con colores y formato legible