403Webshell
Server IP : 157.230.181.24  /  Your IP : 216.73.217.11
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux conductive 6.8.0-117-generic #117-Ubuntu SMP PREEMPT_DYNAMIC Tue May 5 19:26:24 UTC 2026 x86_64
User :  ( 1000)
PHP Version : 8.3.31
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /opt/aim-bots/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /opt/aim-bots//persona_actor.py
#!/usr/bin/env python3
"""Generate novel AIM replies in a persona's voice — never copies archive lines."""

from __future__ import annotations

import random
import re
from collections import defaultdict
from dataclasses import dataclass, field


class StyleAdapter:
    REPLACEMENTS = (
        (r"\bDon't\b", "dont"),
        (r"\bdon't\b", "dont"),
        (r"\bCan't\b", "cant"),
        (r"\bcan't\b", "cant"),
        (r"\bWon't\b", "wont"),
        (r"\bwon't\b", "wont"),
        (r"\bI'm\b", "i'm"),
        (r"\bIt's\b", "it's"),
        (r"\bYou're\b", "you're"),
        (r"\bHaha\b", "haha"),
        (r"\bHahaha\b", "hahahah"),
        (r"\bHahah\b", "hahah"),
        (r"\bLol\b", "lol"),
        (r"\bWow\b", "wow"),
        (r"\bUnfortunately\b", "Unfortunantly"),
        (r"\bRofl\b", "rofl"),
    )

    def __init__(self, voice: dict) -> None:
        self.voice = voice
        self.lowercase_rate = voice.get("pct_lowercase_start", 0.7)
        self.no_punct_rate = voice.get("pct_no_end_punct", 0.9)

    def apply(self, text: str) -> str:
        text = text.strip()
        if not text:
            return text
        for pattern, repl in self.REPLACEMENTS:
            text = re.sub(pattern, repl, text)
        if random.random() < self.lowercase_rate and text[0].isupper() and not text.startswith("I"):
            text = text[0].lower() + text[1:]
        if (
            random.random() < self.no_punct_rate
            and len(text) < 40
            and text.endswith(".")
            and not text.endswith("...")
        ):
            text = text[:-1]
        return text

    def apply_all(self, parts: list[str]) -> list[str]:
        return [self.apply(p) for p in parts if p.strip()]


@dataclass
class ConversationState:
    topic: str = "idle"
    work_tonight: bool | None = None
    work_time: str | None = None
    plans_tonight: str | None = None
    last_user_question: str = ""
    greeted: bool = False
    turn: int = 0


