feat(rag): intégration RAG dans le flux patient (corpus + ingestion + injection live)

Active le RAG de bout en bout dans la vraie conversation :
- Rag.ingestDir(dir) : ingère tous les .txt/.md d'un dossier (source = nom fichier).
- KazeiaService : auto-ingestion du corpus au 1er démarrage si base vide
  (.../kazeia/rag_corpus/), + intent `rag_ingest` (clear+reingest, brique admin future).
- Injection live : seuil relevé à 0.82 (calé e5 : base ~0.80 / pertinent ~0.88), k=3,
  pour ne PAS injecter de hors-sujet.
- Corpus seed FR de psychoéducation (6 fichiers) dans docs/rag_corpus_seed/.

Validé device (ragEnabled ON, Speaker lib qwen3.5-4b) : 6 chunks ingérés, tour
patient « je n'arrive pas à dormir… » -> retrieve 1 hit (top 0.84, le chunk sommeil ;
les 5 autres filtrés par le seuil) -> bloc CONTEXTE injecté (577 c) -> réponse ancrée.
Aucun crash, coexistence RAM OK. ragEnabled reste OFF par défaut (opt-in clinicien).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kazeia Team 2026-06-10 12:12:21 +02:00
parent 95a7a25335
commit 62a9803fdd
9 changed files with 50 additions and 1 deletions

View File

