"""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"], )