Spaces:
Running
Running
File size: 11,184 Bytes
676582c |
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 270 271 272 273 274 275 276 277 278 279 280 281 282 |
"""Chat API endpoint for AI chatbot."""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session
from typing import Dict, Any
import logging
from datetime import datetime
from src.core.database import get_session
from src.core.security import get_current_user
from src.core.config import settings
from src.schemas.chat_request import ChatRequest
from src.schemas.chat_response import ChatResponse
from src.services.conversation_service import ConversationService
from src.agent.agent_config import AgentConfiguration
from src.agent.agent_runner import AgentRunner
from src.mcp import tool_registry
from src.core.exceptions import (
classify_ai_error,
APIKeyMissingException,
APIKeyInvalidException
)
# Configure logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["chat"])
def generate_conversation_title(first_user_message: str) -> str:
"""Generate a conversation title from the first user message.
Args:
first_user_message: The first message from the user
Returns:
A title string (max 50 characters)
"""
# Remove leading/trailing whitespace
message = first_user_message.strip()
# Try to extract the first sentence or first 50 characters
# Split by common sentence endings
for delimiter in ['. ', '! ', '? ', '\n']:
if delimiter in message:
title = message.split(delimiter)[0]
break
else:
# No sentence delimiter found, use first 50 chars
title = message[:50]
# If title is too short (less than 10 chars), use timestamp-based default
if len(title) < 10:
return f"Chat {datetime.now().strftime('%b %d, %I:%M %p')}"
# Truncate to 50 characters and add ellipsis if needed
if len(title) > 50:
title = title[:47] + "..."
return title
@router.post("/{user_id}/chat", response_model=ChatResponse)
async def chat(
user_id: int,
request: ChatRequest,
db: Session = Depends(get_session),
current_user: Dict[str, Any] = Depends(get_current_user)
) -> ChatResponse:
"""Handle chat messages from users.
Args:
user_id: ID of the user sending the message
request: ChatRequest containing the user's message
db: Database session
current_user: Authenticated user from JWT token
Returns:
ChatResponse containing the AI's response
Raises:
HTTPException 401: If user is not authenticated or user_id doesn't match
HTTPException 404: If conversation_id is provided but not found
HTTPException 500: If AI provider fails to generate response
"""
# Verify user authorization
if current_user["id"] != user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authorized to access this user's chat"
)
try:
# Validate request message length
if not request.message or len(request.message.strip()) == 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Message cannot be empty"
)
if len(request.message) > 10000:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Message exceeds maximum length of 10,000 characters"
)
# Initialize services
conversation_service = ConversationService(db)
# Initialize agent configuration from settings
try:
agent_config = AgentConfiguration(
provider=settings.LLM_PROVIDER,
fallback_provider=settings.FALLBACK_PROVIDER,
gemini_api_key=settings.GEMINI_API_KEY,
openrouter_api_key=settings.OPENROUTER_API_KEY,
cohere_api_key=settings.COHERE_API_KEY,
temperature=settings.AGENT_TEMPERATURE,
max_tokens=settings.AGENT_MAX_TOKENS,
max_messages=settings.CONVERSATION_MAX_MESSAGES,
max_conversation_tokens=settings.CONVERSATION_MAX_TOKENS
)
agent_config.validate()
# Create agent runner with tool registry
agent_runner = AgentRunner(agent_config, tool_registry)
except ValueError as e:
logger.error(f"Agent initialization failed: {str(e)}")
# Check if it's an API key issue
error_msg = str(e).lower()
if "api key" in error_msg:
if "not found" in error_msg or "missing" in error_msg:
raise APIKeyMissingException(provider=settings.LLM_PROVIDER)
elif "invalid" in error_msg:
raise APIKeyInvalidException(provider=settings.LLM_PROVIDER)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="AI service is not properly configured. Please contact support."
)
# Get or create conversation
is_new_conversation = False
if request.conversation_id:
conversation = conversation_service.get_conversation(
request.conversation_id,
user_id
)
if not conversation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Conversation {request.conversation_id} not found or you don't have access to it"
)
else:
# Create new conversation with auto-generated title
try:
# Generate title from first user message
title = generate_conversation_title(request.message)
conversation = conversation_service.create_conversation(
user_id=user_id,
title=title
)
is_new_conversation = True
logger.info(f"Created new conversation {conversation.id} with title: {title}")
except Exception as e:
logger.error(f"Failed to create conversation: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create conversation. Please try again."
)
# Add user message to conversation
try:
user_message = conversation_service.add_message(
conversation_id=conversation.id,
role="user",
content=request.message,
token_count=len(request.message) // 4 # Rough token estimate
)
except Exception as e:
logger.error(f"Failed to save user message: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to save your message. Please try again."
)
# Get conversation history and format for agent
history_messages = conversation_service.get_conversation_messages(
conversation_id=conversation.id
)
# Format messages for agent with trimming
formatted_messages = conversation_service.format_messages_for_agent(
messages=history_messages,
max_messages=agent_config.max_messages,
max_tokens=agent_config.max_conversation_tokens
)
# Generate AI response with tool calling support
system_prompt = request.system_prompt or agent_config.system_prompt
try:
agent_result = await agent_runner.execute(
messages=formatted_messages,
user_id=user_id, # Inject user context for security
system_prompt=system_prompt
)
except Exception as e:
# Use classify_ai_error to determine the appropriate exception
logger.error(f"AI service error for user {user_id}: {str(e)}")
provider = agent_result.get("provider") if 'agent_result' in locals() else settings.LLM_PROVIDER
raise classify_ai_error(e, provider=provider)
# Add AI response to conversation with tool call metadata
try:
# Prepare metadata if tools were used
tool_metadata = None
if agent_result.get("tool_calls"):
# Convert ToolExecutionResult objects to dicts for JSON serialization
tool_results = agent_result.get("tool_results", [])
serializable_results = []
for result in tool_results:
if hasattr(result, '__dict__'):
# Convert dataclass/object to dict
serializable_results.append({
"success": result.success,
"data": result.data,
"message": result.message,
"error": result.error
})
else:
# Already a dict
serializable_results.append(result)
tool_metadata = {
"tool_calls": agent_result["tool_calls"],
"tool_results": serializable_results,
"provider": agent_result.get("provider")
}
assistant_message = conversation_service.add_message(
conversation_id=conversation.id,
role="assistant",
content=agent_result["content"],
token_count=len(agent_result["content"]) // 4 # Rough token estimate
)
# Update tool_metadata if tools were used
if tool_metadata:
assistant_message.tool_metadata = tool_metadata
db.add(assistant_message)
db.commit()
except Exception as e:
logger.error(f"Failed to save AI response: {str(e)}")
# Still return the response even if saving fails
# User gets the response but it won't be in history
logger.warning(f"Returning response without saving to database for conversation {conversation.id}")
# Log tool usage if any
if agent_result.get("tool_calls"):
logger.info(f"Agent used {len(agent_result['tool_calls'])} tools for user {user_id}")
# Return response
return ChatResponse(
conversation_id=conversation.id,
message=agent_result["content"],
role="assistant",
timestamp=assistant_message.timestamp if 'assistant_message' in locals() else user_message.timestamp,
token_count=len(agent_result["content"]) // 4,
model=agent_result.get("provider")
)
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
# Catch-all for unexpected errors
logger.exception(f"Unexpected error in chat endpoint: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected error occurred. Please try again later."
)
|