from __future__ import annotations from dataclasses import dataclass from typing import Protocol import numpy as np from engine.brain import Brain, BrainSignals Dialogue = list[dict[str, str]] class BrainClient(Protocol): def step( self, agent_id: str, system_prompt: str, dialogue_so_far: Dialogue, new_user_text: str, silence_flag: bool, ) -> dict[str, object]: ... def step_many( self, agent_payloads: list[dict[str, str]], dialogue_so_far: Dialogue, new_user_text: str, silence_flag: bool, ) -> dict[str, object]: ... def generate(self, agent_id: str, system_prompt: str, dialogue: Dialogue) -> dict[str, object]: ... class ModalBrainClient: """Thin lazy wrapper around `modal_app.brain_modal` remote functions.""" def __init__(self) -> None: from modal_app import brain_modal self._brain_modal = brain_modal def step( self, agent_id: str, system_prompt: str, dialogue_so_far: Dialogue, new_user_text: str, silence_flag: bool, ) -> dict[str, object]: return self._brain_modal.step.remote(agent_id, system_prompt, dialogue_so_far, new_user_text, silence_flag) def step_many( self, agent_payloads: list[dict[str, str]], dialogue_so_far: Dialogue, new_user_text: str, silence_flag: bool, ) -> dict[str, object]: return self._brain_modal.step_many.remote(agent_payloads, dialogue_so_far, new_user_text, silence_flag) def generate(self, agent_id: str, system_prompt: str, dialogue: Dialogue) -> dict[str, object]: return self._brain_modal.generate.remote(agent_id, system_prompt, dialogue) @dataclass(frozen=True) class Persona: agent_id: str display_name: str system_prompt: str class LiveBrain(Brain): """One-agent Brain implementation backed by Modal.""" def __init__(self, persona: Persona, client: BrainClient | None = None) -> None: self.persona = persona self.client = client or ModalBrainClient() self.last_raw: dict[str, object] | None = None self._latest: BrainSignals | None = None def step(self, dialogue_so_far: Dialogue, new_user_text: str, silence_flag: bool) -> BrainSignals: raw = self.client.step( self.persona.agent_id, self.persona.system_prompt, dialogue_so_far, new_user_text, silence_flag, ) self.last_raw = raw self._latest = signals_from_raw(raw) return self._latest def next_signals(self) -> BrainSignals: if self._latest is None: raise RuntimeError("LiveBrain has no queued signals; call step() first") return self._latest def generate(self, dialogue: Dialogue) -> dict[str, object]: return self.client.generate(self.persona.agent_id, self.persona.system_prompt, dialogue) class LiveBrainPanel: """Batched Modal brain calls for a multi-agent panel.""" def __init__(self, personas: list[Persona], client: BrainClient | None = None) -> None: if not personas: raise ValueError("personas must not be empty") self.personas = personas self.client = client or ModalBrainClient() self.last_raw: dict[str, object] | None = None self.last_results: dict[str, dict[str, object]] = {} @property def agent_ids(self) -> list[str]: return [persona.agent_id for persona in self.personas] def step_all(self, dialogue_so_far: Dialogue, new_user_text: str, silence_flag: bool) -> dict[str, BrainSignals]: payloads = [ {"agent_id": persona.agent_id, "system_prompt": persona.system_prompt} for persona in self.personas ] raw = self.client.step_many(payloads, dialogue_so_far, new_user_text, silence_flag) self.last_raw = raw results = raw.get("results", {}) if isinstance(raw, dict) else {} self.last_results = {agent_id: dict(result) for agent_id, result in results.items()} return {agent_id: signals_from_raw(result) for agent_id, result in self.last_results.items()} def generate(self, agent_id: str, dialogue: Dialogue) -> dict[str, object]: persona = self.persona(agent_id) return self.client.generate(agent_id, persona.system_prompt, dialogue) def persona(self, agent_id: str) -> Persona: for persona in self.personas: if persona.agent_id == agent_id: return persona raise KeyError(f"unknown persona {agent_id!r}") def signals_from_raw(raw: dict[str, object]) -> BrainSignals: if not raw.get("ok", False): raise RuntimeError(str(raw.get("failure", "brain call failed"))) return BrainSignals( surprise=float(raw["surprise"]), hidden=np.asarray(raw["hidden"], dtype=np.float32), readiness=float(raw["readiness"]), p_end=float(raw["p_end"]), )