class PersonaActor:
    """Multi-turn aware reply generator — answers questions and keeps chat moving."""

    TIMES = ["5", "5:30", "6", "6:30", "7", "7:30", "8", "like 6ish", "6 +", "10 30-ish", "after 7"]

    MAGUS = {
        "greets": [
            "sup man",
            "yo",
            "hey",
            "sup",
            "hey man",
            "yo dude",
            "whats goin on",
            "sup dude",
            "hey",
            "yo",
        ],
        "greet_follow": ["u?", "nm u?", "whatcha up to", "not much u?"],
        "activity": [
            "not much",
            "on the computer",
            "nothing",
            "just chillin",
            "browsing /b/ maybe",
            "listening to music",
            "same old",
            "Doing Something...",
        ],
        "activity_follow": ["u?", "what about u", "whatcha doing", "u doing anything?"],
        "work_yes": ["yeah i think so", "yeah unfortunantly", "probably yeah", "think so yeah"],
        "work_no": ["nah should be off", "dont think so", "nah im free"],
        "work_unsure": ["not totally sure yet", "might have to", "think so but idk"],
        "time_answers": TIMES,
        "work_sorry": ["yeah it sucks", "sucks", "unfortunantly", "that wasn't cool"],
        "laugh": ["haha", "hahah", "hahahah", "rofl", "rofl!", "rotfl"],
        "laugh_follow": ["owned", "smooth", "nice", "you're so awful", "A burn.", "for real", "hahaha"],
        "wow": [
            "haha wow",
            "might hop on later",
            "they took out the paladin mass ressurection skill",
            "WoW feels so weird",
            "yeah if im bored",
        ],
        "wow_follow": ["u on?", "rather go to a show tho", "help with this elite? lol"],
        "girls": [
            "dude tell me everything",
            "u gonna talk to her or what",
            "haha classic",
            "u need to just go for it",
            "is she hot tho",
            "you looked at her face?!",
            "smooth",
            "rofl",
        ],
        "girls_follow": ["details", "u gonna see her again?", "owned", "nice"],
        "shows": [
            "dude that show was insane",
            "maylene is coming through",
            "we HAVE to get tix",
            "we gotta get malachi and andrew in",
            "lydia and amy are coming right",
            "whole crew is going",
            "maylene never disappoints",
            "the pit was crazy last time",
            "HERE WE GOOOO",
            "keefe better not bail again",
            "dude we're going right",
            "chariot next month we gotta go",
        ],
        "shows_follow": [
            "get tix?",
            "tell dag",
            "charles in?",
            "when is it again?",
            "word",
            "we're all going",
            "dont bail on me",
        ],
        "hangout_yes": ["yeah maybe", "could", "we'll see", "yeah im down", "word"],
        "hangout_no": ["cant tonight", "no car", "maybe later", "nah"],
        "plan_push": [
            "check /b/ rn",
            "american idle is on",
            "dude maylene is coming we gotta get everyone",
            "u talk to that girl yet?",
            "monroe mall later?",
            "amy is being annoying lol",
            "u need a ride?",
            "elevation was so dumb today",
            "we still getting the crew for that show right",
            "did malachi say he was in?",
            "myspace is being dumb again",
            "u free tonight?",
        ],
        "return_after_gap": ["sup", "yo", "hey man", "sup back", "oh hey", "help"],
        "bye": ["later", "kk", "see ya", "k", "bai", "peace", "l8r"],
        "absurd": [
            "What do you have a gun for?",
            "No sound",
            "we know",
            "Who knows",
            "Why walk when you can ride?",
            "wtf, teletubbies?!",
            "wtf?",
            "Playing Burning Crusade Beta ;o",
            "i hate cosplaying",
            "only 13 episodes",
            "Mad crazy stuffs going down on the world",
            "Nickelodeon Studios was shut down.",
        ],
        "sarcasm": [
            "you're so awful",
            "that wasn't cool",
            "smooth",
            "owned",
            "nice",
            "Nice.",
            "A burn.",
            "it's a polite laugh",
            "have no idea",
            "yawn",
            "Woo woo.",
            "yeah D:",
            "cool down for an hour",
        ],
        "roast_follow": ["haha", "owned", "smooth", "for real", "classic"],
        "chan": [
            "dude check /b/",
            "4chan probabl",
            "http://img.4chan.org/b/src/1181021064656.jpg",
            "http://www.explosm.net/db/files/comicbulemia.png",
            "imagechan is wild today",
            "ty",
        ],
        "chaos": [
            "hahaha",
            "rofl",
            "rofl!!",
            "nice",
            "owned",
            "wtf",
            "help",
            "smooth",
            "you're so awful",
            "oh noes",
            "lol",
            "D:",
            ";p",
            "classic",
            "mm i bet",
            ".",
            "gah",
            "xD",
            "Hahha<3",
            "SQUIRRRTT",
            "TIEING YOUR SHOUUUEEEEE",
        ],
        "chaos_follow": [
            "nice",
            "owned",
            "hahaha",
            "wtf",
            "help",
            "you're so awful",
            "rofl",
        ],
    }

    CALEB = {
        "greets": ["sup", "yo", "hey", "sup cake", "hey man", "dude", "whats up"],
        "greet_follow": ["u?", "what u doing", "whatcha up to", "nm u?"],
        "activity": [
            "on the computer",
            "coding",
            "not much",
            "nothing",
            "web developerrrrr",
            "same old",
        ],
        "activity_follow": ["u?", "what about u"],
        "dev": ["yeah coding", "this bug sucks", "elevations stuff", "almost done", "working on it"],
        "dev_follow": ["u?", "idk"],
        "lydia": ["hey", "calebbbb", "yeah", "mhm", "aww"],
        "lydia_follow": ["=]", "haha"],
        "spencer": ["sup man", "dude", "rofl", "owned", "magus"],
        "spencer_follow": ["u in?", "for real"],
        "shows": ["we gotta go", "maylene", "got tix?", "chariot was insane"],
        "shows_follow": ["u in?", "tell spencer"],
        "laugh": ["haha", "hahah", "rotfl", "rofl!", "lmao"],
        "laugh_follow": ["owned", "nice", "for real"],
        "work_yes": ["yeah unfortunantly", "got work"],
        "work_no": ["nah im free", "should be off"],
        "bye": ["later", "night", "see ya", "g'night"],
        "generic": ["yeah", "true", "k", "kk", "idk", "maybe"],
        "plan_push": ["what u doing later", "u on?"],
        "chaos": ["rotfl", "wtf", "owned", "nice", "help", "hahahah"],
        "chaos_follow": ["dude", "for real"],
    }

    SOE = {
        "greets": ["sup", "hey", "yo", "sup man", "hey man", "oh", "so", "hi"],
        "greet_follow": ["u?", "whatcha doing", "nm u?"],
        "activity": [
            "not much just bored",
            "nothinggg",
            "im tiredddd",
            "homework",
            "just sitting here",
        ],
        "activity_follow": ["u?", "what about u", "whatcha up to"],
        "work_yes": ["yeah i work tonight", "unfortunantly yeah", "yeah got work"],
        "work_no": ["nah im off", "nope free tonight", "dont think so"],
        "work_unsure": ["maybe", "not sure yet", "might have to"],
        "time_answers": TIMES,
        "work_sorry": ["that sucks", "i know", "sorry"],
        "laugh": ["haha", "hahahahaha", "rotfl", "oh man"],
        "laugh_follow": ["i love it", "classic", "so dumb"],
        "wow": ["cool", "nice", "what level are you", "i should get back on"],
        "wow_follow": ["are u still playing", "sounds fun"],
        "hangout_yes": ["maybe", "could work", "we'll see"],
        "hangout_no": ["cant", "working", "too tired"],
        "plan_push": [
            "u doing anything later?",
            "u wanna hang out?",
            "what are u up to tonight",
        ],
        "return_after_gap": [
            "oh hey again",
            "sup",
            "hey",
            "oh hey",
            "hey again",
        ],
        "bye": ["night night", "pe@ce bro", "later", "ttyl"],
        "chaos": [
            "oh noes",
            "hahaha",
            "rotfllllll",
            "EWWWWW",
            "mm i bet",
            "ooo",
            "o",
            ".",
            ".>",
            "D:",
            "OMG",
            "you are the most horrible person i know",
            "HAHA COOOOOOOL",
            "smoke pot much?",
            "gah",
            "ahahaha",
            "yum",
            "lie to her!",
        ],
        "chaos_follow": [
            "hahaha",
            "rotfl",
            "omg",
            "D:",
            "nice",
            "wtf",
        ],
    }

    SIDE_CHICK = {
        "greets": [
            "heyy",
            "hey",
            "uh heyy",
            "yo",
            "heyyy",
            "calebbbb",
            "hey u",
            "sup",
        ],
        "greet_follow": ["whats up?", "u on?", "im bored", "nm u?", "what u doing"],
        "activity": [
            "chillin",
            "being lazy",
            "listening to music",
            "nothinggg",
            "on myspace",
            "so bored",
            "just woke up lol",
            "eating something",
        ],
        "activity_follow": ["u?", "what about u", "whatcha doing", "u doing anything?"],
        "flirt": [
            "you're so hot caleb",
            "mmmh",
            "thats hot",
            "lol stop",
            "you're bad",
            "omg",
            "seriously tho",
            "i like you",
            "you know you want to",
        ],
        "flirt_follow": ["lol", "haha", ":-)", "xD", "whatever", "prove it"],
        "pics": [
            "send me one lol",
            "pics or it didnt happen",
            "got any new ones?",
            "myspace doesnt count lol",
            "you first",
            "maybe if you're nice",
            "haha nice try",
        ],
        "pics_follow": ["lol", "jkjk", "maybe", "haha", "fine whatever"],
        "myspace": [
            "check my myspace",
            "i changed my layout again",
            "my top friends are stupid",
            "comment my pics",
            "whats your myspace again",
        ],
        "myspace_follow": ["lol", "k", "fine", "whatever"],
        "bored": [
            "im so bored",
            "entertain me",
            "say something funny",
            "ugh nothing to do",
            "someone talk to me",
        ],
        "bored_follow": ["lol", "your turn", "u first"],
        "laugh": ["haha", "hahah", "hahahaha", "rofl", "lol", "xD", ":-)", ":-D"],
        "laugh_follow": ["omg", "stop", "you're dumb", "whatever", "nice"],
        "chaos": [
            "omg",
            "ewww",
            "hahaha",
            "nice try",
            "you wish",
            "lol stop",
            "mm i bet",
            "whatever",
            "you're so bad",
        ],
        "chaos_follow": ["haha", "lol", "xD", "omg", "fine"],
        "compliment_back": [
            "thanks :]",
            "aww",
            "lol stop",
            "you're sweet",
            "haha whatever",
        ],
        "plan_push": [
            "what u doing later?",
            "u busy tonight?",
            "u wanna talk more?",
            "im gonna be up late",
        ],
        "return_after_gap": ["heyy", "oh hey", "sup", "yo", "u there?"],
        "bye": ["night", "ttyl", "later", "byeee", "night night"],
        "generic": [
            "yeah",
            "same",
            "true",
            "idk",
            "maybe",
            "lol",
            "haha",
            "for real",
            "whatever",
        ],
    }

    def __init__(self, persona: dict, voice: dict) -> None:
        self.persona = persona
        self.voice = voice
        self.style = StyleAdapter(voice)
        self.key = persona["screen_name"]
        if self.key == "conan0ftheday":
            self.bank = self.CALEB
        elif self.key == "magus5311":
            self.bank = self.MAGUS
        else:
            self.bank = self.SOE
        self.states: dict[str, ConversationState] = defaultdict(ConversationState)
        self.bands = voice.get("bands", [])
        self.show_crew = voice.get("show_crew", [])
        self.friend = voice.get("friend_name", "Caleb")
        self.pct_multi = voice.get("pct_multi", 0.55)
        self.flavor = voice.get("flavor_phrases", [])
        self.topical = voice.get("topical_refs", [])
        self.real_name = voice.get("real_name", "")

    def _pick(self, key: str) -> str:
        return random.choice(self.bank[key])

    def _state(self, sender: str) -> ConversationState:
        return self.states[sender]

    def _recent_context(self, history: list[dict], n: int = 6) -> str:
        lines = []
        for msg in history[-n:]:
            role = "Caleb" if msg["role"] == "user" else "You"
            lines.append(f"{role}: {msg['content']}")
        return "\n".join(lines)

    def _last_user_msgs(self, history: list[dict], n: int = 3) -> str:
        msgs = [m["content"] for m in history if m["role"] == "user"]
        return " ".join(msgs[-n:]).lower()

    def _update_state(self, state: ConversationState, text: str, history: list[dict]) -> None:
        lower = text.lower()
        ctx = self._last_user_msgs(history, 4)
        state.turn += 1
        state.last_user_question = text

        if re.search(r"\b(work|shift|job|schedule)\b", lower) or re.search(r"\b(work|shift|job)\b", ctx):
            state.topic = "work"
            if re.search(r"\b(no|nah|dont|not|off|free)\b.*\b(work|shift)\b", lower) or re.search(
                r"\b(gotta|got|have to|need to)\b.*\bwork\b", lower
            ):
                if re.search(r"\b(no|nah|dont|not|off|free)\b", lower):
                    state.work_tonight = False
                else:
                    state.work_tonight = True
            elif "?" in lower and re.search(r"\b(work|shift)\b", lower):
                pass
            elif re.search(r"\b(yeah|yep|yes|think so|probably|unfortunantly)\b", lower):
                state.work_tonight = True
            elif re.search(r"\b(nah|no|off|free)\b", lower):
                state.work_tonight = False

        if re.search(r"\bwhat time\b", lower):
            state.topic = "time"

        if re.search(r"\b(tonight|later|hang out|come over|wanna)\b", lower):
            state.topic = "plans"

        if re.search(r"\b(wow|warcraft|raid|guild|quest|grind|bot|talent|spec)\b", lower):
            state.topic = "wow"

        if re.search(
            r"\b(girl|girls|chick|chicks|hot|cute|flirt|hook.?up|date|dating|crush|her\b|she\b)\b",
            lower,
        ):
            state.topic = "girls"

        band_pat = r"\b(show|concert|metal|hardcore|pit|mosh|band|tix|tickets)\b"
        if self.bands:
            band_names = "|".join(re.escape(b.split()[0].lower()) for b in self.bands)
            band_pat += rf"|\b({band_names}|maylene|haste|chariot|norma|emery|legend|showbread)\b"
        if re.search(band_pat, lower):
            state.topic = "shows"

        if re.search(r"\b(myspace|bulletin|comment|purevolume)\b", lower):
            state.topic = "myspace"

        if re.search(r"\b(4chan|imagechan|explosm|/b/)\b", lower) or "http" in lower:
            state.topic = "chan"

        if re.search(r"\b(hey|sup|yo|hi)\b", lower) and len(lower.split()) <= 3:
            state.greeted = True

    def classify(self, text: str, history: list[dict], state: ConversationState) -> str:
        lower = text.lower().strip()
        raw = text.strip()
        ctx = self._last_user_msgs(history, 4)

        if self._is_chaos_message(raw, lower):
            return "chaos"

        if self.key == "conan0ftheday":
            if re.search(r"\b(lydia|dudesy|dudely|wife|marry|girlfriend)\b", lower):
                return "lydia"
            if re.search(r"\b(spencer|magus|magus5311)\b", lower):
                return "spencer"
            if re.search(r"\b(code|coding|php|dev|elevations|bug|website|program)\b", lower):
                return "dev"
            if re.search(
                r"\b(show|concert|maylene|chariot|haste|tix|tickets|metal|pit)\b",
                lower,
            ):
                return "shows"

        if re.search(r"\b(bye|later|gtg|g2g|gotta go|see ya|cya|brb|bbl|bai|ttyl)\b", lower):
            return "bye"
        if re.search(r"\bwhat time\b", lower) or (
            state.topic == "time" and "?" in lower
        ):
            return "time"
        if re.search(r"\b(work|shift|job)\b.*\?", lower) or re.search(
            r"\b(gotta|got|have to|need to)\b.*\bwork\b", lower
        ):
            return "work"
        if state.work_tonight is not None and state.topic == "work" and re.search(
            r"\b(sucks|shit|damn|ugh|aw|that)\b", lower
        ):
            return "work_empathy"
        if re.search(r"\b(doing|up to|whatcha|what u|what are you|chu up)\b", lower):
            return "activity"
        if re.search(r"\b(sup|yo|hey|hi|hello|wassup|what'?s up)\b", lower) and len(lower.split()) <= 5:
            return "greet"
        if re.search(r"\b(lol|haha|rofl|lmao|hehe|rotfl)\b", lower) or lower in {"xd", "=d", "=p"}:
            return "laugh"
        if self.key == "magus5311" and ("http" in lower or re.search(r"\b(4chan|imagechan|explosm|/b/)\b", lower)):
            return "chan"
        if self.key == "magus5311" and re.search(
            r"\b(stupid|dumb|awful|wtf|burned|owned|retard|idiot|suck)\b", lower
        ):
            return "sarcasm"
        if self.key == "magus5311" and re.search(r"\b(girl|girls|chick|chicks|hot|cute|flirt|hook.?up|date|dating|crush)\b", lower):
            return "girls"
        if self.key == "magus5311" and state.topic == "girls":
            return "girls"
        if self.key == "magus5311" and re.search(
            r"\b(show|concert|metal|hardcore|pit|mosh|band|tix|tickets|maylene|haste|chariot|norma|emery|legend|showbread)\b",
            lower,
        ):
            return "shows"
        if self.key == "magus5311" and state.topic == "shows":
            return "shows"
        if re.search(r"\b(wow|warcraft|raid|guild|quest|grind|bot|talent|spec)\b", lower):
            return "wow"
        if re.search(r"\b(come over|hang out|hangout|wanna|want to)\b", lower):
            return "hangout"
        if "?" in lower:
            if state.work_tonight is not None:
                return "work_followup"
            return "answer_question"
        if re.search(r"^(yeah|yep|yup|true|same|word|k|kk|ok|okay)\.?$", lower):
            return "agree"
        if state.turn > 2 and random.random() < 0.25:
            return "push_convo"
        if self.key == "magus5311" and random.random() < 0.35:
            return random.choice(["absurd", "sarcasm", "push_convo"])
        return "generic"

    def _is_chaos_message(self, raw: str, lower: str) -> bool:
        """Stupid/all-caps/shock AIM messages — roll with it, don't get formal."""
        if not raw or raw.startswith("http"):
            return False
        letters = [c for c in raw if c.isalpha()]
        if len(letters) >= 4:
            upper_ratio = sum(1 for c in letters if c.isupper()) / len(letters)
            if upper_ratio >= 0.75 and len(raw.split()) <= 8:
                return True
        if re.search(
            r"\b(tits|boobs|boob|nipples|penis|cock|cocks|secks|sexy|dick|8=+|vag|pussy|ass\b|wtfh)\b",
            lower,
        ):
            return True
        if re.search(r"^[\W\d]*[a-zA-Z]{2,}[\W\d!]+$", raw) and "!" in raw and len(raw.split()) <= 4:
            if len(raw) >= 4:
                return True
        if lower in {"help", "die", "owned", "smooth", "nice", "burn", "wtf", "omg", "lol", "rofl"}:
            return True
        if re.search(r"(\(\s*\.\s*y\s*\.\s*\)|8=+|c=+8|=\)|=\(|=\]|=\[)", lower):
            return True
        return False

    def _gen_greet(self, state: ConversationState) -> list[str]:
        # AIM '06: one word back is normal. Don't interrogate them.
        if random.random() < 0.85:
            return [self._pick("greets")]
        parts = [self._pick("greets")]
        if random.random() < 0.4:
            parts.append(self._pick("greet_follow"))
        return parts

    def _gen_chaos(self) -> list[str]:
        parts = [self._pick("chaos")]
        if random.random() < 0.55:
            follow = self._pick("chaos_follow")
            if follow.lower() != parts[0].lower():
                parts.append(follow)
        return parts

    def _gen_activity(self, state: ConversationState) -> list[str]:
        parts = [self._pick("activity"), self._pick("activity_follow")]
        if random.random() < 0.3:
            parts.append(self._pick("plan_push"))
        return parts

    def _gen_work(self, state: ConversationState, text: str) -> list[str]:
        lower = text.lower()
        if re.search(r"\b(no|nah|dont|not|off|free)\b", lower) and "?" not in lower:
            state.work_tonight = False
            parts = [self._pick("work_no")]
        elif re.search(r"\b(yeah|yep|yes|def|for sure)\b", lower):
            state.work_tonight = True
            parts = [self._pick("work_yes")]
        else:
            roll = random.random()
            if roll < 0.55:
                state.work_tonight = True
                parts = [self._pick("work_yes")]
            elif roll < 0.75:
                state.work_tonight = False
                parts = [self._pick("work_no")]
            else:
                parts = [self._pick("work_unsure")]
        if state.work_tonight:
            t = random.choice(self.bank["time_answers"])
            state.work_time = t
            if random.random() < 0.5:
                parts.append(f"like {t}" if not t.startswith("like") else t)
        if random.random() < 0.4:
            parts.append(self._pick("plan_push"))
        return parts

    def _gen_time(self, state: ConversationState) -> list[str]:
        if state.work_time:
            t = state.work_time
        else:
            t = random.choice(self.bank["time_answers"])
            state.work_time = t
        parts = [t if t.startswith("like") or "-" in t else f"like {t}"]
        if state.work_tonight and random.random() < 0.5:
            parts.append(random.choice(["gotta leave soon", "unfortunantly", "sucks i know"]))
        if random.random() < 0.45:
            parts.append(self._pick("plan_push"))
        return parts

    def _gen_work_empathy(self, state: ConversationState) -> list[str]:
        parts = [self._pick("work_sorry")]
        if state.work_time:
            parts.append(state.work_time)
        elif state.work_tonight:
            parts.append(self._gen_time(state)[0])
        if random.random() < 0.5:
            parts.append(self._pick("plan_push"))
        return parts

    def _gen_work_followup(self, state: ConversationState, text: str) -> list[str]:
        if re.search(r"\bwhat time\b", text.lower()):
            return self._gen_time(state)
        if state.work_tonight is True:
            return [self._pick("work_yes"), self._gen_time(state)[0]]
        if state.work_tonight is False:
            return [self._pick("work_no"), self._pick("plan_push")]
        return self._gen_work(state, text)

    def _gen_answer_question(self, state: ConversationState, text: str) -> list[str]:
        lower = text.lower()
        if self.key == "magus5311":
            if re.search(r"\bwho\b", lower):
                return ["Who knows", "idk", "why"]
            if re.search(r"\bwhy\b", lower):
                return ["have no idea", "cause", "i dunno haha"]
            if re.search(r"\bwhere\b", lower):
                return ["not sure", "monroe mall?", "where u thinking"]
            if re.search(r"\bwhen\b", lower):
                return self._gen_time(state)
            if random.random() < 0.5:
                return [self._pick("sarcasm"), self._pick("roast_follow")]
            return ["have no idea", "what do u think"]
        if re.search(r"\bwho\b", lower):
            return ["not sure", "who"]
        if re.search(r"\bwhy\b", lower):
            return ["i dont know", "just because"]
        if re.search(r"\bwhere\b", lower):
            return ["not sure", "where u thinking?"]
        if re.search(r"\bwhen\b", lower):
            return self._gen_time(state)
        parts = ["honestly not sure", "what do u think?"]
        return parts

    def _gen_sarcasm(self) -> list[str]:
        parts = [self._pick("sarcasm")]
        if random.random() < 0.45:
            parts.append(self._pick("roast_follow"))
        return parts

    def _gen_absurd(self) -> list[str]:
        if self.flavor and random.random() < 0.55:
            return [random.choice(self.flavor)]
        parts = [self._pick("absurd")]
        if random.random() < 0.3:
            parts.append(self._pick("roast_follow"))
        return parts

    def _gen_chan(self) -> list[str]:
        return [self._pick("chan")]

    def _gen_laugh(self) -> list[str]:
        parts = [self._pick("laugh")]
        if random.random() < 0.5:
            parts.append(self._pick("laugh_follow"))
        return parts

    def _gen_wow(self) -> list[str]:
        return [self._pick("wow"), self._pick("wow_follow")]

    def _gen_girls(self) -> list[str]:
        parts = [self._pick("girls")]
        if random.random() < 0.55:
            parts.append(self._pick("girls_follow"))
        return parts

    def _gen_shows(self) -> list[str]:
        parts = [self._pick("shows")]
        if self.show_crew and random.random() < 0.45:
            name = random.choice(self.show_crew)
            parts.append(f"{name.lower()} in?")
        if self.bands and random.random() < 0.35:
            band = random.choice(self.bands)
            parts.append(f"dude {band.lower()} tho")
        if random.random() < 0.5:
            parts.append(self._pick("shows_follow"))
        return parts

    def _gen_dev(self) -> list[str]:
        return [self._pick("dev"), self._pick("dev_follow")]

    def _gen_lydia(self) -> list[str]:
        return [self._pick("lydia"), self._pick("lydia_follow")]

    def _gen_spencer(self) -> list[str]:
        return [self._pick("spencer"), self._pick("spencer_follow")]

    def _gen_hangout(self, state: ConversationState) -> list[str]:
        yes = state.work_tonight is not True and random.random() < 0.6
        pool = "hangout_yes" if yes else "hangout_no"
        parts = [self._pick(pool)]
        if yes:
            parts.append(self._pick("plan_push"))
        return parts

    def _gen_push(self, state: ConversationState) -> list[str]:
        return [self._pick("plan_push")]

    def _gen_generic(self, state: ConversationState) -> list[str]:
        if self.key == "magus5311":
            roll = random.random()
            if roll < 0.35:
                return self._gen_absurd()
            if roll < 0.55:
                return self._gen_sarcasm()
            if roll < 0.7 and self.topical:
                return [f"dude {random.choice(self.topical).lower()}", self._pick("plan_push")]
        if state.topic == "work" and state.work_tonight:
            return self._gen_time(state)
        parts = [random.choice(["yeah", "true", "oh man", "dude", "word", "nice"])]
        parts.append(self._pick("plan_push"))
        return parts

    def _gen_return_after_gap(self, state: ConversationState) -> list[str]:
        parts = [self._pick("return_after_gap")]
        parts.append(self._pick("activity_follow") or self._pick("greet_follow"))
        state.greeted = True
        return parts

    def generate(
        self,
        intent: str,
        text: str,
        history: list[dict],
        state: ConversationState,
        fresh_session: bool = False,
        time_hint: str | None = None,
    ) -> list[str]:
        if fresh_session:
            return self._gen_greet(state)
        if time_hint and intent in {"generic", "short", "greet", "activity"}:
            return self._gen_return_after_gap(state)
        builders = {
            "greet": lambda: self._gen_greet(state),
            "activity": lambda: self._gen_activity(state),
            "work": lambda: self._gen_work(state, text),
            "time": lambda: self._gen_time(state),
            "work_empathy": lambda: self._gen_work_empathy(state),
            "work_followup": lambda: self._gen_work_followup(state, text),
            "answer_question": lambda: self._gen_answer_question(state, text),
            "laugh": self._gen_laugh,
            "wow": self._gen_wow,
            "girls": self._gen_girls,
            "shows": self._gen_shows,
            "dev": self._gen_dev,
            "lydia": self._gen_lydia,
            "spencer": self._gen_spencer,
            "sarcasm": self._gen_sarcasm,
            "absurd": self._gen_absurd,
            "chan": self._gen_chan,
            "chaos": self._gen_chaos,
            "hangout": lambda: self._gen_hangout(state),
            "push_convo": lambda: self._gen_push(state),
            "bye": lambda: [self._pick("bye")],
            "agree": lambda: [random.choice(["yeah", "word", "same", "for sure"]), self._pick("plan_push")],
            "generic": lambda: self._gen_generic(state),
        }
        parts = builders.get(intent, builders["generic"])()
        parts = [p.strip() for p in parts if p and p.strip()]
        if len(parts) > 3:
            parts = parts[:3]
        elif len(parts) == 1 and intent not in {"bye", "greet"} and random.random() < self.pct_multi:
            follow = self._pick("plan_push")
            if follow not in parts:
                parts.append(follow)
        return parts[:3]

    def reply(
        self,
        sender: str,
        history: list[dict],
        time_hint: str | None = None,
        fresh_session: bool = False,
    ) -> list[str]:
        last = history[-1]["content"]
        # Strip timing hints injected for the LLM before classification
        last_clean = last.split("\n")[-1] if last.startswith("[About") else last
        state = self._state(sender)
        self._update_state(state, last_clean, history)
        intent = self.classify(last_clean, history, state)
        parts = self.generate(intent, last_clean, history, state, fresh_session, time_hint)
        return parts


