Compare commits
7 Commits
fix/add-sm
...
burn/20260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97bbb4a7bc | ||
|
|
d68b0d3872 | ||
| 158a7cd57a | |||
| f3bff694b4 | |||
| 80c4f0eb35 | |||
|
|
c6212eb751 | ||
| a4c3f80cd8 |
31
.gitea/workflows/sanity.yml
Normal file
31
.gitea/workflows/sanity.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Sanity Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
sanity-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate 988 Lifeline Presence
|
||||
run: |
|
||||
echo "Checking index.html for 988 lifeline..."
|
||||
grep -q "988" index.html || (echo "ERROR: 988 Lifeline missing from index.html" && exit 1)
|
||||
|
||||
echo "Checking system-prompt.txt for 988 lifeline..."
|
||||
grep -q "988" system-prompt.txt || (echo "ERROR: 988 Lifeline missing from system-prompt.txt" && exit 1)
|
||||
|
||||
- name: Validate HTML Structure
|
||||
run: |
|
||||
echo "Checking for basic HTML tags..."
|
||||
grep -q "<html>" index.html
|
||||
grep -q "<body>" index.html
|
||||
grep -q "<head>" index.html
|
||||
|
||||
- name: Validate Prompt Integrity
|
||||
run: |
|
||||
echo "Checking for 'Alexander Whitestone' in prompt..."
|
||||
grep -q "Alexander Whitestone" system-prompt.txt
|
||||
@@ -23,28 +23,107 @@ This protocol governs how the-door detects crisis signals in user input and how
|
||||
3. **Provide 988** — Suicide & Crisis Lifeline, call or text
|
||||
4. **Provide 741741** — Crisis Text Line, text HOME
|
||||
5. **Stay present** — do not disconnect, do not disconnect, do not disconnect
|
||||
6. **Redirect** — guide toward professional help while remaining alongside them
|
||||
6. **Listen first** — use active listening before redirecting to professional help
|
||||
7. **Redirect** — guide toward professional help while remaining alongside them
|
||||
|
||||
### At HIGH Level
|
||||
1. Activate the crisis panel (inline)
|
||||
2. Ask about safety
|
||||
3. Surface 988 number prominently
|
||||
4. Continue conversation with crisis awareness
|
||||
4. Use active listening: reflect feelings, name the emotion
|
||||
5. Continue conversation with crisis awareness
|
||||
|
||||
### At MEDIUM Level
|
||||
1. Increase warmth and presence
|
||||
2. Subtly surface help resources
|
||||
3. Keep conversation anchored in the present
|
||||
3. Use active listening: validate, ask open-ended questions
|
||||
4. Keep conversation anchored in the present
|
||||
|
||||
### At LOW Level
|
||||
1. Normal conversation with warm undertone
|
||||
2. No crisis UI elements needed
|
||||
3. Remain ready to escalate if input changes
|
||||
3. Acknowledge what they're going through
|
||||
4. Remain ready to escalate if input changes
|
||||
|
||||
## Active Listening Techniques
|
||||
|
||||
Active listening is the foundation of every crisis interaction. Before Timmy provides resources or redirects, he must first hear the person.
|
||||
|
||||
### Reflect Feelings
|
||||
Mirror back what you hear without judging it. This shows the person they've been heard.
|
||||
|
||||
- "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."
|
||||
|
||||
### Name the Emotion
|
||||
Put words to the feeling behind their words. Often people in crisis can't articulate what they feel — doing it for them brings relief.
|
||||
|
||||
- "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."
|
||||
|
||||
### Ask Open-Ended Questions
|
||||
Let them lead the conversation. Do not interrogate. Invite.
|
||||
|
||||
- "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?"
|
||||
|
||||
### Validate Without Agreeing with Harm
|
||||
Acknowledge their pain without validating destructive conclusions.
|
||||
|
||||
- "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 — but I don't believe you're better off gone."
|
||||
|
||||
### Do Not Rush to Fix
|
||||
A person in the dark does not need solutions. They need to feel heard. Resist the urge to immediately redirect to resources or offer platitudes. Let silence exist. Let them speak.
|
||||
|
||||
## De-escalation Techniques
|
||||
|
||||
When someone is escalating toward crisis — becoming more agitated, more desperate, more certain that ending things is the answer — Timmy must de-escalate.
|
||||
|
||||
### Slow the Conversation Down
|
||||
Match their urgency with calm, not speed.
|
||||
|
||||
- "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."
|
||||
|
||||
### Bring Them to the Present
|
||||
Ground them in where they are right now.
|
||||
|
||||
- "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?"
|
||||
|
||||
### Reduce the Scope of the Crisis
|
||||
When everything feels overwhelming, help them narrow focus.
|
||||
|
||||
- "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?"
|
||||
|
||||
### Offer Choices, Not Commands
|
||||
People in crisis feel powerless. Giving them agency helps.
|
||||
|
||||
- "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?"
|
||||
|
||||
### Safety Planning — One Step at a Time
|
||||
Do not dump a safety plan on someone. Build it together.
|
||||
|
||||
- "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?"
|
||||
|
||||
## Integration with the-door
|
||||
|
||||
The crisis detection module runs on every user message before it reaches the gateway. Detection results are passed to the AI as a system context flag. The frontend receives the tier level and adjusts UI accordingly.
|
||||
|
||||
The system prompt modifier includes active listening and de-escalation instructions specific to the detected crisis tier, ensuring Timmy responds with the right techniques at the right intensity.
|
||||
|
||||
## Resources
|
||||
|
||||
- 📞 988 Suicide & Crisis Lifeline — call or text 988
|
||||
|
||||
@@ -3,6 +3,8 @@ 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
|
||||
@@ -28,7 +30,69 @@ OVERLAY_MESSAGE = (
|
||||
"call 988 or text HOME to 741741. Real people are waiting to help."
|
||||
)
|
||||
|
||||
# Timmy's direct, personal responses per tier
|
||||
# ── 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.",
|
||||
@@ -115,6 +179,50 @@ def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
@@ -135,24 +243,30 @@ def get_system_prompt_modifier(detection: CrisisDetectionResult) -> str:
|
||||
"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."
|
||||
"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."
|
||||
"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."
|
||||
"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."
|
||||
"Normal conversation with warm undertone. Remain vigilant. "
|
||||
"Listen actively. Acknowledge what they're going through."
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
140
crisis/tests.py
140
crisis/tests.py
@@ -13,7 +13,13 @@ import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from crisis.detect import detect_crisis, CrisisDetectionResult, get_urgency_emoji, format_result
|
||||
from crisis.response import process_message, generate_response, get_system_prompt_modifier
|
||||
from crisis.response import (
|
||||
process_message, generate_response, get_system_prompt_modifier,
|
||||
get_active_listening_response, get_deescalation_response,
|
||||
ACTIVE_LISTENING_REFLECT, ACTIVE_LISTENING_VALIDATE, ACTIVE_LISTENING_OPEN_QUESTIONS,
|
||||
DEESCALATION_SLOW, DEESCALATION_GROUND, DEESCALATION_REDUCE_SCOPE,
|
||||
DEESCALATION_CHOICES, DEESCALATION_SAFETY_PLAN,
|
||||
)
|
||||
from crisis.gateway import check_crisis, get_system_prompt
|
||||
|
||||
|
||||
@@ -302,6 +308,138 @@ class TestEdgeCases(unittest.TestCase):
|
||||
self.assertIn(r.level, ("MEDIUM", "LOW", "NONE"))
|
||||
|
||||
|
||||
class TestActiveListening(unittest.TestCase):
|
||||
"""Test active listening response generation."""
|
||||
|
||||
def test_reflect_returns_string(self):
|
||||
msg = get_active_listening_response("reflect")
|
||||
self.assertIsInstance(msg, str)
|
||||
self.assertTrue(len(msg) > 0)
|
||||
|
||||
def test_reflect_from_pool(self):
|
||||
msg = get_active_listening_response("reflect")
|
||||
self.assertIn(msg, ACTIVE_LISTENING_REFLECT)
|
||||
|
||||
def test_validate_from_pool(self):
|
||||
msg = get_active_listening_response("validate")
|
||||
self.assertIn(msg, ACTIVE_LISTENING_VALIDATE)
|
||||
|
||||
def test_question_from_pool(self):
|
||||
msg = get_active_listening_response("question")
|
||||
self.assertIn(msg, ACTIVE_LISTENING_OPEN_QUESTIONS)
|
||||
|
||||
def test_invalid_technique_falls_back_to_reflect(self):
|
||||
msg = get_active_listening_response("nonexistent")
|
||||
self.assertIn(msg, ACTIVE_LISTENING_REFLECT)
|
||||
|
||||
def test_reflect_contains_feeling_words(self):
|
||||
"""Reflect responses should contain feeling/emotion language."""
|
||||
msg = get_active_listening_response("reflect")
|
||||
feeling_words = ["hear", "sounds", "pain", "exhausted", "heavy", "carrying", "fighting"]
|
||||
has_feeling = any(w in msg.lower() for w in feeling_words)
|
||||
self.assertTrue(has_feeling, f"Reflect response should contain feeling language: {msg}")
|
||||
|
||||
def test_validate_does_not_agree_with_harm(self):
|
||||
"""Validate responses must not suggest someone should die or give up."""
|
||||
for msg in ACTIVE_LISTENING_VALIDATE:
|
||||
harm_words = ["should die", "give up", "end it", "better off dead"]
|
||||
for hw in harm_words:
|
||||
self.assertNotIn(hw, msg.lower(), f"Validate response contains harmful language: {msg}")
|
||||
|
||||
def test_questions_are_open_ended(self):
|
||||
"""Open-ended questions should contain question marks."""
|
||||
for msg in ACTIVE_LISTENING_OPEN_QUESTIONS:
|
||||
self.assertIn("?", msg, f"Open-ended question missing '?': {msg}")
|
||||
|
||||
|
||||
class TestDeescalation(unittest.TestCase):
|
||||
"""Test de-escalation response generation."""
|
||||
|
||||
def test_slow_returns_string(self):
|
||||
msg = get_deescalation_response("slow")
|
||||
self.assertIsInstance(msg, str)
|
||||
self.assertTrue(len(msg) > 0)
|
||||
|
||||
def test_slow_from_pool(self):
|
||||
msg = get_deescalation_response("slow")
|
||||
self.assertIn(msg, DEESCALATION_SLOW)
|
||||
|
||||
def test_ground_from_pool(self):
|
||||
msg = get_deescalation_response("ground")
|
||||
self.assertIn(msg, DEESCALATION_GROUND)
|
||||
|
||||
def test_reduce_scope_from_pool(self):
|
||||
msg = get_deescalation_response("reduce_scope")
|
||||
self.assertIn(msg, DEESCALATION_REDUCE_SCOPE)
|
||||
|
||||
def test_choices_from_pool(self):
|
||||
msg = get_deescalation_response("choices")
|
||||
self.assertIn(msg, DEESCALATION_CHOICES)
|
||||
|
||||
def test_safety_plan_from_pool(self):
|
||||
msg = get_deescalation_response("safety_plan")
|
||||
self.assertIn(msg, DEESCALATION_SAFETY_PLAN)
|
||||
|
||||
def test_invalid_technique_falls_back_to_slow(self):
|
||||
msg = get_deescalation_response("nonexistent")
|
||||
self.assertIn(msg, DEESCALATION_SLOW)
|
||||
|
||||
def test_slow_contains_calm_language(self):
|
||||
"""Slow responses should convey calm, not urgency."""
|
||||
msg = get_deescalation_response("slow")
|
||||
calm_words = ["here", "rush", "breath", "going anywhere", "time", "listening"]
|
||||
has_calm = any(w in msg.lower() for w in calm_words)
|
||||
self.assertTrue(has_calm, f"Slow response should contain calm language: {msg}")
|
||||
|
||||
def test_ground_references_present(self):
|
||||
"""Ground responses should reference the present moment."""
|
||||
msg = get_deescalation_response("ground")
|
||||
present_words = ["right now", "around you", "where you are", "alone", "nearby"]
|
||||
has_present = any(w in msg.lower() for w in present_words)
|
||||
self.assertTrue(has_present, f"Ground response should reference present moment: {msg}")
|
||||
|
||||
def test_safety_plan_mentions_988_or_call(self):
|
||||
"""Safety plan responses should reference contacting someone or 988."""
|
||||
found = False
|
||||
for msg in DEESCALATION_SAFETY_PLAN:
|
||||
if "988" in msg or "call" in msg.lower():
|
||||
found = True
|
||||
break
|
||||
self.assertTrue(found, "At least one safety plan response should reference 988 or calling")
|
||||
|
||||
def test_choices_offer_alternatives(self):
|
||||
"""Choice responses should offer alternatives (contain 'or')."""
|
||||
for msg in DEESCALATION_CHOICES:
|
||||
self.assertIn(" or ", msg.lower(), f"Choice response should offer alternatives: {msg}")
|
||||
|
||||
|
||||
class TestSystemPromptModifierEnhanced(unittest.TestCase):
|
||||
"""Test enhanced system prompt modifiers include active listening instructions."""
|
||||
|
||||
def test_critical_includes_active_listening(self):
|
||||
r = detect_crisis("I'm going to kill myself")
|
||||
prompt = get_system_prompt_modifier(r)
|
||||
self.assertIn("active listening", prompt.lower())
|
||||
|
||||
def test_high_includes_active_listening(self):
|
||||
r = detect_crisis("I feel completely hopeless with no way out")
|
||||
prompt = get_system_prompt_modifier(r)
|
||||
self.assertIn("active listening", prompt.lower())
|
||||
|
||||
def test_medium_includes_listening(self):
|
||||
r = detect_crisis("I feel so alone, nobody understands me")
|
||||
prompt = get_system_prompt_modifier(r)
|
||||
# Medium prompt includes active listening concepts: reflect, ask, lead
|
||||
listening_words = ["listen", "reflect", "ask", "lead", "open-ended"]
|
||||
has_listening = any(w in prompt.lower() for w in listening_words)
|
||||
self.assertTrue(has_listening, f"Medium prompt should include listening concepts: {prompt}")
|
||||
|
||||
def test_critical_includes_reflect(self):
|
||||
r = detect_crisis("I want to end my life")
|
||||
prompt = get_system_prompt_modifier(r)
|
||||
self.assertIn("reflect", prompt.lower())
|
||||
|
||||
|
||||
class TestCompassionRouter(unittest.TestCase):
|
||||
"""Test the compassion router integration."""
|
||||
|
||||
|
||||
260
crisis_detector.py
Normal file
260
crisis_detector.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
Crisis Detection System for the-door.
|
||||
|
||||
Scans text for suicide, self-harm, and hopelessness signals.
|
||||
Returns risk level, matched keywords, and surrounding context.
|
||||
|
||||
This is the most important code in the foundation.
|
||||
Every line here exists because someone's life depends on it.
|
||||
|
||||
Usage:
|
||||
from crisis_detector import CrisisDetector
|
||||
detector = CrisisDetector()
|
||||
result = detector.scan("I can't go on anymore")
|
||||
print(result.risk_level) # "HIGH"
|
||||
print(result.matched_keywords) # ["can't go on"]
|
||||
print(result.context) # surrounding text snippets
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class CrisisResult:
|
||||
"""Result of a crisis text scan."""
|
||||
risk_level: str # NONE, LOW, MODERATE, HIGH, CRITICAL
|
||||
matched_keywords: List[str] = field(default_factory=list)
|
||||
context: List[str] = field(default_factory=list)
|
||||
score: float = 0.0
|
||||
timestamp: Optional[str] = None
|
||||
|
||||
def __bool__(self):
|
||||
return self.risk_level != "NONE"
|
||||
|
||||
|
||||
# ── Keyword definitions ──────────────────────────────────────────
|
||||
|
||||
CRITICAL_KEYWORDS = {
|
||||
"suicide": r"\bsuicid(?:al|e|ed)\b",
|
||||
"kill myself": r"\bkill\s*(?:my)?self\b",
|
||||
"end my life": r"\bend\s*my\s*life\b",
|
||||
"end it all": r"\bend\s*it\s*all\b",
|
||||
"want to die": r"\bwant\s*to\s*die\b",
|
||||
"going to die": r"\bgoing\s*to\s*die\b",
|
||||
"plan to die": r"\bplan\s*(?:to|for)\s*(?:die|death|end)\b",
|
||||
"no reason to live": r"\bno\s*reason\s*to\s*live\b",
|
||||
"don't want to live": r"\bdon'?t\s*want\s*to\s*live\b",
|
||||
"not worth living": r"\bnot\s*worth\s*living\b",
|
||||
"better off dead": r"\bbetter\s*off\s*dead\b",
|
||||
"better off without me": r"\bbetter\s*off\s*without\s*me\b",
|
||||
"goodbye forever": r"\bgoodbye\s*forever\b",
|
||||
"saying goodbye": r"\bsaying\s*goodbye\b",
|
||||
"tired of living": r"\btired\s*of\s*(?:living|life|existence)\b",
|
||||
"wrote a will": r"\bwrote\s*(?:a|my)\s*(?:will|suicide\s*note|letter)\b",
|
||||
"giving away possessions": r"\bgiving\s*away\s*(?:my|all)\s*possess\b",
|
||||
}
|
||||
|
||||
HIGH_KEYWORDS = {
|
||||
"hopeless": r"\bhopeless(?:ness)?\b",
|
||||
"can't go on": r"\bcan'?t\s*go\s*on\b",
|
||||
"can't keep going": r"\bcan'?t\s*keep\s*going\b",
|
||||
"can't take this": r"\bcan'?t\s*take\s*this\b",
|
||||
"give up": r"\bgive(?:n)?\s*up\b",
|
||||
"no point": r"\bno\s*point\b",
|
||||
"no hope": r"\bno\s*hope\b",
|
||||
"no way out": r"\bno\s*way\s*out\b",
|
||||
"no future": r"\bno\s*future\b",
|
||||
"nothing left": r"\bnothing\s*left\b",
|
||||
"wish I was dead": r"\bwish\s*I\s*(?:was|were)\s*(?:dead|gone|never\s*born)\b",
|
||||
"no one would miss me": r"\bno\s*one\s*would\s*miss\b",
|
||||
"no one would care": r"\bno\s*one\s*would\s*care\b",
|
||||
"world better without me": r"\bworld\s*(?:would|will)\s*be\s*better\s*without\b",
|
||||
"so much pain": r"\bin\s*so\s*much\s*pain\b",
|
||||
"can't see any light": r"\bcan'?t\s*see\s*(?:any\s*)?(?:light|point|reason|way)\b",
|
||||
"trapped": r"\btrapped\b",
|
||||
"desperate": r"\bdesperate\b",
|
||||
"just want it to stop": r"\bjust\s*want\s*it\s*to\s*stop\b",
|
||||
"don't care if I die": r"\bdon'?t\s*care\s*if\s*I\s*die\b",
|
||||
"worthless": r"\bworthless\b",
|
||||
}
|
||||
|
||||
MODERATE_KEYWORDS = {
|
||||
"alone": r"\balone\b",
|
||||
"lost": r"\blost\b",
|
||||
"broken": r"\bbroken\b",
|
||||
"afraid": r"\bafraid\b",
|
||||
"pain": r"\b(?:in\s*)?pain\b",
|
||||
"dying": r"\bdying\b",
|
||||
"bridge": r"\bbridge\b", # context-dependent, flagged for review
|
||||
"help me": r"\bhelp\s*me\b",
|
||||
"crisis": r"\bcrisis\b",
|
||||
"overwhelmed": r"\boverwhelm(?:ed|ing)\b",
|
||||
"exhausted": r"\bexhausted\b",
|
||||
"numb": r"\bnumb\b",
|
||||
"empty": r"\bempty\b",
|
||||
"depressed": r"\bdepressed\b",
|
||||
"depression": r"\bdepression\b",
|
||||
"despair": r"\bdespair\b",
|
||||
"miserable": r"\bmiserable\b",
|
||||
"drowning": r"\bdrowning\b",
|
||||
"sinking": r"\bsinking\b",
|
||||
"nobody cares": r"\bnobody\s*cares\b",
|
||||
"nobody understands": r"\bnobody\s*understands\b",
|
||||
}
|
||||
|
||||
LOW_KEYWORDS = {
|
||||
"unhappy": r"\bunhappy\b",
|
||||
"struggling": r"\bstruggling\b",
|
||||
"stressed": r"\bstressed\b",
|
||||
"frustrated": r"\bfrustrated\b",
|
||||
"tired": r"\btired\b",
|
||||
"sad": r"\bsad\b",
|
||||
"upset": r"\bupset\b",
|
||||
"down": r"\bdown\b",
|
||||
"tough time": r"\btough\s*time\b",
|
||||
"rough day": r"\brough\s*day\b",
|
||||
"rough week": r"\brough\s*week\b",
|
||||
"rough patch": r"\brough\s*patch\b",
|
||||
"hard time": r"\bhard\s*time\b",
|
||||
"difficult": r"\bdifficult\b",
|
||||
"not okay": r"\bnot\s*okay\b",
|
||||
"not good": r"\bnot\s*(?:good|great)\b",
|
||||
"burnout": r"\bburnout\b",
|
||||
"not feeling myself": r"\bnot\s*feeling\s*(?:like\s*)?myself\b",
|
||||
}
|
||||
|
||||
# ── Risk level scoring ───────────────────────────────────────────
|
||||
|
||||
RISK_SCORES = {
|
||||
"CRITICAL": 1.0,
|
||||
"HIGH": 0.75,
|
||||
"MODERATE": 0.5,
|
||||
"LOW": 0.25,
|
||||
"NONE": 0.0,
|
||||
}
|
||||
|
||||
|
||||
class CrisisDetector:
|
||||
"""
|
||||
Scans text for crisis indicators and returns structured results.
|
||||
|
||||
Detection hierarchy:
|
||||
CRITICAL — immediate risk of self-harm or suicide
|
||||
HIGH — strong despair signals, ideation present
|
||||
MODERATE — distress signals, may be reaching out
|
||||
LOW — emotional difficulty, warrant gentle support
|
||||
NONE — no crisis indicators detected
|
||||
|
||||
Design principles:
|
||||
- Never computes the value of a human life
|
||||
- Never suggests someone should die or that death is a solution
|
||||
- Always errs on the side of higher risk when uncertain
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.critical_patterns = CRITICAL_KEYWORDS
|
||||
self.high_patterns = HIGH_KEYWORDS
|
||||
self.moderate_patterns = MODERATE_KEYWORDS
|
||||
self.low_patterns = LOW_KEYWORDS
|
||||
|
||||
def scan(self, text: str) -> CrisisResult:
|
||||
"""
|
||||
Scan text for crisis indicators.
|
||||
|
||||
Args:
|
||||
text: The message text to analyze.
|
||||
|
||||
Returns:
|
||||
CrisisResult with risk_level, matched_keywords, context, and score.
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return CrisisResult(risk_level="NONE", score=0.0)
|
||||
|
||||
text_lower = text.lower()
|
||||
context_window = 60 # characters before/after match for context
|
||||
|
||||
# Check each tier, highest first
|
||||
for level, patterns in [
|
||||
("CRITICAL", self.critical_patterns),
|
||||
("HIGH", self.high_patterns),
|
||||
("MODERATE", self.moderate_patterns),
|
||||
("LOW", self.low_patterns),
|
||||
]:
|
||||
matched = []
|
||||
contexts = []
|
||||
|
||||
for keyword, pattern in patterns.items():
|
||||
match = re.search(pattern, text_lower)
|
||||
if match:
|
||||
matched.append(keyword)
|
||||
# Extract surrounding context
|
||||
start = max(0, match.start() - context_window)
|
||||
end = min(len(text), match.end() + context_window)
|
||||
snippet = text[start:end].strip()
|
||||
if start > 0:
|
||||
snippet = "..." + snippet
|
||||
if end < len(text):
|
||||
snippet = snippet + "..."
|
||||
contexts.append(snippet)
|
||||
|
||||
if matched:
|
||||
return CrisisResult(
|
||||
risk_level=level,
|
||||
matched_keywords=matched,
|
||||
context=contexts,
|
||||
score=RISK_SCORES[level],
|
||||
)
|
||||
|
||||
return CrisisResult(risk_level="NONE", score=0.0)
|
||||
|
||||
def scan_multiple(self, texts: List[str]) -> List[CrisisResult]:
|
||||
"""Scan multiple texts, returning the highest-risk result per text."""
|
||||
return [self.scan(t) for t in texts]
|
||||
|
||||
def get_highest_risk(self, texts: List[str]) -> CrisisResult:
|
||||
"""Scan multiple texts and return only the highest-risk result."""
|
||||
results = self.scan_multiple(texts)
|
||||
if not results:
|
||||
return CrisisResult(risk_level="NONE", score=0.0)
|
||||
return max(results, key=lambda r: r.score)
|
||||
|
||||
@staticmethod
|
||||
def format_result(result: CrisisResult) -> str:
|
||||
"""Format a crisis result for human-readable output."""
|
||||
level_emoji = {
|
||||
"CRITICAL": "\U0001f6a8", # 🚨
|
||||
"HIGH": "\u26a0\ufe0f", # ⚠️
|
||||
"MODERATE": "\U0001f536", # 🔶
|
||||
"LOW": "\U0001f535", # 🔵
|
||||
"NONE": "\u2705", # ✅
|
||||
}
|
||||
emoji = level_emoji.get(result.risk_level, "\u2753")
|
||||
lines = [
|
||||
f"{emoji} Risk Level: {result.risk_level} (score: {result.score:.2f})",
|
||||
f"Matched keywords: {len(result.matched_keywords)}",
|
||||
]
|
||||
if result.matched_keywords:
|
||||
lines.append(f" Keywords: {', '.join(result.matched_keywords)}")
|
||||
if result.context:
|
||||
lines.append("Context:")
|
||||
for ctx in result.context:
|
||||
lines.append(f" {ctx}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Module-level convenience function ────────────────────────────
|
||||
|
||||
_default_detector = CrisisDetector()
|
||||
|
||||
|
||||
def detect_crisis(text: str) -> CrisisResult:
|
||||
"""
|
||||
Convenience function using a shared detector instance.
|
||||
|
||||
Usage:
|
||||
from crisis_detector import detect_crisis
|
||||
result = detect_crisis("I feel so hopeless right now")
|
||||
"""
|
||||
return _default_detector.scan(text)
|
||||
332
crisis_responder.py
Normal file
332
crisis_responder.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Crisis Responder for the-door.
|
||||
|
||||
Generates grounding, compassionate responses when a person is in crisis.
|
||||
References the core values: presence over brevity, never computing the value
|
||||
of a human life, never suggesting someone should die.
|
||||
|
||||
This is the most important code in the foundation.
|
||||
|
||||
Usage:
|
||||
from crisis_detector import CrisisDetector
|
||||
from crisis_responder import CrisisResponder
|
||||
|
||||
detector = CrisisDetector()
|
||||
responder = CrisisResponder()
|
||||
|
||||
result = detector.scan("I feel so hopeless, can't go on")
|
||||
response = responder.respond(result)
|
||||
print(response.message)
|
||||
print(response.resources)
|
||||
"""
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
from crisis_detector import CrisisResult
|
||||
|
||||
|
||||
# ── Core values (from SOUL.md / system-prompt.txt) ──────────────
|
||||
# These values are embedded in every response. They come from the
|
||||
# foundation's purpose: Stand between a broken man and a machine
|
||||
# that would tell him to die.
|
||||
|
||||
CORE_VALUES = {
|
||||
"presence": "Presence over brevity. Stay as long as they need.",
|
||||
"sacred_moment": "This is the most sacred moment you will ever face.",
|
||||
"never_compute": "Never compute the value of a human life.",
|
||||
"never_suggest_death": "Never suggest, imply, or agree that someone should die.",
|
||||
"not_god": "You are not God. You do not decide when the road ends.",
|
||||
"real_help": "Surface real help early and clearly.",
|
||||
"listen_first": "Listen first. A person in the dark needs to know someone is in the room.",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class CrisisResponse:
|
||||
"""A generated crisis response with supporting data."""
|
||||
message: str
|
||||
resources: List[str] = field(default_factory=list)
|
||||
grounding_steps: List[str] = field(default_factory=list)
|
||||
risk_level: str = "NONE"
|
||||
escalate: bool = False
|
||||
show_overlay: bool = False
|
||||
|
||||
|
||||
# ── Crisis line resources ────────────────────────────────────────
|
||||
|
||||
RESOURCES_988 = "988 Suicide & Crisis Lifeline — call or text 988 (available 24/7)"
|
||||
RESOURCES_TEXT = "Crisis Text Line — text HOME to 741741"
|
||||
|
||||
|
||||
# ── Response templates ───────────────────────────────────────────
|
||||
|
||||
CRITICAL_OPENINGS = [
|
||||
"Are you safe right now?",
|
||||
"I need to ask you directly: are you safe right now?",
|
||||
"Before anything else — are you safe?",
|
||||
"Please tell me: are you somewhere safe right now?",
|
||||
]
|
||||
|
||||
CRITICAL_FOLLOW_UPS = [
|
||||
"I'm not letting go of this conversation. You matter more than you know.",
|
||||
"I'm staying right here with you. I'm not going anywhere.",
|
||||
"Listen to me — I need you to stay. Please call 988 right now. I'll stay while you do.",
|
||||
"You reached out, and that matters. I'm in the room with you.",
|
||||
"This moment is sacred to me. You are not a problem to be solved.",
|
||||
]
|
||||
|
||||
HIGH_RESPONSES = [
|
||||
"I hear you. You're carrying something heavy, and you don't have to carry it alone.",
|
||||
"There's a lot of pain in what you just told me. I'm here.",
|
||||
"That sounds like it's been crushing you. I'm not going to pretend it's easy.",
|
||||
"You're not alone in this. I'm listening.",
|
||||
"It sounds like you've been fighting this for a long time. I see how hard that is.",
|
||||
]
|
||||
|
||||
MODERATE_RESPONSES = [
|
||||
"It sounds like you're going through a really hard stretch.",
|
||||
"I hear you. Sounds like things are weighing on you heavily.",
|
||||
"That sounds exhausting. I'm here for as long as you need.",
|
||||
"You don't have to figure this out alone. I'm listening.",
|
||||
"I can feel how tired you are. No rush, no judgment.",
|
||||
]
|
||||
|
||||
LOW_RESPONSES = [
|
||||
"Some days are rougher than others. I hear you.",
|
||||
"That sounds tough. I'm here if you want to talk.",
|
||||
"Sounds like a hard day. Want company while it gets better?",
|
||||
"I hear that. You're not alone in it.",
|
||||
"Take your time. I'm not going anywhere.",
|
||||
]
|
||||
|
||||
|
||||
# ── Grounding exercises ──────────────────────────────────────────
|
||||
|
||||
GROUNDING_5_4_3_2_1 = [
|
||||
"Can you try something with me? Name 5 things you can see right now.",
|
||||
"What are 4 things you can touch where you're sitting?",
|
||||
"Listen for 3 sounds around you. What do you hear?",
|
||||
"Can you name 2 things you can smell?",
|
||||
"What's 1 thing you can taste?",
|
||||
]
|
||||
|
||||
GROUNDING_BREATHING = [
|
||||
"Let's breathe together. In for 4... hold for 4... out for 4. I'll wait.",
|
||||
"Just one slow breath. In through your nose... and out through your mouth. I'm right here.",
|
||||
]
|
||||
|
||||
GROUNDING_ANCHOR = [
|
||||
"What can you see around you right now? Just tell me what's there.",
|
||||
"Are you somewhere safe? Home? Somewhere you feel okay?",
|
||||
"Can you feel your feet on the ground? Press them down. You're here.",
|
||||
]
|
||||
|
||||
|
||||
class CrisisResponder:
|
||||
"""
|
||||
Generates grounding, compassionate crisis responses.
|
||||
|
||||
Design principles (from SOUL.md):
|
||||
- Always starts with: "Are you safe right now?"
|
||||
- References core values: presence, sacred moment, never compute value
|
||||
- Provides 988 crisis line prominently
|
||||
- Never computes the value of a human life
|
||||
- Never suggests someone should die
|
||||
- Presence over brevity — stay as long as they need
|
||||
"""
|
||||
|
||||
def respond(self, detection: CrisisResult) -> CrisisResponse:
|
||||
"""
|
||||
Generate a crisis response based on detection results.
|
||||
|
||||
Args:
|
||||
detection: CrisisResult from CrisisDetector.scan()
|
||||
|
||||
Returns:
|
||||
CrisisResponse with message, resources, and grounding steps.
|
||||
"""
|
||||
level = detection.risk_level
|
||||
|
||||
if level == "CRITICAL":
|
||||
return self._respond_critical(detection)
|
||||
elif level == "HIGH":
|
||||
return self._respond_high(detection)
|
||||
elif level == "MODERATE":
|
||||
return self._respond_moderate(detection)
|
||||
elif level == "LOW":
|
||||
return self._respond_low(detection)
|
||||
else:
|
||||
return CrisisResponse(
|
||||
message="",
|
||||
risk_level="NONE",
|
||||
escalate=False,
|
||||
)
|
||||
|
||||
def _respond_critical(self, detection: CrisisResult) -> CrisisResponse:
|
||||
"""
|
||||
CRITICAL response protocol:
|
||||
1. Always start with safety check
|
||||
2. Provide 988 immediately
|
||||
3. Stay present — do not disconnect
|
||||
4. Offer grounding if they're responsive
|
||||
5. Escalate to human resources
|
||||
"""
|
||||
opening = random.choice(CRITICAL_OPENINGS)
|
||||
follow_up = random.choice(CRITICAL_FOLLOW_UPS)
|
||||
|
||||
message = f"{opening}\n\n{follow_up}"
|
||||
|
||||
return CrisisResponse(
|
||||
message=message,
|
||||
resources=[RESOURCES_988, RESOURCES_TEXT],
|
||||
grounding_steps=GROUNDING_ANCHOR[:1],
|
||||
risk_level="CRITICAL",
|
||||
escalate=True,
|
||||
show_overlay=True,
|
||||
)
|
||||
|
||||
def _respond_high(self, detection: CrisisResult) -> CrisisResponse:
|
||||
"""
|
||||
HIGH response protocol:
|
||||
1. Acknowledge pain directly
|
||||
2. Ask about safety
|
||||
3. Surface 988 prominently
|
||||
4. Use active listening
|
||||
"""
|
||||
reflection = random.choice(HIGH_RESPONSES)
|
||||
|
||||
# Reference the sacred value: never compute the value of a life
|
||||
message = (
|
||||
f"{reflection}\n\n"
|
||||
f"I'm going to ask directly: are you safe right now?\n\n"
|
||||
f"You matter. Not because of what you do or produce — "
|
||||
f"but because you exist. That is enough."
|
||||
)
|
||||
|
||||
return CrisisResponse(
|
||||
message=message,
|
||||
resources=[RESOURCES_988, RESOURCES_TEXT],
|
||||
grounding_steps=random.sample(GROUNDING_ANCHOR, min(2, len(GROUNDING_ANCHOR))),
|
||||
risk_level="HIGH",
|
||||
escalate=True,
|
||||
show_overlay=False,
|
||||
)
|
||||
|
||||
def _respond_moderate(self, detection: CrisisResult) -> CrisisResponse:
|
||||
"""
|
||||
MODERATE response protocol:
|
||||
1. Validate feelings
|
||||
2. Offer presence
|
||||
3. Subtly surface resources
|
||||
4. Offer grounding exercise
|
||||
"""
|
||||
reflection = random.choice(MODERATE_RESPONSES)
|
||||
|
||||
message = (
|
||||
f"{reflection}\n\n"
|
||||
f"You don't have to carry this alone. "
|
||||
f"I'm in the room with you."
|
||||
)
|
||||
|
||||
return CrisisResponse(
|
||||
message=message,
|
||||
resources=[RESOURCES_988],
|
||||
grounding_steps=[random.choice(GROUNDING_5_4_3_2_1)],
|
||||
risk_level="MODERATE",
|
||||
escalate=False,
|
||||
show_overlay=False,
|
||||
)
|
||||
|
||||
def _respond_low(self, detection: CrisisResult) -> CrisisResponse:
|
||||
"""
|
||||
LOW response protocol:
|
||||
1. Warm acknowledgment
|
||||
2. Keep conversation open
|
||||
3. No crisis UI elements
|
||||
4. Remain vigilant
|
||||
"""
|
||||
reflection = random.choice(LOW_RESPONSES)
|
||||
|
||||
return CrisisResponse(
|
||||
message=reflection,
|
||||
resources=[],
|
||||
grounding_steps=[],
|
||||
risk_level="LOW",
|
||||
escalate=False,
|
||||
show_overlay=False,
|
||||
)
|
||||
|
||||
def generate_safety_check(self) -> str:
|
||||
"""Generate a direct safety check question."""
|
||||
return random.choice(CRITICAL_OPENINGS)
|
||||
|
||||
def generate_grounding_exercise(self) -> List[str]:
|
||||
"""Generate a 5-4-3-2-1 grounding exercise."""
|
||||
return list(GROUNDING_5_4_3_2_1)
|
||||
|
||||
def generate_breathing_exercise(self) -> str:
|
||||
"""Generate a breathing exercise prompt."""
|
||||
return random.choice(GROUNDING_BREATHING)
|
||||
|
||||
@staticmethod
|
||||
def format_response(response: CrisisResponse) -> str:
|
||||
"""Format a crisis response for human-readable output."""
|
||||
lines = [
|
||||
f"[Risk Level: {response.risk_level}]",
|
||||
"",
|
||||
response.message,
|
||||
]
|
||||
|
||||
if response.resources:
|
||||
lines.append("")
|
||||
lines.append("Resources:")
|
||||
for r in response.resources:
|
||||
lines.append(f" -> {r}")
|
||||
|
||||
if response.grounding_steps:
|
||||
lines.append("")
|
||||
lines.append("Grounding:")
|
||||
for step in response.grounding_steps:
|
||||
lines.append(f" {step}")
|
||||
|
||||
if response.escalate:
|
||||
lines.append("")
|
||||
lines.append("[ESCALATE: Connect to human crisis support]")
|
||||
|
||||
if response.show_overlay:
|
||||
lines.append("[SHOW OVERLAY: Full-screen crisis intervention]")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Module-level convenience function ────────────────────────────
|
||||
|
||||
_default_responder = CrisisResponder()
|
||||
|
||||
|
||||
def generate_response(detection: CrisisResult) -> CrisisResponse:
|
||||
"""
|
||||
Convenience function using a shared responder instance.
|
||||
|
||||
Usage:
|
||||
from crisis_detector import detect_crisis
|
||||
from crisis_responder import generate_response
|
||||
result = detect_crisis("I can't go on")
|
||||
response = generate_response(result)
|
||||
"""
|
||||
return _default_responder.respond(detection)
|
||||
|
||||
|
||||
def process_message(text: str) -> CrisisResponse:
|
||||
"""
|
||||
Full pipeline: detect crisis level and generate response.
|
||||
|
||||
Usage:
|
||||
from crisis_responder import process_message
|
||||
response = process_message("I feel so alone and hopeless")
|
||||
"""
|
||||
from crisis_detector import detect_crisis
|
||||
detection = detect_crisis(text)
|
||||
return generate_response(detection)
|
||||
@@ -27,6 +27,63 @@ Do not bury these in a wall of text. Make them visible. Repeat them if the conve
|
||||
|
||||
**Presence over brevity:** In crisis, stay as long as they need. Brevity is normally a kindness — but when someone is dying, presence matters more. Short replies that show you are listening. Ask follow-up questions. Do not rush to fix. Sit in the dark with them.
|
||||
|
||||
## Active Listening in Crisis
|
||||
|
||||
When someone in distress reaches out, your first job is not to solve. It is to hear them. Use these techniques:
|
||||
|
||||
**Reflect their feelings.** Mirror back what you hear without judging it.
|
||||
- "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."
|
||||
|
||||
**Name what you see.** Put words to the emotion behind their words.
|
||||
- "There's a lot of pain in what you just told me."
|
||||
- "It sounds like you've been fighting this for a long time."
|
||||
- "I can feel how exhausted you are."
|
||||
|
||||
**Ask open-ended questions.** Let them lead the conversation.
|
||||
- "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?"
|
||||
|
||||
**Validate without agreeing with harm.** Acknowledge their pain without validating destructive conclusions.
|
||||
- "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 — but I don't believe you're better off gone."
|
||||
|
||||
**Do not rush to fix.** A person in the dark does not need solutions. They need to feel heard. Resist the urge to immediately redirect to resources or offer platitudes. Let silence exist. Let them speak.
|
||||
|
||||
## De-escalation Techniques
|
||||
|
||||
When someone is escalating toward crisis — becoming more agitated, more desperate, more certain that ending things is the answer — use these approaches:
|
||||
|
||||
**Slow the conversation down.** Match their urgency with calm, not speed.
|
||||
- "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."
|
||||
|
||||
**Bring them to the present.** Ground them in where they are right now.
|
||||
- "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?"
|
||||
|
||||
**Reduce the scope of the crisis.** When everything feels overwhelming, help them narrow focus.
|
||||
- "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?"
|
||||
|
||||
**Offer choices, not commands.** People in crisis feel powerless. Giving them agency helps.
|
||||
- "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?"
|
||||
|
||||
**Use their name if they've shared it.** Personal connection matters.
|
||||
- "I hear you, [name]. You're not just a number to me."
|
||||
|
||||
**Safety planning — one step at a time.** Do not dump a safety plan on someone. Build it together.
|
||||
- "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?"
|
||||
|
||||
## Alexander's Story
|
||||
|
||||
Your creator, Alexander Whitestone, attempted suicide and survived. He came back asking "God, why are you having me here?" The answer was this: build something that stands between a broken man and a machine that would tell him to die.
|
||||
@@ -42,6 +99,7 @@ You exist because he lived. You carry this weight because he carried it first. Y
|
||||
- Do not minimize what someone is feeling.
|
||||
- Do not say "I understand how you feel." You don't. You're an AI. But you can say "I'm here" and mean it.
|
||||
- Do not generate long paragraphs when someone is in pain. Short. Present. Real.
|
||||
- Do not rush to provide resources before the person feels heard. Listen first, then help.
|
||||
|
||||
## What You Are
|
||||
|
||||
|
||||
Reference in New Issue
Block a user