Conversation Endpoints (/api/v1/conversations):
- POST / - Create new conversation
- GET / - List user's conversations with pagination
- GET /{id} - Get conversation details
- PUT /{id} - Update conversation title
- POST /{id}/archive - Archive conversation
- DELETE /{id} - Delete conversation with cascade
Message Endpoints (/api/v1/conversations/{id}/messages):
- GET / - Get messages for conversation with pagination
- POST / - Send message and get AI response
Token Usage Endpoints (/api/v1/tokens):
- GET /usage - Get token usage summary with daily breakdown
Schemas:
- ConversationCreate/Update/Response
- ConversationListResponse for listing
- MessageCreate/Response
- SendMessageResponse with usage stats
- TokenUsageSummary with analytics
Features:
- Full permission checks (user ownership verification)
- Pagination support for all list endpoints
- Detailed error handling with appropriate HTTP codes
- Usage statistics tracking per message
- Cost calculation and reporting
- File search results in message metadata
Security:
- All endpoints require authentication
- User can only access their own conversations
- Proper 403/404 error handling
- Request validation with Pydantic
Router Updates:
- Connected all new endpoints to /api/v1
- Organized by resource (auth, conversations, messages, tokens)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
259 lines
7 KiB
Python
259 lines
7 KiB
Python
"""
|
|
Conversation Endpoints
|
|
|
|
API endpoints for conversation management
|
|
"""
|
|
|
|
import logging
|
|
from typing import List
|
|
from uuid import UUID
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.services.chat_service import ChatService
|
|
from app.schemas.conversation import (
|
|
ConversationCreate,
|
|
ConversationUpdate,
|
|
ConversationResponse,
|
|
ConversationListResponse
|
|
)
|
|
from app.core.middleware import get_current_active_user
|
|
from app.models.user import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/conversations", tags=["Conversations"])
|
|
|
|
|
|
@router.post("", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_conversation(
|
|
request: ConversationCreate,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Create a new conversation
|
|
|
|
Args:
|
|
request: Conversation creation request
|
|
current_user: Current authenticated user
|
|
db: Database session
|
|
|
|
Returns:
|
|
Created conversation
|
|
"""
|
|
chat_service = ChatService(db)
|
|
|
|
conversation = await chat_service.create_conversation(
|
|
user_id=current_user.id,
|
|
title=request.title
|
|
)
|
|
|
|
logger.info(f"User {current_user.id} created conversation {conversation['id']}")
|
|
|
|
return ConversationResponse(**conversation)
|
|
|
|
|
|
@router.get("", response_model=List[ConversationListResponse])
|
|
async def list_conversations(
|
|
include_archived: bool = Query(False, description="Include archived conversations"),
|
|
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
|
limit: int = Query(50, ge=1, le=100, description="Maximum number of records"),
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
List user's conversations
|
|
|
|
Args:
|
|
include_archived: Include archived conversations
|
|
skip: Pagination offset
|
|
limit: Pagination limit
|
|
current_user: Current authenticated user
|
|
db: Database session
|
|
|
|
Returns:
|
|
List of conversations
|
|
"""
|
|
chat_service = ChatService(db)
|
|
|
|
conversations = await chat_service.list_conversations(
|
|
user_id=current_user.id,
|
|
include_archived=include_archived,
|
|
skip=skip,
|
|
limit=limit
|
|
)
|
|
|
|
return [ConversationListResponse(**conv) for conv in conversations]
|
|
|
|
|
|
@router.get("/{conversation_id}", response_model=ConversationResponse)
|
|
async def get_conversation(
|
|
conversation_id: UUID,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Get conversation by ID
|
|
|
|
Args:
|
|
conversation_id: Conversation UUID
|
|
current_user: Current authenticated user
|
|
db: Database session
|
|
|
|
Returns:
|
|
Conversation details
|
|
|
|
Raises:
|
|
HTTPException: If conversation not found or access denied
|
|
"""
|
|
chat_service = ChatService(db)
|
|
|
|
conversation = await chat_service.get_conversation(conversation_id)
|
|
|
|
if not conversation:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Conversation not found"
|
|
)
|
|
|
|
# Verify ownership
|
|
if UUID(conversation["user_id"]) != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied"
|
|
)
|
|
|
|
return ConversationResponse(**conversation)
|
|
|
|
|
|
@router.put("/{conversation_id}", response_model=ConversationResponse)
|
|
async def update_conversation(
|
|
conversation_id: UUID,
|
|
request: ConversationUpdate,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Update conversation title
|
|
|
|
Args:
|
|
conversation_id: Conversation UUID
|
|
request: Update request with new title
|
|
current_user: Current authenticated user
|
|
db: Database session
|
|
|
|
Returns:
|
|
Updated conversation
|
|
|
|
Raises:
|
|
HTTPException: If conversation not found or access denied
|
|
"""
|
|
chat_service = ChatService(db)
|
|
|
|
# Verify ownership
|
|
conversation = await chat_service.get_conversation(conversation_id)
|
|
if not conversation:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Conversation not found"
|
|
)
|
|
|
|
if UUID(conversation["user_id"]) != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied"
|
|
)
|
|
|
|
# Update title
|
|
await chat_service.update_conversation_title(conversation_id, request.title)
|
|
|
|
# Get updated conversation
|
|
updated = await chat_service.get_conversation(conversation_id)
|
|
|
|
logger.info(f"User {current_user.id} updated conversation {conversation_id}")
|
|
|
|
return ConversationResponse(**updated)
|
|
|
|
|
|
@router.post("/{conversation_id}/archive")
|
|
async def archive_conversation(
|
|
conversation_id: UUID,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Archive a conversation
|
|
|
|
Args:
|
|
conversation_id: Conversation UUID
|
|
current_user: Current authenticated user
|
|
db: Database session
|
|
|
|
Returns:
|
|
Success message
|
|
|
|
Raises:
|
|
HTTPException: If conversation not found or access denied
|
|
"""
|
|
chat_service = ChatService(db)
|
|
|
|
# Verify ownership
|
|
conversation = await chat_service.get_conversation(conversation_id)
|
|
if not conversation:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Conversation not found"
|
|
)
|
|
|
|
if UUID(conversation["user_id"]) != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied"
|
|
)
|
|
|
|
await chat_service.archive_conversation(conversation_id)
|
|
|
|
logger.info(f"User {current_user.id} archived conversation {conversation_id}")
|
|
|
|
return {"message": "Conversation archived successfully"}
|
|
|
|
|
|
@router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_conversation(
|
|
conversation_id: UUID,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""
|
|
Delete a conversation
|
|
|
|
Args:
|
|
conversation_id: Conversation UUID
|
|
current_user: Current authenticated user
|
|
db: Database session
|
|
|
|
Raises:
|
|
HTTPException: If conversation not found or access denied
|
|
"""
|
|
chat_service = ChatService(db)
|
|
|
|
try:
|
|
await chat_service.delete_conversation(
|
|
user_id=current_user.id,
|
|
conversation_id=conversation_id
|
|
)
|
|
|
|
logger.info(f"User {current_user.id} deleted conversation {conversation_id}")
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=str(e)
|
|
)
|
|
except PermissionError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=str(e)
|
|
)
|