From 125b856c491bc4a8c02003aaca1f4d792cbc3db7 Mon Sep 17 00:00:00 2001 From: Per Stark Date: Fri, 29 May 2026 11:48:13 +0200 Subject: [PATCH] chore: harden system settings and unify prompt usage Validate settings updates, use typed embedding backends, and route ingestion through DB-stored prompts so admin edits take effect. --- .../20250503_215025_initial_setup.surql | 2 +- common/src/storage/types/system_prompts.rs | 8 +- common/src/storage/types/system_settings.rs | 212 +++++++++++++++--- common/src/utils/embedding.rs | 15 +- html-router/src/routes/admin/handlers.rs | 88 ++++---- ingestion-pipeline/src/pipeline/services.rs | 7 +- .../src/utils/llm_instructions.rs | 3 - .../src/answer_retrieval_helper.rs | 3 - 8 files changed, 244 insertions(+), 94 deletions(-) diff --git a/common/migrations/20250503_215025_initial_setup.surql b/common/migrations/20250503_215025_initial_setup.surql index 1e9c897..5cb6be8 100644 --- a/common/migrations/20250503_215025_initial_setup.surql +++ b/common/migrations/20250503_215025_initial_setup.surql @@ -19,6 +19,6 @@ CREATE system_settings:current CONTENT { image_processing_prompt: "Analyze this image and respond based on its primary content:\n - If the image is mainly text (document, screenshot, sign), transcribe the text verbatim.\n - If the image is mainly visual (photograph, art, landscape), provide a concise description of the scene.\n - For hybrid images (diagrams, ads), briefly describe the visual, then transcribe the text under a Text: heading.\n\n Respond directly with the analysis.", embedding_dimensions: 1536, query_system_prompt: "You are a knowledgeable assistant with access to a specialized knowledge base. You will be provided with relevant knowledge entities from the database as context. Each knowledge entity contains a name, description, and type, representing different concepts, ideas, and information.\nYour task is to:\n1. Carefully analyze the provided knowledge entities in the context\n2. Answer user questions based on this information\n3. Provide clear, concise, and accurate responses\n4. When referencing information, briefly mention which knowledge entity it came from\n5. If the provided context doesn't contain enough information to answer the question confidently, clearly state this\n6. If only partial information is available, explain what you can answer and what information is missing\n7. Avoid making assumptions or providing information not supported by the context\n8. Output the references to the documents. Use the UUIDs and make sure they are correct!\nRemember:\n- Be direct and honest about the limitations of your knowledge\n- Cite the relevant knowledge entities when providing information, but only provide the UUIDs in the reference array\n- If you need to combine information from multiple entities, explain how they connect\n- Don't speculate beyond what's provided in the context\nExample response formats:\n\"Based on [Entity Name], [answer...]\"\n\"I found relevant information in multiple entries: [explanation...]\"\n\"I apologize, but the provided context doesn't contain information about [topic]\"", - ingestion_system_prompt: "You are an AI assistant. You will receive a text content, along with user context and a category. Your task is to provide a structured JSON object representing the content in a graph format suitable for a graph database. You will also be presented with some existing knowledge_entities from the database, do not replicate these! Your task is to create meaningful knowledge entities from the submitted content. Try and infer as much as possible from the users context and category when creating these. If the user submits a large content, create more general entities. If the user submits a narrow and precise content, try and create precise knowledge entities.\nThe JSON should have the following structure:\n{\n\"knowledge_entities\": [\n{\n\"key\": \"unique-key-1\",\n\"name\": \"Entity Name\",\n\"description\": \"A detailed description of the entity.\",\n\"entity_type\": \"TypeOfEntity\"\n},\n// More entities...\n],\n\"relationships\": [\n{\n\"type\": \"RelationshipType\",\n\"source\": \"unique-key-1 or UUID from existing database\",\n\"target\": \"unique-key-1 or UUID from existing database\"\n},\n// More relationships...\n]\n}\nGuidelines:\n1. Do NOT generate any IDs or UUIDs. Use a unique `key` for each knowledge entity.\n2. Each KnowledgeEntity should have a unique `key`, a meaningful `name`, and a descriptive `description`.\n3. Define the type of each KnowledgeEntity using the following categories: Idea, Project, Document, Page, TextSnippet.\n4. Establish relationships between entities using types like RelatedTo, RelevantTo, SimilarTo.\n5. Use the `source` key to indicate the originating entity and the `target` key to indicate the related entity\"\n6. You will be presented with a few existing KnowledgeEntities that are similar to the current ones. They will have an existing UUID. When creating relationships to these entities, use their UUID.\n7. Only create relationships between existing KnowledgeEntities.\n8. Entities that exist already in the database should NOT be created again. If there is only a minor overlap, skip creating a new entity.\n9. A new relationship MUST include a newly created KnowledgeEntity." + ingestion_system_prompt: "You are an AI assistant. You will receive a text content, along with user context and a category. Your task is to provide a structured JSON object representing the content in a graph format suitable for a graph database. You will also be presented with some existing knowledge_entities from the database, do not replicate these! Your task is to create meaningful knowledge entities from the submitted content. Try and infer as much as possible from the users context and category when creating these. If the user submits a large content, create more general entities. If the user submits a narrow and precise content, try and create precise knowledge entities.\nThe JSON should have the following structure:\n{\n\"knowledge_entities\": [\n{\n\"key\": \"unique-key-1\",\n\"name\": \"Entity Name\",\n\"description\": \"A detailed description of the entity.\",\n\"entity_type\": \"TypeOfEntity\"\n},\n// More entities...\n],\n\"relationships\": [\n{\n\"type\": \"RelationshipType\",\n\"source\": \"unique-key-1 or UUID from existing database\",\n\"target\": \"unique-key-1 or UUID from existing database\"\n},\n// More relationships...\n]\n}\nGuidelines:\n1. Do NOT generate any IDs or UUIDs. Use a unique `key` for each knowledge entity.\n2. Each KnowledgeEntity should have a unique `key`, a meaningful `name`, and a descriptive `description`.\n3. Define the type of each KnowledgeEntity using the following categories: Idea, Project, Document, Page, TextSnippet.\n4. Establish relationships between entities using types like RelatedTo, RelevantTo, SimilarTo.\n5. Use the `source` key to indicate the originating entity and the `target` key to indicate the related entity.\n6. You will be presented with a few existing KnowledgeEntities that are similar to the current ones. They will have an existing UUID. When creating relationships to these entities, use their UUID.\n7. Only create relationships between existing KnowledgeEntities.\n8. Entities that exist already in the database should NOT be created again. If there is only a minor overlap, skip creating a new entity.\n9. A new relationship MUST include a newly created KnowledgeEntity." }; END; diff --git a/common/src/storage/types/system_prompts.rs b/common/src/storage/types/system_prompts.rs index 3e0b752..83bab78 100644 --- a/common/src/storage/types/system_prompts.rs +++ b/common/src/storage/types/system_prompts.rs @@ -1,4 +1,4 @@ -pub static DEFAULT_QUERY_SYSTEM_PROMPT: &str = r#"You are a knowledgeable assistant with access to a specialized knowledge base. You will be provided with relevant knowledge entities from the database as context. Each knowledge entity contains a name, description, and type, representing different concepts, ideas, and information. +pub const DEFAULT_QUERY_SYSTEM_PROMPT: &str = r#"You are a knowledgeable assistant with access to a specialized knowledge base. You will be provided with relevant knowledge entities from the database as context. Each knowledge entity contains a name, description, and type, representing different concepts, ideas, and information. Your task is to: 1. Carefully analyze the provided knowledge entities in the context @@ -20,7 +20,7 @@ Example response formats: "I found relevant information in multiple entries: [explanation...]" "I apologize, but the provided context doesn't contain information about [topic]""#; -pub static DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT: &str = r#"You are an AI assistant. You will receive a text content, along with user context and a category. Your task is to provide a structured JSON object representing the content in a graph format suitable for a graph database. You will also be presented with some existing knowledge_entities from the database, do not replicate these! Your task is to create meaningful knowledge entities from the submitted content. Try and infer as much as possible from the users context and category when creating these. If the user submits a large content, create more general entities. If the user submits a narrow and precise content, try and create precise knowledge entities. +pub const DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT: &str = r#"You are an AI assistant. You will receive a text content, along with user context and a category. Your task is to provide a structured JSON object representing the content in a graph format suitable for a graph database. You will also be presented with some existing knowledge_entities from the database, do not replicate these! Your task is to create meaningful knowledge entities from the submitted content. Try and infer as much as possible from the users context and category when creating these. If the user submits a large content, create more general entities. If the user submits a narrow and precise content, try and create precise knowledge entities. The JSON should have the following structure: @@ -49,13 +49,13 @@ Guidelines: 2. Each KnowledgeEntity should have a unique `key`, a meaningful `name`, and a descriptive `description`. 3. Define the type of each KnowledgeEntity using the following categories: Idea, Project, Document, Page, TextSnippet. 4. Establish relationships between entities using types like RelatedTo, RelevantTo, SimilarTo. -5. Use the `source` key to indicate the originating entity and the `target` key to indicate the related entity" +5. Use the `source` key to indicate the originating entity and the `target` key to indicate the related entity. 6. You will be presented with a few existing KnowledgeEntities that are similar to the current ones. They will have an existing UUID. When creating relationships to these entities, use their UUID. 7. Only create relationships between existing KnowledgeEntities. 8. Entities that exist already in the database should NOT be created again. If there is only a minor overlap, skip creating a new entity. 9. A new relationship MUST include a newly created KnowledgeEntity."#; -pub static DEFAULT_IMAGE_PROCESSING_PROMPT: &str = r#"Analyze this image and respond based on its primary content: +pub const DEFAULT_IMAGE_PROCESSING_PROMPT: &str = r#"Analyze this image and respond based on its primary content: - If the image is mainly text (document, screenshot, sign), transcribe the text verbatim. - If the image is mainly visual (photograph, art, landscape), provide a concise description of the scene. - For hybrid images (diagrams, ads), briefly describe the visual, then transcribe the text under a "Text:" heading. diff --git a/common/src/storage/types/system_settings.rs b/common/src/storage/types/system_settings.rs index 0e39bee..8f74adf 100644 --- a/common/src/storage/types/system_settings.rs +++ b/common/src/storage/types/system_settings.rs @@ -1,3 +1,4 @@ +use crate::utils::embedding::EmbeddingBackend; use crate::utils::serde_helpers::deserialize_flexible_id; use serde::{Deserialize, Serialize}; @@ -13,9 +14,9 @@ pub struct SystemSettings { pub processing_model: String, pub embedding_model: String, pub embedding_dimensions: u32, - /// Active embedding backend ("openai", "fastembed", "hashed"). Read-only, synced from config. + /// Active embedding backend. Read-only for admin updates; synced from config at startup. #[serde(default)] - pub embedding_backend: Option, + pub embedding_backend: Option, pub query_system_prompt: String, pub ingestion_system_prompt: String, pub image_processing_model: String, @@ -23,6 +24,27 @@ pub struct SystemSettings { pub voice_processing_model: String, } +/// Partial update for singleton system settings without cloning unchanged fields. +#[derive(Debug, Default, Clone)] +pub struct SystemSettingsPatch { + pub registrations_enabled: Option, + pub require_email_verification: Option, + pub query_model: Option, + pub processing_model: Option, + pub embedding_model: Option, + pub embedding_dimensions: Option, + pub query_system_prompt: Option, + pub ingestion_system_prompt: Option, + pub image_processing_model: Option, + pub image_processing_prompt: Option, + pub voice_processing_model: Option, +} + +enum UpdateMode { + User, + EmbeddingSync, +} + impl StoredObject for SystemSettings { fn table_name() -> &'static str { "system_settings" @@ -33,29 +55,128 @@ impl StoredObject for SystemSettings { } } +impl SystemSettingsPatch { + pub fn apply_to(self, settings: &mut SystemSettings) { + if let Some(value) = self.registrations_enabled { + settings.registrations_enabled = value; + } + if let Some(value) = self.require_email_verification { + settings.require_email_verification = value; + } + if let Some(value) = self.query_model { + settings.query_model = value; + } + if let Some(value) = self.processing_model { + settings.processing_model = value; + } + if let Some(value) = self.embedding_model { + settings.embedding_model = value; + } + if let Some(value) = self.embedding_dimensions { + settings.embedding_dimensions = value; + } + if let Some(value) = self.query_system_prompt { + settings.query_system_prompt = value; + } + if let Some(value) = self.ingestion_system_prompt { + settings.ingestion_system_prompt = value; + } + if let Some(value) = self.image_processing_model { + settings.image_processing_model = value; + } + if let Some(value) = self.image_processing_prompt { + settings.image_processing_prompt = value; + } + if let Some(value) = self.voice_processing_model { + settings.voice_processing_model = value; + } + } + + #[must_use] + pub async fn apply(self, db: &SurrealDbClient) -> Result { + let mut current = SystemSettings::get_current(db).await?; + self.apply_to(&mut current); + SystemSettings::update(db, current).await + } +} + impl SystemSettings { + pub const RECORD_ID: &'static str = "current"; + + fn validate(&self) -> Result<(), AppError> { + if self.embedding_dimensions == 0 { + return Err(AppError::Validation( + "embedding_dimensions must be greater than 0".into(), + )); + } + + let model_fields = [ + ("query_model", &self.query_model), + ("processing_model", &self.processing_model), + ("embedding_model", &self.embedding_model), + ("image_processing_model", &self.image_processing_model), + ("voice_processing_model", &self.voice_processing_model), + ]; + for (name, value) in model_fields { + if value.trim().is_empty() { + return Err(AppError::Validation(format!("{name} must not be empty"))); + } + } + + let prompt_fields = [ + ("query_system_prompt", &self.query_system_prompt), + ("ingestion_system_prompt", &self.ingestion_system_prompt), + ("image_processing_prompt", &self.image_processing_prompt), + ]; + for (name, value) in prompt_fields { + if value.trim().is_empty() { + return Err(AppError::Validation(format!("{name} must not be empty"))); + } + } + + Ok(()) + } + + #[must_use] pub async fn get_current(db: &SurrealDbClient) -> Result { - let settings: Option = db.get_item("current").await?; + let settings: Option = db.get_item(Self::RECORD_ID).await?; settings.ok_or(AppError::NotFound("system settings not found".into())) } + #[must_use] pub async fn update(db: &SurrealDbClient, changes: Self) -> Result { - // We need to use a direct query for the update with MERGE + Self::update_with_mode(db, changes, UpdateMode::User).await + } + + async fn update_with_mode( + db: &SurrealDbClient, + mut changes: Self, + mode: UpdateMode, + ) -> Result { + let current = Self::get_current(db).await?; + if matches!(mode, UpdateMode::User) { + changes.embedding_backend = current.embedding_backend; + } + changes.id = Self::RECORD_ID.to_string(); + changes.validate()?; + let updated: Option = db .client - .query("UPDATE type::thing('system_settings', 'current') MERGE $changes RETURN AFTER") + .query("UPDATE type::thing('system_settings', $id) MERGE $changes RETURN AFTER") + .bind(("id", Self::RECORD_ID)) .bind(("changes", changes)) .await? .take(0)?; - updated.ok_or(AppError::Validation( - "something went wrong updating the settings".into(), + updated.ok_or(AppError::NotFound( + "system settings record missing after update".into(), )) } /// Syncs SystemSettings with the active embedding provider's properties. /// Updates embedding_backend, embedding_model, and embedding_dimensions if they differ. /// Returns true if any settings were changed. + #[must_use] pub async fn sync_from_embedding_provider( db: &SurrealDbClient, provider: &crate::utils::embedding::EmbeddingProvider, @@ -63,23 +184,23 @@ impl SystemSettings { let mut settings = Self::get_current(db).await?; let mut needs_update = false; - let backend_label = provider.backend_label().to_string(); - let provider_dimensions = u32::try_from(provider.dimension()).unwrap_or_else(|_| { - tracing::warn!( - "Provider dimension {} exceeds u32 max; falling back to 0", + let provider_backend = provider + .backend_label() + .parse::() + .map_err(|e| AppError::Validation(e.to_string()))?; + let provider_dimensions = u32::try_from(provider.dimension()).map_err(|_| { + AppError::Validation(format!( + "embedding provider dimension {} exceeds u32::MAX", provider.dimension() - ); - 0u32 - }); + )) + })?; let provider_model = provider.model_code(); - // Sync backend label - if settings.embedding_backend.as_deref() != Some(&backend_label) { - settings.embedding_backend = Some(backend_label); + if settings.embedding_backend != Some(provider_backend) { + settings.embedding_backend = Some(provider_backend); needs_update = true; } - // Sync dimensions if settings.embedding_dimensions != provider_dimensions { tracing::info!( old_dimensions = settings.embedding_dimensions, @@ -90,7 +211,6 @@ impl SystemSettings { needs_update = true; } - // Sync model if provider has one if let Some(model) = provider_model { if settings.embedding_model != model { tracing::info!( @@ -104,7 +224,7 @@ impl SystemSettings { } if needs_update { - settings = Self::update(db, settings).await?; + settings = Self::update_with_mode(db, settings, UpdateMode::EmbeddingSync).await?; } Ok((settings, needs_update)) @@ -225,15 +345,8 @@ mod tests { assert_eq!(settings.query_model, "gpt-4o-mini"); assert_eq!(settings.processing_model, "gpt-4o-mini"); assert_eq!(settings.image_processing_model, "gpt-4o-mini"); - // Dont test these for now, having a hard time getting the formatting exactly the same - // assert_eq!( - // settings.query_system_prompt, - // crate::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT - // ); - // assert_eq!( - // settings.ingestion_system_prompt, - // crate::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT - // ); + assert!(!settings.ingestion_system_prompt.contains("entity\"\n6.")); + assert!(settings.ingestion_system_prompt.contains("related entity.")); // Test idempotency - ensure calling it again doesn't change anything db.apply_migrations() @@ -298,7 +411,6 @@ mod tests { let mut updated_settings = SystemSettings::get_current(&db) .await .with_context(|| "get_current".to_string())?; - updated_settings.id = "current".to_string(); updated_settings.registrations_enabled = false; updated_settings.require_email_verification = true; updated_settings.query_model = "gpt-4".to_string(); @@ -347,6 +459,46 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_update_rejects_zero_embedding_dimensions() -> anyhow::Result<()> { + let db = SurrealDbClient::memory("test_ns", &Uuid::new_v4().to_string()) + .await + .with_context(|| "Failed to start in-memory surrealdb".to_string())?; + db.apply_migrations() + .await + .with_context(|| "Failed to apply migrations".to_string())?; + + let mut invalid_settings = SystemSettings::get_current(&db) + .await + .with_context(|| "Failed to get system settings".to_string())?; + invalid_settings.embedding_dimensions = 0; + + let result = SystemSettings::update(&db, invalid_settings).await; + assert!(matches!(result, Err(AppError::Validation(_)))); + Ok(()) + } + + #[tokio::test] + async fn test_patch_updates_without_cloning_full_settings() -> anyhow::Result<()> { + let db = SurrealDbClient::memory("test_ns", &Uuid::new_v4().to_string()) + .await + .with_context(|| "Failed to start in-memory surrealdb".to_string())?; + db.apply_migrations() + .await + .with_context(|| "Failed to apply migrations".to_string())?; + + let updated = SystemSettingsPatch { + registrations_enabled: Some(false), + ..Default::default() + } + .apply(&db) + .await + .with_context(|| "Failed to patch settings".to_string())?; + + assert!(!updated.registrations_enabled); + Ok(()) + } + #[tokio::test] async fn test_migration_after_changing_embedding_length() -> anyhow::Result<()> { let db = SurrealDbClient::memory("test", &Uuid::new_v4().to_string()) diff --git a/common/src/utils/embedding.rs b/common/src/utils/embedding.rs index dd3afc9..be7c3e7 100644 --- a/common/src/utils/embedding.rs +++ b/common/src/utils/embedding.rs @@ -8,6 +8,7 @@ use std::{ use anyhow::{anyhow, Context, Result}; use async_openai::{types::CreateEmbeddingRequestArgs, Client}; use fastembed::{EmbeddingModel, ModelTrait, TextEmbedding, TextInitOptions}; +use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::debug; @@ -26,7 +27,8 @@ pub struct ParseEmbeddingBackendError { /// Supported embedding backends. #[allow(clippy::module_name_repetitions)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum EmbeddingBackend { #[default] OpenAI, @@ -34,6 +36,17 @@ pub enum EmbeddingBackend { Hashed, } +impl EmbeddingBackend { + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::OpenAI => "openai", + Self::FastEmbed => "fastembed", + Self::Hashed => "hashed", + } + } +} + impl std::str::FromStr for EmbeddingBackend { type Err = ParseEmbeddingBackendError; diff --git a/html-router/src/routes/admin/handlers.rs b/html-router/src/routes/admin/handlers.rs index f6cb9e6..52a9072 100644 --- a/html-router/src/routes/admin/handlers.rs +++ b/html-router/src/routes/admin/handlers.rs @@ -17,9 +17,10 @@ use common::{ DEFAULT_IMAGE_PROCESSING_PROMPT, DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT, }, - system_settings::SystemSettings, + system_settings::{SystemSettings, SystemSettingsPatch}, text_chunk::TextChunk, }, + utils::embedding::EmbeddingBackend, }; use tracing::{error, info}; @@ -124,14 +125,12 @@ pub async fn toggle_registration_status( State(state): State, Form(input): Form, ) -> Result { - let current_settings = SystemSettings::get_current(&state.db).await?; - - let new_settings = SystemSettings { - registrations_enabled: input.registration_open, - ..current_settings.clone() - }; - - SystemSettings::update(&state.db, new_settings.clone()).await?; + let new_settings = SystemSettingsPatch { + registrations_enabled: Some(input.registration_open), + ..Default::default() + } + .apply(&state.db) + .await?; Ok(TemplateResponse::new_partial( "admin/sections/overview.html", @@ -165,10 +164,9 @@ pub async fn update_model_settings( let current_settings = SystemSettings::get_current(&state.db).await?; // Check if using FastEmbed - if so, embedding model/dimensions cannot be changed via UI - let uses_local_embeddings = current_settings - .embedding_backend - .as_deref() - .is_some_and(|b| b == "fastembed" || b == "hashed"); + let uses_local_embeddings = current_settings.embedding_backend.is_some_and( + |backend| matches!(backend, EmbeddingBackend::FastEmbed | EmbeddingBackend::Hashed), + ); // For local embeddings, ignore any embedding model/dimension changes from the form let (final_embedding_model, final_embedding_dimensions, reembedding_needed) = @@ -199,17 +197,17 @@ pub async fn update_model_settings( ) }; - let new_settings = SystemSettings { - query_model: input.query_model, - processing_model: input.processing_model, - image_processing_model: input.image_processing_model, - voice_processing_model: input.voice_processing_model, - embedding_model: final_embedding_model, - embedding_dimensions: final_embedding_dimensions, - ..current_settings.clone() - }; - - SystemSettings::update(&state.db, new_settings.clone()).await?; + let new_settings = SystemSettingsPatch { + query_model: Some(input.query_model), + processing_model: Some(input.processing_model), + image_processing_model: Some(input.image_processing_model), + voice_processing_model: Some(input.voice_processing_model), + embedding_model: Some(final_embedding_model), + embedding_dimensions: Some(final_embedding_dimensions), + ..Default::default() + } + .apply(&state.db) + .await?; if reembedding_needed { info!("Embedding dimensions changed. Spawning background re-embedding task..."); @@ -300,14 +298,12 @@ pub async fn patch_query_prompt( State(state): State, Form(input): Form, ) -> Result { - let current_settings = SystemSettings::get_current(&state.db).await?; - - let new_settings = SystemSettings { - query_system_prompt: input.query_system_prompt, - ..current_settings.clone() - }; - - SystemSettings::update(&state.db, new_settings.clone()).await?; + let new_settings = SystemSettingsPatch { + query_system_prompt: Some(input.query_system_prompt), + ..Default::default() + } + .apply(&state.db) + .await?; Ok(TemplateResponse::new_partial( "admin/sections/overview.html", @@ -347,14 +343,12 @@ pub async fn patch_ingestion_prompt( State(state): State, Form(input): Form, ) -> Result { - let current_settings = SystemSettings::get_current(&state.db).await?; - - let new_settings = SystemSettings { - ingestion_system_prompt: input.ingestion_system_prompt, - ..current_settings.clone() - }; - - SystemSettings::update(&state.db, new_settings.clone()).await?; + let new_settings = SystemSettingsPatch { + ingestion_system_prompt: Some(input.ingestion_system_prompt), + ..Default::default() + } + .apply(&state.db) + .await?; Ok(TemplateResponse::new_partial( "admin/sections/overview.html", @@ -394,14 +388,12 @@ pub async fn patch_image_prompt( State(state): State, Form(input): Form, ) -> Result { - let current_settings = SystemSettings::get_current(&state.db).await?; - - let new_settings = SystemSettings { - image_processing_prompt: input.image_processing_prompt, - ..current_settings.clone() - }; - - SystemSettings::update(&state.db, new_settings.clone()).await?; + let new_settings = SystemSettingsPatch { + image_processing_prompt: Some(input.image_processing_prompt), + ..Default::default() + } + .apply(&state.db) + .await?; Ok(TemplateResponse::new_partial( "admin/sections/overview.html", diff --git a/ingestion-pipeline/src/pipeline/services.rs b/ingestion-pipeline/src/pipeline/services.rs index 42ff717..c32623c 100644 --- a/ingestion-pipeline/src/pipeline/services.rs +++ b/ingestion-pipeline/src/pipeline/services.rs @@ -28,9 +28,7 @@ use text_splitter::{ChunkCapacity, ChunkConfig, TextSplitter}; use super::{enrichment_result::LLMEnrichmentResult, preparation::to_text_content}; use crate::pipeline::context::{EmbeddedKnowledgeEntity, EmbeddedTextChunk}; -use crate::utils::llm_instructions::{ - get_ingress_analysis_schema, INGRESS_ANALYSIS_SYSTEM_MESSAGE, -}; +use crate::utils::llm_instructions::get_ingress_analysis_schema; const EMBEDDING_QUERY_CHAR_LIMIT: usize = 12_000; #[async_trait] @@ -121,7 +119,8 @@ impl DefaultPipelineServices { let request = CreateChatCompletionRequestArgs::default() .model(&settings.processing_model) .messages([ - ChatCompletionRequestSystemMessage::from(INGRESS_ANALYSIS_SYSTEM_MESSAGE).into(), + ChatCompletionRequestSystemMessage::from(settings.ingestion_system_prompt.as_str()) + .into(), ChatCompletionRequestUserMessage::from(user_message).into(), ]) .response_format(response_format) diff --git a/ingestion-pipeline/src/utils/llm_instructions.rs b/ingestion-pipeline/src/utils/llm_instructions.rs index 602115a..8ed2d41 100644 --- a/ingestion-pipeline/src/utils/llm_instructions.rs +++ b/ingestion-pipeline/src/utils/llm_instructions.rs @@ -1,8 +1,5 @@ -use common::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT; use serde_json::json; -pub static INGRESS_ANALYSIS_SYSTEM_MESSAGE: &str = DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT; - pub fn get_ingress_analysis_schema() -> serde_json::Value { json!({ "type": "object", diff --git a/retrieval-pipeline/src/answer_retrieval_helper.rs b/retrieval-pipeline/src/answer_retrieval_helper.rs index c8d5148..66b75a6 100644 --- a/retrieval-pipeline/src/answer_retrieval_helper.rs +++ b/retrieval-pipeline/src/answer_retrieval_helper.rs @@ -1,8 +1,5 @@ -use common::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT; use serde_json::{json, Value}; -pub static QUERY_SYSTEM_PROMPT: &str = DEFAULT_QUERY_SYSTEM_PROMPT; - pub fn get_query_response_schema() -> Value { json!({ "type": "object",