feat(bot): dialog engine — scrub → safety → FSM → monitoring, consent, age gate, crisis
CI / ruff · pytest (pull_request) Successful in 25s
CI / ruff · pytest (pull_request) Successful in 25s
- Engine.handle registered via register_engine; lexicon-first safety (no LLM on ACUTE), fixed crisis script once per open crisis, POSSIBLE → C-SSRS screen - FSM registry (@state/@on_enter/@trigger) with auto-loaded flows; core flow: new → consent → age_gate → module onboarding → idle, crisis, consent_revoked - reusable scale session (buttons, extract_item + clarify, item 9 / C-SSRS buttons-only, crisis flags), execution of monitoring dialog actions, texts in config/texts/dialog.yaml Refs #10 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""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"
|
||||
|
||||
|
||||
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")))
|
||||
Reference in New Issue
Block a user