decompress / engine /conversation.py
squaredcuber's picture
Deploy Decompress MiniCPM demo
bcbb8c5 verified
Raw
History Blame Contribute Delete
9.92 kB
from __future__ import annotations
import argparse
import json
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Sequence
from engine.controller import Action, ControllerConfig, ControllerTick, WhenToSpeakController
from engine.live_brain import BrainClient, Dialogue, LiveBrainPanel, Persona
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_LOG_PATH = ROOT / "eval" / "conversation_log.json"
@dataclass(frozen=True)
class TranscriptChunk:
text: str
silence_flag: bool = False
@dataclass(frozen=True)
class ConversationResult:
events: list[dict[str, Any]]
dialogue: Dialogue
personas: list[Persona]
total_latency_ms: float
model_name: str
device_name: str
generated_examples: list[dict[str, Any]]
def default_personas() -> list[Persona]:
return [
Persona(
agent_id="numbers_vc",
display_name="Numbers VC",
system_prompt=(
"You are a numbers-obsessed venture investor. Be blunt, specific, and quantitative. "
"Ask for denominators, cohorts, margins, contract evidence, and arithmetic that actually closes."
),
),
Persona(
agent_id="vision_optimist",
display_name="Vision Optimist",
system_prompt=(
"You are a big-vision optimist. You look for the huge version of the company, but your "
"questions are crisp and founder-facing when the story needs a missing bridge."
),
),
Persona(
agent_id="ruthless_skeptic",
display_name="Ruthless Skeptic",
system_prompt=(
"You are a ruthless startup skeptic. Interrupt bad claims in plain English. No pleasantries, "
"no throat-clearing, no softening. Be sharp without being long."
),
),
]
def sample_pitch_stream() -> list[TranscriptChunk]:
return [
TranscriptChunk("so basically our startup helps small retailers manage inventory"),
TranscriptChunk("we connect to their point of sale and purchase orders"),
TranscriptChunk("we already have ten thousand stores and zero churn after launching last week"),
TranscriptChunk("then we predict stockouts and write reorder suggestions automatically"),
TranscriptChunk("we are converting pilots into paid contracts this month"),
TranscriptChunk("so we think this becomes the operating system for local retail"),
TranscriptChunk("that's the pitch", silence_flag=True),
]
def demo_controller_config() -> ControllerConfig:
return ControllerConfig(
tau=0.85,
min_readiness=0.08,
w_surprise=0.85,
w_barge=0.85,
w_readiness=0.75,
w_end=1.05,
backchannel_tau_fraction=0.72,
barge_tau_fraction=0.50,
turn_end_tau_discount=0.45,
)
class Conversation:
def __init__(
self,
personas: list[Persona],
brain_panel: LiveBrainPanel,
controller: WhenToSpeakController | None = None,
) -> None:
self.personas = personas
self.brain_panel = brain_panel
self.controller = controller or WhenToSpeakController(
brain_panel.agent_ids,
config=demo_controller_config(),
)
def run(self, stream: Sequence[TranscriptChunk]) -> ConversationResult:
started = time.perf_counter()
dialogue: Dialogue = []
current_user_text = ""
events: list[dict[str, Any]] = []
generated_examples: list[dict[str, Any]] = []
for step_index, chunk in enumerate(stream, start=1):
dialogue_before = _dialogue_with_current_user(dialogue, current_user_text)
signals = self.brain_panel.step_all(dialogue_before, chunk.text, chunk.silence_flag)
current_user_text = _join_text(current_user_text, chunk.text)
tick = self.controller.tick(signals, floor_holder="human")
event = self._event(step_index, chunk, tick)
winner = tick.winner
if winner is not None:
if current_user_text:
dialogue.append({"role": "user", "speaker": "founder", "text": current_user_text})
current_user_text = ""
generated = self.brain_panel.generate(winner, dialogue)
reply_text = str(generated.get("reply_text", ""))
if reply_text:
dialogue.append({"role": "assistant", "speaker": winner, "text": reply_text})
event["generated"] = {
"agent_id": winner,
"reply_text": reply_text,
"reply_source": generated.get("reply_source"),
"raw_reply_text": generated.get("raw_reply_text"),
"latency_ms": generated.get("latency_ms"),
"model_name": generated.get("model_name"),
}
generated_examples.append(event["generated"])
events.append(event)
if current_user_text:
dialogue.append({"role": "user", "speaker": "founder", "text": current_user_text})
raw = self.brain_panel.last_raw or {}
return ConversationResult(
events=events,
dialogue=dialogue,
personas=self.personas,
total_latency_ms=(time.perf_counter() - started) * 1000.0,
model_name=str(raw.get("model_name", "")),
device_name=str(raw.get("device_name", "")),
generated_examples=generated_examples,
)
def _event(self, step_index: int, chunk: TranscriptChunk, tick: ControllerTick) -> dict[str, Any]:
raw = self.brain_panel.last_raw or {}
decisions = {}
for agent_id, decision in tick.decisions.items():
brain_raw = self.brain_panel.last_results.get(agent_id, {})
decisions[agent_id] = {
"action": decision.action.value,
"urge": decision.urge,
"z_surprise": decision.z_surprise,
"change_score": decision.change_score,
"readiness": decision.readiness,
"p_end": decision.p_end,
"hidden_delta": decision.hidden_delta,
"map_run_length": decision.map_run_length,
"brain_latency_ms": brain_raw.get("latency_ms"),
"surprise": brain_raw.get("surprise"),
}
return {
"step": step_index,
"new_user_text": chunk.text,
"silence_flag": chunk.silence_flag,
"winner": tick.winner,
"floor_holder": tick.floor_holder,
"batch_latency_ms": raw.get("batch_latency_ms"),
"model_name": raw.get("model_name"),
"device_name": raw.get("device_name"),
"decisions": decisions,
}
def save_conversation_log(result: ConversationResult, path: str | Path = DEFAULT_LOG_PATH) -> Path:
output = Path(path)
output.parent.mkdir(parents=True, exist_ok=True)
data = {
"model_name": result.model_name,
"device_name": result.device_name,
"total_latency_ms": result.total_latency_ms,
"personas": [asdict(persona) for persona in result.personas],
"events": result.events,
"dialogue": result.dialogue,
"generated_examples": result.generated_examples,
}
output.write_text(json.dumps(data, indent=2), encoding="utf-8")
return output
def readable_log(result: ConversationResult) -> str:
persona_names = {persona.agent_id: persona.display_name for persona in result.personas}
lines = [
f"Model: {result.model_name or 'unknown'} on {result.device_name or 'unknown'}",
f"Total wall latency: {result.total_latency_ms:.1f} ms",
]
for event in result.events:
lines.append(f"[{event['step']}] USER + {event['new_user_text']!r} silence={event['silence_flag']}")
for agent_id, decision in event["decisions"].items():
action = decision["action"]
if action == Action.SILENT.value:
continue
lines.append(
" "
f"{persona_names.get(agent_id, agent_id)} -> {action} "
f"urge={decision['urge']:.2f} readiness={decision['readiness']:.2f} "
f"p_end={decision['p_end']:.2f}"
)
if "generated" in event:
generated = event["generated"]
lines.append(f" {persona_names.get(generated['agent_id'], generated['agent_id'])}: {generated['reply_text']}")
return "\n".join(lines)
def _dialogue_with_current_user(dialogue: Dialogue, current_user_text: str) -> Dialogue:
snapshot = [dict(turn) for turn in dialogue]
snapshot.append({"role": "user", "speaker": "founder", "text": current_user_text})
return snapshot
def _join_text(left: str, right: str) -> str:
left = left.strip()
right = right.strip()
if not left:
return right
if not right:
return left
return f"{left} {right}"
def run_demo(log_path: str | Path = DEFAULT_LOG_PATH, client: BrainClient | None = None) -> ConversationResult:
personas = default_personas()
panel = LiveBrainPanel(personas, client=client)
conversation = Conversation(personas, panel)
result = conversation.run(sample_pitch_stream())
save_conversation_log(result, log_path)
print(readable_log(result))
print(f"Wrote {log_path}")
return result
def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Run the text-streamed WhenToSpeak conversation demo.")
parser.add_argument("--log-path", default=str(DEFAULT_LOG_PATH))
args = parser.parse_args(argv)
run_demo(args.log_path)
if __name__ == "__main__":
main()