diff --git a/src/auth/auth_test.rs b/src/auth/auth_test.rs index f262f1115..65fa5fcc7 100644 --- a/src/auth/auth_test.rs +++ b/src/auth/auth_test.rs @@ -1,8 +1,6 @@ - #[cfg(test)] mod tests { use crate::tests::test_util; - #[test] fn test_invalid_token_format() { test_util::setup(); diff --git a/src/automation/automation.test.rs b/src/automation/automation.test.rs index 42be93ab2..dcdeea713 100644 --- a/src/automation/automation.test.rs +++ b/src/automation/automation.test.rs @@ -1,10 +1,7 @@ -//! Tests for automation module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_automation_module() { test_util::setup(); diff --git a/src/automation/compact_prompt.rs b/src/automation/compact_prompt.rs index 7da650601..1b5067fca 100644 --- a/src/automation/compact_prompt.rs +++ b/src/automation/compact_prompt.rs @@ -14,80 +14,58 @@ pub fn start_compact_prompt_scheduler(state: Arc) { let mut interval = interval(Duration::from_secs(60)); loop { interval.tick().await; - if let Err(e) = execute_compact_prompt(Arc::clone(&state)).await { + if let Err(e) = compact_prompt_for_bots(&Arc::clone(&state)).await { error!("Prompt compaction failed: {}", e); } } }); } -async fn execute_compact_prompt( - state: Arc, -) -> Result<(), Box> { - use crate::shared::models::system_automations::dsl::{is_active, system_automations}; - let automations: Vec = { - let mut conn = state - .conn - .get() - .map_err(|e| format!("Failed to acquire lock: {}", e))?; - system_automations - .filter(is_active.eq(true)) - .load::(&mut *conn)? - }; - for automation in automations { - if let Err(e) = compact_prompt_for_bot(&state, &automation).await { - error!( - "Failed to compact prompt for bot {}: {}", - automation.bot_id, e - ); - } - } - Ok(()) -} -async fn compact_prompt_for_bot( +async fn compact_prompt_for_bots( state: &Arc, - automation: &Automation, ) -> Result<(), Box> { use once_cell::sync::Lazy; use scopeguard::guard; - static IN_PROGRESS: Lazy>> = + static SESSION_IN_PROGRESS: Lazy>> = Lazy::new(|| tokio::sync::Mutex::new(HashSet::new())); - { - let mut in_progress = IN_PROGRESS.lock().await; - if in_progress.contains(&automation.bot_id) { - return Ok(()); - } - in_progress.insert(automation.bot_id); - } - let bot_id = automation.bot_id; - let _cleanup = guard((), |_| { - tokio::spawn(async move { - let mut in_progress = IN_PROGRESS.lock().await; - in_progress.remove(&bot_id); - }); - }); - let config_manager = ConfigManager::new(state.conn.clone()); - let compact_threshold = config_manager - .get_config(&automation.bot_id, "prompt-compact", None)? - .parse::() - .unwrap_or(0); - if compact_threshold == 0 { - return Ok(()); - } else if compact_threshold < 0 { - trace!( - "Negative compact threshold detected for bot {}, skipping", - automation.bot_id - ); - } + let sessions = { let mut session_manager = state.session_manager.lock().await; session_manager.get_user_sessions(Uuid::nil())? }; for session in sessions { - if session.bot_id != automation.bot_id { - trace!("Skipping session {} - bot_id {} doesn't match automation bot_id {}", - session.id, session.bot_id, automation.bot_id); - continue; + { + let mut session_in_progress = SESSION_IN_PROGRESS.lock().await; + if session_in_progress.contains(&session.id) { + trace!( + "Skipping session {} - compaction already in progress", + session.id + ); + continue; + } + session_in_progress.insert(session.id); } + + let config_manager = ConfigManager::new(state.conn.clone()); + let compact_threshold = config_manager + .get_config(&session.bot_id, "prompt-compact", None)? + .parse::() + .unwrap_or(0); + + if compact_threshold == 0 { + return Ok(()); + } else if compact_threshold < 0 { + trace!( + "Negative compact threshold detected for bot {}, skipping", + session.bot_id + ); + } + let session_id = session.id; + let _session_cleanup = guard((), |_| { + tokio::spawn(async move { + let mut in_progress = SESSION_IN_PROGRESS.lock().await; + in_progress.remove(&session_id); + }); + }); let history = { let mut session_manager = state.session_manager.lock().await; session_manager.get_conversation_history(session.id, session.user_id)? @@ -95,11 +73,16 @@ async fn compact_prompt_for_bot( let mut messages_since_summary = 0; let mut has_new_messages = false; - let mut last_summary_index = history.iter().position(|(role, _)| - role == "compact") - .unwrap_or(0); - - for (i, (role, _)) in history.iter().enumerate().skip(last_summary_index + 1) { + let last_summary_index = history + .iter() + .rev() + .position(|(role, _)| role == "compact") + .map(|pos| history.len() - pos - 1); + + // Calculate start index: if there's a summary, start after it; otherwise start from 0 + let start_index = last_summary_index.map(|idx| idx + 1).unwrap_or(0); + + for (i, (role, _)) in history.iter().enumerate().skip(start_index) { if role == "compact" { continue; } @@ -107,8 +90,11 @@ async fn compact_prompt_for_bot( has_new_messages = true; } - if !has_new_messages { - trace!("Skipping session {} - no new messages since last summary", session.id); + if !has_new_messages && last_summary_index.is_some() { + trace!( + "Skipping session {} - no new messages since last summary", + session.id + ); continue; } if messages_since_summary < compact_threshold as usize { @@ -123,11 +109,14 @@ async fn compact_prompt_for_bot( messages_since_summary ); let mut compacted = String::new(); - let messages_to_include = history.iter() - .skip(history.len().saturating_sub(messages_since_summary )) - .take(messages_since_summary + 1); - + + // Include messages from start_index onward + let messages_to_include = history.iter().skip(start_index); + for (role, content) in messages_to_include { + if role == "compact" { + continue; + } compacted.push_str(&format!("{}: {}\n", role, content)); } let llm_provider = state.llm_provider.clone(); @@ -141,7 +130,7 @@ async fn compact_prompt_for_bot( ); let handler = llm_models::get_handler( &config_manager - .get_config(&automation.bot_id, "llm-model", None) + .get_config(&session.bot_id, "llm-model", None) .unwrap_or_default(), ); let filtered = handler.process_content(&summary); diff --git a/src/basic/basic.test.rs b/src/basic/basic.test.rs index 117ba2094..b97b0c2ee 100644 --- a/src/basic/basic.test.rs +++ b/src/basic/basic.test.rs @@ -1,10 +1,7 @@ -//! Tests for basic module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_basic_module() { test_util::setup(); diff --git a/src/basic/compiler/compiler.test.rs b/src/basic/compiler/compiler.test.rs index 5229a11d1..a02fbbcf8 100644 --- a/src/basic/compiler/compiler.test.rs +++ b/src/basic/compiler/compiler.test.rs @@ -1,11 +1,8 @@ - #[cfg(test)] mod tests { use super::*; use diesel::Connection; use std::sync::Mutex; - - // Test-only AppState that skips database operations #[cfg(test)] mod test_utils { use super::*; @@ -17,51 +14,39 @@ mod tests { use diesel::sql_types::Untyped; use diesel::deserialize::Queryable; use std::sync::{Arc, Mutex}; - - // Mock PgConnection that implements required traits struct MockPgConnection; - impl Connection for MockPgConnection { type Backend = Pg; type TransactionManager = diesel::connection::AnsiTransactionManager; - fn establish(_: &str) -> diesel::ConnectionResult { Ok(MockPgConnection { transaction_manager: diesel::connection::AnsiTransactionManager::default() }) } - fn execute(&self, _: &str) -> QueryResult { Ok(0) } - fn load(&self, _: &diesel::query_builder::SqlQuery) -> QueryResult where T: Queryable, { unimplemented!() } - fn execute_returning_count(&self, _: &T) -> QueryResult where T: QueryFragment + QueryId, { Ok(0) } - fn transaction_state(&self) -> &diesel::connection::AnsiTransactionManager { &self.transaction_manager } - fn instrumentation(&self) -> &dyn diesel::connection::Instrumentation { &diesel::connection::NoopInstrumentation } - fn set_instrumentation(&mut self, _: Box) {} - fn set_prepared_statement_cache_size(&mut self, _: usize) {} } - impl AppState { pub fn test_default() -> Self { let mut state = Self::default(); @@ -70,11 +55,9 @@ mod tests { } } } - #[test] fn test_normalize_type() { let state = AppState::test_default(); - let compiler = BasicCompiler::new(Arc::new(state), uuid::Uuid::nil()); assert_eq!(compiler.normalize_type("string"), "string"); assert_eq!(compiler.normalize_type("integer"), "integer"); @@ -82,16 +65,12 @@ mod tests { assert_eq!(compiler.normalize_type("boolean"), "boolean"); assert_eq!(compiler.normalize_type("date"), "string"); } - #[test] fn test_parse_param_line() { let state = AppState::test_default(); - let compiler = BasicCompiler::new(Arc::new(state), uuid::Uuid::nil()); - let line = r#"PARAM name AS string LIKE "John Doe" DESCRIPTION "User's full name""#; let result = compiler.parse_param_line(line).unwrap(); - assert!(result.is_some()); let param = result.unwrap(); assert_eq!(param.name, "name"); diff --git a/src/basic/keywords/add_suggestion.test.rs b/src/basic/keywords/add_suggestion.test.rs index 7e05d4ac0..cdf7e2bf5 100644 --- a/src/basic/keywords/add_suggestion.test.rs +++ b/src/basic/keywords/add_suggestion.test.rs @@ -1,16 +1,12 @@ -//! Tests for add_suggestion keyword - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_add_suggestion() { test_util::setup(); assert!(true, "Basic add_suggestion test"); } - #[test] fn test_suggestion_validation() { test_util::setup(); diff --git a/src/basic/keywords/add_tool.test.rs b/src/basic/keywords/add_tool.test.rs index 0843faaf8..6cebe1ba4 100644 --- a/src/basic/keywords/add_tool.test.rs +++ b/src/basic/keywords/add_tool.test.rs @@ -1,16 +1,12 @@ -//! Tests for add_tool keyword - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_add_tool() { test_util::setup(); assert!(true, "Basic add_tool test"); } - #[test] fn test_tool_validation() { test_util::setup(); diff --git a/src/basic/keywords/add_website.rs b/src/basic/keywords/add_website.rs index 26b925da1..0e233da14 100644 --- a/src/basic/keywords/add_website.rs +++ b/src/basic/keywords/add_website.rs @@ -4,48 +4,71 @@ use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; pub fn add_website_keyword(state: Arc, user: UserSession, engine: &mut Engine) { - let state_clone = Arc::clone(&state); - let user_clone = user.clone(); - engine - .register_custom_syntax(&["ADD_WEBSITE", "$expr$"], false, move |context, inputs| { - let url = context.eval_expression_tree(&inputs[0])?; - let url_str = url.to_string().trim_matches('"').to_string(); - trace!("ADD_WEBSITE command executed: {} for user: {}", url_str, user_clone.user_id); - let is_valid = url_str.starts_with("http://") || url_str.starts_with("https://"); - if !is_valid { - return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("Invalid URL format. Must start with http:// or https://".into(), rhai::Position::NONE))); - } - let state_for_task = Arc::clone(&state_clone); - let user_for_task = user_clone.clone(); - let url_for_task = url_str.clone(); - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || { - let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build(); - let send_err = if let Ok(rt) = rt { - let result = rt.block_on(async move { - crawl_and_index_website(&state_for_task, &user_for_task, &url_for_task).await - }); - tx.send(result).err() - } else { - tx.send(Err("Failed to build tokio runtime".to_string())).err() - }; - if send_err.is_some() { - error!("Failed to send result from thread"); - } - }); - match rx.recv_timeout(std::time::Duration::from_secs(120)) { - Ok(Ok(message)) => { - Ok(Dynamic::from(message)) - } - Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { - Err(Box::new(rhai::EvalAltResult::ErrorRuntime("ADD_WEBSITE timed out".into(), rhai::Position::NONE))) - } - Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("ADD_WEBSITE failed: {}", e).into(), rhai::Position::NONE))), - } - }) - .unwrap(); + let state_clone = Arc::clone(&state); + let user_clone = user.clone(); + engine + .register_custom_syntax(&["ADD_WEBSITE", "$expr$"], false, move |context, inputs| { + let url = context.eval_expression_tree(&inputs[0])?; + let url_str = url.to_string().trim_matches('"').to_string(); + trace!( + "ADD_WEBSITE command executed: {} for user: {}", + url_str, + user_clone.user_id + ); + let is_valid = url_str.starts_with("http://") || url_str.starts_with("https://"); + if !is_valid { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + "Invalid URL format. Must start with http:// or https://".into(), + rhai::Position::NONE, + ))); + } + let state_for_task = Arc::clone(&state_clone); + let user_for_task = user_clone.clone(); + let url_for_task = url_str.clone(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(2) + .enable_all() + .build(); + let send_err = if let Ok(rt) = rt { + let result = rt.block_on(async move { + crawl_and_index_website(&state_for_task, &user_for_task, &url_for_task) + .await + }); + tx.send(result).err() + } else { + tx.send(Err("Failed to build tokio runtime".to_string())) + .err() + }; + if send_err.is_some() { + error!("Failed to send result from thread"); + } + }); + match rx.recv_timeout(std::time::Duration::from_secs(120)) { + Ok(Ok(message)) => Ok(Dynamic::from(message)), + Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + e.into(), + rhai::Position::NONE, + ))), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + "ADD_WEBSITE timed out".into(), + rhai::Position::NONE, + ))) + } + Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + format!("ADD_WEBSITE failed: {}", e).into(), + rhai::Position::NONE, + ))), + } + }) + .unwrap(); } -async fn crawl_and_index_website(_state: &AppState, _user: &UserSession, _url: &str) -> Result { - Err("Web automation functionality has been removed from this build".to_string()) +async fn crawl_and_index_website( + _state: &AppState, + _user: &UserSession, + _url: &str, +) -> Result { + Err("Web automation functionality has been removed from this build".to_string()) } diff --git a/src/basic/keywords/bot_memory.rs b/src/basic/keywords/bot_memory.rs index 10b249686..b3824e8a8 100644 --- a/src/basic/keywords/bot_memory.rs +++ b/src/basic/keywords/bot_memory.rs @@ -5,7 +5,6 @@ use log::{error, trace}; use rhai::{Dynamic, Engine}; use std::sync::Arc; use uuid::Uuid; - pub fn set_bot_memory_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); @@ -77,7 +76,6 @@ pub fn set_bot_memory_keyword(state: Arc, user: UserSession, engine: & }) .unwrap(); } - pub fn get_bot_memory_keyword(state: Arc, user: UserSession, engine: &mut Engine) { let state_clone = Arc::clone(&state); let user_clone = user.clone(); diff --git a/src/basic/keywords/format.test.rs b/src/basic/keywords/format.test.rs index 2761ca059..fdb4f4b0d 100644 --- a/src/basic/keywords/format.test.rs +++ b/src/basic/keywords/format.test.rs @@ -1,30 +1,22 @@ -//! Tests for format keyword module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_currency_formatting() { test_util::setup(); - // Test matches actual formatting behavior let formatted = format_currency(1234.56, "R$"); assert_eq!(formatted, "R$ 1.234.56", "Currency formatting should use periods"); } - #[test] fn test_numeric_formatting_with_locale() { test_util::setup(); - // Test matches actual formatting behavior let formatted = format_number(1234.56, 2); assert_eq!(formatted, "1.234.56", "Number formatting should use periods"); } - #[test] fn test_text_formatting() { test_util::setup(); - // Test matches actual behavior let formatted = format_text("hello", "HELLO"); assert_eq!(formatted, "Result: helloHELLO", "Text formatting should concatenate"); } diff --git a/src/basic/keywords/last.test.rs b/src/basic/keywords/last.test.rs index 2982cf47a..5fdb13572 100644 --- a/src/basic/keywords/last.test.rs +++ b/src/basic/keywords/last.test.rs @@ -1,24 +1,18 @@ -//! Tests for last keyword module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_last_keyword_mixed_whitespace() { test_util::setup(); - // Test matches actual parsing behavior let result = std::panic::catch_unwind(|| { parse_input("hello\tworld\n"); }); assert!(result.is_err(), "Should fail on mixed whitespace"); } - #[test] fn test_last_keyword_tabs_and_newlines() { test_util::setup(); - // Test matches actual parsing behavior let result = std::panic::catch_unwind(|| { parse_input("hello\n\tworld"); }); diff --git a/src/basic/keywords/mod.rs b/src/basic/keywords/mod.rs index 3ad5bd360..3d4a04264 100644 --- a/src/basic/keywords/mod.rs +++ b/src/basic/keywords/mod.rs @@ -21,6 +21,5 @@ pub mod wait; pub mod add_suggestion; pub mod set_user; pub mod set_context; - #[cfg(feature = "email")] pub mod create_draft_keyword; diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 672ac63de..1b63434bb 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -5,10 +5,8 @@ use crate::shared::state::AppState; use log::info; use rhai::{Dynamic, Engine, EvalAltResult}; use std::sync::Arc; - pub mod compiler; pub mod keywords; - use self::keywords::add_tool::add_tool_keyword; use self::keywords::add_website::add_website_keyword; use self::keywords::bot_memory::{get_bot_memory_keyword, set_bot_memory_keyword}; @@ -30,25 +28,18 @@ use self::keywords::set::set_keyword; use self::keywords::set_kb::{add_kb_keyword, set_kb_keyword}; use self::keywords::wait::wait_keyword; use self::keywords::add_suggestion::add_suggestion_keyword; - #[cfg(feature = "email")] use self::keywords::create_draft_keyword; - - pub struct ScriptService { pub engine: Engine, } - impl ScriptService { pub fn new(state: Arc, user: UserSession) -> Self { let mut engine = Engine::new(); - engine.set_allow_anonymous_fn(true); engine.set_allow_looping(true); - #[cfg(feature = "email")] create_draft_keyword(&state, user.clone(), &mut engine); - set_bot_memory_keyword(state.clone(), user.clone(), &mut engine); get_bot_memory_keyword(state.clone(), user.clone(), &mut engine); create_site_keyword(&state, user.clone(), &mut engine); @@ -68,8 +59,6 @@ impl ScriptService { set_context_keyword(state.clone(), user.clone(), &mut engine); set_user_keyword(state.clone(), user.clone(), &mut engine); clear_suggestions_keyword(state.clone(), user.clone(), &mut engine); - - // KB and Tools keywords set_kb_keyword(state.clone(), user.clone(), &mut engine); add_kb_keyword(state.clone(), user.clone(), &mut engine); add_tool_keyword(state.clone(), user.clone(), &mut engine); @@ -77,26 +66,19 @@ impl ScriptService { list_tools_keyword(state.clone(), user.clone(), &mut engine); add_website_keyword(state.clone(), user.clone(), &mut engine); add_suggestion_keyword(state.clone(), user.clone(), &mut engine); - - ScriptService { engine, - } } - fn preprocess_basic_script(&self, script: &str) -> String { let mut result = String::new(); let mut for_stack: Vec = Vec::new(); let mut current_indent = 0; - for line in script.lines() { let trimmed = line.trim(); - - if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("REM") { + if trimmed.is_empty() || trimmed.starts_with("//"){ continue; } - if trimmed.starts_with("FOR EACH") { for_stack.push(current_indent); result.push_str(&" ".repeat(current_indent)); @@ -107,7 +89,6 @@ impl ScriptService { result.push('\n'); continue; } - if trimmed.starts_with("NEXT") { if let Some(expected_indent) = for_stack.pop() { if (current_indent - 4) != expected_indent { @@ -125,16 +106,13 @@ impl ScriptService { panic!("NEXT without matching FOR EACH"); } } - if trimmed == "EXIT FOR" { result.push_str(&" ".repeat(current_indent)); result.push_str(trimmed); result.push('\n'); continue; } - result.push_str(&" ".repeat(current_indent)); - let basic_commands = [ "SET", "CREATE", @@ -158,12 +136,10 @@ impl ScriptService { "GET BOT MEMORY", "SET BOT MEMORY", ]; - let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd)); let is_control_flow = trimmed.starts_with("IF") || trimmed.starts_with("ELSE") || trimmed.starts_with("END IF"); - if is_basic_command || !for_stack.is_empty() || is_control_flow { result.push_str(trimmed); result.push(';'); @@ -175,14 +151,11 @@ impl ScriptService { } result.push('\n'); } - if !for_stack.is_empty() { panic!("Unclosed FOR EACH loop"); } - result } - pub fn compile(&self, script: &str) -> Result> { let processed_script = self.preprocess_basic_script(script); info!("Processed Script:\n{}", processed_script); @@ -191,7 +164,6 @@ impl ScriptService { Err(parse_error) => Err(Box::new(parse_error.into())), } } - pub fn run(&self, ast: &rhai::AST) -> Result> { self.engine.eval_ast(ast) } diff --git a/src/bootstrap/bootstrap.test.rs b/src/bootstrap/bootstrap.test.rs index b811839a5..2fa9fbffb 100644 --- a/src/bootstrap/bootstrap.test.rs +++ b/src/bootstrap/bootstrap.test.rs @@ -1,10 +1,7 @@ -//! Tests for bootstrap module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_bootstrap_module() { test_util::setup(); diff --git a/src/bot/bot.test.rs b/src/bot/bot.test.rs index 7d283621d..3d2bf5a0a 100644 --- a/src/bot/bot.test.rs +++ b/src/bot/bot.test.rs @@ -1,10 +1,7 @@ -//! Tests for bot module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_bot_module() { test_util::setup(); diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 69e5ac215..4a7b89c76 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -1,5 +1,4 @@ mod ui; - use crate::config::ConfigManager; use crate::drive_monitor::DriveMonitor; use crate::llm_models; @@ -20,11 +19,9 @@ use tokio::sync::mpsc; use tokio::sync::Mutex as AsyncMutex; use tokio::time::Instant; use uuid::Uuid; - pub fn get_default_bot(conn: &mut PgConnection) -> (Uuid, String) { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - match bots .filter(is_active.eq(true)) .select((id, name)) @@ -42,29 +39,21 @@ pub fn get_default_bot(conn: &mut PgConnection) -> (Uuid, String) { } } } - pub struct BotOrchestrator { pub state: Arc, pub mounted_bots: Arc>>>, } - impl BotOrchestrator { pub fn new(state: Arc) -> Self { let orchestrator = Self { state, mounted_bots: Arc::new(AsyncMutex::new(HashMap::new())), }; - - // Spawn internal automation to run compact prompt every minute if enabled - // Compact automation disabled to avoid Send issues in background task - orchestrator } - pub async fn mount_all_bots(&self) -> Result<(), Box> { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = self.state.conn.get().unwrap(); let active_bots = bots .filter(is_active.eq(true)) @@ -74,12 +63,10 @@ impl BotOrchestrator { error!("Failed to query active bots: {}", e); e })?; - for bot_guid in active_bots { let state_clone = self.state.clone(); let mounted_bots_clone = self.mounted_bots.clone(); let bot_guid_str = bot_guid.to_string(); - tokio::spawn(async move { if let Err(e) = Self::mount_bot_task(state_clone, mounted_bots_clone, bot_guid_str.clone()) @@ -89,10 +76,8 @@ impl BotOrchestrator { } }); } - Ok(()) } - async fn mount_bot_task( state: Arc, mounted_bots: Arc>>>, @@ -100,7 +85,6 @@ impl BotOrchestrator { ) -> Result<(), Box> { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let bot_name: String = { let mut db_conn = state.conn.get().unwrap(); bots.filter(id.eq(Uuid::parse_str(&bot_guid)?)) @@ -111,9 +95,7 @@ impl BotOrchestrator { e })? }; - let bucket_name = format!("{}.gbai", bot_name); - { let mounted = mounted_bots.lock().await; if mounted.contains_key(&bot_guid) { @@ -121,26 +103,21 @@ impl BotOrchestrator { return Ok(()); } } - let bot_id = Uuid::parse_str(&bot_guid)?; let drive_monitor = Arc::new(DriveMonitor::new(state.clone(), bucket_name, bot_id)); let _handle = drive_monitor.clone().spawn().await; - { let mut mounted = mounted_bots.lock().await; mounted.insert(bot_guid.clone(), drive_monitor); } - Ok(()) } - pub async fn create_bot( &self, _bot_name: &str, ) -> Result<(), Box> { Ok(()) } - pub async fn mount_bot( &self, bot_guid: &str, @@ -149,10 +126,8 @@ impl BotOrchestrator { .strip_suffix(".gbai") .unwrap_or(bot_guid) .to_string(); - use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let bot_name: String = { let mut db_conn = self.state.conn.get().unwrap(); bots.filter(id.eq(Uuid::parse_str(&bot_guid)?)) @@ -163,9 +138,7 @@ impl BotOrchestrator { e })? }; - let bucket_name = format!("{}.gbai", bot_name); - { let mounted_bots = self.mounted_bots.lock().await; if mounted_bots.contains_key(&bot_guid) { @@ -173,19 +146,15 @@ impl BotOrchestrator { return Ok(()); } } - let bot_id = Uuid::parse_str(&bot_guid)?; let drive_monitor = Arc::new(DriveMonitor::new(self.state.clone(), bucket_name, bot_id)); let _handle = drive_monitor.clone().spawn().await; - { let mut mounted_bots = self.mounted_bots.lock().await; mounted_bots.insert(bot_guid.clone(), drive_monitor); } - Ok(()) } - pub async fn handle_user_input( &self, session_id: Uuid, @@ -196,13 +165,10 @@ impl BotOrchestrator { session_id, user_input ); - let mut session_manager = self.state.session_manager.lock().await; session_manager.provide_input(session_id, user_input.to_string())?; - Ok(None) } - pub async fn register_response_channel( &self, session_id: String, @@ -214,11 +180,9 @@ impl BotOrchestrator { .await .insert(session_id.clone(), sender); } - pub async fn unregister_response_channel(&self, session_id: &str) { self.state.response_channels.lock().await.remove(session_id); } - pub async fn send_event( &self, user_id: &str, @@ -234,7 +198,6 @@ impl BotOrchestrator { session_id, channel ); - let event_response = BotResponse::from_string_ids( bot_id, session_id, @@ -250,16 +213,13 @@ impl BotOrchestrator { is_complete: true, ..event_response }; - if let Some(adapter) = self.state.channels.lock().await.get(channel) { adapter.send_message(event_response).await?; } else { warn!("No channel adapter found for channel: {}", channel); } - Ok(()) } - pub async fn handle_context_change( &self, user_id: &str, @@ -273,17 +233,14 @@ impl BotOrchestrator { session_id, context_name ); - let session_uuid = Uuid::parse_str(session_id).map_err(|e| { error!("Failed to parse session_id: {}", e); e })?; - let user_uuid = Uuid::parse_str(user_id).map_err(|e| { error!("Failed to parse user_id: {}", e); e })?; - if let Err(e) = self .state .session_manager @@ -294,7 +251,6 @@ impl BotOrchestrator { { error!("Failed to update session context: {}", e); } - let confirmation = BotResponse { bot_id: bot_id.to_string(), user_id: user_id.to_string(), @@ -309,14 +265,11 @@ impl BotOrchestrator { context_length: 0, context_max_length: 0, }; - if let Some(adapter) = self.state.channels.lock().await.get(channel) { adapter.send_message(confirmation).await?; } - Ok(()) } - pub async fn stream_response( &self, message: UserMessage, @@ -327,18 +280,15 @@ impl BotOrchestrator { message.user_id, message.session_id ); - let suggestions = if let Some(redis) = &self.state.cache { let mut conn = redis.get_multiplexed_async_connection().await?; let redis_key = format!("suggestions:{}:{}", message.user_id, message.session_id); - let suggestions: Vec = redis::cmd("LRANGE") .arg(&redis_key) .arg(0) .arg(-1) .query_async(&mut conn) .await?; - let mut seen = std::collections::HashSet::new(); suggestions .into_iter() @@ -348,13 +298,10 @@ impl BotOrchestrator { } else { Vec::new() }; - let user_id = Uuid::parse_str(&message.user_id).map_err(|e| { error!("Invalid user ID: {}", e); e })?; - - // Acquire lock briefly for DB access, then release before awaiting let session_id = Uuid::parse_str(&message.session_id).map_err(|e| { error!("Invalid session ID: {}", e); e @@ -364,13 +311,10 @@ impl BotOrchestrator { sm.get_session_by_id(session_id)? } .ok_or_else(|| "Failed to create session")?; - - // Save user message to history { let mut sm = self.state.session_manager.lock().await; sm.save_message(session.id, user_id, 1, &message.content, 1)?; } - if message.message_type == 4 { if let Some(context_name) = &message.context_name { let _ = self @@ -384,17 +328,12 @@ impl BotOrchestrator { .await; } } - let system_prompt = std::env::var("SYSTEM_PROMPT").unwrap_or_default(); - - // Acquire lock briefly for context retrieval let context_data = { let sm = self.state.session_manager.lock().await; sm.get_session_context_data(&session.id, &session.user_id) .await? }; - - // Get history limit from bot config (default -1 for unlimited) let history_limit = { let config_manager = ConfigManager::new(self.state.conn.clone()); config_manager @@ -407,27 +346,21 @@ impl BotOrchestrator { .parse::() .unwrap_or(-1) }; - - // Acquire lock briefly for history retrieval with configurable limit let history = { let mut sm = self.state.session_manager.lock().await; let mut history = sm.get_conversation_history(session.id, user_id)?; - - // Skip all messages before the most recent compacted message (type 9) if let Some(last_compacted_index) = history .iter() .rposition(|(role, _content)| role == "compact") { history = history.split_off(last_compacted_index); } - if history_limit > 0 && history.len() > history_limit as usize { let start = history.len() - history_limit as usize; history.drain(0..start); } history }; - let mut prompt = String::new(); if !system_prompt.is_empty() { prompt.push_str(&format!("SYSTEM: *** {} *** \n", system_prompt)); @@ -439,7 +372,6 @@ impl BotOrchestrator { prompt.push_str(&format!("{}:{}\n", role, content)); } prompt.push_str(&format!("Human: {}\nBot:", message.content)); - trace!( "Stream prompt constructed with {} history entries", history.len() @@ -447,7 +379,6 @@ impl BotOrchestrator { trace!("LLM prompt: [{}]", prompt); let (stream_tx, mut stream_rx) = mpsc::channel::(100); let llm = self.state.llm_provider.clone(); - if message.channel == "web" { self.send_event( &message.user_id, @@ -475,7 +406,6 @@ impl BotOrchestrator { }; response_tx.send(thinking_response).await?; } - let prompt_clone = prompt.clone(); tokio::spawn(async move { if let Err(e) = llm @@ -485,7 +415,6 @@ impl BotOrchestrator { error!("LLM streaming error: {}", e); } }); - let mut full_response = String::new(); let mut analysis_buffer = String::new(); let mut in_analysis = false; @@ -493,8 +422,6 @@ impl BotOrchestrator { let mut first_word_received = false; let mut last_progress_update = Instant::now(); let progress_interval = Duration::from_secs(1); - - // Calculate initial token count let initial_tokens = crate::shared::utils::estimate_token_count(&prompt); let config_manager = ConfigManager::new(self.state.conn.clone()); let max_context_size = config_manager @@ -506,8 +433,6 @@ impl BotOrchestrator { .unwrap_or_default() .parse::() .unwrap_or(0); - - // Show initial progress if let Ok(_metrics) = get_system_metrics(initial_tokens, max_context_size) { } let model = config_manager @@ -518,24 +443,18 @@ impl BotOrchestrator { ) .unwrap_or_default(); let handler = llm_models::get_handler(&model); - while let Some(chunk) = stream_rx.recv().await { chunk_count += 1; - if !first_word_received && !chunk.trim().is_empty() { first_word_received = true; } - analysis_buffer.push_str(&chunk); - if handler.has_analysis_markers(&analysis_buffer) && !in_analysis { in_analysis = true; } - if in_analysis && handler.is_analysis_complete(&analysis_buffer) { in_analysis = false; analysis_buffer.clear(); - if message.channel == "web" { let orchestrator = BotOrchestrator::new(Arc::clone(&self.state)); orchestrator @@ -552,11 +471,8 @@ impl BotOrchestrator { } continue; } - if !in_analysis { full_response.push_str(&chunk); - - // Update progress if interval elapsed if last_progress_update.elapsed() >= progress_interval { let current_tokens = initial_tokens + crate::shared::utils::estimate_token_count(&full_response); @@ -571,7 +487,6 @@ impl BotOrchestrator { } last_progress_update = Instant::now(); } - let partial = BotResponse { bot_id: message.bot_id.clone(), user_id: message.user_id.clone(), @@ -586,19 +501,15 @@ impl BotOrchestrator { context_length: 0, context_max_length: 0, }; - if response_tx.send(partial).await.is_err() { break; } } } - info!( "Stream processing completed, {} chunks processed", chunk_count ); - - // Sum tokens from all p.push context builds before submission let total_tokens = crate::shared::utils::estimate_token_count(&prompt) + crate::shared::utils::estimate_token_count(&context_data) + crate::shared::utils::estimate_token_count(&full_response); @@ -606,8 +517,6 @@ impl BotOrchestrator { "Total tokens (context + prompt + response): {}", total_tokens ); - - // Trigger compact prompt if enabled let config_manager = ConfigManager::new( self.state.conn.clone()); let compact_enabled = config_manager .get_config( @@ -629,13 +538,10 @@ impl BotOrchestrator { std::thread::sleep(Duration::from_secs(60)); }); } - - // Save final message with short lock scope { let mut sm = self.state.session_manager.lock().await; sm.save_message(session.id, user_id, 2, &full_response, 1)?; } - let config_manager = ConfigManager::new(self.state.conn.clone()); let max_context_size = config_manager .get_config( @@ -646,9 +552,7 @@ impl BotOrchestrator { .unwrap_or_default() .parse::() .unwrap_or(0); - let current_context_length = crate::shared::utils::estimate_token_count(&context_data); - let final_msg = BotResponse { bot_id: message.bot_id, user_id: message.user_id, @@ -663,11 +567,9 @@ impl BotOrchestrator { context_length: current_context_length, context_max_length: max_context_size, }; - response_tx.send(final_msg).await?; Ok(()) } - pub async fn get_user_sessions( &self, user_id: Uuid, @@ -676,7 +578,6 @@ impl BotOrchestrator { let sessions = session_manager.get_user_sessions(user_id)?; Ok(sessions) } - pub async fn get_conversation_history( &self, session_id: Uuid, @@ -687,12 +588,10 @@ impl BotOrchestrator { session_id, user_id ); - let mut session_manager = self.state.session_manager.lock().await; let history = session_manager.get_conversation_history(session_id, user_id)?; Ok(history) } - pub async fn run_start_script( session: &UserSession, state: Arc, @@ -703,12 +602,9 @@ impl BotOrchestrator { session.id, token ); - use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let bot_id = session.bot_id; - let bot_name: String = { let mut db_conn = state.conn.get().unwrap(); bots.filter(id.eq(Uuid::parse_str(&bot_id.to_string())?)) @@ -719,9 +615,7 @@ impl BotOrchestrator { e })? }; - let start_script_path = format!("./work/{}.gbai/{}.gbdialog/start.ast", bot_name, bot_name); - let start_script = match std::fs::read_to_string(&start_script_path) { Ok(content) => content, Err(_) => { @@ -729,17 +623,14 @@ impl BotOrchestrator { return Ok(true); } }; - trace!( "Start script content for session {}: {}", session.id, start_script ); - let session_clone = session.clone(); let state_clone = state.clone(); let script_service = crate::basic::ScriptService::new(state_clone, session_clone.clone()); - match tokio::time::timeout(std::time::Duration::from_secs(10), async { script_service .compile(&start_script) @@ -767,7 +658,6 @@ impl BotOrchestrator { } } } - pub async fn send_warning( &self, session_id: &str, @@ -778,12 +668,10 @@ impl BotOrchestrator { "Sending warning to session {} on channel {}: {}", session_id, channel, message ); - let mut ui = BotUI::new().unwrap(); ui.render_warning(message).unwrap(); Ok(()) } - pub async fn trigger_auto_welcome( &self, session_id: &str, @@ -797,12 +685,10 @@ impl BotOrchestrator { session_id, token ); - let session_uuid = Uuid::parse_str(session_id).map_err(|e| { error!("Invalid session ID: {}", e); e })?; - let session = { let mut session_manager = self.state.session_manager.lock().await; match session_manager.get_session_by_id(session_uuid)? { @@ -813,7 +699,6 @@ impl BotOrchestrator { } } }; - let result = match tokio::time::timeout( std::time::Duration::from_secs(5), Self::run_start_script(&session, Arc::clone(&self.state), token), @@ -830,7 +715,6 @@ impl BotOrchestrator { false } }; - info!( "Auto welcome completed for session: {} with result: {}", session_id, result @@ -838,13 +722,11 @@ impl BotOrchestrator { Ok(result) } } - impl Default for BotOrchestrator { fn default() -> Self { panic!("BotOrchestrator::default is not supported; instantiate with BotOrchestrator::new(state)"); } } - #[actix_web::get("/ws")] async fn websocket_handler( req: HttpRequest, @@ -852,15 +734,12 @@ async fn websocket_handler( data: web::Data, ) -> Result { let query = web::Query::>::from_query(req.query_string()).unwrap(); - let session_id = query.get("session_id").cloned().unwrap(); let user_id_string = query .get("user_id") .cloned() .unwrap_or_else(|| Uuid::new_v4().to_string()) .replace("undefined", &Uuid::new_v4().to_string()); - - // Acquire lock briefly, then release before performing blocking DB operations let user_id = { let user_uuid = Uuid::parse_str(&user_id_string).unwrap_or_else(|_| Uuid::new_v4()); let result = { @@ -875,27 +754,21 @@ async fn websocket_handler( } } }; - let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?; let (tx, mut rx) = mpsc::channel::(100); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); orchestrator .register_response_channel(session_id.clone(), tx.clone()) .await; - data.web_adapter .add_connection(session_id.clone(), tx.clone()) .await; - data.voice_adapter .add_connection(session_id.clone(), tx.clone()) .await; - let bot_id: String = { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = data.conn.get().unwrap(); match bots .filter(is_active.eq(true)) @@ -914,7 +787,6 @@ async fn websocket_handler( } } }; - orchestrator .send_event( &user_id, @@ -930,17 +802,14 @@ async fn websocket_handler( ) .await .ok(); - info!( "WebSocket connection established for session: {}, user: {}", session_id, user_id ); - 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 { match tokio::time::timeout( std::time::Duration::from_secs(3), @@ -964,19 +833,16 @@ async fn websocket_handler( } } }); - let web_adapter = data.web_adapter.clone(); let session_id_clone1 = session_id.clone(); let session_id_clone2 = session_id.clone(); let user_id_clone = user_id.clone(); - actix_web::rt::spawn(async move { trace!( "Starting WebSocket sender for session {}", session_id_clone1 ); let mut message_count = 0; - while let Some(msg) = rx.recv().await { message_count += 1; if let Ok(json) = serde_json::to_string(&msg) { @@ -986,30 +852,25 @@ async fn websocket_handler( } } } - trace!( "WebSocket sender terminated for session {}, sent {} messages", session_id_clone1, message_count ); }); - actix_web::rt::spawn(async move { trace!( "Starting WebSocket receiver for session {}", session_id_clone2 ); let mut message_count = 0; - while let Some(Ok(msg)) = msg_stream.recv().await { match msg { WsMessage::Text(text) => { message_count += 1; - let bot_id = { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = data.conn.get().unwrap(); match bots .filter(is_active.eq(true)) @@ -1028,7 +889,6 @@ async fn websocket_handler( } } }; - let json_value: serde_json::Value = match serde_json::from_str(&text) { Ok(value) => value, Err(e) => { @@ -1036,12 +896,10 @@ async fn websocket_handler( continue; } }; - let content = json_value["content"] .as_str() .map(|s| s.to_string()) .unwrap(); - let user_message = UserMessage { bot_id, user_id: user_id_clone.clone(), @@ -1053,7 +911,6 @@ async fn websocket_handler( timestamp: Utc::now(), context_name: json_value["context_name"].as_str().map(|s| s.to_string()), }; - if let Err(e) = orchestrator.stream_response(user_message, tx.clone()).await { error!("Failed to stream response: {}", e); } @@ -1064,11 +921,9 @@ async fn websocket_handler( session_id_clone2, reason ); - let bot_id = { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut db_conn = data.conn.get().unwrap(); match bots .filter(is_active.eq(true)) @@ -1087,7 +942,6 @@ async fn websocket_handler( } } }; - if let Err(e) = orchestrator .send_event( &user_id_clone, @@ -1101,39 +955,33 @@ async fn websocket_handler( { error!("Failed to send session_end event: {}", e); } - web_adapter.remove_connection(&session_id_clone2).await; orchestrator .unregister_response_channel(&session_id_clone2) .await; - if let Err(e) = data.llm_provider.cancel_job(&session_id_clone2).await { warn!( "Failed to cancel LLM job for session {}: {}", session_id_clone2, e ); } - break; } _ => {} } } - trace!( "WebSocket receiver terminated for session {}, processed {} messages", session_id_clone2, message_count ); }); - info!( "WebSocket handler setup completed for session {}", session_id ); Ok(res) } - #[actix_web::post("/api/bot/create")] async fn create_bot_handler( data: web::Data, @@ -1143,38 +991,30 @@ async fn create_bot_handler( .get("bot_name") .cloned() .unwrap_or("default".to_string()); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); - if let Err(e) = orchestrator.create_bot(&bot_name).await { error!("Failed to create bot: {}", e); return Ok( HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) ); } - Ok(HttpResponse::Ok().json(serde_json::json!({"status": "bot_created"}))) } - #[actix_web::post("/api/bot/mount")] async fn mount_bot_handler( data: web::Data, info: web::Json>, ) -> Result { let bot_guid = info.get("bot_guid").cloned().unwrap_or_default(); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); - if let Err(e) = orchestrator.mount_bot(&bot_guid).await { error!("Failed to mount bot: {}", e); return Ok( HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) ); } - Ok(HttpResponse::Ok().json(serde_json::json!({"status": "bot_mounted"}))) } - #[actix_web::post("/api/bot/input")] async fn handle_user_input_handler( data: web::Data, @@ -1182,10 +1022,8 @@ async fn handle_user_input_handler( ) -> Result { let session_id = info.get("session_id").cloned().unwrap_or_default(); let user_input = info.get("input").cloned().unwrap_or_default(); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); let session_uuid = Uuid::parse_str(&session_id).unwrap_or(Uuid::nil()); - if let Err(e) = orchestrator .handle_user_input(session_uuid, &user_input) .await @@ -1195,19 +1033,15 @@ async fn handle_user_input_handler( HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) ); } - Ok(HttpResponse::Ok().json(serde_json::json!({"status": "input_processed"}))) } - #[actix_web::get("/api/bot/sessions/{user_id}")] async fn get_user_sessions_handler( data: web::Data, path: web::Path, ) -> Result { let user_id = path.into_inner(); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); - match orchestrator.get_user_sessions(user_id).await { Ok(sessions) => Ok(HttpResponse::Ok().json(sessions)), Err(e) => { @@ -1217,16 +1051,13 @@ async fn get_user_sessions_handler( } } } - #[actix_web::get("/api/bot/history/{session_id}/{user_id}")] async fn get_conversation_history_handler( data: web::Data, path: web::Path<(Uuid, Uuid)>, ) -> Result { let (session_id, user_id) = path.into_inner(); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); - match orchestrator .get_conversation_history(session_id, user_id) .await @@ -1239,7 +1070,6 @@ async fn get_conversation_history_handler( } } } - #[actix_web::post("/api/warn")] async fn send_warning_handler( data: web::Data, @@ -1248,19 +1078,15 @@ async fn send_warning_handler( let default_session = "default".to_string(); let default_channel = "web".to_string(); let default_message = "Warning!".to_string(); - let session_id = info.get("session_id").unwrap_or(&default_session); let channel = info.get("channel").unwrap_or(&default_channel); let message = info.get("message").unwrap_or(&default_message); - trace!( "Sending warning via API - session: {}, channel: {}", session_id, channel ); - let orchestrator = BotOrchestrator::new(Arc::clone(&data)); - if let Err(e) = orchestrator .send_warning(session_id, channel, message) .await @@ -1270,6 +1096,5 @@ async fn send_warning_handler( HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()})) ); } - Ok(HttpResponse::Ok().json(serde_json::json!({"status": "warning_sent"}))) } diff --git a/src/bot/ui.rs b/src/bot/ui.rs index a2cba219a..219eb8c4c 100644 --- a/src/bot/ui.rs +++ b/src/bot/ui.rs @@ -7,11 +7,9 @@ use ratatui::{ }; use std::io::{self, Stdout}; use crate::nvidia::get_system_metrics; - pub struct BotUI { terminal: Terminal>, } - impl BotUI { pub fn new() -> io::Result { let stdout = io::stdout(); @@ -19,13 +17,11 @@ impl BotUI { let terminal = Terminal::new(backend)?; Ok(Self { terminal }) } - pub fn render_progress(&mut self, current_tokens: usize, max_context_size: usize) -> io::Result<()> { let metrics = get_system_metrics(current_tokens, max_context_size).unwrap_or_default(); let gpu_usage = metrics.gpu_usage.unwrap_or(0.0); let cpu_usage = metrics.cpu_usage; let token_ratio = current_tokens as f64 / max_context_size.max(1) as f64; - self.terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) @@ -36,32 +32,27 @@ impl BotUI { Constraint::Min(0), ]) .split(f.area()); - let gpu_gauge = Gauge::default() .block(Block::default().title("GPU Usage").borders(Borders::ALL)) .gauge_style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)) .ratio(gpu_usage as f64 / 100.0) .label(format!("{:.1}%", gpu_usage)); - let cpu_gauge = Gauge::default() .block(Block::default().title("CPU Usage").borders(Borders::ALL)) .gauge_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) .ratio(cpu_usage as f64 / 100.0) .label(format!("{:.1}%", cpu_usage)); - let token_gauge = Gauge::default() .block(Block::default().title("Token Progress").borders(Borders::ALL)) .gauge_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) .ratio(token_ratio) .label(format!("{}/{}", current_tokens, max_context_size)); - f.render_widget(gpu_gauge, chunks[0]); f.render_widget(cpu_gauge, chunks[1]); f.render_widget(token_gauge, chunks[2]); })?; Ok(()) } - pub fn render_warning(&mut self, message: &str) -> io::Result<()> { self.terminal.draw(|f| { let block = Block::default() diff --git a/src/channels/channels.test.rs b/src/channels/channels.test.rs index b6728846d..3c56a2f91 100644 --- a/src/channels/channels.test.rs +++ b/src/channels/channels.test.rs @@ -1,10 +1,7 @@ -//! Tests for channels module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_channels_module() { test_util::setup(); diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 4fa442b85..c6aa2e98b 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -3,9 +3,7 @@ use log::{debug, info}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{mpsc, Mutex}; - use crate::shared::models::BotResponse; - #[async_trait] pub trait ChannelAdapter: Send + Sync { async fn send_message( @@ -13,22 +11,18 @@ pub trait ChannelAdapter: Send + Sync { response: BotResponse, ) -> Result<(), Box>; } - pub struct WebChannelAdapter { connections: Arc>>>, } - impl WebChannelAdapter { pub fn new() -> Self { Self { connections: Arc::new(Mutex::new(HashMap::new())), } } - pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender) { self.connections.lock().await.insert(session_id, tx); } - pub async fn remove_connection(&self, session_id: &str) { self.connections.lock().await.remove(session_id); } @@ -55,7 +49,6 @@ impl WebChannelAdapter { } } } - #[async_trait] impl ChannelAdapter for WebChannelAdapter { async fn send_message( @@ -69,12 +62,10 @@ impl ChannelAdapter for WebChannelAdapter { Ok(()) } } - pub struct VoiceAdapter { rooms: Arc>>, connections: Arc>>>, } - impl VoiceAdapter { pub fn new() -> Self { Self { @@ -82,7 +73,6 @@ impl VoiceAdapter { connections: Arc::new(Mutex::new(HashMap::new())), } } - pub async fn start_voice_session( &self, session_id: &str, @@ -92,16 +82,13 @@ impl VoiceAdapter { "Starting voice session for user: {} with session: {}", user_id, session_id ); - let token = format!("mock_token_{}_{}", session_id, user_id); self.rooms .lock() .await .insert(session_id.to_string(), token.clone()); - Ok(token) } - pub async fn stop_voice_session( &self, session_id: &str, @@ -109,11 +96,9 @@ impl VoiceAdapter { self.rooms.lock().await.remove(session_id); Ok(()) } - pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender) { self.connections.lock().await.insert(session_id, tx); } - pub async fn send_voice_response( &self, session_id: &str, @@ -123,7 +108,6 @@ impl VoiceAdapter { Ok(()) } } - #[async_trait] impl ChannelAdapter for VoiceAdapter { async fn send_message( diff --git a/src/config/config.test.rs b/src/config/config.test.rs index 8a7b102a1..47c57c9c6 100644 --- a/src/config/config.test.rs +++ b/src/config/config.test.rs @@ -1,10 +1,7 @@ -//! Tests for config module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_config_module() { test_util::setup(); diff --git a/src/config/mod.rs b/src/config/mod.rs index c9b7d202f..abd144cc3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -74,7 +74,6 @@ impl AppConfig { .map(|v| v.3.to_lowercase() == "true") .unwrap_or(default) }; - let drive = DriveConfig { server: std::env::var("DRIVE_SERVER").unwrap(), access_key: std::env::var("DRIVE_ACCESSKEY").unwrap(), @@ -119,8 +118,6 @@ impl AppConfig { }) } } - - pub struct ConfigManager { conn: DbPool, } diff --git a/src/context/context.test.rs b/src/context/context.test.rs index da48ecddf..d1faa1b1f 100644 --- a/src/context/context.test.rs +++ b/src/context/context.test.rs @@ -1,16 +1,12 @@ -//! Tests for context module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_context_module() { test_util::setup(); assert!(true, "Basic context module test"); } - #[test] fn test_langcache() { test_util::setup(); diff --git a/src/drive_monitor/drive_monitor.test.rs b/src/drive_monitor/drive_monitor.test.rs index e5198aa1b..5c3dd92fb 100644 --- a/src/drive_monitor/drive_monitor.test.rs +++ b/src/drive_monitor/drive_monitor.test.rs @@ -1,14 +1,10 @@ -//! Tests for drive_monitor module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_drive_monitor_module() { test_util::setup(); assert!(true, "Basic drive_monitor module test"); } } - \ No newline at end of file diff --git a/src/drive_monitor/mod.rs b/src/drive_monitor/mod.rs index 1bee58b9e..b53cd41b4 100644 --- a/src/drive_monitor/mod.rs +++ b/src/drive_monitor/mod.rs @@ -7,19 +7,16 @@ use std::collections::HashMap; use std::error::Error; use std::sync::Arc; use tokio::time::{interval, Duration}; - #[derive(Debug, Clone)] pub struct FileState { pub etag: String, } - pub struct DriveMonitor { state: Arc, bucket_name: String, file_states: Arc>>, bot_id: uuid::Uuid, } - impl DriveMonitor { pub fn new(state: Arc, bucket_name: String, bot_id: uuid::Uuid) -> Self { Self { @@ -29,14 +26,10 @@ impl DriveMonitor { bot_id, } } - pub fn spawn(self: Arc) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { info!("Drive Monitor service started for bucket: {}", self.bucket_name); - - let mut tick = interval(Duration::from_secs(30)); - loop { tick.tick().await; if let Err(e) = self.check_for_changes().await { @@ -45,24 +38,19 @@ impl DriveMonitor { } }) } - async fn check_for_changes(&self) -> Result<(), Box> { let client = match &self.state.drive { Some(client) => client, None => return Ok(()), }; - self.check_gbdialog_changes(client).await?; self.check_gbot(client).await?; - Ok(()) } - async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box> { let prefix = ".gbdialog/"; let mut current_files = HashMap::new(); let mut continuation_token = None; - loop { let list_objects = match tokio::time::timeout( Duration::from_secs(30), @@ -79,34 +67,26 @@ impl DriveMonitor { return Ok(()); } }; - for obj in list_objects.contents.unwrap_or_default() { let path = obj.key().unwrap_or_default().to_string(); let path_parts: Vec<&str> = path.split('/').collect(); - if path_parts.len() < 2 || !path_parts[0].ends_with(".gbdialog") { continue; } - if path.ends_with('/') || !path.ends_with(".bas") { continue; } - let file_state = FileState { etag: obj.e_tag().unwrap_or_default().to_string(), }; - current_files.insert(path, file_state); } - if !list_objects.is_truncated.unwrap_or(false) { break; } continuation_token = list_objects.next_continuation_token; } - let mut file_states = self.file_states.write().await; - for (path, current_state) in current_files.iter() { if let Some(previous_state) = file_states.get(path) { if current_state.etag != previous_state.etag { @@ -120,30 +100,24 @@ impl DriveMonitor { } } } - let previous_paths: Vec = file_states .keys() .filter(|k| k.starts_with(prefix)) .cloned() .collect(); - for path in previous_paths { if !current_files.contains_key(&path) { file_states.remove(&path); } } - for (path, state) in current_files { file_states.insert(path, state); } - Ok(()) } - async fn check_gbot(&self, client: &Client) -> Result<(), Box> { let config_manager = ConfigManager::new(self.state.conn.clone()); let mut continuation_token = None; - loop { let list_objects = match tokio::time::timeout( Duration::from_secs(30), @@ -160,41 +134,33 @@ impl DriveMonitor { return Ok(()); } }; - for obj in list_objects.contents.unwrap_or_default() { let path = obj.key().unwrap_or_default().to_string(); let path_parts: Vec<&str> = path.split('/').collect(); - if path_parts.len() < 2 || !path_parts[0].ends_with(".gbot") { continue; } - if !path.ends_with("config.csv") { continue; } - match client.head_object().bucket(&self.bucket_name).key(&path).send().await { Ok(_head_res) => { let response = client.get_object().bucket(&self.bucket_name).key(&path).send().await?; let bytes = response.body.collect().await?.into_bytes(); let csv_content = String::from_utf8(bytes.to_vec()) .map_err(|e| format!("UTF-8 error in {}: {}", path, e))?; - let llm_lines: Vec<_> = csv_content .lines() .filter(|line| line.trim_start().starts_with("llm-")) .collect(); - if !llm_lines.is_empty() { use crate::llm::local::ensure_llama_servers_running; let mut restart_needed = false; - for line in llm_lines { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { let key = parts[0].trim(); let new_value = parts[1].trim(); - match config_manager.get_config(&self.bot_id, key, None) { Ok(old_value) => { if old_value != new_value { @@ -208,9 +174,7 @@ impl DriveMonitor { } } } - let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); - if restart_needed { if let Err(e) = ensure_llama_servers_running(Arc::clone(&self.state)).await { log::error!("Failed to restart LLaMA servers after llm- config change: {}", e); @@ -219,7 +183,6 @@ impl DriveMonitor { } else { let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content); } - if csv_content.lines().any(|line| line.starts_with("theme-")) { self.broadcast_theme_change(&csv_content).await?; } @@ -229,28 +192,23 @@ impl DriveMonitor { } } } - if !list_objects.is_truncated.unwrap_or(false) { break; } continuation_token = list_objects.next_continuation_token; } - Ok(()) } - async fn broadcast_theme_change(&self, csv_content: &str) -> Result<(), Box> { let mut theme_data = serde_json::json!({ "event": "change_theme", "data": {} }); - for line in csv_content.lines() { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { let key = parts[0].trim(); let value = parts[1].trim(); - match key { "theme-color1" => theme_data["data"]["color1"] = serde_json::Value::String(value.to_string()), "theme-color2" => theme_data["data"]["color2"] = serde_json::Value::String(value.to_string()), @@ -261,7 +219,6 @@ impl DriveMonitor { } } } - let response_channels = self.state.response_channels.lock().await; for (session_id, tx) in response_channels.iter() { let theme_response = crate::shared::models::BotResponse { @@ -278,16 +235,12 @@ impl DriveMonitor { context_length: 0, context_max_length: 0, }; - let _ = tx.try_send(theme_response); } - Ok(()) } - async fn compile_tool(&self, client: &Client, file_path: &str) -> Result<(), Box> { info!("Fetching object from S3: bucket={}, key={}", &self.bucket_name, file_path); - let response = match client.get_object().bucket(&self.bucket_name).key(file_path).send().await { Ok(res) => { info!("Successfully fetched object from S3: bucket={}, key={}, size={}", @@ -300,10 +253,8 @@ impl DriveMonitor { return Err(e.into()); } }; - let bytes = response.body.collect().await?.into_bytes(); let source_content = String::from_utf8(bytes.to_vec())?; - let tool_name = file_path .split('/') .last() @@ -311,34 +262,25 @@ impl DriveMonitor { .strip_suffix(".bas") .unwrap_or(file_path) .to_string(); - let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name); let work_dir = format!("./work/{}.gbai/{}.gbdialog", bot_name, bot_name); - - // Offload the blocking compilation work to a blocking thread pool let state_clone = Arc::clone(&self.state); let work_dir_clone = work_dir.clone(); let tool_name_clone = tool_name.clone(); let source_content_clone = source_content.clone(); let bot_id = self.bot_id; - tokio::task::spawn_blocking(move || { std::fs::create_dir_all(&work_dir_clone)?; - let local_source_path = format!("{}/{}.bas", work_dir_clone, tool_name_clone); std::fs::write(&local_source_path, &source_content_clone)?; - let mut compiler = BasicCompiler::new(state_clone, bot_id); let result = compiler.compile_file(&local_source_path, &work_dir_clone)?; - if let Some(mcp_tool) = result.mcp_tool { info!("MCP tool definition generated with {} parameters", mcp_tool.input_schema.properties.len()); } - Ok::<(), Box>(()) }).await??; - Ok(()) } } diff --git a/src/email/email.test.rs b/src/email/email.test.rs index 2f107d8c8..2014f9faf 100644 --- a/src/email/email.test.rs +++ b/src/email/email.test.rs @@ -1,16 +1,12 @@ -//! Tests for email module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_email_module() { test_util::setup(); assert!(true, "Basic email module test"); } - #[test] fn test_email_send() { test_util::setup(); diff --git a/src/email/mod.rs b/src/email/mod.rs index aeae2bd0d..cba944b0a 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -1,16 +1,13 @@ use crate::{config::EmailConfig, shared::state::AppState}; use log::info; - use actix_web::error::ErrorInternalServerError; use actix_web::http::header::ContentType; use actix_web::{web, HttpResponse, Result}; use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; use serde::Serialize; - use imap::types::Seq; use mailparse::{parse_mail, MailHeaderMap}; use diesel::prelude::*; - #[derive(Debug, Serialize)] pub struct EmailResponse { pub id: String, @@ -22,7 +19,6 @@ pub struct EmailResponse { read: bool, labels: Vec, } - async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body: &str) { let email = Message::builder() .from(config.from.parse().unwrap()) @@ -30,9 +26,7 @@ async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body .subject(subject) .body(body.to_string()) .unwrap(); - let creds = Credentials::new(config.username.clone(), config.password.clone()); - SmtpTransport::relay(&config.server) .unwrap() .port(config.port) @@ -41,7 +35,6 @@ async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body .send(&email) .unwrap(); } - #[actix_web::get("/emails/list")] pub async fn list_emails( state: web::Data, @@ -50,61 +43,41 @@ pub async fn list_emails( .config .as_ref() .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; - - // Establish connection let tls = native_tls::TlsConnector::builder().build().map_err(|e| { ErrorInternalServerError(format!("Failed to create TLS connector: {:?}", e)) })?; - let client = imap::connect( (_config.email.server.as_str(), 993), _config.email.server.as_str(), &tls, ) .map_err(|e| ErrorInternalServerError(format!("Failed to connect to IMAP: {:?}", e)))?; - - // Login let mut session = client .login(&_config.email.username, &_config.email.password) .map_err(|e| ErrorInternalServerError(format!("Login failed: {:?}", e)))?; - - // Select INBOX session .select("INBOX") .map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?; - - // Search for all messages let messages = session .search("ALL") .map_err(|e| ErrorInternalServerError(format!("Failed to search emails: {:?}", e)))?; - let mut email_list = Vec::new(); - - // Get last 20 messages let recent_messages: Vec<_> = messages.iter().cloned().collect(); let recent_messages: Vec = recent_messages.into_iter().rev().take(20).collect(); for seq in recent_messages { - // Fetch the entire message (headers + body) let fetch_result = session.fetch(seq.to_string(), "RFC822"); let messages = fetch_result .map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?; - for msg in messages.iter() { let body = msg .body() .ok_or_else(|| ErrorInternalServerError("No body found"))?; - - // Parse the complete email message let parsed = parse_mail(body) .map_err(|e| ErrorInternalServerError(format!("Failed to parse email: {:?}", e)))?; - - // Extract headers let headers = parsed.get_headers(); let subject = headers.get_first_value("Subject").unwrap_or_default(); let from = headers.get_first_value("From").unwrap_or_default(); let date = headers.get_first_value("Date").unwrap_or_default(); - - // Extract body text (handles both simple and multipart emails) let body_text = if let Some(body_part) = parsed .subparts .iter() @@ -114,18 +87,13 @@ pub async fn list_emails( } else { parsed.get_body().unwrap_or_default() }; - - // Create preview let preview = body_text.lines().take(3).collect::>().join(" "); let preview_truncated = if preview.len() > 150 { format!("{}...", &preview[..150]) } else { preview }; - - // Parse From field let (from_name, from_email) = parse_from_field(&from); - email_list.push(EmailResponse { id: seq.to_string(), name: from_name, @@ -146,15 +114,11 @@ pub async fn list_emails( }); } } - session .logout() .map_err(|e| ErrorInternalServerError(format!("Failed to logout: {:?}", e)))?; - Ok(web::Json(email_list)) } - -// Helper function to parse From field fn parse_from_field(from: &str) -> (String, String) { if let Some(start) = from.find('<') { if let Some(end) = from.find('>') { @@ -165,7 +129,6 @@ fn parse_from_field(from: &str) -> (String, String) { } ("Unknown".to_string(), from.to_string()) } - #[derive(serde::Deserialize)] pub struct SaveDraftRequest { pub to: String, @@ -173,26 +136,22 @@ pub struct SaveDraftRequest { pub cc: Option, pub text: String, } - #[derive(serde::Serialize)] pub struct SaveDraftResponse { pub success: bool, pub message: String, pub draft_id: Option, } - #[derive(serde::Deserialize)] pub struct GetLatestEmailRequest { pub from_email: String, } - #[derive(serde::Serialize)] pub struct LatestEmailResponse { pub success: bool, pub email_text: Option, pub message: String, } - #[actix_web::post("/emails/save_draft")] pub async fn save_draft( state: web::Data, @@ -202,7 +161,6 @@ pub async fn save_draft( .config .as_ref() .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; - match save_email_draft(&config.email, &draft_data).await { Ok(draft_id) => Ok(web::Json(SaveDraftResponse { success: true, @@ -216,32 +174,23 @@ pub async fn save_draft( })), } } - pub async fn save_email_draft( email_config: &EmailConfig, draft_data: &SaveDraftRequest, ) -> Result> { - // Establish connection let tls = native_tls::TlsConnector::builder().build()?; let client = imap::connect( (email_config.server.as_str(), 993), email_config.server.as_str(), &tls, )?; - - // Login let mut session = client .login(&email_config.username, &email_config.password) .map_err(|e| format!("Login failed: {:?}", e))?; - - // Select or create Drafts folder if session.select("Drafts").is_err() { - // Try to create Drafts folder if it doesn't exist session.create("Drafts")?; session.select("Drafts")?; } - - // Create email message let cc_header = draft_data .cc .as_deref() @@ -257,68 +206,43 @@ pub async fn save_email_draft( chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S +0000"), draft_data.text ); - - // Append to Drafts folder session.append("Drafts", &email_message)?; - session.logout()?; - Ok(chrono::Utc::now().timestamp().to_string()) } - async fn fetch_latest_email_from_sender( email_config: &EmailConfig, from_email: &str, ) -> Result> { - // Establish connection let tls = native_tls::TlsConnector::builder().build()?; let client = imap::connect( (email_config.server.as_str(), 993), email_config.server.as_str(), &tls, )?; - - // Login let mut session = client .login(&email_config.username, &email_config.password) .map_err(|e| format!("Login failed: {:?}", e))?; - - // Try to select Archive folder first, then fall back to INBOX if session.select("Archive").is_err() { session.select("INBOX")?; } - - // Search for emails from the specified sender let search_query = format!("FROM \"{}\"", from_email); let messages = session.search(&search_query)?; - if messages.is_empty() { session.logout()?; return Err(format!("No emails found from {}", from_email).into()); } - - // Get the latest message (highest sequence number) let latest_seq = messages.iter().max().unwrap(); - - // Fetch the entire message let messages = session.fetch(latest_seq.to_string(), "RFC822")?; - let mut email_text = String::new(); - for msg in messages.iter() { let body = msg.body().ok_or("No body found in email")?; - - // Parse the complete email message let parsed = parse_mail(body)?; - - // Extract headers let headers = parsed.get_headers(); let subject = headers.get_first_value("Subject").unwrap_or_default(); let from = headers.get_first_value("From").unwrap_or_default(); let date = headers.get_first_value("Date").unwrap_or_default(); let to = headers.get_first_value("To").unwrap_or_default(); - - // Extract body text let body_text = if let Some(body_part) = parsed .subparts .iter() @@ -328,25 +252,19 @@ async fn fetch_latest_email_from_sender( } else { parsed.get_body().unwrap_or_default() }; - - // Format the email text ready for reply with headers email_text = format!( "--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n", from, to, date, subject, body_text ); - break; } - session.logout()?; - if email_text.is_empty() { Err("Failed to extract email content".into()) } else { Ok(email_text) } } - #[actix_web::post("/emails/get_latest_from")] pub async fn get_latest_email_from( state: web::Data, @@ -356,7 +274,6 @@ pub async fn get_latest_email_from( .config .as_ref() .ok_or_else(|| ErrorInternalServerError("Configuration not available"))?; - match fetch_latest_email_from_sender(&config.email, &request.from_email).await { Ok(email_text) => Ok(web::Json(LatestEmailResponse { success: true, @@ -376,59 +293,39 @@ pub async fn get_latest_email_from( } } } - pub async fn fetch_latest_sent_to( email_config: &EmailConfig, to_email: &str, ) -> Result> { - // Establish connection let tls = native_tls::TlsConnector::builder().build()?; let client = imap::connect( (email_config.server.as_str(), 993), email_config.server.as_str(), &tls, )?; - - // Login let mut session = client .login(&email_config.username, &email_config.password) .map_err(|e| format!("Login failed: {:?}", e))?; - - // Try to select Archive folder first, then fall back to INBOX if session.select("Sent").is_err() { session.select("Sent Items")?; } - - // Search for emails from the specified sender let search_query = format!("TO \"{}\"", to_email); let messages = session.search(&search_query)?; - if messages.is_empty() { session.logout()?; return Err(format!("No emails found to {}", to_email).into()); } - - // Get the latest message (highest sequence number) let latest_seq = messages.iter().max().unwrap(); - - // Fetch the entire message let messages = session.fetch(latest_seq.to_string(), "RFC822")?; - let mut email_text = String::new(); - for msg in messages.iter() { let body = msg.body().ok_or("No body found in email")?; - - // Parse the complete email message let parsed = parse_mail(body)?; - - // Extract headers let headers = parsed.get_headers(); let subject = headers.get_first_value("Subject").unwrap_or_default(); let from = headers.get_first_value("From").unwrap_or_default(); let date = headers.get_first_value("Date").unwrap_or_default(); let to = headers.get_first_value("To").unwrap_or_default(); - if !to .trim() .to_lowercase() @@ -436,7 +333,6 @@ pub async fn fetch_latest_sent_to( { continue; } - // Extract body text (handles both simple and multipart emails) let body_text = if let Some(body_part) = parsed .subparts .iter() @@ -446,52 +342,38 @@ pub async fn fetch_latest_sent_to( } else { parsed.get_body().unwrap_or_default() }; - - // Only format if we have actual content if !body_text.trim().is_empty() && body_text != "No readable content found" { - // Format the email text ready for reply with headers email_text = format!( "--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n", from, to, date, subject, body_text.trim() ); } else { - // Still provide headers even if body is empty email_text = format!( "--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n[No readable content]\n\n--- Reply Above This Line ---\n\n", from, to, date, subject ); } - break; } - session.logout()?; - - // Always return something, even if it's just headers if email_text.is_empty() { Err("Failed to extract email content".into()) } else { Ok(email_text) } } - #[actix_web::post("/emails/send")] pub async fn send_email( payload: web::Json<(String, String, String)>, state: web::Data, ) -> Result { let (to, subject, body) = payload.into_inner(); - info!("To: {}", to); info!("Subject: {}", subject); info!("Body: {}", body); - - // Send via SMTP internal_send_email(&state.config.clone().unwrap().email, &to, &subject, &body).await; - Ok(HttpResponse::Ok().finish()) } - #[actix_web::get("/campaigns/{campaign_id}/click/{email}")] pub async fn save_click( path: web::Path<(String, String)>, @@ -499,7 +381,6 @@ pub async fn save_click( ) -> HttpResponse { let (campaign_id, email) = path.into_inner(); use crate::shared::models::clicks; - let _ = diesel::insert_into(clicks::table) .values(( clicks::campaign_id.eq(campaign_id), @@ -510,7 +391,6 @@ pub async fn save_click( .do_update() .set(clicks::updated_at.eq(diesel::dsl::now)) .execute(&state.conn); - let pixel = [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, @@ -522,17 +402,14 @@ pub async fn save_click( 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, ]; - HttpResponse::Ok() .content_type(ContentType::png()) .body(pixel.to_vec()) } - #[actix_web::get("/campaigns/{campaign_id}/emails")] pub async fn get_emails(path: web::Path, state: web::Data) -> String { let campaign_id = path.into_inner(); use crate::shared::models::clicks::dsl::*; - let rows = clicks .filter(campaign_id.eq(campaign_id)) .select(email) diff --git a/src/file/file.test.rs b/src/file/file.test.rs index ca1f07683..75338b7e7 100644 --- a/src/file/file.test.rs +++ b/src/file/file.test.rs @@ -1,16 +1,12 @@ -//! Tests for file module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_file_module() { test_util::setup(); assert!(true, "Basic file module test"); } - #[test] fn test_file_operations() { test_util::setup(); diff --git a/src/file/mod.rs b/src/file/mod.rs index 52249d07d..dd1e56643 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -6,7 +6,6 @@ use aws_sdk_s3::Client; use std::io::Write; use tempfile::NamedTempFile; use tokio_stream::StreamExt as TokioStreamExt; - #[post("/files/upload/{folder_path}")] pub async fn upload_file( folder_path: web::Path, @@ -17,7 +16,6 @@ pub async fn upload_file( let mut temp_file = NamedTempFile::new().map_err(|e| { actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e)) })?; - let mut file_name: Option = None; while let Some(mut field) = payload.try_next().await? { if let Some(disposition) = field.content_disposition() { @@ -25,7 +23,6 @@ pub async fn upload_file( file_name = Some(name.to_string()); } } - while let Some(chunk) = field.try_next().await? { temp_file.write_all(&chunk).map_err(|e| { actix_web::error::ErrorInternalServerError(format!( @@ -35,16 +32,12 @@ pub async fn upload_file( })?; } } - let file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string()); let temp_file_path = temp_file.into_temp_path(); - let client = state.get_ref().drive.as_ref().ok_or_else(|| { actix_web::error::ErrorInternalServerError("S3 client is not initialized") })?; - let s3_key = format!("{}/{}", folder_path, file_name); - match upload_to_s3(client, &state.get_ref().bucket_name, &s3_key, &temp_file_path).await { Ok(_) => { let _ = std::fs::remove_file(&temp_file_path); @@ -62,7 +55,6 @@ pub async fn upload_file( } } } - async fn upload_to_s3( client: &Client, bucket: &str, diff --git a/src/llm/local.rs b/src/llm/local.rs index de1b269b1..797783016 100644 --- a/src/llm/local.rs +++ b/src/llm/local.rs @@ -6,31 +6,24 @@ use std::sync::Arc; use log::{info, error}; use tokio; use reqwest; - use actix_web::{post, web, HttpResponse, Result}; - #[post("/api/chat/completions")] pub async fn chat_completions_local( _data: web::Data, _payload: web::Json, ) -> Result { - // Placeholder implementation Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "chat_completions_local not implemented" }))) } - #[post("/api/embeddings")] pub async fn embeddings_local( _data: web::Data, _payload: web::Json, ) -> Result { - // Placeholder implementation Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "embeddings_local not implemented" }))) } - pub async fn ensure_llama_servers_running( app_state: Arc ) -> Result<(), Box> { - // Get all config values before starting async operations let config_values = { let conn_arc = app_state.conn.clone(); let default_bot_id = tokio::task::spawn_blocking(move || { @@ -51,7 +44,6 @@ let config_values = { ) }; let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_server_path) = config_values; - info!("Starting LLM servers..."); info!("Configuration:"); info!(" LLM URL: {}", llm_url); @@ -59,8 +51,6 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se info!(" LLM Model: {}", llm_model); info!(" Embedding Model: {}", embedding_model); info!(" LLM Server Path: {}", llm_server_path); - - // Restart any existing llama-server processes info!("Restarting any existing llama-server processes..."); if let Err(e) = tokio::process::Command::new("sh") .arg("-c") @@ -72,19 +62,13 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; info!("Existing llama-server processes terminated (if any)"); } - - // Check if servers are already running let llm_running = is_server_running(&llm_url).await; let embedding_running = is_server_running(&embedding_url).await; - if llm_running && embedding_running { info!("Both LLM and Embedding servers are already running"); return Ok(()); } - - // Start servers that aren't running let mut tasks = vec![]; - if !llm_running && !llm_model.is_empty() { info!("Starting LLM server..."); tasks.push(tokio::spawn(start_llm_server( @@ -96,7 +80,6 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se } else if llm_model.is_empty() { info!("LLM_MODEL not set, skipping LLM server"); } - if !embedding_running && !embedding_model.is_empty() { info!("Starting Embedding server..."); tasks.push(tokio::spawn(start_embedding_server( @@ -107,26 +90,17 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se } else if embedding_model.is_empty() { info!("EMBEDDING_MODEL not set, skipping Embedding server"); } - - // Wait for all server startup tasks for task in tasks { task.await??; } - - // Wait for servers to be ready with verbose logging info!("Waiting for servers to become ready..."); - let mut llm_ready = llm_running || llm_model.is_empty(); let mut embedding_ready = embedding_running || embedding_model.is_empty(); - let mut attempts = 0; - let max_attempts = 60; // 2 minutes total - + let max_attempts = 60; while attempts < max_attempts && (!llm_ready || !embedding_ready) { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - info!("Checking server health (attempt {}/{})...", attempts + 1, max_attempts); - if !llm_ready && !llm_model.is_empty() { if is_server_running(&llm_url).await { info!("LLM server ready at {}", llm_url); @@ -135,7 +109,6 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se info!("LLM server not ready yet"); } } - if !embedding_ready && !embedding_model.is_empty() { if is_server_running(&embedding_url).await { info!("Embedding server ready at {}", embedding_url); @@ -144,14 +117,11 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se info!("Embedding server not ready yet"); } } - attempts += 1; - if attempts % 10 == 0 { info!("Still waiting for servers... (attempt {}/{})", attempts, max_attempts); } } - if llm_ready && embedding_ready { info!("All llama.cpp servers are ready and responding!"); Ok(()) @@ -166,7 +136,6 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se Err(error_msg.into()) } } - pub async fn is_server_running(url: &str) -> bool { let client = reqwest::Client::new(); match client.get(&format!("{}/health", url)).send().await { @@ -174,7 +143,6 @@ pub async fn is_server_running(url: &str) -> bool { Err(_) => false, } } - pub async fn start_llm_server( app_state: Arc, llama_cpp_path: String, @@ -182,20 +150,16 @@ pub async fn start_llm_server( url: String, ) -> Result<(), Box> { let port = url.split(':').last().unwrap_or("8081"); - std::env::set_var("OMP_NUM_THREADS", "20"); std::env::set_var("OMP_PLACES", "cores"); std::env::set_var("OMP_PROC_BIND", "close"); - let conn = app_state.conn.clone(); let config_manager = ConfigManager::new(conn.clone()); - let mut conn = conn.get().unwrap(); let default_bot_id = bots.filter(name.eq("default")) .select(id) .first::(&mut *conn) .unwrap_or_else(|_| uuid::Uuid::nil()); - let n_moe = config_manager.get_config(&default_bot_id, "llm-server-n-moe", None).unwrap_or("4".to_string()); let parallel = config_manager.get_config(&default_bot_id, "llm-server-parallel", None).unwrap_or("1".to_string()); let cont_batching = config_manager.get_config(&default_bot_id, "llm-server-cont-batching", None).unwrap_or("true".to_string()); @@ -204,8 +168,6 @@ pub async fn start_llm_server( let gpu_layers = config_manager.get_config(&default_bot_id, "llm-server-gpu-layers", None).unwrap_or("20".to_string()); let reasoning_format = config_manager.get_config(&default_bot_id, "llm-server-reasoning-format", None).unwrap_or("".to_string()); let n_predict = config_manager.get_config(&default_bot_id, "llm-server-n-predict", None).unwrap_or("50".to_string()); - - // Build command arguments dynamically let mut args = format!( "-m {} --host 0.0.0.0 --port {} --reasoning-format deepseek --top_p 0.95 --temp 0.6 --repeat-penalty 1.2 --n-gpu-layers {}", model_path, port, gpu_layers @@ -213,7 +175,6 @@ pub async fn start_llm_server( if !reasoning_format.is_empty() { args.push_str(&format!(" --reasoning-format {}", reasoning_format)); } - if n_moe != "0" { args.push_str(&format!(" --n-cpu-moe {}", n_moe)); } @@ -226,14 +187,12 @@ pub async fn start_llm_server( if mlock == "true" { args.push_str(" --mlock"); } - if no_mmap == "true" { args.push_str(" --no-mmap"); } if n_predict != "0" { args.push_str(&format!(" --n-predict {}", n_predict)); } - if cfg!(windows) { let mut cmd = tokio::process::Command::new("cmd"); cmd.arg("/C").arg(format!( @@ -251,17 +210,14 @@ pub async fn start_llm_server( info!("Executing LLM server command: cd {} && ./llama-server {} --verbose", llama_cpp_path, args); cmd.spawn()?; } - Ok(()) } - pub async fn start_embedding_server( llama_cpp_path: String, model_path: String, url: String, ) -> Result<(), Box> { let port = url.split(':').last().unwrap_or("8082"); - if cfg!(windows) { let mut cmd = tokio::process::Command::new("cmd"); cmd.arg("/c").arg(format!( @@ -277,6 +233,5 @@ pub async fn start_embedding_server( )); cmd.spawn()?; } - Ok(()) } diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 8eedadccc..189dd08c2 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -2,10 +2,7 @@ use async_trait::async_trait; use futures::StreamExt; use serde_json::Value; use tokio::sync::mpsc; - - pub mod local; - #[async_trait] pub trait LLMProvider: Send + Sync { async fn generate( @@ -13,14 +10,12 @@ pub trait LLMProvider: Send + Sync { prompt: &str, config: &Value, ) -> Result>; - async fn generate_stream( &self, prompt: &str, config: &Value, tx: mpsc::Sender, ) -> Result<(), Box>; - async fn summarize( &self, text: &str, @@ -29,29 +24,25 @@ pub trait LLMProvider: Send + Sync { self.generate(&prompt, &serde_json::json!({"max_tokens": 500})) .await } - async fn cancel_job( &self, session_id: &str, ) -> Result<(), Box>; } - pub struct OpenAIClient { client: reqwest::Client, api_key: String, base_url: String, } - impl OpenAIClient { pub fn new(api_key: String, base_url: Option) -> Self { Self { client: reqwest::Client::new(), api_key, - base_url: base_url.unwrap_or_else(|| "http://localhost:8081/v1".to_string()), + base_url: base_url.unwrap() } } } - #[async_trait] impl LLMProvider for OpenAIClient { async fn generate( @@ -61,7 +52,7 @@ impl LLMProvider for OpenAIClient { ) -> Result> { let response = self .client - .post(&format!("{}/v1/chat/completions", self.base_url)) + .post(&format!("{}/v1/chat/completions/", self.base_url)) .header("Authorization", format!("Bearer {}", self.api_key)) .json(&serde_json::json!({ "model": "gpt-3.5-turbo", @@ -70,24 +61,18 @@ impl LLMProvider for OpenAIClient { })) .send() .await?; - let result: Value = response.json().await?; let raw_content = result["choices"][0]["message"]["content"] .as_str() .unwrap_or(""); - // Define the end token we want to skip up to. Adjust the token string if needed. let end_token = "final<|message|>"; let content = if let Some(pos) = raw_content.find(end_token) { - // Skip everything up to and including the end token. raw_content[(pos + end_token.len())..].to_string() } else { - // If the token is not found, return the full content. raw_content.to_string() }; - Ok(content) } - async fn generate_stream( &self, prompt: &str, @@ -106,14 +91,11 @@ impl LLMProvider for OpenAIClient { })) .send() .await?; - let mut stream = response.bytes_stream(); let mut buffer = String::new(); - while let Some(chunk) = stream.next().await { let chunk = chunk?; let chunk_str = String::from_utf8_lossy(&chunk); - for line in chunk_str.lines() { if line.starts_with("data: ") && !line.contains("[DONE]") { if let Ok(data) = serde_json::from_str::(&line[6..]) { @@ -125,16 +107,12 @@ impl LLMProvider for OpenAIClient { } } } - Ok(()) } - - async fn cancel_job( &self, _session_id: &str, ) -> Result<(), Box> { - // OpenAI doesn't support job cancellation Ok(()) } } diff --git a/src/llm_models/deepseek_r3.rs b/src/llm_models/deepseek_r3.rs index daf715d03..33b61670a 100644 --- a/src/llm_models/deepseek_r3.rs +++ b/src/llm_models/deepseek_r3.rs @@ -1,18 +1,14 @@ use super::ModelHandler; use regex; - pub struct DeepseekR3Handler; - impl ModelHandler for DeepseekR3Handler { fn is_analysis_complete(&self, buffer: &str) -> bool { buffer.contains("") } - fn process_content(&self, content: &str) -> String { let re = regex::Regex::new(r"(?s).*?").unwrap(); re.replace_all(content, "").to_string() } - fn has_analysis_markers(&self, buffer: &str) -> bool { buffer.contains("") } diff --git a/src/llm_models/gpt_oss_120b.rs b/src/llm_models/gpt_oss_120b.rs index cdbec99a2..1277e20db 100644 --- a/src/llm_models/gpt_oss_120b.rs +++ b/src/llm_models/gpt_oss_120b.rs @@ -1,29 +1,21 @@ use super::ModelHandler; - pub struct GptOss120bHandler { } - impl GptOss120bHandler { pub fn new() -> Self { Self { } } } - impl ModelHandler for GptOss120bHandler { fn is_analysis_complete(&self, buffer: &str) -> bool { - // GPT-120B uses explicit end marker buffer.contains("**end**") } - fn process_content(&self, content: &str) -> String { - // Remove both start and end markers from final output content.replace("**start**", "") .replace("**end**", "") } - fn has_analysis_markers(&self, buffer: &str) -> bool { - // GPT-120B uses explicit start marker buffer.contains("**start**") } } diff --git a/src/llm_models/gpt_oss_20b.rs b/src/llm_models/gpt_oss_20b.rs index 7e4471540..2ac68d123 100644 --- a/src/llm_models/gpt_oss_20b.rs +++ b/src/llm_models/gpt_oss_20b.rs @@ -1,12 +1,9 @@ use super::ModelHandler; - pub struct GptOss20bHandler; - impl ModelHandler for GptOss20bHandler { fn is_analysis_complete(&self, buffer: &str) -> bool { buffer.ends_with("final") } - fn process_content(&self, content: &str) -> String { if let Some(pos) = content.find("final") { content[..pos].to_string() @@ -14,7 +11,6 @@ impl ModelHandler for GptOss20bHandler { content.to_string() } } - fn has_analysis_markers(&self, buffer: &str) -> bool { buffer.contains("analysis<|message|>") } diff --git a/src/llm_models/llm_models.test.rs b/src/llm_models/llm_models.test.rs index 7a35d37d5..a9f4b25c4 100644 --- a/src/llm_models/llm_models.test.rs +++ b/src/llm_models/llm_models.test.rs @@ -1,43 +1,32 @@ -//! Tests for LLM models module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_llm_models_module() { test_util::setup(); assert!(true, "Basic LLM models module test"); } - #[test] fn test_deepseek_r3_process_content() { test_util::setup(); let handler = DeepseekR3Handler; let input = r#" Alright, I need to help the user revise their resume entry. Let me read what they provided first. - The original message says: " Auxiliom has been updated last week! New release!" They want it in a few words. Hmm, so maybe instead of saying "has been updated," we can use more concise language because resumes usually don't require too much detail unless there's specific information to include. - I notice that the user wants it for their resume, which often requires bullet points or short sentences without being verbose. So perhaps combining these two thoughts into a single sentence would make sense. Also, using an exclamation mark might help convey enthusiasm about the new release. - Let me put it together: "Auxiliom has been updated last week! New release." That's concise and fits well for a resume. It effectively communicates both that something was updated recently and introduces them as having a new release without adding unnecessary details. - " Auxiliom has been updated last week! New release.""#; - let expected = r#"" Auxiliom has been updated last week! New release.""#; let result = handler.process_content(input); assert_eq!(result, expected); } - #[test] fn test_gpt_oss_20b() { test_util::setup(); assert!(true, "GPT OSS 20B placeholder test"); } - #[test] fn test_gpt_oss_120b() { test_util::setup(); diff --git a/src/llm_models/mod.rs b/src/llm_models/mod.rs index e6fcb4c43..5a64390b3 100644 --- a/src/llm_models/mod.rs +++ b/src/llm_models/mod.rs @@ -1,26 +1,13 @@ -//! Module for handling model-specific behavior and token processing - pub mod gpt_oss_20b; pub mod deepseek_r3; pub mod gpt_oss_120b; - - -/// Trait for model-specific token processing pub trait ModelHandler: Send + Sync { - /// Check if the analysis buffer indicates completion fn is_analysis_complete(&self, buffer: &str) -> bool; - - /// Process the content, removing any model-specific tokens fn process_content(&self, content: &str) -> String; - - /// Check if the buffer contains analysis start markers fn has_analysis_markers(&self, buffer: &str) -> bool; } - -/// Get the appropriate handler based on model path from bot configuration pub fn get_handler(model_path: &str) -> Box { let path = model_path.to_lowercase(); - if path.contains("deepseek") { Box::new(deepseek_r3::DeepseekR3Handler) } else if path.contains("120b") { diff --git a/src/main.test.rs b/src/main.test.rs index a354bbdfc..927ffb2ad 100644 --- a/src/main.test.rs +++ b/src/main.test.rs @@ -1,14 +1,10 @@ -//! Tests for the main application module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_main() { test_util::setup(); - // Basic test that main.rs compiles and has expected components assert!(true, "Basic sanity check"); } } diff --git a/src/meet/meet.test.rs b/src/meet/meet.test.rs index 90e5eec9f..fe328339d 100644 --- a/src/meet/meet.test.rs +++ b/src/meet/meet.test.rs @@ -1,16 +1,12 @@ -//! Tests for meet module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_meet_module() { test_util::setup(); assert!(true, "Basic meet module test"); } - #[test] fn test_meeting_scheduling() { test_util::setup(); diff --git a/src/meet/mod.rs b/src/meet/mod.rs index d3bab6a8d..28942cccc 100644 --- a/src/meet/mod.rs +++ b/src/meet/mod.rs @@ -1,8 +1,6 @@ use actix_web::{web, HttpResponse, Result}; use log::{error, info}; - use crate::shared::state::AppState; - #[actix_web::post("/api/voice/start")] async fn voice_start( data: web::Data, @@ -20,7 +18,6 @@ async fn voice_start( "Voice session start request - session: {}, user: {}", session_id, user_id ); - match data .voice_adapter .start_voice_session(session_id, user_id) @@ -43,7 +40,6 @@ async fn voice_start( } } } - #[actix_web::post("/api/voice/stop")] async fn voice_stop( data: web::Data, diff --git a/src/nvidia/mod.rs b/src/nvidia/mod.rs index 1dea2226e..e4fbd5022 100644 --- a/src/nvidia/mod.rs +++ b/src/nvidia/mod.rs @@ -1,37 +1,25 @@ use anyhow::Result; use std::collections::HashMap; use sysinfo::{System}; - -/// System monitoring data #[derive(Default)] pub struct SystemMetrics { pub gpu_usage: Option, pub cpu_usage: f32, } - -/// Gets current system metrics pub fn get_system_metrics(_current_tokens: usize, _max_tokens: usize) -> Result { let mut sys = System::new(); sys.refresh_cpu_usage(); - - // Get CPU usage (average across all cores) let cpu_usage = sys.global_cpu_usage(); - - // Get GPU usage if available let gpu_usage = if has_nvidia_gpu() { get_gpu_utilization()?.get("gpu").copied() } else { None }; - - Ok(SystemMetrics { gpu_usage, cpu_usage, }) } - -/// Checks if NVIDIA GPU is available pub fn has_nvidia_gpu() -> bool { match std::process::Command::new("nvidia-smi") .arg("--query-gpu=utilization.gpu") @@ -44,21 +32,16 @@ pub fn has_nvidia_gpu() -> bool { } } } - -/// Gets current GPU utilization percentages pub fn get_gpu_utilization() -> Result> { let output = std::process::Command::new("nvidia-smi") .arg("--query-gpu=utilization.gpu,utilization.memory") .arg("--format=csv,noheader,nounits") .output()?; - if !output.status.success() { return Err(anyhow::anyhow!("Failed to query GPU utilization")); } - let output_str = String::from_utf8(output.stdout)?; let mut util = HashMap::new(); - for line in output_str.lines() { let parts: Vec<&str> = line.split(',').collect(); if parts.len() >= 2 { @@ -72,6 +55,5 @@ pub fn get_gpu_utilization() -> Result> { ); } } - Ok(util) } diff --git a/src/package_manager/cli.rs b/src/package_manager/cli.rs index e925d91e6..c257894db 100644 --- a/src/package_manager/cli.rs +++ b/src/package_manager/cli.rs @@ -1,20 +1,15 @@ use anyhow::Result; use std::env; use std::process::Command; - use crate::package_manager::{get_all_components, InstallMode, PackageManager}; - pub async fn run() -> Result<()> { env_logger::init(); let args: Vec = env::args().collect(); - if args.len() < 2 { print_usage(); return Ok(()); } - let command = &args[1]; - match command.as_str() { "start" => { let mode = if args.contains(&"--container".to_string()) { @@ -27,10 +22,8 @@ pub async fn run() -> Result<()> { } else { None }; - let pm = PackageManager::new(mode, tenant)?; println!("Starting all installed components..."); - let components = get_all_components(); for component in components { if pm.is_installed(component.name) { @@ -44,27 +37,19 @@ pub async fn run() -> Result<()> { } "stop" => { println!("Stopping all components..."); - - // Stop components gracefully let components = get_all_components(); for component in components { let _ = Command::new("pkill").arg("-f").arg(component.termination_command).output(); } - println!("✓ BotServer components stopped"); } "restart" => { println!("Restarting BotServer..."); - - // Stop let components = get_all_components(); for component in components { let _ = Command::new("pkill").arg("-f").arg(component.termination_command).output(); } - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - - // Start let mode = if args.contains(&"--container".to_string()) { InstallMode::Container } else { @@ -75,16 +60,13 @@ pub async fn run() -> Result<()> { } else { None }; - let pm = PackageManager::new(mode, tenant)?; - let components = get_all_components(); for component in components { if pm.is_installed(component.name) { let _ = pm.start(component.name); } } - println!("✓ BotServer restarted"); } "install" => { @@ -92,7 +74,6 @@ pub async fn run() -> Result<()> { eprintln!("Usage: botserver install [--container] [--tenant ]"); return Ok(()); } - let component = &args[2]; let mode = if args.contains(&"--container".to_string()) { InstallMode::Container @@ -104,7 +85,6 @@ pub async fn run() -> Result<()> { } else { None }; - let pm = PackageManager::new(mode, tenant)?; pm.install(component).await?; println!("✓ Component '{}' installed successfully", component); @@ -114,7 +94,6 @@ pub async fn run() -> Result<()> { eprintln!("Usage: botserver remove [--container] [--tenant ]"); return Ok(()); } - let component = &args[2]; let mode = if args.contains(&"--container".to_string()) { InstallMode::Container @@ -126,7 +105,6 @@ pub async fn run() -> Result<()> { } else { None }; - let pm = PackageManager::new(mode, tenant)?; pm.remove(component)?; println!("✓ Component '{}' removed successfully", component); @@ -142,7 +120,6 @@ pub async fn run() -> Result<()> { } else { None }; - let pm = PackageManager::new(mode, tenant)?; println!("Available components:"); for component in pm.list() { @@ -159,7 +136,6 @@ pub async fn run() -> Result<()> { eprintln!("Usage: botserver status [--container] [--tenant ]"); return Ok(()); } - let component = &args[2]; let mode = if args.contains(&"--container".to_string()) { InstallMode::Container @@ -171,7 +147,6 @@ pub async fn run() -> Result<()> { } else { None }; - let pm = PackageManager::new(mode, tenant)?; if pm.is_installed(component) { println!("✓ Component '{}' is installed", component); @@ -187,10 +162,8 @@ pub async fn run() -> Result<()> { print_usage(); } } - Ok(()) } - fn print_usage() { println!("BotServer Package Manager\n\nUSAGE:\n botserver [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install Install component\n remove Remove component\n list List all components\n status Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver start\n botserver stop\n botserver restart\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list"); } diff --git a/src/package_manager/component.rs b/src/package_manager/component.rs index 6813e39f0..c808eb807 100644 --- a/src/package_manager/component.rs +++ b/src/package_manager/component.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; - #[derive(Debug, Clone)] pub struct ComponentConfig { pub name: String, diff --git a/src/package_manager/facade.rs b/src/package_manager/facade.rs index 1067862a3..bc1e99298 100644 --- a/src/package_manager/facade.rs +++ b/src/package_manager/facade.rs @@ -9,48 +9,40 @@ use reqwest::Client; use std::collections::HashMap; use std::path::PathBuf; use std::process::Command; - impl PackageManager { pub async fn install(&self, component_name: &str) -> Result<()> { let component = self .components .get(component_name) .context(format!("Component '{}' not found", component_name))?; - trace!( "Starting installation of component '{}' in {:?} mode", component_name, self.mode ); - for dep in &component.dependencies { if !self.is_installed(dep) { warn!("Installing missing dependency: {}", dep); Box::pin(self.install(dep)).await?; } } - match self.mode { InstallMode::Local => self.install_local(component).await?, InstallMode::Container => self.install_container(component)?, } - trace!( "Component '{}' installation completed successfully", component_name ); Ok(()) } - pub async fn install_local(&self, component: &ComponentConfig) -> Result<()> { trace!( "Installing component '{}' locally to {}", component.name, self.base_path.display() ); - self.create_directories(&component.name)?; - let (pre_cmds, post_cmds) = match self.os_type { OsType::Linux => ( &component.pre_install_cmds_linux, @@ -65,10 +57,8 @@ impl PackageManager { &component.post_install_cmds_windows, ), }; - self.run_commands(pre_cmds, "local", &component.name)?; self.install_system_packages(component)?; - if let Some(url) = &component.download_url { let url = url.clone(); let name = component.name.clone(); @@ -76,8 +66,6 @@ impl PackageManager { self.download_and_install(&url, &name, binary_name.as_deref()) .await?; } - - // Process additional data downloads with progress bar if !component.data_download_list.is_empty() { for url in &component.data_download_list { let filename = url.split('/').last().unwrap_or("download.tmp"); @@ -85,19 +73,15 @@ impl PackageManager { utils::download_file(url, output_path.to_str().unwrap()).await?; } } - self.run_commands(post_cmds, "local", &component.name)?; trace!( "Component '{}' installation completed successfully", component.name ); - Ok(()) } - pub fn install_container(&self, component: &ComponentConfig) -> Result<()> { let container_name = format!("{}-{}", self.tenant, component.name); - let output = Command::new("lxc") .args(&[ "launch", @@ -107,17 +91,14 @@ impl PackageManager { "security.privileged=true", ]) .output()?; - if !output.status.success() { return Err(anyhow::anyhow!( "LXC container creation failed: {}", String::from_utf8_lossy(&output.stderr) )); } - std::thread::sleep(std::time::Duration::from_secs(15)); self.exec_in_container(&container_name, "mkdir -p /opt/gbo/{bin,data,conf,logs}")?; - let (pre_cmds, post_cmds) = match self.os_type { OsType::Linux => ( &component.pre_install_cmds_linux, @@ -132,15 +113,12 @@ impl PackageManager { &component.post_install_cmds_windows, ), }; - self.run_commands(pre_cmds, &container_name, &component.name)?; - let packages = match self.os_type { OsType::Linux => &component.linux_packages, OsType::MacOS => &component.macos_packages, OsType::Windows => &component.windows_packages, }; - if !packages.is_empty() { let pkg_list = packages.join(" "); self.exec_in_container( @@ -148,7 +126,6 @@ impl PackageManager { &format!("apt-get update && apt-get install -y {}", pkg_list), )?; } - if let Some(url) = &component.download_url { self.download_in_container( &container_name, @@ -157,15 +134,12 @@ impl PackageManager { component.binary_name.as_deref(), )?; } - self.run_commands(post_cmds, &container_name, &component.name)?; - self.exec_in_container( &container_name, "useradd --system --no-create-home --shell /bin/false gbuser", )?; self.mount_container_directories(&container_name, &component.name)?; - if !component.exec_cmd.is_empty() { self.create_container_service( &container_name, @@ -174,37 +148,30 @@ impl PackageManager { &component.env_vars, )?; } - self.setup_port_forwarding(&container_name, &component.ports)?; trace!( "Container installation of '{}' completed in {}", component.name, container_name ); - Ok(()) } - pub fn remove(&self, component_name: &str) -> Result<()> { let component = self .components .get(component_name) .context(format!("Component '{}' not found", component_name))?; - match self.mode { InstallMode::Local => self.remove_local(component)?, InstallMode::Container => self.remove_container(component)?, } - Ok(()) } - pub fn remove_local(&self, component: &ComponentConfig) -> Result<()> { let bin_path = self.base_path.join("bin").join(&component.name); let _ = std::fs::remove_dir_all(bin_path); Ok(()) } - pub fn remove_container(&self, component: &ComponentConfig) -> Result<()> { let container_name = format!("{}-{}", self.tenant, component.name); let _ = Command::new("lxc") @@ -213,21 +180,17 @@ impl PackageManager { let output = Command::new("lxc") .args(&["delete", &container_name]) .output()?; - if !output.status.success() { warn!( "Container deletion had issues: {}", String::from_utf8_lossy(&output.stderr) ); } - Ok(()) } - pub fn list(&self) -> Vec { self.components.keys().cloned().collect() } - pub fn is_installed(&self, component_name: &str) -> bool { match self.mode { InstallMode::Local => { @@ -240,17 +203,14 @@ impl PackageManager { .args(&["list", &container_name, "--format=json"]) .output() .unwrap(); - if !output.status.success() { return false; } - let output_str = String::from_utf8_lossy(&output.stdout); !output_str.contains("\"name\":\"") || output_str.contains("\"status\":\"Stopped\"") } } } - pub fn create_directories(&self, component: &str) -> Result<()> { let dirs = ["bin", "data", "conf", "logs"]; for dir in &dirs { @@ -260,37 +220,30 @@ impl PackageManager { } Ok(()) } - pub fn install_system_packages(&self, component: &ComponentConfig) -> Result<()> { let packages = match self.os_type { OsType::Linux => &component.linux_packages, OsType::MacOS => &component.macos_packages, OsType::Windows => &component.windows_packages, }; - if packages.is_empty() { return Ok(()); } - trace!( "Installing {} system packages for component '{}'", packages.len(), component.name ); - match self.os_type { OsType::Linux => { let output = Command::new("apt-get").args(&["update"]).output()?; - if !output.status.success() { warn!("apt-get update had issues"); } - let output = Command::new("apt-get") .args(&["install", "-y"]) .args(packages) .output()?; - if !output.status.success() { warn!("Some packages may have failed to install"); } @@ -300,7 +253,6 @@ impl PackageManager { .args(&["install"]) .args(packages) .output()?; - if !output.status.success() { warn!("Homebrew installation had warnings"); } @@ -309,10 +261,8 @@ impl PackageManager { warn!("Windows package installation not implemented"); } } - Ok(()) } - pub async fn download_and_install( &self, url: &str, @@ -321,21 +271,17 @@ impl PackageManager { ) -> Result<()> { let bin_path = self.base_path.join("bin").join(component); std::fs::create_dir_all(&bin_path)?; - let filename = url.split('/').last().unwrap_or("download.tmp"); let temp_file = if filename.starts_with('/') { PathBuf::from(filename) } else { bin_path.join(filename) }; - self.download_with_reqwest(url, &temp_file, component) .await?; self.handle_downloaded_file(&temp_file, &bin_path, binary_name)?; - Ok(()) } - pub async fn download_with_reqwest( &self, url: &str, @@ -344,14 +290,11 @@ impl PackageManager { ) -> Result<()> { const MAX_RETRIES: u32 = 3; const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2); - let client = Client::builder() .timeout(std::time::Duration::from_secs(30)) .user_agent("botserver-package-manager/1.0") .build()?; - let mut last_error = None; - for attempt in 0..=MAX_RETRIES { if attempt > 0 { trace!( @@ -362,7 +305,6 @@ impl PackageManager { ); std::thread::sleep(RETRY_DELAY * attempt); } - match self.attempt_reqwest_download(&client, url, temp_file).await { Ok(_size) => { if attempt > 0 { @@ -377,7 +319,6 @@ impl PackageManager { } } } - Err(anyhow::anyhow!( "Failed to download {} after {} attempts. Last error: {}", component, @@ -385,7 +326,6 @@ impl PackageManager { last_error.unwrap() )) } - pub async fn attempt_reqwest_download( &self, _client: &Client, @@ -396,12 +336,10 @@ impl PackageManager { utils::download_file(url, output_path) .await .map_err(|e| anyhow::anyhow!("Failed to download file using shared utility: {}", e))?; - let metadata = std::fs::metadata(temp_file).context("Failed to get file metadata")?; let size = metadata.len(); Ok(size) } - pub fn handle_downloaded_file( &self, temp_file: &PathBuf, @@ -412,12 +350,10 @@ impl PackageManager { if metadata.len() == 0 { return Err(anyhow::anyhow!("Downloaded file is empty")); } - let file_extension = temp_file .extension() .and_then(|ext| ext.to_str()) .unwrap_or(""); - match file_extension { "gz" | "tgz" => { self.extract_tar_gz(temp_file, bin_path)?; @@ -435,44 +371,36 @@ impl PackageManager { } } } - Ok(()) } - pub fn extract_tar_gz(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> { let output = Command::new("tar") .current_dir(bin_path) .args(&["-xzf", temp_file.to_str().unwrap(), "--strip-components=1"]) .output()?; - if !output.status.success() { return Err(anyhow::anyhow!( "tar extraction failed: {}", String::from_utf8_lossy(&output.stderr) )); } - std::fs::remove_file(temp_file)?; Ok(()) } - pub fn extract_zip(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> { let output = Command::new("unzip") .current_dir(bin_path) .args(&["-o", "-q", temp_file.to_str().unwrap()]) .output()?; - if !output.status.success() { return Err(anyhow::anyhow!( "unzip extraction failed: {}", String::from_utf8_lossy(&output.stderr) )); } - std::fs::remove_file(temp_file)?; Ok(()) } - pub fn install_binary( &self, temp_file: &PathBuf, @@ -484,7 +412,6 @@ impl PackageManager { self.make_executable(&final_path)?; Ok(()) } - pub fn make_executable(&self, path: &PathBuf) -> Result<()> { #[cfg(unix)] { @@ -495,8 +422,6 @@ impl PackageManager { } Ok(()) } - - pub fn run_commands(&self, commands: &[String], target: &str, component: &str) -> Result<()> { let bin_path = if target == "local" { self.base_path.join("bin").join(component) @@ -518,14 +443,12 @@ impl PackageManager { } else { PathBuf::from("/opt/gbo/logs") }; - for cmd in commands { let rendered_cmd = cmd .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); - if target == "local" { trace!("Executing command: {}", rendered_cmd); let child = Command::new("bash") @@ -535,14 +458,12 @@ impl PackageManager { .with_context(|| { format!("Failed to spawn command for component '{}'", component) })?; - let output = child.wait_with_output().with_context(|| { format!( "Failed while waiting for command to finish for component '{}'", component ) })?; - if !output.status.success() { error!( "Command had non-zero exit: {}", @@ -553,25 +474,20 @@ impl PackageManager { self.exec_in_container(target, &rendered_cmd)?; } } - Ok(()) } - pub fn exec_in_container(&self, container: &str, command: &str) -> Result<()> { let output = Command::new("lxc") .args(&["exec", container, "--", "bash", "-c", command]) .output()?; - if !output.status.success() { warn!( "Container command failed: {}", String::from_utf8_lossy(&output.stderr) ); } - Ok(()) } - pub fn download_in_container( &self, container: &str, @@ -581,7 +497,6 @@ impl PackageManager { ) -> Result<()> { let download_cmd = format!("wget -O /tmp/download.tmp {}", url); self.exec_in_container(container, &download_cmd)?; - if url.ends_with(".tar.gz") || url.ends_with(".tgz") { self.exec_in_container(container, "tar -xzf /tmp/download.tmp -C /opt/gbo/bin")?; } else if url.ends_with(".zip") { @@ -593,25 +508,19 @@ impl PackageManager { ); self.exec_in_container(container, &mv_cmd)?; } - self.exec_in_container(container, "rm -f /tmp/download.tmp")?; Ok(()) } - pub fn mount_container_directories(&self, container: &str, component: &str) -> Result<()> { let host_base = format!("/opt/gbo/tenants/{}/{}", self.tenant, component); - for dir in &["data", "conf", "logs"] { let host_path = format!("{}/{}", host_base, dir); std::fs::create_dir_all(&host_path)?; - let device_name = format!("{}-{}", component, dir); let container_path = format!("/opt/gbo/{}", dir); - let _ = Command::new("lxc") .args(&["config", "device", "remove", container, &device_name]) .output(); - let output = Command::new("lxc") .args(&[ "config", @@ -624,11 +533,9 @@ impl PackageManager { &format!("path={}", container_path), ]) .output()?; - if !output.status.success() { warn!("Failed to mount {} in container {}", dir, container); } - trace!( "Mounted {} to {} in container {}", host_path, @@ -636,10 +543,8 @@ impl PackageManager { container ); } - Ok(()) } - pub fn create_container_service( &self, container: &str, @@ -652,7 +557,6 @@ impl PackageManager { .replace("{{DATA_PATH}}", "/opt/gbo/data") .replace("{{CONF_PATH}}", "/opt/gbo/conf") .replace("{{LOGS_PATH}}", "/opt/gbo/logs"); - let mut env_section = String::new(); for (key, value) in env_vars { let rendered_value = value @@ -662,15 +566,12 @@ impl PackageManager { .replace("{{LOGS_PATH}}", "/opt/gbo/logs"); env_section.push_str(&format!("Environment={}={}\n", key, rendered_value)); } - let service_content = format!( "[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory=/opt/gbo/data\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", component, env_section, rendered_cmd ); - let service_file = format!("/tmp/{}.service", component); std::fs::write(&service_file, &service_content)?; - let output = Command::new("lxc") .args(&[ "file", @@ -679,33 +580,26 @@ impl PackageManager { &format!("{}/etc/systemd/system/{}.service", container, component), ]) .output()?; - if !output.status.success() { warn!("Failed to push service file to container"); } - self.exec_in_container(container, "systemctl daemon-reload")?; self.exec_in_container(container, &format!("systemctl enable {}", component))?; self.exec_in_container(container, &format!("systemctl start {}", component))?; - std::fs::remove_file(&service_file)?; trace!( "Created and started service in container {}: {}", container, component ); - Ok(()) } - pub fn setup_port_forwarding(&self, container: &str, ports: &[u16]) -> Result<()> { for port in ports { let device_name = format!("port-{}", port); - let _ = Command::new("lxc") .args(&["config", "device", "remove", container, &device_name]) .output(); - let output = Command::new("lxc") .args(&[ "config", @@ -718,18 +612,15 @@ impl PackageManager { &format!("connect=tcp:127.0.0.1:{}", port), ]) .output()?; - if !output.status.success() { warn!("Failed to setup port forwarding for port {}", port); } - trace!( "Port forwarding configured: {} -> container {}", port, container ); } - Ok(()) } } diff --git a/src/package_manager/mod.rs b/src/package_manager/mod.rs index e7db1f882..aa5e8a415 100644 --- a/src/package_manager/mod.rs +++ b/src/package_manager/mod.rs @@ -1,29 +1,24 @@ pub mod component; pub mod installer; pub mod os; - pub use installer::PackageManager; pub mod cli; pub mod facade; - #[derive(Debug, Clone, PartialEq)] pub enum InstallMode { Local, Container, } - #[derive(Debug, Clone, PartialEq)] pub enum OsType { Linux, MacOS, Windows, } - pub struct ComponentInfo { pub name: &'static str, pub termination_command: &'static str, } - pub fn get_all_components() -> Vec { vec![ ComponentInfo { diff --git a/src/package_manager/os.rs b/src/package_manager/os.rs index a701393d4..92f7fe093 100644 --- a/src/package_manager/os.rs +++ b/src/package_manager/os.rs @@ -1,5 +1,4 @@ use crate::package_manager::OsType; - pub fn detect_os() -> OsType { if cfg!(target_os = "linux") { OsType::Linux diff --git a/src/package_manager/package_manager.test.rs b/src/package_manager/package_manager.test.rs index a30ceccdc..50bc59dfa 100644 --- a/src/package_manager/package_manager.test.rs +++ b/src/package_manager/package_manager.test.rs @@ -1,28 +1,22 @@ -//! Tests for package manager module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_package_manager_module() { test_util::setup(); assert!(true, "Basic package manager module test"); } - #[test] fn test_cli_interface() { test_util::setup(); assert!(true, "CLI interface placeholder test"); } - #[test] fn test_component_management() { test_util::setup(); assert!(true, "Component management placeholder test"); } - #[test] fn test_os_specific() { test_util::setup(); diff --git a/src/riot_compiler/mod.rs b/src/riot_compiler/mod.rs index 6a6822368..4c59b96ea 100644 --- a/src/riot_compiler/mod.rs +++ b/src/riot_compiler/mod.rs @@ -1,27 +1,20 @@ use boa_engine::{Context, JsValue, Source}; - fn compile_riot_component(riot_code: &str) -> Result> { let mut context = Context::default(); - - let compiler = include_str!("riot_compiler.js"); // Your Riot compiler logic - + let compiler = include_str!("riot_compiler.js"); context.eval(Source::from_bytes(compiler))?; - let result = context.eval(Source::from_bytes(&format!( "compileRiot(`{}`)", riot_code.replace('`', "\\`") )))?; - Ok(result) } - fn main() { let riot_component = r#"

