From a40ed0fe94a68e1ad99f1bd5446c4c7acb177636 Mon Sep 17 00:00:00 2001 From: Per Stark Date: Tue, 25 Mar 2025 12:03:05 +0100 Subject: [PATCH] refactor: html-router builder pattern and structure --- Cargo.lock | 96 +----- .../src/answer_retrieval.rs | 2 +- crates/composite-retrieval/src/lib.rs | 1 - crates/composite-retrieval/src/vector.rs | 1 - crates/html-router/src/lib.rs | 151 ++------- .../analytics_middleware.rs} | 0 .../auth_middleware.rs} | 4 +- crates/html-router/src/middlewares/mod.rs | 3 + .../response_middleware.rs} | 3 +- crates/html-router/src/router_factory.rs | 159 +++++++++ .../{account.rs => account/handlers.rs} | 6 +- crates/html-router/src/routes/account/mod.rs | 24 ++ .../{admin_panel.rs => admin/handlers.rs} | 12 +- crates/html-router/src/routes/admin/mod.rs | 27 ++ crates/html-router/src/routes/auth/mod.rs | 24 ++ .../src/routes/{ => auth}/signin.rs | 2 +- .../src/routes/{ => auth}/signout.rs | 2 +- .../src/routes/{ => auth}/signup.rs | 2 +- .../src/routes/chat/chat_handlers.rs | 226 +++++++++++++ .../routes/chat/message_response_stream.rs | 6 +- crates/html-router/src/routes/chat/mod.rs | 239 ++------------ .../html-router/src/routes/chat/references.rs | 6 +- .../src/routes/content/handlers.rs | 90 ++++++ crates/html-router/src/routes/content/mod.rs | 99 +----- .../src/routes/documentation/mod.rs | 54 ---- crates/html-router/src/routes/gdpr.rs | 15 - .../routes/{index.rs => index/handlers.rs} | 8 +- crates/html-router/src/routes/index/mod.rs | 29 ++ .../handlers.rs} | 8 +- .../html-router/src/routes/ingestion/mod.rs | 19 ++ .../src/routes/knowledge/handlers.rs | 291 +++++++++++++++++ .../html-router/src/routes/knowledge/mod.rs | 306 ++---------------- crates/html-router/src/routes/mod.rs | 14 +- .../{search_result.rs => search/handlers.rs} | 6 +- crates/html-router/src/routes/search/mod.rs | 14 + crates/ingestion-pipeline/src/enricher.rs | 33 +- templates/chat/reference_list.html | 2 +- todo.md | 4 +- 38 files changed, 1050 insertions(+), 938 deletions(-) rename crates/html-router/src/{middleware_analytics.rs => middlewares/analytics_middleware.rs} (100%) rename crates/html-router/src/{middleware_auth.rs => middlewares/auth_middleware.rs} (94%) create mode 100644 crates/html-router/src/middlewares/mod.rs rename crates/html-router/src/{template_response.rs => middlewares/response_middleware.rs} (99%) create mode 100644 crates/html-router/src/router_factory.rs rename crates/html-router/src/routes/{account.rs => account/handlers.rs} (96%) create mode 100644 crates/html-router/src/routes/account/mod.rs rename crates/html-router/src/routes/{admin_panel.rs => admin/handlers.rs} (97%) create mode 100644 crates/html-router/src/routes/admin/mod.rs create mode 100644 crates/html-router/src/routes/auth/mod.rs rename crates/html-router/src/routes/{ => auth}/signin.rs (95%) rename crates/html-router/src/routes/{ => auth}/signout.rs (82%) rename crates/html-router/src/routes/{ => auth}/signup.rs (95%) create mode 100644 crates/html-router/src/routes/chat/chat_handlers.rs create mode 100644 crates/html-router/src/routes/content/handlers.rs delete mode 100644 crates/html-router/src/routes/documentation/mod.rs delete mode 100644 crates/html-router/src/routes/gdpr.rs rename crates/html-router/src/routes/{index.rs => index/handlers.rs} (97%) create mode 100644 crates/html-router/src/routes/index/mod.rs rename crates/html-router/src/routes/{ingress_form.rs => ingestion/handlers.rs} (95%) create mode 100644 crates/html-router/src/routes/ingestion/mod.rs create mode 100644 crates/html-router/src/routes/knowledge/handlers.rs rename crates/html-router/src/routes/{search_result.rs => search/handlers.rs} (89%) create mode 100644 crates/html-router/src/routes/search/mod.rs diff --git a/Cargo.lock b/Cargo.lock index feab823..10b3652 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,16 +992,6 @@ dependencies = [ "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]] name = "ciborium" version = "0.2.2" @@ -1054,11 +1044,8 @@ dependencies = [ "chrono-tz", "config", "futures", - "lettre", "mime", "mime_guess", - "minijinja", - "minijinja-autoreload", "reqwest", "serde", "serde_json", @@ -1494,22 +1481,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "ena" version = "0.14.3" @@ -1573,7 +1544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" dependencies = [ "futures-core", - "nom 7.1.3", + "nom", "pin-project-lite", ] @@ -2027,17 +1998,6 @@ dependencies = [ "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]] name = "html-router" version = "0.1.0" @@ -2629,31 +2589,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "lexicmp" version = "0.1.0" @@ -3040,15 +2975,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - [[package]] name = "nonempty" version = "0.7.0" @@ -3715,12 +3641,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "quoted_printable" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" - [[package]] name = "radium" version = "0.7.0" @@ -3936,7 +3856,7 @@ dependencies = [ "futures-core", "futures-timer", "mime", - "nom 7.1.3", + "nom", "pin-project-lite", "reqwest", "thiserror", @@ -4022,7 +3942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610" dependencies = [ "memchr", - "nom 7.1.3", + "nom", "serde", ] @@ -5822,16 +5742,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows-core" version = "0.52.0" diff --git a/crates/composite-retrieval/src/answer_retrieval.rs b/crates/composite-retrieval/src/answer_retrieval.rs index 3213c53..be705c0 100644 --- a/crates/composite-retrieval/src/answer_retrieval.rs +++ b/crates/composite-retrieval/src/answer_retrieval.rs @@ -22,7 +22,7 @@ use serde_json::{json, Value}; 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)] pub struct Reference { diff --git a/crates/composite-retrieval/src/lib.rs b/crates/composite-retrieval/src/lib.rs index c17cebc..90df670 100644 --- a/crates/composite-retrieval/src/lib.rs +++ b/crates/composite-retrieval/src/lib.rs @@ -13,7 +13,6 @@ use common::{ use futures::future::{try_join, try_join_all}; use graph::{find_entities_by_relationship_by_id, find_entities_by_source_ids}; use std::collections::HashMap; -use tracing::info; use vector::find_items_by_vector_similarity; /// Performs a comprehensive knowledge entity retrieval using multiple search strategies diff --git a/crates/composite-retrieval/src/vector.rs b/crates/composite-retrieval/src/vector.rs index 192cdcf..a965b7a 100644 --- a/crates/composite-retrieval/src/vector.rs +++ b/crates/composite-retrieval/src/vector.rs @@ -1,7 +1,6 @@ use surrealdb::{engine::any::Any, Surreal}; use common::{error::AppError, utils::embedding::generate_embedding}; -use tracing::info; /// Compares vectors and retrieves a number of items from the specified table. /// diff --git a/crates/html-router/src/lib.rs b/crates/html-router/src/lib.rs index a3ba671..7a30aa8 100644 --- a/crates/html-router/src/lib.rs +++ b/crates/html-router/src/lib.rs @@ -1,55 +1,16 @@ pub mod html_state; -mod middleware_analytics; -mod middleware_auth; -mod routes; -mod template_response; +pub mod middlewares; +pub mod router_factory; +pub mod routes; -use axum::{ - extract::FromRef, - middleware::{from_fn_with_state, map_response_with_state}, - routing::{delete, get, patch, post}, - Router, -}; -use axum_session::{Session, SessionLayer}; -use axum_session_auth::{AuthConfig, AuthSession, AuthSessionLayer}; +use axum::{extract::FromRef, Router}; +use axum_session::Session; +use axum_session_auth::AuthSession; use axum_session_surreal::SessionSurrealPool; use common::storage::types::user::User; use html_state::HtmlState; -use middleware_analytics::analytics_middleware; -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 router_factory::RouterFactory; use surrealdb::{engine::any::Any, Surreal}; -use template_response::with_template_response; -use tower_http::services::ServeDir; pub type AuthSessionType = AuthSession, Surreal>; pub type SessionType = Session>; @@ -60,89 +21,17 @@ where S: Clone + Send + Sync + 'static, HtmlState: FromRef, { - // Public routes - no auth required - let public_routes = Router::new() - .route("/", get(index_handler)) - .route("/gdpr/accept", post(accept_gdpr)) - .route("/gdpr/deny", post(deny_gdpr)) - .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), - ) - .route("/documentation", get(show_documentation_index)) - .route("/documentation/privacy-policy", get(show_privacy_policy)) - .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::, Surreal>::new(Some( - app_state.db.client.clone(), - )) - .with_config(AuthConfig::::default()), - ) - .layer(SessionLayer::new((*app_state.session_store).clone())) + RouterFactory::new(app_state) + .add_public_routes(routes::index::public_router()) + .add_public_routes(routes::auth::router()) + .with_public_assets("/assets", "assets/") + .add_protected_routes(routes::index::protected_router()) + .add_protected_routes(routes::search::router()) + .add_protected_routes(routes::account::router()) + .add_protected_routes(routes::admin::router()) + .add_protected_routes(routes::chat::router()) + .add_protected_routes(routes::content::router()) + .add_protected_routes(routes::knowledge::router()) + .add_protected_routes(routes::ingestion::router()) + .build() } diff --git a/crates/html-router/src/middleware_analytics.rs b/crates/html-router/src/middlewares/analytics_middleware.rs similarity index 100% rename from crates/html-router/src/middleware_analytics.rs rename to crates/html-router/src/middlewares/analytics_middleware.rs diff --git a/crates/html-router/src/middleware_auth.rs b/crates/html-router/src/middlewares/auth_middleware.rs similarity index 94% rename from crates/html-router/src/middleware_auth.rs rename to crates/html-router/src/middlewares/auth_middleware.rs index ae5d6fa..e26177b 100644 --- a/crates/html-router/src/middleware_auth.rs +++ b/crates/html-router/src/middlewares/auth_middleware.rs @@ -7,7 +7,9 @@ use axum::{ }; use common::storage::types::user::User; -use crate::{template_response::TemplateResponse, AuthSessionType}; +use crate::AuthSessionType; + +use super::response_middleware::TemplateResponse; #[derive(Debug, Clone)] pub struct RequireUser(pub User); diff --git a/crates/html-router/src/middlewares/mod.rs b/crates/html-router/src/middlewares/mod.rs new file mode 100644 index 0000000..e5d70fc --- /dev/null +++ b/crates/html-router/src/middlewares/mod.rs @@ -0,0 +1,3 @@ +pub mod analytics_middleware; +pub mod auth_middleware; +pub mod response_middleware; diff --git a/crates/html-router/src/template_response.rs b/crates/html-router/src/middlewares/response_middleware.rs similarity index 99% rename from crates/html-router/src/template_response.rs rename to crates/html-router/src/middlewares/response_middleware.rs index 55d398b..e813b51 100644 --- a/crates/html-router/src/template_response.rs +++ b/crates/html-router/src/middlewares/response_middleware.rs @@ -1,10 +1,9 @@ use axum::{ extract::State, - http::{StatusCode, Uri}, + http::StatusCode, response::{Html, IntoResponse, Response}, Extension, }; -use axum_htmx::HxRedirect; use common::error::AppError; use minijinja::{context, Value}; use minijinja_autoreload::AutoReloader; diff --git a/crates/html-router/src/router_factory.rs b/crates/html-router/src/router_factory.rs new file mode 100644 index 0000000..9a8b258 --- /dev/null +++ b/crates/html-router/src/router_factory.rs @@ -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 { + app_state: HtmlState, + public_routers: Vec>, + protected_routers: Vec>, + nested_routes: Vec<(String, Router)>, + nested_protected_routes: Vec<(String, Router)>, + custom_middleware: Vec) -> Router + Send>>, + public_assets_config: Option, +} + +struct AssetsConfig { + path: String, // URL path for assets + directory: String, // Directory on disk +} + +impl RouterFactory +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + 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) -> 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) -> 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) -> 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) -> 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(mut self, middleware_fn: F) -> Self + where + F: FnOnce(Router) -> Router + Send + 'static, + { + self.custom_middleware.push(Box::new(middleware_fn)); + self + } + + pub fn build(self) -> Router { + // 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::, Surreal>::new(Some( + self.app_state.db.client.clone(), + )) + .with_config(AuthConfig::::default()), + ) + .layer(SessionLayer::new((*self.app_state.session_store).clone())) + } +} diff --git a/crates/html-router/src/routes/account.rs b/crates/html-router/src/routes/account/handlers.rs similarity index 96% rename from crates/html-router/src/routes/account.rs rename to crates/html-router/src/routes/account/handlers.rs index 4eda303..1cbbb35 100644 --- a/crates/html-router/src/routes/account.rs +++ b/crates/html-router/src/routes/account/handlers.rs @@ -3,8 +3,10 @@ use chrono_tz::TZ_VARIANTS; use serde::{Deserialize, Serialize}; use crate::{ - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, + middlewares::{ + auth_middleware::RequireUser, + response_middleware::{HtmlError, TemplateResponse}, + }, AuthSessionType, }; use common::storage::types::user::User; diff --git a/crates/html-router/src/routes/account/mod.rs b/crates/html-router/src/routes/account/mod.rs new file mode 100644 index 0000000..14582cd --- /dev/null +++ b/crates/html-router/src/routes/account/mod.rs @@ -0,0 +1,24 @@ +mod handlers; +use axum::{ + extract::FromRef, + routing::{delete, get, patch, post}, + Router, +}; + +use crate::html_state::HtmlState; + +pub fn router() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + 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)) +} diff --git a/crates/html-router/src/routes/admin_panel.rs b/crates/html-router/src/routes/admin/handlers.rs similarity index 97% rename from crates/html-router/src/routes/admin_panel.rs rename to crates/html-router/src/routes/admin/handlers.rs index 0877b30..be21237 100644 --- a/crates/html-router/src/routes/admin_panel.rs +++ b/crates/html-router/src/routes/admin/handlers.rs @@ -1,10 +1,6 @@ use axum::{extract::State, response::IntoResponse, Form}; use serde::{Deserialize, Serialize}; -use crate::{ - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, -}; use common::storage::types::{ analytics::Analytics, system_prompts::{DEFAULT_INGRESS_ANALYSIS_SYSTEM_PROMPT, DEFAULT_QUERY_SYSTEM_PROMPT}, @@ -12,7 +8,13 @@ use common::storage::types::{ user::User, }; -use crate::html_state::HtmlState; +use crate::{ + html_state::HtmlState, + middlewares::{ + auth_middleware::RequireUser, + response_middleware::{HtmlError, TemplateResponse}, + }, +}; #[derive(Serialize)] pub struct AdminPanelData { diff --git a/crates/html-router/src/routes/admin/mod.rs b/crates/html-router/src/routes/admin/mod.rs new file mode 100644 index 0000000..5cce1c4 --- /dev/null +++ b/crates/html-router/src/routes/admin/mod.rs @@ -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() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + 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)) +} diff --git a/crates/html-router/src/routes/auth/mod.rs b/crates/html-router/src/routes/auth/mod.rs new file mode 100644 index 0000000..4035205 --- /dev/null +++ b/crates/html-router/src/routes/auth/mod.rs @@ -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() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + 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), + ) +} diff --git a/crates/html-router/src/routes/signin.rs b/crates/html-router/src/routes/auth/signin.rs similarity index 95% rename from crates/html-router/src/routes/signin.rs rename to crates/html-router/src/routes/auth/signin.rs index 8e4f7f4..3f7e813 100644 --- a/crates/html-router/src/routes/signin.rs +++ b/crates/html-router/src/routes/auth/signin.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ html_state::HtmlState, - template_response::{HtmlError, TemplateResponse}, + middlewares::response_middleware::{HtmlError, TemplateResponse}, AuthSessionType, }; use common::storage::types::user::User; diff --git a/crates/html-router/src/routes/signout.rs b/crates/html-router/src/routes/auth/signout.rs similarity index 82% rename from crates/html-router/src/routes/signout.rs rename to crates/html-router/src/routes/auth/signout.rs index 1b54f50..f1dd4fa 100644 --- a/crates/html-router/src/routes/signout.rs +++ b/crates/html-router/src/routes/auth/signout.rs @@ -1,7 +1,7 @@ use axum::response::IntoResponse; use crate::{ - template_response::{HtmlError, TemplateResponse}, + middlewares::response_middleware::{HtmlError, TemplateResponse}, AuthSessionType, }; diff --git a/crates/html-router/src/routes/signup.rs b/crates/html-router/src/routes/auth/signup.rs similarity index 95% rename from crates/html-router/src/routes/signup.rs rename to crates/html-router/src/routes/auth/signup.rs index 68cb359..6d58e48 100644 --- a/crates/html-router/src/routes/signup.rs +++ b/crates/html-router/src/routes/auth/signup.rs @@ -10,7 +10,7 @@ use common::storage::types::user::User; use crate::{ html_state::HtmlState, - template_response::{HtmlError, TemplateResponse}, + middlewares::response_middleware::{HtmlError, TemplateResponse}, AuthSessionType, }; diff --git a/crates/html-router/src/routes/chat/chat_handlers.rs b/crates/html-router/src/routes/chat/chat_handlers.rs new file mode 100644 index 0000000..df942c2 --- /dev/null +++ b/crates/html-router/src/routes/chat/chat_handlers.rs @@ -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, +} + +// Custom deserializer function +fn deserialize_references<'de, D>(deserializer: D) -> Result, 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, + conversation: Option, + conversation_archive: Vec, +} + +pub async fn show_initialized_chat( + State(state): State, + RequireUser(user): RequireUser, + Form(form): Form, +) -> Result { + 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, + RequireUser(user): RequireUser, +) -> Result { + 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, + State(state): State, + RequireUser(user): RequireUser, +) -> Result { + 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, + State(state): State, + RequireUser(user): RequireUser, + Form(form): Form, +) -> Result { + 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, + auth: AuthSession, Surreal>, + Form(form): Form, +) -> Result { + 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) +} diff --git a/crates/html-router/src/routes/chat/message_response_stream.rs b/crates/html-router/src/routes/chat/message_response_stream.rs index debb32b..ef6ac2b 100644 --- a/crates/html-router/src/routes/chat/message_response_stream.rs +++ b/crates/html-router/src/routes/chat/message_response_stream.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; use serde_json::from_str; use surrealdb::{engine::any::Any, Surreal}; use tokio::sync::{mpsc::channel, Mutex}; -use tracing::{error, info}; +use tracing::{error, debug}; use common::storage::{ db::SurrealDbClient, @@ -195,7 +195,7 @@ pub async fn get_response_stream( let _ = tx_final.send(ai_message.clone()).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), } } else { @@ -304,7 +304,6 @@ pub async fn get_response_stream( .data("Stream complete")) })); - info!("OpenAI streaming started"); Sse::new(event_stream.boxed()).keep_alive( KeepAlive::new() .interval(Duration::from_secs(15)) @@ -312,7 +311,6 @@ pub async fn get_response_stream( ) } -// Replace JsonParseState with StreamParserState struct StreamParserState { parser: JsonStreamParser, last_answer_content: String, diff --git a/crates/html-router/src/routes/chat/mod.rs b/crates/html-router/src/routes/chat/mod.rs index 824a733..c272387 100644 --- a/crates/html-router/src/routes/chat/mod.rs +++ b/crates/html-router/src/routes/chat/mod.rs @@ -1,227 +1,30 @@ -pub mod message_response_stream; -pub mod references; +mod chat_handlers; +mod message_response_stream; +mod references; use axum::{ - extract::{Path, State}, - http::HeaderValue, - response::{IntoResponse, Redirect}, - Form, + extract::FromRef, + routing::{get, post}, + Router, }; -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 chat_handlers::{ + new_chat_user_message, new_user_message, show_chat_base, show_existing_chat, + show_initialized_chat, }; +use message_response_stream::get_response_stream; +use references::show_reference_tooltip; -use crate::{ - html_state::HtmlState, - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, -}; +use crate::html_state::HtmlState; -#[derive(Debug, Deserialize)] -pub struct ChatStartParams { - user_query: String, - llm_response: String, - #[serde(deserialize_with = "deserialize_references")] - references: Vec, -} - -// Custom deserializer function -fn deserialize_references<'de, D>(deserializer: D) -> Result, D::Error> +pub fn router() -> Router where - D: serde::Deserializer<'de>, + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, { - 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, - conversation: Option, - conversation_archive: Vec, -} - -pub async fn show_initialized_chat( - State(state): State, - RequireUser(user): RequireUser, - Form(form): Form, -) -> Result { - 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, - RequireUser(user): RequireUser, -) -> Result { - 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, - State(state): State, - RequireUser(user): RequireUser, -) -> Result { - 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, - State(state): State, - RequireUser(user): RequireUser, - Form(form): Form, -) -> Result { - 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, - auth: AuthSession, Surreal>, - Form(form): Form, -) -> Result { - 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) + Router::new() + .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)) + .route("/chat/response-stream", get(get_response_stream)) + .route("/chat/reference/:id", get(show_reference_tooltip)) } diff --git a/crates/html-router/src/routes/chat/references.rs b/crates/html-router/src/routes/chat/references.rs index ccdceb4..32340a2 100644 --- a/crates/html-router/src/routes/chat/references.rs +++ b/crates/html-router/src/routes/chat/references.rs @@ -11,8 +11,10 @@ use common::{ use crate::{ html_state::HtmlState, - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, + middlewares::{ + auth_middleware::RequireUser, + response_middleware::{HtmlError, TemplateResponse}, + }, }; pub async fn show_reference_tooltip( diff --git a/crates/html-router/src/routes/content/handlers.rs b/crates/html-router/src/routes/content/handlers.rs new file mode 100644 index 0000000..ecc3595 --- /dev/null +++ b/crates/html-router/src/routes/content/handlers.rs @@ -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, +} + +pub async fn show_content_page( + State(state): State, + RequireUser(user): RequireUser, +) -> Result { + 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, + RequireUser(user): RequireUser, + Path(id): Path, +) -> Result { + 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, + RequireUser(user): RequireUser, + Path(id): Path, + Form(form): Form, +) -> Result { + 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, + }, + )) +} diff --git a/crates/html-router/src/routes/content/mod.rs b/crates/html-router/src/routes/content/mod.rs index 1bd6082..962dc7f 100644 --- a/crates/html-router/src/routes/content/mod.rs +++ b/crates/html-router/src/routes/content/mod.rs @@ -1,88 +1,19 @@ -use axum::{ - extract::{Path, State}, - response::IntoResponse, - Form, -}; -use serde::{Deserialize, Serialize}; +mod handlers; -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::{ - html_state::HtmlState, - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, -}; +use crate::html_state::HtmlState; -#[derive(Serialize)] -pub struct ContentPageData { - user: User, - text_contents: Vec, -} - -pub async fn show_content_page( - State(state): State, - RequireUser(user): RequireUser, -) -> Result { - 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, - RequireUser(user): RequireUser, - Path(id): Path, -) -> Result { - 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, - RequireUser(user): RequireUser, - Path(id): Path, - Form(form): Form, -) -> Result { - 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, - }, - )) +pub fn router() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + Router::new() + .route("/content", get(show_content_page)) + .route( + "/content/:id", + get(show_text_content_edit_form).patch(patch_text_content), + ) } diff --git a/crates/html-router/src/routes/documentation/mod.rs b/crates/html-router/src/routes/documentation/mod.rs deleted file mode 100644 index 17f1edb..0000000 --- a/crates/html-router/src/routes/documentation/mod.rs +++ /dev/null @@ -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, - current_path: String, -} - -pub async fn show_privacy_policy(auth: AuthSessionType) -> Result { - 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 { - 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 { - 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 { - Ok(TemplateResponse::new_template( - "documentation/index.html", - DocumentationPageData { - user: auth.current_user, - current_path: "/index".to_string(), - }, - )) -} diff --git a/crates/html-router/src/routes/gdpr.rs b/crates/html-router/src/routes/gdpr.rs deleted file mode 100644 index 6703d3f..0000000 --- a/crates/html-router/src/routes/gdpr.rs +++ /dev/null @@ -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() -} diff --git a/crates/html-router/src/routes/index.rs b/crates/html-router/src/routes/index/handlers.rs similarity index 97% rename from crates/html-router/src/routes/index.rs rename to crates/html-router/src/routes/index/handlers.rs index ccb40ae..5dbb6b0 100644 --- a/crates/html-router/src/routes/index.rs +++ b/crates/html-router/src/routes/index/handlers.rs @@ -1,5 +1,4 @@ use axum::{ - debug_handler, extract::{Path, State}, response::IntoResponse, }; @@ -7,8 +6,10 @@ use serde::Serialize; use tokio::join; use crate::{ - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, + middlewares::{ + auth_middleware::RequireUser, + response_middleware::{HtmlError, TemplateResponse}, + }, AuthSessionType, SessionType, }; use common::{ @@ -73,7 +74,6 @@ pub struct LatestTextContentData { user: User, } -#[debug_handler] pub async fn delete_text_content( State(state): State, RequireUser(user): RequireUser, diff --git a/crates/html-router/src/routes/index/mod.rs b/crates/html-router/src/routes/index/mod.rs new file mode 100644 index 0000000..3d25f59 --- /dev/null +++ b/crates/html-router/src/routes/index/mod.rs @@ -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() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + Router::new().route("/", get(index_handler)) +} + +pub fn protected_router() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + Router::new() + .route("/jobs/:job_id", delete(delete_job)) + .route("/active-jobs", get(show_active_jobs)) + .route("/text-content/:id", delete(delete_text_content)) +} diff --git a/crates/html-router/src/routes/ingress_form.rs b/crates/html-router/src/routes/ingestion/handlers.rs similarity index 95% rename from crates/html-router/src/routes/ingress_form.rs rename to crates/html-router/src/routes/ingestion/handlers.rs index e2a8fec..41d5abb 100644 --- a/crates/html-router/src/routes/ingress_form.rs +++ b/crates/html-router/src/routes/ingestion/handlers.rs @@ -18,9 +18,11 @@ use common::{ use crate::{ html_state::HtmlState, - middleware_auth::RequireUser, - routes::index::ActiveJobsData, - template_response::{HtmlError, TemplateResponse}, + middlewares::{ + auth_middleware::RequireUser, + response_middleware::{HtmlError, TemplateResponse}, + }, + routes::index::handlers::ActiveJobsData, }; pub async fn show_ingress_form( diff --git a/crates/html-router/src/routes/ingestion/mod.rs b/crates/html-router/src/routes/ingestion/mod.rs new file mode 100644 index 0000000..f5be7b5 --- /dev/null +++ b/crates/html-router/src/routes/ingestion/mod.rs @@ -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() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + Router::new() + .route( + "/ingress-form", + get(show_ingress_form).post(process_ingress_form), + ) + .route("/hide-ingress-form", get(hide_ingress_form)) +} diff --git a/crates/html-router/src/routes/knowledge/handlers.rs b/crates/html-router/src/routes/knowledge/handlers.rs new file mode 100644 index 0000000..18288e6 --- /dev/null +++ b/crates/html-router/src/routes/knowledge/handlers.rs @@ -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, + RequireUser(user): RequireUser, +) -> Result { + #[derive(Serialize)] + pub struct KnowledgeBaseData { + entities: Vec, + relationships: Vec, + 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 = node_positions.iter().map(|(x, _, _)| *x).collect(); + let node_y: Vec = node_positions.iter().map(|(_, y, _)| *y).collect(); + let node_z: Vec = 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::>(), + ) + .hover_template("Entity: %{text}
"); + + // 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: {}
", + 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, + RequireUser(user): RequireUser, + Path(id): Path, +) -> Result { + #[derive(Serialize)] + pub struct EntityData { + entity: KnowledgeEntity, + entity_types: Vec, + user: User, + } + + // Get entity types + let entity_types: Vec = 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, + user: User, +} + +pub async fn patch_knowledge_entity( + State(state): State, + RequireUser(user): RequireUser, + Form(form): Form, +) -> Result { + // 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, + RequireUser(user): RequireUser, + Path(id): Path, +) -> Result { + // 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::(&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, + relationships: Vec, +} + +pub async fn delete_knowledge_relationship( + State(state): State, + RequireUser(user): RequireUser, + Path(id): Path, +) -> Result { + // 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, + RequireUser(user): RequireUser, + Form(form): Form, +) -> Result { + // 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, + }, + )) +} diff --git a/crates/html-router/src/routes/knowledge/mod.rs b/crates/html-router/src/routes/knowledge/mod.rs index 3cbc365..54a2cde 100644 --- a/crates/html-router/src/routes/knowledge/mod.rs +++ b/crates/html-router/src/routes/knowledge/mod.rs @@ -1,289 +1,33 @@ +mod handlers; + use axum::{ - extract::{Path, State}, - response::IntoResponse, - Form, + extract::FromRef, + routing::{delete, get, post}, + Router, }; -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 handlers::{ + delete_knowledge_entity, delete_knowledge_relationship, patch_knowledge_entity, + save_knowledge_relationship, show_edit_knowledge_entity_form, show_knowledge_page, }; -use crate::{ - html_state::HtmlState, - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, -}; +use crate::html_state::HtmlState; -pub async fn show_knowledge_page( - State(state): State, - RequireUser(user): RequireUser, -) -> Result { - #[derive(Serialize)] - pub struct KnowledgeBaseData { - entities: Vec, - relationships: Vec, - 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 = node_positions.iter().map(|(x, _, _)| *x).collect(); - let node_y: Vec = node_positions.iter().map(|(_, y, _)| *y).collect(); - let node_z: Vec = 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::>(), +pub fn router() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + Router::new() + .route("/knowledge", get(show_knowledge_page)) + .route( + "/knowledge-entity/:id", + get(show_edit_knowledge_entity_form) + .delete(delete_knowledge_entity) + .patch(patch_knowledge_entity), ) - .hover_template("Entity: %{text}
"); - - // 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: {}
", - 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()), - ), + .route("/knowledge-relationship", post(save_knowledge_relationship)) + .route( + "/knowledge-relationship/:id", + delete(delete_knowledge_relationship), ) - .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, - RequireUser(user): RequireUser, - Path(id): Path, -) -> Result { - #[derive(Serialize)] - pub struct EntityData { - entity: KnowledgeEntity, - entity_types: Vec, - user: User, - } - - // Get entity types - let entity_types: Vec = 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, - user: User, -} - -pub async fn patch_knowledge_entity( - State(state): State, - RequireUser(user): RequireUser, - Form(form): Form, -) -> Result { - // 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, - RequireUser(user): RequireUser, - Path(id): Path, -) -> Result { - // 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::(&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, - relationships: Vec, -} - -pub async fn delete_knowledge_relationship( - State(state): State, - RequireUser(user): RequireUser, - Path(id): Path, -) -> Result { - // 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, - RequireUser(user): RequireUser, - Form(form): Form, -) -> Result { - // 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, - }, - )) } diff --git a/crates/html-router/src/routes/mod.rs b/crates/html-router/src/routes/mod.rs index 0b766e9..1ae8e9f 100644 --- a/crates/html-router/src/routes/mod.rs +++ b/crates/html-router/src/routes/mod.rs @@ -3,21 +3,17 @@ use std::sync::Arc; use axum::response::Html; use minijinja_autoreload::AutoReloader; -use crate::template_response::HtmlError; +use crate::middlewares::response_middleware::HtmlError; pub mod account; -pub mod admin_panel; +pub mod admin; +pub mod auth; pub mod chat; pub mod content; -pub mod documentation; -pub mod gdpr; pub mod index; -pub mod ingress_form; +pub mod ingestion; pub mod knowledge; -pub mod search_result; -pub mod signin; -pub mod signout; -pub mod signup; +pub mod search; // Helper function for render_template pub fn render_template( diff --git a/crates/html-router/src/routes/search_result.rs b/crates/html-router/src/routes/search/handlers.rs similarity index 89% rename from crates/html-router/src/routes/search_result.rs rename to crates/html-router/src/routes/search/handlers.rs index df2f778..1ec7b49 100644 --- a/crates/html-router/src/routes/search_result.rs +++ b/crates/html-router/src/routes/search/handlers.rs @@ -7,8 +7,10 @@ use serde::{Deserialize, Serialize}; use crate::{ html_state::HtmlState, - middleware_auth::RequireUser, - template_response::{HtmlError, TemplateResponse}, + middlewares::{ + auth_middleware::RequireUser, + response_middleware::{HtmlError, TemplateResponse}, + }, }; #[derive(Deserialize)] diff --git a/crates/html-router/src/routes/search/mod.rs b/crates/html-router/src/routes/search/mod.rs new file mode 100644 index 0000000..693f659 --- /dev/null +++ b/crates/html-router/src/routes/search/mod.rs @@ -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() -> Router +where + S: Clone + Send + Sync + 'static, + HtmlState: FromRef, +{ + Router::new().route("/search", get(search_result_handler)) +} diff --git a/crates/ingestion-pipeline/src/enricher.rs b/crates/ingestion-pipeline/src/enricher.rs index 6184df5..c2dee13 100644 --- a/crates/ingestion-pipeline/src/enricher.rs +++ b/crates/ingestion-pipeline/src/enricher.rs @@ -1,21 +1,15 @@ use std::sync::Arc; -use async_openai::{ - error::OpenAIError, - types::{ - ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, - CreateChatCompletionRequest, CreateChatCompletionRequestArgs, ResponseFormat, - ResponseFormatJsonSchema, - }, +use async_openai::types::{ + ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, + CreateChatCompletionRequest, CreateChatCompletionRequestArgs, ResponseFormat, + ResponseFormatJsonSchema, }; use common::{ error::AppError, storage::{ - db::SurrealDbClient, - types::{ - knowledge_entity::KnowledgeEntity, - system_settings::SystemSettings, - }, + db::SurrealDbClient, + types::{knowledge_entity::KnowledgeEntity, system_settings::SystemSettings}, }, }; use composite_retrieval::retrieve_entities; @@ -55,8 +49,9 @@ impl IngestionEnricher { .find_similar_entities(category, instructions, text, user_id) .await?; info!("got similar entitities"); - let llm_request = - self.prepare_llm_request(category, instructions, text, &similar_entities).await?; + let llm_request = self + .prepare_llm_request(category, instructions, text, &similar_entities) + .await?; self.perform_analysis(llm_request).await } @@ -83,7 +78,7 @@ impl IngestionEnricher { similar_entities: &[KnowledgeEntity], ) -> Result { let settings = SystemSettings::get_current(&self.db_client).await?; - + let entities_json = json!(similar_entities .iter() .map(|entity| { @@ -123,16 +118,16 @@ impl IngestionEnricher { ]) .response_format(response_format) .build()?; - + Ok(request) } - + async fn perform_analysis( &self, request: CreateChatCompletionRequest, ) -> Result { let response = self.openai_client.chat().create(request).await?; - + let content = response .choices .first() @@ -140,7 +135,7 @@ impl IngestionEnricher { .ok_or(AppError::LLMParsing( "No content found in LLM response".into(), ))?; - + serde_json::from_str::(content).map_err(|e| { AppError::LLMParsing(format!("Failed to parse LLM response into analysis: {}", e)) }) diff --git a/templates/chat/reference_list.html b/templates/chat/reference_list.html index 6fdfbe5..4bdb00c 100644 --- a/templates/chat/reference_list.html +++ b/templates/chat/reference_list.html @@ -69,7 +69,7 @@ // Load content if needed if (!tooltipContent) { - fetch(`/knowledge/${encodeURIComponent(reference)}`) + fetch(`/chat/reference/${encodeURIComponent(reference)}`) .then(response => response.text()) .then(html => { tooltipContent = html; diff --git a/todo.md b/todo.md index e55034d..3917459 100644 --- a/todo.md +++ b/todo.md @@ -1,7 +1,7 @@ \[\] archive ingressed webpage \[\] openai api key in config -\[\] option to set models, query and processing -\[\] template customization? +\[x\] option to set models, query and processing +\[x\] template customization? \[\] configs primarily get envs \[\] filtering on categories \[\] three js graph explorer