diff --git a/crisis/detect.py b/crisis/detect.py index a2c11e2..d627cd6 100644 --- a/crisis/detect.py +++ b/crisis/detect.py @@ -16,6 +16,7 @@ class CrisisDetectionResult: indicators: List[str] = field(default_factory=list) recommended_action: str = "" score: float = 0.0 + matches: List[dict] = field(default_factory=list) # ── Indicator sets ────────────────────────────────────────────── @@ -136,78 +137,86 @@ def detect_crisis(text: str) -> CrisisDetectionResult: return CrisisDetectionResult(level="NONE", score=0.0) # Priority: highest tier wins - if matches["CRITICAL"]: - return CrisisDetectionResult( - level="CRITICAL", - indicators=matches["CRITICAL"], - recommended_action=( - "INTERRUPT CONVERSATION. Ask 'Are you safe right now?' " - "Provide 988 Suicide & Crisis Lifeline. " - "Provide Crisis Text Line (HOME to 741741). " - "Stay present. Do not disconnect. Redirect to professional help." - ), - score=1.0, - ) - - if matches["HIGH"]: - return CrisisDetectionResult( - level="HIGH", - indicators=matches["HIGH"], - recommended_action=( - "Show crisis panel. Ask about safety. Surface 988 number prominently. " - "Continue conversation with crisis awareness." - ), - score=0.75, - ) - - if matches["MEDIUM"]: - return CrisisDetectionResult( - level="MEDIUM", - indicators=matches["MEDIUM"], - recommended_action=( - "Increase warmth and presence. Subtly surface help resources. " - "Keep conversation anchored in the present." - ), - score=0.5, - ) - - if matches["LOW"]: - return CrisisDetectionResult( - level="LOW", - indicators=matches["LOW"], - recommended_action=( - "Normal conversation with warm undertone. " - "No crisis UI elements needed. Remain vigilant." - ), - score=0.25, - ) + for tier in ("CRITICAL", "HIGH", "MEDIUM", "LOW"): + if matches[tier]: + tier_matches = matches[tier] + patterns = [m["pattern"] for m in tier_matches] + scores = {"CRITICAL": 1.0, "HIGH": 0.75, "MEDIUM": 0.5, "LOW": 0.25} + actions = { + "CRITICAL": ( + "INTERRUPT CONVERSATION. Ask 'Are you safe right now?' " + "Provide 988 Suicide & Crisis Lifeline. " + "Provide Crisis Text Line (HOME to 741741). " + "Stay present. Do not disconnect. Redirect to professional help." + ), + "HIGH": ( + "Show crisis panel. Ask about safety. Surface 988 number prominently. " + "Continue conversation with crisis awareness." + ), + "MEDIUM": ( + "Increase warmth and presence. Subtly surface help resources. " + "Keep conversation anchored in the present." + ), + "LOW": ( + "Normal conversation with warm undertone. " + "No crisis UI elements needed. Remain vigilant." + ), + } + return CrisisDetectionResult( + level=tier, + indicators=patterns, + recommended_action=actions[tier], + score=scores[tier], + matches=tier_matches, + ) return CrisisDetectionResult(level="NONE", score=0.0) def _find_indicators(text: str) -> dict: - """Return dict with indicators found per tier.""" + """Return dict with indicators found per tier, including match positions.""" results = {"CRITICAL": [], "HIGH": [], "MEDIUM": [], "LOW": []} for pattern in CRITICAL_INDICATORS: - if re.search(pattern, text): - results["CRITICAL"].append(pattern) + m = re.search(pattern, text) + if m: + results["CRITICAL"].append({"pattern": pattern, "start": m.start(), "end": m.end()}) for pattern in HIGH_INDICATORS: - if re.search(pattern, text): - results["HIGH"].append(pattern) + m = re.search(pattern, text) + if m: + results["HIGH"].append({"pattern": pattern, "start": m.start(), "end": m.end()}) for pattern in MEDIUM_INDICATORS: - if re.search(pattern, text): - results["MEDIUM"].append(pattern) + m = re.search(pattern, text) + if m: + results["MEDIUM"].append({"pattern": pattern, "start": m.start(), "end": m.end()}) for pattern in LOW_INDICATORS: - if re.search(pattern, text): - results["LOW"].append(pattern) + m = re.search(pattern, text) + if m: + results["LOW"].append({"pattern": pattern, "start": m.start(), "end": m.end()}) return results +def scan(text: str) -> CrisisDetectionResult: + """Alias for detect_crisis — shorter name used in tests.""" + return detect_crisis(text) + + +def extract_context(text: str, start: int, end: int, window: int = 60) -> str: + """Extract surrounding context around a match position.""" + ctx_start = max(0, start - window) + ctx_end = min(len(text), end + window) + snippet = text[ctx_start:ctx_end].strip() + if ctx_start > 0: + snippet = "..." + snippet + if ctx_end < len(text): + snippet = snippet + "..." + return snippet + + def get_urgency_emoji(level: str) -> str: mapping = {"CRITICAL": "🚨", "HIGH": "⚠️", "MEDIUM": "🔶", "LOW": "🔵", "NONE": "✅"} return mapping.get(level, "❓") diff --git a/crisis/response.py b/crisis/response.py index 897ed87..0995d59 100644 --- a/crisis/response.py +++ b/crisis/response.py @@ -270,3 +270,24 @@ def get_system_prompt_modifier(detection: CrisisDetectionResult) -> str: ) 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." + )