lct-22: пульт директив — преподаватель ведёт ситуацию
Мягкие директивы перекрывают дугу сценария и применяются со следующей реплики: разговор не дёргается от того, что преподаватель что-то нажал. Жёсткие правят ситуацию — обрыв связи рвёт звук тем же механизмом, что перебивание, и запускает норматив обратного дозвона; второй пострадавший правит эталон; неточный адрес снимает раскрытый факт, и оператор обязан переспросить. Своя копия сценария на занятие: директивы правят факты и эталон, а сценарий был общим на библиотеку — правка в одной группе протекла бы во все остальные. Тест проверяет, что библиотека не изменилась. Свободный текст честно отказывает: офлайн-дерево предгенерировано, произвольную фразу взять неоткуда, и преподаватель видит это на пульте. Ни одна директива не трогает карточку курсанта — он управляет ситуацией, а не работой обучаемого. Занятие теперь собирается целиком и только потом регистрируется: наблюдатель мог увидеть его без слот-автомата и звонящего.
This commit is contained in:
parent
978d7dc1aa
commit
57f4c858b6
13 changed files with 317 additions and 23 deletions
86
backend/app/dialog/director.py
Normal file
86
backend/app/dialog/director.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
"""Директивы преподавателя: он ведёт ситуацию по ходу разговора.
|
||||
|
||||
«Управление ситуаций с рабочего места преподавателя» — дословное требование ТЗ,
|
||||
понятое буквально (docs/product/CALL-SIM.md#преподаватель-режиссёр).
|
||||
|
||||
Мягкие директивы меняют подачу и живут до конца звонка, жёсткие правят факты
|
||||
и таймеры. **Ни одна не трогает карточку курсанта:** преподаватель управляет
|
||||
ситуацией, а не работой обучаемого.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.domain.events import Mood
|
||||
|
||||
#: Мягкие директивы: только подача. Настроение берёт `PersonaState`.
|
||||
SOFT = {
|
||||
"panic_rises": "Паника нарастает",
|
||||
"screaming": "Переходит на крик",
|
||||
"turns_aggressive": "Переходит в агрессию",
|
||||
"distracted": "Звонящий отвлёкся",
|
||||
}
|
||||
|
||||
#: Жёсткие: правят факты, таймеры или ход звонка.
|
||||
HARD = {
|
||||
"line_dropped": "Связь обрывается",
|
||||
"second_victim": "Появился второй пострадавший",
|
||||
"address_wrong": "Адрес оказался неточным",
|
||||
}
|
||||
|
||||
#: Что звонящий скажет следующей репликой по жёсткой директиве.
|
||||
HARD_LINES = {
|
||||
"second_victim": "Тут ещё женщина на площадке! Она не дышит! Быстрее!",
|
||||
"address_wrong": "Стойте! Я перепутал, адрес другой! Запишите заново!",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirectiveResult:
|
||||
applied: bool
|
||||
#: Реплика, которую звонящий должен сказать сразу.
|
||||
say: str | None = None
|
||||
#: Оборвать связь: звук на полуслове, дальше обратный дозвон.
|
||||
drop_line: bool = False
|
||||
#: Директива требует сети (свободный текст без LLM).
|
||||
needs_network: bool = False
|
||||
|
||||
|
||||
def apply(state, directive: str) -> DirectiveResult:
|
||||
"""Применить директиву к живому занятию."""
|
||||
if directive in SOFT:
|
||||
if state.persona is not None:
|
||||
state.persona.directive = directive
|
||||
return DirectiveResult(applied=True)
|
||||
|
||||
if directive == "line_dropped":
|
||||
return DirectiveResult(applied=True, drop_line=True)
|
||||
|
||||
if directive == "second_victim":
|
||||
# Правка эталона: теперь пострадавших на одного больше, и оценка
|
||||
# карточки считается против нового числа.
|
||||
truth = state.scenario.ground_truth if state.scenario else None
|
||||
if truth is not None:
|
||||
truth.victims = (truth.victims or 0) + 1
|
||||
return DirectiveResult(applied=True, say=HARD_LINES[directive])
|
||||
|
||||
if directive == "address_wrong":
|
||||
# Факт снимается с раскрытых: оператор обязан переспросить адрес,
|
||||
# а в оценке полнота опроса снова считает его недобытым.
|
||||
if state.slots is not None:
|
||||
for fact in list(state.slots.revealed_facts()):
|
||||
if "address" in fact.id:
|
||||
state.slots.invalidate(fact.id)
|
||||
return DirectiveResult(applied=True, say=HARD_LINES[directive])
|
||||
|
||||
# Свободный текст уходит в контекст персоны — но подставить его в реплику
|
||||
# может только LLM. Офлайн-дерево предгенерировано, произвольную фразу
|
||||
# взять неоткуда (docs/arch/CONTRACT.md).
|
||||
if state.persona is not None:
|
||||
state.persona.directive = None
|
||||
if state.directives is not None:
|
||||
state.directives.append(directive)
|
||||
return DirectiveResult(applied=False, needs_network=True)
|
||||
|
||||
|
||||
def mood_of(state) -> Mood:
|
||||
return state.persona.mood if state.persona else Mood.PANIC
|
||||
Loading…
Reference in a new issue