Files
RAG_helper/docs/SPRINTS.md
T
AR 15 M4 d5eccfc342 docs(sprint7.6): закрыть висящий пункт — промпты накатаны в БД
В Спринте 7.7 активный граф new_booking сжат до 4 шагов. Сейчас прокачаны
system_prompt из файлов prompts/intents/new_booking/steps/{intro,qualify,book}.md
в БД активного графа через PATCH. close уже совпадал.

После этого 4-шаговый сценарий реально работает в чате (раньше в БД лежали
старые промпты Спринта 6a, несмотря на новые allowed_next).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:35:20 +05:00

76 KiB
Raw Blame History

Спринты — 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_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.

Статус: Закрыт

Задачи

Данные:

  • Новая таблица 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 — в бэклоге)

Задачи

Данные:

  • Таблица thread_state (thread_id, current_intent_code, current_step, slots_json, updated_at) + миграция Alembic (batch-режим под SQLite)

State machine (первая ветка — new_booking):

  • 6-шаговый скрипт: приветствие → повод → специалист → удобное время → подтверждение → запись
  • Модель на каждой реплике видит блок [ТЕКУЩЕЕ СОСТОЯНИЕ] с step и slots
  • Переход шагов управляется служебным тегом [STATE: step=N; slots={...}] в ответе модели (строковый тег, парсится балансировкой фигурных скобок)

Exit conditions и bouncing:

  • В промпт new_booking добавлен блок условий выхода с сигналом [INTENT_CHANGE: <code>]
  • Парсер в services/chat_service._parse_assistant_signals вырезает служебные теги из ответа
  • Bouncing: одна итерация (MAX_BOUNCES=1) — ветка может передать управление другой, делаем повторный вызов LLM
  • Роутер на каждой реплике: если классификация ≠ thread_state.current_intent_code → сброс step и slots

UI:

  • В «Песочнице» блок «Состояние треда»: intent, шаг, слоты (JSON), список переходов в текущей реплике
  • В отладке роутера — пометка, если ветка «передала управление»

Критерий готовности

  • Сценарий new_booking проходит: ФИО → повод → специалист → время → подтверждение собираются в thread_state.slots
  • Переключение ветки через роутер: «Сколько стоит приём?» внутри записи → state сбрасывается в price_question
  • В отладке видна вся цепочка: роутер-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.

Статус: Закрыт

Задачи

  • Зафиксирован словарь: «намерение» (intent) и «ветка» (branch) разнесены концептуально, в коде остаётся intent_code (связь 1:1, см. идею в «Дальнейшие идеи»). «Маршрутизатор» вместо «роутер». «Защитное условие» вместо «guard» (буквально из v3 §3.2). «Пошаговая ветка» вместо «многошаговая». Введены: «Решение маршрутизатора», «Активная ветка», «Счётчик переключений», «Причина передачи оператору».
  • Документация (static/docs.html) — карточки терминов и текст приведены к словарю. Добавлены карточки «Намерение», «Ветка» (с историческим замечанием про intent в БД), «Решение маршрутизатора», «Активная ветка», «Счётчик переключений», «Причина передачи оператору». «Guard» переименован в «Защитное условие».
  • Песочница (static/sandbox.html) — «Решение роутера» → «Решение маршрутизатора». Бейдж «многошаговая» → «пошаговая ветка». Бейдж «🔒 guard X» → «🔒 защитное условие X». «Решение маршрутизатора» теперь всегда видимый бейдж (зелёный при совпадении с активной веткой, жёлтый при расхождении). Активная ветка названа явно. Счётчик переключений вынесен в визуальный элемент «N из 3» (красный при достижении капа).
  • Настройки (static/settings.html) — поле «Guards (JSON)» → «Защитные условия (guards, JSON)», тост ошибки переименован.
  • Страницы примеров — параметризованная страница 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)

Задачи

