lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
"""Слот-автомат: какие факты раскрыты, какие вопросы заданы.
|
|
|
|
|
|
|
|
|
|
|
|
**Звонящий не выдаёт информацию сам.** Автомат держит состояние явно и отдаёт
|
|
|
|
|
|
звонящему только раскрытые факты — иначе LLM услужливо назовёт адрес без вопроса
|
|
|
|
|
|
(docs/product/CALL-SIM.md).
|
|
|
|
|
|
|
|
|
|
|
|
Тот же автомат детерминированно отвечает на вопрос «спросил ли оператор про этаж»:
|
|
|
|
|
|
на нём стоят подсказки, полнота опроса в оценке (E1) и аналитика группы.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
from app.dialog.embeddings import Embedder
|
|
|
|
|
|
from app.scenarios.schema import ChecklistItem, Fact, Scenario
|
|
|
|
|
|
|
|
|
|
|
|
#: Как решается, что реплика — вопрос пункта чек-листа.
|
|
|
|
|
|
#:
|
|
|
|
|
|
#: Порог по косинусной близости не работает: на multilingual-e5-small настоящие
|
|
|
|
|
|
#: вопросы дают 0.79–0.95, а не-вопросы вроде «Оставайтесь на линии» — до 0.89.
|
|
|
|
|
|
#: Диапазоны перекрываются, и любой порог либо выдаёт адрес на «успокойтесь»,
|
|
|
|
|
|
#: либо не слышит «где вы находитесь?».
|
|
|
|
|
|
#:
|
|
|
|
|
|
#: Работает сравнение с ближайшим соседом: реплика засчитывается пункту, если
|
|
|
|
|
|
#: она ближе к одной из его формулировок, чем к любому не-вопросу из
|
|
|
|
|
|
#: checklists/common.yaml. На отложенных фразах (tests/test_slots_e5.py):
|
|
|
|
|
|
#: 19 из 20 вопросов распознано, 0 из 8 ложных срабатываний.
|
|
|
|
|
|
#:
|
|
|
|
|
|
#: MATCH_MARGIN — насколько ближе к пункту, чем к не-вопросу. Ноль даёт лучшее
|
|
|
|
|
|
#: распознавание при нуле ложных срабатываний; 0.03 теряет уже 5 вопросов из 20.
|
|
|
|
|
|
#: MATCH_FLOOR — нижняя граница на случай сценария без не-вопросов.
|
|
|
|
|
|
MATCH_MARGIN = 0.0
|
|
|
|
|
|
MATCH_FLOOR = 0.75
|
|
|
|
|
|
|
|
|
|
|
|
#: Реплика режется только по границам предложений, и знак остаётся при части.
|
|
|
|
|
|
#: «?» для e5 — сильный признак вопроса: без него «Куда ехать» ближе
|
|
|
|
|
|
#: к «Оставайтесь на линии», чем к пункту про адрес. По запятым и «и» не режем:
|
|
|
|
|
|
#: «Улица, дом?» распадается на обрывки, которые не похожи ни на что.
|
|
|
|
|
|
_SENTENCES = re.compile(r"(?<=[?!.;])\s+")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class TurnResult:
|
|
|
|
|
|
"""Что произошло в одной реплике оператора."""
|
|
|
|
|
|
|
|
|
|
|
|
text: str
|
|
|
|
|
|
matched: list[str] = field(default_factory=list) # пункты чек-листа
|
|
|
|
|
|
revealed: list[str] = field(default_factory=list) # факты, раскрытые впервые
|
|
|
|
|
|
repeated: list[str] = field(default_factory=list) # факты, спрошенные повторно
|
feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.
До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.
- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
метрики address: «записал не тот адрес» и «не спросил» — разные навыки
и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
заказчика (билет 3, вызов 1).
Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.
125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
|
|
|
|
refined: list[str] = field(default_factory=list) # факты, уточнённые переспросом
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
scores: dict[str, float] = field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def understood(self) -> bool:
|
|
|
|
|
|
return bool(self.matched)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SlotMachine:
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
scenario: Scenario,
|
|
|
|
|
|
embedder: Embedder,
|
|
|
|
|
|
floor: float = MATCH_FLOOR,
|
|
|
|
|
|
margin: float = MATCH_MARGIN,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
self.scenario = scenario
|
|
|
|
|
|
self.floor = floor
|
|
|
|
|
|
self.margin = margin
|
|
|
|
|
|
self._embedder = embedder
|
|
|
|
|
|
|
|
|
|
|
|
self._facts: dict[str, Fact] = {fact.id: fact for fact in scenario.facts}
|
|
|
|
|
|
self._items: list[ChecklistItem] = [item for item in scenario.checklist if item.question]
|
|
|
|
|
|
|
|
|
|
|
|
# Каждая формулировка — отдельная строка; `_owner` помнит, чей это пункт.
|
|
|
|
|
|
texts: list[str] = []
|
|
|
|
|
|
self._owner: list[int] = []
|
|
|
|
|
|
for index, item in enumerate(self._items):
|
|
|
|
|
|
for text in [item.question, *item.examples]:
|
|
|
|
|
|
texts.append(text)
|
|
|
|
|
|
self._owner.append(index)
|
|
|
|
|
|
self._anchors = embedder.embed(texts)
|
|
|
|
|
|
self._not_questions = (
|
|
|
|
|
|
embedder.embed(scenario.not_questions) if scenario.not_questions else None
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Какой пункт чек-листа какие факты раскрывает. Скрытые факты вопросом
|
|
|
|
|
|
# не раскрываются никогда — только подходом.
|
|
|
|
|
|
self._reveals: dict[str, list[str]] = {}
|
|
|
|
|
|
for item in self._items:
|
2026-09-26 18:12:27 +03:00
|
|
|
|
if item.fact:
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
self._reveals.setdefault(item.id, []).append(item.fact)
|
|
|
|
|
|
for fact in scenario.facts:
|
|
|
|
|
|
question = fact.reveal_on.question if fact.reveal_on else None
|
2026-09-26 18:12:27 +03:00
|
|
|
|
if question and fact.id not in self._reveals.get(question, []):
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
self._reveals.setdefault(question, []).append(fact.id)
|
|
|
|
|
|
|
feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.
До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.
- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
метрики address: «записал не тот адрес» и «не спросил» — разные навыки
и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
заказчика (билет 3, вызов 1).
Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.
125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
|
|
|
|
# Какой пункт чек-листа какой факт уточняет: «это точно Москва?» меняет
|
|
|
|
|
|
# адрес заявителя на адрес происшествия (docs/spec/TICKETS.md).
|
|
|
|
|
|
self._refines: dict[str, list[str]] = {}
|
|
|
|
|
|
for fact in scenario.facts:
|
|
|
|
|
|
if fact.refine_on:
|
|
|
|
|
|
self._refines.setdefault(fact.refine_on, []).append(fact.id)
|
|
|
|
|
|
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
self.asked: list[str] = []
|
|
|
|
|
|
self.revealed: list[str] = []
|
feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.
До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.
- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
метрики address: «записал не тот адрес» и «не спросил» — разные навыки
и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
заказчика (билет 3, вызов 1).
Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.
125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
|
|
|
|
self.refined: list[str] = []
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
|
|
|
|
|
|
# ── реплика оператора ──
|
|
|
|
|
|
|
|
|
|
|
|
def _clauses(self, text: str) -> list[str]:
|
|
|
|
|
|
parts = [part.strip() for part in _SENTENCES.split(text.strip()) if part.strip()]
|
|
|
|
|
|
return parts or [text]
|
|
|
|
|
|
|
|
|
|
|
|
def hear(self, text: str) -> TurnResult:
|
|
|
|
|
|
"""Сопоставить реплику с чек-листом и раскрыть заслуженные факты.
|
|
|
|
|
|
|
|
|
|
|
|
Реплика режется на предложения: «Где вы? Есть кто внутри?» — два вопроса,
|
|
|
|
|
|
и оба должны засчитаться.
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = TurnResult(text=text)
|
|
|
|
|
|
clauses = self._embedder.embed(self._clauses(text))
|
|
|
|
|
|
by_anchor = self._anchors @ clauses.T # формулировки × части реплики
|
|
|
|
|
|
|
|
|
|
|
|
# Лучшая близость каждого пункта к каждой части реплики.
|
|
|
|
|
|
owner = np.array(self._owner)
|
|
|
|
|
|
item_scores = np.stack(
|
|
|
|
|
|
[by_anchor[owner == index].max(axis=0) for index in range(len(self._items))]
|
|
|
|
|
|
) # пункты × части
|
|
|
|
|
|
|
|
|
|
|
|
# Насколько каждая часть похожа на не-вопрос.
|
|
|
|
|
|
if self._not_questions is not None:
|
|
|
|
|
|
not_question = (self._not_questions @ clauses.T).max(axis=0)
|
|
|
|
|
|
else:
|
|
|
|
|
|
not_question = np.full(clauses.shape[0], -1.0)
|
|
|
|
|
|
|
|
|
|
|
|
# Часть реплики засчитывается только одному, ближайшему пункту:
|
|
|
|
|
|
# «адрес» не должен заодно раскрыть «кто в квартире».
|
|
|
|
|
|
winners: dict[int, float] = {}
|
|
|
|
|
|
for part in range(clauses.shape[0]):
|
|
|
|
|
|
best = int(np.argmax(item_scores[:, part]))
|
|
|
|
|
|
score = float(item_scores[best, part])
|
|
|
|
|
|
if score >= self.floor and score > not_question[part] + self.margin:
|
|
|
|
|
|
winners[best] = max(score, winners.get(best, -1.0))
|
|
|
|
|
|
|
|
|
|
|
|
for index, item in enumerate(self._items):
|
|
|
|
|
|
result.scores[item.id] = float(item_scores[index].max())
|
|
|
|
|
|
if index not in winners:
|
|
|
|
|
|
continue
|
|
|
|
|
|
result.matched.append(item.id)
|
|
|
|
|
|
if item.id not in self.asked:
|
|
|
|
|
|
self.asked.append(item.id)
|
|
|
|
|
|
for fact_id in self._reveals.get(item.id, []):
|
|
|
|
|
|
if fact_id in self.revealed:
|
|
|
|
|
|
if fact_id not in result.repeated:
|
|
|
|
|
|
result.repeated.append(fact_id)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.revealed.append(fact_id)
|
|
|
|
|
|
result.revealed.append(fact_id)
|
feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.
До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.
- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
метрики address: «записал не тот адрес» и «не спросил» — разные навыки
и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
заказчика (билет 3, вызов 1).
Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.
125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
|
|
|
|
|
|
|
|
|
|
# Уточнение — не повтор: оператор спросил о другом и получил другое.
|
|
|
|
|
|
for fact_id in self._refines.get(item.id, []):
|
|
|
|
|
|
if fact_id in self.refined:
|
|
|
|
|
|
continue
|
|
|
|
|
|
self.refined.append(fact_id)
|
|
|
|
|
|
result.refined.append(fact_id)
|
|
|
|
|
|
if fact_id not in self.revealed:
|
|
|
|
|
|
# Уточнили раньше, чем спросили: факт считается раскрытым,
|
|
|
|
|
|
# иначе звонящий промолчит о том, что только что назвал.
|
|
|
|
|
|
self.revealed.append(fact_id)
|
|
|
|
|
|
result.revealed.append(fact_id)
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def invalidate(self, fact_id: str) -> None:
|
|
|
|
|
|
"""Директива «адрес оказался неточным»: оператор обязан переспросить."""
|
|
|
|
|
|
if fact_id in self.revealed:
|
|
|
|
|
|
self.revealed.remove(fact_id)
|
|
|
|
|
|
|
|
|
|
|
|
# ── что видят другие ──
|
|
|
|
|
|
|
feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.
До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.
- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
метрики address: «записал не тот адрес» и «не спросил» — разные навыки
и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
заказчика (билет 3, вызов 1).
Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.
125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
|
|
|
|
def value_of(self, fact_id: str) -> str:
|
|
|
|
|
|
"""Значение факта с учётом уточнения: до переспроса заявитель называет
|
|
|
|
|
|
ориентир, после — настоящий адрес."""
|
|
|
|
|
|
fact = self._facts[fact_id]
|
|
|
|
|
|
if fact_id in self.refined and fact.refined:
|
|
|
|
|
|
return fact.refined
|
|
|
|
|
|
return fact.value
|
|
|
|
|
|
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
def revealed_facts(self) -> list[Fact]:
|
|
|
|
|
|
"""Единственное, что уходит в контекст звонящего."""
|
feat: уточнение адреса — место происшествия не равно адресу заявителя (lct-35)
Главный предмет проверки в билетах заказчика — адрес: названный заявителем
часто неверен, а настоящий добывается переспросом («ул. Станционная, 28»
оказывается Королёвом). Памятка АРМ-112 называет это обычным делом.
До этой правки оценка работала наоборот: ground_truth.address сравнивался
с единственным значением факта, поэтому курсант, правильно переспросивший
и записавший настоящий адрес, получал расхождение с эталоном, а записавший
ориентир — зачёт.
- Схема: у факта появились refined и refine_on, задаются только вместе.
- Слот-автомат различает уточнение и повтор: повтор раздражает звонящего,
уточнение — нет, оператор спросил о другом и получил другое.
- Звонящий поправляется отдельной репликой, а не повторяет прежнее значение;
офлайн-таблица получила секцию refine.
- Оценка: метрика address_refined (E1, опрос). Сделана отдельной, а не правкой
метрики address: «записал не тот адрес» и «не спросил» — разные навыки
и разные компетенции в радаре.
- Общие пункты чек-листа подключаются по требованию сценария: формулировки
в checklists/common.yaml, сценарий объявляет пункт с тем же id без текста.
Добавлять «уточните адрес» во все сценарии нельзя — неотработанный пункт
штрафует за вопрос, которого сценарий не требовал.
- scenarios/fire-private-house-l3.yaml — первый сценарий из библиотеки
заказчика (билет 3, вызов 1).
Риск механики проверен на живой модели: «Назовите адрес» и «Это точно Москва?»
эмбеддинги не путают, тест это фиксирует.
125 тестов зелёных (12 новых), make typecheck чистый.
2026-09-19 20:26:49 +03:00
|
|
|
|
return [
|
|
|
|
|
|
self._facts[fact_id].model_copy(update={"value": self.value_of(fact_id)})
|
|
|
|
|
|
for fact_id in self.revealed
|
|
|
|
|
|
]
|
lct-07 (без LLM): слот-автомат, персона, звонящий на заготовках, make repl
Звонящий не выдаёт данные сам: автомат держит раскрытые факты явно и
отдаёт звонящему только их. Тест прогоняет длинную серию реплик и
проверяет, что ни один нераскрытый факт не прозвучал.
Главная находка — матчинг по порогу близости не работает. На
multilingual-e5-small настоящие вопросы дают 0.79–0.95, а «Оставайтесь
на линии» — до 0.89: диапазоны перекрываются, и любой порог либо выдаёт
адрес на «успокойтесь», либо не слышит «где вы находитесь?».
Решение — сравнение с ближайшим соседом: у пункта чек-листа несколько
формулировок (examples), рядом общий список не-вопросов
(checklists/common.yaml), и реплика засчитывается пункту, только если она
ближе к нему, чем к любому не-вопросу. На отложенных фразах: 19 из 20
вопросов, 0 из 8 ложных срабатываний.
Реплика режется только по границам предложений, со знаком: «?» для e5 —
сильный признак вопроса, без него «Куда ехать» уходит к не-вопросам.
Подсказка берёт неотработанный пункт из автомата. В живой сессии автомат
начнёт слышать оператора, когда голосовой контур передаст ему stt.final.
Ждёт ключа LLM: условие approach у скрытых фактов, звонящий своими
словами, dialog/llm.py с кэшем.
2026-09-17 13:57:43 +03:00
|
|
|
|
|
|
|
|
|
|
def unasked(self) -> list[ChecklistItem]:
|
|
|
|
|
|
"""Неотработанные пункты по порядку чек-листа — источник подсказки."""
|
|
|
|
|
|
return [item for item in self._items if item.id not in self.asked]
|
|
|
|
|
|
|
|
|
|
|
|
def missing_required(self) -> list[str]:
|
|
|
|
|
|
"""Обязательные факты, которые оператор так и не добыл — основание E1."""
|
|
|
|
|
|
return [
|
|
|
|
|
|
fact_id
|
|
|
|
|
|
for fact_id in self.scenario.ground_truth.required_facts
|
|
|
|
|
|
if fact_id not in self.revealed
|
|
|
|
|
|
]
|