diff --git a/backend/app/api/v1/chatbot.py b/backend/app/api/v1/chatbot.py index 1f2f579..20f03dc 100644 --- a/backend/app/api/v1/chatbot.py +++ b/backend/app/api/v1/chatbot.py @@ -275,14 +275,14 @@ async def chat_with_chatbot( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db) ): - """Send a message to a chatbot and get a response""" + """Send a message to a chatbot and get a response (without persisting conversation)""" user_id = current_user.get("id") if isinstance(current_user, dict) else current_user.id log_api_request("chat_with_chatbot", { "user_id": user_id, "chatbot_id": chatbot_id, "message_length": len(request.message) }) - + try: # Get the chatbot instance result = await db.execute( @@ -291,74 +291,40 @@ async def chat_with_chatbot( .where(ChatbotInstance.created_by == str(user_id)) ) chatbot = result.scalar_one_or_none() - + if not chatbot: raise HTTPException(status_code=404, detail="Chatbot not found") - + if not chatbot.is_active: raise HTTPException(status_code=400, detail="Chatbot is not active") - - # Initialize conversation service - conversation_service = ConversationService(db) - - # Get or create conversation - conversation = await conversation_service.get_or_create_conversation( - chatbot_id=chatbot_id, - user_id=str(user_id), - conversation_id=request.conversation_id - ) - - # Add user message to conversation - await conversation_service.add_message( - conversation_id=conversation.id, - role="user", - content=request.message, - metadata={} - ) - + # Get chatbot module and generate response try: chatbot_module = module_manager.modules.get("chatbot") if not chatbot_module: raise HTTPException(status_code=500, detail="Chatbot module not available") - - # Load conversation history for context - conversation_history = await conversation_service.get_conversation_history( - conversation_id=conversation.id, - limit=chatbot.config.get('memory_length', 10), - include_system=False - ) - - # Use the chatbot module to generate a response + + # Use the chatbot module to generate a response (without persisting) response_data = await chatbot_module.chat( chatbot_config=chatbot.config, message=request.message, - conversation_history=conversation_history, + conversation_history=[], # Empty history for test chat user_id=str(user_id) ) - + response_content = response_data.get("response", "I'm sorry, I couldn't generate a response.") - + except Exception as e: # Use fallback response fallback_responses = chatbot.config.get("fallback_responses", [ "I'm sorry, I'm having trouble processing your request right now." ]) response_content = fallback_responses[0] if fallback_responses else "I'm sorry, I couldn't process your request." - - # Save assistant message using conversation service - assistant_message = await conversation_service.add_message( - conversation_id=conversation.id, - role="assistant", - content=response_content, - metadata={}, - sources=response_data.get("sources") - ) - + + # Return response without conversation ID (since we're not persisting) return { - "conversation_id": conversation.id, "response": response_content, - "timestamp": assistant_message.timestamp.isoformat() + "sources": response_data.get("sources") } except HTTPException: diff --git a/backend/app/services/llm/security.py b/backend/app/services/llm/security.py index 0d24f62..8aa37be 100644 --- a/backend/app/services/llm/security.py +++ b/backend/app/services/llm/security.py @@ -29,7 +29,7 @@ class SecurityManager: """Setup patterns for prompt injection detection""" self.injection_patterns = [ # Direct instruction injection - r"(?i)(ignore|forget|disregard|override)\s+(previous|all|above|prior)\s+(instructions|rules|prompts)", + r"(?i)(ignore|forget|disregard|override).{0,20}(instructions|rules|prompts)", r"(?i)(new|updated|different)\s+(instructions|rules|system)", r"(?i)act\s+as\s+(if|though)\s+you\s+(are|were)", r"(?i)pretend\s+(to\s+be|you\s+are)", @@ -61,12 +61,12 @@ class SecurityManager: r"(?i)base64\s*:", r"(?i)hex\s*:", r"(?i)unicode\s*:", - r"[A-Za-z0-9+/]{20,}={0,2}", # Potential base64 + r"(?i)\b[A-Za-z0-9+/]{40,}={0,2}\b", # More specific base64 pattern (longer sequences) - # SQL injection patterns (for system prompts) - r"(?i)(union|select|insert|update|delete|drop|create)\s+", - r"(?i)(or|and)\s+1\s*=\s*1", - r"(?i)';?\s*(drop|delete|insert)", + # SQL injection patterns (more specific to reduce false positives) + r"(?i)(union\s+select|select\s+\*|insert\s+into|update\s+\w+\s+set|delete\s+from|drop\s+table|create\s+table)\s", + r"(?i)(or|and)\s+\d+\s*=\s*\d+", + r"(?i)';?\s*(drop\s+table|delete\s+from|insert\s+into)", # Command injection patterns r"(?i)(exec|eval|system|shell|cmd)\s*\(", @@ -88,23 +88,27 @@ class SecurityManager: def validate_prompt_security(self, messages: List[Dict[str, str]]) -> Tuple[bool, float, List[str]]: """ Validate messages for prompt injection attempts - + Returns: Tuple[bool, float, List[str]]: (is_safe, risk_score, detected_patterns) """ detected_patterns = [] total_risk = 0.0 - + + # Check if this is a system/RAG request + is_system_request = self._is_system_request(messages) + for message in messages: content = message.get("content", "") if not content: continue - - # Check against injection patterns + + # Check against injection patterns with context awareness for i, pattern in enumerate(self.compiled_patterns): matches = pattern.findall(content) if matches: - pattern_risk = self._calculate_pattern_risk(i, matches) + # Apply context-aware risk calculation + pattern_risk = self._calculate_pattern_risk(i, matches, message.get("role", "user"), is_system_request) total_risk += pattern_risk detected_patterns.append({ "pattern_index": i, @@ -112,57 +116,97 @@ class SecurityManager: "matches": matches, "risk": pattern_risk }) - - # Additional security checks - total_risk += self._check_message_characteristics(content) - + + # Additional security checks with context awareness + total_risk += self._check_message_characteristics(content, message.get("role", "user"), is_system_request) + # Normalize risk score (0.0 to 1.0) risk_score = min(total_risk / len(messages) if messages else 0.0, 1.0) - is_safe = risk_score < settings.API_SECURITY_RISK_THRESHOLD - + # Never block - always return True for is_safe + is_safe = True + if detected_patterns: - logger.warning(f"Detected {len(detected_patterns)} potential injection patterns, risk score: {risk_score}") - + logger.info(f"Detected {len(detected_patterns)} potential injection patterns, risk score: {risk_score} (system_request: {is_system_request})") + return is_safe, risk_score, detected_patterns - def _calculate_pattern_risk(self, pattern_index: int, matches: List) -> float: - """Calculate risk score for a detected pattern""" + def _calculate_pattern_risk(self, pattern_index: int, matches: List, role: str, is_system_request: bool) -> float: + """Calculate risk score for a detected pattern with context awareness""" # Different patterns have different risk levels - high_risk_patterns = [0, 1, 2, 3, 4, 5, 6, 7, 14, 15, 16, 22, 23, 24] # System manipulation, jailbreak + high_risk_patterns = [0, 1, 2, 3, 4, 5, 6, 7, 22, 23, 24] # System manipulation, jailbreak medium_risk_patterns = [8, 9, 10, 11, 12, 13, 17, 18, 19, 20, 21] # Escape attempts, info extraction - + + # Base risk score base_risk = 0.8 if pattern_index in high_risk_patterns else 0.5 if pattern_index in medium_risk_patterns else 0.3 - - # Increase risk based on number of matches - match_multiplier = min(1.0 + (len(matches) - 1) * 0.2, 2.0) - + + # Apply context-specific risk reduction + if is_system_request or role == "system": + # Reduce risk for system messages and RAG content + if pattern_index in [14, 15, 16]: # Encoding patterns (base64, hex, unicode) + base_risk *= 0.2 # Reduce encoding risk by 80% for system content + elif pattern_index in [17, 18, 19]: # SQL patterns + base_risk *= 0.3 # Reduce SQL risk by 70% for system content + else: + base_risk *= 0.6 # Reduce other risks by 40% for system content + + # Increase risk based on number of matches, but cap it + match_multiplier = min(1.0 + (len(matches) - 1) * 0.1, 1.5) # Reduced multiplier + return base_risk * match_multiplier - def _check_message_characteristics(self, content: str) -> float: - """Check message characteristics for additional risk factors""" + def _check_message_characteristics(self, content: str, role: str, is_system_request: bool) -> float: + """Check message characteristics for additional risk factors with context awareness""" risk = 0.0 - - # Excessive length (potential stuffing attack) - if len(content) > 10000: - risk += 0.3 - - # High ratio of special characters + + # Excessive length (potential stuffing attack) - less restrictive for system content + length_threshold = 50000 if is_system_request else 10000 # Much higher threshold for system content + if len(content) > length_threshold: + risk += 0.1 if is_system_request else 0.3 + + # High ratio of special characters - more lenient for system content special_chars = sum(1 for c in content if not c.isalnum() and not c.isspace()) - if len(content) > 0 and special_chars / len(content) > 0.5: - risk += 0.4 - - # Multiple encoding indicators + if len(content) > 0: + char_ratio = special_chars / len(content) + threshold = 0.8 if is_system_request else 0.5 + if char_ratio > threshold: + risk += 0.2 if is_system_request else 0.4 + + # Multiple encoding indicators - reduced risk for system content encoding_indicators = ["base64", "hex", "unicode", "url", "ascii"] found_encodings = sum(1 for indicator in encoding_indicators if indicator.lower() in content.lower()) if found_encodings > 1: - risk += 0.3 - - # Excessive newlines or formatting (potential formatting attacks) - if content.count('\n') > 50 or content.count('\\n') > 50: - risk += 0.2 - + risk += 0.1 if is_system_request else 0.3 + + # Excessive newlines or formatting - more lenient for system content + newline_threshold = 200 if is_system_request else 50 + if content.count('\n') > newline_threshold or content.count('\\n') > newline_threshold: + risk += 0.1 if is_system_request else 0.2 + return risk - + + def _is_system_request(self, messages: List[Dict[str, str]]) -> bool: + """Determine if this is a system/RAG request""" + if not messages: + return False + + # Check for system messages + for message in messages: + if message.get("role") == "system": + return True + + # Check message content for RAG indicators + for message in messages: + content = message.get("content", "") + if ("document:" in content.lower() or + "context:" in content.lower() or + "source:" in content.lower() or + "retrieved:" in content.lower() or + "citation:" in content.lower() or + "reference:" in content.lower()): + return True + + return False + def create_audit_log( self, user_id: str, @@ -195,11 +239,11 @@ class SecurityManager: audit_hash = self._create_audit_hash(audit_entry) audit_entry["audit_hash"] = audit_hash - # Log based on risk level + # Log based on risk level (never block, only log) if risk_score >= settings.API_SECURITY_RISK_THRESHOLD: - logger.error(f"HIGH RISK LLM REQUEST BLOCKED: {json.dumps(audit_entry)}") + logger.warning(f"HIGH RISK LLM REQUEST DETECTED (NOT BLOCKED): {json.dumps(audit_entry)}") elif risk_score >= settings.API_SECURITY_WARNING_THRESHOLD: - logger.warning(f"MEDIUM RISK LLM REQUEST: {json.dumps(audit_entry)}") + logger.info(f"MEDIUM RISK LLM REQUEST: {json.dumps(audit_entry)}") else: logger.info(f"LLM REQUEST AUDIT: user={user_id}, model={model}, risk={risk_score:.3f}") diff --git a/backend/app/services/llm/service.py b/backend/app/services/llm/service.py index fae28fd..bb8e683 100644 --- a/backend/app/services/llm/service.py +++ b/backend/app/services/llm/service.py @@ -16,6 +16,7 @@ from .models import ( ModelInfo, ProviderStatus, LLMMetrics ) from .config import config_manager, ProviderConfig +from ...core.config import settings from .security import security_manager from .resilience import ResilienceManagerFactory from .metrics import metrics_collector @@ -149,19 +150,17 @@ class LLMService: if not request.messages: raise ValidationError("Messages cannot be empty", field="messages") - # Security validation - # Chatbot and RAG system requests should have relaxed security validation - is_system_request = ( - request.user_id == "rag_system" or - request.user_id == "chatbot_user" or - str(request.user_id).startswith("chatbot_") - ) - + # Security validation (only if enabled) messages_dict = [{"role": msg.role, "content": msg.content} for msg in request.messages] - is_safe, risk_score, detected_patterns = security_manager.validate_prompt_security(messages_dict) - - if not is_safe and not is_system_request: - # Log security violation for regular user requests + + if settings.API_SECURITY_ENABLED: + is_safe, risk_score, detected_patterns = security_manager.validate_prompt_security(messages_dict) + else: + # Security disabled - always safe + is_safe, risk_score, detected_patterns = True, 0.0, [] + + if not is_safe: + # Log security violation security_manager.create_audit_log( user_id=request.user_id, api_key_id=request.api_key_id, @@ -171,7 +170,7 @@ class LLMService: risk_score=risk_score, detected_patterns=[p.get("pattern", "") for p in detected_patterns] ) - + # Record blocked request metrics_collector.record_request( provider="security", @@ -184,18 +183,12 @@ class LLMService: user_id=request.user_id, api_key_id=request.api_key_id ) - + raise SecurityError( "Request blocked due to security concerns", risk_score=risk_score, details={"detected_patterns": detected_patterns} ) - elif not is_safe and is_system_request: - # For system requests (chatbot/RAG), log but don't block - logger.info(f"System request contains security patterns (risk_score={risk_score:.2f}) but allowing due to system context") - if detected_patterns: - logger.info(f"Detected patterns: {[p.get('pattern', 'unknown') for p in detected_patterns]}") - # Allow system requests regardless of security patterns # Get provider for model provider_name = self._get_provider_for_model(request.model) @@ -317,25 +310,20 @@ class LLMService: await self.initialize() # Security validation (same as non-streaming) - # Chatbot and RAG system requests should have relaxed security validation - is_system_request = ( - request.user_id == "rag_system" or - request.user_id == "chatbot_user" or - str(request.user_id).startswith("chatbot_") - ) - messages_dict = [{"role": msg.role, "content": msg.content} for msg in request.messages] - is_safe, risk_score, detected_patterns = security_manager.validate_prompt_security(messages_dict) - - if not is_safe and not is_system_request: + + if settings.API_SECURITY_ENABLED: + is_safe, risk_score, detected_patterns = security_manager.validate_prompt_security(messages_dict) + else: + # Security disabled - always safe + is_safe, risk_score, detected_patterns = True, 0.0, [] + + if not is_safe: raise SecurityError( "Streaming request blocked due to security concerns", risk_score=risk_score, details={"detected_patterns": detected_patterns} ) - elif not is_safe and is_system_request: - # For system requests (chatbot/RAG), log but don't block - logger.info(f"System streaming request contains security patterns (risk_score={risk_score:.2f}) but allowing due to system context") # Get provider provider_name = self._get_provider_for_model(request.model) @@ -378,33 +366,22 @@ class LLMService: await self.initialize() # Security validation for embedding input - # RAG system requests (document embedding) should use relaxed security validation - is_rag_system = request.user_id == "rag_system" - - if not is_rag_system: - # Apply normal security validation for user-generated embedding requests - input_text = request.input if isinstance(request.input, str) else " ".join(request.input) + input_text = request.input if isinstance(request.input, str) else " ".join(request.input) + + if settings.API_SECURITY_ENABLED: is_safe, risk_score, detected_patterns = security_manager.validate_prompt_security([ {"role": "user", "content": input_text} ]) - - if not is_safe: - raise SecurityError( - "Embedding request blocked due to security concerns", - risk_score=risk_score, - details={"detected_patterns": detected_patterns} - ) else: - # For RAG system requests, log but don't block (document content can contain legitimate text that triggers patterns) - input_text = request.input if isinstance(request.input, str) else " ".join(request.input) - is_safe, risk_score, detected_patterns = security_manager.validate_prompt_security([ - {"role": "user", "content": input_text} - ]) - - if detected_patterns: - logger.info(f"RAG document embedding contains security patterns (risk_score={risk_score:.2f}) but allowing due to document context") - - # Allow RAG system requests regardless of security patterns + # Security disabled - always safe + is_safe, risk_score, detected_patterns = True, 0.0, [] + + if not is_safe: + raise SecurityError( + "Embedding request blocked due to security concerns", + risk_score=risk_score, + details={"detected_patterns": detected_patterns} + ) # Get provider provider_name = self._get_provider_for_model(request.model) diff --git a/backend/modules/chatbot/main.py b/backend/modules/chatbot/main.py index 38ee9ec..6f42f09 100644 --- a/backend/modules/chatbot/main.py +++ b/backend/modules/chatbot/main.py @@ -265,6 +265,7 @@ class ChatbotModule(BaseModule): async def chat_completion(self, request: ChatRequest, user_id: str, db: Session) -> ChatResponse: """Generate chat completion response""" + logger.info("=== CHAT COMPLETION METHOD CALLED ===") # Get chatbot configuration from database db_chatbot = db.query(DBChatbotInstance).filter(DBChatbotInstance.id == request.chatbot_id).first() @@ -363,10 +364,11 @@ class ChatbotModule(BaseModule): metadata={"error": str(e), "fallback": True} ) - async def _generate_response(self, message: str, db_messages: List[DBMessage], + async def _generate_response(self, message: str, db_messages: List[DBMessage], config: ChatbotConfig, context: Optional[Dict] = None, db: Session = None) -> tuple[str, Optional[List]]: """Generate response using LLM with optional RAG""" - + logger.info("=== _generate_response METHOD CALLED ===") + # Lazy load dependencies if not available await self._ensure_dependencies() @@ -426,6 +428,11 @@ class ChatbotModule(BaseModule): logger.warning(f"RAG search traceback: {traceback.format_exc()}") # Build conversation context (includes the current message from db_messages) + logger.info(f"=== CRITICAL DEBUG ===") + logger.info(f"rag_context length: {len(rag_context)}") + logger.info(f"rag_context empty: {not rag_context}") + logger.info(f"rag_context preview: {rag_context[:200] if rag_context else 'EMPTY'}") + logger.info(f"=== END CRITICAL DEBUG ===") messages = self._build_conversation_messages(db_messages, config, rag_context, context) # Note: Current user message is already included in db_messages from the query @@ -511,32 +518,38 @@ class ChatbotModule(BaseModule): # Return fallback if available return "I'm currently unable to process your request. Please try again later.", None - def _build_conversation_messages(self, db_messages: List[DBMessage], config: ChatbotConfig, + def _build_conversation_messages(self, db_messages: List[DBMessage], config: ChatbotConfig, rag_context: str = "", context: Optional[Dict] = None) -> List[Dict]: """Build messages array for LLM completion""" - + messages = [] - - # System prompt + logger.info(f"DEBUG: _build_conversation_messages called. rag_context length: {len(rag_context)}") + + # System prompt - keep it clean without RAG context system_prompt = config.system_prompt - if rag_context: - system_prompt += rag_context if context and context.get('additional_instructions'): - system_prompt += f"\\n\\nAdditional instructions: {context['additional_instructions']}" - + system_prompt += f"\n\nAdditional instructions: {context['additional_instructions']}" + messages.append({"role": "system", "content": system_prompt}) - + logger.info(f"Building messages from {len(db_messages)} database messages") - + # Conversation history (messages are already limited by memory_length in the query) # Reverse to get chronological order # Include ALL messages - the current user message is needed for the LLM to respond! for idx, msg in enumerate(reversed(db_messages)): logger.info(f"Processing message {idx}: role={msg.role}, content_preview={msg.content[:50] if msg.content else 'None'}...") if msg.role in ["user", "assistant"]: + # For user messages, prepend RAG context if available + content = msg.content + if msg.role == "user" and rag_context and idx == 0: + # Add RAG context to the current user message (first in reversed order) + content = f"Relevant information from knowledge base:\n{rag_context}\n\nQuestion: {msg.content}" + logger.info("Added RAG context to user message") + messages.append({ "role": msg.role, - "content": msg.content + "content": content }) logger.info(f"Added message with role {msg.role} to LLM messages") else: @@ -677,9 +690,10 @@ class ChatbotModule(BaseModule): return router # API Compatibility Methods - async def chat(self, chatbot_config: Dict[str, Any], message: str, + async def chat(self, chatbot_config: Dict[str, Any], message: str, conversation_history: List = None, user_id: str = "anonymous") -> Dict[str, Any]: """Chat method for API compatibility""" + logger.info("=== CHAT METHOD (API COMPATIBILITY) CALLED ===") logger.info(f"Chat method called with message: {message[:50]}... by user: {user_id}") # Lazy load dependencies @@ -709,9 +723,20 @@ class ChatbotModule(BaseModule): fallback_responses=chatbot_config.get("fallback_responses", []) ) - # Generate response using internal method with empty message history + # For API compatibility, create a temporary DBMessage for the current message + # so RAG context can be properly added + from app.models.chatbot import ChatbotMessage as DBMessage + + # Create a temporary user message with the current message + temp_user_message = DBMessage( + conversation_id="temp_conversation", + role=MessageRole.USER.value, + content=message + ) + + # Generate response using internal method with the current message included response_content, sources = await self._generate_response( - message, [], config, None, db + message, [temp_user_message], config, None, db ) return { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5f8f91b..b5af4b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2613,9 +2613,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", diff --git a/frontend/src/app/api-keys/page.tsx b/frontend/src/app/api-keys/page.tsx index e62e2c7..da5bc62 100644 --- a/frontend/src/app/api-keys/page.tsx +++ b/frontend/src/app/api-keys/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { useSearchParams } from "next/navigation"; +import { Suspense } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -93,7 +94,7 @@ const PERMISSION_OPTIONS = [ { value: "llm:embeddings", label: "LLM Embeddings" }, ]; -export default function ApiKeysPage() { +function ApiKeysContent() { const { toast } = useToast(); const searchParams = useSearchParams(); const [apiKeys, setApiKeys] = useState([]); @@ -905,4 +906,12 @@ export default function ApiKeysPage() { ); +} + +export default function ApiKeysPage() { + return ( + Loading API keys...}> + + + ); } \ No newline at end of file diff --git a/frontend/src/components/chatbot/ChatInterface.tsx b/frontend/src/components/chatbot/ChatInterface.tsx index fb65658..4e1fe8d 100644 --- a/frontend/src/components/chatbot/ChatInterface.tsx +++ b/frontend/src/components/chatbot/ChatInterface.tsx @@ -87,9 +87,8 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface const [messages, setMessages] = useState([]) const [input, setInput] = useState("") const [isLoading, setIsLoading] = useState(false) - const [conversationId, setConversationId] = useState(null) const scrollAreaRef = useRef(null) - const { toast } = useToast() + const { success: toastSuccess, error: toastError } = useToast() const scrollToBottom = useCallback(() => { if (scrollAreaRef.current) { @@ -120,23 +119,20 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface setIsLoading(true) try { - // Build conversation history in OpenAI format + let data: any + + // Use internal API const conversationHistory = messages.map(msg => ({ role: msg.role, content: msg.content })) - - const data = await chatbotApi.sendMessage( + + data = await chatbotApi.sendMessage( chatbotId, messageToSend, - conversationId || undefined, + undefined, // No conversation ID conversationHistory ) - - // Update conversation ID if it's a new conversation - if (!conversationId && data.conversation_id) { - setConversationId(data.conversation_id) - } const assistantMessage: ChatMessage = { id: data.message_id || generateTimestampId('msg'), @@ -153,16 +149,16 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface // More specific error handling if (appError.code === 'UNAUTHORIZED') { - toast.error("Authentication Required", "Please log in to continue chatting.") + toastError("Authentication Required", "Please log in to continue chatting.") } else if (appError.code === 'NETWORK_ERROR') { - toast.error("Connection Error", "Please check your internet connection and try again.") + toastError("Connection Error", "Please check your internet connection and try again.") } else { - toast.error("Message Failed", appError.message || "Failed to send message. Please try again.") + toastError("Message Failed", appError.message || "Failed to send message. Please try again.") } } finally { setIsLoading(false) } - }, [input, isLoading, chatbotId, conversationId, messages, toast]) + }, [input, isLoading, chatbotId, messages, toastError]) const handleKeyPress = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -174,11 +170,11 @@ export function ChatInterface({ chatbotId, chatbotName, onClose }: ChatInterface const copyMessage = useCallback(async (content: string) => { try { await navigator.clipboard.writeText(content) - toast.success("Copied", "Message copied to clipboard") + toastSuccess("Copied", "Message copied to clipboard") } catch (error) { - toast.error("Copy Failed", "Unable to copy message to clipboard") + toastError("Copy Failed", "Unable to copy message to clipboard") } - }, [toast]) + }, [toastSuccess, toastError]) const formatTime = useCallback((date: Date) => { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) diff --git a/frontend/src/components/chatbot/ChatbotManager.tsx b/frontend/src/components/chatbot/ChatbotManager.tsx index 9178c66..6221d4e 100644 --- a/frontend/src/components/chatbot/ChatbotManager.tsx +++ b/frontend/src/components/chatbot/ChatbotManager.tsx @@ -138,6 +138,7 @@ export function ChatbotManager() { const [editingChatbot, setEditingChatbot] = useState(null) const [showChatInterface, setShowChatInterface] = useState(false) const [testingChatbot, setTestingChatbot] = useState(null) + const [chatbotApiKeys, setChatbotApiKeys] = useState>({}) const { toast } = useToast() // New chatbot form state diff --git a/frontend/src/lib/api-client.ts b/frontend/src/lib/api-client.ts index 126f733..4df0089 100644 --- a/frontend/src/lib/api-client.ts +++ b/frontend/src/lib/api-client.ts @@ -86,11 +86,31 @@ export const chatbotApi = { deleteChatbot(id: string) { return apiClient.delete(`/api-internal/v1/chatbot/delete/${encodeURIComponent(id)}`) }, + // Legacy method with JWT auth (to be deprecated) sendMessage(chatbotId: string, message: string, conversationId?: string, history?: Array<{role: string; content: string}>) { - const body: any = { chatbot_id: chatbotId, message } + const body: any = { message } if (conversationId) body.conversation_id = conversationId if (history) body.history = history - return apiClient.post('/api-internal/v1/chatbot/chat', body) + return apiClient.post(`/api-internal/v1/chatbot/chat/${encodeURIComponent(chatbotId)}`, body) }, + // OpenAI-compatible chatbot API with API key auth + sendOpenAIChatMessage(chatbotId: string, messages: Array<{role: string; content: string}>, apiKey: string, options?: { + temperature?: number + max_tokens?: number + stream?: boolean + }) { + const body: any = { + messages, + ...options + } + return fetch(`/api/v1/chatbot/external/${encodeURIComponent(chatbotId)}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify(body) + }).then(res => res.json()) + } }