From 9c36aa10fabe76b949c0a606f8c41cd2eb2ed9c2 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 20 Oct 2025 23:32:49 -0300 Subject: [PATCH] - More automation from start to web, user sessions. --- Cargo.lock | 2 +- Cargo.toml | 62 +++--- README.md | 116 +++++++++- gbot.sh | 2 +- src/automation/mod.rs | 77 ++++++- src/bootstrap/mod.rs | 176 +++++++++++++++ src/bot/mod.rs | 27 ++- src/config/mod.rs | 207 +++++++++++++----- src/drive_monitor/mod.rs | 12 +- src/main.rs | 40 ++-- src/package_manager/cli.rs | 74 ++++++- src/package_manager/installer.rs | 4 +- src/session/mod.rs | 48 ++-- src/web_server/mod.rs | 13 ++ .../announcements.gbdialog/auth.bas | 69 +++++- .../announcements.gbdialog/start.bas | 18 +- web/index.html | 85 ++++--- 17 files changed, 852 insertions(+), 180 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d60a3c95..1c4f78dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1009,7 +1009,7 @@ dependencies = [ [[package]] name = "botserver" -version = "6.0.4" +version = "6.0.5" dependencies = [ "actix-cors", "actix-multipart", diff --git a/Cargo.toml b/Cargo.toml index dcad4ee0..7cf562a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,39 +1,41 @@ [package] name = "botserver" -version = "6.0.4" +version = "6.0.5" edition = "2021" authors = [ - "@AlanPerdomo", - "@AnaPaulaGil", - "@arenasio", - "@AtyllaL", - "@christopherdecastilho", - "@danielolima96", - "@Dariojunior3", - "@davidlerner26", - "@ExperimentationGarage", - "@flavioandrade91", - "@HeraldoAlmeida", - "@joao-parana", - "@jonathasc", - "@jramos-br", - "@lpicanco", - "@marcosvelasco", - "@matheus39x", - "@oerlabshenrique", - "@othonlima", - "@PH-Nascimento", - "@phpussente", - "@RobsonDantasE", - "@rodrigorodriguez", - "@SarahLourenco", - "@thipatriota", - "@webgus", - "@ZuilhoSe", + "Pragmatismo.com.br ", + "General Bots Community ", + "Rodrigo Rodriguez ", + "Alan Perdomo", + "Ana Paula Gil", + "Arenas.io", + "Atylla L", + "Christopher de Castilho", + "Dario Lima", + "Dario Junior", + "David Lerner", + "Experimentation Garage", + "Flavio Andrade", + "Heraldo Almeida", + "Joao Parana", + "Jonathas C", + "J Ramos", + "Lucas Picanco", + "Marcos Velasco", + "Matheus 39x", + "Oerlabs Henrique", + "Othon Lima", + "PH Nascimento", + "Phpussente", + "Robson Dantas", + "Sarah Lourenco", + "Thi Patriota", + "Webgus", + "Zuilho Se", ] -description = "General Bots Server" +description = "General Bots Server - Open-source bot platform by Pragmatismo.com.br" license = "AGPL-3.0" -repository = "https://alm.pragmatismo.com.br/generalbots/botserver" +repository = "https://github.com/GeneralBots/BotServer" [features] default = ["vectordb"] diff --git a/README.md b/README.md index 5fe19065..b5634886 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,13 @@ editor). LLM and BASIC can be mixed used to build custom dialogs so Bot can be e Before you embark on your General Bots journey, ensure you have the following tools installed: -- **Node.js (version 20 or later)**: General Bots leverages the latest features of Node.js to provide a robust and efficient runtime environment. Download it from [nodejs.org](https://nodejs.org/en/download/). +- **Rust (latest stable version)**: General Bots server is built with Rust for performance and safety. Install from [rustup.rs](https://rustup.rs/). - **Git (latest stable version)**: Essential for version control and collaborating on bot projects. Get it from [git-scm.com](https://git-scm.com/downloads). -### Quick Start Guide +**Optional (for Node.js bots):** +- **Node.js (version 20 or later)**: For Node.js-based bot packages. Download from [nodejs.org](https://nodejs.org/en/download/). + +### Quick Start Guide (Rust Version) Follow these steps to get your General Bots server up and running: @@ -71,24 +74,115 @@ Follow these steps to get your General Bots server up and running: ``` This changes your current directory to the newly cloned BotServer folder. -3. Install dependencies and start the server: +3. Create your configuration file: ```bash - npm install - npm run start + cp .env.example .env ``` - The `npm install` command installs all necessary dependencies for the project. `npm run start` builds your bot server locally and serves it through a development server. + Edit `.env` with your actual configuration values. See [Configuration](#configuration) section below. + +4. Run the server: + ```bash + cargo run + ``` + On first run, BotServer will automatically: + - Install required components (PostgreSQL, MinIO, Redis, LLM) + - Set up the database with migrations + - Download AI models + - Upload template bots from `templates/` folder + - Start the HTTP server on `http://127.0.0.1:8080` (or your configured port) + +**Alternative - Build release version:** +```bash +cargo build --release +./target/release/botserver +``` + +**Management Commands:** +```bash +botserver start # Start all components +botserver stop # Stop all components +botserver restart # Restart all components +botserver list # List available components +botserver status # Check component status +botserver install # Install optional component +``` ### Accessing Your Bot -Once the server is running, you can access your bot at `http://localhost:4242/`. This local server allows you to interact with your bot and test its functionality in real-time. If you want to publish -without password, define [ADMIN_OPEN_PUBLISH](https://github.com/GeneralBots/BotBook/master/docs/chapter-07-gbot-reference#enviroment-variables-reference) as true in BotServer .env file. +Once the server is running, you can access your bot at `http://localhost:8080/` (or your configured `SERVER_PORT`). This local server allows you to interact with your bot and test its functionality in real-time. -To publish bot packages and initiate a conversation with the bot, use the command: +**Anonymous Access:** Every visitor automatically gets a unique session tracked by cookie. No login required to start chatting! +**Authentication:** Users can optionally register/login at `/static/auth/login.html` to save conversations across devices. + +**About Page:** Visit `/static/about/index.html` to learn more about BotServer and its maintainers. + +### Configuration + +BotServer uses environment variables for configuration. Copy `.env.example` to `.env` and customize: + +#### Required Settings (Auto-installed on first run) +```bash +# Database (PostgreSQL - auto-installed) +TABLES_SERVER=localhost +TABLES_PORT=5432 +TABLES_DATABASE=botserver +TABLES_USERNAME=gbuser +TABLES_PASSWORD=changeme + +# Storage (MinIO - auto-installed) +DRIVE_SERVER=http://localhost:9000 +DRIVE_ACCESSKEY=minioadmin +DRIVE_SECRET=minioadmin +DRIVE_ORG_PREFIX=botserver- + +# Cache (Redis - auto-installed) +CACHE_URL=redis://localhost:6379 + +# LLM (llama.cpp - auto-installed with models) +LLM_LOCAL=false +LLM_URL=http://localhost:8081/v1 +EMBEDDING_URL=http://localhost:8082 ``` -/publish + +#### Optional Settings +```bash +# Server Configuration +SERVER_HOST=127.0.0.1 +SERVER_PORT=8080 + +# External AI API (Groq, OpenAI, Azure, etc.) +AI_KEY=your-api-key-here +AI_ENDPOINT=https://api.groq.com/openai/v1/chat/completions +AI_LLM_MODEL=openai/gpt-4 + +# Email (for notifications) +EMAIL_FROM=bot@example.com +EMAIL_SERVER=smtp.example.com +EMAIL_PORT=587 +EMAIL_USER=your-email@example.com +EMAIL_PASS=your-password ``` -This command prepares your bot packages for use and allows you to start interacting with your bot immediately. + +#### Legacy Mode (Use existing infrastructure) +If you already have PostgreSQL, MinIO, etc. running, set these in `.env`: +```bash +# Existing database +TABLES_SERVER=your-db-host +TABLES_USERNAME=your-username +TABLES_PASSWORD=your-password + +# Existing MinIO/S3 +DRIVE_SERVER=https://your-minio-host +DRIVE_ACCESSKEY=your-access-key +DRIVE_SECRET=your-secret-key + +# Existing AI endpoint +AI_ENDPOINT=https://your-llm-endpoint +AI_KEY=your-api-key +``` + +BotServer will detect existing infrastructure and skip auto-installation. ## Development Workflow diff --git a/gbot.sh b/gbot.sh index f2081cd9..5e232670 100755 --- a/gbot.sh +++ b/gbot.sh @@ -1,3 +1,3 @@ set +e -pkill postgres && rm .env -rf botserver-stack && clear && \ +clear && \ RUST_LOG=trace,hyper_util=off cargo run diff --git a/src/automation/mod.rs b/src/automation/mod.rs index 5a370288..b9a28760 100644 --- a/src/automation/mod.rs +++ b/src/automation/mod.rs @@ -325,9 +325,80 @@ impl AutomationService { content } Err(e) => { - error!("Failed to read script {}: {}", full_path.display(), e); - self.cleanup_job_flag(&bot_id, param).await; - return; + warn!( + "Script not found locally at {}, attempting to download from MinIO: {}", + full_path.display(), + e + ); + + // Try to download from MinIO + if let Some(s3_client) = &self.state.s3_client { + let bucket_name = format!( + "{}{}.gbai", + env::var("MINIO_ORG_PREFIX").unwrap_or_else(|_| "org1_".to_string()), + env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()) + ); + let s3_key = format!(".gbdialog/{}", param); + + trace!("Downloading from bucket={} key={}", bucket_name, s3_key); + + match s3_client + .get_object() + .bucket(&bucket_name) + .key(&s3_key) + .send() + .await + { + Ok(response) => { + match response.body.collect().await { + Ok(data) => { + match String::from_utf8(data.into_bytes().to_vec()) { + Ok(content) => { + info!("Downloaded script '{}' from MinIO", param); + + // Save to local cache + if let Err(e) = + std::fs::create_dir_all(&self.scripts_dir) + { + warn!("Failed to create scripts directory: {}", e); + } else if let Err(e) = + tokio::fs::write(&full_path, &content).await + { + warn!("Failed to cache script locally: {}", e); + } else { + trace!("Cached script to {}", full_path.display()); + } + + content + } + Err(e) => { + error!("Failed to decode script {}: {}", param, e); + self.cleanup_job_flag(&bot_id, param).await; + return; + } + } + } + Err(e) => { + error!( + "Failed to read script body from MinIO {}: {}", + param, e + ); + self.cleanup_job_flag(&bot_id, param).await; + return; + } + } + } + Err(e) => { + error!("Failed to download script {} from MinIO: {}", param, e); + self.cleanup_job_flag(&bot_id, param).await; + return; + } + } + } else { + error!("S3 client not available, cannot download script {}", param); + self.cleanup_job_flag(&bot_id, param).await; + return; + } } }; diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index 83f8d144..8d66f873 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -7,6 +7,7 @@ use dotenvy::dotenv; use log::{info, trace}; use rand::distr::Alphanumeric; use sha2::{Digest, Sha256}; +use std::path::Path; pub struct BootstrapManager { pub install_mode: InstallMode, @@ -91,6 +92,12 @@ impl BootstrapManager { match diesel::PgConnection::establish(&database_url) { Ok(mut conn) => { info!("Successfully connected to legacy database, loading configuration"); + + // Apply migrations + if let Err(e) = self.apply_migrations(&mut conn) { + log::warn!("Failed to apply migrations: {}", e); + } + return Ok(AppConfig::from_database(&mut conn)); } Err(e) => { @@ -198,4 +205,173 @@ impl BootstrapManager { hasher.update(password.as_bytes()); format!("{:x}", hasher.finalize()) } + + pub async fn upload_templates_to_minio(&self, config: &AppConfig) -> Result<()> { + use aws_sdk_s3::config::Credentials; + use aws_sdk_s3::config::Region; + + info!("Uploading template bots to MinIO..."); + + let creds = Credentials::new( + &config.minio.access_key, + &config.minio.secret_key, + None, + None, + "minio", + ); + + let s3_config = aws_sdk_s3::Config::builder() + .credentials_provider(creds) + .endpoint_url(&config.minio.server) + .region(Region::new("us-east-1")) + .force_path_style(true) + .behavior_version(aws_sdk_s3::config::BehaviorVersion::latest()) + .build(); + + let client = aws_sdk_s3::Client::from_conf(s3_config); + + // Upload templates from templates/ directory + let templates_dir = Path::new("templates"); + if !templates_dir.exists() { + trace!("Templates directory not found, skipping upload"); + 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_name = path.file_name().unwrap().to_string_lossy().to_string(); + let bucket_name = format!("{}{}", config.minio.org_prefix, bot_name); + + trace!("Creating bucket: {}", bucket_name); + + // Create bucket if it doesn't exist + match client.create_bucket().bucket(&bucket_name).send().await { + Ok(_) => info!("Created bucket: {}", bucket_name), + Err(e) => { + let err_str = e.to_string(); + if err_str.contains("BucketAlreadyOwnedByYou") + || err_str.contains("BucketAlreadyExists") + { + trace!("Bucket {} already exists", bucket_name); + } else { + log::warn!("Failed to create bucket {}: {}", bucket_name, e); + } + } + } + + // Upload all files recursively + self.upload_directory_recursive(&client, &path, &bucket_name, "") + .await?; + info!("Uploaded template bot: {}", bot_name); + } + } + + info!("Template bots uploaded successfully"); + Ok(()) + } + + fn upload_directory_recursive<'a>( + &'a self, + client: &'a aws_sdk_s3::Client, + local_path: &'a Path, + bucket: &'a str, + prefix: &'a str, + ) -> std::pin::Pin> + 'a>> { + Box::pin(async move { + use aws_sdk_s3::primitives::ByteStream; + + for entry in std::fs::read_dir(local_path)? { + let entry = entry?; + let path = entry.path(); + let file_name = path.file_name().unwrap().to_string_lossy().to_string(); + let key = if prefix.is_empty() { + file_name.clone() + } else { + format!("{}/{}", prefix, file_name) + }; + + if path.is_file() { + trace!( + "Uploading file: {} to bucket: {} with key: {}", + path.display(), + bucket, + key + ); + + let body = ByteStream::from_path(&path).await?; + + client + .put_object() + .bucket(bucket) + .key(&key) + .body(body) + .send() + .await?; + + trace!("Uploaded: {}", key); + } else if path.is_dir() { + self.upload_directory_recursive(client, &path, bucket, &key) + .await?; + } + } + + Ok(()) + }) + } + + fn apply_migrations(&self, conn: &mut diesel::PgConnection) -> Result<()> { + use diesel::prelude::*; + + info!("Applying database migrations..."); + + let migrations_dir = std::path::Path::new("migrations"); + if !migrations_dir.exists() { + trace!("No migrations directory found, skipping"); + return Ok(()); + } + + // Get all .sql files sorted + let mut sql_files: Vec<_> = std::fs::read_dir(migrations_dir)? + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry + .path() + .extension() + .and_then(|s| s.to_str()) + .map(|s| s == "sql") + .unwrap_or(false) + }) + .collect(); + + sql_files.sort_by_key(|entry| entry.path()); + + for entry in sql_files { + let path = entry.path(); + let filename = path.file_name().unwrap().to_string_lossy(); + + trace!("Reading migration: {}", filename); + match std::fs::read_to_string(&path) { + Ok(sql) => { + trace!("Applying migration: {}", filename); + match diesel::sql_query(&sql).execute(conn) { + Ok(_) => info!("Applied migration: {}", filename), + Err(e) => { + // Ignore errors for already applied migrations + trace!("Migration {} result: {}", filename, e); + } + } + } + Err(e) => { + log::warn!("Failed to read migration {}: {}", filename, e); + } + } + } + + info!("Migrations check completed"); + Ok(()) + } } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index e67679b1..7a0d8bd9 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -556,10 +556,14 @@ impl BotOrchestrator { "Running start script for session: {} with token: {:?}", session.id, token ); - let start_script_path = "./templates/announcements.gbai/announcements.gbdialog/start.bas"; - let start_script = match std::fs::read_to_string(start_script_path) { + let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()); + let start_script_path = format!("./{}.gbai/.gbdialog/start.bas", bot_guid); + let start_script = match std::fs::read_to_string(&start_script_path) { Ok(content) => content, - Err(_) => r#"TALK "Error loading script file.""#.to_string(), + Err(_) => { + warn!("start.bas not found at {}, skipping", start_script_path); + return Ok(true); + } }; debug!( "Start script content for session {}: {}", @@ -706,7 +710,8 @@ async fn websocket_handler( let user_id = query .get("user_id") .cloned() - .unwrap_or_else(|| "default_user".to_string()); + .unwrap_or_else(|| Uuid::new_v4().to_string()) + .replace("undefined", &Uuid::new_v4().to_string()); let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?; let (tx, mut rx) = mpsc::channel::(100); @@ -747,6 +752,20 @@ async fn websocket_handler( session_id, user_id ); + // Trigger auto welcome (start.bas) + let orchestrator_clone = BotOrchestrator::new(Arc::clone(&data)); + let user_id_welcome = user_id.clone(); + let session_id_welcome = session_id.clone(); + let bot_id_welcome = bot_id.clone(); + actix_web::rt::spawn(async move { + if let Err(e) = orchestrator_clone + .trigger_auto_welcome(&session_id_welcome, &user_id_welcome, &bot_id_welcome, None) + .await + { + warn!("Failed to trigger auto welcome: {}", e); + } + }); + let web_adapter = data.web_adapter.clone(); let session_id_clone1 = session_id.clone(); let session_id_clone2 = session_id.clone(); diff --git a/src/config/mod.rs b/src/config/mod.rs index 03ac543c..a0fccaca 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -79,14 +79,22 @@ impl AppConfig { pub fn database_url(&self) -> String { format!( "postgres://{}:{}@{}:{}/{}", - self.database.username, self.database.password, self.database.server, self.database.port, self.database.database + self.database.username, + self.database.password, + self.database.server, + self.database.port, + self.database.database ) } pub fn database_custom_url(&self) -> String { format!( "postgres://{}:{}@{}:{}/{}", - self.database_custom.username, self.database_custom.password, self.database_custom.server, self.database_custom.port, self.database_custom.database + self.database_custom.username, + self.database_custom.password, + self.database_custom.server, + self.database_custom.port, + self.database_custom.database ) } @@ -125,41 +133,69 @@ impl AppConfig { }; let get_str = |key: &str, default: &str| -> String { - config_map.get(key).map(|v| v.config_value.clone()).unwrap_or_else(|| default.to_string()) + config_map + .get(key) + .map(|v| v.config_value.clone()) + .unwrap_or_else(|| default.to_string()) }; let get_u32 = |key: &str, default: u32| -> u32 { - config_map.get(key).and_then(|v| v.config_value.parse().ok()).unwrap_or(default) + config_map + .get(key) + .and_then(|v| v.config_value.parse().ok()) + .unwrap_or(default) }; let get_u16 = |key: &str, default: u16| -> u16 { - config_map.get(key).and_then(|v| v.config_value.parse().ok()).unwrap_or(default) + config_map + .get(key) + .and_then(|v| v.config_value.parse().ok()) + .unwrap_or(default) }; let get_bool = |key: &str, default: bool| -> bool { - config_map.get(key).map(|v| v.config_value.to_lowercase() == "true").unwrap_or(default) + config_map + .get(key) + .map(|v| v.config_value.to_lowercase() == "true") + .unwrap_or(default) }; let stack_path = PathBuf::from(get_str("STACK_PATH", "./botserver-stack")); + // For database credentials, prioritize environment variables over database values + // because we need the correct credentials to connect to the database in the first place let database = DatabaseConfig { - username: get_str("TABLES_USERNAME", "gbuser"), - password: get_str("TABLES_PASSWORD", ""), - server: get_str("TABLES_SERVER", "localhost"), - port: get_u32("TABLES_PORT", 5432), - database: get_str("TABLES_DATABASE", "botserver"), + username: std::env::var("TABLES_USERNAME") + .unwrap_or_else(|_| get_str("TABLES_USERNAME", "gbuser")), + password: std::env::var("TABLES_PASSWORD") + .unwrap_or_else(|_| get_str("TABLES_PASSWORD", "")), + server: std::env::var("TABLES_SERVER") + .unwrap_or_else(|_| get_str("TABLES_SERVER", "localhost")), + port: std::env::var("TABLES_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or_else(|| get_u32("TABLES_PORT", 5432)), + database: std::env::var("TABLES_DATABASE") + .unwrap_or_else(|_| get_str("TABLES_DATABASE", "botserver")), }; let database_custom = DatabaseConfig { - username: get_str("CUSTOM_USERNAME", "gbuser"), - password: get_str("CUSTOM_PASSWORD", ""), - server: get_str("CUSTOM_SERVER", "localhost"), - port: get_u32("CUSTOM_PORT", 5432), - database: get_str("CUSTOM_DATABASE", "botserver"), + username: std::env::var("CUSTOM_USERNAME") + .unwrap_or_else(|_| get_str("CUSTOM_USERNAME", "gbuser")), + password: std::env::var("CUSTOM_PASSWORD") + .unwrap_or_else(|_| get_str("CUSTOM_PASSWORD", "")), + server: std::env::var("CUSTOM_SERVER") + .unwrap_or_else(|_| get_str("CUSTOM_SERVER", "localhost")), + port: std::env::var("CUSTOM_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or_else(|| get_u32("CUSTOM_PORT", 5432)), + database: std::env::var("CUSTOM_DATABASE") + .unwrap_or_else(|_| get_str("CUSTOM_DATABASE", "botserver")), }; let minio = DriveConfig { - server: get_str("DRIVE_SERVER", "localhost:9000"), + server: get_str("DRIVE_SERVER", "http://localhost:9000"), access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"), secret_key: get_str("DRIVE_SECRET", "minioadmin"), use_ssl: get_bool("DRIVE_USE_SSL", false), @@ -183,7 +219,10 @@ impl AppConfig { AppConfig { minio, - server: ServerConfig { host: get_str("SERVER_HOST", "127.0.0.1"), port: get_u16("SERVER_PORT", 8080) }, + server: ServerConfig { + host: get_str("SERVER_HOST", "127.0.0.1"), + port: get_u16("SERVER_PORT", 8080), + }, database, database_custom, email, @@ -198,10 +237,13 @@ impl AppConfig { pub fn from_env() -> Self { warn!("Loading configuration from environment variables"); - let stack_path = std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string()); + let stack_path = + std::env::var("STACK_PATH").unwrap_or_else(|_| "./botserver-stack".to_string()); - let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); - let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&database_url); + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); + let (db_username, db_password, db_server, db_port, db_name) = + parse_database_url(&database_url); let database = DatabaseConfig { username: db_username, @@ -215,22 +257,35 @@ impl AppConfig { username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()), password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "".to_string()), server: std::env::var("CUSTOM_SERVER").unwrap_or_else(|_| "localhost".to_string()), - port: std::env::var("CUSTOM_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(5432), + port: std::env::var("CUSTOM_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(5432), database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "botserver".to_string()), }; let minio = DriveConfig { - server: std::env::var("DRIVE_SERVER").unwrap_or_else(|_| "localhost:9000".to_string()), - access_key: std::env::var("DRIVE_ACCESSKEY").unwrap_or_else(|_| "minioadmin".to_string()), + server: std::env::var("DRIVE_SERVER") + .unwrap_or_else(|_| "http://localhost:9000".to_string()), + access_key: std::env::var("DRIVE_ACCESSKEY") + .unwrap_or_else(|_| "minioadmin".to_string()), secret_key: std::env::var("DRIVE_SECRET").unwrap_or_else(|_| "minioadmin".to_string()), - use_ssl: std::env::var("DRIVE_USE_SSL").unwrap_or_else(|_| "false".to_string()).parse().unwrap_or(false), - org_prefix: std::env::var("DRIVE_ORG_PREFIX").unwrap_or_else(|_| "botserver".to_string()), + use_ssl: std::env::var("DRIVE_USE_SSL") + .unwrap_or_else(|_| "false".to_string()) + .parse() + .unwrap_or(false), + org_prefix: std::env::var("DRIVE_ORG_PREFIX") + .unwrap_or_else(|_| "botserver".to_string()), }; let email = EmailConfig { from: std::env::var("EMAIL_FROM").unwrap_or_else(|_| "noreply@example.com".to_string()), - server: std::env::var("EMAIL_SERVER").unwrap_or_else(|_| "smtp.example.com".to_string()), - port: std::env::var("EMAIL_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587), + server: std::env::var("EMAIL_SERVER") + .unwrap_or_else(|_| "smtp.example.com".to_string()), + port: std::env::var("EMAIL_PORT") + .unwrap_or_else(|_| "587".to_string()) + .parse() + .unwrap_or(587), username: std::env::var("EMAIL_USER").unwrap_or_else(|_| "user".to_string()), password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()), }; @@ -238,28 +293,36 @@ impl AppConfig { let ai = AIConfig { instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()), key: std::env::var("AI_KEY").unwrap_or_else(|_| "".to_string()), - version: std::env::var("AI_VERSION").unwrap_or_else(|_| "2023-12-01-preview".to_string()), - endpoint: std::env::var("AI_ENDPOINT").unwrap_or_else(|_| "https://api.openai.com".to_string()), + version: std::env::var("AI_VERSION") + .unwrap_or_else(|_| "2023-12-01-preview".to_string()), + endpoint: std::env::var("AI_ENDPOINT") + .unwrap_or_else(|_| "https://api.openai.com".to_string()), }; AppConfig { minio, server: ServerConfig { host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), - port: std::env::var("SERVER_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080), + port: std::env::var("SERVER_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(8080), }, database, database_custom, email, ai, s3_bucket: std::env::var("DRIVE_BUCKET").unwrap_or_else(|_| "default".to_string()), - site_path: std::env::var("SITES_ROOT").unwrap_or_else(|_| "./botserver-stack/sites".to_string()), + site_path: std::env::var("SITES_ROOT") + .unwrap_or_else(|_| "./botserver-stack/sites".to_string()), stack_path: PathBuf::from(stack_path), db_conn: None, } } - fn load_config_from_db(conn: &mut PgConnection) -> Result, diesel::result::Error> { + fn load_config_from_db( + conn: &mut PgConnection, + ) -> Result, diesel::result::Error> { let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::(conn)?; let mut map = HashMap::new(); @@ -270,13 +333,26 @@ impl AppConfig { Ok(map) } - pub fn set_config(&self, conn: &mut PgConnection, key: &str, value: &str) -> Result<(), diesel::result::Error> { - diesel::sql_query("SELECT set_config($1, $2)").bind::(key).bind::(value).execute(conn)?; + pub fn set_config( + &self, + conn: &mut PgConnection, + key: &str, + value: &str, + ) -> Result<(), diesel::result::Error> { + diesel::sql_query("SELECT set_config($1, $2)") + .bind::(key) + .bind::(value) + .execute(conn)?; info!("Updated configuration: {} = {}", key, value); Ok(()) } - pub fn get_config(&self, conn: &mut PgConnection, key: &str, fallback: Option<&str>) -> Result { + pub fn get_config( + &self, + conn: &mut PgConnection, + key: &str, + fallback: Option<&str>, + ) -> Result { let fallback_str = fallback.unwrap_or(""); #[derive(Debug, QueryableByName)] @@ -285,7 +361,11 @@ impl AppConfig { value: String, } - let result = diesel::sql_query("SELECT get_config($1, $2) as value").bind::(key).bind::(fallback_str).get_result::(conn).map(|row| row.value)?; + let result = diesel::sql_query("SELECT get_config($1, $2) as value") + .bind::(key) + .bind::(fallback_str) + .get_result::(conn) + .map(|row| row.value)?; Ok(result) } } @@ -296,22 +376,31 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) { if parts.len() == 2 { let user_pass: Vec<&str> = parts[0].split(':').collect(); let host_db: Vec<&str> = parts[1].split('/').collect(); - + if user_pass.len() >= 2 && host_db.len() >= 2 { let username = user_pass[0].to_string(); let password = user_pass[1].to_string(); - + let host_port: Vec<&str> = host_db[0].split(':').collect(); let server = host_port[0].to_string(); - let port = host_port.get(1).and_then(|p| p.parse().ok()).unwrap_or(5432); + let port = host_port + .get(1) + .and_then(|p| p.parse().ok()) + .unwrap_or(5432); let database = host_db[1].to_string(); - + return (username, password, server, port, database); } } } - - ("gbuser".to_string(), "".to_string(), "localhost".to_string(), 5432, "botserver".to_string()) + + ( + "gbuser".to_string(), + "".to_string(), + "localhost".to_string(), + 5432, + "botserver".to_string(), + ) } pub struct ConfigManager { @@ -323,17 +412,25 @@ impl ConfigManager { Self { conn } } - pub fn sync_gbot_config(&self, bot_id: &uuid::Uuid, config_path: &str) -> Result { + pub fn sync_gbot_config( + &self, + bot_id: &uuid::Uuid, + config_path: &str, + ) -> Result { use sha2::{Digest, Sha256}; use std::fs; - let content = fs::read_to_string(config_path).map_err(|e| format!("Failed to read config file: {}", e))?; + let content = fs::read_to_string(config_path) + .map_err(|e| format!("Failed to read config file: {}", e))?; let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); let file_hash = format!("{:x}", hasher.finalize()); - let mut conn = self.conn.lock().map_err(|e| format!("Failed to acquire lock: {}", e))?; + let mut conn = self + .conn + .lock() + .map_err(|e| format!("Failed to acquire lock: {}", e))?; #[derive(QueryableByName)] struct SyncHash { @@ -341,12 +438,13 @@ impl ConfigManager { file_hash: String, } - let last_hash: Option = diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1") - .bind::(bot_id) - .get_result::(&mut *conn) - .optional() - .map_err(|e| format!("Database error: {}", e))? - .map(|row| row.file_hash); + let last_hash: Option = + diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1") + .bind::(bot_id) + .get_result::(&mut *conn) + .optional() + .map_err(|e| format!("Database error: {}", e))? + .map(|row| row.file_hash); if last_hash.as_ref() == Some(&file_hash) { info!("Config file unchanged for bot {}", bot_id); @@ -378,7 +476,10 @@ impl ConfigManager { .execute(&mut *conn) .map_err(|e| format!("Failed to update sync record: {}", e))?; - info!("Synced {} config values for bot {} from {}", updated, bot_id, config_path); + info!( + "Synced {} config values for bot {} from {}", + updated, bot_id, config_path + ); Ok(updated) } } diff --git a/src/drive_monitor/mod.rs b/src/drive_monitor/mod.rs index fa07bd17..7f706fee 100644 --- a/src/drive_monitor/mod.rs +++ b/src/drive_monitor/mod.rs @@ -296,9 +296,13 @@ impl DriveMonitor { // Calculate file hash for change detection let _file_hash = format!("{:x}", source_content.len()); - // Create work directory - let work_dir = "./work/default.gbai/default.gbdialog"; - std::fs::create_dir_all(work_dir)?; + // Create work directory using bot from bucket name + let bot_name = self + .bucket_name + .strip_suffix(".gbai") + .unwrap_or(&self.bucket_name); + let work_dir = format!("./work/{}.gbai/.gbdialog", bot_name); + std::fs::create_dir_all(&work_dir)?; // Write source to local file let local_source_path = format!("{}/{}.bas", work_dir, tool_name); @@ -306,7 +310,7 @@ impl DriveMonitor { // Compile using BasicCompiler let compiler = BasicCompiler::new(Arc::clone(&self.state)); - let result = compiler.compile_file(&local_source_path, work_dir)?; + let result = compiler.compile_file(&local_source_path, &work_dir)?; info!("Tool compiled successfully: {}", tool_name); info!(" AST: {}", result.ast_path); diff --git a/src/main.rs b/src/main.rs index c8570075..6ae00a79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,7 @@ use crate::meet::{voice_start, voice_stop}; use crate::package_manager::InstallMode; use crate::session::{create_session, get_session_history, get_sessions}; use crate::shared::state::AppState; -use crate::web_server::{index, static_files}; +use crate::web_server::{bot_index, index, static_files}; use crate::whatsapp::whatsapp_webhook_verify; use crate::whatsapp::WhatsAppAdapter; @@ -63,18 +63,17 @@ async fn main() -> std::io::Result<()> { if args.len() > 1 { let command = &args[1]; match command.as_str() { - "install" | "remove" | "list" | "status" | "--help" | "-h" => { - match package_manager::cli::run().await { - Ok(_) => return Ok(()), - Err(e) => { - eprintln!("CLI error: {}", e); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("CLI command failed: {}", e), - )); - } + "install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help" + | "-h" => match package_manager::cli::run().await { + Ok(_) => return Ok(()), + Err(e) => { + eprintln!("CLI error: {}", e); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("CLI command failed: {}", e), + )); } - } + }, _ => { eprintln!("Unknown command: {}", command); eprintln!("Run 'botserver --help' for usage information"); @@ -126,6 +125,12 @@ async fn main() -> std::io::Result<()> { }; let _ = bootstrap.start_all(); + + // Upload template bots to MinIO on first startup + if let Err(e) = bootstrap.upload_templates_to_minio(&cfg).await { + log::warn!("Failed to upload templates to MinIO: {}", e); + } + let config = std::sync::Arc::new(cfg.clone()); info!("Establishing database connection to {}", cfg.database_url()); @@ -237,16 +242,16 @@ async fn main() -> std::io::Result<()> { let local = tokio::task::LocalSet::new(); local.block_on(&rt, async move { - let automation = AutomationService::new( - automation_state, - "templates/announcements.gbai/announcements.gbdialog", - ); + let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()); + let scripts_dir = format!("work/{}.gbai/.gbdialog", bot_guid); + let automation = AutomationService::new(automation_state, &scripts_dir); automation.spawn().await.ok(); }); }); let drive_state = app_state.clone(); - let bucket_name = format!("{}default.gbai", cfg.minio.org_prefix); + let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string()); + let bucket_name = format!("{}{}.gbai", cfg.minio.org_prefix, bot_guid); let drive_monitor = Arc::new(DriveMonitor::new(drive_state, bucket_name)); let _drive_handle = drive_monitor.spawn(); @@ -267,6 +272,7 @@ 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) diff --git a/src/package_manager/cli.rs b/src/package_manager/cli.rs index 90a66396..f00273cb 100644 --- a/src/package_manager/cli.rs +++ b/src/package_manager/cli.rs @@ -1,5 +1,6 @@ use anyhow::Result; use std::env; +use std::process::Command; use crate::package_manager::{InstallMode, PackageManager}; @@ -15,6 +16,77 @@ pub async fn run() -> Result<()> { let command = &args[1]; match command.as_str() { + "start" => { + let mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + + let pm = PackageManager::new(mode, tenant)?; + println!("Starting all installed components..."); + + let components = vec!["tables", "cache", "drive", "llm"]; + for component in components { + if pm.is_installed(component) { + match pm.start(component) { + Ok(_) => println!("✓ Started {}", component), + Err(e) => eprintln!("✗ Failed to start {}: {}", component, e), + } + } + } + println!("✓ BotServer components started"); + } + "stop" => { + println!("Stopping all components..."); + + // Stop components gracefully + let _ = Command::new("pkill").arg("-f").arg("redis-server").output(); + let _ = Command::new("pkill").arg("-f").arg("minio").output(); + let _ = Command::new("pkill").arg("-f").arg("postgres").output(); + let _ = Command::new("pkill").arg("-f").arg("llama-server").output(); + + println!("✓ BotServer components stopped"); + } + "restart" => { + println!("Restarting BotServer..."); + + // Stop + let _ = Command::new("pkill").arg("-f").arg("redis-server").output(); + let _ = Command::new("pkill").arg("-f").arg("minio").output(); + let _ = Command::new("pkill").arg("-f").arg("postgres").output(); + let _ = Command::new("pkill").arg("-f").arg("llama-server").output(); + + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + + // Start + let mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + + let pm = PackageManager::new(mode, tenant)?; + + let components = vec!["tables", "cache", "drive", "llm"]; + for component in components { + if pm.is_installed(component) { + let _ = pm.start(component); + } + } + + println!("✓ BotServer restarted"); + } "install" => { if args.len() < 3 { eprintln!("Usage: botserver install [--container] [--tenant ]"); @@ -120,5 +192,5 @@ pub async fn run() -> Result<()> { } fn print_usage() { - println!("BotServer Package Manager\n\nUSAGE:\n botserver [options]\n\nCOMMANDS:\n install Install component\n remove Remove component\n list List all components\n status Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list"); + println!("BotServer Package Manager\n\nUSAGE:\n botserver [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install Install component\n remove Remove component\n list List all components\n status Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver start\n botserver stop\n botserver restart\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list"); } diff --git a/src/package_manager/installer.rs b/src/package_manager/installer.rs index d23b6035..891101a6 100644 --- a/src/package_manager/installer.rs +++ b/src/package_manager/installer.rs @@ -96,7 +96,7 @@ impl PackageManager { ("MINIO_ROOT_USER".to_string(), "gbdriveuser".to_string()), ("MINIO_ROOT_PASSWORD".to_string(), drive_password) ]), - exec_cmd: "{{BIN_PATH}}/minio server {{DATA_PATH}} --address :9000 --console-address :9001".to_string(), + exec_cmd: "nohup {{BIN_PATH}}/minio server {{DATA_PATH}} --address :9000 --console-address :9001 > {{LOGS_PATH}}/minio.log 2>&1 &".to_string(), }); self.update_drive_credentials_in_database(&encrypted_drive_password) @@ -230,7 +230,7 @@ impl PackageManager { pre_install_cmds_windows: vec![], post_install_cmds_windows: vec![], env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/llama-server -m {{DATA_PATH}}/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf --port 8081 & {{BIN_PATH}}/llama-server -m {{DATA_PATH}}/bge-small-en-v1.5-f32.gguf --port 8082 --embedding".to_string(), + exec_cmd: "nohup {{BIN_PATH}}/llama-server -m {{DATA_PATH}}/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf --port 8081 > {{LOGS_PATH}}/llm-main.log 2>&1 & nohup {{BIN_PATH}}/llama-server -m {{DATA_PATH}}/bge-small-en-v1.5-f32.gguf --port 8082 --embedding > {{LOGS_PATH}}/llm-embed.log 2>&1 &".to_string(), }); } diff --git a/src/session/mod.rs b/src/session/mod.rs index cd30c6a4..ee291265 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -109,33 +109,32 @@ impl SessionManager { self.create_session(uid, bid, session_title).map(Some) } - pub fn create_session( + pub fn get_or_create_anonymous_user( &mut self, - uid: Uuid, - bid: Uuid, - session_title: &str, - ) -> Result> { - use crate::shared::models::user_sessions::dsl::*; + uid: Option, + ) -> Result> { use crate::shared::models::users::dsl as users_dsl; - let now = Utc::now(); + let user_id = uid.unwrap_or_else(Uuid::new_v4); + let user_exists: Option = users_dsl::users - .filter(users_dsl::id.eq(uid)) + .filter(users_dsl::id.eq(user_id)) .select(users_dsl::id) .first(&mut self.conn) .optional()?; if user_exists.is_none() { - warn!( - "User {} does not exist in database, creating placeholder user", - uid - ); + let now = Utc::now(); + info!("Creating anonymous user with ID {}", user_id); diesel::insert_into(users_dsl::users) .values(( - users_dsl::id.eq(uid), - users_dsl::username.eq(format!("anonymous_{}", rand::random::())), - users_dsl::email.eq(format!("anonymous_{}@local", rand::random::())), - users_dsl::password_hash.eq("placeholder"), + users_dsl::id.eq(user_id), + users_dsl::username.eq(format!("guest_{}", &user_id.to_string()[..8])), + users_dsl::email.eq(format!( + "guest_{}@anonymous.local", + &user_id.to_string()[..8] + )), + users_dsl::password_hash.eq(""), users_dsl::is_active.eq(true), users_dsl::created_at.eq(now), users_dsl::updated_at.eq(now), @@ -143,10 +142,25 @@ impl SessionManager { .execute(&mut self.conn)?; } + Ok(user_id) + } + + pub fn create_session( + &mut self, + uid: Uuid, + bid: Uuid, + session_title: &str, + ) -> Result> { + use crate::shared::models::user_sessions::dsl::*; + + // Ensure user exists (create anonymous if needed) + let verified_uid = self.get_or_create_anonymous_user(Some(uid))?; + let now = Utc::now(); + let inserted: UserSession = diesel::insert_into(user_sessions) .values(( id.eq(Uuid::new_v4()), - user_id.eq(uid), + user_id.eq(verified_uid), bot_id.eq(bid), title.eq(session_title), context_data.eq(serde_json::json!({})), diff --git a/src/web_server/mod.rs b/src/web_server/mod.rs index 0c36d745..1e8c513e 100644 --- a/src/web_server/mod.rs +++ b/src/web_server/mod.rs @@ -13,6 +13,19 @@ async fn index() -> Result { } } +#[actix_web::get("/{botname}")] +async fn bot_index(req: HttpRequest) -> Result { + let botname = req.match_info().query("botname"); + debug!("Serving bot interface for: {}", botname); + match fs::read_to_string("web/index.html") { + Ok(html) => Ok(HttpResponse::Ok().content_type("text/html").body(html)), + Err(e) => { + error!("Failed to load index page for bot {}: {}", botname, e); + Ok(HttpResponse::InternalServerError().body("Failed to load index page")) + } + } +} + #[actix_web::get("/static/{filename:.*}")] async fn static_files(req: HttpRequest) -> Result { let filename = req.match_info().query("filename"); diff --git a/templates/announcements.gbai/announcements.gbdialog/auth.bas b/templates/announcements.gbai/announcements.gbdialog/auth.bas index 829136df..fa57e7bc 100644 --- a/templates/announcements.gbai/announcements.gbdialog/auth.bas +++ b/templates/announcements.gbai/announcements.gbdialog/auth.bas @@ -1,6 +1,67 @@ -REM result = POST "http://0.0.0.0/api/isvalid", "token=" + token; +REM Simple KISS authentication - signup/login only, no recovery +REM This script is called when user needs authentication -REM user = FIND "users", "external_id=" + result.user_id +TALK "Welcome! Please choose an option:" +TALK "Type 'signup' to create a new account" +TALK "Type 'login' to access your existing account" -SET_USER "92fcffaa-bf0a-41a9-8d99-5541709d695b" -return true; +HEAR choice + +IF choice = "signup" THEN + TALK "Great! Let's create your account." + TALK "Enter your email:" + HEAR email + + TALK "Enter your password:" + HEAR password + + TALK "Confirm your password:" + HEAR confirm_password + + IF password <> confirm_password THEN + TALK "Passwords don't match. Please try again." + RETURN false + END IF + + REM Create user in database + LET user_id = GENERATE_UUID() + LET result = EXEC "INSERT INTO users (id, email, password_hash, created_at) VALUES (?, ?, ?, NOW())", user_id, email, SHA256(password) + + IF result > 0 THEN + SET_USER user_id + TALK "Account created successfully! You are now logged in." + RETURN true + ELSE + TALK "Error creating account. Email may already exist." + RETURN false + END IF + +ELSE IF choice = "login" THEN + TALK "Please enter your email:" + HEAR email + + TALK "Enter your password:" + HEAR password + + REM Query user from database + LET user = FIND "users", "email=" + email + + IF user = NULL THEN + TALK "Invalid email or password." + RETURN false + END IF + + LET password_hash = SHA256(password) + IF user.password_hash = password_hash THEN + SET_USER user.id + TALK "Welcome back! You are now logged in." + RETURN true + ELSE + TALK "Invalid email or password." + RETURN false + END IF + +ELSE + TALK "Invalid option. Please type 'signup' or 'login'." + RETURN false +END IF diff --git a/templates/announcements.gbai/announcements.gbdialog/start.bas b/templates/announcements.gbai/announcements.gbdialog/start.bas index e55ea9d9..687c70e3 100644 --- a/templates/announcements.gbai/announcements.gbdialog/start.bas +++ b/templates/announcements.gbai/announcements.gbdialog/start.bas @@ -1,6 +1,16 @@ -let resume = GET_BOT_MEMORY ("resume") -TALK resume +REM start.bas - Runs automatically when user connects via web +REM This is the entry point for each session -ADD_KB "weekly" +LET resume = GET_BOT_MEMORY("resume") -TALK "Olá, pode me perguntar sobre qualquer coisa destas circulares..." +IF resume <> "" THEN + TALK resume +ELSE + TALK "Welcome! I'm loading the latest information..." +END IF + +REM Add knowledge base for weekly announcements +ADD_KB "weekly" + +TALK "You can ask me about any of the announcements or circulars." +TALK "If you'd like to login or signup, just type 'auth'." diff --git a/web/index.html b/web/index.html index 4ea27238..d3b626ee 100644 --- a/web/index.html +++ b/web/index.html @@ -689,7 +689,9 @@ height: 100%; background: #90ee90; border-radius: 3px; - transition: width 0.3s ease, background-color 0.3s ease; + transition: + width 0.3s ease, + background-color 0.3s ease; } .context-progress-bar.warning { @@ -880,15 +882,27 @@ - -