Мягкие директивы перекрывают дугу сценария и применяются со следующей реплики: разговор не дёргается от того, что преподаватель что-то нажал. Жёсткие правят ситуацию — обрыв связи рвёт звук тем же механизмом, что перебивание, и запускает норматив обратного дозвона; второй пострадавший правит эталон; неточный адрес снимает раскрытый факт, и оператор обязан переспросить. Своя копия сценария на занятие: директивы правят факты и эталон, а сценарий был общим на библиотеку — правка в одной группе протекла бы во все остальные. Тест проверяет, что библиотека не изменилась. Свободный текст честно отказывает: офлайн-дерево предгенерировано, произвольную фразу взять неоткуда, и преподаватель видит это на пульте. Ни одна директива не трогает карточку курсанта — он управляет ситуацией, а не работой обучаемого. Занятие теперь собирается целиком и только потом регистрируется: наблюдатель мог увидеть его без слот-автомата и звонящего.
86 lines
4.2 KiB
Python
86 lines
4.2 KiB
Python
"""Директивы преподавателя: он ведёт ситуацию по ходу разговора.
|
||
|
||
«Управление ситуаций с рабочего места преподавателя» — дословное требование ТЗ,
|
||
понятое буквально (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
|