diff --git a/src/bin/server.rs b/src/bin/server.rs index 0685692..cc05c2d 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -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)) diff --git a/src/ingress/analysis/prompt.rs b/src/ingress/analysis/prompt.rs index a3c028f..4b6a020 100644 --- a/src/ingress/analysis/prompt.rs +++ b/src/ingress/analysis/prompt.rs @@ -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: diff --git a/src/ingress/analysis/types/llm_analysis_result.rs b/src/ingress/analysis/types/llm_analysis_result.rs index 6ec9442..ec62225 100644 --- a/src/ingress/analysis/types/llm_analysis_result.rs +++ b/src/ingress/analysis/types/llm_analysis_result.rs @@ -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>, ) -> Result, 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() diff --git a/src/server/routes/html/knowledge/entities.rs b/src/server/routes/html/knowledge/entities.rs index 75e9ac4..3477981 100644 --- a/src/server/routes/html/knowledge/entities.rs +++ b/src/server/routes/html/knowledge/entities.rs @@ -26,16 +26,42 @@ use crate::{ }, }; -page_data!(KnowledgeEntitiesData, "todo", { - gdpr_accepted: bool, - user: Option, - latest_text_contents: Vec, - active_jobs: Vec +page_data!(KnowledgeBaseData, "knowledge/base.html", { + entities: Vec, + relationships: Vec, + user: User }); -pub async fn index_handler( + +pub async fn show_knowledge_page( State(state): State, auth: AuthSession, Surreal>, session: Session>, ) -> Result { - 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()) } diff --git a/src/server/routes/html/knowledge/mod.rs b/src/server/routes/html/knowledge/mod.rs new file mode 100644 index 0000000..0b8f0b5 --- /dev/null +++ b/src/server/routes/html/knowledge/mod.rs @@ -0,0 +1 @@ +pub mod entities; diff --git a/src/server/routes/html/mod.rs b/src/server/routes/html/mod.rs index 6ec72a7..438382e 100644 --- a/src/server/routes/html/mod.rs +++ b/src/server/routes/html/mod.rs @@ -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; diff --git a/src/storage/types/knowledge_relationship.rs b/src/storage/types/knowledge_relationship.rs index eda215e..d58cc50 100644 --- a/src/storage/types/knowledge_relationship.rs +++ b/src/storage/types/knowledge_relationship.rs @@ -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 -}); +#[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, ) -> 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) -> 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 ); diff --git a/src/storage/types/user.rs b/src/storage/types/user.rs index 4fa1ffb..f75b93b 100644 --- a/src/storage/types/user.rs +++ b/src/storage/types/user.rs @@ -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, AppError> { let entities: Vec = 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, AppError> { + let relationships: Vec = 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, diff --git a/templates/index/signed_in/quick_actions.html b/templates/index/signed_in/quick_actions.html index 36f1504..6b36e47 100644 --- a/templates/index/signed_in/quick_actions.html +++ b/templates/index/signed_in/quick_actions.html @@ -1,5 +1,6 @@
+ View Knowledge
\ No newline at end of file diff --git a/templates/knowledge/base.html b/templates/knowledge/base.html new file mode 100644 index 0000000..7193266 --- /dev/null +++ b/templates/knowledge/base.html @@ -0,0 +1,20 @@ +{% extends 'body_base.html' %} +{% block main %} +
+
+
+ +

Entities

+ + {% for entity in entities %} +

{{entity.description}} /p> + {% endfor %} + + +

Relationships

+

{{relationships}}

+ +
+
+
+{% endblock %} \ No newline at end of file