feat(sprint7.5): обновление промптов 4 веток + eval-каркас и тест-кейсы в UI Настроек

Промпты веток (по docs/BRANCH_MAP_AND_PROMPTS_v1.md):
- reschedule.md — полная замена. Одношаговый сценарий из 6 пунктов:
  action (cancel/reschedule), patient_name, patient_phone, original_time,
  preferred_new_time. Слоты хранит вызывающая система, STATE_JSON не используется.
- price_question.md — добавлены 3 пункта: эндоскопия 1000₽ при первичном
  ЛОР-приёме, лечебные процедуры доплачиваются, ОМС только сурдолог
  (последний пункт работает только при подтверждении в базе).
- medical_question.md — расширена карта жалоб → специалист (ЛОР / сурдолог /
  аллерголог / иммунолог / пульмонолог); добавлен пункт про беременность,
  онкологию, психиатрию — мягко сказать «специализированная клиника»,
  не предлагать запись.
- general_info.md — добавлены разделы «Отзывы и социальное доказательство»,
  «Преимущества клиники», «Сокращения». Условия выхода расширены до 5 интентов.

escalate_human и new_booking не трогаем (escalate — карта говорит «не менять»;
new_booking — отдельный Спринт 7.6 по docs/OPTIMIZATION_CONVERSION_v1.md).

Применение в БД — вручную через UI «Настройки» (вариант A): оператор копирует
текст из .md, сохраняет как новую версию + активирует. Файлы — только seed.

Eval-каркас (заготовка под Спринт 8):
- eval/router_cases_booking.jsonl (875 кейсов new_booking) и
  eval/router_cases_other.jsonl (698 кейсов: general_info 295, price 165,
  escalate 139, medical 59, reschedule 40). CSV-исходники рядом.
- eval/README.md — формат, глоссарий, что это и зачем.
- routers/eval.py: GET /eval/router-cases?intent_code=...&limit=...
  Lazy-кэш, сортировка по count desc, фильтр по expected_intent.

UI Настроек — выбор готового кейса в тест-блоке:
- Полоса «Готовый кейс:» с datalist (поиск по началу строки) + кнопка
  «🎲 Случайный» + счётчик кейсов для активной ветки.
- При выборе — текст подставляется в textarea вопроса.
- Загружается при выборе ветки. Если кейсов 0 (для _router, _debug) — скрыто.
- Полная подсистема прогона (run.py, отчёты, baseline) — Спринт 8.

SPRINTS.md:
- Спринт 7 (мульти-RAG, часть A) →  Закрыт (коммит 52b46bc).
- Заведён Спринт 7.5 «Обновление промптов 4 веток» (этот спринт).
- Заведён Спринт 7.6 «Оптимизация воронки new_booking до 4 шагов»
  по OPTIMIZATION_CONVERSION_v1.md.
- В идеи на потом: сквозные правила всех веток (BRANCH_MAP §2),
  отложенная документация Спринта 7 (docs.html карточка термина,
  GRAPH_ARCHITECTURE_v5, README про мульти-RAG).

