b24e985f82
Первый шаг графовой архитектуры из GRAPH_ARCHITECTURE.md. Заменили
«один активный промпт на всё» на «свой промпт на каждую ветку +
роутер выбирает ветку на каждой реплике».
Данные:
- Новая таблица intents (code, name, description, is_enabled,
order_index). Коды с префиксом `_` — системные (не responder).
- В agent_configs добавлен intent_id (nullable, FK SET NULL); убрана
глобальная уникальность version, вместо неё UniqueConstraint
(intent_id, version) — у каждой ветки свой счётчик версий.
- В messages добавлен intent_id (nullable, FK) — фиксируем, какую
ветку выбрал роутер для каждой реплики.
- Миграция cd0a88ef9080 в batch-режиме (SQLite не умеет ALTER для
constraints напрямую).
Сид:
- Стартовые 7 веток: new_booking, reschedule, price_question,
medical_question, general_info, escalate_human + `_router` как
системная ветка для промпта классификатора.
- Для каждой ветки — свой v1-промпт из prompts/intents/{code}.md.
- migrate_legacy_config_to_general_info: старый v1 из Спринта 3
(без intent_id) переносится на general_info с сохранением версии.
- ensure_seed_intents досиживает недостающие коды, существующие не
трогает — безопасно при добавлении новых веток.
Оркестрация и роутер:
- services/router_client.RouterClient — отдельный класс от LLMClient
(под будущую смену модели на более дешёвую). Метод classify(session,
history, text) возвращает {code, version}. Промпт классификатора
подтягивается из активного конфига ветки `_router`, fallback —
prompts/intents/_router.md. При сомнении/ошибке возвращает
general_info.
- services/chat_service.send_message теперь идёт через router.classify
→ берёт активный конфиг выбранной ветки → llm.chat. В сообщения
пишется intent_id, в треде фиксируется начальный agent_config_id.
В ответе — intent_code, intent_name, config_version, router_version.
API:
- GET /intents, GET /intents/{code}, PATCH /intents/{code} —
список веток со счётчиком версий, получение и переключение
is_enabled.
- /configs теперь требует intent_code как Query-параметр
(GET /configs, GET /configs/active) — выборка версий в рамках
ветки. POST /configs принимает intent_id.
- get_thread_detail JOIN-ит Intent — каждая реплика возвращает
intent_code + intent_name.
UI:
- settings.html переработан в 3-колоночный макет: слева список веток
с подгруппой «Системные» для `_router` (пометка «система» вместо
свитча), в центре редактор промпта/правил активной версии выбранной
ветки, справа список версий с активировать/удалить/загрузить.
Каждая ветка редактируется независимо — своя история версий,
своя активная.
- sandbox.html: у каждой реплики бейдж с intent_code, в отладке новый
блок «Решение роутера» (подсвеченный зелёным) с названием ветки,
версией её активного конфига и версией промпта роутера. Старый
«активная: v1» индикатор убран — он больше не имеет смысла (активная
у каждой ветки своя).
E2E проверено: разные реплики уходят в корректные ветки, каждая
отвечает по своему узкому промпту, промпт роутера редактируется в UI
как v2/v3 и откатывается — классификация сразу использует новую
версию.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
300 lines
26 KiB
Markdown
300 lines
26 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**. Подробности — в `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`. После первого живого прогона посмотрим на реальные ошибки.
|
||
|
||
Старые Спринт 4 (сценарии) и Спринт 5 (экспорт) не удалены — они переехали в Спринт 7 с дополнением под граф (прогон сценариев проверяет маршрутизацию, экспорт — снапшот графа).
|
||
|
||
---
|
||
|
||
## Спринт 4. Фундамент графа — `intents` + роутер + переключение веток
|
||
|
||
### Цель
|
||
Заменить «один активный промпт на всё» на «свой промпт на каждую ветку + роутер выбирает ветку на каждой реплике». Это первый шаг к графовой архитектуре из `GRAPH_ARCHITECTURE.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)
|
||
|
||
### Цель
|
||
Научить ветки вести многошаговые скрипты и бесшовно передавать тред в другую ветку, если пациент сменил тему.
|
||
|
||
### Статус: ⏳ Запланирован
|
||
|
||
### Задачи
|
||
|
||
**Данные:**
|
||
- [ ] Таблица `thread_state` (thread_id, current_intent, current_step, slots JSON)
|
||
|
||
**State machine (первая ветка — `new_booking`):**
|
||
- [ ] 6-шаговый скрипт: приветствие → перехват инициативы → мини-интервью по симптому → презентация двух слотов → подтверждение → запись
|
||
- [ ] Модель на каждой реплике видит текущий шаг + собранные слоты (имя, симптом, выбранный слот)
|
||
- [ ] Переход шагов управляется правилами в промпте ветки («если на шаге 3 пациент назвал время — перейди к шагу 5»)
|
||
|
||
**Exit conditions и bouncing:**
|
||
- [ ] В промпт каждой ветки добавляется блок условий выхода
|
||
- [ ] Парсер ответа ассистента ловит служебный сигнал `[INTENT_CHANGE: <code>]` → останавливает ветку
|
||
- [ ] Роутер на каждой реплике: если классификация ≠ текущему `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-запуск
|
||
|
||
---
|
||
|
||
## Бэклог
|
||
|
||
- Раздельные правила по доменам — **перекрыто архитектурой: теперь это ветки (`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) — если свободный текст в промпте будет пропускать реальные случаи смены темы
|