28 KiB
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 PROMPTinclude i chunk iniettati) - ✅ Supporto multilingue (IT/ES/EN configurabile)
2. Installazione dipendenze
Le dipendenze RAG sono opzionali (gruppo rag in pyproject.toml):
cd /home/azurian/speech-to-speech
uv pip install -e ".[rag]"
Contenuto del gruppo:
sentence-transformers>=3.0.0(porta automaticamentetorch,numpy,transformersgià 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:
- Letto in UTF-8 (fallback latin-1 con sostituzione errori)
- Splittato automaticamente in chunk:
- chunk size default:
512caratteri (parametro--rag_chunk_size) - overlap default:
64caratteri (parametro--rag_chunk_overlap) - Algoritmo: splitter ricorsivo per separatori
\n\n → \n → . ? ! ; , → spazio → carattere
- chunk size default:
- 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:
{"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:
- Flag CLI: aggiungi
--rag_force_rebuildall'avvio (consigliato) - Manuale: cancella
kb/_index.npzekb/_chunks.jsonl
5. Configurazione (CLI / JSON)
Tutti i parametri sono definiti in 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 le righe seguenti (alla fine del comando speech-to-speech):
#!/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):
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 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, 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'è"
- Controlla i log — che messaggio compare?
iniettati Xonessun chunk? - Se
nessun chunk→ abbassa la soglia:--rag_threshold 0.20o anche0.15. - Aumenta top_k:
--rag_top_k 5. - Ricostruisci indice: aggiungi
--rag_force_rebuild(magari hai modificato i file dopo il primo avvio). - 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"
- Riduci batch size embedding:
--rag_embedding_batch_size 4(default 32). - 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"
- Rinforza le
instructionsdi 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'."
- Passa a
--rag_inject_as user(alcuni modelli rispettano di più i blocchi injection dentro i messaggi utente). - Aumenta la soglia (riduci falsi positivi):
--rag_threshold 0.40. - 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 (logRAG: Attivoin 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 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 | Export pubblici (singleton + dataclass) |
| RAG/retriever.py | Core: chunking, embedding, persistenza NPZ, search |
| arguments_classes/rag_arguments.py | Dataclass HfArgumentParser 12 parametri |
s2s_pipeline.py (main(), righe 1029-1059) |
Setup singleton globale + build index |
| 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 | Gruppo optional-deps rag |
| kb/README.md | Scaffold iniziale KB |
| kb/01_faq_producto.md | Esempio FAQ spagnolo |
| kb/02_politicas_internas.md | Esempio politiche spagnolo |
| start_pipeline.sh | Aggiungere flag --rag_* al comando (vedi §6) |
| start_pipeline_rag.sh | Script preconfigurato (variabili d'ambiente + tutti i flag RAG) |
| RAG/router.py | NUOVO — Router FastAPI /v1/rag/* (7 endpoint HTTP) per amministrazione dinamica |
| api/openai_realtime/websocket_router.py | 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/documentcontext: ""oppure/removeconexact: 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)
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:
{
"added": 1,
"sources": ["crm/cliente_456_nota_20260826"],
"total_after": 10
}
💡
persist: true→ il chunk viene scritto anche inkb/_dynamic.jsonl, quindi al prossimo riavvio della pipeline sarà ancora presente. Imposta afalseper chunk temporanei (solo sessione corrente).
📤 Aggiungere N chunk pre-formattati (es. da DB)
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)
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:
{
"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
curl -sS http://127.0.0.1:12345/v1/rag/status | jq
🗑️ Eliminare tutti i chunk di un cliente
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)
# 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)
# Tutte le sorgenti nell'indice, con conteggio chunk
curl -sS http://127.0.0.1:12345/v1/rag/sources | jq
Esempio output:
{
"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
# 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:
{
"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:nullsenza 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.
# 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:
{
"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_FOUND409 AMBIGUOUS_SOURCE→ hai passatosourceda sola ma esistono N chunk con stessa source; aggiungichunk_indexoppure usaindex
🔄 Upsert documento (CRUD consigliato)
Questo è l'endpoint che userai nel 90% dei casi.
Idempotente sul campo source:
- Se la
sourcenon esiste → CREATE - Se la
sourceesiste → DELETE tutti i chunk vecchi + split del nuovo testo + INSERT - Se mandi
text: ""→ DELETE atomico (nessun re-insert)
# 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:
{
"removed": 2,
"added": 1,
"sources": ["crm/cliente_456/perfil"],
"total_after": 10,
"mode": "upsert"
}
DELETE tramite upsert:
# 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)
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:
- Viene ricostruito l'indice statico da file md/txt/jsonl
- 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:
- ✅ Più chiamate
/searchpossono andare in parallelo durante la conversazione - ✅ Una
add_document/removeblocca 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
pypdfopdfplumberin 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-completionseresponses-api - Watchdog KB: auto-rebuild indice quando i file cambiano (inotify / watchdog)