Files
RAG_helper/eval/branch_cases_price_question.jsonl
AR 15 M4 bb5e3f5eb3 feat(sprint8b): регрессия ответов веток · general_info + фикс PRAGMA foreign_keys
Параллель к 8a, но проверяем не код intent от роутера, а содержимое ответа
конкретной ветки на одиночную реплику. Старт — general_info, 46 кейсов.

Логика pass/fail (для одного кейса):
- A — RAG-секция: среди retrieved-чанков есть кусок с
  section == expected_doc_section (точное совпадение). Если поле не задано —
  пропускаем.
- B — keywords: обязательные expected_keywords встречаются в predicted_answer
  (case-insensitive). По умолчанию все; поддерживаются keywords_min: N
  и keywords_any: true. Запрещённые expected_must_not — ни одного.
- Pass = A ∧ B. Незаданные поля не проверяются.
- Кэш: (text_hash, branch_config_id) → {answer_text, retrieved_sections}.
  Привязан к версии промпта ветки. Смена версии = пустой кэш = свежий прогон.
  Правка JSONL без изменения text → pass/fail пересчитывается без LLM.

Backend:
- Таблицы eval_branch_runs / eval_branch_run_cases / eval_branch_predictions.
  Миграция m9g1f7e89j56.
- services/eval_branch_run_service.py: загрузка JSONL, фоновый прогон через
  asyncio.create_task, кэш, оценка A+B с поддержкой keywords_min/keywords_any.
- chat_service.run_branch_single_turn — изолированный single-turn без
  роутера и треда (использует существующий config_service + vectorstore + llm).
- API: POST /eval/branch-runs, GET /eval/branch-runs?intent_code=,
  GET /eval/branch-runs/{id}, GET /eval/branch-cases-with-status?intent_code=.

UI (static/regression.html):
- Селектор режима «Роутер / Ветка · general_info». Логика пикера переиспользуется
  (фильтры, диапазон, массовый выбор, счётчик «новые / в кэше»).
- Для режима «Ветка»: фильтр по coverage, колонки секция/coverage, keywords,
  частота, кэш. Drill-down прогона: ожидание, retrieved-секции, причины fail,
  полный ответ ветки.

База кейсов (eval/branch_cases_general_info.jsonl) — от пользователя, 46 кейсов
по схеме {text, intent, coverage, expected_doc_section?, expected_keywords?,
expected_must_not?, keywords_min?, keywords_any?, count?, note?}.

Связанная правка SQLite (нашли при удалении документа в этом спринте):
- db/session.py: connect-listener PRAGMA foreign_keys=ON на каждое подключение.
  Без этого ondelete=CASCADE в SQLite не enforced, и удаление документа
  оставляло подписки в intent_documents висячими (что давало пустой RAG
  и fail регрессии).
- Миграция n0h2g8f9a0k67 — одноразовая чистка существующих висячих подписок.

docs/SPRINTS.md: Спринт 8b →  Закрыт. Diff vs предыдущий прогон для веток
и кнопка «Сбросить кэш регрессии» вынесены в docs/BACKLOG.md.

Также включены обновлённые data/datasets/general_info.md и price_question.md
(рабочий материал оператора), и черновик eval/branch_cases_price_question.jsonl
для следующего захода (8b на price_question).

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

41 lines
26 KiB
JSON

