wip: node relationships and improved prompt

This commit is contained in:
Per Stark
2025-02-01 17:56:32 +01:00
parent bdbb23d839
commit 33380b3bcf
10 changed files with 121 additions and 35 deletions

View File

@@ -33,6 +33,7 @@ use zettle_db::{
gdpr::{accept_gdpr, deny_gdpr},
index::{delete_job, delete_text_content, index_handler},
ingress_form::{hide_ingress_form, process_ingress_form, show_ingress_form},
knowledge::entities::show_knowledge_page,
privacy_policy::show_privacy_policy,
search_result::search_result_handler,
signin::{authenticate_user, show_signin_form},
@@ -170,6 +171,7 @@ fn html_routes(
.route("/hide-ingress-form", get(hide_ingress_form))
.route("/text-content/:id", delete(delete_text_content))
.route("/jobs/:job_id", delete(delete_job))
.route("/knowledge", get(show_knowledge_page))
.route("/account", get(show_account_page))
.route("/admin", get(show_admin_panel))
.route("/toggle-registrations", patch(toggle_registration_status))

View File

@@ -1,7 +1,7 @@
use serde_json::{json, Value};
pub static INGRESS_ANALYSIS_SYSTEM_MESSAGE: &str = r#"
You are an expert document analyzer. You will receive a document's 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!
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:

View File

@@ -66,7 +66,7 @@ impl LLMGraphAnalysisResult {
.await?;
// Process relationships
let relationships = self.process_relationships(source_id, Arc::clone(&mapper))?;
let relationships = self.process_relationships(source_id, user_id, Arc::clone(&mapper))?;
Ok((entities, relationships))
}
@@ -117,6 +117,7 @@ impl LLMGraphAnalysisResult {
fn process_relationships(
&self,
source_id: &str,
user_id: &str,
mapper: Arc<Mutex<GraphMapper>>,
) -> Result<Vec<KnowledgeRelationship>, AppError> {
let mut mapper_guard = mapper
@@ -131,9 +132,9 @@ impl LLMGraphAnalysisResult {
Ok(KnowledgeRelationship::new(
source_db_id.to_string(),
target_db_id.to_string(),
user_id.to_string(),
source_id.to_string(),
rel.type_.clone(),
None,
))
})
.collect()

View File

@@ -26,16 +26,42 @@ use crate::{
},
};
page_data!(KnowledgeEntitiesData, "todo", {
gdpr_accepted: bool,
user: Option<User>,
latest_text_contents: Vec<TextContent>,
active_jobs: Vec<Job>
page_data!(KnowledgeBaseData, "knowledge/base.html", {
entities: Vec<KnowledgeEntity>,
relationships: Vec<KnowledgeRelationship>,
user: User
});
pub async fn index_handler(
pub async fn show_knowledge_page(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
session: Session<SessionSurrealPool<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
Ok("Hi".into_response())
// Early return if the user is not authenticated
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/signin").into_response()),
};
let entities = User::get_knowledge_entities(&user.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
info!("Got entities ok");
let relationships = User::get_knowledge_relationships(&user.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_template(
KnowledgeBaseData::template_name(),
KnowledgeBaseData {
entities,
relationships,
user,
},
state.templates,
)?;
Ok(output.into_response())
}

View File

@@ -0,0 +1 @@
pub mod entities;

View File

@@ -11,6 +11,7 @@ pub mod documentation;
pub mod gdpr;
pub mod index;
pub mod ingress_form;
pub mod knowledge;
pub mod privacy_policy;
pub mod search_result;
pub mod signin;

View File

@@ -1,46 +1,62 @@
use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
use crate::storage::types::file_info::deserialize_flexible_id;
use crate::{error::AppError, storage::db::SurrealDbClient};
use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use tracing::debug;
use tracing::{debug, info};
use uuid::Uuid;
stored_object!(KnowledgeRelationship, "relates_to", {
#[serde(rename = "in")]
in_: String,
out: String,
relationship_type: String,
source_id: String,
metadata: Option<serde_json::Value>
});
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RelationshipMetadata {
pub user_id: String,
pub source_id: String,
pub relationship_type: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct KnowledgeRelationship {
#[serde(deserialize_with = "deserialize_flexible_id")]
pub id: String,
#[serde(rename = "in", deserialize_with = "deserialize_flexible_id")]
pub in_: String,
#[serde(deserialize_with = "deserialize_flexible_id")]
pub out: String,
pub metadata: RelationshipMetadata,
}
impl KnowledgeRelationship {
pub fn new(
in_: String,
out: String,
user_id: String,
source_id: String,
relationship_type: String,
metadata: Option<serde_json::Value>,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
created_at: now,
updated_at: now,
in_,
out,
source_id,
relationship_type,
metadata,
metadata: RelationshipMetadata {
user_id,
source_id,
relationship_type,
},
}
}
pub async fn store_relationship(&self, db_client: &Surreal<Any>) -> Result<(), AppError> {
let query = format!(
"RELATE knowledge_entity:`{}` -> relates_to -> knowledge_entity:`{}`",
self.in_, self.out
r#"RELATE knowledge_entity:`{}`->relates_to:`{}`->knowledge_entity:`{}`
SET
metadata.user_id = '{}',
metadata.source_id = '{}',
metadata.relationship_type = '{}'"#,
self.in_,
self.id,
self.out,
self.metadata.user_id,
self.metadata.source_id,
self.metadata.relationship_type
);
let result = db_client.query(query).await?;
debug!("{:?}", result);
db_client.query(query).await?;
Ok(())
}
@@ -50,7 +66,7 @@ impl KnowledgeRelationship {
db_client: &SurrealDbClient,
) -> Result<(), AppError> {
let query = format!(
"DELETE knowledge_entity -> relates_to WHERE source_id = `{}`",
"DELETE knowledge_entity -> relates_to WHERE metadata.source_id = '{}'",
source_id
);

View File

@@ -5,10 +5,12 @@ use crate::{
};
use axum_session_auth::Authentication;
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use uuid::Uuid;
use super::{
knowledge_entity::KnowledgeEntity, system_settings::SystemSettings, text_content::TextContent,
knowledge_entity::KnowledgeEntity, knowledge_relationship::KnowledgeRelationship,
system_settings::SystemSettings, text_content::TextContent,
};
#[derive(Deserialize)]
@@ -199,7 +201,8 @@ impl User {
) -> Result<Vec<KnowledgeEntity>, AppError> {
let entities: Vec<KnowledgeEntity> = db
.client
.query("SELECT * FROM knowledge_entity WHERE user_id = $user_id")
.query("SELECT * FROM type::table($table) WHERE user_id = $user_id")
.bind(("table", KnowledgeEntity::table_name()))
.bind(("user_id", user_id.to_owned()))
.await?
.take(0)?;
@@ -207,6 +210,21 @@ impl User {
Ok(entities)
}
pub async fn get_knowledge_relationships(
user_id: &str,
db: &SurrealDbClient,
) -> Result<Vec<KnowledgeRelationship>, AppError> {
let relationships: Vec<KnowledgeRelationship> = db
.client
.query("SELECT * FROM type::table($table) WHERE metadata.user_id = $user_id")
.bind(("table", "relates_to"))
.bind(("user_id", user_id.to_owned()))
.await?
.take(0)?;
Ok(relationships)
}
pub async fn get_latest_text_contents(
user_id: &str,
db: &SurrealDbClient,

View File

@@ -1,5 +1,6 @@
<div class="p-4 shadow mt-4 rounded-box">
<div class="flex gap-4">
<button class="btn btn-primary" hx-get="/ingress-form" hx-swap="outerHTML">Add Content</button>
<a class="btn btn-secondary" href="/knowledge" hx-boost="true">View Knowledge</a>
</div>
</div>

View File

@@ -0,0 +1,20 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="flex justify-center grow mt-2 sm:mt-4 gap-6">
<div class="container">
<div class="p-4">
<h2>Entities</h2>
{% for entity in entities %}
<p>{{entity.description}} /p>
{% endfor %}
<h2 class="mt-10">Relationships</h2>
<p>{{relationships}}</p>
</div>
</div>
</main>
{% endblock %}