Files
the-door/crisis/response.py
Timmy Time dcc931e946
All checks were successful
Smoke Test / smoke (push) Successful in 5s
fix: implement missing functions from rescued PR — test_rescue.py now passes (#27)
Auto-merged by Timmy overnight cycle
2026-04-13 14:05:07 +00:00

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."
)