feat: system prompt customisable

This commit is contained in:
Per Stark
2025-03-23 22:20:57 +01:00
parent 12cbf66d21
commit b956bdbe2b
19 changed files with 493 additions and 141 deletions
+29 -22
View File
@@ -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<LLMEnrichmentResult, AppError> {
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<CreateChatCompletionRequest, OpenAIError> {
) -> Result<CreateChatCompletionRequest, AppError> {
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<LLMEnrichmentResult, AppError> {
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::<LLMEnrichmentResult>(content).map_err(|e| {
AppError::LLMParsing(format!("Failed to parse LLM response into analysis: {}", e))
})
}
}
+1 -1
View File
@@ -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(_) => {
+14 -7
View File
@@ -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<async_openai::Client<async_openai::config::OpenAIConfig>>,
db_client: &Arc<SurrealDbClient>,
) -> Result<TextContent, AppError> {
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<async_openai::Client<async_openai::config::OpenAIConfig>>,
db_client: &Arc<SurrealDbClient>,
) -> Result<String, AppError> {
// 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<async_openai::Client<async_openai::config::OpenAIConfig>>,
db_client: &Arc<SurrealDbClient>,
) -> Result<String, AppError> {
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(
@@ -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
})
}