apac-ops-bot/backend/app/api/v1/endpoints/conversations.py
SamoilenkoVadym f3f62fef24 Implement chat API endpoints (conversations, messages, tokens)
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>
2026-01-27 14:34:39 +00:00

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)
)