"""Perceive: read the moment from what Brian just said — cheap, deterministic, no LLM. The control plane's senses. A lexicon + signal heuristic that estimates emotional charge (sentiment, intensity, tilt) and the kind of turn (emotional / strategic / meta / build / casual). It's rough on purpose — the point of the society-of-parts design is that *most* parts are free heuristics and the LLM is the exception. What it's GOOD at: catching the obvious, action-relevant signal — especially tilt (the mental-game core of her job). What it's NOT: nuanced understanding (that's the LLM's job downstream). `route` turns this read into a per-turn register nudge. """ from __future__ import annotations import re # Negative / tilt charge — frustration, downswing, mental-game trouble. _NEG = ( "tilt", "tilted", "steaming", "steam", "frustrated", "pissed", "angry", "annoyed", "hate", "sick of", "fed up", "card dead", "carddead", "cold deck", "brutal", "cooler", "punt", "punted", "spew", "spewing", "stuck", "losing", "bad beat", "badbeat", "unlucky", "rigged", "sigh", "ugh", "fml", "can't win", "cant win", "miserable", "over it", "fuck this", "hate this", "can't catch", "cant catch", ) # Positive / up charge — running good, energized. _POS = ( "great", "awesome", "love", "crushing", "running good", "rungood", "hell yeah", "let's go", "lets go", "stoked", "pumped", "feeling good", "on fire", "dialed", "killing it", "in the zone", "so good", "amazing", ) _PROFANITY = ("fuck", "fucking", "shit", "damn", "bullshit", "fml") # Strategic / poker-analysis cues. _STRATEGY = ( "fold", "call", "raise", "3bet", "three-bet", "range", "equity", "gto", "bluff", "value", "river", "turn", "flop", "preflop", "pot odds", "outs", "should i", "what would you", "sizing", "check-raise", "overbet", "line", ) # Meta / about-her cues. _META = ( "do you", "are you", "yourself", "conscious", "sentient", "you feel", "you exist", "your thoughts", "your mind", "who are you", "what are you", "your own", ) # Building / technical cues. _BUILD = ( "code", "function", "bug", "build", "implement", "refactor", "architecture", "prompt", "python", "commit", "deploy", "pipeline", "algorithm", "repo", "api", "schema", "module", "wire it", "the model", ) def _clamp(x: float, lo: float = 0.0, hi: float = 1.0) -> float: return max(lo, min(hi, x)) def _hits(text: str, lexicon: tuple[str, ...]) -> int: """Count lexicon matches. Multi-token terms match as substrings ('card dead'); single words match on word boundaries so 'line' doesn't fire inside 'pipeline'.""" n = 0 for term in lexicon: if " " in term or "-" in term or "'" in term: n += 1 if term in text else 0 else: n += 1 if re.search(rf"\b{re.escape(term)}\b", text) else 0 return n def read(user_msg: str) -> dict: """Estimate the emotional charge + kind of this turn. Returns {sentiment: -1..1, intensity: 0..1, tilt: 0..1, kind: str}.""" t = (user_msg or "").lower() words = re.findall(r"[a-z']+", t) neg = _hits(t, _NEG) pos = _hits(t, _POS) prof = _hits(t, _PROFANITY) exclam = user_msg.count("!") caps = sum(1 for w in re.findall(r"[A-Za-z]{2,}", user_msg) if w.isupper()) short_and_hot = len(words) <= 6 and (neg or exclam or prof) intensity = _clamp(0.2 * exclam + 0.25 * caps + 0.3 * prof + (0.2 if short_and_hot else 0)) sentiment = _clamp((pos - neg) * 0.5, -1.0, 1.0) tilt = _clamp(0.35 * neg + 0.5 * intensity) if (neg or prof) else 0.0 if tilt >= 0.4 or (neg and sentiment < 0): kind = "emotional" elif _hits(t, _STRATEGY): kind = "strategic" elif _hits(t, _META): kind = "meta" elif _hits(t, _BUILD): kind = "build" elif pos and intensity >= 0.3: kind = "emotional" # up/energized still wants an emotional read else: kind = "casual" return {"sentiment": round(sentiment, 2), "intensity": round(intensity, 2), "tilt": round(tilt, 2), "kind": kind}