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