"""Scenario runtime — maps a Scenario to a Pipecat Flows state machine (TASK-03-03). The v0.1 branch point is a post-hoc outcome classification (G-002): the conversation is linear, and at session end an LLM-as-judge (TASK-03-06) classifies the learner's signals into accept_resolution or escalate. Pipecat Flows is wired so the branch field is part of the data model; Phase 2+ can activate true in-flight branching without a schema change. This module: - builds the system prompt from scenario.setup.system_prompt - provides the opening line (scenario.setup.opening_line) as the first TTS utterance - exposes the branch transition logic (driven by the classifier in TASK-03-06) TASK-03-07: the pipeline uses scenario-driven prompts instead of the SLICE-02 hardcoded walking-skeleton prompt. """ from __future__ import annotations from dataclasses import dataclass from typing import Any from server.scenarios.schema import Branch, Scenario @dataclass class ScenarioRuntime: """Runtime state for one scenario session.""" scenario: Scenario branch: Branch | None = None turn_count: int = 0 @property def system_prompt(self) -> str: return self.scenario.setup.system_prompt @property def opening_line(self) -> str: return self.scenario.setup.opening_line @property def branch_id(self) -> str | None: return self.branch.id if self.branch else None @property def outcome(self) -> str | None: return self.branch.outcome if self.branch else None def set_branch(self, branch_id: str) -> Branch: """Set the session's branch outcome (from the classifier, TASK-03-06).""" b = self.scenario.branch_by_id(branch_id) if b is None: raise ValueError( f"Unknown branch id {branch_id!r} for scenario {self.scenario.id!r}; " f"known: {self.scenario.branch_ids()}" ) self.branch = b return b def debrief_focus(self) -> str: """The debrief focus for the resolved branch (or a default).""" if self.branch: return self.branch.debrief_focus return "General coaching feedback for this session." def as_flow_spec(self) -> dict[str, Any]: """Render the scenario as a Pipecat Flows state-machine spec. v0.1: a single 'conversation' state with the system prompt; branch transitions are post-hoc (G-002). The spec carries the branch metadata so Phase 2+ can fork in-flight. """ return { "initial_state": "conversation", "states": { "conversation": { "system_prompt": self.system_prompt, "opening_line": self.opening_line, "branches": [ {"id": b.id, "outcome": b.outcome, "learner_signals": b.trigger.learner_signals} for b in self.scenario.branches ], }, }, "transitions": [], # v0.1: no in-flight transitions (G-002) } def build_runtime(scenario: Scenario) -> ScenarioRuntime: """Construct a ScenarioRuntime for the given scenario.""" return ScenarioRuntime(scenario=scenario) def build_runtime_from_id(scenario_id: str) -> ScenarioRuntime: """Load + build a runtime by scenario id (convenience for the pipeline).""" from server.scenarios.loader import load return build_runtime(load(scenario_id)) __all__ = ["ScenarioRuntime", "build_runtime", "build_runtime_from_id"]