@ -0,0 +1,9 @@
# Corpus seed RAG (psychoéducation FR)
Corpus de DÉPART pour le RAG Kazeia — contenu standard et sûr (hygiène du sommeil,
respiration, ancrage 5-4-3-2-1, activation comportementale, rumination, solitude).
**À curer/étendre par le clinicien.** En prod il est distribué via le catalogue
Nextcloud et déposé sous `.../kazeia/rag_corpus/` ; l'app l'ingère au 1er démarrage
si la base RAG est vide (ou via l'intent `rag_ingest`).
Modèle d'embedding : `multilingual-e5-small` (384-dim, préfixes query:/passage:).

View File

@ -0,0 +1 @@
Technique d'ancrage 5-4-3-2-1. Lorsqu'une émotion devient envahissante ou en cas d'attaque de panique, ramener son attention au présent aide à reprendre pied. Nommez cinq choses que vous voyez autour de vous, quatre choses que vous pouvez toucher, trois sons que vous entendez, deux odeurs que vous percevez, et une chose que vous pouvez goûter. Cet exercice d'ancrage sensoriel interrompt le cycle des pensées anxieuses en reconnectant le corps à l'environnement immédiat.

View File

@ -0,0 +1 @@
Activation comportementale et humeur basse. Quand le moral est bas, l'envie de faire des choses diminue, ce qui réduit encore les occasions de ressentir du plaisir : un cercle vicieux s'installe. L'activation comportementale consiste à programmer de petites activités agréables ou valorisantes, même sans motivation au départ. Commencez petit : une courte marche, un appel à un proche, une tâche simple menée à son terme. L'action précède souvent la motivation, et non l'inverse. Noter ce qui a procuré un peu de satisfaction aide à repérer ce qui fait du bien.

View File

@ -0,0 +1 @@
Respiration et apaisement de l'anxiété. La respiration lente et profonde active le système nerveux parasympathique et réduit la tension physique. Un exercice simple : inspirez doucement par le nez pendant quatre secondes, puis expirez lentement par la bouche pendant six secondes. L'expiration plus longue que l'inspiration est ce qui calme le rythme cardiaque. Répétez ce cycle pendant quelques minutes, en posant une main sur le ventre pour sentir le souffle. Cet exercice peut se pratiquer discrètement, à tout moment d'une montée d'angoisse.

View File

@ -0,0 +1 @@
Rumination mentale. Ruminer, c'est tourner en boucle sur des pensées négatives sans parvenir à une solution. Cela amplifie la détresse sans rien résoudre. Pour s'en dégager, il peut aider de fixer un court moment dédié aux soucis dans la journée, puis de reporter les pensées qui surgissent en dehors de ce créneau. Distinguer ce qui est sous votre contrôle de ce qui ne l'est pas oriente l'énergie vers l'action possible. Prendre du recul en observant la pensée comme un événement mental passager, plutôt que comme une vérité, réduit son emprise.

View File

@ -0,0 +1 @@
Sentiment de solitude. Se sentir seul est une expérience douloureuse et fréquente, qui ne reflète pas une valeur personnelle. Le lien social, même bref et modeste, nourrit le bien-être : un message, un sourire échangé, une courte conversation comptent. Reprendre contact progressivement, à son rythme, vaut mieux que d'attendre de se sentir prêt. Les activités partagées autour d'un intérêt commun créent du lien sans la pression de la conversation directe. Tenir un journal de trois moments positifs par jour aide à rééquilibrer l'attention souvent captée par le manque.

View File

@ -0,0 +1 @@
Hygiène du sommeil. Pour favoriser l'endormissement, il est utile de se coucher et se lever à des horaires réguliers, même le week-end. Évitez les écrans lumineux dans l'heure qui précède le coucher, car la lumière bleue retarde la sécrétion de mélatonine. La chambre doit rester fraîche, sombre et silencieuse. La caféine et l'alcool perturbent le sommeil profond : limitez-les en fin de journée. Si le sommeil ne vient pas après vingt minutes, mieux vaut se lever et faire une activité calme plutôt que de rester au lit à ruminer.

View File

@ -59,6 +59,22 @@ class Rag(
return n
}
/** Ingestion de tous les .txt/.md d'un dossier (source = nom de fichier). */
@Synchronized
fun ingestDir(dir: String): Int {
val d = java.io.File(dir)
if (!d.isDirectory) { log("ingestDir: '$dir' absent"); return 0 }
val files = d.listFiles { f -> f.isFile && (f.name.endsWith(".txt") || f.name.endsWith(".md")) }
?.sortedBy { it.name } ?: emptyList()
var total = 0
for (f in files) {
val txt = try { f.readText() } catch (e: Exception) { log("ingestDir: lecture ${f.name} échec"); continue }
total += ingest(f.nameWithoutExtension, txt)
}
log("ingestDir '$dir': ${files.size} fichiers, $total chunks")
return total
}
/**
* Récupère le contexte pertinent pour [query] et le formate en bloc prêt à
* injecter, ou null si rien au-dessus du seuil. Le seuil évite d'injecter du

View File

@ -660,6 +660,19 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
} catch (e: Exception) { log("RAG real test error: ${e.message}"); e.printStackTrace() }
}
}
intent?.getStringExtra("rag_ingest")?.let { dirArg ->
// (Ré)ingestion du corpus dans l'instance RAG live : clear + ingestDir.
// dirArg vide -> dossier corpus par défaut. Brique de l'app admin future.
val dir = dirArg.ifBlank { "${KazeiaApplication.MODELS_DIR}/../rag_corpus" }
log("RAG ingest: $dir")
serviceScope.launch {
val r = rag
if (r == null || !r.isReady) { log("RAG ingest: RAG non prêt (ragEnabled off ?)"); return@launch }
r.clear()
val n = r.ingestDir(dir)
log("RAG ingest terminé: $n chunks (corpus=${r.count()})")
}
}
if (intent?.hasExtra("cfg_set") == true) {
// Maintenance/bench : persiste engine+model+prompt sans passer par le
// ContentProvider CLI (qui casse sur les valeurs contenant ':'). Les
@ -834,6 +847,9 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
val emb = com.kazeia.rag.EngineEmbedder(embModel, nThreads = 4, pooling = -1,
queryPrefix = "query: ", passagePrefix = "passage: ", log = { m -> log(m) })
com.kazeia.rag.Rag(this@KazeiaService, emb, "e5-small") { m -> log(m) }.also {
// Auto-ingestion du corpus au 1er démarrage (base vide). Le corpus
// (catalogue Nextcloud en prod) est déposé sous .../kazeia/rag_corpus/.
if (it.isReady && it.count() == 0) it.ingestDir("${KazeiaApplication.MODELS_DIR}/../rag_corpus")
it.loadIndex(); log("[RAG] activé (ready=${it.isReady}, chunks=${it.count()})")
}
} catch (e: Throwable) { log("[RAG] init échec: ${e.message}"); null }
@ -1747,7 +1763,9 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
// JAMAIS la gestion de crise.
rag?.takeIf { it.isReady }?.let { r ->
try {
r.retrieve(patientMessage)?.let { ctx ->
// Seuil ~0.82 calé sur e5 (cosinus de base ~0.80 ; pertinent ~0.88) pour
// ne PAS injecter de hors-sujet. k=3. À tuner sur corpus réel (logs "top=").
r.retrieve(patientMessage, k = 3, threshold = 0.82f)?.let { ctx ->
prompt = "$ctx\n\n$prompt"
log("[RAG] contexte injecté (${ctx.length} chars)")
}