Files
PTSRaiAgent/app/api/fixtures.py
Arxip222andClaude Opus 5.5 44814b7293 fix(clinical): red flags, one crisis rule, callback after a crisis, one table of numbers
Clinical review of the questionnaires (ADR-013):
- Red-flag block first in stage 1 (config/instruments/red_flags.yaml); any «Да» stops the
  questionnaire with an acute alert and the help screen of its category.
- One crisis rule: the crisis screen opens at a HIGH C-SSRS risk only; moderate stops and goes
  to a psychiatrist (24 h as a guide), low is a flag with the safety plan. PHQ-9 item 9 with a
  negative screener is a discrepancy flag; without a completed screener it asks the C-SSRS at once.
  config/questionnaire.yaml: cssrs_positive_policy graded (C01).
- Every crisis leaves a callback task for the doctor on duty (post_crisis_call, one per patient)
  and the app asks how the person is the next day, the safety plan one tap away.
- Routing: risk and flags first, scales second; PCL-5 >= 50 no longer means a priority
  appointment (pcl5_severe is moderate on the dashboard too).
- Crisis numbers live only in config/texts/crisis.yaml; texts and the agent prompt refer to them
  via {phone:<key>}. Fixes the children's helpline offered to adults by the chat agent.

Refs #92

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

