Первый шаг графовой архитектуры из 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>
26 KiB
Спринты — 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_promptGET /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) — если свободный текст в промпте будет пропускать реальные случаи смены темы