1use std::fmt::Display;
6use std::panic::Location;
7
8use backtrace::Backtrace;
9use headless_lms_authorization::error::AuthorizationError;
10use headless_lms_models::ModelError;
11use headless_lms_utils::error::util_error::UtilError;
12use tracing_error::SpanTrace;
13
14use headless_lms_base::error::backend_error::BackendError;
15
16use crate::azure_chatbot::azure::protocol::ResponseError as AzureResponseError;
17use crate::search_filter::SearchFilterError;
18
19pub type ChatbotResult<T> = Result<T, ChatbotError>;
23
24#[derive(Debug, PartialEq, Eq)]
26pub enum ChatbotErrorType {
27 InvalidMessageShape,
28 InvalidToolName,
29 InvalidToolArguments,
30 InvalidToolAnswer,
33 ToolUseError,
34 ChatbotModelError,
35 ChatbotMessageSuggestError,
36 UrlParse,
37 TokioIo,
38 SerdeJson,
39 SqlxError,
40 ReqwestError,
41 Other,
42 DeserializationError,
43 AzureAISearchFilterError,
44 UpstreamReportedError,
46 ResponseIncomplete,
48 StreamEndedEarly,
51 UnexpectedProtocolShape,
54 StreamInvariantViolation,
58 ContentCleaning,
59 AzureRequestBuildError,
60 FailedAzureResponse,
61 SisuDescriptionError,
62 ChatbotUtilError,
63}
64
65impl ChatbotErrorType {
66 pub fn should_terminate_stream(&self) -> bool {
70 match self {
71 ChatbotErrorType::SerdeJson
72 | ChatbotErrorType::DeserializationError
73 | ChatbotErrorType::SqlxError
74 | ChatbotErrorType::ReqwestError
75 | ChatbotErrorType::UrlParse => true,
76 ChatbotErrorType::InvalidMessageShape
77 | ChatbotErrorType::InvalidToolName
78 | ChatbotErrorType::InvalidToolArguments
79 | ChatbotErrorType::InvalidToolAnswer
80 | ChatbotErrorType::ToolUseError
81 | ChatbotErrorType::ChatbotModelError
82 | ChatbotErrorType::ChatbotMessageSuggestError
83 | ChatbotErrorType::TokioIo
84 | ChatbotErrorType::Other
85 | ChatbotErrorType::AzureAISearchFilterError
86 | ChatbotErrorType::UpstreamReportedError
87 | ChatbotErrorType::ResponseIncomplete
88 | ChatbotErrorType::StreamEndedEarly
89 | ChatbotErrorType::UnexpectedProtocolShape
90 | ChatbotErrorType::StreamInvariantViolation
91 | ChatbotErrorType::ContentCleaning
92 | ChatbotErrorType::AzureRequestBuildError
93 | ChatbotErrorType::FailedAzureResponse
94 | ChatbotErrorType::SisuDescriptionError
95 | ChatbotErrorType::ChatbotUtilError => false,
96 }
97 }
98}
99
100pub struct ChatbotError {
152 error_type: <ChatbotError as BackendError>::ErrorType,
153 message: String,
154 source: Option<anyhow::Error>,
156 span_trace: Box<SpanTrace>,
158 backtrace: Box<Backtrace>,
160 location: Option<&'static Location<'static>>,
162}
163
164impl std::error::Error for ChatbotError {
165 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
166 self.source
167 .as_deref()
168 .map(|e| e as &(dyn std::error::Error + 'static))
169 }
170
171 fn cause(&self) -> Option<&dyn std::error::Error> {
172 self.source()
173 }
174}
175
176headless_lms_base::impl_clean_debug!(ChatbotError, [ChatbotError, ModelError, UtilError]);
178
179impl Display for ChatbotError {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 write!(f, "ChatbotError {:?} {:?}", self.error_type, self.message)
182 }
183}
184
185impl BackendError for ChatbotError {
186 type ErrorType = ChatbotErrorType;
187
188 fn backtrace(&self) -> Option<&Backtrace> {
189 Some(&self.backtrace)
190 }
191
192 fn error_type(&self) -> &Self::ErrorType {
193 &self.error_type
194 }
195
196 fn message(&self) -> &str {
197 &self.message
198 }
199
200 fn span_trace(&self) -> &SpanTrace {
201 &self.span_trace
202 }
203
204 fn location(&self) -> Option<&'static Location<'static>> {
205 self.location
206 }
207
208 fn new_with_traces_and_location<M: Into<String>, S: Into<Option<anyhow::Error>>>(
209 error_type: Self::ErrorType,
210 message: M,
211 source_error: S,
212 backtrace: Backtrace,
213 span_trace: SpanTrace,
214 location: Option<&'static Location<'static>>,
215 ) -> Self {
216 Self {
217 error_type,
218 message: message.into(),
219 source: source_error.into(),
220 span_trace: Box::new(span_trace),
221 backtrace: Box::new(backtrace),
222 location,
223 }
224 }
225}
226
227impl ChatbotError {
228 pub fn azure_source(&self) -> Option<AzureResponseError> {
230 self.source
231 .as_ref()
232 .and_then(|source| source.downcast_ref::<AzureResponseError>())
233 .cloned()
234 }
235
236 pub fn add_azure_source(&mut self, err: AzureResponseError) {
240 self.source = Some(err.into());
241 }
242
243 pub fn into_model_error(mut self) -> Result<ModelError, Self> {
247 if !matches!(self.error_type, ChatbotErrorType::ChatbotModelError) {
248 return Err(self);
249 }
250 match self.source.take().map(|source| source.downcast()) {
251 Some(Ok(model_error)) => Ok(model_error),
252 Some(Err(source)) => {
253 self.source = Some(source);
254 Err(self)
255 }
256 None => Err(self),
257 }
258 }
259}
260
261impl std::error::Error for AzureResponseError {}
262
263impl From<url::ParseError> for ChatbotError {
264 fn from(source: url::ParseError) -> Self {
265 Self::new(
266 ChatbotErrorType::UrlParse,
267 source.to_string(),
268 Some(source.into()),
269 )
270 }
271}
272
273impl From<tokio::io::Error> for ChatbotError {
274 fn from(source: tokio::io::Error) -> Self {
275 Self::new(
276 ChatbotErrorType::TokioIo,
277 source.to_string(),
278 Some(source.into()),
279 )
280 }
281}
282
283impl From<serde_json::Error> for ChatbotError {
284 fn from(source: serde_json::Error) -> Self {
285 Self::new(
286 ChatbotErrorType::SerdeJson,
287 source.to_string(),
288 Some(source.into()),
289 )
290 }
291}
292
293impl From<sqlx::Error> for ChatbotError {
294 fn from(err: sqlx::Error) -> ChatbotError {
295 Self::new(
296 ChatbotErrorType::SqlxError,
297 err.to_string(),
298 Some(err.into()),
299 )
300 }
301}
302
303impl From<reqwest::Error> for ChatbotError {
304 fn from(err: reqwest::Error) -> ChatbotError {
305 Self::new(
306 ChatbotErrorType::ReqwestError,
307 err.to_string(),
308 Some(err.into()),
309 )
310 }
311}
312
313impl From<anyhow::Error> for ChatbotError {
314 fn from(err: anyhow::Error) -> ChatbotError {
315 Self::new(ChatbotErrorType::Other, err.to_string(), Some(err))
316 }
317}
318
319impl From<ModelError> for ChatbotError {
320 fn from(err: ModelError) -> ChatbotError {
321 Self::new(
322 ChatbotErrorType::ChatbotModelError,
323 err.to_string(),
324 Some(err.into()),
325 )
326 }
327}
328
329impl From<AuthorizationError> for ChatbotError {
330 fn from(err: AuthorizationError) -> ChatbotError {
331 let err = match err.into_model_error() {
334 Ok(model_error) => return model_error.into(),
335 Err(err) => err,
336 };
337 Self::new(ChatbotErrorType::Other, err.to_string(), Some(err.into()))
338 }
339}
340
341impl From<UtilError> for ChatbotError {
342 fn from(err: UtilError) -> ChatbotError {
343 Self::new(
344 ChatbotErrorType::ChatbotUtilError,
345 err.to_string(),
346 Some(err.into()),
347 )
348 }
349}
350
351impl From<SearchFilterError> for ChatbotError {
352 fn from(err: SearchFilterError) -> ChatbotError {
353 Self::new(
354 ChatbotErrorType::AzureAISearchFilterError,
355 "Couldn't create search filter for AI search: ".to_string() + &err.to_string(),
356 Some(err.into()),
357 )
358 }
359}
360
361headless_lms_utils::define_err_macro!(
363 chatbot_err,
364 ChatbotError,
365 ChatbotErrorType,
366 ChatbotErrorType,
367 "Create a ChatbotError with less boilerplate."
368);
369
370pub fn as_chatbot_error<E>(
385 error_type: ChatbotErrorType,
386 message: impl Into<String>,
387) -> impl FnOnce(E) -> ChatbotError
388where
389 E: Into<anyhow::Error>,
390{
391 let msg = message.into();
392 move |e| ChatbotError::new(error_type, msg, Some(e.into()))
393}
394
395pub fn missing_chatbot_error(
410 error_type: ChatbotErrorType,
411 message: impl Into<String>,
412) -> impl FnOnce() -> ChatbotError {
413 let msg = message.into();
414 move || ChatbotError::new(error_type, msg, None)
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_chatbot_err_macro_without_source() {
423 let err = chatbot_err!(Other, "Test error message".to_string());
424 assert_eq!(err.message(), "Test error message");
425 assert!(matches!(err.error_type(), ChatbotErrorType::Other));
426 }
427
428 #[test]
429 fn test_chatbot_err_macro_with_source() {
430 let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
431 let err = chatbot_err!(TokioIo, "Wrapped error".to_string(), source_err);
432 assert_eq!(err.message(), "Wrapped error");
433 }
434
435 #[test]
436 fn test_as_chatbot_error_helper() {
437 let result: Result<(), std::io::Error> = Err(std::io::Error::new(
438 std::io::ErrorKind::NotFound,
439 "test error",
440 ));
441 let chatbot_result = result.map_err(as_chatbot_error(
442 ChatbotErrorType::Other,
443 "Failed to process".to_string(),
444 ));
445
446 assert!(chatbot_result.is_err());
447 let err = chatbot_result.unwrap_err();
448 assert_eq!(err.message(), "Failed to process");
449 assert!(matches!(err.error_type(), ChatbotErrorType::Other));
450 }
451
452 #[test]
453 fn test_missing_chatbot_error_helper() {
454 let option: Option<String> = None;
455 let result = option.ok_or_else(missing_chatbot_error(
456 ChatbotErrorType::InvalidMessageShape,
457 "Message not found".to_string(),
458 ));
459
460 assert!(result.is_err());
461 let err = result.unwrap_err();
462 assert_eq!(err.message(), "Message not found");
463 assert!(matches!(
464 err.error_type(),
465 ChatbotErrorType::InvalidMessageShape
466 ));
467 }
468
469 #[test]
470 fn test_chatbot_err_with_format() {
471 let tool_name = "test_tool";
472 let err = chatbot_err!(InvalidToolName, format!("Unknown tool: {}", tool_name));
473 assert_eq!(err.message(), "Unknown tool: test_tool");
474 }
475
476 #[test]
479 fn debug_renders_deep_cross_crate_chain() {
480 use headless_lms_utils::error::util_error::UtilErrorType;
481 let util_error = UtilError::new(UtilErrorType::Other, "disk on fire".to_string(), None);
482 let model_error = ModelError::from(util_error);
483 let chatbot_error = ChatbotError::from(model_error);
484
485 let debug = format!("{chatbot_error:?}");
486 assert!(
487 debug.contains("ChatbotError · ChatbotModelError"),
488 "got: {debug}"
489 );
490 assert!(debug.contains("caused by:"), "got: {debug}");
491 assert!(debug.contains("1. ModelError · Util"), "got: {debug}");
492 assert!(
493 debug.contains("2. UtilError · Other: disk on fire"),
494 "got: {debug}"
495 );
496 assert!(!debug.contains("(external)"), "got: {debug}");
497 }
498
499 #[test]
501 fn debug_tags_external_cause() {
502 let io_error = std::io::Error::other("connection reset");
503 let chatbot_error = chatbot_err!(TokioIo, "request failed".to_string(), io_error);
504
505 let debug = format!("{chatbot_error:?}");
506 assert!(
507 debug.contains("ChatbotError · TokioIo: request failed"),
508 "got: {debug}"
509 );
510 assert!(
511 debug.contains("connection reset (external)"),
512 "got: {debug}"
513 );
514 }
515}