lct-15: самооценка курсанта до показа автооценки
Правило сервера, а не интерфейса: score.ready курсанту уходит только после self_assessment.submit, обойти его через DevTools нельзя. Преподаватель и монитор получают оценку сразу — им ждать нечего. Чек-лист для самооценки отдаётся отдельной точкой и только после конца звонка: во время разговора это содержимое подсказок, и открыть его значило бы выдать в контрольном режиме то, чего там быть не должно. Попутно подключён расчёт оценки при завершении звонка — детерминированный слой был написан в lct-12, но его никто не вызывал. Тест на отсутствие события смотрит очередь курсанта, а не ждёт из сокета: ожидание того, чего не будет, вешает прогон навсегда.
This commit is contained in:
parent
78f66cf3b5
commit
25143645d3
10 changed files with 260 additions and 2 deletions
53
backend/app/session/finish.py
Normal file
53
backend/app/session/finish.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Завершение занятия: посчитать оценку и положить её в журнал.
|
||||
|
||||
Детерминированный слой считается сразу по завершении звонка (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 = 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))
|
||||
|
|
@ -34,6 +34,7 @@ class Journal(Protocol):
|
|||
async def hint(self, session_id: UUID, checklist_id: str, question: str, at) -> None: ...
|
||||
async def note(self, session_id: UUID, ref: str, text: str, author: str) -> None: ...
|
||||
async def self_assessment(self, session_id: UUID, missed: list[str], comment: str, at) -> None: ...
|
||||
async def score(self, session_id: UUID, score_auto: float, report: dict) -> None: ...
|
||||
async def session_started(self, session_id: UUID, at) -> None: ...
|
||||
async def session_ended(self, session_id: UUID, at, reason: str) -> None: ...
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from sqlalchemy import update
|
|||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from app.db import repo
|
||||
from app.db.models import SelfAssessment, Session
|
||||
from app.db.models import Score, SelfAssessment, Session
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -84,6 +84,13 @@ class DbJournal:
|
|||
|
||||
await self._write(lambda db: action(db))
|
||||
|
||||
async def score(self, session_id: UUID, score_auto: float, report: dict) -> None:
|
||||
async def action(db):
|
||||
db.add(Score(session_id=session_id, score_auto=score_auto, score_final=score_auto, report=report))
|
||||
await db.commit()
|
||||
|
||||
await self._write(lambda db: action(db))
|
||||
|
||||
async def session_started(self, session_id: UUID, at: datetime) -> None:
|
||||
async def action(db):
|
||||
await db.execute(update(Session).where(Session.id == session_id).values(started_at=at))
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ class SessionState:
|
|||
audio_frames: int = 0
|
||||
bad_frames: int = 0
|
||||
|
||||
# Самооценка сдана — до неё курсант не видит автооценку.
|
||||
self_assessed: bool = False
|
||||
self_assessment: dict | None = None
|
||||
score: dict | None = None
|
||||
|
||||
started_at: datetime | None = None
|
||||
ended_at: datetime | None = None
|
||||
end_reason: CallEndReason | None = None
|
||||
|
|
|
|||
Loading…
Reference in a new issue