fix(bot): close dialog engine gaps (C-SSRS repeat, scale intros, button payloads, step timeout)
CI / ruff · pytest (pull_request) Successful in 1m19s
CI / ruff · pytest (pull_request) Successful in 1m19s
- 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>
This commit is contained in:
@@ -24,14 +24,14 @@ Data algorithm (medical data, jury requirement):
|
||||
alcohol interaction, visit offer) come from the rules through `MonitoringService` (ADR-007).
|
||||
* A free-text message classified POSSIBLE by the safety layer is taken over by the engine (C-SSRS). When the
|
||||
screen returns to `daily.text` the message counts as the answer: the check-in is closed, not asked again.
|
||||
* A check-in left unfinished is dropped on the next system event of a later day.
|
||||
* A check-in left unfinished is dropped on the next event of a later day, or after
|
||||
`settings.abandon_after_hours` of silence (engine abandon timeout, #56) — answers given so far stay stored.
|
||||
|
||||
Also here: friendly intros for scheduled scales (pcl5, phq9, dar5, auditc) — the engine starts `scale_due`
|
||||
scales without an intro, so an exact `scale.<id>` enter hook adds one and delegates to the generic scale hook.
|
||||
Also here: friendly intros for scheduled scales (pcl5, phq9, dar5, auditc), registered with
|
||||
`register_scale_intro` so the engine's `scale_due` / rule-triggered sessions show them.
|
||||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
@@ -59,9 +59,8 @@ from app.dialog.flows.meds import (
|
||||
save_checkin,
|
||||
side_effect_reply,
|
||||
)
|
||||
from app.dialog.fsm import Ctx, Step, enter_hook_for, on_enter, state, trigger
|
||||
from app.dialog.scale_session import PREFIX as SCALE_PREFIX
|
||||
from app.dialog.scale_session import active_scale, scale_state
|
||||
from app.dialog.fsm import Ctx, Step, abandonable, on_enter, state, trigger
|
||||
from app.dialog.scale_session import register_scale_intro
|
||||
|
||||
PREFIX = "daily"
|
||||
STEPS = ("mood", "med", "sleep", "nightmares", "alcohol", "text")
|
||||
@@ -346,6 +345,9 @@ async def _free_text(ctx: Ctx, ck: dict[str, Any]) -> Step:
|
||||
return _finish(ctx, replies)
|
||||
|
||||
|
||||
abandonable(PREFIX)
|
||||
|
||||
|
||||
@state(PREFIX)
|
||||
async def _handle(ctx: Ctx, event: Event) -> Step:
|
||||
step = ctx.state.split(".", 1)[1] if "." in ctx.state else ""
|
||||
@@ -451,30 +453,13 @@ async def _idle_text(ctx: Ctx, event: Event) -> Step:
|
||||
# ------------------------------------------------------------------------------------------- scale intros
|
||||
|
||||
|
||||
async def _scale_intro(ctx: Ctx) -> Step:
|
||||
"""Add the friendly intro of a scheduled (or rule-triggered) scale, then run the generic scale enter hook.
|
||||
|
||||
Onboarding sessions (#36) and sessions started with their own intro are left untouched.
|
||||
"""
|
||||
scale_id = active_scale(ctx.state)
|
||||
sessions = dict(ctx.data.get("scales") or {})
|
||||
session = sessions.get(scale_id) if scale_id else None
|
||||
if (
|
||||
scale_id is not None
|
||||
and session is not None
|
||||
and session.get("idx") == 0
|
||||
and not session.get("intro_shown")
|
||||
and not session.get("intro")
|
||||
and ".onboarding" not in str(session.get("return_state", ""))
|
||||
):
|
||||
intros = load_daily_texts().scale_intros
|
||||
intro = (intros.scheduled if session.get("scheduled", True) else intros.unscheduled).get(scale_id)
|
||||
if intro:
|
||||
sessions[scale_id] = {**session, "intro": intro}
|
||||
ctx = replace(ctx, data={**ctx.data, "scales": sessions})
|
||||
generic = enter_hook_for(SCALE_PREFIX)
|
||||
return await generic(ctx) if generic is not None else Step()
|
||||
def _scale_intro(ctx: Ctx, scale_id: str, scheduled: bool, return_state: str) -> str | None:
|
||||
"""Friendly intro of a scheduled (or rule-triggered) scale. Onboarding sessions (#36) have their own texts."""
|
||||
if ".onboarding" in return_state:
|
||||
return None
|
||||
intros = load_daily_texts().scale_intros
|
||||
return (intros.scheduled if scheduled else intros.unscheduled).get(scale_id)
|
||||
|
||||
|
||||
for _scale_id in load_daily_texts().settings.intro_scales:
|
||||
on_enter(scale_state(_scale_id))(_scale_intro)
|
||||
register_scale_intro(_scale_id, _scale_intro)
|
||||
|
||||
Reference in New Issue
Block a user