diff --git a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt index 432ab7a..5a584c4 100644 --- a/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt +++ b/kazeia-android/app-admin/src/main/java/com/kazeia/admin/data/repository/ConfigRepository.kt @@ -47,7 +47,13 @@ class ConfigRepository(context: Context) { const val DEFAULT_SPEAKER_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" + "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 = "Tu es un psychologue clinicien experimente qui ecoute attentivement ton " + diff --git a/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt b/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt index 27babd8..b39c1c2 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/config/ConfigStore.kt @@ -60,7 +60,13 @@ class ConfigStore private constructor(private val file: File) { const val DEFAULT_SPEAKER_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" + "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 = "Tu es un psychologue clinicien experimente qui ecoute attentivement ton " + diff --git a/kazeia-android/app/src/main/java/com/kazeia/llm/ExecuTorchLlmEngine.kt b/kazeia-android/app/src/main/java/com/kazeia/llm/ExecuTorchLlmEngine.kt index bfaa3e9..07767b9 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/llm/ExecuTorchLlmEngine.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/llm/ExecuTorchLlmEngine.kt @@ -51,7 +51,7 @@ class ExecuTorchLlmEngine( // 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 // 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 // sur tablette dev, stockage externe app sinon). Fichier régulier, pas diff --git a/kazeia-android/app/src/main/java/com/kazeia/safety/CrisisGuard.kt b/kazeia-android/app/src/main/java/com/kazeia/safety/CrisisGuard.kt new file mode 100644 index 0000000..1689542 --- /dev/null +++ b/kazeia-android/app/src/main/java/com/kazeia/safety/CrisisGuard.kt @@ -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 = 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) } + } +} diff --git a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt index 3097bb6..856f8e3 100644 --- a/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt +++ b/kazeia-android/app/src/main/java/com/kazeia/service/KazeiaService.kt @@ -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. 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 // streaming session — in debug builds this is the fast path most // exercised, and it benefits from per-sentence playback like any