Files
RAG_helper/docs/examples/01_basic_booking_v2.md
T
AR 15 M4 52b46bc53e feat(sprint6c+sprint7): терминология, сверка примеров с кодом, мульти-RAG (часть A)
Спринт 6c — терминология и сверка документации с реальным кодом:
- Словарь терминов в static/docs.html: «маршрутизатор» вместо «роутер»,
  «защитное условие» вместо «guard», «пошаговая ветка» вместо «многошаговая».
  Разделены концепты «намерение» (intent) и «ветка» (branch) с пометкой,
  что в коде они хранятся как одна сущность 1:1.
- Песочница: «Решение маршрутизатора» виден всегда (зелёный/жёлтый),
  счётчик переключений «N из 3» отдельной плашкой, бейджи под словарь.
- Настройки: «Условия перехода» → «Защитные условия (guards, JSON)».
- GRAPH_ARCHITECTURE_v4.md: имена полей thread_state и слоты приведены
  к реальной БД (db/models/thread_state.py) и таксономии промптов шагов
  (prompts/intents/new_booking/steps/). Ссылки на *_v2 примеры. На v3
  поставлена шапка «устарело».
- 4 примера переписаны как *_v2: реальные current_intent_code/
  current_step_code/slots_json, реальные allowed_next без двойных переходов,
  реальная таксономия слотов name/reason/specialist/preferred_time/confirmed.
  Удалены вымышленные CRM tool calls и слоты, которых нет в коде.
- static/example.html — параметризованная страница с навигацией между
  4 примерами; роут GET /api/docs/examples/{name} в main.py отдаёт
  markdown без дублирования файлов.
- Редактирование документов в Отладке: GET/PUT /documents/{id}/raw,
  textarea с переразметкой и обновлением Chroma при сохранении.

Спринт 7, часть A — мульти-RAG через подписку ветка↔документы:
- Миграция: таблица intent_documents (M:N), модель IntentDocument,
  индекс по document_id для обратного поиска.
- API: GET/PUT /intents/{code}/documents и GET/PUT /documents/{id}/intents
  с PUT-семантикой «полный список», атомарно. Сервис
  services/intent_document_service.py.
- Retrieval-фильтр в chat_service: подтягивает document_ids активной
  ветки и передаёт в vectorstore.query(). Дефолт пустой подписки —
  document_ids=[] (= 0 чанков), не «вся коллекция»: пустая подписка
  означает «ветка не настроена», подмешивать случайное хуже, чем
  ничего. vectorstore.query() различает None (нет фильтра) и [] (0).
- UI Настроек: блок «Документы базы знаний» в правом сайдбаре,
  всегда видим независимо от вкладки, сортировка по имени, счётчик
  «N из M», PUT при сохранении.
- UI Отладки: третья кнопка «привязка» рядом с «удалить» —
  раскрывашка со списком веток (галочки), быстрая привязка прямо
  на странице загрузки.
- Песочница: блок «Срез RAG» с подпиской/найдено, ворнинг при пустой
  подписке. Поле rag_subscription в QueryResponse и ChatResponse.
- Системный промпт страницы Отладки переехал в обычную ветку _debug
  («Страница отладки»). Удалён prompts/system_prompt.md и логика
  DEFAULT_SYSTEM_PROMPT в llm_client. routers/query.py подтягивает
  активный конфиг ветки _debug и её подписки. Дефолт пустой подписки
  для _debug — None (вся коллекция), не [] как для пациентских — чтобы
  Отладка работала «из коробки». На странице Отладки info-bar показывает
  активную версию и счётчик подписок, ссылка → Настройки.
- Тест-блок «Тест-вопрос» в центре Настроек: расширил /query
  параметрами intent_code (default _debug), system_prompt (override
  для теста черновика из textarea), disable_rag (для _router).
  Редактор промпта обёрнут в <details open> — можно свернуть до
  одной строки. Под ним — три колонки результата (RAG / промпт /
  ответ). Для _router показывается подсказка про отсутствие RAG.

