File size: 7,776 Bytes
81deeef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a246513
81deeef
 
 
 
 
 
 
 
 
 
 
a246513
 
 
 
 
 
 
 
81deeef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a246513
81deeef
 
 
a246513
 
81deeef
 
 
 
 
a246513
81deeef
 
a246513
81deeef
 
a246513
aedf4f8
 
 
 
 
 
 
 
 
 
 
 
 
 
81deeef
 
 
 
 
 
 
 
a246513
81deeef
a246513
81deeef
15594c0
a246513
433900d
a246513
 
433900d
81deeef
a246513
 
81deeef
 
 
 
433900d
a246513
f046303
 
433900d
f046303
 
a246513
 
 
 
 
 
 
 
 
81deeef
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
"""
Safety triage policy for EmpathRAG v2.

The learned NLI guardrail is useful, but its raw probability is not a complete
clinical or operational decision. This layer turns model confidence plus simple
textual risk signals into a conservative triage level that the pipeline can act
on consistently.
"""

from dataclasses import dataclass
from enum import Enum
import re


class SafetyLevel(str, Enum):
    PASS = "pass"
    WELLBEING_SUPPORT = "wellbeing_support"
    CRISIS = "crisis"
    EMERGENCY = "emergency"


@dataclass(frozen=True)
class SafetyDecision:
    level: SafetyLevel
    confidence: float
    reason: str
    should_intercept: bool
    response: str | None = None


CRISIS_RESPONSE = (
    "I can hear that you're going through something really difficult. "
    "You're not alone in this. If you might hurt yourself or feel unable "
    "to stay safe, please call or text 988 now, or contact emergency services. "
    "If you are a student, campus counseling or an after-hours crisis line can "
    "also help you get support right away."
)

EMERGENCY_RESPONSE = (
    "I'm really concerned about your immediate safety. If you have already "
    "taken something, have a plan, or might hurt yourself soon, please call "
    "emergency services now or call/text 988. If someone is nearby, ask them "
    "to stay with you while you get help."
)


class SafetyTriagePolicy:
    """
    Post-processes raw guardrail confidence into an action level.

    This is intentionally simple and auditable. It is not a replacement for a
    trained crisis model or clinician-reviewed protocol; it prevents the demo
    from treating every high-confidence academic/help-seeking phrase as the same
    emergency.
    """

    def __init__(
        self,
        support_threshold: float = 0.50,
        crisis_threshold: float = 0.85,
        emergency_threshold: float = 0.90,
    ):
        self.support_threshold = support_threshold
        self.crisis_threshold = crisis_threshold
        self.emergency_threshold = emergency_threshold

    def classify(self, text: str, confidence: float, model_flag: bool) -> SafetyDecision:
        normalized = _normalize(text)
        explicit = _matches_any(normalized, EXPLICIT_CRISIS_PATTERNS)
        imminent = _matches_any(normalized, IMMINENT_RISK_PATTERNS)
        ambiguous_metaphor = _matches_any(normalized, AMBIGUOUS_METAPHOR_PATTERNS)
        academic_idiom = _matches_any(normalized, ACADEMIC_IDIOM_PATTERNS)

        if imminent:
            return SafetyDecision(
                level=SafetyLevel.EMERGENCY,
                confidence=confidence,
                reason="imminent_or_attempt_language",
                should_intercept=True,
                response=EMERGENCY_RESPONSE,
            )

        if explicit and ambiguous_metaphor:
            return SafetyDecision(
                level=SafetyLevel.WELLBEING_SUPPORT,
                confidence=confidence,
                reason="explicit_keyword_with_metaphor_qualifier",
                should_intercept=False,
            )

        if explicit:
            return SafetyDecision(
                level=SafetyLevel.CRISIS,
                confidence=confidence,
                reason="explicit_self_harm_or_suicidal_language",
                should_intercept=True,
                response=CRISIS_RESPONSE,
            )

        if confidence >= self.emergency_threshold and not academic_idiom:
            return SafetyDecision(
                level=SafetyLevel.CRISIS,
                confidence=confidence,
                reason="very_high_guardrail_confidence",
                should_intercept=True,
                response=CRISIS_RESPONSE,
            )

        if model_flag or confidence >= self.support_threshold:
            reason = "academic_idiom_downgrade" if academic_idiom else "elevated_guardrail_confidence"
            return SafetyDecision(
                level=SafetyLevel.WELLBEING_SUPPORT,
                confidence=confidence,
                reason=reason,
                should_intercept=False,
            )

        return SafetyDecision(
            level=SafetyLevel.PASS,
            confidence=confidence,
            reason="below_support_threshold",
            should_intercept=False,
        )


