From 64313a62fb8b02d80ba197d8ec9ed35ad8f5b114 Mon Sep 17 00:00:00 2001 From: gglamer Date: Sat, 26 Sep 2026 22:13:35 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D1=81=20=D0=B7=D0=B2=D0=BE=D0=BD=D1=8F=D1=89=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=B2=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20SlotProgress=20?= =?UTF-8?q?=D0=B8=20PersonaProgress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/dialog/persona.py | 44 ++++++++- backend/app/dialog/slots.py | 29 +++++- .../tests/golden/checkpoint_before_desk.json | 1 - backend/tests/test_caller_progress.py | 97 +++++++++++++++++++ 4 files changed, 163 insertions(+), 8 deletions(-) delete mode 100644 backend/tests/golden/checkpoint_before_desk.json create mode 100644 backend/tests/test_caller_progress.py diff --git a/backend/app/dialog/persona.py b/backend/app/dialog/persona.py index 4a63b30..68e307c 100644 --- a/backend/app/dialog/persona.py +++ b/backend/app/dialog/persona.py @@ -7,6 +7,8 @@ from dataclasses import dataclass, field +from pydantic import BaseModel, Field + from app.domain.events import Mood from app.scenarios.schema import Persona @@ -37,13 +39,47 @@ DIRECTIVE_MOOD: dict[str, Mood] = { } -@dataclass -class PersonaState: - persona: Persona +class PersonaProgress(BaseModel): + """Где звонящий по дуге и что накопил; профиль берётся из сценария.""" + stage: str = "registration" repeats: int = 0 directive: str | None = None - history: list[Mood] = field(default_factory=list) + history: list[Mood] = Field(default_factory=list) + + +@dataclass +class PersonaState: + persona: Persona + progress: PersonaProgress = field(default_factory=PersonaProgress) + + @property + def stage(self) -> str: + return self.progress.stage + + @stage.setter + def stage(self, value: str) -> None: + self.progress.stage = value + + @property + def repeats(self) -> int: + return self.progress.repeats + + @repeats.setter + def repeats(self, value: int) -> None: + self.progress.repeats = value + + @property + def directive(self) -> str | None: + return self.progress.directive + + @directive.setter + def directive(self, value: str | None) -> None: + self.progress.directive = value + + @property + def history(self) -> list[Mood]: + return self.progress.history @property def base(self) -> str: diff --git a/backend/app/dialog/slots.py b/backend/app/dialog/slots.py index dc127b0..8668ff0 100644 --- a/backend/app/dialog/slots.py +++ b/backend/app/dialog/slots.py @@ -12,6 +12,7 @@ import re from dataclasses import dataclass, field import numpy as np +from pydantic import BaseModel, Field from app.dialog.embeddings import Embedder from app.scenarios.schema import ChecklistItem, Fact, Scenario @@ -57,6 +58,17 @@ class TurnResult: return bool(self.matched) +class SlotProgress(BaseModel): + """Что уже произошло в разговоре — единственное, что стоит сохранять. + + Якоря, словари раскрытия и уточнения собираются из сценария заново. + """ + + asked: list[str] = Field(default_factory=list) + revealed: list[str] = Field(default_factory=list) + refined: list[str] = Field(default_factory=list) + + class SlotMachine: def __init__( self, @@ -64,6 +76,7 @@ class SlotMachine: embedder: Embedder, floor: float = MATCH_FLOOR, margin: float = MATCH_MARGIN, + progress: SlotProgress | None = None, ) -> None: self.scenario = scenario self.floor = floor @@ -103,9 +116,19 @@ class SlotMachine: if fact.refine_on: self._refines.setdefault(fact.refine_on, []).append(fact.id) - self.asked: list[str] = [] - self.revealed: list[str] = [] - self.refined: list[str] = [] + self.progress = progress if progress is not None else SlotProgress() + + @property + def asked(self) -> list[str]: + return self.progress.asked + + @property + def revealed(self) -> list[str]: + return self.progress.revealed + + @property + def refined(self) -> list[str]: + return self.progress.refined # ── реплика оператора ── diff --git a/backend/tests/golden/checkpoint_before_desk.json b/backend/tests/golden/checkpoint_before_desk.json deleted file mode 100644 index b1b0c73..0000000 --- a/backend/tests/golden/checkpoint_before_desk.json +++ /dev/null @@ -1 +0,0 @@ -{"dds": {"version": 1, "session_id": "35003185-d27a-4552-9c1a-b819456b06df", "scenario_id": "t20-2-stroke", "scenario_title": "Речь невнятная, лицо перекошено", "level": "L2", "mode": "training", "owner_login": null, "backend_fencing_epoch": 0, "exercise": "dds", "handoff_to_dds": false, "required_fields": ["address", "incident_type", "description", "victims_count"], "trainee_name": null, "trainee_id": null, "dds_service": null, "attempt": 1, "criteria": {"decision_time_limit_seconds": 30, "card_fill_time_limit_seconds": 180, "dds_card_work_time_limit_seconds": 180, "allowed_errors": 0, "require_correct_grammar": true, "score_weights": {}}, "kio": {"card_id": "91966e62-a166-490c-81da-a590b06cbe51", "registered_at": "2026-09-26T21:28:46.374117Z", "response_status": "transferred", "caller_number": "916 896 3254", "caller_name": "Титов Олег Иванович", "caller_contact": "916 896 3254", "phone_on_scene": null, "language": "ru", "okato": null, "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 22, "signs": ["Парализовало"], "incident_code": "22350000", "incident_type": "medical", "description": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП; речь невнятная, лицо перекошено; прохожий просит вызвать 03; женщина на вид семидесяти лет, неизвестная", "victims_count": 1, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["СМП", "ГКУ НТУ"], "dds": "03", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "transcript": [], "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "hints_shown": [], "hints_log": [], "notes": [], "directives": [], "scenario": {"id": "t20-2-stroke", "title": "Речь невнятная, лицо перекошено", "type": "medical", "level": "L2", "topics": ["medical"], "modes": ["training", "exam", "self"], "extends": "checklists/medical.yaml", "persona": {"base": "worried", "arc": [{"stage": "registration", "mood": "worried"}, {"stage": "interview", "mood": "worried"}]}, "background": null, "first_line": "Женщине плохо, говорит невнятно, лицо перекосило.", "signs": ["Парализовало"], "ticket": 20, "position": 2, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_happened", "value": "речь невнятная, лицо перекошено; прохожий просит вызвать 03", "reveal_on": {"question": "q_what_happened"}, "refined": null, "refine_on": null}, {"id": "f_who", "value": "женщина на вид семидесяти лет, неизвестная", "reveal_on": {"question": "q_who"}, "refined": null, "refine_on": null}, {"id": "f_conscious", "value": "сведения о сознании женщины не указаны", "reveal_on": {"question": "q_conscious"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "вызывает прохожий Титов Олег Иванович, телефон 916 896 3254", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж, код домофона?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "Куда ехать скорой?", "Код домофона какой?", "Номер квартиры?"]}, {"id": "q_what_happened", "question": "Что случилось с пострадавшим?", "fact": "f_what_happened", "examples": ["Что с человеком?", "Что произошло?", "На что жалуется?", "Как это случилось?"]}, {"id": "q_who", "question": "Кто пострадавший: имя, возраст, пол?", "fact": "f_who", "examples": ["Сколько лет?", "Как зовут больного?", "Это мужчина или женщина?", "Возраст какой?"]}, {"id": "q_conscious", "question": "Человек в сознании, дышит?", "fact": "f_conscious", "examples": ["В сознании?", "Дышит?", "Он отвечает вам?", "Реагирует на голос?"]}, {"id": "q_can_move", "question": "Может ли пострадавший передвигаться самостоятельно?", "fact": null, "examples": ["Он может идти?", "Ходит сам?", "Встать может?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?", "Кем вы приходитесь пострадавшему?"]}], "required_fields": ["address", "incident_type", "description", "victims_count"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "medical", "dds": "03", "incident_code": "22350000", "notify": ["СМП", "ГКУ НТУ"], "required_facts": ["f_address", "f_what_happened", "f_who", "f_conscious", "f_caller"], "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "victims": 1}, "era_glonass": null, "tree": {"pregenerated": false}}, "audio_frames": 0, "bad_frames": 0, "self_assessed": false, "self_assessment": null, "score": null, "started_at": null, "ended_at": null, "end_reason": null, "dispatched_card": {"card_id": "91966e62-a166-490c-81da-a590b06cbe51", "registered_at": "2026-09-26T21:28:46.374117Z", "response_status": "transferred", "caller_number": "916 896 3254", "caller_name": "Титов Олег Иванович", "caller_contact": "916 896 3254", "phone_on_scene": null, "language": "ru", "okato": null, "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 22, "signs": ["Парализовало"], "incident_code": "22350000", "incident_type": "medical", "description": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП; речь невнятная, лицо перекошено; прохожий просит вызвать 03; женщина на вид семидесяти лет, неизвестная", "victims_count": 1, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["СМП", "ГКУ НТУ"], "dds": "03", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.374340+00:00", "bounced_fields": [], "dds_log": [], "status_log": [], "crew_selected": null, "crew_assignments": {}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "dds_scenarios": [{"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, {"id": "t20-2-stroke", "title": "Речь невнятная, лицо перекошено", "type": "medical", "level": "L2", "topics": ["medical"], "modes": ["training", "exam", "self"], "extends": "checklists/medical.yaml", "persona": {"base": "worried", "arc": [{"stage": "registration", "mood": "worried"}, {"stage": "interview", "mood": "worried"}]}, "background": null, "first_line": "Женщине плохо, говорит невнятно, лицо перекосило.", "signs": ["Парализовало"], "ticket": 20, "position": 2, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_happened", "value": "речь невнятная, лицо перекошено; прохожий просит вызвать 03", "reveal_on": {"question": "q_what_happened"}, "refined": null, "refine_on": null}, {"id": "f_who", "value": "женщина на вид семидесяти лет, неизвестная", "reveal_on": {"question": "q_who"}, "refined": null, "refine_on": null}, {"id": "f_conscious", "value": "сведения о сознании женщины не указаны", "reveal_on": {"question": "q_conscious"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "вызывает прохожий Титов Олег Иванович, телефон 916 896 3254", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж, код домофона?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "Куда ехать скорой?", "Код домофона какой?", "Номер квартиры?"]}, {"id": "q_what_happened", "question": "Что случилось с пострадавшим?", "fact": "f_what_happened", "examples": ["Что с человеком?", "Что произошло?", "На что жалуется?", "Как это случилось?"]}, {"id": "q_who", "question": "Кто пострадавший: имя, возраст, пол?", "fact": "f_who", "examples": ["Сколько лет?", "Как зовут больного?", "Это мужчина или женщина?", "Возраст какой?"]}, {"id": "q_conscious", "question": "Человек в сознании, дышит?", "fact": "f_conscious", "examples": ["В сознании?", "Дышит?", "Он отвечает вам?", "Реагирует на голос?"]}, {"id": "q_can_move", "question": "Может ли пострадавший передвигаться самостоятельно?", "fact": null, "examples": ["Он может идти?", "Ходит сам?", "Встать может?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?", "Кем вы приходитесь пострадавшему?"]}], "required_fields": ["address", "incident_type", "description", "victims_count"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "medical", "dds": "03", "incident_code": "22350000", "notify": ["СМП", "ГКУ НТУ"], "required_facts": ["f_address", "f_what_happened", "f_who", "f_conscious", "f_caller"], "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "victims": 1}, "era_glonass": null, "tree": {"pregenerated": false}}, {"id": "t01-1-fire-container", "title": "Возгорание мусорного контейнера", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "calm", "arc": [{"stage": "registration", "mood": "calm"}, {"stage": "interview", "mood": "calm"}]}, "background": null, "first_line": "Здравствуйте. Тут мусорный контейнер горит, у депо.", "signs": ["на улице", "мусор", "открытое пламя"], "ticket": 1, "position": 1, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции", "reveal_on": {"question": "q_address"}, "refined": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "refine_on": "q_address_check"}, {"id": "f_what_burns", "value": "горит мусорный контейнер, пламя открытое", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "пострадавших нет", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "Сидоров Иван Сергеевич, телефон 916-126-34-71", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address_check", "question": "Уточните: происшествие по этому адресу, вы сейчас там?", "fact": null, "examples": ["Это точно Москва?", "Вы сами находитесь по этому адресу?", "Происшествие там же, где вы?", "Какой город?", "Это в черте города?", "Уточните адрес происшествия", "Вы смотрите на это или вам рассказали?", "Адрес происшествия и ваш адрес совпадают?"]}, {"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": null, "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": null, "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "incident_type", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1010101", "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_caller"], "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "victims": 0}, "era_glonass": null, "tree": {"pregenerated": false}}], "pending_dds_scenarios": [], "operator_kio": null, "operator_scenario": null, "dds_live_cards": [{"original_index": 0, "scenario": {"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, "kio": {"card_id": "85c1845e-c97e-4573-a376-113bf53bb95e", "registered_at": "2026-09-26T21:28:46.373191Z", "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": "5", "intercom_code": null, "coords": null, "incident_group": 1, "signs": ["жилой дом", "балкон", "открытое пламя"], "incident_code": "1050201", "incident_type": "fire", "description": "улица Ленина, 14, квартира 47, 5-й этаж; горит балкон, дым пошёл в квартиру; в квартире жена и ребёнок, не выходят", "victims_count": 2, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "dds": "01", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_card": {"card_id": "85c1845e-c97e-4573-a376-113bf53bb95e", "registered_at": "2026-09-26T21:28:46.373191Z", "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": "5", "intercom_code": null, "coords": null, "incident_group": 1, "signs": ["жилой дом", "балкон", "открытое пламя"], "incident_code": "1050201", "incident_type": "fire", "description": "улица Ленина, 14, квартира 47, 5-й этаж; горит балкон, дым пошёл в квартиру; в квартире жена и ребёнок, не выходят", "victims_count": 2, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "dds": "01", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.373637+00:00", "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 1, "attempt": 1, "stopped": true}, "close": {"elapsed_ms": 1, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "bounced_fields": [], "dds_log": [["crew.select", "2026-09-26T21:28:46.375082+00:00", "Служба 101 — бригада №1"]], "status_log": [{"service": "Служба 101", "status": "accepted", "at": "2026-09-26T21:28:46.375034Z", "comment": "Основание: доклад старшего.\nСведения: карточка принята.", "author": "диспетчер"}], "crew_selected": "Служба 101 — бригада №1", "crew_assignments": {"Служба 101": "Служба 101 — бригада №1"}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "reply_text": "", "reply_log": []}, {"original_index": 1, "scenario": {"id": "t20-2-stroke", "title": "Речь невнятная, лицо перекошено", "type": "medical", "level": "L2", "topics": ["medical"], "modes": ["training", "exam", "self"], "extends": "checklists/medical.yaml", "persona": {"base": "worried", "arc": [{"stage": "registration", "mood": "worried"}, {"stage": "interview", "mood": "worried"}]}, "background": null, "first_line": "Женщине плохо, говорит невнятно, лицо перекосило.", "signs": ["Парализовало"], "ticket": 20, "position": 2, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_happened", "value": "речь невнятная, лицо перекошено; прохожий просит вызвать 03", "reveal_on": {"question": "q_what_happened"}, "refined": null, "refine_on": null}, {"id": "f_who", "value": "женщина на вид семидесяти лет, неизвестная", "reveal_on": {"question": "q_who"}, "refined": null, "refine_on": null}, {"id": "f_conscious", "value": "сведения о сознании женщины не указаны", "reveal_on": {"question": "q_conscious"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "вызывает прохожий Титов Олег Иванович, телефон 916 896 3254", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж, код домофона?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "Куда ехать скорой?", "Код домофона какой?", "Номер квартиры?"]}, {"id": "q_what_happened", "question": "Что случилось с пострадавшим?", "fact": "f_what_happened", "examples": ["Что с человеком?", "Что произошло?", "На что жалуется?", "Как это случилось?"]}, {"id": "q_who", "question": "Кто пострадавший: имя, возраст, пол?", "fact": "f_who", "examples": ["Сколько лет?", "Как зовут больного?", "Это мужчина или женщина?", "Возраст какой?"]}, {"id": "q_conscious", "question": "Человек в сознании, дышит?", "fact": "f_conscious", "examples": ["В сознании?", "Дышит?", "Он отвечает вам?", "Реагирует на голос?"]}, {"id": "q_can_move", "question": "Может ли пострадавший передвигаться самостоятельно?", "fact": null, "examples": ["Он может идти?", "Ходит сам?", "Встать может?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?", "Кем вы приходитесь пострадавшему?"]}], "required_fields": ["address", "incident_type", "description", "victims_count"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "medical", "dds": "03", "incident_code": "22350000", "notify": ["СМП", "ГКУ НТУ"], "required_facts": ["f_address", "f_what_happened", "f_who", "f_conscious", "f_caller"], "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "victims": 1}, "era_glonass": null, "tree": {"pregenerated": false}}, "kio": {"card_id": "91966e62-a166-490c-81da-a590b06cbe51", "registered_at": "2026-09-26T21:28:46.374117Z", "response_status": "transferred", "caller_number": "916 896 3254", "caller_name": "Титов Олег Иванович", "caller_contact": "916 896 3254", "phone_on_scene": null, "language": "ru", "okato": null, "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 22, "signs": ["Парализовало"], "incident_code": "22350000", "incident_type": "medical", "description": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП; речь невнятная, лицо перекошено; прохожий просит вызвать 03; женщина на вид семидесяти лет, неизвестная", "victims_count": 1, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["СМП", "ГКУ НТУ"], "dds": "03", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_card": {"card_id": "91966e62-a166-490c-81da-a590b06cbe51", "registered_at": "2026-09-26T21:28:46.374117Z", "response_status": "transferred", "caller_number": "916 896 3254", "caller_name": "Титов Олег Иванович", "caller_contact": "916 896 3254", "phone_on_scene": null, "language": "ru", "okato": null, "address": "дорога от Каширского шоссе в сторону деревни Истомиха, у остановки Домодедовское кладбище, рядом с КПП", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 22, "signs": ["Парализовало"], "incident_code": "22350000", "incident_type": "medical", "description": "дорога от Каширского шоссе в сторону деревни Истомиха, около автобусной остановки Домодедовское кладбище, рядом с КПП; речь невнятная, лицо перекошено; прохожий просит вызвать 03; женщина на вид семидесяти лет, неизвестная", "victims_count": 1, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["СМП", "ГКУ НТУ"], "dds": "03", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.374340+00:00", "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "bounced_fields": [], "dds_log": [], "status_log": [], "crew_selected": null, "crew_assignments": {}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "reply_text": "Ответ по второй карточке", "reply_log": [["2026-09-26T21:28:46.375115+00:00", "Ответ по второй карточке"]]}, {"original_index": 2, "scenario": {"id": "t01-1-fire-container", "title": "Возгорание мусорного контейнера", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "calm", "arc": [{"stage": "registration", "mood": "calm"}, {"stage": "interview", "mood": "calm"}]}, "background": null, "first_line": "Здравствуйте. Тут мусорный контейнер горит, у депо.", "signs": ["на улице", "мусор", "открытое пламя"], "ticket": 1, "position": 1, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции", "reveal_on": {"question": "q_address"}, "refined": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "refine_on": "q_address_check"}, {"id": "f_what_burns", "value": "горит мусорный контейнер, пламя открытое", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "пострадавших нет", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "Сидоров Иван Сергеевич, телефон 916-126-34-71", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address_check", "question": "Уточните: происшествие по этому адресу, вы сейчас там?", "fact": null, "examples": ["Это точно Москва?", "Вы сами находитесь по этому адресу?", "Происшествие там же, где вы?", "Какой город?", "Это в черте города?", "Уточните адрес происшествия", "Вы смотрите на это или вам рассказали?", "Адрес происшествия и ваш адрес совпадают?"]}, {"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": null, "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": null, "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "incident_type", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1010101", "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_caller"], "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "victims": 0}, "era_glonass": null, "tree": {"pregenerated": false}}, "kio": {"card_id": "017cc2bd-fa73-4746-a656-bf5a7dcb6531", "registered_at": "2026-09-26T21:28:46.374694Z", "response_status": "transferred", "caller_number": "916-126-34-71", "caller_name": "Сидоров Иван Сергеевич", "caller_contact": "916-126-34-71", "phone_on_scene": null, "language": "ru", "okato": null, "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 1, "signs": ["на улице", "мусор", "открытое пламя"], "incident_code": "1010101", "incident_type": "fire", "description": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции; горит мусорный контейнер, пламя открытое; пострадавших нет", "victims_count": 0, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "dds": "01", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_card": {"card_id": "017cc2bd-fa73-4746-a656-bf5a7dcb6531", "registered_at": "2026-09-26T21:28:46.374694Z", "response_status": "transferred", "caller_number": "916-126-34-71", "caller_name": "Сидоров Иван Сергеевич", "caller_contact": "916-126-34-71", "phone_on_scene": null, "language": "ru", "okato": null, "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 1, "signs": ["на улице", "мусор", "открытое пламя"], "incident_code": "1010101", "incident_type": "fire", "description": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции; горит мусорный контейнер, пламя открытое; пострадавших нет", "victims_count": 0, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "dds": "01", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.374962+00:00", "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "bounced_fields": [], "dds_log": [], "status_log": [], "crew_selected": null, "crew_assignments": {}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "reply_text": "", "reply_log": []}], "dds_active_card_id": "91966e62-a166-490c-81da-a590b06cbe51", "dds_card_index": 1, "dds_arrival_interval_seconds": 0, "dds_max_waiting": 3, "dds_next_scenario_index": 3, "dds_next_arrival_at": null, "dds_completed": [], "reply_text": "Ответ по второй карточке", "reply_log": [["2026-09-26T21:28:46.375115+00:00", "Ответ по второй карточке"]], "text_revealed_facts": {}, "resolved_outcome": null, "resolve_comment": "", "processed_station_commands": []}, "call": {"version": 1, "session_id": "d3470132-b311-4a3a-bbd8-d503fcbf987b", "scenario_id": "fire-apartment-l2", "scenario_title": "Пожар в квартире, паникующий заявитель", "level": "L2", "mode": "training", "owner_login": null, "backend_fencing_epoch": 0, "exercise": "call", "handoff_to_dds": false, "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "trainee_name": null, "trainee_id": null, "dds_service": null, "attempt": 1, "criteria": {"decision_time_limit_seconds": 30, "card_fill_time_limit_seconds": 180, "dds_card_work_time_limit_seconds": 180, "allowed_errors": 0, "require_correct_grammar": true, "score_weights": {}}, "kio": {"card_id": "9f21be83-9749-4f65-a1b9-6bbc08e2c1de", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": ["жилой дом", "балкон", "открытое пламя"], "incident_code": "1050201", "incident_type": null, "description": null, "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "МВД", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "transcript": [], "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"answer": {"elapsed_ms": 0, "attempt": 1, "stopped": true}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}, "dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": true}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}}}, "hints_shown": [], "hints_log": [], "notes": [], "directives": [], "scenario": {"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, "audio_frames": 0, "bad_frames": 0, "self_assessed": false, "self_assessment": null, "score": null, "started_at": null, "ended_at": null, "end_reason": null, "dispatched_card": {"card_id": "9f21be83-9749-4f65-a1b9-6bbc08e2c1de", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": ["жилой дом", "балкон", "открытое пламя"], "incident_code": "1050201", "incident_type": null, "description": null, "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "МВД", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.384104+00:00", "bounced_fields": [], "dds_log": [["crew.select", "2026-09-26T21:28:46.384182+00:00", "Служба 101 — бригада №1"]], "status_log": [{"service": "Служба 101", "status": "accepted", "at": "2026-09-26T21:28:46.384136Z", "comment": "Основание: доклад старшего.\nСведения: карточка принята.", "author": "диспетчер"}], "crew_selected": "Служба 101 — бригада №1", "crew_assignments": {"Служба 101": "Служба 101 — бригада №1"}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "dds_scenarios": [], "pending_dds_scenarios": [], "operator_kio": null, "operator_scenario": null, "dds_live_cards": [], "dds_active_card_id": null, "dds_card_index": 0, "dds_arrival_interval_seconds": 0, "dds_max_waiting": 3, "dds_next_scenario_index": 0, "dds_next_arrival_at": null, "dds_completed": [], "reply_text": "", "reply_log": [], "text_revealed_facts": {}, "resolved_outcome": null, "resolve_comment": "", "processed_station_commands": []}, "handoff": {"version": 1, "session_id": "ad457f80-7401-4691-bf35-d8dd2b5e978d", "scenario_id": "fire-apartment-l2", "scenario_title": "Пожар в квартире, паникующий заявитель", "level": "L2", "mode": "training", "owner_login": null, "backend_fencing_epoch": 0, "exercise": "card", "handoff_to_dds": true, "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "trainee_name": null, "trainee_id": null, "dds_service": null, "attempt": 1, "criteria": {"decision_time_limit_seconds": 30, "card_fill_time_limit_seconds": 180, "dds_card_work_time_limit_seconds": 180, "allowed_errors": 0, "require_correct_grammar": true, "score_weights": {}}, "kio": {"card_id": "2592720c-1463-4a25-a2a2-343f8fe30194", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": [], "incident_code": null, "incident_type": null, "description": "горит балкон", "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "transcript": [], "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "hints_shown": [], "hints_log": [], "notes": [], "directives": [], "scenario": {"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, "audio_frames": 0, "bad_frames": 0, "self_assessed": false, "self_assessment": null, "score": null, "started_at": null, "ended_at": null, "end_reason": null, "dispatched_card": {"card_id": "2592720c-1463-4a25-a2a2-343f8fe30194", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": [], "incident_code": null, "incident_type": null, "description": "горит балкон", "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.391009+00:00", "bounced_fields": [], "dds_log": [], "status_log": [{"service": "Служба 101", "status": "accepted", "at": "2026-09-26T21:28:46.392282Z", "comment": "Основание: доклад старшего.\nСведения: карточка принята.", "author": "диспетчер"}], "crew_selected": null, "crew_assignments": {}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "dds_scenarios": [{"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, {"id": "t01-1-fire-container", "title": "Возгорание мусорного контейнера", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "calm", "arc": [{"stage": "registration", "mood": "calm"}, {"stage": "interview", "mood": "calm"}]}, "background": null, "first_line": "Здравствуйте. Тут мусорный контейнер горит, у депо.", "signs": ["на улице", "мусор", "открытое пламя"], "ticket": 1, "position": 1, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции", "reveal_on": {"question": "q_address"}, "refined": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "refine_on": "q_address_check"}, {"id": "f_what_burns", "value": "горит мусорный контейнер, пламя открытое", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "пострадавших нет", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "Сидоров Иван Сергеевич, телефон 916-126-34-71", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address_check", "question": "Уточните: происшествие по этому адресу, вы сейчас там?", "fact": null, "examples": ["Это точно Москва?", "Вы сами находитесь по этому адресу?", "Происшествие там же, где вы?", "Какой город?", "Это в черте города?", "Уточните адрес происшествия", "Вы смотрите на это или вам рассказали?", "Адрес происшествия и ваш адрес совпадают?"]}, {"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": null, "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": null, "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "incident_type", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1010101", "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_caller"], "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "victims": 0}, "era_glonass": null, "tree": {"pregenerated": false}}], "pending_dds_scenarios": [], "operator_kio": {"card_id": "2592720c-1463-4a25-a2a2-343f8fe30194", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": [], "incident_code": null, "incident_type": null, "description": "горит балкон", "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "operator_scenario": {"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, "dds_live_cards": [{"original_index": 0, "scenario": {"id": "fire-apartment-l2", "title": "Пожар в квартире, паникующий заявитель", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "panic", "arc": [{"stage": "registration", "mood": "panic"}, {"stage": "interview", "mood": "panic"}, {"stage": "dispatch", "mood": "worried"}]}, "background": {"loop": "fire_crackle.mp3", "gain_db": -18.0}, "first_line": "Алло! Алло! Помогите! Горим! А-а-а!", "signs": ["жилой дом", "балкон", "открытое пламя"], "ticket": null, "position": null, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "улица Ленина, 14, квартира 47, 5-й этаж", "reveal_on": {"question": "q_address"}, "refined": null, "refine_on": null}, {"id": "f_what_burns", "value": "горит балкон, дым пошёл в квартиру", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "в квартире жена и ребёнок, не выходят", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_smoke", "value": "дым идёт в подъезд, на площадке ничего не видно", "reveal_on": {"question": "q_smoke"}, "refined": null, "refine_on": null}, {"id": "f_gas", "value": "плита газовая, вентиль в кухне, перекрыть не успел", "reveal_on": {"question": "q_gas"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": "f_smoke", "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": "f_gas", "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": null, "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "floor", "incident_type", "dds", "victims_count", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1050201", "notify": ["Служба 101", "МВД", "СМП", "ЦЭМП", "Мособлгаз", "Гор. Хозяйство", "Мослифт", "ЦОДД", "Департамент РБиПК (ГКУ МОСБЕЗ)", "МКП, Аналитика", "Аппарат МЭРА", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "Мосжилинспекция", "ГКУ НТУ", "ФСО", "ДГП (Департамент градостроительной политики)", "ЦУКБ Министерство обороны"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_smoke", "f_gas"], "address": "улица Ленина, 14", "victims": 2}, "era_glonass": null, "tree": {"pregenerated": false}}, "kio": {"card_id": "2592720c-1463-4a25-a2a2-343f8fe30194", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": [], "incident_code": null, "incident_type": null, "description": "горит балкон", "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_card": {"card_id": "2592720c-1463-4a25-a2a2-343f8fe30194", "registered_at": null, "response_status": "transferred", "caller_number": null, "caller_name": null, "caller_contact": null, "phone_on_scene": null, "language": "ru", "okato": null, "address": "улица Ленина, 14", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": null, "signs": [], "incident_code": null, "incident_type": null, "description": "горит балкон", "victims_count": null, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101"], "dds": null, "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.391009+00:00", "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "bounced_fields": [], "dds_log": [], "status_log": [{"service": "Служба 101", "status": "accepted", "at": "2026-09-26T21:28:46.392282Z", "comment": "Основание: доклад старшего.\nСведения: карточка принята.", "author": "диспетчер"}], "crew_selected": null, "crew_assignments": {}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "reply_text": "", "reply_log": []}, {"original_index": 1, "scenario": {"id": "t01-1-fire-container", "title": "Возгорание мусорного контейнера", "type": "fire", "level": "L2", "topics": ["fires"], "modes": ["training", "exam", "self"], "extends": "checklists/fire.yaml", "persona": {"base": "calm", "arc": [{"stage": "registration", "mood": "calm"}, {"stage": "interview", "mood": "calm"}]}, "background": null, "first_line": "Здравствуйте. Тут мусорный контейнер горит, у депо.", "signs": ["на улице", "мусор", "открытое пламя"], "ticket": 1, "position": 1, "outcome": "card", "dds_decision": {"expected": "accept", "reason": null}, "student_card": null, "facts": [{"id": "f_address", "value": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции", "reveal_on": {"question": "q_address"}, "refined": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "refine_on": "q_address_check"}, {"id": "f_what_burns", "value": "горит мусорный контейнер, пламя открытое", "reveal_on": {"question": "q_what_burns"}, "refined": null, "refine_on": null}, {"id": "f_people", "value": "пострадавших нет", "reveal_on": {"question": "q_people"}, "refined": null, "refine_on": null}, {"id": "f_caller", "value": "Сидоров Иван Сергеевич, телефон 916-126-34-71", "reveal_on": {"question": "q_caller"}, "refined": null, "refine_on": null}], "checklist": [{"id": "q_address_check", "question": "Уточните: происшествие по этому адресу, вы сейчас там?", "fact": null, "examples": ["Это точно Москва?", "Вы сами находитесь по этому адресу?", "Происшествие там же, где вы?", "Какой город?", "Это в черте города?", "Уточните адрес происшествия", "Вы смотрите на это или вам рассказали?", "Адрес происшествия и ваш адрес совпадают?"]}, {"id": "q_address", "question": "Назовите адрес: улица, дом, квартира, этаж?", "fact": "f_address", "examples": ["Какой у вас адрес?", "Где вы находитесь?", "На каком этаже?", "Номер дома и квартиры?"]}, {"id": "q_what_burns", "question": "Что именно горит?", "fact": "f_what_burns", "examples": ["Что горит?", "Что именно загорелось?", "Откуда идёт огонь?"]}, {"id": "q_people", "question": "Есть ли люди в помещении?", "fact": "f_people", "examples": ["Кто-то остался внутри?", "Все успели выйти?"]}, {"id": "q_smoke", "question": "Куда идёт дым, есть ли задымление подъезда?", "fact": null, "examples": ["Куда идёт дым?", "Есть ли задымление в подъезде?", "Сильный дым?"]}, {"id": "q_gas", "question": "Есть ли газ, можно ли перекрыть?", "fact": null, "examples": ["У вас газовая плита?", "Перекрыть газ сможете?"]}, {"id": "q_caller", "question": "Представьтесь и оставьте контактный телефон", "fact": "f_caller", "examples": ["Как вас зовут?", "Ваш номер телефона?"]}], "required_fields": ["address", "incident_type", "description"], "score_weights": {}, "not_questions": ["Служба 112, что случилось?", "Успокойтесь, пожалуйста", "Оставайтесь на линии", "Помощь уже едет", "Я вас понял", "Говорите громче, плохо слышно", "Выходите из квартиры", "Не кладите трубку", "Хорошо, записываю"], "ground_truth": {"incident_type": "fire", "dds": "01", "incident_code": "1010101", "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "required_facts": ["f_address", "f_what_burns", "f_people", "f_caller"], "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "victims": 0}, "era_glonass": null, "tree": {"pregenerated": false}}, "kio": {"card_id": "fde25b52-b8c4-4702-a5fc-e431cb24d7d4", "registered_at": "2026-09-26T21:28:46.391943Z", "response_status": "transferred", "caller_number": "916-126-34-71", "caller_name": "Сидоров Иван Сергеевич", "caller_contact": "916-126-34-71", "phone_on_scene": null, "language": "ru", "okato": null, "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 1, "signs": ["на улице", "мусор", "открытое пламя"], "incident_code": "1010101", "incident_type": "fire", "description": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции; горит мусорный контейнер, пламя открытое; пострадавших нет", "victims_count": 0, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "dds": "01", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_card": {"card_id": "fde25b52-b8c4-4702-a5fc-e431cb24d7d4", "registered_at": "2026-09-26T21:28:46.391943Z", "response_status": "transferred", "caller_number": "916-126-34-71", "caller_name": "Сидоров Иван Сергеевич", "caller_contact": "916-126-34-71", "phone_on_scene": null, "language": "ru", "okato": null, "address": "Москва, Московская железная дорога, Киевская, 1 км, дом 2, строение 2", "street": null, "building": null, "entrance": null, "floor": null, "intercom_code": null, "coords": null, "incident_group": 1, "signs": ["на улице", "мусор", "открытое пламя"], "incident_code": "1010101", "incident_type": "fire", "description": "Москва, Депо, около станции Москва-Пассажирская Киевская, длинное помещение недалеко от участкового пункта полиции; горит мусорный контейнер, пламя открытое; пострадавших нет", "victims_count": 0, "is_emergency": false, "life_threat": false, "evacuation_needed": false, "notify": ["Служба 101", "Гор. Хозяйство", "ЦОДД", "Аппарат МЭРА", "ОАТИ", "Территориальные ОИВ", "Территориальные ОИВ ТиНАО", "ГКУ НТУ", "ФСО", "ГУП МСР", "ЦУКБ Министерство обороны", "ГПБУ Мосэкомониторинг"], "dds": "01", "dispatch_order_at": null, "arrival_at": null, "fire": null, "police": null, "medical": null, "utility": null}, "dispatched_at": "2026-09-26T21:28:46.392216+00:00", "timers": {"limits": {"answer": 8000, "interview": 75000, "dds_notify": 60000, "dds_ack": 30000, "card_fill": 180000, "dds_work": 180000, "zone_check": 30000, "callback": 10000, "close": 300000}, "items": {"dds_ack": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "close": {"elapsed_ms": 0, "attempt": 1, "stopped": false}, "interview": {"elapsed_ms": 0, "attempt": 1, "stopped": true}}}, "bounced_fields": [], "dds_log": [], "status_log": [], "crew_selected": null, "crew_assignments": {}, "phone_reports": [], "phone_lines": [], "phone_pending": null, "reply_text": "", "reply_log": []}], "dds_active_card_id": "2592720c-1463-4a25-a2a2-343f8fe30194", "dds_card_index": 0, "dds_arrival_interval_seconds": 0, "dds_max_waiting": 3, "dds_next_scenario_index": 2, "dds_next_arrival_at": null, "dds_completed": [], "reply_text": "", "reply_log": [], "text_revealed_facts": {}, "resolved_outcome": null, "resolve_comment": "", "processed_station_commands": []}} diff --git a/backend/tests/test_caller_progress.py b/backend/tests/test_caller_progress.py new file mode 100644 index 0000000..7279e65 --- /dev/null +++ b/backend/tests/test_caller_progress.py @@ -0,0 +1,97 @@ +"""Прогресс звонящего — чистые данные, из которых собирается живой звонящий. + +В снимок занятия прогресс пока не пишется; модель заложена, чтобы включение +сохранения было одним полем сохраняемой части и передачей в пересборку. +""" + +from uuid import uuid4 + +from app.dialog.persona import PersonaProgress, PersonaState +from app.dialog.slots import SlotMachine, SlotProgress +from app.domain.events import Exercise, Mood, SessionMode +from app.session import checkpoint +from app.session.checkpoint import CallerProgress, dump_state, rebuild_live +from app.session.state import SessionState +from tests.test_refinement import SCENARIO +from tests.test_slots import StemEmbedder + + +def slot_progress() -> SlotProgress: + return SlotProgress(asked=["q_address", "q_city"], revealed=["f_address"], + refined=["f_address"]) + + +def persona_progress() -> PersonaProgress: + return PersonaProgress(stage="dispatch", repeats=2, directive="panic_rises", + history=[Mood.CALM, Mood.PANIC]) + + +def test_slot_machine_resumes_given_progress(): + progress = slot_progress() + + slots = SlotMachine(SCENARIO, StemEmbedder(), progress=progress) + + assert slots.progress == progress + assert [fact.id for fact in slots.revealed_facts()] == ["f_address"] + assert slots.refined == ["f_address"] + assert slots.value_of("f_address") == "Королёв, улица Станционная, дом 28" + assert [item.id for item in slots.unasked()] == [] + + +def test_slot_machine_without_progress_starts_clean(): + slots = SlotMachine(SCENARIO, StemEmbedder()) + + assert slots.progress == SlotProgress() + slots.hear("Назовите адрес") + assert slots.progress.revealed == ["f_address"] + + +def test_persona_resumes_given_progress(): + progress = persona_progress() + + persona = PersonaState(SCENARIO.persona, progress=progress) + + assert persona.progress == progress + assert persona.mood is Mood.PANIC + assert persona.repeats == 2 + + +def call_state() -> SessionState: + return SessionState( + session_id=uuid4(), scenario_id=SCENARIO.id, scenario_title=SCENARIO.title, + level=SCENARIO.level.value, mode=SessionMode.TRAINING, + exercise=Exercise.CALL, scenario=SCENARIO, + ) + + +def test_rebuild_live_uses_caller_progress(monkeypatch): + monkeypatch.setattr(checkpoint, "get_embedder", StemEmbedder) + state = call_state() + progress = CallerProgress(slots=slot_progress(), persona=persona_progress()) + + rebuild_live(state, progress) + + assert state.slots.progress == progress.slots + assert state.persona.progress == progress.persona + assert state.caller is not None + + +def test_rebuild_live_without_progress_starts_clean(monkeypatch): + monkeypatch.setattr(checkpoint, "get_embedder", StemEmbedder) + state = call_state() + + rebuild_live(state) + + assert state.slots.progress == SlotProgress() + assert state.persona.progress == PersonaProgress() + + +def test_snapshot_has_no_caller_progress(monkeypatch): + monkeypatch.setattr(checkpoint, "get_embedder", StemEmbedder) + state = call_state() + rebuild_live(state, CallerProgress(slots=slot_progress(), persona=persona_progress())) + + payload = dump_state(state) + + assert not {"slots", "persona", "caller", "caller_progress"} & set(payload) + assert "panic_rises" not in str(payload)