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
13pub type ChatbotResult<T> = Result<T, ChatbotError>;
17
18#[derive(Debug)]
20pub enum ChatbotErrorType {
21 InvalidMessageShape,
22 InvalidToolName,
23 InvalidToolArguments,
24 ChatbotModelError,
25 ChatbotMessageSuggestError,
26 UrlParse,
27 TokioIo,
28 SerdeJson,
29 Other,
30 DeserializationError,
31}
32
33#[derive(Debug)]
85pub struct ChatbotError {
86 error_type: <ChatbotError as BackendError>::ErrorType,
87 message: String,
88 source: Option<anyhow::Error>,
90 span_trace: Box<SpanTrace>,
92 backtrace: Box<Backtrace>,
94}
95
96impl std::error::Error for ChatbotError {
97 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
98 self.source.as_ref().and_then(|o| o.source())
99 }
100
101 fn cause(&self) -> Option<&dyn std::error::Error> {
102 self.source()
103 }
104}
105
106impl Display for ChatbotError {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "ChatbotError {:?} {:?}", self.error_type, self.message)
109 }
110}
111
112impl BackendError for ChatbotError {
113 type ErrorType = ChatbotErrorType;
114
115 fn new<M: Into<String>, S: Into<Option<anyhow::Error>>>(
116 error_type: Self::ErrorType,
117 message: M,
118 source_error: S,
119 ) -> Self {
120 Self::new_with_traces(
121 error_type,
122 message,
123 source_error,
124 Backtrace::new(),
125 SpanTrace::capture(),
126 )
127 }
128
129 fn backtrace(&self) -> Option<&Backtrace> {
130 Some(&self.backtrace)
131 }
132
133 fn error_type(&self) -> &Self::ErrorType {
134 &self.error_type
135 }
136
137 fn message(&self) -> &str {
138 &self.message
139 }
140
141 fn span_trace(&self) -> &SpanTrace {
142 &self.span_trace
143 }
144
145 fn new_with_traces<M: Into<String>, S: Into<Option<anyhow::Error>>>(
146 error_type: Self::ErrorType,
147 message: M,
148 source_error: S,
149 backtrace: Backtrace,
150 span_trace: SpanTrace,
151 ) -> Self {
152 Self {
153 error_type,
154 message: message.into(),
155 source: source_error.into(),
156 span_trace: Box::new(span_trace),
157 backtrace: Box::new(backtrace),
158 }
159 }
160}
161
162impl From<url::ParseError> for ChatbotError {
163 fn from(source: url::ParseError) -> Self {
164 ChatbotError::new(
165 ChatbotErrorType::UrlParse,
166 source.to_string(),
167 Some(source.into()),
168 )
169 }
170}
171
172impl From<tokio::io::Error> for ChatbotError {
173 fn from(source: tokio::io::Error) -> Self {
174 ChatbotError::new(
175 ChatbotErrorType::TokioIo,
176 source.to_string(),
177 Some(source.into()),
178 )
179 }
180}
181
182impl From<serde_json::Error> for ChatbotError {
183 fn from(source: serde_json::Error) -> Self {
184 ChatbotError::new(
185 ChatbotErrorType::SerdeJson,
186 source.to_string(),
187 Some(source.into()),
188 )
189 }
190}
191
192impl From<anyhow::Error> for ChatbotError {
193 fn from(err: anyhow::Error) -> ChatbotError {
194 Self::new(ChatbotErrorType::Other, err.to_string(), Some(err))
195 }
196}
197
198impl From<ModelError> for ChatbotError {
199 fn from(err: ModelError) -> ChatbotError {
200 Self::new(
201 ChatbotErrorType::ChatbotModelError,
202 err.to_string(),
203 Some(err.into()),
204 )
205 }
206}
207
208headless_lms_utils::define_err_macro!(
210 chatbot_err,
211 ChatbotError,
212 ChatbotErrorType,
213 ChatbotErrorType,
214 "Create a ChatbotError with less boilerplate."
215);
216
217pub fn as_chatbot_error<E>(
232 error_type: ChatbotErrorType,
233 message: impl Into<String>,
234) -> impl FnOnce(E) -> ChatbotError
235where
236 E: Into<anyhow::Error>,
237{
238 let msg = message.into();
239 move |e| ChatbotError::new(error_type, msg, Some(e.into()))
240}
241
242pub fn missing_chatbot_error(
257 error_type: ChatbotErrorType,
258 message: impl Into<String>,
259) -> impl FnOnce() -> ChatbotError {
260 let msg = message.into();
261 move || ChatbotError::new(error_type, msg, None)
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_chatbot_err_macro_without_source() {
270 let err = chatbot_err!(Other, "Test error message".to_string());
271 assert_eq!(err.message(), "Test error message");
272 assert!(matches!(err.error_type(), ChatbotErrorType::Other));
273 }
274
275 #[test]
276 fn test_chatbot_err_macro_with_source() {
277 let source_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
278 let err = chatbot_err!(TokioIo, "Wrapped error".to_string(), source_err);
279 assert_eq!(err.message(), "Wrapped error");
280 }
281
282 #[test]
283 fn test_as_chatbot_error_helper() {
284 let result: Result<(), std::io::Error> = Err(std::io::Error::new(
285 std::io::ErrorKind::NotFound,
286 "test error",
287 ));
288 let chatbot_result = result.map_err(as_chatbot_error(
289 ChatbotErrorType::Other,
290 "Failed to process".to_string(),
291 ));
292
293 assert!(chatbot_result.is_err());
294 let err = chatbot_result.unwrap_err();
295 assert_eq!(err.message(), "Failed to process");
296 assert!(matches!(err.error_type(), ChatbotErrorType::Other));
297 }
298
299 #[test]
300 fn test_missing_chatbot_error_helper() {
301 let option: Option<String> = None;
302 let result = option.ok_or_else(missing_chatbot_error(
303 ChatbotErrorType::InvalidMessageShape,
304 "Message not found".to_string(),
305 ));
306
307 assert!(result.is_err());
308 let err = result.unwrap_err();
309 assert_eq!(err.message(), "Message not found");
310 assert!(matches!(
311 err.error_type(),
312 ChatbotErrorType::InvalidMessageShape
313 ));
314 }
315
316 #[test]
317 fn test_chatbot_err_with_format() {
318 let tool_name = "test_tool";
319 let err = chatbot_err!(InvalidToolName, format!("Unknown tool: {}", tool_name));
320 assert_eq!(err.message(), "Unknown tool: test_tool");
321 }
322}