lct-20: АРМ диспетчера ДДС и цепочка 112 → ДДС
Возврат карточки — самая ценная механика цепочки: неполнота КИО перестаёт быть процентом в отчёте и становится сорванным выездом с конкретной причиной. В разборе это отметка E6: «не заполнено floor, victims_count — выезд сорван». Снимок карточки не меняется после передачи: оператор не дописывает задним числом поле, которое забыл. Тест правит карточку после передачи и проверяет, что в снимке этого нет. Диспетчер садится за АРМ, когда вызов уже идёт, — станция, подключённая после передачи, всё равно получает карточку. У станции нет аудио вообще: ни VAD, ни распознавания, ни синтеза.
This commit is contained in:
parent
38082e2af3
commit
ee2a526831
15 changed files with 439 additions and 13 deletions
|
|
@ -32,6 +32,7 @@ async def finish(session_id: UUID, state) -> None:
|
|||
timers=state.timers,
|
||||
revealed_facts=[fact.id for fact in state.slots.revealed_facts()] if state.slots else None,
|
||||
end_reason=state.end_reason,
|
||||
bounced_fields=state.bounced_fields,
|
||||
)
|
||||
# Сводка числами: по ней считается дельта между попытками в профиле.
|
||||
# Вытаскивать её разбором текста метрик («94 с») — путь к тихим ошибкам.
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class SessionHub:
|
|||
self._sessions: dict[UUID, SessionState] = {}
|
||||
self._observers: dict[UUID, set[asyncio.Queue]] = {}
|
||||
self._trainees: dict[UUID, set[asyncio.Queue]] = {}
|
||||
self._stations: dict[UUID, set[asyncio.Queue]] = {}
|
||||
self._tickers: dict[UUID, asyncio.Task] = {}
|
||||
|
||||
# ── реестр ──
|
||||
|
|
@ -77,6 +78,9 @@ class SessionHub:
|
|||
def trainee(self, session_id: UUID):
|
||||
return self._subscribe(self._trainees, session_id)
|
||||
|
||||
def station(self, session_id: UUID):
|
||||
return self._subscribe(self._stations, session_id)
|
||||
|
||||
# ── вещание ──
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -93,6 +97,9 @@ class SessionHub:
|
|||
def to_trainee(self, session_id: UUID, event: BaseModel | bytes) -> None:
|
||||
self._put(self._trainees.get(session_id, set()), event)
|
||||
|
||||
def to_station(self, session_id: UUID, event: BaseModel) -> None:
|
||||
self._put(self._stations.get(session_id, set()), event)
|
||||
|
||||
def broadcast(self, session_id: UUID, event: BaseModel) -> None:
|
||||
self.to_trainee(session_id, event)
|
||||
self.to_observers(session_id, event)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ class SessionState:
|
|||
ended_at: datetime | None = None
|
||||
end_reason: CallEndReason | None = None
|
||||
dispatched_card: KIO | None = None
|
||||
dispatched_at: datetime | None = None
|
||||
#: Поля, из-за которых диспетчер вернул карточку, — основание E6.
|
||||
bounced_fields: list[str] = field(default_factory=list)
|
||||
dds_log: list[tuple[str, datetime, str | None]] = field(default_factory=list)
|
||||
|
||||
def on_event(self, event_type: str) -> None:
|
||||
"""Единственная точка, где событие двигает таймеры."""
|
||||
|
|
@ -102,8 +106,19 @@ class SessionState:
|
|||
возможности дописать задним числом поле, которое забыл."""
|
||||
self.kio = apply_patch(self.kio, {"dds": service, "response_status": "transferred"})
|
||||
self.dispatched_card = self.kio.model_copy(deep=True)
|
||||
self.dispatched_at = now_utc()
|
||||
return self.dispatched_card
|
||||
|
||||
def card_received_event(self):
|
||||
"""Снимок карточки для станции ДДС."""
|
||||
from app.domain.events import CardReceived
|
||||
|
||||
return CardReceived(
|
||||
card=self.dispatched_card,
|
||||
from_operator=self.trainee_name or "оператор 112",
|
||||
at=self.dispatched_at or now_utc(),
|
||||
)
|
||||
|
||||
@property
|
||||
def ended(self) -> bool:
|
||||
return self.ended_at is not None
|
||||
|
|
|
|||
Loading…
Reference in a new issue