From c242aa010bffc1e90e5a635cc1b8e5aeaa001e69 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 21 Oct 2025 22:43:28 -0300 Subject: [PATCH] Enable template bot creation and fix bot schema --- migrations/6.0.5.sql | 1 + src/auth/mod.rs | 46 ++++++++++++++-- src/bootstrap/mod.rs | 76 ++++++++++++++++++++++++-- src/bot/mod.rs | 91 +++++++++++++++++++++++++++++--- src/llm_legacy/llm_local.rs | 6 +-- src/main.rs | 14 +++-- src/package_manager/installer.rs | 20 +++++-- src/shared/models.rs | 15 ++++-- 8 files changed, 236 insertions(+), 33 deletions(-) diff --git a/migrations/6.0.5.sql b/migrations/6.0.5.sql index 190e2cf3f..b5c81b027 100644 --- a/migrations/6.0.5.sql +++ b/migrations/6.0.5.sql @@ -1,6 +1,7 @@ -- Migration 6.0.5: Add update-summary.bas scheduled automation -- Description: Creates a scheduled automation that runs every minute to update summaries -- This replaces the announcements system in legacy mode +-- Note: Bots are now created dynamically during bootstrap based on template folders -- Add name column to system_automations if it doesn't exist ALTER TABLE public.system_automations ADD COLUMN IF NOT EXISTS name VARCHAR(255); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index b27e5d86e..17d13bc42 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -152,7 +152,20 @@ async fn auth_handler( web::Query(params): web::Query>, ) -> Result { let _token = params.get("token").cloned().unwrap_or_default(); - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); + + // Create or get anonymous user with proper UUID + let user_id = { + let mut sm = data.session_manager.lock().await; + match sm.get_or_create_anonymous_user(None) { + Ok(uid) => uid, + Err(e) => { + error!("Failed to create anonymous user: {}", e); + return Ok(HttpResponse::InternalServerError() + .json(serde_json::json!({"error": "Failed to create user"}))); + } + } + }; + let bot_id = if let Ok(bot_guid) = std::env::var("BOT_GUID") { match Uuid::parse_str(&bot_guid) { Ok(uuid) => uuid, @@ -163,8 +176,35 @@ async fn auth_handler( } } } else { - warn!("BOT_GUID not set in environment, using nil UUID"); - Uuid::nil() + // BOT_GUID not set, get first available bot from database + use crate::shared::models::schema::bots::dsl::*; + use diesel::prelude::*; + + let mut db_conn = data.conn.lock().unwrap(); + match bots + .filter(is_active.eq(true)) + .select(id) + .first::(&mut *db_conn) + .optional() + { + Ok(Some(first_bot_id)) => { + log::info!( + "BOT_GUID not set, using first available bot: {}", + first_bot_id + ); + first_bot_id + } + Ok(None) => { + error!("No active bots found in database"); + return Ok(HttpResponse::ServiceUnavailable() + .json(serde_json::json!({"error": "No bots available"}))); + } + Err(e) => { + error!("Failed to query bots: {}", e); + return Ok(HttpResponse::InternalServerError() + .json(serde_json::json!({"error": "Failed to query bots"}))); + } + } }; let session = { diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index 8d66f873c..0bac6b661 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -210,7 +210,12 @@ impl BootstrapManager { use aws_sdk_s3::config::Credentials; use aws_sdk_s3::config::Region; - info!("Uploading template bots to MinIO..."); + info!("Uploading template bots to MinIO and creating bot entries..."); + + // First, create bot entries in database for each template + let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| config.database_url()); + let mut conn = diesel::PgConnection::establish(&database_url)?; + self.create_bots_from_templates(&mut conn)?; let creds = Credentials::new( &config.minio.access_key, @@ -274,6 +279,71 @@ impl BootstrapManager { Ok(()) } + fn create_bots_from_templates(&self, conn: &mut diesel::PgConnection) -> Result<()> { + use crate::shared::models::schema::bots; + use diesel::prelude::*; + + info!("Creating bot entries from template folders..."); + + let templates_dir = Path::new("templates"); + if !templates_dir.exists() { + trace!("Templates directory not found, skipping bot creation"); + return Ok(()); + } + + // Walk through each .gbai folder in templates/ + for entry in std::fs::read_dir(templates_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() && path.extension().map(|e| e == "gbai").unwrap_or(false) { + let bot_folder = path.file_name().unwrap().to_string_lossy().to_string(); + // Remove .gbai extension to get bot name + let bot_name = bot_folder.trim_end_matches(".gbai"); + + // Format the name nicely (capitalize first letter of each word) + let formatted_name = bot_name + .split('_') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => { + first.to_uppercase().collect::() + chars.as_str() + } + } + }) + .collect::>() + .join(" "); + + // Check if bot already exists + let existing: Option = bots::table + .filter(bots::name.eq(&formatted_name)) + .select(bots::name) + .first(conn) + .optional()?; + + if existing.is_none() { + // Insert new bot + diesel::sql_query( + "INSERT INTO bots (id, name, description, llm_provider, llm_config, context_provider, context_config, is_active) \ + VALUES (gen_random_uuid(), $1, $2, 'openai', '{\"model\": \"gpt-4\", \"temperature\": 0.7}', 'database', '{}', true)" + ) + .bind::(&formatted_name) + .bind::(format!("Bot for {} template", bot_name)) + .execute(conn)?; + + info!("Created bot entry: {}", formatted_name); + } else { + trace!("Bot already exists: {}", formatted_name); + } + } + } + + info!("Bot creation from templates completed"); + Ok(()) + } + fn upload_directory_recursive<'a>( &'a self, client: &'a aws_sdk_s3::Client, @@ -324,8 +394,6 @@ impl BootstrapManager { } fn apply_migrations(&self, conn: &mut diesel::PgConnection) -> Result<()> { - use diesel::prelude::*; - info!("Applying database migrations..."); let migrations_dir = std::path::Path::new("migrations"); @@ -357,7 +425,7 @@ impl BootstrapManager { match std::fs::read_to_string(&path) { Ok(sql) => { trace!("Applying migration: {}", filename); - match diesel::sql_query(&sql).execute(conn) { + match conn.batch_execute(&sql) { Ok(_) => info!("Applied migration: {}", filename), Err(e) => { // Ignore errors for already applied migrations diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 7a0d8bd98..3f4d4ae0c 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -707,12 +707,25 @@ async fn websocket_handler( ) -> Result { let query = web::Query::>::from_query(req.query_string()).unwrap(); let session_id = query.get("session_id").cloned().unwrap(); - let user_id = query + let user_id_string = query .get("user_id") .cloned() .unwrap_or_else(|| Uuid::new_v4().to_string()) .replace("undefined", &Uuid::new_v4().to_string()); + // Ensure user exists in database before proceeding + let user_id = { + let user_uuid = Uuid::parse_str(&user_id_string).unwrap_or_else(|_| Uuid::new_v4()); + let mut sm = data.session_manager.lock().await; + match sm.get_or_create_anonymous_user(Some(user_uuid)) { + Ok(uid) => uid.to_string(), + Err(e) => { + error!("Failed to ensure user exists for WebSocket: {}", e); + user_id_string + } + } + }; + let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?; let (tx, mut rx) = mpsc::channel::(100); let orchestrator = BotOrchestrator::new(Arc::clone(&data)); @@ -729,7 +742,31 @@ async fn websocket_handler( .add_connection(session_id.clone(), tx.clone()) .await; - let bot_id = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()); + // Get first available bot from database + let bot_id = { + use crate::shared::models::schema::bots::dsl::*; + use diesel::prelude::*; + + let mut db_conn = data.conn.lock().unwrap(); + match bots + .filter(is_active.eq(true)) + .select(id) + .first::(&mut *db_conn) + .optional() + { + Ok(Some(first_bot_id)) => first_bot_id.to_string(), + Ok(None) => { + error!("No active bots found in database for WebSocket"); + return Err(actix_web::error::ErrorServiceUnavailable( + "No bots available", + )); + } + Err(e) => { + error!("Failed to query bots for WebSocket: {}", e); + return Err(actix_web::error::ErrorInternalServerError("Database error")); + } + } + }; orchestrator .send_event( @@ -802,8 +839,29 @@ async fn websocket_handler( match msg { WsMessage::Text(text) => { message_count += 1; - let bot_id = - std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()); + // Get first available bot from database + let bot_id = { + use crate::shared::models::schema::bots::dsl::*; + use diesel::prelude::*; + + let mut db_conn = data.conn.lock().unwrap(); + match bots + .filter(is_active.eq(true)) + .select(id) + .first::(&mut *db_conn) + .optional() + { + Ok(Some(first_bot_id)) => first_bot_id.to_string(), + Ok(None) => { + error!("No active bots found"); + continue; + } + Err(e) => { + error!("Failed to query bots: {}", e); + continue; + } + } + }; // Parse the text as JSON to extract the content field let json_value: serde_json::Value = match serde_json::from_str(&text) { @@ -840,8 +898,29 @@ async fn websocket_handler( } WsMessage::Close(_) => { - let bot_id = - std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()); + // Get first available bot from database + let bot_id = { + use crate::shared::models::schema::bots::dsl::*; + use diesel::prelude::*; + + let mut db_conn = data.conn.lock().unwrap(); + match bots + .filter(is_active.eq(true)) + .select(id) + .first::(&mut *db_conn) + .optional() + { + Ok(Some(first_bot_id)) => first_bot_id.to_string(), + Ok(None) => { + error!("No active bots found"); + "".to_string() + } + Err(e) => { + error!("Failed to query bots: {}", e); + "".to_string() + } + } + }; orchestrator .send_event( &user_id_clone, diff --git a/src/llm_legacy/llm_local.rs b/src/llm_legacy/llm_local.rs index ba7daddf6..a0a7f46fb 100644 --- a/src/llm_legacy/llm_local.rs +++ b/src/llm_legacy/llm_local.rs @@ -55,7 +55,7 @@ struct LlamaCppResponse { pub async fn ensure_llama_servers_running() -> Result<(), Box> { - let llm_local = env::var("LLM_LOCAL").unwrap_or_else(|_| "false".to_string()); + let llm_local = env::var("LLM_LOCAL").unwrap_or_else(|_| "true".to_string()); if llm_local.to_lowercase() != "true" { info!("ℹ️ LLM_LOCAL is not enabled, skipping local server startup"); @@ -215,7 +215,7 @@ async fn start_embedding_server( ) -> Result<(), Box> { let port = url.split(':').last().unwrap_or("8082"); - if cfg!(windows) { + if cfg!(windows) { let mut cmd = tokio::process::Command::new("cmd"); cmd.arg("/c").arg(format!( "cd {} && .\\llama-server.exe -m {} --host 0.0.0.0 --port {} --embedding --n-gpu-layers 99", @@ -230,7 +230,7 @@ async fn start_embedding_server( )); cmd.spawn()?; } - + Ok(()) } async fn is_server_running(url: &str) -> bool { diff --git a/src/main.rs b/src/main.rs index 6ae00a791..87e151555 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,15 +152,13 @@ async fn main() -> std::io::Result<()> { .await .expect("Failed to initialize LLM local server"); - let cache_url = cfg - .config_path("cache") - .join("redis.conf") - .display() - .to_string(); + let cache_url = std::env::var("CACHE_URL") + .or_else(|_| std::env::var("REDIS_URL")) + .unwrap_or_else(|_| "redis://localhost:6379".to_string()); let redis_client = match redis::Client::open(cache_url.as_str()) { Ok(client) => Some(Arc::new(client)), Err(e) => { - log::warn!("Failed to connect to Redis: {}", e); + log::warn!("Failed to connect to Redis: Redis URL did not parse- {}", e); None } }; @@ -272,7 +270,6 @@ async fn main() -> std::io::Result<()> { app = app .service(upload_file) .service(index) - .service(bot_index) .service(static_files) .service(websocket_handler) .service(auth_handler) @@ -284,7 +281,8 @@ async fn main() -> std::io::Result<()> { .service(start_session) .service(get_session_history) .service(chat_completions_local) - .service(embeddings_local); + .service(embeddings_local) + .service(bot_index); // Must be last - catches all remaining paths #[cfg(feature = "email")] { diff --git a/src/package_manager/installer.rs b/src/package_manager/installer.rs index 891101a6a..a6e5a4caf 100644 --- a/src/package_manager/installer.rs +++ b/src/package_manager/installer.rs @@ -115,8 +115,8 @@ impl PackageManager { if let Ok(mut conn) = PgConnection::establish(&database_url) { let system_bot_id = Uuid::parse_str("00000000-0000-0000-0000-000000000000")?; diesel::update(bots) - .filter(bot_id.eq(system_bot_id)) - .set(config.eq(serde_json::json!({ + .filter(id.eq(system_bot_id)) + .set(llm_config.eq(serde_json::json!({ "encrypted_drive_password": encrypted_drive_password, }))) .execute(&mut conn)?; @@ -165,8 +165,20 @@ impl PackageManager { "echo \"host all all all md5\" > {{CONF_PATH}}/pg_hba.conf".to_string(), "touch {{CONF_PATH}}/pg_ident.conf".to_string(), - format!("./bin/pg_ctl -D {{{{DATA_PATH}}}}/pgdata -l {{{{LOGS_PATH}}}}/postgres.log start; for i in 1 2 3 4 5 6 7 8 9 10; do ./bin/pg_isready -h localhost -p 5432 >/dev/null 2>&1 && break; echo 'Waiting for PostgreSQL to start...' >&2; sleep 1; done; ./bin/pg_isready -h localhost -p 5432"), - format!("PGPASSWORD={} ./bin/psql -h localhost -U gbuser -d postgres -c \"CREATE DATABASE botserver WITH OWNER gbuser\" 2>&1 | grep -v 'already exists' || true", db_password) + // Start PostgreSQL with wait flag + "./bin/pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start -w -t 30".to_string(), + + // Wait for PostgreSQL to be fully ready + "sleep 5".to_string(), + + // Check if PostgreSQL is accepting connections with retries + "for i in $(seq 1 30); do ./bin/pg_isready -h localhost -p 5432 -U gbuser >/dev/null 2>&1 && echo 'PostgreSQL is ready' && break || echo \"Waiting for PostgreSQL... attempt $i/30\" >&2; sleep 2; done".to_string(), + + // Final verification + "./bin/pg_isready -h localhost -p 5432 -U gbuser || { echo 'ERROR: PostgreSQL failed to start properly' >&2; cat {{LOGS_PATH}}/postgres.log >&2; exit 1; }".to_string(), + + // Create database (separate command to ensure previous steps completed) + format!("PGPASSWORD={} ./bin/psql -h localhost -p 5432 -U gbuser -d postgres -c \"CREATE DATABASE botserver WITH OWNER gbuser\" 2>&1 | grep -v 'already exists' || true", db_password) ], pre_install_cmds_macos: vec![], post_install_cmds_macos: vec![ diff --git a/src/shared/models.rs b/src/shared/models.rs index 72f7a34e0..9adcf8e46 100644 --- a/src/shared/models.rs +++ b/src/shared/models.rs @@ -236,13 +236,18 @@ pub mod schema { } diesel::table! { - bots (bot_id) { - bot_id -> Uuid, - name -> Text, - status -> Int4, - config -> Jsonb, + bots (id) { + id -> Uuid, + name -> Varchar, + description -> Nullable, + llm_provider -> Varchar, + llm_config -> Jsonb, + context_provider -> Varchar, + context_config -> Jsonb, created_at -> Timestamptz, updated_at -> Timestamptz, + is_active -> Nullable, + tenant_id -> Nullable, } }