diff --git a/backend/app/api/v1/llm_internal.py b/backend/app/api/v1/llm_internal.py index 4f023ca..7b5a9a0 100644 --- a/backend/app/api/v1/llm_internal.py +++ b/backend/app/api/v1/llm_internal.py @@ -3,14 +3,17 @@ Internal LLM API endpoints - for frontend use with JWT authentication """ import logging -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, Request, status +from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.db.database import get_db from app.core.security import get_current_user from app.services.llm.service import llm_service +from app.services.llm.models import ChatRequest, ChatMessage as LLMChatMessage +from app.services.llm.exceptions import LLMError, ProviderError, SecurityError, ValidationError from app.api.v1.llm import get_cached_models # Reuse the caching logic logger = logging.getLogger(__name__) @@ -108,4 +111,96 @@ async def get_metrics( raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve metrics" + ) + + +class ChatCompletionRequest(BaseModel): + """Request model for chat completions""" + model: str + messages: List[Dict[str, str]] + temperature: Optional[float] = Field(default=0.7, ge=0.0, le=2.0) + max_tokens: Optional[int] = Field(default=1000, ge=1) + top_p: Optional[float] = Field(default=1.0, ge=0.0, le=1.0) + stream: Optional[bool] = False + + +@router.post("/chat/completions") +async def create_chat_completion( + request: ChatCompletionRequest, + current_user: Dict[str, Any] = Depends(get_current_user), + db: AsyncSession = Depends(get_db) +): + """ + Create chat completion for authenticated frontend users. + This endpoint is for playground and internal use only, using JWT authentication. + """ + try: + # Get user ID from JWT token context + user_id = str(current_user.get("id", current_user.get("sub", "0"))) + + # Convert request to LLM service format + # For internal use, we use a special api_key_id of 0 to indicate JWT auth + chat_request = ChatRequest( + model=request.model, + messages=[ + LLMChatMessage(role=msg["role"], content=msg["content"]) + for msg in request.messages + ], + temperature=request.temperature, + max_tokens=request.max_tokens, + top_p=request.top_p, + stream=request.stream, + user_id=user_id, + api_key_id=0 # Special value for JWT-authenticated requests + ) + + # Log the request for debugging + logger.info(f"Internal chat completion request from user {current_user.get('id')}: model={request.model}") + + # Process the request through the LLM service + response = await llm_service.create_chat_completion(chat_request) + + # Format the response to match OpenAI's structure + formatted_response = { + "id": response.id, + "object": "chat.completion", + "created": response.created, + "model": response.model, + "choices": [ + { + "index": choice.index, + "message": { + "role": choice.message.role, + "content": choice.message.content + }, + "finish_reason": choice.finish_reason + } + for choice in response.choices + ], + "usage": { + "prompt_tokens": response.usage.prompt_tokens if response.usage else 0, + "completion_tokens": response.usage.completion_tokens if response.usage else 0, + "total_tokens": response.usage.total_tokens if response.usage else 0 + } if response.usage else None + } + + return formatted_response + + except ValidationError as e: + logger.error(f"Validation error in chat completion: {e}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid request: {str(e)}" + ) + except LLMError as e: + logger.error(f"LLM service error: {e}") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"LLM service error: {str(e)}" + ) + except Exception as e: + logger.error(f"Unexpected error in chat completion: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to process chat completion" ) \ No newline at end of file diff --git a/frontend/src/components/chatbot/ChatInterface.css b/frontend/src/components/chatbot/ChatInterface.css new file mode 100644 index 0000000..8ce6afa --- /dev/null +++ b/frontend/src/components/chatbot/ChatInterface.css @@ -0,0 +1,118 @@ +/* Dark mode improvements for chat interface */ +.dark .chat-message-assistant { + background-color: rgb(51 65 85); /* slate-700 */ + color: white !important; +} + +.dark .chat-message-assistant * { + color: white !important; +} + +.dark .chat-message-user { + /* User messages keep their primary color */ +} + +.dark .chat-input { + background-color: rgb(30 41 59); /* slate-800 */ + color: white !important; + border-color: rgb(71 85 105); /* slate-600 */ +} + +.dark .chat-input::placeholder { + color: rgb(148 163 184); /* slate-400 */ +} + +.dark .chat-timestamp { + color: rgb(148 163 184); /* slate-400 */ +} + +.dark .chat-thinking { + background-color: rgb(51 65 85); /* slate-700 */ + color: white !important; +} + +.dark .chat-thinking * { + color: white !important; +} + +/* Ensure ALL text in markdown content is white in dark mode */ +.dark .markdown-content, +.dark .markdown-content * { + color: white !important; +} + +.dark .markdown-content p, +.dark .markdown-content span, +.dark .markdown-content div, +.dark .markdown-content li, +.dark .markdown-content h1, +.dark .markdown-content h2, +.dark .markdown-content h3, +.dark .markdown-content h4, +.dark .markdown-content h5, +.dark .markdown-content h6, +.dark .markdown-content strong, +.dark .markdown-content b, +.dark .markdown-content em, +.dark .markdown-content i, +.dark .markdown-content a, +.dark .markdown-content blockquote { + color: white !important; +} + +.dark .markdown-content code { + background-color: rgb(30 41 59); /* slate-800 */ + color: white !important; + border-color: rgb(71 85 105); /* slate-600 */ +} + +.dark .markdown-content pre, +.dark .markdown-content pre * { + background-color: rgb(30 41 59); /* slate-800 */ + color: white !important; + border-color: rgb(71 85 105); /* slate-600 */ +} + +/* Make sure bold text is white */ +.dark strong, +.dark b { + color: white !important; + font-weight: bold; +} + +/* Prose styles override for dark mode */ +.dark .prose, +.dark .prose-sm { + color: white !important; +} + +.dark .prose h1, +.dark .prose h2, +.dark .prose h3, +.dark .prose h4, +.dark .prose h5, +.dark .prose h6, +.dark .prose-sm h1, +.dark .prose-sm h2, +.dark .prose-sm h3, +.dark .prose-sm h4, +.dark .prose-sm h5, +.dark .prose-sm h6 { + color: white !important; + font-weight: bold; +} + +.dark .prose strong, +.dark .prose-sm strong, +.dark .prose b, +.dark .prose-sm b { + color: white !important; + font-weight: bold; +} + +.dark .prose em, +.dark .prose-sm em, +.dark .prose i, +.dark .prose-sm i { + color: white !important; +} \ No newline at end of file