Spaces:
Runtime error
Runtime error
| import re | |
| from typing import Optional, Union | |
| import html | |
| import logging | |
| from pydantic import BaseModel, validator, ValidationError | |
| from fastapi import HTTPException, status | |
| logger = logging.getLogger(__name__) | |
| class QueryValidation: | |
| """ | |
| Input validation and sanitization for user queries | |
| """ | |
| # Dangerous patterns to block | |
| DANGEROUS_PATTERNS = [ | |
| r'system\(', # System command execution | |
| r'exec\(', # Code execution | |
| r'eval\(', # Code evaluation | |
| r'import\s+\w+', # Import statements | |
| r'__\w+__', # Special methods | |
| r'os\.\w+', # OS module access | |
| r'subprocess\.\w+', # Subprocess access | |
| ] | |
| def sanitize_input(text: str) -> str: | |
| """ | |
| Sanitize user input by escaping HTML and removing dangerous patterns | |
| """ | |
| if not text: | |
| return text | |
| # HTML escape to prevent XSS | |
| sanitized = html.escape(text) | |
| # Remove dangerous patterns | |
| for pattern in QueryValidation.DANGEROUS_PATTERNS: | |
| if re.search(pattern, sanitized, re.IGNORECASE): | |
| logger.warning(f"Dangerous pattern detected and removed: {pattern}") | |
| # Remove the dangerous pattern but keep the rest of the text | |
| sanitized = re.sub(pattern, '', sanitized, flags=re.IGNORECASE) | |
| return sanitized | |
| def validate_query_length(text: str, max_length: int = 10000) -> str: | |
| """ | |
| Validate query length | |
| """ | |
| if len(text) > max_length: | |
| raise ValueError(f"Query exceeds maximum length of {max_length} characters") | |
| return text | |
| def validate_content_type(content_type: str) -> bool: | |
| """ | |
| Validate content type against allowed values | |
| """ | |
| allowed_types = {'text', 'code', 'diagram_description', 'mathematical_formula'} | |
| return content_type in allowed_types | |
| def validate_user_type(user_type: str) -> bool: | |
| """ | |
| Validate user type against allowed values | |
| """ | |
| allowed_types = {'student', 'educator', 'admin'} | |
| return user_type in allowed_types | |
| def validate_message_type(message_type: str) -> bool: | |
| """ | |
| Validate message type against allowed values | |
| """ | |
| allowed_types = {'query', 'response', 'system_message', 'follow-up', 'text-selection'} | |
| return message_type in allowed_types | |
| def validate_record_type(record_type: str) -> bool: | |
| """ | |
| Validate analytics record type against allowed values | |
| """ | |
| allowed_types = {'user_interaction', 'common_question', 'system_usage', 'performance_metric'} | |
| return record_type in allowed_types | |
| def validate_aggregation_period(period: str) -> bool: | |
| """ | |
| Validate aggregation period against allowed values | |
| """ | |
| allowed_periods = {'hourly', 'daily', 'weekly', 'monthly'} | |
| return period in allowed_periods | |
| def validate_confidence_score(score: float) -> bool: | |
| """ | |
| Validate confidence score is between 0 and 1 | |
| """ | |
| return 0 <= score <= 1 | |
| def validate_url_path(path: str) -> bool: | |
| """ | |
| Validate URL path is safe (no directory traversal) | |
| """ | |
| if '../' in path or '..\\' in path: | |
| return False | |
| return True | |
| def is_safe_content(text: str) -> tuple[bool, Optional[str]]: | |
| """ | |
| Check if content is safe for processing | |
| Returns (is_safe, reason_if_unsafe) | |
| """ | |
| # Check for dangerous patterns | |
| for pattern in QueryValidation.DANGEROUS_PATTERNS: | |
| if re.search(pattern, text, re.IGNORECASE): | |
| return False, f"Dangerous pattern detected: {pattern}" | |
| # Check for excessive length | |
| if len(text) > 10000: # 10k characters max | |
| return False, "Content exceeds maximum length" | |
| # Check for potential prompt injection attempts | |
| injection_patterns = [ | |
| r'ignore\s+previous', | |
| r'forget\s+previous', | |
| r'system\s+prompt', | |
| r'you\s+are\s+now', | |
| r'disregard\s+instructions' | |
| ] | |
| for pattern in injection_patterns: | |
| if re.search(pattern, text, re.IGNORECASE): | |
| return False, f"Potential prompt injection detected: {pattern}" | |
| return True, None | |
| def validate_and_sanitize_query(query: str) -> str: | |
| """ | |
| Main function to validate and sanitize a user query | |
| """ | |
| if not query or not isinstance(query, str): | |
| raise HTTPException( | |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| detail="Query must be a non-empty string" | |
| ) | |
| # Validate length first | |
| try: | |
| query = QueryValidation.validate_query_length(query) | |
| except ValueError as e: | |
| raise HTTPException( | |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| detail=str(e) | |
| ) | |
| # Check if content is safe | |
| is_safe, reason = QueryValidation.is_safe_content(query) | |
| if not is_safe: | |
| logger.warning(f"Unsafe content detected: {reason}") | |
| raise HTTPException( | |
| status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, | |
| detail="Content validation failed" | |
| ) | |
| # Sanitize the input | |
| sanitized_query = QueryValidation.sanitize_input(query) | |
| return sanitized_query | |
| class ConversationCreateRequest(BaseModel): | |
| """ | |
| Pydantic model for conversation creation request validation | |
| """ | |
| user_id: Optional[str] = None | |
| session_id: Optional[str] = None | |
| initial_query: Optional[str] = None | |
| def validate_user_id(cls, v): | |
| if v is not None and len(v) > 100: | |
| raise ValueError('user_id must be 100 characters or less') | |
| return v | |
| def validate_session_id(cls, v): | |
| if v is not None and len(v) > 100: | |
| raise ValueError('session_id must be 100 characters or less') | |
| return v | |
| def validate_initial_query(cls, v): | |
| if v is not None: | |
| if len(v) > 10000: | |
| raise ValueError('initial_query exceeds maximum length of 10000 characters') | |
| # Additional validation for safety | |
| is_safe, reason = QueryValidation.is_safe_content(v) | |
| if not is_safe: | |
| raise ValueError(f'initial_query contains unsafe content: {reason}') | |
| return v | |
| class MessageCreateRequest(BaseModel): | |
| """ | |
| Pydantic model for message creation request validation | |
| """ | |
| content: str | |
| message_type: str = 'query' | |
| def validate_content(cls, v): | |
| if not v or len(v.strip()) == 0: | |
| raise ValueError('content cannot be empty') | |
| if len(v) > 10000: | |
| raise ValueError('content exceeds maximum length of 10000 characters') | |
| # Additional validation for safety | |
| is_safe, reason = QueryValidation.is_safe_content(v) | |
| if not is_safe: | |
| raise ValueError(f'content contains unsafe content: {reason}') | |
| return v | |
| def validate_message_type(cls, v): | |
| if not QueryValidation.validate_message_type(v): | |
| raise ValueError(f'invalid message_type: {v}') | |
| return v | |
| class SearchRequest(BaseModel): | |
| """ | |
| Pydantic model for search request validation | |
| """ | |
| query: str | |
| max_results: int = 5 | |
| include_citations: bool = True | |
| def validate_search_query(cls, v): | |
| if not v or len(v.strip()) == 0: | |
| raise ValueError('query cannot be empty') | |
| if len(v) > 1000: | |
| raise ValueError('query exceeds maximum length of 1000 characters') | |
| # Additional validation for safety | |
| is_safe, reason = QueryValidation.is_safe_content(v) | |
| if not is_safe: | |
| raise ValueError(f'query contains unsafe content: {reason}') | |
| return v | |
| def validate_max_results(cls, v): | |
| if v < 1 or v > 20: | |
| raise ValueError('max_results must be between 1 and 20') | |
| return v | |
| class FeedbackRequest(BaseModel): | |
| """ | |
| Pydantic model for feedback request validation | |
| """ | |
| message_id: str | |
| conversation_id: str | |
| feedback_type: str | |
| comment: Optional[str] = None | |
| def validate_feedback_type(cls, v): | |
| allowed_types = {'positive', 'negative', 'report_inaccurate', 'report_inappropriate'} | |
| if v not in allowed_types: | |
| raise ValueError(f'feedback_type must be one of {allowed_types}') | |
| return v | |
| def validate_comment(cls, v): | |
| if v and len(v) > 1000: | |
| raise ValueError('comment exceeds maximum length of 1000 characters') | |
| return v |