Skip to main content

headless_lms_chatbot/
chatbot_error.rs

1/*!
2Contains error and result types for all the chatbot functions.
3*/
4
5use std::fmt::Display;
6
7use backtrace::Backtrace;
8use headless_lms_models::ModelError;
9use tracing_error::SpanTrace;
10
11use headless_lms_base::error::backend_error::BackendError;
12
13use crate::search_filter::SearchFilterError;
14
15/**
16Used as the result types for all of chatbot.
17*/
18pub type ChatbotResult<T> = Result<T, ChatbotError>;
19
20/// The type of [ChatbotError] that occured.
21#[derive(Debug)]
22pub enum ChatbotErrorType {
23    InvalidMessageShape,
24    InvalidToolName,
25    InvalidToolArguments,
26    ChatbotModelError,
27    ChatbotMessageSuggestError,
28    UrlParse,
29    TokioIo,
30    SerdeJson,
31    Other,
32    DeserializationError,
33    AzureAISearchFilterError,
34    StreamingError,
35}
36
37/**
38Error type used in [ChatbotError], which is used for errors related to chatbot functionality.
39
40All the information in the error is meant to be seen by the user. The type of error is determined by the [ChatbotErrorType] enum, which is stored inside this struct.
41
42## Examples
43
44### Usage without source error
45
46```no_run
47# use headless_lms_chatbot::prelude::*;
48# fn random_function() -> ChatbotResult<()> {
49#    let erroneous_condition = 1 == 1;
50if erroneous_condition {
51    return Err(ChatbotError::new(
52        ChatbotErrorType::Other,
53        "File not found".to_string(),
54        None,
55    ));
56}
57# Ok(())
58# }
59```
60
61### Usage with a source error
62
63Used when calling a function that returns an error that cannot be automatically converted to an ChatbotError. (See `impl From<X>` implementations on this struct.)
64
65```no_run
66# use headless_lms_chatbot::prelude::*;
67# fn some_function_returning_an_error() -> ChatbotResult<()> {
68#    return Err(ChatbotError::new(
69#        ChatbotErrorType::Other,
70#        "File not found".to_string(),
71#        None,
72#    ));
73# }
74#
75# fn random_function() -> ChatbotResult<()> {
76#    let erroneous_condition = 1 == 1;
77some_function_returning_an_error().map_err(|original_error| {
78    ChatbotError::new(
79        ChatbotErrorType::Other,
80        "Library x failed to do y".to_string(),
81        Some(original_error.into()),
82    )
83})?;
84# Ok(())
85# }
86```
87*/
88#[derive(Debug)]
89pub struct ChatbotError {
90    error_type: <ChatbotError as BackendError>::ErrorType,
91    message: String,
92    /// Original error that caused this error.
93    source: Option<anyhow::Error>,
94    /// A trace of tokio tracing spans, generated automatically when the error is generated.
95    span_trace: Box<SpanTrace>,
96    /// Stack trace, generated automatically when the error is created.
97    backtrace: Box<Backtrace>,
98}
99
100impl std::error::Error for ChatbotError {
101    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
102        self.source.as_ref().and_then(|o| o.source())
103    }
104
105    fn cause(&self) -> Option<&dyn std::error::Error> {
106        self.source()
107    }
108}
109
110impl Display for ChatbotError {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        write!(f, "ChatbotError {:?} {:?}", self.error_type, self.message)
113    }
114}
115
116impl BackendError for ChatbotError {
117    type ErrorType = ChatbotErrorType;
118
119    fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
120        error_type: Self::ErrorType,
121        message: M,
122        source_error: S,
123    ) -> Self {
124        Self::new_with_traces(
125            error_type,
126            message,
127            source_error,
128            Backtrace::new(),
129            SpanTrace::capture(),
130        )
131    }
132
133    fn backtrace(&self) -> Option<&Backtrace> {
134        Some(&self.backtrace)
135    }
136
137    fn error_type(&self) -> &Self::ErrorType {
138        &self.error_type
139    }
140
141    fn message(&self) -> &str {
142        &self.message
143    }
144
145    fn span_trace(&self) -> &SpanTrace {
146        &self.span_trace
147    }
148
149    fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
150        error_type: Self::ErrorType,
151        message: M,
152        source_error: S,
153        backtrace: Backtrace,
154        span_trace: SpanTrace,
155    ) -> Self {
156        Self {
157            error_type,
158            message: message.into(),
159            source: source_error.into(),
160            span_trace: Box::new(span_trace),
161            backtrace: Box::new(backtrace),
162        }
163    }
164}
165
166impl From<url::ParseError> for ChatbotError {
167    fn from(source: url::ParseError) -> Self {
168        ChatbotError::new(
169            ChatbotErrorType::UrlParse,
170            source.to_string(),
171            Some(source.into()),
172        )
173    }
174}
175
176impl From<tokio::io::Error> for ChatbotError {
177    fn from(source: tokio::io::Error) -> Self {
178        ChatbotError::new(
179            ChatbotErrorType::TokioIo,
180            source.to_string(),
181            Some(source.into()),
182        )
183    }
184}
185
186impl From<serde_json::Error> for ChatbotError {
187    fn from(source: serde_json::Error) -> Self {
188        ChatbotError::new(
189            ChatbotErrorType::SerdeJson,
190            source.to_string(),
191            Some(source.into()),
192        )
193    }
194}
195
196impl From<anyhow::Error> for ChatbotError {
197    fn from(err: anyhow::Error) -> ChatbotError {
198        Self::new(ChatbotErrorType::Other, err.to_string(), Some(err))
199    }
200}
201
202impl From<ModelError> for ChatbotError {
203    fn from(err: ModelError) -> ChatbotError {
204        Self::new(
205            ChatbotErrorType::ChatbotModelError,
206            err.to_string(),
207            Some(err.into()),
208        )
209    }
210}
211
212impl From<SearchFilterError> for ChatbotError {
213    fn from(err: SearchFilterError) -> ChatbotError {
214        Self::new(
215            ChatbotErrorType::AzureAISearchFilterError,
216            "Couldn't create search filter for AI search: ".to_string() + &err.to_string(),
217            Some(err.into()),
218        )
219    }
220}
221
222// Generate error creation macros for ChatbotError
223headless_lms_utils::define_err_macro!(
224    chatbot_err,
225    ChatbotError,
226    ChatbotErrorType,
227    ChatbotErrorType,
228    "Create a ChatbotError with less boilerplate."
229);
230
231/// Helper function for `.map_err()` chains to wrap any error as ChatbotError.
232///
233/// This function creates a closure that converts any error into a `ChatbotError`
234/// with the specified error type and message, including the original error as the source.
235///
236/// # Examples
237///
238/// ```ignore
239/// // Instead of:
240/// .map_err(|e| ChatbotError::new(ChatbotErrorType::Other, e.to_string(), Some(e.into())))?
241///
242/// // You can write:
243/// .map_err(as_chatbot_error(ChatbotErrorType::Other, "Failed to process".to_string()))?
244/// ```
245pub fn as_chatbot_error<E>(
246    error_type: ChatbotErrorType,
247    message: impl Into<String>,
248) -> impl FnOnce(E) -> ChatbotError
249where
250    E: Into<anyhow::Error>,
251{
252    let msg = message.into();
253    move |e| ChatbotError::new(error_type, msg, Some(e.into()))
254}
255
256/// Helper function for `.ok_or_else()` to create ChatbotError on None.
257///
258/// This function creates a closure that generates a `ChatbotError` with the
259/// specified error type and message when called.
260///
261/// # Examples
262///
263/// ```ignore
264/// // Instead of:
265/// .ok_or_else(|| ChatbotError::new(ChatbotErrorType::Other, "Item not found".to_string(), None))
266///
267/// // You can write:
268/// .ok_or_else(missing_chatbot_error(ChatbotErrorType::Other, "Item not found".to_string()))
269/// ```
270pub fn missing_chatbot_error(
271    error_type: ChatbotErrorType,
272    message: impl Into<String>,
273) -> impl FnOnce() -> ChatbotError {
274    let msg = message.into();
275    move || ChatbotError::new(error_type, msg, None)
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_chatbot_err_macro_without_source() {
284        let err = chatbot_err!(Other, "Test error message".to_string());
285        assert_eq!(err.message(), "Test error message");
286        assert!(matches!(err.error_type(), ChatbotErrorType::Other));
287    }
288
289    #[test]
290    fn test_chatbot_err_macro_with_source() {
291        let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
292        let err = chatbot_err!(TokioIo, "Wrapped error".to_string(), source_err);
293        assert_eq!(err.message(), "Wrapped error");
294    }
295
296    #[test]
297    fn test_as_chatbot_error_helper() {
298        let result: Result<(), std::io::Error> = Err(std::io::Error::new(
299            std::io::ErrorKind::NotFound,
300            "test error",
301        ));
302        let chatbot_result = result.map_err(as_chatbot_error(
303            ChatbotErrorType::Other,
304            "Failed to process".to_string(),
305        ));
306
307        assert!(chatbot_result.is_err());
308        let err = chatbot_result.unwrap_err();
309        assert_eq!(err.message(), "Failed to process");
310        assert!(matches!(err.error_type(), ChatbotErrorType::Other));
311    }
312
313    #[test]
314    fn test_missing_chatbot_error_helper() {
315        let option: Option<String> = None;
316        let result = option.ok_or_else(missing_chatbot_error(
317            ChatbotErrorType::InvalidMessageShape,
318            "Message not found".to_string(),
319        ));
320
321        assert!(result.is_err());
322        let err = result.unwrap_err();
323        assert_eq!(err.message(), "Message not found");
324        assert!(matches!(
325            err.error_type(),
326            ChatbotErrorType::InvalidMessageShape
327        ));
328    }
329
330    #[test]
331    fn test_chatbot_err_with_format() {
332        let tool_name = "test_tool";
333        let err = chatbot_err!(InvalidToolName, format!("Unknown tool: {}", tool_name));
334        assert_eq!(err.message(), "Unknown tool: test_tool");
335    }
336}