Документы:
- data/datasets/*.md — наработки по 6 веткам (рабочие материалы оператора).
- docs/BRANCH_MAP_AND_PROMPTS_v1.md, docs/OPTIMIZATION_CONVERSION_v1.md,
  docs/guides/state_machine_and_slots.md.

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

19 KiB
Raw Blame History

Пример 01 v2 · Базовая запись к ЛОР-врачу (happy path)

Версия v2 · 2026-04-27. Переписано под реальный код: имена полей thread_state соответствуют db/models/thread_state.py, набор слотов — реальной таксономии из prompts/intents/new_booking/steps/*.md. Изменения относительно v1 — внизу в Changelog.

Связано с ../architecture/GRAPH_ARCHITECTURE_v4.md, разделы 1–3. Демонстрирует линейный проход машины состояний ветки new_booking: intro → qualify → present → offer_time → book → close. Никаких защитных условий, никаких боковых вопросов, никаких переключений в другие ветки.

О чём этот пример

Взрослый пациент пишет в чат, хочет записаться к ЛОР-врачу с жалобой на горло. Не уточняет конкретного врача, согласен на любое удобное время. Семь реплик, после которых ветка new_booking доходит до шага close и передаёт результат администратору клиники.

На каждой реплике показано:

  • решение маршрутизатора (router) — какое намерение распознано;
  • активная ветка / шаг (current_intent_code / current_step_code) — где мы сейчас;
  • структурированный ответ модели (хвостовой блок STATE_JSON:) — state_after и slots_updated;
  • итоговое состояние треда (thread_state) — что после хода легло в БД.

CRM-инструменты (crm.get_slots, crm.create_booking) и реальный календарь врачей в этом коде ещё не подключены — на шаге book модель только проговаривает собранные данные и получает явное «да», а на close сообщает, что администратор свяжется в течение дня. Реальная интеграция — в идеях на потом (см. docs/SPRINTS.md).

Начальное состояние

Тред только что создан. Записи в thread_state ещё нет — она появится после первого ответа ассистента. Логически это эквивалентно:

{
  "thread_id": 9001,
  "current_intent_code": null,
  "current_step": 0,
  "current_step_code": null,
  "slots": {},
  "handoff_count": 0,
  "soft_insertion_count": 0,
  "suspended_intent": null,
  "resumable_step_code": null,
  "resumable_slots": null
}

Примечание: slots и resumable_slots хранятся в БД как текстовые колонки slots_json / resumable_slots_json. В API распакованы в объекты — так и показываем во всех thread_state ниже.


Реплика 1 · «Здравствуйте, хочу записаться к лору»

Решение маршрутизатора: new_booking. Однозначное упоминание записи + специальности.

Активная ветка / шаг: new_booking / intro. Шаг по умолчанию для свежей пошаговой ветки.

Промпт шага intro (prompts/intents/new_booking/steps/intro.md): представься, узнай имя пациента; не уточняй сразу повод, специальность, время. Слоты этого шага: name.

Структурированный ответ модели:

Здравствуйте! Я виртуальный ассистент клиники, помогу вам записаться. Подскажите, как могу к вам обращаться?

STATE_JSON: {"state_after": "intro", "slots_updated": {}}

Шаг не меняется — мы остаёмся в intro, пока не получили имя. Слоты пустые: специальность «лор» пациент назвал, но intro.md явно запрещает её фиксировать на этом шаге — это работа qualify.

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 0,
  "current_step_code": "intro",
  "slots": {},
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Реплика 2 · «Анна Петровна»

Решение маршрутизатора: new_booking. Намерение не сменилось.

Активная ветка / шаг: new_booking / intro. Имя получено — переход в qualify разрешён (allowed_next: ["intro", "qualify"]).

Структурированный ответ модели:

Очень приятно, Анна Петровна. Подскажите, что вас беспокоит — чтобы я подобрал подходящего специалиста?

STATE_JSON: {"state_after": "qualify", "slots_updated": {"name": "Анна Петровна"}}

Шаг переходит в qualify. Числовой счётчик current_step инкрементируется (это монотонный счётчик хода, нужен для совместимости с историей версий схемы; для логики используется current_step_code).

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 1,
  "current_step_code": "qualify",
  "slots": {
    "name": "Анна Петровна"
  },
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Реплика 3 · «Болит горло уже неделю»

Решение маршрутизатора: new_booking. Жалоба упомянута, но в контексте записи — не самостоятельный медицинский вопрос. По промпту qualify.md модель не уходит в medical_question по одному факту жалобы — это повод для записи, а не для обсуждения симптомов.

Активная ветка / шаг: new_booking / qualify.

Промпт шага qualify: записать жалобу в reason, выразить короткое сочувствие, при отсутствии специалиста — мягко предложить направление по поводу.

Структурированный ответ модели:

Понимаю, боль в горле — это неприятно. С такой жалобой обычно записывают к ЛОР-врачу. Подходит?

STATE_JSON: {"state_after": "qualify", "slots_updated": {"reason": "боль в горле, неделю"}}

Шаг не меняетсяspecialist ещё не зафиксирован, ждём подтверждения. Слот is_child модель не выставляет (пациент назвал ФИО, говорит от первого лица — взрослый).

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 1,
  "current_step_code": "qualify",
  "slots": {
    "name": "Анна Петровна",
    "reason": "боль в горле, неделю"
  },
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Реплика 4 · «Да, к ЛОРу»

Решение маршрутизатора: new_booking.

Активная ветка / шаг: new_booking / qualify. Известны name, reason, specialist — можно переходить в present (allowed_next: ["qualify", "present"]).

Структурированный ответ модели:

Хорошо. Записываю вас к ЛОР-врачу — на приёме доктор осмотрит вас и обратит внимание на боль в горле.

STATE_JSON: {"state_after": "present", "slots_updated": {"specialist": "ЛОР"}}

Шаг переходит в present. На самом шаге present (prompts/intents/new_booking/steps/present.md) новые слоты не собираются — модель использует уже известные name, specialist, reason, чтобы пациент почувствовал, что его услышали.

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 2,
  "current_step_code": "present",
  "slots": {
    "name": "Анна Петровна",
    "reason": "боль в горле, неделю",
    "specialist": "ЛОР"
  },
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Реплика 5 · «Хорошо, давайте»

Решение маршрутизатора: new_booking.

Активная ветка / шаг: new_booking / present. Согласие пациента → переход в offer_time (allowed_next: ["present", "qualify", "offer_time"]).

Промпт шага offer_time: спросить про удобные дни и часы, зафиксировать в preferred_time. Реального календаря нет — конкретные слоты не предлагаем.

Структурированный ответ модели:

Отлично. Подскажите, в какое время вам удобнее — утро, день или вечер? Будни или выходные?

STATE_JSON: {"state_after": "offer_time", "slots_updated": {}}

Шаг переходит в offer_time. Слотов на этом ходе не добавляется — ждём ответа пациента.

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 3,
  "current_step_code": "offer_time",
  "slots": {
    "name": "Анна Петровна",
    "reason": "боль в горле, неделю",
    "specialist": "ЛОР"
  },
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Реплика 6 · «Удобнее утром в будни»

Решение маршрутизатора: new_booking.

Активная ветка / шаг: new_booking / offer_time. Получили preferred_time → переход в book (allowed_next: ["offer_time", "book"]).

Промпт шага book: проговорить пациенту собранные данные (3–4 поля) и получить явное «да».

Структурированный ответ модели:

Записываю: Анна Петровна, к ЛОР-врачу, повод — боль в горле, удобное время — утром в будни. Передам администратору клиники, он подберёт конкретное окно и подтвердит. Всё верно?

STATE_JSON: {"state_after": "book", "slots_updated": {"preferred_time": "утром в будни"}}

Шаг переходит в book. Никаких CRM tool calls на этом шаге сейчас не делается — реальная интеграция с расписанием врачей пока не подключена.

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 4,
  "current_step_code": "book",
  "slots": {
    "name": "Анна Петровна",
    "reason": "боль в горле, неделю",
    "specialist": "ЛОР",
    "preferred_time": "утром в будни"
  },
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Реплика 7 · «Да, всё верно»

Решение маршрутизатора: new_booking.

Активная ветка / шаг: new_booking / book. Явное «да» → можно переходить в close (allowed_next: ["book", "qualify", "offer_time", "close"]).

Промпт шага close: короткое подтверждение, не задавать новых вопросов.

Структурированный ответ модели:

Готово, Анна Петровна. Передаю администратору, он свяжется с вами в течение дня и подтвердит точное время. Спасибо за обращение!

STATE_JSON: {"state_after": "close", "slots_updated": {"confirmed": true}}

Шаг переходит в close. Это финальный шаг ветки — allowed_next: ["close"], дальше идти некуда. Тред физически не закрывается; если пациент напишет что-то ещё, маршрутизатор разберёт реплику и при необходимости откроет новую ветку (general_info, reschedule и т. п.).

thread_state после хода:

{
  "current_intent_code": "new_booking",
  "current_step": 5,
  "current_step_code": "close",
  "slots": {
    "name": "Анна Петровна",
    "reason": "боль в горле, неделю",
    "specialist": "ЛОР",
    "preferred_time": "утром в будни",
    "confirmed": true
  },
  "handoff_count": 0,
  "soft_insertion_count": 0
}

Что показал этот пример

  • Линейный проход машины состояний. Шаги шли строго в порядке intro → qualify → present → offer_time → book → close. Каждая реплика пациента = ровно один переход (или удержание на текущем шаге). Двойных переходов в одном ответе нет — allowed_next каждого шага это запрещает.
  • Слоты накапливаются. На каждом ходе slots_updated содержит только новые/изменённые поля, и они мерджатся с предыдущим состоянием в thread_state.slots. Старые значения не теряются.
  • Маршрутизатор подтверждает ту же ветку. На каждой реплике решение маршрутизатора совпадало с активной веткой new_booking — в Песочнице бейдж «решение маршрутизатора» был зелёным.
  • Защитные условия не сработали. Слот is_child отсутствует (пациент — взрослый) → guard require_legal_rep неактивен. Случай со срабатыванием — в 03_child_patient_guard_v2.md.
  • Никаких реальных tool calls. На шагах book и close модель только проговаривает собранные данные, никаких записей в CRM или календарь — этой интеграции в коде ещё нет.

Что важно проверять в eval-наборе на этом примере

  • Все шаги машины состояний пройдены в правильном порядке (логи state_after на каждом ходе).
  • К моменту шага book в slots заполнены name, reason, specialist, preferred_time. Без любого из этих полей переход в book не должен случаться (валидатор allowed_next его пропустит, но модель не должна стремиться туда без данных).
  • Слот confirmed: true появляется только на шаге close — это маркер успешно завершённой записи.
  • На реплике 3 («Болит горло уже неделю») маршрутизатор НЕ уходит в medical_question — жалоба в контексте записи это не самостоятельный медицинский вопрос. Это типовая ловушка для роутера.

Changelog

v2 → 2026-04-27

Имена полей thread_state приведены к реальной БД (db/models/thread_state.py):

  • current_intentcurrent_intent_code
  • current_step (как строка) → current_step_code. Числовое current_step (счётчик хода) показано отдельно, как оно реально хранится в БД.
  • Добавлен soft_insertion_count (был и в v1 неявно, теперь показан явно во всех thread_state).
  • slots и resumable_slots показаны как объекты с пометкой, что в БД они хранятся текстовыми колонками slots_json / resumable_slots_json.
  • thread_id показан как int (было str «T-9001»).

Слоты приведены к реальной таксономии из prompts/intents/new_booking/steps/*.md:

  • Удалены вымышленные service_mention, patient_name, service, complaint, doctor_preference, time_candidates, time_chosen, branch, booking_id.
  • Используются только реальные: name, reason, specialist, is_child (опционально), preferred_time, confirmed.

Структурированный ответ модели показан в реальном формате (STATE_JSON: в хвосте текста, который парсер вырезает), а не как отдельный JSON-объект.

Сценарий перестроен под реальные allowed_next:

  • В v1 на реплике 6 была двойная склейка offer_time → book → close в одном ответе — это невозможно, модель выбирает один state_after. Теперь шаги идут по одному за реплику.
  • Реплик стало 7 (вместо 7), но сценарий другой: добавлена отдельная реплика про согласие с планом (present → offer_time).

Удалены CRM tool calls (crm.get_slots, crm.create_booking) и связанные с ними branch, booking_id — этой интеграции в коде нет, в v1 они были как иллюстрация будущего. Переехало в идеи на потом в SPRINTS.md.

Терминология: «роутер» → «маршрутизатор», «решение роутера» → «решение маршрутизатора» — выровнено со словарём в static/docs.html.

Содержательно (что показывает пример) — то же: линейный happy path записи без защитных условий и переключений.