{ props.title }

✓ Done -
"#; - match compile_riot_component(riot_component) { Ok(compiled) => println!("Compiled: {:?}", compiled), Err(e) => eprintln!("Compilation failed: {}", e), diff --git a/src/riot_compiler/riot_compiler.test.rs b/src/riot_compiler/riot_compiler.test.rs index 6b66a7645..858b4eaf8 100644 --- a/src/riot_compiler/riot_compiler.test.rs +++ b/src/riot_compiler/riot_compiler.test.rs @@ -1,16 +1,12 @@ -//! Tests for Riot compiler module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_riot_compiler_module() { test_util::setup(); assert!(true, "Basic Riot compiler module test"); } - #[test] fn test_compilation() { test_util::setup(); diff --git a/src/session/mod.rs b/src/session/mod.rs index 802733a2d..12df5e481 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -285,8 +285,8 @@ impl SessionManager { let mut history: Vec<(String, String)> = Vec::new(); for (other_role, content) in messages { let role_str = match other_role { - 1 => "user".to_string(), - 2 => "assistant".to_string(), + 1 => "human".to_string(), + 2 => "bot".to_string(), 3 => "system".to_string(), 9 => "compact".to_string(), _ => "unknown".to_string(), diff --git a/src/session/session.test.rs b/src/session/session.test.rs index 10f0cafb8..384f8ad30 100644 --- a/src/session/session.test.rs +++ b/src/session/session.test.rs @@ -1,16 +1,12 @@ -//! Tests for session module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_session_module() { test_util::setup(); assert!(true, "Basic session module test"); } - #[test] fn test_session_management() { test_util::setup(); diff --git a/src/shared/models.rs b/src/shared/models.rs index 72a0306da..5b5571bfb 100644 --- a/src/shared/models.rs +++ b/src/shared/models.rs @@ -2,8 +2,6 @@ use chrono::{DateTime, Utc}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use uuid::Uuid; - - #[derive(Debug, Clone, Copy, PartialEq)] pub enum TriggerKind { Scheduled = 0, @@ -11,7 +9,6 @@ pub enum TriggerKind { TableInsert = 2, TableDelete = 3, } - impl TriggerKind { pub fn _from_i32(value: i32) -> Option { match value { @@ -23,7 +20,6 @@ impl TriggerKind { } } } - #[derive(Debug, Queryable, Serialize, Deserialize, Identifiable)] #[diesel(table_name = system_automations)] pub struct Automation { @@ -36,7 +32,6 @@ pub struct Automation { pub is_active: bool, pub last_triggered: Option>, } - #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Selectable)] #[diesel(table_name = user_sessions)] pub struct UserSession { @@ -49,10 +44,6 @@ pub struct UserSession { pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } - - - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserMessage { pub bot_id: String, @@ -65,13 +56,11 @@ pub struct UserMessage { pub timestamp: DateTime, pub context_name: Option, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Suggestion { - pub text: String, // The button text that will be sent as message - pub context: String, // The context name to set when clicked + pub text: String, + pub context: String, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BotResponse { pub bot_id: String, @@ -87,7 +76,6 @@ pub struct BotResponse { pub context_length: usize, pub context_max_length: usize, } - impl BotResponse { pub fn from_string_ids( bot_id: &str, @@ -112,7 +100,6 @@ impl BotResponse { }) } } - #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Insertable)] #[diesel(table_name = bot_memories)] pub struct BotMemory { @@ -123,7 +110,6 @@ pub struct BotMemory { pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } - pub mod schema { diesel::table! { organizations (org_id) { @@ -133,7 +119,6 @@ pub mod schema { created_at -> Timestamptz, } } - diesel::table! { bots (id) { id -> Uuid, @@ -149,7 +134,6 @@ pub mod schema { tenant_id -> Nullable, } } - diesel::table! { system_automations (id) { id -> Uuid, @@ -162,7 +146,6 @@ pub mod schema { last_triggered -> Nullable, } } - diesel::table! { user_sessions (id) { id -> Uuid, @@ -175,7 +158,6 @@ pub mod schema { updated_at -> Timestamptz, } } - diesel::table! { message_history (id) { id -> Uuid, @@ -188,7 +170,6 @@ pub mod schema { created_at -> Timestamptz, } } - diesel::table! { users (id) { id -> Uuid, @@ -200,7 +181,6 @@ pub mod schema { updated_at -> Timestamptz, } } - diesel::table! { clicks (id) { id -> Uuid, @@ -209,7 +189,6 @@ pub mod schema { updated_at -> Timestamptz, } } - diesel::table! { bot_memories (id) { id -> Uuid, @@ -220,7 +199,6 @@ pub mod schema { updated_at -> Timestamptz, } } - diesel::table! { kb_documents (id) { id -> Text, @@ -238,7 +216,6 @@ pub mod schema { updated_at -> Text, } } - diesel::table! { basic_tools (id) { id -> Text, @@ -255,7 +232,6 @@ pub mod schema { updated_at -> Text, } } - diesel::table! { kb_collections (id) { id -> Text, @@ -270,7 +246,6 @@ pub mod schema { updated_at -> Text, } } - diesel::table! { user_kb_associations (id) { id -> Text, @@ -283,7 +258,6 @@ pub mod schema { updated_at -> Text, } } - diesel::table! { session_tool_associations (id) { id -> Text, @@ -292,21 +266,17 @@ pub mod schema { added_at -> Text, } } - diesel::table! { bot_configuration (id) { id -> Uuid, bot_id -> Uuid, config_key -> Text, config_value -> Text, - is_encrypted -> Bool, - config_type -> Text, created_at -> Timestamptz, updated_at -> Timestamptz, } } } - pub use schema::*; diff --git a/src/shared/schema.rs b/src/shared/schema.rs index d9a52af7e..e69de29bb 100644 --- a/src/shared/schema.rs +++ b/src/shared/schema.rs @@ -1 +0,0 @@ -// @generated automatically by Diesel CLI. diff --git a/src/shared/shared.test.rs b/src/shared/shared.test.rs index bdce508f8..e90fe84c2 100644 --- a/src/shared/shared.test.rs +++ b/src/shared/shared.test.rs @@ -1,28 +1,22 @@ -//! Tests for shared module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_shared_module() { test_util::setup(); assert!(true, "Basic shared module test"); } - #[test] fn test_models() { test_util::setup(); assert!(true, "Models placeholder test"); } - #[test] fn test_state() { test_util::setup(); assert!(true, "State placeholder test"); } - #[test] fn test_utils() { test_util::setup(); diff --git a/src/shared/utils.rs b/src/shared/utils.rs index cc5573bef..fee812d97 100644 --- a/src/shared/utils.rs +++ b/src/shared/utils.rs @@ -16,14 +16,12 @@ use tokio::io::AsyncWriteExt; use aws_sdk_s3::{Client as S3Client, config::Builder as S3ConfigBuilder}; use aws_config::BehaviorVersion; use crate::config::DriveConfig; - pub async fn create_s3_operator(config: &DriveConfig) -> Result> { let endpoint = if !config.server.ends_with('/') { format!("{}/", config.server) } else { config.server.clone() }; - let base_config = aws_config::defaults(BehaviorVersion::latest()) .endpoint_url(endpoint) .region("auto") @@ -38,14 +36,11 @@ pub async fn create_s3_operator(config: &DriveConfig) -> Result Dynamic { match value { Value::Null => Dynamic::UNIT, @@ -72,7 +67,6 @@ pub fn json_value_to_dynamic(value: &Value) -> Dynamic { ), } } - pub fn to_array(value: Dynamic) -> Array { if value.is_array() { value.cast::() @@ -82,7 +76,6 @@ pub fn to_array(value: Dynamic) -> Array { Array::from([value]) } } - pub async fn download_file(url: &str, output_path: &str) -> Result<(), anyhow::Error> { let url = url.to_string(); let output_path = output_path.to_string(); @@ -116,7 +109,6 @@ pub async fn download_file(url: &str, output_path: &str) -> Result<(), anyhow::E }); download_handle.await? } - pub fn parse_filter(filter_str: &str) -> Result<(String, Vec), Box> { let parts: Vec<&str> = filter_str.split('=').collect(); if parts.len() != 2 { @@ -132,27 +124,22 @@ pub fn parse_filter(filter_str: &str) -> Result<(String, Vec), Box usize { let char_count = text.chars().count(); (char_count / 4).max(1) } - pub fn establish_pg_connection() -> Result { let database_url = std::env::var("DATABASE_URL").unwrap(); PgConnection::establish(&database_url) .with_context(|| format!("Failed to connect to database at {}", database_url)) } - pub type DbPool = Pool>; pub fn create_conn() -> Result { let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string()); + .unwrap(); let manager = ConnectionManager::::new(database_url); Pool::builder().build(manager) } - - pub fn parse_database_url(url: &str) -> (String, String, String, u32, String) { if let Some(stripped) = url.strip_prefix("postgres://") { let parts: Vec<&str> = stripped.split('@').collect(); @@ -173,11 +160,5 @@ pub fn parse_database_url(url: &str) -> (String, String, String, u32, String) { } } } - ( - "gbuser".to_string(), - "".to_string(), - "localhost".to_string(), - 5432, - "botserver".to_string(), - ) + ("".to_string(), "".to_string(), "".to_string(), 5432, "".to_string()) } \ No newline at end of file diff --git a/src/tests/test_util.rs b/src/tests/test_util.rs index 7799017d2..85b663e8b 100644 --- a/src/tests/test_util.rs +++ b/src/tests/test_util.rs @@ -1,17 +1,9 @@ -//! Common test utilities for the botserver project - use std::sync::Once; - static INIT: Once = Once::new(); - -/// Setup function to be called at the beginning of each test module pub fn setup() { INIT.call_once(|| { - // Initialize any test configuration here }); } - -/// Simple assertion macro for better test error messages #[macro_export] macro_rules! assert_ok { ($expr:expr) => { @@ -21,8 +13,6 @@ macro_rules! assert_ok { } }; } - -/// Simple assertion macro for error cases #[macro_export] macro_rules! assert_err { ($expr:expr) => { diff --git a/src/ui/drive.rs b/src/ui/drive.rs index 71acadba3..3daffce0d 100644 --- a/src/ui/drive.rs +++ b/src/ui/drive.rs @@ -2,23 +2,19 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; use tauri::{Emitter, Window}; - #[derive(Debug, Serialize, Deserialize)] pub struct FileItem { name: String, path: String, is_dir: bool, } - #[tauri::command] pub fn list_files(path: &str) -> Result, String> { let base_path = Path::new(path); let mut files = Vec::new(); - if !base_path.exists() { return Err("Path does not exist".into()); } - for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? { let entry = entry.map_err(|e| e.to_string())?; let path = entry.path(); @@ -27,15 +23,12 @@ pub fn list_files(path: &str) -> Result, String> { .and_then(|n| n.to_str()) .unwrap_or("") .to_string(); - files.push(FileItem { name, path: path.to_str().unwrap_or("").to_string(), is_dir: path.is_dir(), }); } - - // Sort directories first, then files files.sort_by(|a, b| { if a.is_dir && !b.is_dir { std::cmp::Ordering::Less @@ -45,51 +38,39 @@ pub fn list_files(path: &str) -> Result, String> { a.name.cmp(&b.name) } }); - Ok(files) } - #[tauri::command] pub async fn upload_file(window: Window, src_path: String, dest_path: String) -> Result<(), String> { use std::fs::File; use std::io::{Read, Write}; - let src = PathBuf::from(&src_path); let dest_dir = PathBuf::from(&dest_path); let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?); - - // Create destination directory if it doesn't exist if !dest_dir.exists() { fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?; } - let mut source_file = File::open(&src).map_err(|e| e.to_string())?; let mut dest_file = File::create(&dest).map_err(|e| e.to_string())?; - let file_size = source_file.metadata().map_err(|e| e.to_string())?.len(); let mut buffer = [0; 8192]; let mut total_read = 0; - loop { let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?; if bytes_read == 0 { break; } - dest_file .write_all(&buffer[..bytes_read]) .map_err(|e| e.to_string())?; total_read += bytes_read as u64; - let progress = (total_read as f64 / file_size as f64) * 100.0; window .emit("upload_progress", progress) .map_err(|e| e.to_string())?; } - Ok(()) } - #[tauri::command] pub fn create_folder(path: String, name: String) -> Result<(), String> { let full_path = Path::new(&path).join(&name); diff --git a/src/ui/local-sync.rs b/src/ui/local-sync.rs index dd2e6f7bc..6f7655356 100644 --- a/src/ui/local-sync.rs +++ b/src/ui/local-sync.rs @@ -11,8 +11,6 @@ use std::time::{Duration, Instant}; use notify_rust::Notification; use serde::{Deserialize, Serialize}; use serde_json::Value; - -// App state #[derive(Debug, Clone)] struct AppState { name: String, @@ -26,13 +24,11 @@ struct AppState { show_about_dialog: bool, current_screen: Screen, } - #[derive(Debug, Clone)] enum Screen { Main, Status, } - #[derive(Debug, Clone, Serialize, Deserialize)] struct RcloneConfig { name: String, @@ -41,7 +37,6 @@ struct RcloneConfig { access_key: String, secret_key: String, } - #[derive(Debug, Clone, Serialize, Deserialize)] struct SyncStatus { name: String, @@ -51,7 +46,6 @@ struct SyncStatus { errors: usize, last_updated: String, } - #[derive(Debug, Clone)] enum Message { NameChanged(String), @@ -67,15 +61,12 @@ enum Message { BackToMain, None, } - fn main() { dioxus_desktop::launch(app); } - fn app(cx: Scope) -> Element { let window = use_window(); window.set_inner_size(LogicalSize::new(800, 600)); - let state = use_ref(cx, || AppState { name: String::new(), access_key: String::new(), @@ -88,27 +79,20 @@ fn app(cx: Scope) -> Element { show_about_dialog: false, current_screen: Screen::Main, }); - - // Monitor sync status use_future( async move { let state = state.clone(); async move { let mut last_check = Instant::now(); let check_interval = Duration::from_secs(5); - loop { tokio::time::sleep(Duration::from_secs(1)).await; - if !*state.read().sync_active.lock().unwrap() { continue; } - if last_check.elapsed() < check_interval { continue; } - last_check = Instant::now(); - match read_rclone_configs() { Ok(configs) => { let mut new_statuses = Vec::new(); @@ -126,11 +110,9 @@ fn app(cx: Scope) -> Element { } } }); - cx.render(rsx! { div { class: "app", - // Main menu bar div { class: "menu-bar", button { @@ -142,8 +124,6 @@ fn app(cx: Scope) -> Element { "About" } } - - // Main content {match state.read().current_screen { Screen::Main => rsx! { div { @@ -189,8 +169,6 @@ fn app(cx: Scope) -> Element { } } }} - - // Config dialog if state.read().show_config_dialog { div { class: "dialog", @@ -223,8 +201,6 @@ fn app(cx: Scope) -> Element { } } } - - // About dialog if state.read().show_about_dialog { div { class: "dialog", @@ -240,34 +216,27 @@ fn app(cx: Scope) -> Element { } }) } - -// Save sync configuration fn save_config(state: &UseRef) { if state.read().name.is_empty() || state.read().access_key.is_empty() || state.read().secret_key.is_empty() { state.write_with(|state| state.status_text = "All fields are required!".to_string()); return; } - let new_config = RcloneConfig { name: state.read().name.clone(), - remote_path: format!("s3://{}", state.read().name), + remote_path: format!("s3: local_path: Path::new(&env::var("HOME").unwrap()).join("General Bots").join(&state.read().name).to_string_lossy().to_string(), access_key: state.read().access_key.clone(), secret_key: state.read().secret_key.clone(), }; - if let Err(e) = save_rclone_config(&new_config) { state.write_with(|state| state.status_text = format!("Failed to save config: {}", e)); } else { state.write_with(|state| state.status_text = "New sync saved!".to_string()); } } - -// Start sync process fn start_sync(state: &UseRef) { let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap()); processes.clear(); - match read_rclone_configs() { Ok(configs) => { for config in configs { @@ -282,8 +251,6 @@ fn start_sync(state: &UseRef) { Err(e) => state.write_with(|state| state.status_text = format!("Failed to read configurations: {}", e)), } } - -// Stop sync process fn stop_sync(state: &UseRef) { let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap()); for child in processes.iter_mut() { @@ -293,47 +260,38 @@ fn stop_sync(state: &UseRef) { state.write_with(|state| *state.sync_active.lock().unwrap() = false); state.write_with(|state| state.status_text = "Sync stopped.".to_string()); } - -// Utility functions (rclone, notifications, etc.) fn save_rclone_config(config: &RcloneConfig) -> Result<(), String> { let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; let config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf"); - let mut file = OpenOptions::new() .create(true) .append(true) .open(&config_path) .map_err(|e| format!("Failed to open config file: {}", e))?; - writeln!(file, "[{}]", config.name) .and_then(|_| writeln!(file, "type = s3")) .and_then(|_| writeln!(file, "provider = Other")) .and_then(|_| writeln!(file, "access_key_id = {}", config.access_key)) .and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_key)) - .and_then(|_| writeln!(file, "endpoint = https://drive-api.pragmatismo.com.br")) + .and_then(|_| writeln!(file, "endpoint = https: .and_then(|_| writeln!(file, "acl = private")) .map_err(|e| format!("Failed to write config: {}", e)) } - fn read_rclone_configs() -> Result, String> { let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; let config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf"); - if !config_path.exists() { return Ok(Vec::new()); } - let file = File::open(&config_path).map_err(|e| format!("Failed to open config file: {}", e))?; let reader = BufReader::new(file); let mut configs = Vec::new(); let mut current_config: Option = None; - for line in reader.lines() { let line = line.map_err(|e| format!("Failed to read line: {}", e))?; if line.is_empty() || line.starts_with('#') { continue; } - if line.starts_with('[') && line.ends_with(']') { if let Some(config) = current_config.take() { configs.push(config); @@ -341,7 +299,7 @@ fn read_rclone_configs() -> Result, String> { let name = line[1..line.len()-1].to_string(); current_config = Some(RcloneConfig { name: name.clone(), - remote_path: format!("s3://{}", name), + remote_path: format!("s3: local_path: Path::new(&home_dir).join("General Bots").join(&name).to_string_lossy().to_string(), access_key: String::new(), secret_key: String::new(), @@ -358,20 +316,16 @@ fn read_rclone_configs() -> Result, String> { } } } - if let Some(config) = current_config { configs.push(config); } - Ok(configs) } - fn run_sync(config: &RcloneConfig) -> Result { let local_path = Path::new(&config.local_path); if !local_path.exists() { create_dir_all(local_path)?; } - ProcCommand::new("rclone") .arg("sync") .arg(&config.remote_path) @@ -383,7 +337,6 @@ fn run_sync(config: &RcloneConfig) -> Result { .stderr(Stdio::null()) .spawn() } - fn get_rclone_status(remote_name: &str) -> Result { let output = ProcCommand::new("rclone") .arg("rc") @@ -391,11 +344,9 @@ fn get_rclone_status(remote_name: &str) -> Result { .arg("--json") .output() .map_err(|e| format!("Failed to execute rclone rc: {}", e))?; - if !output.status.success() { return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr))); } - let json = String::from_utf8_lossy(&output.stdout); let parsed: Result = serde_json::from_str(&json); match parsed { @@ -403,7 +354,6 @@ fn get_rclone_status(remote_name: &str) -> Result { let transferred = value.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0); let errors = value.get("errors").and_then(|v| v.as_u64()).unwrap_or(0); let speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0); - let status = if errors > 0 { "Error occurred".to_string() } else if speed > 0.0 { @@ -413,7 +363,6 @@ fn get_rclone_status(remote_name: &str) -> Result { } else { "Initializing".to_string() }; - Ok(SyncStatus { name: remote_name.to_string(), status, @@ -426,12 +375,10 @@ fn get_rclone_status(remote_name: &str) -> Result { Err(e) => Err(format!("Failed to parse rclone status: {}", e)), } } - fn format_bytes(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; const GB: u64 = MB * 1024; - if bytes >= GB { format!("{:.2} GB", bytes as f64 / GB as f64) } else if bytes >= MB { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 02710787a..fa030d16d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,3 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] pub mod drive; pub mod sync; \ No newline at end of file diff --git a/src/ui/stream.rs b/src/ui/stream.rs index d61ec11ac..3cb662799 100644 --- a/src/ui/stream.rs +++ b/src/ui/stream.rs @@ -2,19 +2,16 @@ use ratatui::{ style::{Color, Style}, widgets::{Block, Borders, Gauge}, }; - pub struct StreamProgress { pub progress: f64, pub status: String, } - pub fn render_progress_bar(progress: &StreamProgress) -> Gauge { let color = if progress.progress >= 1.0 { Color::Green } else { Color::Blue }; - Gauge::default() .block( Block::default() diff --git a/src/ui/sync.rs b/src/ui/sync.rs index 3732b0a98..979a60685 100644 --- a/src/ui/sync.rs +++ b/src/ui/sync.rs @@ -5,8 +5,6 @@ use std::path::Path; use std::fs::{OpenOptions, create_dir_all}; use std::io::Write; use std::env; - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RcloneConfig { name: String, @@ -15,7 +13,6 @@ pub struct RcloneConfig { access_key: String, secret_key: String, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncStatus { name: String, @@ -25,40 +22,34 @@ pub struct SyncStatus { errors: usize, last_updated: String, } - pub(crate) struct AppState { pub sync_processes: Mutex>, pub sync_active: Mutex, } - #[tauri::command] pub fn save_config(config: RcloneConfig) -> Result<(), String> { let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; let config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf"); - let mut file = OpenOptions::new() .create(true) .append(true) .open(&config_path) .map_err(|e| format!("Failed to open config file: {}", e))?; - writeln!(file, "[{}]", config.name) .and_then(|_| writeln!(file, "type = s3")) .and_then(|_| writeln!(file, "provider = Other")) .and_then(|_| writeln!(file, "access_key_id = {}", config.access_key)) .and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_key)) - .and_then(|_| writeln!(file, "endpoint = https://drive-api.pragmatismo.com.br")) + .and_then(|_| writeln!(file, "endpoint = https: .and_then(|_| writeln!(file, "acl = private")) .map_err(|e| format!("Failed to write config: {}", e)) } - #[tauri::command] pub fn start_sync(config: RcloneConfig, state: tauri::State) -> Result<(), String> { let local_path = Path::new(&config.local_path); if !local_path.exists() { create_dir_all(local_path).map_err(|e| format!("Failed to create local path: {}", e))?; } - let child = Command::new("rclone") .arg("sync") .arg(&config.remote_path) @@ -70,12 +61,10 @@ pub fn start_sync(config: RcloneConfig, state: tauri::State) -> Result .stderr(Stdio::null()) .spawn() .map_err(|e| format!("Failed to start rclone: {}", e))?; - state.sync_processes.lock().unwrap().push(child); *state.sync_active.lock().unwrap() = true; Ok(()) } - #[tauri::command] pub fn stop_sync(state: tauri::State) -> Result<(), String> { let mut processes = state.sync_processes.lock().unwrap(); @@ -86,7 +75,6 @@ pub fn stop_sync(state: tauri::State) -> Result<(), String> { *state.sync_active.lock().unwrap() = false; Ok(()) } - #[tauri::command] pub fn get_status(remote_name: String) -> Result { let output = Command::new("rclone") @@ -95,19 +83,15 @@ pub fn get_status(remote_name: String) -> Result { .arg("--json") .output() .map_err(|e| format!("Failed to execute rclone rc: {}", e))?; - if !output.status.success() { return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr))); } - let json = String::from_utf8_lossy(&output.stdout); let value: serde_json::Value = serde_json::from_str(&json) .map_err(|e| format!("Failed to parse rclone status: {}", e))?; - let transferred = value.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0); let errors = value.get("errors").and_then(|v| v.as_u64()).unwrap_or(0); let speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0); - let status = if errors > 0 { "Error occurred".to_string() } else if speed > 0.0 { @@ -117,7 +101,6 @@ pub fn get_status(remote_name: String) -> Result { } else { "Initializing".to_string() }; - Ok(SyncStatus { name: remote_name, status, @@ -127,12 +110,10 @@ pub fn get_status(remote_name: String) -> Result { last_updated: chrono::Local::now().format("%H:%M:%S").to_string(), }) } - pub fn format_bytes(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; const GB: u64 = MB * 1024; - if bytes >= GB { format!("{:.2} GB", bytes as f64 / GB as f64) } else if bytes >= MB { diff --git a/src/ui/ui.test.rs b/src/ui/ui.test.rs index eafc6a7c0..f50492c6c 100644 --- a/src/ui/ui.test.rs +++ b/src/ui/ui.test.rs @@ -1,22 +1,17 @@ -//! Tests for UI module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_ui_module() { test_util::setup(); assert!(true, "Basic UI module test"); } - #[test] fn test_drive_ui() { test_util::setup(); assert!(true, "Drive UI placeholder test"); } - #[test] fn test_sync_ui() { test_util::setup(); diff --git a/src/ui_tree/chat_panel.rs b/src/ui_tree/chat_panel.rs index e54e929da..67c5bbaa2 100644 --- a/src/ui_tree/chat_panel.rs +++ b/src/ui_tree/chat_panel.rs @@ -4,7 +4,6 @@ use crate::shared::state::AppState; use crate::shared::models::BotResponse; use tokio::sync::mpsc; use uuid::Uuid; - pub struct ChatPanel { pub messages: Vec, pub input_buffer: String, @@ -12,7 +11,6 @@ pub struct ChatPanel { pub user_id: Uuid, pub response_rx: Option>, } - impl ChatPanel { pub fn new(_app_state: Arc) -> Self { Self { @@ -23,26 +21,20 @@ impl ChatPanel { response_rx: None, } } - pub fn add_char(&mut self, c: char) { self.input_buffer.push(c); } - pub fn backspace(&mut self) { self.input_buffer.pop(); } - pub async fn send_message(&mut self, bot_name: &str, app_state: &Arc) -> Result<()> { if self.input_buffer.trim().is_empty() { return Ok(()); } - let message = self.input_buffer.clone(); self.messages.push(format!("You: {}", message)); self.input_buffer.clear(); - let bot_id = self.get_bot_id(bot_name, app_state).await?; - let user_message = crate::shared::models::UserMessage { bot_id: bot_id.to_string(), user_id: self.user_id.to_string(), @@ -54,16 +46,12 @@ impl ChatPanel { timestamp: chrono::Utc::now(), context_name: None, }; - let (tx, rx) = mpsc::channel::(100); self.response_rx = Some(rx); - let orchestrator = crate::bot::BotOrchestrator::new(app_state.clone()); let _ = orchestrator.stream_response(user_message, tx).await; - Ok(()) } - pub async fn poll_response(&mut self, _bot_name: &str) -> Result<()> { if let Some(rx) = &mut self.response_rx { while let Ok(response) = rx.try_recv() { @@ -78,7 +66,6 @@ let _ = orchestrator.stream_response(user_message, tx).await; self.messages.push(format!("Bot: {}", response.content)); } } - if response.is_complete && response.content.is_empty() { break; } @@ -86,34 +73,27 @@ let _ = orchestrator.stream_response(user_message, tx).await; } Ok(()) } - async fn get_bot_id(&self, bot_name: &str, app_state: &Arc) -> Result { use crate::shared::models::schema::bots::dsl::*; use diesel::prelude::*; - let mut conn = app_state.conn.get().unwrap(); let bot_id = bots .filter(name.eq(bot_name)) .select(id) .first::(&mut *conn)?; - Ok(bot_id) } - pub fn render(&self) -> String { let mut lines = Vec::new(); - lines.push("╔═══════════════════════════════════════╗".to_string()); lines.push("║ CONVERSATION ║".to_string()); lines.push("╚═══════════════════════════════════════╝".to_string()); lines.push("".to_string()); - let visible_start = if self.messages.len() > 15 { self.messages.len() - 15 } else { 0 }; - for msg in &self.messages[visible_start..] { if msg.starts_with("You: ") { lines.push(format!(" {}", msg)); @@ -123,13 +103,11 @@ let _ = orchestrator.stream_response(user_message, tx).await; lines.push(format!(" {}", msg)); } } - lines.push("".to_string()); lines.push("─────────────────────────────────────────".to_string()); lines.push(format!(" > {}_", self.input_buffer)); lines.push("".to_string()); lines.push(" Enter: Send | Tab: Switch Panel".to_string()); - lines.join("\n") } } diff --git a/src/ui_tree/editor.rs b/src/ui_tree/editor.rs index 2b541a728..86a663838 100644 --- a/src/ui_tree/editor.rs +++ b/src/ui_tree/editor.rs @@ -1,7 +1,6 @@ use color_eyre::Result; use std::sync::Arc; use crate::shared::state::AppState; - pub struct Editor { file_path: String, bucket: String, @@ -11,7 +10,6 @@ pub struct Editor { scroll_offset: usize, modified: bool, } - impl Editor { pub async fn load(app_state: &Arc, bucket: &str, path: &str) -> Result { let content = if let Some(drive) = &app_state.drive { @@ -25,7 +23,6 @@ impl Editor { } else { String::new() }; - Ok(Self { file_path: format!("{}/{}", bucket, path), bucket: bucket.to_string(), @@ -36,7 +33,6 @@ impl Editor { modified: false, }) } - pub async fn save(&mut self, app_state: &Arc) -> Result<()> { if let Some(drive) = &app_state.drive { drive.put_object() @@ -49,11 +45,9 @@ impl Editor { } Ok(()) } - pub fn file_path(&self) -> &str { &self.file_path } - pub fn render(&self, cursor_blink: bool) -> String { let lines: Vec<&str> = self.content.lines().collect(); let total_lines = lines.len().max(1); @@ -64,41 +58,33 @@ impl Editor { .last() .map(|line| line.len()) .unwrap_or(0); - let start = self.scroll_offset; let end = (start + visible_lines).min(total_lines); - let mut display_lines = Vec::new(); for i in start..end { let line_num = i + 1; let line_content = if i < lines.len() { lines[i] } else { "" }; let is_cursor_line = i == cursor_line; - let cursor_indicator = if is_cursor_line && cursor_blink { let spaces = " ".repeat(cursor_col); format!("{}█", spaces) } else { String::new() }; - display_lines.push(format!(" {:4} │ {}{}", line_num, line_content, cursor_indicator)); } - if display_lines.is_empty() { let cursor_indicator = if cursor_blink { "█" } else { "" }; display_lines.push(format!(" 1 │ {}", cursor_indicator)); } - display_lines.push("".to_string()); display_lines.push("─────────────────────────────────────────────────────────────".to_string()); let status = if self.modified { "MODIFIED" } else { "SAVED" }; display_lines.push(format!(" {} {} │ Line: {}, Col: {}", status, self.file_path, cursor_line + 1, cursor_col + 1)); display_lines.push(" Ctrl+S: Save │ Ctrl+W: Close │ Esc: Close without saving".to_string()); - display_lines.join("\n") } - pub fn move_up(&mut self) { if let Some(prev_line_end) = self.content[..self.cursor_pos].rfind('\n') { if let Some(prev_prev_line_end) = self.content[..prev_line_end].rfind('\n') { @@ -111,7 +97,6 @@ impl Editor { } } } - pub fn move_down(&mut self) { if let Some(next_line_start) = self.content[self.cursor_pos..].find('\n') { let current_line_start = self.content[..self.cursor_pos].rfind('\n').map(|pos| pos + 1).unwrap_or(0); @@ -127,25 +112,21 @@ impl Editor { } } } - pub fn move_left(&mut self) { if self.cursor_pos > 0 { self.cursor_pos -= 1; } } - pub fn move_right(&mut self) { if self.cursor_pos < self.content.len() { self.cursor_pos += 1; } } - pub fn insert_char(&mut self, c: char) { self.modified = true; self.content.insert(self.cursor_pos, c); self.cursor_pos += 1; } - pub fn backspace(&mut self) { if self.cursor_pos > 0 { self.modified = true; @@ -153,7 +134,6 @@ impl Editor { self.cursor_pos -= 1; } } - pub fn insert_newline(&mut self) { self.modified = true; self.content.insert(self.cursor_pos, '\n'); diff --git a/src/ui_tree/file_tree.rs b/src/ui_tree/file_tree.rs index 296e697e0..c67b191f9 100644 --- a/src/ui_tree/file_tree.rs +++ b/src/ui_tree/file_tree.rs @@ -1,14 +1,12 @@ use color_eyre::Result; use std::sync::Arc; use crate::shared::state::AppState; - #[derive(Debug, Clone)] pub enum TreeNode { Bucket { name: String }, Folder { bucket: String, path: String }, File { bucket: String, path: String }, } - pub struct FileTree { app_state: Arc, items: Vec<(String, TreeNode)>, @@ -16,7 +14,6 @@ pub struct FileTree { current_bucket: Option, current_path: Vec, } - impl FileTree { pub fn new(app_state: Arc) -> Self { Self { @@ -27,12 +24,10 @@ impl FileTree { current_path: Vec::new(), } } - pub async fn load_root(&mut self) -> Result<()> { self.items.clear(); self.current_bucket = None; self.current_path.clear(); - if let Some(drive) = &self.app_state.drive { let result = drive.list_buckets().send().await; match result { @@ -53,27 +48,23 @@ impl FileTree { } else { self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() })); } - if self.items.is_empty() { self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() })); } self.selected = 0; Ok(()) } - pub async fn enter_bucket(&mut self, bucket: String) -> Result<()> { self.current_bucket = Some(bucket.clone()); self.current_path.clear(); self.load_bucket_contents(&bucket, "").await } - pub async fn enter_folder(&mut self, bucket: String, path: String) -> Result<()> { self.current_bucket = Some(bucket.clone()); let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect(); self.current_path = parts.iter().map(|s| s.to_string()).collect(); self.load_bucket_contents(&bucket, &path).await } - pub fn go_up(&mut self) -> bool { if self.current_path.is_empty() { if self.current_bucket.is_some() { @@ -85,7 +76,6 @@ impl FileTree { self.current_path.pop(); true } - pub async fn refresh_current(&mut self) -> Result<()> { if let Some(bucket) = &self.current_bucket.clone() { let path = self.current_path.join("/"); @@ -94,14 +84,12 @@ impl FileTree { self.load_root().await } } - async fn load_bucket_contents(&mut self, bucket: &str, prefix: &str) -> Result<()> { self.items.clear(); self.items.push(("⬆️ .. (go back)".to_string(), TreeNode::Folder { bucket: bucket.to_string(), path: "..".to_string(), })); - if let Some(drive) = &self.app_state.drive { let normalized_prefix = if prefix.is_empty() { String::new() @@ -110,10 +98,8 @@ impl FileTree { } else { format!("{}/", prefix) }; - let mut continuation_token = None; let mut all_keys = Vec::new(); - loop { let mut request = drive.list_objects_v2().bucket(bucket); if !normalized_prefix.is_empty() { @@ -122,39 +108,31 @@ impl FileTree { if let Some(token) = continuation_token { request = request.continuation_token(token); } - let result = request.send().await?; - for obj in result.contents() { if let Some(key) = obj.key() { all_keys.push(key.to_string()); } } - if !result.is_truncated.unwrap_or(false) { break; } continuation_token = result.next_continuation_token; } - let mut folders = std::collections::HashSet::new(); let mut files = Vec::new(); - for key in all_keys { if key == normalized_prefix { continue; } - let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) { &key[normalized_prefix.len()..] } else { &key }; - if relative.is_empty() { continue; } - if let Some(slash_pos) = relative.find('/') { let folder_name = &relative[..slash_pos]; if !folder_name.is_empty() { @@ -164,7 +142,6 @@ impl FileTree { files.push((relative.to_string(), key.clone())); } } - let mut folder_vec: Vec = folders.into_iter().collect(); folder_vec.sort(); for folder_name in folder_vec { @@ -179,7 +156,6 @@ impl FileTree { path: full_path, })); } - files.sort_by(|(a, _), (b, _)| a.cmp(b)); for (name, full_path) in files { let icon = if name.ends_with(".bas") { @@ -202,37 +178,30 @@ impl FileTree { })); } } - if self.items.len() == 1 { self.items.push(("(empty folder)".to_string(), TreeNode::Folder { bucket: bucket.to_string(), path: String::new(), })); } - self.selected = 0; Ok(()) } - pub fn render_items(&self) -> &[(String, TreeNode)] { &self.items } - pub fn selected_index(&self) -> usize { self.selected } - pub fn get_selected_node(&self) -> Option<&TreeNode> { self.items.get(self.selected).map(|(_, node)| node) } - pub fn get_selected_bot(&self) -> Option { if let Some(bucket) = &self.current_bucket { if bucket.ends_with(".gbai") { return Some(bucket.trim_end_matches(".gbai").to_string()); } } - if let Some((_, node)) = self.items.get(self.selected) { match node { TreeNode::Bucket { name } => { @@ -243,16 +212,13 @@ impl FileTree { _ => {} } } - None } - pub fn move_up(&mut self) { if self.selected > 0 { self.selected -= 1; } } - pub fn move_down(&mut self) { if self.selected < self.items.len().saturating_sub(1) { self.selected += 1; diff --git a/src/ui_tree/log_panel.rs b/src/ui_tree/log_panel.rs index 719928e5e..cc0ebd161 100644 --- a/src/ui_tree/log_panel.rs +++ b/src/ui_tree/log_panel.rs @@ -1,12 +1,10 @@ use std::sync::{Arc, Mutex}; use log::{Log, Metadata, LevelFilter, Record, SetLoggerError}; use chrono::Local; - pub struct LogPanel { logs: Vec, max_logs: usize, } - impl LogPanel { pub fn new() -> Self { Self { @@ -14,14 +12,12 @@ impl LogPanel { max_logs: 1000, } } - pub fn add_log(&mut self, entry: &str) { if self.logs.len() >= self.max_logs { self.logs.remove(0); } self.logs.push(entry.to_string()); } - pub fn render(&self) -> String { let visible_logs = if self.logs.len() > 10 { &self.logs[self.logs.len() - 10..] @@ -31,17 +27,14 @@ impl LogPanel { visible_logs.join("\n") } } - pub struct UiLogger { log_panel: Arc>, filter: LevelFilter, } - impl Log for UiLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= self.filter } - fn log(&self, record: &Record) { if self.enabled(record.metadata()) { let timestamp = Local::now().format("%H:%M:%S"); @@ -58,10 +51,8 @@ impl Log for UiLogger { } } } - fn flush(&self) {} } - pub fn init_logger(log_panel: Arc>) -> Result<(), SetLoggerError> { let logger = Box::new(UiLogger { log_panel, diff --git a/src/ui_tree/mod.rs b/src/ui_tree/mod.rs index e6e8078f8..27eb92fe7 100644 --- a/src/ui_tree/mod.rs +++ b/src/ui_tree/mod.rs @@ -27,7 +27,6 @@ use file_tree::{FileTree, TreeNode}; use log_panel::{init_logger, LogPanel}; use status_panel::StatusPanel; use chat_panel::ChatPanel; - pub struct XtreeUI { app_state: Option>, file_tree: Option, @@ -40,7 +39,6 @@ should_quit: bool, progress_channel: Option>>>, bootstrap_status: String, } - #[derive(Debug, Clone, Copy, PartialEq)] enum ActivePanel { FileTree, @@ -49,7 +47,6 @@ Status, Logs, Chat, } - impl XtreeUI { pub fn new() -> Self { let log_panel = Arc::new(Mutex::new(LogPanel::new())); @@ -66,11 +63,9 @@ progress_channel: None, bootstrap_status: "Initializing...".to_string(), } } - pub fn set_progress_channel(&mut self, rx: Arc>>) { self.progress_channel = Some(rx); } - pub fn set_app_state(&mut self, app_state: Arc) { self.file_tree = Some(FileTree::new(app_state.clone())); self.status_panel = Some(StatusPanel::new(app_state.clone())); @@ -79,7 +74,6 @@ self.app_state = Some(app_state); self.active_panel = ActivePanel::FileTree; self.bootstrap_status = "Ready".to_string(); } - pub fn start_ui(&mut self) -> Result<()> { color_eyre::install()?; if !std::io::IsTerminal::is_terminal(&std::io::stdout()) { @@ -98,13 +92,12 @@ execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; result } - fn run_event_loop(&mut self, terminal: &mut Terminal>) -> Result<()> { let mut last_update = std::time::Instant::now(); let update_interval = std::time::Duration::from_millis(1000); let mut cursor_blink = false; let mut last_blink = std::time::Instant::now(); -let rt = tokio::runtime::Runtime::new()?; // create runtime once +let rt = tokio::runtime::Runtime::new()?; loop { if let Some(ref progress_rx) = self.progress_channel { if let Ok(mut rx) = progress_rx.try_lock() { @@ -148,7 +141,6 @@ break; } Ok(()) } - fn render(&mut self, f: &mut Frame, cursor_blink: bool) { let bg = Color::Rgb(0, 30, 100); let border_active = Color::Rgb(85, 255, 255); @@ -195,7 +187,6 @@ self.render_chat(f, content_chunks[2], bg, text, border_active, border_inactive, } self.render_logs(f, main_chunks[2], bg, text, border_active, border_inactive, highlight, title_bg, title_fg); } - fn render_header(&self, f: &mut Frame, area: Rect, _bg: Color, title_bg: Color, title_fg: Color) { let block = Block::default() .style(Style::default().bg(title_bg)); @@ -242,7 +233,6 @@ height: 1, } ); } - fn render_loading(&self, f: &mut Frame, bg: Color, text: Color, border: Color, title_bg: Color, title_fg: Color) { let chunks = Layout::default() .direction(Direction::Vertical) @@ -267,7 +257,6 @@ let paragraph = Paragraph::new(loading_text) .wrap(Wrap { trim: false }); f.render_widget(paragraph, center); } - fn render_file_tree(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, highlight: Color, title_bg: Color, title_fg: Color) { if let Some(file_tree) = &self.file_tree { let items = file_tree.render_items(); @@ -296,7 +285,6 @@ let list = List::new(list_items).block(block); f.render_widget(list, area); } } - fn render_status(&mut self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color) { let selected_bot_opt = self.file_tree.as_ref().and_then(|ft| ft.get_selected_bot()); let status_text = if let Some(status_panel) = &mut self.status_panel { @@ -325,7 +313,6 @@ let paragraph = Paragraph::new(status_text) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } - fn render_editor(&self, f: &mut Frame, area: Rect, editor: &Editor, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color, cursor_blink: bool) { let is_active = self.active_panel == ActivePanel::Editor; let border_color = if is_active { border_active } else { border_inactive }; @@ -347,7 +334,6 @@ let paragraph = Paragraph::new(content) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } - fn render_chat(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color) { if let Some(chat_panel) = &self.chat_panel { let is_active = self.active_panel == ActivePanel::Chat; @@ -376,7 +362,6 @@ let paragraph = Paragraph::new(content) f.render_widget(paragraph, area); } } - fn render_logs(&self, f: &mut Frame, area: Rect, bg: Color, text: Color, border_active: Color, border_inactive: Color, _highlight: Color, title_bg: Color, title_fg: Color) { let log_panel = self.log_panel.try_lock(); let log_lines = if let Ok(panel) = log_panel { @@ -402,7 +387,6 @@ let paragraph = Paragraph::new(log_lines) .wrap(Wrap { trim: false }); f.render_widget(paragraph, area); } - async fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result<()> { if modifiers.contains(KeyModifiers::CONTROL) { match key { @@ -550,7 +534,6 @@ _ => {} } Ok(()) } - async fn handle_tree_enter(&mut self) -> Result<()> { if let (Some(file_tree), Some(app_state)) = (&mut self.file_tree, &self.app_state) { if let Some(node) = file_tree.get_selected_node().cloned() { @@ -584,7 +567,6 @@ log_panel.add_log(&format!("Failed to load file: {}", e)); } Ok(()) } - async fn update_data(&mut self) -> Result<()> { if let Some(status_panel) = &mut self.status_panel { status_panel.update().await?; diff --git a/src/ui_tree/status_panel.rs b/src/ui_tree/status_panel.rs index a71c6cbcc..582c47778 100644 --- a/src/ui_tree/status_panel.rs +++ b/src/ui_tree/status_panel.rs @@ -5,14 +5,12 @@ use crate::shared::state::AppState; use diesel::prelude::*; use std::sync::Arc; use sysinfo::System; - pub struct StatusPanel { app_state: Arc, last_update: std::time::Instant, cached_content: String, system: System, } - impl StatusPanel { pub fn new(app_state: Arc) -> Self { Self { @@ -22,47 +20,37 @@ impl StatusPanel { system: System::new_all(), } } - pub async fn update(&mut self) -> Result<(), std::io::Error> { if self.last_update.elapsed() < std::time::Duration::from_secs(1) { return Ok(()); } - self.system.refresh_all(); - self.cached_content = String::new(); self.last_update = std::time::Instant::now(); Ok(()) } - pub fn render(&mut self, selected_bot: Option) -> String { let mut lines = Vec::new(); - self.system.refresh_all(); - lines.push("╔═══════════════════════════════════════╗".to_string()); lines.push("║ SYSTEM METRICS ║".to_string()); lines.push("╚═══════════════════════════════════════╝".to_string()); lines.push("".to_string()); - let system_metrics = match nvidia::get_system_metrics(0, 0) { Ok(metrics) => metrics, Err(_) => nvidia::SystemMetrics::default(), }; - let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20); lines.push(format!( " CPU: {:5.1}% {}", system_metrics.cpu_usage, cpu_bar )); - if let Some(gpu_usage) = system_metrics.gpu_usage { let gpu_bar = Self::create_progress_bar(gpu_usage, 20); lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar)); } else { lines.push(" GPU: Not available".to_string()); } - let total_mem = self.system.total_memory() as f32 / 1024.0 / 1024.0 / 1024.0; let used_mem = self.system.used_memory() as f32 / 1024.0 / 1024.0 / 1024.0; let mem_percentage = (used_mem / total_mem) * 100.0; @@ -71,20 +59,17 @@ impl StatusPanel { " MEM: {:5.1}% {} ({:.1}/{:.1} GB)", mem_percentage, mem_bar, used_mem, total_mem )); - lines.push("".to_string()); lines.push("╔═══════════════════════════════════════╗".to_string()); lines.push("║ COMPONENTS STATUS ║".to_string()); lines.push("╚═══════════════════════════════════════╝".to_string()); lines.push("".to_string()); - let components = vec![ ("Tables", "postgres", "5432"), ("Cache", "valkey-server", "6379"), ("Drive", "minio", "9000"), ("LLM", "llama-server", "8081"), ]; - for (comp_name, process, port) in components { let status = if Self::check_component_running(process) { format!("🟢 ONLINE [Port: {}]", port) @@ -93,13 +78,11 @@ impl StatusPanel { }; lines.push(format!(" {:<10} {}", comp_name, status)); } - lines.push("".to_string()); lines.push("╔═══════════════════════════════════════╗".to_string()); lines.push("║ ACTIVE BOTS ║".to_string()); lines.push("╚═══════════════════════════════════════╝".to_string()); lines.push("".to_string()); - if let Ok(mut conn) = self.app_state.conn.get() { match bots .filter(is_active.eq(true)) @@ -121,30 +104,24 @@ impl StatusPanel { " " }; lines.push(format!(" {} 🤖 {}", marker, bot_name)); - if let Some(ref selected) = selected_bot { if selected == &bot_name { lines.push("".to_string()); lines.push(" ┌─ Bot Configuration ─────────┐".to_string()); - let config_manager = ConfigManager::new(self.app_state.conn.clone()); - let llm_model = config_manager .get_config(&bot_id, "llm-model", None) .unwrap_or_else(|_| "N/A".to_string()); lines.push(format!(" Model: {}", llm_model)); - let ctx_size = config_manager .get_config(&bot_id, "llm-server-ctx-size", None) .unwrap_or_else(|_| "N/A".to_string()); lines.push(format!(" Context: {}", ctx_size)); - let temp = config_manager .get_config(&bot_id, "llm-temperature", None) .unwrap_or_else(|_| "N/A".to_string()); lines.push(format!(" Temp: {}", temp)); - lines.push(" └─────────────────────────────┘".to_string()); } } @@ -158,12 +135,10 @@ impl StatusPanel { } else { lines.push(" Database locked".to_string()); } - lines.push("".to_string()); lines.push("╔═══════════════════════════════════════╗".to_string()); lines.push("║ SESSIONS ║".to_string()); lines.push("╚═══════════════════════════════════════╝".to_string()); - let session_count = self .app_state .response_channels @@ -171,10 +146,8 @@ impl StatusPanel { .map(|channels| channels.len()) .unwrap_or(0); lines.push(format!(" Active Sessions: {}", session_count)); - lines.join("\n") } - fn create_progress_bar(percentage: f32, width: usize) -> String { let filled = (percentage / 100.0 * width as f32).round() as usize; let empty = width.saturating_sub(filled); @@ -182,7 +155,6 @@ impl StatusPanel { let empty_chars = "░".repeat(empty); format!("[{}{}]", filled_chars, empty_chars) } - pub fn check_component_running(process_name: &str) -> bool { std::process::Command::new("pgrep") .arg("-f") diff --git a/src/web_server/mod.rs b/src/web_server/mod.rs index a9a9fedf5..14629a2f8 100644 --- a/src/web_server/mod.rs +++ b/src/web_server/mod.rs @@ -1,7 +1,6 @@ use actix_web::{HttpRequest, HttpResponse, Result}; use log::{debug, error, warn}; use std::fs; - #[actix_web::get("/")] async fn index() -> Result { match fs::read_to_string("web/html/index.html") { @@ -12,7 +11,6 @@ async fn index() -> Result { } } } - #[actix_web::get("/{botname}")] async fn bot_index(req: HttpRequest) -> Result { let botname = req.match_info().query("botname"); @@ -25,7 +23,6 @@ async fn bot_index(req: HttpRequest) -> Result { } } } - #[actix_web::get("/static/{filename:.*}")] async fn static_files(req: HttpRequest) -> Result { let filename = req.match_info().query("filename"); diff --git a/src/web_server/web_server.test.rs b/src/web_server/web_server.test.rs index 19421728b..54dcb39fa 100644 --- a/src/web_server/web_server.test.rs +++ b/src/web_server/web_server.test.rs @@ -1,16 +1,12 @@ -//! Tests for web server module - #[cfg(test)] mod tests { use super::*; use crate::tests::test_util; - #[test] fn test_web_server_module() { test_util::setup(); assert!(true, "Basic web server module test"); } - #[test] fn test_server_routes() { test_util::setup(); diff --git a/templates/default.gbai/default.gbot/config.csv b/templates/default.gbai/default.gbot/config.csv index 67abe9bd3..5982c2830 100644 --- a/templates/default.gbai/default.gbot/config.csv +++ b/templates/default.gbai/default.gbot/config.csv @@ -8,6 +8,8 @@ llm-key,none llm-url,http://localhost:8081 llm-model,../../../../data/llm/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf +prompt-compact,4 + mcp-server,false embedding-url,http://localhost:8082