mirror of
https://github.com/perstarkse/minne.git
synced 2026-05-30 03:10:45 +02:00
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.
This commit is contained in:
@@ -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.",
|
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,
|
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]\"",
|
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;
|
END;
|
||||||
|
|||||||
@@ -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:
|
Your task is to:
|
||||||
1. Carefully analyze the provided knowledge entities in the context
|
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 found relevant information in multiple entries: [explanation...]"
|
||||||
"I apologize, but the provided context doesn't contain information about [topic]""#;
|
"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:
|
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`.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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."#;
|
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 text (document, screenshot, sign), transcribe the text verbatim.
|
||||||
- If the image is mainly visual (photograph, art, landscape), provide a concise description of the scene.
|
- 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.
|
- For hybrid images (diagrams, ads), briefly describe the visual, then transcribe the text under a "Text:" heading.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::utils::embedding::EmbeddingBackend;
|
||||||
use crate::utils::serde_helpers::deserialize_flexible_id;
|
use crate::utils::serde_helpers::deserialize_flexible_id;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -13,9 +14,9 @@ pub struct SystemSettings {
|
|||||||
pub processing_model: String,
|
pub processing_model: String,
|
||||||
pub embedding_model: String,
|
pub embedding_model: String,
|
||||||
pub embedding_dimensions: u32,
|
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)]
|
#[serde(default)]
|
||||||
pub embedding_backend: Option<String>,
|
pub embedding_backend: Option<EmbeddingBackend>,
|
||||||
pub query_system_prompt: String,
|
pub query_system_prompt: String,
|
||||||
pub ingestion_system_prompt: String,
|
pub ingestion_system_prompt: String,
|
||||||
pub image_processing_model: String,
|
pub image_processing_model: String,
|
||||||
@@ -23,6 +24,27 @@ pub struct SystemSettings {
|
|||||||
pub voice_processing_model: String,
|
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<bool>,
|
||||||
|
pub require_email_verification: Option<bool>,
|
||||||
|
pub query_model: Option<String>,
|
||||||
|
pub processing_model: Option<String>,
|
||||||
|
pub embedding_model: Option<String>,
|
||||||
|
pub embedding_dimensions: Option<u32>,
|
||||||
|
pub query_system_prompt: Option<String>,
|
||||||
|
pub ingestion_system_prompt: Option<String>,
|
||||||
|
pub image_processing_model: Option<String>,
|
||||||
|
pub image_processing_prompt: Option<String>,
|
||||||
|
pub voice_processing_model: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UpdateMode {
|
||||||
|
User,
|
||||||
|
EmbeddingSync,
|
||||||
|
}
|
||||||
|
|
||||||
impl StoredObject for SystemSettings {
|
impl StoredObject for SystemSettings {
|
||||||
fn table_name() -> &'static str {
|
fn table_name() -> &'static str {
|
||||||
"system_settings"
|
"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<SystemSettings, AppError> {
|
||||||
|
let mut current = SystemSettings::get_current(db).await?;
|
||||||
|
self.apply_to(&mut current);
|
||||||
|
SystemSettings::update(db, current).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SystemSettings {
|
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<Self, AppError> {
|
pub async fn get_current(db: &SurrealDbClient) -> Result<Self, AppError> {
|
||||||
let settings: Option<Self> = db.get_item("current").await?;
|
let settings: Option<Self> = db.get_item(Self::RECORD_ID).await?;
|
||||||
settings.ok_or(AppError::NotFound("system settings not found".into()))
|
settings.ok_or(AppError::NotFound("system settings not found".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub async fn update(db: &SurrealDbClient, changes: Self) -> Result<Self, AppError> {
|
pub async fn update(db: &SurrealDbClient, changes: Self) -> Result<Self, AppError> {
|
||||||
// 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<Self, AppError> {
|
||||||
|
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<Self> = db
|
let updated: Option<Self> = db
|
||||||
.client
|
.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))
|
.bind(("changes", changes))
|
||||||
.await?
|
.await?
|
||||||
.take(0)?;
|
.take(0)?;
|
||||||
|
|
||||||
updated.ok_or(AppError::Validation(
|
updated.ok_or(AppError::NotFound(
|
||||||
"something went wrong updating the settings".into(),
|
"system settings record missing after update".into(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Syncs SystemSettings with the active embedding provider's properties.
|
/// Syncs SystemSettings with the active embedding provider's properties.
|
||||||
/// Updates embedding_backend, embedding_model, and embedding_dimensions if they differ.
|
/// Updates embedding_backend, embedding_model, and embedding_dimensions if they differ.
|
||||||
/// Returns true if any settings were changed.
|
/// Returns true if any settings were changed.
|
||||||
|
#[must_use]
|
||||||
pub async fn sync_from_embedding_provider(
|
pub async fn sync_from_embedding_provider(
|
||||||
db: &SurrealDbClient,
|
db: &SurrealDbClient,
|
||||||
provider: &crate::utils::embedding::EmbeddingProvider,
|
provider: &crate::utils::embedding::EmbeddingProvider,
|
||||||
@@ -63,23 +184,23 @@ impl SystemSettings {
|
|||||||
let mut settings = Self::get_current(db).await?;
|
let mut settings = Self::get_current(db).await?;
|
||||||
let mut needs_update = false;
|
let mut needs_update = false;
|
||||||
|
|
||||||
let backend_label = provider.backend_label().to_string();
|
let provider_backend = provider
|
||||||
let provider_dimensions = u32::try_from(provider.dimension()).unwrap_or_else(|_| {
|
.backend_label()
|
||||||
tracing::warn!(
|
.parse::<EmbeddingBackend>()
|
||||||
"Provider dimension {} exceeds u32 max; falling back to 0",
|
.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()
|
provider.dimension()
|
||||||
);
|
))
|
||||||
0u32
|
})?;
|
||||||
});
|
|
||||||
let provider_model = provider.model_code();
|
let provider_model = provider.model_code();
|
||||||
|
|
||||||
// Sync backend label
|
if settings.embedding_backend != Some(provider_backend) {
|
||||||
if settings.embedding_backend.as_deref() != Some(&backend_label) {
|
settings.embedding_backend = Some(provider_backend);
|
||||||
settings.embedding_backend = Some(backend_label);
|
|
||||||
needs_update = true;
|
needs_update = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync dimensions
|
|
||||||
if settings.embedding_dimensions != provider_dimensions {
|
if settings.embedding_dimensions != provider_dimensions {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
old_dimensions = settings.embedding_dimensions,
|
old_dimensions = settings.embedding_dimensions,
|
||||||
@@ -90,7 +211,6 @@ impl SystemSettings {
|
|||||||
needs_update = true;
|
needs_update = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync model if provider has one
|
|
||||||
if let Some(model) = provider_model {
|
if let Some(model) = provider_model {
|
||||||
if settings.embedding_model != model {
|
if settings.embedding_model != model {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -104,7 +224,7 @@ impl SystemSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if needs_update {
|
if needs_update {
|
||||||
settings = Self::update(db, settings).await?;
|
settings = Self::update_with_mode(db, settings, UpdateMode::EmbeddingSync).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((settings, needs_update))
|
Ok((settings, needs_update))
|
||||||
@@ -225,15 +345,8 @@ mod tests {
|
|||||||
assert_eq!(settings.query_model, "gpt-4o-mini");
|
assert_eq!(settings.query_model, "gpt-4o-mini");
|
||||||
assert_eq!(settings.processing_model, "gpt-4o-mini");
|
assert_eq!(settings.processing_model, "gpt-4o-mini");
|
||||||
assert_eq!(settings.image_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!(!settings.ingestion_system_prompt.contains("entity\"\n6."));
|
||||||
// assert_eq!(
|
assert!(settings.ingestion_system_prompt.contains("related entity."));
|
||||||
// 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
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Test idempotency - ensure calling it again doesn't change anything
|
// Test idempotency - ensure calling it again doesn't change anything
|
||||||
db.apply_migrations()
|
db.apply_migrations()
|
||||||
@@ -298,7 +411,6 @@ mod tests {
|
|||||||
let mut updated_settings = SystemSettings::get_current(&db)
|
let mut updated_settings = SystemSettings::get_current(&db)
|
||||||
.await
|
.await
|
||||||
.with_context(|| "get_current".to_string())?;
|
.with_context(|| "get_current".to_string())?;
|
||||||
updated_settings.id = "current".to_string();
|
|
||||||
updated_settings.registrations_enabled = false;
|
updated_settings.registrations_enabled = false;
|
||||||
updated_settings.require_email_verification = true;
|
updated_settings.require_email_verification = true;
|
||||||
updated_settings.query_model = "gpt-4".to_string();
|
updated_settings.query_model = "gpt-4".to_string();
|
||||||
@@ -347,6 +459,46 @@ mod tests {
|
|||||||
Ok(())
|
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]
|
#[tokio::test]
|
||||||
async fn test_migration_after_changing_embedding_length() -> anyhow::Result<()> {
|
async fn test_migration_after_changing_embedding_length() -> anyhow::Result<()> {
|
||||||
let db = SurrealDbClient::memory("test", &Uuid::new_v4().to_string())
|
let db = SurrealDbClient::memory("test", &Uuid::new_v4().to_string())
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::{
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use async_openai::{types::CreateEmbeddingRequestArgs, Client};
|
use async_openai::{types::CreateEmbeddingRequestArgs, Client};
|
||||||
use fastembed::{EmbeddingModel, ModelTrait, TextEmbedding, TextInitOptions};
|
use fastembed::{EmbeddingModel, ModelTrait, TextEmbedding, TextInitOptions};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
@@ -26,7 +27,8 @@ pub struct ParseEmbeddingBackendError {
|
|||||||
|
|
||||||
/// Supported embedding backends.
|
/// Supported embedding backends.
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[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 {
|
pub enum EmbeddingBackend {
|
||||||
#[default]
|
#[default]
|
||||||
OpenAI,
|
OpenAI,
|
||||||
@@ -34,6 +36,17 @@ pub enum EmbeddingBackend {
|
|||||||
Hashed,
|
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 {
|
impl std::str::FromStr for EmbeddingBackend {
|
||||||
type Err = ParseEmbeddingBackendError;
|
type Err = ParseEmbeddingBackendError;
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ use common::{
|
|||||||
DEFAULT_IMAGE_PROCESSING_PROMPT, DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT,
|
DEFAULT_IMAGE_PROCESSING_PROMPT, DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT,
|
||||||
DEFAULT_QUERY_SYSTEM_PROMPT,
|
DEFAULT_QUERY_SYSTEM_PROMPT,
|
||||||
},
|
},
|
||||||
system_settings::SystemSettings,
|
system_settings::{SystemSettings, SystemSettingsPatch},
|
||||||
text_chunk::TextChunk,
|
text_chunk::TextChunk,
|
||||||
},
|
},
|
||||||
|
utils::embedding::EmbeddingBackend,
|
||||||
};
|
};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
@@ -124,14 +125,12 @@ pub async fn toggle_registration_status(
|
|||||||
State(state): State<HtmlState>,
|
State(state): State<HtmlState>,
|
||||||
Form(input): Form<RegistrationToggleInput>,
|
Form(input): Form<RegistrationToggleInput>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
let current_settings = SystemSettings::get_current(&state.db).await?;
|
let new_settings = SystemSettingsPatch {
|
||||||
|
registrations_enabled: Some(input.registration_open),
|
||||||
let new_settings = SystemSettings {
|
..Default::default()
|
||||||
registrations_enabled: input.registration_open,
|
}
|
||||||
..current_settings.clone()
|
.apply(&state.db)
|
||||||
};
|
.await?;
|
||||||
|
|
||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/sections/overview.html",
|
"admin/sections/overview.html",
|
||||||
@@ -165,10 +164,9 @@ pub async fn update_model_settings(
|
|||||||
let current_settings = SystemSettings::get_current(&state.db).await?;
|
let current_settings = SystemSettings::get_current(&state.db).await?;
|
||||||
|
|
||||||
// Check if using FastEmbed - if so, embedding model/dimensions cannot be changed via UI
|
// Check if using FastEmbed - if so, embedding model/dimensions cannot be changed via UI
|
||||||
let uses_local_embeddings = current_settings
|
let uses_local_embeddings = current_settings.embedding_backend.is_some_and(
|
||||||
.embedding_backend
|
|backend| matches!(backend, EmbeddingBackend::FastEmbed | EmbeddingBackend::Hashed),
|
||||||
.as_deref()
|
);
|
||||||
.is_some_and(|b| b == "fastembed" || b == "hashed");
|
|
||||||
|
|
||||||
// For local embeddings, ignore any embedding model/dimension changes from the form
|
// For local embeddings, ignore any embedding model/dimension changes from the form
|
||||||
let (final_embedding_model, final_embedding_dimensions, reembedding_needed) =
|
let (final_embedding_model, final_embedding_dimensions, reembedding_needed) =
|
||||||
@@ -199,17 +197,17 @@ pub async fn update_model_settings(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_settings = SystemSettings {
|
let new_settings = SystemSettingsPatch {
|
||||||
query_model: input.query_model,
|
query_model: Some(input.query_model),
|
||||||
processing_model: input.processing_model,
|
processing_model: Some(input.processing_model),
|
||||||
image_processing_model: input.image_processing_model,
|
image_processing_model: Some(input.image_processing_model),
|
||||||
voice_processing_model: input.voice_processing_model,
|
voice_processing_model: Some(input.voice_processing_model),
|
||||||
embedding_model: final_embedding_model,
|
embedding_model: Some(final_embedding_model),
|
||||||
embedding_dimensions: final_embedding_dimensions,
|
embedding_dimensions: Some(final_embedding_dimensions),
|
||||||
..current_settings.clone()
|
..Default::default()
|
||||||
};
|
}
|
||||||
|
.apply(&state.db)
|
||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
.await?;
|
||||||
|
|
||||||
if reembedding_needed {
|
if reembedding_needed {
|
||||||
info!("Embedding dimensions changed. Spawning background re-embedding task...");
|
info!("Embedding dimensions changed. Spawning background re-embedding task...");
|
||||||
@@ -300,14 +298,12 @@ pub async fn patch_query_prompt(
|
|||||||
State(state): State<HtmlState>,
|
State(state): State<HtmlState>,
|
||||||
Form(input): Form<SystemPromptUpdateInput>,
|
Form(input): Form<SystemPromptUpdateInput>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
let current_settings = SystemSettings::get_current(&state.db).await?;
|
let new_settings = SystemSettingsPatch {
|
||||||
|
query_system_prompt: Some(input.query_system_prompt),
|
||||||
let new_settings = SystemSettings {
|
..Default::default()
|
||||||
query_system_prompt: input.query_system_prompt,
|
}
|
||||||
..current_settings.clone()
|
.apply(&state.db)
|
||||||
};
|
.await?;
|
||||||
|
|
||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/sections/overview.html",
|
"admin/sections/overview.html",
|
||||||
@@ -347,14 +343,12 @@ pub async fn patch_ingestion_prompt(
|
|||||||
State(state): State<HtmlState>,
|
State(state): State<HtmlState>,
|
||||||
Form(input): Form<IngestionPromptUpdateInput>,
|
Form(input): Form<IngestionPromptUpdateInput>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
let current_settings = SystemSettings::get_current(&state.db).await?;
|
let new_settings = SystemSettingsPatch {
|
||||||
|
ingestion_system_prompt: Some(input.ingestion_system_prompt),
|
||||||
let new_settings = SystemSettings {
|
..Default::default()
|
||||||
ingestion_system_prompt: input.ingestion_system_prompt,
|
}
|
||||||
..current_settings.clone()
|
.apply(&state.db)
|
||||||
};
|
.await?;
|
||||||
|
|
||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/sections/overview.html",
|
"admin/sections/overview.html",
|
||||||
@@ -394,14 +388,12 @@ pub async fn patch_image_prompt(
|
|||||||
State(state): State<HtmlState>,
|
State(state): State<HtmlState>,
|
||||||
Form(input): Form<ImagePromptUpdateInput>,
|
Form(input): Form<ImagePromptUpdateInput>,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
let current_settings = SystemSettings::get_current(&state.db).await?;
|
let new_settings = SystemSettingsPatch {
|
||||||
|
image_processing_prompt: Some(input.image_processing_prompt),
|
||||||
let new_settings = SystemSettings {
|
..Default::default()
|
||||||
image_processing_prompt: input.image_processing_prompt,
|
}
|
||||||
..current_settings.clone()
|
.apply(&state.db)
|
||||||
};
|
.await?;
|
||||||
|
|
||||||
SystemSettings::update(&state.db, new_settings.clone()).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_partial(
|
Ok(TemplateResponse::new_partial(
|
||||||
"admin/sections/overview.html",
|
"admin/sections/overview.html",
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ use text_splitter::{ChunkCapacity, ChunkConfig, TextSplitter};
|
|||||||
|
|
||||||
use super::{enrichment_result::LLMEnrichmentResult, preparation::to_text_content};
|
use super::{enrichment_result::LLMEnrichmentResult, preparation::to_text_content};
|
||||||
use crate::pipeline::context::{EmbeddedKnowledgeEntity, EmbeddedTextChunk};
|
use crate::pipeline::context::{EmbeddedKnowledgeEntity, EmbeddedTextChunk};
|
||||||
use crate::utils::llm_instructions::{
|
use crate::utils::llm_instructions::get_ingress_analysis_schema;
|
||||||
get_ingress_analysis_schema, INGRESS_ANALYSIS_SYSTEM_MESSAGE,
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMBEDDING_QUERY_CHAR_LIMIT: usize = 12_000;
|
const EMBEDDING_QUERY_CHAR_LIMIT: usize = 12_000;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -121,7 +119,8 @@ impl DefaultPipelineServices {
|
|||||||
let request = CreateChatCompletionRequestArgs::default()
|
let request = CreateChatCompletionRequestArgs::default()
|
||||||
.model(&settings.processing_model)
|
.model(&settings.processing_model)
|
||||||
.messages([
|
.messages([
|
||||||
ChatCompletionRequestSystemMessage::from(INGRESS_ANALYSIS_SYSTEM_MESSAGE).into(),
|
ChatCompletionRequestSystemMessage::from(settings.ingestion_system_prompt.as_str())
|
||||||
|
.into(),
|
||||||
ChatCompletionRequestUserMessage::from(user_message).into(),
|
ChatCompletionRequestUserMessage::from(user_message).into(),
|
||||||
])
|
])
|
||||||
.response_format(response_format)
|
.response_format(response_format)
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use common::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT;
|
|
||||||
use serde_json::json;
|
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 {
|
pub fn get_ingress_analysis_schema() -> serde_json::Value {
|
||||||
json!({
|
json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use common::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT;
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
pub static QUERY_SYSTEM_PROMPT: &str = DEFAULT_QUERY_SYSTEM_PROMPT;
|
|
||||||
|
|
||||||
pub fn get_query_response_schema() -> Value {
|
pub fn get_query_response_schema() -> Value {
|
||||||
json!({
|
json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
Reference in New Issue
Block a user