1use std::fmt::Display;
6use std::panic::Location;
7
8use backtrace::Backtrace;
9use headless_lms_models::ModelError;
10use headless_lms_utils::error::util_error::UtilError;
11use tracing_error::SpanTrace;
12
13use headless_lms_base::error::backend_error::BackendError;
14
15use crate::azure_chatbot::ResponseError as AzureResponseError;
16use crate::search_filter::SearchFilterError;
17
18pub type ChatbotResult<T> = Result<T, ChatbotError>;
22
23#[derive(Debug, PartialEq, Eq)]
25pub enum ChatbotErrorType {
26 InvalidMessageShape,
27 InvalidToolName,
28 InvalidToolArguments,
29 ToolUseError,
30 ChatbotModelError,
31 ChatbotMessageSuggestError,
32 UrlParse,
33 TokioIo,
34 SerdeJson,
35 SqlxError,
36 ReqwestError,
37 Other,
38 DeserializationError,
39 AzureAISearchFilterError,
40 StreamingError,
41 ContentCleaning,
42 AzureRequestBuildError,
43 FailedAzureResponse,
44 SisuDescriptionError,
45}
46
47pub struct ChatbotError {
99 error_type: <ChatbotError as BackendError>::ErrorType,
100 message: String,
101 source: Option<anyhow::Error>,
103 span_trace: Box<SpanTrace>,
105 backtrace: Box<Backtrace>,
107 location: Option<&'static Location<'static>>,
109 azure_source: Option<Box<AzureResponseError>>,
110}
111
112impl std::error::Error for ChatbotError {
113 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
114 self.source
115 .as_deref()
116 .map(|e| e as &(dyn std::error::Error + 'static))
117 }
118
119 fn cause(&self) -> Option<&dyn std::error::Error> {
120 self.source()
121 }
122}
123
124headless_lms_base::impl_clean_debug!(ChatbotError, [ChatbotError, ModelError, UtilError]);
126
127impl Display for ChatbotError {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 write!(f, "ChatbotError {:?} {:?}", self.error_type, self.message)
130 }
131}
132
133impl BackendError for ChatbotError {
134 type ErrorType = ChatbotErrorType;
135
136 fn backtrace(&self) -> Option<&Backtrace> {
137 Some(&self.backtrace)
138 }
139
140 fn error_type(&self) -> &Self::ErrorType {
141 &self.error_type
142 }
143
144 fn message(&self) -> &str {
145 &self.message
146 }
147
148 fn span_trace(&self) -> &SpanTrace {
149 &self.span_trace
150 }
151
152 fn location(&self) -> Option<&'static Location<'static>> {
153 self.location
154 }
155
156 fn new_with_traces_and_location<M: Into<String>, S: Into<Option<anyhow::Error>>>(
157 error_type: Self::ErrorType,
158 message: M,
159 source_error: S,
160 backtrace: Backtrace,
161 span_trace: SpanTrace,
162 location: Option<&'static Location<'static>>,
163 ) -> Self {
164 Self {
165 error_type,
166 message: message.into(),
167 source: source_error.into(),
168 span_trace: Box::new(span_trace),
169 backtrace: Box::new(backtrace),
170 location,
171 azure_source: None,
172 }
173 }
174}
175
176impl ChatbotError {
177 pub fn azure_source(&self) -> Option<AzureResponseError> {
178 self.azure_source.as_deref().cloned()
179 }
180
181 pub fn add_azure_source(&mut self, err: AzureResponseError) {
182 self.azure_source = Some(Box::new(err));
183 }
184}
185
186impl From<url::ParseError> for ChatbotError {
187 fn from(source: url::ParseError) -> Self {
188 Self::new(
189 ChatbotErrorType::UrlParse,
190 source.to_string(),
191 Some(source.into()),
192 )
193 }
194}
195
196impl From<tokio::io::Error> for ChatbotError {
197 fn from(source: tokio::io::Error) -> Self {
198 Self::new(
199 ChatbotErrorType::TokioIo,
200 source.to_string(),
201 Some(source.into()),
202 )
203 }
204}
205
206impl From<serde_json::Error> for ChatbotError {
207 fn from(source: serde_json::Error) -> Self {
208 Self::new(
209 ChatbotErrorType::SerdeJson,
210 source.to_string(),
211 Some(source.into()),
212 )
213 }
214}
215
216impl From<sqlx::Error> for ChatbotError {
217 fn from(err: sqlx::Error) -> ChatbotError {
218 Self::new(
219 ChatbotErrorType::SqlxError,
220 err.to_string(),
221 Some(err.into()),
222 )
223 }
224}
225
226impl From<reqwest::Error> for ChatbotError {
227 fn from(err: reqwest::Error) -> ChatbotError {
228 Self::new(
229 ChatbotErrorType::ReqwestError,
230 err.to_string(),
231 Some(err.into()),
232 )
233 }
234}
235
236impl From<anyhow::Error> for ChatbotError {
237 fn from(err: anyhow::Error) -> ChatbotError {
238 Self::new(ChatbotErrorType::Other, err.to_string(), Some(err))
239 }
240}
241
242impl From<ModelError> for ChatbotError {
243 fn from(err: ModelError) -> ChatbotError {
244 Self::new(
245 ChatbotErrorType::ChatbotModelError,
246 err.to_string(),
247 Some(err.into()),
248 )
249 }
250}
251
252impl From<SearchFilterError> for ChatbotError {
253 fn from(err: SearchFilterError) -> ChatbotError {
254 Self::new(
255 ChatbotErrorType::AzureAISearchFilterError,
256 "Couldn't create search filter for AI search: ".to_string() + &err.to_string(),
257 Some(err.into()),
258 )
259 }
260}
261
262headless_lms_utils::define_err_macro!(
264 chatbot_err,
265 ChatbotError,
266 ChatbotErrorType,
267 ChatbotErrorType,
268 "Create a ChatbotError with less boilerplate."
269);
270
271pub fn as_chatbot_error<E>(
286 error_type: ChatbotErrorType,
287 message: impl Into<String>,
288) -> impl FnOnce(E) -> ChatbotError
289where
290 E: Into<anyhow::Error>,
291{
292 let msg = message.into();
293 move |e| ChatbotError::new(error_type, msg, Some(e.into()))
294}
295
296pub fn missing_chatbot_error(
311 error_type: ChatbotErrorType,
312 message: impl Into<String>,
313) -> impl FnOnce() -> ChatbotError {
314 let msg = message.into();
315 move || ChatbotError::new(error_type, msg, None)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_chatbot_err_macro_without_source() {
324 let err = chatbot_err!(Other, "Test error message".to_string());
325 assert_eq!(err.message(), "Test error message");
326 assert!(matches!(err.error_type(), ChatbotErrorType::Other));
327 }
328
329 #[test]
330 fn test_chatbot_err_macro_with_source() {
331 let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
332 let err = chatbot_err!(TokioIo, "Wrapped error".to_string(), source_err);
333 assert_eq!(err.message(), "Wrapped error");
334 }
335
336 #[test]
337 fn test_as_chatbot_error_helper() {
338 let result: Result<(), std::io::Error> = Err(std::io::Error::new(
339 std::io::ErrorKind::NotFound,
340 "test error",
341 ));
342 let chatbot_result = result.map_err(as_chatbot_error(
343 ChatbotErrorType::Other,
344 "Failed to process".to_string(),
345 ));
346
347 assert!(chatbot_result.is_err());
348 let err = chatbot_result.unwrap_err();
349 assert_eq!(err.message(), "Failed to process");
350 assert!(matches!(err.error_type(), ChatbotErrorType::Other));
351 }
352
353 #[test]
354 fn test_missing_chatbot_error_helper() {
355 let option: Option<String> = None;
356 let result = option.ok_or_else(missing_chatbot_error(
357 ChatbotErrorType::InvalidMessageShape,
358 "Message not found".to_string(),
359 ));
360
361 assert!(result.is_err());
362 let err = result.unwrap_err();
363 assert_eq!(err.message(), "Message not found");
364 assert!(matches!(
365 err.error_type(),
366 ChatbotErrorType::InvalidMessageShape
367 ));
368 }
369
370 #[test]
371 fn test_chatbot_err_with_format() {
372 let tool_name = "test_tool";
373 let err = chatbot_err!(InvalidToolName, format!("Unknown tool: {}", tool_name));
374 assert_eq!(err.message(), "Unknown tool: test_tool");
375 }
376
377 #[test]
380 fn debug_renders_deep_cross_crate_chain() {
381 use headless_lms_utils::error::util_error::UtilErrorType;
382 let util_error = UtilError::new(UtilErrorType::Other, "disk on fire".to_string(), None);
383 let model_error = ModelError::from(util_error);
384 let chatbot_error = ChatbotError::from(model_error);
385
386 let debug = format!("{chatbot_error:?}");
387 assert!(
388 debug.contains("ChatbotError · ChatbotModelError"),
389 "got: {debug}"
390 );
391 assert!(debug.contains("caused by:"), "got: {debug}");
392 assert!(debug.contains("1. ModelError · Util"), "got: {debug}");
393 assert!(
394 debug.contains("2. UtilError · Other: disk on fire"),
395 "got: {debug}"
396 );
397 assert!(!debug.contains("(external)"), "got: {debug}");
398 }
399
400 #[test]
402 fn debug_tags_external_cause() {
403 let io_error = std::io::Error::other("connection reset");
404 let chatbot_error = chatbot_err!(TokioIo, "request failed".to_string(), io_error);
405
406 let debug = format!("{chatbot_error:?}");
407 assert!(
408 debug.contains("ChatbotError · TokioIo: request failed"),
409 "got: {debug}"
410 );
411 assert!(
412 debug.contains("connection reset (external)"),
413 "got: {debug}"
414 );
415 }
416}