Skip to main content

brainwires_rag/rag/
error.rs

1/// Centralized error types for brainwires-rag using thiserror
2///
3/// Provides domain-specific error types for better error handling and user-facing messages.
4use thiserror::Error;
5
6/// Main error type for the RAG system
7#[derive(Error, Debug)]
8pub enum RagError {
9    /// Embedding generation error.
10    #[error("Embedding error: {0}")]
11    Embedding(#[from] EmbeddingError),
12
13    /// Vector database error.
14    #[error("Vector database error: {0}")]
15    VectorDb(#[from] VectorDbError),
16
17    /// File indexing error.
18    #[error("Indexing error: {0}")]
19    Indexing(#[from] IndexingError),
20
21    /// Code chunking error.
22    #[error("Chunking error: {0}")]
23    Chunking(#[from] ChunkingError),
24
25    /// Configuration error.
26    #[error("Configuration error: {0}")]
27    Config(#[from] ConfigError),
28
29    /// Input validation error.
30    #[error("Validation error: {0}")]
31    Validation(#[from] ValidationError),
32
33    /// Git operation error.
34    #[error("Git error: {0}")]
35    Git(#[from] GitError),
36
37    /// Cache operation error.
38    #[error("Cache error: {0}")]
39    Cache(#[from] CacheError),
40
41    /// I/O error.
42    #[error("IO error: {0}")]
43    Io(#[from] std::io::Error),
44
45    /// Catch-all error with a message string.
46    #[error("{0}")]
47    Other(String),
48}
49
50/// Errors related to embedding generation
51#[derive(Error, Debug)]
52pub enum EmbeddingError {
53    /// Model initialization failure.
54    #[error("Failed to initialize embedding model: {0}")]
55    InitializationFailed(String),
56
57    /// Embedding generation failure.
58    #[error("Failed to generate embeddings: {0}")]
59    GenerationFailed(String),
60
61    /// Empty batch provided for embedding.
62    #[error("Embedding batch is empty")]
63    EmptyBatch,
64
65    /// Embedding generation timed out.
66    #[error("Embedding generation timed out after {0} seconds")]
67    Timeout(u64),
68
69    /// Dimension mismatch between expected and actual embedding vectors.
70    #[error("Invalid embedding dimension: expected {expected}, got {actual}")]
71    DimensionMismatch {
72        /// Expected dimension.
73        expected: usize,
74        /// Actual dimension received.
75        actual: usize,
76    },
77
78    /// Internal model lock was poisoned.
79    #[error("Model lock was poisoned: {0}")]
80    LockPoisoned(String),
81}
82
83/// Errors related to vector database operations
84#[derive(Error, Debug)]
85pub enum VectorDbError {
86    /// Database initialization failure.
87    #[error("Failed to initialize vector database: {0}")]
88    InitializationFailed(String),
89
90    /// Database connection failure.
91    #[error("Failed to connect to vector database: {0}")]
92    ConnectionFailed(String),
93
94    /// Collection creation failure.
95    #[error("Failed to create collection '{collection}': {reason}")]
96    CollectionCreationFailed {
97        /// Collection name.
98        collection: String,
99        /// Failure reason.
100        reason: String,
101    },
102
103    /// Collection not found.
104    #[error("Collection '{0}' not found")]
105    CollectionNotFound(String),
106
107    /// Failed to store embeddings.
108    #[error("Failed to store embeddings: {0}")]
109    StoreFailed(String),
110
111    /// Failed to search embeddings.
112    #[error("Failed to search embeddings: {0}")]
113    SearchFailed(String),
114
115    /// Failed to delete embeddings.
116    #[error("Failed to delete embeddings: {0}")]
117    DeleteFailed(String),
118
119    /// Failed to get statistics.
120    #[error("Failed to get statistics: {0}")]
121    StatisticsFailed(String),
122
123    /// Failed to clear database.
124    #[error("Failed to clear database: {0}")]
125    ClearFailed(String),
126
127    /// Invalid search parameters.
128    #[error("Invalid search parameters: {0}")]
129    InvalidSearchParams(String),
130
131    /// Database not initialized.
132    #[error("Database is not initialized")]
133    NotInitialized,
134}
135
136/// Errors related to file indexing
137#[derive(Error, Debug)]
138pub enum IndexingError {
139    /// Directory not found.
140    #[error("Directory not found: {0}")]
141    DirectoryNotFound(String),
142
143    /// Path is not a directory.
144    #[error("Path is not a directory: {0}")]
145    NotADirectory(String),
146
147    /// Failed to walk directory tree.
148    #[error("Failed to walk directory: {0}")]
149    WalkFailed(String),
150
151    /// Failed to read a file.
152    #[error("Failed to read file '{file}': {reason}")]
153    FileReadFailed {
154        /// File path that failed.
155        file: String,
156        /// Failure reason.
157        reason: String,
158    },
159
160    /// File is not valid UTF-8.
161    #[error("File is not valid UTF-8: {0}")]
162    InvalidUtf8(String),
163
164    /// File is binary and cannot be indexed.
165    #[error("File is binary and cannot be indexed: {0}")]
166    BinaryFile(String),
167
168    /// File size exceeds maximum.
169    #[error("File size exceeds maximum: {size} > {max}")]
170    FileTooLarge {
171        /// Actual file size.
172        size: usize,
173        /// Maximum allowed size.
174        max: usize,
175    },
176
177    /// Failed to calculate file hash.
178    #[error("Failed to calculate file hash: {0}")]
179    HashCalculationFailed(String),
180
181    /// No files found to index.
182    #[error("No files found to index")]
183    NoFilesFound,
184
185    /// Indexing was cancelled.
186    #[error("Indexing was cancelled")]
187    Cancelled,
188}
189
190/// Errors related to code chunking
191#[derive(Error, Debug)]
192pub enum ChunkingError {
193    /// Code parsing failure.
194    #[error("Failed to parse code: {0}")]
195    ParseFailed(String),
196
197    /// Unsupported programming language.
198    #[error("Unsupported language: {0}")]
199    UnsupportedLanguage(String),
200
201    /// Invalid chunk size configuration.
202    #[error("Invalid chunk size: {0}")]
203    InvalidChunkSize(String),
204
205    /// No chunks generated from the file.
206    #[error("No chunks generated from file: {0}")]
207    NoChunksGenerated(String),
208
209    /// AST parsing failure.
210    #[error("AST parsing failed: {0}")]
211    AstParsingFailed(String),
212}
213
214/// Errors related to configuration
215#[derive(Error, Debug)]
216pub enum ConfigError {
217    /// Configuration file loading failure.
218    #[error("Failed to load configuration file: {0}")]
219    LoadFailed(String),
220
221    /// Configuration parsing failure.
222    #[error("Failed to parse configuration: {0}")]
223    ParseFailed(String),
224
225    /// Invalid configuration value.
226    #[error("Invalid configuration value for '{key}': {reason}")]
227    InvalidValue {
228        /// Configuration key name.
229        key: String,
230        /// Reason why the value is invalid.
231        reason: String,
232    },
233
234    /// Missing required configuration.
235    #[error("Missing required configuration: {0}")]
236    MissingRequired(String),
237
238    /// Configuration saving failure.
239    #[error("Failed to save configuration: {0}")]
240    SaveFailed(String),
241
242    /// Configuration file not found.
243    #[error("Configuration file not found: {0}")]
244    FileNotFound(String),
245}
246
247/// Errors related to input validation
248#[derive(Error, Debug)]
249pub enum ValidationError {
250    /// Path does not exist.
251    #[error("Path does not exist: {0}")]
252    PathNotFound(String),
253
254    /// Path is not absolute.
255    #[error("Path is not absolute: {0}")]
256    PathNotAbsolute(String),
257
258    /// Invalid path.
259    #[error("Invalid path: {0}")]
260    InvalidPath(String),
261
262    /// Invalid project name.
263    #[error("Invalid project name: {0}")]
264    InvalidProjectName(String),
265
266    /// Invalid glob pattern.
267    #[error("Invalid pattern: {0}")]
268    InvalidPattern(String),
269
270    /// Constraint violation on a field value.
271    #[error("{field} must be {constraint}, got {actual}")]
272    ConstraintViolation {
273        /// Field name that violated the constraint.
274        field: String,
275        /// Description of the constraint.
276        constraint: String,
277        /// Actual value provided.
278        actual: String,
279    },
280
281    /// Invalid value for a parameter.
282    #[error("Invalid value for {0}: {1}")]
283    InvalidValue(String, String),
284
285    /// Required field is empty.
286    #[error("Empty {0}")]
287    Empty(String),
288}
289
290/// Errors related to git operations
291#[derive(Error, Debug)]
292pub enum GitError {
293    /// Git repository not found.
294    #[error("Git repository not found at: {0}")]
295    RepoNotFound(String),
296
297    /// Failed to open git repository.
298    #[error("Failed to open git repository: {0}")]
299    OpenFailed(String),
300
301    /// Git reference not found.
302    #[error("Failed to get git reference: {0}")]
303    RefNotFound(String),
304
305    /// Failed to iterate over commits.
306    #[error("Failed to iterate commits: {0}")]
307    IterFailed(String),
308
309    /// Invalid commit hash.
310    #[error("Invalid commit hash: {0}")]
311    InvalidCommitHash(String),
312
313    /// Failed to parse commit data.
314    #[error("Failed to parse commit: {0}")]
315    ParseFailed(String),
316
317    /// Branch not found.
318    #[error("Branch not found: {0}")]
319    BranchNotFound(String),
320
321    /// No commits found matching search criteria.
322    #[error("No commits found matching criteria")]
323    NoCommitsFound,
324}
325
326/// Errors related to cache operations
327#[derive(Error, Debug)]
328pub enum CacheError {
329    /// Failed to load cache.
330    #[error("Failed to load cache from '{path}': {reason}")]
331    LoadFailed {
332        /// Cache file path.
333        path: String,
334        /// Failure reason.
335        reason: String,
336    },
337
338    /// Failed to save cache.
339    #[error("Failed to save cache to '{path}': {reason}")]
340    SaveFailed {
341        /// Cache file path.
342        path: String,
343        /// Failure reason.
344        reason: String,
345    },
346
347    /// Cache file parsing failure.
348    #[error("Failed to parse cache file: {0}")]
349    ParseFailed(String),
350
351    /// Cache data is corrupted.
352    #[error("Cache is corrupted: {0}")]
353    Corrupted(String),
354
355    /// Failed to create cache directory.
356    #[error("Failed to create cache directory: {0}")]
357    DirectoryCreationFailed(String),
358}
359
360// Conversion from anyhow::Error to RagError
361impl From<anyhow::Error> for RagError {
362    fn from(err: anyhow::Error) -> Self {
363        RagError::Other(format!("{:#}", err))
364    }
365}
366
367// Helper methods for RagError
368impl RagError {
369    /// Create a new error from a string message
370    pub fn other(msg: impl Into<String>) -> Self {
371        RagError::Other(msg.into())
372    }
373
374    /// Convert to a user-facing error string suitable for MCP responses
375    pub fn to_user_string(&self) -> String {
376        format!("{}", self)
377    }
378
379    /// Check if this is a user error (validation, not found) vs system error
380    pub fn is_user_error(&self) -> bool {
381        matches!(
382            self,
383            RagError::Validation(_) | RagError::Config(ConfigError::InvalidValue { .. })
384        )
385    }
386
387    /// Check if this error is retryable
388    pub fn is_retryable(&self) -> bool {
389        matches!(
390            self,
391            RagError::VectorDb(VectorDbError::ConnectionFailed(_))
392                | RagError::Embedding(EmbeddingError::Timeout(_))
393                | RagError::Io(_)
394        )
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn test_error_display() {
404        let err = RagError::Validation(ValidationError::PathNotFound("/test".to_string()));
405        assert_eq!(
406            err.to_string(),
407            "Validation error: Path does not exist: /test"
408        );
409    }
410
411    #[test]
412    fn test_error_from_io() {
413        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
414        let rag_err: RagError = io_err.into();
415        assert!(matches!(rag_err, RagError::Io(_)));
416    }
417
418    #[test]
419    fn test_error_from_anyhow() {
420        let anyhow_err = anyhow::anyhow!("test error");
421        let rag_err: RagError = anyhow_err.into();
422        assert!(matches!(rag_err, RagError::Other(_)));
423    }
424
425    #[test]
426    fn test_is_user_error() {
427        let user_err = RagError::Validation(ValidationError::InvalidPath("test".to_string()));
428        assert!(user_err.is_user_error());
429
430        let system_err = RagError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
431        assert!(!system_err.is_user_error());
432    }
433
434    #[test]
435    fn test_is_retryable() {
436        let retryable = RagError::VectorDb(VectorDbError::ConnectionFailed("test".to_string()));
437        assert!(retryable.is_retryable());
438
439        let not_retryable = RagError::Validation(ValidationError::InvalidPath("test".to_string()));
440        assert!(!not_retryable.is_retryable());
441    }
442
443    #[test]
444    fn test_embedding_error_timeout() {
445        let err = EmbeddingError::Timeout(30);
446        assert_eq!(
447            err.to_string(),
448            "Embedding generation timed out after 30 seconds"
449        );
450    }
451
452    #[test]
453    fn test_embedding_error_dimension_mismatch() {
454        let err = EmbeddingError::DimensionMismatch {
455            expected: 384,
456            actual: 512,
457        };
458        assert_eq!(
459            err.to_string(),
460            "Invalid embedding dimension: expected 384, got 512"
461        );
462    }
463
464    #[test]
465    fn test_vector_db_error_collection_creation() {
466        let err = VectorDbError::CollectionCreationFailed {
467            collection: "test_collection".to_string(),
468            reason: "already exists".to_string(),
469        };
470        assert_eq!(
471            err.to_string(),
472            "Failed to create collection 'test_collection': already exists"
473        );
474    }
475
476    #[test]
477    fn test_indexing_error_file_too_large() {
478        let err = IndexingError::FileTooLarge {
479            size: 1000000,
480            max: 500000,
481        };
482        assert_eq!(
483            err.to_string(),
484            "File size exceeds maximum: 1000000 > 500000"
485        );
486    }
487
488    #[test]
489    fn test_validation_error_constraint() {
490        let err = ValidationError::ConstraintViolation {
491            field: "max_file_size".to_string(),
492            constraint: "less than 100MB".to_string(),
493            actual: "200MB".to_string(),
494        };
495        assert_eq!(
496            err.to_string(),
497            "max_file_size must be less than 100MB, got 200MB"
498        );
499    }
500
501    #[test]
502    fn test_config_error_invalid_value() {
503        let err = ConfigError::InvalidValue {
504            key: "port".to_string(),
505            reason: "must be between 1-65535".to_string(),
506        };
507        assert_eq!(
508            err.to_string(),
509            "Invalid configuration value for 'port': must be between 1-65535"
510        );
511    }
512
513    #[test]
514    fn test_cache_error_load_failed() {
515        let err = CacheError::LoadFailed {
516            path: "/tmp/cache.json".to_string(),
517            reason: "permission denied".to_string(),
518        };
519        assert_eq!(
520            err.to_string(),
521            "Failed to load cache from '/tmp/cache.json': permission denied"
522        );
523    }
524
525    #[test]
526    fn test_rag_error_other() {
527        let err = RagError::other("custom error message");
528        assert_eq!(err.to_string(), "custom error message");
529    }
530
531    #[test]
532    fn test_error_chain() {
533        let embedding_err = EmbeddingError::GenerationFailed("model error".to_string());
534        let rag_err: RagError = embedding_err.into();
535        assert!(matches!(rag_err, RagError::Embedding(_)));
536        assert_eq!(
537            rag_err.to_string(),
538            "Embedding error: Failed to generate embeddings: model error"
539        );
540    }
541}