in progress, routers and main split up

This commit is contained in:
Per Stark
2025-03-04 07:44:00 +01:00
parent 037bc52a64
commit 847571729b
80 changed files with 599 additions and 1577 deletions

179
Cargo.lock generated
View File

@@ -156,6 +156,21 @@ version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "api-router"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"axum_typed_multipart",
"common",
"futures",
"serde",
"tempfile",
"tokio",
"tracing",
]
[[package]]
name = "approx"
version = "0.4.0"
@@ -1029,6 +1044,50 @@ dependencies = [
"inout",
]
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"anyhow",
"async-openai",
"async-stream",
"axum",
"axum-htmx",
"axum_session",
"axum_session_auth",
"axum_session_surreal",
"axum_typed_multipart",
"chrono",
"chrono-tz",
"config",
"futures",
"json-stream-parser",
"lettre",
"mime",
"mime_guess",
"minijinja",
"minijinja-autoreload",
"minijinja-contrib",
"mockall",
"plotly",
"reqwest",
"scraper",
"serde",
"serde_json",
"sha2",
"surrealdb",
"tempfile",
"text-splitter",
"thiserror",
"tiktoken-rs",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -1981,6 +2040,35 @@ dependencies = [
"windows",
]
[[package]]
name = "html-router"
version = "0.1.0"
dependencies = [
"async-openai",
"async-stream",
"axum",
"axum-htmx",
"axum_session",
"axum_session_auth",
"axum_session_surreal",
"axum_typed_multipart",
"chrono-tz",
"common",
"futures",
"json-stream-parser",
"minijinja",
"minijinja-autoreload",
"minijinja-contrib",
"plotly",
"serde",
"serde_json",
"surrealdb",
"tempfile",
"tokio",
"tower-http",
"tracing",
]
[[package]]
name = "html5ever"
version = "0.27.0"
@@ -2626,6 +2714,53 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "main"
version = "0.1.0"
dependencies = [
"anyhow",
"api-router",
"async-openai",
"async-stream",
"axum",
"axum-htmx",
"axum_session",
"axum_session_auth",
"axum_session_surreal",
"axum_typed_multipart",
"chrono",
"chrono-tz",
"common",
"config",
"futures",
"html-router",
"json-stream-parser",
"lettre",
"mime",
"mime_guess",
"minijinja",
"minijinja-autoreload",
"minijinja-contrib",
"mockall",
"plotly",
"reqwest",
"scraper",
"serde",
"serde_json",
"sha2",
"surrealdb",
"tempfile",
"text-splitter",
"thiserror",
"tiktoken-rs",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
[[package]]
name = "maplit"
version = "1.0.2"
@@ -6034,47 +6169,3 @@ dependencies = [
"quote",
"syn 2.0.87",
]
[[package]]
name = "zettle_db"
version = "0.1.0"
dependencies = [
"anyhow",
"async-openai",
"async-stream",
"axum",
"axum-htmx",
"axum_session",
"axum_session_auth",
"axum_session_surreal",
"axum_typed_multipart",
"chrono",
"chrono-tz",
"config",
"futures",
"json-stream-parser",
"lettre",
"mime",
"mime_guess",
"minijinja",
"minijinja-autoreload",
"minijinja-contrib",
"mockall",
"plotly",
"reqwest",
"scraper",
"serde",
"serde_json",
"sha2",
"surrealdb",
"tempfile",
"text-splitter",
"thiserror",
"tiktoken-rs",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]

View File

@@ -1,54 +1,16 @@
[package]
name = "zettle_db"
version = "0.1.0"
edition = "2021"
[workspace]
members = [
"crates/main",
"crates/common",
"crates/api-router"
, "crates/html-router"]
resolver = "2"
[dependencies]
anyhow = "1.0.94"
async-openai = "0.24.1"
async-stream = "0.3.6"
axum = { version = "0.7.5", features = ["multipart", "macros"] }
axum-htmx = "0.6.0"
axum_session = "0.14.4"
axum_session_auth = "0.14.1"
axum_session_surreal = "0.2.1"
axum_typed_multipart = "0.12.1"
chrono = { version = "0.4.39", features = ["serde"] }
chrono-tz = "0.10.1"
config = "0.15.4"
futures = "0.3.31"
json-stream-parser = "0.1.4"
lettre = { version = "0.11.11", features = ["rustls-tls"] }
mime = "0.3.17"
mime_guess = "2.0.5"
minijinja = { version = "2.5.0", features = ["loader", "multi_template"] }
minijinja-autoreload = "2.5.0"
minijinja-contrib = { version = "2.6.0", features = ["datetime", "timezone"] }
mockall = "0.13.0"
plotly = "0.12.1"
reqwest = {version = "0.12.12", features = ["charset", "json"]}
scraper = "0.22.0"
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
sha2 = "0.10.8"
surrealdb = "2.0.4"
tempfile = "3.12.0"
text-splitter = "0.18.1"
thiserror = "1.0.63"
tiktoken-rs = "0.6.0"
[workspace.dependencies]
tokio = { version = "1.40.0", features = ["full"] }
tower-http = { version = "0.6.2", features = ["fs"] }
serde = { version = "1.0.210", features = ["derive"] }
axum = { version = "0.7.5", features = ["multipart", "macros"] }
serde_json = "1.0.128"
thiserror = "1.0.63"
anyhow = "1.0.94"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.10.0", features = ["v4", "serde"] }
[[bin]]
name = "server"
path = "src/bin/server.rs"
[[bin]]
name = "worker"
path = "src/bin/worker.rs"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
[package]
name = "api-router"
version = "0.1.0"
edition = "2021"
[dependencies]
# Workspace dependencies
tokio = { workspace = true }
serde = { workspace = true }
axum = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
tempfile = "3.12.0"
futures = "0.3.31"
axum_typed_multipart = "0.12.1"
common = { path = "../common" }

View File

@@ -0,0 +1,33 @@
use std::sync::Arc;
use common::{ingress::jobqueue::JobQueue, storage::db::SurrealDbClient, utils::config::AppConfig};
#[derive(Clone)]
pub struct ApiState {
pub surreal_db_client: Arc<SurrealDbClient>,
pub job_queue: Arc<JobQueue>,
}
impl ApiState {
pub async fn new(config: &AppConfig) -> Result<Self, Box<dyn std::error::Error>> {
let surreal_db_client = Arc::new(
SurrealDbClient::new(
&config.surrealdb_address,
&config.surrealdb_username,
&config.surrealdb_password,
&config.surrealdb_namespace,
&config.surrealdb_database,
)
.await?,
);
surreal_db_client.ensure_initialized().await?;
let app_state = ApiState {
surreal_db_client: surreal_db_client.clone(),
job_queue: Arc::new(JobQueue::new(surreal_db_client)),
};
Ok(app_state)
}
}

View File

@@ -0,0 +1,25 @@
use api_state::ApiState;
use axum::{
extract::{DefaultBodyLimit, FromRef},
middleware::from_fn_with_state,
routing::post,
Router,
};
use middleware_api_auth::api_auth;
use routes::ingress::ingress_data;
pub mod api_state;
mod middleware_api_auth;
mod routes;
/// Router for API functionality, version 1
pub fn api_routes_v1<S>(app_state: &ApiState) -> Router<S>
where
S: Clone + Send + Sync + 'static,
ApiState: FromRef<S>,
{
Router::new()
.route("/ingress", post(ingress_data))
.layer(DefaultBodyLimit::max(1024 * 1024 * 1024))
.route_layer(from_fn_with_state(app_state.clone(), api_auth))
}

View File

@@ -4,12 +4,12 @@ use axum::{
response::Response,
};
use crate::{error::ApiError, storage::types::user::User};
use common::{error::ApiError, storage::types::user::User};
use super::AppState;
use crate::api_state::ApiState;
pub async fn api_auth(
State(state): State<AppState>,
State(state): State<ApiState>,
mut request: Request,
next: Next,
) -> Result<Response, ApiError> {

View File

@@ -1,15 +1,16 @@
use crate::{
error::{ApiError, AppError},
ingress::types::ingress_input::{create_ingress_objects, IngressInput},
server::AppState,
storage::types::{file_info::FileInfo, user::User},
};
use axum::{extract::State, http::StatusCode, response::IntoResponse, Extension};
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
use common::{
error::{ApiError, AppError},
ingress::ingress_input::{create_ingress_objects, IngressInput},
storage::types::{file_info::FileInfo, user::User},
};
use futures::{future::try_join_all, TryFutureExt};
use tempfile::NamedTempFile;
use tracing::{debug, info};
use crate::api_state::ApiState;
#[derive(Debug, TryFromMultipart)]
pub struct IngressParams {
pub content: Option<String>,
@@ -21,7 +22,7 @@ pub struct IngressParams {
}
pub async fn ingress_data(
State(state): State<AppState>,
State(state): State<ApiState>,
Extension(user): Extension<User>,
TypedMultipart(input): TypedMultipart<IngressParams>,
) -> Result<impl IntoResponse, ApiError> {

View File

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

47
crates/common/Cargo.toml Normal file
View File

@@ -0,0 +1,47 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
# Workspace dependencies
tokio = { workspace = true }
serde = { workspace = true }
axum = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
serde_json = { workspace = true }
async-openai = "0.24.1"
async-stream = "0.3.6"
axum-htmx = "0.6.0"
axum_session = "0.14.4"
axum_session_auth = "0.14.1"
axum_session_surreal = "0.2.1"
axum_typed_multipart = "0.12.1"
chrono = { version = "0.4.39", features = ["serde"] }
chrono-tz = "0.10.1"
config = "0.15.4"
futures = "0.3.31"
json-stream-parser = "0.1.4"
lettre = { version = "0.11.11", features = ["rustls-tls"] }
mime = "0.3.17"
mime_guess = "2.0.5"
minijinja = { version = "2.5.0", features = ["loader", "multi_template"] }
minijinja-autoreload = "2.5.0"
minijinja-contrib = { version = "2.6.0", features = ["datetime", "timezone"] }
mockall = "0.13.0"
plotly = "0.12.1"
reqwest = {version = "0.12.12", features = ["charset", "json"]}
scraper = "0.22.0"
sha2 = "0.10.8"
surrealdb = "2.0.4"
tempfile = "3.12.0"
text-splitter = "0.18.1"
tiktoken-rs = "0.6.0"
tower-http = { version = "0.6.2", features = ["fs"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.10.0", features = ["v4", "serde"] }

View File

@@ -15,21 +15,17 @@ use crate::{
},
};
use super::{content_processor::ContentProcessor, types::ingress_object::IngressObject};
use super::{content_processor::ContentProcessor, ingress_object::IngressObject};
pub struct JobQueue {
pub db: Arc<SurrealDbClient>,
pub openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
}
pub const MAX_ATTEMPTS: u32 = 3;
impl JobQueue {
pub fn new(
db: Arc<SurrealDbClient>,
openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
) -> Self {
Self { db, openai_client }
pub fn new(db: Arc<SurrealDbClient>) -> Self {
Self { db }
}
/// Creates a new job and stores it in the database
@@ -147,6 +143,7 @@ impl JobQueue {
&self,
job: Job,
processor: &ContentProcessor,
openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
) -> Result<(), AppError> {
let current_attempts = match job.status {
JobStatus::InProgress { attempts, .. } => attempts + 1,
@@ -163,7 +160,7 @@ impl JobQueue {
)
.await?;
let text_content = job.content.to_text_content(&self.openai_client).await?;
let text_content = job.content.to_text_content(&openai_client).await?;
match processor.process(&text_content).await {
Ok(_) => {

View File

@@ -1,3 +1,6 @@
pub mod analysis;
pub mod content_processor;
pub mod ingress_input;
pub mod ingress_object;
pub mod jobqueue;
pub mod queue_task;

View File

@@ -1,4 +1,4 @@
use crate::ingress::types::ingress_object::IngressObject;
use crate::ingress::ingress_object::IngressObject;
use serde::Serialize;
#[derive(Serialize)]

View File

@@ -1,6 +1,6 @@
use uuid::Uuid;
use crate::{ingress::types::ingress_object::IngressObject, stored_object};
use crate::{ingress::ingress_object::IngressObject, stored_object};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum JobStatus {

View File

@@ -0,0 +1,35 @@
[package]
name = "html-router"
version = "0.1.0"
edition = "2021"
[dependencies]
# Workspace dependencies
tokio = { workspace = true }
serde = { workspace = true }
axum = { workspace = true }
tracing = { workspace = true }
serde_json = { workspace = true }
axum-htmx = "0.6.0"
axum_session = "0.14.4"
axum_session_auth = "0.14.1"
axum_session_surreal = "0.2.1"
axum_typed_multipart = "0.12.1"
futures = "0.3.31"
tempfile = "3.12.0"
async-stream = "0.3.6"
json-stream-parser = "0.1.4"
minijinja = { version = "2.5.0", features = ["loader", "multi_template"] }
minijinja-autoreload = "2.5.0"
minijinja-contrib = { version = "2.6.0", features = ["datetime", "timezone"] }
plotly = "0.12.1"
surrealdb = "2.0.4"
tower-http = { version = "0.6.2", features = ["fs"] }
chrono-tz = "0.10.1"
async-openai = "0.24.1"
common = { path = "../common" }

View File

@@ -1,21 +1,17 @@
use crate::ingress::jobqueue::JobQueue;
use crate::storage::db::SurrealDbClient;
use crate::utils::config::AppConfig;
use crate::utils::mailer::Mailer;
use axum_session::SessionStore;
use axum_session_surreal::SessionSurrealPool;
use common::ingress::jobqueue::JobQueue;
use common::storage::db::SurrealDbClient;
use common::utils::config::AppConfig;
use common::utils::mailer::Mailer;
use minijinja::{path_loader, Environment};
use minijinja_autoreload::AutoReloader;
use std::path::PathBuf;
use std::sync::Arc;
use surrealdb::engine::any::Any;
pub mod middleware_analytics;
pub mod middleware_api_auth;
pub mod routes;
#[derive(Clone)]
pub struct AppState {
pub struct HtmlState {
pub surreal_db_client: Arc<SurrealDbClient>,
pub openai_client: Arc<async_openai::Client<async_openai::config::OpenAIConfig>>,
pub templates: Arc<AutoReloader>,
@@ -24,10 +20,10 @@ pub struct AppState {
pub session_store: Arc<SessionStore<SessionSurrealPool<Any>>>,
}
impl AppState {
impl HtmlState {
pub async fn new(config: &AppConfig) -> Result<Self, Box<dyn std::error::Error>> {
let reloader = AutoReloader::new(move |notifier| {
let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
let template_path = get_templates_dir();
let mut env = Environment::new();
env.set_loader(path_loader(&template_path));
@@ -54,7 +50,7 @@ impl AppState {
let session_store = Arc::new(surreal_db_client.create_session_store().await?);
let app_state = AppState {
let app_state = HtmlState {
surreal_db_client: surreal_db_client.clone(),
templates: Arc::new(reloader),
openai_client: openai_client.clone(),
@@ -63,10 +59,30 @@ impl AppState {
&config.smtp_relayer,
&config.smtp_password,
)?),
job_queue: Arc::new(JobQueue::new(surreal_db_client, openai_client)),
job_queue: Arc::new(JobQueue::new(surreal_db_client)),
session_store,
};
Ok(app_state)
}
}
pub fn get_workspace_root() -> PathBuf {
// Starts from CARGO_MANIFEST_DIR (e.g., /project/crates/html-router/)
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
// Navigate up to /path/to/project/crates
let crates_dir = manifest_dir
.parent()
.expect("Failed to find parent of manifest directory");
// Navigate up to workspace root
crates_dir
.parent()
.expect("Failed to find workspace root")
.to_path_buf()
}
pub fn get_templates_dir() -> PathBuf {
get_workspace_root().join("templates")
}

View File

@@ -1,19 +1,20 @@
use api::{
ingress::ingress_data,
ingress_task::{delete_queue_task, get_queue_tasks},
query::query_handler,
queue_length::queue_length_handler,
};
pub mod html_state;
mod middleware_analytics;
mod routes;
use axum::{
extract::DefaultBodyLimit,
extract::FromRef,
middleware::from_fn_with_state,
routing::{delete, get, patch, post},
Router,
};
use axum_session::{SessionLayer, SessionStore};
use axum_session::SessionLayer;
use axum_session_auth::{AuthConfig, AuthSessionLayer};
use axum_session_surreal::SessionSurrealPool;
use html::{
use common::storage::types::user::User;
use html_state::HtmlState;
use middleware_analytics::analytics_middleware;
use routes::{
account::{delete_account, set_api_key, show_account_page, update_timezone},
admin_panel::{show_admin_panel, toggle_registration_status},
chat::{
@@ -40,29 +41,12 @@ use html::{
use surrealdb::{engine::any::Any, Surreal};
use tower_http::services::ServeDir;
use crate::storage::types::user::User;
use super::{middleware_analytics::analytics_middleware, middleware_api_auth::api_auth, AppState};
pub mod api;
pub mod html;
/// Router for API functionality, version 1
pub fn api_routes_v1(app_state: &AppState) -> Router<AppState> {
Router::new()
// Ingress routes
.route("/ingress", post(ingress_data))
.route("/message_count", get(queue_length_handler))
.route("/queue", get(get_queue_tasks))
.route("/queue/:delivery_tag", delete(delete_queue_task))
.layer(DefaultBodyLimit::max(1024 * 1024 * 1024))
// Query routes
.route("/query", post(query_handler))
.route_layer(from_fn_with_state(app_state.clone(), api_auth))
}
/// Router for HTML endpoints
pub fn html_routes(app_state: &AppState) -> Router<AppState> {
pub fn html_routes<S>(app_state: &HtmlState) -> Router<S>
where
S: Clone + Send + Sync + 'static,
HtmlState: FromRef<S>,
{
Router::new()
.route("/", get(index_handler))
.route("/gdpr/accept", post(accept_gdpr))

View File

@@ -6,12 +6,12 @@ use axum::{
use axum_session_surreal::SessionSurrealPool;
use surrealdb::engine::any::Any;
use crate::storage::types::analytics::Analytics;
use common::storage::types::analytics::Analytics;
use super::AppState;
use crate::html_state::HtmlState;
pub async fn analytics_middleware(
State(state): State<AppState>,
State(state): State<HtmlState>,
session: axum_session::Session<SessionSurrealPool<Any>>,
request: Request,
next: Next,

View File

@@ -10,14 +10,14 @@ use axum_session_surreal::SessionSurrealPool;
use chrono_tz::TZ_VARIANTS;
use surrealdb::{engine::any::Any, Surreal};
use crate::{
use common::{
error::{AppError, HtmlError},
page_data,
server::{routes::html::render_template, AppState},
storage::{db::delete_item, types::user::User},
};
use super::render_block;
use crate::{html_state::HtmlState, page_data};
use super::{render_block, render_template};
page_data!(AccountData, "auth/account_settings.html", {
user: User,
@@ -25,7 +25,7 @@ page_data!(AccountData, "auth/account_settings.html", {
});
pub async fn show_account_page(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
@@ -46,7 +46,7 @@ pub async fn show_account_page(
}
pub async fn set_api_key(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
@@ -83,7 +83,7 @@ pub async fn set_api_key(
}
pub async fn delete_account(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
@@ -109,7 +109,7 @@ pub struct UpdateTimezoneForm {
}
pub async fn update_timezone(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<UpdateTimezoneForm>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -7,14 +7,14 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use crate::{
use common::{
error::HtmlError,
page_data,
server::{routes::html::render_template, AppState},
storage::types::{analytics::Analytics, system_settings::SystemSettings, user::User},
};
use super::render_block;
use crate::{html_state::HtmlState, page_data};
use super::{render_block, render_template};
page_data!(AdminPanelData, "auth/admin_panel.html", {
user: User,
@@ -24,7 +24,7 @@ page_data!(AdminPanelData, "auth/admin_panel.html", {
});
pub async fn show_admin_panel(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated and admin
@@ -82,7 +82,7 @@ pub struct RegistrationToggleData {
}
pub async fn toggle_registration_status(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(input): Form<RegistrationToggleInput>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -21,14 +21,13 @@ use surrealdb::{engine::any::Any, Surreal};
use tokio::sync::{mpsc::channel, Mutex};
use tracing::{error, info};
use crate::{
use common::{
retrieval::{
combined_knowledge_entity_retrieval,
query_helper::{
create_chat_request, create_user_message, format_entities_json, LLMResponseFormat,
},
},
server::{routes::html::render_template, AppState},
storage::{
db::{get_item, store_item, SurrealDbClient},
types::{
@@ -38,6 +37,8 @@ use crate::{
},
};
use crate::{html_state::HtmlState, routes::render_template};
// Error handling function
fn create_error_stream(
message: impl Into<String>,
@@ -87,7 +88,7 @@ pub struct QueryParams {
}
pub async fn get_response_stream(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Query(params): Query<QueryParams>,
) -> Sse<Pin<Box<dyn Stream<Item = Result<Event, axum::Error>> + Send>>> {

View File

@@ -12,10 +12,8 @@ use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
use common::{
error::{AppError, HtmlError},
page_data,
server::{routes::html::render_template, AppState},
storage::{
db::{get_item, store_item},
types::{
@@ -26,6 +24,8 @@ use crate::{
},
};
use crate::{html_state::HtmlState, page_data, routes::render_template};
// Update your ChatStartParams struct to properly deserialize the references
#[derive(Debug, Deserialize)]
pub struct ChatStartParams {
@@ -52,7 +52,7 @@ page_data!(ChatData, "chat/base.html", {
});
pub async fn show_initialized_chat(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<ChatStartParams>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -116,7 +116,7 @@ pub async fn show_initialized_chat(
}
pub async fn show_chat_base(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying empty chat start");
@@ -151,7 +151,7 @@ pub struct NewMessageForm {
pub async fn show_existing_chat(
Path(conversation_id): Path<String>,
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
info!("Displaying initialized chat with id: {}", conversation_id);
@@ -189,7 +189,7 @@ pub async fn show_existing_chat(
pub async fn new_user_message(
Path(conversation_id): Path<String>,
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<NewMessageForm>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -241,7 +241,7 @@ pub async fn new_user_message(
}
pub async fn new_chat_user_message(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<NewMessageForm>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -8,17 +8,18 @@ use serde::Serialize;
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
use common::{
error::{AppError, HtmlError},
server::{routes::html::render_template, AppState},
storage::{
db::get_item,
types::{knowledge_entity::KnowledgeEntity, user::User},
},
};
use crate::{html_state::HtmlState, routes::render_template};
pub async fn show_reference_tooltip(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(reference_id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -6,13 +6,13 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use crate::{
use common::{
error::HtmlError,
page_data,
server::AppState,
storage::types::{text_content::TextContent, user::User},
};
use crate::{html_state::HtmlState, page_data};
use super::render_template;
page_data!(ContentPageData, "content/base.html", {
@@ -21,7 +21,7 @@ page_data!(ContentPageData, "content/base.html", {
});
pub async fn show_content_page(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
@@ -53,7 +53,7 @@ pub struct TextContentEditModal {
}
pub async fn show_text_content_edit_form(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -77,7 +77,7 @@ pub async fn show_text_content_edit_form(
}
pub async fn patch_text_content(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -3,12 +3,11 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use crate::{
error::HtmlError,
page_data,
server::{routes::html::render_template, AppState},
storage::types::user::User,
};
use common::{error::HtmlError, storage::types::user::User};
use crate::{html_state::HtmlState, page_data};
use super::render_template;
page_data!(DocumentationData, "do_not_use_this", {
user: Option<User>,
@@ -16,7 +15,7 @@ page_data!(DocumentationData, "do_not_use_this", {
});
pub async fn show_privacy_policy(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let output = render_template(
@@ -32,7 +31,7 @@ pub async fn show_privacy_policy(
}
pub async fn show_get_started(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let output = render_template(
@@ -47,7 +46,7 @@ pub async fn show_get_started(
Ok(output.into_response())
}
pub async fn show_mobile_friendly(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let output = render_template(
@@ -63,7 +62,7 @@ pub async fn show_mobile_friendly(
}
pub async fn show_documentation_index(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let output = render_template(

View File

@@ -3,7 +3,7 @@ use axum_session::Session;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::engine::any::Any;
use crate::error::HtmlError;
use common::error::HtmlError;
pub async fn accept_gdpr(
session: Session<SessionSurrealPool<Any>>,

View File

@@ -9,13 +9,8 @@ use surrealdb::{engine::any::Any, Surreal};
use tokio::join;
use tracing::info;
use crate::{
use common::{
error::{AppError, HtmlError},
page_data,
server::{
routes::html::{render_block, render_template},
AppState,
},
storage::{
db::{delete_item, get_item},
types::{
@@ -26,6 +21,10 @@ use crate::{
},
};
use crate::{html_state::HtmlState, page_data, routes::render_template};
use super::render_block;
page_data!(IndexData, "index/index.html", {
gdpr_accepted: bool,
user: Option<User>,
@@ -34,7 +33,7 @@ page_data!(IndexData, "index/index.html", {
});
pub async fn index_handler(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
session: Session<SessionSurrealPool<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -92,7 +91,7 @@ pub struct LatestTextContentData {
}
pub async fn delete_text_content(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -153,7 +152,7 @@ pub async fn delete_text_content(
// Helper function to get and validate text content
async fn get_and_validate_text_content(
state: &AppState,
state: &HtmlState,
id: &str,
user: &User,
) -> Result<TextContent, HtmlError> {
@@ -184,7 +183,7 @@ pub struct ActiveJobsData {
}
pub async fn delete_job(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -219,7 +218,7 @@ pub async fn delete_job(
}
pub async fn show_active_jobs(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {

View File

@@ -10,17 +10,18 @@ use surrealdb::{engine::any::Any, Surreal};
use tempfile::NamedTempFile;
use tracing::info;
use crate::{
use common::{
error::{AppError, HtmlError, IntoHtmlError},
ingress::types::ingress_input::{create_ingress_objects, IngressInput},
page_data,
server::{
routes::html::{index::ActiveJobsData, render_block},
AppState,
},
ingress::ingress_input::{create_ingress_objects, IngressInput},
storage::types::{file_info::FileInfo, user::User},
};
use crate::{
html_state::HtmlState,
page_data,
routes::{index::ActiveJobsData, render_block},
};
use super::render_template;
#[derive(Serialize)]
@@ -29,7 +30,7 @@ pub struct ShowIngressFormData {
}
pub async fn show_ingress_form(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
if !auth.is_authenticated() {
@@ -80,7 +81,7 @@ page_data!(IngressFormData, "ingress_form.html", {
});
pub async fn process_ingress_form(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
TypedMultipart(input): TypedMultipart<IngressParams>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -13,10 +13,8 @@ use plotly::{
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
use common::{
error::{AppError, HtmlError},
page_data,
server::{routes::html::render_template, AppState},
storage::{
db::delete_item,
types::{
@@ -27,6 +25,8 @@ use crate::{
},
};
use crate::{html_state::HtmlState, page_data, routes::render_template};
page_data!(KnowledgeBaseData, "knowledge/base.html", {
entities: Vec<KnowledgeEntity>,
relationships: Vec<KnowledgeRelationship>,
@@ -35,7 +35,7 @@ page_data!(KnowledgeBaseData, "knowledge/base.html", {
});
pub async fn show_knowledge_page(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {
// Early return if the user is not authenticated
@@ -152,7 +152,7 @@ pub struct EntityData {
}
pub async fn show_edit_knowledge_entity_form(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -201,7 +201,7 @@ pub struct PatchKnowledgeEntityParams {
}
pub async fn patch_knowledge_entity(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<PatchKnowledgeEntityParams>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -246,7 +246,7 @@ pub async fn patch_knowledge_entity(
}
pub async fn delete_knowledge_entity(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -288,7 +288,7 @@ pub struct RelationshipTableData {
}
pub async fn delete_knowledge_relationship(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, HtmlError> {
@@ -333,7 +333,7 @@ pub struct SaveKnowledgeRelationshipInput {
}
pub async fn save_knowledge_relationship(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<SaveKnowledgeRelationshipInput>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use axum::response::Html;
use minijinja_autoreload::AutoReloader;
use crate::error::{HtmlError, IntoHtmlError};
use common::error::{HtmlError, IntoHtmlError};
pub mod account;
pub mod admin_panel;
@@ -75,7 +75,7 @@ where
macro_rules! page_data {
($name:ident, $template_name:expr, {$($(#[$attr:meta])* $field:ident: $ty:ty),*$(,)?}) => {
use serde::{Serialize, Deserialize};
use $crate::server::routes::html::PageData;
use $crate::routes::PageData;
#[derive(Debug, Deserialize, Serialize)]
pub struct $name {

View File

@@ -8,12 +8,9 @@ use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
error::HtmlError,
retrieval::query_helper::get_answer_with_references,
server::{routes::html::render_template, AppState},
storage::types::user::User,
};
use common::{error::HtmlError, storage::types::user::User};
use crate::{html_state::HtmlState, routes::render_template};
#[derive(Deserialize)]
pub struct SearchParams {
query: String,
@@ -27,7 +24,7 @@ pub struct AnswerData {
}
pub async fn search_result_handler(
State(state): State<AppState>,
State(state): State<HtmlState>,
Query(query): Query<SearchParams>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -9,7 +9,9 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use crate::{error::HtmlError, page_data, server::AppState, storage::types::user::User};
use common::{error::HtmlError, storage::types::user::User};
use crate::{html_state::HtmlState, page_data};
use super::{render_block, render_template};
@@ -23,7 +25,7 @@ pub struct SignupParams {
page_data!(ShowSignInForm, "auth/signin_form.html", {});
pub async fn show_signin_form(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
HxBoosted(boosted): HxBoosted,
) -> Result<impl IntoResponse, HtmlError> {
@@ -48,7 +50,7 @@ pub async fn show_signin_form(
}
pub async fn authenticate_user(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<SignupParams>,
) -> Result<impl IntoResponse, HtmlError> {

View File

@@ -3,7 +3,7 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use crate::{error::ApiError, storage::types::user::User};
use common::{error::ApiError, storage::types::user::User};
pub async fn sign_out_user(
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,

View File

@@ -10,7 +10,9 @@ use axum_session_surreal::SessionSurrealPool;
use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use crate::{error::HtmlError, server::AppState, storage::types::user::User};
use common::{error::HtmlError, storage::types::user::User};
use crate::html_state::HtmlState;
use super::{render_block, render_template};
@@ -21,13 +23,8 @@ pub struct SignupParams {
pub timezone: String,
}
#[derive(Serialize)]
struct PageData {
// name: String,
}
pub async fn show_signup_form(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
HxBoosted(boosted): HxBoosted,
) -> Result<impl IntoResponse, HtmlError> {
@@ -35,24 +32,15 @@ pub async fn show_signup_form(
return Ok(Redirect::to("/").into_response());
}
let output = match boosted {
true => render_block(
"auth/signup_form.html",
"body",
PageData {},
state.templates.clone(),
)?,
false => render_template(
"auth/signup_form.html",
PageData {},
state.templates.clone(),
)?,
true => render_block("auth/signup_form.html", "body", {}, state.templates.clone())?,
false => render_template("auth/signup_form.html", {}, state.templates.clone())?,
};
Ok(output.into_response())
}
pub async fn process_signup_and_show_verification(
State(state): State<AppState>,
State(state): State<HtmlState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
Form(form): Form<SignupParams>,
) -> Result<impl IntoResponse, HtmlError> {

58
crates/main/Cargo.toml Normal file
View File

@@ -0,0 +1,58 @@
[package]
name = "main"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
axum = { workspace = true }
async-openai = "0.24.1"
async-stream = "0.3.6"
axum-htmx = "0.6.0"
axum_session = "0.14.4"
axum_session_auth = "0.14.1"
axum_session_surreal = "0.2.1"
axum_typed_multipart = "0.12.1"
chrono = { version = "0.4.39", features = ["serde"] }
chrono-tz = "0.10.1"
config = "0.15.4"
futures = "0.3.31"
json-stream-parser = "0.1.4"
lettre = { version = "0.11.11", features = ["rustls-tls"] }
mime = "0.3.17"
mime_guess = "2.0.5"
minijinja = { version = "2.5.0", features = ["loader", "multi_template"] }
minijinja-autoreload = "2.5.0"
minijinja-contrib = { version = "2.6.0", features = ["datetime", "timezone"] }
mockall = "0.13.0"
plotly = "0.12.1"
reqwest = {version = "0.12.12", features = ["charset", "json"]}
scraper = "0.22.0"
sha2 = "0.10.8"
surrealdb = "2.0.4"
tempfile = "3.12.0"
text-splitter = "0.18.1"
tiktoken-rs = "0.6.0"
tower-http = { version = "0.6.2", features = ["fs"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.2", features = ["serde"] }
uuid = { version = "1.10.0", features = ["v4", "serde"] }
# Reference to api-router
api-router = { path = "../api-router" }
html-router = { path = "../html-router" }
common = { path = "../common" }
[[bin]]
name = "server"
path = "src/server.rs"
[[bin]]
name = "worker"
path = "src/worker.rs"

47
crates/main/src/server.rs Normal file
View File

@@ -0,0 +1,47 @@
use api_router::{api_routes_v1, api_state::ApiState};
use axum::{extract::FromRef, Router};
use common::utils::config::get_config;
use html_router::{html_routes, html_state::HtmlState};
use tracing::info;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up tracing
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.try_init()
.ok();
// Get config
let config = get_config()?;
// Set up router states
let html_state = HtmlState::new(&config).await?;
let api_state = ApiState {
surreal_db_client: html_state.surreal_db_client.clone(),
job_queue: html_state.job_queue.clone(),
};
// Create Axum router
let app = Router::new()
.nest("/api/v1", api_routes_v1(&api_state))
.nest("/", html_routes(&html_state))
.with_state(AppState {
api_state,
html_state,
});
info!("Listening on 0.0.0.0:3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
#[derive(Clone, FromRef)]
struct AppState {
api_state: ApiState,
html_state: HtmlState,
}

View File

@@ -1,10 +1,6 @@
use std::sync::Arc;
use futures::StreamExt;
use surrealdb::Action;
use tracing::{error, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use zettle_db::{
use common::{
ingress::{
content_processor::ContentProcessor,
jobqueue::{JobQueue, MAX_ATTEMPTS},
@@ -15,6 +11,10 @@ use zettle_db::{
},
utils::config::get_config,
};
use futures::StreamExt;
use surrealdb::Action;
use tracing::{error, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -40,9 +40,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let openai_client = Arc::new(async_openai::Client::new());
let job_queue = JobQueue::new(surreal_db_client.clone(), openai_client.clone());
let job_queue = JobQueue::new(surreal_db_client.clone());
let content_processor = ContentProcessor::new(surreal_db_client, openai_client).await?;
let content_processor = ContentProcessor::new(surreal_db_client, openai_client.clone()).await?;
loop {
// First, check for any unfinished jobs
@@ -52,7 +52,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("Found {} unfinished jobs", unfinished_jobs.len());
for job in unfinished_jobs {
job_queue.process_job(job, &content_processor).await?;
job_queue
.process_job(job, &content_processor, openai_client.clone())
.await?;
}
}
@@ -68,7 +70,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
match notification.action {
Action::Create => {
if let Err(e) = job_queue
.process_job(notification.data, &content_processor)
.process_job(
notification.data,
&content_processor,
openai_client.clone(),
)
.await
{
error!("Error processing job: {}", e);
@@ -95,7 +101,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
JobStatus::Error(_) if attempts < MAX_ATTEMPTS => {
// This is a retry after an error
if let Err(e) = job_queue
.process_job(current_job, &content_processor)
.process_job(
current_job,
&content_processor,
openai_client.clone(),
)
.await
{
error!("Error processing job retry: {}", e);
@@ -114,7 +124,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
JobStatus::Created => {
// Shouldn't happen with Update action, but process if it does
if let Err(e) = job_queue
.process_job(notification.data, &content_processor)
.process_job(
notification.data,
&content_processor,
openai_client.clone(),
)
.await
{
error!("Error processing job: {}", e);

View File

@@ -1,57 +0,0 @@
{
inputs = {
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
systems.url = "github:nix-systems/default";
devenv.url = "github:cachix/devenv";
devenv.inputs.nixpkgs.follows = "nixpkgs";
};
nixConfig = {
extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
extra-substituters = "https://devenv.cachix.org";
};
outputs = {
self,
nixpkgs,
devenv,
systems,
...
} @ inputs: let
forEachSystem = nixpkgs.lib.genAttrs (import systems);
in {
packages = forEachSystem (system: {
devenv-up = self.devShells.${system}.default.config.procfileScript;
});
devShells =
forEachSystem
(system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
default = devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
# https://devenv.sh/reference/options/
enterShell = ''
echo "run devenv up -d to start and monitor services"
'';
languages.rust.enable = true;
services = {
redis = {
enable = true;
};
rabbitmq = {
enable = true;
plugins = ["tracing"];
};
};
}
];
};
});
};
}

View File

@@ -1,33 +0,0 @@
Your proposed structure for the API sounds solid and modular, making it easier to manage files and their relationships with other data. Heres a breakdown of how this can work and how it could be used with an iOS shortcut or a custom application:
API Structure
File Management Endpoints:
POST /file: Accepts a file upload and returns a unique identifier (ID) for that file.
PUT /file/{id}: Updates the metadata of the file identified by {id}.
DELETE /file/{id}: Deletes the file and its associated metadata from the database.
Data Ingress Endpoint:
POST /ingress: Accepts a JSON body containing references (IDs) to files and other necessary data, linking them in the database as needed.
Using with iOS Shortcuts
You can create shortcuts to interact with your API endpoints without the need for a full-fledged application. Here's how:
File Upload Shortcut:
Use the "Get File" action to select a file from the device.
Use the "Post" action to send a multipart/form-data request to the /file endpoint.
Parse the response to get the returned file ID.
Data Ingress Shortcut:
Use the "Ask for Input" action to gather the necessary fields for the ingress (like instructions, category, etc.) and the file ID(s).
Use another "Post" action to send this data to the /ingress endpoint as JSON.
Developing a CLI Tool
A CLI tool could also be developed for easier interaction with your API. This tool could:
Upload Files: Handle file uploads and return file IDs.
Link Data: Accept user input for instructions, category, and linked file IDs, then submit this data to the /ingress endpoint.
Additional Considerations
Error Handling: Ensure that both the upload and ingress endpoints handle errors gracefully and provide meaningful messages.
Documentation: Create clear API documentation to guide users in constructing requests correctly, whether they are using a shortcut, CLI, or custom application.
Authentication: Consider adding authentication to your API endpoints for security, especially if sensitive data is being handled.
This approach gives you the flexibility to support various clients, streamline interactions, and keep the server-side implementation clean and manageable. Would you like more specifics on any part of this setup?

View File

@@ -1,5 +0,0 @@
{% extends 'body_base.html' %}
{% block main %}
<main class="container justify-center flex-grow flex mx-auto mt-4 p-5">
</main>
{% endblock %}

View File

@@ -1,9 +0,0 @@
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
require('cssnano')({
preset: 'default', // Minify CSS
}),
],
};

View File

@@ -1,36 +0,0 @@
use axum::Router;
use tracing::info;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use zettle_db::{
server::{
routes::{api_routes_v1, html_routes},
AppState,
},
utils::config::get_config,
};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set up tracing
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.try_init()
.ok();
let config = get_config()?;
let app_state = AppState::new(&config).await?;
// Create Axum router
let app = Router::new()
.nest("/api/v1", api_routes_v1(&app_state))
.nest("/", html_routes(&app_state))
.with_state(app_state);
info!("Listening on 0.0.0.0:3000");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}

View File

@@ -1,4 +0,0 @@
pub mod analysis;
pub mod content_processor;
pub mod jobqueue;
pub mod types;

View File

@@ -1,34 +0,0 @@
use crate::{error::ApiError, server::AppState, storage::types::user::User};
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
Extension, Json,
};
pub async fn get_queue_tasks(
State(state): State<AppState>,
Extension(user): Extension<User>,
) -> Result<impl IntoResponse, ApiError> {
let user_tasks = state
.job_queue
.get_user_jobs(&user.id)
.await
.map_err(ApiError::from)?;
Ok(Json(user_tasks))
}
pub async fn delete_queue_task(
State(state): State<AppState>,
Extension(user): Extension<User>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, ApiError> {
state
.job_queue
.delete_job(&id, &user.id)
.await
.map_err(ApiError::from)?;
Ok(StatusCode::OK)
}

View File

@@ -1,4 +0,0 @@
pub mod ingress;
pub mod ingress_task;
pub mod query;
pub mod queue_length;

View File

@@ -1,34 +0,0 @@
use crate::{
error::ApiError, retrieval::query_helper::get_answer_with_references, server::AppState,
storage::types::user::User,
};
use axum::{extract::State, response::IntoResponse, Extension, Json};
use serde::Deserialize;
use tracing::info;
#[derive(Debug, Deserialize)]
pub struct QueryInput {
query: String,
}
pub async fn query_handler(
State(state): State<AppState>,
Extension(user): Extension<User>,
Json(query): Json<QueryInput>,
) -> Result<impl IntoResponse, ApiError> {
info!("Received input: {:?}", query);
info!("{:?}", user);
let answer = get_answer_with_references(
&state.surreal_db_client,
&state.openai_client,
&query.query,
&user.id,
)
.await?;
Ok(
Json(serde_json::json!({"answer": answer.content, "references": answer.references}))
.into_response(),
)
}

View File

@@ -1,29 +0,0 @@
use axum::{extract::State, http::StatusCode, response::IntoResponse};
use tracing::info;
use crate::{
error::{ApiError, AppError},
server::AppState,
storage::{db::get_all_stored_items, types::job::Job},
};
pub async fn queue_length_handler(
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiError> {
info!("Getting queue length");
let queue_length = get_all_stored_items::<Job>(&state.surreal_db_client)
.await
.map_err(AppError::from)?
.len();
info!("Queue length: {}", queue_length);
state
.mailer
.send_email_verification("per@starks.cloud", "1001010", &state.templates)
.map_err(AppError::from)?;
// Return the queue length with a 200 OK status
Ok((StatusCode::OK, queue_length.to_string()))
}