4aac59313d
Sprint 8.5 — чанкер v2 (services/document_processor.py):
- markdown-it-py для md-входа: каждый H2 открывает свою секцию, H3 идёт в тело
- множественные H1 — штатный кейс (new_booking.md = 8 H1, шаги воронки + группы);
H1 без H2 → секция heading=H1; преамбула H1 (тело до первого H2) игнорируется
- YAML frontmatter (--- ... ---) отрезается, в индекс не попадает
- breadcrumb «## {H2}» как первая строка каждого subchunk'а
- merge коротких хвостов и sentence-overlap — только внутри одной H2-секции
- excluded_section_headings в config.py
- 17 unit-тестов на stdlib unittest (tests/test_document_processor_v2.py),
включая smoke по реальным general_info.md (тимпанометрия → правильная секция)
и new_booking.md (защита от регрессии множественных H1)
- ТЗ: docs/CHUNKER_v2_TZ.md
Sprint 8.6 — регрессия остальных 4 веток (static/regression.html):
- 4 опции в селекторе режима: branch:price_question (40 кейсов),
branch:medical_question (29), branch:escalate_human (14), branch:reschedule (16)
- бэкенд из 8b уже параметрический — правок в сервисе не потребовалось
- new_booking вне скоупа — state-machine, под него отдельный 8c (multi-turn)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
799 lines
89 KiB
Markdown
799 lines
89 KiB
Markdown
# Спринты — 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`.
|
||
|
||
### Статус: ✅ Закрыт по коду (применение промптов в БД и ручная регрессия — за оператором)
|
||
|
||
Выбран **вариант 2** блока C — `present` убран как самостоятельный шаг. Воронка: `intro → qualify → book → close`.
|
||
|
||
### Задачи
|
||
|
||
**Блок A — сжатие воронки:**
|
||
- [x] `intro.md` — приветствие + открытый вопрос, имя НЕ спрашиваем (слот `name` со шага снят).
|
||
- [x] `book.md` — подтверждение плана + запрос телефона/имени в одной реплике.
|
||
- [x] `qualify.md` — снято требование «не уходи дальше пока нет name».
|
||
|
||
**Блок B — содержательный `qualify`:**
|
||
- [x] `qualify.md` — 5-пунктовый шаблон (эмпатия → 2–3 гипотезы из RAG → специалист → услуга/цена → CTA).
|
||
- [x] Три особые ситуации сохранены: запись ребёнка с `require_legal_rep`, конкретный врач с `waitlist_flag`, первичная жалоба на слух с `needs_surgologist_first`.
|
||
- [x] Деградация: при отсутствии гипотез/цен в RAG — пропускать пункт, не сочинять.
|
||
|
||
**Блок C — `present` (вариант 2):**
|
||
- [x] `present.md` помечен как DEPRECATED, оставлен в репозитории на случай отката.
|
||
- [x] `SEED_INTENT_STEPS` обновлён: `qualify → [qualify, book]`, `present → [book]` (изоляция), `book → [book, qualify, close]` (без `offer_time`).
|
||
- [x] `migrate_new_booking_allowed_next_v2()` — одноразовая миграция при старте сервиса. Идемпотентна. Если оператор правил `allowed_next` руками — пропускает (warning в лог).
|
||
|
||
**Блок D — регрессия:**
|
||
- [x] `eval/MANUAL_CASES.md` — чеклист на 5 конверсионных кейсов + 8 ручных сценариев из блока H Спринта 6b.
|
||
|
||
**Применение промптов в БД:**
|
||
- [x] В Спринте 7.7 (коммит `a79b6f9`) активный граф new_booking сжат до 4 шагов (`intro`, `qualify`, `book`, `close`); архивный 6-шаговый — отдельный граф v1.
|
||
- [x] Промпты `intro`, `qualify`, `book` накатаны из файлов в БД активного графа через PATCH (sync-скрипт, 2026-05-02). `close` уже совпадал.
|
||
|
||
**Регрессия (ручная, за оператором):**
|
||
- [ ] Прогнать в Песочнице 5 кейсов из `eval/MANUAL_CASES.md` §A — проверить структуру первого ответа (5 пунктов) и сжатие воронки (≤ 3 реплик до телефона).
|
||
- [ ] Прогнать 8 кейсов из `eval/MANUAL_CASES.md` §B — все должны проходить как раньше.
|
||
|
||
### Критерий готовности
|
||
- [x] Файлы промптов и `allowed_next` обновлены в коде, миграция отрабатывает.
|
||
- [ ] (за оператором) На контрольном кейсе «храп + заложенность ушей» бот отвечает по 5-пунктовому шаблону, до запроса телефона ≤ 3 реплик.
|
||
- [ ] (за оператором) Все 8 ручных сценариев из блока H Спринта 6b проходят.
|
||
- [x] Промпты `intro.md`, `qualify.md`, `book.md` обновлены и активированы в БД.
|
||
|
||
---
|
||
|
||
## Спринт 7.7. Версионирование графа шагов в БД + UI переключения
|
||
|
||
### Цель
|
||
Хранить старый 6-шаговый сценарий `new_booking` параллельно с новым 4-шаговым, чтобы можно было откатиться или сравнить варианты, не теряя историю. Оператор переключает активную версию из UI «Настройки → Шаги».
|
||
|
||
### Статус: ✅ Закрыт
|
||
|
||
### Задачи
|
||
|
||
**Модель и миграция:**
|
||
- [x] Таблица `intent_step_graphs` (id, intent_id, version, name, is_active, created_at). Активный ровно один на ветку.
|
||
- [x] `intent_steps.graph_id` FK на `intent_step_graphs`. UNIQUE сменён с `(intent_id, code)` на `(graph_id, code)`.
|
||
- [x] Alembic-миграция `j6d8c4b56g23` (batch_alter_table для SQLite).
|
||
|
||
**Сервис и сидинг:**
|
||
- [x] `services/intent_step_graph_service.py`: `ensure_seed_graphs`, `list_graphs`, `get_active_graph`, `set_active_graph`.
|
||
- [x] `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`.
|
||
- [x] Активный граф `new_booking` сжат до 4 шагов: deprecated `present` и `offer_time` удаляются (живут только в архивном v1).
|
||
- [x] `SEED_INTENT_STEPS["new_booking"]` обновлён под 4 шага.
|
||
|
||
**API (`routers/intents.py`):**
|
||
- [x] `GET /intents/{code}/step-graphs` — список графов с `steps_count` и `is_active`.
|
||
- [x] `POST /intents/{code}/step-graphs/{graph_id}/activate` — переключение активного.
|
||
- [x] Чтение шагов (`list_steps_for_intent`, `get_step_by_code`, `get_first_step`) фильтруется по активному графу.
|
||
|
||
**UI (`static/settings.html`):**
|
||
- [x] На вкладке «Шаги» вверху блок «Версии графа шагов» с карточками: имя, кол-во шагов, бейдж «активная» / кнопка «Активировать».
|
||
- [x] Заголовок вкладки «Шаги (N)» считается по активному графу: для new_booking активный = 4, после переключения на v1 = 6.
|
||
- [x] Раздел «Тест-вопрос от пациента» сделан сворачиваемым.
|
||
|
||
### Критерий готовности
|
||
- [x] В БД 2 графа для `new_booking`: активный v2 (4 шага) и архивный v1 (6 шагов).
|
||
- [x] API `/step-graphs` возвращает оба, `/steps` — только шаги активного.
|
||
- [x] Переключение через UI меняет `Шаги (4)` ↔ `Шаги (6)` и список шагов.
|
||
- [x] Все остальные ветки получили один активный граф автоматически.
|
||
|
||
---
|
||
|
||
## Спринт 8a. Регрессия роутера в UI
|
||
|
||
### Цель
|
||
Дать оператору-настройщику кнопку: «после правки промпта `_router` нажми и увидь, что сломалось». Не CLI, не для разработчика — встроено в страницу «Регрессия» рядом с Настройками. Кэш ответов привязан к версии роутера: повторный прогон на той же версии — мгновенный, на новой — пересчитывается.
|
||
|
||
### Статус: ✅ Закрыт
|
||
|
||
### Задачи
|
||
|
||
**Backend:**
|
||
- [x] Таблицы `eval_runs`, `eval_run_cases` (с `is_pass`), `eval_router_predictions` (кэш `text_hash + router_config_id → predicted_intent`). Alembic-миграции `k7e9d5c67h34`, `l8f0e6d78i45`.
|
||
- [x] Сервис `services/eval_run_service.py`: `start_router_run(text_hashes)` запускает фоновую корутину, использует кэш, фиксирует активную версию `_router`. `compute_diff_vs_previous` — сравнение с предыдущим прогоном на той же версии (новые fail / новые pass).
|
||
- [x] API: `POST /eval/runs` (фон), `GET /eval/runs`, `GET /eval/runs/{id}`, `GET /eval/router-cases-with-status` (все 1573 кейса + кэш на активной версии).
|
||
|
||
**UI (`static/regression.html` + новая вкладка «Регрессия» в шапках):**
|
||
- [x] Сворачиваемый блок «Выбор кейсов»: фильтр по intent, ввод диапазона (`1-50, 200-300`), кнопки массового выбора (Все / Снять / Только без кэша / Только FAIL в кэше / Снять кэшированные).
|
||
- [x] Таблица 1573 кейсов (отсортированы по count desc): #, чекбокс, запрос, intent, частота, кэш (PASS / FAIL → predicted / —). Цветной фон строки.
|
||
- [x] Счётчик «выбрано N (новых: X, в кэше: Y)»; кнопка «Прогнать выбранное (X новых + Y из кэша)».
|
||
- [x] История прогонов с polling раз в 2 секунды, прогресс-бар, drill-down: все кейсы прогона + фильтр pass/fail + поиск + diff vs предыдущий.
|
||
|
||
### Критерий готовности
|
||
- [x] На пустой версии роутера прогон 50 кейсов за ~1 минуту, повторный — мгновенный.
|
||
- [x] При активации новой версии `_router` — кэш пуст, прогон полный.
|
||
- [x] Diff показывает «новые fail / новые pass» при сравнении с предыдущим прогоном на той же версии.
|
||
|
||
---
|
||
|
||
## Спринт 8b. Регрессия ответов веток (RAG + keywords)
|
||
|
||
### Цель
|
||
По принципу 8a, но проверяем уже не код intent-а от роутера, а **содержимое ответа конкретной ветки** на одиночную реплику. Старт — только `general_info`: «вопрос про адрес / часы / маршрут → ответ должен ссылаться на нужный документ и содержать ключевые слова». Дальше расширим на остальные ветки.
|
||
|
||
### Статус: ✅ Закрыт
|
||
|
||
### Скоуп MVP (что взяли)
|
||
- **Ветка:** `general_info`. JSONL `eval/branch_cases_general_info.jsonl` (46 кейсов).
|
||
- **Способы pass/fail:**
|
||
- **A — RAG-проверка:** среди retrieved-чанков есть кусок с `section == expected_doc_section` (точное совпадение). Если поле не задано — пропускаем.
|
||
- **B — keywords в ответе:** обязательные `expected_keywords` встречаются в `predicted_answer` (case-insensitive). По умолчанию нужны **все**; поддерживаются `keywords_min: N` и `keywords_any: true` (алиас для `keywords_min: 1`). Запрещённые `expected_must_not` — ни одного.
|
||
- **Pass = A ∧ B** (если поле задано). Незаданные поля не проверяются.
|
||
- **Кэш:** `(text_hash, branch_config_id) → {answer_text, retrieved_sections}`. При смене активной версии промпта ветки — кэш по новой версии пуст, прогон полный. При правке полей JSONL без изменения `text` — pass/fail пересчитывается без LLM.
|
||
|
||
### Что осознанно вынесено в `docs/BACKLOG.md`
|
||
- **Вариант C — LLM-judge** (отдельный LLM-вызов оценивает «подходит ли ответ»).
|
||
- **Вариант D — эталон + embeddings** (cosine similarity с эталонным ответом).
|
||
- **Diff vs предыдущий прогон** для веток (для роутера в 8a уже есть).
|
||
- **Кнопка «Сбросить кэш регрессии»** на странице (сейчас инвалидация — через создание новой версии промпта).
|
||
|
||
### Задачи
|
||
|
||
**База кейсов (от пользователя):**
|
||
- [x] `eval/branch_cases_general_info.jsonl` (46 кейсов). Схема: `{text, intent, coverage, expected_doc_section?, expected_keywords?, expected_must_not?, keywords_min?, keywords_any?, count?, note?}`.
|
||
- [x] `coverage` (`covered` / `partial` / `not_covered`) — метаинфо: есть ли материал в RAG. Для `not_covered` keywords обычно `["оператор"]` — бот должен передать живому.
|
||
|
||
**Backend:**
|
||
- [x] Таблицы `eval_branch_runs` / `eval_branch_run_cases` / `eval_branch_predictions`. Миграция `m9g1f7e89j56`.
|
||
- [x] `services/eval_branch_run_service.py`: загрузка JSONL, фоновый прогон, кэш по (`text_hash`, `branch_config_id`), оценка A+B с поддержкой `keywords_min`/`keywords_any`.
|
||
- [x] `chat_service.run_branch_single_turn` — изолированный single-turn без роутера и треда.
|
||
- [x] API: `POST /eval/branch-runs`, `GET /eval/branch-runs`, `GET /eval/branch-runs/{id}`, `GET /eval/branch-cases-with-status?intent_code=`.
|
||
|
||
**UI (`static/regression.html`):**
|
||
- [x] Селектор режима «Роутер / Ветка · general_info» в шапке страницы.
|
||
- [x] Для режима «Ветка»: фильтр по `coverage`, столбцы `секция / coverage`, `keywords` (краткая сводка), `частота`, `кэш`. Drill-down прогона: ожидание (секция / keywords / must_not), retrieved-секции, причины fail, **полный ответ ветки**.
|
||
|
||
**Связанная правка SQLite (нашли при удалении документа):**
|
||
- [x] `db/session.py` — connect-listener `PRAGMA foreign_keys=ON` на каждое подключение. Без этого `ondelete=CASCADE` в SQLite не enforced — удаление документа не очищало подписки в `intent_documents`, и регрессия валилась на пустом RAG.
|
||
- [x] Миграция `n0h2g8f9a0k67` — одноразовая чистка существующих висячих подписок.
|
||
|
||
### Критерий готовности
|
||
- [x] На стартовом наборе `general_info` (46 кейсов) прогон проходит за ~3–5 минут (последовательные LLM-вызовы). Повторный на той же версии — мгновенный.
|
||
- [x] При активации новой версии промпта ветки кэш пуст, прогон полный.
|
||
- [x] Удаление документа на «Отладка» автоматически очищает подписки веток.
|
||
|
||
---
|
||
|
||
## Спринт 8c. Дополнительные регрессионные сценарии
|
||
|
||
### Статус: ⏳ Запланирован (после 8b и накопления кейсов)
|
||
|
||
Темы: handoff между ветками (multi-turn), resumable detour-и-возврат, петли роутера, защитные условия (ребёнок, waitlist), мульти-RAG. Эти сценарии в SPRINTS.md изначально шли в одном Спринте 8 — разделили, чтобы 8a/8b закрыть быстрее.
|
||
|
||
Точечные наборы из исходного плана:
|
||
- [ ] `eval/handoff_cases.jsonl` — 5–10 многошаговых мини-диалогов.
|
||
- [ ] `eval/resumable_cases.jsonl` — 3–5 detour-и-возврат.
|
||
- [ ] `eval/loop_cases.jsonl` — 1–2 искусственная петля.
|
||
- [ ] `eval/guard_cases.jsonl` — `require_legal_rep`, `waitlist`.
|
||
- [ ] `eval/rag_cases.jsonl` — мульти-RAG.
|
||
|
||
---
|
||
|
||
## Спринт 8.5. Чанкер v2 (markdown с иерархией H1/H2/H3)
|
||
|
||
### Цель
|
||
Сделать нарезку wiki-датасетов предсказуемой и совместимой с eval-контрактом «`metadata.section` чанка == заголовок H2 раздела». Триггер — фейл регрессии 8b по тимпанометрии: `expected_doc_section: "Направления приёма"` не находится из-за того, что текущий парсер режет markdown эвристиками без учёта иерархии, склеивает соседей через границы H2 и подмешивает overlap чужой секции. Полное ТЗ — `docs/CHUNKER_v2_TZ.md`.
|
||
|
||
### Статус: ✅ Закрыт
|
||
|
||
### Задачи
|
||
|
||
**Парсинг (`services/document_processor.py::parse_markdown`, ветка `is_markdown=True`):**
|
||
- [x] Перейти на `markdown-it-py` (уже в зависимостях транзитивно), регэкспные эвристики `numbered_heading_re` / `faq_question_re` / ALL-CAPS — отключить для md-входа (для txt оставить как есть).
|
||
- [x] Каждый H2 открывает свою секцию; H3 и ниже идут в тело текущей H2 как строка `### {текст}`.
|
||
- [x] Множественные H1 — штатный кейс (`new_booking.md` имеет 8 H1 — шаги воронки + группы). Каждый H1 группирует свои H2-секции; преамбула H1 (тело до первого H2) игнорируется. Если внутри H1 нет ни одного H2 — H1 сам становится одной секцией с heading=H1. Служебные блоки операторы держат в отдельном файле `docs/wiki_meta_<branch>.md` (вне `data/datasets/`), парсеру их различать не нужно.
|
||
- [x] YAML frontmatter (`--- ... ---` в самом начале файла) распарсить, вернуть отдельным полем `document_metadata`, в текст не пропускать.
|
||
|
||
**Чанкинг (`services/document_processor.py::chunk_sections`):**
|
||
- [x] Резка длинных H2 по абзацам (`\n\n`, не `\n`).
|
||
- [x] В каждый subchunk добавлять breadcrumb-префикс `## {heading H2}` как первую строку. `section` во всех subchunk'ах одинаков.
|
||
- [x] Merge коротких хвостов — только внутри одной H2-секции. Через границу H2 склеивать запрещено.
|
||
- [x] Sentence-overlap — только между subchunk'ами одной H2. Между разными секциями overlap'а нет.
|
||
|
||
**Конфиг (`config.py`):**
|
||
- [x] `excluded_section_headings: list[str] = []` — H2 из этого списка не индексируются (под будущую внешнюю вики).
|
||
|
||
**Тесты (новый каталог `tests/`, на stdlib `unittest` — без новых зависимостей):**
|
||
- [x] `tests/test_document_processor_v2.py`. Запуск: `.venv/bin/python -m unittest tests.test_document_processor_v2 -v` (17 кейсов, все зелёные).
|
||
- `general_info.md` → все `section` непустые, нет ни одного `section`, начинающегося с цифры; чанк, содержащий «тимпанометр», имеет `section == "Направления приёма"`; в каждом чанке первая строка — breadcrumb `## {section}`.
|
||
- `new_booking.md` (8 H1) → секции из всех H1-групп индексируются; точечно проверяем «Тон и формулировки», «Шаблон ответа (5 пунктов)», «Текст-завершение».
|
||
- Файл с frontmatter → frontmatter не утекает в чанки; первый чанк начинается с `## {первый H2}`.
|
||
- Множественные H1 с H2 → секции из всех H1; преамбула H1 (тело до первого H2) выкидывается; WARN на втором H1 не возникает (старое правило отозвано).
|
||
- H1 без H2 → одна секция с heading=H1.
|
||
- H3 внутри H2 → один чанк с `section == H2`, в теле строка `### {H3}`.
|
||
- Длинная H2-секция → N subchunk'ов, у всех одинаковый `section`, у каждого первая строка `## {H2}`.
|
||
- Нумерованный список «1. … 2. …» в md-входе → не парсится как заголовок.
|
||
|
||
### Что не делаем
|
||
- Не трогаем embeddings, reranker, гибридный retrieval, HyDE — отдельные спринты.
|
||
- `parse_pdf` / `parse_docx` не трогаем; если в них всплывут аналогичные проблемы — отдельной итерацией.
|
||
- Формат хранения в Chroma не меняем — `metadata.section` остаётся строкой.
|
||
|
||
### Критерий готовности
|
||
- [x] `python -m unittest tests.test_document_processor_v2 -v` — 17 кейсов, все зелёные.
|
||
- [x] Smoke-прогон чанкера на `data/datasets/general_info.md`: чанк с «тимпанометр» имеет `section == "Направления приёма"`, нет чанков с пустым `section`, нет чанков с `section`, начинающимся с цифры.
|
||
- [x] После переиндексации документа через UI «Отладка» прогон регрессии `branch:general_info` показал PASS на тимпанометрии и других ранее падавших кейсах (подтверждено пользователем 2026-05-04).
|
||
|
||
---
|
||
|
||
## Спринт 8.6. Регрессия остальных веток (price_question, medical_question, escalate_human, reschedule)
|
||
|
||
### Цель
|
||
Расширить регрессию ответов веток (механика 8b) на все остальные ветки, кроме `new_booking`. Бэкенд из 8b уже универсальный — читает `eval/branch_cases_<intent_code>.jsonl` по имени, никаких правок в сервисе. Минимальная работа — добавить опции в селектор режима на странице «Регрессия».
|
||
|
||
`new_booking` намеренно оставлен вне скоупа: это state-machine-ветка с многошаговой воронкой, single-turn регрессия неправильно покажет результат — отдельная задача в Спринте 8c (multi-turn).
|
||
|
||
### Статус: ✅ Закрыт
|
||
|
||
### Задачи
|
||
|
||
**UI (`static/regression.html`):**
|
||
- [x] В select `id="mode-select"` добавлены 4 опции:
|
||
- `<option value="branch:price_question">Ветка · price_question</option>` (40 кейсов)
|
||
- `<option value="branch:medical_question">Ветка · medical_question</option>` (29 кейсов)
|
||
- `<option value="branch:escalate_human">Ветка · escalate_human</option>` (14 кейсов)
|
||
- `<option value="branch:reschedule">Ветка · reschedule</option>` (16 кейсов)
|
||
- [x] `setMode` / `currentBranchIntent()` параметричны (`mode.split(":", 2)[1]`) — правок не потребовалось.
|
||
|
||
**База кейсов (уже в репо):**
|
||
- [x] `eval/branch_cases_price_question.jsonl`
|
||
- [x] `eval/branch_cases_medical_question.jsonl`
|
||
- [x] `eval/branch_cases_escalate_human.jsonl`
|
||
- [x] `eval/branch_cases_reschedule.jsonl`
|
||
|
||
### Критерий готовности
|
||
- [x] На странице «Регрессия» в селекторе режима видны 5 опций веток (general_info + 4 новые).
|
||
- [x] Smoke-прогон через UI для каждой из 4 новых веток — осмысленный ответ + retrieved-секции (подтверждено пользователем 2026-05-04).
|
||
- [x] При активации новой версии промпта ветки кэш для неё пуст — поведение 8b сохраняется параметрически.
|
||
|
||
---
|
||
|
||
## Спринт 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 покажет, что нужно
|