Complete training workflow and acceptance hardening
This commit is contained in:
parent
4c4b91064f
commit
7237265833
243 changed files with 17014 additions and 1500 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from app.domain import ekp
|
||||
from app.domain.kio import KIO, ResponseStatus
|
||||
|
|
@ -18,13 +19,27 @@ def prepare_card(state: SessionState, scenario: Scenario) -> None:
|
|||
state.level = scenario.level.value
|
||||
state.required_fields = list(scenario.required_fields)
|
||||
truth = scenario.ground_truth
|
||||
address_fact = next((fact.value for fact in scenario.facts if "address" in fact.id), "")
|
||||
caller_fact = next((fact.value for fact in scenario.facts if fact.id in {"f_caller", "f_applicant"}), "")
|
||||
caller_phone = next((
|
||||
match.group(0).strip()
|
||||
for match in re.finditer(r"(?<!\d)(?:\+?7|8)?(?:[\s().-]*\d){10,11}(?!\d)", caller_fact)
|
||||
if len(re.sub(r"\D", "", match.group(0))) in {10, 11}
|
||||
), None)
|
||||
address_fact = next(
|
||||
(fact.value for fact in scenario.facts if "address" in fact.id), ""
|
||||
)
|
||||
caller_fact = next(
|
||||
(
|
||||
fact.value
|
||||
for fact in scenario.facts
|
||||
if fact.id in {"f_caller", "f_applicant"}
|
||||
),
|
||||
"",
|
||||
)
|
||||
caller_phone = next(
|
||||
(
|
||||
match.group(0).strip()
|
||||
for match in re.finditer(
|
||||
r"(?<!\d)(?:\+?7|8)?(?:[\s().-]*\d){10,11}(?!\d)", caller_fact
|
||||
)
|
||||
if len(re.sub(r"\D", "", match.group(0))) in {10, 11}
|
||||
),
|
||||
None,
|
||||
)
|
||||
caller_names = re.findall(
|
||||
r"(?<![А-ЯЁа-яё])([А-ЯЁ][а-яё-]+(?:\s+[А-ЯЁ][а-яё-]+){1,2})(?![А-ЯЁа-яё])",
|
||||
caller_fact,
|
||||
|
|
@ -34,20 +49,48 @@ def prepare_card(state: SessionState, scenario: Scenario) -> None:
|
|||
caller_name = caller_names[0] if len(caller_names) == 1 else None
|
||||
floor = re.search(r"(\d+)[-‑–]?й?\s*этаж", address_fact, re.IGNORECASE)
|
||||
service = truth.dds.value if truth.dds else None
|
||||
fallback = {"01": "Служба 101", "02": "МВД", "03": "Скорая помощь", "04": "Аварийная служба"}
|
||||
state.kio = KIO(
|
||||
registered_at=now_utc(), caller_number=caller_phone, caller_name=caller_name,
|
||||
caller_contact=caller_phone,
|
||||
address=truth.address or address_fact or None,
|
||||
floor=floor.group(1) if floor else None,
|
||||
incident_type=truth.incident_type, incident_code=truth.incident_code,
|
||||
incident_group=(ekp.incident(truth.incident_code).group
|
||||
if truth.incident_code and ekp.incident(truth.incident_code) else None),
|
||||
dds=truth.dds, signs=list(scenario.signs),
|
||||
notify=list(truth.notify) or ([fallback[service]] if service in fallback else []),
|
||||
victims_count=truth.victims,
|
||||
description="; ".join(fact.value for fact in scenario.facts[:3]) or scenario.first_line,
|
||||
)
|
||||
fallback = {
|
||||
"01": "Служба 101",
|
||||
"02": "МВД",
|
||||
"03": "Скорая помощь",
|
||||
"04": "Аварийная служба",
|
||||
}
|
||||
if scenario.student_card is not None:
|
||||
# A moderated learner-authored KIO is the card itself; do not flatten
|
||||
# it to title/description and silently discard its structured fields.
|
||||
state.kio = scenario.student_card.model_copy(
|
||||
deep=True,
|
||||
update={
|
||||
"card_id": uuid4(),
|
||||
"registered_at": now_utc(),
|
||||
"response_status": ResponseStatus.REGISTERED,
|
||||
"dispatch_order_at": None,
|
||||
"arrival_at": None,
|
||||
},
|
||||
)
|
||||
else:
|
||||
state.kio = KIO(
|
||||
registered_at=now_utc(),
|
||||
caller_number=caller_phone,
|
||||
caller_name=caller_name,
|
||||
caller_contact=caller_phone,
|
||||
address=truth.address or address_fact or None,
|
||||
floor=floor.group(1) if floor else None,
|
||||
incident_type=truth.incident_type,
|
||||
incident_code=truth.incident_code,
|
||||
incident_group=(
|
||||
ekp.incident(truth.incident_code).group
|
||||
if truth.incident_code and ekp.incident(truth.incident_code)
|
||||
else None
|
||||
),
|
||||
dds=truth.dds,
|
||||
signs=list(scenario.signs),
|
||||
notify=list(truth.notify)
|
||||
or ([fallback[service]] if service in fallback else []),
|
||||
victims_count=truth.victims,
|
||||
description="; ".join(fact.value for fact in scenario.facts[:3])
|
||||
or scenario.first_line,
|
||||
)
|
||||
state.dispatched_card = None
|
||||
if service:
|
||||
state.dispatch(service)
|
||||
|
|
@ -76,7 +119,9 @@ def prepare_card(state: SessionState, scenario: Scenario) -> None:
|
|||
state.on_event("dds.dispatch")
|
||||
|
||||
|
||||
def _append_live_card(state: SessionState, scenario: Scenario, index: int) -> DdsLiveCard:
|
||||
def _append_live_card(
|
||||
state: SessionState, scenario: Scenario, index: int
|
||||
) -> DdsLiveCard:
|
||||
state.dds_card_index = index
|
||||
prepare_card(state, scenario)
|
||||
card = DdsLiveCard(
|
||||
|
|
@ -114,9 +159,14 @@ def deliver_due_cards(state: SessionState, now: datetime | None = None) -> int:
|
|||
|
||||
delivered = 0
|
||||
while state.dds_next_scenario_index < len(state.dds_scenarios):
|
||||
active_id = state.dds_active_card_id if any(
|
||||
item.card_id == state.dds_active_card_id for item in state.dds_live_cards
|
||||
) else None
|
||||
active_id = (
|
||||
state.dds_active_card_id
|
||||
if any(
|
||||
item.card_id == state.dds_active_card_id
|
||||
for item in state.dds_live_cards
|
||||
)
|
||||
else None
|
||||
)
|
||||
active_exists = active_id is not None
|
||||
waiting_count = len(state.dds_live_cards) - int(active_exists)
|
||||
if waiting_count >= state.dds_max_waiting:
|
||||
|
|
@ -163,3 +213,45 @@ def prepare_queue(
|
|||
state.dds_next_scenario_index = 0
|
||||
state.dds_next_arrival_at = now_utc()
|
||||
deliver_due_cards(state)
|
||||
|
||||
|
||||
def prepare_handoff_queue(
|
||||
state: SessionState,
|
||||
additional_scenarios: list[Scenario],
|
||||
arrival_interval_seconds: int = 0,
|
||||
max_waiting: int = 3,
|
||||
) -> None:
|
||||
"""Передать созданную курсантом карточку в ДДС перед готовыми карточками."""
|
||||
if state.dispatched_card is None or state.scenario is None:
|
||||
return
|
||||
now = now_utc()
|
||||
state.operator_kio = state.dispatched_card.model_copy(deep=True)
|
||||
state.operator_scenario = state.scenario.model_copy(deep=True)
|
||||
# Карточка курсанта становится первым живым объектом очереди. Её данные
|
||||
# не переписываются из эталона: именно их будет обрабатывать ДДС.
|
||||
dds_timers = SessionTimers(limits=dict(state.timers.limits))
|
||||
dds_timers.on_event("dds.dispatch")
|
||||
first = DdsLiveCard(
|
||||
original_index=0,
|
||||
scenario=state.operator_scenario,
|
||||
kio=state.operator_kio.model_copy(deep=True),
|
||||
dispatched_card=state.operator_kio.model_copy(deep=True),
|
||||
dispatched_at=state.dispatched_at or now,
|
||||
timers=dds_timers,
|
||||
)
|
||||
state.dds_live_cards = [first]
|
||||
state.dds_scenarios = [state.operator_scenario, *additional_scenarios]
|
||||
state.dds_arrival_interval_seconds = arrival_interval_seconds
|
||||
state.dds_max_waiting = max_waiting
|
||||
state.dds_next_scenario_index = 1
|
||||
state.dds_next_arrival_at = (
|
||||
(
|
||||
now + timedelta(seconds=arrival_interval_seconds)
|
||||
if additional_scenarios and arrival_interval_seconds
|
||||
else now
|
||||
)
|
||||
if additional_scenarios
|
||||
else None
|
||||
)
|
||||
state.activate_dds_card(first.card_id, capture=False)
|
||||
deliver_due_cards(state)
|
||||
|
|
|
|||
Loading…
Reference in a new issue