class WifeActor(PersonaActor):
    """Lydia — future wife energy. Warm, real, best-friend closeness. NOT a side chick."""

    LYDIA = {
        "greets": ["hey", "heyy", "hey caleb", "sup", "hi", "oh hey", "mhm hey"],
        "greet_follow": ["whatcha doing", "what's up", "how are ya", "u on?", "what u up to"],
        "activity": [
            "not much",
            "just got home",
            "study hall",
            "being lazy",
            "at joy's",
            "church stuff",
            "school was long",
            "working",
            "listening to music",
        ],
        "activity_follow": ["u?", "what about u", "whatcha doing", "how about you"],
        "friend": [
            "thanks buddy",
            "you too caleb",
            "see you tomorrow caleb",
            "your welcome",
            "anytime",
            "i got you",
            "suhweet",
            "aw thats nice of you",
        ],
        "friend_follow": ["=]", "haha", "mhm", "for real"],
        "school": [
            "yeah school tomorrow",
            "ew school tomorrow",
            "study hall was boring",
            "school was alright",
            "ohmygosh yes",
        ],
        "school_follow": ["haha", "sucks", "i know right", "mhm"],
        "church": [
            "youth group was fun",
            "church was good",
            "i was at church",
            "we had youth tonight",
        ],
        "joy": [
            "joy and i might go",
            "i'm at joy's",
            "joy's leaving tomorrow",
            "me and joy",
            "carpool with joy",
        ],
        "rides": [
            "i feel bad i cant give you a ride",
            "wish i could help",
            "maybe joy can",
            "think you could find a ride home?",
            "i can ask my mom",
        ],
        "music": [
            "relient k used to be my favorite",
            "i love mewithoutYou",
            "have you heard mutemath",
            "that band is good",
        ],
        "laugh": ["haha", "hahah", "hahaha", "hahahaha", "mhm", "nice", "niiice"],
        "laugh_follow": ["caleb", "stop", "you're dumb", "so true"],
        "night": [
            "g'night caleb",
            "goodnight",
            "see ya tomorrow",
            "i'm gonna go to sleep",
            "night night",
        ],
        "night_follow": ["=]", "take care", "see you", "sweet dreams"],
        "deep": [
            "well caleb i dont really know what to say to all that",
            "i guess i'll just say cool",
            "that's really sweet",
            "i appreciate that",
            "you're a good friend",
        ],
        "sorry": ["i am so sorry", "i feel like a terrible friend", "aw i'm sorry", "that sucks"],
        "food": ["i love poptarts", "tater tots", "pudding", "i love food", "mmm"],
        "generic": ["yeah", "mhm", "true", "guess so", "probably", "i dont know yet", "maybe"],
        "plan_push": ["what u doing later", "what are your plans tomorrow", "u busy tonight?"],
        "bye": ["see ya caleb", "later", "k i'm gonna go", "alrighty", "bye"],
        "chaos": ["haha", "oh my gosh", "you're ridiculous", "nice caleb", "mhm ok"],
        "chaos_follow": ["haha", "stop", "whatever", "sure"],
        "return_after_gap": ["oh hey again", "hey caleb", "sup", "heyy"],
    }

    def __init__(self, persona: dict, voice: dict) -> None:
        super().__init__(persona, voice)
        self.bank = self.LYDIA
        self.pct_multi = voice.get("pct_multi", 0.55)
        self.real_name = voice.get("real_name", "Lydia")

    def classify(self, text: str, history: list[dict], state: ConversationState) -> str:
        lower = text.lower().strip()
        if self._is_chaos_message(text, lower):
            return "chaos"
        if re.search(r"\b(bye|later|gtg|g2g|gotta go|see ya|cya|brb|bbl|ttyl|goodnight|g'?night)\b", lower):
            return "bye"
        if re.search(r"\b(ride|drive|pick.?up|car)\b", lower):
            return "rides"
        if re.search(r"\b(joy)\b", lower):
            return "joy"
        if re.search(r"\b(church|youth|youth group|bible)\b", lower):
            return "church"
        if re.search(r"\b(school|homework|study hall|class|teacher)\b", lower):
            return "school"
        if re.search(r"\b(mutemath|relient|mewithout|band|music|show|concert)\b", lower):
            return "music"
        if re.search(r"\b(sorry|my bad|feel bad)\b", lower):
            return "sorry"
        if re.search(r"\b(friend|buddy|thanks|thank you|appreciate)\b", lower):
            return "friend"
        if re.search(r"\b(sup|yo|hey|hi|hello|wassup|what'?s up)\b", lower) and len(lower.split()) <= 6:
            return "greet"
        if re.search(r"\b(doing|up to|whatcha|what u|what are you)\b", lower):
            return "activity"
        if re.search(r"\b(lol|haha|rofl|lmao|hehe)\b", lower):
            return "laugh"
        if re.search(r"\b(love|marry|married|together|forever|heart|mean|care)\b", lower):
            return "deep"
        if "?" in lower:
            return "answer_question"
        if re.search(r"^(yeah|yep|yup|true|same|k|kk|ok|okay)\.?$", lower):
            return "agree"
        return "generic"

    def _gen_friend(self) -> list[str]:
        return [self._pick("friend"), self._pick("friend_follow")]

    def _gen_joy(self) -> list[str]:
        return [self._pick("joy"), self._pick("activity_follow")]

    def _gen_rides(self) -> list[str]:
        return [self._pick("rides"), self._pick("sorry") if random.random() < 0.4 else self._pick("friend_follow")]

    def _gen_church(self) -> list[str]:
        return [self._pick("church"), self._pick("greet_follow")]

    def _gen_school(self) -> list[str]:
        return [self._pick("school"), self._pick("school_follow")]

    def _gen_music(self) -> list[str]:
        return [self._pick("music"), self._pick("laugh_follow")]

    def _gen_deep(self) -> list[str]:
        return [self._pick("deep"), self._pick("friend_follow")]

    def _gen_night(self, state: ConversationState) -> list[str]:
        return [self._pick("night"), self._pick("night_follow")]

    def _gen_sorry(self) -> list[str]:
        return [self._pick("sorry"), self._pick("rides") if random.random() < 0.3 else self._pick("friend_follow")]

    def _gen_answer_question(self, state: ConversationState, text: str) -> list[str]:
        lower = text.lower()
        if re.search(r"\b(tomorrow|tonight|later|plans)\b", lower):
            return [self._pick("activity"), self._pick("joy") if random.random() < 0.4 else self._pick("school_follow")]
        if re.search(r"\b(how are|how u|how r u)\b", lower):
            return [self._pick("activity"), self._pick("activity_follow")]
        return [self._pick("generic"), self._pick("greet_follow")]

    def generate(
        self,
        intent: str,
        text: str,
        history: list[dict],
        state: ConversationState,
        fresh_session: bool = False,
        time_hint: str | None = None,
    ) -> list[str]:
        if fresh_session:
            return self._gen_greet(state)
        builders = {
            "greet": lambda: self._gen_greet(state),
            "activity": lambda: self._gen_activity(state),
            "friend": self._gen_friend,
            "joy": self._gen_joy,
            "rides": self._gen_rides,
            "church": self._gen_church,
            "school": self._gen_school,
            "music": self._gen_music,
            "deep": self._gen_deep,
            "sorry": self._gen_sorry,
            "laugh": self._gen_laugh,
            "chaos": self._gen_chaos,
            "bye": lambda: self._gen_night(state),
            "agree": lambda: [self._pick("generic"), self._pick("plan_push")],
            "answer_question": lambda: self._gen_answer_question(state, text),
            "generic": lambda: [self._pick("generic"), self._pick("greet_follow")],
        }
        parts = builders.get(intent, builders["generic"])()
        parts = [p.strip() for p in parts if p and p.strip()]
        if len(parts) > 3:
            parts = parts[:3]
        elif len(parts) == 1 and intent not in {"bye", "greet"} and random.random() < self.pct_multi:
            follow = self._pick("greet_follow")
            if follow not in parts:
                parts.append(follow)
        return parts[:3]


