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