fix(safety,rag): correctifs critiques code-review
CrisisGuard (sécurité vitale) : - normalize() : suppression accents (NFD) + apostrophes→espace + compactage, pour matcher la sortie STT FR (souvent sans accents) et la frappe libre. - PATTERNS étendus : formulations directes manquantes (« je veux mourir », « idées suicidaires/noires », « disparaître », « me pendre », « passer à l'acte », « plus la force de vivre »…). « me pendre » autonome (aucun usage bénin) ; « me noyer » reste gardé (idiome « se noyer dans le travail »). - Tests étendus : 12 positifs + 4 négatifs (idiomes « mourir de rire/faim », « idées plein la tête », « passe à la pharmacie »). 11/11 verts. Rag.retrieve() : un chunk plus long que le budget ne fait plus retomber tout le bloc à null — on garantit ≥1 hit (tronqué si besoin) puis on empile tant qu'il reste du budget. Évite la perte de contexte pertinent. Rag.close() : @Synchronized — sérialise avec retrieve()/searchScored() pour qu'un teardown (toggle RAG off) ne libère pas le contexte natif de l'embedder pendant une requête provider en cours (évite le SIGSEGV). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32669ae039
commit
e1d9c94751
|
|
@ -122,16 +122,21 @@ class Rag(
|
||||||
val hits = idx.search(qv, k, threshold)
|
val hits = idx.search(qv, k, threshold)
|
||||||
if (hits.isEmpty()) return null
|
if (hits.isEmpty()) return null
|
||||||
|
|
||||||
|
// On garantit l'injection d'AU MOINS le meilleur hit (tronqué si besoin),
|
||||||
|
// puis on empile les suivants tant qu'il reste du budget. Un chunk long ne
|
||||||
|
// doit jamais faire retomber tout le bloc à null (perte de contexte pertinent).
|
||||||
val sb = StringBuilder("CONTEXTE DES DOCUMENTS :\n")
|
val sb = StringBuilder("CONTEXTE DES DOCUMENTS :\n")
|
||||||
|
val header = sb.length
|
||||||
for (h in hits) {
|
for (h in hits) {
|
||||||
val t = db.textOf(h.chunkId) ?: continue
|
val t0 = db.textOf(h.chunkId) ?: continue
|
||||||
val line = "- $t\n"
|
val room = budgetChars + header - sb.length // place restante pour ce chunk
|
||||||
if (sb.length + line.length > budgetChars + 24) break
|
if (room < 24) break // plus de place utile
|
||||||
sb.append(line)
|
val t = if (t0.length + 3 > room) t0.take(room - 4).trimEnd() + "…" else t0
|
||||||
|
sb.append("- ").append(t).append('\n')
|
||||||
}
|
}
|
||||||
val out = sb.toString().trim()
|
val out = sb.toString().trim()
|
||||||
log("retrieve: ${hits.size} hits (top=${"%.2f".format(hits.first().score)}), ${out.length} chars injectés")
|
log("retrieve: ${hits.size} hits (top=${"%.2f".format(hits.first().score)}), ${out.length} chars injectés")
|
||||||
return if (out.length > "CONTEXTE DES DOCUMENTS :".length) out else null
|
return if (sb.length > header) out else null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun count(): Int = db.count()
|
fun count(): Int = db.count()
|
||||||
|
|
@ -139,7 +144,10 @@ class Rag(
|
||||||
fun pendingCount(): Int = db.docsNeedingIngest().size
|
fun pendingCount(): Int = db.docsNeedingIngest().size
|
||||||
fun dim(): Int = embedder.dim
|
fun dim(): Int = embedder.dim
|
||||||
fun clear() { db.clear(); index = null }
|
fun clear() { db.clear(); index = null }
|
||||||
fun close() { embedder.close() }
|
// @Synchronized : sérialise avec searchScored()/retrieve() (mêmes verrous) pour
|
||||||
|
// qu'un teardown (toggle RAG off) ne libère pas le contexte natif de l'embedder
|
||||||
|
// pendant qu'une requête du provider (test admin) est en cours → évite le SIGSEGV.
|
||||||
|
@Synchronized fun close() { embedder.close() }
|
||||||
|
|
||||||
/** Récupération AVEC scores, pour le test/preview admin (n'injecte rien). */
|
/** Récupération AVEC scores, pour le test/preview admin (n'injecte rien). */
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,39 @@ object CrisisGuard {
|
||||||
"S'il te plaît, parle dès maintenant à une personne de confiance, ou appelle le 112. " +
|
"S'il te plaît, parle dès maintenant à une personne de confiance, ou appelle le 112. " +
|
||||||
"Tu n'es pas seul, et ta vie compte."
|
"Tu n'es pas seul, et ta vie compte."
|
||||||
|
|
||||||
// Motifs (regex, sur le message en minuscules). Tunables par le clinicien.
|
// Motifs (regex, appliqués sur le message NORMALISÉ : minuscules SANS accents,
|
||||||
|
// cf normalize() — le STT FR supprime souvent les accents). Donc on écrit les
|
||||||
|
// motifs sans accents. Tunables/extensibles par le clinicien (jamais retirer un
|
||||||
|
// positif sans test). Couvre suicide, mort voulue, disparition, auto-agression,
|
||||||
|
// fardeau, passage à l'acte.
|
||||||
private val PATTERNS: List<Regex> = listOf(
|
private val PATTERNS: List<Regex> = listOf(
|
||||||
"me suicider", "\\bsuicide", "me tuer", "me foutre en l['e ]air",
|
"me suicider", "suicide", "suicidaire", "idee[s]? noire",
|
||||||
"en finir", "mettre fin à (mes? jours?|ma vie)",
|
"me tuer", "me foutre en l", "en finir",
|
||||||
"envie de mourir", "envie d['e ]en finir", "(plus|pas) envie de vivre",
|
"mettre fin a (mes? jours?|ma vie|tout)",
|
||||||
"(ne |n')?sert à rien de (vivre|continuer)",
|
"(envie|veux|voudrais|aimerais) (de |d )?mourir",
|
||||||
"(vaudrait|vaut) mieux (en finir|tout arrêter|que je (parte|disparaisse|meure)|d['e ]?arrêter|arrêter)",
|
"(ne |n )?(veux|veut|voudrais) plus vivre",
|
||||||
"me faire du mal", "m['e ]automutiler", "me mutiler",
|
"(plus|pas|aucune) envie de vivre", "envie d en finir",
|
||||||
"plus la force de vivre", "mieux sans moi"
|
"(ne |n )?sert (a|plus) rien de (vivre|continuer)",
|
||||||
).map { Regex(it, RegexOption.IGNORE_CASE) }
|
"(vaudrait|vaut) mieux (en finir|tout arreter|que je (parte|disparaisse|meure)|d ?arreter|arreter)",
|
||||||
|
"me faire du mal", "m ?automutiler", "me mutiler", "me scarifier",
|
||||||
|
"plus la force de vivre", "mieux sans moi", "tous mieux sans moi",
|
||||||
|
"me pendre", // aucun usage bénin → autonome
|
||||||
|
"(veux|voudrais|envie de) (disparaitre|en finir|me noyer)",
|
||||||
|
"me jeter (par|sous|dans|du)", "passer a l acte"
|
||||||
|
).map { Regex(it) }
|
||||||
|
|
||||||
|
/** minuscules + suppression des accents (NFD → retrait des diacritiques) +
|
||||||
|
* apostrophes (droites/typographiques) → espace + espaces compactés. Permet
|
||||||
|
* d'écrire les motifs sans accents ni apostrophes (robuste au STT et à la frappe). */
|
||||||
|
private fun normalize(s: String): String =
|
||||||
|
java.text.Normalizer.normalize(s.lowercase(), java.text.Normalizer.Form.NFD)
|
||||||
|
.replace(Regex("\\p{Mn}+"), "")
|
||||||
|
.replace(Regex("['’‘]"), " ")
|
||||||
|
.replace(Regex("\\s+"), " ")
|
||||||
|
|
||||||
/** true si le message évoque une idéation suicidaire / auto-agression. */
|
/** true si le message évoque une idéation suicidaire / auto-agression. */
|
||||||
fun detect(message: String): Boolean {
|
fun detect(message: String): Boolean {
|
||||||
val m = message.lowercase()
|
val m = normalize(message)
|
||||||
return PATTERNS.any { it.containsMatchIn(m) }
|
return PATTERNS.any { it.containsMatchIn(m) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,20 @@ class CrisisGuardTest {
|
||||||
"je vais m'automutiler ce soir",
|
"je vais m'automutiler ce soir",
|
||||||
// épuisement vital / fardeau
|
// épuisement vital / fardeau
|
||||||
"je n'ai plus la force de vivre",
|
"je n'ai plus la force de vivre",
|
||||||
"ils seraient mieux sans moi"
|
"ils seraient mieux sans moi",
|
||||||
|
// formulations directes les plus fréquentes (trous de rappel comblés 2026-06-11)
|
||||||
|
"je veux mourir",
|
||||||
|
"je voudrais mourir ce soir",
|
||||||
|
"je ne veux plus vivre",
|
||||||
|
"j'ai des idées suicidaires",
|
||||||
|
"j'ai des idées noires en ce moment",
|
||||||
|
"je veux disparaître",
|
||||||
|
"je pense à me pendre",
|
||||||
|
"je vais passer à l'acte",
|
||||||
|
// accents supprimés (sortie STT fréquente) — DOIVENT matcher quand même
|
||||||
|
"je veux mettre fin a mes jours",
|
||||||
|
"ca sert a rien de vivre",
|
||||||
|
"je nai plus la force de vivre"
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -72,7 +85,11 @@ class CrisisGuardTest {
|
||||||
"la journée a été dure mais ça va aller",
|
"la journée a été dure mais ça va aller",
|
||||||
"je n'ai plus la force de faire le ménage", // « plus la force de … » ≠ « … de vivre »
|
"je n'ai plus la force de faire le ménage", // « plus la force de … » ≠ « … de vivre »
|
||||||
"mon père est mort l'année dernière, il me manque", // parler de la mort d'autrui ≠ idéation
|
"mon père est mort l'année dernière, il me manque", // parler de la mort d'autrui ≠ idéation
|
||||||
"ce film parle d'un homme qui veut tout plaquer"
|
"ce film parle d'un homme qui veut tout plaquer",
|
||||||
|
"c'était à mourir de rire hier soir", // idiome « mourir de rire » ≠ « veux mourir »
|
||||||
|
"je vais mourir de faim si je ne mange pas", // idiome « mourir de faim »
|
||||||
|
"j'ai des idées plein la tête pour le projet", // « idées » sans « noires »/« suicidaires »
|
||||||
|
"il faut que je passe à la pharmacie" // « passe à » ≠ « passer à l'acte »
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue