1use std::fmt;
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, FerricLinkError>;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub enum ErrorCode {
15 InvalidPromptInput,
17 InvalidToolResults,
19 MessageCoercionFailure,
21 ModelAuthentication,
23 ModelNotFound,
25 ModelRateLimit,
27 OutputParsingFailure,
29 SerializationError,
31 IoError,
33 HttpError,
35 ValidationError,
37 ConfigurationError,
39 RuntimeError,
41 NotImplemented,
43 GenericError,
45}
46
47impl ErrorCode {
48 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 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#[derive(Error, Debug)]
86pub enum FerricLinkError {
87 #[error("FerricLink error: {0}")]
89 General(String),
90
91 #[error("Tracer error: {0}")]
93 Tracer(#[from] TracerException),
94
95 #[error("Output parser error: {0}")]
97 OutputParser(#[from] OutputParserException),
98
99 #[error("Serialization error: {0}")]
101 Serialization(#[from] serde_json::Error),
102
103 #[error("IO error: {0}")]
105 Io(#[from] std::io::Error),
106
107 #[cfg(feature = "http")]
109 #[error("HTTP error: {0}")]
110 Http(#[from] reqwest::Error),
111
112 #[error("Validation error: {0}")]
114 Validation(String),
115
116 #[error("Configuration error: {0}")]
118 Configuration(String),
119
120 #[error("Runtime error: {0}")]
122 Runtime(String),
123
124 #[error("Not implemented: {0}")]
126 NotImplemented(String),
127
128 #[error("Error: {0}")]
130 Generic(String),
131}
132
133#[derive(Error, Debug)]
135#[error("Tracer error: {message}")]
136pub struct TracerException {
137 pub message: String,
139 pub error_code: ErrorCode,
141}
142
143impl TracerException {
144 pub fn new(message: impl Into<String>) -> Self {
146 Self {
147 message: message.into(),
148 error_code: ErrorCode::RuntimeError,
149 }
150 }
151
152 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#[derive(Error, Debug)]
163#[error("Output parser error: {message}")]
164pub struct OutputParserException {
165 pub message: String,
167 pub error_code: ErrorCode,
169 pub observation: Option<String>,
171 pub llm_output: Option<String>,
173 pub send_to_llm: bool,
175}
176
177impl OutputParserException {
178 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 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 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 pub fn observation(&self) -> Option<&str> {
222 self.observation.as_deref()
223 }
224
225 pub fn llm_output(&self) -> Option<&str> {
227 self.llm_output.as_deref()
228 }
229
230 pub fn should_send_to_llm(&self) -> bool {
232 self.send_to_llm
233 }
234}
235
236impl FerricLinkError {
237 pub fn general(msg: impl Into<String>) -> Self {
239 Self::General(msg.into())
240 }
241
242 pub fn validation(msg: impl Into<String>) -> Self {
244 Self::Validation(msg.into())
245 }
246
247 pub fn configuration(msg: impl Into<String>) -> Self {
249 Self::Configuration(msg.into())
250 }
251
252 pub fn runtime(msg: impl Into<String>) -> Self {
254 Self::Runtime(msg.into())
255 }
256
257 pub fn not_implemented(msg: impl Into<String>) -> Self {
259 Self::NotImplemented(msg.into())
260 }
261
262 pub fn generic(msg: impl Into<String>) -> Self {
264 Self::Generic(msg.into())
265 }
266
267 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 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 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 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 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 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
348pub 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
371pub trait IntoFerricLinkError {
373 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
386impl FerricLinkError {
388 pub fn invalid_prompt_input(msg: impl Into<String>) -> Self {
390 Self::General(create_error_message(msg, ErrorCode::InvalidPromptInput))
391 }
392
393 pub fn invalid_tool_results(msg: impl Into<String>) -> Self {
395 Self::General(create_error_message(msg, ErrorCode::InvalidToolResults))
396 }
397
398 pub fn message_coercion_failure(msg: impl Into<String>) -> Self {
400 Self::General(create_error_message(msg, ErrorCode::MessageCoercionFailure))
401 }
402
403 pub fn model_authentication(msg: impl Into<String>) -> Self {
405 Self::General(create_error_message(msg, ErrorCode::ModelAuthentication))
406 }
407
408 pub fn model_not_found(msg: impl Into<String>) -> Self {
410 Self::General(create_error_message(msg, ErrorCode::ModelNotFound))
411 }
412
413 pub fn model_rate_limit(msg: impl Into<String>) -> Self {
415 Self::General(create_error_message(msg, ErrorCode::ModelRateLimit))
416 }
417
418 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}