"""API schemas (frozen contract, #34). Dashboard (lane C) is built against these; lane B implements them (#12). Change only via a separate small contract PR. Colors follow ADR-006: red = acute, yellow = high/moderate, green = nothing open, grey = info only (e.g. silence). """ from datetime import date, datetime from typing import Literal from pydantic import BaseModel, Field from app.dialog.contracts import Event, Reply from app.integrations.base import Prescription Color = Literal["red", "yellow", "green", "grey"] LevelName = Literal["info", "moderate", "high", "acute"] Module = Literal["veteran", "depression", "family"] Metric = Literal["phq9", "pcl5", "gad7", "auditc", "dar5", "mood", "sleep_hours", "alcohol"] class DiscontinuationRisk(BaseModel): """Rule-based index (not ML) of the risk to drop therapy: missed doses, side effects, quit intent, low mood.""" score: int = Field(ge=0, le=100) level: Literal["low", "medium", "high"] factors: list[str] = Field(default_factory=list) # short Russian labels for the doctor class PatientListItem(BaseModel): token: str display_name: str # "Андрей С." — minimal identification for the treating doctor age: int modules: list[Module] drug: str | None = None # "сертралин 50 мг" therapy_day: int next_visit: date | None = None status_color: Color top_alert_level: LevelName | None = None open_alerts: int = 0 phq9_last: int | None = None pcl5_last: int | None = None trend: Literal["up", "down", "flat", "unknown"] = "unknown" # direction of symptom scores (up = worse) adherence_7d: float | None = Field(default=None, ge=0, le=1) discontinuation_risk: DiscontinuationRisk last_event: str = "" class SeriesPoint(BaseModel): day: date metric: Metric value: float scheduled: bool = True # False for unscheduled scale sessions class DoseOut(BaseModel): planned_at: datetime drug: str status: Literal["taken", "missed", "skipped", "pending"] class AlertOut(BaseModel): id: int token: str patient_name: str ts: datetime level: LevelName color: Color code: str reason: str acked_by: str | None = None acked_at: datetime | None = None class PatientCard(BaseModel): patient: PatientListItem prescriptions: list[Prescription] series: list[SeriesPoint] doses: list[DoseOut] alerts: list[AlertOut] safety_plan: list[str] | None = None # Stanley–Brown steps, if composed with the doctor summary: str = "" # LLM summary built from structured data only (#19) class AckIn(BaseModel): doctor_id: str class ConnectPatientIn(BaseModel): doctor_id: str display_name: str birth_year: int sex: Literal["m", "f"] snils: str # synthetic only drug: str dose: str times: list[str] = Field(default_factory=lambda: ["08:00"]) instructions: str = "" next_visit: date | None = None modules: list[Module] = Field(default_factory=lambda: ["veteran"]) class ConnectPatientOut(BaseModel): token: str code: str # one-time connect code (esia_mock) deep_link: str # t.me/?start= or web-console link; dashboard renders it as QR class DialogIn(Event): """Same as dialog Event; kept as a separate name for OpenAPI clarity.""" class DialogOut(BaseModel): replies: list[Reply] class MessageOut(BaseModel): ts: datetime direction: Literal["in", "out"] text: str # scrubbed for "in", as sent for "out" kind: str = "normal" class ClockOut(BaseModel): now: datetime virtual: bool offset_days: float class ClockAdvanceIn(BaseModel): days: float = 0 hours: float = 0 class DemoPersona(BaseModel): token: str display_name: str therapy_day: int scenario: str | None = None # path of data/synthetic/.jsonl if scripted