Files
ElebobaandClaude Opus 5.5 531bef1e73
CI / ruff · pytest (pull_request) Successful in 2m33s
feat: crisis message after the test and non-blocking; weapon answer triggers nothing; ЕМИАС demo = Никита
- CRISIS_BLOCKING (default off for the demo): a crisis no longer locks the dialog; the message and the
  doctor's acute alert still happen. The clinical protocol (blocking, immediate stop) stays behind the flag and
  keeps its tests.
- Crisis signals inside a questionnaire (intake red flags, high C-SSRS, PHQ-9 item 9) no longer stop it: the
  test is finished and the crisis message comes at the end; the doctor's alert is raised at once/at the end.
- Weapon access: asked and stored, triggers nothing (rule disabled, no red flag, no safety block).
- ЕМИАС demo persona is «Никита В.»; the one-click demo stays «Андрей С.».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:30:05 +03:00

58 lines
2.9 KiB
Python

"""Crisis path of the dialog (guardrail #2): fixed texts only, never generated by the LLM.
Decisions:
* The crisis script (`app.core.safety.crisis_reply`, `config/texts/crisis.yaml`) is sent verbatim, once per
open crisis. "Open" = `data["crisis"]["open"]` — set when the crisis starts, cleared only by the doctor's
acknowledgement (`doctor_ack`), so a patient repeating the phrase does not get the same long script again:
they get a short fixed support message and emergency resources instead.
* Entering the crisis drops unfinished scale sessions (their answered items are already stored item by item):
nothing else is asked while the patient is in crisis, except the C-SSRS screen.
* Without valid consent (before consent, after refusal/revocation, minors) we still answer a crisis message —
protecting life is the one processing ground that does not need consent (152-FZ art. 10 p. 2.3) — but with
a variant WITHOUT "your doctor has been notified", because no doctor is notified, and nothing is stored.
"""
from typing import Any
from app.config import get_settings
from app.core.safety import CrisisResource, CrisisScript, crisis_reply
from app.dialog.contracts import Reply
from app.dialog.fsm import Ctx, Step
def crisis_open(data: dict[str, Any]) -> bool:
return bool((data.get("crisis") or {}).get("open"))
def _resource_lines(resources: list[CrisisResource]) -> list[str]:
return [f"• {r.name}: {r.phone}" + (f" ({r.note})" if r.note else "") for r in resources]
def open_crisis(ctx: Ctx, categories: list[str], source: str) -> Step:
"""Start the crisis (full script) or, if one is already open, send the short repeat text.
`source` is a code for the dashboard/audit trail: "safety:lexicon", "safety:llm", "scale:phq9", "rule:<id>".
"""
script = crisis_reply(categories)
if crisis_open(ctx.data):
text = "\n".join([ctx.texts.crisis.repeat, *_resource_lines(script.resources)])
return Step(replies=[Reply(text=text, kind="crisis")])
crisis = {"open": True, "opened_at": ctx.now.isoformat(), "source": source, "variant": script.variant}
if not get_settings().crisis_blocking:
# Non-blocking (demo default): the script is shown, the doctor is alerted, the patient keeps going.
return Step(replies=[Reply(text=script.text, kind="crisis")], data={"crisis": crisis})
return Step(
replies=[Reply(text=script.text, kind="crisis")],
next_state="crisis",
data={"crisis": crisis, "scales": None},
)
def crisis_resource_lines(resources: list[CrisisResource]) -> list[str]:
return _resource_lines(resources)
def no_consent_crisis_text(script: CrisisScript) -> str:
"""Crisis script without the "doctor notified" line — for people we may not (or did not) notify about."""
return "\n".join([script.body, "", *_resource_lines(script.resources), "", script.grounding])