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
677 lines
28 KiB
Markdown
677 lines
28 KiB
Markdown
# 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)
|