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", ""), rag_subscription=result.get("rag_subscription"), )