feat(sprint5): state machine + bouncing — thread_state и служебные теги
Таблица thread_state (intent, step, slots) ведётся per-thread. В системный
промпт ветки дописывается текущее состояние, LLM возвращает служебный тег
[STATE: step=N; slots={...}] после основного ответа — парсер в chat_service
вырезает его и обновляет состояние. Если ветка решила, что тема ушла в другую,
она выдаёт [INTENT_CHANGE: code] — делаем один повторный вызов LLM с новой
веткой и сброшенным state (bouncing, MAX_BOUNCES=1). Если роутер сам выбрал
другую ветку, чем в thread_state, — state тоже сбрасывается. Промпт new_booking
переписан под 6-шаговый сценарий (имя → повод → специалист → время → подтверждение
→ запись), в «Песочнице» появился блок «Состояние треда» с intent/step/slots
и списком переходов.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+38
-6
@@ -430,6 +430,10 @@
|
||||
<aside class="col-panel">
|
||||
<div class="col-head">Отладка ответа</div>
|
||||
<div class="col-body">
|
||||
<div class="debug-section">
|
||||
<h3>Состояние треда</h3>
|
||||
<div id="debug-state"><div class="mini">— пока пусто —</div></div>
|
||||
</div>
|
||||
<div class="debug-section">
|
||||
<h3>Решение роутера</h3>
|
||||
<div id="debug-router"><div class="mini">— пока пусто —</div></div>
|
||||
@@ -543,8 +547,12 @@ async function openThread(id) {
|
||||
$("chat-title").textContent = d.name;
|
||||
renderMessages(d.messages);
|
||||
const lastAssistant = [...d.messages].reverse().find(m => m.role === "assistant");
|
||||
if (lastAssistant) renderDebug(lastAssistant.sources, lastAssistant.assembled_prompt, lastAssistant.intent_code, lastAssistant.intent_name, null, null);
|
||||
else clearDebug();
|
||||
if (lastAssistant) {
|
||||
renderDebug(lastAssistant.sources, lastAssistant.assembled_prompt, lastAssistant.intent_code, lastAssistant.intent_name, null, null, null);
|
||||
renderState(d.thread_state, []);
|
||||
} else {
|
||||
clearDebug();
|
||||
}
|
||||
refreshThreads();
|
||||
} catch (e) {
|
||||
toast("Ошибка: " + e.message, "err");
|
||||
@@ -596,12 +604,34 @@ function appendMessage(role, text, iso, intentCode, intentName) {
|
||||
}
|
||||
|
||||
/* ---------- отладка ---------- */
|
||||
function renderDebug(sources, prompt, intentCode, intentName, configVersion, routerVersion) {
|
||||
const routerTag = routerVersion != null ? ` · роутер v${routerVersion}` : "";
|
||||
function renderState(state, bounces) {
|
||||
const box = $("debug-state");
|
||||
if (!state || !state.current_intent_code) {
|
||||
box.innerHTML = '<div class="mini">state machine ещё не запускалась</div>';
|
||||
return;
|
||||
}
|
||||
const slotsJson = JSON.stringify(state.slots || {}, null, 2);
|
||||
const bounceHtml = (bounces && bounces.length)
|
||||
? `<div style="margin-top:8px;font-size:11px;">
|
||||
<div style="color:var(--muted);margin-bottom:3px;">переходы в этой реплике:</div>
|
||||
${bounces.map(b => `<div>• <b>${esc(b.from)}</b> → <b>${esc(b.to)}</b>${b.preface ? ` <span style="color:var(--muted);">(«${esc(b.preface).slice(0,60)}»)</span>` : ''}</div>`).join("")}
|
||||
</div>`
|
||||
: "";
|
||||
box.innerHTML = `
|
||||
<div style="font-size:12px;">
|
||||
<div><b>${esc(state.current_intent_code)}</b> · шаг <b>${state.current_step}</b></div>
|
||||
<div class="prompt-box" style="margin-top:6px;max-height:200px;">${esc(slotsJson)}</div>
|
||||
${bounceHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDebug(sources, prompt, intentCode, intentName, configVersion, routerVersion, routerIntentCode) {
|
||||
const bounced = routerIntentCode && intentCode && routerIntentCode !== intentCode;
|
||||
const routerLine = intentCode
|
||||
? `<div style="padding:10px 16px;background:#ecfdf5;font-size:12px;">
|
||||
<div><b>${esc(intentCode)}</b> — ${esc(intentName || '')}${configVersion ? ' · ветка v' + configVersion : ''}</div>
|
||||
${routerVersion != null ? `<div style="color:var(--muted);font-size:11px;margin-top:2px;">классифицировано роутером${routerTag.replace(' · роутер', '')}</div>` : ''}
|
||||
${routerVersion != null ? `<div style="color:var(--muted);font-size:11px;margin-top:2px;">роутер v${routerVersion}${bounced ? ` сказал <b>${esc(routerIntentCode)}</b>, ветка передала управление` : ''}</div>` : ''}
|
||||
</div>`
|
||||
: "";
|
||||
$("debug-router").innerHTML = routerLine || '<div class="mini">— маршрутизация пока не выполнена —</div>';
|
||||
@@ -626,6 +656,7 @@ function renderDebug(sources, prompt, intentCode, intentName, configVersion, rou
|
||||
}
|
||||
|
||||
function clearDebug() {
|
||||
$("debug-state").innerHTML = '<div class="mini">— пока пусто —</div>';
|
||||
$("debug-router").innerHTML = '<div class="mini">— пока пусто —</div>';
|
||||
$("debug-chunks").innerHTML = '<div class="mini">— пока пусто —</div>';
|
||||
$("debug-prompt").innerHTML = '<div class="mini">— пока пусто —</div>';
|
||||
@@ -668,7 +699,8 @@ async function sendMessage() {
|
||||
appendMessage("assistant", r.answer, null, r.intent_code, r.intent_name);
|
||||
$("chat-title").className = "chat-title";
|
||||
$("chat-title").textContent = r.thread_name;
|
||||
renderDebug(r.sources, r.assembled_prompt, r.intent_code, r.intent_name, r.config_version, r.router_version);
|
||||
renderDebug(r.sources, r.assembled_prompt, r.intent_code, r.intent_name, r.config_version, r.router_version, r.router_intent_code);
|
||||
renderState(r.thread_state, r.bounces);
|
||||
refreshThreads();
|
||||
} catch (e) {
|
||||
pending.remove();
|
||||
|
||||
Reference in New Issue
Block a user