diff --git a/SPRINTS.md b/SPRINTS.md index 0de65d5..c8f84e3 100644 --- a/SPRINTS.md +++ b/SPRINTS.md @@ -142,60 +142,158 @@ --- -## Спринт 4. Сценарии: сохранение и прогон +## Архитектурный разворот после Спринта 3 (2026-04-23) -### Цель -Позволить операторам сохранять отработанные диалоги из песочницы как «сценарии» (с пометкой «эталон / ок / не ок» и заметкой), а потом прогонять их на текущей конфигурации, чтобы сразу увидеть, не сломалось ли что-то после правок. +После пилота Спринтов 1–3 решили уходить от одного «мега-промпта» ко графовой архитектуре: **роутер намерений + изолированные ветки + state machine + exit conditions**. Подробности — в `GRAPH_ARCHITECTURE.md`. -### Статус: ⏳ Запланирован +**Принятые решения по открытым вопросам:** +- **Фреймворк оркестровки:** пишем вручную на Python. LangGraph/n8n не берём — проект компактный, свой стек работает, не тянем лишних зависимостей. +- **Модель для роутера:** остаёмся на DeepSeek, но `RouterClient` делаем отдельным классом от `LLMClient` — потом сменим модель в одном месте, если станет дорого. +- **Exit conditions:** свободный текст в промпте ветки + независимый роутер на каждой реплике. Если ветка пропустит триггер — роутер подстрахует. +- **Эскалация на человека:** одна ветка `escalate_human` с полем `reason` (`acute_pain` / `surgery` / `angry` / `explicit_request`). Отдельная маршрутизация «куда именно» — задача смежного разработчика при подключении каналов. +- **Confidence score:** не тянем в первый спринт. Роутер всегда возвращает один из intent'ов, при сомнении — `general_info`. После первого живого прогона посмотрим на реальные ошибки. -### Задачи -- [ ] Хранилище (SQLite): `scenarios` (id, name, note, label, messages_json, reference_answers_json, config_version_id) -- [ ] Эндпоинты: - - [ ] `POST /scenarios` — сохранить текущий тред песочницы как сценарий - - [ ] `GET /scenarios`, `GET /scenarios/{id}` - - [ ] `POST /scenarios/{id}/run` — прогнать реплики пациента на текущей активной конфигурации, вернуть новые ответы агента -- [ ] Возможность пометить «правильный ответ оператора» для каждой реплики пациента (эталон) -- [ ] Веб-страница «Сценарии»: - - [ ] список сценариев с метками и датой - - [ ] открытая карточка: реплики пациента, ответы агента при сохранении, опциональные «эталонные ответы» - - [ ] кнопка «Прогнать на текущей конфигурации» → показывает side-by-side: старый ответ / новый ответ - - [ ] счётчик «сколько сценариев остались в статусе ок» после последнего прогона - -### Критерий готовности -- [ ] Оператор может сохранить диалог как сценарий, добавить эталонные ответы, пометить «ок» -- [ ] После изменения промпта прогон сценариев показывает, где ответы расходятся -- [ ] Виден общий счётчик «ок / изменилось» по всей базе сценариев +Старые Спринт 4 (сценарии) и Спринт 5 (экспорт) не удалены — они переехали в Спринт 7 с дополнением под граф (прогон сценариев проверяет маршрутизацию, экспорт — снапшот графа). --- -## Спринт 5. Экспорт конфигурации для внешней интеграции +## Спринт 4. Фундамент графа — `intents` + роутер + переключение веток ### Цель -Зафиксировать API-контракт и упаковать активную конфигурацию так, чтобы другой разработчик мог подключить чат-канал (приложение / МАКС-бот) без обращений к нам. +Заменить «один активный промпт на всё» на «свой промпт на каждую ветку + роутер выбирает ветку на каждой реплике». Это первый шаг к графовой архитектуре из `GRAPH_ARCHITECTURE.md`. ### Статус: ⏳ Запланирован ### Задачи -- [ ] Документация API: `POST /chat` (с `channel_id`, `user_id`, `thread_id`, `text`), `GET /health` -- [ ] Эндпоинт `GET /configs/active/export` — JSON со снапшотом: активный промпт + правила + список документов RAG -- [ ] Инструкция «как подключиться» в README (пример curl-запроса + минимальный webhook-адаптер) -- [ ] Проверка: внешний разработчик может поднять сервис по docker-compose и получить валидный ответ от `/chat` + +**Данные:** +- [ ] Новая таблица `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 + какая ветка ответила ### Критерий готовности -- [ ] README раздел «Как подключить канал» готов -- [ ] Docker-compose поднимается одной командой -- [ ] На заданном тестовом запросе `/chat` возвращает ответ, который мы видим и в веб-песочнице +- [ ] «У меня острая боль» → `medical_question` +- [ ] «Сколько стоит приём» → `price_question` +- [ ] «Как доехать» → `general_info` +- [ ] В отладочной панели «Песочницы» виден intent и какая ветка дала ответ +- [ ] Для каждой ветки можно отдельно править промпт и сохранять версии + +--- + +## Спринт 5. State machine + exit conditions (bouncing) + +### Цель +Научить ветки вести многошаговые скрипты и бесшовно передавать тред в другую ветку, если пациент сменил тему. + +### Статус: ⏳ Запланирован + +### Задачи + +**Данные:** +- [ ] Таблица `thread_state` (thread_id, current_intent, current_step, slots JSON) + +**State machine (первая ветка — `new_booking`):** +- [ ] 6-шаговый скрипт: приветствие → перехват инициативы → мини-интервью по симптому → презентация двух слотов → подтверждение → запись +- [ ] Модель на каждой реплике видит текущий шаг + собранные слоты (имя, симптом, выбранный слот) +- [ ] Переход шагов управляется правилами в промпте ветки («если на шаге 3 пациент назвал время — перейди к шагу 5») + +**Exit conditions и bouncing:** +- [ ] В промпт каждой ветки добавляется блок условий выхода +- [ ] Парсер ответа ассистента ловит служебный сигнал `[INTENT_CHANGE: ]` → останавливает ветку +- [ ] Роутер на каждой реплике: если классификация ≠ текущему `thread_state.current_intent` → `thread_state` сбрасывается, тред идёт в новую ветку с полной историей + +**UI:** +- [ ] В «Песочнице» новый блок «состояние треда»: текущий intent, шаг, собранные слоты +- [ ] История переходов между ветками в рамках треда (timeline) + +### Критерий готовности +- [ ] Сценарий из `GRAPH_ARCHITECTURE.md` («запись → пациент упомянул операцию → хирургия/оператор») проходит без сброса контекста +- [ ] Ветка `new_booking` уверенно ведёт 6-шаговый скрипт на 3+ тестовых диалогах +- [ ] В отладке видна вся цепочка: начальный intent → шаги → смена ветки → финальный intent + +--- + +## Спринт 6. Мульти-RAG + +### Цель +Дать каждой ветке свою коллекцию в Chroma, чтобы детская wiki не засоряла ответы общей записи, а скрипты возражений — ответы по ценам. + +### Статус: ⏳ Запланирован + +### Задачи +- [ ] Рефакторинг `services/vectorstore.py`: фабрика коллекций, `collection_by_intent(intent_code)` вместо единственной `operators_wiki` +- [ ] В `intents` — поле `collection_name` (nullable; если пусто — используется общая `common_wiki`) +- [ ] В UI загрузки документа — селектор «в какую ветку залить (или в общую)» +- [ ] `POST /documents/upload` принимает `intent_code` как опциональный параметр +- [ ] `reindex-all` учитывает коллекции (одна команда — все коллекции) +- [ ] В «Отладке» — фильтр по веткам для просмотра документов + +### Критерий готовности +- [ ] Документ, загруженный в ветку «детский приём», не появляется в retrieval для других веток +- [ ] Общая коллекция `common_wiki` — fallback для веток без собственной базы (например, `general_info`) +- [ ] После переключения ветки в диалоге retrieved-чанки берутся из нужной коллекции + +--- + +## Спринт 7. Сценарии + экспорт графа + +### Цель +То, что изначально планировалось как Спринты 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-запуск --- ## Бэклог -- Раздельные правила по доменам (детский приём / ДМС / взрослый приём) -- A/B сравнение двух версий промпта на одном тест-наборе +- Раздельные правила по доменам — **перекрыто архитектурой: теперь это ветки (`intents`)** +- A/B сравнение двух версий промпта на одном тест-наборе (в рамках одной ветки или между ветками) - Метрики качества ответов (MRR, CSAT по сценариям) - Подсветка цитат источников в ответе агента - Автосинхронизация wiki - Перевод правил из свободного текста в структурированный список (pattern → instruction) - Мультипользовательский режим (несколько операторов одновременно настраивают) - Хранение исходных файлов (`./data/uploads/{document_id}.{ext}` + `source_path` в метаданных Chroma) — чтобы переиндексировать без повторной загрузки и показывать оператору оригинал документа +- Confidence score роутера + clarifying question при низкой уверенности — включить после реального прогона, если будет много ошибок классификации +- Визуализация графа (веток и переходов между ними) — возможно, в виде отдельной панели +- Вынесение роутера на отдельную более дешёвую модель (gpt-4o-mini, локальная Qwen) — когда вызовов станет много +- Структурированные exit conditions (список триггеров с keyword-match) — если свободный текст в промпте будет пропускать реальные случаи смены темы