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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user