70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
|
|
"""Эталонный диалог: как должен был пройти опрос.
|
|||
|
|
|
|||
|
|
Собирается **кодом** из чек-листа и фактов сценария. Писать его руками нельзя:
|
|||
|
|
методист поправит факт и забудет эталон, и курсант получит штраф за правильный
|
|||
|
|
ответ (docs/spec/SCENARIO-FORMAT.md).
|
|||
|
|
|
|||
|
|
Работает в двух местах: в разборе — «вот какой вопрос стоял на этом шаге»
|
|||
|
|
(docs/product/DEBRIEF.md), и в эталон-плеере на внешнем мониторе, когда
|
|||
|
|
появится предгенерация озвучки (lct-19, требует ключа LLM).
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from pydantic import BaseModel
|
|||
|
|
|
|||
|
|
from app.dialog.caller import REVEAL
|
|||
|
|
from app.dialog.persona import BASE_MOOD
|
|||
|
|
from app.domain.events import Mood
|
|||
|
|
from app.scenarios.schema import Scenario
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ReferenceStep(BaseModel):
|
|||
|
|
"""Шаг эталонного опроса: вопрос оператора и ответ звонящего на него."""
|
|||
|
|
|
|||
|
|
checklist_id: str
|
|||
|
|
question: str
|
|||
|
|
fact_id: str | None = None
|
|||
|
|
answer: str | None = None
|
|||
|
|
required: bool = False
|
|||
|
|
hidden: bool = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ReferenceDialog(BaseModel):
|
|||
|
|
scenario_id: str
|
|||
|
|
first_line: str
|
|||
|
|
steps: list[ReferenceStep]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build(scenario: Scenario) -> ReferenceDialog:
|
|||
|
|
facts = {fact.id: fact for fact in scenario.facts}
|
|||
|
|
required = set(scenario.ground_truth.required_facts)
|
|||
|
|
mood = BASE_MOOD.get(scenario.persona.base, Mood.CALM)
|
|||
|
|
|
|||
|
|
steps: list[ReferenceStep] = []
|
|||
|
|
for item in scenario.checklist:
|
|||
|
|
if not item.question:
|
|||
|
|
continue
|
|||
|
|
fact = facts.get(item.fact) if item.fact else None
|
|||
|
|
steps.append(
|
|||
|
|
ReferenceStep(
|
|||
|
|
checklist_id=item.id,
|
|||
|
|
question=item.question,
|
|||
|
|
fact_id=fact.id if fact else None,
|
|||
|
|
# Ответ звонящего — тем же шаблоном, каким он отвечает в живом
|
|||
|
|
# звонке: эталон должен звучать так же, а не литературно.
|
|||
|
|
answer=REVEAL[mood].format(fact=fact.value) if fact else None,
|
|||
|
|
required=bool(fact and fact.id in required),
|
|||
|
|
hidden=bool(fact and fact.hidden),
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
return ReferenceDialog(scenario_id=scenario.id, first_line=scenario.first_line, steps=steps)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def missed_steps(scenario: Scenario, revealed: list[str] | None) -> list[ReferenceStep]:
|
|||
|
|
"""Шаги, которые курсант не отработал. `None` — слот-автомата не было."""
|
|||
|
|
if revealed is None:
|
|||
|
|
return []
|
|||
|
|
return [
|
|||
|
|
step
|
|||
|
|
for step in build(scenario).steps
|
|||
|
|
if step.fact_id and step.fact_id not in revealed and step.required
|
|||
|
|
]
|