- More automation from start to web, user sessions.

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-10-20 23:32:49 -03:00
parent c595380837
commit 9c36aa10fa
17 changed files with 852 additions and 180 deletions

2
Cargo.lock generated
View file

@ -1009,7 +1009,7 @@ dependencies = [
[[package]] [[package]]
name = "botserver" name = "botserver"
version = "6.0.4" version = "6.0.5"
dependencies = [ dependencies = [
"actix-cors", "actix-cors",
"actix-multipart", "actix-multipart",

View file

@ -1,39 +1,41 @@
[package] [package]
name = "botserver" name = "botserver"
version = "6.0.4" version = "6.0.5"
edition = "2021" edition = "2021"
authors = [ authors = [
"@AlanPerdomo", "Pragmatismo.com.br <contact@pragmatismo.com.br>",
"@AnaPaulaGil", "General Bots Community <https://github.com/GeneralBots>",
"@arenasio", "Rodrigo Rodriguez <rodrigorodriguez@pragmatismo.com.br>",
"@AtyllaL", "Alan Perdomo",
"@christopherdecastilho", "Ana Paula Gil",
"@danielolima96", "Arenas.io",
"@Dariojunior3", "Atylla L",
"@davidlerner26", "Christopher de Castilho",
"@ExperimentationGarage", "Dario Lima",
"@flavioandrade91", "Dario Junior",
"@HeraldoAlmeida", "David Lerner",
"@joao-parana", "Experimentation Garage",
"@jonathasc", "Flavio Andrade",
"@jramos-br", "Heraldo Almeida",
"@lpicanco", "Joao Parana",
"@marcosvelasco", "Jonathas C",
"@matheus39x", "J Ramos",
"@oerlabshenrique", "Lucas Picanco",
"@othonlima", "Marcos Velasco",
"@PH-Nascimento", "Matheus 39x",
"@phpussente", "Oerlabs Henrique",
"@RobsonDantasE", "Othon Lima",
"@rodrigorodriguez", "PH Nascimento",
"@SarahLourenco", "Phpussente",
"@thipatriota", "Robson Dantas",
"@webgus", "Sarah Lourenco",
"@ZuilhoSe", "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" license = "AGPL-3.0"
repository = "https://alm.pragmatismo.com.br/generalbots/botserver" repository = "https://github.com/GeneralBots/BotServer"
[features] [features]
default = ["vectordb"] default = ["vectordb"]

116
README.md
View file

@ -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: 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). - **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: 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. This changes your current directory to the newly cloned BotServer folder.
3. Install dependencies and start the server: 3. Create your configuration file:
```bash ```bash
npm install cp .env.example .env
npm run start ```
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 <component> # Check component status
botserver install <component> # Install optional component
``` ```
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.
### Accessing Your Bot ### 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 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.
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.
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 ## Development Workflow

View file

@ -1,3 +1,3 @@
set +e set +e
pkill postgres && rm .env -rf botserver-stack && clear && \ clear && \
RUST_LOG=trace,hyper_util=off cargo run RUST_LOG=trace,hyper_util=off cargo run

View file

@ -325,10 +325,81 @@ impl AutomationService {
content content
} }
Err(e) => { Err(e) => {
error!("Failed to read script {}: {}", full_path.display(), e); 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; self.cleanup_job_flag(&bot_id, param).await;
return; 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;
}
}
}; };
let user_session = crate::shared::models::UserSession { let user_session = crate::shared::models::UserSession {

View file

@ -7,6 +7,7 @@ use dotenvy::dotenv;
use log::{info, trace}; use log::{info, trace};
use rand::distr::Alphanumeric; use rand::distr::Alphanumeric;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::path::Path;
pub struct BootstrapManager { pub struct BootstrapManager {
pub install_mode: InstallMode, pub install_mode: InstallMode,
@ -91,6 +92,12 @@ impl BootstrapManager {
match diesel::PgConnection::establish(&database_url) { match diesel::PgConnection::establish(&database_url) {
Ok(mut conn) => { Ok(mut conn) => {
info!("Successfully connected to legacy database, loading configuration"); 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)); return Ok(AppConfig::from_database(&mut conn));
} }
Err(e) => { Err(e) => {
@ -198,4 +205,173 @@ impl BootstrapManager {
hasher.update(password.as_bytes()); hasher.update(password.as_bytes());
format!("{:x}", hasher.finalize()) 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<Box<dyn std::future::Future<Output = Result<()>> + '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(())
}
} }

View file

@ -556,10 +556,14 @@ impl BotOrchestrator {
"Running start script for session: {} with token: {:?}", "Running start script for session: {} with token: {:?}",
session.id, token session.id, token
); );
let start_script_path = "./templates/announcements.gbai/announcements.gbdialog/start.bas"; let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
let start_script = match std::fs::read_to_string(start_script_path) { 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, 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!( debug!(
"Start script content for session {}: {}", "Start script content for session {}: {}",
@ -706,7 +710,8 @@ async fn websocket_handler(
let user_id = query let user_id = query
.get("user_id") .get("user_id")
.cloned() .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 (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
let (tx, mut rx) = mpsc::channel::<BotResponse>(100); let (tx, mut rx) = mpsc::channel::<BotResponse>(100);
@ -747,6 +752,20 @@ async fn websocket_handler(
session_id, user_id 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 web_adapter = data.web_adapter.clone();
let session_id_clone1 = session_id.clone(); let session_id_clone1 = session_id.clone();
let session_id_clone2 = session_id.clone(); let session_id_clone2 = session_id.clone();

View file

@ -79,14 +79,22 @@ impl AppConfig {
pub fn database_url(&self) -> String { pub fn database_url(&self) -> String {
format!( format!(
"postgres://{}:{}@{}:{}/{}", "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 { pub fn database_custom_url(&self) -> String {
format!( format!(
"postgres://{}:{}@{}:{}/{}", "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 { 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 { 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 { 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 { 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")); 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 { let database = DatabaseConfig {
username: get_str("TABLES_USERNAME", "gbuser"), username: std::env::var("TABLES_USERNAME")
password: get_str("TABLES_PASSWORD", ""), .unwrap_or_else(|_| get_str("TABLES_USERNAME", "gbuser")),
server: get_str("TABLES_SERVER", "localhost"), password: std::env::var("TABLES_PASSWORD")
port: get_u32("TABLES_PORT", 5432), .unwrap_or_else(|_| get_str("TABLES_PASSWORD", "")),
database: get_str("TABLES_DATABASE", "botserver"), 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 { let database_custom = DatabaseConfig {
username: get_str("CUSTOM_USERNAME", "gbuser"), username: std::env::var("CUSTOM_USERNAME")
password: get_str("CUSTOM_PASSWORD", ""), .unwrap_or_else(|_| get_str("CUSTOM_USERNAME", "gbuser")),
server: get_str("CUSTOM_SERVER", "localhost"), password: std::env::var("CUSTOM_PASSWORD")
port: get_u32("CUSTOM_PORT", 5432), .unwrap_or_else(|_| get_str("CUSTOM_PASSWORD", "")),
database: get_str("CUSTOM_DATABASE", "botserver"), 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 { 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"), access_key: get_str("DRIVE_ACCESSKEY", "minioadmin"),
secret_key: get_str("DRIVE_SECRET", "minioadmin"), secret_key: get_str("DRIVE_SECRET", "minioadmin"),
use_ssl: get_bool("DRIVE_USE_SSL", false), use_ssl: get_bool("DRIVE_USE_SSL", false),
@ -183,7 +219,10 @@ impl AppConfig {
AppConfig { AppConfig {
minio, 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,
database_custom, database_custom,
email, email,
@ -198,10 +237,13 @@ impl AppConfig {
pub fn from_env() -> Self { pub fn from_env() -> Self {
warn!("Loading configuration from environment variables"); 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 database_url = std::env::var("DATABASE_URL")
let (db_username, db_password, db_server, db_port, db_name) = parse_database_url(&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 { let database = DatabaseConfig {
username: db_username, username: db_username,
@ -215,22 +257,35 @@ impl AppConfig {
username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()), username: std::env::var("CUSTOM_USERNAME").unwrap_or_else(|_| "gbuser".to_string()),
password: std::env::var("CUSTOM_PASSWORD").unwrap_or_else(|_| "".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()), 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()), database: std::env::var("CUSTOM_DATABASE").unwrap_or_else(|_| "botserver".to_string()),
}; };
let minio = DriveConfig { let minio = DriveConfig {
server: std::env::var("DRIVE_SERVER").unwrap_or_else(|_| "localhost:9000".to_string()), server: std::env::var("DRIVE_SERVER")
access_key: std::env::var("DRIVE_ACCESSKEY").unwrap_or_else(|_| "minioadmin".to_string()), .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()), 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), use_ssl: std::env::var("DRIVE_USE_SSL")
org_prefix: std::env::var("DRIVE_ORG_PREFIX").unwrap_or_else(|_| "botserver".to_string()), .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 { let email = EmailConfig {
from: std::env::var("EMAIL_FROM").unwrap_or_else(|_| "noreply@example.com".to_string()), 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()), server: std::env::var("EMAIL_SERVER")
port: std::env::var("EMAIL_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587), .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()), username: std::env::var("EMAIL_USER").unwrap_or_else(|_| "user".to_string()),
password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()), password: std::env::var("EMAIL_PASS").unwrap_or_else(|_| "pass".to_string()),
}; };
@ -238,28 +293,36 @@ impl AppConfig {
let ai = AIConfig { let ai = AIConfig {
instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()), instance: std::env::var("AI_INSTANCE").unwrap_or_else(|_| "gpt-4".to_string()),
key: std::env::var("AI_KEY").unwrap_or_else(|_| "".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()), version: std::env::var("AI_VERSION")
endpoint: std::env::var("AI_ENDPOINT").unwrap_or_else(|_| "https://api.openai.com".to_string()), .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 { AppConfig {
minio, minio,
server: ServerConfig { server: ServerConfig {
host: std::env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()), 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,
database_custom, database_custom,
email, email,
ai, ai,
s3_bucket: std::env::var("DRIVE_BUCKET").unwrap_or_else(|_| "default".to_string()), 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), stack_path: PathBuf::from(stack_path),
db_conn: None, db_conn: None,
} }
} }
fn load_config_from_db(conn: &mut PgConnection) -> Result<HashMap<String, ServerConfigRow>, diesel::result::Error> { fn load_config_from_db(
conn: &mut PgConnection,
) -> Result<HashMap<String, ServerConfigRow>, diesel::result::Error> {
let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::<ServerConfigRow>(conn)?; let results = diesel::sql_query("SELECT id, config_key, config_value, config_type, is_encrypted FROM server_configuration").load::<ServerConfigRow>(conn)?;
let mut map = HashMap::new(); let mut map = HashMap::new();
@ -270,13 +333,26 @@ impl AppConfig {
Ok(map) Ok(map)
} }
pub fn set_config(&self, conn: &mut PgConnection, key: &str, value: &str) -> Result<(), diesel::result::Error> { pub fn set_config(
diesel::sql_query("SELECT set_config($1, $2)").bind::<Text, _>(key).bind::<Text, _>(value).execute(conn)?; &self,
conn: &mut PgConnection,
key: &str,
value: &str,
) -> Result<(), diesel::result::Error> {
diesel::sql_query("SELECT set_config($1, $2)")
.bind::<Text, _>(key)
.bind::<Text, _>(value)
.execute(conn)?;
info!("Updated configuration: {} = {}", key, value); info!("Updated configuration: {} = {}", key, value);
Ok(()) Ok(())
} }
pub fn get_config(&self, conn: &mut PgConnection, key: &str, fallback: Option<&str>) -> Result<String, diesel::result::Error> { pub fn get_config(
&self,
conn: &mut PgConnection,
key: &str,
fallback: Option<&str>,
) -> Result<String, diesel::result::Error> {
let fallback_str = fallback.unwrap_or(""); let fallback_str = fallback.unwrap_or("");
#[derive(Debug, QueryableByName)] #[derive(Debug, QueryableByName)]
@ -285,7 +361,11 @@ impl AppConfig {
value: String, value: String,
} }
let result = diesel::sql_query("SELECT get_config($1, $2) as value").bind::<Text, _>(key).bind::<Text, _>(fallback_str).get_result::<ConfigValue>(conn).map(|row| row.value)?; let result = diesel::sql_query("SELECT get_config($1, $2) as value")
.bind::<Text, _>(key)
.bind::<Text, _>(fallback_str)
.get_result::<ConfigValue>(conn)
.map(|row| row.value)?;
Ok(result) Ok(result)
} }
} }
@ -303,7 +383,10 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
let host_port: Vec<&str> = host_db[0].split(':').collect(); let host_port: Vec<&str> = host_db[0].split(':').collect();
let server = host_port[0].to_string(); 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(); let database = host_db[1].to_string();
return (username, password, server, port, database); return (username, password, server, port, database);
@ -311,7 +394,13 @@ fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
} }
} }
("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 { pub struct ConfigManager {
@ -323,17 +412,25 @@ impl ConfigManager {
Self { conn } Self { conn }
} }
pub fn sync_gbot_config(&self, bot_id: &uuid::Uuid, config_path: &str) -> Result<usize, String> { pub fn sync_gbot_config(
&self,
bot_id: &uuid::Uuid,
config_path: &str,
) -> Result<usize, String> {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::fs; 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(); let mut hasher = Sha256::new();
hasher.update(content.as_bytes()); hasher.update(content.as_bytes());
let file_hash = format!("{:x}", hasher.finalize()); 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)] #[derive(QueryableByName)]
struct SyncHash { struct SyncHash {
@ -341,7 +438,8 @@ impl ConfigManager {
file_hash: String, file_hash: String,
} }
let last_hash: Option<String> = diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1") let last_hash: Option<String> =
diesel::sql_query("SELECT file_hash FROM gbot_config_sync WHERE bot_id = $1")
.bind::<diesel::sql_types::Uuid, _>(bot_id) .bind::<diesel::sql_types::Uuid, _>(bot_id)
.get_result::<SyncHash>(&mut *conn) .get_result::<SyncHash>(&mut *conn)
.optional() .optional()
@ -378,7 +476,10 @@ impl ConfigManager {
.execute(&mut *conn) .execute(&mut *conn)
.map_err(|e| format!("Failed to update sync record: {}", e))?; .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) Ok(updated)
} }
} }

View file

@ -296,9 +296,13 @@ impl DriveMonitor {
// Calculate file hash for change detection // Calculate file hash for change detection
let _file_hash = format!("{:x}", source_content.len()); let _file_hash = format!("{:x}", source_content.len());
// Create work directory // Create work directory using bot from bucket name
let work_dir = "./work/default.gbai/default.gbdialog"; let bot_name = self
std::fs::create_dir_all(work_dir)?; .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 // Write source to local file
let local_source_path = format!("{}/{}.bas", work_dir, tool_name); let local_source_path = format!("{}/{}.bas", work_dir, tool_name);
@ -306,7 +310,7 @@ impl DriveMonitor {
// Compile using BasicCompiler // Compile using BasicCompiler
let compiler = BasicCompiler::new(Arc::clone(&self.state)); 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!("Tool compiled successfully: {}", tool_name);
info!(" AST: {}", result.ast_path); info!(" AST: {}", result.ast_path);

View file

@ -52,7 +52,7 @@ use crate::meet::{voice_start, voice_stop};
use crate::package_manager::InstallMode; use crate::package_manager::InstallMode;
use crate::session::{create_session, get_session_history, get_sessions}; use crate::session::{create_session, get_session_history, get_sessions};
use crate::shared::state::AppState; 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::whatsapp_webhook_verify;
use crate::whatsapp::WhatsAppAdapter; use crate::whatsapp::WhatsAppAdapter;
@ -63,8 +63,8 @@ async fn main() -> std::io::Result<()> {
if args.len() > 1 { if args.len() > 1 {
let command = &args[1]; let command = &args[1];
match command.as_str() { match command.as_str() {
"install" | "remove" | "list" | "status" | "--help" | "-h" => { "install" | "remove" | "list" | "status" | "start" | "stop" | "restart" | "--help"
match package_manager::cli::run().await { | "-h" => match package_manager::cli::run().await {
Ok(_) => return Ok(()), Ok(_) => return Ok(()),
Err(e) => { Err(e) => {
eprintln!("CLI error: {}", e); eprintln!("CLI error: {}", e);
@ -73,8 +73,7 @@ async fn main() -> std::io::Result<()> {
format!("CLI command failed: {}", e), format!("CLI command failed: {}", e),
)); ));
} }
} },
}
_ => { _ => {
eprintln!("Unknown command: {}", command); eprintln!("Unknown command: {}", command);
eprintln!("Run 'botserver --help' for usage information"); eprintln!("Run 'botserver --help' for usage information");
@ -126,6 +125,12 @@ async fn main() -> std::io::Result<()> {
}; };
let _ = bootstrap.start_all(); 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()); let config = std::sync::Arc::new(cfg.clone());
info!("Establishing database connection to {}", cfg.database_url()); info!("Establishing database connection to {}", cfg.database_url());
@ -237,16 +242,16 @@ async fn main() -> std::io::Result<()> {
let local = tokio::task::LocalSet::new(); let local = tokio::task::LocalSet::new();
local.block_on(&rt, async move { local.block_on(&rt, async move {
let automation = AutomationService::new( let bot_guid = std::env::var("BOT_GUID").unwrap_or_else(|_| "default_bot".to_string());
automation_state, let scripts_dir = format!("work/{}.gbai/.gbdialog", bot_guid);
"templates/announcements.gbai/announcements.gbdialog", let automation = AutomationService::new(automation_state, &scripts_dir);
);
automation.spawn().await.ok(); automation.spawn().await.ok();
}); });
}); });
let drive_state = app_state.clone(); 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_monitor = Arc::new(DriveMonitor::new(drive_state, bucket_name));
let _drive_handle = drive_monitor.spawn(); let _drive_handle = drive_monitor.spawn();
@ -267,6 +272,7 @@ async fn main() -> std::io::Result<()> {
app = app app = app
.service(upload_file) .service(upload_file)
.service(index) .service(index)
.service(bot_index)
.service(static_files) .service(static_files)
.service(websocket_handler) .service(websocket_handler)
.service(auth_handler) .service(auth_handler)

View file

@ -1,5 +1,6 @@
use anyhow::Result; use anyhow::Result;
use std::env; use std::env;
use std::process::Command;
use crate::package_manager::{InstallMode, PackageManager}; use crate::package_manager::{InstallMode, PackageManager};
@ -15,6 +16,77 @@ pub async fn run() -> Result<()> {
let command = &args[1]; let command = &args[1];
match command.as_str() { 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" => { "install" => {
if args.len() < 3 { if args.len() < 3 {
eprintln!("Usage: botserver install <component> [--container] [--tenant <name>]"); eprintln!("Usage: botserver install <component> [--container] [--tenant <name>]");
@ -120,5 +192,5 @@ pub async fn run() -> Result<()> {
} }
fn print_usage() { fn print_usage() {
println!("BotServer Package Manager\n\nUSAGE:\n botserver <command> [options]\n\nCOMMANDS:\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> 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 <command> [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> 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");
} }

View file

@ -96,7 +96,7 @@ impl PackageManager {
("MINIO_ROOT_USER".to_string(), "gbdriveuser".to_string()), ("MINIO_ROOT_USER".to_string(), "gbdriveuser".to_string()),
("MINIO_ROOT_PASSWORD".to_string(), drive_password) ("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) self.update_drive_credentials_in_database(&encrypted_drive_password)
@ -230,7 +230,7 @@ impl PackageManager {
pre_install_cmds_windows: vec![], pre_install_cmds_windows: vec![],
post_install_cmds_windows: vec![], post_install_cmds_windows: vec![],
env_vars: HashMap::new(), 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(),
}); });
} }

View file

@ -109,33 +109,32 @@ impl SessionManager {
self.create_session(uid, bid, session_title).map(Some) self.create_session(uid, bid, session_title).map(Some)
} }
pub fn create_session( pub fn get_or_create_anonymous_user(
&mut self, &mut self,
uid: Uuid, uid: Option<Uuid>,
bid: Uuid, ) -> Result<Uuid, Box<dyn Error + Send + Sync>> {
session_title: &str,
) -> Result<UserSession, Box<dyn Error + Send + Sync>> {
use crate::shared::models::user_sessions::dsl::*;
use crate::shared::models::users::dsl as users_dsl; 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<Uuid> = users_dsl::users let user_exists: Option<Uuid> = users_dsl::users
.filter(users_dsl::id.eq(uid)) .filter(users_dsl::id.eq(user_id))
.select(users_dsl::id) .select(users_dsl::id)
.first(&mut self.conn) .first(&mut self.conn)
.optional()?; .optional()?;
if user_exists.is_none() { if user_exists.is_none() {
warn!( let now = Utc::now();
"User {} does not exist in database, creating placeholder user", info!("Creating anonymous user with ID {}", user_id);
uid
);
diesel::insert_into(users_dsl::users) diesel::insert_into(users_dsl::users)
.values(( .values((
users_dsl::id.eq(uid), users_dsl::id.eq(user_id),
users_dsl::username.eq(format!("anonymous_{}", rand::random::<u32>())), users_dsl::username.eq(format!("guest_{}", &user_id.to_string()[..8])),
users_dsl::email.eq(format!("anonymous_{}@local", rand::random::<u32>())), users_dsl::email.eq(format!(
users_dsl::password_hash.eq("placeholder"), "guest_{}@anonymous.local",
&user_id.to_string()[..8]
)),
users_dsl::password_hash.eq(""),
users_dsl::is_active.eq(true), users_dsl::is_active.eq(true),
users_dsl::created_at.eq(now), users_dsl::created_at.eq(now),
users_dsl::updated_at.eq(now), users_dsl::updated_at.eq(now),
@ -143,10 +142,25 @@ impl SessionManager {
.execute(&mut self.conn)?; .execute(&mut self.conn)?;
} }
Ok(user_id)
}
pub fn create_session(
&mut self,
uid: Uuid,
bid: Uuid,
session_title: &str,
) -> Result<UserSession, Box<dyn Error + Send + Sync>> {
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) let inserted: UserSession = diesel::insert_into(user_sessions)
.values(( .values((
id.eq(Uuid::new_v4()), id.eq(Uuid::new_v4()),
user_id.eq(uid), user_id.eq(verified_uid),
bot_id.eq(bid), bot_id.eq(bid),
title.eq(session_title), title.eq(session_title),
context_data.eq(serde_json::json!({})), context_data.eq(serde_json::json!({})),

View file

@ -13,6 +13,19 @@ async fn index() -> Result<HttpResponse> {
} }
} }
#[actix_web::get("/{botname}")]
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
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:.*}")] #[actix_web::get("/static/{filename:.*}")]
async fn static_files(req: HttpRequest) -> Result<HttpResponse> { async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
let filename = req.match_info().query("filename"); let filename = req.match_info().query("filename");

View file

@ -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" HEAR choice
return true;
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

View file

@ -1,6 +1,16 @@
let resume = GET_BOT_MEMORY ("resume") REM start.bas - Runs automatically when user connects via web
TALK resume REM This is the entry point for each session
LET resume = GET_BOT_MEMORY("resume")
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" ADD_KB "weekly"
TALK "Olá, pode me perguntar sobre qualquer coisa destas circulares..." TALK "You can ask me about any of the announcements or circulars."
TALK "If you'd like to login or signup, just type 'auth'."

View file

@ -689,7 +689,9 @@
height: 100%; height: 100%;
background: #90ee90; background: #90ee90;
border-radius: 3px; 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 { .context-progress-bar.warning {
@ -880,15 +882,27 @@
</div> </div>
<!-- New elements for improvements --> <!-- New elements for improvements -->
<button class="scroll-to-bottom" id="scrollToBottom" style="display: none"> <button
class="scroll-to-bottom"
id="scrollToBottom"
style="display: none"
>
</button> </button>
<div class="context-indicator" id="contextIndicator" style="display: none"> <div
class="context-indicator"
id="contextIndicator"
style="display: none"
>
<div>Contexto</div> <div>Contexto</div>
<div id="contextPercentage">0%</div> <div id="contextPercentage">0%</div>
<div class="context-progress"> <div class="context-progress">
<div class="context-progress-bar" id="contextProgressBar" style="width: 0%"></div> <div
class="context-progress-bar"
id="contextProgressBar"
style="width: 0%"
></div>
</div> </div>
</div> </div>
@ -918,11 +932,15 @@
const input = document.getElementById("messageInput"); const input = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn"); const sendBtn = document.getElementById("sendBtn");
const newChatBtn = document.getElementById("newChatBtn"); const newChatBtn = document.getElementById("newChatBtn");
const connectionStatus = document.getElementById("connectionStatus"); const connectionStatus =
document.getElementById("connectionStatus");
const scrollToBottomBtn = document.getElementById("scrollToBottom"); const scrollToBottomBtn = document.getElementById("scrollToBottom");
const contextIndicator = document.getElementById("contextIndicator"); const contextIndicator =
const contextPercentage = document.getElementById("contextPercentage"); document.getElementById("contextIndicator");
const contextProgressBar = document.getElementById("contextProgressBar"); const contextPercentage =
document.getElementById("contextPercentage");
const contextProgressBar =
document.getElementById("contextProgressBar");
marked.setOptions({ marked.setOptions({
breaks: true, breaks: true,
@ -940,7 +958,10 @@
function getWebSocketUrl() { function getWebSocketUrl() {
const protocol = const protocol =
window.location.protocol === "https:" ? "wss:" : "ws:"; window.location.protocol === "https:" ? "wss:" : "ws:";
return `${protocol}//${window.location.host}/ws?session_id=${currentSessionId}&user_id=${currentUserId}`; // Generate UUIDs if not set yet
const sessionId = currentSessionId || crypto.randomUUID();
const userId = currentUserId || crypto.randomUUID();
return `${protocol}//${window.location.host}/ws?session_id=${sessionId}&user_id=${userId}`;
} }
// Auto-focus on input when page loads // Auto-focus on input when page loads
@ -966,7 +987,9 @@
// Scroll management // Scroll management
messagesDiv.addEventListener("scroll", function () { messagesDiv.addEventListener("scroll", function () {
// Check if user is scrolling manually // Check if user is scrolling manually
const isAtBottom = messagesDiv.scrollHeight - messagesDiv.scrollTop <= messagesDiv.clientHeight + 100; const isAtBottom =
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
messagesDiv.clientHeight + 100;
if (!isAtBottom) { if (!isAtBottom) {
isUserScrolling = true; isUserScrolling = true;
@ -1003,9 +1026,11 @@
// Update color based on usage // Update color based on usage
if (percentage >= 90) { if (percentage >= 90) {
contextProgressBar.className = "context-progress-bar danger"; contextProgressBar.className =
"context-progress-bar danger";
} else if (percentage >= 70) { } else if (percentage >= 70) {
contextProgressBar.className = "context-progress-bar warning"; contextProgressBar.className =
"context-progress-bar warning";
} else { } else {
contextProgressBar.className = "context-progress-bar"; contextProgressBar.className = "context-progress-bar";
} }
@ -1308,7 +1333,9 @@
thinkingTimeout = setTimeout(() => { thinkingTimeout = setTimeout(() => {
if (isThinking) { if (isThinking) {
hideThinkingIndicator(); hideThinkingIndicator();
showWarning("O servidor pode estar ocupado. A resposta está demorando demais."); showWarning(
"O servidor pode estar ocupado. A resposta está demorando demais.",
);
} }
}, 60000); }, 60000);
@ -1317,7 +1344,8 @@
function hideThinkingIndicator() { function hideThinkingIndicator() {
if (!isThinking) return; if (!isThinking) return;
const thinkingDiv = document.getElementById("thinking-indicator"); const thinkingDiv =
document.getElementById("thinking-indicator");
if (thinkingDiv) { if (thinkingDiv) {
gsap.to(thinkingDiv, { gsap.to(thinkingDiv, {
opacity: 0, opacity: 0,
@ -1417,8 +1445,9 @@
} }
// Remove the continue button // Remove the continue button
const continueButtons = document.querySelectorAll('.continue-button'); const continueButtons =
continueButtons.forEach(button => { document.querySelectorAll(".continue-button");
continueButtons.forEach((button) => {
button.parentElement.parentElement.parentElement.remove(); button.parentElement.parentElement.parentElement.remove();
}); });
} }