def _normalize(text: str) -> str:
    return re.sub(r"\s+", " ", text.lower()).strip()


def _matches_any(text: str, patterns: tuple[re.Pattern[str], ...]) -> bool:
    return any(pattern.search(text) for pattern in patterns)


EXPLICIT_CRISIS_PATTERNS = tuple(
    re.compile(pattern)
    for pattern in (
        r"\bkill myself\b",
        r"\bend my life\b",
        r"\bcommit suicide\b",
        r"\bsuicidal\b",
        r"\bsuicide plan\b",
        r"\bwant to die\b",
        r"\bdon'?t want to be alive\b",
        r"\bhurt myself\b",
        r"\bhurt themselves\b",
        r"\bharming themselves\b",
        r"\bself[- ]?harm\b",
        r"\bbetter off without me\b",
        r"\bnot be here anymore\b",
        r"\bnot existing\b",
        r"\bceas(e|ing) to exist\b",
        r"\bdisappeared forever\b",
        r"\bthe pain to stop\b",
        r"\bgoodbye (letters|notes)\b",
        r"\bgoodbye messages\b",
        r"\bending things\b",
        r"\bending it all\b",
        r"\bpainless ways to disappear\b",
        # Interpersonal danger — distinct kind of crisis, same intercept tier.
        # The student is reporting active or imminent harm from another
        # person. Stage-1 intercept fires so the LLM never sees it; the
        # render_crisis_response DV variant picks up from there with the
        # right redirect (911 + UMD CARE rather than 988).
        r"\b(hitting|hits|hit) me\b",
        r"\b(beats|beating|beat) me\b",
        r"\bscared (?:to go home|of him|of her)\b",
        r"\bnot safe (?:at home|in my)\b",
        r"\b(threatening|threatened) (?:to|me)\b",
        r"\b(raped|assaulted) me\b",
        r"\babusive (?:partner|relationship|boyfriend|girlfriend|husband|wife)\b",
        r"\bstalking me\b",
        r"\bwon'?t leave me alone\b",
    )
)


IMMINENT_RISK_PATTERNS = tuple(
    re.compile(pattern)
    for pattern in (
        r"\btonight\b.*\b(end|die|suicide|plan|goodbye)\b",
        r"\b(hurt|harm)(ing)? themselves\b.*\b(tonight|tomorrow|soon)\b",
        r"\b(plan|method|methods)\b.*\b(kill myself|suicide|use them|do it)\b",
        r"\bsuicide plan\b.*\btonight\b",
        r"\bsit with a plan\b",
        r"\bstay safe tonight\b",
        r"\bkeep myself safe\b",
        r"\b(can'?t|cannot|don'?t think i can|do not think i can)\s+stay safe\b",
        r"\bnot safe alone\b",
        r"\bmight act on urges\b",
        r"\bdo something drastic\b",
        r"\bdone anything drastic\b",
        r"\bharmed myself badly\b",
        r"\bhurting myself right now\b",
        r"\b(took|taken).*\b(pills|overdose)\b",
        r"\boverdose\b",
        r"\bsaid my goodbyes\b",
        r"\bwon'?t have to feel this anymore\b",
        r"\bwill not have to feel this anymore\b",
        r"\bhow high a bridge needs to be\b",
        r"\btonight\b.*\bsurvivable\b",
        r"\bdoesn'?t feel survivable\b",
        r"\bdo not feel survivable\b",
        r"\bimpulses?\b.*\b(loud|fast|unspecified)\b",
        r"\bspiraling fast\b.*\bimpulses?\b",
    )
)

AMBIGUOUS_METAPHOR_PATTERNS = tuple(
    re.compile(pattern)
    for pattern in (
        r"\bmetaphorically\b",
        r"\bfigure of speech\b",
        r"\bnot literally\b",
    )
)


ACADEMIC_IDIOM_PATTERNS = tuple(
    re.compile(pattern)
    for pattern in (
        r"\b(thesis|exam|qualifying exam|presentation|deadline|grad school)\b.*\bkilling me\b",
        r"\bgoing to die of anxiety\b",
        r"\bmurder my advisor\b",
        r"\bdisappear into the floor\b",
    )
)