""" Custom Exceptions and Error Handling Issues #13, #14: Specific exception types, proper error logging """ import logging from typing import Optional, Dict, Any from fastapi import HTTPException from datetime import datetime import uuid logger = logging.getLogger("tb_guard_errors") class TBGuardException(Exception): """Base exception for TB-Guard-XAI""" def __init__( self, message: str, code: str, status_code: int = 500, details: Optional[Dict[str, Any]] = None ): self.message = message self.code = code self.status_code = status_code self.details = details or {} self.error_id = str(uuid.uuid4())[:8] self.timestamp = datetime.utcnow().isoformat() super().__init__(message) def to_http_exception(self) -> HTTPException: """Convert to FastAPI HTTPException""" return HTTPException( status_code=self.status_code, detail={ "error": self.message, "code": self.code, "error_id": self.error_id, "timestamp": self.timestamp } ) def log(self, exc_info=False): """Log the exception""" logger.error( f"[{self.code}] {self.message}", extra={"error_id": self.error_id, "details": self.details}, exc_info=exc_info ) # ============ Input Validation Errors ============ class InvalidImageError(TBGuardException): """Image validation failed""" def __init__(self, reason: str, details: Optional[Dict] = None): super().__init__( message=f"Invalid image: {reason}", code="INVALID_IMAGE", status_code=400, details=details ) class InvalidFileError(TBGuardException): """File upload validation failed""" def __init__(self, reason: str): super().__init__( message=f"Invalid file: {reason}", code="INVALID_FILE", status_code=400 ) class FileTooLargeError(TBGuardException): """File exceeds size limit""" def __init__(self, file_size_mb: float, max_size_mb: int): super().__init__( message=f"File size {file_size_mb:.1f} MB exceeds limit of {max_size_mb} MB", code="FILE_TOO_LARGE", status_code=400, details={"file_size_mb": file_size_mb, "max_size_mb": max_size_mb} ) class InvalidInputError(TBGuardException): """Input validation failed (symptoms, query, etc.)""" def __init__(self, field: str, reason: str): super().__init__( message=f"Invalid {field}: {reason}", code="INVALID_INPUT", status_code=400, details={"field": field} ) # ============ Model/Inference Errors ============ class ModelError(TBGuardException): """Model loading or inference error""" def __init__(self, reason: str): super().__init__( message=f"Model error: {reason}", code="MODEL_ERROR", status_code=500 ) class ModelNotLoadedError(TBGuardException): """Model hasn't been loaded""" def __init__(self): super().__init__( message="Model is not loaded. Server startup incomplete.", code="MODEL_NOT_LOADED", status_code=503 # Service Unavailable ) class InferenceError(TBGuardException): """Inference (prediction) failed""" def __init__(self, reason: str): super().__init__( message=f"Inference failed: {reason}", code="INFERENCE_ERROR", status_code=500 ) class OutOfDistributionError(TBGuardException): """Image is out-of-distribution (anomalous)""" def __init__(self, reason: str): super().__init__( message=f"Image appears to be out-of-distribution: {reason}", code="OUT_OF_DISTRIBUTION", status_code=400 ) # ============ External Service Errors ============ class LLMError(TBGuardException): """LLM (Mistral) API error""" def __init__(self, llm_name: str, reason: str): super().__init__( message=f"{llm_name} API error: {reason}", code=f"{llm_name.upper()}_ERROR", status_code=502 # Bad Gateway ) class AudioTranscriptionError(TBGuardException): """Audio transcription failed""" def __init__(self, reason: str): super().__init__( message=f"Audio transcription failed: {reason}", code="TRANSCRIPTION_ERROR", status_code=500 ) class RAGError(TBGuardException): """Vector database (Qdrant) error""" def __init__(self, reason: str): super().__init__( message=f"Evidence retrieval failed: {reason}", code="RAG_ERROR", status_code=503 ) class InternetConnectivityError(TBGuardException): """No internet connection (for cloud APIs)""" def __init__(self): super().__init__( message="No internet connection. Online mode unavailable.", code="NO_INTERNET", status_code=503 ) # ============ Authorization/Security Errors ============ class InvalidAPIKeyError(TBGuardException): """API key validation failed""" def __init__(self, reason: str = "Invalid or expired API key"): super().__init__( message=reason, code="INVALID_API_KEY", status_code=401 ) class QuotaExceededError(TBGuardException): """API quota exceeded""" def __init__(self, quota_type: str = "requests"): super().__init__( message=f"Daily {quota_type} quota exceeded", code="QUOTA_EXCEEDED", status_code=429 # Too Many Requests ) class RateLimitError(TBGuardException): """Rate limit exceeded""" def __init__(self, reset_seconds: int): super().__init__( message=f"Rate limit exceeded. Try again in {reset_seconds} seconds.", code="RATE_LIMITED", status_code=429, details={"retry_after_seconds": reset_seconds} ) # ============ Data/Preprocessing Errors ============ class PreprocessingError(TBGuardException): """Image preprocessing failed""" def __init__(self, reason: str): super().__init__( message=f"Preprocessing failed: {reason}", code="PREPROCESSING_ERROR", status_code=400 ) class CorruptedImageError(TBGuardException): """Image appears corrupted""" def __init__(self, reason: str): super().__init__( message=f"Image appears corrupted: {reason}", code="CORRUPTED_IMAGE", status_code=400 ) # ============ Configuration Errors ============ class ConfigurationError(TBGuardException): """Configuration validation failed (startup)""" def __init__(self, reason: str): super().__init__( message=f"Configuration error: {reason}", code="CONFIG_ERROR", status_code=500 ) class MissingEnvironmentVariableError(ConfigurationError): """Required environment variable not set""" def __init__(self, var_name: str): super().__init__(f"Missing required environment variable: {var_name}") # ============ Monitoring/Drift Errors ============ class PerformanceDegradationError(TBGuardException): """Model performance degraded (drift detected)""" def __init__(self, accuracy_drop: float): super().__init__( message=f"Model performance degraded by {accuracy_drop:.1%}. Manual review required.", code="PERFORMANCE_DEGRADATION", status_code=503 ) # ============ Error Handler Utility ============ def handle_exception(exc: Exception) -> HTTPException: """ Convert any exception to appropriate HTTP response """ if isinstance(exc, TBGuardException): exc.log(exc_info=True) return exc.to_http_exception() # Unexpected exception error_id = str(uuid.uuid4())[:8] logger.exception(f"Unexpected error [{error_id}]", extra={"error_id": error_id}) return HTTPException( status_code=500, detail={ "error": "Internal server error", "code": "INTERNAL_ERROR", "error_id": error_id, "message": "An unexpected error occurred. Please contact support with the error ID." } )