Files
PTSRaiAgent/app/api/fixtures.py
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

287 lines
9.8 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] = [
# ЕМИАС 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