docs+ui: страница «Документация», единый стиль заголовков, перевод на оператора
Добавлена /docs.html — обзор мультиагентной системы для оператора. Все термины в формате «русский (english)», жирным: ветка (intent), маршрутизатор (router), пошаговый сценарий (state machine), шаг (step), допустимые переходы (allowed_next), слоты (slots), условия выхода (exit conditions), переключение ветки (hard handoff), удержание в ветке (sticky state machine), структурированный ответ (structured output), отложенный сценарий (suspended/resume), защита от петли (routing loop guard), состояние диалога (thread state). Плюс пошаговая схема обработки реплики и резюме защитных механизмов. Ссылка «Документация» добавлена в шапку всех страниц. Унификация заголовков под стиль «Версии» в правом сайдбаре Настроек: убран uppercase, переход на 13px / var(--fg) / font-weight 600 / зажатый letter-spacing. Применилось к .col-head во всех колонках, .field label в редакторе, .section-header в списке веток, заголовкам столбцов на странице Отладки и заголовкам секций RAG-результата. Бейджи (АКТИВНАЯ, система) оставлены прежними — это статусные метки, не заголовки. Переименование ветки escalate_human для согласованности с русским UI: «Эскалация на оператора» → «Перевод на оператора», описание тоже. Точечная миграция при старте (intent_service.migrate_intent_copy) обновляет существующие записи в БД, только если поле в точности совпадает со старым значением — операторские правки не затираются. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
async with SessionLocal() as session:
|
||||
await intent_service.ensure_seed_intents(session)
|
||||
await intent_service.migrate_intent_copy(session)
|
||||
await config_service.migrate_legacy_config_to_general_info(session)
|
||||
await config_service.ensure_seed_configs(session)
|
||||
await config_service.migrate_exit_conditions_to_field(session)
|
||||
|
||||
@@ -22,7 +22,7 @@ SEED_INTENTS: list[dict] = [
|
||||
{"code": "price_question", "name": "Цены и ДМС", "description": "Вопросы про стоимость услуг, оплату, ДМС."},
|
||||
{"code": "medical_question", "name": "Медицинский вопрос", "description": "Симптомы, лекарства, диагноз — требует врача."},
|
||||
{"code": "general_info", "name": "Общая справка", "description": "Адрес, часы работы, как доехать, общие вопросы."},
|
||||
{"code": "escalate_human", "name": "Эскалация на оператора", "description": "Передача диалога живому оператору."},
|
||||
{"code": "escalate_human", "name": "Перевод на оператора", "description": "Перевод диалога на живого оператора."},
|
||||
{"code": ROUTER_INTENT_CODE, "name": "Маршрутизатор", "description": "Системная ветка: промпт классификатора намерений. Пациенту напрямую не отвечает."},
|
||||
]
|
||||
|
||||
@@ -81,3 +81,38 @@ async def ensure_seed_intents(session: AsyncSession) -> None:
|
||||
if added:
|
||||
await session.commit()
|
||||
logger.info("Seeded %d missing intents", added)
|
||||
|
||||
|
||||
# Точечные переименования: пользователь правит UI-копию, но в БД уже залит
|
||||
# старый сид. Применяем мягко — только если поле в точности совпадает со старым
|
||||
# значением (значит оператор не правил его сам).
|
||||
_INTENT_NAME_MIGRATIONS: list[dict] = [
|
||||
{
|
||||
"code": "escalate_human",
|
||||
"old_name": "Эскалация на оператора",
|
||||
"new_name": "Перевод на оператора",
|
||||
"old_description": "Передача диалога живому оператору.",
|
||||
"new_description": "Перевод диалога на живого оператора.",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def migrate_intent_copy(session: AsyncSession) -> None:
|
||||
"""Обновляет name/description у системных веток, если в БД лежит старый текст."""
|
||||
updated = 0
|
||||
for spec in _INTENT_NAME_MIGRATIONS:
|
||||
intent = await get_intent_by_code(session, spec["code"])
|
||||
if intent is None:
|
||||
continue
|
||||
changed = False
|
||||
if intent.name == spec["old_name"]:
|
||||
intent.name = spec["new_name"]
|
||||
changed = True
|
||||
if intent.description == spec["old_description"]:
|
||||
intent.description = spec["new_description"]
|
||||
changed = True
|
||||
if changed:
|
||||
updated += 1
|
||||
if updated:
|
||||
await session.commit()
|
||||
logger.info("Migrated copy for %d intent(s)", updated)
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
<!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: 32px 24px 80px 24px;
|
||||
}
|
||||
article {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
article h1 { font-size: 26px; font-weight: 700; margin: 0 0 6px 0; letter-spacing: -0.02em; }
|
||||
article .lead {
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
article h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 36px 0 12px 0;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
article h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 20px 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 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; }
|
||||
|
||||
/* Карточка термина */
|
||||
.term-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.term-card .term-head {
|
||||
font-size: 15px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.term-card .term-head strong { font-weight: 700; }
|
||||
.term-card .term-en {
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
.term-card .term-body {
|
||||
font-size: 13.5px;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* Схема пошагово */
|
||||
.flow-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
margin: 12px 0 18px 0;
|
||||
}
|
||||
.flow-step {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.flow-step:last-child { border-bottom: none; }
|
||||
.flow-num {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.flow-step b { font-size: 14px; }
|
||||
.flow-step .flow-text { font-size: 13.5px; color: var(--fg); }
|
||||
.flow-step .flow-text .muted { color: var(--muted); font-size: 13px; }
|
||||
|
||||
/* TOC */
|
||||
.toc {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.toc strong { display: block; margin-bottom: 6px; font-size: 13px; }
|
||||
.toc ol { margin: 0; padding-left: 22px; }
|
||||
.toc a { color: var(--accent); text-decoration: none; }
|
||||
.toc a:hover { text-decoration: underline; }
|
||||
|
||||
.callout {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fde68a;
|
||||
color: #78350f;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13.5px;
|
||||
margin: 10px 0 16px 0;
|
||||
}
|
||||
</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>
|
||||
<article>
|
||||
|
||||
<h1>Как работает мультиагентная система</h1>
|
||||
<p class="lead">Здесь объясняется, что такое <b>ветка (intent)</b>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</p>
|
||||
|
||||
<div class="toc">
|
||||
<strong>Содержание</strong>
|
||||
<ol>
|
||||
<li><a href="#why">Зачем это всё</a></li>
|
||||
<li><a href="#terms">Главные термины</a></li>
|
||||
<li><a href="#flow">Что происходит на каждой реплике</a></li>
|
||||
<li><a href="#guards">Защитные механизмы</a></li>
|
||||
<li><a href="#where">Где что настраивается</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2 id="why">Зачем это всё</h2>
|
||||
<p>На пилоте у нас был «один большой системный промпт» — модель пыталась одновременно записывать на приём, отвечать на вопросы по ценам и эскалировать острые случаи. По мере усложнения скрипта запись начинала «плыть»: модель забывала шаги, путала ветки, перескакивала через мини-интервью.</p>
|
||||
<p>Мы перешли на <b>графовую архитектуру (graph-based routing)</b>: реплика пациента сначала идёт в <b>маршрутизатор (router)</b>, который определяет тему, а потом — в <b>ветку (intent)</b>, отвечающую только за свой узкий сценарий. У сложных веток внутри есть собственный <b>пошаговый сценарий (state machine)</b>.</p>
|
||||
|
||||
<h2 id="terms">Главные термины</h2>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Ветка</strong> <span class="term-en">(intent)</span></div>
|
||||
<div class="term-body">Изолированный «под-агент» с собственным системным промптом. Отвечает за одну тему: запись, перенос, цены, медицинский вопрос, общая справка, перевод на оператора. У каждой ветки — свой код (<code>new_booking</code>, <code>price_question</code> и т. п.) и активная версия настроек.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Маршрутизатор</strong> <span class="term-en">(router)</span></div>
|
||||
<div class="term-body">Системная ветка <code>_router</code>: отдельный, дешёвый вызов модели, который по последней реплике пациента возвращает один из кодов веток. Не отвечает пациенту напрямую — только классифицирует. Вызывается на КАЖДОЙ реплике, не один раз при входе.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Пошаговый сценарий</strong> <span class="term-en">(state machine)</span></div>
|
||||
<div class="term-body">Внутренний граф шагов внутри ветки. Сейчас есть только у <code>new_booking</code>: 6 шагов от приветствия до подтверждения записи. Модель на каждой реплике видит, на каком шаге сейчас, и какие <b>слоты</b> уже собраны.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Шаг</strong> <span class="term-en">(step)</span></div>
|
||||
<div class="term-body">Узел пошагового сценария. У каждого шага свой код (<code>intro</code>, <code>qualify</code>, <code>present</code>, <code>offer_time</code>, <code>book</code>, <code>close</code>), свой кусок промпта и список <b>допустимых переходов</b>.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Допустимые переходы</strong> <span class="term-en">(allowed_next)</span></div>
|
||||
<div class="term-body">Список кодов шагов, в которые можно перейти с текущего. Например, с <code>qualify</code> разрешено в <code>qualify</code> (остаться) или <code>present</code> (двигаться вперёд), но не в <code>close</code>. Если модель попытается перепрыгнуть через шаг — <b>валидатор переходов (transition validator)</b> отклонит запрос, мы останемся на шаге.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Слоты</strong> <span class="term-en">(slots)</span></div>
|
||||
<div class="term-body">JSON-словарь данных, которые ветка собирает по ходу разговора. Для записи это <code>name</code>, <code>reason</code>, <code>specialist</code>, <code>preferred_time</code>, <code>confirmed</code>. Модель на каждой реплике их видит и обновляет — старые не переспрашиваются.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Условия выхода</strong> <span class="term-en">(exit conditions)</span></div>
|
||||
<div class="term-body">Список ситуаций, когда ветка должна вместо обычного ответа выдать служебный сигнал <code>[INTENT_CHANGE: <код_ветки>]</code> и передать диалог другой ветке. Например, если пациент в записи упомянул хирургию — ветка <code>new_booking</code> сама вернёт <code>[INTENT_CHANGE: escalate_human]</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Переключение ветки</strong> <span class="term-en">(hard handoff)</span></div>
|
||||
<div class="term-body">Полная смена ветки внутри одного диалога с обнулением шага и слотов. Бывает в двух случаях: ветка сама выдала <code>[INTENT_CHANGE]</code> или маршрутизатор предложил другую ветку, которая не имеет пошагового сценария.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Удержание в ветке</strong> <span class="term-en">(sticky state machine)</span></div>
|
||||
<div class="term-body">Защита от ложных переключений: если диалог идёт по пошаговому сценарию (например, <code>new_booking · qualify</code>) и маршрутизатор на короткой реплике («Алексей», «болит ухо») предлагает другую ветку — мы НЕ сбрасываем состояние, а передаём модели подсказку «маршрутизатор думает X, но ты в Y». Модель сама решает: остаться в сценарии (заполнить слот) или явно выйти через <code>[INTENT_CHANGE]</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Структурированный ответ ветки</strong> <span class="term-en">(structured output)</span></div>
|
||||
<div class="term-body">Каждая sm-ветка возвращает не только текст пациенту, но и служебный JSON-блок в хвосте ответа:
|
||||
<pre><code>STATE_JSON: {"state_after": "qualify", "slots_updated": {"name": "Алексей"}}</code></pre>
|
||||
Парсер вырезает этот блок (пациент его не видит), валидатор проверяет легальность <code>state_after</code>, обновляет состояние диалога.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Отложенный сценарий</strong> <span class="term-en">(suspended intent / resume)</span></div>
|
||||
<div class="term-body">Если из пошаговой ветки произошёл переход в другую (например, посреди записи спросили про цены), её состояние (текущий шаг и собранные слоты) запоминается в полях <code>suspended_intent</code>, <code>resumable_step_code</code>, <code>resumable_slots</code>. Когда маршрутизатор увидит, что пациент возвращается к исходной теме («ладно, продолжаем запись»), мы автоматически восстановим шаг и слоты.</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Защита от петли</strong> <span class="term-en">(routing loop guard)</span></div>
|
||||
<div class="term-body">Счётчик <code>handoff_count</code> в состоянии диалога считает все переключения ветки. При превышении 3 переключений за диалог следующее переключение блокируется: диалог автоматически уходит в <code>escalate_human</code> с шаблонным ответом «Уточню детали с администратором клиники, свяжемся с вами в течение ближайшего часа». Это страховка от циклов вроде «запись ↔ цены ↔ запись ↔ цены».</div>
|
||||
</div>
|
||||
|
||||
<div class="term-card">
|
||||
<div class="term-head"><strong>Состояние диалога</strong> <span class="term-en">(thread state)</span></div>
|
||||
<div class="term-body">Запись в БД (одна на диалог), хранящая текущую ветку, текущий шаг (если есть), собранные слоты, <code>handoff_count</code> и поля отложенного сценария. Видно в Песочнице справа, в блоке «Состояние диалога».</div>
|
||||
</div>
|
||||
|
||||
<h2 id="flow">Что происходит на каждой реплике</h2>
|
||||
<div class="flow-card">
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">1</div>
|
||||
<div class="flow-text">
|
||||
<b>Маршрутизатор классифицирует реплику.</b>
|
||||
<div class="muted">Отдельный вызов модели с короткой системой и историей. Возвращает один код ветки.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">2</div>
|
||||
<div class="flow-text">
|
||||
<b>Проверяем отложенный сценарий.</b>
|
||||
<div class="muted">Если в состоянии диалога есть <code>suspended_intent</code> и маршрутизатор вернул именно его — восстанавливаем шаг и слоты, очищаем поля, обнуляем счётчик переключений.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">3</div>
|
||||
<div class="flow-text">
|
||||
<b>Применяем удержание в ветке.</b>
|
||||
<div class="muted">Если диалог уже идёт по sm-ветке и маршрутизатор предлагает другую — состояние не сбрасываем, в системный промпт ветки добавляется блок <code>[ПОДСКАЗКА РОУТЕРА]</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">4</div>
|
||||
<div class="flow-text">
|
||||
<b>Если переключение всё-таки происходит — инкрементим счётчик.</b>
|
||||
<div class="muted">При превышении 3 — авто-перевод на оператора, без вызова модели.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">5</div>
|
||||
<div class="flow-text">
|
||||
<b>Собираем системный промпт ветки.</b>
|
||||
<div class="muted">Базовый промпт + промпт текущего шага (если есть) + блок текущего состояния (шаг, слоты, подсказка). Зовём модель.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">6</div>
|
||||
<div class="flow-text">
|
||||
<b>Парсим ответ.</b>
|
||||
<div class="muted">Если есть <code>[INTENT_CHANGE]</code> — переключаемся в новую ветку (если из sm-ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть <code>STATE_JSON:</code> — валидируем переход, обновляем шаг и сливаем слоты.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-num">7</div>
|
||||
<div class="flow-text">
|
||||
<b>Сохраняем сообщение пациента, ответ модели и обновлённое состояние диалога.</b>
|
||||
<div class="muted">Всё одной транзакцией. Если что-то упадёт — откатываем целиком, «диалог-призрак» в списке не появится.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 id="guards">Защитные механизмы</h2>
|
||||
<ul>
|
||||
<li><b>Удержание в ветке</b> защищает от ложного сброса сценария на коротких репликах вроде «Алексей» или «болит ухо».</li>
|
||||
<li><b>Валидатор переходов</b> блокирует «прыжки через шаг» — модель не сможет уйти из <code>intro</code> сразу в <code>book</code>.</li>
|
||||
<li><b>Защита от петли</b> ограничивает число переключений ветки за диалог. После 3-го — авто-перевод на оператора.</li>
|
||||
<li><b>Отложенный сценарий</b> возвращает прерванный сценарий с теми же слотами и шагом — пациент не должен повторять имя или повод.</li>
|
||||
<li><b>Ретрай LLM</b>: и маршрутизатор, и ветка делают один повтор при сетевом сбое DeepSeek. При полном падении — откат транзакции и понятный ответ «модель временно недоступна».</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="where">Где что настраивается</h2>
|
||||
<ul>
|
||||
<li><a href="/settings.html">Настройки</a> — список веток, активные версии промптов, поля «Системный промпт», «Правила», «Условия выхода». Для веток с пошаговым сценарием — вкладка «Шаги» с редактором каждого шага и его допустимых переходов.</li>
|
||||
<li><a href="/sandbox.html">Песочница</a> — живые диалоги от лица пациента. В правой панели видны: состояние диалога (ветка, шаг, слоты, счётчик переключений, отложенный сценарий), решение маршрутизатора, RAG-фрагменты и собранный системный промпт.</li>
|
||||
<li><a href="/">Отладка</a> — база знаний (загрузка / переразметка документов), одиночные тестовые вопросы без памяти диалога.</li>
|
||||
</ul>
|
||||
|
||||
<div class="callout">
|
||||
Документ описывает текущее состояние после Спринта 6a. Перевод на оператора с указанием причины (acute_pain / surgery / routing_loop / …), сводка для оператора и умный маршрутизатор, видящий состояние диалога — в Спринте 6b.
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+7
-8
@@ -125,10 +125,9 @@
|
||||
}
|
||||
th {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--fg);
|
||||
font-size: 13px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr.doc-row { cursor: pointer; }
|
||||
@@ -250,11 +249,10 @@
|
||||
}
|
||||
.col h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: var(--fg);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.chunk {
|
||||
border: 1px solid var(--border);
|
||||
@@ -343,6 +341,7 @@
|
||||
<a href="/" class="nav-link active">Отладка</a>
|
||||
<a href="/sandbox.html" class="nav-link">Песочница</a>
|
||||
<a href="/settings.html" class="nav-link">Настройки</a>
|
||||
<a href="/docs.html" class="nav-link">Документация</a>
|
||||
</nav>
|
||||
<span class="status"><span class="dot" id="dot"></span><span id="status-text">проверяю…</span></span>
|
||||
<span class="stats" id="stats"></span>
|
||||
|
||||
+55
-39
@@ -93,14 +93,25 @@
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.col-panel:last-child { border-right: none; border-left: 1px solid var(--border); }
|
||||
.col-panel:last-child {
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
/* Правая панель — стек карточек на сером фоне */
|
||||
.col-panel:last-child .col-body {
|
||||
padding: 14px 14px 18px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.col-head {
|
||||
padding: 12px 16px;
|
||||
padding: 14px 16px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -300,53 +311,56 @@
|
||||
.chat-input button:hover { background: var(--accent-hover); }
|
||||
.chat-input button:disabled { background: var(--muted); cursor: not-allowed; }
|
||||
|
||||
/* Правая панель — отладка */
|
||||
.debug-section { padding: 14px 16px; border-bottom: 1px solid var(--border); }
|
||||
/* Правая панель — карточки */
|
||||
.debug-section {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.debug-section h3 {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
/* Сворачиваемая секция (details/summary) */
|
||||
/* Сворачиваемая секция (details/summary) с тем же видом, что и обычная карточка */
|
||||
.debug-section.collapsible > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.debug-section.collapsible[open] > summary { margin: 0 0 10px 0; }
|
||||
.debug-section.collapsible > summary::-webkit-details-marker { display: none; }
|
||||
.debug-section.collapsible > summary::before {
|
||||
content: "▸";
|
||||
display: inline-block;
|
||||
transition: transform 0.15s;
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.debug-section.collapsible[open] > summary::before { transform: rotate(90deg); }
|
||||
.debug-section.collapsible > summary:hover { color: var(--fg); }
|
||||
.debug-section.collapsible > summary .summary-count {
|
||||
.debug-section.collapsible > summary::after {
|
||||
content: "⌄";
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
color: var(--muted);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.debug-section.collapsible[open] > summary::after { transform: rotate(180deg); }
|
||||
.debug-section.collapsible > summary:hover { color: var(--accent); }
|
||||
.debug-section.collapsible > summary .summary-count {
|
||||
background: var(--chip-bg);
|
||||
color: var(--accent);
|
||||
padding: 1px 7px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chunk-card {
|
||||
background: var(--panel);
|
||||
background: #fafbfd;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
@@ -408,7 +422,7 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
.prompt-box {
|
||||
background: var(--panel);
|
||||
background: #fafbfd;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 12px;
|
||||
@@ -464,6 +478,7 @@
|
||||
<a href="/" class="nav-link">Отладка</a>
|
||||
<a href="/sandbox.html" class="nav-link active">Песочница</a>
|
||||
<a href="/settings.html" class="nav-link">Настройки</a>
|
||||
<a href="/docs.html" class="nav-link">Документация</a>
|
||||
</nav>
|
||||
<span class="status" style="margin-left:auto;"><span class="dot" id="dot"></span><span id="status-text">проверяю…</span></span>
|
||||
</header>
|
||||
@@ -494,7 +509,6 @@
|
||||
</section>
|
||||
|
||||
<aside class="col-panel">
|
||||
<div class="col-head">Отладка ответа</div>
|
||||
<div class="col-body">
|
||||
<div class="debug-section">
|
||||
<h3>Состояние диалога</h3>
|
||||
@@ -511,10 +525,12 @@
|
||||
</summary>
|
||||
<div id="debug-chunks"><div class="mini">— пока пусто —</div></div>
|
||||
</details>
|
||||
<div class="debug-section">
|
||||
<h3>Собранный промпт</h3>
|
||||
<details class="debug-section collapsible" id="debug-prompt-section">
|
||||
<summary>
|
||||
<span>Собранный промпт</span>
|
||||
</summary>
|
||||
<div id="debug-prompt"><div class="mini">— пока пусто —</div></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
+37
-20
@@ -72,15 +72,30 @@
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.col-panel:last-child { border-right: none; border-left: 1px solid var(--border); }
|
||||
.col-head {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
.col-panel:last-child {
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
}
|
||||
/* Правая колонка Настроек — серый фон, заголовок без рамки и стек карточек */
|
||||
.col-panel:last-child > .col-head {
|
||||
border-bottom: none;
|
||||
padding: 16px 14px 8px 14px;
|
||||
}
|
||||
.col-panel:last-child > .col-head #versions-intent {
|
||||
color: var(--muted);
|
||||
font-weight: normal;
|
||||
}
|
||||
.col-panel:last-child > .col-body {
|
||||
padding: 0 14px 18px 14px;
|
||||
}
|
||||
.col-head {
|
||||
padding: 14px 16px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
color: var(--fg);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.col-body {
|
||||
flex: 1;
|
||||
@@ -91,9 +106,7 @@
|
||||
/* Список веток */
|
||||
.section-header {
|
||||
padding: 10px 16px 6px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
background: #fafbfd;
|
||||
@@ -182,10 +195,11 @@
|
||||
.field { margin-bottom: 14px; position: relative; }
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
letter-spacing: -0.01em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.field label.with-hint {
|
||||
display: flex;
|
||||
@@ -313,16 +327,18 @@
|
||||
}
|
||||
|
||||
/* Версии */
|
||||
.versions { padding: 10px; }
|
||||
.versions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.version-card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
background: #fafbfd;
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.version-card.active {
|
||||
background: #fff;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent);
|
||||
}
|
||||
@@ -486,6 +502,7 @@
|
||||
<a href="/" class="nav-link">Отладка</a>
|
||||
<a href="/sandbox.html" class="nav-link">Песочница</a>
|
||||
<a href="/settings.html" class="nav-link active">Настройки</a>
|
||||
<a href="/docs.html" class="nav-link">Документация</a>
|
||||
</nav>
|
||||
<span class="stats" id="stats"></span>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user