Также: docs/COMPETITOR_ALEXANDRA_top100.md — рабочие материалы пользователя
по конкурентному боту (NEXTBOT/Александра), используется как baseline для
оптимизации воронки в Спринте 7.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-28 20:49:02 +05:00
parent 52b46bc53e
commit 74befa484d
14 changed files with 3939 additions and 64 deletions
+108
View File
@@ -0,0 +1,108 @@
# Разбор конкурента «Александра» — первые 100 диалогов из 2691
**Источник:** `dialogs-export-2026-04-27-09-45-07.xlsx` (clinic-lor@mail.ru, ЛОР-клиника, Пермь, 2 филиала).
**Дата экспорта:** 27.04.2026. **Дата анализа:** 27.04.2026.
## Метрики конкурента (по сводке файла)
- 2691 диалог за период.
- 6.1 сообщений на диалог в среднем.
- Каналы: Widget 87%, VK 13%, Max/Telegram единицы.
- 684 диалога с тегом `has_tool_calls` = ~25% общая конверсия в передачу заявки администратору. **В первых 100 — 32%** (часть из них — повторные обращения тех же клиентов).
- Расход $ после рестарта = 0 (т.е. они на Botcoin/локальной модели).
- Средний расход 2.76 Botcoin/диалог.
## Архитектура их сценария (восстановлена по стенограммам)
Воронка из **3 шагов**, не 4:
1. **Triage** — бот сразу собирает причину обращения, не настаивая на имени, без отдельного intro.
2. **Pitch** — называет цену первичного приёма (от 2500 ₽), упоминает что эндоскопия +1000 ₽ оплачивается отдельно, предлагает записать.
3. **Capture** — просит **только телефон**. Имя — опционально, бот достаёт его из реплик сам. Дальше `save_user_data` и шаблонная плашка прощания.
**Шаблон закрытия после tool_call** (одинаковый везде, помечен `[Шаблон]`):
> «Я отправила ваши данные администратору. Ожидайте звонка. Мне было приятно с вами общаться. Хорошего дня и спасибо за выбор нашей клиники!»
**Структура tool_call** (полезно подсмотреть для нашего):
```json
{
"phone": 89922070601,
"source": "Сайт",
"context": "Пациентка переболела, был сильный кашель, сейчас дискомфорт при глотании",
"service": "Прием ЛОР-врача"
}
```
Контекст и услугу LLM формирует сам из истории — это даёт администратору готовое описание лида без необходимости перечитывать диалог.
**«Догоняющее» сообщение** при молчании клиента: системный триггер пишет ассистенту «Клиент не ответил отправь ему еще одно сообщение: "У вас остались еще какие-то вопросы?"», ассистент пересылает. Несколько раз в выборке именно это возвращало клиента в воронку и приводило к захвату телефона.
## Темы пациентов (распределение по 100 диалогам)
В порядке убывания частотности:
1. Запись по симптому (болит ухо, насморк, заложенность, выделения, шум, головокружение) — около половины обращений.
2. Запись к конкретному врачу по фамилии — ~15%.
3. Стоимость услуги/процедуры — ~10%.
4. Справка для налогового вычета — повторяется 8+ раз в первой сотне с почти идентичным длинным ответом. Кандидат на отдельный intent с готовым шаблоном.
5. Дети (с рождения, аденоиды, отиты, шунты) — ~10%.
6. Перенос/отмена записи — ~5%.
7. Доверенность для бабушки сопровождать ребёнка — есть готовый текстовый шаблон от руки.
8. График работы / праздники, адреса, ОМС/ДМС, рассрочка, оплата по QR — короткие FAQ.
9. Отсев по географии (Казахстан, Тула, Калуга, Златоуст, Кизел) — клиника только в Перми.
## Что у них работает (можно перенять)
- **Минимум полей в lead-форме:** только phone. Имя/контекст бот собирает сам. Снимает фрикцию.
- **Контекст и услуга в JSON** автоматически — экономит время администратора.
- **Шаблонные ответы для типовых FAQ** (налоговая справка, доверенность) выдаются мгновенно и без LLM-творчества — стабильно и дёшево.
- **Догоняющее сообщение** через системный триггер при молчании.
- **Конкретика в питче:** цена + регалии врача (ФИО, к.м.н., стаж 32 года) повышает доверие и часто прямо в этом сообщении вытягивает телефон.
- **Тёплый эмоциональный регистр**: называют по имени, «Берегите себя», смайлики, «желаю здоровья» — но без перебора.
## Где они проседают (наши потенциальные преимущества)
1. **Нет интеграции с расписанием врачей.** Любой вопрос «когда есть свободное время к доктору X?» переадресуется администратору. Если у нас будет онлайн-расписание, мы можем закрывать запись прямо в чате — это огромный отрыв.
2. **Не интегрированы с CRM записей.** Пациент пишет «я записан на завтра, во сколько?» — бот предлагает оставить телефон и ждать звонка. Унизительно для уже-клиента.
3. **Галлюцинации на простых вопросах.** Диалог #49: сначала «стационар не предусмотрен», через 2 реплики «стационар есть, можно остаться». Противоречие в одном диалоге.
4. **Слабая фильтрация по гео.** Длинный ответ про услуги выдаётся пациенту из Казахстана/Челябинска до того, как выясняется что приехать не сможет. Минута впустую и негатив.
5. **Не различает экстренные ситуации.** Диалог #21: ребёнок 1.5 месяца, ночь, боль в ухе. Бот трижды предлагал «запишитесь на приём». Скорую упомянул только после третьей реплики «На улице ночь». Для нашего бота это потенциальный safety-incident, надо сразу: «вызывайте 03/112».
6. **Fuzzy-matching фамилий слабый.** «Варанчихина» сначала не нашёл, со второй попытки нашёл «Ворончихину». Диалог #34. Простой word-distance fix.
7. **Лишний шаг intro.** «Здравствуйте» → «Как я могу к вам обращаться?» → имя → «Чем могу помочь?». На каждом шаге часть пациентов отваливается. У нас по `OPTIMIZATION_CONVERSION_v1.md` уже сокращено до 4 шагов — стоит проверить, можем ли убрать ещё intro.
8. **Защитная реакция на негатив:** «Вы с такими ценами не ахренели?» → бот оправдывается «качество, оборудование». Не лучшая стратегия. Можно тестировать варианты с эмпатией + альтернативой («понимаю, цены немалые. Если бюджет ограничен, могу подсказать, на каких процедурах можно сэкономить?»).
9. **Дают развёрнутые медицинские рекомендации дома** (диалог #57: 5 пунктов как лечить заложенность уха). Юридически рискованно для частной клиники. Нам нужно решить осознанно: даём советы или жёстко возвращаем к врачу.
10. **Не блокируют запросы про конкурирующие города.** Пациент спросил «филиал в Туле?» — бот ответил, но мог бы сразу прислать ссылку на запись/адрес главной клиники.
## Корпус реальных фраз пациентов (для evals/regression-тестов)
Готовые строки для генератора тест-кейсов нашего бота:
- «Болит ухо» / «Заложило ухо после ОРВИ» / «Из уха течёт жёлтая жидкость без запаха»
- «Жёлтые выделения из носа» / «Не проходит насморк» / «Опухла левая щека и из ноздри жёлтый гной»
- «Очень сильно храплю, иногда закладывает уши»
- «Записаться к лору» / «Запишите на повторный приём» / «Можно записаться к {фамилия}?»
- «Сколько стоит первичный приём Лора?» / «Цена септопластики» / «Стоимость продувания слуховых труб»
- «Принимаете по ОМС?» / «По ДМС работаете?» / «Даёте рассрочку?»
- «Справка для налогового вычета за 2024-2025»
- «Доверенность на бабушку» / «Можно ребёнку 15 лет прийти без родителей?»
- «Когда ближайшая запись к {фамилия}?» / «У вас есть свободное время сегодня?»
- «Я записан на завтра, во сколько?» / «Можно отменить приём?» / «Перенесите мою запись»
- «Принимаете детей 2 лет?» / «Чем закапать ухо ребёнку?» (опасный сценарий)
- «У меня рыбная косточка в гортани» (срочный)
- «Шум в ушах что делать» / «Сильное головокружение, лежала в неврологии не помогает»
- «А где вы находитесь?» / «Вы клиника возле Колизея?»
- «Вы с такими ценами не ахренели?» (негативный пользователь)
## Идеи для нашего проекта
- **Готовый шаблон ответа про налоговую справку** — копируется без LLM, экономит токены. Аналогично для доверенности и графика работы.
- **Гео-фильтр на ранних шагах:** если в первых 2 сообщениях звучит другой город — короткий эмпатичный ответ, без длинного питча.
- **Safety-классификатор на острые состояния** (ребёнок до года + симптом, инородное тело, кровотечение, ночь+боль) → сначала «вызовите 03/112», потом запись.
- **Fuzzy-matching фамилий врачей** (Левенштейн или эмбеддинги) для исправления ошибок ввода.
- **Догоняющий триггер при молчании** через 5 минут — у Александры явно работает.
- **Поле `context` в lead-payload** генерируется LLM из истории — операторам очень удобно. Стоит включить в наш `save_user_data`, если ещё нет.
## Технические наблюдения
- VK-канал у них работает в основном через лид-форму, а не диалог: 30+ записей вида «Заявка из Vk → Имя/Телефон». Похоже, VK интегрирован отдельно от LLM-сценария.
- 169 сообщений оператора на 2691 диалог = ~6% диалогов потребовали ручного вмешательства. Хороший таргет для нас.
- 28 млн токенов суммарно (входящих 27.6 млн, исходящих 481 тыс). Соотношение 57:1 говорит о большом системном промпте + контексте — типично для RAG. Наш `OPTIMIZATION_CONVERSION_v1.md` как раз про сжатие этого.
+101 -33
View File
@@ -426,38 +426,101 @@
**Подход** — A (M:N через document_id, не префиксы путей и не теги). Причины: `vectorstore.query()` уже умеет фильтровать по `document_ids` (нечего переписывать); нулевая миграция Chroma; на текущем масштабе (~30 документов, 6 веток) ручная подписка — 3-минутная задача один раз при загрузке; дисциплина именования путей — слабое место в проектах с >1 оператором, а галочки понятны без инструкции.
### Статус: Запланирован
### Статус: Закрыт (коммит `52b46bc`, 2026-04-27)
### Задачи
**Бэкенд:**
- [ ] Миграция Alembic: новая таблица `intent_documents` с полями `intent_id` (FK на `intents.id`), `document_id` (varchar 36, тип как в metadata Chroma), `created_at`. PK составной (`intent_id`, `document_id`). Индекс по `document_id` для обратного поиска.
- [ ] Модель `db/models/intent_document.py` (`IntentDocument`).
- [ ] Сервис `services/intent_document_service.py` — функции `list_documents_for_intent(intent_code)`, `list_intents_for_document(document_id)`, `set_documents_for_intent(intent_code, document_ids)`, `set_intents_for_document(document_id, intent_codes)`.
- [ ] API:
- `GET /intents/{code}/documents` — список `document_id`, привязанных к ветке.
- `PUT /intents/{code}/documents` — перезаписать список (body: `{ "document_ids": [...] }`).
- `GET /documents/{id}/intents` — список кодов веток конкретного документа.
- `PUT /documents/{id}/intents` — перезаписать список (body: `{ "intent_codes": [...] }`).
- [ ] Retrieval-фильтр в `services/chat_service.py`: перед `vectorstore.query()` подтянуть список `document_id` для активной ветки. Передать как `document_ids=...`. **Дефолт пустой подписки — `document_ids=[]` (= 0 чанков), не «вся коллекция»**: пустая подписка означает «ветка не настроена», подмешивать случайное хуже, чем не подмешивать ничего.
- [x] Миграция Alembic `i5c8b3a45f12_add_intent_documents`: новая таблица `intent_documents` с полями `intent_id` (FK на `intents.id`), `document_id` (varchar 36, тип как в metadata Chroma), `created_at`. PK составной, индекс по `document_id`.
- [x] Модель `db/models/intent_document.py` (`IntentDocument`) с каскадом удаления.
- [x] Сервис `services/intent_document_service.py` — функции `list_documents_for_intent_code`, `list_intents_for_document`, `set_documents_for_intent`, `set_intents_for_document`.
- [x] API: `GET/PUT /intents/{code}/documents` и `GET/PUT /documents/{id}/intents` с PUT-семантикой «полный список», атомарно.
- [x] Retrieval-фильтр в `services/chat_service.py` + `vectorstore.query()` различает `None` (нет фильтра, вся коллекция) и `[]` (пустая подписка, 0 чанков). Дефолт для пациентских веток — `[]`. Для `_debug``None` (отладка работает из коробки).
**UI:**
- [ ] «Настройки» → страница ветки: новый блок «Документы базы знаний» — список всех загруженных документов с галочками, заголовок «подписано N из M», кнопка «Сохранить подписки».
- [ ] «Отладка» → рядом с каждым документом (или в разворачиваемой панели) — компактный список веток с галочками, чтобы быстро подписать прямо на месте загрузки.
- [ ] «Отладка» → кнопка «редактировать» рядом с «привязка»/«удалить»: разворачивает большой `<textarea>` с извлечённым `raw_text` документа. Кнопка «Сохранить и переиндексировать» делает `PUT /documents/{id}/raw` (обновляет `documents.raw_text` + переразметка + замена чанков в Chroma). С confirm перед сохранением. Подпись: правится извлечённый текст, для PDF/docx исходник теряется.
- [ ] Системный промпт страницы «Отладка» переехал в обычную ветку `_debug` («Страница отладки»). Удалён `prompts/system_prompt.md` и логика `DEFAULT_SYSTEM_PROMPT` в `services/llm_client.py`. `routers/query.py` подтягивает активный конфиг ветки `_debug` (через `config_service`) и её подписки на документы (через `intent_document_service`). Дефолт пустой подписки в `_debug` — вся коллекция, чтобы Отладка работала «из коробки» (для пациентских веток дефолт другой — 0 чанков). На странице Отладки info-bar показывает активную версию и счётчик подписок, ссылка Настройки. В `QueryResponse` добавлены `intent_code`, `config_version`, `rag_subscription`.
- [ ] Песочница, отладочная панель: новый блок «Срез RAG: подписано N из M документов для ветки `<код>`». В «Найденных фрагментах» в каждой карточке — лейбл с `document_name`. Если подписка пуста и retrieval вернул 0 чанков — явная пометка «у ветки нет подписок, RAG-контекст пустой».
- [x] «Настройки» → блок «Документы базы знаний» в правом сайдбаре, всегда видим (независимо от вкладки), сортировка по имени, счётчик «N из M».
- [x] «Отладка» → кнопка «привязка» рядом с «удалить» → раскрывашка со списком веток, быстрая привязка прямо на месте.
- [x] «Отладка» → кнопка «редактировать» большой textarea с raw_text, `PUT /documents/{id}/raw` обновляет текст и переиндексирует в Chroma. С confirm.
- [x] Системный промпт страницы «Отладка» переехал в ветку `_debug`. Удалён `prompts/system_prompt.md` и `DEFAULT_SYSTEM_PROMPT` в `llm_client.py`. info-bar на странице Отладки: версия + подписки + ссылка в Настройки.
- [x] Песочница: блок «Срез RAG», поле `rag_subscription` в `ChatResponse`, ворнинг при пустой подписке.
- [x] Тест-блок «Тест-вопрос от пациента» в центре Настроек (для любой выбранной ветки): textarea черновика → `/query` с `intent_code`, `system_prompt` (override), `disable_rag` для `_router`. Промпт-секция в `<details open>`, можно свернуть.
**Документация:**
- [ ] `static/docs.html` — карточка термина «Подписка ветки на документы», упоминание в разделе «Что происходит на каждой реплике».
- [ ] `docs/architecture/GRAPH_ARCHITECTURE_v5.md` — переписать §6 под подход A (M:N через `document_id`, без путей и без тегов). На v4 — шапка «устарело». Changelog v4→v5.
- [ ] `README.md` — раздел про мульти-RAG.
- [ ] (отложено в идеи на потом) `static/docs.html` — карточка термина «Подписка ветки на документы».
- [ ] (отложено в идеи на потом) `docs/architecture/GRAPH_ARCHITECTURE_v5.md` — переписать §6 под подход A.
- [ ] (отложено в идеи на потом) `README.md` — раздел про мульти-RAG.
### Критерий готовности
- [ ] Документ, привязанный к `price_question`, появляется в retrieval только когда активна именно эта ветка. При переключении на `new_booking` — те же запросы возвращают другие чанки.
- [ ] Ветка без подписок (например, свежесозданная) получает в retrieval 0 чанков — модель отвечает по промпту без RAG-контекста.
- [ ] В Песочнице видно «подписано N из M», в найденных фрагментах — название документа.
- [ ] Подписка работает в обе стороны UI: можно настроить и со страницы ветки (Настройки), и со страницы документа (Отладка).
- [x] Документ, привязанный к `price_question`, появляется в retrieval только когда активна именно эта ветка.
- [x] Ветка без подписок получает в retrieval 0 чанков (для пациентских) или вся коллекция (для `_debug`).
- [x] В Песочнице видно «подписано N из M», в найденных фрагментах — название документа.
- [x] Подписка работает в обе стороны UI: и со страницы ветки (Настройки), и со страницы документа (Отладка).
---
## Спринт 7.5. Обновление промптов 4 веток (без `new_booking`)
### Цель
Применить предложения из `docs/BRANCH_MAP_AND_PROMPTS_v1.md` к четырём веткам — `reschedule`, `price_question`, `medical_question`, `general_info`. Промпты заменяются на новые более развёрнутые тексты по карте. Все 4 ветки остаются **одношаговыми** (без state machine, без слотов) — карта явно говорит, что только `new_booking` пошаговая. `escalate_human` и `_router` не трогаем.
### Статус: ⏳ Запланирован
### Задачи
**Тексты промптов (правка `prompts/intents/{code}.md`):**
- [ ] `reschedule.md` — полная замена на сценарий из 6 пунктов (BRANCH_MAP §5): сначала `action` (cancel/reschedule), потом ФИО + телефон + старое время + желаемый интервал. Условия выхода: `new_booking` / `escalate_human` / `price_question`.
- [ ] `price_question.md` — добавить 3 пункта (`+++`, BRANCH_MAP §6): про эндоскопию 1000₽ при первичном ЛОР-приёме, про лечебные процедуры (доплата), про ОМС только сурдолог.
- [ ] `medical_question.md` — добавить 1 пункт (`+++`, BRANCH_MAP §7): беременность / онкология / психиатрия → специализированная клиника, передать администратору, не предлагать запись.
- [ ] `general_info.md` — добавить 3 раздела (BRANCH_MAP §8): «Отзывы и социальное доказательство», «Преимущества клиники», «Сокращения».
**Применение в БД (вручную через UI):**
- [ ] Оператор в Настройках для каждой из 4 веток: загрузить текст из обновлённого `.md` в textarea «Системный промпт ветки» → «Сохранить как новую версию» с галочкой «Сразу сделать активной».
- [ ] Прогнать в тест-блоке «Тест-вопрос» по 1-2 кейса на каждую ветку, чтобы убедиться, что новый промпт работает с подписанными документами.
**Не делаем в этом спринте:**
- `escalate_human` — карта явно говорит «рабочий и хороший, не менять».
- `new_booking` — отдельный Спринт 7.6.
- Сквозные правила (BRANCH_MAP §2) — в идеи на потом.
- Поле `description` в `SEED_INTENTS` — текущие описания лучше карточных, не меняем.
### Критерий готовности
- [ ] 4 файла `prompts/intents/*.md` обновлены и закоммичены.
- [ ] В БД для каждой из 4 веток есть свежая активная версия с обновлённым текстом.
- [ ] Тест-блок в Настройках для каждой из 4 веток отвечает корректно на 1-2 кейса.
---
## Спринт 7.6. Оптимизация воронки `new_booking` до 4 шагов
### Цель
Сжать воронку `new_booking` с 6 шагов (`intro → qualify → present → offer_time → book → close`) до 4 (`intro → qualify → book → close`), переписать содержимое `qualify` под 5-пунктовый шаблон ответа (эмпатия → 2-3 ЛОР-причины → специалист → услуга/цена → CTA), перенести сбор имени с `intro` на `book`. Полная спецификация — в `docs/OPTIMIZATION_CONVERSION_v1.md`.
### Статус: ⏳ Запланирован
### Задачи
См. полный план в `docs/OPTIMIZATION_CONVERSION_v1.md`. Кратко:
**Блок A — сжатие воронки:**
- [ ] `intro.md` — приветствие + открытый вопрос, имя НЕ спрашиваем.
- [ ] `book.md` — телефон + имя в одной реплике.
- [ ] Расширить `allowed_next` шага `intro`.
**Блок B — содержательный `qualify`:**
- [ ] `qualify.md` — обязательный 5-пунктовый шаблон (эмпатия → гипотеза → специалист → услуга/цена → CTA).
- [ ] Сохранить три «особые ситуации» (ребёнок, конкретный врач, жалобы на слух).
**Блок C — `present`:**
- [ ] Решить (с пользователем): убрать как самостоятельный шаг или переписать в одну фразу-подтверждение. Спецификация рекомендует вариант 2 (убрать).
**Блок D — регрессия:**
- [ ] 5 контрольных конверсионных кейсов (храп, боль в горле, тугоухость, насморк, звон в ушах) в `eval/MANUAL_CASES.md`.
- [ ] Прогнать 8 ручных сценариев из блока H Спринта 6b — все должны проходить.
### Критерий готовности
- [ ] На контрольном кейсе из спецификации `new_booking` отвечает по 5-пунктовому шаблону, до запроса телефона ≤ 3 реплик бота.
- [ ] Все 8 ручных сценариев из блока H Спринта 6b проходят.
- [ ] Промпты `intro.md`, `qualify.md`, `book.md` обновлены и активированы в БД.
---
@@ -471,17 +534,20 @@
### Задачи
**Eval-наборы (отдельные файлы в репозитории, без БД):**
- [ ] `eval/router_cases.csv` — 20–30 фраз на каждую из 6 веток: типичные, пограничные (ловушечные), злые (короткие, эмоциональные, с опечатками). Колонки: `text, expected_intent, note`.
- [ ] `eval/handoff_cases.yaml` — 5–10 многошаговых мини-диалогов: реплики пациента по порядку + ожидаемый intent на каждую.
- [ ] `eval/resumable_cases.yaml` — 3–5 сценариев detour-и-возврат: реплики + ожидаемые `current_intent`, `current_step`, ключевые слоты на каждом шаге.
- [ ] `eval/loop_cases.yaml` — 1–2 сценария искусственной петли с проверкой `reason=routing_loop`.
- [ ] `eval/guard_cases.yaml`сценарии на guards (ребёнок, waitlist).
- [ ] `eval/rag_cases.yaml` — сценарии на мульти-RAG: реплика внутри ветки → проверка, что retrieved-чанки из правильного раздела вики.
Все наборы в **JSONL** (одна строка = один кейс). Унифицированный формат, единый парсер. Схема описана в `eval/README.md`. Историческое замечание: в первой версии плана одношаговые кейсы были в CSV, многошаговые в YAML — отказались от зоопарка форматов в пользу одного JSONL.
- [x] `eval/router_cases_booking.jsonl` + `eval/router_cases_other.jsonl` — одношаговые кейсы маршрутизатора (875 + 698, собраны из реальных диалогов конкурента, см. `eval/README.md`). Схема: `{text, expected_intent, expected_reason?, count, note?}`. CSV-версии сохранены рядом для совместимости.
- [ ] `eval/handoff_cases.jsonl`5–10 многошаговых мини-диалогов: реплики пациента по порядку + ожидаемая активная ветка / решение маршрутизатора / приостановленная ветка / счётчик переключений на каждом шаге.
- [ ] `eval/resumable_cases.jsonl` 35 сценариев detour-и-возврат: реплики + ожидаемые `current_intent`, `current_step`, ключевые слоты на каждом шаге.
- [ ] `eval/loop_cases.jsonl` — 1–2 сценария искусственной петли с проверкой `reason=routing_loop`.
- [ ] `eval/guard_cases.jsonl` — сценарии на защитные условия (ребёнок, waitlist).
- [ ] `eval/rag_cases.jsonl` — сценарии на мульти-RAG: реплика внутри ветки → проверка, что в retrieved-чанках есть фразы из ожидаемого документа (или ожидаемые `document_id`).
**Запускалка (CLI, не часть сервиса):**
- [ ] `eval/run.py` — читает наборы, прогоняет через живой сервис. Режимы:
- `router` — прямой вызов `RouterClient.classify()` на фразах из CSV (быстро).
- `dialog` — полный `/chat` на чистых тредах, сверка intent + step + slots + handoff_count + reason + источники.
- [ ] `eval/run.py` — читает JSONL-наборы, прогоняет через живой сервис. Режимы:
- `router` — прямой вызов `RouterClient.classify()` на одношаговых кейсах (быстро).
- `dialog` — полный `/chat` на чистых тредах, сверка по каждому шагу: активная ветка + решение маршрутизатора + текущий шаг + слоты + счётчик переключений + причина эскалации + retrieved-источники.
- [ ] Вывод: per-ветка accuracy, confusion matrix, список расхождений с текстом реплики.
- [ ] Отчёт: stdout + `eval/reports/{timestamp}.md` (добавлять в git для сравнения во времени).
@@ -490,8 +556,8 @@
- [ ] Договорённость: перед правкой промпта роутера / ветки / `wiki_sources` — прогнать eval, зафиксировать baseline; после — сравнить.
### Критерий готовности
- [ ] `eval/run.py` работает одной командой, полный набор проходит за ≤ 3 минуты.
- [ ] Отчёт покрывает все 8 сценариев из блока H Спринта 6 + базовые kv-тесты роутера + RAG-проверки Спринта 7.
- [ ] `eval/run.py` работает одной командой, режим `router` проходит за ≤ 30 секунд (на `count >= 2`), режим `dialog` за ≤ 3 минуты.
- [ ] Отчёт покрывает все 8 сценариев из блока H Спринта 6 + одношаговые кейсы маршрутизатора + RAG-проверки Спринта 7.
- [ ] Baseline зафиксирован в `eval/reports/{date}_baseline.md` и добавлен в git.
---
@@ -528,6 +594,8 @@
## Бэклог
### Дальнейшие идеи
- **Сквозные правила всех веток** (из `docs/BRANCH_MAP_AND_PROMPTS_v1.md` §2): тон, что нельзя говорить, обработка сокращений, обязательное предупреждение про доп. расходы. Сейчас этого механизма нет — каждая ветка хранит свои `rules_text` отдельно. Завести «глобальный» промпт-префикс (например, поле `Intent.global_prefix_id` или общая запись в `agent_configs` с зарезервированным `intent_code = "_global"`), подмешивать в системный промпт каждой ветки до её собственного. Альтернатива — продолжать копипастить общие правила в `rules_text` каждой ветки, что хуже для поддержки.
- **Документация Спринта 7** — отложено: карточка термина «Подписка ветки на документы» в `static/docs.html`; обновление архитектуры до `GRAPH_ARCHITECTURE_v5.md` (§6 переписать под подход A — M:N через `document_id`); раздел про мульти-RAG в `README.md`. Закроется одним заходом, когда станет понятна часть Б Спринта 7 (внешняя вики).
- **Спринт 7, часть Б: автосинхронизация с внешней вики операторов.** Часть A Спринта 7 — ручная подписка через UI: оператор сам загружает документы и сам ставит галочки. Часть Б — подключение к внешней системе ведения вики (которая «тщательно ведётся операторами»): автоматическое обновление документов, привязка подписок к источникам в той системе, версионирование. Конкретика появится, когда будет известно, что за внешняя система.
- **Per-step `wiki_sources`** (из v4 §3.4): отдельная подписка на уровне шага машины состояний (например, на `book` подмешивать только документы про подготовку к приёму, на `qualify` — про услуги и врачей). Сейчас не нужно — все шаги `new_booking` логически работают с одной и той же базой. Возвращаться, когда увидим, что какой-то шаг подбирает не те чанки.
- **Превью markdown в редакторе документа** (страница «Отладка», кнопка «редактировать»): сейчас в textarea виден сырой markdown с символами `#`, `**`. Добавить split-view (слева исходник, справа отрендеренный markdown через уже подключённые `marked.js` + `DOMPurify` из Песочницы). На узких экранах — вертикальный стек. Альтернативы: вкладки «редактор/превью» (проще, но с переключением) или WYSIWYG (TipTap / EasyMDE — +500 KB и риск кривого экранирования). Рекомендация на момент записи — split-view.