MCP_todo / api /routes /chat.py
MAWB's picture
Update api/routes/chat.py
99d22d0 verified
"""
Chat API endpoints for Todo AI Chatbot.
Per @specs/001-chatbot-mcp/contracts/openapi.yaml and @specs/001-chatbot-mcp/plan.md
"""
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlmodel import Session
from uuid import UUID
from datetime import datetime
import time
import logging
from config import engine
from api.dependencies import get_current_user
from api.schemas.chat import ChatRequest, ChatResponse, Message, ChatError
from services.chat import ChatService
from agents.todo_agent import create_todo_agent
from models.message import MessageRole
logger = logging.getLogger(__name__)
# Create router
router = APIRouter(prefix="/api", tags=["chat"])
@router.post("/{user_id}/chat", response_model=ChatResponse)
async def chat_endpoint(
user_id: str,
request: ChatRequest,
http_request: Request,
current_user: str = Depends(get_current_user)
):
"""
Chat endpoint for AI-powered task management.
Processes user messages through OpenAI agent with MCP tool integration.
Creates new conversations or continues existing ones.
Per @specs/001-chatbot-mcp/plan.md:
- Stateless architecture: history loaded from DB each request
- MCP First: all task operations through MCP tools
- Data isolation: all queries filter by user_id
Per @specs/001-chatbot-mcp/contracts/openapi.yaml
"""
start_time = time.time()
# Verify user ownership
if current_user != user_id:
logger.warning(f"User {current_user} attempted to access user {user_id} chat")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access forbidden: You can only access your own chat"
)
# Parse user_id as UUID
try:
user_uuid = UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
with Session(engine) as session:
try:
# Get or create conversation
conversation = None
conversation_id = request.conversation_id
if conversation_id:
# Validate user owns this conversation
conversation = ChatService.get_conversation(session, conversation_id, user_uuid)
if not conversation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Conversation not found or access denied"
)
else:
# Create new conversation for first message
conversation = ChatService.create_conversation(
session=session,
user_id=user_uuid,
title="New Chat" # Can be updated based on first message content
)
conversation_id = conversation.id
logger.info(f"Created new conversation {conversation_id} for user {user_id}")
# Sanitize user input
sanitized_message = ChatService.sanitize_user_input(request.message)
# Store user message
user_message = ChatService.store_message(
session=session,
conversation_id=conversation_id,
role=MessageRole.USER,
content=sanitized_message
)
# Load conversation history
history = ChatService.get_conversation_history(
session=session,
conversation_id=conversation_id,
user_id=user_uuid
)
# Format for OpenAI (exclude the message we just added)
formatted_history = ChatService.format_messages_for_openai(history[:-1])
# Create agent and process message
agent = create_todo_agent(user_uuid)
# Collect agent response
response_parts = []
async for chunk in agent.process_message(sanitized_message, formatted_history):
response_parts.append(chunk)
assistant_response = "".join(response_parts)
# Store assistant response
assistant_message = ChatService.store_message(
session=session,
conversation_id=conversation_id,
role=MessageRole.ASSISTANT,
content=assistant_response,
metadata={"processing_time": time.time() - start_time}
)
# Calculate processing time
processing_time = time.time() - start_time
# Log request
logger.info(
f"Chat processed: user={user_id}, "
f"conversation={conversation_id}, "
f"processing_time={processing_time:.2f}s, "
f"message_length={len(request.message)}"
)
# Build response
return ChatResponse(
conversation_id=conversation_id,
message=Message(
id=assistant_message.id,
role="assistant",
content=assistant_response,
created_at=assistant_message.created_at
),
tasks=None # Could be populated with affected tasks if needed
)
except HTTPException:
# Re-raise HTTP exceptions as-is
raise
except Exception as e:
# Log error for debugging
logger.error(f"Error processing chat request: {e}", exc_info=True)
# Return user-friendly error message
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={
"error": "Failed to process chat message",
"message": "I encountered an error processing your request. Please try again.",
"conversation_id": str(conversation_id) if conversation_id else None
}
)
@router.get("/{user_id}/conversations")
async def list_conversations(
user_id: str,
current_user: str = Depends(get_current_user)
):
"""
List all conversations for a user.
Returns conversations ordered by most recently updated.
"""
# Verify user ownership
if current_user != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access forbidden: You can only access your own conversations"
)
try:
user_uuid = UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
with Session(engine) as session:
conversations = ChatService.get_user_conversations(session, user_uuid)
return {
"conversations": [
{
"id": str(conv.id),
"title": conv.title,
"created_at": conv.created_at.isoformat(),
"updated_at": conv.updated_at.isoformat()
}
for conv in conversations
],
"count": len(conversations)
}
@router.get("/{user_id}/conversations/{conversation_id}/messages")
async def get_conversation_messages(
user_id: str,
conversation_id: str,
current_user: str = Depends(get_current_user)
):
"""
Get all messages in a conversation.
Requires user owns the conversation.
"""
# Verify user ownership
if current_user != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access forbidden"
)
try:
user_uuid = UUID(user_id)
conv_uuid = UUID(conversation_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid ID format"
)
with Session(engine) as session:
# Verify user owns the conversation
conversation = ChatService.get_conversation(session, conv_uuid, user_uuid)
if not conversation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Conversation not found"
)
# Get messages
messages = ChatService.get_conversation_history(
session=session,
conversation_id=conv_uuid,
user_id=user_uuid
)
return {
"conversation_id": conversation_id,
"messages": [
{
"id": str(msg.id),
"role": msg.role.value,
"content": msg.content,
"created_at": msg.created_at.isoformat()
}
for msg in messages
],
"count": len(messages)
}