""" Chat endpoint for AI Service. [AC-AISVC-01, AC-AISVC-02, AC-AISVC-06] Main chat endpoint with streaming/non-streaming modes. """ import logging from typing import Annotated, Any from fastapi import APIRouter, Depends, Header, Request from fastapi.responses import JSONResponse from sse_starlette.sse import EventSourceResponse from app.core.middleware import get_response_mode, is_sse_request from app.core.sse import create_error_event from app.core.tenant import get_tenant_id from app.models import ChatRequest, ChatResponse, ErrorResponse from app.services.orchestrator import OrchestratorService, get_orchestrator_service logger = logging.getLogger(__name__) router = APIRouter(tags=["AI Chat"]) @router.post( "/ai/chat", operation_id="generateReply", summary="Generate AI reply", description=""" [AC-AISVC-01, AC-AISVC-02, AC-AISVC-06] Generate AI reply based on user message. Response mode is determined by Accept header: - Accept: text/event-stream -> SSE streaming response - Other -> JSON response """, responses={ 200: { "description": "Success - JSON or SSE stream", "content": { "application/json": {"schema": {"$ref": "#/components/schemas/ChatResponse"}}, "text/event-stream": {"schema": {"type": "string"}}, }, }, 400: {"description": "Invalid request", "model": ErrorResponse}, 500: {"description": "Internal error", "model": ErrorResponse}, 503: {"description": "Service unavailable", "model": ErrorResponse}, }, ) async def generate_reply( request: Request, chat_request: ChatRequest, accept: Annotated[str | None, Header()] = None, orchestrator: OrchestratorService = Depends(get_orchestrator_service), ) -> Any: """ [AC-AISVC-06] Generate AI reply with automatic response mode switching. Based on Accept header: - text/event-stream: Returns SSE stream with message/final/error events - Other: Returns JSON ChatResponse """ tenant_id = get_tenant_id() if not tenant_id: from app.core.exceptions import MissingTenantIdException raise MissingTenantIdException() logger.info( f"[AC-AISVC-06] Processing chat request: tenant={tenant_id}, " f"session={chat_request.session_id}, mode={get_response_mode(request)}" ) if is_sse_request(request): return await _handle_streaming_request(tenant_id, chat_request, orchestrator) else: return await _handle_json_request(tenant_id, chat_request, orchestrator) async def _handle_json_request( tenant_id: str, chat_request: ChatRequest, orchestrator: OrchestratorService, ) -> JSONResponse: """ [AC-AISVC-02] Handle non-streaming JSON request. Returns ChatResponse with reply, confidence, shouldTransfer. """ logger.info(f"[AC-AISVC-02] Processing JSON request for tenant={tenant_id}") try: response = await orchestrator.generate(tenant_id, chat_request) return JSONResponse( content=response.model_dump(exclude_none=True, by_alias=True), ) except Exception as e: logger.error(f"[AC-AISVC-04] Error generating response: {e}") from app.core.exceptions import AIServiceException, ErrorCode if isinstance(e, AIServiceException): raise e from app.core.exceptions import AIServiceException raise AIServiceException( code=ErrorCode.INTERNAL_ERROR, message=str(e), ) async def _handle_streaming_request( tenant_id: str, chat_request: ChatRequest, orchestrator: OrchestratorService, ) -> EventSourceResponse: """ [AC-AISVC-06, AC-AISVC-07, AC-AISVC-08, AC-AISVC-09] Handle SSE streaming request. Yields message events followed by final or error event. """ logger.info(f"[AC-AISVC-06] Processing SSE request for tenant={tenant_id}") async def event_generator(): try: async for event in orchestrator.generate_stream(tenant_id, chat_request): yield event except Exception as e: logger.error(f"[AC-AISVC-09] Streaming error: {e}") yield create_error_event( code="STREAMING_ERROR", message=str(e), ) return EventSourceResponse(event_generator(), ping=15)