Бэкенд:

  • Миграция Alembic i5c8b3a45f12_add_intent_documents: новая таблица intent_documents с полями intent_id (FK на intents.id), document_id (varchar 36, тип как в metadata Chroma), created_at. PK составной, индекс по document_id.
  • Модель db/models/intent_document.py (IntentDocument) с каскадом удаления.
  • Сервис services/intent_document_service.py — функции list_documents_for_intent_code, list_intents_for_document, set_documents_for_intent, set_intents_for_document.
  • API: GET/PUT /intents/{code}/documents и GET/PUT /documents/{id}/intents с PUT-семантикой «полный список», атомарно.
  • Retrieval-фильтр в services/chat_service.py + vectorstore.query() различает None (нет фильтра, вся коллекция) и [] (пустая подписка, 0 чанков). Дефолт для пациентских веток — []. Для _debugNone (отладка работает из коробки).

UI:

  • «Настройки» → блок «Документы базы знаний» в правом сайдбаре, всегда видим (независимо от вкладки), сортировка по имени, счётчик «N из M».
  • «Отладка» → кнопка «привязка» рядом с «удалить» → раскрывашка со списком веток, быстрая привязка прямо на месте.
  • «Отладка» → кнопка «редактировать» → большой textarea с raw_text, PUT /documents/{id}/raw обновляет текст и переиндексирует в Chroma. С confirm.
  • Системный промпт страницы «Отладка» переехал в ветку _debug. Удалён prompts/system_prompt.md и DEFAULT_SYSTEM_PROMPT в llm_client.py. info-bar на странице Отладки: версия + подписки + ссылка в Настройки.
  • Песочница: блок «Срез RAG», поле rag_subscription в ChatResponse, ворнинг при пустой подписке.
  • Тест-блок «Тест-вопрос от пациента» в центре Настроек (для любой выбранной ветки): 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.

Критерий готовности

  • Документ, привязанный к price_question, появляется в retrieval только когда активна именно эта ветка.
  • Ветка без подписок получает в retrieval 0 чанков (для пациентских) или вся коллекция (для _debug).
  • В Песочнице видно «подписано N из M», в найденных фрагментах — название документа.
  • Подписка работает в обе стороны 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.

Статус: Закрыт по коду (применение промптов в БД и ручная регрессия — за оператором)

Выбран вариант 2 блока C — present убран как самостоятельный шаг. Воронка: intro → qualify → book → close.

Задачи

Блок A — сжатие воронки:

  • intro.md — приветствие + открытый вопрос, имя НЕ спрашиваем (слот name со шага снят).
  • book.md — подтверждение плана + запрос телефона/имени в одной реплике.
  • qualify.md — снято требование «не уходи дальше пока нет name».

Блок B — содержательный qualify:

  • qualify.md — 5-пунктовый шаблон (эмпатия → 2–3 гипотезы из RAG → специалист → услуга/цена → CTA).
  • Три особые ситуации сохранены: запись ребёнка с require_legal_rep, конкретный врач с waitlist_flag, первичная жалоба на слух с needs_surgologist_first.
  • Деградация: при отсутствии гипотез/цен в RAG — пропускать пункт, не сочинять.

Блок C — present (вариант 2):

  • present.md помечен как DEPRECATED, оставлен в репозитории на случай отката.
  • SEED_INTENT_STEPS обновлён: qualify → [qualify, book], present → [book] (изоляция), book → [book, qualify, close] (без offer_time).
  • migrate_new_booking_allowed_next_v2() — одноразовая миграция при старте сервиса. Идемпотентна. Если оператор правил allowed_next руками — пропускает (warning в лог).

Блок D — регрессия:

  • eval/MANUAL_CASES.md — чеклист на 5 конверсионных кейсов + 8 ручных сценариев из блока H Спринта 6b.

Применение промптов в БД:

  • В Спринте 7.7 (коммит a79b6f9) активный граф new_booking сжат до 4 шагов (intro, qualify, book, close); архивный 6-шаговый — отдельный граф v1.
  • Промпты intro, qualify, book накатаны из файлов в БД активного графа через PATCH (sync-скрипт, 2026-05-02). close уже совпадал.

