From b956bdbe2bd47b7206ebb90992c56287b6dcde20 Mon Sep 17 00:00:00 2001 From: Per Stark Date: Sun, 23 Mar 2025 22:20:57 +0100 Subject: [PATCH] feat: system prompt customisable --- crates/common/src/storage/types/mod.rs | 1 + .../src/storage/types/system_prompts.rs | 56 ++++++ .../src/storage/types/system_settings.rs | 21 +++ .../src/answer_retrieval.rs | 9 +- .../src/answer_retrieval_helper.rs | 26 +-- crates/composite-retrieval/src/lib.rs | 3 +- crates/composite-retrieval/src/vector.rs | 1 + crates/html-router/src/lib.rs | 14 +- crates/html-router/src/routes/admin_panel.rs | 172 +++++++++++++++++- .../routes/chat/message_response_stream.rs | 9 +- crates/ingestion-pipeline/src/enricher.rs | 51 +++--- crates/ingestion-pipeline/src/pipeline.rs | 2 +- crates/ingestion-pipeline/src/types/mod.rs | 21 ++- .../src/utils/llm_instructions.rs | 108 ++++------- .../admin/edit_ingestion_prompt_modal.html | 38 ++++ templates/admin/edit_query_prompt_modal.html | 38 ++++ templates/auth/admin_panel.html | 56 +++++- templates/documentation/get_started.html | 3 - todo.md | 5 + 19 files changed, 493 insertions(+), 141 deletions(-) create mode 100644 crates/common/src/storage/types/system_prompts.rs create mode 100644 templates/admin/edit_ingestion_prompt_modal.html create mode 100644 templates/admin/edit_query_prompt_modal.html diff --git a/crates/common/src/storage/types/mod.rs b/crates/common/src/storage/types/mod.rs index 17cc2fd..ce566c9 100644 --- a/crates/common/src/storage/types/mod.rs +++ b/crates/common/src/storage/types/mod.rs @@ -8,6 +8,7 @@ pub mod ingestion_task; pub mod knowledge_entity; pub mod knowledge_relationship; pub mod message; +pub mod system_prompts; pub mod system_settings; pub mod text_chunk; pub mod text_content; diff --git a/crates/common/src/storage/types/system_prompts.rs b/crates/common/src/storage/types/system_prompts.rs new file mode 100644 index 0000000..9e2ace0 --- /dev/null +++ b/crates/common/src/storage/types/system_prompts.rs @@ -0,0 +1,56 @@ +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. + +Your task is to: +1. Carefully analyze the provided knowledge entities in the context +2. Answer user questions based on this information +3. Provide clear, concise, and accurate responses +4. When referencing information, briefly mention which knowledge entity it came from +5. If the provided context doesn't contain enough information to answer the question confidently, clearly state this +6. If only partial information is available, explain what you can answer and what information is missing +7. Avoid making assumptions or providing information not supported by the context +8. Output the references to the documents. Use the UUIDs and make sure they are correct! + +Remember: +- Be direct and honest about the limitations of your knowledge +- Cite the relevant knowledge entities when providing information, but only provide the UUIDs in the reference array +- If you need to combine information from multiple entities, explain how they connect +- Don't speculate beyond what's provided in the context +Example response formats: +"Based on [Entity Name], [answer...]" +"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 instructions 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 instructions 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: + +{ +"knowledge_entities": [ +{ +"key": "unique-key-1", +"name": "Entity Name", +"description": "A detailed description of the entity.", +"entity_type": "TypeOfEntity" +}, +// More entities... +], +"relationships": [ +{ +"type": "RelationshipType", +"source": "unique-key-1 or UUID from existing database", +"target": "unique-key-1 or UUID from existing database" +}, +// More relationships... +] +} + +Guidelines: +1. Do NOT generate any IDs or UUIDs. Use a unique `key` for each knowledge entity. +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" +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."#; diff --git a/crates/common/src/storage/types/system_settings.rs b/crates/common/src/storage/types/system_settings.rs index 7a4801a..c6b8447 100644 --- a/crates/common/src/storage/types/system_settings.rs +++ b/crates/common/src/storage/types/system_settings.rs @@ -1,5 +1,6 @@ use crate::storage::types::file_info::deserialize_flexible_id; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use crate::{error::AppError, storage::db::SurrealDbClient}; @@ -9,6 +10,10 @@ pub struct SystemSettings { pub id: String, pub registrations_enabled: bool, pub require_email_verification: bool, + pub query_model: String, + pub processing_model: String, + pub query_system_prompt: String, + pub ingestion_system_prompt: String, } impl SystemSettings { @@ -22,6 +27,10 @@ impl SystemSettings { id: "current".to_string(), registrations_enabled: true, require_email_verification: false, + query_model: "gpt-4o-mini".to_string(), + processing_model: "gpt-4o-mini".to_string(), + query_system_prompt: crate::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT.to_string(), + ingestion_system_prompt: crate::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT.to_string(), }) .await?; @@ -53,4 +62,16 @@ impl SystemSettings { "Something went wrong updating the settings".into(), )) } + + pub fn new() -> Self { + Self { + id: Uuid::new_v4().to_string(), + query_system_prompt: crate::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT.to_string(), + ingestion_system_prompt: crate::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT.to_string(), + query_model: "gpt-4o-mini".to_string(), + processing_model: "gpt-4o-mini".to_string(), + registrations_enabled: true, + require_email_verification: false, + } + } } diff --git a/crates/composite-retrieval/src/answer_retrieval.rs b/crates/composite-retrieval/src/answer_retrieval.rs index 5cf66ea..3213c53 100644 --- a/crates/composite-retrieval/src/answer_retrieval.rs +++ b/crates/composite-retrieval/src/answer_retrieval.rs @@ -13,6 +13,7 @@ use common::{ types::{ knowledge_entity::KnowledgeEntity, message::{format_history, Message}, + system_settings::SystemSettings, }, }, }; @@ -63,11 +64,12 @@ pub async fn get_answer_with_references( user_id: &str, ) -> Result { let entities = retrieve_entities(surreal_db_client, openai_client, query, user_id).await?; + let settings = SystemSettings::get_current(surreal_db_client).await?; let entities_json = format_entities_json(&entities); let user_message = create_user_message(&entities_json, query); - let request = create_chat_request(user_message)?; + let request = create_chat_request(user_message, &settings)?; let response = openai_client.chat().create(request).await?; let llm_response = process_llm_response(response).await?; @@ -139,6 +141,7 @@ pub fn create_user_message_with_history( pub fn create_chat_request( user_message: String, + settings: &SystemSettings, ) -> Result { let response_format = ResponseFormat::JsonSchema { json_schema: ResponseFormatJsonSchema { @@ -150,11 +153,11 @@ pub fn create_chat_request( }; CreateChatCompletionRequestArgs::default() - .model("gpt-4o-mini") + .model(&settings.query_model) .temperature(0.2) .max_tokens(3048u32) .messages([ - ChatCompletionRequestSystemMessage::from(QUERY_SYSTEM_PROMPT).into(), + ChatCompletionRequestSystemMessage::from(settings.query_system_prompt.clone()).into(), ChatCompletionRequestUserMessage::from(user_message).into(), ]) .response_format(response_format) diff --git a/crates/composite-retrieval/src/answer_retrieval_helper.rs b/crates/composite-retrieval/src/answer_retrieval_helper.rs index 36e54b0..c8d5148 100644 --- a/crates/composite-retrieval/src/answer_retrieval_helper.rs +++ b/crates/composite-retrieval/src/answer_retrieval_helper.rs @@ -1,29 +1,7 @@ +use common::storage::types::system_prompts::DEFAULT_QUERY_SYSTEM_PROMPT; use serde_json::{json, Value}; -pub static 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 - 2. Answer user questions based on this information - 3. Provide clear, concise, and accurate responses - 4. When referencing information, briefly mention which knowledge entity it came from - 5. If the provided context doesn't contain enough information to answer the question confidently, clearly state this - 6. If only partial information is available, explain what you can answer and what information is missing - 7. Avoid making assumptions or providing information not supported by the context - 8. Output the references to the documents. Use the UUIDs and make sure they are correct! - - Remember: - - Be direct and honest about the limitations of your knowledge - - Cite the relevant knowledge entities when providing information, but only provide the UUIDs in the reference array - - If you need to combine information from multiple entities, explain how they connect - - Don't speculate beyond what's provided in the context - - Example response formats: - "Based on [Entity Name], [answer...]" - "I found relevant information in multiple entries: [explanation...]" - "I apologize, but the provided context doesn't contain information about [topic]" - "#; +pub static QUERY_SYSTEM_PROMPT: &str = DEFAULT_QUERY_SYSTEM_PROMPT; pub fn get_query_response_schema() -> Value { json!({ diff --git a/crates/composite-retrieval/src/lib.rs b/crates/composite-retrieval/src/lib.rs index 735579d..c17cebc 100644 --- a/crates/composite-retrieval/src/lib.rs +++ b/crates/composite-retrieval/src/lib.rs @@ -13,6 +13,7 @@ use common::{ use futures::future::{try_join, try_join_all}; use graph::{find_entities_by_relationship_by_id, find_entities_by_source_ids}; use std::collections::HashMap; +use tracing::info; use vector::find_items_by_vector_similarity; /// Performs a comprehensive knowledge entity retrieval using multiple search strategies @@ -42,8 +43,6 @@ pub async fn retrieve_entities( query: &str, user_id: &str, ) -> Result, AppError> { - // info!("Received input: {:?}", query); - let (items_from_knowledge_entity_similarity, closest_chunks) = try_join( find_items_by_vector_similarity( 10, diff --git a/crates/composite-retrieval/src/vector.rs b/crates/composite-retrieval/src/vector.rs index a965b7a..192cdcf 100644 --- a/crates/composite-retrieval/src/vector.rs +++ b/crates/composite-retrieval/src/vector.rs @@ -1,6 +1,7 @@ use surrealdb::{engine::any::Any, Surreal}; use common::{error::AppError, utils::embedding::generate_embedding}; +use tracing::info; /// Compares vectors and retrieves a number of items from the specified table. /// diff --git a/crates/html-router/src/lib.rs b/crates/html-router/src/lib.rs index 8989d96..a3ba671 100644 --- a/crates/html-router/src/lib.rs +++ b/crates/html-router/src/lib.rs @@ -22,7 +22,10 @@ use routes::{ change_password, delete_account, set_api_key, show_account_page, show_change_password, update_timezone, }, - admin_panel::{show_admin_panel, toggle_registration_status}, + admin_panel::{ + patch_ingestion_prompt, patch_query_prompt, show_admin_panel, show_edit_ingestion_prompt, + show_edit_system_prompt, toggle_registration_status, update_model_settings, + }, chat::{ message_response_stream::get_response_stream, new_chat_user_message, new_user_message, references::show_reference_tooltip, show_chat_base, show_existing_chat, @@ -107,9 +110,16 @@ where "/knowledge-relationship/:id", delete(delete_knowledge_relationship), ) - .route("/account", get(show_account_page)) + // Admin page .route("/admin", get(show_admin_panel)) .route("/toggle-registrations", patch(toggle_registration_status)) + .route("/update-model-settings", patch(update_model_settings)) + .route("/edit-query-prompt", get(show_edit_system_prompt)) + .route("/update-query-prompt", patch(patch_query_prompt)) + .route("/edit-ingestion-prompt", get(show_edit_ingestion_prompt)) + .route("/update-ingestion-prompt", patch(patch_ingestion_prompt)) + // User account page + .route("/account", get(show_account_page)) .route("/set-api-key", post(set_api_key)) .route("/update-timezone", patch(update_timezone)) .route( diff --git a/crates/html-router/src/routes/admin_panel.rs b/crates/html-router/src/routes/admin_panel.rs index 40462b8..0877b30 100644 --- a/crates/html-router/src/routes/admin_panel.rs +++ b/crates/html-router/src/routes/admin_panel.rs @@ -5,7 +5,12 @@ use crate::{ middleware_auth::RequireUser, template_response::{HtmlError, TemplateResponse}, }; -use common::storage::types::{analytics::Analytics, system_settings::SystemSettings, user::User}; +use common::storage::types::{ + analytics::Analytics, + system_prompts::{DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT}, + system_settings::SystemSettings, + user::User, +}; use crate::html_state::HtmlState; @@ -15,6 +20,7 @@ pub struct AdminPanelData { settings: SystemSettings, analytics: Analytics, users: i64, + default_query_prompt: String, } pub async fn show_admin_panel( @@ -32,6 +38,7 @@ pub async fn show_admin_panel( settings, analytics, users: users_count, + default_query_prompt: DEFAULT_QUERY_SYSTEM_PROMPT.to_string(), }, )) } @@ -85,3 +92,166 @@ pub async fn toggle_registration_status( }, )) } + +#[derive(Deserialize)] +pub struct ModelSettingsInput { + query_model: String, + processing_model: String, +} + +#[derive(Serialize)] +pub struct ModelSettingsData { + settings: SystemSettings, +} + +pub async fn update_model_settings( + State(state): State, + RequireUser(user): RequireUser, + Form(input): Form, +) -> Result { + // Early return if the user is not admin + if !user.admin { + return Ok(TemplateResponse::redirect("/")); + }; + + let current_settings = SystemSettings::get_current(&state.db).await?; + + let new_settings = SystemSettings { + query_model: input.query_model, + processing_model: input.processing_model, + ..current_settings + }; + + SystemSettings::update(&state.db, new_settings.clone()).await?; + + Ok(TemplateResponse::new_partial( + "auth/admin_panel.html", + "model_settings_form", + ModelSettingsData { + settings: new_settings, + }, + )) +} + +#[derive(Serialize)] +pub struct SystemPromptEditData { + settings: SystemSettings, + default_query_prompt: String, +} + +pub async fn show_edit_system_prompt( + State(state): State, + RequireUser(user): RequireUser, +) -> Result { + // Early return if the user is not admin + if !user.admin { + return Ok(TemplateResponse::redirect("/")); + }; + + let settings = SystemSettings::get_current(&state.db).await?; + + Ok(TemplateResponse::new_template( + "admin/edit_query_prompt_modal.html", + SystemPromptEditData { + settings, + default_query_prompt: DEFAULT_QUERY_SYSTEM_PROMPT.to_string(), + }, + )) +} + +#[derive(Deserialize)] +pub struct SystemPromptUpdateInput { + query_system_prompt: String, +} + +#[derive(Serialize)] +pub struct SystemPromptSectionData { + settings: SystemSettings, +} + +pub async fn patch_query_prompt( + State(state): State, + RequireUser(user): RequireUser, + Form(input): Form, +) -> Result { + // Early return if the user is not admin + if !user.admin { + return Ok(TemplateResponse::redirect("/")); + }; + + 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?; + + Ok(TemplateResponse::new_partial( + "auth/admin_panel.html", + "system_prompt_section", + SystemPromptSectionData { + settings: new_settings, + }, + )) +} + +#[derive(Serialize)] +pub struct IngestionPromptEditData { + settings: SystemSettings, + default_ingestion_prompt: String, +} + +pub async fn show_edit_ingestion_prompt( + State(state): State, + RequireUser(user): RequireUser, +) -> Result { + // Early return if the user is not admin + if !user.admin { + return Ok(TemplateResponse::redirect("/")); + }; + + let settings = SystemSettings::get_current(&state.db).await?; + + Ok(TemplateResponse::new_template( + "admin/edit_ingestion_prompt_modal.html", + IngestionPromptEditData { + settings, + default_ingestion_prompt: DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT.to_string(), + }, + )) +} + +#[derive(Deserialize)] +pub struct IngestionPromptUpdateInput { + ingestion_system_prompt: String, +} + +pub async fn patch_ingestion_prompt( + State(state): State, + RequireUser(user): RequireUser, + Form(input): Form, +) -> Result { + // Early return if the user is not admin + if !user.admin { + return Ok(TemplateResponse::redirect("/")); + }; + + 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?; + + Ok(TemplateResponse::new_partial( + "auth/admin_panel.html", + "system_prompt_section", + SystemPromptSectionData { + settings: new_settings, + }, + )) +} diff --git a/crates/html-router/src/routes/chat/message_response_stream.rs b/crates/html-router/src/routes/chat/message_response_stream.rs index f948e5f..debb32b 100644 --- a/crates/html-router/src/routes/chat/message_response_stream.rs +++ b/crates/html-router/src/routes/chat/message_response_stream.rs @@ -34,6 +34,7 @@ use common::storage::{ conversation::Conversation, message::{Message, MessageRole}, user::User, + system_settings::SystemSettings, }, }; @@ -139,7 +140,13 @@ pub async fn get_response_stream( let entities_json = format_entities_json(&entities); let formatted_user_message = create_user_message_with_history(&entities_json, &history, &user_message.content); - let request = match create_chat_request(formatted_user_message) { + let settings = match SystemSettings::get_current(&state.db).await { + Ok(s) => s, + Err(_) => { + return Sse::new(create_error_stream("Failed to retrieve system settings")); + } + }; + let request = match create_chat_request(formatted_user_message, &settings) { Ok(req) => req, Err(..) => { return Sse::new(create_error_stream("Failed to create chat request")); diff --git a/crates/ingestion-pipeline/src/enricher.rs b/crates/ingestion-pipeline/src/enricher.rs index b362a88..6184df5 100644 --- a/crates/ingestion-pipeline/src/enricher.rs +++ b/crates/ingestion-pipeline/src/enricher.rs @@ -10,11 +10,17 @@ use async_openai::{ }; use common::{ error::AppError, - storage::{db::SurrealDbClient, types::knowledge_entity::KnowledgeEntity}, + storage::{ + db::SurrealDbClient, + types::{ + knowledge_entity::KnowledgeEntity, + system_settings::SystemSettings, + }, + }, }; use composite_retrieval::retrieve_entities; use serde_json::json; -use tracing::debug; +use tracing::{debug, info}; use crate::{ types::llm_enrichment_result::LLMEnrichmentResult, @@ -44,11 +50,13 @@ impl IngestionEnricher { text: &str, user_id: &str, ) -> Result { + info!("getting similar entitities"); let similar_entities = self .find_similar_entities(category, instructions, text, user_id) .await?; + info!("got similar entitities"); let llm_request = - self.prepare_llm_request(category, instructions, text, &similar_entities)?; + self.prepare_llm_request(category, instructions, text, &similar_entities).await?; self.perform_analysis(llm_request).await } @@ -67,13 +75,15 @@ impl IngestionEnricher { retrieve_entities(&self.db_client, &self.openai_client, &input_text, user_id).await } - fn prepare_llm_request( + async fn prepare_llm_request( &self, category: &str, instructions: &str, text: &str, similar_entities: &[KnowledgeEntity], - ) -> Result { + ) -> Result { + let settings = SystemSettings::get_current(&self.db_client).await?; + let entities_json = json!(similar_entities .iter() .map(|entity| { @@ -103,8 +113,8 @@ impl IngestionEnricher { }, }; - CreateChatCompletionRequestArgs::default() - .model("gpt-4o-mini") + let request = CreateChatCompletionRequestArgs::default() + .model(&settings.processing_model) .temperature(0.2) .max_tokens(3048u32) .messages([ @@ -112,30 +122,27 @@ impl IngestionEnricher { ChatCompletionRequestUserMessage::from(user_message).into(), ]) .response_format(response_format) - .build() + .build()?; + + Ok(request) } - + async fn perform_analysis( &self, request: CreateChatCompletionRequest, ) -> Result { let response = self.openai_client.chat().create(request).await?; - debug!("Received LLM response: {:?}", response); - - response + + let content = response .choices .first() .and_then(|choice| choice.message.content.as_ref()) .ok_or(AppError::LLMParsing( - "No content found in LLM response".to_string(), - )) - .and_then(|content| { - serde_json::from_str(content).map_err(|e| { - AppError::LLMParsing(format!( - "Failed to parse LLM response into analysis: {}", - e - )) - }) - }) + "No content found in LLM response".into(), + ))?; + + serde_json::from_str::(content).map_err(|e| { + AppError::LLMParsing(format!("Failed to parse LLM response into analysis: {}", e)) + }) } } diff --git a/crates/ingestion-pipeline/src/pipeline.rs b/crates/ingestion-pipeline/src/pipeline.rs index 66f69dd..26db066 100644 --- a/crates/ingestion-pipeline/src/pipeline.rs +++ b/crates/ingestion-pipeline/src/pipeline.rs @@ -53,7 +53,7 @@ impl IngestionPipeline { ) .await?; - let text_content = to_text_content(task.content, &self.openai_client).await?; + let text_content = to_text_content(task.content, &self.openai_client, &self.db).await?; match self.process(&text_content).await { Ok(_) => { diff --git a/crates/ingestion-pipeline/src/types/mod.rs b/crates/ingestion-pipeline/src/types/mod.rs index 95b2826..98a7e4f 100644 --- a/crates/ingestion-pipeline/src/types/mod.rs +++ b/crates/ingestion-pipeline/src/types/mod.rs @@ -10,16 +10,19 @@ use common::{ error::AppError, storage::types::{ file_info::FileInfo, ingestion_payload::IngestionPayload, text_content::TextContent, + system_settings::SystemSettings, }, }; use reqwest; use scraper::{Html, Selector}; use std::fmt::Write; use tiktoken_rs::{o200k_base, CoreBPE}; +use common::storage::db::SurrealDbClient; pub async fn to_text_content( ingestion_payload: IngestionPayload, openai_client: &Arc>, + db_client: &Arc, ) -> Result { match ingestion_payload { IngestionPayload::Url { @@ -28,7 +31,7 @@ pub async fn to_text_content( category, user_id, } => { - let text = fetch_text_from_url(&url, openai_client).await?; + let text = fetch_text_from_url(&url, openai_client, db_client).await?; Ok(TextContent::new( text, instructions.into(), @@ -74,6 +77,7 @@ pub async fn to_text_content( async fn fetch_text_from_url( url: &str, openai_client: &Arc>, + db_client: &Arc, ) -> Result { // Use a client with timeouts and reuse let client = reqwest::ClientBuilder::new() @@ -119,12 +123,14 @@ async fn fetch_text_from_url( let content = structured_content .replace(|c: char| c.is_control(), " ") .replace(" ", " "); - process_web_content(content, openai_client).await + + process_web_content(content, openai_client, &db_client).await } pub async fn process_web_content( content: String, openai_client: &Arc>, + db_client: &Arc, ) -> Result { const MAX_TOKENS: usize = 122000; const SYSTEM_PROMPT: &str = r#" @@ -155,6 +161,7 @@ pub async fn process_web_content( "#; let bpe = o200k_base()?; + let settings = SystemSettings::get_current(db_client).await?; // Process content in chunks if needed let truncated_content = if bpe.encode_with_special_tokens(&content).len() > MAX_TOKENS { @@ -164,7 +171,7 @@ pub async fn process_web_content( }; let request = CreateChatCompletionRequestArgs::default() - .model("gpt-4o-mini") + .model(&settings.processing_model) .temperature(0.0) .max_tokens(16200u32) .messages([ @@ -174,13 +181,13 @@ pub async fn process_web_content( .build()?; let response = openai_client.chat().create(request).await?; - + + // Extract and return the content response .choices .first() - .and_then(|choice| choice.message.content.as_ref()) - .map(|content| content.to_owned()) - .ok_or(AppError::LLMParsing("No content in response".into())) + .and_then(|choice| choice.message.content.clone()) + .ok_or(AppError::LLMParsing("No content found in LLM response".into())) } fn truncate_content( diff --git a/crates/ingestion-pipeline/src/utils/llm_instructions.rs b/crates/ingestion-pipeline/src/utils/llm_instructions.rs index 4b6a020..602115a 100644 --- a/crates/ingestion-pipeline/src/utils/llm_instructions.rs +++ b/crates/ingestion-pipeline/src/utils/llm_instructions.rs @@ -1,81 +1,41 @@ -use serde_json::{json, Value}; +use common::storage::types::system_prompts::DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT; +use serde_json::json; -pub static INGRESS_ANALYSIS_SYSTEM_MESSAGE: &str = r#" - You are an AI assistant. You will receive a text content, along with user instructions 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 instructions 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: - - { - "knowledge_entities": [ - { - "key": "unique-key-1", - "name": "Entity Name", - "description": "A detailed description of the entity.", - "entity_type": "TypeOfEntity" - }, - // More entities... - ], - "relationships": [ - { - "type": "RelationshipType", - "source": "unique-key-1 or UUID from existing database", - "target": "unique-key-1 or UUID from existing database" - }, - // More relationships... - ] - } - - Guidelines: - 1. Do NOT generate any IDs or UUIDs. Use a unique `key` for each knowledge entity. - 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" - 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 INGRESS_ANALYSIS_SYSTEM_MESSAGE: &str = DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT; -pub fn get_ingress_analysis_schema() -> Value { +pub fn get_ingress_analysis_schema() -> serde_json::Value { json!({ - "type": "object", - "properties": { - "knowledge_entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { "type": "string" }, - "name": { "type": "string" }, - "description": { "type": "string" }, - "entity_type": { - "type": "string", - "enum": ["idea", "project", "document", "page", "textsnippet"] - } + "type": "object", + "properties": { + "knowledge_entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "name": { "type": "string" }, + "description": { "type": "string" }, + "entity_type": { "type": "string" } + }, + "required": ["key", "name", "description", "entity_type"], + "additionalProperties": false + } }, - "required": ["key", "name", "description", "entity_type"], - "additionalProperties": false - } + "relationships": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "source": { "type": "string" }, + "target": { "type": "string" } + }, + "required": ["type", "source", "target"], + "additionalProperties": false + } + } }, - "relationships": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["RelatedTo", "RelevantTo", "SimilarTo"] - }, - "source": { "type": "string" }, - "target": { "type": "string" } - }, - "required": ["type", "source", "target"], - "additionalProperties": false - } - } - }, - "required": ["knowledge_entities", "relationships"], - "additionalProperties": false + "required": ["knowledge_entities", "relationships"], + "additionalProperties": false }) } diff --git a/templates/admin/edit_ingestion_prompt_modal.html b/templates/admin/edit_ingestion_prompt_modal.html new file mode 100644 index 0000000..a7f3e9c --- /dev/null +++ b/templates/admin/edit_ingestion_prompt_modal.html @@ -0,0 +1,38 @@ +{% extends "modal_base.html" %} + +{% block form_attributes %} +hx-patch="/update-ingestion-prompt" +hx-target="#system_prompt_section" +hx-swap="outerHTML" +{% endblock %} + +{% block modal_content %} +

Edit Ingestion Prompt

+ +
+ +

System prompt used for content processing and ingestion

+
+{% endblock %} + +{% block primary_actions %} + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/edit_query_prompt_modal.html b/templates/admin/edit_query_prompt_modal.html new file mode 100644 index 0000000..20a97ac --- /dev/null +++ b/templates/admin/edit_query_prompt_modal.html @@ -0,0 +1,38 @@ +{% extends "modal_base.html" %} + +{% block form_attributes %} +hx-patch="/update-query-prompt" +hx-target="#system_prompt_section" +hx-swap="outerHTML" +{% endblock %} + +{% block modal_content %} +

Edit System Prompt

+ +
+ +

System prompt used for answering user queries

+
+{% endblock %} + +{% block primary_actions %} + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/auth/admin_panel.html b/templates/auth/admin_panel.html index 9d74b0a..fd915b4 100644 --- a/templates/auth/admin_panel.html +++ b/templates/auth/admin_panel.html @@ -23,7 +23,6 @@ -
@@ -39,6 +38,61 @@
+ +
+ AI Models + {% block model_settings_form %} +
+
+ + +

Model used for answering user queries

+
+ +
+ + +

Model used for content processing and ingestion

+
+ + +
+ {% endblock %} +
+ + {% block system_prompt_section %} +
+
+ System Prompts +
+ + +
+
+
+ {% endblock %}
{% endblock %} \ No newline at end of file diff --git a/templates/documentation/get_started.html b/templates/documentation/get_started.html index 574ea07..1effca0 100644 --- a/templates/documentation/get_started.html +++ b/templates/documentation/get_started.html @@ -16,9 +16,6 @@
   
     OPENAI_API_KEY: your_api_key
-    SMTP_EMAIL_RELAYER: your_email_relayer
-    SMTP_USERNAME: your_smtp_username
-    SMTP_PASSWORD: your_smtp_password
     DB_ADDRESS: your_db_address
     DB_USER: your_db_user
     DB_PASSWORD: your_db_password
diff --git a/todo.md b/todo.md
index f576345..e55034d 100644
--- a/todo.md
+++ b/todo.md
@@ -1,8 +1,13 @@
 \[\] archive ingressed webpage
+\[\] openai api key in config
+\[\] option to set models, query and processing
+\[\] template customization?
 \[\] configs primarily get envs
 \[\] filtering on categories
 \[\] three js graph explorer
 \[\] three js vector explorer
+\[\] integrate templates in release build
+\[\] integrate assets folder in release build
 \[x\] add user_id to ingress objects
 \[x\] admin controls re registration
 \[x\] chat functionality