| from typing import List, Optional, Dict |
| from fastapi import APIRouter, Depends, HTTPException, status, Query |
| from sqlmodel import Session |
| from src.services.conversation_service import ConversationService |
| from src.services.chat_service import ChatService |
| from src.models.conversation import Conversation, ConversationResponse, ConversationCreate |
| from src.models.message import Message, MessageResponse |
| from src.core.database import get_session |
| from src.core.logging import log_operation |
| from src.auth.deps import get_current_user_id |
| from src.auth.security import authorize_user_for_conversation |
| from src.api.v1.chat_docs import ( |
| ERROR_RESPONSES, MessageCreate, ChatErrorResponse, |
| MessageListResponse, ConversationListResponse, ChatRequest, ChatResponse |
| ) |
|
|
| router = APIRouter( |
| tags=["chat"], |
| responses=ERROR_RESPONSES |
| ) |
|
|
| @router.post( |
| "/", |
| response_model=ConversationResponse, |
| status_code=status.HTTP_201_CREATED, |
| summary="Create a new conversation", |
| description="Create a new AI chat conversation for the authenticated user", |
| responses={ |
| status.HTTP_201_CREATED: { |
| "description": "Conversation created successfully", |
| "content": { |
| "application/json": { |
| "example": { |
| "id": 1, |
| "user_id": "user-123", |
| "title": "My First Conversation", |
| "created_at": "2024-01-15T10:30:00.000000", |
| "updated_at": "2024-01-15T10:30:00.000000", |
| "message_count": 0, |
| "last_message": None |
| } |
| } |
| } |
| } |
| } |
| ) |
|
|
| def create_conversation( |
| conversation_create: ConversationCreate, |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ) -> ConversationResponse: |
| """ |
| Create a new conversation for the authenticated user |
| |
| This endpoint creates a new AI chat conversation. The authenticated user |
| is automatically assigned as the owner of the conversation. |
| |
| - **conversation_create**: Conversation creation data |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Security |
| - Requires JWT authentication |
| - User ID is automatically set to the authenticated user |
| - Prevents user ID spoofing |
| |
| ## Response |
| - Returns the created conversation with metadata |
| - Includes message count and last message preview |
| |
| ## Error Responses |
| - 400: Invalid request data |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 500: Internal server error |
| """ |
| try: |
| |
| conversation_create.user_id = current_user_id |
|
|
| |
| db_conversation = ConversationService.create_conversation(session, current_user_id, conversation_create.title) |
|
|
| log_operation("CONVERSATION_CREATED_SUCCESSFULLY", user_id=current_user_id, conversation_id=db_conversation.id) |
| return ConversationResponse.model_validate(db_conversation) |
| except HTTPException: |
| raise |
| except Exception as e: |
| log_operation("CREATE_CONVERSATION_ERROR", user_id=current_user_id) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while creating the conversation: {str(e)}" |
| ) |
|
|
| @router.get( |
| "/", |
| response_model=List[ConversationResponse], |
| summary="List user conversations", |
| description="List all conversations for the authenticated user with pagination", |
| responses={ |
| status.HTTP_200_OK: { |
| "description": "Conversations retrieved successfully", |
| "content": { |
| "application/json": { |
| "example": { |
| "conversations": [ |
| { |
| "id": 1, |
| "user_id": "user-123", |
| "title": "My First Conversation", |
| "created_at": "2024-01-15T10:30:00.000000", |
| "updated_at": "2024-01-15T10:35:00.000000", |
| "message_count": 1, |
| "last_message": "Hello! I need help with my project." |
| } |
| ], |
| "total": 1, |
| "limit": 20, |
| "offset": 0, |
| "has_more": False |
| } |
| } |
| } |
| } |
| } |
| ) |
|
|
| def list_conversations( |
| limit: int = Query(default=20, ge=1, le=100, description="Number of conversations to return (1-100)"), |
| offset: int = Query(default=0, ge=0, description="Number of conversations to skip"), |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ) -> List[ConversationResponse]: |
| """ |
| List conversations for the authenticated user with pagination |
| |
| This endpoint returns a paginated list of conversations for the authenticated user. |
| |
| - **limit**: Number of conversations to return (1-100, default: 20) |
| - **offset**: Number of conversations to skip (default: 0) |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Pagination |
| - Use `limit` and `offset` parameters for pagination |
| - Returns total count and has_more flag for client-side pagination |
| |
| ## Response |
| - Returns list of conversation summaries |
| - Each includes message count and last message preview |
| |
| ## Error Responses |
| - 400: Invalid pagination parameters |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 500: Internal server error |
| """ |
| try: |
| |
| conversations = ConversationService.list_conversations(session, current_user_id, limit, offset) |
|
|
| log_operation(f"GET_CONVERSATIONS_SUCCESS ({len(conversations)} conversations)", user_id=current_user_id) |
|
|
| |
| return [ConversationResponse.model_validate(conversation, from_attributes=True) for conversation in conversations] |
| except HTTPException: |
| raise |
| except Exception as e: |
| log_operation("GET_CONVERSATIONS_ERROR", user_id=current_user_id) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while retrieving conversations: {str(e)}" |
| ) |
|
|
| @router.get( |
| "/{conversation_id}", |
| response_model=ConversationResponse, |
| summary="Get conversation details", |
| description="Get a specific conversation by ID with ownership verification", |
| responses={ |
| status.HTTP_200_OK: { |
| "description": "Conversation retrieved successfully", |
| "content": { |
| "application/json": { |
| "example": { |
| "id": 1, |
| "user_id": "user-123", |
| "title": "My First Conversation", |
| "created_at": "2024-01-15T10:30:00.000000", |
| "updated_at": "2024-01-15T10:35:00.000000", |
| "message_count": 1, |
| "last_message": "Hello! I need help with my project." |
| } |
| } |
| } |
| }, |
| status.HTTP_404_NOT_FOUND: { |
| "description": "Conversation not found or access denied" |
| } |
| } |
| ) |
|
|
| def get_conversation( |
| conversation_id: int, |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ) -> ConversationResponse: |
| """ |
| Get a conversation by ID with ownership verification |
| |
| This endpoint retrieves a specific conversation. The user must own the |
| conversation to access it (data isolation). |
| |
| - **conversation_id**: ID of the conversation to retrieve |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Security |
| - Requires JWT authentication |
| - Verifies user owns the conversation |
| - Prevents unauthorized access to other users' conversations |
| |
| ## Response |
| - Returns complete conversation details |
| - Includes message count and last message preview |
| |
| ## Error Responses |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 404: Conversation not found or access denied |
| - 500: Internal server error |
| """ |
| try: |
| |
| conversation = ConversationService.get_conversation(session, conversation_id, current_user_id) |
| if not conversation: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Conversation with id {conversation_id} not found or access denied" |
| ) |
|
|
| log_operation("CONVERSATION_RETRIEVED_SUCCESSFULLY", user_id=current_user_id, conversation_id=conversation_id) |
| return ConversationResponse.model_validate(conversation) |
| except HTTPException: |
| raise |
| except Exception as e: |
| log_operation("GET_CONVERSATION_ERROR", user_id=current_user_id, conversation_id=conversation_id) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while retrieving the conversation: {str(e)}" |
| ) |
|
|
| @router.delete( |
| "/{conversation_id}", |
| status_code=status.HTTP_204_NO_CONTENT, |
| summary="Delete a conversation", |
| description="Delete a conversation by ID (only if the conversation belongs to the authenticated user)", |
| responses={ |
| status.HTTP_204_NO_CONTENT: { |
| "description": "Conversation deleted successfully" |
| }, |
| status.HTTP_404_NOT_FOUND: { |
| "description": "Conversation not found or access denied" |
| } |
| } |
| ) |
|
|
| def delete_conversation( |
| conversation_id: int, |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ): |
| """ |
| Delete a conversation by ID (only if the conversation belongs to the authenticated user) |
| |
| This endpoint permanently deletes a conversation and all its messages. |
| Only the conversation owner can delete their conversations. |
| |
| - **conversation_id**: ID of the conversation to delete |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Security |
| - Requires JWT authentication |
| - Verifies user owns the conversation |
| - Permanently deletes conversation and messages |
| |
| ## Response |
| - Returns 204 No Content on successful deletion |
| - No response body |
| |
| ## Error Responses |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 404: Conversation not found or access denied |
| - 500: Internal server error |
| """ |
| try: |
| |
| success = ConversationService.delete_conversation(session, conversation_id, current_user_id) |
| if not success: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Conversation with id {conversation_id} not found or access denied" |
| ) |
|
|
| log_operation("CONVERSATION_DELETED_SUCCESSFULLY", user_id=current_user_id, conversation_id=conversation_id) |
| except HTTPException: |
| raise |
| except Exception as e: |
| log_operation("DELETE_CONVERSATION_ERROR", user_id=current_user_id, conversation_id=conversation_id) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while deleting the conversation: {str(e)}" |
| ) |
|
|
| @router.get( |
| "/{conversation_id}/messages", |
| response_model=List[MessageResponse], |
| summary="Get conversation messages", |
| description="Get all messages in a conversation with ownership verification", |
| responses={ |
| status.HTTP_200_OK: { |
| "description": "Messages retrieved successfully", |
| "content": { |
| "application/json": { |
| "example": { |
| "messages": [ |
| { |
| "id": 1, |
| "conversation_id": 1, |
| "content": "Hello! I need help with my project.", |
| "sender": "user", |
| "created_at": "2024-01-15T10:35:00.000000", |
| "metadata": None |
| } |
| ], |
| "total": 1, |
| "limit": 20, |
| "offset": 0, |
| "has_more": False |
| } |
| } |
| } |
| }, |
| status.HTTP_404_NOT_FOUND: { |
| "description": "Conversation not found or access denied" |
| } |
| } |
| ) |
|
|
| def get_conversation_messages( |
| conversation_id: int, |
| limit: int = Query(default=20, ge=1, le=100, description="Number of messages to return (1-100)"), |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ) -> List[MessageResponse]: |
| """ |
| Get all messages in a conversation with ownership verification |
| |
| This endpoint returns a paginated list of messages for a specific conversation. |
| The user must own the conversation to access its messages. |
| |
| - **conversation_id**: ID of the conversation |
| - **limit**: Number of messages to return (1-100, default: 20) |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Pagination |
| - Use `limit` parameter for pagination |
| - Returns messages in chronological order (oldest first) |
| |
| ## Message Types |
| - `user`: Messages sent by the user |
| - `ai`: Messages sent by the AI assistant |
| |
| ## Response |
| - Returns list of message objects |
| - Each includes sender, content, and timestamp |
| |
| ## Error Responses |
| - 400: Invalid limit parameter |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 404: Conversation not found or access denied |
| - 500: Internal server error |
| """ |
| try: |
| |
| conversation = ConversationService.get_conversation(session, conversation_id, current_user_id) |
| if not conversation: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Conversation with id {conversation_id} not found or access denied" |
| ) |
|
|
| |
| messages = ConversationService.get_conversation_messages(session, conversation_id, limit) |
|
|
| log_operation(f"GET_CONVERSATION_MESSAGES_SUCCESS ({len(messages)} messages)", |
| user_id=current_user_id, conversation_id=conversation_id) |
|
|
| |
| return [MessageResponse.model_validate(message, from_attributes=True) for message in messages] |
| except HTTPException: |
| raise |
| except Exception as e: |
| log_operation("GET_CONVERSATION_MESSAGES_ERROR", user_id=current_user_id, conversation_id=conversation_id) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while retrieving conversation messages: {str(e)}" |
| ) |
|
|
| @router.post( |
| "/{conversation_id}/messages", |
| response_model=MessageResponse, |
| status_code=status.HTTP_201_CREATED, |
| summary="Add message to conversation", |
| description="Add a message to a conversation with ownership verification", |
| responses={ |
| status.HTTP_201_CREATED: { |
| "description": "Message added successfully", |
| "content": { |
| "application/json": { |
| "example": { |
| "id": 1, |
| "conversation_id": 1, |
| "content": "Hello! I need help with my project.", |
| "sender": "user", |
| "created_at": "2024-01-15T10:35:00.000000", |
| "metadata": None |
| } |
| } |
| } |
| }, |
| status.HTTP_400_BAD_REQUEST: { |
| "description": "Invalid message data (invalid sender or content)" |
| }, |
| status.HTTP_404_NOT_FOUND: { |
| "description": "Conversation not found or access denied" |
| } |
| } |
| ) |
|
|
| def add_message( |
| conversation_id: int, |
| content: str = Query(..., min_length=1, max_length=10000, description="Message content (1-10,000 characters)"), |
| sender: str = Query(..., description="Message sender ('user' or 'ai')"), |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ) -> MessageResponse: |
| """ |
| Add a message to a conversation with ownership verification |
| |
| This endpoint adds a new message to a conversation. The user must own the |
| conversation to add messages. |
| |
| - **conversation_id**: ID of the conversation |
| - **content**: Message content (1-10,000 characters) |
| - **sender**: Message sender ('user' or 'ai') |
| - **metadata**: Optional message metadata |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Message Types |
| - `user`: Messages sent by the user (human) |
| - `ai`: Messages sent by the AI assistant |
| |
| ## Content Validation |
| - Minimum 1 character, maximum 10,000 characters |
| - Required field |
| |
| ## Metadata |
| - Optional dictionary for additional message data |
| - Can include timestamps, message IDs, or other custom data |
| |
| ## Response |
| - Returns the created message with ID and timestamp |
| - Includes conversation ID for reference |
| |
| ## Error Responses |
| - 400: Invalid sender value or content length |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 404: Conversation not found or access denied |
| - 500: Internal server error |
| """ |
| try: |
| |
| conversation = ConversationService.get_conversation(session, conversation_id, current_user_id) |
| if not conversation: |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=f"Conversation with id {conversation_id} not found or access denied" |
| ) |
|
|
| |
| if sender not in ['user', 'ai']: |
| raise HTTPException( |
| status_code=status.HTTP_400_BAD_REQUEST, |
| detail="Sender must be 'user' or 'ai'" |
| ) |
|
|
| |
| message = ConversationService.add_message(session, conversation_id, content, sender) |
|
|
| log_operation("MESSAGE_ADDED_SUCCESSFULLY", user_id=current_user_id, conversation_id=conversation_id, message_id=message.id) |
| return MessageResponse.model_validate(message) |
| except HTTPException: |
| raise |
| except Exception as e: |
| log_operation("ADD_MESSAGE_ERROR", user_id=current_user_id, conversation_id=conversation_id) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while adding the message: {str(e)}" |
| ) |
|
|
|
|
| @router.post( |
| "/chat", |
| response_model=ChatResponse, |
| status_code=status.HTTP_200_OK, |
| summary="Send a chat message to AI agent", |
| description="Send a natural language message to the AI agent for task management", |
| responses={ |
| status.HTTP_200_OK: { |
| "description": "Chat message processed successfully", |
| "content": { |
| "application/json": { |
| "example": { |
| "conversation_id": 1, |
| "message": "I've created a task 'Buy groceries' for you.", |
| "context_metadata": { |
| "tasks_modified": 1, |
| "action_taken": True, |
| "tool_calls": [{"tool": "add_task", "success": True}] |
| } |
| } |
| } |
| } |
| }, |
| status.HTTP_401_UNAUTHORIZED: { |
| "description": "Unauthorized - missing or invalid JWT token" |
| }, |
| status.HTTP_404_NOT_FOUND: { |
| "description": "Conversation not found (if conversation_id provided)" |
| } |
| } |
| ) |
| async def send_chat_message( |
| chat_request: ChatRequest, |
| current_user_id: str = Depends(get_current_user_id), |
| session: Session = Depends(get_session) |
| ) -> ChatResponse: |
| """ |
| Send a chat message to the AI agent |
| |
| This endpoint processes a natural language message through the AI agent, |
| which can create, list, complete, update, or delete tasks based on |
| the user's request. |
| |
| - **chat_request**: Chat request with message and optional conversation_id |
| - **current_user_id**: Authenticated user ID (from JWT token) |
| - **session**: Database session |
| |
| ## Conversation Handling |
| - If conversation_id is provided: continues existing conversation |
| - If conversation_id is not provided: creates new conversation |
| |
| ## AI Agent Capabilities |
| - Create new tasks |
| - List existing tasks |
| - Mark tasks as complete |
| - Update task properties |
| - Delete tasks |
| - Maintain conversation context |
| |
| ## Response |
| - Returns AI response message |
| - Includes conversation_id for future messages |
| - Includes metadata about actions taken |
| |
| ## Error Responses |
| - 401: Unauthorized (missing/invalid JWT token) |
| - 404: Conversation not found (if conversation_id provided but invalid) |
| - 500: Internal server error |
| """ |
| try: |
| |
| chat_service = ChatService() |
|
|
| |
| result = await chat_service.process_chat_message( |
| session=session, |
| user_id=current_user_id, |
| message=chat_request.message, |
| conversation_id=chat_request.conversation_id |
| ) |
|
|
| log_operation("CHAT_MESSAGE_PROCESSED", user_id=current_user_id, conversation_id=result["conversationId"]) |
| return ChatResponse(**result) |
|
|
| except ValueError as e: |
| log_operation("CHAT_MESSAGE_ERROR", user_id=current_user_id, error=str(e)) |
| raise HTTPException( |
| status_code=status.HTTP_404_NOT_FOUND, |
| detail=str(e) |
| ) |
| except Exception as e: |
| log_operation("CHAT_MESSAGE_ERROR", user_id=current_user_id, error=str(e)) |
| raise HTTPException( |
| status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| detail=f"An error occurred while processing your message: {str(e)}" |
| ) |