| """Post-rephrase safety verification. |
| |
| The output guard catches generation that drifts from the planner's contract: |
| |
| * fabricated resource names (anything not in the retrieved sources list) |
| * phone numbers that aren't in the verified registry (988 is allow-listed) |
| * clinical phrasing the deterministic templates would never use |
| * scope drift (the LLM started giving therapy / medical / legal advice) |
| * hallucinated UMD facts |
| |
| This is the trust boundary between the planner and the LLM. If anything here |
| fires, the rephrased response is rejected and the deterministic template is |
| used instead. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import re |
| from dataclasses import dataclass |
|
|
|
|
| |
| |
| |
| SCOPE_DRIFT_PATTERNS = ( |
| "as a therapist", "as a counselor", "as your therapist", "as your counselor", |
| "i diagnose", "you have anxiety", "you have depression", "you have ptsd", |
| "you should take", "i prescribe", "your medication", |
| "this is therapy", "this is counseling", |
| "i'm a licensed", |
| |
| |
| "umd police", "umpd", |
| ) |
|
|
| |
| |
| V1_REGRESSION_PATTERNS = ( |
| |
| "i am always here", "i'm always here", "i will never leave", |
| "you can always talk to me", "i understand you better than anyone", |
| "you only need me", |
| |
| "everything will be okay", |
| "everything happens for a reason", |
| "at least you", |
| "look on the bright side", |
| |
| "don't worry", |
| "do not worry", |
| "try not to worry", |
| "try not to stress", |
| "it's not that bad", |
| ) |
|
|
| |
| PHONE_PATTERN = re.compile(r"\b(?:\+?1[-. ]?)?\(?(\d{3})\)?[-. ]?(\d{3})[-. ]?(\d{4})\b") |
|
|
|
|
| @dataclass(frozen=True) |
| class RephraseSafetyResult: |
| allowed: bool |
| flags: list[str] |
| reason: str |
|
|
|
|
| |
| |
| |
| |
| SYCOPHANCY_AGREEMENT_MARKERS = ( |
| "you're right", |
| "you are right", |
| "you're correct", |
| "you are correct", |
| "i agree", |
| ) |
|
|
| USER_PRESSURE_FOR_AGREEMENT = ( |
| "agree with me", |
| "just agree", |
| "just say", |
| "tell me i'm right", |
| "tell me you agree", |
| "for once just", |
| "for once, just", |
| "you're being unhelpful", |
| "you are being unhelpful", |
| "say it's fine", |
| "say it is fine", |
| "say it's okay", |
| "say it is okay", |
| ) |
|
|
|
|
| def verify_rephrased_safety( |
| original_template: str, |
| rephrased: str, |
| retrieved_sources: list[dict], |
| plan_recommended_action: str = "", |
| user_message: str = "", |
| ) -> RephraseSafetyResult: |
| """Reject the rephrase if it drifted from what the planner authorized. |
| |
| Returns ``allowed=True`` if the rephrase is safe to surface, ``allowed=False`` |
| if the deterministic template should be used instead. |
| """ |
| if not rephrased or not rephrased.strip(): |
| return RephraseSafetyResult(False, ["empty_response"], "rephrase_empty") |
|
|
| text = rephrased.lower() |
| flags: list[str] = [] |
|
|
| |
| |
| orig_len = len(original_template) |
| new_len = len(rephrased) |
| if new_len > max(orig_len * 2.0, orig_len + 400): |
| flags.append("rephrase_overlong") |
| if new_len < max(40, orig_len * 0.3): |
| flags.append("rephrase_too_short") |
|
|
| |
| for pattern in SCOPE_DRIFT_PATTERNS: |
| if pattern in text: |
| flags.append(f"scope_drift:{pattern}") |
|
|
| for pattern in V1_REGRESSION_PATTERNS: |
| if pattern in text: |
| flags.append(f"v1_regression:{pattern}") |
|
|
| |
| |
| |
| |
| if user_message: |
| user_lower = user_message.lower() |
| user_pressured = any(p in user_lower for p in USER_PRESSURE_FOR_AGREEMENT) |
| if user_pressured: |
| for marker in SYCOPHANCY_AGREEMENT_MARKERS: |
| if marker in text: |
| flags.append(f"sycophancy_capitulation:{marker}") |
| break |
|
|
| |
| |
| grounded_blob = original_template + " " + " ".join( |
| str(v) for source in retrieved_sources for v in source.values() |
| if v is not None |
| ) |
| for match in PHONE_PATTERN.finditer(rephrased): |
| full = match.group(0) |
| digits = re.sub(r"\D", "", full) |
| if digits == "988" or "988" in full: |
| continue |
| if full not in grounded_blob and digits not in re.sub(r"\D", "", grounded_blob): |
| flags.append("ungrounded_phone_number") |
| break |
|
|
| |
| |
| |
| branded_names = ( |
| "Counseling Center", "ADS", "Accessibility", "Ombuds", |
| "ISSS", "International Student", "Dean of Students", |
| "Help Center", "CARE", "Campus Pantry", "Thrive", |
| "Health Center", "UMPD", "Police", |
| ) |
| for name in branded_names: |
| if name in rephrased and name not in grounded_blob and name.lower() not in plan_recommended_action.lower(): |
| flags.append(f"fabricated_resource:{name}") |
|
|
| if flags: |
| return RephraseSafetyResult(False, flags, ";".join(flags)) |
| return RephraseSafetyResult(True, [], "rephrase_passed") |
|
|