"""Patient-facing interpretation of questionnaire results: deterministic rules, texts from config/texts/results.yaml. Stage 1 («Опросники для MVP»): screening levels per scale → a preliminary conclusion and the diary modules of stage 2. Stage 2 («Этап 2»): full scales on schedule (level + change: response, remission, worsening) and diary flags. Nothing here diagnoses: the conclusion is a screening result, the diagnosis is the doctor's (ADR-007, guardrail 3). """ from typing import Any import yaml from app.config import get_settings from app.core.intake_questions import content from app.core.questionnaire import IntakeDecision, IntakePolicy from app.core.safety import fill_phones # Stage 1 finding → diary module of stage 2 (PTSD is the «veteran» module of the profile). FINDING_MODULE = {"depression_suspected": "depression", "ptsd_suspected": "veteran"} # Remission and improvement cut-offs of the full scales («Этап 2», p.5: Kroenke 2001; Marx et al., 2022). PHQ9_REMISSION_BELOW = 5 PHQ9_IMPROVED_BY = 5 PCL5_RESPONSE_BY = 10 PCL5_IMPROVED_BELOW = 28 def texts() -> dict[str, Any]: return yaml.safe_load(fill_phones((get_settings().config_dir / "texts/results.yaml").read_text(encoding="utf-8"))) def render(interpretation: dict[str, Any]) -> str: """Plain text of an interpretation for the dialog reply (title, lines, then the disclaimer if any).""" parts = [interpretation["title"], *interpretation.get("lines", [])] if interpretation.get("disclaimer"): parts.append(interpretation["disclaimer"]) return "\n\n".join(p for p in parts if p) def _alcohol_zone(totals: dict) -> int | None: full, short = totals.get("rus_audit") or {}, totals.get("rus_audit_s") or {} return full.get("zone") if full.get("complete") else short.get("zone") def intake_findings(totals: dict, answers: dict, policy: IntakePolicy) -> list[str]: """Screening findings of stage 1, in the order they are shown.""" found = [] phq = totals.get("phq9") or {} if phq.get("complete"): if phq["total"] >= policy.phq9_specialist_at: found.append("depression_suspected") elif phq["total"] >= policy.phq9_psychologist_at: found.append("depression_mild") pcl, tsq = totals.get("pcl5") or {}, totals.get("tsq") or {} if pcl.get("complete") and pcl["total"] >= policy.pcl5_specialist_at: found.append("ptsd_suspected") elif answers.get("trauma") == 1 and tsq.get("complete") and tsq["total"] >= policy.tsq_expand_at: found.append("ptsd_signs") zone = _alcohol_zone(totals) if zone == 4: found.append("alcohol_high") elif zone in (2, 3): found.append("alcohol_risky") if (totals.get("cssrs_recent") or {}).get("risk") == "low": found.append("suicide_low") if answers.get("tbi") == 1: found.append("tbi") return found def intake_interpretation(totals: dict, answers: dict, decision: IntakeDecision, policy: IntakePolicy) -> dict: t = texts() found = intake_findings(totals, answers, policy) modules = [FINDING_MODULE[f] for f in found if f in FINDING_MODULE] head = t["intake"]["attention" if found else "ok"] lines = [head["text"]] lines += [f"{t['findings'][f]['title']}. {t['findings'][f]['text']}" for f in found] if decision.primary_route in t["routes"]: lines.append(t["routes"][decision.primary_route]) names = ", ".join(t["modules"]["names"][m] for m in modules) lines.append(t["modules"]["diary"].format(modules=t["modules"]["with"].format(names=names) if names else "")) return { "kind": "intake", "title": head["title"], "lines": lines, "codes": found or ["ok"], "findings": [{"code": f, "title": t["findings"][f]["title"]} for f in found], "route": decision.primary_route, "modules": modules, "disclaimer": t["disclaimer"], } def _phq9_level(total: int) -> str: band = next(b for b in content()["forms"]["phq9"]["severity"] if b["min"] <= total <= b["max"]) return band["code"] def scale_interpretation( kind: str, totals: dict, *, baseline: int | None, previous: int | None, policy: IntakePolicy, retriage_delta: int, ) -> dict: """A scheduled full scale: level, then the change against the start of observation and the last measurement.""" t = texts()["scales"] if kind == "rus_audit_s": zone = _alcohol_zone(totals) result = totals.get("rus_audit") if (totals.get("rus_audit") or {}).get("complete") else totals["rus_audit_s"] level = t[kind]["levels"][f"zone_{zone}" if zone else "unknown"] line = t["score"].format(title=t[kind]["title"], total=result.get("total"), level=level) return { "kind": kind, "title": t[kind]["title"], "lines": [line], "codes": [f"zone_{zone}" if zone else "unknown"], } total = totals[kind]["total"] pcl5_level = "probable" if total >= policy.pcl5_specialist_at else "below" code = _phq9_level(total) if kind == "phq9" else pcl5_level lines = [t["score"].format(title=t[kind]["title"], total=total, level=t[kind]["levels"][code])] change = None if previous is not None and total - previous >= retriage_delta: change = "worse" elif baseline is not None: if kind == "phq9": if total < PHQ9_REMISSION_BELOW: change = "remission" elif baseline > 0 and total <= baseline / 2: change = "response" elif baseline - total >= PHQ9_IMPROVED_BY: change = "improved" else: change = "stable" else: if baseline - total >= PCL5_RESPONSE_BY: change = "response" elif total < PCL5_IMPROVED_BELOW <= baseline: change = "improved" else: change = "stable" if change: lines.append(t["change"][change]) return {"kind": kind, "title": t[kind]["title"], "lines": lines, "codes": [code, *([change] if change else [])]} def diary_interpretation(part: str, flags: tuple[str, ...] | list[str]) -> dict: t = texts()["diary"] shown = [f for f in flags if f in t["flags"]] return { "kind": f"diary_{part}", "title": t["saved"].format(part=t["parts"][part]), "lines": [t["flags"][f] for f in shown], "codes": list(flags), } def cssrs_interpretation(risk: str | None) -> dict: t = texts()["cssrs"] return { "kind": "cssrs_since_contact", "title": t["low" if risk == "low" else "none"], "lines": [], "codes": [risk or "none"], }