Files
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

264 lines
7.2 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Agent for Patients — Пример</title>
<style>
:root {
--bg: #f5f6f8;
--panel: #ffffff;
--border: #e1e4ea;
--muted: #6b7280;
--fg: #111827;
--accent: #2563eb;
--accent-hover: #1d4ed8;
--ok: #16a34a;
--warn: #d97706;
--err: #dc2626;
--chip-bg: #eef2ff;
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--fg);
font-size: 14px;
line-height: 1.6;
display: flex;
flex-direction: column;
}
header {
background: var(--panel);
border-bottom: 1px solid var(--border);
padding: 14px 24px;
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
header h1 { margin: 0; font-size: 16px; font-weight: 600; }
.nav { display: flex; gap: 4px; }
.nav-link {
text-decoration: none;
color: var(--muted);
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.nav-link:hover { background: var(--chip-bg); color: var(--fg); }
.nav-link.active { background: var(--accent); color: #fff; }
main {
flex: 1;
overflow-y: auto;
padding: 24px 24px 80px 24px;
}
article {
max-width: 860px;
margin: 0 auto;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 28px 36px;
}
.breadcrumbs {
max-width: 860px;
margin: 0 auto 14px auto;
font-size: 12.5px;
color: var(--muted);
}
.breadcrumbs a {
color: var(--accent);
text-decoration: none;
}
.breadcrumbs a:hover { text-decoration: underline; }
.examples-nav {
max-width: 860px;
margin: 0 auto 14px auto;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 16px;
display: flex;
flex-wrap: wrap;
gap: 4px;
font-size: 13px;
}
.examples-nav .ex-link {
padding: 6px 10px;
border-radius: 6px;
text-decoration: none;
color: var(--muted);
font-weight: 500;
}
.examples-nav .ex-link:hover { background: var(--chip-bg); color: var(--fg); }
.examples-nav .ex-link.active {
background: var(--accent);
color: #fff;
}
.examples-nav .ex-num {
font-family: var(--mono);
font-size: 11.5px;
opacity: 0.7;
margin-right: 4px;
}
article h1 { font-size: 24px; font-weight: 700; margin: 0 0 6px 0; letter-spacing: -0.02em; }
article h2 {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
margin: 28px 0 10px 0;
padding-bottom: 6px;
border-bottom: 1px solid var(--border);
}
article h3 {
font-size: 15px;
font-weight: 600;
margin: 18px 0 8px 0;
}
article p { margin: 0 0 12px 0; }
article ul, article ol { margin: 0 0 12px 0; padding-left: 22px; }
article li { margin: 4px 0; }
article blockquote {
border-left: 3px solid var(--border);
margin: 8px 0 14px 0;
padding: 4px 12px;
color: var(--muted);
background: #fafbfd;
border-radius: 0 6px 6px 0;
font-size: 13.5px;
}
article blockquote p { margin: 0 0 6px 0; }
article blockquote p:last-child { margin-bottom: 0; }
article code {
background: var(--chip-bg);
color: var(--accent);
padding: 1px 6px;
border-radius: 4px;
font-family: var(--mono);
font-size: 12.5px;
}
article pre {
background: #fafbfd;
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
overflow-x: auto;
font-family: var(--mono);
font-size: 12px;
line-height: 1.55;
margin: 8px 0 16px 0;
}
article pre code { background: none; color: var(--fg); padding: 0; font-size: 12px; }
article hr {
border: none;
border-top: 1px solid var(--border);
margin: 24px 0;
}
article table {
border-collapse: collapse;
width: 100%;
margin: 8px 0 14px 0;
font-size: 13px;
}
article table th, article table td {
border: 1px solid var(--border);
padding: 6px 10px;
text-align: left;
}
article table th {
background: var(--chip-bg);
font-weight: 600;
}
.loading, .err {
max-width: 860px;
margin: 0 auto;
padding: 30px;
text-align: center;
color: var(--muted);
font-size: 13px;
}
.err { color: var(--err); }
</style>
</head>
<body>
<header>
<h1>Chat Agent for Patients</h1>
<nav class="nav">
<a href="/" class="nav-link">Отладка</a>
<a href="/sandbox.html" class="nav-link">Песочница</a>
<a href="/settings.html" class="nav-link">Настройки</a>
<a href="/docs.html" class="nav-link active">Документация</a>
</nav>
</header>
<main>
<div class="breadcrumbs">
<a href="/docs.html">Документация</a> · <span id="bc-title">Разобранный пример</span>
</div>
<nav class="examples-nav" id="ex-nav"></nav>
<article id="content">
<div class="loading">загружаю пример…</div>
</article>
</main>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.9/dist/purify.min.js"></script>
<script>
const EXAMPLES = [
{ id: "01_basic_booking_v2", num: "01", title: "Базовая запись к ЛОР-врачу" },
{ id: "02_price_during_booking_v2", num: "02", title: "Вопрос про цену в середине записи" },
{ id: "03_child_patient_guard_v2", num: "03", title: "Запись ребёнка — защитное условие" },
{ id: "04_general_info_simple_v2", num: "04", title: "Простые информационные запросы" },
];
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function renderNav(activeId) {
$("ex-nav").innerHTML = EXAMPLES.map(e =>
`<a class="ex-link${e.id === activeId ? ' active' : ''}" href="/example.html?id=${esc(e.id)}">
<span class="ex-num">${esc(e.num)}</span>${esc(e.title)}
</a>`
).join("");
}
async function loadExample(id) {
const meta = EXAMPLES.find(e => e.id === id);
if (!meta) {
$("content").innerHTML = `<div class="err">Пример «${esc(id)}» не найден.</div>`;
return;
}
$("bc-title").textContent = `Пример ${meta.num} · ${meta.title}`;
document.title = `Пример ${meta.num} · ${meta.title}`;
try {
const res = await fetch(`/api/docs/examples/${encodeURIComponent(id)}`);
if (!res.ok) throw new Error(`${res.status}`);
const md = await res.text();
marked.setOptions({ breaks: false, gfm: true });
const html = marked.parse(md);
$("content").innerHTML = DOMPurify.sanitize(html);
} catch (e) {
$("content").innerHTML = `<div class="err">Не удалось загрузить пример: ${esc(e.message)}</div>`;
}
}
const params = new URLSearchParams(window.location.search);
const requestedId = params.get("id") || EXAMPLES[0].id;
renderNav(requestedId);
loadExample(requestedId);
</script>
</body>
</html>