Files
PTSRaiAgent/app/api/schemas.py
ElebobaandClaude Opus 5.5 b4731fb1d6
CI / ruff · pytest (pull_request) Successful in 22s
chore(core): stage 1 contracts — dialog, API schemas + fixtures, integrations, monitoring, modules
Freeze interfaces between lanes A (bot), B (backend), C (dashboard) so three developers work in
parallel without touching each other's files: dialog Event/Reply + engine registry with echo stub,
integration protocols with in-memory defaults, MonitoringService with noop stub, /api/v1 schemas
with fixture-backed routes, frozen main.py wiring, config/modules.yaml + due_scales(), DAR-5,
tzdata, Andrey trajectory, lanes in WORKFLOW, ADR-010.

Refs #34

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:22:25 +03:00

139 lines
3.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.
"""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/<bot>?start=<code> 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/<persona>.jsonl if scripted