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