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 чистый.
This commit is contained in:
parent
7212251112
commit
842a333d16
11 changed files with 381 additions and 8 deletions
|
|
@ -69,6 +69,16 @@ REVEAL: dict[Mood, str] = {
|
|||
Mood.CONFUSED: "Так... {fact}... кажется.",
|
||||
}
|
||||
|
||||
#: Уточнение: оператор переспросил, и выясняется, что адрес был не тот.
|
||||
#: Это не повтор — звонящий не раздражается, он поправляется.
|
||||
REFINE: dict[Mood, str] = {
|
||||
Mood.PANIC: "Нет, не там! {fact}!",
|
||||
Mood.WORRIED: "Ой, нет... {fact}.",
|
||||
Mood.CALM: "Уточню: {fact}.",
|
||||
Mood.AGGRESSIVE: "Да я же про другое! {fact}!",
|
||||
Mood.CONFUSED: "А... нет, кажется {fact}...",
|
||||
}
|
||||
|
||||
#: Повторный вопрос по уже сказанному.
|
||||
REPEAT: dict[Mood, str] = {
|
||||
Mood.PANIC: "Я же сказал — {fact}! Записывайте!",
|
||||
|
|
@ -94,7 +104,11 @@ class TemplateCaller:
|
|||
|
||||
parts: list[str] = []
|
||||
for fact_id in turn.revealed:
|
||||
parts.append(REVEAL[mood].format(fact=facts[fact_id]))
|
||||
template = REFINE if fact_id in turn.refined else REVEAL
|
||||
parts.append(template[mood].format(fact=facts[fact_id]))
|
||||
for fact_id in turn.refined:
|
||||
if fact_id not in turn.revealed:
|
||||
parts.append(REFINE[mood].format(fact=facts[fact_id]))
|
||||
for fact_id in turn.repeated:
|
||||
parts.append(REPEAT[mood].format(fact=facts[fact_id]))
|
||||
|
||||
|
|
@ -139,7 +153,11 @@ class LlmCaller:
|
|||
mood = persona.remember()
|
||||
|
||||
facts = {fact.id: fact.value for fact in slots.revealed_facts()}
|
||||
say_now = [facts[fact_id] for fact_id in turn.revealed if fact_id in facts]
|
||||
say_now = [
|
||||
facts[fact_id]
|
||||
for fact_id in [*turn.revealed, *turn.refined]
|
||||
if fact_id in facts
|
||||
]
|
||||
repeated = [facts[fact_id] for fact_id in turn.repeated if fact_id in facts]
|
||||
|
||||
system = _prompt("caller.md").format(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class TurnResult:
|
|||
matched: list[str] = field(default_factory=list) # пункты чек-листа
|
||||
revealed: list[str] = field(default_factory=list) # факты, раскрытые впервые
|
||||
repeated: list[str] = field(default_factory=list) # факты, спрошенные повторно
|
||||
refined: list[str] = field(default_factory=list) # факты, уточнённые переспросом
|
||||
scores: dict[str, float] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
|
|
@ -95,8 +96,16 @@ class SlotMachine:
|
|||
if question and not fact.hidden and fact.id not in self._reveals.get(question, []):
|
||||
self._reveals.setdefault(question, []).append(fact.id)
|
||||
|
||||
# Какой пункт чек-листа какой факт уточняет: «это точно Москва?» меняет
|
||||
# адрес заявителя на адрес происшествия (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)
|
||||
|
||||
self.asked: list[str] = []
|
||||
self.revealed: list[str] = []
|
||||
self.refined: list[str] = []
|
||||
|
||||
# ── реплика оператора ──
|
||||
|
||||
|
|
@ -149,6 +158,18 @@ class SlotMachine:
|
|||
else:
|
||||
self.revealed.append(fact_id)
|
||||
result.revealed.append(fact_id)
|
||||
|
||||
# Уточнение — не повтор: оператор спросил о другом и получил другое.
|
||||
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)
|
||||
return result
|
||||
|
||||
def reveal_by_approach(self, fact_id: str) -> bool:
|
||||
|
|
@ -168,9 +189,20 @@ class SlotMachine:
|
|||
|
||||
# ── что видят другие ──
|
||||
|
||||
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
|
||||
|
||||
def revealed_facts(self) -> list[Fact]:
|
||||
"""Единственное, что уходит в контекст звонящего."""
|
||||
return [self._facts[fact_id] for fact_id in self.revealed]
|
||||
return [
|
||||
self._facts[fact_id].model_copy(update={"value": self.value_of(fact_id)})
|
||||
for fact_id in self.revealed
|
||||
]
|
||||
|
||||
def unasked(self) -> list[ChecklistItem]:
|
||||
"""Неотработанные пункты по порядку чек-листа — источник подсказки."""
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ class TreeCaller:
|
|||
|
||||
parts: list[str] = []
|
||||
for fact_id in turn.revealed:
|
||||
parts.append(self._line("reveal", fact_id, mood) or "")
|
||||
section = "refine" if fact_id in turn.refined else "reveal"
|
||||
parts.append(self._line(section, fact_id, mood) or "")
|
||||
for fact_id in turn.refined:
|
||||
if fact_id not in turn.revealed:
|
||||
parts.append(self._line("refine", fact_id, mood) or "")
|
||||
for fact_id in turn.repeated:
|
||||
parts.append(self._line("repeat", fact_id, mood) or "")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue