lct-hack/backend/app/session/checkpoint.py

70 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Переносимый JSON-снимок незавершённого занятия.
Снимок хранится в PostgreSQL после каждого подтверждённого действия. Это
`model_dump` сохраняемой части сессии (`PersistedSession`): сокеты, аудиобуферы
и объекты моделей в неё не входят и после перезапуска процесса собираются
заново (`rebuild_live`).
Снимки прежних версий не читаются: снимок живёт только у незавершённого
занятия, а плохой снимок реестр и восстановление узла логируют и пропускают.
"""
from datetime import datetime
from pydantic import BaseModel, Field
from app.dialog.factory import build_caller
from app.dialog.persona import PersonaProgress, PersonaState
from app.dialog.runtime import get_embedder
from app.dialog.slots import SlotMachine, SlotProgress
from app.domain.events import Exercise
from app.session.state import SessionState
from app.session.timers import downtime_ms
CHECKPOINT_VERSION = 2
class CallerProgress(BaseModel):
"""Прогресс голосового звонящего. В снимок пока не входит — см. `SessionState`."""
slots: SlotProgress = Field(default_factory=SlotProgress)
persona: PersonaProgress = Field(default_factory=PersonaProgress)
def dump_state(state: SessionState) -> dict:
"""Сериализовать сохраняемую часть. Чистое чтение."""
return {"version": CHECKPOINT_VERSION, **state.model_dump(mode="json")}
def rebuild_live(state: SessionState, caller: CallerProgress | None = None) -> None:
"""Собрать runtime-объекты звонящего; без прогресса — с чистого листа.
Карточка и оценка при этом остаются прежними.
"""
if state.exercise is not Exercise.CALL or state.scenario is None:
return
progress = caller or CallerProgress()
state.persona = PersonaState(state.scenario.persona, progress=progress.persona)
state.caller = build_caller(
state.scenario.id,
use_pregenerated=state.scenario.tree.pregenerated,
)
embedder = get_embedder()
if embedder is not None:
state.slots = SlotMachine(state.scenario, embedder, progress=progress.slots)
def load_state(payload: dict, saved_at: datetime) -> SessionState:
"""Восстановить состояние; неизвестная версия отклоняется явно."""
if payload.get("version") != CHECKPOINT_VERSION:
raise ValueError("неподдерживаемая версия снимка занятия")
state = SessionState.model_validate(
payload, context={"downtime_ms": downtime_ms(saved_at)}
)
if state.exercise is Exercise.CALL:
# Таймеры звонка и диспетчера — одна цепочка 112 → ДДС (см. dispatch):
# в снимке это копии, в живой сессии — один объект.
for card in state.desk.cards.values():
card.timers = state.timers
rebuild_live(state)
return state