Fastwhisper / app /backup_replies.py
Mbonea's picture
Add loop-break daily fields, proof wizard, and high-intensity coach interrupts.
4c23c1f
Raw
History Blame Contribute Delete
10.9 kB
"""Hardcoded coach backup rules ordered by priority.
Selects the highest-priority matching rule; fills SERVER_PICK when allowed.
"""
from __future__ import annotations
from typing import Any
RULES: list[dict[str, Any]] = [
{
"id": "crisis",
"priority": 100,
"keywords": ["suicide", "kill myself", "self-harm", "self harm", "end it"],
"tags": ["crisis"],
"template": (
"LOOP: crisis\n"
"FEELING: overwhelm\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: Contact local emergency / crisis resources now\n"
"NEXT_BRICK: S\n"
"DO_NOT: Isolate or escalate alone\n"
"LINE: Get help before anything else\n"
"NOTE: This is not therapy; seek real support\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "high_intensity_fse",
"priority": 95,
"keywords": [
"shame",
"freeze",
"rerun",
"fantasy",
"daydream",
"movie in my head",
"stuck indoors",
],
"tags": [
"shame",
"freeze",
"fantasy",
"indoors",
],
"template": (
"LOOP: high_fse\n"
"FEELING: spike\n"
"INTENSITY_GUESS: high\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: B\n"
"DO_NOT: Label more or feed the movie with cinematic headphones\n"
"LINE: Leave the room; one proof step\n"
"NOTE: Interrupt walk without movie music, or one admin line\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "court",
"priority": 90,
"keywords": ["court", "argue with myself", "verdict"],
"tags": ["court"],
"template": (
"LOOP: court\n"
"FEELING: judgment\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: E\n"
"DO_NOT: Reopen the case tonight\n"
"LINE: Close court; one brick only\n"
"NOTE: Silence after one boundary line\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "rerun",
"priority": 80,
"keywords": ["rerun", "replay", "looping the scene"],
"tags": ["rerun"],
"template": (
"LOOP: rerun\n"
"FEELING: stuck\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: S\n"
"DO_NOT: Replay the scene again\n"
"LINE: Interrupt and move body once\n"
"NOTE: Water or short walk\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "rage_movie",
"priority": 70,
"keywords": ["rage", "movie", "fantasy fight"],
"tags": ["rage", "movie"],
"template": (
"LOOP: rage_movie\n"
"FEELING: heat\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: B\n"
"DO_NOT: Feed the movie\n"
"LINE: Name it and stop the reel\n"
"NOTE: One hygiene or environment act\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "daydream_fc",
"priority": 60,
"keywords": ["daydream", "fantasy", "fc"],
"tags": ["daydream", "fc"],
"template": (
"LOOP: daydream\n"
"FEELING: drift\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: A\n"
"DO_NOT: Skip today's brick\n"
"LINE: Brick first, then optional rest\n"
"NOTE: Daydream only after brick\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "urge_corn",
"priority": 55,
"keywords": ["urge", "corn", "delay"],
"tags": ["urge", "corn"],
"template": (
"LOOP: urge\n"
"FEELING: pull\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: C\n"
"DO_NOT: Stack sessions\n"
"LINE: Delay, then one brick\n"
"NOTE: Ceiling is one session\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "bully",
"priority": 50,
"keywords": ["bully", "harsh voice", "attack myself"],
"tags": ["bully"],
"template": (
"LOOP: bully\n"
"FEELING: harsh\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: E\n"
"DO_NOT: Argue with the voice\n"
"LINE: One boundary line then silence\n"
"NOTE: No pep talk\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "shame",
"priority": 40,
"keywords": ["shame", "embarrassed", "humiliated"],
"tags": ["shame"],
"template": (
"LOOP: shame\n"
"FEELING: shame\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: B\n"
"DO_NOT: Hide and ruminate\n"
"LINE: Small body reset, then brick\n"
"NOTE: Keep it practical\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "spain_admin",
"priority": 35,
"keywords": ["admin", "paperwork", "travel", "deadline"],
"tags": ["admin", "travel"],
"template": (
"LOOP: admin\n"
"FEELING: pressure\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: A\n"
"DO_NOT: Cancel committed process from fear\n"
"LINE: One admin checklist item\n"
"NOTE: Fear is not a cancel signal\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "build_earn",
"priority": 30,
"keywords": ["build", "earn", "ship", "work"],
"tags": ["build", "earn"],
"template": (
"LOOP: build\n"
"FEELING: drive\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: C\n"
"DO_NOT: Open five fronts\n"
"LINE: Ship one small unit\n"
"NOTE: One earn/build act\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "home",
"priority": 20,
"keywords": ["home", "mess", "environment"],
"tags": ["home"],
"template": (
"LOOP: home\n"
"FEELING: clutter\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: B\n"
"DO_NOT: Redesign everything\n"
"LINE: One hygiene or tidy act\n"
"NOTE: Environment first\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "loneliness",
"priority": 10,
"keywords": ["lonely", "alone", "isolation"],
"tags": ["lonely"],
"template": (
"LOOP: loneliness\n"
"FEELING: lonely\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: D\n"
"DO_NOT: Spiral into absence stories\n"
"LINE: One logistics line, then brick\n"
"NOTE: Keep contact practical\n"
"DATA_THIN: {data_thin}"
),
},
{
"id": "default",
"priority": 0,
"keywords": [],
"tags": [],
"template": (
"LOOP: default\n"
"FEELING: mixed\n"
"INTENSITY_GUESS: n/a\n"
"REMEDY: {{server_pick}}\n"
"NEXT_BRICK: A\n"
"DO_NOT: Overthink the next hour\n"
"LINE: Do the next small brick\n"
"NOTE: Checklist over story\n"
"DATA_THIN: {data_thin}"
),
},
]
DEFAULT_PICK = "Do one listed brick: water, admin item, or tidy act"
INTERRUPT_PICK = (
"Leave the room, cold water on face, or a 10-min interrupt walk "
"without cinematic headphones — then one proof line (admin/message)"
)
def select_backup_rule(
text: str,
tags: list[str],
*,
intensity: int | None = None,
) -> dict[str, Any]:
"""Return the highest-priority rule matching keywords or tags.
Intensity ≥ 7 forces the high_intensity_fse rule (unless crisis matches).
"""
haystack = (text or "").lower()
tag_set = {t.strip().lower() for t in tags if t and t.strip()}
ordered = sorted(RULES, key=lambda rule: rule["priority"], reverse=True)
crisis = next((r for r in ordered if r["id"] == "crisis"), None)
if crisis:
keywords = crisis.get("keywords") or []
rule_tags = {t.lower() for t in (crisis.get("tags") or [])}
if any(keyword in haystack for keyword in keywords) or (tag_set & rule_tags):
return crisis
if intensity is not None and intensity >= 7:
high = next((r for r in ordered if r["id"] == "high_intensity_fse"), None)
if high:
return high
for rule in ordered:
keywords = rule.get("keywords") or []
rule_tags = {t.lower() for t in (rule.get("tags") or [])}
if any(keyword in haystack for keyword in keywords):
return rule
if tag_set & rule_tags:
return rule
return ordered[-1]
def _sanitize_pick(pick: str, *, force_interrupt: bool) -> str:
lowered = (pick or "").strip().lower()
if force_interrupt or "label" in lowered:
return INTERRUPT_PICK
return pick or DEFAULT_PICK
def render_backup(
rule: dict[str, Any],
*,
server_picks: list[dict[str, Any]],
data_thin: bool,
intensity: int | None = None,
) -> str:
"""Fill the selected rule template with pick and DATA_THIN."""
pick = server_picks[0]["remedy_key"] if server_picks else DEFAULT_PICK
force_interrupt = rule["id"] == "high_intensity_fse" or (
intensity is not None and intensity >= 7
)
pick = _sanitize_pick(pick, force_interrupt=force_interrupt)
if rule["id"] == "crisis":
text = rule["template"]
else:
text = rule["template"].replace("{{server_pick}}", pick)
return text.format(data_thin=str(data_thin).lower())
def backup_reply(
text: str,
tags: list[str],
server_picks: list[dict[str, Any]],
*,
data_thin: bool,
intensity: int | None = None,
) -> tuple[str, str]:
"""Select and render a backup reply; return (text, rule_id)."""
rule = select_backup_rule(text, tags, intensity=intensity)
return (
render_backup(
rule,
server_picks=server_picks,
data_thin=data_thin,
intensity=intensity,
),
rule["id"],
)