"""Persistent assistant conversation endpoints.""" import json from typing import Annotated from fastapi import APIRouter, Depends, Query from fastapi.responses import StreamingResponse from app.api.dependencies import DatabaseSession, require_permission from app.models.entities import UserAccount from app.schemas.common import ApiResponse from app.schemas.conversation import ( ConversationCreateRequest, ConversationData, ConversationMessageCreateRequest, ConversationSummary, ) from app.services.conversations import ConversationService router = APIRouter(prefix="/conversations", tags=["conversations"]) @router.get("", response_model=ApiResponse[list[ConversationSummary]]) async def list_conversations( session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess"))], limit: int = Query(50, ge=1, le=100), search: str | None = Query(default=None, max_length=120), ) -> ApiResponse[list[ConversationSummary]]: return ApiResponse(data=await ConversationService(session).list(current_user, limit=limit, search=search)) @router.post("", response_model=ApiResponse[ConversationData]) async def create_conversation( payload: ConversationCreateRequest, session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], ) -> ApiResponse[ConversationData]: return ApiResponse(data=await ConversationService(session).create(current_user, payload.title)) @router.get("/{conversation_id}", response_model=ApiResponse[ConversationData]) async def get_conversation( conversation_id: int, session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess"))], limit: int = Query(20, ge=1, le=20), offset: int = Query(0, ge=0), ) -> ApiResponse[ConversationData]: return ApiResponse( data=await ConversationService(session).get( conversation_id, current_user, limit=limit, offset=offset, ) ) @router.delete("/{conversation_id}", response_model=ApiResponse[None]) async def delete_conversation( conversation_id: int, session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], ) -> ApiResponse[None]: await ConversationService(session).delete(conversation_id, current_user) return ApiResponse(data=None, message="对话已删除") @router.post("/{conversation_id}/messages", response_model=ApiResponse[ConversationData]) async def send_conversation_message( conversation_id: int, payload: ConversationMessageCreateRequest, session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], ) -> ApiResponse[ConversationData]: return ApiResponse( data=await ConversationService(session).send( conversation_id, payload.content, current_user, message_type=payload.message_type, ), ) @router.delete("/{conversation_id}/messages/{message_id}", response_model=ApiResponse[ConversationData]) async def withdraw_conversation_message( conversation_id: int, message_id: int, session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], ) -> ApiResponse[ConversationData]: return ApiResponse( data=await ConversationService(session).withdraw_turn( conversation_id, message_id, current_user, ) ) @router.post("/{conversation_id}/stream") async def stream_conversation_message( conversation_id: int, payload: ConversationMessageCreateRequest, session: DatabaseSession, current_user: Annotated[UserAccount, Depends(require_permission("risk.assess", operate=True))], ) -> StreamingResponse: service = ConversationService(session) async def events(): try: async for event in service.stream( conversation_id, payload.content, current_user, message_type=payload.message_type, ): yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n" except Exception as exc: # noqa: BLE001 - turn route errors into SSE events yield f"data: {json.dumps({'type': 'error', 'message': str(exc)}, ensure_ascii=False)}\n\n" return StreamingResponse( events(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, )