82bba34937
- 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>
83 lines
3.4 KiB
Python
83 lines
3.4 KiB
Python
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from db.session import get_session
|
|
from models.requests import ChatRequest
|
|
from models.responses import (
|
|
BounceInfo,
|
|
ChatResponse,
|
|
SourceInfo,
|
|
ThreadStateInfo,
|
|
ValidationEventInfo,
|
|
)
|
|
from services import chat_service
|
|
from services.llm_client import LLMUnavailableError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/chat", tags=["chat"])
|
|
|
|
|
|
@router.post("", response_model=ChatResponse)
|
|
async def chat(req: ChatRequest, session: AsyncSession = Depends(get_session)):
|
|
from main import llm_client, router_client, vectorstore_service
|
|
|
|
if vectorstore_service is None or llm_client is None or router_client is None:
|
|
raise HTTPException(status_code=503, detail="Service not ready")
|
|
|
|
try:
|
|
result = await chat_service.send_message(
|
|
session=session,
|
|
vectorstore=vectorstore_service,
|
|
llm=llm_client,
|
|
router=router_client,
|
|
text=req.text,
|
|
thread_id=req.thread_id,
|
|
top_k=req.top_k,
|
|
temperature=req.temperature,
|
|
max_tokens=req.max_tokens,
|
|
)
|
|
except LookupError as e:
|
|
await session.rollback()
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except LLMUnavailableError as e:
|
|
# Внешний LLM недоступен даже после ретрая — откатываем, чтобы не оставлять
|
|
# «тред-призрак» с одной пользовательской репликой и без ответа ассистента.
|
|
await session.rollback()
|
|
logger.warning("LLM unavailable: %s", e)
|
|
raise HTTPException(
|
|
status_code=503,
|
|
detail="Внешняя модель временно недоступна. Попробуйте ещё раз через минуту.",
|
|
)
|
|
except Exception as e:
|
|
await session.rollback()
|
|
logger.exception("Chat failed")
|
|
raise HTTPException(status_code=500, detail=f"Chat error [{type(e).__name__}]: {e}")
|
|
|
|
return ChatResponse(
|
|
thread_id=result["thread_id"],
|
|
thread_name=result["thread_name"],
|
|
message_id=result["message_id"],
|
|
intent_code=result["intent_code"],
|
|
intent_name=result["intent_name"],
|
|
router_intent_code=result.get("router_intent_code", ""),
|
|
config_version=result["config_version"],
|
|
router_version=result.get("router_version"),
|
|
answer=result["answer"],
|
|
sources=[SourceInfo(**s) for s in result["sources"]],
|
|
model_used=result["model_used"],
|
|
assembled_prompt=result["assembled_prompt"],
|
|
thread_state=ThreadStateInfo(**result["thread_state"]),
|
|
bounces=[BounceInfo(**b) for b in result.get("bounces", [])],
|
|
validation_events=[ValidationEventInfo(**v) for v in result.get("validation_events", [])],
|
|
parse_error=result.get("parse_error"),
|
|
routing_loop_triggered=result.get("routing_loop_triggered", False),
|
|
resumed_from_suspended=result.get("resumed_from_suspended", False),
|
|
message_meta=result.get("message_meta"),
|
|
escalation_reason=result.get("escalation_reason"),
|
|
operator_summary=result.get("operator_summary"),
|
|
router_assembled_prompt=result.get("router_assembled_prompt", ""),
|
|
)
|