fbd6602814
---ci--- phase: 0 milestone: v0.1 status: complete ---/ci---
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""Praxis scenario schema — YAML DSL → Pydantic (D-018, D-009, D-010).
|
|
|
|
Defines the typed model for a branching role-play scenario. Loaded from YAML
|
|
by server/scenarios/loader.py. Drives Pipecat Flows (TASK-03-03).
|
|
|
|
Per RESEARCH.md example + PROJECT.md D-010: one branch point (escalate vs
|
|
accept), failure_mode field present (D-009 — not provoked in v0.1).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, ValidationError
|
|
|
|
|
|
class ScenarioPersona(BaseModel):
|
|
"""The AI character's voice + identity (D-006 — one voice for role-play + mentor)."""
|
|
|
|
voice_id: str = Field(..., description="TTS voice id (Cartesia/Piper) — same as mentor per D-006")
|
|
character: str = Field(..., description="Character name + role, e.g. 'Customer (Jordan)'")
|
|
|
|
|
|
class ScenarioSetup(BaseModel):
|
|
"""The system prompt + opening line that start the role-play."""
|
|
|
|
system_prompt: str = Field(..., description="LLM system prompt (stays in character)")
|
|
opening_line: str = Field(..., description="First TTS utterance the AI speaks")
|
|
|
|
|
|
class BranchTrigger(BaseModel):
|
|
"""Learner signals that trigger a branch transition (R7 classification)."""
|
|
|
|
learner_signals: list[str] = Field(
|
|
..., description="Signals the branch classifier looks for (e.g. 'empathy', 'defensive')"
|
|
)
|
|
|
|
|
|
class Branch(BaseModel):
|
|
"""One branch outcome (D-010 — v0.1 has two: accept_resolution + escalate)."""
|
|
|
|
id: str = Field(..., description="Branch id, e.g. 'accept_resolution' / 'escalate'")
|
|
trigger: BranchTrigger
|
|
outcome: Literal["success", "failure"] = Field(..., description="Branch outcome label")
|
|
failure_mode: str | None = Field(
|
|
None, description="D-009 failure_mode (present, not provoked in v0.1)"
|
|
)
|
|
debrief_focus: str = Field(..., description="What the debrief emphasizes for this branch")
|
|
|
|
|
|
class ScenarioDebrief(BaseModel):
|
|
"""Debrief generation config (D-020 — deepseek-v4-flash:cloud, no-think)."""
|
|
|
|
model: str = Field("deepseek-v4-flash:cloud", description="Ollama model for the debrief")
|
|
mode: Literal["no_think", "think", "max_think"] = Field(
|
|
"no_think", description="Reasoning mode (no_think for latency, D-020)"
|
|
)
|
|
prompt_template: str = Field(
|
|
"debrief/default", description="Prompt template id (resolved by server/debrief.py)"
|
|
)
|
|
|
|
|
|
class Scenario(BaseModel):
|
|
"""A Praxis role-play scenario (D-018 — YAML → Pydantic → Pipecat Flows)."""
|
|
|
|
id: str = Field(..., description="Scenario id, e.g. 'cs_refund_ca_v01'")
|
|
path: str = Field(..., description="Skill path, e.g. 'customer_service'")
|
|
market: str = Field(..., description="Market code, e.g. 'CA'")
|
|
language: str = Field("en-CA", description="Language code")
|
|
title: str = Field(..., description="Human-readable scenario title")
|
|
difficulty: int = Field(1, ge=1, le=5, description="Difficulty 1-5")
|
|
failure_mode: str = Field(
|
|
..., description="D-009 failure_mode — present (not provoked in v0.1)"
|
|
)
|
|
persona: ScenarioPersona
|
|
setup: ScenarioSetup
|
|
success_criteria: list[str] = Field(..., min_length=1)
|
|
common_mistakes: list[str] = Field(..., min_length=1)
|
|
branches: list[Branch] = Field(..., min_length=1, description="Branch points (v0.1: 2)")
|
|
debrief: ScenarioDebrief
|
|
|
|
def branch_ids(self) -> list[str]:
|
|
return [b.id for b in self.branches]
|
|
|
|
def branch_by_id(self, branch_id: str) -> Branch | None:
|
|
for b in self.branches:
|
|
if b.id == branch_id:
|
|
return b
|
|
return None
|
|
|
|
|
|
__all__ = [
|
|
"Scenario",
|
|
"ScenarioPersona",
|
|
"ScenarioSetup",
|
|
"Branch",
|
|
"BranchTrigger",
|
|
"ScenarioDebrief",
|
|
"ValidationError",
|
|
] |