Files
RAG_helper/docs/SPRINTS.md
T
AR 15 M4 74befa484d 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>
2026-04-28 20:49:02 +05:00

619 lines
71 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Спринты — Chat Agent for Patients (инструмент настройки)
Поэтапный план MVP: RAG-ядро + веб-инструмент для настройки агента операторами. Подключение реальных каналов (приложение, МАКС) — вне скоупа, это задача другого разработчика.
---
## Спринт 1. RAG-ядро, загрузка документов и тестовая страница
### Цель
Поднять FastAPI-сервис с ChromaDB и сразу получить воспроизводимый «пайплайн в действии»: на одной тестовой странице видно, какие файлы загружены, можно задать одиночный вопрос от лица пациента и увидеть одновременно три вещи — какие чанки нашёл RAG, какой промпт собрался, какой ответ вернул DeepSeek. Аналог Debug UI из `work-pcs-dr-cdss`.
### Статус: ⏳ Запланирован
### Задачи
**RAG-ядро:**
- [ ] Инициализация проекта (main.py, config.py, requirements.txt, Dockerfile, docker-compose, .env.example)
- [ ] Переиспользовать паттерны из `work-pcs-dr-cdss`: `services/embeddings.py`, `vectorstore.py`, `document_processor.py`, `llm_client.py`
- [ ] Адаптировать чанкер под wiki-статьи (не клинреки)
**Эндпоинты:**
- [ ] `GET /health` — статус, кол-во документов и чанков
- [ ] `POST /documents/upload` — загрузка + превью первых 3 чанков в ответе
- [ ] `GET /documents` — список загруженных
- [ ] `DELETE /documents/{id}` — удаление
- [ ] `POST /query` — одиночный вопрос от лица пациента → ответ + источники со `score` + `assembled_prompt` (как RAG for Doctors, но без полей карты — только текст вопроса)
**Тестовая страница (одна HTML-страница, vanilla JS):**
- [ ] Шапка со статусом сервиса (auto-refresh `/health`, счётчики документов и чанков)
- [ ] Блок «База знаний»: drag & drop загрузка, таблица документов с превью первых чанков, кнопка удаления
- [ ] Блок «Тест-вопрос от пациента»: поле ввода вопроса, поле `top_k`, кнопка «Отправить»
- [ ] 3-колоночный результат ответа: релевантные фрагменты (текст + document, section, page, score) | собранный промпт | ответ LLM
### Критерий готовности
- [ ] Оператор открывает `http://localhost:PORT/` → видит Debug UI со статусом сервиса
- [ ] Загружает wiki-статью → она появляется в таблице, превью чанков отображается
- [ ] Пишет вопрос «как записать ребёнка к лору?» → получает ответ DeepSeek с указанием источников
- [ ] В средней колонке виден собранный промпт, в левой — какие чанки подтянулись со score
- [ ] Может удалить статью, счётчики в шапке обновляются
---
## Спринт 2. Многошаговый диалог с памятью треда
### Цель
Перейти от одиночного `/query` к полноценному диалогу: агент помнит историю, оператор ведёт разговор из 5+ реплик. Текущую страницу отладки (одиночный вопрос) оставляем без изменений, добавляем **вторую отладочную страницу** — «Песочница» со списком всех сохранённых диалогов.
### Статус: ✅ Закрыт
### Задачи
**Хранилище:**
- [ ] Стек: SQLite + SQLAlchemy 2.0 (async, ORM-стиль) + Alembic для миграций
- [ ] Таблицы:
- `threads` (id, name, user_id nullable, agent_config_id nullable, created_at, updated_at)
- `messages` (id, thread_id FK, role, text, sources_json, assembled_prompt, created_at)
- Колонки `user_id` и `agent_config_id` заводим сразу nullable — под будущие Спринты 3+ (мульти-пользователи, мульти-промпты), чтобы не тащить миграции задним числом
- [ ] Первая миграция Alembic с этими двумя таблицами
- [ ] Все диалоги сохраняются навсегда (никакого авто-удаления)
- [ ] Имя треда генерируется автоматически по первой реплике пациента + дата; оператор может переименовать вручную
**Эндпоинты:**
- [ ] `POST /chat` — принимает `thread_id` (или создаёт новый если не передан) + `text` → возвращает ответ агента + источники со score + `assembled_prompt`
- [ ] `GET /threads` — список всех диалогов (id, name, created_at, messages_count, превью первой реплики)
- [ ] `GET /threads/{id}` — тред целиком с историей сообщений
- [ ] `PATCH /threads/{id}` — переименовать тред
- [ ] `DELETE /threads/{id}` — удалить тред со всеми сообщениями
**Сборка ответа:**
- [ ] Базовый системный промпт (хардкод для старта): роль агента, тон клиники, что можно и нельзя
- [ ] Сборка контекста для LLM: системный промпт + история треда + RAG-чанки по последней реплике
**Веб-интерфейс:**
- [ ] В шапке обеих страниц — ссылки «Отладка» (текущая `/`) / «Песочница» (новая `/sandbox`)
- [ ] Текущий `static/index.html` остаётся без изменений
- [ ] Новая страница `static/sandbox.html` на отдельном маршруте `/sandbox`:
- [ ] левая колонка — список сохранённых диалогов: превью, дата, кнопка «переименовать», кнопка «удалить», кнопка «новый тред»
- [ ] центральная колонка — сам чат (оператор пишет как пациент, видит ответы агента, история подгружается при клике на тред из списка)
- [ ] правая колонка — retrieved-чанки со score + собранный промпт по последней реплике
### Критерий готовности
- [ ] Оператор может провести диалог из 5+ реплик, агент помнит контекст
- [ ] Все диалоги сохраняются и видны в левой колонке после перезагрузки страницы
- [ ] Оператор может открыть старый диалог, переименовать его, удалить
- [ ] В правой колонке видно, что нашёл RAG и что улетело в LLM на последнем шаге
- [ ] Старая страница отладки (`/`) работает как раньше, ничего не сломано
---
## Спринт 2.5. Доработки после пилота Спринтов 1–2
### Цель
Закрыть технический долг, накопленный за первые два спринта: почистить чанки от markdown-мусора, сделать ответ агента читаемым в UI, подготовить системный промпт к вынесению в редактор (Спринт 3) и навести порядок в логах и README.
### Статус: ✅ Закрыт
### Задачи
**Качество RAG:**
- [ ] Почистить чанки: убрать markdown-ссылки `[текст](url)`, блоки навигации `**Вернуться на:**`, дубликаты меню
- [ ] Эндпоинт `POST /documents/{id}/reindex` — переразметить существующий документ с новыми правилами чанкера (без повторной загрузки файла — но у нас пока нет хранения исходников, поэтому надо хранить исходный текст в метаданных чанков или сохранять оригинал при `upload`); решение по способу — в рамках задачи
- [ ] Эндпоинт `POST /documents/reindex-all` — прогнать переиндексацию по всей базе
**UI:**
- [ ] Markdown-рендер ответов ассистента в «Песочнице» (жирный, курсив, списки, код); реплики пациента оставить plain text
**Системность:**
- [ ] Вынести системный промпт из `services/llm_client.py` в отдельный файл (например, `prompts/system_prompt.md`), загружать при старте — задел под Спринт 3
- [ ] Привести логи в порядок: настроить root-logger так, чтобы `logger.exception` писался в stderr/файл; не ломать uvicorn access/error
- [ ] Обновить `README.md` под текущее состояние: две страницы, `/chat` + `/threads`, SQLite + Alembic, как запустить и как мигрировать
### Критерий готовности
- [ ] Загружаем свежую wiki-статью → в её чанках нет markdown-ссылок и блоков «Вернуться на:»
- [ ] На «Песочнице» ответ агента рендерится с жирным/курсивом/списками
- [ ] Системный промпт хранится в отдельном файле, правится без трогания кода
- [ ] При ошибке в `/chat` в логах виден читаемый traceback
- [ ] README описывает актуальное состояние (две страницы, эндпоинты, запуск, миграции)
---
## Спринт 3. Настройки агента: системный промпт и правила
### Цель
Дать операторам веб-редактор системного промпта и списка правил («если спрашивают про X — отвечай так-то», «если пациент злится — делай то-то»). Версионирование: можно сохранить конфигурацию и откатиться.
### Статус: ✅ Закрыт
### Задачи
- [ ] Хранилище (SQLite): `agent_configs` (version, created_at, system_prompt, rules_text, is_active)
- [ ] Эндпоинты: `GET /configs`, `POST /configs` (создать новую версию), `POST /configs/{id}/activate`
- [ ] Песочница использует активную версию при каждом `/chat`
- [ ] Веб-страница «Настройки агента»:
- [ ] редактор системного промпта (textarea)
- [ ] редактор правил (отдельным блоком; на старте — просто textarea, позже — список записей)
- [ ] кнопка «Сохранить как новую версию»
- [ ] список версий с кнопкой «Сделать активной» и пометкой активной
- [ ] Показ активной версии в шапке песочницы
### Критерий готовности
- [ ] Оператор меняет промпт → сохраняет как v2 → активирует → тестирует в песочнице → при желании откатывается к v1
- [ ] Правила реально влияют на ответы агента (проверяется вручную через песочницу)
---
## Архитектурный разворот после Спринта 3 (2026-04-23)
После пилота Спринтов 1–3 решили уходить от одного «мега-промпта» ко графовой архитектуре: **роутер намерений + изолированные ветки + state machine + exit conditions**. Подробности — в [`architecture/GRAPH_ARCHITECTURE_v3.md`](./architecture/GRAPH_ARCHITECTURE_v3.md) (последняя версия). Исторические снапшоты — `architecture/GRAPH_ARCHITECTURE_v1.md` (изначальный, на момент разворота) и `architecture/GRAPH_ARCHITECTURE_v2.md`.
**Принятые решения по открытым вопросам:**
- **Фреймворк оркестровки:** пишем вручную на Python. LangGraph/n8n не берём — проект компактный, свой стек работает, не тянем лишних зависимостей.
- **Модель для роутера:** остаёмся на DeepSeek, но `RouterClient` делаем отдельным классом от `LLMClient` — потом сменим модель в одном месте, если станет дорого.
- **Exit conditions:** свободный текст в промпте ветки + независимый роутер на каждой реплике. Если ветка пропустит триггер — роутер подстрахует.
- **Эскалация на человека:** одна ветка `escalate_human` с полем `reason` (`acute_pain` / `surgery` / `angry` / `explicit_request`). Отдельная маршрутизация «куда именно» — задача смежного разработчика при подключении каналов.
- **Confidence score:** не тянем в первый спринт. Роутер всегда возвращает один из intent'ов, при сомнении — `general_info`. После первого живого прогона посмотрим на реальные ошибки.
Старые Спринт 4 (сценарии) и Спринт 5 (экспорт) не удалены — они переехали в Спринт 7 с дополнением под граф (прогон сценариев проверяет маршрутизацию, экспорт — снапшот графа).
---
## Спринт 4. Фундамент графа — `intents` + роутер + переключение веток
### Цель
Заменить «один активный промпт на всё» на «свой промпт на каждую ветку + роутер выбирает ветку на каждой реплике». Это первый шаг к графовой архитектуре из [`architecture/GRAPH_ARCHITECTURE_v3.md`](./architecture/GRAPH_ARCHITECTURE_v3.md).
### Статус: ✅ Закрыт
### Задачи
**Данные:**
- [ ] Новая таблица `intents` (code, name, description, is_enabled, order_index)
- [ ] Миграция Alembic + в `agent_configs` добавить `intent_id` (nullable для обратной совместимости)
- [ ] Сид при первом запуске: 6 стартовых веток — `new_booking`, `reschedule`, `price_question`, `medical_question`, `general_info`, `escalate_human`
- [ ] Перенос текущего v1 конфига в ветку `general_info` как стартовый промпт
**Роутер:**
- [ ] `services/router_client.py` — отдельный класс под DeepSeek, метод `classify(history, text) → intent_code`
- [ ] Короткий промпт-классификатор с фиксированным перечнем категорий
- [ ] При сомнении возвращает `general_info` (без confidence score на этом спринте)
**Оркестрация:**
- [ ] В `chat_service.send_message`: сначала `router.classify()` → активный конфиг выбранной ветки → `llm.chat()` с этим промптом
- [ ] В таблице `messages` сохраняется `intent_id` каждого обмена
**API:**
- [ ] `GET /intents` — список веток
- [ ] `PATCH /intents/{code}` — включить/выключить
- [ ] `POST /configs` принимает `intent_id`; создание новой версии — всегда в рамках ветки
**UI:**
- [ ] «Настройки»: слева список веток, справа редактор промпта/правил активной версии выбранной ветки
- [ ] В «Песочнице» в отладке показывать: решение роутера + выбранный intent + какая ветка ответила
### Критерий готовности
- [ ] «У меня острая боль» → `medical_question`
- [ ] «Сколько стоит приём» → `price_question`
- [ ] «Как доехать» → `general_info`
- [ ] В отладочной панели «Песочницы» виден intent и какая ветка дала ответ
- [ ] Для каждой ветки можно отдельно править промпт и сохранять версии
---
## Спринт 5. State machine + exit conditions (bouncing)
### Цель
Научить ветки вести многошаговые скрипты и бесшовно передавать тред в другую ветку, если пациент сменил тему.
### Статус: ✅ Закрыт (ядро; дотяжка до GRAPH_ARCHITECTURE v2 — в бэклоге)
### Задачи
**Данные:**
- [x] Таблица `thread_state` (thread_id, current_intent_code, current_step, slots_json, updated_at) + миграция Alembic (batch-режим под SQLite)
**State machine (первая ветка — `new_booking`):**
- [x] 6-шаговый скрипт: приветствие → повод → специалист → удобное время → подтверждение → запись
- [x] Модель на каждой реплике видит блок `[ТЕКУЩЕЕ СОСТОЯНИЕ]` с `step` и `slots`
- [x] Переход шагов управляется служебным тегом `[STATE: step=N; slots={...}]` в ответе модели (строковый тег, парсится балансировкой фигурных скобок)
**Exit conditions и bouncing:**
- [x] В промпт `new_booking` добавлен блок условий выхода с сигналом `[INTENT_CHANGE: <code>]`
- [x] Парсер в `services/chat_service._parse_assistant_signals` вырезает служебные теги из ответа
- [x] Bouncing: одна итерация (`MAX_BOUNCES=1`) — ветка может передать управление другой, делаем повторный вызов LLM
- [x] Роутер на каждой реплике: если классификация ≠ `thread_state.current_intent_code` → сброс `step` и `slots`
**UI:**
- [x] В «Песочнице» блок «Состояние треда»: intent, шаг, слоты (JSON), список переходов в текущей реплике
- [x] В отладке роутера — пометка, если ветка «передала управление»
### Критерий готовности
- [x] Сценарий new_booking проходит: ФИО → повод → специалист → время → подтверждение собираются в `thread_state.slots`
- [x] Переключение ветки через роутер: «Сколько стоит приём?» внутри записи → state сбрасывается в `price_question`
- [x] В отладке видна вся цепочка: роутер-intent, served-intent, шаг, слоты, переходы
### Что НЕ вошло в этот спринт (по сравнению с GRAPH_ARCHITECTURE_v2.md)
Реализовано ядро v1. Вся дотяжка до v2 — Спринт 6.
---
## Спринт 6a. State machine v2 — ядро, защита от петель, возврат в ветку
### Цель
Заменить строковый тег `[STATE: ...]` на структурированный выход модели с валидатором переходов по таблице `intent_steps`; добавить `handoff_count` с автовыходом в `escalate_human: routing_loop`; научить систему возобновлять прерванную ветку через `suspended_intent`. В конце Спринта 6a уже видно глазами: вкладка «Шаги» в «Настройках» для `new_booking`, в «Песочнице» — handoff_count и suspended_intent, timeline переходов первой версии.
Попутно реализована **sticky state machine** (мини-G): когда тред идёт по sm-ветке и роутер предлагает другую — state не сбрасывается, в системный промпт ветки подаётся `[ПОДСКАЗКА РОУТЕРА]`, LLM сама решает. Это сняло ключевую дыру Спринта 5 с коротким repликами внутри сценария.
### Статус: ⏳ Запланирован
### Принятые решения (зафиксировано 2026-04-24, действуют и для 6b)
- **Момент обновления `current_step`** — после успешного коммита сообщения ассистента в БД.
- **Cap на soft-insertion'ы подряд** — 3 (реализация в 6b).
- **Шаги `new_booking` — сверить с вики клиники по ЛОР** до переписывания промпта в блоке A.
- **Формат структурированного выхода** — JSON-блок в хвосте ответа, парсим сами балансировкой скобок + `json.loads`.
### Задачи и UI-чекпойнты (порядок: A → A2 → B → C)
**Блок A. Структурированный выход + таблица `intent_steps` + валидатор переходов (v2 §3.3)**
*Бекенд:*
- [ ] Новая таблица `intent_steps`: `id, intent_id FK, code (intro/qualify/present/offer_time/book/close), name, order_index, system_prompt Text, allowed_next JSON, guards JSON (пустой на этом спринте — наполняется в 6b/F)`. Миграция Alembic.
- [ ] Сид шагов `new_booking` при старте: читает `prompts/intents/new_booking/steps/{code}.md` + `prompts/intents/new_booking/transitions.yaml`.
- [ ] Разделить `prompts/intents/new_booking.md` на базовый промпт ветки (общие правила) + отдельные файлы на каждый из 6 шагов.
- [ ] В `services/chat_service` — сборка промпта: `base_prompt + intent_steps[current_step].system_prompt + state_context`.
- [ ] Парсер нового формата ответа: `{reply, state_after, slots_updated}` — JSON-блок в хвосте.
- [ ] Валидатор: сверка `state_after` с `intent_steps.allowed_next`. Легален → применяем, иначе — остаёмся на текущем шаге + warning в лог. Слоты сливаем `{**old, **slots_updated}`.
- [ ] `assistant_msg.text` = `reply`; служебные поля — в `assembled_prompt` для отладки.
*UI-чекпойнт A:*
- [ ] В «Настройках» для ветки `new_booking` появляется вкладка **«Шаги»**: список шагов из `intent_steps` + на клик открывается редактор (textarea с `system_prompt`, чекбоксы с `allowed_next`). Кнопка «Сохранить шаг» — `PATCH /intents/{code}/steps/{step_code}` пишет сразу (без версионирования).
- [ ] В «Песочнице» бейдж текущего шага берётся из `intent_steps.name`, а не из сырого числа. Если валидатор отклонил `state_after` — красная пометка «модель просилась в `X`, остались на `Y`».
- [ ] **Что проверяем глазами:** открыть `new_booking` → вкладка «Шаги» видит 6 шагов; править любой промпт → применяется в новом треде; в песочнице прогнать «Здравствуйте, хочу записаться» → шаг подписан словами («Приветствие»), а не числом.
**Блок A2. `exit_conditions_text` — отдельное поле в `agent_configs` (v2 §UI)**
*Бекенд:*
- [ ] Миграция: добавить `exit_conditions_text Text NULLABLE` в `agent_configs`.
- [ ] `compose_full_system_prompt` склеивает: `system_prompt + rules_text + exit_conditions_text`.
- [ ] Миграция данных: при старте для существующих конфигов попытаться распарсить блок «Условия выхода» / `[INTENT_CHANGE: ...]` из хвоста `system_prompt` и перенести в новое поле. Не удалось — оставить пусто.
*UI-чекпойнт A2:*
- [ ] В «Настройках» на вкладке активной версии — третья textarea `exit_conditions_text` рядом с `system_prompt` и `rules_text`.
- [ ] **Что проверяем глазами:** у ветки `general_info` после миграции данных в поле `exit_conditions_text` лежат правила `[INTENT_CHANGE: ...]`, а не в теле промпта. В песочнице поведение не изменилось.
**Блок B. `handoff_count` и защита от петель (v2 §4.3)**
*Бекенд:*
- [ ] Миграция `thread_state`: добавить `handoff_count INT NOT NULL DEFAULT 0`.
- [ ] В `chat_service` инкрементить при каждом hard-handoff (INTENT_CHANGE или router-инициированное переключение).
- [ ] При `handoff_count >= 2` — авто-уход в `escalate_human` c `reason=routing_loop`. Ответ-заглушка формируется без нового вызова LLM («Передаю ваш вопрос администратору»).
- [ ] Счётчик сбрасывается на 0 при возврате из `suspended_intent` (блок C) и при переходе в `escalate_human`.
*UI-чекпойнт B:*
- [ ] В «Песочнице» в «Состоянии треда» — строка `handoff_count: N`. При автоуходе в `escalate_human: routing_loop` — явная отметка в timeline.
- [ ] **Что проверяем глазами:** искусственная петля «хочу записаться → сколько стоит → хочу записаться → сколько стоит» → после второго-третьего handoff'а бот говорит «передаю администратору»; в песочнице `handoff_count` вырос, ветка сменилась на `escalate_human`.
**Блок C. `suspended_intent` + `resumable_step` + `resumable_slots` (v2 §4.4)**
*Бекенд:*
- [ ] Миграция: добавить колонки `suspended_intent`, `resumable_step INT`, `resumable_slots_json TEXT` (все nullable) в `thread_state`.
- [ ] При hard-handoff из многошаговой ветки (`new_booking`) — сохранять `current_*` в `suspended_*` перед сбросом.
- [ ] Возврат: роутер классифицировал реплику в `suspended_intent` → восстанавливаем `current_*` из `suspended_*` и очищаем поля. Альтернативный триггер — сигнал `[RESUME]` из ветки detour'а (наполняем в 6b).
*UI-чекпойнт C:*
- [ ] В «Состоянии треда» — `suspended_intent` и `resumable_step` (если заполнены).
- [ ] Timeline переходов между ветками в рамках треда: список типа `new_booking (step=4) → price_question → new_booking (step=4, восстановлено)`. Собирается на бекенде из diff'ов `intent_id` у соседних сообщений + лога handoff'ов.
- [ ] **Что проверяем глазами:** запись до 4 шага → «сколько это стоит?» → `suspended_intent=new_booking, resumable_step=4` видно в панели → «ок, тогда бронируем» → слоты `new_booking` вернулись, шаг=4, timeline показывает три перехода.
### Критерий готовности 6a
- [ ] Сценарии 1 (базовая запись), 3 (handoff с suspended), 4 (возврат из suspended), 6 (routing_loop) из блока H Спринта 6b проходят в «Песочнице».
- [ ] `handoff_count` и `suspended_intent` видны глазами в «Состоянии треда».
- [ ] Вкладка «Шаги» в «Настройках» работает — можно отредактировать промпт шага и увидеть эффект в песочнице без рестарта.
- [ ] Третья textarea `exit_conditions_text` работает; данные старых веток мигрированы.
- [ ] `current_step` пишется только после коммита `assistant_msg` — проверяется код-ревью.
- [ ] Парсер структурированного выхода устойчив к невалидному `state_after`.
---
## Спринт 6b. Глубина сценария — soft-insertion, guards, reason, умный роутер
### Цель
Поверх ядра из 6a — добавить различение soft/hard-handoff, guards в `new_booking`, структурированный reason в `escalate_human`, умный роутер, видящий `thread_state`. В конце Спринта 6b все 8 ручных сценариев из блока H проходят в «Песочнице».
### Статус: ⏳ Запланирован (после 6a)
### Задачи и UI-чекпойнты (порядок: D → F → E → G → H)
**Блок D. Soft-insertion vs hard-handoff (v2 §4.2)**
*Бекенд:*
- [ ] В промпт ветки `new_booking` (базовый + шаги `qualify/present/offer_time`) — правило «короткие боковые вопросы (цена услуги, адрес, часы, длительность приёма, требования к документам) отвечай сам, не покидая шаг». Модель возвращает `state_after=текущий_шаг`, `slots_updated={}`.
- [ ] Миграция `thread_state`: добавить `soft_insertion_count INT NOT NULL DEFAULT 0`.
- [ ] На soft-insertion счётчик инкрементится; на продвижение по шагу — сбрасывается в 0.
- [ ] При `soft_insertion_count >= 3` — ветка в промпте получает явное указание «вернуть пациента к вопросу шага».
*UI-чекпойнт D:*
- [ ] В «Состоянии треда» — `soft_insertion_count: N`.
- [ ] В timeline переходов помечать soft-insertion как `new_booking · soft-answer (price)` — без смены ветки.
- [ ] **Что проверяем глазами:** запись до шага 3 → «а сколько стоит?» → ответ по цене, шаг=3 сохранился, `soft_insertion_count=1`. Повторить 3 раза → на 3-м ответе бот возвращает к вопросу шага.
**Блок F. Guards в `new_booking` (v2 §3.2)**
*Бекенд:*
- [ ] В `intent_steps.guards` наполняем условия для `new_booking`: ребёнок → `legal_rep_name+legal_rep_phone` до перехода из `qualify`; запрос конкретного врача с листом ожидания → рукав `waitlist`; жалоба на слух без предварительного сурдолога → сначала `surgologist` в `specialist`.
- [ ] Слоты: `is_child`, `legal_rep_name`, `legal_rep_phone`, `requested_doctor`, `waitlist_flag`, `needs_surgologist_first`.
- [ ] Валидатор переходов (блок A 6a) проверяет `guards`: если не пройден — блокирует `state_after`, оставляет на шаге, возвращает пациенту ответ модели как есть.
- [ ] Обновить промпты шагов под сценарии guard'ов.
*UI-чекпойнт F:*
- [ ] На вкладке «Шаги» — отдельная textarea для `guards` (JSON) с валидацией формата.
- [ ] В «Состоянии треда» — если валидатор заблокировал переход guard'ом, явная отметка «guard `require_legal_rep` не пройден, ждём `legal_rep_phone`».
- [ ] **Что проверяем глазами:** сценарий 7 (ребёнок) — на шаге `qualify` после «это для сына, 5 лет» бот спрашивает ФИО и телефон родителя; пока не заполнены — не переходит; в песочнице видна причина блокировки. Сценарий 8 (конкретный врач) — переход в рукав `waitlist`.
**Блок E. `reason` в `escalate_human` (v2 §1, §5)**
*Бекенд:*
- [ ] Обновить промпт `_router`: при `escalate_human` возвращать пару `code + reason` (`acute_pain / surgery / angry / explicit_request / routing_loop`).
- [ ] `RouterClient.classify` парсит reason, дефолт при неразобранном — `explicit_request`.
- [ ] Ветка `escalate_human.md` и шаги (если есть) — reason влияет на текст первой реплики.
- [ ] В `messages` — колонка `escalation_reason NULLABLE` (миграция). В API-ответе `/chat` поле `escalation_reason`.
- [ ] Заготовка саммари для оператора: при эскалации формируется `{reason, history, slots_from_suspended}`, логируется в файл/консоль (канал передачи — Спринт 9).
*UI-чекпойнт E:*
- [ ] В «Состоянии треда» — при активной эскалации показывать `reason`.
- [ ] В «Отладке ответа» под блоком роутера — сгенерированное саммари оператора (read-only preview).
- [ ] **Что проверяем глазами:** сценарий 5 («упомянул хирургию») → эскалация с `reason=surgery`, превью саммари содержит всю историю + собранные слоты. Сценарий 6 (петля) → эскалация с `reason=routing_loop`.
**Блок G. Умный роутер (видит `thread_state`)**
Частично уже реализовано в Спринте 6a: **sticky state machine** — если тред в sm-ветке и роутер предлагает другую, state НЕ сбрасывается, а в системный промпт ветки подаётся блок `[ПОДСКАЗКА РОУТЕРА]`, LLM сама решает (STATE_JSON или INTENT_CHANGE). Это сняло основную проблему с короткими репликами («Кук», «болит ухо») внутри сценария.
*Что осталось на 6b:*
- [ ] Вторая линия защиты: в `RouterClient.classify` принимать снимок `thread_state` и вставлять в системный промпт **самого роутера** блок «Сейчас идёт сценарий X на шаге Y, слоты Z — если реплика укладывается в сценарий, предпочитай текущую ветку». Это помогает роутеру изначально реже ошибаться, а не только «поправляться» sticky-логикой.
- [ ] Обновить `prompts/intents/_router.md` под новый формат.
*UI-чекпойнт G:*
- [ ] В «Отладке ответа» → блок «Решение роутера» — кнопка развернуть промпт, который ушёл в роутер (включая блок состояния треда). Полезно для отладки.
- [ ] **Что проверяем глазами:** сценарий из 6a («болит ухо» внутри new_booking) — роутер теперь изначально возвращает `new_booking`, а не `medical_question` → без sticky-коррекции.
**Блок H. Финальный прогон 8 ручных сценариев (прокси-eval)**
- [ ] Зафиксировать в `eval/MANUAL_CASES.md` полный список 8 сценариев (уже описан в этом документе выше, просто консолидируем).
- [ ] Прогнать в «Песочнице». Для каждого сценария — в `eval/MANUAL_REPORT.md` фиксируем результат (ок / расхождение + детали).
1. Базовая запись (6 шагов → `confirmed=true`).
2. Запись → вопрос про цену (soft-insertion, без смены шага).
3. Запись → перенос старой записи (hard-handoff в `reschedule`, `suspended=new_booking`).
4. Запись → detour → возврат «бронируем на четверг» (восстановление из `suspended`).
5. Запись → упоминание хирургии (`escalate_human: surgery`, саммари).
6. Искусственная петля (`routing_loop` после cap).
7. Запись ребёнка (guard блокирует переход).
8. Конкретный врач (waitlist-рукав).
### Критерий готовности 6b
- [ ] Все 8 сценариев из блока H проходят в «Песочнице» без ручной правки state. `MANUAL_REPORT.md` закоммичен.
- [ ] Все UI-чекпойнты (D, F, E, G) проверены глазами.
- [ ] Роутер при активной state machine не сбрасывает intent на коротких репликах внутри сценария.
- [ ] Саммари оператору формируется и логируется при эскалации — пусть пока и без канала передачи.
---
## Спринт 6c. Терминология: словарь, документация, UI, страницы примеров
### Цель
Устранить терминологический кавардак между v3-архитектурой, кодом и UI: единый словарь, протянуть его сквозь страницу документации и UI Песочницы/Настроек, добавить разобранные примеры из `docs/examples/` как читаемые страницы внутри приложения. Делается **перед** Спринтом 8 (мини-eval), чтобы тесты роутера и handoff'а уже опирались на устоявшиеся термины и читаемое UI.
### Статус: ✅ Закрыт
### Задачи
- [x] Зафиксирован словарь: «намерение» (intent) и «ветка» (branch) разнесены концептуально, в коде остаётся `intent_code` (связь 1:1, см. идею в «Дальнейшие идеи»). «Маршрутизатор» вместо «роутер». «Защитное условие» вместо «guard» (буквально из v3 §3.2). «Пошаговая ветка» вместо «многошаговая». Введены: «Решение маршрутизатора», «Активная ветка», «Счётчик переключений», «Причина передачи оператору».
- [x] **Документация (`static/docs.html`)** — карточки терминов и текст приведены к словарю. Добавлены карточки «Намерение», «Ветка» (с историческим замечанием про intent в БД), «Решение маршрутизатора», «Активная ветка», «Счётчик переключений», «Причина передачи оператору». «Guard» переименован в «Защитное условие».
- [x] **Песочница (`static/sandbox.html`)** — «Решение роутера» → «Решение маршрутизатора». Бейдж «многошаговая» → «пошаговая ветка». Бейдж «🔒 guard X» → «🔒 защитное условие X». «Решение маршрутизатора» теперь всегда видимый бейдж (зелёный при совпадении с активной веткой, жёлтый при расхождении). Активная ветка названа явно. Счётчик переключений вынесен в визуальный элемент «N из 3» (красный при достижении капа).
- [x] **Настройки (`static/settings.html`)** — поле «Guards (JSON)» → «Защитные условия (guards, JSON)», тост ошибки переименован.
- [x] **Страницы примеров** — параметризованная страница `static/example.html`, рендерит markdown через marked.js + DOMPurify. Маленький роут `GET /api/docs/examples/{name}` в `main.py` отдаёт markdown из `docs/examples/` без дублирования. Навигация между 4 примерами + хлебные крошки обратно. Раздел «Разобранные примеры» добавлен в `docs.html`.
### Критерий готовности
- [ ] Слово «роутер» в UI отсутствует (только в коде как `_router` и в служебной константе `[ПОДСКАЗКА РОУТЕРА]`).
- [ ] Слово «guard» в UI заменено на «защитное условие». В коде остаётся `guards_json`, `check_guards()`.
- [ ] В Песочнице на каждой реплике видно отдельно «Решение маршрутизатора» и «Активная ветка»; счётчик переключений виден как «N из 3».
- [ ] Из `docs.html` есть навигация к 4 страницам примеров; со страницы примера — обратно в документацию.
---
## Спринт 7. Мульти-RAG, часть A: подписка ветки на загруженные документы
### Цель
Дать каждой ветке собственный срез базы знаний, чтобы документы для одной темы (например, скрипты по детскому приёму) не засоряли ответы другой темы (цены / общая справка). Делаем **до** мини-eval Спринта 8, чтобы тесты прогонялись уже с реальным per-intent retrieval.
**Часть A** этого спринта — ручная подписка через UI: оператор загружает документы как сейчас (на странице «Отладка»), а в «Настройках» ветки указывает галочками, какие из них в неё подмешивать. **Часть Б** (автосинхронизация с внешней вики операторов) — отдельной задачей в идеях на потом.
**Подход** — A (M:N через document_id, не префиксы путей и не теги). Причины: `vectorstore.query()` уже умеет фильтровать по `document_ids` (нечего переписывать); нулевая миграция Chroma; на текущем масштабе (~30 документов, 6 веток) ручная подписка — 3-минутная задача один раз при загрузке; дисциплина именования путей — слабое место в проектах с >1 оператором, а галочки понятны без инструкции.
### Статус: ✅ Закрыт (коммит `52b46bc`, 2026-04-27)
### Задачи
**Бэкенд:**
- [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:**
- [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.
- [ ] (отложено в идеи на потом) `README.md` — раздел про мульти-RAG.
### Критерий готовности
- [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` обновлены и активированы в БД.
---
## Спринт 8. Мини-eval: роутер, handoff, resumable
### Цель
После дотяжки v2 (Спринт 6) и мульти-RAG (Спринт 7) — зафиксировать автоматизированный тест-набор, чтобы следующие правки промптов и `wiki_sources` не ломали собранное. Формализует ручные сценарии из блока H Спринта 6.
### Статус: ⏳ Запланирован
### Задачи
**Eval-наборы (отдельные файлы в репозитории, без БД):**
Все наборы в **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` — 3–5 сценариев 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` — читает JSONL-наборы, прогоняет через живой сервис. Режимы:
- `router` — прямой вызов `RouterClient.classify()` на одношаговых кейсах (быстро).
- `dialog` — полный `/chat` на чистых тредах, сверка по каждому шагу: активная ветка + решение маршрутизатора + текущий шаг + слоты + счётчик переключений + причина эскалации + retrieved-источники.
- [ ] Вывод: per-ветка accuracy, confusion matrix, список расхождений с текстом реплики.
- [ ] Отчёт: stdout + `eval/reports/{timestamp}.md` (добавлять в git для сравнения во времени).
**Документация:**
- [ ] В `README.md` — раздел «Как прогнать eval» (одна команда).
- [ ] Договорённость: перед правкой промпта роутера / ветки / `wiki_sources` — прогнать eval, зафиксировать baseline; после — сравнить.
### Критерий готовности
- [ ] `eval/run.py` работает одной командой, режим `router` проходит за ≤ 30 секунд (на `count >= 2`), режим `dialog` — за ≤ 3 минуты.
- [ ] Отчёт покрывает все 8 сценариев из блока H Спринта 6 + одношаговые кейсы маршрутизатора + RAG-проверки Спринта 7.
- [ ] Baseline зафиксирован в `eval/reports/{date}_baseline.md` и добавлен в git.
---
## Спринт 9. Сценарии + экспорт графа
### Цель
То, что изначально планировалось как Спринты 4 + 5 до архитектурного разворота. Теперь встроено в граф: прогон сценария проверяет не только текст ответов, но и правильность маршрутизации; экспорт — снапшот всего графа (intents + промпты + коллекции).
### Статус: ⏳ Запланирован
### Задачи
**Сценарии:**
- [ ] Таблица `scenarios` (id, name, note, label, messages_json, expected_intents_json, config_snapshot_id)
- [ ] `POST /scenarios` — сохранить текущий тред «Песочницы» как сценарий, зафиксировать ожидаемый intent на каждую реплику пациента
- [ ] `POST /scenarios/{id}/run` — прогнать реплики пациента на текущих активных конфигах всех веток; вернуть новые ответы + распознанные intents
- [ ] Веб-страница «Сценарии»: список + открытая карточка со side-by-side (старый ответ / новый), подсветка «маршрутизация совпала / разошлась»
- [ ] Счётчик «ок / расхождение» по всей базе сценариев после последнего прогона
**Экспорт:**
- [ ] `GET /configs/export` — JSON-снапшот графа: все intents, для каждого — активный промпт и правила, список коллекций RAG и документов в них
- [ ] Документация API в README: `POST /chat`, `GET /health`, контракт ответов
- [ ] Инструкция «Как подключить канал» + пример curl / минимальный webhook-адаптер
- [ ] docker-compose поднимается одной командой, внешний разработчик получает рабочий `/chat`
### Критерий готовности
- [ ] После изменения промпта в одной из веток — прогон сценариев показывает расхождения именно в этой ветке
- [ ] Виден общий счётчик «ок / изменилось» по базе сценариев
- [ ] В README готов раздел «Как подключить канал», работает docker-compose-запуск
---
## Бэклог
### Дальнейшие идеи
- **Сквозные правила всех веток** (из `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.
- **Confidence threshold для RAG в `general_info`** (из v3 + пример 04, A.4): если score лучшего чанка ниже порога (например 0.50) — модель отвечает шаблоном «уточним и перезвоню», ставит слот `needs_followup=true`. Защита от выдумывания фактов в случаях вроде «работаете в праздник?» при отсутствии чанка.
- **Технические слоты для `general_info`** (из примера 04): `info_topic` (`hours` / `branches` / `transit` / `parking` / `contacts` / `preparation` / `scope_of_services`), `branch_mention`, `needs_followup`. Сейчас у `general_info` нет машины состояний и слоты не сохраняются — при втором вопросе в треде ретривер не знает, про какой филиал шла речь раньше. Подключить минимальный `answer→done` со слотами.
- **CRM-инструменты (`crm.get_slots`, `crm.create_booking`)** (из v3 + примеры 01/02): сейчас в коде нет интеграции с CRM, на шагах `offer_time` / `book` модель «обещает» запись, но никуда её не сохраняет. Реальная интеграция — задача смежника при подключении каналов, но мок-инструменты можно завести раньше, чтобы поддерживать сквозной сценарий в Песочнице.
- **Sub-states типа `qualify.legal_rep`** (из примера 03): сейчас тот же эффект достигается через conditional transitions + guards, и v3 сама рекомендует не плодить sub-states. Возвращаться, если guard'ов на одном шаге станет много и состояние перестанет читаться.
- **Разделение «намерения» и «ветки» в коде и БД** (из v3, раздел «Архитектура, к которой идём»): сейчас в коде и в таблице `intents` это одна сущность, связь намерение↔ветка жёстко 1:1. В словаре терминов их разнесли только концептуально (см. словарь в `static/docs.html`). Возвращаться к этому, **когда появится сценарий «одно намерение → разные ветки в зависимости от контекста»** — например, отдельные ветки записи для детей и взрослых под одно намерение `new_booking`. Тогда понадобится завести `branch_code` рядом с `intent_code`, пересобрать модель `Intent`, поменять выбор ветки в `chat_service.py`. До такого сценария — лишняя сложность.
- Раздельные правила по доменам — **перекрыто архитектурой: теперь это ветки (`intents`)**
- A/B сравнение двух версий промпта на одном тест-наборе (в рамках одной ветки или между ветками)
- Метрики качества ответов (MRR, CSAT по сценариям)
- Подсветка цитат источников в ответе агента
- Перевод правил из свободного текста в структурированный список (pattern → instruction)
- Мультипользовательский режим (несколько операторов одновременно настраивают)
- Хранение исходных файлов (`./data/uploads/{document_id}.{ext}` + `source_path` в метаданных Chroma) — чтобы переиндексировать без повторной загрузки и показывать оператору оригинал документа
- Confidence score роутера + clarifying question при низкой уверенности — включить после реального прогона eval'а, если будет много ошибок классификации
- Визуализация графа (веток и переходов между ними) — возможно, в виде отдельной панели
- Вынесение роутера на отдельную более дешёвую модель (gpt-4o-mini, локальная Qwen) — когда вызовов станет много
- Структурированные exit conditions (список триггеров с keyword-match) — если свободный текст в промпте будет пропускать реальные случаи смены темы
- `routing_log` (таблица решений роутера по каждой реплике) — для офлайн-анализа и тюнинга, когда eval покажет, что нужно