lct-hack/backend/app/scoring/reference.py

69 lines
2.8 KiB
Python
Raw Normal View History

lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон Эталонный диалог собирается кодом из фактов и чек-листа: написанный руками, он разошёлся бы с фактами при первой же правке сценария, и курсанта оштрафовали бы за правильный ответ. Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый недобытый факт с эталонным вопросом, расхождение самооценки — что курсант заметил сам, чего не заметил, что отметил зря. Не заметил — самое ценное для разбора. Внешний монитор — не отдельное приложение, а другой режим отрисовки тех же событий: крупный таймер опроса, ход разговора, карточка, после оценки разбор на весь экран. Коррекция преподавателем сохраняет автооценку рядом: видно, что скорректировано и кем. Найдено: все метрики весили одинаково, и курсант, не задавший ни одного вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные веса (полнота опроса — 4) дают 74; окончательные утверждает методист, вопрос записан в DEBRIEF.md.
2026-09-17 21:32:12 +03:00
"""Эталонный диалог: как должен был пройти опрос.
Собирается **кодом** из чек-листа и фактов сценария. Писать его руками нельзя:
методист поправит факт и забудет эталон, и курсант получит штраф за правильный
ответ (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),
)
)
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
]