Регрессия (ручная, за оператором):

  • Прогнать в Песочнице 5 кейсов из eval/MANUAL_CASES.md §A — проверить структуру первого ответа (5 пунктов) и сжатие воронки (≤ 3 реплик до телефона).
  • Прогнать 8 кейсов из eval/MANUAL_CASES.md §B — все должны проходить как раньше.

Критерий готовности

  • Файлы промптов и allowed_next обновлены в коде, миграция отрабатывает.
  • (за оператором) На контрольном кейсе «храп + заложенность ушей» бот отвечает по 5-пунктовому шаблону, до запроса телефона ≤ 3 реплик.
  • (за оператором) Все 8 ручных сценариев из блока H Спринта 6b проходят.
  • Промпты intro.md, qualify.md, book.md обновлены и активированы в БД.

Спринт 7.7. Версионирование графа шагов в БД + UI переключения

Цель

Хранить старый 6-шаговый сценарий new_booking параллельно с новым 4-шаговым, чтобы можно было откатиться или сравнить варианты, не теряя историю. Оператор переключает активную версию из UI «Настройки → Шаги».

Статус: Закрыт

Задачи

Модель и миграция:

  • Таблица intent_step_graphs (id, intent_id, version, name, is_active, created_at). Активный ровно один на ветку.
  • intent_steps.graph_id FK на intent_step_graphs. UNIQUE сменён с (intent_id, code) на (graph_id, code).
  • Alembic-миграция j6d8c4b56g23 (batch_alter_table для SQLite).

Сервис и сидинг:

  • services/intent_step_graph_service.py: ensure_seed_graphs, list_graphs, get_active_graph, set_active_graph.
  • ensure_seed_graphs идемпотентен: создаёт активный граф для каждой state-machine-ветки, привязывает существующие шаги (graph_id IS NULL), для new_booking восстанавливает архивный 6-шаговый граф из prompts/intents/new_booking/steps/_archived_v1/*.md и _PRE_SPRINT_7_6_ALLOWED_NEXT.
  • Активный граф new_booking сжат до 4 шагов: deprecated present и offer_time удаляются (живут только в архивном v1).
  • SEED_INTENT_STEPS["new_booking"] обновлён под 4 шага.

API (routers/intents.py):

  • GET /intents/{code}/step-graphs — список графов с steps_count и is_active.
  • POST /intents/{code}/step-graphs/{graph_id}/activate — переключение активного.
  • Чтение шагов (list_steps_for_intent, get_step_by_code, get_first_step) фильтруется по активному графу.

UI (static/settings.html):

  • На вкладке «Шаги» вверху блок «Версии графа шагов» с карточками: имя, кол-во шагов, бейдж «активная» / кнопка «Активировать».
  • Заголовок вкладки «Шаги (N)» считается по активному графу: для new_booking активный = 4, после переключения на v1 = 6.
  • Раздел «Тест-вопрос от пациента» сделан сворачиваемым.

Критерий готовности

  • В БД 2 графа для new_booking: активный v2 (4 шага) и архивный v1 (6 шагов).
  • API /step-graphs возвращает оба, /steps — только шаги активного.
  • Переключение через UI меняет Шаги (4)Шаги (6) и список шагов.
  • Все остальные ветки получили один активный граф автоматически.

Спринт 8. Мини-eval: роутер, handoff, resumable

Цель

После дотяжки v2 (Спринт 6) и мульти-RAG (Спринт 7) — зафиксировать автоматизированный тест-набор, чтобы следующие правки промптов и wiki_sources не ломали собранное. Формализует ручные сценарии из блока H Спринта 6.

Статус: Запланирован

Задачи

Eval-наборы (отдельные файлы в репозитории, без БД):

Все наборы в JSONL (одна строка = один кейс). Унифицированный формат, единый парсер. Схема описана в eval/README.md. Историческое замечание: в первой версии плана одношаговые кейсы были в CSV, многошаговые в YAML — отказались от зоопарка форматов в пользу одного JSONL.

  • 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 покажет, что нужно