File size: 8,906 Bytes
67f8819 99d22d0 67f8819 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | """
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)
}
|