74befa484d
Промпты веток (по docs/BRANCH_MAP_AND_PROMPTS_v1.md):
- reschedule.md — полная замена. Одношаговый сценарий из 6 пунктов:
action (cancel/reschedule), patient_name, patient_phone, original_time,
preferred_new_time. Слоты хранит вызывающая система, STATE_JSON не используется.
- price_question.md — добавлены 3 пункта: эндоскопия 1000₽ при первичном
ЛОР-приёме, лечебные процедуры доплачиваются, ОМС только сурдолог
(последний пункт работает только при подтверждении в базе).
- medical_question.md — расширена карта жалоб → специалист (ЛОР / сурдолог /
аллерголог / иммунолог / пульмонолог); добавлен пункт про беременность,
онкологию, психиатрию — мягко сказать «специализированная клиника»,
не предлагать запись.
- general_info.md — добавлены разделы «Отзывы и социальное доказательство»,
«Преимущества клиники», «Сокращения». Условия выхода расширены до 5 интентов.
escalate_human и new_booking не трогаем (escalate — карта говорит «не менять»;
new_booking — отдельный Спринт 7.6 по docs/OPTIMIZATION_CONVERSION_v1.md).
Применение в БД — вручную через UI «Настройки» (вариант A): оператор копирует
текст из .md, сохраняет как новую версию + активирует. Файлы — только seed.
Eval-каркас (заготовка под Спринт 8):
- eval/router_cases_booking.jsonl (875 кейсов new_booking) и
eval/router_cases_other.jsonl (698 кейсов: general_info 295, price 165,
escalate 139, medical 59, reschedule 40). CSV-исходники рядом.
- eval/README.md — формат, глоссарий, что это и зачем.
- routers/eval.py: GET /eval/router-cases?intent_code=...&limit=...
Lazy-кэш, сортировка по count desc, фильтр по expected_intent.
UI Настроек — выбор готового кейса в тест-блоке:
- Полоса «Готовый кейс:» с datalist (поиск по началу строки) + кнопка
«🎲 Случайный» + счётчик кейсов для активной ветки.
- При выборе — текст подставляется в textarea вопроса.
- Загружается при выборе ветки. Если кейсов 0 (для _router, _debug) — скрыто.
- Полная подсистема прогона (run.py, отчёты, baseline) — Спринт 8.
SPRINTS.md:
- Спринт 7 (мульти-RAG, часть A) → ✅ Закрыт (коммит 52b46bc).
- Заведён Спринт 7.5 «Обновление промптов 4 веток» (этот спринт).
- Заведён Спринт 7.6 «Оптимизация воронки new_booking до 4 шагов»
по OPTIMIZATION_CONVERSION_v1.md.
- В идеи на потом: сквозные правила всех веток (BRANCH_MAP §2),
отложенная документация Спринта 7 (docs.html карточка термина,
GRAPH_ARCHITECTURE_v5, README про мульти-RAG).
Также: docs/COMPETITOR_ALEXANDRA_top100.md — рабочие материалы пользователя
по конкурентному боту (NEXTBOT/Александра), используется как baseline для
оптимизации воронки в Спринте 7.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
"""Эндпоинты вокруг eval-наборов (Спринт 8 — заготовка под мини-eval).
|
|
|
|
Сейчас отдаёт только готовые кейсы маршрутизатора для интеграции в тест-блок
|
|
страницы Настроек: оператор может выбрать готовую фразу пациента из реального
|
|
корпуса вместо того, чтобы придумывать руками. Полная подсистема прогона
|
|
(`eval/run.py`, метрики, отчёты) — в Спринте 8.
|
|
"""
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/eval", tags=["eval"])
|
|
|
|
EVAL_DIR = Path(__file__).resolve().parent.parent / "eval"
|
|
ROUTER_CASE_FILES = [
|
|
EVAL_DIR / "router_cases_booking.jsonl",
|
|
EVAL_DIR / "router_cases_other.jsonl",
|
|
]
|
|
|
|
|
|
# Кэш загруженных кейсов: грузим один раз при первом запросе. Файлы JSONL не
|
|
# меняются на лету (это часть репо), поэтому горячая перезагрузка не нужна.
|
|
_router_cases_cache: list[dict] | None = None
|
|
|
|
|
|
def _load_router_cases() -> list[dict]:
|
|
global _router_cases_cache
|
|
if _router_cases_cache is not None:
|
|
return _router_cases_cache
|
|
|
|
all_cases: list[dict] = []
|
|
for path in ROUTER_CASE_FILES:
|
|
if not path.is_file():
|
|
logger.warning("Router case file not found: %s", path)
|
|
continue
|
|
with path.open(encoding="utf-8") as f:
|
|
for line_no, raw in enumerate(f, 1):
|
|
line = raw.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
data = json.loads(line)
|
|
except json.JSONDecodeError as e:
|
|
logger.warning("%s:%d JSON decode error: %s", path.name, line_no, e)
|
|
continue
|
|
all_cases.append(data)
|
|
|
|
_router_cases_cache = all_cases
|
|
logger.info("Loaded %d router eval cases from %d file(s)", len(all_cases), len(ROUTER_CASE_FILES))
|
|
return all_cases
|
|
|
|
|
|
@router.get("/router-cases")
|
|
def list_router_cases(intent_code: str | None = None, limit: int = 500):
|
|
"""Список кейсов маршрутизатора, опционально с фильтром по `expected_intent`.
|
|
|
|
Сортировка — по `count` desc (самые частотные фразы вверху). `limit` ограничивает
|
|
объём, чтобы UI не давился на 800+ опциях datalist.
|
|
"""
|
|
cases = _load_router_cases()
|
|
|
|
if intent_code:
|
|
filtered = [c for c in cases if c.get("expected_intent") == intent_code]
|
|
else:
|
|
filtered = list(cases)
|
|
|
|
filtered.sort(key=lambda c: c.get("count", 0), reverse=True)
|
|
filtered = filtered[:max(1, min(limit, 5000))]
|
|
|
|
items = [
|
|
{
|
|
"text": c.get("text", ""),
|
|
"expected_intent": c.get("expected_intent", ""),
|
|
"count": int(c.get("count", 0) or 0),
|
|
"note": c.get("note") or None,
|
|
}
|
|
for c in filtered
|
|
]
|
|
|
|
return {
|
|
"intent_code": intent_code,
|
|
"total": len(items),
|
|
"cases": items,
|
|
}
|