Disable /opt/gbo/data loading, use drive (MinIO) only for bot sources
Some checks failed
BotServer CI/CD / build (push) Failing after 8m28s

- 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)
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-04-08 16:55:50 -03:00
parent 9b04af9e7b
commit 9e799dd6b1
7 changed files with 162 additions and 210 deletions

View file

@ -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<dyn Error + Send + Sync>> {
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() {

View file

@ -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};

View file

@ -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::<Vec<&str>>()
@ -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<String> = 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<Dynamic, Box<EvalAltResult>> {
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<bool> = 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<String> = 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" { ... }

View file

@ -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<std::path::PathBuf> = 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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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::<diesel::sql_types::Uuid, _>(bot_id)
.bind::<diesel::sql_types::Text, _>(org_id.clone())
.bind::<diesel::sql_types::Text, _>(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);

View file

@ -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)

View file

@ -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<Arc<AppState>>,
@ -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<Vec<(String, bool)>, 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<KbRow> = 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)

View file

@ -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<AppState>) {
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<AppState>) {
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;
}