52b46bc53e
Спринт 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>
264 lines
7.2 KiB
HTML
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|