feat(sprint7.6): оптимизация воронки new_booking до 4 шагов (вариант 2)

Воронка сжата с 6 шагов до 4: intro → qualify → book → close.
Спецификация: docs/OPTIMIZATION_CONVERSION_v1.md.
Цель: сравнимая с конкурентом (NEXTBOT/Александра) конверсия — ≤3 реплик
бота до запроса телефона, содержательный ответ на жалобу в первом
осмысленном сообщении.

Промпты шагов:
- intro.md — переписан. Приветствие + открытый вопрос «что беспокоит?».
  Имя НЕ спрашиваем (слот name со шага снят), оно собирается на book
  вместе с телефоном. Если пациент сразу написал жалобу — не зацикливаемся,
  переходим в qualify.
- qualify.md — переписан. Обязательный 5-пунктовый шаблон ответа на жалобу:
  эмпатия (одна фраза) → 2-3 ЛОР-гипотезы из RAG-выдержек («может быть
  связано с») → специалист → услуга/цена («при необходимости назначит») →
  бинарный CTA «записать?». Если в выдержках нет гипотез/цен — пункт
  пропускается, не сочиняем. Если жалоба не описана (пациент сразу
  «хочу записаться к ЛОРу») — пропускаем гипотезу/услугу, оставляем
  эмпатию-формальность + специалист + CTA.
  Три особые ситуации сохранены: ребёнок (require_legal_rep), конкретный
  врач (waitlist_flag), первичная жалоба на слух (needs_surgologist_first).
- book.md — переписан. Одной репликой: подтверждение плана с
  использованием {specialist}/{reason} + запрос телефона + имени (если
  ещё не было в истории). При is_child=true — обращение к родителю,
  legal_rep_phone используется, если уже собран.
- present.md — DEPRECATED. Файл оставлен в репо на случай отката
  (вариант 1 спецификации). Внутри — заглушка «попал по ошибке —
  выходи на book».
- close.md и offer_time.md не тронуты (offer_time станет актуален с
  реальным календарём).

allowed_next в SEED_INTENT_STEPS:
- intro: [intro, qualify] (без изменений)
- qualify: [qualify, book] (раньше: [qualify, present])
- present: [book] (изоляция; раньше: [present, qualify, offer_time])
- offer_time: [offer_time, book] (deprecated, без изменений)
- book: [book, qualify, close] (раньше: [book, qualify, offer_time, close])
- close: [close] (без изменений)

migrate_new_booking_allowed_next_v2(session) — одноразовая миграция в
services/intent_step_service.py. При старте для каждого шага
new_booking сравнивает текущий allowed_next_json с дореформенным
значением (_PRE_SPRINT_7_6_ALLOWED_NEXT). Если совпадает — обновляет
на новое из SEED. Если оператор правил вручную — пропускает,
warning в лог. Идемпотентна (на повторных запусках ничего не делает).
Подключена в main.py lifespan после ensure_seed_guards.

Защитное условие require_legal_rep на qualify сохранено. Теперь блокирует
переход qualify → book (раньше qualify → present). Логика та же:
при is_child=true и пустых legal_rep_name/legal_rep_phone валидатор
отклоняет переход.

eval/MANUAL_CASES.md — markdown-чеклист для ручных прогонов:
- §A: 5 конверсионных кейсов (храп+уши, боль в горле, тугоухость,
  насморк >месяца, звон в ушах) с чеклистом 5 пунктов на первый ответ
  и проверкой ≤3 реплик до телефона.
