# 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]`.
• `0.15`–`0.25` → recall alto, recuperi quasi tutto
• `0.35`–`0.45` → precisione alta, solo match certi
• `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:
• `system` → concatenato alle istruzioni system (**consigliato**)
• `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" │ ├─► (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: ``` --- ## 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)