CI / ruff · pytest (pull_request) Successful in 2m33s
- 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>
287 lines
9.8 KiB
Python
287 lines
9.8 KiB
Python
"""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] = [
|
||
# ЕМИАС demo sign-in shows this persona as «Никита В.» (the one-click demo on the welcome screen is «Андрей С.»).
|
||
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
|