"""Read-only catalog of questionnaire items for patient clients (web app). The dialog engine sends a questionnaire question as plain text plus buttons; the button payload carries the item id (`iq:::`, `scale:::`). A client looks the item up here to show which form it belongs to, its position and recall period. Nothing here changes what is asked or how it is scored — the texts stay those of the versioned sources in config/. """ from functools import lru_cache from typing import Any, Literal import yaml from pydantic import BaseModel from app.config import get_settings from app.core.cssrs_content import load_cssrs_russian from app.core.intake_questions import content, texts from app.core.scales import load_scale Section = Literal["safety", "mood", "trauma", "alcohol", "about", "diary", "scale"] # Forms whose item order depends on earlier answers: a client shows the position without «из N». ADAPTIVE = {"cssrs", "rus_audit", "diary", "context"} class FormInfo(BaseModel): id: str title: str section: Section recall_period: str | None = None instruction: str | None = None class ItemInfo(BaseModel): form: str index: int # 1-based position in the form total: int adaptive: bool optional: bool = False numeric: bool = False class DrinksTable(BaseModel): """RUS-AUDIT item 2 standard-drink table, exactly as in config/texts/intake.yaml (the approved form).""" title: str header: list[str] rows: list[tuple[str, list[str]]] note: str class Catalog(BaseModel): forms: dict[str, FormInfo] items: dict[str, ItemInfo] standard_drinks: DrinksTable | None def _drinks() -> DrinksTable | None: raw = texts().get("standard_drinks") if not raw: return None lines = [line.strip() for line in raw.splitlines() if line.strip()] title, header_line, *rest = lines rows = [line for line in rest if ":" in line] note = " ".join(line for line in rest if ":" not in line) return DrinksTable( title=title, header=[h.strip() for h in header_line.split("·")], rows=[ (label.strip(), [c.strip() for c in cells.split("·")]) for label, cells in (r.split(":", 1) for r in rows) ], note=note, ) @lru_cache(maxsize=1) def catalog() -> Catalog: forms: dict[str, FormInfo] = {} items: dict[str, ItemInfo] = {} def add(form: FormInfo, ids: list[str], *, optional: set[str] = frozenset(), numeric: set[str] = frozenset()): forms[form.id] = form for i, item in enumerate(ids, 1): items.setdefault( item, ItemInfo( form=form.id, index=i, total=len(ids), adaptive=form.id in ADAPTIVE, optional=item in optional, numeric=item in numeric, ), ) cssrs = load_cssrs_russian() add(FormInfo(id="cssrs", title=cssrs.title, section="safety"), ["cssrs_" + i.id for i in cssrs.items]) intake_forms: dict[str, Any] = content()["forms"] for sid, section in (("phq9", "mood"), ("tsq", "trauma"), ("pcl5", "trauma"), ("rus_audit", "alcohol")): raw = intake_forms.get(sid) or {} scale = load_scale(sid) add( FormInfo( id=sid, title=raw.get("title") or scale.title, section=section, recall_period=raw.get("recall_period") or scale.recall_period, instruction=raw.get("instruction") or scale.instruction, ), list(scale.item_ids), ) add(FormInfo(id="trauma", title="Травматическое событие", section="trauma"), ["trauma", "recent_trauma"]) add(FormInfo(id="context", title="Общие вопросы", section="about"), ["sex", "tbi", "psychotropics"]) diary = yaml.safe_load((get_settings().config_dir / "instruments/diary.yaml").read_text(encoding="utf-8")) diary_items = [item for key in ("core", "ptsd", "depression", "suicide") for item in diary.get(key, [])] add( FormInfo(id="diary", title="Дневник самочувствия", section="diary"), [i["id"] for i in diary_items], optional={i["id"] for i in diary_items if i.get("optional")}, numeric={i["id"] for i in diary_items if i.get("numeric")}, ) # Scales of the onboarding / scheduled sessions (`scale::...` buttons): the form header only. for path in sorted((get_settings().config_dir / "scales").glob("*.yaml")): scale = load_scale(path.stem) forms.setdefault( scale.id, FormInfo( id=scale.id, title=scale.title, section="scale", recall_period=scale.recall_period, instruction=scale.instruction, ), ) return Catalog(forms=forms, items=items, standard_drinks=_drinks())