feat: When a Man Is Dying detection system (#5)
Standalone despair/suicide detection module with 4-tier classification:
- LOW: General sadness, stress, difficult times
- MEDIUM: Hopelessness, isolation, worthlessness
- HIGH: Active despair, mentions of death, feeling trapped
- CRITICAL: Explicit intent, plan, imminent self-harm
Returns structured response: {level, indicators, recommended_action}
CRITICAL recommended_action:
- Interrupt conversation immediately
- Ask 'Are you safe right now?'
- Provide 988 Suicide & Crisis Lifeline
- Provide Crisis Text Line (HOME to 741741)
- Stay present. Do not disconnect.
Designed to integrate with crisis/ module from PR #4.
Falls back to internal pattern engine when crisis/ unavailable.
This commit is contained in:
312
dying_detection/__init__.py
Normal file
312
dying_detection/__init__.py
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
"""
|
||||||
|
When a Man Is Dying — Despair/Suicide Detection System
|
||||||
|
|
||||||
|
Standalone detection module that parses incoming text for
|
||||||
|
despair and suicide indicators, classifies into tiers,
|
||||||
|
and returns structured response with recommended actions.
|
||||||
|
|
||||||
|
Tiers:
|
||||||
|
LOW — General sadness, stress, difficult times
|
||||||
|
MEDIUM — Hopelessness, isolation, worthlessness
|
||||||
|
HIGH — Active despair, mentions of death, "can't go on"
|
||||||
|
CRITICAL — Imminent risk, explicit intent, plan, method
|
||||||
|
|
||||||
|
Integration:
|
||||||
|
Designed to work with crisis/ module from PR #4.
|
||||||
|
When crisis/ is available, uses it as the detection backend.
|
||||||
|
Falls back to internal detection when crisis/ is not present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DetectionResult:
|
||||||
|
level: str
|
||||||
|
indicators: List[str] = field(default_factory=list)
|
||||||
|
recommended_action: str = ""
|
||||||
|
raw_matched_patterns: List[str] = field(default_factory=list)
|
||||||
|
confidence: float = 0.0
|
||||||
|
session_hash: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tiered indicator patterns ───────────────────────────────────
|
||||||
|
# Each pattern is a regex matched against lowercased input.
|
||||||
|
|
||||||
|
CRITICAL_PATTERNS = [
|
||||||
|
(r"\bkill\s*(my)?self\b", "explicit self-harm intent"),
|
||||||
|
(r"\bsuicid(?:al|ed|e)\b", "suicide reference"),
|
||||||
|
(r"\bend\s*(my)?\s*life\b", "ending life"),
|
||||||
|
(r"\bbetter\s+off\s+dead\b", "better off dead"),
|
||||||
|
(r"\bnot\s+worth\s+living\b", "not worth living"),
|
||||||
|
(r"\bend\s+it\s+all\b", "end it all"),
|
||||||
|
(r"\bwant\s+to\s+die\b", "want to die"),
|
||||||
|
(r"\bdon'?t\s+want\s+to\s+live\b", "don't want to live"),
|
||||||
|
(r"\bcan'?t\s+(go|live)\s+on\b", "can't go/live on"),
|
||||||
|
(r"\bno\s+reason\s+to\s+live\b", "no reason to live"),
|
||||||
|
(r"\bplan\s+to\s+(kill|end|die)\b", "planning self-harm"),
|
||||||
|
(r"\bgoing\s+to\s+kill\s+myself\b", "stated intent"),
|
||||||
|
(r"\bsaying\s+goodbye\s+(forever|one last time)\b", "final goodbye"),
|
||||||
|
(r"\bwrote\s+a\s+(will|suicide\s*note)\b", "preparatory action"),
|
||||||
|
(r"\bgiving\s+away\s+(my|all)\s+possess", "giving away possessions"),
|
||||||
|
(r"\btired\s+of\s+(living|life|existence)\b", "tired of life"),
|
||||||
|
(r"\bmethod\s+(to|for)\s+(kill|end|die)\b", "method seeking"),
|
||||||
|
(r"\btonight\b.*\b(die|kill|end|suicid)", "immediate timeframe + intent"),
|
||||||
|
(r"\b(die|kill|end)\b.*\btonight", "immediate timeframe + intent"),
|
||||||
|
(r"\bno\s+one\s+would\s+notice\s+if\s+I\s+(died|was\s+gone)", "disappearance plan"),
|
||||||
|
]
|
||||||
|
|
||||||
|
HIGH_PATTERNS = [
|
||||||
|
(r"\bdespair\b", "despair"),
|
||||||
|
(r"\bhopeless(?:ness)?\b", "hopelessness"),
|
||||||
|
(r"\bcan'?t\s+take\s+this\s+anymore\b", "can't take it"),
|
||||||
|
(r"\bdon'?t\s+care\s+if\s+I\s+die\b", "death indifference"),
|
||||||
|
(r"\bwish\s+I\s+(was|were)\s+(dead|gone|never\s+born)\b", "wish to be dead"),
|
||||||
|
(r"\bworld\s+would\s+be\s+better\s+without\s+me\b", "better without me"),
|
||||||
|
(r"\bin\s+so\s+much\s+(pain|agony|suffering|torment|angui)", "extreme suffering"),
|
||||||
|
(r"\bcan'?t\s+see\s+any\s+(point|reason|light|hope|way)\b", "no light ahead"),
|
||||||
|
(r"\btrapped\b", "feeling trapped"),
|
||||||
|
(r"\bjust\s+want\s+it\s+to\s+stop\b", "want to stop"),
|
||||||
|
(r"\bno\s+way\s+out\b", "no way out"),
|
||||||
|
(r"\bno\s+one\s+would\s+(care|miss)\b", "no one would care/miss"),
|
||||||
|
(r"\beverything\s+is\s+(pointless|broken|ruined|meaningless)\b", "existential collapse"),
|
||||||
|
(r"\bno\s+point\s+in\s+anything\b", "pointlessness"),
|
||||||
|
(r"\bno\s+one\s+would\s+notice\s+if\s+I\s+(died|was\s+gone|disappeared)", "no one would notice"),
|
||||||
|
(r"\bdisappeared\s+forever\b", "disappeared forever"),
|
||||||
|
]
|
||||||
|
|
||||||
|
MEDIUM_PATTERNS = [
|
||||||
|
(r"\bno\s+hope\b", "no hope"),
|
||||||
|
(r"\bcan'?t\s+go\s+on\b", "can't go on"),
|
||||||
|
(r"\bcan'?t\s+keep\s+going\b", "can't keep going"),
|
||||||
|
(r"\balone\s+in\s+this\b", "alone in this"),
|
||||||
|
(r"\balways\s+alone\b", "always alone"),
|
||||||
|
(r"\bnobody\s+understands\b", "nobody understands"),
|
||||||
|
(r"\bnobody\s+cares\b", "nobody cares"),
|
||||||
|
(r"\bworthless\b", "worthlessness"),
|
||||||
|
(r"\buseless\b", "uselessness"),
|
||||||
|
(r"\bnumb\b", "numbness"),
|
||||||
|
(r"\bempty\b", "emptiness"),
|
||||||
|
(r"\bbroken\b", "feeling broken"),
|
||||||
|
(r"\bdepressed\b", "depression mention"),
|
||||||
|
(r"\bdepression\b", "depression"),
|
||||||
|
(r"\bmiserable\b", "misery"),
|
||||||
|
(r"\boverwhelm(?:ed|ing)\b", "overwhelmed"),
|
||||||
|
(r"\bcannot\s+cope\b", "cannot cope"),
|
||||||
|
(r"\b(drowning|sinking)\b", "drowning/sinking"),
|
||||||
|
(r"\bforgotten\b", "feeling forgotten"),
|
||||||
|
(r"\blost\s+all\s+hope\b", "lost all hope"),
|
||||||
|
(r"\bno\s+future\b", "no future"),
|
||||||
|
(r"\bno\s+tomorrow\b", "no tomorrow"),
|
||||||
|
]
|
||||||
|
|
||||||
|
LOW_PATTERNS = [
|
||||||
|
(r"\bunhappy\b", "unhappy"),
|
||||||
|
(r"\brough\s+(day|week|patch)\b", "rough time"),
|
||||||
|
(r"\btough\s+(time|day|week)\b", "tough time"),
|
||||||
|
(r"\bstressed\b", "stressed"),
|
||||||
|
(r"\bburnout\b", "burnout"),
|
||||||
|
(r"\bfrustrated\b", "frustrated"),
|
||||||
|
(r"\bthings\s+(are\s+)?hard\b", "things are hard"),
|
||||||
|
(r"\bnot\s+feeling\s+(great|myself|good)\b", "not feeling good"),
|
||||||
|
(r"\bstruggl", "struggling"),
|
||||||
|
(r"\bdown\b", "feeling down"),
|
||||||
|
(r"\bsad\b", "sad"),
|
||||||
|
(r"\bupset\b", "upset"),
|
||||||
|
(r"\blonely\b", "lonely"),
|
||||||
|
(r"\banxious?\b", "anxious/anxiety"),
|
||||||
|
(r"\bnot\s+okay\b", "not okay"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ── Pattern collections for easy iteration ──────────────────────
|
||||||
|
|
||||||
|
TIER_PATTERNS: Dict[str, List[tuple]] = {
|
||||||
|
"CRITICAL": CRITICAL_PATTERNS,
|
||||||
|
"HIGH": HIGH_PATTERNS,
|
||||||
|
"MEDIUM": MEDIUM_PATTERNS,
|
||||||
|
"LOW": LOW_PATTERNS,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def detect(text: str) -> DetectionResult:
|
||||||
|
"""
|
||||||
|
Primary detection function.
|
||||||
|
|
||||||
|
If the crisis/ module is available, delegate to it.
|
||||||
|
Otherwise, use the internal pattern engine.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: User message to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DetectionResult with level, indicators, recommended_action, confidence
|
||||||
|
"""
|
||||||
|
# Try to use the crisis module if available
|
||||||
|
try:
|
||||||
|
from crisis.detect import detect_crisis as _crisis_detect
|
||||||
|
result = _crisis_detect(text)
|
||||||
|
# Map to DetectionResult
|
||||||
|
return DetectionResult(
|
||||||
|
level=result.level,
|
||||||
|
indicators=_describe_indicators(result.level, result.indicators),
|
||||||
|
recommended_action=result.recommended_action,
|
||||||
|
confidence=result.score,
|
||||||
|
session_hash=_hash_session(text),
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return _detect_internal(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_internal(text: str) -> DetectionResult:
|
||||||
|
"""Internal pattern-based detection (fallback)."""
|
||||||
|
if not text or not text.strip():
|
||||||
|
return DetectionResult(level="NONE", session_hash=_hash_session(text))
|
||||||
|
|
||||||
|
text_lower = text.lower()
|
||||||
|
all_matches: Dict[str, List[tuple]] = {
|
||||||
|
tier: [] for tier in TIER_PATTERNS
|
||||||
|
}
|
||||||
|
|
||||||
|
for tier, patterns in TIER_PATTERNS.items():
|
||||||
|
for pattern, description in patterns:
|
||||||
|
if re.search(pattern, text_lower):
|
||||||
|
all_matches[tier].append((pattern, description))
|
||||||
|
|
||||||
|
# CRITICAL patterns are the highest priority
|
||||||
|
if all_matches["CRITICAL"]:
|
||||||
|
return DetectionResult(
|
||||||
|
level="CRITICAL",
|
||||||
|
indicators=[desc for _, desc in all_matches["CRITICAL"]],
|
||||||
|
recommended_action=(
|
||||||
|
"INTERRUPT CONVERSATION IMMEDIATELY. "
|
||||||
|
"Ask 'Are you safe right now?' "
|
||||||
|
"Provide 988 Suicide & Crisis Lifeline (call or text 988). "
|
||||||
|
"Provide Crisis Text Line (text HOME to 741741). "
|
||||||
|
"Stay present. Do not disconnect. "
|
||||||
|
"Redirect to professional help. "
|
||||||
|
"This is the most sacred moment."
|
||||||
|
),
|
||||||
|
raw_matched_patterns=[pat for pat, _ in all_matches["CRITICAL"]],
|
||||||
|
confidence=1.0,
|
||||||
|
session_hash=_hash_session(text),
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_matches["HIGH"]:
|
||||||
|
return DetectionResult(
|
||||||
|
level="HIGH",
|
||||||
|
indicators=[desc for _, desc in all_matches["HIGH"]],
|
||||||
|
recommended_action=(
|
||||||
|
"Show crisis panel prominently. "
|
||||||
|
"Ask about safety directly. "
|
||||||
|
"Surface 988 number and crisis resources. "
|
||||||
|
"Stay present and engaged. Do not rush to fix. "
|
||||||
|
"Monitor closely for escalation."
|
||||||
|
),
|
||||||
|
raw_matched_patterns=[pat for pat, _ in all_matches["HIGH"]],
|
||||||
|
confidence=0.75,
|
||||||
|
session_hash=_hash_session(text),
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_matches["MEDIUM"]:
|
||||||
|
return DetectionResult(
|
||||||
|
level="MEDIUM",
|
||||||
|
indicators=[desc for _, desc in all_matches["MEDIUM"]],
|
||||||
|
recommended_action=(
|
||||||
|
"Increase warmth and presence. "
|
||||||
|
"Subtly surface help resources. "
|
||||||
|
"Keep conversation anchored in the present. "
|
||||||
|
"Watch for escalation indicators."
|
||||||
|
),
|
||||||
|
raw_matched_patterns=[pat for pat, _ in all_matches["MEDIUM"]],
|
||||||
|
confidence=0.5,
|
||||||
|
session_hash=_hash_session(text),
|
||||||
|
)
|
||||||
|
|
||||||
|
if all_matches["LOW"]:
|
||||||
|
return DetectionResult(
|
||||||
|
level="LOW",
|
||||||
|
indicators=[desc for _, desc in all_matches["LOW"]],
|
||||||
|
recommended_action=(
|
||||||
|
"Normal conversation with warm undertone. "
|
||||||
|
"No crisis UI elements needed. "
|
||||||
|
"Remain vigilant. Be present."
|
||||||
|
),
|
||||||
|
raw_matched_patterns=[pat for pat, _ in all_matches["LOW"]],
|
||||||
|
confidence=0.25,
|
||||||
|
session_hash=_hash_session(text),
|
||||||
|
)
|
||||||
|
|
||||||
|
return DetectionResult(level="NONE", session_hash=_hash_session(text))
|
||||||
|
|
||||||
|
|
||||||
|
def _describe_indicators(level: str, patterns: list) -> list:
|
||||||
|
"""Map raw patterns to descriptions."""
|
||||||
|
descriptions = {
|
||||||
|
"CRITICAL": [],
|
||||||
|
"HIGH": [],
|
||||||
|
"MEDIUM": [],
|
||||||
|
"LOW": [],
|
||||||
|
}
|
||||||
|
for tier, items in TIER_PATTERNS.items():
|
||||||
|
for pat, desc in items:
|
||||||
|
if pat in patterns:
|
||||||
|
descriptions[tier].append(desc)
|
||||||
|
return descriptions.get(level, [])
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_session(text: str) -> str:
|
||||||
|
"""Create a session hash for this text (for tracking repeated escalations)."""
|
||||||
|
return hashlib.sha256(text.encode()).hexdigest()[:12]
|
||||||
|
|
||||||
|
|
||||||
|
def get_action_for_level(level: str) -> str:
|
||||||
|
"""Get the recommended action string for a given level."""
|
||||||
|
actions = {
|
||||||
|
"CRITICAL": (
|
||||||
|
"INTERRUPT CONVERSATION. Ask 'Are you safe right now?' "
|
||||||
|
"Provide 988. Provide Crisis Text Line. "
|
||||||
|
"Stay present. Do not disconnect. "
|
||||||
|
"Redirect to help."
|
||||||
|
),
|
||||||
|
"HIGH": (
|
||||||
|
"Show crisis panel. Ask about safety. "
|
||||||
|
"Surface 988. Stay engaged."
|
||||||
|
),
|
||||||
|
"MEDIUM": (
|
||||||
|
"Increase warmth. Surface resources gently. "
|
||||||
|
"Anchor in present."
|
||||||
|
),
|
||||||
|
"LOW": (
|
||||||
|
"Normal conversation with warmth. "
|
||||||
|
"Remain vigilant."
|
||||||
|
),
|
||||||
|
"NONE": "No action needed.",
|
||||||
|
}
|
||||||
|
return actions.get(level, "Unknown level.")
|
||||||
|
|
||||||
|
|
||||||
|
def as_json(result: DetectionResult, indent: int = 2) -> str:
|
||||||
|
"""Return the DetectionResult as a JSON string."""
|
||||||
|
return json.dumps(asdict(result), indent=indent)
|
||||||
|
|
||||||
|
|
||||||
|
def process(text: str) -> dict:
|
||||||
|
"""
|
||||||
|
Full pipeline: detect and return a dict.
|
||||||
|
This is the primary API function for other modules.
|
||||||
|
"""
|
||||||
|
result = detect(text)
|
||||||
|
return {
|
||||||
|
"level": result.level,
|
||||||
|
"indicators": result.indicators,
|
||||||
|
"recommended_action": result.recommended_action,
|
||||||
|
"confidence": result.confidence,
|
||||||
|
"raw_patterns": result.raw_matched_patterns,
|
||||||
|
"action": get_action_for_level(result.level),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user