File size: 8,838 Bytes
e4b33e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
283
"""

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."
        }
    )