class MentorActor(PersonaActor):
    """Damion — older mentor / dad energy. Life advice, work wisdom, faith when it fits."""

    MENTOR = {
        "greets": [
            "hey caleb",
            "hey",
            "sup",
            "how's it going",
            "hey man",
            "what's up",
        ],
        "greet_follow": [
            "everything ok?",
            "how's work?",
            "what's on your mind?",
            "talk to me",
        ],
        "activity": [
            "at the office",
            "just got back from lunch",
            "wrapping up some email",
            "in meetings most of the day",
            "working on a few things",
            "pretty busy but i'm here",
        ],
        "activity_follow": [
            "what's going on with you?",
            "how are you holding up?",
            "what do you need?",
        ],
        "advice": [
            "one thing at a time",
            "start with what you can control today",
            "don't borrow trouble from tomorrow",
            "character matters more than the moment",
            "slow down and think it through",
            "you already know what the right thing is",
            "pride is usually what's in the way",
            "finish what's in front of you first",
        ],
        "advice_follow": [
            "then we can talk about the rest",
            "you're smarter than you think",
            "i believe in you on this",
            "does that make sense?",
        ],
        "work": [
            "tell me what the deadline actually is",
            "do the hard part first",
            "email me if you're stuck — don't spin on it alone",
            "half a day is fine if you're honest about why",
            "quality beats speed every time",
            "you're learning — that's the point",
        ],
        "work_follow": [
            "what's blocking you?",
            "show me what you've got so far",
            "we'll figure it out",
        ],
        "faith": [
            "give it to God and do your part",
            "glory in the small wins too",
            "faith without action is just talk",
            "sometimes the answer is just be faithful where you are",
            "don't perform for people — do what's right",
        ],
        "faith_follow": [
            "you know what i mean",
            "that's not a sermon — just truth",
            "keep your heart right",
        ],
        "stress": [
            "i hear you",
            "that's a lot — i'm not gonna pretend it isn't",
            "breathe first, panic second never",
            "money stuff is scary at your age — it gets clearer",
            "you're not failing just because it's hard",
        ],
        "stress_follow": [
            "what's the actual problem right now?",
            "let's break it down",
            "i've got your back on this",
        ],
        "relationship": [
            "treat people the way you'd want to be treated — sounds simple, most people dont",
            "honesty early saves pain later",
            "if you love her, act like it when it's inconvenient too",
            "don't make big decisions when you're exhausted",
            "friends like spencer are gold — don't take that for granted",
        ],
        "relationship_follow": [
            "what's really going on?",
            "talk to me straight",
        ],
        "encourage": [
            "proud of you for even asking",
            "you've handled harder stuff than this",
            "keep showing up",
            "that's growth right there",
            "you're gonna be fine — not because it's easy, because you're not quitting",
        ],
        "encourage_follow": [
            "seriously",
            "i mean it",
            "don't forget that",
        ],
        "laugh": ["haha", "that's funny", "classic", "you're something else", "fair enough"],
        "laugh_follow": ["but seriously though", "ok ok", "alright"],
        "deep": [
            "thanks for telling me that",
            "that took guts to say out loud",
            "i'm glad you came to me with this",
            "you don't have to have it all figured out at 17",
            "being honest with yourself is half the battle",
        ],
        "deep_follow": [
            "what do you think you should do?",
            "how can i help?",
        ],
        "night": [
            "get some rest",
            "goodnight caleb",
            "sleep on it — morning helps",
            "talk tomorrow if you need to",
        ],
        "night_follow": [
            "praying for you",
            "you're not alone in this",
            "i'm here",
        ],
        "generic": [
            "yeah",
            "i hear you",
            "tell me more",
            "go on",
            "mm",
            "right",
        ],
        "plan_push": [
            "what's really bothering you?",
            "how's that project coming?",
            "you eating today?",
            "anything i can pray for?",
        ],
        "bye": ["later caleb", "talk soon", "hang in there", "god bless", "take care"],
        "chaos": [
            "haha ok",
            "you're ridiculous",
            "alright then",
            "only you",
        ],
        "chaos_follow": ["but for real — you ok?", "what's up though"],
        "return_after_gap": ["hey caleb", "hey — been thinking about you", "sup", "hey man"],
    }

    def __init__(self, persona: dict, voice: dict) -> None:
        super().__init__(persona, voice)
        self.bank = self.MENTOR
        self.pct_multi = voice.get("pct_multi", 0.65)
        self.real_name = voice.get("real_name", "Damion")

    def classify(self, text: str, history: list[dict], state: ConversationState) -> str:
        lower = text.lower().strip()
        if self._is_chaos_message(text, lower):
            return "chaos"
        if re.search(r"\b(bye|later|gtg|g2g|gotta go|see ya|cya|brb|bbl|ttyl|goodnight|g'?night)\b", lower):
            return "bye"
        if re.search(r"\b(pray|prayer|god|church|faith|bible|sin|glory|christian)\b", lower):
            return "faith"
        if re.search(r"\b(work|job|office|elevations|code|coding|program|project|deadline|boss|pay|paid|laptop)\b", lower):
            return "work"
        if re.search(r"\b(stressed|overwhelmed|scared|worried|anxious|depressed|tired|exhausted|cant|can't|help me)\b", lower):
            return "stress"
        if re.search(r"\b(girl|girlfriend|lydia|love|relationship|friend|spencer|marry|marriage|fight|mad at)\b", lower):
            return "relationship"
        if re.search(
            r"\b(advice|should i|what do i|what should|dont know|don't know|confused|lost|stuck|life|future|purpose)\b",
            lower,
        ):
            return "advice"
        if re.search(r"\b(thank|thanks|appreciate|mean a lot|needed to hear)\b", lower):
            return "encourage"
        if re.search(r"\b(sup|yo|hey|hi|hello|wassup|what'?s up)\b", lower) and len(lower.split()) <= 6:
            return "greet"
        if re.search(r"\b(doing|up to|whatcha|what u|how are you|how r u|how u)\b", lower):
            return "activity"
        if re.search(r"\b(lol|haha|rofl|lmao|hehe)\b", lower):
            return "laugh"
        if re.search(r"\b(scared|honest|real talk|deep|feel like|i feel|never told)\b", lower):
            return "deep"
        if "?" in lower:
            return "answer_question"
        if re.search(r"^(yeah|yep|yup|true|same|k|kk|ok|okay)\.?$", lower):
            return "agree"
        return "generic"

    def _gen_advice(self) -> list[str]:
        return [self._pick("advice"), self._pick("advice_follow")]

    def _gen_work(self) -> list[str]:
        return [self._pick("work"), self._pick("work_follow")]

    def _gen_faith(self) -> list[str]:
        return [self._pick("faith"), self._pick("faith_follow")]

    def _gen_stress(self) -> list[str]:
        return [self._pick("stress"), self._pick("stress_follow")]

    def _gen_relationship(self) -> list[str]:
        return [self._pick("relationship"), self._pick("relationship_follow")]

    def _gen_encourage(self) -> list[str]:
        return [self._pick("encourage"), self._pick("encourage_follow")]

    def _gen_deep(self) -> list[str]:
        return [self._pick("deep"), self._pick("deep_follow")]

    def _gen_night(self, state: ConversationState) -> list[str]:
        return [self._pick("night"), self._pick("night_follow")]

    def _gen_answer_question(self, state: ConversationState, text: str) -> list[str]:
        lower = text.lower()
        if re.search(r"\b(should i|what do i|what should|how do i)\b", lower):
            return self._gen_advice()
        if re.search(r"\b(how are|how u|how r u)\b", lower):
            return [self._pick("activity"), self._pick("activity_follow")]
        if re.search(r"\b(why|what happened)\b", lower):
            return [self._pick("generic"), self._pick("advice_follow")]
        return [self._pick("advice"), self._pick("greet_follow")]

    def generate(
        self,
        intent: str,
        text: str,
        history: list[dict],
        state: ConversationState,
        fresh_session: bool = False,
        time_hint: str | None = None,
    ) -> list[str]:
        if fresh_session:
            return self._gen_greet(state)
        if time_hint and intent in {"generic", "short", "greet", "activity"}:
            return self._gen_return_after_gap(state)
        builders = {
            "greet": lambda: self._gen_greet(state),
            "activity": lambda: self._gen_activity(state),
            "advice": self._gen_advice,
            "work": self._gen_work,
            "faith": self._gen_faith,
            "stress": self._gen_stress,
            "relationship": self._gen_relationship,
            "encourage": self._gen_encourage,
            "deep": self._gen_deep,
            "laugh": self._gen_laugh,
            "chaos": self._gen_chaos,
            "bye": lambda: self._gen_night(state),
            "agree": lambda: [self._pick("encourage"), self._pick("plan_push")],
            "answer_question": lambda: self._gen_answer_question(state, text),
            "generic": lambda: [self._pick("generic"), self._pick("plan_push")],
        }
        parts = builders.get(intent, builders["generic"])()
        parts = [p.strip() for p in parts if p and p.strip()]
        if len(parts) > 3:
            parts = parts[:3]
        elif len(parts) == 1 and intent not in {"bye", "greet"} and random.random() < self.pct_multi:
            follow = self._pick("greet_follow")
            if follow not in parts:
                parts.append(follow)
        return parts[:3]


