Мягкие директивы перекрывают дугу сценария и применяются со следующей реплики: разговор не дёргается от того, что преподаватель что-то нажал. Жёсткие правят ситуацию — обрыв связи рвёт звук тем же механизмом, что перебивание, и запускает норматив обратного дозвона; второй пострадавший правит эталон; неточный адрес снимает раскрытый факт, и оператор обязан переспросить. Своя копия сценария на занятие: директивы правят факты и эталон, а сценарий был общим на библиотеку — правка в одной группе протекла бы во все остальные. Тест проверяет, что библиотека не изменилась. Свободный текст честно отказывает: офлайн-дерево предгенерировано, произвольную фразу взять неоткуда, и преподаватель видит это на пульте. Ни одна директива не трогает карточку курсанта — он управляет ситуацией, а не работой обучаемого. Занятие теперь собирается целиком и только потом регистрируется: наблюдатель мог увидеть его без слот-автомата и звонящего.
54 lines
2.4 KiB
Python
54 lines
2.4 KiB
Python
"""Завершение занятия: посчитать оценку и положить её в журнал.
|
||
|
||
Детерминированный слой считается сразу по завершении звонка (lct-12).
|
||
Курсанту `score.ready` уходит **только после самооценки**: сначала он сверяет
|
||
своё ощущение с объективной картиной, и расхождение — отдельный материал
|
||
для преподавателя (docs/product/DEBRIEF.md). Преподаватель и монитор получают
|
||
событие сразу: им ждать нечего.
|
||
"""
|
||
|
||
import logging
|
||
from uuid import UUID
|
||
|
||
from app.domain.events import ScoreReady
|
||
from app.scenarios import store
|
||
from app.scoring.competency import radar
|
||
from app.scoring.gost import evaluate
|
||
from app.session.hub import hub
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
|
||
async def finish(session_id: UUID, state) -> None:
|
||
# Сценарий занятия, а не библиотечный: директивы могли поправить эталон.
|
||
scenario = state.scenario or store.get(state.scenario_id)
|
||
if scenario is None:
|
||
return
|
||
|
||
result = evaluate(
|
||
scenario=scenario,
|
||
kio=state.kio,
|
||
timers=state.timers,
|
||
revealed_facts=[fact.id for fact in state.slots.revealed_facts()] if state.slots else None,
|
||
end_reason=state.end_reason,
|
||
)
|
||
state.score = {
|
||
"score_auto": result.score,
|
||
"metrics": [metric.model_dump() for metric in result.metrics],
|
||
"findings": [finding.model_dump(mode="json") for finding in result.findings],
|
||
"competencies": [item.model_dump() for item in radar(result.metrics)],
|
||
"unavailable": result.unavailable,
|
||
}
|
||
log.info("сессия %s: оценка %.1f, отметок %d", session_id, result.score, len(result.findings))
|
||
|
||
if hub.journal:
|
||
await hub.journal.score(session_id, result.score, state.score)
|
||
|
||
hub.to_observers(session_id, ScoreReady(session_id=session_id))
|
||
await release_score(session_id, state)
|
||
|
||
|
||
async def release_score(session_id: UUID, state) -> None:
|
||
"""Отдать оценку курсанту, когда самооценка сдана."""
|
||
if state.score is not None and state.self_assessed:
|
||
hub.to_trainee(session_id, ScoreReady(session_id=session_id))
|