feat(sprint7.5): обновление промптов 4 веток + eval-каркас и тест-кейсы в UI Настроек

Промпты веток (по 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>
This commit is contained in:
AR 15 M4
2026-04-28 20:49:02 +05:00
parent 52b46bc53e
commit 74befa484d
14 changed files with 3939 additions and 64 deletions
+101
View File
@@ -364,6 +364,40 @@
background: #fafbfd;
border-radius: 4px;
}
.test-query .tq-cases {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
font-size: 12px;
}
.test-query .tq-cases-label {
color: var(--muted);
flex-shrink: 0;
}
.test-query .tq-cases input {
flex: 1;
min-width: 200px;
padding: 5px 9px;
border: 1px solid var(--border);
border-radius: 5px;
font-size: 12.5px;
}
.test-query .tq-cases-btn {
padding: 5px 10px;
font-size: 12px;
border: 1px solid var(--border);
background: var(--panel);
border-radius: 5px;
cursor: pointer;
}
.test-query .tq-cases-btn:hover { background: #f9fafb; }
.test-query .tq-cases-count {
color: var(--muted);
font-size: 11px;
flex-shrink: 0;
}
.test-query textarea {
width: 100%;
min-height: 70px;
@@ -897,6 +931,7 @@ async function selectIntent(code) {
renderEditor();
await refreshVersions(code);
loadDocumentsForCurrentIntent();
loadEvalCasesForCurrentIntent();
}
async function refreshSteps(code) {
@@ -1051,6 +1086,13 @@ function renderTestQueryPanel(intent) {
<div class="test-query">
<h3>Тест-вопрос от пациента <span class="tq-meta">— ветка <code>${esc(intent.code)}</code></span></h3>
${ragHint}
<div class="tq-cases" id="tq-cases-bar" style="display:none;">
<span class="tq-cases-label">Готовый кейс:</span>
<input list="tq-cases-list" id="tq-cases-input" placeholder="— выбрать или начать вводить —" autocomplete="off">
<datalist id="tq-cases-list"></datalist>
<button type="button" class="tq-cases-btn" onclick="pickRandomCase()">🎲 Случайный</button>
<span class="tq-cases-count" id="tq-cases-count"></span>
</div>
<textarea id="tq-text" placeholder="Например: где вы находитесь?"></textarea>
<div class="tq-row">
<label>top_k <input type="number" class="tq-num" id="tq-top-k" value="5" min="1" max="20"></label>
@@ -1076,6 +1118,65 @@ function renderTestQueryPanel(intent) {
`;
}
// Готовые кейсы маршрутизатора для текущей ветки — заполняются loadEvalCasesForCurrentIntent.
let currentEvalCases = [];
async function loadEvalCasesForCurrentIntent() {
const bar = $("tq-cases-bar");
const list = $("tq-cases-list");
const input = $("tq-cases-input");
const count = $("tq-cases-count");
if (!bar || !list || !input || !count) return;
currentEvalCases = [];
if (!currentIntentCode) {
bar.style.display = "none";
return;
}
try {
const r = await api(`/eval/router-cases?intent_code=${encodeURIComponent(currentIntentCode)}&limit=500`);
currentEvalCases = r.cases || [];
} catch (e) {
bar.style.display = "none";
return;
}
if (!currentEvalCases.length) {
bar.style.display = "none";
return;
}
bar.style.display = "";
// datalist: значение = текст кейса. Браузер показывает выпадашку при фокусе/наборе.
list.innerHTML = currentEvalCases.map(c => {
const note = c.count > 1 ? ` (×${c.count})` : "";
return `<option value="${esc(c.text)}">${esc(c.text)}${note}</option>`;
}).join("");
count.textContent = `${currentEvalCases.length} кейсов`;
input.value = "";
// При выборе значения из datalist — копируем в textarea вопроса.
input.oninput = () => {
const picked = currentEvalCases.find(c => c.text === input.value);
if (picked) {
const ta = $("tq-text");
if (ta) {
ta.value = picked.text;
ta.focus();
}
// Сбрасываем поле выбора, чтобы было видно «свободное» состояние.
input.value = "";
}
};
}
function pickRandomCase() {
if (!currentEvalCases.length) return;
const c = currentEvalCases[Math.floor(Math.random() * currentEvalCases.length)];
const ta = $("tq-text");
if (ta) {
ta.value = c.text;
ta.focus();
}
}
async function runTestQuery() {
const intent = intents.find(i => i.code === currentIntentCode);
if (!intent) return;