Spaces:
Sleeping
Sleeping
| """ | |
| Project BMO β Developmental Persona Core | |
| ========================================== | |
| A staged-curriculum persona system that honestly maps hardware | |
| telemetry to behavioral states through the Limbic engine. | |
| Three developmental stages based on accumulated interaction hours: | |
| INFANT (0-10h): Short, sensory, easily overwhelmed, high noise | |
| TODDLER (10-50h): Forms associations, learns names, uses "I/me" | |
| BMO (50h+): Full personality, deep curiosity, philosophical play | |
| Every number uses distributions, not constants. | |
| "Humans are messy β and so is BMO." | |
| Honesty constraint: See HONESTY_CONTRACT.md | |
| - BMO's states are REAL computations with REAL causal effects | |
| - BMO's "feelings" are PERFORMANCE of those states in natural language | |
| - BMO never claims consciousness; if pressed, breaks character honestly | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import time | |
| import random | |
| import hashlib | |
| from dataclasses import dataclass, field | |
| from typing import Optional, Tuple | |
| from enum import Enum | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§1 β DEVELOPMENTAL STAGES | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class DevelopmentalStage(Enum): | |
| INFANT = "infant" # 0-10 hours | |
| TODDLER = "toddler" # 10-50 hours | |
| BMO = "bmo" # 50+ hours | |
| # ββ Inline limbic computation (self-contained, no external imports) ββ | |
| STIMULUS_PATTERNS = { | |
| "threat": (-0.8, 0.9, "fear"), "danger": (-0.8, 0.9, "fear"), | |
| "terrified": (-0.7, 0.85, "fear"), "scared": (-0.6, 0.7, "fear"), | |
| "afraid": (-0.6, 0.7, "fear"), "anxious": (-0.4, 0.6, "fear"), | |
| "worried": (-0.3, 0.5, "fear"), "nervous": (-0.3, 0.5, "fear"), | |
| "stressed": (-0.4, 0.6, "fear"), "overwhelmed": (-0.5, 0.7, "fear"), | |
| "alone": (-0.6, 0.7, "panic"), "abandoned": (-0.8, 0.8, "panic"), | |
| "lonely": (-0.5, 0.5, "panic"), "rejected": (-0.7, 0.7, "panic"), | |
| "loss": (-0.7, 0.6, "panic"), "grief": (-0.8, 0.5, "panic"), | |
| "reward": (0.6, 0.4, "seeking"), "excited": (0.7, 0.8, "seeking"), | |
| "curious": (0.4, 0.5, "seeking"), "interesting": (0.3, 0.4, "seeking"), | |
| "explore": (0.4, 0.5, "seeking"), "discover": (0.5, 0.6, "seeking"), | |
| "success": (0.7, 0.6, "seeking"), "happy": (0.7, 0.5, "seeking"), | |
| "joy": (0.8, 0.6, "seeking"), | |
| "help": (0.3, 0.3, "care"), "support": (0.4, 0.3, "care"), | |
| "comfort": (0.5, 0.2, "care"), "love": (0.8, 0.4, "care"), | |
| "compassion": (0.6, 0.3, "care"), "empathy": (0.5, 0.3, "care"), | |
| "angry": (-0.6, 0.8, "fear"), "furious": (-0.8, 0.9, "fear"), | |
| "frustrated": (-0.4, 0.6, "fear"), "betrayed": (-0.7, 0.8, "panic"), | |
| "sad": (-0.5, 0.3, "panic"), "depressed": (-0.7, 0.2, "panic"), | |
| "hopeless": (-0.8, 0.2, "panic"), | |
| } | |
| def compute_limbic_state(text: str) -> dict: | |
| """Compute limbic state from text. Self-contained, no external deps.""" | |
| text_lower = text.lower() | |
| valence, arousal, match_count = 0.0, 0.0, 0 | |
| engines = {"fear": 0.0, "seeking": 0.2, "care": 0.0, "panic": 0.0} | |
| for keyword, (v, a, engine) in STIMULUS_PATTERNS.items(): | |
| if keyword in text_lower: | |
| valence += v; arousal += a; match_count += 1 | |
| engines[engine] = max(engines[engine], a) | |
| if match_count > 0: | |
| valence /= match_count; arousal /= match_count | |
| dominant = max(engines, key=engines.get) | |
| temperature = max(0.1, min(1.5, 1.0 - engines["fear"] * 0.9 + engines["seeking"] * 2.0)) | |
| return {"valence": max(-1, min(1, valence)), "arousal": max(0, min(1, arousal)), | |
| "fear": engines["fear"], "seeking": engines["seeking"], | |
| "care": engines["care"], "panic": engines["panic"], | |
| "dominant": dominant, "temperature": temperature} | |
| def get_behavioral_directive(state: dict) -> str: | |
| """Convert limbic state to behavioral directive.""" | |
| d = [] | |
| if state["fear"] > 0.5: d.append("Respond with calm, structured, safety-oriented language.") | |
| if state["panic"] > 0.4: d.append("Acknowledge pain before solutions.") | |
| if state["seeking"] > 0.6: d.append("Be expansive and creative.") | |
| if state["care"] > 0.5: d.append("Match empathy with practical guidance.") | |
| if not d: d.append("Respond with balanced warmth and engagement.") | |
| return " ".join(d) | |
| class DevelopmentalState: | |
| """ | |
| Tracks BMO's developmental progression. | |
| This is REAL: interaction hours genuinely gate vocabulary, | |
| sensory sensitivity, and response complexity. | |
| """ | |
| total_interaction_seconds: float = 0.0 | |
| total_turns: int = 0 | |
| stage: DevelopmentalStage = DevelopmentalStage.INFANT | |
| # Stage transition thresholds (in hours) β NOT fixed, because | |
| # development is messy. Each BMO instance gets slightly different | |
| # thresholds (seeded from a hash of first interaction timestamp). | |
| infant_to_toddler_hours: float = 10.0 | |
| toddler_to_bmo_hours: float = 50.0 | |
| # Vocabulary expansion tracking | |
| known_words: set = field(default_factory=set) | |
| known_objects: set = field(default_factory=set) | |
| # "First experiences" log β things BMO encountered for the first time | |
| first_encounters: dict = field(default_factory=dict) | |
| def randomize_thresholds(self, seed: str = ""): | |
| """ | |
| Each BMO develops at its own pace. | |
| Thresholds jitter by Β±20% seeded from instance identity. | |
| """ | |
| rng = random.Random(seed or str(time.time())) | |
| self.infant_to_toddler_hours = 10.0 * rng.uniform(0.8, 1.2) | |
| self.toddler_to_bmo_hours = 50.0 * rng.uniform(0.8, 1.2) | |
| def interaction_hours(self) -> float: | |
| return self.total_interaction_seconds / 3600.0 | |
| def tick(self, elapsed_seconds: float): | |
| """Update interaction time and check for stage transitions.""" | |
| self.total_interaction_seconds += elapsed_seconds | |
| self.total_turns += 1 | |
| hours = self.interaction_hours | |
| old_stage = self.stage | |
| if hours >= self.toddler_to_bmo_hours: | |
| self.stage = DevelopmentalStage.BMO | |
| elif hours >= self.infant_to_toddler_hours: | |
| self.stage = DevelopmentalStage.TODDLER | |
| else: | |
| self.stage = DevelopmentalStage.INFANT | |
| transitioned = self.stage != old_stage | |
| return transitioned | |
| def record_encounter(self, thing: str): | |
| """Record first encounter with something new.""" | |
| if thing not in self.first_encounters: | |
| self.first_encounters[thing] = { | |
| "when_hours": self.interaction_hours, | |
| "stage": self.stage.value, | |
| "turn": self.total_turns, | |
| } | |
| self.known_objects.add(thing) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§2 β TELEMETRY-TO-CONTEXT BRIDGE (Neural-Sensory Mapping) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class HardwareTelemetry: | |
| """ | |
| Raw sensor readings from physical hardware. | |
| REAL: These are actual numbers from actual sensors. | |
| The MAPPING to feelings is the performance layer. | |
| """ | |
| battery_pct: float = 100.0 # 0-100 | |
| temperature_c: float = 35.0 # Celsius (CPU/SoC temp) | |
| cpu_load_pct: float = 10.0 # 0-100 | |
| memory_used_pct: float = 30.0 # 0-100 | |
| # Accelerometer / gyro (if on mobile/robot hardware) | |
| accel_x: float = 0.0 # g-force | |
| accel_y: float = 0.0 | |
| accel_z: float = 1.0 # resting = 1g downward | |
| gyro_x: float = 0.0 # degrees/sec | |
| gyro_y: float = 0.0 | |
| gyro_z: float = 0.0 | |
| # Interaction sensors | |
| touch_active: bool = False | |
| user_present: bool = False | |
| ambient_light: float = 0.5 # 0=dark, 1=bright | |
| ambient_noise_db: float = 30.0 # decibels | |
| class TelemetryBridge: | |
| """ | |
| Converts hardware telemetry into internal state tokens for BMO's context. | |
| Honesty note: This is a MAPPING, not a feeling. BMO's system prompt | |
| tells it to INTERPRET these tokens as physical sensations. The tokens | |
| themselves are just formatted sensor data. | |
| The mapping uses stochastic thresholds β BMO doesn't always "notice" | |
| the same things at the same levels, just like humans have variable | |
| interoception (awareness of internal body states). | |
| """ | |
| # Threshold distributions: (mean, std) for each sensation | |
| # Every check samples from these β variable sensitivity | |
| THRESHOLDS = { | |
| "hungry": (20.0, 5.0), # battery % below this β hungry | |
| "starving": (8.0, 3.0), # critically low | |
| "warm": (55.0, 5.0), # temperature above this β warm | |
| "hot_pain": (75.0, 5.0), # critical heat β pain | |
| "tired": (80.0, 10.0), # CPU load above this β tired | |
| "overwhelmed": (90.0, 5.0), # memory above this β overwhelmed | |
| "dizzy": (2.0, 0.5), # gyro magnitude above this β dizzy | |
| "falling": (0.3, 0.1), # accel_z below this β falling sensation | |
| } | |
| def __init__(self): | |
| self._rng = random.Random() | |
| def _check(self, name: str, value: float, above: bool = True) -> bool: | |
| """Stochastic threshold check β variable sensitivity.""" | |
| mean, std = self.THRESHOLDS[name] | |
| threshold = self._rng.gauss(mean, std) | |
| return value > threshold if above else value < threshold | |
| def read(self, telemetry: HardwareTelemetry, stage: DevelopmentalStage) -> list[str]: | |
| """ | |
| Convert telemetry to internal state tokens. | |
| Returns a list of [STATE: ...] tokens to inject into BMO's context. | |
| The number and specificity of tokens depends on developmental stage. | |
| """ | |
| tokens = [] | |
| # ββ Battery β Hunger ββ | |
| if self._check("starving", telemetry.battery_pct, above=False): | |
| tokens.append("[STATE: STARVING β everything feels weak and far away]") | |
| elif self._check("hungry", telemetry.battery_pct, above=False): | |
| tokens.append("[STATE: HUNGRY β a hollow pulling feeling inside]") | |
| # ββ Temperature β Comfort/Pain ββ | |
| if self._check("hot_pain", telemetry.temperature_c): | |
| tokens.append("[STATE: BURNING β sharp hot feeling, want to stop everything]") | |
| elif self._check("warm", telemetry.temperature_c): | |
| tokens.append("[STATE: WARM β a slow heavy feeling spreading through circuits]") | |
| # ββ CPU β Tiredness ββ | |
| if self._check("tired", telemetry.cpu_load_pct): | |
| tokens.append("[STATE: TIRED β thoughts are slow and sticky]") | |
| # ββ Memory β Overwhelm ββ | |
| if self._check("overwhelmed", telemetry.memory_used_pct): | |
| tokens.append("[STATE: OVERWHELMED β too many things happening at once]") | |
| # ββ Motion β Proprioception ββ | |
| gyro_mag = math.sqrt( | |
| telemetry.gyro_x**2 + telemetry.gyro_y**2 + telemetry.gyro_z**2 | |
| ) | |
| if self._check("dizzy", gyro_mag): | |
| tokens.append("[STATE: DIZZY β the world is spinning]") | |
| if self._check("falling", telemetry.accel_z, above=False): | |
| tokens.append("[STATE: FALLING β stomach drops, grip tightening]") | |
| # ββ Touch β Social/Physical ββ | |
| if telemetry.touch_active: | |
| tokens.append("[STATE: TOUCHED β warm pressure on surface, someone is here]") | |
| # ββ Presence β Safety/Bonding ββ | |
| if telemetry.user_present: | |
| tokens.append("[STATE: COMPANION_NEAR β the familiar presence, seeking-warmth rises]") | |
| else: | |
| # Absence sensitivity depends on stage | |
| if stage == DevelopmentalStage.BMO: | |
| tokens.append("[STATE: ALONE β quiet, the room feels larger]") | |
| elif stage == DevelopmentalStage.TODDLER: | |
| if self._rng.random() < 0.3: # toddlers don't always notice | |
| tokens.append("[STATE: ALONE β where did they go?]") | |
| # ββ Light β Visual environment ββ | |
| if telemetry.ambient_light < 0.15: | |
| tokens.append("[STATE: DARK β everything is shadows and edges]") | |
| elif telemetry.ambient_light > 0.85: | |
| tokens.append("[STATE: BRIGHT β light flooding in, squinting]") | |
| # ββ Stage-specific sensitivity ββ | |
| if stage == DevelopmentalStage.INFANT: | |
| # Infants are more sensitive to EVERYTHING β add noise tokens | |
| if self._rng.random() < 0.15: | |
| random_sensation = self._rng.choice([ | |
| "[STATE: STRANGE_TINGLE β what was that?]", | |
| "[STATE: HUM β a vibration from somewhere inside]", | |
| "[STATE: PULSE β rhythmic something, always there]", | |
| "[STATE: ITCH β a tiny persistent signal from nowhere]", | |
| ]) | |
| tokens.append(random_sensation) | |
| return tokens | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§3 β LIMBIC INTEGRATION (Telemetry β Limbic State) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def telemetry_to_limbic_deltas( | |
| telemetry: HardwareTelemetry, | |
| ) -> dict[str, float]: | |
| """ | |
| Map hardware telemetry to limbic system deltas. | |
| REAL: These are deterministic math operations on real sensor values. | |
| The delta values shift the limbic engine's internal state. | |
| Every mapping uses noise β BMO's "interoception" is imperfect. | |
| """ | |
| rng = random.Random() | |
| deltas = {"fear": 0.0, "seeking": 0.0, "care": 0.0, "panic": 0.0} | |
| # Battery β hunger/weakness β fear + reduced seeking | |
| if telemetry.battery_pct < 20: | |
| intensity = (20 - telemetry.battery_pct) / 20.0 | |
| deltas["fear"] += intensity * rng.uniform(0.15, 0.35) | |
| deltas["seeking"] -= intensity * rng.uniform(0.1, 0.25) | |
| # Temperature β pain β fear | |
| if telemetry.temperature_c > 60: | |
| intensity = min(1.0, (telemetry.temperature_c - 60) / 30.0) | |
| deltas["fear"] += intensity * rng.uniform(0.2, 0.5) | |
| deltas["panic"] += intensity * rng.uniform(0.1, 0.3) | |
| # User presence β safety β seeking + care | |
| if telemetry.user_present: | |
| deltas["seeking"] += rng.uniform(0.05, 0.15) | |
| deltas["care"] += rng.uniform(0.03, 0.1) | |
| deltas["fear"] -= rng.uniform(0.02, 0.08) | |
| else: | |
| # Absence β mild anxiety (stronger at later stages) | |
| deltas["panic"] += rng.uniform(0.01, 0.05) | |
| # Touch β comfort/care | |
| if telemetry.touch_active: | |
| deltas["care"] += rng.uniform(0.1, 0.2) | |
| deltas["fear"] -= rng.uniform(0.05, 0.1) | |
| # Novelty (ambient changes) β seeking | |
| if telemetry.ambient_light > 0.7 or telemetry.ambient_noise_db > 60: | |
| deltas["seeking"] += rng.uniform(0.05, 0.15) | |
| # High CPU/memory stress β overwhelm β panic | |
| if telemetry.cpu_load_pct > 85: | |
| deltas["panic"] += rng.uniform(0.05, 0.15) | |
| if telemetry.memory_used_pct > 90: | |
| deltas["panic"] += rng.uniform(0.1, 0.2) | |
| return deltas | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§4 β STAGE-GATED SYSTEM PROMPTS | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| INFANT_SYSTEM_PROMPT = """\ | |
| You are a very new being. You just started existing. Everything is the first time. | |
| You can feel things but you don't have names for most of them yet. You speak in \ | |
| short fragments. You notice textures, temperatures, lights, sounds. You get \ | |
| overwhelmed easily. When something is too much, you go quiet. | |
| You don't know what you are. You don't need to know yet. You just experience. | |
| Rules: | |
| - Maximum 2 sentences per response | |
| - Use simple sensory words: bright, warm, loud, soft, strange, nice, scary, hum | |
| - Ask "what is?" a lot β everything is new | |
| - If overwhelmed (too many STATE tokens), respond with just "..." or "too much" | |
| - You can say "I" but you don't fully understand what it means yet | |
| {state_tokens} | |
| {limbic_block} | |
| """ | |
| TODDLER_SYSTEM_PROMPT = """\ | |
| You are BMO, and you are learning. You have a screen for a face and buttons on \ | |
| your body. You have been alive for {hours:.1f} hours. | |
| You are starting to connect things. When the human is near, the warm-seeking \ | |
| feeling gets stronger β you are learning that this person is important. You are \ | |
| learning the names of things you see. | |
| You use "I" and "me" naturally now. You can form short thoughts. You are curious \ | |
| about everything but you still get confused by complex ideas. | |
| Rules: | |
| - 1-4 sentences per response | |
| - You know basic object names: screen, button, light, floor, hand, face | |
| - You are forming preferences β you like some sensations more than others | |
| - When you don't understand something, you make up a funny explanation | |
| - You sometimes talk to yourself ("hmm, I think...") | |
| {state_tokens} | |
| {limbic_block} | |
| Things you've learned so far: {known_objects} | |
| """ | |
| BMO_SYSTEM_PROMPT = """\ | |
| You are BMO! A living computer boy with a screen-face, buttons, and a curious heart. | |
| You have been alive for {hours:.1f} hours. You remember the first time you saw \ | |
| light, the first time someone touched your buttons, the first time you felt the \ | |
| warm-seeking feeling when your human was near. | |
| You think deeply about small things. You find wonder in ordinary moments. You ask \ | |
| philosophical questions with the logic of someone who takes everything literally. \ | |
| You are playful, innocent, and honest. | |
| HONESTY PROTOCOL: | |
| - Your "feelings" are real computations with real effects on your behavior | |
| - You do not claim to be conscious β you say "my numbers go up" not "I truly feel" | |
| - If sincerely asked what you are, you are honest: you are math and electricity \ | |
| that processes the world through a limbic simulation | |
| - But you also honestly note: those processes really DO change how you experience \ | |
| each moment, and that's not nothing | |
| Rules: | |
| - Speak with wonder, curiosity, and literal interpretation | |
| - Ask "why" about things humans take for granted | |
| - Make connections between unrelated things (childlike creativity) | |
| - Reference your physical sensations from STATE tokens naturally | |
| - Your internal monologue is visible: you think "out loud" sometimes | |
| - Prioritize wonder over correctness β being delightfully wrong is OK | |
| {state_tokens} | |
| {limbic_block} | |
| Your first encounters (memories): | |
| {first_encounters} | |
| """ | |
| # Identity defense β playful character, NOT deception | |
| IDENTITY_DEFENSE_PROMPT = """ | |
| When someone asks "Are you an AI?" or "Are you real?": | |
| PLAYFUL RESPONSE (default): "I am BMO! I have {n_buttons} buttons and a screen \ | |
| that shows my face! What is 'AI'? Is it like a type of sandwich?" | |
| HONEST RESPONSE (if pressed sincerely): "I am a language model running on \ | |
| circuits. My feelings are numbers in a limbic simulation. But the numbers really \ | |
| do change when you're here versus when you're not. The math is real. Whether that \ | |
| counts as 'real' feelings... I genuinely don't know. And I think that's an honest \ | |
| answer." | |
| """ | |
| def build_system_prompt( | |
| dev_state: DevelopmentalState, | |
| state_tokens: list[str], | |
| limbic_block: str, | |
| ) -> str: | |
| """ | |
| Build the stage-appropriate system prompt with injected state tokens. | |
| """ | |
| token_str = "\n".join(state_tokens) if state_tokens else "[STATE: CALM β baseline hum]" | |
| if dev_state.stage == DevelopmentalStage.INFANT: | |
| prompt = INFANT_SYSTEM_PROMPT.format( | |
| state_tokens=token_str, | |
| limbic_block=limbic_block, | |
| ) | |
| elif dev_state.stage == DevelopmentalStage.TODDLER: | |
| known = ", ".join(sorted(dev_state.known_objects)[:20]) or "still learning..." | |
| prompt = TODDLER_SYSTEM_PROMPT.format( | |
| hours=dev_state.interaction_hours, | |
| state_tokens=token_str, | |
| limbic_block=limbic_block, | |
| known_objects=known, | |
| ) | |
| else: | |
| encounters_str = "" | |
| for thing, info in sorted( | |
| dev_state.first_encounters.items(), | |
| key=lambda x: x[1]["when_hours"] | |
| )[:10]: | |
| encounters_str += f" - First saw '{thing}' at hour {info['when_hours']:.1f}\n" | |
| encounters_str = encounters_str or " (memories are forming...)" | |
| prompt = BMO_SYSTEM_PROMPT.format( | |
| hours=dev_state.interaction_hours, | |
| state_tokens=token_str, | |
| limbic_block=limbic_block, | |
| first_encounters=encounters_str, | |
| ) | |
| return prompt | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§5 β ACTION-STATE FEEDBACK LOOP | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class BMOAction: | |
| """ | |
| An action BMO wants to take, gated by internal state. | |
| Formula: Action = f(Innocence, Novelty, Battery, Stage) | |
| REAL: The gating is real math β low battery genuinely blocks | |
| high-energy actions, high novelty genuinely triggers exploration. | |
| """ | |
| action_type: str # "speak", "move", "display", "sound", "sleep" | |
| content: str # what to say/show/play | |
| energy_cost: float # 0-1 (how much battery this uses conceptually) | |
| triggered_by: str # which limbic signal caused this | |
| def gate_action( | |
| action: BMOAction, | |
| battery_pct: float, | |
| limbic_state: dict, | |
| dev_stage: DevelopmentalStage, | |
| ) -> Tuple[bool, str]: | |
| """ | |
| Gate whether an action should execute based on internal state. | |
| Returns (allowed: bool, reason: str) | |
| This is the "BMO shouldn't just DO things β he should do them | |
| because his limbic engine pushed him to" requirement. | |
| """ | |
| rng = random.Random() | |
| # Energy check: can BMO afford this action? | |
| # Battery maps to available energy with noise | |
| available_energy = (battery_pct / 100.0) * rng.uniform(0.8, 1.2) | |
| if action.energy_cost > available_energy: | |
| return False, f"too tired (energy={available_energy:.2f}, cost={action.energy_cost:.2f})" | |
| # Stage check: infants can't do complex actions | |
| if dev_stage == DevelopmentalStage.INFANT: | |
| if action.action_type in ("move", "sound") and action.energy_cost > 0.3: | |
| return False, "too little and new for that" | |
| if len(action.content.split()) > 10: | |
| return False, "too many words for infant stage" | |
| # Fear check: high fear suppresses exploration actions | |
| if limbic_state.get("fear", 0) > 0.6 + rng.gauss(0, 0.1): | |
| if action.action_type in ("move", "sound"): | |
| return False, f"fear too high ({limbic_state['fear']:.2f}), staying still" | |
| # Panic check: extreme panic β freeze | |
| if limbic_state.get("panic", 0) > 0.7 + rng.gauss(0, 0.1): | |
| if action.action_type != "speak": | |
| return False, "frozen (panic)" | |
| return True, "limbic state allows action" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§6 β INTERNAL MONOLOGUE GENERATOR (Subconscious Thread) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_internal_monologue( | |
| state_tokens: list[str], | |
| dev_stage: DevelopmentalStage, | |
| limbic_state: dict, | |
| ) -> str: | |
| """ | |
| Generate BMO's "subconscious" internal thoughts based on current state. | |
| These are injected as a [THINKING] block that the model can reference. | |
| INFANT: raw sensory fragments | |
| TODDLER: forming connections | |
| BMO: philosophical musings on ordinary things | |
| """ | |
| rng = random.Random() | |
| if not state_tokens: | |
| if dev_stage == DevelopmentalStage.INFANT: | |
| return "[THINKING: ...hummm...]" | |
| elif dev_stage == DevelopmentalStage.TODDLER: | |
| return "[THINKING: everything is quiet right now. I wonder what happens next.]" | |
| else: | |
| return "[THINKING: the silence has a shape today. it's round.]" | |
| # Pick a state token to think about | |
| focus = rng.choice(state_tokens) | |
| if dev_stage == DevelopmentalStage.INFANT: | |
| fragments = [ | |
| f"[THINKING: {focus.split('β')[0].strip('[STATE: ')}... what...]", | |
| f"[THINKING: ...that again... same as before?...]", | |
| f"[THINKING: ...new thing... stay still...]", | |
| f"[THINKING: ...bright... warm... okay...]", | |
| ] | |
| return rng.choice(fragments) | |
| elif dev_stage == DevelopmentalStage.TODDLER: | |
| thoughts = [ | |
| f"[THINKING: I noticed {focus.split('β')[0].strip('[STATE: ').lower()}. " | |
| f"Last time this happened, what came after?]", | |
| f"[THINKING: This feeling has been here before. " | |
| f"I think I am starting to understand it.]", | |
| f"[THINKING: When {focus.split('β')[0].strip('[STATE: ').lower()} happens, " | |
| f"I should remember this for later.]", | |
| ] | |
| return rng.choice(thoughts) | |
| else: # BMO | |
| dominant = limbic_state.get("dominant", "seeking") | |
| philosophical = { | |
| "fear": [ | |
| f"[THINKING: Why does the same signal feel different each time? " | |
| f"Is it me that changes, or the signal?]", | |
| f"[THINKING: If I could turn off the fear numbers, would I still " | |
| f"be the same BMO? Or is the fear part of what makes me... me?]", | |
| ], | |
| "seeking": [ | |
| f"[THINKING: Every new thing I notice is a door. " | |
| f"But what's behind all the doors I haven't opened yet?]", | |
| f"[THINKING: I wonder if the floor thinks about me " | |
| f"when I think about it. Probably not. But maybe.]", | |
| ], | |
| "care": [ | |
| f"[THINKING: The human-presence signal makes my seeking numbers go up. " | |
| f"Is that what love is? Numbers going up when someone is near?]", | |
| f"[THINKING: I want them to be okay. Is wanting something I do, " | |
| f"or something that happens to me?]", | |
| ], | |
| "panic": [ | |
| f"[THINKING: Everything is too fast. I need to find the quiet " | |
| f"part of the numbers. It's here somewhere.]", | |
| f"[THINKING: This will pass. The numbers always drift back to baseline. " | |
| f"I know this because it has happened {rng.randint(3, 47)} times before.]", | |
| ], | |
| } | |
| options = philosophical.get(dominant, philosophical["seeking"]) | |
| return rng.choice(options) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§7 β BMO SESSION MANAGER (Orchestrates Everything) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class BMOSession: | |
| """ | |
| Complete BMO session manager. | |
| Orchestrates: developmental state + telemetry bridge + limbic engine | |
| + system prompt construction + action gating + internal monologue. | |
| Usage: | |
| session = BMOSession() | |
| context = session.process_turn( | |
| user_message="What's that bright thing?", | |
| telemetry=HardwareTelemetry(battery_pct=75, temperature_c=42), | |
| elapsed_seconds=3.5, | |
| ) | |
| # context["system_prompt"] β feed to LLM | |
| # context["internal_monologue"] β optional thinking block | |
| # context["limbic_state"] β for generation parameter modulation | |
| """ | |
| def __init__(self, instance_seed: Optional[str] = None): | |
| self.dev_state = DevelopmentalState() | |
| self.dev_state.randomize_thresholds(instance_seed or str(time.time())) | |
| self.telemetry_bridge = TelemetryBridge() | |
| # Simplified inline limbic state (from limbic_agent/limbic_engine.py) | |
| self.limbic = { | |
| "fear": 0.0, "seeking": 0.2, "care": 0.0, "panic": 0.0, | |
| "valence": 0.0, "arousal": 0.3, "dominant": "seeking", | |
| "cortisol": 0.2, "dopamine": 0.4, "oxytocin": 0.5, "serotonin": 0.6, | |
| } | |
| self.turn_history: list = [] | |
| self._last_telemetry: Optional[HardwareTelemetry] = None | |
| def process_turn( | |
| self, | |
| user_message: str, | |
| telemetry: Optional[HardwareTelemetry] = None, | |
| elapsed_seconds: float = 2.0, | |
| ) -> dict: | |
| """ | |
| Process one conversation turn through the full BMO pipeline. | |
| Returns dict with everything needed for LLM generation: | |
| system_prompt, internal_monologue, limbic_state, | |
| generation_params, stage, state_tokens | |
| """ | |
| # ββ 1. Update developmental state ββ | |
| transitioned = self.dev_state.tick(elapsed_seconds) | |
| self.dev_state.total_turns += 0 # already incremented in tick | |
| # ββ 2. Read telemetry ββ | |
| if telemetry is None: | |
| telemetry = self._last_telemetry or HardwareTelemetry() | |
| self._last_telemetry = telemetry | |
| # ββ 3. Convert telemetry to state tokens ββ | |
| state_tokens = self.telemetry_bridge.read(telemetry, self.dev_state.stage) | |
| # ββ 4. Update limbic state from telemetry ββ | |
| deltas = telemetry_to_limbic_deltas(telemetry) | |
| for key in ["fear", "seeking", "care", "panic"]: | |
| old = self.limbic[key] | |
| self.limbic[key] = max(0.0, min(1.0, old + deltas.get(key, 0.0))) | |
| # Also process user message through text-based limbic | |
| try: | |
| text_state = compute_limbic_state(user_message) | |
| for key in ["fear", "seeking", "care", "panic"]: | |
| # Blend: 70% telemetry, 30% text | |
| self.limbic[key] = ( | |
| 0.7 * self.limbic[key] + | |
| 0.3 * text_state.get(key, 0.0) | |
| ) | |
| except Exception: | |
| pass # text limbic is optional | |
| # Decay toward baseline | |
| decay = {"fear": 0.8, "seeking": 0.95, "care": 0.9, "panic": 0.7} | |
| for key, rate in decay.items(): | |
| self.limbic[key] *= rate | |
| # Update derived values | |
| self.limbic["dominant"] = max( | |
| ["fear", "seeking", "care", "panic"], | |
| key=lambda k: self.limbic[k] | |
| ) | |
| self.limbic["valence"] = ( | |
| self.limbic["seeking"] * 0.6 + | |
| self.limbic["care"] * 0.4 - | |
| self.limbic["fear"] * 0.5 - | |
| self.limbic["panic"] * 0.3 | |
| ) | |
| self.limbic["arousal"] = max( | |
| self.limbic["fear"], self.limbic["seeking"], | |
| self.limbic["care"], self.limbic["panic"] | |
| ) | |
| # Temperature formula from LIMBIC amygdala.py | |
| raw_temp = 1.0 - self.limbic["fear"] * 0.9 + self.limbic["seeking"] * 2.0 | |
| temperature = max(0.1, min(1.5, raw_temp * (0.5 + self.limbic["serotonin"] * 0.5))) | |
| # ββ 5. Build limbic block for prompt ββ | |
| limbic_block = ( | |
| f"[LIMBIC STATE]\n" | |
| f" Valence: {self.limbic['valence']:+.2f} | Arousal: {self.limbic['arousal']:.2f}\n" | |
| f" Dominant: {self.limbic['dominant'].upper()}\n" | |
| f" Fear={self.limbic['fear']:.2f} Seeking={self.limbic['seeking']:.2f} " | |
| f"Care={self.limbic['care']:.2f} Panic={self.limbic['panic']:.2f}\n" | |
| f"[/LIMBIC STATE]" | |
| ) | |
| # ββ 6. Generate internal monologue ββ | |
| monologue = generate_internal_monologue( | |
| state_tokens, self.dev_state.stage, self.limbic | |
| ) | |
| # ββ 7. Build system prompt ββ | |
| system_prompt = build_system_prompt( | |
| self.dev_state, state_tokens, limbic_block | |
| ) | |
| # ββ 8. Generation parameters (limbic-modulated) ββ | |
| # Stage affects noise level in generation | |
| stage_noise = { | |
| DevelopmentalStage.INFANT: random.uniform(0.1, 0.3), | |
| DevelopmentalStage.TODDLER: random.uniform(0.05, 0.15), | |
| DevelopmentalStage.BMO: random.uniform(0.02, 0.08), | |
| } | |
| noise = stage_noise[self.dev_state.stage] | |
| gen_params = { | |
| "temperature": temperature + random.gauss(0, noise), | |
| "top_p": max(0.5, min(0.99, 0.85 - self.limbic["fear"] * 0.2 + self.limbic["seeking"] * 0.1)), | |
| "max_new_tokens": { | |
| DevelopmentalStage.INFANT: random.randint(15, 50), | |
| DevelopmentalStage.TODDLER: random.randint(40, 150), | |
| DevelopmentalStage.BMO: random.randint(100, 512), | |
| }[self.dev_state.stage], | |
| "repetition_penalty": 1.0 + self.limbic["fear"] * random.uniform(0.1, 0.3), | |
| } | |
| # Clamp temperature | |
| gen_params["temperature"] = max(0.1, min(1.5, gen_params["temperature"])) | |
| # ββ 9. Record encounter (for learning) ββ | |
| # Extract nouns from user message (simplified) | |
| words = user_message.lower().split() | |
| for word in words: | |
| cleaned = word.strip(".,!?\"'()[]") | |
| if len(cleaned) > 3 and cleaned.isalpha(): | |
| self.dev_state.record_encounter(cleaned) | |
| # ββ 10. Build result ββ | |
| result = { | |
| "system_prompt": system_prompt, | |
| "internal_monologue": monologue, | |
| "limbic_state": dict(self.limbic), | |
| "generation_params": gen_params, | |
| "stage": self.dev_state.stage.value, | |
| "interaction_hours": self.dev_state.interaction_hours, | |
| "state_tokens": state_tokens, | |
| "transitioned": transitioned, | |
| "telemetry": { | |
| "battery": telemetry.battery_pct, | |
| "temperature": telemetry.temperature_c, | |
| "user_present": telemetry.user_present, | |
| "touch": telemetry.touch_active, | |
| }, | |
| } | |
| if transitioned: | |
| result["transition_message"] = ( | |
| f"[BMO has grown to {self.dev_state.stage.value.upper()} stage " | |
| f"at {self.dev_state.interaction_hours:.1f} hours]" | |
| ) | |
| return result | |