128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""
|
|
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)
|