ferriclink_core/
errors.rs

1//! Error types for FerricLink Core
2//!
3//! This module provides comprehensive error handling for the FerricLink ecosystem,
4//! inspired by LangChain's exception system with Rust-specific improvements.
5
6use std::fmt;
7use thiserror::Error;
8
9/// Result type alias for FerricLink operations
10pub type Result<T> = std::result::Result<T, FerricLinkError>;
11
12/// Error codes for structured error handling
13#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub enum ErrorCode {
15    /// Invalid prompt input provided
16    InvalidPromptInput,
17    /// Invalid tool results received
18    InvalidToolResults,
19    /// Message coercion failed
20    MessageCoercionFailure,
21    /// Model authentication failed
22    ModelAuthentication,
23    /// Model not found
24    ModelNotFound,
25    /// Model rate limit exceeded
26    ModelRateLimit,
27    /// Output parsing failed
28    OutputParsingFailure,
29    /// Serialization/deserialization error
30    SerializationError,
31    /// IO operation failed
32    IoError,
33    /// HTTP request failed
34    HttpError,
35    /// Validation failed
36    ValidationError,
37    /// Configuration error
38    ConfigurationError,
39    /// Runtime error
40    RuntimeError,
41    /// Feature not implemented
42    NotImplemented,
43    /// Generic error
44    GenericError,
45}
46
47impl ErrorCode {
48    /// Get the string representation of the error code
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            ErrorCode::InvalidPromptInput => "INVALID_PROMPT_INPUT",
52            ErrorCode::InvalidToolResults => "INVALID_TOOL_RESULTS",
53            ErrorCode::MessageCoercionFailure => "MESSAGE_COERCION_FAILURE",
54            ErrorCode::ModelAuthentication => "MODEL_AUTHENTICATION",
55            ErrorCode::ModelNotFound => "MODEL_NOT_FOUND",
56            ErrorCode::ModelRateLimit => "MODEL_RATE_LIMIT",
57            ErrorCode::OutputParsingFailure => "OUTPUT_PARSING_FAILURE",
58            ErrorCode::SerializationError => "SERIALIZATION_ERROR",
59            ErrorCode::IoError => "IO_ERROR",
60            ErrorCode::HttpError => "HTTP_ERROR",
61            ErrorCode::ValidationError => "VALIDATION_ERROR",
62            ErrorCode::ConfigurationError => "CONFIGURATION_ERROR",
63            ErrorCode::RuntimeError => "RUNTIME_ERROR",
64            ErrorCode::NotImplemented => "NOT_IMPLEMENTED",
65            ErrorCode::GenericError => "GENERIC_ERROR",
66        }
67    }
68
69    /// Get the troubleshooting URL for this error code
70    pub fn troubleshooting_url(&self) -> String {
71        format!(
72            "https://ferrum-labs.github.io/FerricLink/docs/troubleshooting/errors/{}",
73            self.as_str().to_lowercase()
74        )
75    }
76}
77
78impl std::fmt::Display for ErrorCode {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}", self.as_str())
81    }
82}
83
84/// Main error type for FerricLink Core
85#[derive(Error, Debug)]
86pub enum FerricLinkError {
87    /// General FerricLink exception
88    #[error("FerricLink error: {0}")]
89    General(String),
90
91    /// Tracer-related errors
92    #[error("Tracer error: {0}")]
93    Tracer(#[from] TracerException),
94
95    /// Output parser errors with special handling
96    #[error("Output parser error: {0}")]
97    OutputParser(#[from] OutputParserException),
98
99    /// Serialization/deserialization errors
100    #[error("Serialization error: {0}")]
101    Serialization(#[from] serde_json::Error),
102
103    /// IO errors
104    #[error("IO error: {0}")]
105    Io(#[from] std::io::Error),
106
107    /// HTTP client errors
108    #[cfg(feature = "http")]
109    #[error("HTTP error: {0}")]
110    Http(#[from] reqwest::Error),
111
112    /// Validation errors
113    #[error("Validation error: {0}")]
114    Validation(String),
115
116    /// Configuration errors
117    #[error("Configuration error: {0}")]
118    Configuration(String),
119
120    /// Runtime errors
121    #[error("Runtime error: {0}")]
122    Runtime(String),
123
124    /// Not implemented errors
125    #[error("Not implemented: {0}")]
126    NotImplemented(String),
127
128    /// Generic errors
129    #[error("Error: {0}")]
130    Generic(String),
131}
132
133/// Tracer exception for tracing-related errors
134#[derive(Error, Debug)]
135#[error("Tracer error: {message}")]
136pub struct TracerException {
137    /// Error message
138    pub message: String,
139    /// Error code
140    pub error_code: ErrorCode,
141}
142
143impl TracerException {
144    /// Create a new tracer exception
145    pub fn new(message: impl Into<String>) -> Self {
146        Self {
147            message: message.into(),
148            error_code: ErrorCode::RuntimeError,
149        }
150    }
151
152    /// Create a new tracer exception with specific error code
153    pub fn with_code(message: impl Into<String>, error_code: ErrorCode) -> Self {
154        Self {
155            message: message.into(),
156            error_code,
157        }
158    }
159}
160
161/// Output parser exception with special handling for LLM feedback
162#[derive(Error, Debug)]
163#[error("Output parser error: {message}")]
164pub struct OutputParserException {
165    /// Error message
166    pub message: String,
167    /// Error code
168    pub error_code: ErrorCode,
169    /// Observation that can be sent to the model
170    pub observation: Option<String>,
171    /// LLM output that caused the error
172    pub llm_output: Option<String>,
173    /// Whether to send context back to the LLM
174    pub send_to_llm: bool,
175}
176
177impl OutputParserException {
178    /// Create a new output parser exception
179    pub fn new(message: impl Into<String>) -> Self {
180        Self {
181            message: message.into(),
182            error_code: ErrorCode::OutputParsingFailure,
183            observation: None,
184            llm_output: None,
185            send_to_llm: false,
186        }
187    }
188
189    /// Create a new output parser exception with error code
190    pub fn with_code(message: impl Into<String>, error_code: ErrorCode) -> Self {
191        Self {
192            message: message.into(),
193            error_code,
194            observation: None,
195            llm_output: None,
196            send_to_llm: false,
197        }
198    }
199
200    /// Create a new output parser exception with LLM feedback context
201    pub fn with_llm_context(
202        message: impl Into<String>,
203        observation: Option<String>,
204        llm_output: Option<String>,
205        send_to_llm: bool,
206    ) -> Self {
207        if send_to_llm && (observation.is_none() || llm_output.is_none()) {
208            panic!("Arguments 'observation' & 'llm_output' are required if 'send_to_llm' is True");
209        }
210
211        Self {
212            message: message.into(),
213            error_code: ErrorCode::OutputParsingFailure,
214            observation,
215            llm_output,
216            send_to_llm,
217        }
218    }
219
220    /// Get the observation for LLM feedback
221    pub fn observation(&self) -> Option<&str> {
222        self.observation.as_deref()
223    }
224
225    /// Get the LLM output that caused the error
226    pub fn llm_output(&self) -> Option<&str> {
227        self.llm_output.as_deref()
228    }
229
230    /// Check if this error should be sent back to the LLM
231    pub fn should_send_to_llm(&self) -> bool {
232        self.send_to_llm
233    }
234}
235
236impl FerricLinkError {
237    /// Create a new general FerricLink error
238    pub fn general(msg: impl Into<String>) -> Self {
239        Self::General(msg.into())
240    }
241
242    /// Create a new validation error
243    pub fn validation(msg: impl Into<String>) -> Self {
244        Self::Validation(msg.into())
245    }
246
247    /// Create a new configuration error
248    pub fn configuration(msg: impl Into<String>) -> Self {
249        Self::Configuration(msg.into())
250    }
251
252    /// Create a new runtime error
253    pub fn runtime(msg: impl Into<String>) -> Self {
254        Self::Runtime(msg.into())
255    }
256
257    /// Create a new not implemented error
258    pub fn not_implemented(msg: impl Into<String>) -> Self {
259        Self::NotImplemented(msg.into())
260    }
261
262    /// Create a new generic error
263    pub fn generic(msg: impl Into<String>) -> Self {
264        Self::Generic(msg.into())
265    }
266
267    /// Create a new error with error code and troubleshooting link
268    pub fn with_code(msg: impl Into<String>, error_code: ErrorCode) -> Self {
269        let message = create_error_message(msg, error_code);
270        Self::General(message)
271    }
272
273    /// Create a new error with specific error code (for testing and internal use)
274    pub fn with_error_code(msg: impl Into<String>, error_code: ErrorCode) -> Self {
275        match error_code {
276            ErrorCode::InvalidPromptInput => Self::invalid_prompt_input(msg),
277            ErrorCode::InvalidToolResults => Self::invalid_tool_results(msg),
278            ErrorCode::MessageCoercionFailure => Self::message_coercion_failure(msg),
279            ErrorCode::ModelAuthentication => Self::model_authentication(msg),
280            ErrorCode::ModelNotFound => Self::model_not_found(msg),
281            ErrorCode::ModelRateLimit => Self::model_rate_limit(msg),
282            ErrorCode::OutputParsingFailure => Self::output_parsing_failure(msg),
283            ErrorCode::SerializationError => Self::validation(msg),
284            ErrorCode::IoError => Self::runtime(msg),
285            ErrorCode::HttpError => Self::runtime(msg),
286            ErrorCode::ValidationError => Self::validation(msg),
287            ErrorCode::ConfigurationError => Self::configuration(msg),
288            ErrorCode::RuntimeError => Self::runtime(msg),
289            ErrorCode::NotImplemented => Self::not_implemented(msg),
290            ErrorCode::GenericError => Self::generic(msg),
291        }
292    }
293
294    /// Get the error code if available
295    pub fn error_code(&self) -> Option<ErrorCode> {
296        match self {
297            FerricLinkError::Tracer(e) => Some(e.error_code.clone()),
298            FerricLinkError::OutputParser(e) => Some(e.error_code.clone()),
299            FerricLinkError::Serialization(_) => Some(ErrorCode::SerializationError),
300            FerricLinkError::Io(_) => Some(ErrorCode::IoError),
301            #[cfg(feature = "http")]
302            FerricLinkError::Http(_) => Some(ErrorCode::HttpError),
303            FerricLinkError::Validation(_) => Some(ErrorCode::ValidationError),
304            FerricLinkError::Configuration(_) => Some(ErrorCode::ConfigurationError),
305            FerricLinkError::Runtime(_) => Some(ErrorCode::RuntimeError),
306            FerricLinkError::NotImplemented(_) => Some(ErrorCode::NotImplemented),
307            FerricLinkError::General(msg) => {
308                // Try to determine error code from message content
309                if msg.contains("INVALID_PROMPT_INPUT") {
310                    Some(ErrorCode::InvalidPromptInput)
311                } else if msg.contains("INVALID_TOOL_RESULTS") {
312                    Some(ErrorCode::InvalidToolResults)
313                } else if msg.contains("MESSAGE_COERCION_FAILURE") {
314                    Some(ErrorCode::MessageCoercionFailure)
315                } else if msg.contains("MODEL_AUTHENTICATION") {
316                    Some(ErrorCode::ModelAuthentication)
317                } else if msg.contains("MODEL_NOT_FOUND") {
318                    Some(ErrorCode::ModelNotFound)
319                } else if msg.contains("MODEL_RATE_LIMIT") {
320                    Some(ErrorCode::ModelRateLimit)
321                } else if msg.contains("OUTPUT_PARSING_FAILURE") {
322                    Some(ErrorCode::OutputParsingFailure)
323                } else {
324                    Some(ErrorCode::GenericError)
325                }
326            }
327            FerricLinkError::Generic(_) => Some(ErrorCode::GenericError),
328        }
329    }
330
331    /// Check if this is an output parser error that should be sent to LLM
332    pub fn should_send_to_llm(&self) -> bool {
333        match self {
334            FerricLinkError::OutputParser(e) => e.should_send_to_llm(),
335            _ => false,
336        }
337    }
338
339    /// Get LLM context if this is an output parser error
340    pub fn llm_context(&self) -> Option<(Option<&str>, Option<&str>)> {
341        match self {
342            FerricLinkError::OutputParser(e) => Some((e.observation(), e.llm_output())),
343            _ => None,
344        }
345    }
346}
347
348/// Create an error message with troubleshooting link
349///
350/// This function creates a comprehensive error message that includes
351/// a link to the troubleshooting guide, similar to LangChain's approach.
352///
353/// # Arguments
354///
355/// * `message` - The base error message
356/// * `error_code` - The error code for categorization
357///
358/// # Returns
359///
360/// A formatted error message with troubleshooting information
361pub fn create_error_message(message: impl Into<String>, error_code: ErrorCode) -> String {
362    let message = message.into();
363    let troubleshooting_url = error_code.troubleshooting_url();
364    let error_code_str = error_code.as_str();
365
366    format!(
367        "{message}\nError Code: {error_code_str}\nFor troubleshooting, visit: {troubleshooting_url}"
368    )
369}
370
371/// Trait for converting errors to FerricLinkError
372pub trait IntoFerricLinkError {
373    /// Convert to FerricLinkError
374    fn into_ferriclink_error(self) -> FerricLinkError;
375}
376
377impl<T> IntoFerricLinkError for T
378where
379    T: fmt::Display,
380{
381    fn into_ferriclink_error(self) -> FerricLinkError {
382        FerricLinkError::Generic(self.to_string())
383    }
384}
385
386/// Convenience functions for creating specific error types
387impl FerricLinkError {
388    /// Create an invalid prompt input error
389    pub fn invalid_prompt_input(msg: impl Into<String>) -> Self {
390        Self::General(create_error_message(msg, ErrorCode::InvalidPromptInput))
391    }
392
393    /// Create an invalid tool results error
394    pub fn invalid_tool_results(msg: impl Into<String>) -> Self {
395        Self::General(create_error_message(msg, ErrorCode::InvalidToolResults))
396    }
397
398    /// Create a message coercion failure error
399    pub fn message_coercion_failure(msg: impl Into<String>) -> Self {
400        Self::General(create_error_message(msg, ErrorCode::MessageCoercionFailure))
401    }
402
403    /// Create a model authentication error
404    pub fn model_authentication(msg: impl Into<String>) -> Self {
405        Self::General(create_error_message(msg, ErrorCode::ModelAuthentication))
406    }
407
408    /// Create a model not found error
409    pub fn model_not_found(msg: impl Into<String>) -> Self {
410        Self::General(create_error_message(msg, ErrorCode::ModelNotFound))
411    }
412
413    /// Create a model rate limit error
414    pub fn model_rate_limit(msg: impl Into<String>) -> Self {
415        Self::General(create_error_message(msg, ErrorCode::ModelRateLimit))
416    }
417
418    /// Create an output parsing failure error
419    pub fn output_parsing_failure(msg: impl Into<String>) -> Self {
420        Self::General(create_error_message(msg, ErrorCode::OutputParsingFailure))
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_error_creation() {
430        let validation_err = FerricLinkError::validation("test validation error");
431        assert!(matches!(validation_err, FerricLinkError::Validation(_)));
432
433        let config_err = FerricLinkError::configuration("test config error");
434        assert!(matches!(config_err, FerricLinkError::Configuration(_)));
435
436        let runtime_err = FerricLinkError::runtime("test runtime error");
437        assert!(matches!(runtime_err, FerricLinkError::Runtime(_)));
438    }
439
440    #[test]
441    fn test_error_display() {
442        let err = FerricLinkError::validation("test error");
443        assert!(err.to_string().contains("test error"));
444    }
445
446    #[test]
447    fn test_error_codes() {
448        let code = ErrorCode::InvalidPromptInput;
449        assert_eq!(code.as_str(), "INVALID_PROMPT_INPUT");
450        assert!(code.troubleshooting_url().contains("ferrum-labs.github.io"));
451    }
452
453    #[test]
454    fn test_tracer_exception() {
455        let tracer_err = TracerException::new("test tracer error");
456        assert_eq!(tracer_err.error_code, ErrorCode::RuntimeError);
457        assert_eq!(tracer_err.message, "test tracer error");
458
459        let tracer_err_with_code = TracerException::with_code("test", ErrorCode::ModelNotFound);
460        assert_eq!(tracer_err_with_code.error_code, ErrorCode::ModelNotFound);
461    }
462
463    #[test]
464    fn test_output_parser_exception() {
465        let parser_err = OutputParserException::new("test parser error");
466        assert_eq!(parser_err.error_code, ErrorCode::OutputParsingFailure);
467        assert!(!parser_err.should_send_to_llm());
468
469        let parser_err_with_context = OutputParserException::with_llm_context(
470            "test error",
471            Some("observation".to_string()),
472            Some("llm output".to_string()),
473            true,
474        );
475        assert!(parser_err_with_context.should_send_to_llm());
476        assert_eq!(parser_err_with_context.observation(), Some("observation"));
477        assert_eq!(parser_err_with_context.llm_output(), Some("llm output"));
478    }
479
480    #[test]
481    fn test_error_with_code() {
482        let err = FerricLinkError::with_error_code("test error", ErrorCode::ModelAuthentication);
483        assert!(err.to_string().contains("test error"));
484        assert_eq!(err.error_code(), Some(ErrorCode::ModelAuthentication));
485    }
486
487    #[test]
488    fn test_convenience_error_functions() {
489        let invalid_prompt = FerricLinkError::invalid_prompt_input("bad prompt");
490        assert_eq!(
491            invalid_prompt.error_code(),
492            Some(ErrorCode::InvalidPromptInput)
493        );
494
495        let model_auth = FerricLinkError::model_authentication("auth failed");
496        assert_eq!(
497            model_auth.error_code(),
498            Some(ErrorCode::ModelAuthentication)
499        );
500
501        let rate_limit = FerricLinkError::model_rate_limit("too many requests");
502        assert_eq!(rate_limit.error_code(), Some(ErrorCode::ModelRateLimit));
503    }
504
505    #[test]
506    fn test_llm_context() {
507        let parser_err = OutputParserException::with_llm_context(
508            "test",
509            Some("obs".to_string()),
510            Some("output".to_string()),
511            true,
512        );
513        let ferric_err = FerricLinkError::OutputParser(parser_err);
514
515        assert!(ferric_err.should_send_to_llm());
516        let context = ferric_err.llm_context();
517        assert_eq!(context, Some((Some("obs"), Some("output"))));
518    }
519
520    #[test]
521    fn test_create_error_message() {
522        let message = create_error_message("test error", ErrorCode::OutputParsingFailure);
523        assert!(message.contains("test error"));
524        assert!(message.contains("troubleshooting"));
525        assert!(message.contains("ferrum-labs.github.io"));
526    }
527
528    #[test]
529    fn test_into_ferriclink_error() {
530        let err: FerricLinkError = "test error".into_ferriclink_error();
531        assert!(matches!(err, FerricLinkError::Generic(_)));
532        assert!(err.to_string().contains("test error"));
533    }
534}