| """ |
| 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__) |
|
|
| |
| 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() |
|
|
| |
| 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" |
| ) |
|
|
| |
| 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: |
| |
| conversation = None |
| conversation_id = request.conversation_id |
|
|
| if conversation_id: |
| |
| 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: |
| |
| conversation = ChatService.create_conversation( |
| session=session, |
| user_id=user_uuid, |
| title="New Chat" |
| ) |
| conversation_id = conversation.id |
| logger.info(f"Created new conversation {conversation_id} for user {user_id}") |
|
|
| |
| sanitized_message = ChatService.sanitize_user_input(request.message) |
|
|
| |
| user_message = ChatService.store_message( |
| session=session, |
| conversation_id=conversation_id, |
| role=MessageRole.USER, |
| content=sanitized_message |
| ) |
|
|
| |
| history = ChatService.get_conversation_history( |
| session=session, |
| conversation_id=conversation_id, |
| user_id=user_uuid |
| ) |
|
|
| |
| formatted_history = ChatService.format_messages_for_openai(history[:-1]) |
|
|
| |
| agent = create_todo_agent(user_uuid) |
|
|
| |
| response_parts = [] |
| async for chunk in agent.process_message(sanitized_message, formatted_history): |
| response_parts.append(chunk) |
|
|
| assistant_response = "".join(response_parts) |
|
|
| |
| assistant_message = ChatService.store_message( |
| session=session, |
| conversation_id=conversation_id, |
| role=MessageRole.ASSISTANT, |
| content=assistant_response, |
| metadata={"processing_time": time.time() - start_time} |
| ) |
|
|
| |
| processing_time = time.time() - start_time |
|
|
| |
| 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)}" |
| ) |
|
|
| |
| 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 |
| ) |
|
|
| except HTTPException: |
| |
| raise |
|
|
| except Exception as e: |
| |
| logger.error(f"Error processing chat request: {e}", exc_info=True) |
|
|
| |
| 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. |
| """ |
| |
| 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. |
| """ |
| |
| 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: |
| |
| 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" |
| ) |
|
|
| |
| 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) |
| } |
|
|