CI / ruff · pytest (pull_request) Successful in 22s
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>
80 lines
2.3 KiB
Python
80 lines
2.3 KiB
Python
"""Clinical modules config (`config/modules.yaml`, ADR-001) and scale scheduling.
|
|
|
|
Shared by the dialog engine (lane A: start screening, daily question) and the scheduler (lane B: due scales).
|
|
"""
|
|
|
|
from datetime import date
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
import yaml
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from app.config import get_settings
|
|
|
|
ModuleId = Literal["veteran", "depression", "family"]
|
|
|
|
|
|
class _Strict(BaseModel):
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
|
|
class ScheduleEntry(_Strict):
|
|
scale: str
|
|
every_days: int = Field(gt=0)
|
|
from_day: int = 0
|
|
until_day: int | None = None
|
|
|
|
|
|
class Threshold(_Strict):
|
|
scale: str
|
|
op: Literal[">=", ">"]
|
|
value: int
|
|
|
|
|
|
class ModuleDef(_Strict):
|
|
title: str
|
|
start_scales: list[str]
|
|
start_questions: list[str] = Field(default_factory=list)
|
|
daily_question: str | None = None
|
|
schedule: list[ScheduleEntry] = Field(default_factory=list)
|
|
enables_depression_if: Threshold | None = None
|
|
enhanced_monitoring_under_age: int | None = None
|
|
|
|
|
|
class CoreDef(_Strict):
|
|
daily: list[str]
|
|
silence_alert_days: int
|
|
item9_followup: str
|
|
|
|
|
|
class ModulesConfig(_Strict):
|
|
core: CoreDef
|
|
modules: dict[ModuleId, ModuleDef]
|
|
|
|
|
|
@lru_cache
|
|
def load_modules(path: Path | None = None) -> ModulesConfig:
|
|
path = path or get_settings().config_dir / "modules.yaml"
|
|
return ModulesConfig.model_validate(yaml.safe_load(path.read_text(encoding="utf-8")))
|
|
|
|
|
|
def due_scales(
|
|
modules: set[str], therapy_day: int, last_done: dict[str, date], today: date, config: ModulesConfig | None = None
|
|
) -> list[str]:
|
|
"""Scales due today for a patient: per enabled module, a scale is due if never done in its window or
|
|
`every_days` have passed since it was last completed. Deduplicated, in config order."""
|
|
cfg = config or load_modules()
|
|
out: list[str] = []
|
|
for mod_id, mod in cfg.modules.items():
|
|
if mod_id not in modules:
|
|
continue
|
|
for e in mod.schedule:
|
|
if therapy_day < e.from_day or (e.until_day is not None and therapy_day > e.until_day):
|
|
continue
|
|
last = last_done.get(e.scale)
|
|
if (last is None or (today - last).days >= e.every_days) and e.scale not in out:
|
|
out.append(e.scale)
|
|
return out
|