1use 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
15pub type ChatbotResult<T> = Result<T, ChatbotError>;
19
20#[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#[derive(Debug)]
89pub struct ChatbotError {
90 error_type: <ChatbotError as BackendError>::ErrorType,
91 message: String,
92 source: Option<anyhow::Error>,
94 span_trace: Box<SpanTrace>,
96 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
222headless_lms_utils::define_err_macro!(
224 chatbot_err,
225 ChatbotError,
226 ChatbotErrorType,
227 ChatbotErrorType,
228 "Create a ChatbotError with less boilerplate."
229);
230
231pub 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
256pub 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}