From 77cc3678b52cfeed6d9f387ac2a31e564c50af02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renault?= Date: Wed, 11 Jun 2025 09:27:14 +0200 Subject: [PATCH] Make sure template errors are reported to the LLM and front-end without panicking --- crates/meilisearch-types/src/error.rs | 1 + .../src/routes/chats/chat_completions.rs | 12 ++++-- crates/meilisearch/src/routes/chats/errors.rs | 41 +++++++++++++------ crates/meilisearch/src/routes/chats/utils.rs | 12 +++++- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/crates/meilisearch-types/src/error.rs b/crates/meilisearch-types/src/error.rs index 10fd645ae..d2500b7e1 100644 --- a/crates/meilisearch-types/src/error.rs +++ b/crates/meilisearch-types/src/error.rs @@ -394,6 +394,7 @@ UnimplementedExternalFunctionCalling , InvalidRequest , NOT_IMPL UnimplementedNonStreamingChatCompletions , InvalidRequest , NOT_IMPLEMENTED ; UnimplementedMultiChoiceChatCompletions , InvalidRequest , NOT_IMPLEMENTED ; ChatNotFound , InvalidRequest , NOT_FOUND ; +InvalidChatSettingDocumentTemplate , InvalidRequest , BAD_REQUEST ; InvalidChatCompletionOrgId , InvalidRequest , BAD_REQUEST ; InvalidChatCompletionProjectId , InvalidRequest , BAD_REQUEST ; InvalidChatCompletionApiVersion , InvalidRequest , BAD_REQUEST ; diff --git a/crates/meilisearch/src/routes/chats/chat_completions.rs b/crates/meilisearch/src/routes/chats/chat_completions.rs index 143d2cddf..60f28d638 100644 --- a/crates/meilisearch/src/routes/chats/chat_completions.rs +++ b/crates/meilisearch/src/routes/chats/chat_completions.rs @@ -646,7 +646,7 @@ async fn handle_meili_tools( } let result = match serde_json::from_str(&call.function.arguments) { - Ok(SearchInIndexParameters { index_uid, q }) => process_search_request( + Ok(SearchInIndexParameters { index_uid, q }) => match process_search_request( index_scheduler, auth_ctrl.clone(), search_queue, @@ -655,7 +655,14 @@ async fn handle_meili_tools( q, ) .await - .map_err(|e| e.to_string()), + { + Ok(output) => Ok(output), + Err(err) => { + let error_text = err.to_string(); + tx.send_error(&StreamErrorEvent::from_response_error(err)).await?; + Err(error_text) + } + }, Err(err) => Err(err.to_string()), }; @@ -664,7 +671,6 @@ async fn handle_meili_tools( if report_sources { tx.report_sources(resp.clone(), &call.id, &documents).await?; } - text } Err(err) => err, diff --git a/crates/meilisearch/src/routes/chats/errors.rs b/crates/meilisearch/src/routes/chats/errors.rs index f1aa9722b..efa60ba50 100644 --- a/crates/meilisearch/src/routes/chats/errors.rs +++ b/crates/meilisearch/src/routes/chats/errors.rs @@ -1,5 +1,6 @@ use async_openai::error::{ApiError, OpenAIError}; use async_openai::reqwest_eventsource::Error as EventSourceError; +use meilisearch_types::error::ResponseError; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -53,12 +54,13 @@ pub struct StreamError { } impl StreamErrorEvent { + const ERROR_TYPE: &str = "error"; + pub async fn from_openai_error(error: OpenAIError) -> Result { - let error_type = "error".to_string(); match error { OpenAIError::Reqwest(e) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "internal_reqwest_error".to_string(), code: Some("internal".to_string()), @@ -69,7 +71,7 @@ impl StreamErrorEvent { }), OpenAIError::ApiError(ApiError { message, r#type, param, code }) => { Ok(StreamErrorEvent { - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), event_id: Uuid::new_v4().to_string(), error: StreamError { r#type: r#type.unwrap_or_else(|| "unknown".to_string()), @@ -82,7 +84,7 @@ impl StreamErrorEvent { } OpenAIError::JSONDeserialize(error) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "json_deserialize_error".to_string(), code: Some("internal".to_string()), @@ -100,7 +102,7 @@ impl StreamErrorEvent { Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type, code, message, param, event_id: None }, }) } @@ -111,13 +113,13 @@ impl StreamErrorEvent { Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type, code, message, param, event_id: None }, }) } EventSourceError::Utf8(error) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "invalid_utf8_error".to_string(), code: None, @@ -128,7 +130,7 @@ impl StreamErrorEvent { }), EventSourceError::Parser(error) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "parser_error".to_string(), code: None, @@ -139,7 +141,7 @@ impl StreamErrorEvent { }), EventSourceError::Transport(error) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "transport_error".to_string(), code: None, @@ -150,7 +152,7 @@ impl StreamErrorEvent { }), EventSourceError::InvalidLastEventId(message) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "invalid_last_event_id".to_string(), code: None, @@ -161,7 +163,7 @@ impl StreamErrorEvent { }), EventSourceError::StreamEnded => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "stream_ended".to_string(), code: None, @@ -173,7 +175,7 @@ impl StreamErrorEvent { }, OpenAIError::InvalidArgument(message) => Ok(StreamErrorEvent { event_id: Uuid::new_v4().to_string(), - r#type: error_type, + r#type: Self::ERROR_TYPE.to_string(), error: StreamError { r#type: "invalid_argument".to_string(), code: None, @@ -184,4 +186,19 @@ impl StreamErrorEvent { }), } } + + pub fn from_response_error(error: ResponseError) -> Self { + let ResponseError { code, message, .. } = error; + StreamErrorEvent { + event_id: Uuid::new_v4().to_string(), + r#type: Self::ERROR_TYPE.to_string(), + error: StreamError { + r#type: "response_error".to_string(), + code: Some(code.as_str().to_string()), + message, + param: None, + event_id: None, + }, + } + } } diff --git a/crates/meilisearch/src/routes/chats/utils.rs b/crates/meilisearch/src/routes/chats/utils.rs index 752eb10e6..61961bd4b 100644 --- a/crates/meilisearch/src/routes/chats/utils.rs +++ b/crates/meilisearch/src/routes/chats/utils.rs @@ -9,7 +9,7 @@ use async_openai::types::{ FunctionCall, FunctionCallStream, Role, }; use bumpalo::Bump; -use meilisearch_types::error::ResponseError; +use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::heed::RoTxn; use meilisearch_types::milli::index::ChatConfig; use meilisearch_types::milli::prompt::{Prompt, PromptData}; @@ -237,7 +237,15 @@ pub fn format_documents<'doc>( Some(doc) => doc, None => unreachable!("Document with internal ID {docid} not found"), }; - let text = prompt.render_document(&external_docid, document, &gfid_map, doc_alloc).unwrap(); + let text = match prompt.render_document(&external_docid, document, &gfid_map, doc_alloc) { + Ok(text) => text, + Err(err) => { + return Err(ResponseError::from_msg( + err.to_string(), + Code::InvalidChatSettingDocumentTemplate, + )) + } + }; renders.push(text); }