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