Some checks are pending
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
679 lines
29 KiB
Markdown
679 lines
29 KiB
Markdown
# 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)
|