286 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Deterministic fixture data returned by stub routes until #12 implements them (synthetic only).
Lets the dashboard (lane C) be built against the frozen schemas right away. Mirrors the demo cabinet from
docs/product/01_demo_persona.md: veteran Andrey on day 13 (red) plus seven other patients.
"""
from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo
from app.api.schemas import (
AlertOut,
DemoPersona,
DiscontinuationRisk,
DoseOut,
PatientCard,
PatientListItem,
SeriesPoint,
)
from app.core.safety import fill_phones
from app.integrations.base import Prescription
TZ = ZoneInfo("Europe/Moscow")
TODAY = date(2026, 10, 3)
DOCTOR_ID = "doc-smirnova"
# Synthetic personas for the demo console and the demo ЕМИАС sign-in of the patient web app.
DEMO_PERSONAS: list[DemoPersona] = [
DemoPersona(token="tok-andrey", display_name="Андрей С.", therapy_day=13, scenario="data/synthetic/andrey.jsonl"),
]
def _risk(score: int, factors: list[str]) -> DiscontinuationRisk:
level = "high" if score >= 60 else "medium" if score >= 30 else "low"
return DiscontinuationRisk(score=score, level=level, factors=factors)
PATIENTS: list[PatientListItem] = [
PatientListItem(
token="tok-andrey",
display_name="Андрей С.",
age=34,
modules=["veteran", "depression"],
drug="сертралин 50 мг",
therapy_day=13,
next_visit=TODAY + timedelta(days=15),
status_color="red",
top_alert_level="acute",
open_alerts=3,
phq9_last=18,
pcl5_last=44,
trend="up",
adherence_7d=1.0,
discontinuation_risk=_risk(55, ["хотел бросить (день 5)", "алкоголь 3 дня", "настроение ≤ 3"]),
last_event="PHQ-9 п.9 = 2 — кризисный протокол",
),
PatientListItem(
token="tok-dmitry",
display_name="Дмитрий В.",
age=41,
modules=["veteran"],
drug="сертралин 100 мг",
therapy_day=45,
status_color="yellow",
top_alert_level="moderate",
open_alerts=1,
phq9_last=11,
pcl5_last=48,
trend="up",
adherence_7d=0.86,
discontinuation_risk=_risk(25, ["бессонница"]),
last_event="кошмары, сон < 4 ч 3 ночи",
),
PatientListItem(
token="tok-sergey",
display_name="Сергей П.",
age=37,
modules=["veteran"],
drug="пароксетин 20 мг",
therapy_day=30,
status_color="yellow",
top_alert_level="moderate",
open_alerts=1,
phq9_last=12,
pcl5_last=41,
trend="flat",
adherence_7d=0.57,
discontinuation_risk=_risk(70, ["пропуски 3 из 7", "нет ответа 2 дня"]),
last_event="пропуски 3 из 7 дней",
),
PatientListItem(
token="tok-nikolay",
display_name="Николай Ф.",
age=52,
modules=["veteran"],
drug="сертралин 50 мг",
therapy_day=14,
status_color="yellow",
top_alert_level="moderate",
open_alerts=1,
phq9_last=13,
pcl5_last=39,
trend="flat",
adherence_7d=0.86,
discontinuation_risk=_risk(40, ["алкоголь 4 из 7"]),
last_event="алкоголь 4 из 7 дней",
),
PatientListItem(
token="tok-anna",
display_name="Анна Л.",
age=23,
modules=["depression"],
drug="флуоксетин 20 мг",
therapy_day=8,
status_color="yellow",
top_alert_level="info",
open_alerts=1,
phq9_last=15,
trend="flat",
adherence_7d=1.0,
discontinuation_risk=_risk(30, ["беспокойство"]),
last_event="возраст < 25: усиленный мониторинг",
),
PatientListItem(
token="tok-irina",
display_name="Ирина К.",
age=34,
modules=["depression"],
drug="эсциталопрам 10 мг",
therapy_day=21,
status_color="green",
phq9_last=9,
trend="down",
adherence_7d=1.0,
discontinuation_risk=_risk(5, []),
last_event="PHQ-9 16 → 9",
),
PatientListItem(
token="tok-olga",
display_name="Ольга Н.",
age=29,
modules=["depression"],
drug="сертралин 50 мг",
therapy_day=10,
status_color="green",
phq9_last=12,
trend="down",
adherence_7d=1.0,
discontinuation_risk=_risk(10, []),
last_event="тошнота прошла",
),
PatientListItem(
token="tok-pavel",
display_name="Павел Р.",
age=45,
modules=["depression"],
drug="эсциталопрам 10 мг",
therapy_day=60,
status_color="green",
phq9_last=4,
trend="down",
adherence_7d=1.0,
discontinuation_risk=_risk(5, []),
last_event="ремиссия, PHQ-9 = 4",
),
]
# Andrey's 13 days (docs/product/01_demo_persona.md): mood, sleep hours (bucket midpoint), alcohol 0/1/2.
_ANDREY_DAYS = [
(4, 7, 0),
(4, 5, 0),
(4, 5, 0),
(3, 5, 0),
(3, 5, 0),
(4, 7, 0),
(4, 7, 0),
(4, 3, 1),
(4, 3, 2),
(4, 3, 2),
(3, 5, 1),
(3, 5, 0),
(2, 3, 0),
]
def _dt(d: date, hh: int = 9) -> datetime:
return datetime.combine(d, time(hh), TZ)
def andrey_card() -> PatientCard:
start = TODAY - timedelta(days=12)
series: list[SeriesPoint] = []
for i, (mood, sleep, alc) in enumerate(_ANDREY_DAYS):
d = start + timedelta(days=i)
series += [
SeriesPoint(day=d, metric="mood", value=mood),
SeriesPoint(day=d, metric="sleep_hours", value=sleep),
SeriesPoint(day=d, metric="alcohol", value=alc),
]
series += [
SeriesPoint(day=start - timedelta(days=3), metric="phq9", value=14),
SeriesPoint(day=start - timedelta(days=3), metric="pcl5", value=46),
SeriesPoint(day=start - timedelta(days=3), metric="auditc", value=3),
SeriesPoint(day=start + timedelta(days=6), metric="phq9", value=13),
SeriesPoint(day=start + timedelta(days=6), metric="pcl5", value=44),
SeriesPoint(day=TODAY, metric="phq9", value=18, scheduled=False),
]
doses = [
DoseOut(planned_at=_dt(start + timedelta(days=i), 8), drug="сертралин", status="skipped" if i == 4 else "taken")
for i in range(13)
]
alerts = [
AlertOut(
id=3,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(TODAY, 21),
level="acute",
color="red",
code="phq9_item9",
reason="PHQ-9 п.9 = 2 (мысли о смерти/самоповреждении)",
),
AlertOut(
id=2,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(start + timedelta(days=9), 21),
level="moderate",
color="yellow",
code="alcohol_3d",
reason="Алкоголь 3 дня подряд на фоне СИОЗС",
),
AlertOut(
id=1,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(start + timedelta(days=9), 21),
level="moderate",
color="yellow",
code="sleep_lt4_3n",
reason="Сон < 4 ч три ночи подряд",
),
AlertOut(
id=0,
token="tok-andrey",
patient_name="Андрей С.",
ts=_dt(start + timedelta(days=4), 21),
level="high",
color="yellow",
code="quit_intent",
reason="Намерение прекратить приём",
acked_by=DOCTOR_ID,
acked_at=_dt(start + timedelta(days=5), 11),
),
]
return PatientCard(
patient=PATIENTS[0],
prescriptions=[Prescription(drug="сертралин", dose="50 мг", instructions="утром после еды", start_date=start)],
series=series,
doses=doses,
alerts=alerts,
safety_plan=[
"Мои сигналы: не сплю, пью, «всё бессмысленно»",
"Что помогает: прогулка с собакой, душ",
"Люди и места: брат Олег, спортзал",
"Кому звонить: жена, сослуживец Миша",
fill_phones("Специалисты: д-р Смирнова, {phone:helpline}, 112"),
"Безопасная среда: ружьё хранится у брата",
],
summary="За 13 дней: приверженность 12/13, эпизод намерения бросить на 5-й день (снят после звонка врача). "
"С 8-го дня сон < 4 ч и алкоголь; к 13-му дню настроение 2/10, внеплановый PHQ-9 = 18 (рост с 13), "
"п.9 = 2. Обсудить: безопасность, коррекция терапии сна, алкоголь.",
)
def card(token: str) -> PatientCard | None:
if token == "tok-andrey":
return andrey_card()
item = next((p for p in PATIENTS if p.token == token), None)
if item is None:
return None
return PatientCard(patient=item, prescriptions=[], series=[], doses=[], alerts=[], summary=item.last_event)
def all_alerts() -> list[AlertOut]:
return andrey_card().alerts