first git
CI / ruff (push) Waiting to run
CI / mypy (push) Waiting to run
CI / pytest (push) Waiting to run
CI / package (push) Waiting to run
CI / Install smoke (${{ matrix.label }}) (linux, ubuntu-latest) (push) Blocked by required conditions
CI / Install smoke (${{ matrix.label }}) (macos-arm64, macos-14) (push) Blocked by required conditions
CI / ruff (push) Waiting to run
CI / mypy (push) Waiting to run
CI / pytest (push) Waiting to run
CI / package (push) Waiting to run
CI / Install smoke (${{ matrix.label }}) (linux, ubuntu-latest) (push) Blocked by required conditions
CI / Install smoke (${{ matrix.label }}) (macos-arm64, macos-14) (push) Blocked by required conditions
This commit is contained in:
@@ -0,0 +1,678 @@
|
||||
# RAG Server-Side — Guía Oficial
|
||||
|
||||
> **Funcionalidad**: Retrieval Augmented Generation integrado en la pipeline speech-to-speech.
|
||||
> **Enfoque**: 2 — Inyección transparente en el lado servidor (sin cambios en el cliente).
|
||||
> **Versión mínima speech-to-speech**: `0.2.11`
|
||||
|
||||
---
|
||||
|
||||
## 1. Visión general
|
||||
|
||||
El RAG server-side enriquece **automáticamente** cada respuesta del LLM con fragmentos relevantes extraídos de una base de conocimientos local. Todo el ciclo ocurre de forma invisible para el cliente Realtime:
|
||||
|
||||
```
|
||||
Usuario habla → STT → 🟡 RAG RETRIEVAL (hook interno) → LLM → TTS → Audio al usuario
|
||||
↓
|
||||
kb/*.md, kb/*.txt, kb/*.jsonl
|
||||
↓
|
||||
top-k chunk inyectados en el prompt
|
||||
```
|
||||
|
||||
### Ventajas
|
||||
- ✅ **Cero cambios en el cliente** — funciona con cualquier SDK Realtime
|
||||
- ✅ **Singleton global compartido** (1 sola copia de embeddings para N pipelines paralelas)
|
||||
- ✅ **Persistencia del índice NPZ** — reconstrucción solo si los documentos cambian
|
||||
- ✅ **100% compatible con los logs existentes** (`LLM REQUEST PROMPT` incluye los chunk inyectados)
|
||||
- ✅ **Soporte multilingüe** (ES/IT/EN configurable)
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalación de dependencias
|
||||
|
||||
Las dependencias RAG son opcionales (grupo `rag` en [pyproject.toml](file:///home/azurian/speech-to-speech/pyproject.toml#L92-L94)):
|
||||
|
||||
```bash
|
||||
cd /home/azurian/speech-to-speech
|
||||
uv pip install -e ".[rag]"
|
||||
```
|
||||
|
||||
**Contenido del grupo**:
|
||||
- `sentence-transformers>=3.0.0` (arrastra automáticamente `torch`, `numpy`, `transformers` que ya están presentes)
|
||||
|
||||
> ✅ **En DGX Spark GB10**: el modelo de embedding se carga nativamente sobre CUDA (detectado por `--rag_device auto`, valor por defecto).
|
||||
|
||||
---
|
||||
|
||||
## 3. Estructura de la base de conocimientos
|
||||
|
||||
La KB reside en la carpeta configurada mediante el parámetro `--rag_kb_path` (por defecto: `./kb`).
|
||||
|
||||
```
|
||||
kb/
|
||||
├── README.md ← instrucciones del scaffold (auto-generado)
|
||||
├── 01_faq_producto.md ← ejemplo español incluido
|
||||
├── 02_politicas_internas.md ← ejemplo español incluido
|
||||
│
|
||||
├── manual/ ← subcarpetas soportadas
|
||||
│ ├── 01_instalacion.md
|
||||
│ └── 02_facturacion.txt
|
||||
│
|
||||
├── datos/
|
||||
│ └── clientes.jsonl ← formato pre-chunkizado
|
||||
│
|
||||
├── _index.npz ← ⚙️ índice generado (NO modificar)
|
||||
├── _chunks.jsonl ← ⚙️ catálogo chunk (NO modificar)
|
||||
└── _dynamic.jsonl ← ⚙️ chunk dinámicos persistidos vía API
|
||||
```
|
||||
|
||||
### Formatos soportados
|
||||
|
||||
#### A. Archivos Markdown / TXT (recomendado, esfuerzo cero)
|
||||
|
||||
Cualquier `*.md` o `*.txt` en la carpeta o subcarpetas se procesa así:
|
||||
1. Lectura en UTF-8 (con fallback latin-1 y sustitución de errores)
|
||||
2. División automática en chunk:
|
||||
- **tamaño de chunk** por defecto: `512` caracteres (parámetro `--rag_chunk_size`)
|
||||
- **overlap** por defecto: `64` caracteres (parámetro `--rag_chunk_overlap`)
|
||||
- Algoritmo: splitter recursivo con separadores `\n\n → \n → . ? ! ; , → espacio → carácter`
|
||||
3. Cada chunk recibe `source = path_relativo#chunk_index`
|
||||
|
||||
#### B. JSONL pre-chunkizado (control total)
|
||||
|
||||
Si prefieres gestionar chunk y metadatos manualmente (ej. extracción PDF estructurada), crea `*.jsonl` con **una línea por chunk**:
|
||||
|
||||
```jsonl
|
||||
{"text": "Horario soporte lun-vie 09 a 18h", "source": "faq_horarios", "chunk_index": 0, "metadata": {"categoria": "soporte", "pagina": 12}}
|
||||
{"text": "Devolución 14 días naturales", "source": "faq_compras", "chunk_index": 0, "metadata": {"categoria": "ventas"}}
|
||||
```
|
||||
|
||||
Campos soportados:
|
||||
|
||||
| Campo | Obligatorio | Notas |
|
||||
|---|---|---|
|
||||
| `text` | ✅ | Cuerpo del chunk (string) |
|
||||
| `source` | ❌ | Por defecto: `nombre_archivo.jsonl#lineaN` |
|
||||
| `chunk_index` | ❌ | Por defecto: número de línea 0-based |
|
||||
| cualquier otro | ❌ | Guardado en `chunk.metadata` y mostrado en los logs |
|
||||
|
||||
---
|
||||
|
||||
## 4. Persistencia del índice NPZ
|
||||
|
||||
Para evitar recalcular cientos/miles de embeddings en cada arranque:
|
||||
|
||||
### Primer arranque
|
||||
```
|
||||
archivos md/txt/jsonl → chunking → embedding → guarda:
|
||||
kb/_index.npz (matriz numpy float32 N × embedding_dim)
|
||||
kb/_chunks.jsonl (texto, source, metadata por cada línea)
|
||||
```
|
||||
Tiempo estimado: ~500 chunk/s en GB10 con MiniLM-L12-v2.
|
||||
|
||||
### Arranques sucesivos
|
||||
```
|
||||
Si existen _index.npz Y _chunks.jsonl Y las dimensiones coinciden:
|
||||
→ carga directamente desde disco (<1 segundo)
|
||||
Si no:
|
||||
→ reconstruye desde cero
|
||||
```
|
||||
|
||||
### Forzar reconstrucción
|
||||
Usa **un solo** método cuando añadas/modifiques documentos:
|
||||
1. **Flag CLI**: añade `--rag_force_rebuild` al arranque (recomendado)
|
||||
2. **Manual**: borra `kb/_index.npz` y `kb/_chunks.jsonl`
|
||||
|
||||
---
|
||||
|
||||
## 5. Configuración (CLI / JSON)
|
||||
|
||||
Todos los parámetros están definidos en [rag_arguments.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/arguments_classes/rag_arguments.py) y son accesibles:
|
||||
- Vía **flag CLI** en el comando `speech-to-speech ... --rag_enabled`
|
||||
- Vía **archivo JSON** pasado como único argumento
|
||||
- Vía `HfArgumentParser` (patrón estándar del proyecto)
|
||||
|
||||
### Tabla de parámetros
|
||||
|
||||
| Flag CLI | Valor por defecto | Descripción |
|
||||
|---|---|---|
|
||||
| `--rag_enabled` | `False` | **Interruptor maestro**. Sin este flag, todo lo demás se ignora. |
|
||||
| `--rag_kb_path` | `./kb` | Ruta absoluta/relativa de la carpeta KB. |
|
||||
| `--rag_embedding_model` | `paraphrase-multilingual-MiniLM-L12-v2` | Modelo HuggingFace `sentence-transformers`. *Multilingüe*, buen compromiso calidad/velocidad. |
|
||||
| `--rag_device` | `auto` | `auto` / `cuda` / `cpu` / `mps`. |
|
||||
| `--rag_top_k` | `3` | Número máximo de chunk inyectados por turno. **No superar 5-6 si la salida del TTS es larga** (crecen los tokens de contexto). |
|
||||
| `--rag_threshold` | `0.25` | Umbral mínimo de similitud coseno. Rango `[0,1]`.<br>• `0.15`–`0.25` → recall alto, recupera casi todo<br>• `0.35`–`0.45` → precisión alta, solo coincidencias seguras<br>• `0.5+` → muy restrictivo |
|
||||
| `--rag_language` | `es` | Idioma del encabezado de inyección: `es`, `it`, `en`. Cambia la cabecera del bloque RAG que pasa al LLM. |
|
||||
| `--rag_inject_as` | `system` | Dónde inyectar los resultados:<br>• `system` → concatena a las instrucciones system (**recomendado**)<br>• `user` → añade como último mensaje del usuario |
|
||||
| `--rag_chunk_size` | `512` | Caracteres máximos por chunk (solo md/txt). |
|
||||
| `--rag_chunk_overlap` | `64` | Overlap en caracteres entre chunk adyacentes. |
|
||||
| `--rag_embedding_batch_size` | `32` | Batch size de embeddings durante la construcción del índice. |
|
||||
| `--rag_force_rebuild` | `False` | Si `True`, ignora el índice NPZ y regenera todo. |
|
||||
|
||||
### Modelos de embedding recomendados
|
||||
|
||||
| Modelo | Idiomas | Dim embedding | Velocidad GB10 | Mejor para |
|
||||
|---|---|---|---|---|
|
||||
| `paraphrase-multilingual-MiniLM-L12-v2` (por defecto) | 50+ | 384 | ⚡⚡⚡ | ES/IT/EN mezclado, KB genéricas |
|
||||
| `hiiamsid/sentence_similarity_spanish_es` | Solo ES | 768 | ⚡⚡ | KB en español exclusivamente |
|
||||
| `BAAI/bge-m3` | Multilingüe | 1024 | ⚡ | KB grandes, consultas complejas |
|
||||
| `intfloat/multilingual-e5-large-instruct` | Multilingüe | 1024 | ⚡ | Consultas complejas con instrucciones |
|
||||
|
||||
---
|
||||
|
||||
## 6. Integración en start_pipeline.sh
|
||||
|
||||
Añade a tu [start_pipeline.sh](file:///home/azurian/speech-to-speech/start_pipeline.sh) las líneas siguientes (al final del comando `speech-to-speech`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# ... (flags existentes: llm_backend, model_name, base_url, etc.)
|
||||
|
||||
# ========== RAG SERVER-SIDE ==========
|
||||
--rag_enabled \
|
||||
--rag_kb_path ./kb \
|
||||
--rag_top_k 3 \
|
||||
--rag_threshold 0.30 \
|
||||
--rag_language es \
|
||||
--rag_inject_as system \
|
||||
--rag_device auto
|
||||
# --rag_force_rebuild # ⚠️ COMENTA después de reconstruir el índice!
|
||||
```
|
||||
|
||||
**Ejemplo de arranque completo** (con los parámetros que ya usas):
|
||||
|
||||
```bash
|
||||
uv run speech-to-speech \
|
||||
--llm_backend chat-completions \
|
||||
--chat_completions_handler_base_url "$LLM_BASE_URL" \
|
||||
--chat_completions_handler_model_name "$LLM_MODEL" \
|
||||
--chat_completions_handler_api_key "$LLM_API_KEY" \
|
||||
--mode realtime \
|
||||
--realtime_host 0.0.0.0 --realtime_port 8000 \
|
||||
--tts qwen3 --qwen3_tts_model_name "$TTS_MODEL" \
|
||||
--stt whisper --whisper_stt_model_name "$STT_MODEL" \
|
||||
\
|
||||
--rag_enabled \
|
||||
--rag_kb_path ./kb \
|
||||
--rag_top_k 3 \
|
||||
--rag_threshold 0.30 \
|
||||
--rag_language es \
|
||||
--rag_inject_as system
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Arquitectura interna — Secuencia de operaciones
|
||||
|
||||
### 7.1 Setup (una sola vez al arranque)
|
||||
Ocurre en [s2s_pipeline.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/s2s_pipeline.py#L1029-L1059) dentro de `main()`:
|
||||
|
||||
```
|
||||
main()
|
||||
├─► si rag_enabled == True:
|
||||
│ ├─► RAGRetriever(...) → carga modelo sentence-transformers
|
||||
│ ├─► .build_index() → carga NPZ o rebuild desde cero
|
||||
│ │ ├─► _load_index() si existe → carga inmediata
|
||||
│ │ └─► _load_text_files() + _load_jsonl() + encode() + _save_index()
|
||||
│ └─► set_global_rag(rag) → registra el singleton global
|
||||
└─► build_pipeline(...)
|
||||
└─► cada LLM handler, en su setup(): self.rag = get_global_rag()
|
||||
```
|
||||
|
||||
### 7.2 Por cada turno de usuario (runtime)
|
||||
Hook en [base_openai_compatible_language_model.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/LLM/base_openai_compatible_language_model.py#L609-L613), línea **610**:
|
||||
|
||||
```
|
||||
process(request: LLMIn)
|
||||
├─► construye active_chat
|
||||
├─► _apply_config(instructions, wants_audio) → system message con instrucciones
|
||||
├─► _inject_rag_context(active_chat, turn_id, turn_revision) ← 🔴 NUESTRO HOOK
|
||||
│ ├─► (1) query = extrae último texto de usuario desde .buffer
|
||||
│ ├─► (2) results = rag.search(query, top_k, threshold)
|
||||
│ │ ├─► embedding query (1 vector)
|
||||
│ │ ├─► @ (matriz embeddings @ query.T) → similitud coseno
|
||||
│ │ ├─► argpartition top-k + argsort score
|
||||
│ │ └─► filtro por threshold
|
||||
│ ├─► (3) si results vacío → log debug, NINGUNA inyección
|
||||
│ ├─► (4) si no, formatea en bloque de texto:
|
||||
│ │ "Fragmentos relevantes recuperados..."
|
||||
│ │ "[1] (source=xxx.md, score=0.672)\n<texto>"
|
||||
│ ├─► (5a) inject_as=system → añade como system message
|
||||
│ └─► (5b) inject_as=user → añade como último user message
|
||||
├─► resolve_auto_language(lang prompt)
|
||||
├─► _generate() →
|
||||
│ ├─► LOG: LLM REQUEST PROMPT ← incluye los chunk RAG!
|
||||
│ ├─► stream LLM
|
||||
│ └─► LOG: LLM RESPONSE ← respuesta final generada con el contexto
|
||||
└─► yield chunks → TTS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Mensajes de log
|
||||
|
||||
Todos los logs RAG usan el mismo formato de la pipeline (prefijo `pipeline X`, nivel INFO o DEBUG).
|
||||
|
||||
### Setup OK
|
||||
```
|
||||
RAG: Inizializzazione modello embedding=paraphrase-multilingual-MiniLM-L12-v2 device=cuda su kb_path=/home/kb
|
||||
RAG: Índice cargado desde disco: 27 chunk (shape=(27, 384)).
|
||||
RAG: Activo. kb=/home/kb top_k=3 umbral=0.300 inject_as=system idioma=es chunk=512
|
||||
RAG hook activo en BaseOpenAICompatibleHandler (inject_as=system, idioma=es)
|
||||
```
|
||||
|
||||
### Setup: ningún documento
|
||||
```
|
||||
RAG: Ningún documento encontrado en /home/kb. Índice vacío (retrieval no inyectará nada).
|
||||
```
|
||||
|
||||
### Retrieval exitoso
|
||||
```
|
||||
RAG: inyectados 2 chunk para turn=turn_91a75 rev=1 — 01_faq_producto.md#c1(0.67); 02_politicas_internas.md#c0(0.52)
|
||||
```
|
||||
|
||||
### Ninguna coincidencia
|
||||
```
|
||||
RAG: ningún chunk relevante para turn=turn_91a75 (query='Hola, ¿cómo te llamas?' umbral=0.300)
|
||||
```
|
||||
|
||||
### Fallo de search (¡no bloquea la pipeline!)
|
||||
```
|
||||
RAG search fallida para turn=turn_91a75 rev=1: <excepción>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Depuración y troubleshooting
|
||||
|
||||
### Síntoma 1: "RAG no inyecta nada, aunque sé que el documento está ahí"
|
||||
1. Revisa los logs — ¿qué mensaje aparece? `inyectados X` o `ningún chunk`?
|
||||
2. Si es `ningún chunk` → baja el umbral: `--rag_threshold 0.20` o incluso `0.15`.
|
||||
3. Sube top_k: `--rag_top_k 5`.
|
||||
4. Reconstruye el índice: añade `--rag_force_rebuild` (quizás modificaste archivos después del primer arranque).
|
||||
5. Prueba directamente: compara el texto de la pregunta vs el texto del chunk — los modelos MiniLM pueden tener resultados escasos con frases muy coloquiales. **Solución**: aumenta el overlap o redacta chunk con preguntas+respuestas explícitas.
|
||||
|
||||
### Síntoma 2: "Primer arranque lentísimo / crash CUDA OOM"
|
||||
1. Reduce el batch size de embedding: `--rag_embedding_batch_size 4` (por defecto 32).
|
||||
2. Si tienes miles de chunk, valora `paraphrase-multilingual-MiniLM-L12-v2` (384 dim, la mitad de memoria que bge-m3).
|
||||
|
||||
### Síntoma 3: "El LLM ignora los chunk e inventa datos"
|
||||
1. Refuerza las `instructions` de sistema (vía session.update) con un prompt como:
|
||||
> *"Responde SOLO con la información contenida en los Fragmentos relevantes recuperados. Si no hay información, di 'No tengo información sobre eso'."*
|
||||
2. Prueba `--rag_inject_as user` (algunos modelos respetan más los bloques inyectados dentro de los mensajes de usuario).
|
||||
3. Sube el **umbral** (reduce falsos positivos): `--rag_threshold 0.40`.
|
||||
4. Verifica que los chunk tengan sentido (elimina cabeceras Markdown, metadatos no pertinentes).
|
||||
|
||||
### Síntoma 4: "El log LLM REQUEST PROMPT no muestra los chunk"
|
||||
Verifica:
|
||||
- Que `--rag_enabled` se haya pasado realmente (¿aparece el log `RAG: Activo` en el arranque?).
|
||||
- Que el handler LLM sea una subclase de `BaseOpenAICompatibleHandler` (chat-completions y responses-api lo son; mlx-lm local y transformers no — usan [language_model_handler.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/LLM/language_model_handler.py) que NO comparte la base; para estos últimos el RAG no está enganchado todavía).
|
||||
|
||||
---
|
||||
|
||||
## 10. Ejemplos de sesiones reales
|
||||
|
||||
### Caso: FAQ de producto en español
|
||||
- **Usuario (audio)**: *"¿Cuánto tiempo tengo para solicitar un reembolso?"*
|
||||
- **STT**: *"¿Cuánto tiempo tengo para solicitar un reembolso?"*
|
||||
- **RAG retrieval**:
|
||||
```
|
||||
Score 0.71 → 01_faq_producto.md#c3: "¿Ofrecéis reembolsos? Sí. Todos los planes tienen un periodo de devolución de 14 días naturales..."
|
||||
Score 0.23 → 02_politicas_internas.md#c1: "Retención de registros: datos usuario dado de baja 6 meses..." ← descartado por threshold=0.30
|
||||
```
|
||||
- **Texto inyectado en el system**:
|
||||
```
|
||||
Fragmentos relevantes recuperados de la base de conocimientos:
|
||||
[1] (source=01_faq_producto.md, score=0.712)
|
||||
¿Ofrecéis reembolsos?
|
||||
|
||||
Sí. Todos los planes tienen un periodo de devolución de **14 días naturales**
|
||||
desde la compra, sin necesidad de justificación. Pasado ese plazo, los reembolsos
|
||||
se evalúan caso por caso por el equipo de soporte.
|
||||
```
|
||||
- **Respuesta LLM**: *"Tienes 14 días naturales desde la compra para solicitar un reembolso sin necesidad de dar justificaciones. Pasado ese plazo, el equipo de soporte lo evalúa caso por caso."*
|
||||
- **TTS**: el audio se genera y se envía al cliente.
|
||||
|
||||
---
|
||||
|
||||
## 11. Archivos del proyecto involucrados
|
||||
|
||||
| Archivo | Rol |
|
||||
|---|---|
|
||||
| [RAG/\_\_init\_\_.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/__init__.py) | Exports públicos (singleton + dataclass) |
|
||||
| [RAG/retriever.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/retriever.py) | Core: chunking, embedding, persistencia NPZ, search |
|
||||
| [arguments_classes/rag_arguments.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/arguments_classes/rag_arguments.py) | Dataclass HfArgumentParser 12 parámetros |
|
||||
| [s2s_pipeline.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/s2s_pipeline.py) (`main()`, líneas 1029-1059) | Setup singleton global + build index |
|
||||
| [base_openai_compatible_language_model.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/LLM/base_openai_compatible_language_model.py) | Import singleton (línea 41); setup hook (líneas 163-166); injection (línea 610); helper `_extract_last_user_text` + `_inject_rag_context` (líneas 274-347) |
|
||||
| [pyproject.toml](file:///home/azurian/speech-to-speech/pyproject.toml#L92-L94) | Grupo optional-deps `rag` |
|
||||
| [kb/README.md](file:///home/azurian/speech-to-speech/kb/README.md) | Scaffold inicial de la KB |
|
||||
| [kb/01_faq_producto.md](file:///home/azurian/speech-to-speech/kb/01_faq_producto.md) | Ejemplo FAQ español |
|
||||
| [kb/02_politicas_internas.md](file:///home/azurian/speech-to-speech/kb/02_politicas_internas.md) | Ejemplo políticas español |
|
||||
| [start_pipeline.sh](file:///home/azurian/speech-to-speech/start_pipeline.sh) | Añadir flags --rag_* al comando (véase §6) |
|
||||
| [start_pipeline_rag.sh](file:///home/azurian/speech-to-speech/start_pipeline_rag.sh) | Script preconfigurado (variables de entorno + todos los flags RAG) |
|
||||
| [RAG/router.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/router.py) | **NUEVO** — Router FastAPI `/v1/rag/*` (11 endpoints HTTP) para administración dinámica |
|
||||
| [api/openai_realtime/websocket_router.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/api/openai_realtime/websocket_router.py#L511-L526) | Montaje condicional del router RAG en el mismo FastAPI del websocket |
|
||||
|
||||
---
|
||||
|
||||
## 13. 🆕 API HTTP dinámicas (puebla la KB en caliente)
|
||||
|
||||
La pipeline expone **11 endpoints JSON** en el mismo servidor que sirve el websocket
|
||||
(`ws://host:12345` → HTTP sobre `http://host:12345`).
|
||||
Base path común: `/v1/rag`.
|
||||
|
||||
> ⚠️ Si RAG está desactivado (sin `--rag_enabled`) todos los endpoints responden
|
||||
> **503 Service Unavailable** con `{"code": "RAG_NOT_ENABLED", ...}`.
|
||||
|
||||
### 13.1 Endpoints disponibles
|
||||
|
||||
| Método | Path | Descripción |
|
||||
|---|---|---|
|
||||
| `GET` | `/v1/rag/status` | Estado: número chunk, dimensión embedding, fuentes, dispositivo, umbrales |
|
||||
| `GET` | `/v1/rag/sources` | **LIST** — fuentes únicas + contador de chunk por cada una |
|
||||
| `GET` | `/v1/rag/chunks` | **LIST** con query params: filtros, paginación, ordenación por relevancia |
|
||||
| `POST` | `/v1/rag/chunks/list` | Mismo listado pero con body JSON (recomendado para consultas complejas) |
|
||||
| `POST` | `/v1/rag/chunks/update` | **UPDATE** de un solo chunk (texto / metadatos / fuente) |
|
||||
| `POST` | `/v1/rag/upsert/document` | **UPSERT atómico** de un documento entero (borra lo viejo + inserta lo nuevo). ✅ Uso más común para CRUD |
|
||||
| `POST` | `/v1/rag/search` | Busca chunk con el mismo algoritmo que se usa para la inyección (depuración/pruebas) |
|
||||
| `POST` | `/v1/rag/add/document` | INSERT puro de texto libre (chunking automático) |
|
||||
| `POST` | `/v1/rag/add/chunks` | INSERT puro de chunk preformateados |
|
||||
| `POST` | `/v1/rag/remove` | **REMOVE**: por prefijo o coincidencia exacta |
|
||||
| `POST` | `/v1/rag/reload` | Reconstrucción total del índice desde los archivos en `kb/` |
|
||||
|
||||
> 💡 **Patrón CRUD recomendado**:
|
||||
> - CREATE → `/upsert/document` (idempotente: si la source no existe la crea)
|
||||
> - READ → `/sources` (lista fuentes) + `/chunks?source_exact=X` (detalle)
|
||||
> - UPDATE → `/upsert/document` (misma source, texto nuevo → sustitución atómica)
|
||||
> - DELETE → `/upsert/document` con `text: ""` o `/remove` con `exact: true`
|
||||
|
||||
---
|
||||
|
||||
### 13.2 Ejemplos curl
|
||||
|
||||
> Base URL: `http://127.0.0.1:12345/v1/rag` (cambia puerto y host si modificaste el script).
|
||||
|
||||
#### 📤 Añadir un documento (texto libre, chunking automático)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/add/document \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"source": "crm/cliente_456_nota_20260826",
|
||||
"text": "Cliente: Javier García, id=456. Plan contratado: Premium Anual, renovación 15/02/2027. Contacto: +34 600 111 222, javier@empresa.es. Notas: prefiere que le llamen por la mañana antes de las 11h. Ha reportado un incidente con la factura número 2026-08-114 el 20/08/2026 que ya fue resuelto con un descuento del 15% aplicado en la siguiente factura.",
|
||||
"metadata": {"cliente_id": 456, "pais": "es", "categoria": "crm"},
|
||||
"persist": true,
|
||||
"dynamic": true
|
||||
}' | jq
|
||||
```
|
||||
|
||||
**Respuesta:**
|
||||
```json
|
||||
{
|
||||
"added": 1,
|
||||
"sources": ["crm/cliente_456_nota_20260826"],
|
||||
"total_after": 10
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **`persist: true`** → el chunk también se escribe en `kb/_dynamic.jsonl`,
|
||||
> por lo tanto **en el próximo arranque** de la pipeline seguirá presente.
|
||||
> Ponlo en `false` para chunk temporales (solo la sesión actual).
|
||||
|
||||
#### 📤 Añadir N chunk preformateados (ej. desde DB)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/add/chunks \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"items": [
|
||||
{"text": "Pedido 8942 — 25/08/2026. Productos: Teclado RGB Pro x1, Ratón Ergonómico x1. Estado: enviado, tracking 1Z999AA10123456784. Entrega estimada: 27/08/2026.",
|
||||
"source": "pedidos/cliente_456/8942",
|
||||
"metadata": {"pedido_id": 8942, "estado": "enviado"}},
|
||||
{"text": "Dirección de envío habitual del cliente 456: Av. Diagonal 444, puerta 3, 08013 Barcelona, España. Contacto entrega: +34 600 111 222.",
|
||||
"source": "direcciones/cliente_456/principal",
|
||||
"metadata": {"tipo": "envio", "predeterminada": true}}
|
||||
],
|
||||
"persist": true,
|
||||
"dynamic": true
|
||||
}' | jq
|
||||
```
|
||||
|
||||
#### 🔎 Probar el retrieval vía HTTP (depuración, sin audio)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/search \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"query": "¿Qué pedido tiene Javier García y cuándo llega?",
|
||||
"top_k": 3,
|
||||
"threshold": 0.25
|
||||
}' | jq
|
||||
```
|
||||
|
||||
Respuesta con los chunk recuperados y el score:
|
||||
```json
|
||||
{
|
||||
"query": "¿Qué pedido tiene Javier García y cuándo llega?",
|
||||
"count": 2,
|
||||
"results": [
|
||||
{"text": "Pedido 8942 — 25/08/2026...",
|
||||
"source": "pedidos/cliente_456/8942", "score": 0.813},
|
||||
{"text": "Cliente: Javier García, id=456. Plan contratado: Premium Anual...",
|
||||
"source": "crm/cliente_456_nota_20260826", "score": 0.742}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 📊 Ver estado general
|
||||
|
||||
```bash
|
||||
curl -sS http://127.0.0.1:12345/v1/rag/status | jq
|
||||
```
|
||||
|
||||
#### 🗑️ Eliminar todos los chunk de un cliente
|
||||
|
||||
```bash
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/remove \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"source_prefix": "crm/cliente_456"}' | jq
|
||||
```
|
||||
|
||||
Sugerencia: usa prefijos inteligentes para organizar la KB:
|
||||
```
|
||||
source_prefix = "crm/" → borra toda la agenda
|
||||
source_prefix = "pedidos/" → borra todos los datos de pedidos
|
||||
source_prefix = "dynamic#" → borra todos los chunk temporales auto-numerados
|
||||
```
|
||||
|
||||
#### 🛡️ Remove con coincidencia exacta (solo source EXACTA)
|
||||
|
||||
```bash
|
||||
# Borra SÓLO el documento 'crm/cliente_456_nota_20260826'
|
||||
# (no borra 'crm/cliente_456_nota_20260827')
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/remove \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"source_prefix": "crm/cliente_456_nota_20260826", "exact": true}' | jq
|
||||
```
|
||||
|
||||
#### 📋 Listado de fuentes (inventario del índice)
|
||||
|
||||
```bash
|
||||
# Todas las fuentes del índice, con conteo de chunk
|
||||
curl -sS http://127.0.0.1:12345/v1/rag/sources | jq
|
||||
```
|
||||
|
||||
Ejemplo salida:
|
||||
```json
|
||||
{
|
||||
"count": 5,
|
||||
"items": [
|
||||
{"source": "01_faq_producto.md", "chunk_count": 5, "has_dynamic": false},
|
||||
{"source": "crm/cliente_456_nota_20260826", "chunk_count": 1, "has_dynamic": true},
|
||||
{"source": "pedidos/cliente_456/8942", "chunk_count": 1, "has_dynamic": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 🔍 Listar chunk con filtros y paginación
|
||||
|
||||
```bash
|
||||
# GET con query params: solo chunk de cliente_456, paginado
|
||||
curl -sS 'http://127.0.0.1:12345/v1/rag/chunks?source_prefix=crm/cliente_456&limit=10&offset=0' | jq
|
||||
|
||||
# POST con body JSON: ORDENA por relevancia sobre una consulta + filtro
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/chunks/list \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"source_prefix": "crm/",
|
||||
"query": "descuento aplicado en factura",
|
||||
"min_score": 0.20,
|
||||
"limit": 10,
|
||||
"include_text": true
|
||||
}' | jq
|
||||
```
|
||||
|
||||
Respuesta:
|
||||
```json
|
||||
{
|
||||
"total": 2,
|
||||
"offset": 0,
|
||||
"limit": 10,
|
||||
"query": "descuento aplicado en factura",
|
||||
"items": [
|
||||
{
|
||||
"index": 9,
|
||||
"source": "crm/cliente_456_nota_20260826",
|
||||
"chunk_index": 1000009,
|
||||
"score": 0.731,
|
||||
"metadata": {"cliente_id": 456, "categoria": "crm"},
|
||||
"is_dynamic": true,
|
||||
"text": "Cliente: Javier García... 15% descuento..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Campos especiales:
|
||||
- `index`: la posición numérica en el vector de chunk — pásala directamente a `/chunks/update`
|
||||
- `score`: `null` sin query, float 0-1 con ordenación por relevancia
|
||||
|
||||
#### 🆙 Actualizar un solo chunk
|
||||
|
||||
Usa `/chunks/update` cuando debas modificar **únicamente un trozo específico** (ej. corregir una errata, cambiar metadatos). Para actualizar **un documento entero** usa en su lugar `/upsert/document`.
|
||||
|
||||
```bash
|
||||
# Opción A: actualiza por `index` (de list_chunks → campo "index": 9)
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/chunks/update \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"index": 9,
|
||||
"new_text": "Cliente: Javier García, id=456. Plan Premium Anual, renovación 15/02/2028 (2 años). Descuento 20% acordado en la llamada del 10/08.",
|
||||
"new_metadata": {"cliente_id": 456, "categoria": "crm", "ultima_revision": "2026-08-26"}
|
||||
}' | jq
|
||||
|
||||
# Opción B: actualiza por (source, chunk_index)
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/chunks/update \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"source": "crm/cliente_456_nota_20260826",
|
||||
"chunk_index": 1000009,
|
||||
"new_source": "crm/cliente_456/nota_principal"
|
||||
}' | jq
|
||||
```
|
||||
|
||||
Respuesta éxito `200`:
|
||||
```json
|
||||
{
|
||||
"updated": 1,
|
||||
"chunk": {
|
||||
"index": 9,
|
||||
"source": "crm/cliente_456/nota_principal",
|
||||
"chunk_index": 1000009,
|
||||
"text": "Cliente: Javier García...",
|
||||
"metadata": {"cliente_id": 456, "...": "..."},
|
||||
"is_dynamic": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Errores HTTP posibles:
|
||||
- `404 CHUNK_NOT_FOUND`
|
||||
- `409 AMBIGUOUS_SOURCE` → pasaste `source` sola pero existen N chunk con la misma source; añade `chunk_index` o usa `index`
|
||||
|
||||
#### 🔄 Upsert documento (CRUD recomendado)
|
||||
|
||||
**Éste es el endpoint que usarás en el 90% de los casos.**
|
||||
Idempotente sobre el campo `source`:
|
||||
- Si la `source` no existe → CREATE
|
||||
- Si la `source` existe → DELETE de todos los chunk antiguos + split del nuevo texto + INSERT
|
||||
- Si mandas `text: ""` → DELETE atómico (sin re-insert)
|
||||
|
||||
```bash
|
||||
# CREATE/UPDATE de un documento entero (ej. nota cliente)
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/upsert/document \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"source": "crm/cliente_456/perfil",
|
||||
"text": "Javier García — ID 456. Plan Premium Anual, renovación automática 15/02/2028. Precio: 24€/mes. Email javier@empresa.es. Tel +34 600 111 222. Dirección envío habitual: Av. Diagonal 444, puerta 3, 08013 Barcelona. Notas: siempre prefiere atención por la mañana, antes de las 11h. Caso abierto 2026-08-0124: seguimiento calidad postventa, cerrado con valoración 5/5.",
|
||||
"metadata": {"cliente_id": 456, "pais": "es", "actualizado": "2026-08-26"},
|
||||
"dynamic": true,
|
||||
"persist": true
|
||||
}' | jq
|
||||
```
|
||||
|
||||
Respuesta:
|
||||
```json
|
||||
{
|
||||
"removed": 2,
|
||||
"added": 1,
|
||||
"sources": ["crm/cliente_456/perfil"],
|
||||
"total_after": 10,
|
||||
"mode": "upsert"
|
||||
}
|
||||
```
|
||||
|
||||
DELETE vía upsert:
|
||||
```bash
|
||||
# Elimina del todo todos los chunk asociados a esta source (atómico)
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/upsert/document \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"source": "crm/cliente_456/perfil", "text": ""}' | jq
|
||||
```
|
||||
|
||||
#### 🔄 Recargar toda la KB desde archivos (después de copiar nuevos md/txt)
|
||||
|
||||
```bash
|
||||
curl -sS -X POST http://127.0.0.1:12345/v1/rag/reload \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"force_rebuild": true}' | jq
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 13.3 Persistencia entre reinicios: `kb/_dynamic.jsonl`
|
||||
|
||||
Todos los chunk añadidos vía API con `persist: true` se añaden a `kb/_dynamic.jsonl` (un JSON por línea). En el próximo arranque de la pipeline:
|
||||
1. Se reconstruye el índice estático desde los archivos md/txt/jsonl
|
||||
2. **Inmediatamente después** se recargan y re-embeddan también todos los chunk de `_dynamic.jsonl`
|
||||
|
||||
Por lo tanto el conocimiento añadido por HTTP es **duradero** y no se pierde al reiniciar.
|
||||
|
||||
---
|
||||
|
||||
### 13.4 Seguridad en hilos (thread safety)
|
||||
|
||||
Todas las API usan un `threading.RLock()` dentro de [RAGRetriever](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/retriever.py):
|
||||
- ✅ Varias llamadas `/search` pueden ejecutarse en paralelo durante la conversación
|
||||
- ✅ Una `add_document`/`remove` bloquea brevemente el índice solo para las operaciones numpy de vstack/take — el retrieval no se pierde, espera
|
||||
- ✅ La persistencia en disco (NPZ + JSONL) ocurre fuera del lock, por lo que no ralentiza la llamada de la conversación
|
||||
|
||||
---
|
||||
|
||||
## 14. Hoja de ruta / posibles mejoras
|
||||
|
||||
- [ ] **Soporte PDF**: añadir `pypdf` o `pdfplumber` en un grupo opcional
|
||||
- [ ] **MMR reranking** en lugar del solo top-k por coseno (mejor diversidad entre chunk)
|
||||
- [ ] **HyDE** (el LLM genera una consulta hipotética + embedding de la misma, para consultas coloquiales)
|
||||
- [ ] **Cross-encoder reranker** de 2 etapas para KB grandes (>10k chunk)
|
||||
- [ ] **Hook sobre LanguageModelHandler local** (mlx-lm / transformers) — actualmente el RAG solo está enganchado para los backends `chat-completions` y `responses-api`
|
||||
- [ ] **Watchdog KB**: auto-rebuild del índice cuando cambian los archivos (inotify / watchdog)
|
||||
Reference in New Issue
Block a user