feat(sprint6b): блок G — умный роутер видит thread_state

- load_snapshot перенесён до вызова router.classify
- RouterClient.classify принимает snapshot; добавляет блок [ТЕКУЩИЙ СЦЕНАРИЙ]
  в промпт роутера: ветка + шаг + слоты + инструкция предпочитать текущую ветку
- Возвращает router_assembled_prompt для отладки
- Промпт _router.md: объяснение блока [ТЕКУЩИЙ СЦЕНАРИЙ] и правило «предпочитай»
- ChatResponse: поле router_assembled_prompt
- Sandbox: раскрывающийся «промпт роутера» в блоке «Решение роутера»

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-26 20:54:17 +05:00
parent 231e1f2d01
commit 82bba34937
6 changed files with 59 additions and 12 deletions
+8 -4
View File
@@ -204,19 +204,22 @@ async def send_message(
rows = (await session.execute(stmt)).scalars().all()
history = [{"role": m.role, "content": m.text} for m in reversed(rows)]
# 1. Роутер — куда направляем.
routing = await router.classify(session=session, history=history, text=text)
# 1a. Снимок состояния — нужен роутеру, чтобы предпочитать текущую ветку.
snapshot = await thread_state_service.load_snapshot(session, thread.id)
# 1b. Роутер — куда направляем.
routing = await router.classify(session=session, history=history, text=text, snapshot=snapshot)
router_code = routing["code"]
router_version = routing.get("version")
escalation_reason: str | None = routing.get("escalation_reason")
router_assembled_prompt: str = routing.get("router_assembled_prompt", "")
# 2. Снимок состояния. Логика выбора effective_code:
# 2. Логика выбора effective_code:
# 2.1. Если есть suspended_intent и роутер вернулся в него — RESUME: восстанавливаем
# прерванный сценарий, очищаем suspended_*, handoff_count=0.
# 2.2. Иначе если диалог идёт по sm-ветке и роутер предлагает другую — sticky:
# НЕ сбрасываем state, передаём LLM [ПОДСКАЗКА РОУТЕРА].
# 2.3. Иначе если prev — не-sm и роутер ведёт в другую ветку — hard-handoff.
snapshot = await thread_state_service.load_snapshot(session, thread.id)
prev_intent_code = snapshot["current_intent_code"]
handoff_count = snapshot.get("handoff_count", 0)
soft_insertion_count = snapshot.get("soft_insertion_count", 0)
@@ -670,6 +673,7 @@ async def send_message(
"message_meta": meta,
"escalation_reason": escalation_reason if served_code == ESCALATE_INTENT_CODE else None,
"operator_summary": operator_summary,
"router_assembled_prompt": router_assembled_prompt,
}