mirror of
https://github.com/perstarkse/minne.git
synced 2026-04-25 02:08:30 +02:00
refactor: html-router builder pattern and structure
This commit is contained in:
96
Cargo.lock
generated
96
Cargo.lock
generated
@@ -992,16 +992,6 @@ dependencies = [
|
|||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chumsky"
|
|
||||||
version = "0.9.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown 0.14.5",
|
|
||||||
"stacker",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ciborium"
|
name = "ciborium"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -1054,11 +1044,8 @@ dependencies = [
|
|||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"config",
|
"config",
|
||||||
"futures",
|
"futures",
|
||||||
"lettre",
|
|
||||||
"mime",
|
"mime",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"minijinja",
|
|
||||||
"minijinja-autoreload",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1494,22 +1481,6 @@ version = "1.13.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "email-encoding"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "email_address"
|
|
||||||
version = "0.2.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ena"
|
name = "ena"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@@ -1573,7 +1544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
|
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"nom 7.1.3",
|
"nom",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2027,17 +1998,6 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hostname"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"windows",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html-router"
|
name = "html-router"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2629,31 +2589,6 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lettre"
|
|
||||||
version = "0.11.14"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d476fe7a4a798f392ce34947aa7d53d981127e37523c5251da3c927f7fa901f"
|
|
||||||
dependencies = [
|
|
||||||
"base64 0.22.1",
|
|
||||||
"chumsky",
|
|
||||||
"email-encoding",
|
|
||||||
"email_address",
|
|
||||||
"fastrand",
|
|
||||||
"futures-util",
|
|
||||||
"hostname",
|
|
||||||
"httpdate",
|
|
||||||
"idna",
|
|
||||||
"mime",
|
|
||||||
"native-tls",
|
|
||||||
"nom 8.0.0",
|
|
||||||
"percent-encoding",
|
|
||||||
"quoted_printable",
|
|
||||||
"socket2",
|
|
||||||
"tokio",
|
|
||||||
"url",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lexicmp"
|
name = "lexicmp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3040,15 +2975,6 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "8.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonempty"
|
name = "nonempty"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3715,12 +3641,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quoted_printable"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radium"
|
name = "radium"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -3936,7 +3856,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-timer",
|
"futures-timer",
|
||||||
"mime",
|
"mime",
|
||||||
"nom 7.1.3",
|
"nom",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -4022,7 +3942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
|
checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"nom 7.1.3",
|
"nom",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5822,16 +5742,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows"
|
|
||||||
version = "0.52.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
|
||||||
dependencies = [
|
|
||||||
"windows-core",
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use serde_json::{json, Value};
|
|||||||
|
|
||||||
use crate::retrieve_entities;
|
use crate::retrieve_entities;
|
||||||
|
|
||||||
use super::answer_retrieval_helper::{get_query_response_schema, QUERY_SYSTEM_PROMPT};
|
use super::answer_retrieval_helper::get_query_response_schema;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Reference {
|
pub struct Reference {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use common::{
|
|||||||
use futures::future::{try_join, try_join_all};
|
use futures::future::{try_join, try_join_all};
|
||||||
use graph::{find_entities_by_relationship_by_id, find_entities_by_source_ids};
|
use graph::{find_entities_by_relationship_by_id, find_entities_by_source_ids};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tracing::info;
|
|
||||||
use vector::find_items_by_vector_similarity;
|
use vector::find_items_by_vector_similarity;
|
||||||
|
|
||||||
/// Performs a comprehensive knowledge entity retrieval using multiple search strategies
|
/// Performs a comprehensive knowledge entity retrieval using multiple search strategies
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use surrealdb::{engine::any::Any, Surreal};
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
|
|
||||||
use common::{error::AppError, utils::embedding::generate_embedding};
|
use common::{error::AppError, utils::embedding::generate_embedding};
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
/// Compares vectors and retrieves a number of items from the specified table.
|
/// Compares vectors and retrieves a number of items from the specified table.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,55 +1,16 @@
|
|||||||
pub mod html_state;
|
pub mod html_state;
|
||||||
mod middleware_analytics;
|
pub mod middlewares;
|
||||||
mod middleware_auth;
|
pub mod router_factory;
|
||||||
mod routes;
|
pub mod routes;
|
||||||
mod template_response;
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{extract::FromRef, Router};
|
||||||
extract::FromRef,
|
use axum_session::Session;
|
||||||
middleware::{from_fn_with_state, map_response_with_state},
|
use axum_session_auth::AuthSession;
|
||||||
routing::{delete, get, patch, post},
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use axum_session::{Session, SessionLayer};
|
|
||||||
use axum_session_auth::{AuthConfig, AuthSession, AuthSessionLayer};
|
|
||||||
use axum_session_surreal::SessionSurrealPool;
|
use axum_session_surreal::SessionSurrealPool;
|
||||||
use common::storage::types::user::User;
|
use common::storage::types::user::User;
|
||||||
use html_state::HtmlState;
|
use html_state::HtmlState;
|
||||||
use middleware_analytics::analytics_middleware;
|
use router_factory::RouterFactory;
|
||||||
use middleware_auth::require_auth;
|
|
||||||
use routes::{
|
|
||||||
account::{
|
|
||||||
change_password, delete_account, set_api_key, show_account_page, show_change_password,
|
|
||||||
update_timezone,
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
show_initialized_chat,
|
|
||||||
},
|
|
||||||
content::{patch_text_content, show_content_page, show_text_content_edit_form},
|
|
||||||
documentation::{
|
|
||||||
show_documentation_index, show_get_started, show_mobile_friendly, show_privacy_policy,
|
|
||||||
},
|
|
||||||
gdpr::{accept_gdpr, deny_gdpr},
|
|
||||||
index::{delete_job, delete_text_content, index_handler, show_active_jobs},
|
|
||||||
ingress_form::{hide_ingress_form, process_ingress_form, show_ingress_form},
|
|
||||||
knowledge::{
|
|
||||||
delete_knowledge_entity, delete_knowledge_relationship, patch_knowledge_entity,
|
|
||||||
save_knowledge_relationship, show_edit_knowledge_entity_form, show_knowledge_page,
|
|
||||||
},
|
|
||||||
search_result::search_result_handler,
|
|
||||||
signin::{authenticate_user, show_signin_form},
|
|
||||||
signout::sign_out_user,
|
|
||||||
signup::{process_signup_and_show_verification, show_signup_form},
|
|
||||||
};
|
|
||||||
use surrealdb::{engine::any::Any, Surreal};
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
use template_response::with_template_response;
|
|
||||||
use tower_http::services::ServeDir;
|
|
||||||
|
|
||||||
pub type AuthSessionType = AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>;
|
pub type AuthSessionType = AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>;
|
||||||
pub type SessionType = Session<SessionSurrealPool<Any>>;
|
pub type SessionType = Session<SessionSurrealPool<Any>>;
|
||||||
@@ -60,89 +21,17 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
HtmlState: FromRef<S>,
|
HtmlState: FromRef<S>,
|
||||||
{
|
{
|
||||||
// Public routes - no auth required
|
RouterFactory::new(app_state)
|
||||||
let public_routes = Router::new()
|
.add_public_routes(routes::index::public_router())
|
||||||
.route("/", get(index_handler))
|
.add_public_routes(routes::auth::router())
|
||||||
.route("/gdpr/accept", post(accept_gdpr))
|
.with_public_assets("/assets", "assets/")
|
||||||
.route("/gdpr/deny", post(deny_gdpr))
|
.add_protected_routes(routes::index::protected_router())
|
||||||
.route("/signout", get(sign_out_user))
|
.add_protected_routes(routes::search::router())
|
||||||
.route("/signin", get(show_signin_form).post(authenticate_user))
|
.add_protected_routes(routes::account::router())
|
||||||
.route(
|
.add_protected_routes(routes::admin::router())
|
||||||
"/signup",
|
.add_protected_routes(routes::chat::router())
|
||||||
get(show_signup_form).post(process_signup_and_show_verification),
|
.add_protected_routes(routes::content::router())
|
||||||
)
|
.add_protected_routes(routes::knowledge::router())
|
||||||
.route("/documentation", get(show_documentation_index))
|
.add_protected_routes(routes::ingestion::router())
|
||||||
.route("/documentation/privacy-policy", get(show_privacy_policy))
|
.build()
|
||||||
.route("/documentation/get-started", get(show_get_started))
|
|
||||||
.route("/documentation/mobile-friendly", get(show_mobile_friendly))
|
|
||||||
.nest_service("/assets", ServeDir::new("assets/"));
|
|
||||||
|
|
||||||
// Protected routes - auth required
|
|
||||||
let protected_routes = Router::new()
|
|
||||||
.route("/chat", get(show_chat_base).post(new_chat_user_message))
|
|
||||||
.route("/initialized-chat", post(show_initialized_chat))
|
|
||||||
.route("/chat/:id", get(show_existing_chat).post(new_user_message))
|
|
||||||
.route(
|
|
||||||
"/ingress-form",
|
|
||||||
get(show_ingress_form).post(process_ingress_form),
|
|
||||||
)
|
|
||||||
.route("/hide-ingress-form", get(hide_ingress_form))
|
|
||||||
.route("/text-content/:id", delete(delete_text_content))
|
|
||||||
.route("/jobs/:job_id", delete(delete_job))
|
|
||||||
.route("/active-jobs", get(show_active_jobs))
|
|
||||||
.route("/content", get(show_content_page))
|
|
||||||
.route(
|
|
||||||
"/content/:id",
|
|
||||||
get(show_text_content_edit_form).patch(patch_text_content),
|
|
||||||
)
|
|
||||||
.route("/search", get(search_result_handler))
|
|
||||||
.route("/chat/response-stream", get(get_response_stream))
|
|
||||||
.route("/knowledge/:id", get(show_reference_tooltip))
|
|
||||||
.route("/knowledge", get(show_knowledge_page))
|
|
||||||
.route(
|
|
||||||
"/knowledge-entity/:id",
|
|
||||||
get(show_edit_knowledge_entity_form)
|
|
||||||
.delete(delete_knowledge_entity)
|
|
||||||
.patch(patch_knowledge_entity),
|
|
||||||
)
|
|
||||||
.route("/knowledge-relationship", post(save_knowledge_relationship))
|
|
||||||
.route(
|
|
||||||
"/knowledge-relationship/:id",
|
|
||||||
delete(delete_knowledge_relationship),
|
|
||||||
)
|
|
||||||
// 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(
|
|
||||||
"/change-password",
|
|
||||||
get(show_change_password).patch(change_password),
|
|
||||||
)
|
|
||||||
.route("/delete-account", delete(delete_account))
|
|
||||||
.route_layer(from_fn_with_state(app_state.clone(), require_auth));
|
|
||||||
|
|
||||||
// Combine routes and add common middleware
|
|
||||||
Router::new()
|
|
||||||
.merge(public_routes)
|
|
||||||
.merge(protected_routes)
|
|
||||||
.layer(from_fn_with_state(app_state.clone(), analytics_middleware))
|
|
||||||
.layer(map_response_with_state(
|
|
||||||
app_state.clone(),
|
|
||||||
with_template_response,
|
|
||||||
))
|
|
||||||
.layer(
|
|
||||||
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
|
|
||||||
app_state.db.client.clone(),
|
|
||||||
))
|
|
||||||
.with_config(AuthConfig::<String>::default()),
|
|
||||||
)
|
|
||||||
.layer(SessionLayer::new((*app_state.session_store).clone()))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use common::storage::types::user::User;
|
use common::storage::types::user::User;
|
||||||
|
|
||||||
use crate::{template_response::TemplateResponse, AuthSessionType};
|
use crate::AuthSessionType;
|
||||||
|
|
||||||
|
use super::response_middleware::TemplateResponse;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RequireUser(pub User);
|
pub struct RequireUser(pub User);
|
||||||
3
crates/html-router/src/middlewares/mod.rs
Normal file
3
crates/html-router/src/middlewares/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod analytics_middleware;
|
||||||
|
pub mod auth_middleware;
|
||||||
|
pub mod response_middleware;
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{StatusCode, Uri},
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
Extension,
|
Extension,
|
||||||
};
|
};
|
||||||
use axum_htmx::HxRedirect;
|
|
||||||
use common::error::AppError;
|
use common::error::AppError;
|
||||||
use minijinja::{context, Value};
|
use minijinja::{context, Value};
|
||||||
use minijinja_autoreload::AutoReloader;
|
use minijinja_autoreload::AutoReloader;
|
||||||
159
crates/html-router/src/router_factory.rs
Normal file
159
crates/html-router/src/router_factory.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::FromRef,
|
||||||
|
middleware::{from_fn_with_state, map_response_with_state},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use axum_session::SessionLayer;
|
||||||
|
use axum_session_auth::{AuthConfig, AuthSessionLayer};
|
||||||
|
use axum_session_surreal::SessionSurrealPool;
|
||||||
|
use common::storage::types::user::User;
|
||||||
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
html_state::HtmlState,
|
||||||
|
middlewares::{
|
||||||
|
analytics_middleware::analytics_middleware, auth_middleware::require_auth,
|
||||||
|
response_middleware::with_template_response,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RouterFactory<S> {
|
||||||
|
app_state: HtmlState,
|
||||||
|
public_routers: Vec<Router<S>>,
|
||||||
|
protected_routers: Vec<Router<S>>,
|
||||||
|
nested_routes: Vec<(String, Router<S>)>,
|
||||||
|
nested_protected_routes: Vec<(String, Router<S>)>,
|
||||||
|
custom_middleware: Vec<Box<dyn FnOnce(Router<S>) -> Router<S> + Send>>,
|
||||||
|
public_assets_config: Option<AssetsConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AssetsConfig {
|
||||||
|
path: String, // URL path for assets
|
||||||
|
directory: String, // Directory on disk
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> RouterFactory<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
pub fn new(app_state: &HtmlState) -> Self {
|
||||||
|
Self {
|
||||||
|
app_state: app_state.to_owned(),
|
||||||
|
public_routers: Vec::new(),
|
||||||
|
protected_routers: Vec::new(),
|
||||||
|
nested_routes: Vec::new(),
|
||||||
|
nested_protected_routes: Vec::new(),
|
||||||
|
custom_middleware: Vec::new(),
|
||||||
|
public_assets_config: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a serving of assets
|
||||||
|
pub fn with_public_assets(mut self, path: &str, directory: &str) -> Self {
|
||||||
|
self.public_assets_config = Some(AssetsConfig {
|
||||||
|
path: path.to_string(),
|
||||||
|
directory: directory.to_string(),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a public router that will be merged at the root level
|
||||||
|
pub fn add_public_routes(mut self, routes: Router<S>) -> Self {
|
||||||
|
self.public_routers.push(routes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a protected router that will be merged at the root level
|
||||||
|
pub fn add_protected_routes(mut self, routes: Router<S>) -> Self {
|
||||||
|
self.protected_routers.push(routes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nest a public router under a path prefix
|
||||||
|
pub fn nest_public_routes(mut self, path: &str, routes: Router<S>) -> Self {
|
||||||
|
self.nested_routes.push((path.to_string(), routes));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nest a protected router under a path prefix
|
||||||
|
pub fn nest_protected_routes(mut self, path: &str, routes: Router<S>) -> Self {
|
||||||
|
self.nested_protected_routes
|
||||||
|
.push((path.to_string(), routes));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom middleware to be applied before the standard ones
|
||||||
|
pub fn with_middleware<F>(mut self, middleware_fn: F) -> Self
|
||||||
|
where
|
||||||
|
F: FnOnce(Router<S>) -> Router<S> + Send + 'static,
|
||||||
|
{
|
||||||
|
self.custom_middleware.push(Box::new(middleware_fn));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> Router<S> {
|
||||||
|
// Start with an empty router
|
||||||
|
let mut public_router = Router::new();
|
||||||
|
|
||||||
|
// Merge all public routers
|
||||||
|
for router in self.public_routers {
|
||||||
|
public_router = public_router.merge(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add nested public routes
|
||||||
|
for (path, router) in self.nested_routes {
|
||||||
|
public_router = public_router.nest(&path, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add public assets to public router
|
||||||
|
if let Some(assets) = self.public_assets_config {
|
||||||
|
public_router =
|
||||||
|
public_router.nest_service(&assets.path, ServeDir::new(assets.directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with an empty protected router
|
||||||
|
let mut protected_router = Router::new();
|
||||||
|
|
||||||
|
// Merge all protected routers
|
||||||
|
for router in self.protected_routers {
|
||||||
|
protected_router = protected_router.merge(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add nested protected routes
|
||||||
|
for (path, router) in self.nested_protected_routes {
|
||||||
|
protected_router = protected_router.nest(&path, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply auth middleware to all protected routes
|
||||||
|
let protected_router =
|
||||||
|
protected_router.route_layer(from_fn_with_state(self.app_state.clone(), require_auth));
|
||||||
|
|
||||||
|
// Combine public and protected routes
|
||||||
|
let mut router = Router::new().merge(public_router).merge(protected_router);
|
||||||
|
|
||||||
|
// Apply custom middleware in order they were added
|
||||||
|
for middleware_fn in self.custom_middleware {
|
||||||
|
router = middleware_fn(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply common middleware
|
||||||
|
router
|
||||||
|
.layer(from_fn_with_state(
|
||||||
|
self.app_state.clone(),
|
||||||
|
analytics_middleware,
|
||||||
|
))
|
||||||
|
.layer(map_response_with_state(
|
||||||
|
self.app_state.clone(),
|
||||||
|
with_template_response,
|
||||||
|
))
|
||||||
|
.layer(
|
||||||
|
AuthSessionLayer::<User, String, SessionSurrealPool<Any>, Surreal<Any>>::new(Some(
|
||||||
|
self.app_state.db.client.clone(),
|
||||||
|
))
|
||||||
|
.with_config(AuthConfig::<String>::default()),
|
||||||
|
)
|
||||||
|
.layer(SessionLayer::new((*self.app_state.session_store).clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ use chrono_tz::TZ_VARIANTS;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middleware_auth::RequireUser,
|
middlewares::{
|
||||||
template_response::{HtmlError, TemplateResponse},
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
AuthSessionType,
|
AuthSessionType,
|
||||||
};
|
};
|
||||||
use common::storage::types::user::User;
|
use common::storage::types::user::User;
|
||||||
24
crates/html-router/src/routes/account/mod.rs
Normal file
24
crates/html-router/src/routes/account/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
mod handlers;
|
||||||
|
use axum::{
|
||||||
|
extract::FromRef,
|
||||||
|
routing::{delete, get, patch, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::html_state::HtmlState;
|
||||||
|
|
||||||
|
pub fn router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new()
|
||||||
|
.route("/account", get(handlers::show_account_page))
|
||||||
|
.route("/set-api-key", post(handlers::set_api_key))
|
||||||
|
.route("/update-timezone", patch(handlers::update_timezone))
|
||||||
|
.route(
|
||||||
|
"/change-password",
|
||||||
|
get(handlers::show_change_password).patch(handlers::change_password),
|
||||||
|
)
|
||||||
|
.route("/delete-account", delete(handlers::delete_account))
|
||||||
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
use axum::{extract::State, response::IntoResponse, Form};
|
use axum::{extract::State, response::IntoResponse, Form};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
middleware_auth::RequireUser,
|
|
||||||
template_response::{HtmlError, TemplateResponse},
|
|
||||||
};
|
|
||||||
use common::storage::types::{
|
use common::storage::types::{
|
||||||
analytics::Analytics,
|
analytics::Analytics,
|
||||||
system_prompts::{DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT},
|
system_prompts::{DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT},
|
||||||
@@ -12,7 +8,13 @@ use common::storage::types::{
|
|||||||
user::User,
|
user::User,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::html_state::HtmlState;
|
use crate::{
|
||||||
|
html_state::HtmlState,
|
||||||
|
middlewares::{
|
||||||
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct AdminPanelData {
|
pub struct AdminPanelData {
|
||||||
27
crates/html-router/src/routes/admin/mod.rs
Normal file
27
crates/html-router/src/routes/admin/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
mod handlers;
|
||||||
|
use axum::{
|
||||||
|
extract::FromRef,
|
||||||
|
routing::{get, patch},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use handlers::{
|
||||||
|
patch_ingestion_prompt, patch_query_prompt, show_admin_panel, show_edit_ingestion_prompt,
|
||||||
|
show_edit_system_prompt, toggle_registration_status, update_model_settings,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::html_state::HtmlState;
|
||||||
|
|
||||||
|
pub fn router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new()
|
||||||
|
.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))
|
||||||
|
}
|
||||||
24
crates/html-router/src/routes/auth/mod.rs
Normal file
24
crates/html-router/src/routes/auth/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
pub mod signin;
|
||||||
|
pub mod signout;
|
||||||
|
pub mod signup;
|
||||||
|
|
||||||
|
use axum::{extract::FromRef, routing::get, Router};
|
||||||
|
use signin::{authenticate_user, show_signin_form};
|
||||||
|
use signout::sign_out_user;
|
||||||
|
use signup::{process_signup_and_show_verification, show_signup_form};
|
||||||
|
|
||||||
|
use crate::html_state::HtmlState;
|
||||||
|
|
||||||
|
pub fn router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new()
|
||||||
|
.route("/signout", get(sign_out_user))
|
||||||
|
.route("/signin", get(show_signin_form).post(authenticate_user))
|
||||||
|
.route(
|
||||||
|
"/signup",
|
||||||
|
get(show_signup_form).post(process_signup_and_show_verification),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
html_state::HtmlState,
|
html_state::HtmlState,
|
||||||
template_response::{HtmlError, TemplateResponse},
|
middlewares::response_middleware::{HtmlError, TemplateResponse},
|
||||||
AuthSessionType,
|
AuthSessionType,
|
||||||
};
|
};
|
||||||
use common::storage::types::user::User;
|
use common::storage::types::user::User;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
template_response::{HtmlError, TemplateResponse},
|
middlewares::response_middleware::{HtmlError, TemplateResponse},
|
||||||
AuthSessionType,
|
AuthSessionType,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ use common::storage::types::user::User;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
html_state::HtmlState,
|
html_state::HtmlState,
|
||||||
template_response::{HtmlError, TemplateResponse},
|
middlewares::response_middleware::{HtmlError, TemplateResponse},
|
||||||
AuthSessionType,
|
AuthSessionType,
|
||||||
};
|
};
|
||||||
|
|
||||||
226
crates/html-router/src/routes/chat/chat_handlers.rs
Normal file
226
crates/html-router/src/routes/chat/chat_handlers.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::HeaderValue,
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
Form,
|
||||||
|
};
|
||||||
|
use axum_session_auth::AuthSession;
|
||||||
|
use axum_session_surreal::SessionSurrealPool;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
|
|
||||||
|
use common::{
|
||||||
|
error::AppError,
|
||||||
|
storage::types::{
|
||||||
|
conversation::Conversation,
|
||||||
|
message::{Message, MessageRole},
|
||||||
|
user::User,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
html_state::HtmlState,
|
||||||
|
middlewares::{
|
||||||
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct ChatStartParams {
|
||||||
|
user_query: String,
|
||||||
|
llm_response: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_references")]
|
||||||
|
references: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom deserializer function
|
||||||
|
fn deserialize_references<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
serde_json::from_str(&s).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ChatPageData {
|
||||||
|
user: User,
|
||||||
|
history: Vec<Message>,
|
||||||
|
conversation: Option<Conversation>,
|
||||||
|
conversation_archive: Vec<Conversation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_initialized_chat(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Form(form): Form<ChatStartParams>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let conversation = Conversation::new(user.id.clone(), "Test".to_owned());
|
||||||
|
|
||||||
|
let user_message = Message::new(
|
||||||
|
conversation.id.to_string(),
|
||||||
|
MessageRole::User,
|
||||||
|
form.user_query,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let ai_message = Message::new(
|
||||||
|
conversation.id.to_string(),
|
||||||
|
MessageRole::AI,
|
||||||
|
form.llm_response,
|
||||||
|
Some(form.references),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.db.store_item(conversation.clone()).await?;
|
||||||
|
state.db.store_item(ai_message.clone()).await?;
|
||||||
|
state.db.store_item(user_message.clone()).await?;
|
||||||
|
|
||||||
|
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let messages = vec![user_message, ai_message];
|
||||||
|
|
||||||
|
let mut response = TemplateResponse::new_template(
|
||||||
|
"chat/base.html",
|
||||||
|
ChatPageData {
|
||||||
|
history: messages,
|
||||||
|
user,
|
||||||
|
conversation_archive,
|
||||||
|
conversation: Some(conversation.clone()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
|
||||||
|
response.headers_mut().insert(
|
||||||
|
"HX-Push",
|
||||||
|
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_chat_base(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"chat/base.html",
|
||||||
|
ChatPageData {
|
||||||
|
history: vec![],
|
||||||
|
user,
|
||||||
|
conversation_archive,
|
||||||
|
conversation: None,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NewMessageForm {
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_existing_chat(
|
||||||
|
Path(conversation_id): Path<String>,
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let (conversation, messages) =
|
||||||
|
Conversation::get_complete_conversation(conversation_id.as_str(), &user.id, &state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"chat/base.html",
|
||||||
|
ChatPageData {
|
||||||
|
history: messages,
|
||||||
|
user,
|
||||||
|
conversation: Some(conversation.clone()),
|
||||||
|
conversation_archive,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_user_message(
|
||||||
|
Path(conversation_id): Path<String>,
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Form(form): Form<NewMessageForm>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let conversation: Conversation = state
|
||||||
|
.db
|
||||||
|
.get_item(&conversation_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| AppError::NotFound("Conversation was not found".into()))?;
|
||||||
|
|
||||||
|
if conversation.user_id != user.id {
|
||||||
|
return Ok(TemplateResponse::unauthorized().into_response());
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_message = Message::new(conversation_id, MessageRole::User, form.content, None);
|
||||||
|
|
||||||
|
state.db.store_item(user_message.clone()).await?;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SSEResponseInitData {
|
||||||
|
user_message: Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut response = TemplateResponse::new_template(
|
||||||
|
"chat/streaming_response.html",
|
||||||
|
SSEResponseInitData { user_message },
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
|
||||||
|
response.headers_mut().insert(
|
||||||
|
"HX-Push",
|
||||||
|
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_chat_user_message(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
||||||
|
Form(form): Form<NewMessageForm>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let user = match auth.current_user {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Ok(Redirect::to("/").into_response()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let conversation = Conversation::new(user.id, "New chat".to_string());
|
||||||
|
let user_message = Message::new(
|
||||||
|
conversation.id.clone(),
|
||||||
|
MessageRole::User,
|
||||||
|
form.content,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
state.db.store_item(conversation.clone()).await?;
|
||||||
|
state.db.store_item(user_message.clone()).await?;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SSEResponseInitData {
|
||||||
|
user_message: Message,
|
||||||
|
conversation: Conversation,
|
||||||
|
}
|
||||||
|
let mut response = TemplateResponse::new_template(
|
||||||
|
"chat/new_chat_first_response.html",
|
||||||
|
SSEResponseInitData {
|
||||||
|
user_message,
|
||||||
|
conversation: conversation.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
|
||||||
|
response.headers_mut().insert(
|
||||||
|
"HX-Push",
|
||||||
|
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
use surrealdb::{engine::any::Any, Surreal};
|
use surrealdb::{engine::any::Any, Surreal};
|
||||||
use tokio::sync::{mpsc::channel, Mutex};
|
use tokio::sync::{mpsc::channel, Mutex};
|
||||||
use tracing::{error, info};
|
use tracing::{error, debug};
|
||||||
|
|
||||||
use common::storage::{
|
use common::storage::{
|
||||||
db::SurrealDbClient,
|
db::SurrealDbClient,
|
||||||
@@ -195,7 +195,7 @@ pub async fn get_response_stream(
|
|||||||
let _ = tx_final.send(ai_message.clone()).await;
|
let _ = tx_final.send(ai_message.clone()).await;
|
||||||
|
|
||||||
match db_client.store_item(ai_message).await {
|
match db_client.store_item(ai_message).await {
|
||||||
Ok(_) => info!("Successfully stored AI message with references"),
|
Ok(_) => debug!("Successfully stored AI message with references"),
|
||||||
Err(e) => error!("Failed to store AI message: {:?}", e),
|
Err(e) => error!("Failed to store AI message: {:?}", e),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -304,7 +304,6 @@ pub async fn get_response_stream(
|
|||||||
.data("Stream complete"))
|
.data("Stream complete"))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
info!("OpenAI streaming started");
|
|
||||||
Sse::new(event_stream.boxed()).keep_alive(
|
Sse::new(event_stream.boxed()).keep_alive(
|
||||||
KeepAlive::new()
|
KeepAlive::new()
|
||||||
.interval(Duration::from_secs(15))
|
.interval(Duration::from_secs(15))
|
||||||
@@ -312,7 +311,6 @@ pub async fn get_response_stream(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace JsonParseState with StreamParserState
|
|
||||||
struct StreamParserState {
|
struct StreamParserState {
|
||||||
parser: JsonStreamParser,
|
parser: JsonStreamParser,
|
||||||
last_answer_content: String,
|
last_answer_content: String,
|
||||||
|
|||||||
@@ -1,227 +1,30 @@
|
|||||||
pub mod message_response_stream;
|
mod chat_handlers;
|
||||||
pub mod references;
|
mod message_response_stream;
|
||||||
|
mod references;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::FromRef,
|
||||||
http::HeaderValue,
|
routing::{get, post},
|
||||||
response::{IntoResponse, Redirect},
|
Router,
|
||||||
Form,
|
|
||||||
};
|
};
|
||||||
use axum_session_auth::AuthSession;
|
use chat_handlers::{
|
||||||
use axum_session_surreal::SessionSurrealPool;
|
new_chat_user_message, new_user_message, show_chat_base, show_existing_chat,
|
||||||
use serde::{Deserialize, Serialize};
|
show_initialized_chat,
|
||||||
use surrealdb::{engine::any::Any, Surreal};
|
|
||||||
|
|
||||||
use common::{
|
|
||||||
error::AppError,
|
|
||||||
storage::types::{
|
|
||||||
conversation::Conversation,
|
|
||||||
message::{Message, MessageRole},
|
|
||||||
user::User,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
use message_response_stream::get_response_stream;
|
||||||
|
use references::show_reference_tooltip;
|
||||||
|
|
||||||
use crate::{
|
use crate::html_state::HtmlState;
|
||||||
html_state::HtmlState,
|
|
||||||
middleware_auth::RequireUser,
|
|
||||||
template_response::{HtmlError, TemplateResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
pub fn router<S>() -> Router<S>
|
||||||
pub struct ChatStartParams {
|
|
||||||
user_query: String,
|
|
||||||
llm_response: String,
|
|
||||||
#[serde(deserialize_with = "deserialize_references")]
|
|
||||||
references: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom deserializer function
|
|
||||||
fn deserialize_references<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
|
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
{
|
{
|
||||||
let s = String::deserialize(deserializer)?;
|
Router::new()
|
||||||
serde_json::from_str(&s).map_err(serde::de::Error::custom)
|
.route("/chat", get(show_chat_base).post(new_chat_user_message))
|
||||||
}
|
.route("/chat/:id", get(show_existing_chat).post(new_user_message))
|
||||||
|
.route("/initialized-chat", post(show_initialized_chat))
|
||||||
#[derive(Serialize)]
|
.route("/chat/response-stream", get(get_response_stream))
|
||||||
pub struct ChatPageData {
|
.route("/chat/reference/:id", get(show_reference_tooltip))
|
||||||
user: User,
|
|
||||||
history: Vec<Message>,
|
|
||||||
conversation: Option<Conversation>,
|
|
||||||
conversation_archive: Vec<Conversation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_initialized_chat(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Form(form): Form<ChatStartParams>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
let conversation = Conversation::new(user.id.clone(), "Test".to_owned());
|
|
||||||
|
|
||||||
let user_message = Message::new(
|
|
||||||
conversation.id.to_string(),
|
|
||||||
MessageRole::User,
|
|
||||||
form.user_query,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
let ai_message = Message::new(
|
|
||||||
conversation.id.to_string(),
|
|
||||||
MessageRole::AI,
|
|
||||||
form.llm_response,
|
|
||||||
Some(form.references),
|
|
||||||
);
|
|
||||||
|
|
||||||
state.db.store_item(conversation.clone()).await?;
|
|
||||||
state.db.store_item(ai_message.clone()).await?;
|
|
||||||
state.db.store_item(user_message.clone()).await?;
|
|
||||||
|
|
||||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let messages = vec![user_message, ai_message];
|
|
||||||
|
|
||||||
let mut response = TemplateResponse::new_template(
|
|
||||||
"chat/base.html",
|
|
||||||
ChatPageData {
|
|
||||||
history: messages,
|
|
||||||
user,
|
|
||||||
conversation_archive,
|
|
||||||
conversation: Some(conversation.clone()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"HX-Push",
|
|
||||||
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_chat_base(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"chat/base.html",
|
|
||||||
ChatPageData {
|
|
||||||
history: vec![],
|
|
||||||
user,
|
|
||||||
conversation_archive,
|
|
||||||
conversation: None,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct NewMessageForm {
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_existing_chat(
|
|
||||||
Path(conversation_id): Path<String>,
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let (conversation, messages) =
|
|
||||||
Conversation::get_complete_conversation(conversation_id.as_str(), &user.id, &state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"chat/base.html",
|
|
||||||
ChatPageData {
|
|
||||||
history: messages,
|
|
||||||
user,
|
|
||||||
conversation: Some(conversation.clone()),
|
|
||||||
conversation_archive,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_user_message(
|
|
||||||
Path(conversation_id): Path<String>,
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Form(form): Form<NewMessageForm>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
let conversation: Conversation = state
|
|
||||||
.db
|
|
||||||
.get_item(&conversation_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| AppError::NotFound("Conversation was not found".into()))?;
|
|
||||||
|
|
||||||
if conversation.user_id != user.id {
|
|
||||||
return Ok(TemplateResponse::unauthorized().into_response());
|
|
||||||
};
|
|
||||||
|
|
||||||
let user_message = Message::new(conversation_id, MessageRole::User, form.content, None);
|
|
||||||
|
|
||||||
state.db.store_item(user_message.clone()).await?;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SSEResponseInitData {
|
|
||||||
user_message: Message,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut response = TemplateResponse::new_template(
|
|
||||||
"chat/streaming_response.html",
|
|
||||||
SSEResponseInitData { user_message },
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"HX-Push",
|
|
||||||
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_chat_user_message(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
|
|
||||||
Form(form): Form<NewMessageForm>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
let user = match auth.current_user {
|
|
||||||
Some(user) => user,
|
|
||||||
None => return Ok(Redirect::to("/").into_response()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let conversation = Conversation::new(user.id, "New chat".to_string());
|
|
||||||
let user_message = Message::new(
|
|
||||||
conversation.id.clone(),
|
|
||||||
MessageRole::User,
|
|
||||||
form.content,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
|
|
||||||
state.db.store_item(conversation.clone()).await?;
|
|
||||||
state.db.store_item(user_message.clone()).await?;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SSEResponseInitData {
|
|
||||||
user_message: Message,
|
|
||||||
conversation: Conversation,
|
|
||||||
}
|
|
||||||
let mut response = TemplateResponse::new_template(
|
|
||||||
"chat/new_chat_first_response.html",
|
|
||||||
SSEResponseInitData {
|
|
||||||
user_message,
|
|
||||||
conversation: conversation.clone(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.into_response();
|
|
||||||
|
|
||||||
response.headers_mut().insert(
|
|
||||||
"HX-Push",
|
|
||||||
HeaderValue::from_str(&format!("/chat/{}", conversation.id)).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ use common::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
html_state::HtmlState,
|
html_state::HtmlState,
|
||||||
middleware_auth::RequireUser,
|
middlewares::{
|
||||||
template_response::{HtmlError, TemplateResponse},
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn show_reference_tooltip(
|
pub async fn show_reference_tooltip(
|
||||||
|
|||||||
90
crates/html-router/src/routes/content/handlers.rs
Normal file
90
crates/html-router/src/routes/content/handlers.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
Form,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use common::storage::types::{text_content::TextContent, user::User};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
html_state::HtmlState,
|
||||||
|
middlewares::{
|
||||||
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ContentPageData {
|
||||||
|
user: User,
|
||||||
|
text_contents: Vec<TextContent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_content_page(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"content/base.html",
|
||||||
|
ContentPageData {
|
||||||
|
user,
|
||||||
|
text_contents,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_text_content_edit_form(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TextContentEditModal {
|
||||||
|
pub user: User,
|
||||||
|
pub text_content: TextContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"content/edit_text_content_modal.html",
|
||||||
|
TextContentEditModal { user, text_content },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PatchTextContentParams {
|
||||||
|
instructions: String,
|
||||||
|
category: String,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
pub async fn patch_text_content(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
Form(form): Form<PatchTextContentParams>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
|
||||||
|
|
||||||
|
TextContent::patch(
|
||||||
|
&id,
|
||||||
|
&form.instructions,
|
||||||
|
&form.category,
|
||||||
|
&form.text,
|
||||||
|
&state.db,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"content/content_list.html",
|
||||||
|
ContentPageData {
|
||||||
|
user,
|
||||||
|
text_contents,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,88 +1,19 @@
|
|||||||
use axum::{
|
mod handlers;
|
||||||
extract::{Path, State},
|
|
||||||
response::IntoResponse,
|
|
||||||
Form,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use common::storage::types::{text_content::TextContent, user::User};
|
use axum::{extract::FromRef, routing::get, Router};
|
||||||
|
use handlers::{patch_text_content, show_content_page, show_text_content_edit_form};
|
||||||
|
|
||||||
use crate::{
|
use crate::html_state::HtmlState;
|
||||||
html_state::HtmlState,
|
|
||||||
middleware_auth::RequireUser,
|
|
||||||
template_response::{HtmlError, TemplateResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
pub fn router<S>() -> Router<S>
|
||||||
pub struct ContentPageData {
|
where
|
||||||
user: User,
|
S: Clone + Send + Sync + 'static,
|
||||||
text_contents: Vec<TextContent>,
|
HtmlState: FromRef<S>,
|
||||||
}
|
{
|
||||||
|
Router::new()
|
||||||
pub async fn show_content_page(
|
.route("/content", get(show_content_page))
|
||||||
State(state): State<HtmlState>,
|
.route(
|
||||||
RequireUser(user): RequireUser,
|
"/content/:id",
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
get(show_text_content_edit_form).patch(patch_text_content),
|
||||||
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
|
)
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"content/base.html",
|
|
||||||
ContentPageData {
|
|
||||||
user,
|
|
||||||
text_contents,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_text_content_edit_form(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Path(id): Path<String>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
let text_content = User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct TextContentEditModal {
|
|
||||||
pub user: User,
|
|
||||||
pub text_content: TextContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"content/edit_text_content_modal.html",
|
|
||||||
TextContentEditModal { user, text_content },
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct PatchTextContentParams {
|
|
||||||
instructions: String,
|
|
||||||
category: String,
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
pub async fn patch_text_content(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Path(id): Path<String>,
|
|
||||||
Form(form): Form<PatchTextContentParams>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
User::get_and_validate_text_content(&id, &user.id, &state.db).await?;
|
|
||||||
|
|
||||||
TextContent::patch(
|
|
||||||
&id,
|
|
||||||
&form.instructions,
|
|
||||||
&form.category,
|
|
||||||
&form.text,
|
|
||||||
&state.db,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let text_contents = User::get_text_contents(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"content/content_list.html",
|
|
||||||
ContentPageData {
|
|
||||||
user,
|
|
||||||
text_contents,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
use axum::response::IntoResponse;
|
|
||||||
use common::storage::types::user::User;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::template_response::{HtmlError, TemplateResponse};
|
|
||||||
use crate::AuthSessionType;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct DocumentationPageData {
|
|
||||||
user: Option<User>,
|
|
||||||
current_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_privacy_policy(auth: AuthSessionType) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"documentation/privacy.html",
|
|
||||||
DocumentationPageData {
|
|
||||||
user: auth.current_user,
|
|
||||||
current_path: "/privacy-policy".to_string(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_get_started(auth: AuthSessionType) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"documentation/get_started.html",
|
|
||||||
DocumentationPageData {
|
|
||||||
user: auth.current_user,
|
|
||||||
current_path: "/get-started".to_string(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_mobile_friendly(auth: AuthSessionType) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"documentation/mobile_friendly.html",
|
|
||||||
DocumentationPageData {
|
|
||||||
user: auth.current_user,
|
|
||||||
current_path: "/mobile-friendly".to_string(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_documentation_index(
|
|
||||||
auth: AuthSessionType,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"documentation/index.html",
|
|
||||||
DocumentationPageData {
|
|
||||||
user: auth.current_user,
|
|
||||||
current_path: "/index".to_string(),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
use axum::response::{Html, IntoResponse};
|
|
||||||
|
|
||||||
use crate::SessionType;
|
|
||||||
|
|
||||||
pub async fn accept_gdpr(session: SessionType) -> impl IntoResponse {
|
|
||||||
session.set("gdpr_accepted", true);
|
|
||||||
|
|
||||||
Html("").into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn deny_gdpr(session: SessionType) -> impl IntoResponse {
|
|
||||||
session.set("gdpr_accepted", true);
|
|
||||||
|
|
||||||
Html("").into_response()
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
debug_handler,
|
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
@@ -7,8 +6,10 @@ use serde::Serialize;
|
|||||||
use tokio::join;
|
use tokio::join;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
middleware_auth::RequireUser,
|
middlewares::{
|
||||||
template_response::{HtmlError, TemplateResponse},
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
AuthSessionType, SessionType,
|
AuthSessionType, SessionType,
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
@@ -73,7 +74,6 @@ pub struct LatestTextContentData {
|
|||||||
user: User,
|
user: User,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[debug_handler]
|
|
||||||
pub async fn delete_text_content(
|
pub async fn delete_text_content(
|
||||||
State(state): State<HtmlState>,
|
State(state): State<HtmlState>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
29
crates/html-router/src/routes/index/mod.rs
Normal file
29
crates/html-router/src/routes/index/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
pub mod handlers;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::FromRef,
|
||||||
|
routing::{delete, get},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use handlers::{delete_job, delete_text_content, index_handler, show_active_jobs};
|
||||||
|
|
||||||
|
use crate::html_state::HtmlState;
|
||||||
|
|
||||||
|
pub fn public_router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new().route("/", get(index_handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn protected_router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new()
|
||||||
|
.route("/jobs/:job_id", delete(delete_job))
|
||||||
|
.route("/active-jobs", get(show_active_jobs))
|
||||||
|
.route("/text-content/:id", delete(delete_text_content))
|
||||||
|
}
|
||||||
@@ -18,9 +18,11 @@ use common::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
html_state::HtmlState,
|
html_state::HtmlState,
|
||||||
middleware_auth::RequireUser,
|
middlewares::{
|
||||||
routes::index::ActiveJobsData,
|
auth_middleware::RequireUser,
|
||||||
template_response::{HtmlError, TemplateResponse},
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
|
routes::index::handlers::ActiveJobsData,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn show_ingress_form(
|
pub async fn show_ingress_form(
|
||||||
19
crates/html-router/src/routes/ingestion/mod.rs
Normal file
19
crates/html-router/src/routes/ingestion/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
mod handlers;
|
||||||
|
|
||||||
|
use axum::{extract::FromRef, routing::get, Router};
|
||||||
|
use handlers::{hide_ingress_form, process_ingress_form, show_ingress_form};
|
||||||
|
|
||||||
|
use crate::html_state::HtmlState;
|
||||||
|
|
||||||
|
pub fn router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/ingress-form",
|
||||||
|
get(show_ingress_form).post(process_ingress_form),
|
||||||
|
)
|
||||||
|
.route("/hide-ingress-form", get(hide_ingress_form))
|
||||||
|
}
|
||||||
291
crates/html-router/src/routes/knowledge/handlers.rs
Normal file
291
crates/html-router/src/routes/knowledge/handlers.rs
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
Form,
|
||||||
|
};
|
||||||
|
use plotly::{
|
||||||
|
common::{Line, Marker, Mode},
|
||||||
|
layout::{Axis, Camera, LayoutScene, ProjectionType},
|
||||||
|
Layout, Plot, Scatter3D,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use common::storage::types::{
|
||||||
|
knowledge_entity::{KnowledgeEntity, KnowledgeEntityType},
|
||||||
|
knowledge_relationship::KnowledgeRelationship,
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
html_state::HtmlState,
|
||||||
|
middlewares::{
|
||||||
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn show_knowledge_page(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct KnowledgeBaseData {
|
||||||
|
entities: Vec<KnowledgeEntity>,
|
||||||
|
relationships: Vec<KnowledgeRelationship>,
|
||||||
|
user: User,
|
||||||
|
plot_html: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let mut plot = Plot::new();
|
||||||
|
|
||||||
|
// Fibonacci sphere distribution
|
||||||
|
let node_count = entities.len();
|
||||||
|
let golden_ratio = (1.0 + 5.0_f64.sqrt()) / 2.0;
|
||||||
|
let node_positions: Vec<(f64, f64, f64)> = (0..node_count)
|
||||||
|
.map(|i| {
|
||||||
|
let i = i as f64;
|
||||||
|
let theta = 2.0 * std::f64::consts::PI * i / golden_ratio;
|
||||||
|
let phi = (1.0 - 2.0 * (i + 0.5) / node_count as f64).acos();
|
||||||
|
let x = phi.sin() * theta.cos();
|
||||||
|
let y = phi.sin() * theta.sin();
|
||||||
|
let z = phi.cos();
|
||||||
|
(x, y, z)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let node_x: Vec<f64> = node_positions.iter().map(|(x, _, _)| *x).collect();
|
||||||
|
let node_y: Vec<f64> = node_positions.iter().map(|(_, y, _)| *y).collect();
|
||||||
|
let node_z: Vec<f64> = node_positions.iter().map(|(_, _, z)| *z).collect();
|
||||||
|
|
||||||
|
// Nodes trace
|
||||||
|
let nodes = Scatter3D::new(node_x.clone(), node_y.clone(), node_z.clone())
|
||||||
|
.mode(Mode::Markers)
|
||||||
|
.marker(Marker::new().size(8).color("#1f77b4"))
|
||||||
|
.text_array(
|
||||||
|
entities
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.description.clone())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.hover_template("Entity: %{text}<br>");
|
||||||
|
|
||||||
|
// Edges traces
|
||||||
|
for rel in &relationships {
|
||||||
|
let from_idx = entities.iter().position(|e| e.id == rel.out).unwrap_or(0);
|
||||||
|
let to_idx = entities.iter().position(|e| e.id == rel.in_).unwrap_or(0);
|
||||||
|
|
||||||
|
let edge_x = vec![node_x[from_idx], node_x[to_idx]];
|
||||||
|
let edge_y = vec![node_y[from_idx], node_y[to_idx]];
|
||||||
|
let edge_z = vec![node_z[from_idx], node_z[to_idx]];
|
||||||
|
|
||||||
|
let edge_trace = Scatter3D::new(edge_x, edge_y, edge_z)
|
||||||
|
.mode(Mode::Lines)
|
||||||
|
.line(Line::new().color("#888").width(2.0))
|
||||||
|
.hover_template(&format!(
|
||||||
|
"Relationship: {}<br>",
|
||||||
|
rel.metadata.relationship_type
|
||||||
|
))
|
||||||
|
.show_legend(false);
|
||||||
|
|
||||||
|
plot.add_trace(edge_trace);
|
||||||
|
}
|
||||||
|
plot.add_trace(nodes);
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
let layout = Layout::new()
|
||||||
|
.scene(
|
||||||
|
LayoutScene::new()
|
||||||
|
.x_axis(Axis::new().visible(false))
|
||||||
|
.y_axis(Axis::new().visible(false))
|
||||||
|
.z_axis(Axis::new().visible(false))
|
||||||
|
.camera(
|
||||||
|
Camera::new()
|
||||||
|
.projection(ProjectionType::Perspective.into())
|
||||||
|
.eye((1.5, 1.5, 1.5).into()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.show_legend(false)
|
||||||
|
.paper_background_color("rbga(250,100,0,0)")
|
||||||
|
.plot_background_color("rbga(0,0,0,0)");
|
||||||
|
|
||||||
|
plot.set_layout(layout);
|
||||||
|
|
||||||
|
// Convert to HTML
|
||||||
|
let html = plot.to_html();
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"knowledge/base.html",
|
||||||
|
KnowledgeBaseData {
|
||||||
|
entities,
|
||||||
|
relationships,
|
||||||
|
user,
|
||||||
|
plot_html: html,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn show_edit_knowledge_entity_form(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct EntityData {
|
||||||
|
entity: KnowledgeEntity,
|
||||||
|
entity_types: Vec<String>,
|
||||||
|
user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get entity types
|
||||||
|
let entity_types: Vec<String> = KnowledgeEntityType::variants()
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Get the entity and validate ownership
|
||||||
|
let entity = User::get_and_validate_knowledge_entity(&id, &user.id, &state.db).await?;
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"knowledge/edit_knowledge_entity_modal.html",
|
||||||
|
EntityData {
|
||||||
|
entity,
|
||||||
|
user,
|
||||||
|
entity_types,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct PatchKnowledgeEntityParams {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub entity_type: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct EntityListData {
|
||||||
|
entities: Vec<KnowledgeEntity>,
|
||||||
|
user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn patch_knowledge_entity(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Form(form): Form<PatchKnowledgeEntityParams>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
// Get the existing entity and validate that the user is allowed
|
||||||
|
User::get_and_validate_knowledge_entity(&form.id, &user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let entity_type: KnowledgeEntityType = KnowledgeEntityType::from(form.entity_type);
|
||||||
|
|
||||||
|
// Update the entity
|
||||||
|
KnowledgeEntity::patch(
|
||||||
|
&form.id,
|
||||||
|
&form.name,
|
||||||
|
&form.description,
|
||||||
|
&entity_type,
|
||||||
|
&state.db,
|
||||||
|
&state.openai_client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Get updated list of entities
|
||||||
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
// Render updated list
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"knowledge/entity_list.html",
|
||||||
|
EntityListData { entities, user },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_knowledge_entity(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
// Get the existing entity and validate that the user is allowed
|
||||||
|
User::get_and_validate_knowledge_entity(&id, &user.id, &state.db).await?;
|
||||||
|
|
||||||
|
// Delete the entity
|
||||||
|
state.db.delete_item::<KnowledgeEntity>(&id).await?;
|
||||||
|
|
||||||
|
// Get updated list of entities
|
||||||
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"knowledge/entity_list.html",
|
||||||
|
EntityListData { entities, user },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RelationshipTableData {
|
||||||
|
entities: Vec<KnowledgeEntity>,
|
||||||
|
relationships: Vec<KnowledgeRelationship>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_knowledge_relationship(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
// GOTTA ADD AUTH VALIDATION
|
||||||
|
|
||||||
|
KnowledgeRelationship::delete_relationship_by_id(&id, &state.db).await?;
|
||||||
|
|
||||||
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
// Render updated list
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"knowledge/relationship_table.html",
|
||||||
|
RelationshipTableData {
|
||||||
|
entities,
|
||||||
|
relationships,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SaveKnowledgeRelationshipInput {
|
||||||
|
pub in_: String,
|
||||||
|
pub out: String,
|
||||||
|
pub relationship_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_knowledge_relationship(
|
||||||
|
State(state): State<HtmlState>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
Form(form): Form<SaveKnowledgeRelationshipInput>,
|
||||||
|
) -> Result<impl IntoResponse, HtmlError> {
|
||||||
|
// Construct relationship
|
||||||
|
let relationship = KnowledgeRelationship::new(
|
||||||
|
form.in_,
|
||||||
|
form.out,
|
||||||
|
user.id.clone(),
|
||||||
|
"manual".into(),
|
||||||
|
form.relationship_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
relationship.store_relationship(&state.db).await?;
|
||||||
|
|
||||||
|
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
||||||
|
|
||||||
|
// Render updated list
|
||||||
|
Ok(TemplateResponse::new_template(
|
||||||
|
"knowledge/relationship_table.html",
|
||||||
|
RelationshipTableData {
|
||||||
|
entities,
|
||||||
|
relationships,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -1,289 +1,33 @@
|
|||||||
|
mod handlers;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::FromRef,
|
||||||
response::IntoResponse,
|
routing::{delete, get, post},
|
||||||
Form,
|
Router,
|
||||||
};
|
};
|
||||||
use plotly::{
|
use handlers::{
|
||||||
common::{Line, Marker, Mode},
|
delete_knowledge_entity, delete_knowledge_relationship, patch_knowledge_entity,
|
||||||
layout::{Axis, Camera, LayoutScene, ProjectionType},
|
save_knowledge_relationship, show_edit_knowledge_entity_form, show_knowledge_page,
|
||||||
Layout, Plot, Scatter3D,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use common::storage::types::{
|
|
||||||
knowledge_entity::{KnowledgeEntity, KnowledgeEntityType},
|
|
||||||
knowledge_relationship::KnowledgeRelationship,
|
|
||||||
user::User,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::html_state::HtmlState;
|
||||||
html_state::HtmlState,
|
|
||||||
middleware_auth::RequireUser,
|
|
||||||
template_response::{HtmlError, TemplateResponse},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn show_knowledge_page(
|
pub fn router<S>() -> Router<S>
|
||||||
State(state): State<HtmlState>,
|
where
|
||||||
RequireUser(user): RequireUser,
|
S: Clone + Send + Sync + 'static,
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
HtmlState: FromRef<S>,
|
||||||
#[derive(Serialize)]
|
{
|
||||||
pub struct KnowledgeBaseData {
|
Router::new()
|
||||||
entities: Vec<KnowledgeEntity>,
|
.route("/knowledge", get(show_knowledge_page))
|
||||||
relationships: Vec<KnowledgeRelationship>,
|
.route(
|
||||||
user: User,
|
"/knowledge-entity/:id",
|
||||||
plot_html: String,
|
get(show_edit_knowledge_entity_form)
|
||||||
}
|
.delete(delete_knowledge_entity)
|
||||||
|
.patch(patch_knowledge_entity),
|
||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let mut plot = Plot::new();
|
|
||||||
|
|
||||||
// Fibonacci sphere distribution
|
|
||||||
let node_count = entities.len();
|
|
||||||
let golden_ratio = (1.0 + 5.0_f64.sqrt()) / 2.0;
|
|
||||||
let node_positions: Vec<(f64, f64, f64)> = (0..node_count)
|
|
||||||
.map(|i| {
|
|
||||||
let i = i as f64;
|
|
||||||
let theta = 2.0 * std::f64::consts::PI * i / golden_ratio;
|
|
||||||
let phi = (1.0 - 2.0 * (i + 0.5) / node_count as f64).acos();
|
|
||||||
let x = phi.sin() * theta.cos();
|
|
||||||
let y = phi.sin() * theta.sin();
|
|
||||||
let z = phi.cos();
|
|
||||||
(x, y, z)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let node_x: Vec<f64> = node_positions.iter().map(|(x, _, _)| *x).collect();
|
|
||||||
let node_y: Vec<f64> = node_positions.iter().map(|(_, y, _)| *y).collect();
|
|
||||||
let node_z: Vec<f64> = node_positions.iter().map(|(_, _, z)| *z).collect();
|
|
||||||
|
|
||||||
// Nodes trace
|
|
||||||
let nodes = Scatter3D::new(node_x.clone(), node_y.clone(), node_z.clone())
|
|
||||||
.mode(Mode::Markers)
|
|
||||||
.marker(Marker::new().size(8).color("#1f77b4"))
|
|
||||||
.text_array(
|
|
||||||
entities
|
|
||||||
.iter()
|
|
||||||
.map(|e| e.description.clone())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
)
|
||||||
.hover_template("Entity: %{text}<br>");
|
.route("/knowledge-relationship", post(save_knowledge_relationship))
|
||||||
|
.route(
|
||||||
// Edges traces
|
"/knowledge-relationship/:id",
|
||||||
for rel in &relationships {
|
delete(delete_knowledge_relationship),
|
||||||
let from_idx = entities.iter().position(|e| e.id == rel.out).unwrap_or(0);
|
|
||||||
let to_idx = entities.iter().position(|e| e.id == rel.in_).unwrap_or(0);
|
|
||||||
|
|
||||||
let edge_x = vec![node_x[from_idx], node_x[to_idx]];
|
|
||||||
let edge_y = vec![node_y[from_idx], node_y[to_idx]];
|
|
||||||
let edge_z = vec![node_z[from_idx], node_z[to_idx]];
|
|
||||||
|
|
||||||
let edge_trace = Scatter3D::new(edge_x, edge_y, edge_z)
|
|
||||||
.mode(Mode::Lines)
|
|
||||||
.line(Line::new().color("#888").width(2.0))
|
|
||||||
.hover_template(&format!(
|
|
||||||
"Relationship: {}<br>",
|
|
||||||
rel.metadata.relationship_type
|
|
||||||
))
|
|
||||||
.show_legend(false);
|
|
||||||
|
|
||||||
plot.add_trace(edge_trace);
|
|
||||||
}
|
|
||||||
plot.add_trace(nodes);
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
let layout = Layout::new()
|
|
||||||
.scene(
|
|
||||||
LayoutScene::new()
|
|
||||||
.x_axis(Axis::new().visible(false))
|
|
||||||
.y_axis(Axis::new().visible(false))
|
|
||||||
.z_axis(Axis::new().visible(false))
|
|
||||||
.camera(
|
|
||||||
Camera::new()
|
|
||||||
.projection(ProjectionType::Perspective.into())
|
|
||||||
.eye((1.5, 1.5, 1.5).into()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.show_legend(false)
|
|
||||||
.paper_background_color("rbga(250,100,0,0)")
|
|
||||||
.plot_background_color("rbga(0,0,0,0)");
|
|
||||||
|
|
||||||
plot.set_layout(layout);
|
|
||||||
|
|
||||||
// Convert to HTML
|
|
||||||
let html = plot.to_html();
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"knowledge/base.html",
|
|
||||||
KnowledgeBaseData {
|
|
||||||
entities,
|
|
||||||
relationships,
|
|
||||||
user,
|
|
||||||
plot_html: html,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn show_edit_knowledge_entity_form(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Path(id): Path<String>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct EntityData {
|
|
||||||
entity: KnowledgeEntity,
|
|
||||||
entity_types: Vec<String>,
|
|
||||||
user: User,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get entity types
|
|
||||||
let entity_types: Vec<String> = KnowledgeEntityType::variants()
|
|
||||||
.iter()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Get the entity and validate ownership
|
|
||||||
let entity = User::get_and_validate_knowledge_entity(&id, &user.id, &state.db).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"knowledge/edit_knowledge_entity_modal.html",
|
|
||||||
EntityData {
|
|
||||||
entity,
|
|
||||||
user,
|
|
||||||
entity_types,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct PatchKnowledgeEntityParams {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
pub entity_type: String,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct EntityListData {
|
|
||||||
entities: Vec<KnowledgeEntity>,
|
|
||||||
user: User,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn patch_knowledge_entity(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Form(form): Form<PatchKnowledgeEntityParams>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
// Get the existing entity and validate that the user is allowed
|
|
||||||
User::get_and_validate_knowledge_entity(&form.id, &user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let entity_type: KnowledgeEntityType = KnowledgeEntityType::from(form.entity_type);
|
|
||||||
|
|
||||||
// Update the entity
|
|
||||||
KnowledgeEntity::patch(
|
|
||||||
&form.id,
|
|
||||||
&form.name,
|
|
||||||
&form.description,
|
|
||||||
&entity_type,
|
|
||||||
&state.db,
|
|
||||||
&state.openai_client,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Get updated list of entities
|
|
||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
// Render updated list
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"knowledge/entity_list.html",
|
|
||||||
EntityListData { entities, user },
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_knowledge_entity(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Path(id): Path<String>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
// Get the existing entity and validate that the user is allowed
|
|
||||||
User::get_and_validate_knowledge_entity(&id, &user.id, &state.db).await?;
|
|
||||||
|
|
||||||
// Delete the entity
|
|
||||||
state.db.delete_item::<KnowledgeEntity>(&id).await?;
|
|
||||||
|
|
||||||
// Get updated list of entities
|
|
||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"knowledge/entity_list.html",
|
|
||||||
EntityListData { entities, user },
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RelationshipTableData {
|
|
||||||
entities: Vec<KnowledgeEntity>,
|
|
||||||
relationships: Vec<KnowledgeRelationship>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_knowledge_relationship(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Path(id): Path<String>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
// GOTTA ADD AUTH VALIDATION
|
|
||||||
|
|
||||||
KnowledgeRelationship::delete_relationship_by_id(&id, &state.db).await?;
|
|
||||||
|
|
||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
// Render updated list
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"knowledge/relationship_table.html",
|
|
||||||
RelationshipTableData {
|
|
||||||
entities,
|
|
||||||
relationships,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct SaveKnowledgeRelationshipInput {
|
|
||||||
pub in_: String,
|
|
||||||
pub out: String,
|
|
||||||
pub relationship_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn save_knowledge_relationship(
|
|
||||||
State(state): State<HtmlState>,
|
|
||||||
RequireUser(user): RequireUser,
|
|
||||||
Form(form): Form<SaveKnowledgeRelationshipInput>,
|
|
||||||
) -> Result<impl IntoResponse, HtmlError> {
|
|
||||||
// Construct relationship
|
|
||||||
let relationship = KnowledgeRelationship::new(
|
|
||||||
form.in_,
|
|
||||||
form.out,
|
|
||||||
user.id.clone(),
|
|
||||||
"manual".into(),
|
|
||||||
form.relationship_type,
|
|
||||||
);
|
|
||||||
|
|
||||||
relationship.store_relationship(&state.db).await?;
|
|
||||||
|
|
||||||
let entities = User::get_knowledge_entities(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
let relationships = User::get_knowledge_relationships(&user.id, &state.db).await?;
|
|
||||||
|
|
||||||
// Render updated list
|
|
||||||
Ok(TemplateResponse::new_template(
|
|
||||||
"knowledge/relationship_table.html",
|
|
||||||
RelationshipTableData {
|
|
||||||
entities,
|
|
||||||
relationships,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,17 @@ use std::sync::Arc;
|
|||||||
use axum::response::Html;
|
use axum::response::Html;
|
||||||
use minijinja_autoreload::AutoReloader;
|
use minijinja_autoreload::AutoReloader;
|
||||||
|
|
||||||
use crate::template_response::HtmlError;
|
use crate::middlewares::response_middleware::HtmlError;
|
||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod admin_panel;
|
pub mod admin;
|
||||||
|
pub mod auth;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod content;
|
pub mod content;
|
||||||
pub mod documentation;
|
|
||||||
pub mod gdpr;
|
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod ingress_form;
|
pub mod ingestion;
|
||||||
pub mod knowledge;
|
pub mod knowledge;
|
||||||
pub mod search_result;
|
pub mod search;
|
||||||
pub mod signin;
|
|
||||||
pub mod signout;
|
|
||||||
pub mod signup;
|
|
||||||
|
|
||||||
// Helper function for render_template
|
// Helper function for render_template
|
||||||
pub fn render_template<T>(
|
pub fn render_template<T>(
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
html_state::HtmlState,
|
html_state::HtmlState,
|
||||||
middleware_auth::RequireUser,
|
middlewares::{
|
||||||
template_response::{HtmlError, TemplateResponse},
|
auth_middleware::RequireUser,
|
||||||
|
response_middleware::{HtmlError, TemplateResponse},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
14
crates/html-router/src/routes/search/mod.rs
Normal file
14
crates/html-router/src/routes/search/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
mod handlers;
|
||||||
|
|
||||||
|
use axum::{extract::FromRef, routing::get, Router};
|
||||||
|
use handlers::search_result_handler;
|
||||||
|
|
||||||
|
use crate::html_state::HtmlState;
|
||||||
|
|
||||||
|
pub fn router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
HtmlState: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new().route("/search", get(search_result_handler))
|
||||||
|
}
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_openai::{
|
use async_openai::types::{
|
||||||
error::OpenAIError,
|
ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage,
|
||||||
types::{
|
CreateChatCompletionRequest, CreateChatCompletionRequestArgs, ResponseFormat,
|
||||||
ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage,
|
ResponseFormatJsonSchema,
|
||||||
CreateChatCompletionRequest, CreateChatCompletionRequestArgs, ResponseFormat,
|
|
||||||
ResponseFormatJsonSchema,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use common::{
|
use common::{
|
||||||
error::AppError,
|
error::AppError,
|
||||||
storage::{
|
storage::{
|
||||||
db::SurrealDbClient,
|
db::SurrealDbClient,
|
||||||
types::{
|
types::{knowledge_entity::KnowledgeEntity, system_settings::SystemSettings},
|
||||||
knowledge_entity::KnowledgeEntity,
|
|
||||||
system_settings::SystemSettings,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use composite_retrieval::retrieve_entities;
|
use composite_retrieval::retrieve_entities;
|
||||||
@@ -55,8 +49,9 @@ impl IngestionEnricher {
|
|||||||
.find_similar_entities(category, instructions, text, user_id)
|
.find_similar_entities(category, instructions, text, user_id)
|
||||||
.await?;
|
.await?;
|
||||||
info!("got similar entitities");
|
info!("got similar entitities");
|
||||||
let llm_request =
|
let llm_request = self
|
||||||
self.prepare_llm_request(category, instructions, text, &similar_entities).await?;
|
.prepare_llm_request(category, instructions, text, &similar_entities)
|
||||||
|
.await?;
|
||||||
self.perform_analysis(llm_request).await
|
self.perform_analysis(llm_request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +78,7 @@ impl IngestionEnricher {
|
|||||||
similar_entities: &[KnowledgeEntity],
|
similar_entities: &[KnowledgeEntity],
|
||||||
) -> Result<CreateChatCompletionRequest, AppError> {
|
) -> Result<CreateChatCompletionRequest, AppError> {
|
||||||
let settings = SystemSettings::get_current(&self.db_client).await?;
|
let settings = SystemSettings::get_current(&self.db_client).await?;
|
||||||
|
|
||||||
let entities_json = json!(similar_entities
|
let entities_json = json!(similar_entities
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entity| {
|
.map(|entity| {
|
||||||
@@ -123,16 +118,16 @@ impl IngestionEnricher {
|
|||||||
])
|
])
|
||||||
.response_format(response_format)
|
.response_format(response_format)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
Ok(request)
|
Ok(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn perform_analysis(
|
async fn perform_analysis(
|
||||||
&self,
|
&self,
|
||||||
request: CreateChatCompletionRequest,
|
request: CreateChatCompletionRequest,
|
||||||
) -> Result<LLMEnrichmentResult, AppError> {
|
) -> Result<LLMEnrichmentResult, AppError> {
|
||||||
let response = self.openai_client.chat().create(request).await?;
|
let response = self.openai_client.chat().create(request).await?;
|
||||||
|
|
||||||
let content = response
|
let content = response
|
||||||
.choices
|
.choices
|
||||||
.first()
|
.first()
|
||||||
@@ -140,7 +135,7 @@ impl IngestionEnricher {
|
|||||||
.ok_or(AppError::LLMParsing(
|
.ok_or(AppError::LLMParsing(
|
||||||
"No content found in LLM response".into(),
|
"No content found in LLM response".into(),
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
serde_json::from_str::<LLMEnrichmentResult>(content).map_err(|e| {
|
serde_json::from_str::<LLMEnrichmentResult>(content).map_err(|e| {
|
||||||
AppError::LLMParsing(format!("Failed to parse LLM response into analysis: {}", e))
|
AppError::LLMParsing(format!("Failed to parse LLM response into analysis: {}", e))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
// Load content if needed
|
// Load content if needed
|
||||||
if (!tooltipContent) {
|
if (!tooltipContent) {
|
||||||
fetch(`/knowledge/${encodeURIComponent(reference)}`)
|
fetch(`/chat/reference/${encodeURIComponent(reference)}`)
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
tooltipContent = html;
|
tooltipContent = html;
|
||||||
|
|||||||
4
todo.md
4
todo.md
@@ -1,7 +1,7 @@
|
|||||||
\[\] archive ingressed webpage
|
\[\] archive ingressed webpage
|
||||||
\[\] openai api key in config
|
\[\] openai api key in config
|
||||||
\[\] option to set models, query and processing
|
\[x\] option to set models, query and processing
|
||||||
\[\] template customization?
|
\[x\] template customization?
|
||||||
\[\] configs primarily get envs
|
\[\] configs primarily get envs
|
||||||
\[\] filtering on categories
|
\[\] filtering on categories
|
||||||
\[\] three js graph explorer
|
\[\] three js graph explorer
|
||||||
|
|||||||
Reference in New Issue
Block a user