vocero-s2s/docs/RAG_SERVER_SIDE.md
valenti b5f82fb48c
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
first git
2026-08-26 11:30:14 +00:00

677 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RAG Server-Side — Guida Ufficiale
> **Feature**: Retrieval Augmented Generation integrato nella pipeline speech-to-speech.
> **Approccio**: 2 — Injection trasparente lato server (nessuna modifica lato client).
> **Versione minima speech-to-speech**: `0.2.11`
---
## 1. Panoramica
L'RAG server-side arricchisce **automaticamente** ogni risposta dell'LLM con brani rilevanti estratti da una knowledge base locale. L'intero ciclo avviene in modo invisibile al client Realtime:
```
Utente parla → STT → 🟡 RAG RETRIEVAL (hook interno) → LLM → TTS → Audio all'utente
kb/*.md, kb/*.txt, kb/*.jsonl
top-k chunk iniettati nel prompt
```
### Vantaggi
- ✅ Zero modifiche lato client — funziona con qualunque SDK Realtime
- ✅ Riuso del singleton globale (1 sola copia degli embeddings per N pipeline parallele)
- ✅ Persistenza dell'indice NPZ — ricostruzione solo se i documenti cambiano
- ✅ 100% compatibile con i log già esistenti (`LLM REQUEST PROMPT` include i chunk iniettati)
- ✅ Supporto multilingue (IT/ES/EN configurabile)
---
## 2. Installazione dipendenze
Le dipendenze RAG sono opzionali (gruppo `rag` in [pyproject.toml](file:///home/azurian/speech-to-speech/pyproject.toml#L92-L94)):
```bash
cd /home/azurian/speech-to-speech
uv pip install -e ".[rag]"
```
**Contenuto del gruppo**:
- `sentence-transformers>=3.0.0` (porta automaticamente `torch`, `numpy`, `transformers` già presenti)
> ✅ **Su DGX Spark GB10**: il modello embedding viene caricato nativamente su CUDA (detected da `--rag_device auto`, default).
---
## 3. Struttura Knowledge Base
La KB risiede nella cartella configurata dal parametro `--rag_kb_path` (default: `./kb`).
```
kb/
├── README.md ← istruzioni scaffold (auto-generato)
├── 01_faq_producto.md ← esempio spagnolo incluso
├── 02_politicas_internas.md ← esempio spagnolo incluso
├── manual/ ← sotto-cartelle supportate
│ ├── 01_instalacion.md
│ └── 02_facturacion.txt
├── datos/
│ └── clientes.jsonl ← formato pre-chunkizzato
├── _index.npz ← ⚙️ indice generato (NON modificare)
└── _chunks.jsonl ← ⚙️ catalogo chunk (NON modificare)
```
### Formati supportati
#### A. File Markdown / TXT (raccomandato, zero sforzo)
Qualsiasi `*.md` o `*.txt` nella cartella o sottocartelle viene:
1. Letto in UTF-8 (fallback latin-1 con sostituzione errori)
2. Splittato automaticamente in chunk:
- **chunk size** default: `512` caratteri (parametro `--rag_chunk_size`)
- **overlap** default: `64` caratteri (parametro `--rag_chunk_overlap`)
- Algoritmo: splitter ricorsivo per separatori `\n\n → \n → . ? ! ; , → spazio → carattere`
3. Ogni chunk riceve `source = path_relativo#chunk_index`
#### B. JSONL pre-chunkizzato (controllo fine)
Se preferisci gestire chunk e metadata manualmente (es. da estrazione PDF strutturata), crea `*.jsonl` con **una riga per chunk**:
```jsonl
{"text": "Horario soporte lun-vier 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"}}
```
Campi supportati:
| Campo | Obbligatorio | Note |
|---|---|---|
| `text` | ✅ | Corpo del chunk (stringa) |
| `source` | ❌ | Default: `nome_file.jsonl#rigaN` |
| `chunk_index` | ❌ | Default: numero riga 0-based |
| qualsiasi altro | ❌ | Salvato in `chunk.metadata` e riportato nei log |
---
## 4. Persistenza indice NPZ
Per evitare di ricalcolare centinaia/migliaia di embeddings ad ogni avvio:
### Primo avvio
```
file md/txt/jsonl → chunking → embedding → salva:
kb/_index.npz (matrice numpy float32 N × embedding_dim)
kb/_chunks.jsonl (testo, source, metadata per ogni riga)
```
Tempo stimato: ~500 chunk/s su GB10 con MiniLM-L12-v2.
### Avvii successivi
```
Se esiste _index.npz E _chunks.jsonl E le dimensioni coincidono:
→ carica direttamente da disco (<1 secondo)
Altrimenti:
→ ricostruisce da zero
```
### Forzare ricostruzione
Usa **uno** di questi metodi quando aggiungi/modifichi documenti:
1. **Flag CLI**: aggiungi `--rag_force_rebuild` all'avvio (consigliato)
2. **Manuale**: cancella `kb/_index.npz` e `kb/_chunks.jsonl`
---
## 5. Configurazione (CLI / JSON)
Tutti i parametri sono definiti in [rag_arguments.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/arguments_classes/rag_arguments.py) e sono accessibili:
- Via **flag CLI** nel comando `speech-to-speech ... --rag_enabled`
- Via **file JSON** passato come unico argomento
- Via `HfArgumentParser` (pattern standard del progetto)
### Tabella parametri
| Flag CLI | Default | Descrizione |
|---|---|---|
| `--rag_enabled` | `False` | **Master switch**. Senza questo flag, tutto il resto viene ignorato. |
| `--rag_kb_path` | `./kb` | Path assoluto/relativo della cartella KB. |
| `--rag_embedding_model` | `paraphrase-multilingual-MiniLM-L12-v2` | Modello HuggingFace `sentence-transformers`. *Multilingue*, ottimo compromesso qualità/velocità. |
| `--rag_device` | `auto` | `auto` / `cuda` / `cpu` / `mps`. |
| `--rag_top_k` | `3` | Numero massimo di chunk iniettati per turno. **Non superare 5-6 se TTS output lungo** (crescono i token di contesto). |
| `--rag_threshold` | `0.25` | Soglia minima similarità coseno. Range `[0,1]`.<br>`0.15``0.25` → recall alto, recuperi quasi tutto<br>`0.35``0.45` → precisione alta, solo match certi<br>`0.5+` → molto restrittivo |
| `--rag_language` | `es` | Lingua header injection: `es`, `it`, `en`. Cambia l'intestazione del blocco RAG passato all'LLM. |
| `--rag_inject_as` | `system` | Dove iniettare i risultati:<br>`system` → concatenato alle istruzioni system (**consigliato**)<br>`user` → accodato all'ultimo messaggio utente |
| `--rag_chunk_size` | `512` | Caratteri massimi per chunk (solo md/txt). |
| `--rag_chunk_overlap` | `64` | Overlap caratteri tra chunk adiacenti. |
| `--rag_embedding_batch_size` | `32` | Batch size embedding durante build indice. |
| `--rag_force_rebuild` | `False` | Se `True`, ignora l'indice NPZ e rigenera tutto. |
### Modelli embedding consigliati
| Modello | Lingue | Dim embedding | Velocità GB10 | Migliore per |
|---|---|---|---|---|
| `paraphrase-multilingual-MiniLM-L12-v2` (default) | 50+ | 384 | ⚡⚡⚡ | IT/ES/EN mix, KB generiche |
| `hiiamsid/sentence_similarity_spanish_es` | Solo ES | 768 | ⚡⚡ | KB solo spagnola |
| `BAAI/bge-m3` | Multilingue | 1024 | ⚡ | KB grandi, query complesse |
| `intfloat/multilingual-e5-large-instruct` | Multilingue | 1024 | ⚡ | Query complesse, istruzioni |
---
## 6. Integrazione in start_pipeline.sh
Aggiungi al tuo [start_pipeline.sh](file:///home/azurian/speech-to-speech/start_pipeline.sh) le righe seguenti (alla fine del comando `speech-to-speech`):
```bash
#!/bin/bash
# ... (flags esistenti: llm_backend, model_name, base_url, ecc.)
# ========== 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 # ⚠️ COMMENTA dopo aver ricostruito l'indice!
```
**Esempio start completo** (con i parametri che già usi):
```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. Architettura interna — Sequenza operazioni
### 7.1 Setup (una tantum ad avvio)
Avviene in [s2s_pipeline.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/s2s_pipeline.py#L1029-L1059) dentro `main()`:
```
main()
├─► if rag_enabled == True:
│ ├─► RAGRetriever(...) → carica modello sentence-transformers
│ ├─► .build_index() → NPZ o rebuild da zero
│ │ ├─► _load_index() se esiste → carica immediato
│ │ └─► _load_text_files() + _load_jsonl() + encode() + _save_index()
│ └─► set_global_rag(rag) → registra singleton globale
└─► build_pipeline(...)
└─► ogni LLM handler, nel suo setup(): self.rag = get_global_rag()
```
### 7.2 Per ogni turno utente (runtime)
Hook in [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), riga **610**:
```
process(request: LLMIn)
├─► build active_chat
├─► _apply_config(instructions, wants_audio) → system message con le istruzioni
├─► _inject_rag_context(active_chat, turn_id, turn_revision) ← 🔴 NOSTRO HOOK
│ ├─► (1) query = estrai ultimo testo utente da .buffer
│ ├─► (2) results = rag.search(query, top_k, threshold)
│ │ ├─► embedding query (1 vettore)
│ │ ├─► @ (matrice embeddings @ query.T) → coseno similarity
│ │ ├─► argpartition top-k + argsort score
│ │ └─► filtro per threshold
│ ├─► (3) se results vuoti → log debug, NIENTE injection
│ ├─► (4) altrimenti formatta in blocco testuale:
│ │ "Fragmentos relevantes recuperados..."
│ │ "[1] (source=xxx.md, score=0.672)\n<testo>"
│ ├─► (5a) inject_as=system → aggiungi come system message
│ └─► (5b) inject_as=user → aggiungi come user message finale
├─► resolve_auto_language(lang prompt)
├─► _generate() →
│ ├─► LOG: LLM REQUEST PROMPT ← include i chunk RAG!
│ ├─► stream LLM
│ └─► LOG: LLM RESPONSE ← risposta finale generata col contesto
└─► yield chunks → TTS
```
---
## 8. Messaggi di log
Tutti i log RAG usano lo stesso format della pipeline (prefix `pipeline X`, livello INFO o DEBUG).
### Setup OK
```
RAG: Inizializzazione modello embedding=paraphrase-multilingual-MiniLM-L12-v2 device=cuda su kb_path=/home/kb
RAG: Indice caricato da disco: 27 chunk (shape=(27, 384)).
RAG: Attivo. kb=/home/kb top_k=3 soglia=0.300 inject_as=system lingua=es chunk=512
RAG hook attivo in BaseOpenAICompatibleHandler (inject_as=system, lingua=es)
```
### Setup: nessun documento
```
RAG: Nessun documento trovato in /home/kb. Indice vuoto (retrieval non inietterà niente).
```
### Retrieval riuscito
```
RAG: iniettati 2 chunk per turn=turn_91a75 rev=1 — 01_faq_producto.md#c1(0.67); 02_politicas_internas.md#c0(0.52)
```
### Nessun match
```
RAG: nessun chunk rilevante per turn=turn_91a75 (query='Hola, ¿cómo te llamas?' soglia=0.300)
```
### Fallimento search (non blocca la pipeline!)
```
RAG search fallita per turn=turn_91a75 rev=1: <eccezione>
```
---
## 9. Debug & troubleshooting
### Sintomo 1: "RAG non inietta niente, anche se so che il documento c'è"
1. Controlla i log — che messaggio compare? `iniettati X` o `nessun chunk`?
2. Se `nessun chunk` → abbassa la soglia: `--rag_threshold 0.20` o anche `0.15`.
3. Aumenta top_k: `--rag_top_k 5`.
4. Ricostruisci indice: aggiungi `--rag_force_rebuild` (magari hai modificato i file dopo il primo avvio).
5. Testa direttamente: confronta il testo della domanda vs il testo del chunk — i modelli MiniLM possono avere scarsi risultati con frasi troppo colloquiali. **Soluzione**: aumenta overlap o scrivi chunk con domande+risposte esplicite.
### Sintomo 2: "Primo avvio lentissimo / crash CUDA OOM"
1. Riduci batch size embedding: `--rag_embedding_batch_size 4` (default 32).
2. Se hai migliaia di chunk, prendi in considerazione `paraphrase-multilingual-MiniLM-L12-v2` (384 dim, metà memoria di bge-m3).
### Sintomo 3: "L'LLM ignora i chunk e inventa fatti"
1. Rinforza le `instructions` di sistema (via session.update) con un prompt tipo:
> *"Responde SOLO con información contenuta nei Fragmentos relevantes recuperados. Si no hay información, di 'No tengo información sobre eso'."*
2. Passa a `--rag_inject_as user` (alcuni modelli rispettano di più i blocchi injection dentro i messaggi utente).
3. Aumenta la **soglia** (riduci falsi positivi): `--rag_threshold 0.40`.
4. Verifica che i chunk abbiano senso (elimina intestazioni Markdown, metadati non pertinenti).
### Sintomo 4: "Il log LLM REQUEST PROMPT non mostra i chunk"
Verifica:
- `--rag_enabled` è effettivamente passato (log `RAG: Attivo` in avvio?)
- L'handler LLM è una sottoclasse di `BaseOpenAICompatibleHandler` (chat-completions e responses-api lo sono; mlx-lm locale e transformers no — usano [language_model_handler.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/LLM/language_model_handler.py) che NON condivide la base; per questi ultimi il RAG al momento non è agganciato).
---
## 10. Esempi di sessioni reali
### Caso: FAQ prodotto in spagnolo
- **Utente 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..." ← scartato da threshold=0.30
```
- **Testo iniettato nel 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.
```
- **Risposta 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**: l'audio viene generato e spedito al client.
---
## 11. File del progetto coinvolti
| File | Ruolo |
|---|---|
| [RAG/\_\_init\_\_.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/__init__.py) | Export pubblici (singleton + dataclass) |
| [RAG/retriever.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/retriever.py) | Core: chunking, embedding, persistenza 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 parametri |
| [s2s_pipeline.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/s2s_pipeline.py) (`main()`, righe 1029-1059) | Setup singleton globale + 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 (riga 41); setup hook (riga 163-166); injection (riga 610); helper `_extract_last_user_text` + `_inject_rag_context` (righe 274-347) |
| [pyproject.toml](file:///home/azurian/speech-to-speech/pyproject.toml#L92-L94) | Gruppo optional-deps `rag` |
| [kb/README.md](file:///home/azurian/speech-to-speech/kb/README.md) | Scaffold iniziale KB |
| [kb/01_faq_producto.md](file:///home/azurian/speech-to-speech/kb/01_faq_producto.md) | Esempio FAQ spagnolo |
| [kb/02_politicas_internas.md](file:///home/azurian/speech-to-speech/kb/02_politicas_internas.md) | Esempio politiche spagnolo |
| [start_pipeline.sh](file:///home/azurian/speech-to-speech/start_pipeline.sh) | Aggiungere flag --rag_* al comando (vedi §6) |
| [start_pipeline_rag.sh](file:///home/azurian/speech-to-speech/start_pipeline_rag.sh) | Script preconfigurato (variabili d'ambiente + tutti i flag RAG) |
| [RAG/router.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/router.py) | **NUOVO** — Router FastAPI `/v1/rag/*` (7 endpoint HTTP) per amministrazione dinamica |
| [api/openai_realtime/websocket_router.py](file:///home/azurian/speech-to-speech/src/speech_to_speech/api/openai_realtime/websocket_router.py#L511-L526) | Montaggio condizionale del router RAG nello stesso FastAPI del websocket |
---
## 13. 🆕 API HTTP dinamiche (popola la KB a caldo)
La pipeline espone **7 endpoint JSON** sullo stesso server che serve il websocket
(`ws://host:12345` → HTTP su `http://host:12345`).
Base path comune: `/v1/rag`.
> ⚠️ Se RAG è disattivato (no `--rag_enabled`) tutti gli endpoint rispondono
> **503 Service Unavailable** con `{"code": "RAG_NOT_ENABLED", ...}`.
### 13.1 Endpoint disponibili
| Metodo | Path | Descrizione |
|---|---|---|
| `GET` | `/v1/rag/status` | Stato: numero chunk, embedding dim, sorgenti, device, soglie |
| `GET` | `/v1/rag/sources` | **LIST** — sorgenti uniche + chunk count per ognuna |
| `GET` | `/v1/rag/chunks` | **LIST** con query params: filtri, paginazione, ordinamento per rilevanza |
| `POST` | `/v1/rag/chunks/list` | Stesso listing, ma body JSON (consigliato per query complesse) |
| `POST` | `/v1/rag/chunks/update` | **UPDATE** singolo chunk (testo / metadata / sorgente) |
| `POST` | `/v1/rag/upsert/document` | **UPSERT atomico** di un documento intero (delete old + insert new). ✅ Uso più comune per CRUD |
| `POST` | `/v1/rag/search` | Cerca chunk con lo stesso algoritmo usato per l'iniezione (debug/test) |
| `POST` | `/v1/rag/add/document` | INSERT puro di testo libero (chunking auto) |
| `POST` | `/v1/rag/add/chunks` | INSERT puro di chunk preformattati |
| `POST` | `/v1/rag/remove` | **REMOVE**: per prefisso o exact match |
| `POST` | `/v1/rag/reload` | Ricostruzione totale indice da file in `kb/` |
> 💡 **Pattern CRUD consigliato**:
> - CREATE → `/upsert/document` (idempotente: se la source non esiste la crea)
> - READ → `/sources` (lista sorgenti) + `/chunks?source_exact=X` (dettaglio)
> - UPDATE → `/upsert/document` (stessa source, testo nuovo → sostituzione atomica)
> - DELETE → `/upsert/document` con `text: ""` oppure `/remove` con `exact: true`
---
### 13.2 Esempi curl
> Base URL: `http://127.0.0.1:12345/v1/rag` (cambia porta e host se hai modificato lo script).
#### 📤 Aggiungere un documento (testo libero, chunking automatico)
```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
```
**Risposta:**
```json
{
"added": 1,
"sources": ["crm/cliente_456_nota_20260826"],
"total_after": 10
}
```
> 💡 **`persist: true`** → il chunk viene scritto anche in `kb/_dynamic.jsonl`,
> quindi **al prossimo riavvio** della pipeline sarà ancora presente.
> Imposta a `false` per chunk temporanei (solo sessione corrente).
#### 📤 Aggiungere N chunk pre-formattati (es. da 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
```
#### 🔎 Provare il retrieval da HTTP (debug, no 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
```
Risposta con i chunk recuperati e lo 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 stato generale
```bash
curl -sS http://127.0.0.1:12345/v1/rag/status | jq
```
#### 🗑️ Eliminare tutti i chunk di 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
```
Suggerimento: usa prefissi intelligenti per organizzare la KB:
```
source_prefix = "crm/" → cancella tutta la rubrica
source_prefix = "pedidos/" → cancella tutti i dati ordini
source_prefix = "dynamic#" → cancella tutti i chunk temporanei auto-numerati
```
#### 🛡️ Remove con exact match (solo source ESATTA)
```bash
# Cancella SOLO il documento 'crm/cliente_456_nota_20260826'
# (non cancella '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
```
#### 📋 Lista sorgenti (index inventory)
```bash
# Tutte le sorgenti nell'indice, con conteggio chunk
curl -sS http://127.0.0.1:12345/v1/rag/sources | jq
```
Esempio output:
```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}
]
}
```
#### 🔍 Elencare chunk con filtri e paginazione
```bash
# GET con query params: solo chunk di cliente_456, paged
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: ORDINA per rilevanza su una query + 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
```
Risposta:
```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..."
}
]
}
```
Campi speciali:
- `index`: la posizione numerica nel vettore di chunk — passala direttamente a `/chunks/update?index=...`
- `score`: `null` senza query, float 0-1 con ordinamento per rilevanza
#### 🆙 Aggiornare un singolo chunk
Usa `/chunks/update` quando devi modificare **solo un pezzetto specifico** (es. correggere un errore di battitura, cambiare metadata). Per aggiornare **un intero documento** usa invece `/upsert/document`.
```bash
# Opzione A: aggiorna tramite `index` (da 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
# Opzione B: aggiorna tramite (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
```
Risposta successo `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
}
}
```
Errori HTTP possibili:
- `404 CHUNK_NOT_FOUND`
- `409 AMBIGUOUS_SOURCE` → hai passato `source` da sola ma esistono N chunk con stessa source; aggiungi `chunk_index` oppure usa `index`
#### 🔄 Upsert documento (CRUD consigliato)
**Questo è l'endpoint che userai nel 90% dei casi.**
Idempotente sul campo `source`:
- Se la `source` non esiste → CREATE
- Se la `source` esiste → DELETE tutti i chunk vecchi + split del nuovo testo + INSERT
- Se mandi `text: ""` → DELETE atomico (nessun re-insert)
```bash
# CREATE/UPDATE di un documento intero (es. 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
```
Risposta:
```json
{
"removed": 2,
"added": 1,
"sources": ["crm/cliente_456/perfil"],
"total_after": 10,
"mode": "upsert"
}
```
DELETE tramite upsert:
```bash
# Elimina del tutto tutti i chunk associati a questa source (atomico)
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
```
#### 🔄 Ricaricare tutta la KB da file (dopo aver copiato nuovi 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 Persistenza cross-riavvio: `kb/_dynamic.jsonl`
Tutti i chunk aggiunti via API con `persist: true` vengono appesi a `kb/_dynamic.jsonl` (un JSON per riga). Al prossimo avvio della pipeline:
1. Viene ricostruito l'indice statico da file md/txt/jsonl
2. **Subito dopo** vengono ricaricati e ri-embeddati anche tutti i chunk da `_dynamic.jsonl`
Quindi la conoscenza aggiunta via HTTP è **durevole** e non si perde riavviando.
---
### 13.4 Thread safety
Tutte le API usano un `threading.RLock()` all'interno di [RAGRetriever](file:///home/azurian/speech-to-speech/src/speech_to_speech/RAG/retriever.py):
- ✅ Più chiamate `/search` possono andare in parallelo durante la conversazione
- ✅ Una `add_document`/`remove` blocca brevemente l'indice solo per le operazioni di vstack/take numpy — retrieval non si perde, viene atteso
- ✅ Persistenza su disco (NPZ + JSONL) avviene fuori dal lock, quindi non rallenta la chiamata conversazionale
---
## 14. Roadmap / miglioramenti possibili
- [ ] **Supporto PDF**: aggiungere `pypdf` o `pdfplumber` in gruppo opzionale
- [ ] **MMR reranking** invece del solo top-k coseno (migliore diversità chunk)
- [ ] **HyDE** (LLM genera query ipotetica + embedding di quella, per query colloquiali)
- [ ] **Cross-encoder reranker** 2-stage per KB grandi (>10k chunk)
- [ ] **Hook su LanguageModelHandler locale** (mlx-lm / transformers) — attualmente l'RAG è attivo solo per backends `chat-completions` e `responses-api`
- [ ] **Watchdog KB**: auto-rebuild indice quando i file cambiano (inotify / watchdog)