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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,29 @@ class RouterClient:
|
||||
lines.append(f"{role_ru}: {content}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _format_state_hint(self, snapshot: dict | None) -> str:
|
||||
"""Блок [ТЕКУЩИЙ СЦЕНАРИЙ] для промпта роутера."""
|
||||
if not snapshot:
|
||||
return ""
|
||||
intent = snapshot.get("current_intent_code")
|
||||
if not intent:
|
||||
return ""
|
||||
step = snapshot.get("current_step_code")
|
||||
slots = snapshot.get("slots") or {}
|
||||
lines = [
|
||||
"",
|
||||
"[ТЕКУЩИЙ СЦЕНАРИЙ]",
|
||||
f"Сейчас в диалоге активна ветка: {intent}" + (f", шаг: {step}" if step else ""),
|
||||
]
|
||||
if slots:
|
||||
import json as _json
|
||||
lines.append(f"Собранные слоты: {_json.dumps(slots, ensure_ascii=False)}")
|
||||
lines.append(
|
||||
"Если новая реплика пациента логично продолжает этот сценарий "
|
||||
"или относится к нему косвенно — предпочитай ту же ветку."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _get_system_prompt(self, session: AsyncSession) -> tuple[str, int | None]:
|
||||
"""Активный промпт роутера из БД (ветка _router). Возвращает (prompt, version_or_None)."""
|
||||
pair = await config_service.get_active_config_by_intent_code(
|
||||
@@ -82,15 +105,20 @@ class RouterClient:
|
||||
_, cfg = pair
|
||||
return config_service.compose_full_system_prompt(cfg), cfg.version
|
||||
|
||||
async def classify(self, session: AsyncSession, history: list[dict], text: str) -> dict:
|
||||
async def classify(
|
||||
self, session: AsyncSession, history: list[dict], text: str,
|
||||
snapshot: dict | None = None,
|
||||
) -> dict:
|
||||
"""Классифицировать реплику. Возвращает {code, version} — версия роутера для отладки.
|
||||
|
||||
При сомнении или парсинг-ошибке — general_info (безопасный fallback).
|
||||
"""
|
||||
system_prompt, version = await self._get_system_prompt(session)
|
||||
|
||||
state_hint = self._format_state_hint(snapshot)
|
||||
user_message = (
|
||||
f"История последних реплик:\n{self._format_history(history)}\n\n"
|
||||
f"История последних реплик:\n{self._format_history(history)}"
|
||||
f"{state_hint}\n\n"
|
||||
f"Новая реплика пациента:\n{text}\n\n"
|
||||
f"Код ветки:"
|
||||
)
|
||||
@@ -151,7 +179,11 @@ class RouterClient:
|
||||
escalation_reason = "explicit_request"
|
||||
logger.info("Router v%s: %r → %s%s", version, text[:80], code,
|
||||
f"|{escalation_reason}" if escalation_reason else "")
|
||||
return {"code": code, "version": version, "escalation_reason": escalation_reason}
|
||||
return {
|
||||
"code": code, "version": version,
|
||||
"escalation_reason": escalation_reason,
|
||||
"router_assembled_prompt": f"[system]\n{system_prompt}\n\n[user]\n{user_message}",
|
||||
}
|
||||
|
||||
logger.warning("Router returned unrecognized response %r, falling back to general_info", raw)
|
||||
return {"code": "general_info", "version": version, "escalation_reason": None}
|
||||
return {"code": "general_info", "version": version, "escalation_reason": None, "router_assembled_prompt": ""}
|
||||
|
||||
Reference in New Issue
Block a user