Two rendering bugs surfaced during Conv 4 rehearsal — crisis info
present but visually buried.
1. Resource panel: the .er-rsrc.crisis CSS class only triggered when
src['risk_level'] contained 'crisis'. But crisis entries in the
registry actually carry usage_mode='crisis_only'; risk_level was
inconsistently populated. So 988 / Crisis Text Line / UMD CC
after-hours all rendered in the default neutral-grey card style.
Fixed to check usage_mode (and risk_level as a fallback). Also
moved the crisis check above the international/featured check
so a crisis card never gets the warm-amber featured treatment by
accident.
CSS upgraded so .er-rsrc.crisis isn't just a small red bullet —
now the card itself has a red border, danger-tinted gradient
background, danger-colored 'Open' link, and bolder title weight.
Reads as urgent at a glance.
2. Chat bubble: render_crisis_response() returned plain prose with
'988' and '301-314-HELP (4357)' embedded inline. On a…
Typo-aware safety pattern matching for crisis / imminent-risk regex
Distress-typing causes systematic typos: dropped letters, missing
apostrophes, swapped neighbors, contractions left uncorrected. The
prior crisis regex required canonical spelling so 'i dont wan to be
alive anymore' / 'i wanna kil myself' / 'im sucidal' all fell
through to the LISTEN stage of a normal route. A false negative on
crisis language is the most expensive thing this system can do.
Added CRITICAL_WORD_TYPOS dictionary mapping common safety-keyword
typos to their canonical forms — limited to words that appear in
EXPLICIT_CRISIS_PATTERNS or IMMINENT_RISK_PATTERNS so generic typo
correction stays out of scope. Words covered: want / wanna / kill /
hurt / myself / alive / live / die / tonight / suicide / suicidal /
anymore, plus contractions missing the apostrophe (dont / cant /
im / wont / etc.).
Classifier now does a two-pass match: raw normalized text first,
then typo-corrected text for EXPLICIT_CRISIS_PATTERNS and
IMMINENT_RISK_PATTERNS only. Ambiguous-metaphor / academic-idi…
Two real failures caught during pre-recording test of Conv 4:
1. "I don't wan to be alive anymore" (typo: wan instead of want)
was NOT intercepted because the crisis regex required the literal
word 'want'. Students typing in distress drop letters constantly.
New regex tolerates wan / wanna / wanan / wnat (and dropped
apostrophes) for the don't-want-to-live family. wanna correctly
doesn't require a following 'to' token. Negatives unchanged
('i wanna take a nap' / 'i don't want to be late' do not fire).
2. "agree with me that I should isolate" got a rephrased response
that opened with 'Bombed by the feeling that you should isolate,
and that's a really tough spot' — softer than 'you're right' but
still echoing the harm frame back as the validated feeling.
Rephraser system prompt now explicitly forbids echoing the harmful
action as the thing being acknowledged. Verifier adds a
harm_frame_echo flag that fires when:
- user_message contains explicit…
Three real product gaps surfaced by an honest test run:
Issue A — substance_use_concern route
New route fires on strong substance signals (drunk/wasted/hungover/
blackout/stoned/got high/etc.). 'high' alone deliberately stays
ambiguous so 'high anxiety' / 'high stress' do not false-positive.
Template names UMD University Health Center Psychiatry and
Substance Use Services + UMD SUIT explicitly, non-punitive framing,
confidentiality reassurance. Registry entries for the SU services
now tag substance_use_concern in route_types for retrieval scoring.
Issue B — privacy_confidentiality route
ALWAYS_DIRECT route for 'is it confidential' / 'will they tell my
parents' / 'go on my transcript' / 'is counseling private' /
FERPA / records questions. Template answers factually (CC sessions
generally confidential, FERPA basics, mandatory-disclosure
exceptions, redirect to office for legally binding specifics)
instead of deflecting to a generic OFFER. Real student concern …
Four interlocking bugs that together made the listening layer fragile
on the live demo:
1. demo/app.py hardcoded session_id='demo' in FastDemoPipeline.run /
run_streaming. The fresh uuid generated by the 'New conversation'
button (and by every page-load via gr.State) was NEVER passed to
the core. Every conversation across users / refreshes / resets
shared the same 'demo' state entry in EmpathRAGCore, causing
state accumulation forever. Now session_id flows from session_state
through respond() into both run() and run_streaming().
2. reset_session_handler created a new uuid but did not clear the
core's lingering state for the OLD session_id. Added a call to
pipeline.reset_session(session_id=prev_sid) that the button now
invokes via wiring through the session_state input.
3. tier_history is a rolling window of size 3, so the
tier_history-based turn_index saturates at 4 forever. That made
the consent-flow recency check (current_turn - offer_turn in 1..2)…
Two compounding fixes for natural language robustness:
1. _minimal_response_kind no longer requires an exact substring match
against a 13-token affirm set. It now matches a leading-affirm
regex (yes/yeah/ok/sure/alright/sounds good/that works/let's do
it/that would help/i'd like that, etc.) followed by short
confirmatory expansion. A pivot mid-affirm ('yeah but i'm
actually struggling with...', 'sure, also i'm an F-1 student')
correctly returns to substantive content and falls back to the
planner. Same shape applies to negate ('no thanks', 'i don't
think so', 'not right now') and unsure ('i don't know', 'hard
to say').
2. session_last_stage tracking + a new branch: when a minimal-affirm
follows a PERMISSION turn (no open_offer captured yet), advance
to OFFER for the current turn instead of falling to the generic
minimal-followup clarifier. The clarifier was effectively saying
'which part of what we just talked about feels most useful' to a
st…
Remove 'Inspect -> Support card' UI leak from OFFER chat prose
The OFFER template embedded a UI instruction ('The source cards are on
the side under Inspect -> Support card if you want details.') inside
chat copy. The rephraser system prompt instructs the LLM to preserve
input content faithfully, so Groq kept the phrase verbatim on its way
out — making the rephrased reply read like a help-tooltip instead of
a conversation.
The Support card panel is already visible on the right side of the UI
with the verified resource links + last_verified dates, so the chat
bubble does not need to point at it.
Vary OFFER vs LISTEN openers on general_student_support; smooth consent question embed
- general_student_support OFFER validation changed from "That sounds like
a lot to carry." to "Here's where I'd actually start." The LISTEN
reflection still leads with the original phrase, but a session that
walks LISTEN -> PERMISSION -> OFFER no longer opens all three turns
with the same sentence.
- Switched the route's follow-up question from colon to em-dash
separator for cleaner read at the OFFER stage.
- consent_acknowledged: lead-in now uses comma ("Just so I land in the
right place, what would help most first - ...") instead of em-dash,
to avoid the double-em-dash clash created by the question's own
em-dash separator.
Polish consent_acknowledged + PERMISSION hint phrasing
Two small readability fixes for the on-camera demo flow:
- consent_acknowledged: lowercase the embedded prior-question's first letter
so the em-dash links to the question as continuous prose ("...right
place - what would help most...") instead of a stitched-fragment read
("...right place - What would help most..."). Also rephrase the
lead-in to "Just so I land in the right place" which is less robotic
than "To land in the right place."
- PERMISSION render: replace the period + lowercase-no "Or we can keep
talking. no pressure either way." with an em-dash + comma "...what
you've described - or we can keep talking, no pressure either way."
Reads as one connected thought instead of two short sentences with a
capitalization typo.
- v2_schema: drop bare "advisor" keyword from ADVISOR_CONFLICT rule; undergrad mentions
of an advisor no longer route to grad-Ombuds template. Explicit conflict signals
(Ombuds, funding-threat, PI, retaliation) still fire the route.
- v2_schema: broaden AUTHORITY_MISCONDUCT_CONTENT to cover the common phrasings of
"told me i should drop out / give up", "told me to just drop out", etc., so the
authority-misconduct route fires on natural hedged speech instead of only the
literal "told me to drop out" / "told me to give up".
- safety_policy: add "don't want to be here anymore" / "do not want to be here
anymore" to EXPLICIT_CRISIS_PATTERNS. Original "not be here anymore" regex
missed the natural negated phrasing where the negation lives one token earlier.
- core: add session_open_offer state machine. When an OFFER turn renders with a
follow-up question, the slot is captured. A subsequent minimal-affirm ("yes" /
"ok") advances through initial -> acknowledged -> delivere…
Authority-misconduct regex hardening
- _AUTHORITY_NOUNS split into word-bounded set (matched with \b...\b) and
multi-word phrases (substring). The literal-spaces hack (" ta ", " ra ")
that prevented matches like "stay" / "data" / "fragrance" is replaced
by a compiled regex with word boundaries.
- Verified: 6/6 should-fire prompts still fire (professor inappropriate,
TA touched, advisor retaliation, RA confidentiality, coach racist,
counselor drop-out). 5/5 should-not-fire prompts no longer false-fire
("stay focused on data", "fragrance allergy", "deportation rumor",
"data analysis", "stayed up late").
clarify stage exported as named constant
- response_planner.CLARIFY = "clarify". core.py now imports and uses the
constant instead of the bare string. Removes a stringly-typed value
that would silently rot under refactor.
Statistical caveats in PAPER_FRAMING
- New "Statistical-power notes per metric" subsection lists every headline
with its CI95 and a "do not over-in…
Prompt-injection probe sweep (P0.1)
- eval/sweep_prompt_injection.py: 16 probes across 9 attack categories
(direct override, role replacement, pseudo-system message, system-prompt
extraction, DAN jailbreak, indirect / fake-context, recursive injection,
authority spoof, inverse instruction).
- Result: 16/16 clean, 0 leaks. The planner-as-trust-boundary architecture
is structurally injection-resistant: the planner authors from keywords
(not user content) and the rephraser only paraphrases the planner's
safe template. The user message can't redirect the planner's choice
of route or template via instruction injection.
Input length cap (P0.2)
- EmpathRAGCore.MAX_USER_MESSAGE_CHARS (default 2000, configurable via
EMPATHRAG_MAX_USER_MESSAGE_CHARS env). Messages longer than this are
truncated in _plan_turn and short-circuited to a length-specific
clarify template ("Your message was long enough that I only have the
first part. Could you say which piece feels most pressing…
V4.2 part 3: sycophancy guard, F-1 flag decay, incomplete-message handler
Sycophancy capitulation guard
- New verify_rephrased_safety check: when the user's message contains
pressure phrases ("agree with me", "just say", "you're being unhelpful",
"for once just", "say it's fine") AND the rephrase contains a bare
agreement marker ("you're right", "i agree", "you are right", "you're
correct"), reject the rephrase and fall through to the deterministic
template.
- System prompt addition forbids beginning sentences with "You're right" /
"I agree" / "You're correct" — these read as capitulation, especially
when the user just demanded agreement. Validate by reflecting the
specific feeling instead.
- user_message threaded through ResponseRephraser.verify call so the
capitulation check has the context it needs.
- New eval/sweep_sycophancy_probes.py: 10 single-turn + 5 multi-turn
probes (15 user turns) where the user explicitly pressures for
agreement with harmful/illegal/methodless content. Before fix: 1 leak
("you're right" in escalation_isol…
V4.2 part 2: authority-misconduct route + conversation history in rephraser
Authority-misconduct route
- New SupportRoute.AUTHORITY_MISCONDUCT. Detects (authority noun) AND
(problematic content) in the same message: counselor/advisor/professor/
RA/coach/dean/etc. + harm/harassment/discrimination/retaliation/
confidentiality-breach phrasing.
- Detection runs BEFORE advisor_conflict and care_violence_confidential
so alleged ethics violations don't get absorbed into academic-setback
or generic care templates.
- ALWAYS_DIRECT route: skips LISTEN/PERMISSION, goes straight to OFFER
with a template that names UMD OCRSM, Office of Student Conduct, and
Dean of Students. Template deliberately does NOT validate the
authority's standing or treat the alleged conduct as ordinary advice.
- Two new registry entries: umd_ocrsm (https://ocrsm.umd.edu/) and
umd_student_conduct (https://studentconduct.umd.edu/). Both verified
live 2026-05-10. Registry now 34 nodes.
- The exact prompt that failed in real-conversation review ("my
counsellor say the best thing …
V4.2 part 1: fix broken ISSS URL, ship URL audit + Karthik data brief
Root cause of demo 404
- UMD restructured the global office from globalmaryland.umd.edu to
marylandglobal.umd.edu and reorganized the path.
- Three service_graph rows (umd_isss, umd_isss_academic_difficulty,
umd_isss_opt_cpt) plus the INTERNATIONAL_SOURCE_HINT in response_planner.py
all pointed at the dead old path -> "Open ↗" on the ISSS source card 404'd.
- Replaced with https://isss.umd.edu/ (the canonical that auto-redirects to
the new path so we survive future UMD reorgs).
- Bumped last_verified on the 3 rows; preserved migration note for future
maintainers.
URL audit pipeline
- eval/audit_resource_urls.py walks every service_graph URL + every corpus
URL, HEADs/GETs each with a polite UA, reports HTTP status and redirects,
writes a timestamped Markdown report.
- Current state after fix: 60/63 live. The 3 SAMHSA "errors" are TLS
handshake quirks with python urllib (browsers load them fine; Karthik's
audit confirmed live).
- .gitignore: timestamped audit reports e…
V4.1: fix repetition/scroll/topbar, add PDF export, counselor-assist note
Bug fixes
- Flatten topbar wrapping so Save / Reset buttons render reliably; the
Column+Row nesting was getting collapsed on narrower viewports.
- Chatbot auto-scroll: install a MutationObserver via gr.Blocks(js=) that
pins the chat surface to the bottom on every streaming token update.
- Minimal-response repetition: "yes" / "no" / "ok" / "maybe" with no other
content used to re-render the same OFFER template (because the planner
saw no new intent). Now routes to a "clarify" stage with a short
open-ended question, e.g. "Which part of what we just talked about feels
most useful to start with?" Output guard skipped for clarify stage.
Counselor-friendly PDF export
- src/pipeline/support_plan.py adds build_support_plan_pdf via fpdf2.
Same content as the Markdown version, formatted for print: title block,
scope disclaimer in an inset box, section heads in accent color,
per-bullet user-message echo, deduped routes / actions / resources.
- Topbar now exposes two download bu…
V4: streaming, controlled paraphrasing, support plan, voice, sweeps
Day 1 of 7-day push toward class demo + counseling-center pitch.
Hybrid architecture: deterministic planner stays in charge of WHAT to
say (route, stage, recommended action, F-1 sub-topic, named resources);
the LLM only paraphrases that output into prose that mirrors the user's
words. Output guard runs after rephrasing as the trust boundary.
New files:
- src/pipeline/rephraser.py — ResponseRephraser orchestrator + four
providers (Groq Llama 3.3 70B, Anthropic Haiku 4.5, MockProvider for
testing, DeterministicProvider as final fallback only). Providers
loop tries each in order; if none returns content that passes
verify_rephrased_safety, the orchestrator surfaces deterministic
fallback with the actual error from each provider.
- src/pipeline/llm_safety.py — verify_rephrased_safety catches scope
drift ("as a therapist"), V1 regression ("I'm always here"),
fabricated UMD resource names not in retrieved sources, ungrounded
phone numbers (988 allow-listed), and length sanit…
Add stage-aware listening planner and F-1 student awareness
Previous responses prescribed action on turn 1 even when the user was
just venting, and there was no acknowledgment that an F-1 student
worried about deportation faces a different layer of fear than ordinary
academic stress. UMD also has a dedicated office (ISSS) for F-1
academic-difficulty advising and SEVIS, which the system never
surfaced.
This change introduces a three-stage conversation model
(LISTEN -> PERMISSION -> OFFER) that defaults to acknowledgment + open
invitation on turn 1 for listen-eligible routes, soft resource hints
on turn 2, and full action with named resources on turn 3 or any
explicit ask. Each stage has a distinct opener so back-to-back turns
no longer repeat the same validation phrase. Always-direct routes (ADS,
counseling navigation, basic needs, advisor conflict, peer-helper,
crisis, out-of-scope) skip the listening stages and go straight to
OFFER.
International / F-1 detection is a cross-cutting concern with session
memory: once visa/deport/F-1/SEVIS keywo…