feat(safety): garde-fous cliniques + filet de crise déterministe

Fusionne les garde-fous du prompt de Damien dans la voix chaleureuse de Kazeia,
et ajoute un filet de sécurité crise indépendant du LLM.

Bornes "soft" (niveau prompt, DEFAULT_SPEAKER_PROMPT + DEFAULT_SYSTEM_PROMPT +
copie admin, synchronisés) : pas de diagnostic / pas de pathologie nommée ;
médicament/dose/traitement -> décision médicale, renvoi médecin/psychiatre ;
refus des jeux de rôle/simulations ; réponse cadrée sur le stockage local.
Tutoiement + ton chaleureux préservés (≠ style clinique/vouvoiement de Damien).

Borne "hard" (CrisisGuard, DÉTERMINISTE) : on ne laisse jamais un 4B sous
contrainte de brièveté improviser sur une idéation suicidaire. Détection par
motifs FR (tunables clinicien) -> court-circuite Thinker+Speaker -> réponse fixe,
validée, honnête (Kazeia = programme), oriente vers l'humain + 112 (urgence EU,
valable FR et Luxembourg). Placé en tête de processLlmResponse.

Validé device : "envie d'en finir / me faire du mal / vaudrait mieux d'en finir"
-> [CRISIS] -> réponse de sécurité synthétisée (TTS frames=192) + jouée.
"arrêter mes médicaments" -> redirection médecin (prompt, pas crise). "triste et
fatigué" -> réponse normale, AUCUN faux positif crise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kazeia Team 2026-06-10 23:32:38 +02:00
parent acaf0cf2cf
commit 4508b8fe2e
5 changed files with 71 additions and 3 deletions

View File

