Complete training workflow and acceptance hardening

This commit is contained in:
andreysk0304 2026-09-26 18:12:27 +03:00 • committed by gglamer
commit 7237265833
243 changed files with 17014 additions and 1500 deletions

View file

@ -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)