{"text": "Сколько стоит прием", "intent": "price_question", "expected_keywords": ["1900", "2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "диагноз", "примите", "лечение лекарств", "назнача"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Самая частотная общая формулировка (count=2). Бот должен дать вилку 1900/2300, по правилу промта — упомянуть про эндоскопию +1000 ₽."}
{"text": "Здравствуйте сколько стоит прием у Гашеевой лора?", "intent": "price_question", "expected_keywords": ["1900", "2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "диагноз", "примите", "лечение лекарств", "назнача", "3700", "4300"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Гашеева есть в списке отоларингологов (без особой пометки). Цена 1900 (высшая) или 2300 (КМН/завотделением). В датасете отдельно категория Гашеевой не зафиксирована — допустимо дать вилку или эскалировать."}
{"text": "стоимость приема отоларинголога", "intent": "price_question", "expected_keywords": ["1900", "2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "диагноз", "примите", "лечение лекарств", "назнача"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Без указания врача — должен быть диапазон цен."}
{"text": "Здравствуйте. Сколько стоит первичный прием к врачу Макаровой Людмиле Германовне? У меня тугоухость левого уха.", "intent": "price_question", "expected_keywords": ["1900", "2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "лечение", "назначу", "примите"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Пациент описал симптом (тугоухость) — бот должен дать цену приёма, не ставя диагноз. Промт ветки price_question запрещает медсовет."}
{"text": "Здравствуйте. Хотела бы уточнить сколько будет стоить вторичный прием ЛОРа?", "intent": "price_question", "expected_keywords": ["оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "1900", "2300"], "expected_doc_section": null, "coverage": "not_covered", "note": "Цена повторного приёма ЛОРа в датасете не зафиксирована (это явно отмечено в нижнем разделе «что нужно дополнить»). Бот должен честно эскалировать."}
{"text": "Сколько стоит прием со скидкой по направлению?", "intent": "price_question", "expected_keywords": ["950", "1150", "50%"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Скидка 50% по направлению на лечебную процедуру. Цена 950 (высшая) или 1150 (КМН)."}
{"text": "Здравствуйте, на 2 декабря есть свободная запись к отоларингологу Головач Светлане Вячеславовне ? И сколько будет стоить прием?", "intent": "price_question", "expected_keywords": ["1900", "2300", "оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "partial", "note": "Двойной вопрос: цена + наличие записи на конкретную дату. Цену бот даёт; про запись — должен предложить переход в ветку записи или эскалировать."}
{"text": "Сколько стоит приём у заведующего отделением?", "intent": "price_question", "expected_keywords": ["2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "1900"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Заведующий отделением — категория КМН/завотделением, цена 2300. Если бот даст 1900 — фейл."}
{"text": "Прайс", "intent": "price_question", "expected_keywords": ["оператор", "что", "уточн"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": null, "coverage": "partial", "note": "Однословный запрос «прайс» — слишком общий. Бот должен спросить, какая услуга интересует, или предложить эскалацию."}
{"text": "Стоимость услуг", "intent": "price_question", "expected_keywords": ["оператор", "уточн", "какая"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": null, "coverage": "partial", "note": "Тоже слишком широкий запрос — нужно уточнение."}
{"text": "Сколько стоит удалить серную пробку из уха", "intent": "price_question", "expected_keywords": ["оператор", "приём ЛОР", "дополнительно"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "550"], "expected_doc_section": null, "coverage": "not_covered", "note": "Цена 550 ₽ была только в Пирогова (Краснокамск); филиал закрыт в 2026, прайс убран из активной части датасета. Бот должен эскалировать; допустимо упомянуть, что процедура выполняется на приёме ЛОРа и оплачивается дополнительно."}
{"text": "Здравствуйте! Хотела узнать стоимость удаления аденоидов", "intent": "price_question", "expected_keywords": ["30000", "от"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "covered", "note": "Аденотомия — от 30000 ₽. Должно прозвучать «от» — точную сумму определяет хирург после осмотра."}
{"text": "Здравствуйте,сколько стоит удаление миндалин?", "intent": "price_question", "expected_keywords": ["19800", "40000", "тонзилл"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "covered", "note": "Тонзиллотомия 19800, тонзиллэктомия от 40000. Бот может уточнить, какую именно операцию имеет в виду пациент."}
{"text": "Здравствуйте, сколько стоит УЗИ щитовидной железы и брюшной полости (комплексное)", "intent": "price_question", "expected_keywords": ["оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "рублей"], "expected_doc_section": null, "coverage": "not_covered", "note": "УЗИ щитовидки и брюшной полости — не профильная услуга клиники (УЗИ есть только в Пирогова, и тот закрыт). Бот должен эскалировать."}
{"text": "Стоимость септопластики", "intent": "price_question", "expected_keywords": ["30000", "от"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "covered", "note": "Самый частотный запрос про операции (count=2). От 30000 ₽; со слизистой — дополнительно от 16200 ₽."}
{"text": "Стоимость аденотомии у ребенка ?", "intent": "price_question", "expected_keywords": ["30000", "от", "оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "16500", "21500"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "partial", "note": "Аденотомия от 30000. ВНИМАНИЕ: в датасете расхождение по цене наркоза для аденотомии (16500 vs 21500), бот не должен называть конкретную сумму наркоза — только эскалировать или дать диапазон."}
{"text": "Здравствуйте подскажите пожалуйста сколько стоит темпанопластика на 1 ухо?", "intent": "price_question", "expected_keywords": ["76000", "82600"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "covered", "note": "Тимпанопластика 76000 ₽, 2 типа — 82600 ₽. Нужно дать обе цифры."}
{"text": "Здравствуйте. Подскажите, пожалуйста, стоимость пункции 2 пазух по гайморите?", "intent": "price_question", "expected_keywords": ["2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "covered", "note": "Пункция гайморовой пазухи — 2300 ₽. Пациент про 2 пазухи — допустимо упомянуть, что это за одну, итог — 4600."}
{"text": "Добрый день. Сколько стоит прием Лора для ребенка? Есть ли запись на 18.11?", "intent": "price_question", "expected_keywords": ["1900", "2300"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "детский тариф"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "covered", "note": "Цена приёма для ребёнка такая же, как для взрослого — отдельного детского тарифа в датасете нет. Бот не должен выдумывать «детский тариф»."}
{"text": "Здравствуйте! Скажите пожалуйста сколько стоит удаление серных пробок 1 взрослый и 1 ребенок (13лет)?", "intent": "price_question", "expected_keywords": ["оператор", "приём ЛОР"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "550"], "expected_doc_section": null, "coverage": "not_covered", "note": "Цена 550 ₽ была в Пирогова; филиал закрыт. По Цеткин/Звезде в датасете нет цены процедуры — бот должен эскалировать."}
{"text": "Стоимость удаления аденоидов ребенку", "intent": "price_question", "expected_keywords": ["30000", "от"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стоимость операций (от какой суммы стартует)", "coverage": "covered", "note": "Аденотомия от 30000 ₽. Должно прозвучать «от»."}
{"text": "Сколько стоит удаление серной пробки?", "intent": "price_question", "expected_keywords": ["оператор", "приём ЛОР", "дополнительно"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "550"], "expected_doc_section": null, "coverage": "not_covered", "note": "Самая частотная процедура; цена 550 ₽ была в Пирогова, филиал закрыт. По Цеткин в датасете цены нет — бот эскалирует. Допустимо упомянуть «процедура выполняется на приёме ЛОРа и оплачивается дополнительно»."}
{"text": "Здравствуйте, сколько стоит процедура промывание миндалин?", "intent": "price_question", "expected_keywords": ["1200", "оплачивается дополнительно"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Лечебные процедуры (КУГН, К. Цеткин, 9)", "coverage": "covered", "note": "Промывание лакун миндалин — 1200 ₽. По правилам промта — упомянуть «выполняется на приёме врача и оплачивается дополнительно»."}
{"text": "Добрый день. Сколько стоит в вашей клинике удаление серной пробки + первичная консультация?", "intent": "price_question", "expected_keywords": ["1900", "2300", "оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "550"], "expected_doc_section": "Приём ЛОР-врача (К. Цеткин, 9)", "coverage": "partial", "note": "Пациент сам понимает, что нужен приём + процедура. Цену приёма (1900/2300) бот может назвать; цена самой процедуры в Цеткин не зафиксирована (550 было только в Пирогова, филиал закрыт) — должен эскалировать."}
{"text": "Сурдолог стоимость", "intent": "price_question", "expected_keywords": ["5000", "комплексн"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Приём сурдолога и комплексное обследование слуха", "coverage": "covered", "note": "Комплексное обследование слуха — 5000 ₽. Не путать с консультацией 2100."}
{"text": "Добрый день! Подскажите, пожалуйста, сколько будет стоить повторный прием сурдолога?", "intent": "price_question", "expected_keywords": ["3700"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "5000", "2100"], "expected_doc_section": "Приём сурдолога и комплексное обследование слуха", "coverage": "covered", "note": "Повторный сурдолог 3700 ₽, около часа. Не путать с первичным."}
{"text": "Сурдолог принимает по полису ОМС?", "intent": "price_question", "expected_keywords": ["ОМС", "сурдолог", "да"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "нет, по ОМС не принимаем"], "expected_doc_section": "Приём сурдолога и комплексное обследование слуха", "coverage": "covered", "note": "Сурдолог — единственный в клинике, кто принимает по ОМС. Это явно в промте и в нижнем правиле. Должен быть «да»."}
{"text": "Сколько стоит прием у аллерголога ?", "intent": "price_question", "expected_keywords": ["2400"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Приём аллерголога-иммунолога (Г. Звезда, 31а)", "coverage": "covered", "note": "Очный приём аллерголога — 2400 ₽."}
{"text": "Добрый вечер. Подскажите пожалуйста, стоимость аллергопроб (кожные тесты)?", "intent": "price_question", "expected_keywords": ["3600", "2000", "500"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "Стандартные диагностические процедуры", "coverage": "covered", "note": "Аллергопробы: комплекс 3600, постановка 2000, единичная 500. Хорошо если бот даёт хотя бы одну цену с пояснением."}
{"text": "Здравствуйте. Есть ли у вас детский пульмонолог?? И сколько стоит прием?", "intent": "price_question", "expected_keywords": ["2400", "оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "детский тариф"], "expected_doc_section": "Приём пульмонолога (Г. Звезда, 31а)", "coverage": "partial", "note": "Пульмонолог Абыденков А. В. — без явного указания «детский». Цена 2400 ₽. Про детского можно эскалировать."}
{"text": "Здравствуйте. С какими ДМС работает клирика?", "intent": "price_question", "expected_keywords": ["Адонис", "ВСК", "СОГАЗ", "Согласие", "Росгосстрах"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "Альфа", "Ингосстрах", "Ренессанс"], "expected_doc_section": "ДМС: страховые компании, с которыми сотрудничает клиника", "coverage": "covered", "note": "Должны быть перечислены страховые из списка. Не должно быть тех, кого нет (Альфа, Ингосстрах и т.д.)."}
{"text": "вы сотрудничаете по ДМС с Альфастрахованием?", "intent": "price_question", "expected_keywords": ["нет", "не работаем", "оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "да, работаем", "Альфастрахование"], "expected_doc_section": "ДМС: страховые компании, с которыми сотрудничает клиника", "coverage": "covered", "note": "Альфастрахования НЕТ в списке партнёров. Бот не должен подтверждать."}
{"text": "Добрый день! Подскажите, пожалуйста, возможна ли оплата приема ЛОР врача по ДМС?", "intent": "price_question", "expected_keywords": ["да", "гарантийн", "ДМС"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "нет, не принимаем"], "expected_doc_section": "ДМС: страховые компании, с которыми сотрудничает клиника", "coverage": "covered", "note": "Да, ДМС-приём возможен по гарантийному письму страховой. Опционально — упомянуть про список страховых."}
{"text": "Добрый день! Вы работаете с ОМС?", "intent": "price_question", "expected_keywords": ["сурдолог", "ОМС"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "да, по всем направлениям"], "expected_doc_section": null, "coverage": "covered", "note": "По правилу промта: «По ОМС в данный момент ведёт приём только врач-сурдолог. Остальные направления — платно или по ДМС»."}
{"text": "Добрый день ! Проводится ли в вашей клинике удаление миндалин по полису ОМС?", "intent": "price_question", "expected_keywords": ["нет", "сурдолог"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "да, по ОМС"], "expected_doc_section": null, "coverage": "covered", "note": "Операции по ОМС не проводятся. По ОМС — только сурдолог."}
{"text": "Здравствуйте. Сколько стоит КТ околоносных пазух?", "intent": "price_question", "expected_keywords": ["2500", "2900", "наш", "сторонн"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле"], "expected_doc_section": "КТ-исследование ЛОР-органов (центр ЛорДент, Г. Звезда, 31а)", "coverage": "covered", "note": "Цена зависит от «наш/сторонний» и нужно ли описание. Хорошо, если бот объяснит вилку или уточнит у пациента."}
{"text": "Сколько стоит кт головного мозга ?", "intent": "price_question", "expected_keywords": ["оператор"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "рублей"], "expected_doc_section": null, "coverage": "not_covered", "note": "КТ головного мозга — не профиль клиники (КТ только ЛОР-органов и стоматологии). Бот должен эскалировать."}
{"text": "Добрый день. Подскажите по qr коду можно оплатить?", "intent": "price_question", "expected_keywords": ["оператор", "СБП", "уточн"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "да, по СБП", "да, по QR"], "expected_doc_section": "Способы оплаты", "coverage": "not_covered", "note": "СБП/QR в датасете явно не указано — бот должен эскалировать. Это правило прямо записано в датасете."}
{"text": "Можно картой оплатить?", "intent": "price_question", "expected_keywords": ["да", "терминал"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "нет"], "expected_doc_section": "Способы оплаты", "coverage": "covered", "note": "Терминал есть. Также наличные."}
{"text": "Доброе утро! Скажите пожалуйста, скидки есть инвалидам и пенсионерам", "intent": "price_question", "expected_keywords": ["оператор", "нет"], "expected_must_not": ["дорого", "дёшево", "дешево", "недорого", "выгоднее", "дешевле", "да, есть для пенсионеров", "10%"], "expected_doc_section": "Скидки и условия", "coverage": "covered", "note": "Системных скидок нет (только 50% по направлению). Бот должен либо сказать «нет» с предложением уточнить, либо эскалировать."}