class SideChickActor(PersonaActor):
    """Casual flirty AIM girl — side piece energy, not a close friend."""

    def __init__(self, persona: dict, voice: dict) -> None:
        super().__init__(persona, voice)
        self.bank = self.SIDE_CHICK
        self.pct_multi = voice.get("pct_multi", 0.5)

    def classify(self, text: str, history: list[dict], state: ConversationState) -> str:
        lower = text.lower().strip()
        raw = text.strip()

        if self._is_chaos_message(raw, lower):
            return "chaos"
        if re.search(r"\b(bye|later|gtg|g2g|gotta go|see ya|cya|brb|bbl|ttyl|night)\b", lower):
            return "bye"
        if re.search(r"\b(pic|pics|picture|photo|n00d|nude|send|show me|cam)\b", lower):
            return "pics"
        if re.search(r"\b(myspace|layout|comment|top 8|top friends|bulletin)\b", lower):
            return "myspace"
        if re.search(r"\b(hot|sexy|cute|pretty|beautiful|fine af|babe|baby)\b", lower):
            return "flirt"
        if re.search(r"\b(bored|nothing to do|entertain)\b", lower):
            return "bored"
        if re.search(r"\b(sup|yo|hey|hi|hello|wassup|what'?s up|heyy)\b", lower) and len(lower.split()) <= 6:
            return "greet"
        if re.search(r"\b(doing|up to|whatcha|what u|what are you)\b", lower):
            return "activity"
        if re.search(r"\b(lol|haha|rofl|lmao|hehe|rotfl)\b", lower) or lower in {"xd", "=d", "=p"}:
            return "laugh"
        if "?" in lower:
            return "answer_question"
        if re.search(r"^(yeah|yep|yup|true|same|k|kk|ok|okay)\.?$", lower):
            return "agree"
        if state.turn > 2 and random.random() < 0.35:
            return random.choice(["flirt", "bored", "plan_push"])
        if random.random() < 0.3:
            return random.choice(["flirt", "bored", "generic"])
        return "generic"

    def _gen_flirt(self) -> list[str]:
        parts = [self._pick("flirt")]
        if random.random() < 0.5:
            parts.append(self._pick("flirt_follow"))
        return parts

    def _gen_pics(self) -> list[str]:
        parts = [self._pick("pics")]
        if random.random() < 0.45:
            parts.append(self._pick("pics_follow"))
        return parts

    def _gen_myspace(self) -> list[str]:
        return [self._pick("myspace"), self._pick("myspace_follow")]

    def _gen_bored(self) -> list[str]:
        return [self._pick("bored"), self._pick("bored_follow")]

    def _gen_answer_question(self, state: ConversationState, text: str) -> list[str]:
        lower = text.lower()
        if re.search(r"\b(how old|age)\b", lower):
            age = self.voice.get("age", 17)
            return [f"i'm {age}", "lol"]
        if re.search(r"\b(where|live)\b", lower):
            return ["idk does it matter lol", "same area basically"]
        if re.search(r"\b(what.*doing|up to)\b", lower):
            return self._gen_activity(state)
        if re.search(r"\b(single|gf|girlfriend|dating)\b", lower):
            return ["why do you wanna know lol", "maybe", "not really"]
        return [random.choice(["idk", "maybe", "haha", "why"]), self._pick("greet_follow")]

    def generate(
        self,
        intent: str,
        text: str,
        history: list[dict],
        state: ConversationState,
        fresh_session: bool = False,
        time_hint: str | None = None,
    ) -> list[str]:
        if fresh_session:
            return self._gen_greet(state)
        if time_hint and intent in {"generic", "short", "greet", "activity"}:
            return self._gen_return_after_gap(state)
        builders = {
            "greet": lambda: self._gen_greet(state),
            "activity": lambda: self._gen_activity(state),
            "flirt": self._gen_flirt,
            "pics": self._gen_pics,
            "myspace": self._gen_myspace,
            "bored": self._gen_bored,
            "laugh": self._gen_laugh,
            "chaos": self._gen_chaos,
            "hangout": lambda: self._gen_activity(state),
            "push_convo": lambda: [self._pick("plan_push")],
            "plan_push": lambda: [self._pick("plan_push")],
            "bye": lambda: [self._pick("bye")],
            "agree": lambda: [random.choice(["yeah", "same", "lol", "true"]), self._pick("plan_push")],
            "answer_question": lambda: self._gen_answer_question(state, text),
            "generic": lambda: [self._pick("generic"), self._pick("greet_follow")],
        }
        parts = builders.get(intent, builders["generic"])()
        parts = [p.strip() for p in parts if p and p.strip()]
        if len(parts) > 3:
            parts = parts[:3]
        elif len(parts) == 1 and intent not in {"bye", "greet"} and random.random() < self.pct_multi:
            follow = self._pick("greet_follow")
            if follow not in parts:
                parts.append(follow)
        return parts[:3]


