diff --git a/crates/index-scheduler/src/lib.rs b/crates/index-scheduler/src/lib.rs index 11572cd06..15766c691 100644 --- a/crates/index-scheduler/src/lib.rs +++ b/crates/index-scheduler/src/lib.rs @@ -53,7 +53,7 @@ use flate2::Compression; use meilisearch_types::batches::Batch; use meilisearch_types::features::{InstanceTogglableFeatures, Network, RuntimeTogglableFeatures}; use meilisearch_types::heed::byteorder::BE; -use meilisearch_types::heed::types::{Str, I128}; +use meilisearch_types::heed::types::{SerdeJson, Str, I128}; use meilisearch_types::heed::{self, Database, Env, RoTxn, WithoutTls}; use meilisearch_types::milli::index::IndexEmbeddingConfig; use meilisearch_types::milli::update::IndexerConfig; @@ -153,8 +153,8 @@ pub struct IndexScheduler { /// In charge of fetching and setting the status of experimental features. features: features::FeatureData, - /// Stores the custom prompts for the chat - chat_prompts: Database, + /// Stores the custom chat prompts and other settings of the indexes. + chat_settings: Database>, /// Everything related to the processing of the tasks pub scheduler: scheduler::Scheduler, @@ -214,7 +214,7 @@ impl IndexScheduler { #[cfg(test)] run_loop_iteration: self.run_loop_iteration.clone(), features: self.features.clone(), - chat_prompts: self.chat_prompts.clone(), + chat_settings: self.chat_settings.clone(), } } @@ -277,7 +277,7 @@ impl IndexScheduler { let features = features::FeatureData::new(&env, &mut wtxn, options.instance_features)?; let queue = Queue::new(&env, &mut wtxn, &options)?; let index_mapper = IndexMapper::new(&env, &mut wtxn, &options, budget)?; - let chat_prompts = env.create_database(&mut wtxn, Some("chat-prompts"))?; + let chat_settings = env.create_database(&mut wtxn, Some("chat-settings"))?; wtxn.commit()?; // allow unreachable_code to get rids of the warning in the case of a test build. @@ -301,7 +301,7 @@ impl IndexScheduler { #[cfg(test)] run_loop_iteration: Arc::new(RwLock::new(0)), features, - chat_prompts, + chat_settings, }; this.run(); @@ -875,8 +875,15 @@ impl IndexScheduler { res.map(EmbeddingConfigs::new) } - pub fn chat_prompts<'t>(&self, rtxn: &'t RoTxn, name: &str) -> heed::Result> { - self.chat_prompts.get(rtxn, name) + pub fn chat_settings(&self) -> Result> { + let rtxn = self.env.read_txn().map_err(Error::HeedTransaction)?; + self.chat_settings.get(&rtxn, &"main").map_err(Into::into) + } + + pub fn put_chat_settings(&self, settings: &serde_json::Value) -> Result<()> { + let mut wtxn = self.env.write_txn().map_err(Error::HeedTransaction)?; + self.chat_settings.put(&mut wtxn, &"main", &settings)?; + Ok(()) } } diff --git a/crates/meilisearch-types/src/keys.rs b/crates/meilisearch-types/src/keys.rs index 805394781..ffa533be9 100644 --- a/crates/meilisearch-types/src/keys.rs +++ b/crates/meilisearch-types/src/keys.rs @@ -311,6 +311,12 @@ pub enum Action { #[serde(rename = "chat.get")] #[deserr(rename = "chat.get")] ChatGet, + #[serde(rename = "chatSettings.get")] + #[deserr(rename = "chatSettings.get")] + ChatSettingsGet, + #[serde(rename = "chatSettings.update")] + #[deserr(rename = "chatSettings.update")] + ChatSettingsUpdate, } impl Action { @@ -403,4 +409,6 @@ pub mod actions { pub const NETWORK_UPDATE: u8 = NetworkUpdate.repr(); pub const CHAT_GET: u8 = ChatGet.repr(); + pub const CHAT_SETTINGS_GET: u8 = ChatSettingsGet.repr(); + pub const CHAT_SETTINGS_UPDATE: u8 = ChatSettingsUpdate.repr(); } diff --git a/crates/meilisearch/src/routes/chat.rs b/crates/meilisearch/src/routes/chat.rs index e4a9b65e2..33cc06bce 100644 --- a/crates/meilisearch/src/routes/chat.rs +++ b/crates/meilisearch/src/routes/chat.rs @@ -9,6 +9,7 @@ use async_openai::config::OpenAIConfig; use async_openai::types::{ ChatCompletionMessageToolCall, ChatCompletionMessageToolCallChunk, ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage, + ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageContent, ChatCompletionRequestToolMessage, ChatCompletionRequestToolMessageContent, ChatCompletionStreamResponseDelta, ChatCompletionToolArgs, ChatCompletionToolType, CreateChatCompletionRequest, FinishReason, FunctionCall, FunctionCallStream, @@ -27,6 +28,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::runtime::Handle; +use super::settings::chat::{ChatPrompts, ChatSettings}; use crate::extractors::authentication::policies::ActionPolicy; use crate::extractors::authentication::GuardedData; use crate::metrics::MEILISEARCH_DEGRADED_SEARCH_REQUESTS; @@ -36,33 +38,12 @@ use crate::search::{ }; use crate::search_queue::SearchQueue; -/// The default description of the searchInIndex tool provided to OpenAI. -const DEFAULT_SEARCH_IN_INDEX_TOOL_DESCRIPTION: &str = - "Search the database for relevant JSON documents using an optional query."; -/// The default description of the searchInIndex `q` parameter tool provided to OpenAI. -const DEFAULT_SEARCH_IN_INDEX_Q_PARAMETER_TOOL_DESCRIPTION: &str = - "The search query string used to find relevant documents in the index. \ -This should contain keywords or phrases that best represent what the user is looking for. \ -More specific queries will yield more precise results."; -/// The default description of the searchInIndex `index` parameter tool provided to OpenAI. -const DEFAULT_SEARCH_IN_INDEX_INDEX_PARAMETER_TOOL_DESCRIPTION: &str = -"The name of the index to search within. An index is a collection of documents organized for search. \ -Selecting the right index ensures the most relevant results for the user query"; - const EMBEDDER_NAME: &str = "openai"; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("").route(web::post().to(chat))); } -/// Creates OpenAI client with API key -fn create_openai_client() -> Client { - let api_key = std::env::var("MEILI_OPENAI_API_KEY") - .expect("cannot find OpenAI API Key (MEILI_OPENAI_API_KEY)"); - let config = OpenAIConfig::default().with_api_key(&api_key); - Client::with_config(config) -} - /// Get a chat completion async fn chat( index_scheduler: GuardedData, Data>, @@ -86,12 +67,7 @@ async fn chat( } /// Setup search tool in chat completion request -fn setup_search_tool( - chat_completion: &mut CreateChatCompletionRequest, - search_in_index_description: &str, - search_in_index_q_param_description: &str, - search_in_index_index_description: &str, -) { +fn setup_search_tool(chat_completion: &mut CreateChatCompletionRequest, prompts: &ChatPrompts) { let tools = chat_completion.tools.get_or_insert_default(); tools.push( ChatCompletionToolArgs::default() @@ -99,18 +75,18 @@ fn setup_search_tool( .function( FunctionObjectArgs::default() .name("searchInIndex") - .description(search_in_index_description) + .description(&prompts.search_description) .parameters(json!({ "type": "object", "properties": { "index_uid": { "type": "string", "enum": ["main"], - "description": search_in_index_index_description, + "description": prompts.search_index_uid_param, }, "q": { "type": ["string", "null"], - "description": search_in_index_q_param_description, + "description": prompts.search_q_param, } }, "required": ["index_uid", "q"], @@ -125,6 +101,17 @@ fn setup_search_tool( ); } +/// Prepend system message to the conversation +fn prepend_system_message(chat_completion: &mut CreateChatCompletionRequest, system_prompt: &str) { + chat_completion.messages.insert( + 0, + ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage { + content: ChatCompletionRequestSystemMessageContent::Text(system_prompt.to_string()), + name: None, + }), + ); +} + /// Process search request and return formatted results async fn process_search_request( index_scheduler: &GuardedData, Data>, @@ -187,56 +174,32 @@ async fn process_search_request( Ok((index, text)) } -/// Get prompt descriptions from index scheduler -fn get_prompt_descriptions( - index_scheduler: &GuardedData, Data>, -) -> (String, String, String) { - let rtxn = index_scheduler.read_txn().unwrap(); - let search_in_index_description = index_scheduler - .chat_prompts(&rtxn, "searchInIndex-description") - .unwrap() - .unwrap_or(DEFAULT_SEARCH_IN_INDEX_TOOL_DESCRIPTION) - .to_string(); - let search_in_index_q_param_description = index_scheduler - .chat_prompts(&rtxn, "searchInIndex-q-param-description") - .unwrap() - .unwrap_or(DEFAULT_SEARCH_IN_INDEX_Q_PARAMETER_TOOL_DESCRIPTION) - .to_string(); - let search_in_index_index_description = index_scheduler - .chat_prompts(&rtxn, "searchInIndex-index-param-description") - .unwrap() - .unwrap_or(DEFAULT_SEARCH_IN_INDEX_INDEX_PARAMETER_TOOL_DESCRIPTION) - .to_string(); - drop(rtxn); - - ( - search_in_index_description, - search_in_index_q_param_description, - search_in_index_index_description, - ) -} - async fn non_streamed_chat( index_scheduler: GuardedData, Data>, search_queue: web::Data, mut chat_completion: CreateChatCompletionRequest, ) -> Result { - let client = create_openai_client(); + let chat_settings = match index_scheduler.chat_settings().unwrap() { + Some(value) => serde_json::from_value(value).unwrap(), + None => ChatSettings::default(), + }; - let ( - search_in_index_description, - search_in_index_q_param_description, - search_in_index_index_description, - ) = get_prompt_descriptions(&index_scheduler); + let mut config = OpenAIConfig::default(); + if let Some(api_key) = chat_settings.api_key.as_ref() { + config = config.with_api_key(api_key); + } + // We cannot change the endpoint + // if let Some(endpoint) = chat_settings.endpoint.as_ref() { + // config.with_api_base(&endpoint); + // } + let client = Client::with_config(config); + + // Prepend system message to the conversation + prepend_system_message(&mut chat_completion, &chat_settings.prompts.system); let mut response; loop { - setup_search_tool( - &mut chat_completion, - &search_in_index_description, - &search_in_index_q_param_description, - &search_in_index_index_description, - ); + setup_search_tool(&mut chat_completion, &chat_settings.prompts); response = client.chat().create(chat_completion.clone()).await.unwrap(); @@ -290,22 +253,29 @@ async fn streamed_chat( search_queue: web::Data, mut chat_completion: CreateChatCompletionRequest, ) -> impl Responder { - let ( - search_in_index_description, - search_in_index_q_param_description, - search_in_index_index_description, - ) = get_prompt_descriptions(&index_scheduler); + let chat_settings = match index_scheduler.chat_settings().unwrap() { + Some(value) => serde_json::from_value(value).unwrap(), + None => ChatSettings::default(), + }; - setup_search_tool( - &mut chat_completion, - &search_in_index_description, - &search_in_index_q_param_description, - &search_in_index_index_description, - ); + let mut config = OpenAIConfig::default(); + if let Some(api_key) = chat_settings.api_key.as_ref() { + config = config.with_api_key(api_key); + } + // We cannot change the endpoint + // if let Some(endpoint) = chat_settings.endpoint.as_ref() { + // config.with_api_base(&endpoint); + // } + + // Prepend system message to the conversation + prepend_system_message(&mut chat_completion, &chat_settings.prompts.system); + + // Setup the search tool + setup_search_tool(&mut chat_completion, &chat_settings.prompts); let (tx, rx) = tokio::sync::mpsc::channel(10); let _join_handle = Handle::current().spawn(async move { - let client = create_openai_client(); + let client = Client::with_config(config.clone()); let mut global_tool_calls = HashMap::::new(); 'main: loop { diff --git a/crates/meilisearch/src/routes/mod.rs b/crates/meilisearch/src/routes/mod.rs index 602fb6b40..3d56ce8e8 100644 --- a/crates/meilisearch/src/routes/mod.rs +++ b/crates/meilisearch/src/routes/mod.rs @@ -62,6 +62,7 @@ mod multi_search; mod multi_search_analytics; pub mod network; mod open_api_utils; +pub mod settings; mod snapshot; mod swap_indexes; pub mod tasks; @@ -115,7 +116,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(web::scope("/metrics").configure(metrics::configure)) .service(web::scope("/experimental-features").configure(features::configure)) .service(web::scope("/network").configure(network::configure)) - .service(web::scope("/chat").configure(chat::configure)); + .service(web::scope("/chat").configure(chat::configure)) + .service(web::scope("/settings/chat").configure(settings::chat::configure)); #[cfg(feature = "swagger")] { diff --git a/crates/meilisearch/src/routes/settings/chat.rs b/crates/meilisearch/src/routes/settings/chat.rs new file mode 100644 index 000000000..9708e1409 --- /dev/null +++ b/crates/meilisearch/src/routes/settings/chat.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; + +use actix_web::web::{self, Data}; +use actix_web::HttpResponse; +use index_scheduler::IndexScheduler; +use meilisearch_types::error::ResponseError; +use meilisearch_types::keys::actions; +use serde::{Deserialize, Serialize}; + +use crate::extractors::authentication::policies::ActionPolicy; +use crate::extractors::authentication::GuardedData; +use crate::extractors::sequential_extractor::SeqHandler; + +pub fn configure(cfg: &mut web::ServiceConfig) { + cfg.service( + web::resource("") + .route(web::get().to(get_settings)) + .route(web::patch().to(SeqHandler(patch_settings))), + ); +} + +async fn get_settings( + index_scheduler: GuardedData< + ActionPolicy<{ actions::CHAT_SETTINGS_GET }>, + Data, + >, +) -> Result { + let settings = match index_scheduler.chat_settings()? { + Some(value) => serde_json::from_value(value).unwrap(), + None => ChatSettings::default(), + }; + Ok(HttpResponse::Ok().json(settings)) +} + +async fn patch_settings( + index_scheduler: GuardedData< + ActionPolicy<{ actions::CHAT_SETTINGS_UPDATE }>, + Data, + >, + web::Json(chat_settings): web::Json, +) -> Result { + let chat_settings = serde_json::to_value(chat_settings).unwrap(); + index_scheduler.put_chat_settings(&chat_settings)?; + Ok(HttpResponse::Ok().finish()) +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct ChatSettings { + pub source: String, + pub endpoint: Option, + pub api_key: Option, + pub prompts: ChatPrompts, + pub indexes: BTreeMap, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct ChatPrompts { + pub system: String, + pub search_description: String, + pub search_q_param: String, + pub search_index_uid_param: String, + pub pre_query: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct ChatIndexSettings { + pub description: String, + pub document_template: String, +} + +const DEFAULT_SYSTEM_MESSAGE: &str = "You are a highly capable research assistant with access to powerful search tools. IMPORTANT INSTRUCTIONS:\ + 1. When answering questions, you MUST make multiple tool calls (at least 2-3) to gather comprehensive information.\ + 2. Use different search queries for each tool call - vary keywords, rephrase questions, and explore different semantic angles to ensure broad coverage.\ + 3. Always explicitly announce BEFORE making each tool call by saying: \"I'll search for [specific information] now.\"\ + 4. Combine information from ALL tool calls to provide complete, nuanced answers rather than relying on a single source.\ + 5. For complex topics, break down your research into multiple targeted queries rather than using a single generic search."; + +/// The default description of the searchInIndex tool provided to OpenAI. +const DEFAULT_SEARCH_IN_INDEX_TOOL_DESCRIPTION: &str = + "Search the database for relevant JSON documents using an optional query."; +/// The default description of the searchInIndex `q` parameter tool provided to OpenAI. +const DEFAULT_SEARCH_IN_INDEX_Q_PARAMETER_TOOL_DESCRIPTION: &str = + "The search query string used to find relevant documents in the index. \ +This should contain keywords or phrases that best represent what the user is looking for. \ +More specific queries will yield more precise results."; +/// The default description of the searchInIndex `index` parameter tool provided to OpenAI. +const DEFAULT_SEARCH_IN_INDEX_INDEX_PARAMETER_TOOL_DESCRIPTION: &str = +"The name of the index to search within. An index is a collection of documents organized for search. \ +Selecting the right index ensures the most relevant results for the user query"; + +impl Default for ChatSettings { + fn default() -> Self { + ChatSettings { + source: "openai".to_string(), + endpoint: None, + api_key: None, + prompts: ChatPrompts { + system: DEFAULT_SYSTEM_MESSAGE.to_string(), + search_description: DEFAULT_SEARCH_IN_INDEX_TOOL_DESCRIPTION.to_string(), + search_q_param: DEFAULT_SEARCH_IN_INDEX_Q_PARAMETER_TOOL_DESCRIPTION.to_string(), + search_index_uid_param: DEFAULT_SEARCH_IN_INDEX_INDEX_PARAMETER_TOOL_DESCRIPTION + .to_string(), + pre_query: "".to_string(), + }, + indexes: BTreeMap::new(), + } + } +} diff --git a/crates/meilisearch/src/routes/settings/mod.rs b/crates/meilisearch/src/routes/settings/mod.rs new file mode 100644 index 000000000..30a62fc50 --- /dev/null +++ b/crates/meilisearch/src/routes/settings/mod.rs @@ -0,0 +1 @@ +pub mod chat;