Files
PTSRaiAgent/app/dialog/texts.py
T
ElebobaandClaude Opus 5.5 d06720fdab
CI / ruff · pytest (pull_request) Successful in 1m19s
fix(bot): close dialog engine gaps (C-SSRS repeat, scale intros, button payloads, step timeout)
- No same-day C-SSRS repeat: a risk screen completed within cssrs_cooldown_hours is not queued again
  after PHQ-9 item 9 >= 1 (scale crisis path, rule schedule_scale, scale_due); crisis script and alerts stay.
- Scale intro registry: register_scale_intro(scale_id, text_or_fn); start_scale(intro=None) uses it.
  veteran_daily migrated from per-scale on_enter hooks.
- Prefix triggers "button:<prefix>:*" with trigger_payload(event); medication buttons carry the dose
  (med:taken:<dose_id>, ...), med_pending dropped; payload-less legacy buttons answer as stale.
- Abandoned-step timeout: settings.abandon_after_hours (default 6 h) returns abandonable states
  (daily.*, meds.*, scale.* not in onboarding) to idle before the next event; never in crisis or with intake on.

Refs #56

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

123 lines
2.4 KiB
Python

"""Patient-facing dialog texts from `config/texts/dialog.yaml` (ADR-008: texts live in config, not code)."""
from functools import lru_cache
from pathlib import Path
import yaml
from pydantic import BaseModel, ConfigDict, Field
from app.config import get_settings
class _Strict(BaseModel):
# A typo in a YAML key must fail at load time, not silently show an empty message to a patient.
model_config = ConfigDict(extra="forbid", frozen=True)
class DialogSettings(_Strict):
cssrs_cooldown_hours: float = 24
max_safety_events: int = 50
crisis_flags: list[str] = Field(default_factory=list)
risk_screen_scale: str = "cssrs_screen"
abandon_after_hours: float = 6
class Commands(_Strict):
revoke: list[str]
class ConsentTexts(_Strict):
version: str
text: str
accept: str
decline: str
refused: str
reconsider: str
code_invalid: str
class AgeGateTexts(_Strict):
question: str
adult: str
minor: str
unclear: str
minor_stop: str
class IdleButtons(_Strict):
checkin: str
med_question: str
contact_doctor: str
revoke: str
class IdleTexts(_Strict):
welcome: str
menu_prompt: str
buttons: IdleButtons
stub_checkin: str
stub_med_question: str
contact_doctor_ack: str
visit_stub: str
class RevokedTexts(_Strict):
confirm: str
class CrisisTexts(_Strict):
repeat: str
hold: str
closed: str
class ScaleTexts(_Strict):
intro: str
question: str
clarify: str
buttons_only: str
done: str
risk_screen_intro: str
class SafetyPlanTexts(_Strict):
title: str
template: list[str]
means_restriction: str
class ReachOutTexts(_Strict):
silence: str
missed_doses: str
default: str
class VisitTexts(_Strict):
button: str
class DialogTexts(_Strict):
status: str
settings: DialogSettings
commands: Commands
consent: ConsentTexts
age_gate: AgeGateTexts
idle: IdleTexts
revoked: RevokedTexts
crisis: CrisisTexts
scale: ScaleTexts
safety_plan: SafetyPlanTexts
reach_out: ReachOutTexts
visit: VisitTexts
unavailable: str
def dialog_texts_path() -> Path:
return get_settings().config_dir / "texts" / "dialog.yaml"
@lru_cache
def load_dialog_texts(path: Path | None = None) -> DialogTexts:
path = path or dialog_texts_path()
return DialogTexts.model_validate(yaml.safe_load(path.read_text(encoding="utf-8")))