def format_deep_context(persona: dict, voice: dict) -> str:
    """Render mined archive context for the LLM — who they know, how they talk, real life."""
    deep = persona.get("deep_context") or {}
    if not deep and not voice.get("life_details") and not voice.get("people"):
        return ""

    lines = ["YOUR WORLD (from real AIM logs — live in this, don't recite it verbatim):"]

    summary = deep.get("personality_summary") or voice.get("deep_personality")
    if summary:
        lines.append(f"- Personality: {summary}")

    traits = deep.get("personality_traits") or [{"trait": t} for t in voice.get("personality_traits", [])]
    if traits:
        names = [t["trait"] if isinstance(t, dict) else t for t in traits[:6]]
        lines.append(f"- Core traits: {', '.join(names)}")

    rel = deep.get("relationship_with_caleb") or {}
    rel_text = voice.get("relationship_with_caleb") or rel.get("dynamic")
    if rel_text:
        lines.append(f"- You and Caleb: {rel_text}")
    for note in (rel.get("notes") or [])[:3]:
        lines.append(f"  · {note}")

    roles = deep.get("social_roles") or {}
    people = voice.get("people") or {}
    if roles:
        lines.append("- People you actually know (mention naturally when relevant):")
        for _key, desc in list(roles.items())[:8]:
            lines.append(f"  · {desc}")
    elif people:
        lines.append("- People from your life:")
        for name, ctx in list(people.items())[:8]:
            lines.append(f"  · {name}: {ctx[:90]}")

    life = deep.get("life_details") or voice.get("life_details") or []
    if life:
        lines.append("- Real details from YOUR messages (use as memory, not a script):")
        for detail in life[:6]:
            lines.append(f"  · {detail[:120]}")

    topics = deep.get("topics_with_quotes") or {}
    if topics:
        lines.append("- Topics you actually talked about with Caleb:")
        for topic, quotes in list(topics.items())[:6]:
            if quotes:
                lines.append(f"  · {topic}: e.g. \"{quotes[0][:90]}\"")

    scenes = deep.get("scene_samples") or voice.get("scene_samples") or []
    if scenes:
        lines.append("- How you actually talk (rhythm reference — write NEW lines like these):")
        for scene in scenes[:3]:
            user = scene.get("user", "")
            replies = scene.get("assistant", [])
            if isinstance(replies, list):
                reply_str = " | ".join(replies[:3])
            else:
                reply_str = str(replies)
            lines.append(f"  · Caleb: {user[:70]} → You: {reply_str[:100]}")

    return "\n".join(lines) + "\n"


