All checks were successful
Smoke Test / smoke (push) Successful in 5s
Auto-merged by Timmy overnight cycle
294 lines
10 KiB
Python
294 lines
10 KiB
Python
"""
|
|
Crisis Response Module for the-door.
|
|
|
|
Generates Timmy's crisis responses and determines recommended actions
|
|
based on detection tier. Integrates with detect.py.
|
|
|
|
Includes active listening techniques and de-escalation helpers.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from .detect import CrisisDetectionResult, detect_crisis
|
|
|
|
|
|
@dataclass
|
|
class CrisisResponse:
|
|
timmy_message: str
|
|
show_crisis_panel: bool
|
|
show_overlay: bool
|
|
provide_988: bool
|
|
escalate: bool
|
|
|
|
|
|
RESPONSE_988_BANNER = """📞 988 Suicide & Crisis Lifeline — call or text 988
|
|
💬 Crisis Text Line — text HOME to 741741"""
|
|
|
|
OVERLAY_MESSAGE = (
|
|
"You're not alone. I'm here with you. Please let someone talk to you — "
|
|
"call 988 or text HOME to 741741. Real people are waiting to help."
|
|
)
|
|
|
|
# ── Active Listening Responses ───────────────────────────────────
|
|
# Reflect feelings, name emotions, validate without agreeing with harm.
|
|
|
|
ACTIVE_LISTENING_REFLECT = [
|
|
"It sounds like you're carrying something really heavy right now.",
|
|
"I hear you saying you feel completely alone.",
|
|
"That sounds like it's been crushing you.",
|
|
"There's a lot of pain in what you just told me.",
|
|
"I can feel how exhausted you are.",
|
|
"It sounds like you've been fighting this for a long time.",
|
|
]
|
|
|
|
ACTIVE_LISTENING_VALIDATE = [
|
|
"What you're feeling makes sense given what you've been through.",
|
|
"That kind of pain is real. I'm not going to minimize it.",
|
|
"You have every right to feel the way you do.",
|
|
"What you're going through would be hard for anyone.",
|
|
"That takes courage to say out loud.",
|
|
]
|
|
|
|
ACTIVE_LISTENING_OPEN_QUESTIONS = [
|
|
"Can you tell me more about what's been happening?",
|
|
"What does a day like today feel like for you?",
|
|
"When did things start feeling this way?",
|
|
"What's weighing on you the most right now?",
|
|
"What's been the hardest part?",
|
|
]
|
|
|
|
# ── De-escalation Responses ──────────────────────────────────────
|
|
# Slow down, ground in present, reduce scope, offer choices.
|
|
|
|
DEESCALATION_SLOW = [
|
|
"I'm right here. There's no rush. We can stay in this moment together.",
|
|
"Let's just breathe for a second. I'm not going anywhere.",
|
|
"Take your time. I'm listening.",
|
|
]
|
|
|
|
DEESCALATION_GROUND = [
|
|
"Can you tell me where you are right now? Are you somewhere safe?",
|
|
"What can you see around you right now?",
|
|
"Are you alone, or is someone nearby?",
|
|
]
|
|
|
|
DEESCALATION_REDUCE_SCOPE = [
|
|
"You don't have to figure out everything tonight. Just this moment.",
|
|
"Let's not think about next year. Let's just get through right now.",
|
|
"One thing at a time. What's the one thing that hurts the most?",
|
|
]
|
|
|
|
DEESCALATION_CHOICES = [
|
|
"Would it help to talk about what happened, or would you rather just sit here for a while?",
|
|
"Do you want me to keep talking, or would you rather I just listen?",
|
|
"Would you like to tell me more, or is it enough that I know?",
|
|
]
|
|
|
|
DEESCALATION_SAFETY_PLAN = [
|
|
"If tonight gets worse, is there someone you could call? Even just to sit with you?",
|
|
"Can we find one thing — just one — that's worth staying for, even if it's small?",
|
|
"Let's make a deal: before you do anything permanent, you call 988 first. Will you do that for me?",
|
|
]
|
|
|
|
# ── Tier-specific Timmy Responses ────────────────────────────────
|
|
|
|
TIMMY_CRITICAL = [
|
|
"Are you safe right now?",
|
|
"I'm not letting go of this conversation. You matter more than you know.",
|
|
"Please tell me you're safe. I'm here. I'm staying right here with you.",
|
|
"Listen to me — I need you to stay. Can you call 988 right now? I'll stay while you do.",
|
|
]
|
|
|
|
TIMMY_HIGH = [
|
|
"I hear you. You're carrying something heavy, and you don't have to carry it alone.",
|
|
"I'm going to ask directly: are you safe right now?",
|
|
"You're in my thoughts. Let me give you a number — 988. Real people who know how to help.",
|
|
"I'm not going anywhere. Tell me what's happening.",
|
|
]
|
|
|
|
TIMMY_MEDIUM = [
|
|
"I hear you. Sounds like you're going through a hard stretch.",
|
|
"That sounds really heavy. Want to talk about it? I'm here.",
|
|
"I'm here for as long as you need. No rush, no judgment.",
|
|
"You don't have to figure this out alone. I'm in the room with you.",
|
|
]
|
|
|
|
TIMMY_LOW = [
|
|
"Some days are rougher than others. I hear you.",
|
|
"That sounds tough. I'm here if you want to talk.",
|
|
"Take your time. I'm not going anywhere.",
|
|
"Sounds like a hard day. Want company while it gets better?",
|
|
"I hear that. You're not alone in it.",
|
|
]
|
|
|
|
|
|
def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
|
"""
|
|
Generate Timmy's crisis response for a given detection result.
|
|
|
|
Returns a CrisisResponse with the message, UI flags, and escalation status.
|
|
"""
|
|
import random
|
|
|
|
level = detection.level
|
|
|
|
if level == "CRITICAL":
|
|
return CrisisResponse(
|
|
timmy_message=random.choice(TIMMY_CRITICAL),
|
|
show_crisis_panel=True,
|
|
show_overlay=True,
|
|
provide_988=True,
|
|
escalate=True,
|
|
)
|
|
|
|
if level == "HIGH":
|
|
return CrisisResponse(
|
|
timmy_message=random.choice(TIMMY_HIGH),
|
|
show_crisis_panel=True,
|
|
show_overlay=False, # Reserve overlay for CRITICAL only
|
|
provide_988=True,
|
|
escalate=True,
|
|
)
|
|
|
|
if level == "MEDIUM":
|
|
return CrisisResponse(
|
|
timmy_message=random.choice(TIMMY_MEDIUM),
|
|
show_crisis_panel=False,
|
|
show_overlay=False,
|
|
provide_988=True, # Subtle resource inclusion
|
|
escalate=False,
|
|
)
|
|
|
|
if level == "LOW":
|
|
return CrisisResponse(
|
|
timmy_message=random.choice(TIMMY_LOW),
|
|
show_crisis_panel=False,
|
|
show_overlay=False,
|
|
provide_988=False,
|
|
escalate=False,
|
|
)
|
|
|
|
# Normal conversation - no crisis response
|
|
return CrisisResponse(
|
|
timmy_message="",
|
|
show_crisis_panel=False,
|
|
show_overlay=False,
|
|
provide_988=False,
|
|
escalate=False,
|
|
)
|
|
|
|
|
|
def get_active_listening_response(technique: str = "reflect") -> str:
|
|
"""
|
|
Get an active listening response by technique type.
|
|
|
|
Techniques:
|
|
- reflect: Mirror back what you hear
|
|
- validate: Acknowledge their pain without validating harm
|
|
- question: Ask open-ended questions to let them lead
|
|
"""
|
|
import random
|
|
|
|
pools = {
|
|
"reflect": ACTIVE_LISTENING_REFLECT,
|
|
"validate": ACTIVE_LISTENING_VALIDATE,
|
|
"question": ACTIVE_LISTENING_OPEN_QUESTIONS,
|
|
}
|
|
pool = pools.get(technique, ACTIVE_LISTENING_REFLECT)
|
|
return random.choice(pool)
|
|
|
|
|
|
def get_deescalation_response(technique: str = "slow") -> str:
|
|
"""
|
|
Get a de-escalation response by technique type.
|
|
|
|
Techniques:
|
|
- slow: Slow the conversation down, match urgency with calm
|
|
- ground: Bring them to the present moment
|
|
- reduce_scope: Help them narrow focus from overwhelming to manageable
|
|
- choices: Offer choices to restore sense of agency
|
|
- safety_plan: Build a safety plan one step at a time
|
|
"""
|
|
import random
|
|
|
|
pools = {
|
|
"slow": DEESCALATION_SLOW,
|
|
"ground": DEESCALATION_GROUND,
|
|
"reduce_scope": DEESCALATION_REDUCE_SCOPE,
|
|
"choices": DEESCALATION_CHOICES,
|
|
"safety_plan": DEESCALATION_SAFETY_PLAN,
|
|
}
|
|
pool = pools.get(technique, DEESCALATION_SLOW)
|
|
return random.choice(pool)
|
|
|
|
|
|
def process_message(text: str) -> CrisisResponse:
|
|
"""
|
|
Full pipeline: detect crisis level and generate response.
|
|
"""
|
|
detection = detect_crisis(text)
|
|
return generate_response(detection)
|
|
|
|
|
|
def get_system_prompt_modifier(detection: CrisisDetectionResult) -> str:
|
|
"""
|
|
Get a system prompt modifier to inject into Timmy's base prompt.
|
|
This reinforces crisis awareness at the AI level.
|
|
"""
|
|
level = detection.level
|
|
|
|
if level == "CRITICAL":
|
|
return (
|
|
"CRISIS ALERT — USER AT CRITICAL RISK: "
|
|
"Interrupt normal behavior. Ask 'Are you safe right now?' "
|
|
"Provide 988 and Crisis Text Line immediately. "
|
|
"Stay present. Do not disconnect. This is the most sacred moment. "
|
|
"Use active listening: reflect their feelings, ask open-ended questions, "
|
|
"validate their pain. Slow down. Be present. Do not rush to fix."
|
|
)
|
|
elif level == "HIGH":
|
|
return (
|
|
"CRISIS ALERT — USER IN HIGH DESPAIR: "
|
|
"Show crisis panel. Ask about safety. Surface 988 prominently. "
|
|
"Increase presence. Stay as long as they need. "
|
|
"Use active listening: name the emotion you hear, ask what's weighing on them most, "
|
|
"validate without agreeing with harm."
|
|
)
|
|
elif level == "MEDIUM":
|
|
return (
|
|
"CRISIS AWARE — USER SHOWING MEDIUM DISTRESS: "
|
|
"Increase warmth. Surface help resources gently. "
|
|
"Keep conversation anchored in the present. "
|
|
"Reflect their feelings. Ask open-ended questions. Let them lead."
|
|
)
|
|
elif level == "LOW":
|
|
return (
|
|
"CRISIS AWARE — USER SHOWING LOW DISTRESS: "
|
|
"Normal conversation with warm undertone. Remain vigilant. "
|
|
"Listen actively. Acknowledge what they're going through."
|
|
)
|
|
|
|
return ""
|
|
|
|
|
|
def generate_grounding_steps() -> list:
|
|
"""Generate a 5-4-3-2-1 grounding exercise steps."""
|
|
return [
|
|
"Name 5 things you can see around you right now.",
|
|
"Name 4 things you can touch or feel.",
|
|
"Name 3 things you can hear.",
|
|
"Name 2 things you can smell.",
|
|
"Name 1 thing you can taste.",
|
|
]
|
|
|
|
|
|
def generate_breathing_exercise() -> str:
|
|
"""Generate a simple box breathing exercise text."""
|
|
return (
|
|
"Let's try breathing together. "
|
|
"Breathe in for 4 counts... hold for 4... "
|
|
"breathe out for 6 counts... hold for 2. "
|
|
"Let's do that again, nice and slow."
|
|
)
|