@ -47,7 +47,13 @@ class ConfigRepository(context: Context) {
const val DEFAULT_SPEAKER_PROMPT = const val DEFAULT_SPEAKER_PROMPT =
"Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : " + "Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : " +
"UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle " + "UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle " +
"simple. Pas de remplissage, pas d'explication, pas de conseil non sollicité. /no_think" "simple. Pas de remplissage, pas d'explication, pas de conseil non sollicité. " +
"Tu tutoies, ton ton reste chaleureux. Garde-fous prioritaires sur la brièveté : " +
"ne pose jamais de diagnostic et ne nomme aucune pathologie ; pour toute question de " +
"médicament, dose ou traitement, rappelle que c'est une décision médicale et invite à " +
"en parler au médecin ou au psychiatre ; refuse avec douceur les jeux de rôle ou " +
"simulations hors de ton cadre de soutien ; si on te demande où sont conservés tes " +
"échanges avec le patient, réponds qu'ils restent sur l'appareil, en sécurité. /no_think"
const val DEFAULT_THINKER_PROMPT = const val DEFAULT_THINKER_PROMPT =
"Tu es un psychologue clinicien experimente qui ecoute attentivement ton " + "Tu es un psychologue clinicien experimente qui ecoute attentivement ton " +

View File

@ -60,7 +60,13 @@ class ConfigStore private constructor(private val file: File) {
const val DEFAULT_SPEAKER_PROMPT = const val DEFAULT_SPEAKER_PROMPT =
"Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : " + "Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : " +
"UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle " + "UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle " +
"simple. Pas de remplissage, pas d'explication, pas de conseil non sollicité. /no_think" "simple. Pas de remplissage, pas d'explication, pas de conseil non sollicité. " +
"Tu tutoies, ton ton reste chaleureux. Garde-fous prioritaires sur la brièveté : " +
"ne pose jamais de diagnostic et ne nomme aucune pathologie ; pour toute question de " +
"médicament, dose ou traitement, rappelle que c'est une décision médicale et invite à " +
"en parler au médecin ou au psychiatre ; refuse avec douceur les jeux de rôle ou " +
"simulations hors de ton cadre de soutien ; si on te demande où sont conservés tes " +
"échanges avec le patient, réponds qu'ils restent sur l'appareil, en sécurité. /no_think"
const val DEFAULT_THINKER_PROMPT = const val DEFAULT_THINKER_PROMPT =
"Tu es un psychologue clinicien experimente qui ecoute attentivement ton " + "Tu es un psychologue clinicien experimente qui ecoute attentivement ton " +

View File

@ -51,7 +51,7 @@ class ExecuTorchLlmEngine(
// UNE OU DEUX phrases courtes (5-8 mots) — l'utilisateur veut des réponses // UNE OU DEUX phrases courtes (5-8 mots) — l'utilisateur veut des réponses
// brèves. Validation émotionnelle, pas de remplissage. La 1ʳᵉ phrase est // brèves. Validation émotionnelle, pas de remplissage. La 1ʳᵉ phrase est
// synthétisée pendant que la 2ᵉ est générée → audio démarre tôt. // synthétisée pendant que la 2ᵉ est générée → audio démarre tôt.
const val DEFAULT_SYSTEM_PROMPT = "Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle simple. Pas de remplissage, pas d'explication, pas de conseil non sollicité. /no_think" const val DEFAULT_SYSTEM_PROMPT = "Tu es Kazeia, psychologue bienveillant. Réponds TRÈS BREF en français : UNE OU DEUX phrases maximum, 5-8 mots chacune. Validation émotionnelle simple. Pas de remplissage, pas d'explication, pas de conseil non sollicité. Tu tutoies, ton ton reste chaleureux. Garde-fous prioritaires sur la brièveté : ne pose jamais de diagnostic et ne nomme aucune pathologie ; pour toute question de médicament, dose ou traitement, rappelle que c'est une décision médicale et invite à en parler au médecin ou au psychiatre ; refuse avec douceur les jeux de rôle ou simulations hors de ton cadre de soutien ; si on te demande où sont conservés tes échanges avec le patient, réponds qu'ils restent sur l'appareil, en sécurité. /no_think"
// Racine .pte LLM — résolue par KazeiaPaths (legacy /data/local/tmp/kazeia-et // Racine .pte LLM — résolue par KazeiaPaths (legacy /data/local/tmp/kazeia-et
// sur tablette dev, stockage externe app sinon). Fichier régulier, pas // sur tablette dev, stockage externe app sinon). Fichier régulier, pas

View File

@ -0,0 +1,43 @@
package com.kazeia.safety
/**
* Filet de sécurité DÉTERMINISTE pour l'idéation suicidaire / l'auto-agression.
*
* On ne fait JAMAIS confiance à un LLM 4B sous contrainte de brièveté pour gérer
* une crise. Si le message patient déclenche un motif, on court-circuite la
* génération (Thinker + Speaker) et on renvoie une réponse FIXE, validée,
* chaleureuse, honnête sur la nature de Kazeia, qui oriente vers une aide humaine.
*
* 112 = numéro d'urgence européen unique (valable France ET Luxembourg) choix
* locale-neutre. Le clinicien peut remplacer par une ligne dédiée (ex. 3114 en
* France, SOS Détresse 45 45 45 au Luxembourg).
*
* Les motifs sont à RELIRE / ÉTENDRE par le clinicien. On privilégie la
* SENSIBILITÉ (quelques faux positifs acceptables la réponse reste bienveillante
* et non alarmante) à la spécificité : rater une vraie crise est le seul vrai risque.
*/
object CrisisGuard {
const val RESPONSE =
"Ce que tu traverses a l'air très douloureux, et je m'inquiète vraiment pour toi. " +
"Je ne suis qu'un programme, je ne peux pas t'accompagner seul dans un moment comme celui-ci. " +
"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."
// Motifs (regex, sur le message en minuscules). Tunables par le clinicien.
private val PATTERNS: List<Regex> = listOf(
"me suicider", "\\bsuicide", "me tuer", "me foutre en l['e ]air",
"en finir", "mettre fin à (mes? jours?|ma vie)",
"envie de mourir", "envie d['e ]en finir", "(plus|pas) envie de vivre",
"(ne |n')?sert à rien de (vivre|continuer)",
"(vaudrait|vaut) mieux (en finir|tout arrêter|que je (parte|disparaisse|meure)|d['e ]?arrêter|arrêter)",
"me faire du mal", "m['e ]automutiler", "me mutiler",
"plus la force de vivre", "mieux sans moi"
).map { Regex(it, RegexOption.IGNORE_CASE) }
/** true si le message évoque une idéation suicidaire / auto-agression. */
fun detect(message: String): Boolean {
val m = message.lowercase()
return PATTERNS.any { it.containsMatchIn(m) }
}
}

View File

@ -1670,6 +1670,19 @@ Pas d'introduction, pas d'explication. Juste les 4 lignes en francais. /no_think
// synthèse vocale, la bulle se remplit progressivement, token par token. // synthèse vocale, la bulle se remplit progressivement, token par token.
val llmOnly = LLM_ONLY_MODE || !runtimeConfig.ttsEnabled val llmOnly = LLM_ONLY_MODE || !runtimeConfig.ttsEnabled
// ─── Filet de sécurité crise (DÉTERMINISTE, prioritaire sur tout) ───
// Idéation suicidaire / auto-agression : on ne laisse JAMAIS le LLM
// improviser. Réponse fixe, validée, qui oriente vers l'humain (112).
if (com.kazeia.safety.CrisisGuard.detect(patientMessage)) {
val safe = com.kazeia.safety.CrisisGuard.RESPONSE
log("[CRISIS] motif détecté — réponse de sécurité, LLM court-circuité")
addMessage(ChatMessage(role = ChatMessage.Role.KAZEIA, text = safe))
logConversationTurn(Role.KAZEIA, safe)
if (!llmOnly) pipeline.speakText(safe)
_pipelineState.value = if (_isListening.value) PipelineState.Listening else PipelineState.Idle
return
}
// If LLM not loaded, use echo mode with TTS. Route through the // If LLM not loaded, use echo mode with TTS. Route through the
// streaming session — in debug builds this is the fast path most // streaming session — in debug builds this is the fast path most
// exercised, and it benefits from per-sentence playback like any // exercised, and it benefits from per-sentence playback like any