125 lines
4 KiB
Python
125 lines
4 KiB
Python
|
|
"""Схема сценария. Формат: docs/spec/SCENARIO-FORMAT.md
|
|||
|
|
|
|||
|
|
Сценарий — контент, а не код: его пишет методист, и читается он глазами.
|
|||
|
|
Поэтому схема строгая (`extra="forbid"`): опечатка в имени поля должна падать
|
|||
|
|
на старте приложения, а не тихо игнорироваться и всплывать посреди занятия.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|||
|
|
|
|||
|
|
from app.domain.classifiers import DDSCode, IncidentType, Level
|
|||
|
|
from app.domain.events import Mood
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Strict(BaseModel):
|
|||
|
|
model_config = ConfigDict(extra="forbid")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ArcStage(Strict):
|
|||
|
|
stage: str
|
|||
|
|
mood: Mood
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Persona(Strict):
|
|||
|
|
base: str
|
|||
|
|
arc: list[ArcStage] = []
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Background(Strict):
|
|||
|
|
loop: str
|
|||
|
|
gain_db: float = -18
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RevealOn(Strict):
|
|||
|
|
"""Два вида условий: вопрос из чек-листа либо подход оператора."""
|
|||
|
|
|
|||
|
|
question: str | None = None
|
|||
|
|
approach: str | None = None
|
|||
|
|
|
|||
|
|
@model_validator(mode="after")
|
|||
|
|
def exactly_one(self):
|
|||
|
|
if bool(self.question) == bool(self.approach):
|
|||
|
|
raise ValueError("reveal_on: ровно одно из `question` или `approach`")
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Fact(Strict):
|
|||
|
|
id: str
|
|||
|
|
value: str
|
|||
|
|
hidden: bool = False
|
|||
|
|
reveal_on: RevealOn | None = None
|
|||
|
|
|
|||
|
|
@model_validator(mode="after")
|
|||
|
|
def hidden_needs_condition(self):
|
|||
|
|
if self.hidden and (self.reveal_on is None or not self.reveal_on.approach):
|
|||
|
|
raise ValueError(
|
|||
|
|
f"факт {self.id}: hidden требует reveal_on.approach — "
|
|||
|
|
"скрытый факт не раскрывается прямым вопросом"
|
|||
|
|
)
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ChecklistItem(Strict):
|
|||
|
|
id: str
|
|||
|
|
question: str | None = None
|
|||
|
|
fact: str | None = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class EraGlonass(Strict):
|
|||
|
|
vin: str
|
|||
|
|
coords: dict
|
|||
|
|
passengers: int
|
|||
|
|
impact_force: str
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Tree(Strict):
|
|||
|
|
pregenerated: bool = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class GroundTruth(Strict):
|
|||
|
|
"""Выводится кодом. В YAML допускаются только нормализованные ожидания
|
|||
|
|
(адрес и число пострадавших): вывести «улица Ленина, 14» из фразы
|
|||
|
|
«улица Ленина, 14, квартира 47, 5-й этаж» кодом нельзя, а сверять оценку
|
|||
|
|
с сырым текстом факта — значит штрафовать курсанта за правильный ответ.
|
|||
|
|
|
|||
|
|
Всё остальное загрузчик проставляет сам и запрещает писать руками —
|
|||
|
|
иначе генератор сценариев рассинхронизирует факты и эталон.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
incident_type: IncidentType | None = None
|
|||
|
|
dds: DDSCode | None = None
|
|||
|
|
required_facts: list[str] = []
|
|||
|
|
address: str | None = None
|
|||
|
|
victims: int | None = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class Scenario(Strict):
|
|||
|
|
id: str
|
|||
|
|
title: str
|
|||
|
|
type: IncidentType
|
|||
|
|
level: Level
|
|||
|
|
topics: list[str] = []
|
|||
|
|
modes: list[str] = ["training"]
|
|||
|
|
extends: str | None = None
|
|||
|
|
|
|||
|
|
persona: Persona
|
|||
|
|
background: Background | None = None
|
|||
|
|
first_line: str
|
|||
|
|
|
|||
|
|
facts: list[Fact] = []
|
|||
|
|
checklist: list[ChecklistItem] = []
|
|||
|
|
required_fields: list[str] = Field(default_factory=list)
|
|||
|
|
ground_truth: GroundTruth = GroundTruth()
|
|||
|
|
era_glonass: EraGlonass | None = None
|
|||
|
|
tree: Tree = Tree()
|
|||
|
|
|
|||
|
|
@model_validator(mode="after")
|
|||
|
|
def era_only_for_era_type(self):
|
|||
|
|
if self.era_glonass is not None and self.type is not IncidentType.ERA_GLONASS:
|
|||
|
|
raise ValueError("era_glonass задан, но type не era_glonass")
|
|||
|
|
if self.type is IncidentType.ERA_GLONASS and self.era_glonass is None:
|
|||
|
|
raise ValueError("type era_glonass требует блок era_glonass")
|
|||
|
|
return self
|
|||
|
|
|
|||
|
|
def fact_ids(self) -> set[str]:
|
|||
|
|
return {fact.id for fact in self.facts}
|