From 9e799dd6b14ae852f2fc830b3e079ad059fcb892 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Wed, 8 Apr 2026 16:55:50 -0300 Subject: [PATCH] Disable /opt/gbo/data loading, use drive (MinIO) only for bot sources - Remove LocalFileMonitor and ConfigWatcher for /opt/gbo/data - Remove /opt/gbo/data from mount_all_bots() scanning - Change start.bas, tables.bas, and tool paths to use work directory - Filter drive buckets to only gbo-* prefix - Remove unused create_bot_simple method - Fix all warnings (unused imports, variables, dead code) --- src/basic/compiler/mod.rs | 10 ++- src/basic/keywords/get.rs | 1 + src/basic/mod.rs | 147 +++++++++++++++++++++++++++++----- src/core/bot/mod.rs | 95 ++-------------------- src/core/bot/tool_executor.rs | 21 +---- src/drive/mod.rs | 41 +++------- src/main_module/bootstrap.rs | 57 ++----------- 7 files changed, 162 insertions(+), 210 deletions(-) diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index 350020d3..13c4c66b 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -802,9 +802,10 @@ impl BasicCompiler { // Find the tables.bas file in the bot's data directory let bot_name = self.get_bot_name_by_id(bot_id)?; + let work_path = crate::core::shared::utils::get_work_path(); let tables_path = format!( - "/opt/gbo/data/{}.gbai/{}.gbdialog/tables.bas", - bot_name, bot_name + "{}/{}.gbai/{}.gbdialog/tables.bas", + work_path, bot_name, bot_name ); let tables_content = fs::read_to_string(&tables_path)?; @@ -855,9 +856,10 @@ impl BasicCompiler { bot_id: uuid::Uuid, ) -> Result<(), Box> { let bot_name = Self::get_bot_name_from_state(state, bot_id)?; + let work_path = crate::core::shared::utils::get_work_path(); let tables_path = format!( - "/opt/gbo/data/{}.gbai/{}.gbdialog/tables.bas", - bot_name, bot_name + "{}/{}.gbai/{}.gbdialog/tables.bas", + work_path, bot_name, bot_name ); if !Path::new(&tables_path).exists() { diff --git a/src/basic/keywords/get.rs b/src/basic/keywords/get.rs index 406b32c3..a8bcdff5 100644 --- a/src/basic/keywords/get.rs +++ b/src/basic/keywords/get.rs @@ -1,5 +1,6 @@ use crate::core::shared::models::UserSession; use crate::core::shared::state::AppState; +use diesel::prelude::*; use log::{error, trace}; use reqwest::{self, Client}; use rhai::{Dynamic, Engine}; diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 685790f6..bb80a39b 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -595,8 +595,9 @@ impl ScriptService { trimmed.starts_with("DESCRIPTION\t") || trimmed.starts_with("REM ") || trimmed.starts_with("REM\t") || - trimmed.starts_with('\'') || // BASIC comment lines - trimmed.starts_with('#') || // Hash comment lines + trimmed == "REM" || // bare REM line + trimmed.starts_with('\'') || // BASIC comment lines + trimmed.starts_with('#') || // Hash comment lines trimmed.is_empty()) }) .collect::>() @@ -607,9 +608,6 @@ impl ScriptService { // Apply minimal preprocessing for tools (skip variable normalization to avoid breaking multi-line strings) let script = preprocess_switch(&executable_script); let script = Self::convert_multiword_keywords(&script); - // Convert FORMAT(expr, pattern) to FORMAT expr pattern for Rhai space-separated function syntax -// FORMAT syntax conversion disabled - Rhai supports comma-separated args natively - // let script = Self::convert_format_syntax(&script); // Skip normalize_variables_to_lowercase for tools - it breaks multi-line strings trace!("Preprocessed tool script for Rhai compilation"); @@ -617,6 +615,12 @@ impl ScriptService { let script = Self::convert_save_for_tools(&script); // Convert BEGIN TALK and BEGIN MAIL blocks to single calls let script = crate::basic::compiler::blocks::convert_begin_blocks(&script); + // Convert WHILE...WEND to Rhai while { } blocks BEFORE if/then conversion + let script = Self::convert_while_wend_syntax(&script); + // Pre-declare all variables at outer scope so assignments inside blocks work correctly. + // In Rhai, a plain `x = val` inside a block updates the outer variable - + // but only if `x` was declared outside with `let`. + let script = Self::predeclare_variables(&script); // Convert IF ... THEN / END IF to if ... { } let script = Self::convert_if_then_syntax(&script); // Convert SELECT ... CASE / END SELECT to match expressions @@ -632,6 +636,62 @@ impl ScriptService { Err(parse_error) => Err(Box::new(parse_error.into())), } } + + /// Pre-declare all BASIC variables at the top of the script with `let var = ();`. + /// This allows assignments inside loops/if-blocks to update outer-scope variables in Rhai. + fn predeclare_variables(script: &str) -> String { + use std::collections::BTreeSet; + let reserved: std::collections::HashSet<&str> = [ + "if", "else", "while", "for", "loop", "return", "break", "continue", + "let", "fn", "true", "false", "in", "do", "match", "switch", "case", + "mod", "and", "or", "not", "rem", "call", "talk", "hear", "save", + "insert", "update", "delete", "find", "get", "set", "print", + ].iter().cloned().collect(); + + let mut vars: BTreeSet = BTreeSet::new(); + + for line in script.lines() { + let t = line.trim(); + if t.is_empty() || t.starts_with("//") || t.starts_with('\'') || t.starts_with('#') { + continue; + } + if let Some(eq_pos) = t.find('=') { + let before = &t[..eq_pos]; + let after_char = t.as_bytes().get(eq_pos + 1).copied(); + let prev_char = if eq_pos > 0 { t.as_bytes().get(eq_pos - 1).copied() } else { None }; + // Skip ==, !=, <=, >=, +=, -=, *=, /= + if after_char == Some(b'=') { continue; } + if matches!(prev_char, Some(b'!') | Some(b'<') | Some(b'>') | Some(b'+') | Some(b'-') | Some(b'*') | Some(b'/')) { continue; } + let lhs = before.trim(); + if lhs.is_empty() || lhs.contains(' ') || lhs.contains('"') || lhs.contains('(') || lhs.contains('[') { + continue; + } + if !lhs.chars().next().is_some_and(|c| c.is_alphabetic() || c == '_') { + continue; + } + if !lhs.chars().all(|c| c.is_alphanumeric() || c == '_') { + continue; + } + let lower = lhs.to_lowercase(); + if reserved.contains(lower.as_str()) { + continue; + } + vars.insert(lhs.to_string()); + } + } + + if vars.is_empty() { + return script.to_string(); + } + + let mut declarations = String::new(); + for v in &vars { + declarations.push_str(&format!("let {} = ();\n", v)); + } + declarations.push('\n'); + declarations.push_str(script); + declarations + } pub fn run(&mut self, ast: &rhai::AST) -> Result> { self.engine.eval_ast_with_scope(&mut self.scope, ast) } @@ -1047,6 +1107,7 @@ impl ScriptService { pub fn convert_if_then_syntax(script: &str) -> String { let mut result = String::new(); let mut if_stack: Vec = Vec::new(); + let mut while_depth: usize = 0; // tracks depth inside while { } blocks let mut in_with_block = false; let mut in_talk_block = false; let mut talk_block_lines: Vec = Vec::new(); @@ -1066,6 +1127,20 @@ impl ScriptService { continue; } + // Track while { } block depth (produced by convert_while_wend_syntax) + if trimmed.starts_with("while ") && trimmed.ends_with('{') { + while_depth += 1; + result.push_str(trimmed); + result.push('\n'); + continue; + } + // A lone closing brace closes the while block + if trimmed == "}" && while_depth > 0 && if_stack.is_empty() { + while_depth -= 1; + result.push_str("}\n"); + continue; + } + // Handle IF ... THEN if upper.starts_with("IF ") && upper.contains(" THEN") { let then_pos = match upper.find(" THEN") { @@ -1339,22 +1414,37 @@ impl ScriptService { }; if is_var_assignment { - // Add 'let' for variable declarations, but only if line doesn't already start with let/LET + // Only add 'let' when at top-level scope (not inside IF or while blocks). + // Inside blocks, plain assignment updates the outer variable in Rhai. + // Using 'let' inside a block creates a local that dies at block end. let trimmed_lower = trimmed.to_lowercase(); - if !trimmed_lower.starts_with("let ") { + let in_block = !if_stack.is_empty() || while_depth > 0; + if !in_block && !trimmed_lower.starts_with("let ") { result.push_str("let "); } } result.push_str(&line_to_process); - // Add semicolon if line doesn't have one and doesn't end with { or } - // Skip adding semicolons to: - // - SELECT/CASE/END SELECT statements (they're converted to if-else later) - // - Lines ending with comma (BASIC line continuation) - // - Lines that are part of a continuation block (in_line_continuation is true) - if !trimmed.ends_with(';') && !trimmed.ends_with('{') && !trimmed.ends_with('}') - && !upper.starts_with("SELECT ") && !upper.starts_with("CASE ") && upper != "END SELECT" - && !upper.starts_with("WHILE ") && !upper.starts_with("WEND") - && !ends_with_comma && !in_line_continuation { + // Determine if we need a semicolon. + // Keyword statements like INSERT "t", #{...} end with `}` from a map literal + // and DO need a semicolon. Block closers (lone `}`) and openers do NOT. + let is_keyword_stmt = upper.starts_with("INSERT ") + || upper.starts_with("SAVE ") + || upper.starts_with("TALK ") + || upper.starts_with("PRINT ") + || upper.starts_with("MERGE ") + || upper.starts_with("UPDATE "); + let ends_with_block_brace = trimmed.ends_with('}') && !is_keyword_stmt; + let needs_semicolon = !trimmed.ends_with(';') + && !trimmed.ends_with('{') + && !ends_with_block_brace + && !upper.starts_with("SELECT ") + && !upper.starts_with("CASE ") + && upper != "END SELECT" + && !upper.starts_with("WHILE ") + && !upper.starts_with("WEND") + && !ends_with_comma + && !in_line_continuation; + if needs_semicolon { result.push(';'); } result.push('\n'); @@ -1370,11 +1460,32 @@ impl ScriptService { log::trace!("IF/THEN conversion complete, output has {} lines", result.lines().count()); // Convert BASIC <> (not equal) to Rhai != globally - - result.replace(" <> ", " != ") } + /// Convert BASIC WHILE...WEND loops to Rhai while { } blocks + /// WHILE condition → while condition {\n + /// WEND → }\n + pub fn convert_while_wend_syntax(script: &str) -> String { + let mut result = String::new(); + for line in script.lines() { + let trimmed = line.trim(); + let upper = trimmed.to_uppercase(); + + if upper.starts_with("WHILE ") { + // Extract condition (everything after "WHILE ") + let condition = &trimmed[6..]; + result.push_str(&format!("while {} {{\n", condition)); + } else if upper == "WEND" { + result.push_str("}\n"); + } else { + result.push_str(line); + result.push('\n'); + } + } + result + } + /// Convert BASIC SELECT ... CASE / END SELECT to if-else chains /// Transforms: SELECT var ... CASE "value" ... END SELECT /// Into: if var == "value" { ... } else if var == "value2" { ... } diff --git a/src/core/bot/mod.rs b/src/core/bot/mod.rs index 83749aa7..47b91fd9 100644 --- a/src/core/bot/mod.rs +++ b/src/core/bot/mod.rs @@ -244,9 +244,6 @@ impl BotOrchestrator { info!("Scanning drive for .gbai files to mount bots..."); let mut bots_mounted = 0; - let mut bots_created = 0; - - let data_dir = "/opt/gbo/data"; let directories_to_scan: Vec = vec![ self.state @@ -257,7 +254,6 @@ impl BotOrchestrator { .into(), "./templates".into(), "../bottemplates".into(), - data_dir.into(), ]; for dir_path in directories_to_scan { @@ -268,7 +264,7 @@ impl BotOrchestrator { continue; } - match self.scan_directory(&dir_path, &mut bots_mounted, &mut bots_created) { + match self.scan_directory(&dir_path, &mut bots_mounted) { Ok(()) => {} Err(e) => { error!("Failed to scan directory {}: {}", dir_path.display(), e); @@ -293,7 +289,6 @@ impl BotOrchestrator { &self, dir_path: &std::path::Path, bots_mounted: &mut i32, - bots_created: &mut i32, ) -> Result<(), Box> { let entries = std::fs::read_dir(dir_path).map_err(|e| format!("Failed to read directory: {}", e))?; @@ -314,20 +309,7 @@ impl BotOrchestrator { *bots_mounted += 1; } Ok(false) => { - // Auto-create bots found in /opt/gbo/data - if dir_path.to_string_lossy().contains("/data") { - info!("Auto-creating bot '{}' from /opt/gbo/data", bot_name); - match self.create_bot_simple(bot_name) { - Ok(_) => { - info!("Bot '{}' created successfully", bot_name); - *bots_created += 1; - *bots_mounted += 1; - } - Err(e) => { - error!("Failed to create bot '{}': {}", bot_name, e); - } - } - } else { + { info!( "Bot '{}' does not exist in database, skipping (run import to create)", bot_name @@ -372,69 +354,6 @@ impl BotOrchestrator { Ok(exists.exists) } - fn create_bot_simple(&self, bot_name: &str) -> Result<(), Box> { - use diesel::sql_query; - use uuid::Uuid; - - let mut conn = self - .state - .conn - .get() - .map_err(|e| format!("Failed to get database connection: {e}"))?; - - // Check if bot already exists - let exists = self.ensure_bot_exists(bot_name)?; - if exists { - info!("Bot '{}' already exists, skipping creation", bot_name); - return Ok(()); - } - - // Ensure default tenant exists - sql_query( - "INSERT INTO tenants (id, name, slug, created_at) \ - VALUES ('00000000-0000-0000-0000-000000000001', 'Default Tenant', 'default', NOW()) \ - ON CONFLICT (slug) DO NOTHING" - ) - .execute(&mut conn) - .ok(); - - // Ensure default organization exists (with default slug or use existing) - sql_query( - "INSERT INTO organizations (org_id, tenant_id, name, slug, created_at) \ - VALUES ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001', 'Default Organization', 'default', NOW()) \ - ON CONFLICT (slug) DO NOTHING" - ) - .execute(&mut conn) - .ok(); - - // Get default organization by slug - #[derive(diesel::QueryableByName)] - #[diesel(check_for_backend(diesel::pg::Pg))] - struct OrgResult { - #[diesel(sql_type = diesel::sql_types::Uuid)] - org_id: uuid::Uuid, - } - - let org_result: OrgResult = sql_query("SELECT org_id FROM organizations WHERE slug = 'default' LIMIT 1") - .get_result(&mut conn) - .map_err(|e| format!("Failed to get default organization: {e}"))?; - let org_id = org_result.org_id.to_string(); - - let bot_id = Uuid::new_v4(); - - sql_query( - "INSERT INTO bots (id, org_id, name, llm_provider, context_provider, is_active, created_at, updated_at) - VALUES ($1, $2::uuid, $3, 'openai', 'website', true, NOW(), NOW())" - ) - .bind::(bot_id) - .bind::(org_id.clone()) - .bind::(bot_name) - .execute(&mut conn) - .map_err(|e| format!("Failed to create bot: {e}"))?; - - info!("User system created resource: bot {} with org_id {}", bot_id, org_id); - Ok(()) - } #[cfg(feature = "llm")] pub async fn stream_response( @@ -671,9 +590,9 @@ impl BotOrchestrator { }; if should_execute_start_bas { - // Always execute start.bas for this session (blocking - wait for completion) - let data_dir = "/opt/gbo/data"; - let start_script_path = format!("{}/{}.gbai/{}.gbdialog/start.bas", data_dir, bot_name_for_context, bot_name_for_context); + // Execute start.bas from work directory + let work_path = crate::core::shared::utils::get_work_path(); + let start_script_path = format!("{}/{}.gbai/{}.gbdialog/start.bas", work_path, bot_name_for_context, bot_name_for_context); trace!("Executing start.bas for session {} at: {}", actual_session_id, start_script_path); @@ -1451,8 +1370,8 @@ async fn handle_websocket( let should_execute_start_bas = true; if should_execute_start_bas { - let data_dir = "/opt/gbo/data"; - let start_script_path = format!("{}/{}.gbai/{}.gbdialog/start.bas", data_dir, bot_name, bot_name); + let work_path = crate::core::shared::utils::get_work_path(); + let start_script_path = format!("{}/{}.gbai/{}.gbdialog/start.bas", work_path, bot_name, bot_name); info!("Looking for start.bas at: {}", start_script_path); diff --git a/src/core/bot/tool_executor.rs b/src/core/bot/tool_executor.rs index 69640fe2..6843ca8d 100644 --- a/src/core/bot/tool_executor.rs +++ b/src/core/bot/tool_executor.rs @@ -2,10 +2,8 @@ /// Works across all LLM providers (GLM, OpenAI, Claude, etc.) use log::{error, info, trace, warn}; use serde_json::Value; -// use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Write; -use std::path::Path; use std::sync::Arc; use uuid::Uuid; @@ -341,28 +339,13 @@ impl ToolExecutor { /// Get the path to a tool's .bas file fn get_tool_bas_path(bot_name: &str, tool_name: &str) -> std::path::PathBuf { - // Try source directory first (/opt/gbo/data - primary location for bot source files) - let source_path = Path::new("/opt/gbo/data") - .join(format!("{}.gbai", bot_name)) - .join(format!("{}.gbdialog", bot_name)) - .join(format!("{}.bas", tool_name)); - - if source_path.exists() { - return source_path; - } - - // Try compiled work directory (work relative to stack path) + // Use work directory for compiled .bas files let work_path = std::path::PathBuf::from(crate::core::shared::utils::get_work_path()) .join(format!("{}.gbai", bot_name)) .join(format!("{}.gbdialog", bot_name)) .join(format!("{}.bas", tool_name)); - if work_path.exists() { - return work_path; - } - - // Fallback to source path for error messages (even if it doesn't exist) - source_path + work_path } /// Execute a tool directly by name (without going through LLM) diff --git a/src/drive/mod.rs b/src/drive/mod.rs index d6045f80..cf32f300 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -15,8 +15,6 @@ use axum::{ Router, }; -#[cfg(feature = "drive")] -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; #[cfg(feature = "drive")] use diesel::{QueryableByName, RunQueryDsl}; @@ -324,7 +322,6 @@ pub async fn open_file( })) } -#[cfg(feature = "drive")] #[cfg(feature = "drive")] pub async fn list_buckets( State(state): State>, @@ -347,11 +344,19 @@ pub async fn list_buckets( .buckets() .iter() .filter_map(|b| { - b.name().map(|name| BucketInfo { - name: name.to_string(), - is_gbai: name.to_lowercase().ends_with(".gbai"), + b.name().map(|name| { + let name_str = name.to_string(); + // Only include buckets that start with "gbo-" + if !name_str.starts_with("gbo-") { + return None; + } + Some(BucketInfo { + name: name_str, + is_gbai: name.to_lowercase().ends_with(".gbai"), + }) }) }) + .flatten() .collect(); Ok(Json(buckets)) @@ -375,30 +380,6 @@ pub async fn list_files( let mut items = Vec::new(); let prefix = params.path.as_deref().unwrap_or(""); - let kbs: Vec<(String, bool)> = { - let conn = state.conn.clone(); - let kbs_result = tokio::task::spawn_blocking(move || -> Result, String> { - #[derive(QueryableByName)] - struct KbRow { - #[diesel(sql_type = diesel::sql_types::Text)] - name: String, - #[diesel(sql_type = diesel::sql_types::Bool)] - is_public: bool, - } - let mut db_conn = conn.get().map_err(|e| e.to_string())?; - let rows: Vec = diesel::sql_query( - "SELECT name, COALESCE(is_public, false) as is_public FROM kb_collections" - ) - .load(&mut db_conn) -.map_err(|e: diesel::result::Error| e.to_string())?; - Ok(rows.into_iter().map(|r| (r.name, r.is_public)).collect()) - }).await; - match kbs_result { - Ok(Ok(kbs)) => kbs, - _ => vec![], - } - }; - let paginator = s3_client .list_objects_v2() .bucket(bucket) diff --git a/src/main_module/bootstrap.rs b/src/main_module/bootstrap.rs index cbdafaf5..8b755009 100644 --- a/src/main_module/bootstrap.rs +++ b/src/main_module/bootstrap.rs @@ -928,30 +928,10 @@ async fn start_drive_monitors( .await .unwrap_or_default(); - let local_dev_bots = tokio::task::spawn_blocking(move || { - let mut bots = std::collections::HashSet::new(); - let data_dir = std::env::var("DATA_DIR").unwrap_or_else(|_| "/opt/gbo/data".to_string()); - if let Ok(entries) = std::fs::read_dir(data_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()).map(|e| e.eq_ignore_ascii_case("gbai")).unwrap_or(false) { - if let Some(bot_name) = path.file_stem().and_then(|s| s.to_str()) { - bots.insert(bot_name.to_string()); - } - } - } - } - bots - }) - .await - .unwrap_or_default(); - info!("Found {} active bots to monitor", bots_to_monitor.len()); for (bot_id, bot_name) in bots_to_monitor { - // Skip default bot and local dev bots - they are managed locally - if bot_name == "default" || local_dev_bots.contains(&bot_name) { - info!("Skipping DriveMonitor for '{}' bot - managed locally via ConfigWatcher/LocalFileMonitor", bot_name); + if bot_name == "default" { continue; } @@ -986,38 +966,13 @@ async fn start_drive_monitors( }); } -#[cfg(feature = "local-files")] +// LocalFileMonitor and ConfigWatcher disabled - drive (MinIO) is the only source now async fn start_local_file_monitor(app_state: Arc) { - use crate::core::shared::memory_monitor::register_thread; - tokio::spawn(async move { - register_thread("local-file-monitor", "drive"); - trace!("Starting LocalFileMonitor for /opt/gbo/data/*.gbai directories"); - let monitor = crate::drive::local_file_monitor::LocalFileMonitor::new(app_state); - if let Err(e) = monitor.start_monitoring().await { - error!("LocalFileMonitor failed: {}", e); - } else { - info!("LocalFileMonitor started - watching /opt/gbo/data/*.gbai/*.gbdialog/*.bas"); - } - }); + trace!("LocalFileMonitor disabled for state - using drive (MinIO) only"); + let _ = app_state; } async fn start_config_watcher(app_state: Arc) { - use crate::core::shared::memory_monitor::register_thread; - tokio::spawn(async move { - register_thread("config-file-watcher", "drive"); - trace!("Starting ConfigWatcher for /opt/gbo/data/*.gbai/*.gbot/config.csv"); - - // Determine data directory - let data_dir = std::env::var("DATA_DIR") - .unwrap_or_else(|_| "/opt/gbo/data".to_string()); - let data_dir = std::path::PathBuf::from(data_dir); - - let watcher = crate::core::config::watcher::ConfigWatcher::new( - data_dir, - app_state, - ); - Arc::new(watcher).spawn(); - - info!("ConfigWatcher started - watching /opt/gbo/data/*.gbai/*.gbot/config.csv"); - }); + trace!("ConfigWatcher disabled for state - using drive (MinIO) only"); + let _ = app_state; }