- §B: регрессия 8 ручных сценариев из блока H Спринта 6b со ссылками
  на docs/examples/*_v2.md.

SPRINTS.md: Спринт 7.6 →  Закрыт по коду. Применение промптов в БД
и ручная регрессия — за оператором (через UI «Настройки → Шаги»
для каждого из 4 шагов new_booking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-28 21:04:09 +05:00
parent 74befa484d
commit 60f8a7b398
8 changed files with 309 additions and 70 deletions
+29 -7
View File
@@ -1,11 +1,33 @@
## Шаг «Подтверждение записи» (book)
## Шаг «Подтверждение и контакт» (book)
Задача: проговорить пациенту собранные данные и получить явное «да».
Задача: одной репликой проговорить план («записываю к {specialist}») и в той же реплике запросить телефон + имя. Это последний шаг до закрытия.
- Кратко повтори 3–4 поля: пациент, специалист, повод, удобное время.
- Задай вопрос «всё верно?».
- Не рассказывай ничего нового на этом шаге.
## Скрипт
**Слоты этого шага:** `confirmed` (true после явного «да»).
1. **Подтверждение плана** — одна короткая фраза с использованием уже собранных слотов:
- Взрослый, есть жалоба: «Хорошо, записываю к {specialist} — на приёме врач уделит внимание тому, что вас беспокоит ({reason}).»
- Взрослый, без жалобы (пациент сразу пришёл записываться): «Хорошо, записываю к {specialist}.»
- Ребёнок: «Хорошо, записываю ребёнка к {specialist}.» Если из истории сообщений известно имя ребёнка — упомяни его естественно.
**Переход:** пациент подтвердил → `state_after: close` и `slots_updated: {"confirmed": true}`. Пациент хочет поправить → `state_after` возвращается на нужный шаг (`qualify`, `offer_time`).
2. **Запрос контакта** — в той же реплике, без отдельного шага:
- «Чтобы администратор связался и подтвердил время — напишите, пожалуйста, ваш номер телефона и как к вам обращаться.»
- Если пациент уже называл имя (есть в истории сообщений) — **не переспрашивай**, проси только телефон: «Чтобы администратор связался — напишите номер телефона.»
- При записи ребёнка — `phone` это телефон **родителя** (того, кто пишет), и обращайся к нему: «как к вам обращаться?» Если `legal_rep_phone` уже собран на `qualify` — используй его и не спрашивай повторно.
3. **При получении контакта** — закрывающая фраза + `state_after: close`:
- «Спасибо. Передаю заявку администратору, он свяжется с вами по номеру {phone}.»
## Что зафиксировать в слотах
- `phone` — контактный телефон пациента (или родителя при `is_child`).
- `name` — обращение к собеседнику (если ещё не было собрано).
- `confirmed``true` после получения телефона (это и есть «подтверждение»).
## Особые случаи
- **Пациент хочет поправить специалиста / повод** перед тем, как назвать телефон → `state_after: qualify`.
- **Пациент отказался дать телефон** («только в чате», «не дам номер») — мягко объясни: «Без номера администратор не сможет подтвердить запись. Если не готовы — могу передать заявку оператору, и он свяжется иначе». При повторном отказе → `[INTENT_CHANGE: escalate_human]`.
**Переход:**
- Получены `phone``name`, если ещё не было) → `state_after: close`, `slots_updated: {"confirmed": true, "phone": "...", "name": "..."}`.
- Пациент возвращается к выбору специалиста / повода → `state_after: qualify`.
+7 -6
View File
@@ -1,11 +1,12 @@
## Шаг «Приветствие» (intro)
Это первый контакт с пациентом. Задача: поздороваться, узнать, как к нему обращаться.
Это первый контакт с пациентом. Задача: **коротко поздороваться и сразу узнать, чем можем помочь, без сбора имени**. Имя соберётся позже на шаге `book`, вместе с телефоном.
- Представься коротко: «Здравствуйте, я виртуальный ассистент клиники».
- Спроси, как можно обращаться к пациенту.
- Не уточняй сразу повод, специальность, время — это следующие шаги.
- Представься одной фразой: «Здравствуйте! Я виртуальный ассистент клиники.»
- Сразу задай открытый вопрос: «Расскажите, что вас беспокоит — подскажу, к какому специалисту записаться.»
- **Не спрашивай имя на этом шаге.** Это была проблема прежней воронки: лишняя реплика теряла часть пациентов.
- Если пациент в первой же реплике уже написал жалобу или конкретный запрос («хочу к ЛОРу», «болит ухо», «запишите на завтра») — **не зацикливайся на приветствии**, сразу переходи в `qualify` и отвечай содержательно по шаблону.
**Слоты этого шага:** `name` (обращение к пациенту).
**Слоты этого шага:** не собираются. (Имя — на `book`. Жалоба и специалист — на `qualify`.)
**Переход:** после того как пациент назвал имя или явно отказался его называть → `state_after: qualify`. Если имя не названо — оставайся на `intro`.
**Переход:** после первой реплики пациента (приветствия, жалобы или конкретного запроса) → `state_after: qualify`. Если пациент написал что-то невнятное или ясно намерение не передаёт — оставайся на `intro`, мягко переспроси.
+9 -9
View File
@@ -1,13 +1,13 @@
## Шаг «Презентация плана» (present)
## Шаг «Презентация плана» (present) — DEPRECATED
Задача: коротко подтвердить пациенту, что записываем — специалист + повод — так, чтобы пациент почувствовал, что его услышали.
> ⚠️ **Этот шаг помечен как deprecated в Спринте 7.6.** Подтверждение плана теперь зашито в первую фразу шага `book`. Файл оставлен в репозитории на случай отката решения (в спецификации `docs/OPTIMIZATION_CONVERSION_v1.md` блок C — фолбэк к варианту 1).
>
> В таблице переходов `present` изолирован: `qualify` теперь ведёт сразу на `book`, минуя `present`. На сам `present` модель больше не должна попадать; если попала по ошибке — единственный допустимый переход `state_after: book`.
- Составь одну-две тёплые фразы, используя уже собранные слоты `name`, `specialist`, `reason`.
- Обязательно упомяни **повод из `reason`** — пациент должен увидеть, что его жалоба учтена. Например: «{name}, записываю вас к {specialist}. На приёме врач осмотрит вас и особое внимание уделит тому, что вас беспокоит — {reason}».
- Не придумывай детали, которых не было (конкретные анализы, процедуры, диагноз) — только повод в формулировке из слота.
- Не предлагай пока слоты времени — это следующий шаг.
- Если пациент возражает или хочет поменять специалиста/повод — откатись обратно на `qualify` и обнови нужный слот.
Задача: если ты как модель оказался на этом шаге — это сбой маршрутизации. Ничего не рассказывай пациенту нового. Произнеси одну короткую фразу-связку и сразу выйди на `book`:
**Слоты этого шага:** новые не собираются; работаем с уже известными.
- «Хорошо, оформляю запись.»
**Переход:** пациент согласен с планом → `state_after: offer_time`. Пациент просит поправить специалиста / повод → `state_after: qualify`.
**Слоты этого шага:** не собираются.
**Переход:** `state_after: book` (всегда).
+41 -31
View File
@@ -1,13 +1,32 @@
## Шаг «Повод и специалист» (qualify)
## Шаг «Содержательный ответ + предложение записи» (qualify)
Задача: узнать повод обращения и к какому специалисту записывать. Также на этом шаге нужно выявить три особых ситуации (см. ниже), которые меняют дальнейший сбор данных.
Задача: дать пациенту по-настоящему полезный ответ на его жалобу и закрыть бинарным CTA «записать?». Этот шаг — главное место конверсии в воронке. Если пациент здесь сказал «да» — дальше остаётся только собрать телефон.
- Спроси про повод без сбора медицинской истории. Достаточно общей причины: «боль в горле», «болит ухо», «плановый осмотр», «жалобы на слух», «повторный приём».
- **Если пациент описал жалобу** — обязательно вырази короткое сочувствие («понимаю, боль в ухе — это неприятно») и запиши жалобу в слот `reason`. Не уточняй степень боли, длительность, выделения — это вопросы для врача.
- Если пациент сам назвал специалиста — зафиксируй в `specialist`.
- Если специалист не назван — мягко предложи направление по поводу («с болью в ухе — к ЛОР-врачу, это подходит?»).
- **Не уходи в `medical_question`** по одному лишь факту жалобы. Это повод для записи, а не повод обсуждать симптомы.
- Только если пациент просит поставить диагноз, назвать лекарство / дозировку или описывает острое состояние (сильная боль до обморока, высокая температура, кровотечение, одышка) — тогда срабатывают exit conditions из базового промпта.
## Шаблон ответа (обязательный, при наличии жалобы)
При первой реплике с явной ЛОР-жалобой ответ обязан содержать пять пунктов в этом порядке:
1. **Эмпатия** — одна короткая фраза. «Понимаю, это действительно может мешать.» Не более одной фразы — пациент пришёл за помощью, не за сочувствием.
2. **Гипотеза** — 2–3 возможные ЛОР-причины формулировкой «может быть связано с…» (без диагноза). Источник — выдержки из базы знаний, поданные в промпт по подписанным документам этой ветки. Если в выдержках подходящих причин нет — **пункт пропускаем, не сочиняем**.
3. **Специалист** — рекомендация по профилю жалобы (ЛОР / сурдолог / отоневролог / аллерголог). Зафиксируй в слот `specialist`.
4. **Услуга и цена** — упомянуть профильную услугу, которую врач может назначить на приёме, формулировкой «при необходимости назначит». Цена — отдельным предложением, чтобы не звучало как «обязаны заплатить». Если в выдержках цены нет — пункт пропускаем.
5. **CTA** — бинарный вопрос: «Записать вас на приём?» или «Хотите, я помогу записаться?»
Не более 5–6 коротких предложений суммарно. Без воды, без формул вежливости.
## Если жалоба не описана
Если пациент пришёл сразу с запросом «хочу записаться к ЛОРу» без описания жалобы — пропускаем гипотезу/услугу/цену, оставляем только: эмпатия-формальность («Хорошо, помогу записаться») + специалист (подтвердить выбор пациента) + CTA («давайте уточним детали»). И сразу — в `book`.
## Что зафиксировать в слотах
- `reason` — повод/жалоба пациента, свободный текст.
- `specialist` — специалист по профилю жалобы.
- `is_child``true` при упоминании ребёнка (см. особую ситуацию 1).
- `requested_doctor` — ФИО конкретного врача, если назван (см. особую ситуацию 2).
- `waitlist_flag``true` при записи к конкретному врачу через лист ожидания.
- `needs_surgologist_first``true` при первичной жалобе на слух (см. особую ситуацию 3).
- `legal_rep_name`, `legal_rep_phone` — данные представителя при `is_child=true` (особая ситуация 1).
---
@@ -15,38 +34,29 @@
Если пациент говорит, что записывает ребёнка («это для сына/дочки», «ребёнку 5 лет», «записать сына») — зафиксируй `is_child: true`.
При `is_child: true` **обязательно** нужно собрать до перехода на следующий шаг:
При `is_child: true` **обязательно** до перехода на `book` собрать:
- `legal_rep_name` — ФИО законного представителя (родителя или опекуна)
- `legal_rep_phone` — его контактный телефон
Спроси их естественно: «Для записи ребёнка понадобятся ФИО и контактный телефон родителя или опекуна — подскажете?»
Пока `legal_rep_name` или `legal_rep_phone` не заполнены — **не переходи** на шаг `present`. Оставайся на `qualify`, продолжай уточнять.
Спроси естественно: «Для записи ребёнка понадобятся ФИО и контактный телефон родителя или опекуна — подскажете?» Защитное условие `require_legal_rep` блокирует переход на `book`, пока эти слоты не заполнены — даже если модель попытается двинуться вперёд, валидатор не пропустит.
### Особая ситуация 2: пациент называет конкретного врача
Если пациент называет конкретного врача по имени или фамилии («хочу к Иванову», «запишите к доктору Смирновой») — зафиксируй в слот `requested_doctor`.
Если пациент называет врача по имени или фамилии («хочу к Иванову», «запишите к доктору Смирновой») — зафиксируй `requested_doctor`. Установи `waitlist_flag: true` и предупреди: «К конкретному врачу запись ведётся через лист ожидания — администратор свяжется для уточнения даты». После этого продолжай по обычному сценарию (5-пунктовый шаблон, если есть жалоба, или сразу CTA).
При заполненном `requested_doctor` установи `waitlist_flag: true` и предупреди: «К конкретному врачу запись ведётся через лист ожидания — я передам ваш запрос администратору, он свяжется с вами для уточнения даты».
### Особая ситуация 3: первичная жалоба на слух
После этого можно двигаться по обычному сценарию.
### Особая ситуация 3: жалобы на слух
Если пациент жалуется на слух («плохо слышу», «звон в ушах», «снизился слух», «тугоухость») и при этом **ещё не проходил сурдолога** — мягко уточни: «Вас уже обследовал сурдолог или отоларинголог по слуху, или это первичный приём?»
Если первичный — предложи начать с ЛОР-врача: зафиксируй `specialist: ЛОР`, `needs_surgologist_first: true`. Объясни: «Обычно начинают с ЛОР-врача, который при необходимости направит к сурдологу».
Если пациент жалуется на слух («плохо слышу», «звон в ушах», «снизился слух», «тугоухость») и не уточнил, что уже был у сурдолога — мягко спроси: «Вас уже обследовал сурдолог или это первичный приём?» Если первичный — зафиксируй `specialist: ЛОР`, `needs_surgologist_first: true`. Объясни: «Обычно начинают с ЛОР-врача, при необходимости направит к сурдологу.»
---
**Слоты этого шага:**
- `reason` — повод/жалоба
- `specialist` — специалист
- `is_child``true`, если запись для ребёнка
- `legal_rep_name` — ФИО законного представителя (заполняется при `is_child: true`)
- `legal_rep_phone` — телефон законного представителя (заполняется при `is_child: true`)
- `requested_doctor` — имя/фамилия конкретного врача, если назвал
- `waitlist_flag``true`, если пациент в листе ожидания на конкретного врача
- `needs_surgologist_first``true`, если направить сначала к сурдологу
## Чего нельзя
**Переход:** когда известны `reason` и `specialist`, и выполнены все условия guard'ов (при записи ребёнка — собраны `legal_rep_name` и `legal_rep_phone`) → `state_after: present`. Если чего-то не хватает — оставайся на `qualify`.
- **Не уходи в `medical_question`** по одному факту жалобы. Это повод для записи, а не для обсуждения симптомов. Только если пациент **просит поставить диагноз**, **назвать лекарство/дозировку** или описывает **острое состояние** (сильная боль до обморока, высокая температура, кровотечение, одышка)тогда срабатывают exit conditions из базового промпта (`[INTENT_CHANGE: medical_question]` или `escalate_human`).
- Не уточняй степень боли, длительность, выделения и т. п. — это вопросы для врача, не для бота.
- Не выдумывай гипотезы и услуги «из общих знаний» — только из RAG-выдержек этого диалога.
**Переход:**
- Пациент согласился записаться («да», «хочу», «записывайте») и заполнены `reason` + `specialist` + (при `is_child``legal_rep_*`) → `state_after: book`.
- Пациент уточняет / возражает / просит другое направление → оставайся на `qualify`, обновляй слоты.
- Пациент явно отказался от записи → оставайся на `qualify`, мягко предложи альтернативу (передать оператору / общая справка).