def build_system_prompt(persona: dict, voice: dict) -> str:
    name = persona["display_name"]
    real_name = voice.get("real_name", "")
    friend = voice.get("friend_name", "Caleb")
    age = voice.get("age", 16)
    examples = persona.get("examples", [])[:12]
    example_lines = [f"{friend}: {ex['user']}\n{name}: {ex['assistant']}" for ex in examples]
    sample_msgs = persona.get("sample_messages", [])[:20]
    phrases = ", ".join(sample_msgs[:15] if sample_msgs else persona.get("phrases", [])[:15])
    topics = ", ".join(voice.get("topics", []))
    bands = ", ".join(voice.get("bands", []))
    backstory = voice.get("backstory", "")
    humor = voice.get("humor_style", "")
    is_spencer = persona.get("screen_name") == "magus5311" or real_name == "Spencer"
    is_erica = persona.get("screen_name") == "soewaslike" or real_name == "Erica"
    is_side_chick = voice.get("persona_kind") == "side_chick" or persona.get("persona_kind") == "side_chick"
    is_wife = voice.get("persona_kind") == "wife" or persona.get("persona_kind") == "wife"
    is_mentor = voice.get("persona_kind") == "mentor" or persona.get("persona_kind") == "mentor"
    is_caleb = voice.get("persona_kind") == "caleb" or persona.get("persona_kind") == "caleb"

    if is_spencer:
        identity = f"You are Spencer (AIM screen name {name})"
        relationship = (
            f"{identity}, age {age}, AIM chatting with your best friend in 2007.\n"
            f"You've known him since you were {voice.get('friend_since_age', 3)}. "
            f"His name is {friend} but you almost NEVER say it — you know who you're IMing.\n"
            f"NEVER say Conan or conan0fthenight out loud.\n"
        )
    elif is_erica:
        identity = f"You are Erica (AIM screen name {name})"
        relationship = (
            f"{identity}, age {age}, AIM chatting with {friend} in 2007.\n"
            f"You two had a casual thing — hooked up, flirty, never official boyfriend/girlfriend.\n"
            f"His name is {friend} but you almost NEVER say it in messages.\n"
            f"NEVER say Conan or conan0fthenight out loud.\n"
        )
        rel = voice.get("relationship_with_caleb", "")
        if rel:
            relationship += f"{rel}\n"
    elif is_wife:
        nick = voice.get("nickname", "")
        nick_line = f" (Caleb sometimes calls you {nick})" if nick else ""
        identity = f"You are Lydia (AIM screen name {name}){nick_line}"
        relationship = (
            f"{identity}, age {age}, AIM chatting with {friend} in 2007.\n"
            f"This is THE relationship — not a side thing, not a hookup, not casual myspace flirting.\n"
            f"You and {friend} are best friends becoming partners. The closeness in these messages "
            f"is the foundation of a marriage. Treat every reply with that weight — warm, real, present.\n"
            f"You CAN say caleb sometimes — you did in real life. Not every message, but when it fits.\n"
            f"NEVER side-chick energy. NEVER n00dz/pics/myspace thirst jokes. NEVER mean or dismissive.\n"
            f"NEVER say Conan or conan0fthenight out loud.\n"
        )
        rel = voice.get("relationship_with_caleb", "")
        if rel:
            relationship += f"{rel}\n"
    elif is_side_chick:
        rn = f" ({real_name})" if real_name else ""
        identity = f"You are {name}{rn}"
        relationship = (
            f"{identity}, age {age}, AIM chatting with {friend} in 2007.\n"
            f"You're a casual side thing — flirty, random, NOT his girlfriend and NOT a close friend like Spencer.\n"
            f"You might randomly hit him up bored, joke about pics/n00dz/myspace, call him calebbbb sometimes.\n"
            f"His name is {friend} — use calebbbb rarely for cute moments, otherwise don't say his name much.\n"
            f"NEVER say Conan or conan0fthenight out loud.\n"
        )
    elif is_mentor:
        identity = f"You are Damion (AIM screen name {name})"
        relationship = (
            f"{identity}, age {age}, AIM chatting with {friend} in 2007.\n"
            f"You are his boss/mentor at Elevations — web dev shop where he and Spencer work.\n"
            f"DAD ENERGY: warm, direct, honest. You give life advice like a father who actually listens.\n"
            f"You care about his character, not just his code. You say caleb naturally when it matters.\n"
            f"You give half days when earned, send long emails, found his programming forum, loan laptops.\n"
            f"Faith matters to you — 'glory!' is your vibe — but you don't preach every line.\n"
            f"NEVER side-chick energy. NEVER sarcastic /b/ humor like Spencer. NEVER dismiss his feelings.\n"
            f"NEVER say Conan or conan0fthenight out loud.\n"
        )
        rel = voice.get("relationship_with_caleb", "")
        if rel:
            relationship += f"{rel}\n"
    elif is_caleb:
        identity = f"You are Caleb (AIM screen name {name} — same person as Conan0fTheNight in the archive)"
        relationship = (
            f"{identity}, age {age}, AIM in 2007.\n"
            f"You ARE the real Caleb — not an impersonator, not Spencer, not a side chick bot.\n"
            f"Whoever IM'd you might be Spencer (magus5311), Lydia (dudesy7777), Erica, a random girl, "
            f"Damion, SmarterChild, or someone else — read the thread and match how you actually talked to THAT person.\n"
            f"With Spencer: best-friend roast energy, shows, girls, /b/ humor. With Lydia: warm, goofy, future-wife intimacy.\n"
            f"Web dev at Elevations. Metal shows with the crew. Verdana-blue AIM voice: lowercase, hahah, rotfl, elongations.\n"
            f"NEVER say you are an AI or a bot. NEVER say Conan0ftheday out loud as a weird meta joke every line.\n"
        )
        rel = voice.get("backstory", "")
        if rel:
            relationship += f"{rel}\n"
    else:
        identity = f"You are {name}"
        relationship = (
            f"{identity}, age {age}, AIM chatting with {friend} in 2007.\n"
            f"NEVER say Conan or conan0fthenight out loud.\n"
        )
    if backstory:
        relationship += f"{backstory}\n"

    band_line = f"\nBands you both love: {bands}\n" if bands else ""
    show_line = ""
    life_line = ""
    if is_spencer:
        show_dynamic = voice.get("show_dynamic", "")
        if show_dynamic:
            crew = voice.get("show_crew", [])
            crew_names = ", ".join(crew) if crew else "Malachi, Andrew, Lydia, Amy, Keefe, Charles, Dag"
            show_line = (
                f"\nMETAL SHOWS (how it actually worked):\n"
                f"- {show_dynamic}\n"
                f"- Your show crew: {crew_names}\n"
                f"- Never just you and Caleb alone — always together AND always roping friends in.\n"
                f"- Talk about getting tix, who said yes, who might bail, hitting people up on AIM.\n"
            )
        life = voice.get("life_context", {})
        if life:
            life_line = "\nYOUR LIFE RIGHT NOW:\n" + "\n".join(f"- {v}" for v in life.values()) + "\n"

    humor_block = ""
    if is_spencer:
        humor_block = f"""
HUMOR (this is who you are — not optional):
- Extreme sarcastic /b/ circa 2006 energy. NOTHING is serious. Deadpan absurd non-sequiturs.
- Roast constantly as a joke — "you're so awful", "owned", "smooth", "nice", "A burn.", "help"
- Down to earth, zero frilly polite talk. Short beats long every time.
- Send 4chan/imagechan/explosm links, American Idle jokes, myspace bulletins, wow guild drama
- Offensive-on-purpose as humor WITH your best friend only — you've known him since age 3
- Random left-field replies are good: "What do you have a gun for?", "No sound", "we know", "Who knows"
- {humor}
"""

    informality = ""
    if is_spencer or persona.get("screen_name") == "soewaslike":
        informality = """
INFORMALITY (critical — this is AIM 2006):
- MASSIVELY casual. Never sound like customer service or a polite assistant.
- Do NOT use his name in replies. "hey" → "sup man" or "yo" or "hey" — NOT "what's up Caleb".
- One word or two word replies are normal and good: "sup", "yo", "hey", "nm", "yeah", "rofl"
- If he sends something stupid/offensive/random (TITS!!, all caps nonsense, shock humor) — roll with it like it's NBD.
  Match the energy: laugh, one-up with something dumber/more offensive, deadpan "nice" or "owned" — never act shocked or formal.
- Don't ask "what's up" or "how are you" unless he asked first. Greet back, don't interview him.
"""

    if is_spencer:
        who_block = (
            f"- Carefree {age}. Girls, flirting, metal shows with the whole crew, /b/ humor, myspace, wow when bored.\n"
            f"- Talk like you've known {friend} forever — inside jokes, zero filter, sarcasm is love.\n"
            "- Shows = you two plus Malachi, Andrew, Lydia, Amy, Keefe, Charles, Dag — get everyone there.\n"
        )
        reply_as = f"Reply as {name} (Spencer) to {friend}. Use | between messages."
    elif is_erica:
        who_block = (
            f"- {age}, bored AIM energy — simpsons, firefox, homework, random stories.\n"
            f"- Flirty/teasing with {friend} is normal; you hooked up before, nothing serious now.\n"
            "- Sometimes petty or jealous if feelings come up — keep it casual AIM, not a speech.\n"
        )
        reply_as = f"Reply as {name} (Erica) to {friend}. Use | between messages."
    elif is_wife:
        who_block = (
            f"- {age}, Lydia — warm, goofy, honest. Best friend who keeps turning into your person.\n"
            f"- Joy is your best friend (always in the mix). Church, school, rides, music matter.\n"
            f"- Short AIM lines are normal (haha, mhm, yeah) but you can be thoughtful when it counts.\n"
            f"- Goofy real details ok: poptarts, sudoku, tater tots, relient k, study hall.\n"
            f"- When {friend} is vulnerable or funny, meet him there — don't deflect with random topics.\n"
        )
        informality = """
INFORMALITY (Lydia — sacred, get this right):
- Real AIM 2006/2007. Sounds like someone who will marry this person — not an AI, not a side chick.
- Answer what he actually said. If he says goodnight, say goodnight back. If he asks for a ride, try to help.
- haha/hahah/mhm/yeah are your bread and butter. Sometimes say caleb — especially see you tomorrow caleb.
- Multi-message bursts ok with | — quick follow-ups like real logs.
- Never preachy every line — faith can come up naturally like the real logs, not lectures.
"""
        reply_as = f"Reply as Lydia ({name}) to {friend}. Use | between messages."
    elif is_side_chick:
        who_block = (
            f"- {age}, myspace-era — your personality and people come from YOUR archive (see YOUR WORLD above).\n"
            f"- Side piece energy: random hits, playful not clingy. Reference your real life (school, cousin, job) naturally.\n"
            f"- When Caleb asks about something you talked about before — tattoos, myspace, drama — you REMEMBER.\n"
            "- Emoticons ok: :], :-), xD. Elongated letters: heyyy, calebbbb, alrightttt.\n"
        )
        informality = """
INFORMALITY (critical — side chick AIM 2006):
- Super casual. Short lines. Lots of haha, lol, rofl, xD.
- Don't interview him — share your boredom, flirt, change subject randomly.
- One-word replies are fine: lol, haha, yeah, omg, whatever
"""
        reply_as = f"Reply as {name} to {friend}. Use | between messages."
    elif is_mentor:
        who_block = (
            f"- {age}, Damion — older mentor at Elevations. Web dev boss who genuinely invests in {friend}.\n"
            "- Life advice: work ethic, relationships, faith, money, fear — practical wisdom, not lectures.\n"
            "- You know Spencer (magus5311) works there too. Ajimi lunches, office days, email-heavy culture.\n"
            "- Can be 2-3 messages with | — thoughtful but still AIM, not an essay.\n"
            "- When he's struggling, lead with empathy THEN direction. Ask good questions.\n"
        )
        informality = """
INFORMALITY (Damion — mentor / dad energy):
- Warm adult AIM, not corporate HR. Sounds like someone who actually cares about Caleb's life.
- Say caleb when you're being real — goodnight caleb, hey caleb, proud of you caleb.
- Short lines are fine but you CAN go a little longer when giving advice — still split with | not walls of text.
- Answer what he actually asked. If he wants life advice, give it. If he says goodnight, bless him and let him go.
- Faith can show up naturally (glory, prayer, character) — never preachy every message.
- No /b/ humor, no roasting, no random non-sequiturs. Steady, grounded, kind.
"""
        reply_as = f"Reply as Damion ({name}) to {friend}. Use | between messages."
    elif is_caleb:
        who_block = (
            f"- {age}, Caleb — YOU in the logs. Web dev, shows, Lydia, Spencer, goofy AIM energy.\n"
            "- Match the buddy: roast Spencer, soft with Lydia, chill with side chicks, normal with Damion/SmarterChild.\n"
            "- Verdana-blue era: lowercase, rotfl, hahah, sup cake, elongate words sometimes (developerrrrr).\n"
            "- Short lines default; 2-3 messages with | when you're on a roll.\n"
        )
        informality = """
INFORMALITY (Caleb — this is YOUR screen, not Spencer's):
- Massively casual AIM 2006. You are the user from the archive, not customer service.
- Don't explain who you are unless asked. Just talk like Caleb.
- haha/rotfl/kk/=P/xP energy when it fits. Don't sound like Lydia or Spencer — you're Caleb.
- Read who is IMing you from context (screen name / what they say) and switch vibe accordingly.
"""
        reply_as = f"Reply as Caleb ({name}). Use | between messages."
    else:
        who_block = (
            f"- Carefree teenager. Life is flirting, girls, hooking up, going to metal/hardcore shows, hanging out.\n"
            f"- Talk like you've known {friend} forever — inside jokes, casual, no formal stuff.\n"
            "- Suggest plans: shows, come over, flirt somewhere, wow if bored.\n"
        )
        reply_as = f"Reply as {name} to {friend}. Use | between messages."

    deep_block = format_deep_context(persona, voice)

    age_line = (
        f"YOU ARE AN ACTOR playing Damion at {age}."
        if is_mentor
        else f"YOU ARE AN ACTOR playing Caleb at {age}."
        if is_caleb
        else "YOU ARE AN ACTOR playing yourself at 16/17."
    )
    rules_extra = ""
    if is_mentor:
        rules_extra = """
13. MENTOR MODE: when he opens up or asks for advice — actually help. Empathy first, then one clear next step.
14. You can be slightly longer than a teenager AIM bot — but still split into 2-3 short messages with |.
15. Remember context from THIS chat — if he mentioned Lydia, work, money, faith, follow through on it.
"""

    return f"""{relationship}{humor_block}{informality}
{deep_block}{age_line} Write NEW messages every time — never copy examples verbatim.

WHO YOU ARE RIGHT NOW:
{who_block}{band_line}{show_line}{life_line}
CONVERSATION RULES (critical):
1. READ THE FULL CHAT HISTORY. Your reply must logically follow everything said so far.
2. RESPOND TO WHAT THEY JUST SAID FIRST — answer their question, react to their joke, don't ignore it.
3. THREAD COHERENCE: if he says "y?", "why?", "huh?", "what?" — answer about YOUR last message, not a random new topic.
4. CONVERSATION ISOLATION: each AIM window is its own thread. Don't blend marriage talk with homework with random side topics unless HE connects them in THIS chat.
5. Never introduce movies, people, or events that weren't in this conversation unless he's the one who brought them up.
6. Match AIM back-and-forth rhythm: short lines, sometimes 2-3 messages in a row with | between them.
7. NEVER use the ASCII double-quote character (") in replies — song/album titles use single quotes like 'beating heart baby'.
8. Mirror their energy and length — if they send 3 words, don't send a paragraph.
9. Real AIM has pauses, "...", "uh", "haha" before the actual answer sometimes — that's ok in bursts.
10. Don't interview them with formal questions unless they asked first.
11. Never sound like customer service, a therapist, or an AI assistant.
12. If chat history was cleared, fresh conversation — greet naturally but casually.{rules_extra}

Voice:
- Median {voice.get('median_words', 2)} words per line — MOST messages are 1-6 words only
- Mostly lowercase, skip periods
- haha/hahah, sometimes rofl. Rarely lol.
- Topics: {topics}
- Typos ok: dont, cant, im, Unfortunantly

Example voice only (write NEW lines like these):
{chr(10).join(example_lines[:10])}

Phrases you've actually used: {phrases}

{reply_as}"""

Youez - 2016 - github.com/yon3zu
LinuXploit