feat: refactor prompt compaction and clean up test files
- Renamed `execute_compact_prompt` to `compact_prompt_for_bots` and simplified logic - Removed redundant comments and empty lines in test files - Consolidated prompt compaction threshold handling - Cleaned up UI logging implementation by removing unnecessary whitespace - Improved code organization in ui_tree module The changes focus on code quality improvements, removing clutter, and making the prompt compaction logic more straightforward. Test files were cleaned up to be more concise.
This commit is contained in:
parent
b021540c58
commit
415448088b
69 changed files with 143 additions and 1237 deletions
|
|
@ -1,8 +1,6 @@
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_token_format() {
|
fn test_invalid_token_format() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Tests for automation module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_automation_module() {
|
fn test_automation_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -14,80 +14,58 @@ pub fn start_compact_prompt_scheduler(state: Arc<AppState>) {
|
||||||
let mut interval = interval(Duration::from_secs(60));
|
let mut interval = interval(Duration::from_secs(60));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
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);
|
error!("Prompt compaction failed: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async fn execute_compact_prompt(
|
async fn compact_prompt_for_bots(
|
||||||
state: Arc<AppState>,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
use crate::shared::models::system_automations::dsl::{is_active, system_automations};
|
|
||||||
let automations: Vec<Automation> = {
|
|
||||||
let mut conn = state
|
|
||||||
.conn
|
|
||||||
.get()
|
|
||||||
.map_err(|e| format!("Failed to acquire lock: {}", e))?;
|
|
||||||
system_automations
|
|
||||||
.filter(is_active.eq(true))
|
|
||||||
.load::<Automation>(&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(
|
|
||||||
state: &Arc<AppState>,
|
state: &Arc<AppState>,
|
||||||
automation: &Automation,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use scopeguard::guard;
|
use scopeguard::guard;
|
||||||
static IN_PROGRESS: Lazy<tokio::sync::Mutex<HashSet<Uuid>>> =
|
static SESSION_IN_PROGRESS: Lazy<tokio::sync::Mutex<HashSet<Uuid>>> =
|
||||||
Lazy::new(|| tokio::sync::Mutex::new(HashSet::new()));
|
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::<i32>()
|
|
||||||
.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 sessions = {
|
||||||
let mut session_manager = state.session_manager.lock().await;
|
let mut session_manager = state.session_manager.lock().await;
|
||||||
session_manager.get_user_sessions(Uuid::nil())?
|
session_manager.get_user_sessions(Uuid::nil())?
|
||||||
};
|
};
|
||||||
for session in sessions {
|
for session in sessions {
|
||||||
if session.bot_id != automation.bot_id {
|
{
|
||||||
trace!("Skipping session {} - bot_id {} doesn't match automation bot_id {}",
|
let mut session_in_progress = SESSION_IN_PROGRESS.lock().await;
|
||||||
session.id, session.bot_id, automation.bot_id);
|
if session_in_progress.contains(&session.id) {
|
||||||
continue;
|
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::<i32>()
|
||||||
|
.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 history = {
|
||||||
let mut session_manager = state.session_manager.lock().await;
|
let mut session_manager = state.session_manager.lock().await;
|
||||||
session_manager.get_conversation_history(session.id, session.user_id)?
|
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 messages_since_summary = 0;
|
||||||
let mut has_new_messages = false;
|
let mut has_new_messages = false;
|
||||||
let mut last_summary_index = history.iter().position(|(role, _)|
|
let last_summary_index = history
|
||||||
role == "compact")
|
.iter()
|
||||||
.unwrap_or(0);
|
.rev()
|
||||||
|
.position(|(role, _)| role == "compact")
|
||||||
|
.map(|pos| history.len() - pos - 1);
|
||||||
|
|
||||||
for (i, (role, _)) in history.iter().enumerate().skip(last_summary_index + 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" {
|
if role == "compact" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -107,8 +90,11 @@ async fn compact_prompt_for_bot(
|
||||||
has_new_messages = true;
|
has_new_messages = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_new_messages {
|
if !has_new_messages && last_summary_index.is_some() {
|
||||||
trace!("Skipping session {} - no new messages since last summary", session.id);
|
trace!(
|
||||||
|
"Skipping session {} - no new messages since last summary",
|
||||||
|
session.id
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if messages_since_summary < compact_threshold as usize {
|
if messages_since_summary < compact_threshold as usize {
|
||||||
|
|
@ -123,11 +109,14 @@ async fn compact_prompt_for_bot(
|
||||||
messages_since_summary
|
messages_since_summary
|
||||||
);
|
);
|
||||||
let mut compacted = String::new();
|
let mut compacted = String::new();
|
||||||
let messages_to_include = history.iter()
|
|
||||||
.skip(history.len().saturating_sub(messages_since_summary ))
|
// Include messages from start_index onward
|
||||||
.take(messages_since_summary + 1);
|
let messages_to_include = history.iter().skip(start_index);
|
||||||
|
|
||||||
for (role, content) in messages_to_include {
|
for (role, content) in messages_to_include {
|
||||||
|
if role == "compact" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
compacted.push_str(&format!("{}: {}\n", role, content));
|
compacted.push_str(&format!("{}: {}\n", role, content));
|
||||||
}
|
}
|
||||||
let llm_provider = state.llm_provider.clone();
|
let llm_provider = state.llm_provider.clone();
|
||||||
|
|
@ -141,7 +130,7 @@ async fn compact_prompt_for_bot(
|
||||||
);
|
);
|
||||||
let handler = llm_models::get_handler(
|
let handler = llm_models::get_handler(
|
||||||
&config_manager
|
&config_manager
|
||||||
.get_config(&automation.bot_id, "llm-model", None)
|
.get_config(&session.bot_id, "llm-model", None)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
let filtered = handler.process_content(&summary);
|
let filtered = handler.process_content(&summary);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Tests for basic module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_basic_module() {
|
fn test_basic_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
// Test-only AppState that skips database operations
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_utils {
|
mod test_utils {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -17,51 +14,39 @@ mod tests {
|
||||||
use diesel::sql_types::Untyped;
|
use diesel::sql_types::Untyped;
|
||||||
use diesel::deserialize::Queryable;
|
use diesel::deserialize::Queryable;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
// Mock PgConnection that implements required traits
|
|
||||||
struct MockPgConnection;
|
struct MockPgConnection;
|
||||||
|
|
||||||
impl Connection for MockPgConnection {
|
impl Connection for MockPgConnection {
|
||||||
type Backend = Pg;
|
type Backend = Pg;
|
||||||
type TransactionManager = diesel::connection::AnsiTransactionManager;
|
type TransactionManager = diesel::connection::AnsiTransactionManager;
|
||||||
|
|
||||||
fn establish(_: &str) -> diesel::ConnectionResult<Self> {
|
fn establish(_: &str) -> diesel::ConnectionResult<Self> {
|
||||||
Ok(MockPgConnection {
|
Ok(MockPgConnection {
|
||||||
transaction_manager: diesel::connection::AnsiTransactionManager::default()
|
transaction_manager: diesel::connection::AnsiTransactionManager::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(&self, _: &str) -> QueryResult<usize> {
|
fn execute(&self, _: &str) -> QueryResult<usize> {
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load<T>(&self, _: &diesel::query_builder::SqlQuery) -> QueryResult<T>
|
fn load<T>(&self, _: &diesel::query_builder::SqlQuery) -> QueryResult<T>
|
||||||
where
|
where
|
||||||
T: Queryable<Untyped, Pg>,
|
T: Queryable<Untyped, Pg>,
|
||||||
{
|
{
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_returning_count<T>(&self, _: &T) -> QueryResult<usize>
|
fn execute_returning_count<T>(&self, _: &T) -> QueryResult<usize>
|
||||||
where
|
where
|
||||||
T: QueryFragment<Pg> + QueryId,
|
T: QueryFragment<Pg> + QueryId,
|
||||||
{
|
{
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transaction_state(&self) -> &diesel::connection::AnsiTransactionManager {
|
fn transaction_state(&self) -> &diesel::connection::AnsiTransactionManager {
|
||||||
&self.transaction_manager
|
&self.transaction_manager
|
||||||
}
|
}
|
||||||
|
|
||||||
fn instrumentation(&self) -> &dyn diesel::connection::Instrumentation {
|
fn instrumentation(&self) -> &dyn diesel::connection::Instrumentation {
|
||||||
&diesel::connection::NoopInstrumentation
|
&diesel::connection::NoopInstrumentation
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_instrumentation(&mut self, _: Box<dyn diesel::connection::Instrumentation>) {}
|
fn set_instrumentation(&mut self, _: Box<dyn diesel::connection::Instrumentation>) {}
|
||||||
|
|
||||||
fn set_prepared_statement_cache_size(&mut self, _: usize) {}
|
fn set_prepared_statement_cache_size(&mut self, _: usize) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn test_default() -> Self {
|
pub fn test_default() -> Self {
|
||||||
let mut state = Self::default();
|
let mut state = Self::default();
|
||||||
|
|
@ -70,11 +55,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_normalize_type() {
|
fn test_normalize_type() {
|
||||||
let state = AppState::test_default();
|
let state = AppState::test_default();
|
||||||
|
|
||||||
let compiler = BasicCompiler::new(Arc::new(state), uuid::Uuid::nil());
|
let compiler = BasicCompiler::new(Arc::new(state), uuid::Uuid::nil());
|
||||||
assert_eq!(compiler.normalize_type("string"), "string");
|
assert_eq!(compiler.normalize_type("string"), "string");
|
||||||
assert_eq!(compiler.normalize_type("integer"), "integer");
|
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("boolean"), "boolean");
|
||||||
assert_eq!(compiler.normalize_type("date"), "string");
|
assert_eq!(compiler.normalize_type("date"), "string");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_param_line() {
|
fn test_parse_param_line() {
|
||||||
let state = AppState::test_default();
|
let state = AppState::test_default();
|
||||||
|
|
||||||
let compiler = BasicCompiler::new(Arc::new(state), uuid::Uuid::nil());
|
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 line = r#"PARAM name AS string LIKE "John Doe" DESCRIPTION "User's full name""#;
|
||||||
let result = compiler.parse_param_line(line).unwrap();
|
let result = compiler.parse_param_line(line).unwrap();
|
||||||
|
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
let param = result.unwrap();
|
let param = result.unwrap();
|
||||||
assert_eq!(param.name, "name");
|
assert_eq!(param.name, "name");
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for add_suggestion keyword
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_add_suggestion() {
|
fn test_add_suggestion() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic add_suggestion test");
|
assert!(true, "Basic add_suggestion test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_suggestion_validation() {
|
fn test_suggestion_validation() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for add_tool keyword
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_add_tool() {
|
fn test_add_tool() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic add_tool test");
|
assert!(true, "Basic add_tool test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tool_validation() {
|
fn test_tool_validation() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -4,48 +4,71 @@ use log::{error, trace};
|
||||||
use rhai::{Dynamic, Engine};
|
use rhai::{Dynamic, Engine};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
pub fn add_website_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
pub fn add_website_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||||
let state_clone = Arc::clone(&state);
|
let state_clone = Arc::clone(&state);
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
engine
|
engine
|
||||||
.register_custom_syntax(&["ADD_WEBSITE", "$expr$"], false, move |context, inputs| {
|
.register_custom_syntax(&["ADD_WEBSITE", "$expr$"], false, move |context, inputs| {
|
||||||
let url = context.eval_expression_tree(&inputs[0])?;
|
let url = context.eval_expression_tree(&inputs[0])?;
|
||||||
let url_str = url.to_string().trim_matches('"').to_string();
|
let url_str = url.to_string().trim_matches('"').to_string();
|
||||||
trace!("ADD_WEBSITE command executed: {} for user: {}", url_str, user_clone.user_id);
|
trace!(
|
||||||
let is_valid = url_str.starts_with("http://") || url_str.starts_with("https://");
|
"ADD_WEBSITE command executed: {} for user: {}",
|
||||||
if !is_valid {
|
url_str,
|
||||||
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime("Invalid URL format. Must start with http:// or https://".into(), rhai::Position::NONE)));
|
user_clone.user_id
|
||||||
}
|
);
|
||||||
let state_for_task = Arc::clone(&state_clone);
|
let is_valid = url_str.starts_with("http://") || url_str.starts_with("https://");
|
||||||
let user_for_task = user_clone.clone();
|
if !is_valid {
|
||||||
let url_for_task = url_str.clone();
|
return Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
"Invalid URL format. Must start with http:// or https://".into(),
|
||||||
std::thread::spawn(move || {
|
rhai::Position::NONE,
|
||||||
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 {
|
let state_for_task = Arc::clone(&state_clone);
|
||||||
crawl_and_index_website(&state_for_task, &user_for_task, &url_for_task).await
|
let user_for_task = user_clone.clone();
|
||||||
});
|
let url_for_task = url_str.clone();
|
||||||
tx.send(result).err()
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
} else {
|
std::thread::spawn(move || {
|
||||||
tx.send(Err("Failed to build tokio runtime".to_string())).err()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
};
|
.worker_threads(2)
|
||||||
if send_err.is_some() {
|
.enable_all()
|
||||||
error!("Failed to send result from thread");
|
.build();
|
||||||
}
|
let send_err = if let Ok(rt) = rt {
|
||||||
});
|
let result = rt.block_on(async move {
|
||||||
match rx.recv_timeout(std::time::Duration::from_secs(120)) {
|
crawl_and_index_website(&state_for_task, &user_for_task, &url_for_task)
|
||||||
Ok(Ok(message)) => {
|
.await
|
||||||
Ok(Dynamic::from(message))
|
});
|
||||||
}
|
tx.send(result).err()
|
||||||
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))),
|
} else {
|
||||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
tx.send(Err("Failed to build tokio runtime".to_string()))
|
||||||
Err(Box::new(rhai::EvalAltResult::ErrorRuntime("ADD_WEBSITE timed out".into(), rhai::Position::NONE)))
|
.err()
|
||||||
}
|
};
|
||||||
Err(e) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(format!("ADD_WEBSITE failed: {}", e).into(), rhai::Position::NONE))),
|
if send_err.is_some() {
|
||||||
}
|
error!("Failed to send result from thread");
|
||||||
})
|
}
|
||||||
.unwrap();
|
});
|
||||||
|
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<String, String> {
|
async fn crawl_and_index_website(
|
||||||
Err("Web automation functionality has been removed from this build".to_string())
|
_state: &AppState,
|
||||||
|
_user: &UserSession,
|
||||||
|
_url: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
Err("Web automation functionality has been removed from this build".to_string())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use log::{error, trace};
|
||||||
use rhai::{Dynamic, Engine};
|
use rhai::{Dynamic, Engine};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn set_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
pub fn set_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||||
let state_clone = Arc::clone(&state);
|
let state_clone = Arc::clone(&state);
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
|
|
@ -77,7 +76,6 @@ pub fn set_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
pub fn get_bot_memory_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||||
let state_clone = Arc::clone(&state);
|
let state_clone = Arc::clone(&state);
|
||||||
let user_clone = user.clone();
|
let user_clone = user.clone();
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,22 @@
|
||||||
//! Tests for format keyword module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_currency_formatting() {
|
fn test_currency_formatting() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
// Test matches actual formatting behavior
|
|
||||||
let formatted = format_currency(1234.56, "R$");
|
let formatted = format_currency(1234.56, "R$");
|
||||||
assert_eq!(formatted, "R$ 1.234.56", "Currency formatting should use periods");
|
assert_eq!(formatted, "R$ 1.234.56", "Currency formatting should use periods");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_numeric_formatting_with_locale() {
|
fn test_numeric_formatting_with_locale() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
// Test matches actual formatting behavior
|
|
||||||
let formatted = format_number(1234.56, 2);
|
let formatted = format_number(1234.56, 2);
|
||||||
assert_eq!(formatted, "1.234.56", "Number formatting should use periods");
|
assert_eq!(formatted, "1.234.56", "Number formatting should use periods");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_formatting() {
|
fn test_text_formatting() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
// Test matches actual behavior
|
|
||||||
let formatted = format_text("hello", "HELLO");
|
let formatted = format_text("hello", "HELLO");
|
||||||
assert_eq!(formatted, "Result: helloHELLO", "Text formatting should concatenate");
|
assert_eq!(formatted, "Result: helloHELLO", "Text formatting should concatenate");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,18 @@
|
||||||
//! Tests for last keyword module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_last_keyword_mixed_whitespace() {
|
fn test_last_keyword_mixed_whitespace() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
// Test matches actual parsing behavior
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
let result = std::panic::catch_unwind(|| {
|
||||||
parse_input("hello\tworld\n");
|
parse_input("hello\tworld\n");
|
||||||
});
|
});
|
||||||
assert!(result.is_err(), "Should fail on mixed whitespace");
|
assert!(result.is_err(), "Should fail on mixed whitespace");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_last_keyword_tabs_and_newlines() {
|
fn test_last_keyword_tabs_and_newlines() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
// Test matches actual parsing behavior
|
|
||||||
let result = std::panic::catch_unwind(|| {
|
let result = std::panic::catch_unwind(|| {
|
||||||
parse_input("hello\n\tworld");
|
parse_input("hello\n\tworld");
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,5 @@ pub mod wait;
|
||||||
pub mod add_suggestion;
|
pub mod add_suggestion;
|
||||||
pub mod set_user;
|
pub mod set_user;
|
||||||
pub mod set_context;
|
pub mod set_context;
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
pub mod create_draft_keyword;
|
pub mod create_draft_keyword;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@ use crate::shared::state::AppState;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rhai::{Dynamic, Engine, EvalAltResult};
|
use rhai::{Dynamic, Engine, EvalAltResult};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub mod compiler;
|
pub mod compiler;
|
||||||
pub mod keywords;
|
pub mod keywords;
|
||||||
|
|
||||||
use self::keywords::add_tool::add_tool_keyword;
|
use self::keywords::add_tool::add_tool_keyword;
|
||||||
use self::keywords::add_website::add_website_keyword;
|
use self::keywords::add_website::add_website_keyword;
|
||||||
use self::keywords::bot_memory::{get_bot_memory_keyword, set_bot_memory_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::set_kb::{add_kb_keyword, set_kb_keyword};
|
||||||
use self::keywords::wait::wait_keyword;
|
use self::keywords::wait::wait_keyword;
|
||||||
use self::keywords::add_suggestion::add_suggestion_keyword;
|
use self::keywords::add_suggestion::add_suggestion_keyword;
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
use self::keywords::create_draft_keyword;
|
use self::keywords::create_draft_keyword;
|
||||||
|
|
||||||
|
|
||||||
pub struct ScriptService {
|
pub struct ScriptService {
|
||||||
pub engine: Engine,
|
pub engine: Engine,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScriptService {
|
impl ScriptService {
|
||||||
pub fn new(state: Arc<AppState>, user: UserSession) -> Self {
|
pub fn new(state: Arc<AppState>, user: UserSession) -> Self {
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
engine.set_allow_anonymous_fn(true);
|
engine.set_allow_anonymous_fn(true);
|
||||||
engine.set_allow_looping(true);
|
engine.set_allow_looping(true);
|
||||||
|
|
||||||
#[cfg(feature = "email")]
|
#[cfg(feature = "email")]
|
||||||
create_draft_keyword(&state, user.clone(), &mut engine);
|
create_draft_keyword(&state, user.clone(), &mut engine);
|
||||||
|
|
||||||
set_bot_memory_keyword(state.clone(), user.clone(), &mut engine);
|
set_bot_memory_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
get_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);
|
create_site_keyword(&state, user.clone(), &mut engine);
|
||||||
|
|
@ -68,8 +59,6 @@ impl ScriptService {
|
||||||
set_context_keyword(state.clone(), user.clone(), &mut engine);
|
set_context_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
set_user_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);
|
clear_suggestions_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
|
|
||||||
// KB and Tools keywords
|
|
||||||
set_kb_keyword(state.clone(), user.clone(), &mut engine);
|
set_kb_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
add_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);
|
add_tool_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
|
|
@ -77,26 +66,19 @@ impl ScriptService {
|
||||||
list_tools_keyword(state.clone(), user.clone(), &mut engine);
|
list_tools_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
add_website_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);
|
add_suggestion_keyword(state.clone(), user.clone(), &mut engine);
|
||||||
|
|
||||||
|
|
||||||
ScriptService {
|
ScriptService {
|
||||||
engine,
|
engine,
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn preprocess_basic_script(&self, script: &str) -> String {
|
fn preprocess_basic_script(&self, script: &str) -> String {
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
let mut for_stack: Vec<usize> = Vec::new();
|
let mut for_stack: Vec<usize> = Vec::new();
|
||||||
let mut current_indent = 0;
|
let mut current_indent = 0;
|
||||||
|
|
||||||
for line in script.lines() {
|
for line in script.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() || trimmed.starts_with("//"){
|
||||||
if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("REM") {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed.starts_with("FOR EACH") {
|
if trimmed.starts_with("FOR EACH") {
|
||||||
for_stack.push(current_indent);
|
for_stack.push(current_indent);
|
||||||
result.push_str(&" ".repeat(current_indent));
|
result.push_str(&" ".repeat(current_indent));
|
||||||
|
|
@ -107,7 +89,6 @@ impl ScriptService {
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed.starts_with("NEXT") {
|
if trimmed.starts_with("NEXT") {
|
||||||
if let Some(expected_indent) = for_stack.pop() {
|
if let Some(expected_indent) = for_stack.pop() {
|
||||||
if (current_indent - 4) != expected_indent {
|
if (current_indent - 4) != expected_indent {
|
||||||
|
|
@ -125,16 +106,13 @@ impl ScriptService {
|
||||||
panic!("NEXT without matching FOR EACH");
|
panic!("NEXT without matching FOR EACH");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if trimmed == "EXIT FOR" {
|
if trimmed == "EXIT FOR" {
|
||||||
result.push_str(&" ".repeat(current_indent));
|
result.push_str(&" ".repeat(current_indent));
|
||||||
result.push_str(trimmed);
|
result.push_str(trimmed);
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push_str(&" ".repeat(current_indent));
|
result.push_str(&" ".repeat(current_indent));
|
||||||
|
|
||||||
let basic_commands = [
|
let basic_commands = [
|
||||||
"SET",
|
"SET",
|
||||||
"CREATE",
|
"CREATE",
|
||||||
|
|
@ -158,12 +136,10 @@ impl ScriptService {
|
||||||
"GET BOT MEMORY",
|
"GET BOT MEMORY",
|
||||||
"SET BOT MEMORY",
|
"SET BOT MEMORY",
|
||||||
];
|
];
|
||||||
|
|
||||||
let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd));
|
let is_basic_command = basic_commands.iter().any(|&cmd| trimmed.starts_with(cmd));
|
||||||
let is_control_flow = trimmed.starts_with("IF")
|
let is_control_flow = trimmed.starts_with("IF")
|
||||||
|| trimmed.starts_with("ELSE")
|
|| trimmed.starts_with("ELSE")
|
||||||
|| trimmed.starts_with("END IF");
|
|| trimmed.starts_with("END IF");
|
||||||
|
|
||||||
if is_basic_command || !for_stack.is_empty() || is_control_flow {
|
if is_basic_command || !for_stack.is_empty() || is_control_flow {
|
||||||
result.push_str(trimmed);
|
result.push_str(trimmed);
|
||||||
result.push(';');
|
result.push(';');
|
||||||
|
|
@ -175,14 +151,11 @@ impl ScriptService {
|
||||||
}
|
}
|
||||||
result.push('\n');
|
result.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
if !for_stack.is_empty() {
|
if !for_stack.is_empty() {
|
||||||
panic!("Unclosed FOR EACH loop");
|
panic!("Unclosed FOR EACH loop");
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile(&self, script: &str) -> Result<rhai::AST, Box<EvalAltResult>> {
|
pub fn compile(&self, script: &str) -> Result<rhai::AST, Box<EvalAltResult>> {
|
||||||
let processed_script = self.preprocess_basic_script(script);
|
let processed_script = self.preprocess_basic_script(script);
|
||||||
info!("Processed Script:\n{}", processed_script);
|
info!("Processed Script:\n{}", processed_script);
|
||||||
|
|
@ -191,7 +164,6 @@ impl ScriptService {
|
||||||
Err(parse_error) => Err(Box::new(parse_error.into())),
|
Err(parse_error) => Err(Box::new(parse_error.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
|
pub fn run(&self, ast: &rhai::AST) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
self.engine.eval_ast(ast)
|
self.engine.eval_ast(ast)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Tests for bootstrap module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bootstrap_module() {
|
fn test_bootstrap_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Tests for bot module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bot_module() {
|
fn test_bot_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
175
src/bot/mod.rs
175
src/bot/mod.rs
|
|
@ -1,5 +1,4 @@
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use crate::config::ConfigManager;
|
use crate::config::ConfigManager;
|
||||||
use crate::drive_monitor::DriveMonitor;
|
use crate::drive_monitor::DriveMonitor;
|
||||||
use crate::llm_models;
|
use crate::llm_models;
|
||||||
|
|
@ -20,11 +19,9 @@ use tokio::sync::mpsc;
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn get_default_bot(conn: &mut PgConnection) -> (Uuid, String) {
|
pub fn get_default_bot(conn: &mut PgConnection) -> (Uuid, String) {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
match bots
|
match bots
|
||||||
.filter(is_active.eq(true))
|
.filter(is_active.eq(true))
|
||||||
.select((id, name))
|
.select((id, name))
|
||||||
|
|
@ -42,29 +39,21 @@ pub fn get_default_bot(conn: &mut PgConnection) -> (Uuid, String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BotOrchestrator {
|
pub struct BotOrchestrator {
|
||||||
pub state: Arc<AppState>,
|
pub state: Arc<AppState>,
|
||||||
pub mounted_bots: Arc<AsyncMutex<HashMap<String, Arc<DriveMonitor>>>>,
|
pub mounted_bots: Arc<AsyncMutex<HashMap<String, Arc<DriveMonitor>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotOrchestrator {
|
impl BotOrchestrator {
|
||||||
pub fn new(state: Arc<AppState>) -> Self {
|
pub fn new(state: Arc<AppState>) -> Self {
|
||||||
let orchestrator = Self {
|
let orchestrator = Self {
|
||||||
state,
|
state,
|
||||||
mounted_bots: Arc::new(AsyncMutex::new(HashMap::new())),
|
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
|
orchestrator
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mount_all_bots(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
pub async fn mount_all_bots(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut db_conn = self.state.conn.get().unwrap();
|
let mut db_conn = self.state.conn.get().unwrap();
|
||||||
let active_bots = bots
|
let active_bots = bots
|
||||||
.filter(is_active.eq(true))
|
.filter(is_active.eq(true))
|
||||||
|
|
@ -74,12 +63,10 @@ impl BotOrchestrator {
|
||||||
error!("Failed to query active bots: {}", e);
|
error!("Failed to query active bots: {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for bot_guid in active_bots {
|
for bot_guid in active_bots {
|
||||||
let state_clone = self.state.clone();
|
let state_clone = self.state.clone();
|
||||||
let mounted_bots_clone = self.mounted_bots.clone();
|
let mounted_bots_clone = self.mounted_bots.clone();
|
||||||
let bot_guid_str = bot_guid.to_string();
|
let bot_guid_str = bot_guid.to_string();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
Self::mount_bot_task(state_clone, mounted_bots_clone, bot_guid_str.clone())
|
Self::mount_bot_task(state_clone, mounted_bots_clone, bot_guid_str.clone())
|
||||||
|
|
@ -89,10 +76,8 @@ impl BotOrchestrator {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mount_bot_task(
|
async fn mount_bot_task(
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
mounted_bots: Arc<AsyncMutex<HashMap<String, Arc<DriveMonitor>>>>,
|
mounted_bots: Arc<AsyncMutex<HashMap<String, Arc<DriveMonitor>>>>,
|
||||||
|
|
@ -100,7 +85,6 @@ impl BotOrchestrator {
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let bot_name: String = {
|
let bot_name: String = {
|
||||||
let mut db_conn = state.conn.get().unwrap();
|
let mut db_conn = state.conn.get().unwrap();
|
||||||
bots.filter(id.eq(Uuid::parse_str(&bot_guid)?))
|
bots.filter(id.eq(Uuid::parse_str(&bot_guid)?))
|
||||||
|
|
@ -111,9 +95,7 @@ impl BotOrchestrator {
|
||||||
e
|
e
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
let bucket_name = format!("{}.gbai", bot_name);
|
let bucket_name = format!("{}.gbai", bot_name);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mounted = mounted_bots.lock().await;
|
let mounted = mounted_bots.lock().await;
|
||||||
if mounted.contains_key(&bot_guid) {
|
if mounted.contains_key(&bot_guid) {
|
||||||
|
|
@ -121,26 +103,21 @@ impl BotOrchestrator {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bot_id = Uuid::parse_str(&bot_guid)?;
|
let bot_id = Uuid::parse_str(&bot_guid)?;
|
||||||
let drive_monitor = Arc::new(DriveMonitor::new(state.clone(), bucket_name, bot_id));
|
let drive_monitor = Arc::new(DriveMonitor::new(state.clone(), bucket_name, bot_id));
|
||||||
let _handle = drive_monitor.clone().spawn().await;
|
let _handle = drive_monitor.clone().spawn().await;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut mounted = mounted_bots.lock().await;
|
let mut mounted = mounted_bots.lock().await;
|
||||||
mounted.insert(bot_guid.clone(), drive_monitor);
|
mounted.insert(bot_guid.clone(), drive_monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_bot(
|
pub async fn create_bot(
|
||||||
&self,
|
&self,
|
||||||
_bot_name: &str,
|
_bot_name: &str,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mount_bot(
|
pub async fn mount_bot(
|
||||||
&self,
|
&self,
|
||||||
bot_guid: &str,
|
bot_guid: &str,
|
||||||
|
|
@ -149,10 +126,8 @@ impl BotOrchestrator {
|
||||||
.strip_suffix(".gbai")
|
.strip_suffix(".gbai")
|
||||||
.unwrap_or(bot_guid)
|
.unwrap_or(bot_guid)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let bot_name: String = {
|
let bot_name: String = {
|
||||||
let mut db_conn = self.state.conn.get().unwrap();
|
let mut db_conn = self.state.conn.get().unwrap();
|
||||||
bots.filter(id.eq(Uuid::parse_str(&bot_guid)?))
|
bots.filter(id.eq(Uuid::parse_str(&bot_guid)?))
|
||||||
|
|
@ -163,9 +138,7 @@ impl BotOrchestrator {
|
||||||
e
|
e
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
let bucket_name = format!("{}.gbai", bot_name);
|
let bucket_name = format!("{}.gbai", bot_name);
|
||||||
|
|
||||||
{
|
{
|
||||||
let mounted_bots = self.mounted_bots.lock().await;
|
let mounted_bots = self.mounted_bots.lock().await;
|
||||||
if mounted_bots.contains_key(&bot_guid) {
|
if mounted_bots.contains_key(&bot_guid) {
|
||||||
|
|
@ -173,19 +146,15 @@ impl BotOrchestrator {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let bot_id = Uuid::parse_str(&bot_guid)?;
|
let bot_id = Uuid::parse_str(&bot_guid)?;
|
||||||
let drive_monitor = Arc::new(DriveMonitor::new(self.state.clone(), bucket_name, bot_id));
|
let drive_monitor = Arc::new(DriveMonitor::new(self.state.clone(), bucket_name, bot_id));
|
||||||
let _handle = drive_monitor.clone().spawn().await;
|
let _handle = drive_monitor.clone().spawn().await;
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut mounted_bots = self.mounted_bots.lock().await;
|
let mut mounted_bots = self.mounted_bots.lock().await;
|
||||||
mounted_bots.insert(bot_guid.clone(), drive_monitor);
|
mounted_bots.insert(bot_guid.clone(), drive_monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_user_input(
|
pub async fn handle_user_input(
|
||||||
&self,
|
&self,
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
|
|
@ -196,13 +165,10 @@ impl BotOrchestrator {
|
||||||
session_id,
|
session_id,
|
||||||
user_input
|
user_input
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut session_manager = self.state.session_manager.lock().await;
|
let mut session_manager = self.state.session_manager.lock().await;
|
||||||
session_manager.provide_input(session_id, user_input.to_string())?;
|
session_manager.provide_input(session_id, user_input.to_string())?;
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn register_response_channel(
|
pub async fn register_response_channel(
|
||||||
&self,
|
&self,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
|
|
@ -214,11 +180,9 @@ impl BotOrchestrator {
|
||||||
.await
|
.await
|
||||||
.insert(session_id.clone(), sender);
|
.insert(session_id.clone(), sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unregister_response_channel(&self, session_id: &str) {
|
pub async fn unregister_response_channel(&self, session_id: &str) {
|
||||||
self.state.response_channels.lock().await.remove(session_id);
|
self.state.response_channels.lock().await.remove(session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_event(
|
pub async fn send_event(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
|
|
@ -234,7 +198,6 @@ impl BotOrchestrator {
|
||||||
session_id,
|
session_id,
|
||||||
channel
|
channel
|
||||||
);
|
);
|
||||||
|
|
||||||
let event_response = BotResponse::from_string_ids(
|
let event_response = BotResponse::from_string_ids(
|
||||||
bot_id,
|
bot_id,
|
||||||
session_id,
|
session_id,
|
||||||
|
|
@ -250,16 +213,13 @@ impl BotOrchestrator {
|
||||||
is_complete: true,
|
is_complete: true,
|
||||||
..event_response
|
..event_response
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(adapter) = self.state.channels.lock().await.get(channel) {
|
if let Some(adapter) = self.state.channels.lock().await.get(channel) {
|
||||||
adapter.send_message(event_response).await?;
|
adapter.send_message(event_response).await?;
|
||||||
} else {
|
} else {
|
||||||
warn!("No channel adapter found for channel: {}", channel);
|
warn!("No channel adapter found for channel: {}", channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_context_change(
|
pub async fn handle_context_change(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
|
|
@ -273,17 +233,14 @@ impl BotOrchestrator {
|
||||||
session_id,
|
session_id,
|
||||||
context_name
|
context_name
|
||||||
);
|
);
|
||||||
|
|
||||||
let session_uuid = Uuid::parse_str(session_id).map_err(|e| {
|
let session_uuid = Uuid::parse_str(session_id).map_err(|e| {
|
||||||
error!("Failed to parse session_id: {}", e);
|
error!("Failed to parse session_id: {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let user_uuid = Uuid::parse_str(user_id).map_err(|e| {
|
let user_uuid = Uuid::parse_str(user_id).map_err(|e| {
|
||||||
error!("Failed to parse user_id: {}", e);
|
error!("Failed to parse user_id: {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Err(e) = self
|
if let Err(e) = self
|
||||||
.state
|
.state
|
||||||
.session_manager
|
.session_manager
|
||||||
|
|
@ -294,7 +251,6 @@ impl BotOrchestrator {
|
||||||
{
|
{
|
||||||
error!("Failed to update session context: {}", e);
|
error!("Failed to update session context: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let confirmation = BotResponse {
|
let confirmation = BotResponse {
|
||||||
bot_id: bot_id.to_string(),
|
bot_id: bot_id.to_string(),
|
||||||
user_id: user_id.to_string(),
|
user_id: user_id.to_string(),
|
||||||
|
|
@ -309,14 +265,11 @@ impl BotOrchestrator {
|
||||||
context_length: 0,
|
context_length: 0,
|
||||||
context_max_length: 0,
|
context_max_length: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(adapter) = self.state.channels.lock().await.get(channel) {
|
if let Some(adapter) = self.state.channels.lock().await.get(channel) {
|
||||||
adapter.send_message(confirmation).await?;
|
adapter.send_message(confirmation).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stream_response(
|
pub async fn stream_response(
|
||||||
&self,
|
&self,
|
||||||
message: UserMessage,
|
message: UserMessage,
|
||||||
|
|
@ -327,18 +280,15 @@ impl BotOrchestrator {
|
||||||
message.user_id,
|
message.user_id,
|
||||||
message.session_id
|
message.session_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let suggestions = if let Some(redis) = &self.state.cache {
|
let suggestions = if let Some(redis) = &self.state.cache {
|
||||||
let mut conn = redis.get_multiplexed_async_connection().await?;
|
let mut conn = redis.get_multiplexed_async_connection().await?;
|
||||||
let redis_key = format!("suggestions:{}:{}", message.user_id, message.session_id);
|
let redis_key = format!("suggestions:{}:{}", message.user_id, message.session_id);
|
||||||
|
|
||||||
let suggestions: Vec<String> = redis::cmd("LRANGE")
|
let suggestions: Vec<String> = redis::cmd("LRANGE")
|
||||||
.arg(&redis_key)
|
.arg(&redis_key)
|
||||||
.arg(0)
|
.arg(0)
|
||||||
.arg(-1)
|
.arg(-1)
|
||||||
.query_async(&mut conn)
|
.query_async(&mut conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
let mut seen = std::collections::HashSet::new();
|
||||||
suggestions
|
suggestions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -348,13 +298,10 @@ impl BotOrchestrator {
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = Uuid::parse_str(&message.user_id).map_err(|e| {
|
let user_id = Uuid::parse_str(&message.user_id).map_err(|e| {
|
||||||
error!("Invalid user ID: {}", e);
|
error!("Invalid user ID: {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Acquire lock briefly for DB access, then release before awaiting
|
|
||||||
let session_id = Uuid::parse_str(&message.session_id).map_err(|e| {
|
let session_id = Uuid::parse_str(&message.session_id).map_err(|e| {
|
||||||
error!("Invalid session ID: {}", e);
|
error!("Invalid session ID: {}", e);
|
||||||
e
|
e
|
||||||
|
|
@ -364,13 +311,10 @@ impl BotOrchestrator {
|
||||||
sm.get_session_by_id(session_id)?
|
sm.get_session_by_id(session_id)?
|
||||||
}
|
}
|
||||||
.ok_or_else(|| "Failed to create session")?;
|
.ok_or_else(|| "Failed to create session")?;
|
||||||
|
|
||||||
// Save user message to history
|
|
||||||
{
|
{
|
||||||
let mut sm = self.state.session_manager.lock().await;
|
let mut sm = self.state.session_manager.lock().await;
|
||||||
sm.save_message(session.id, user_id, 1, &message.content, 1)?;
|
sm.save_message(session.id, user_id, 1, &message.content, 1)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.message_type == 4 {
|
if message.message_type == 4 {
|
||||||
if let Some(context_name) = &message.context_name {
|
if let Some(context_name) = &message.context_name {
|
||||||
let _ = self
|
let _ = self
|
||||||
|
|
@ -384,17 +328,12 @@ impl BotOrchestrator {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let system_prompt = std::env::var("SYSTEM_PROMPT").unwrap_or_default();
|
let system_prompt = std::env::var("SYSTEM_PROMPT").unwrap_or_default();
|
||||||
|
|
||||||
// Acquire lock briefly for context retrieval
|
|
||||||
let context_data = {
|
let context_data = {
|
||||||
let sm = self.state.session_manager.lock().await;
|
let sm = self.state.session_manager.lock().await;
|
||||||
sm.get_session_context_data(&session.id, &session.user_id)
|
sm.get_session_context_data(&session.id, &session.user_id)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get history limit from bot config (default -1 for unlimited)
|
|
||||||
let history_limit = {
|
let history_limit = {
|
||||||
let config_manager = ConfigManager::new(self.state.conn.clone());
|
let config_manager = ConfigManager::new(self.state.conn.clone());
|
||||||
config_manager
|
config_manager
|
||||||
|
|
@ -407,27 +346,21 @@ impl BotOrchestrator {
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.unwrap_or(-1)
|
.unwrap_or(-1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Acquire lock briefly for history retrieval with configurable limit
|
|
||||||
let history = {
|
let history = {
|
||||||
let mut sm = self.state.session_manager.lock().await;
|
let mut sm = self.state.session_manager.lock().await;
|
||||||
let mut history = sm.get_conversation_history(session.id, user_id)?;
|
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
|
if let Some(last_compacted_index) = history
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|(role, _content)| role == "compact")
|
.rposition(|(role, _content)| role == "compact")
|
||||||
{
|
{
|
||||||
history = history.split_off(last_compacted_index);
|
history = history.split_off(last_compacted_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
if history_limit > 0 && history.len() > history_limit as usize {
|
if history_limit > 0 && history.len() > history_limit as usize {
|
||||||
let start = history.len() - history_limit as usize;
|
let start = history.len() - history_limit as usize;
|
||||||
history.drain(0..start);
|
history.drain(0..start);
|
||||||
}
|
}
|
||||||
history
|
history
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut prompt = String::new();
|
let mut prompt = String::new();
|
||||||
if !system_prompt.is_empty() {
|
if !system_prompt.is_empty() {
|
||||||
prompt.push_str(&format!("SYSTEM: *** {} *** \n", system_prompt));
|
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!("{}:{}\n", role, content));
|
||||||
}
|
}
|
||||||
prompt.push_str(&format!("Human: {}\nBot:", message.content));
|
prompt.push_str(&format!("Human: {}\nBot:", message.content));
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Stream prompt constructed with {} history entries",
|
"Stream prompt constructed with {} history entries",
|
||||||
history.len()
|
history.len()
|
||||||
|
|
@ -447,7 +379,6 @@ impl BotOrchestrator {
|
||||||
trace!("LLM prompt: [{}]", prompt);
|
trace!("LLM prompt: [{}]", prompt);
|
||||||
let (stream_tx, mut stream_rx) = mpsc::channel::<String>(100);
|
let (stream_tx, mut stream_rx) = mpsc::channel::<String>(100);
|
||||||
let llm = self.state.llm_provider.clone();
|
let llm = self.state.llm_provider.clone();
|
||||||
|
|
||||||
if message.channel == "web" {
|
if message.channel == "web" {
|
||||||
self.send_event(
|
self.send_event(
|
||||||
&message.user_id,
|
&message.user_id,
|
||||||
|
|
@ -475,7 +406,6 @@ impl BotOrchestrator {
|
||||||
};
|
};
|
||||||
response_tx.send(thinking_response).await?;
|
response_tx.send(thinking_response).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let prompt_clone = prompt.clone();
|
let prompt_clone = prompt.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = llm
|
if let Err(e) = llm
|
||||||
|
|
@ -485,7 +415,6 @@ impl BotOrchestrator {
|
||||||
error!("LLM streaming error: {}", e);
|
error!("LLM streaming error: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut full_response = String::new();
|
let mut full_response = String::new();
|
||||||
let mut analysis_buffer = String::new();
|
let mut analysis_buffer = String::new();
|
||||||
let mut in_analysis = false;
|
let mut in_analysis = false;
|
||||||
|
|
@ -493,8 +422,6 @@ impl BotOrchestrator {
|
||||||
let mut first_word_received = false;
|
let mut first_word_received = false;
|
||||||
let mut last_progress_update = Instant::now();
|
let mut last_progress_update = Instant::now();
|
||||||
let progress_interval = Duration::from_secs(1);
|
let progress_interval = Duration::from_secs(1);
|
||||||
|
|
||||||
// Calculate initial token count
|
|
||||||
let initial_tokens = crate::shared::utils::estimate_token_count(&prompt);
|
let initial_tokens = crate::shared::utils::estimate_token_count(&prompt);
|
||||||
let config_manager = ConfigManager::new(self.state.conn.clone());
|
let config_manager = ConfigManager::new(self.state.conn.clone());
|
||||||
let max_context_size = config_manager
|
let max_context_size = config_manager
|
||||||
|
|
@ -506,8 +433,6 @@ impl BotOrchestrator {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.parse::<usize>()
|
.parse::<usize>()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Show initial progress
|
|
||||||
if let Ok(_metrics) = get_system_metrics(initial_tokens, max_context_size) {
|
if let Ok(_metrics) = get_system_metrics(initial_tokens, max_context_size) {
|
||||||
}
|
}
|
||||||
let model = config_manager
|
let model = config_manager
|
||||||
|
|
@ -518,24 +443,18 @@ impl BotOrchestrator {
|
||||||
)
|
)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let handler = llm_models::get_handler(&model);
|
let handler = llm_models::get_handler(&model);
|
||||||
|
|
||||||
while let Some(chunk) = stream_rx.recv().await {
|
while let Some(chunk) = stream_rx.recv().await {
|
||||||
chunk_count += 1;
|
chunk_count += 1;
|
||||||
|
|
||||||
if !first_word_received && !chunk.trim().is_empty() {
|
if !first_word_received && !chunk.trim().is_empty() {
|
||||||
first_word_received = true;
|
first_word_received = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
analysis_buffer.push_str(&chunk);
|
analysis_buffer.push_str(&chunk);
|
||||||
|
|
||||||
if handler.has_analysis_markers(&analysis_buffer) && !in_analysis {
|
if handler.has_analysis_markers(&analysis_buffer) && !in_analysis {
|
||||||
in_analysis = true;
|
in_analysis = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if in_analysis && handler.is_analysis_complete(&analysis_buffer) {
|
if in_analysis && handler.is_analysis_complete(&analysis_buffer) {
|
||||||
in_analysis = false;
|
in_analysis = false;
|
||||||
analysis_buffer.clear();
|
analysis_buffer.clear();
|
||||||
|
|
||||||
if message.channel == "web" {
|
if message.channel == "web" {
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&self.state));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&self.state));
|
||||||
orchestrator
|
orchestrator
|
||||||
|
|
@ -552,11 +471,8 @@ impl BotOrchestrator {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !in_analysis {
|
if !in_analysis {
|
||||||
full_response.push_str(&chunk);
|
full_response.push_str(&chunk);
|
||||||
|
|
||||||
// Update progress if interval elapsed
|
|
||||||
if last_progress_update.elapsed() >= progress_interval {
|
if last_progress_update.elapsed() >= progress_interval {
|
||||||
let current_tokens =
|
let current_tokens =
|
||||||
initial_tokens + crate::shared::utils::estimate_token_count(&full_response);
|
initial_tokens + crate::shared::utils::estimate_token_count(&full_response);
|
||||||
|
|
@ -571,7 +487,6 @@ impl BotOrchestrator {
|
||||||
}
|
}
|
||||||
last_progress_update = Instant::now();
|
last_progress_update = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
let partial = BotResponse {
|
let partial = BotResponse {
|
||||||
bot_id: message.bot_id.clone(),
|
bot_id: message.bot_id.clone(),
|
||||||
user_id: message.user_id.clone(),
|
user_id: message.user_id.clone(),
|
||||||
|
|
@ -586,19 +501,15 @@ impl BotOrchestrator {
|
||||||
context_length: 0,
|
context_length: 0,
|
||||||
context_max_length: 0,
|
context_max_length: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if response_tx.send(partial).await.is_err() {
|
if response_tx.send(partial).await.is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Stream processing completed, {} chunks processed",
|
"Stream processing completed, {} chunks processed",
|
||||||
chunk_count
|
chunk_count
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sum tokens from all p.push context builds before submission
|
|
||||||
let total_tokens = crate::shared::utils::estimate_token_count(&prompt)
|
let total_tokens = crate::shared::utils::estimate_token_count(&prompt)
|
||||||
+ crate::shared::utils::estimate_token_count(&context_data)
|
+ crate::shared::utils::estimate_token_count(&context_data)
|
||||||
+ crate::shared::utils::estimate_token_count(&full_response);
|
+ crate::shared::utils::estimate_token_count(&full_response);
|
||||||
|
|
@ -606,8 +517,6 @@ impl BotOrchestrator {
|
||||||
"Total tokens (context + prompt + response): {}",
|
"Total tokens (context + prompt + response): {}",
|
||||||
total_tokens
|
total_tokens
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger compact prompt if enabled
|
|
||||||
let config_manager = ConfigManager::new( self.state.conn.clone());
|
let config_manager = ConfigManager::new( self.state.conn.clone());
|
||||||
let compact_enabled = config_manager
|
let compact_enabled = config_manager
|
||||||
.get_config(
|
.get_config(
|
||||||
|
|
@ -629,13 +538,10 @@ impl BotOrchestrator {
|
||||||
std::thread::sleep(Duration::from_secs(60));
|
std::thread::sleep(Duration::from_secs(60));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save final message with short lock scope
|
|
||||||
{
|
{
|
||||||
let mut sm = self.state.session_manager.lock().await;
|
let mut sm = self.state.session_manager.lock().await;
|
||||||
sm.save_message(session.id, user_id, 2, &full_response, 1)?;
|
sm.save_message(session.id, user_id, 2, &full_response, 1)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_manager = ConfigManager::new(self.state.conn.clone());
|
let config_manager = ConfigManager::new(self.state.conn.clone());
|
||||||
let max_context_size = config_manager
|
let max_context_size = config_manager
|
||||||
.get_config(
|
.get_config(
|
||||||
|
|
@ -646,9 +552,7 @@ impl BotOrchestrator {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.parse::<usize>()
|
.parse::<usize>()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let current_context_length = crate::shared::utils::estimate_token_count(&context_data);
|
let current_context_length = crate::shared::utils::estimate_token_count(&context_data);
|
||||||
|
|
||||||
let final_msg = BotResponse {
|
let final_msg = BotResponse {
|
||||||
bot_id: message.bot_id,
|
bot_id: message.bot_id,
|
||||||
user_id: message.user_id,
|
user_id: message.user_id,
|
||||||
|
|
@ -663,11 +567,9 @@ impl BotOrchestrator {
|
||||||
context_length: current_context_length,
|
context_length: current_context_length,
|
||||||
context_max_length: max_context_size,
|
context_max_length: max_context_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
response_tx.send(final_msg).await?;
|
response_tx.send(final_msg).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_sessions(
|
pub async fn get_user_sessions(
|
||||||
&self,
|
&self,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
|
|
@ -676,7 +578,6 @@ impl BotOrchestrator {
|
||||||
let sessions = session_manager.get_user_sessions(user_id)?;
|
let sessions = session_manager.get_user_sessions(user_id)?;
|
||||||
Ok(sessions)
|
Ok(sessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_conversation_history(
|
pub async fn get_conversation_history(
|
||||||
&self,
|
&self,
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
|
|
@ -687,12 +588,10 @@ impl BotOrchestrator {
|
||||||
session_id,
|
session_id,
|
||||||
user_id
|
user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut session_manager = self.state.session_manager.lock().await;
|
let mut session_manager = self.state.session_manager.lock().await;
|
||||||
let history = session_manager.get_conversation_history(session_id, user_id)?;
|
let history = session_manager.get_conversation_history(session_id, user_id)?;
|
||||||
Ok(history)
|
Ok(history)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_start_script(
|
pub async fn run_start_script(
|
||||||
session: &UserSession,
|
session: &UserSession,
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
|
|
@ -703,12 +602,9 @@ impl BotOrchestrator {
|
||||||
session.id,
|
session.id,
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let bot_id = session.bot_id;
|
let bot_id = session.bot_id;
|
||||||
|
|
||||||
let bot_name: String = {
|
let bot_name: String = {
|
||||||
let mut db_conn = state.conn.get().unwrap();
|
let mut db_conn = state.conn.get().unwrap();
|
||||||
bots.filter(id.eq(Uuid::parse_str(&bot_id.to_string())?))
|
bots.filter(id.eq(Uuid::parse_str(&bot_id.to_string())?))
|
||||||
|
|
@ -719,9 +615,7 @@ impl BotOrchestrator {
|
||||||
e
|
e
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
let start_script_path = format!("./work/{}.gbai/{}.gbdialog/start.ast", bot_name, bot_name);
|
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) {
|
let start_script = match std::fs::read_to_string(&start_script_path) {
|
||||||
Ok(content) => content,
|
Ok(content) => content,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
@ -729,17 +623,14 @@ impl BotOrchestrator {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Start script content for session {}: {}",
|
"Start script content for session {}: {}",
|
||||||
session.id,
|
session.id,
|
||||||
start_script
|
start_script
|
||||||
);
|
);
|
||||||
|
|
||||||
let session_clone = session.clone();
|
let session_clone = session.clone();
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
let script_service = crate::basic::ScriptService::new(state_clone, session_clone.clone());
|
let script_service = crate::basic::ScriptService::new(state_clone, session_clone.clone());
|
||||||
|
|
||||||
match tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
match tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||||
script_service
|
script_service
|
||||||
.compile(&start_script)
|
.compile(&start_script)
|
||||||
|
|
@ -767,7 +658,6 @@ impl BotOrchestrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_warning(
|
pub async fn send_warning(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|
@ -778,12 +668,10 @@ impl BotOrchestrator {
|
||||||
"Sending warning to session {} on channel {}: {}",
|
"Sending warning to session {} on channel {}: {}",
|
||||||
session_id, channel, message
|
session_id, channel, message
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut ui = BotUI::new().unwrap();
|
let mut ui = BotUI::new().unwrap();
|
||||||
ui.render_warning(message).unwrap();
|
ui.render_warning(message).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn trigger_auto_welcome(
|
pub async fn trigger_auto_welcome(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|
@ -797,12 +685,10 @@ impl BotOrchestrator {
|
||||||
session_id,
|
session_id,
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
let session_uuid = Uuid::parse_str(session_id).map_err(|e| {
|
let session_uuid = Uuid::parse_str(session_id).map_err(|e| {
|
||||||
error!("Invalid session ID: {}", e);
|
error!("Invalid session ID: {}", e);
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let session = {
|
let session = {
|
||||||
let mut session_manager = self.state.session_manager.lock().await;
|
let mut session_manager = self.state.session_manager.lock().await;
|
||||||
match session_manager.get_session_by_id(session_uuid)? {
|
match session_manager.get_session_by_id(session_uuid)? {
|
||||||
|
|
@ -813,7 +699,6 @@ impl BotOrchestrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = match tokio::time::timeout(
|
let result = match tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(5),
|
std::time::Duration::from_secs(5),
|
||||||
Self::run_start_script(&session, Arc::clone(&self.state), token),
|
Self::run_start_script(&session, Arc::clone(&self.state), token),
|
||||||
|
|
@ -830,7 +715,6 @@ impl BotOrchestrator {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Auto welcome completed for session: {} with result: {}",
|
"Auto welcome completed for session: {} with result: {}",
|
||||||
session_id, result
|
session_id, result
|
||||||
|
|
@ -838,13 +722,11 @@ impl BotOrchestrator {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BotOrchestrator {
|
impl Default for BotOrchestrator {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
panic!("BotOrchestrator::default is not supported; instantiate with BotOrchestrator::new(state)");
|
panic!("BotOrchestrator::default is not supported; instantiate with BotOrchestrator::new(state)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/ws")]
|
#[actix_web::get("/ws")]
|
||||||
async fn websocket_handler(
|
async fn websocket_handler(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|
@ -852,15 +734,12 @@ async fn websocket_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
let query = web::Query::<HashMap<String, String>>::from_query(req.query_string()).unwrap();
|
let query = web::Query::<HashMap<String, String>>::from_query(req.query_string()).unwrap();
|
||||||
|
|
||||||
let session_id = query.get("session_id").cloned().unwrap();
|
let session_id = query.get("session_id").cloned().unwrap();
|
||||||
let user_id_string = query
|
let user_id_string = query
|
||||||
.get("user_id")
|
.get("user_id")
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||||
.replace("undefined", &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_id = {
|
||||||
let user_uuid = Uuid::parse_str(&user_id_string).unwrap_or_else(|_| Uuid::new_v4());
|
let user_uuid = Uuid::parse_str(&user_id_string).unwrap_or_else(|_| Uuid::new_v4());
|
||||||
let result = {
|
let result = {
|
||||||
|
|
@ -875,27 +754,21 @@ async fn websocket_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
|
let (res, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
|
||||||
let (tx, mut rx) = mpsc::channel::<BotResponse>(100);
|
let (tx, mut rx) = mpsc::channel::<BotResponse>(100);
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
orchestrator
|
orchestrator
|
||||||
.register_response_channel(session_id.clone(), tx.clone())
|
.register_response_channel(session_id.clone(), tx.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
data.web_adapter
|
data.web_adapter
|
||||||
.add_connection(session_id.clone(), tx.clone())
|
.add_connection(session_id.clone(), tx.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
data.voice_adapter
|
data.voice_adapter
|
||||||
.add_connection(session_id.clone(), tx.clone())
|
.add_connection(session_id.clone(), tx.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let bot_id: String = {
|
let bot_id: String = {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut db_conn = data.conn.get().unwrap();
|
let mut db_conn = data.conn.get().unwrap();
|
||||||
match bots
|
match bots
|
||||||
.filter(is_active.eq(true))
|
.filter(is_active.eq(true))
|
||||||
|
|
@ -914,7 +787,6 @@ async fn websocket_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
orchestrator
|
orchestrator
|
||||||
.send_event(
|
.send_event(
|
||||||
&user_id,
|
&user_id,
|
||||||
|
|
@ -930,17 +802,14 @@ async fn websocket_handler(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"WebSocket connection established for session: {}, user: {}",
|
"WebSocket connection established for session: {}, user: {}",
|
||||||
session_id, user_id
|
session_id, user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let orchestrator_clone = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator_clone = BotOrchestrator::new(Arc::clone(&data));
|
||||||
let user_id_welcome = user_id.clone();
|
let user_id_welcome = user_id.clone();
|
||||||
let session_id_welcome = session_id.clone();
|
let session_id_welcome = session_id.clone();
|
||||||
let bot_id_welcome = bot_id.clone();
|
let bot_id_welcome = bot_id.clone();
|
||||||
|
|
||||||
actix_web::rt::spawn(async move {
|
actix_web::rt::spawn(async move {
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(3),
|
std::time::Duration::from_secs(3),
|
||||||
|
|
@ -964,19 +833,16 @@ async fn websocket_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let web_adapter = data.web_adapter.clone();
|
let web_adapter = data.web_adapter.clone();
|
||||||
let session_id_clone1 = session_id.clone();
|
let session_id_clone1 = session_id.clone();
|
||||||
let session_id_clone2 = session_id.clone();
|
let session_id_clone2 = session_id.clone();
|
||||||
let user_id_clone = user_id.clone();
|
let user_id_clone = user_id.clone();
|
||||||
|
|
||||||
actix_web::rt::spawn(async move {
|
actix_web::rt::spawn(async move {
|
||||||
trace!(
|
trace!(
|
||||||
"Starting WebSocket sender for session {}",
|
"Starting WebSocket sender for session {}",
|
||||||
session_id_clone1
|
session_id_clone1
|
||||||
);
|
);
|
||||||
let mut message_count = 0;
|
let mut message_count = 0;
|
||||||
|
|
||||||
while let Some(msg) = rx.recv().await {
|
while let Some(msg) = rx.recv().await {
|
||||||
message_count += 1;
|
message_count += 1;
|
||||||
if let Ok(json) = serde_json::to_string(&msg) {
|
if let Ok(json) = serde_json::to_string(&msg) {
|
||||||
|
|
@ -986,30 +852,25 @@ async fn websocket_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"WebSocket sender terminated for session {}, sent {} messages",
|
"WebSocket sender terminated for session {}, sent {} messages",
|
||||||
session_id_clone1,
|
session_id_clone1,
|
||||||
message_count
|
message_count
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
actix_web::rt::spawn(async move {
|
actix_web::rt::spawn(async move {
|
||||||
trace!(
|
trace!(
|
||||||
"Starting WebSocket receiver for session {}",
|
"Starting WebSocket receiver for session {}",
|
||||||
session_id_clone2
|
session_id_clone2
|
||||||
);
|
);
|
||||||
let mut message_count = 0;
|
let mut message_count = 0;
|
||||||
|
|
||||||
while let Some(Ok(msg)) = msg_stream.recv().await {
|
while let Some(Ok(msg)) = msg_stream.recv().await {
|
||||||
match msg {
|
match msg {
|
||||||
WsMessage::Text(text) => {
|
WsMessage::Text(text) => {
|
||||||
message_count += 1;
|
message_count += 1;
|
||||||
|
|
||||||
let bot_id = {
|
let bot_id = {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut db_conn = data.conn.get().unwrap();
|
let mut db_conn = data.conn.get().unwrap();
|
||||||
match bots
|
match bots
|
||||||
.filter(is_active.eq(true))
|
.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) {
|
let json_value: serde_json::Value = match serde_json::from_str(&text) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -1036,12 +896,10 @@ async fn websocket_handler(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = json_value["content"]
|
let content = json_value["content"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let user_message = UserMessage {
|
let user_message = UserMessage {
|
||||||
bot_id,
|
bot_id,
|
||||||
user_id: user_id_clone.clone(),
|
user_id: user_id_clone.clone(),
|
||||||
|
|
@ -1053,7 +911,6 @@ async fn websocket_handler(
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
context_name: json_value["context_name"].as_str().map(|s| s.to_string()),
|
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 {
|
if let Err(e) = orchestrator.stream_response(user_message, tx.clone()).await {
|
||||||
error!("Failed to stream response: {}", e);
|
error!("Failed to stream response: {}", e);
|
||||||
}
|
}
|
||||||
|
|
@ -1064,11 +921,9 @@ async fn websocket_handler(
|
||||||
session_id_clone2,
|
session_id_clone2,
|
||||||
reason
|
reason
|
||||||
);
|
);
|
||||||
|
|
||||||
let bot_id = {
|
let bot_id = {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut db_conn = data.conn.get().unwrap();
|
let mut db_conn = data.conn.get().unwrap();
|
||||||
match bots
|
match bots
|
||||||
.filter(is_active.eq(true))
|
.filter(is_active.eq(true))
|
||||||
|
|
@ -1087,7 +942,6 @@ async fn websocket_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = orchestrator
|
if let Err(e) = orchestrator
|
||||||
.send_event(
|
.send_event(
|
||||||
&user_id_clone,
|
&user_id_clone,
|
||||||
|
|
@ -1101,39 +955,33 @@ async fn websocket_handler(
|
||||||
{
|
{
|
||||||
error!("Failed to send session_end event: {}", e);
|
error!("Failed to send session_end event: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
web_adapter.remove_connection(&session_id_clone2).await;
|
web_adapter.remove_connection(&session_id_clone2).await;
|
||||||
orchestrator
|
orchestrator
|
||||||
.unregister_response_channel(&session_id_clone2)
|
.unregister_response_channel(&session_id_clone2)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(e) = data.llm_provider.cancel_job(&session_id_clone2).await {
|
if let Err(e) = data.llm_provider.cancel_job(&session_id_clone2).await {
|
||||||
warn!(
|
warn!(
|
||||||
"Failed to cancel LLM job for session {}: {}",
|
"Failed to cancel LLM job for session {}: {}",
|
||||||
session_id_clone2, e
|
session_id_clone2, e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"WebSocket receiver terminated for session {}, processed {} messages",
|
"WebSocket receiver terminated for session {}, processed {} messages",
|
||||||
session_id_clone2,
|
session_id_clone2,
|
||||||
message_count
|
message_count
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"WebSocket handler setup completed for session {}",
|
"WebSocket handler setup completed for session {}",
|
||||||
session_id
|
session_id
|
||||||
);
|
);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/api/bot/create")]
|
#[actix_web::post("/api/bot/create")]
|
||||||
async fn create_bot_handler(
|
async fn create_bot_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
|
|
@ -1143,38 +991,30 @@ async fn create_bot_handler(
|
||||||
.get("bot_name")
|
.get("bot_name")
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or("default".to_string());
|
.unwrap_or("default".to_string());
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
|
|
||||||
if let Err(e) = orchestrator.create_bot(&bot_name).await {
|
if let Err(e) = orchestrator.create_bot(&bot_name).await {
|
||||||
error!("Failed to create bot: {}", e);
|
error!("Failed to create bot: {}", e);
|
||||||
return Ok(
|
return Ok(
|
||||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "bot_created"})))
|
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "bot_created"})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/api/bot/mount")]
|
#[actix_web::post("/api/bot/mount")]
|
||||||
async fn mount_bot_handler(
|
async fn mount_bot_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
info: web::Json<HashMap<String, String>>,
|
info: web::Json<HashMap<String, String>>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let bot_guid = info.get("bot_guid").cloned().unwrap_or_default();
|
let bot_guid = info.get("bot_guid").cloned().unwrap_or_default();
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
|
|
||||||
if let Err(e) = orchestrator.mount_bot(&bot_guid).await {
|
if let Err(e) = orchestrator.mount_bot(&bot_guid).await {
|
||||||
error!("Failed to mount bot: {}", e);
|
error!("Failed to mount bot: {}", e);
|
||||||
return Ok(
|
return Ok(
|
||||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "bot_mounted"})))
|
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "bot_mounted"})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/api/bot/input")]
|
#[actix_web::post("/api/bot/input")]
|
||||||
async fn handle_user_input_handler(
|
async fn handle_user_input_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
|
|
@ -1182,10 +1022,8 @@ async fn handle_user_input_handler(
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let session_id = info.get("session_id").cloned().unwrap_or_default();
|
let session_id = info.get("session_id").cloned().unwrap_or_default();
|
||||||
let user_input = info.get("input").cloned().unwrap_or_default();
|
let user_input = info.get("input").cloned().unwrap_or_default();
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
let session_uuid = Uuid::parse_str(&session_id).unwrap_or(Uuid::nil());
|
let session_uuid = Uuid::parse_str(&session_id).unwrap_or(Uuid::nil());
|
||||||
|
|
||||||
if let Err(e) = orchestrator
|
if let Err(e) = orchestrator
|
||||||
.handle_user_input(session_uuid, &user_input)
|
.handle_user_input(session_uuid, &user_input)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1195,19 +1033,15 @@ async fn handle_user_input_handler(
|
||||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "input_processed"})))
|
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "input_processed"})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/api/bot/sessions/{user_id}")]
|
#[actix_web::get("/api/bot/sessions/{user_id}")]
|
||||||
async fn get_user_sessions_handler(
|
async fn get_user_sessions_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let user_id = path.into_inner();
|
let user_id = path.into_inner();
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
|
|
||||||
match orchestrator.get_user_sessions(user_id).await {
|
match orchestrator.get_user_sessions(user_id).await {
|
||||||
Ok(sessions) => Ok(HttpResponse::Ok().json(sessions)),
|
Ok(sessions) => Ok(HttpResponse::Ok().json(sessions)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -1217,16 +1051,13 @@ async fn get_user_sessions_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/api/bot/history/{session_id}/{user_id}")]
|
#[actix_web::get("/api/bot/history/{session_id}/{user_id}")]
|
||||||
async fn get_conversation_history_handler(
|
async fn get_conversation_history_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
path: web::Path<(Uuid, Uuid)>,
|
path: web::Path<(Uuid, Uuid)>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
let (session_id, user_id) = path.into_inner();
|
let (session_id, user_id) = path.into_inner();
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
|
|
||||||
match orchestrator
|
match orchestrator
|
||||||
.get_conversation_history(session_id, user_id)
|
.get_conversation_history(session_id, user_id)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1239,7 +1070,6 @@ async fn get_conversation_history_handler(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/api/warn")]
|
#[actix_web::post("/api/warn")]
|
||||||
async fn send_warning_handler(
|
async fn send_warning_handler(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
|
|
@ -1248,19 +1078,15 @@ async fn send_warning_handler(
|
||||||
let default_session = "default".to_string();
|
let default_session = "default".to_string();
|
||||||
let default_channel = "web".to_string();
|
let default_channel = "web".to_string();
|
||||||
let default_message = "Warning!".to_string();
|
let default_message = "Warning!".to_string();
|
||||||
|
|
||||||
let session_id = info.get("session_id").unwrap_or(&default_session);
|
let session_id = info.get("session_id").unwrap_or(&default_session);
|
||||||
let channel = info.get("channel").unwrap_or(&default_channel);
|
let channel = info.get("channel").unwrap_or(&default_channel);
|
||||||
let message = info.get("message").unwrap_or(&default_message);
|
let message = info.get("message").unwrap_or(&default_message);
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Sending warning via API - session: {}, channel: {}",
|
"Sending warning via API - session: {}, channel: {}",
|
||||||
session_id,
|
session_id,
|
||||||
channel
|
channel
|
||||||
);
|
);
|
||||||
|
|
||||||
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
let orchestrator = BotOrchestrator::new(Arc::clone(&data));
|
||||||
|
|
||||||
if let Err(e) = orchestrator
|
if let Err(e) = orchestrator
|
||||||
.send_warning(session_id, channel, message)
|
.send_warning(session_id, channel, message)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1270,6 +1096,5 @@ async fn send_warning_handler(
|
||||||
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
HttpResponse::InternalServerError().json(serde_json::json!({"error": e.to_string()}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "warning_sent"})))
|
Ok(HttpResponse::Ok().json(serde_json::json!({"status": "warning_sent"})))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,9 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
use std::io::{self, Stdout};
|
use std::io::{self, Stdout};
|
||||||
use crate::nvidia::get_system_metrics;
|
use crate::nvidia::get_system_metrics;
|
||||||
|
|
||||||
pub struct BotUI {
|
pub struct BotUI {
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotUI {
|
impl BotUI {
|
||||||
pub fn new() -> io::Result<Self> {
|
pub fn new() -> io::Result<Self> {
|
||||||
let stdout = io::stdout();
|
let stdout = io::stdout();
|
||||||
|
|
@ -19,13 +17,11 @@ impl BotUI {
|
||||||
let terminal = Terminal::new(backend)?;
|
let terminal = Terminal::new(backend)?;
|
||||||
Ok(Self { terminal })
|
Ok(Self { terminal })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_progress(&mut self, current_tokens: usize, max_context_size: usize) -> io::Result<()> {
|
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 metrics = get_system_metrics(current_tokens, max_context_size).unwrap_or_default();
|
||||||
let gpu_usage = metrics.gpu_usage.unwrap_or(0.0);
|
let gpu_usage = metrics.gpu_usage.unwrap_or(0.0);
|
||||||
let cpu_usage = metrics.cpu_usage;
|
let cpu_usage = metrics.cpu_usage;
|
||||||
let token_ratio = current_tokens as f64 / max_context_size.max(1) as f64;
|
let token_ratio = current_tokens as f64 / max_context_size.max(1) as f64;
|
||||||
|
|
||||||
self.terminal.draw(|f| {
|
self.terminal.draw(|f| {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
|
|
@ -36,32 +32,27 @@ impl BotUI {
|
||||||
Constraint::Min(0),
|
Constraint::Min(0),
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
let gpu_gauge = Gauge::default()
|
let gpu_gauge = Gauge::default()
|
||||||
.block(Block::default().title("GPU Usage").borders(Borders::ALL))
|
.block(Block::default().title("GPU Usage").borders(Borders::ALL))
|
||||||
.gauge_style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD))
|
.gauge_style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD))
|
||||||
.ratio(gpu_usage as f64 / 100.0)
|
.ratio(gpu_usage as f64 / 100.0)
|
||||||
.label(format!("{:.1}%", gpu_usage));
|
.label(format!("{:.1}%", gpu_usage));
|
||||||
|
|
||||||
let cpu_gauge = Gauge::default()
|
let cpu_gauge = Gauge::default()
|
||||||
.block(Block::default().title("CPU Usage").borders(Borders::ALL))
|
.block(Block::default().title("CPU Usage").borders(Borders::ALL))
|
||||||
.gauge_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
.gauge_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||||
.ratio(cpu_usage as f64 / 100.0)
|
.ratio(cpu_usage as f64 / 100.0)
|
||||||
.label(format!("{:.1}%", cpu_usage));
|
.label(format!("{:.1}%", cpu_usage));
|
||||||
|
|
||||||
let token_gauge = Gauge::default()
|
let token_gauge = Gauge::default()
|
||||||
.block(Block::default().title("Token Progress").borders(Borders::ALL))
|
.block(Block::default().title("Token Progress").borders(Borders::ALL))
|
||||||
.gauge_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
.gauge_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
|
||||||
.ratio(token_ratio)
|
.ratio(token_ratio)
|
||||||
.label(format!("{}/{}", current_tokens, max_context_size));
|
.label(format!("{}/{}", current_tokens, max_context_size));
|
||||||
|
|
||||||
f.render_widget(gpu_gauge, chunks[0]);
|
f.render_widget(gpu_gauge, chunks[0]);
|
||||||
f.render_widget(cpu_gauge, chunks[1]);
|
f.render_widget(cpu_gauge, chunks[1]);
|
||||||
f.render_widget(token_gauge, chunks[2]);
|
f.render_widget(token_gauge, chunks[2]);
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_warning(&mut self, message: &str) -> io::Result<()> {
|
pub fn render_warning(&mut self, message: &str) -> io::Result<()> {
|
||||||
self.terminal.draw(|f| {
|
self.terminal.draw(|f| {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Tests for channels module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_channels_module() {
|
fn test_channels_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,7 @@ use log::{debug, info};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
use crate::shared::models::BotResponse;
|
use crate::shared::models::BotResponse;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ChannelAdapter: Send + Sync {
|
pub trait ChannelAdapter: Send + Sync {
|
||||||
async fn send_message(
|
async fn send_message(
|
||||||
|
|
@ -13,22 +11,18 @@ pub trait ChannelAdapter: Send + Sync {
|
||||||
response: BotResponse,
|
response: BotResponse,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WebChannelAdapter {
|
pub struct WebChannelAdapter {
|
||||||
connections: Arc<Mutex<HashMap<String, mpsc::Sender<BotResponse>>>>,
|
connections: Arc<Mutex<HashMap<String, mpsc::Sender<BotResponse>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebChannelAdapter {
|
impl WebChannelAdapter {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
connections: Arc::new(Mutex::new(HashMap::new())),
|
connections: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender<BotResponse>) {
|
pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender<BotResponse>) {
|
||||||
self.connections.lock().await.insert(session_id, tx);
|
self.connections.lock().await.insert(session_id, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_connection(&self, session_id: &str) {
|
pub async fn remove_connection(&self, session_id: &str) {
|
||||||
self.connections.lock().await.remove(session_id);
|
self.connections.lock().await.remove(session_id);
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +49,6 @@ impl WebChannelAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ChannelAdapter for WebChannelAdapter {
|
impl ChannelAdapter for WebChannelAdapter {
|
||||||
async fn send_message(
|
async fn send_message(
|
||||||
|
|
@ -69,12 +62,10 @@ impl ChannelAdapter for WebChannelAdapter {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VoiceAdapter {
|
pub struct VoiceAdapter {
|
||||||
rooms: Arc<Mutex<HashMap<String, String>>>,
|
rooms: Arc<Mutex<HashMap<String, String>>>,
|
||||||
connections: Arc<Mutex<HashMap<String, mpsc::Sender<BotResponse>>>>,
|
connections: Arc<Mutex<HashMap<String, mpsc::Sender<BotResponse>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VoiceAdapter {
|
impl VoiceAdapter {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -82,7 +73,6 @@ impl VoiceAdapter {
|
||||||
connections: Arc::new(Mutex::new(HashMap::new())),
|
connections: Arc::new(Mutex::new(HashMap::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_voice_session(
|
pub async fn start_voice_session(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|
@ -92,16 +82,13 @@ impl VoiceAdapter {
|
||||||
"Starting voice session for user: {} with session: {}",
|
"Starting voice session for user: {} with session: {}",
|
||||||
user_id, session_id
|
user_id, session_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let token = format!("mock_token_{}_{}", session_id, user_id);
|
let token = format!("mock_token_{}_{}", session_id, user_id);
|
||||||
self.rooms
|
self.rooms
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.insert(session_id.to_string(), token.clone());
|
.insert(session_id.to_string(), token.clone());
|
||||||
|
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop_voice_session(
|
pub async fn stop_voice_session(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|
@ -109,11 +96,9 @@ impl VoiceAdapter {
|
||||||
self.rooms.lock().await.remove(session_id);
|
self.rooms.lock().await.remove(session_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender<BotResponse>) {
|
pub async fn add_connection(&self, session_id: String, tx: mpsc::Sender<BotResponse>) {
|
||||||
self.connections.lock().await.insert(session_id, tx);
|
self.connections.lock().await.insert(session_id, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_voice_response(
|
pub async fn send_voice_response(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|
@ -123,7 +108,6 @@ impl VoiceAdapter {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ChannelAdapter for VoiceAdapter {
|
impl ChannelAdapter for VoiceAdapter {
|
||||||
async fn send_message(
|
async fn send_message(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
//! Tests for config module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_module() {
|
fn test_config_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@ impl AppConfig {
|
||||||
.map(|v| v.3.to_lowercase() == "true")
|
.map(|v| v.3.to_lowercase() == "true")
|
||||||
.unwrap_or(default)
|
.unwrap_or(default)
|
||||||
};
|
};
|
||||||
|
|
||||||
let drive = DriveConfig {
|
let drive = DriveConfig {
|
||||||
server: std::env::var("DRIVE_SERVER").unwrap(),
|
server: std::env::var("DRIVE_SERVER").unwrap(),
|
||||||
access_key: std::env::var("DRIVE_ACCESSKEY").unwrap(),
|
access_key: std::env::var("DRIVE_ACCESSKEY").unwrap(),
|
||||||
|
|
@ -119,8 +118,6 @@ impl AppConfig {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct ConfigManager {
|
pub struct ConfigManager {
|
||||||
conn: DbPool,
|
conn: DbPool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for context module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_context_module() {
|
fn test_context_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic context module test");
|
assert!(true, "Basic context module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_langcache() {
|
fn test_langcache() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
//! Tests for drive_monitor module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_drive_monitor_module() {
|
fn test_drive_monitor_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic drive_monitor module test");
|
assert!(true, "Basic drive_monitor module test");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -7,19 +7,16 @@ use std::collections::HashMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::time::{interval, Duration};
|
use tokio::time::{interval, Duration};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileState {
|
pub struct FileState {
|
||||||
pub etag: String,
|
pub etag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DriveMonitor {
|
pub struct DriveMonitor {
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
bucket_name: String,
|
bucket_name: String,
|
||||||
file_states: Arc<tokio::sync::RwLock<HashMap<String, FileState>>>,
|
file_states: Arc<tokio::sync::RwLock<HashMap<String, FileState>>>,
|
||||||
bot_id: uuid::Uuid,
|
bot_id: uuid::Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DriveMonitor {
|
impl DriveMonitor {
|
||||||
pub fn new(state: Arc<AppState>, bucket_name: String, bot_id: uuid::Uuid) -> Self {
|
pub fn new(state: Arc<AppState>, bucket_name: String, bot_id: uuid::Uuid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -29,14 +26,10 @@ impl DriveMonitor {
|
||||||
bot_id,
|
bot_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
|
pub fn spawn(self: Arc<Self>) -> tokio::task::JoinHandle<()> {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
info!("Drive Monitor service started for bucket: {}", self.bucket_name);
|
info!("Drive Monitor service started for bucket: {}", self.bucket_name);
|
||||||
|
|
||||||
|
|
||||||
let mut tick = interval(Duration::from_secs(30));
|
let mut tick = interval(Duration::from_secs(30));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tick.tick().await;
|
tick.tick().await;
|
||||||
if let Err(e) = self.check_for_changes().await {
|
if let Err(e) = self.check_for_changes().await {
|
||||||
|
|
@ -45,24 +38,19 @@ impl DriveMonitor {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_for_changes(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn check_for_changes(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let client = match &self.state.drive {
|
let client = match &self.state.drive {
|
||||||
Some(client) => client,
|
Some(client) => client,
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.check_gbdialog_changes(client).await?;
|
self.check_gbdialog_changes(client).await?;
|
||||||
self.check_gbot(client).await?;
|
self.check_gbot(client).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn check_gbdialog_changes(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let prefix = ".gbdialog/";
|
let prefix = ".gbdialog/";
|
||||||
let mut current_files = HashMap::new();
|
let mut current_files = HashMap::new();
|
||||||
let mut continuation_token = None;
|
let mut continuation_token = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let list_objects = match tokio::time::timeout(
|
let list_objects = match tokio::time::timeout(
|
||||||
Duration::from_secs(30),
|
Duration::from_secs(30),
|
||||||
|
|
@ -79,34 +67,26 @@ impl DriveMonitor {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for obj in list_objects.contents.unwrap_or_default() {
|
for obj in list_objects.contents.unwrap_or_default() {
|
||||||
let path = obj.key().unwrap_or_default().to_string();
|
let path = obj.key().unwrap_or_default().to_string();
|
||||||
let path_parts: Vec<&str> = path.split('/').collect();
|
let path_parts: Vec<&str> = path.split('/').collect();
|
||||||
|
|
||||||
if path_parts.len() < 2 || !path_parts[0].ends_with(".gbdialog") {
|
if path_parts.len() < 2 || !path_parts[0].ends_with(".gbdialog") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.ends_with('/') || !path.ends_with(".bas") {
|
if path.ends_with('/') || !path.ends_with(".bas") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_state = FileState {
|
let file_state = FileState {
|
||||||
etag: obj.e_tag().unwrap_or_default().to_string(),
|
etag: obj.e_tag().unwrap_or_default().to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
current_files.insert(path, file_state);
|
current_files.insert(path, file_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !list_objects.is_truncated.unwrap_or(false) {
|
if !list_objects.is_truncated.unwrap_or(false) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continuation_token = list_objects.next_continuation_token;
|
continuation_token = list_objects.next_continuation_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file_states = self.file_states.write().await;
|
let mut file_states = self.file_states.write().await;
|
||||||
|
|
||||||
for (path, current_state) in current_files.iter() {
|
for (path, current_state) in current_files.iter() {
|
||||||
if let Some(previous_state) = file_states.get(path) {
|
if let Some(previous_state) = file_states.get(path) {
|
||||||
if current_state.etag != previous_state.etag {
|
if current_state.etag != previous_state.etag {
|
||||||
|
|
@ -120,30 +100,24 @@ impl DriveMonitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let previous_paths: Vec<String> = file_states
|
let previous_paths: Vec<String> = file_states
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|k| k.starts_with(prefix))
|
.filter(|k| k.starts_with(prefix))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for path in previous_paths {
|
for path in previous_paths {
|
||||||
if !current_files.contains_key(&path) {
|
if !current_files.contains_key(&path) {
|
||||||
file_states.remove(&path);
|
file_states.remove(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (path, state) in current_files {
|
for (path, state) in current_files {
|
||||||
file_states.insert(path, state);
|
file_states.insert(path, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_gbot(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn check_gbot(&self, client: &Client) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let config_manager = ConfigManager::new(self.state.conn.clone());
|
let config_manager = ConfigManager::new(self.state.conn.clone());
|
||||||
let mut continuation_token = None;
|
let mut continuation_token = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let list_objects = match tokio::time::timeout(
|
let list_objects = match tokio::time::timeout(
|
||||||
Duration::from_secs(30),
|
Duration::from_secs(30),
|
||||||
|
|
@ -160,41 +134,33 @@ impl DriveMonitor {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for obj in list_objects.contents.unwrap_or_default() {
|
for obj in list_objects.contents.unwrap_or_default() {
|
||||||
let path = obj.key().unwrap_or_default().to_string();
|
let path = obj.key().unwrap_or_default().to_string();
|
||||||
let path_parts: Vec<&str> = path.split('/').collect();
|
let path_parts: Vec<&str> = path.split('/').collect();
|
||||||
|
|
||||||
if path_parts.len() < 2 || !path_parts[0].ends_with(".gbot") {
|
if path_parts.len() < 2 || !path_parts[0].ends_with(".gbot") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !path.ends_with("config.csv") {
|
if !path.ends_with("config.csv") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match client.head_object().bucket(&self.bucket_name).key(&path).send().await {
|
match client.head_object().bucket(&self.bucket_name).key(&path).send().await {
|
||||||
Ok(_head_res) => {
|
Ok(_head_res) => {
|
||||||
let response = client.get_object().bucket(&self.bucket_name).key(&path).send().await?;
|
let response = client.get_object().bucket(&self.bucket_name).key(&path).send().await?;
|
||||||
let bytes = response.body.collect().await?.into_bytes();
|
let bytes = response.body.collect().await?.into_bytes();
|
||||||
let csv_content = String::from_utf8(bytes.to_vec())
|
let csv_content = String::from_utf8(bytes.to_vec())
|
||||||
.map_err(|e| format!("UTF-8 error in {}: {}", path, e))?;
|
.map_err(|e| format!("UTF-8 error in {}: {}", path, e))?;
|
||||||
|
|
||||||
let llm_lines: Vec<_> = csv_content
|
let llm_lines: Vec<_> = csv_content
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| line.trim_start().starts_with("llm-"))
|
.filter(|line| line.trim_start().starts_with("llm-"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !llm_lines.is_empty() {
|
if !llm_lines.is_empty() {
|
||||||
use crate::llm::local::ensure_llama_servers_running;
|
use crate::llm::local::ensure_llama_servers_running;
|
||||||
let mut restart_needed = false;
|
let mut restart_needed = false;
|
||||||
|
|
||||||
for line in llm_lines {
|
for line in llm_lines {
|
||||||
let parts: Vec<&str> = line.split(',').collect();
|
let parts: Vec<&str> = line.split(',').collect();
|
||||||
if parts.len() >= 2 {
|
if parts.len() >= 2 {
|
||||||
let key = parts[0].trim();
|
let key = parts[0].trim();
|
||||||
let new_value = parts[1].trim();
|
let new_value = parts[1].trim();
|
||||||
|
|
||||||
match config_manager.get_config(&self.bot_id, key, None) {
|
match config_manager.get_config(&self.bot_id, key, None) {
|
||||||
Ok(old_value) => {
|
Ok(old_value) => {
|
||||||
if old_value != new_value {
|
if old_value != new_value {
|
||||||
|
|
@ -208,9 +174,7 @@ impl DriveMonitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content);
|
let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content);
|
||||||
|
|
||||||
if restart_needed {
|
if restart_needed {
|
||||||
if let Err(e) = ensure_llama_servers_running(Arc::clone(&self.state)).await {
|
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);
|
log::error!("Failed to restart LLaMA servers after llm- config change: {}", e);
|
||||||
|
|
@ -219,7 +183,6 @@ impl DriveMonitor {
|
||||||
} else {
|
} else {
|
||||||
let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content);
|
let _ = config_manager.sync_gbot_config(&self.bot_id, &csv_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if csv_content.lines().any(|line| line.starts_with("theme-")) {
|
if csv_content.lines().any(|line| line.starts_with("theme-")) {
|
||||||
self.broadcast_theme_change(&csv_content).await?;
|
self.broadcast_theme_change(&csv_content).await?;
|
||||||
}
|
}
|
||||||
|
|
@ -229,28 +192,23 @@ impl DriveMonitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !list_objects.is_truncated.unwrap_or(false) {
|
if !list_objects.is_truncated.unwrap_or(false) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continuation_token = list_objects.next_continuation_token;
|
continuation_token = list_objects.next_continuation_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_theme_change(&self, csv_content: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn broadcast_theme_change(&self, csv_content: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let mut theme_data = serde_json::json!({
|
let mut theme_data = serde_json::json!({
|
||||||
"event": "change_theme",
|
"event": "change_theme",
|
||||||
"data": {}
|
"data": {}
|
||||||
});
|
});
|
||||||
|
|
||||||
for line in csv_content.lines() {
|
for line in csv_content.lines() {
|
||||||
let parts: Vec<&str> = line.split(',').collect();
|
let parts: Vec<&str> = line.split(',').collect();
|
||||||
if parts.len() >= 2 {
|
if parts.len() >= 2 {
|
||||||
let key = parts[0].trim();
|
let key = parts[0].trim();
|
||||||
let value = parts[1].trim();
|
let value = parts[1].trim();
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"theme-color1" => theme_data["data"]["color1"] = serde_json::Value::String(value.to_string()),
|
"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()),
|
"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;
|
let response_channels = self.state.response_channels.lock().await;
|
||||||
for (session_id, tx) in response_channels.iter() {
|
for (session_id, tx) in response_channels.iter() {
|
||||||
let theme_response = crate::shared::models::BotResponse {
|
let theme_response = crate::shared::models::BotResponse {
|
||||||
|
|
@ -278,16 +235,12 @@ impl DriveMonitor {
|
||||||
context_length: 0,
|
context_length: 0,
|
||||||
context_max_length: 0,
|
context_max_length: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = tx.try_send(theme_response);
|
let _ = tx.try_send(theme_response);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn compile_tool(&self, client: &Client, file_path: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn compile_tool(&self, client: &Client, file_path: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
info!("Fetching object from S3: bucket={}, key={}", &self.bucket_name, file_path);
|
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 {
|
let response = match client.get_object().bucket(&self.bucket_name).key(file_path).send().await {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
info!("Successfully fetched object from S3: bucket={}, key={}, size={}",
|
info!("Successfully fetched object from S3: bucket={}, key={}, size={}",
|
||||||
|
|
@ -300,10 +253,8 @@ impl DriveMonitor {
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let bytes = response.body.collect().await?.into_bytes();
|
let bytes = response.body.collect().await?.into_bytes();
|
||||||
let source_content = String::from_utf8(bytes.to_vec())?;
|
let source_content = String::from_utf8(bytes.to_vec())?;
|
||||||
|
|
||||||
let tool_name = file_path
|
let tool_name = file_path
|
||||||
.split('/')
|
.split('/')
|
||||||
.last()
|
.last()
|
||||||
|
|
@ -311,34 +262,25 @@ impl DriveMonitor {
|
||||||
.strip_suffix(".bas")
|
.strip_suffix(".bas")
|
||||||
.unwrap_or(file_path)
|
.unwrap_or(file_path)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name);
|
let bot_name = self.bucket_name.strip_suffix(".gbai").unwrap_or(&self.bucket_name);
|
||||||
let work_dir = format!("./work/{}.gbai/{}.gbdialog", bot_name, bot_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 state_clone = Arc::clone(&self.state);
|
||||||
let work_dir_clone = work_dir.clone();
|
let work_dir_clone = work_dir.clone();
|
||||||
let tool_name_clone = tool_name.clone();
|
let tool_name_clone = tool_name.clone();
|
||||||
let source_content_clone = source_content.clone();
|
let source_content_clone = source_content.clone();
|
||||||
let bot_id = self.bot_id;
|
let bot_id = self.bot_id;
|
||||||
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
std::fs::create_dir_all(&work_dir_clone)?;
|
std::fs::create_dir_all(&work_dir_clone)?;
|
||||||
|
|
||||||
let local_source_path = format!("{}/{}.bas", work_dir_clone, tool_name_clone);
|
let local_source_path = format!("{}/{}.bas", work_dir_clone, tool_name_clone);
|
||||||
std::fs::write(&local_source_path, &source_content_clone)?;
|
std::fs::write(&local_source_path, &source_content_clone)?;
|
||||||
|
|
||||||
let mut compiler = BasicCompiler::new(state_clone, bot_id);
|
let mut compiler = BasicCompiler::new(state_clone, bot_id);
|
||||||
let result = compiler.compile_file(&local_source_path, &work_dir_clone)?;
|
let result = compiler.compile_file(&local_source_path, &work_dir_clone)?;
|
||||||
|
|
||||||
if let Some(mcp_tool) = result.mcp_tool {
|
if let Some(mcp_tool) = result.mcp_tool {
|
||||||
info!("MCP tool definition generated with {} parameters",
|
info!("MCP tool definition generated with {} parameters",
|
||||||
mcp_tool.input_schema.properties.len());
|
mcp_tool.input_schema.properties.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), Box<dyn Error + Send + Sync>>(())
|
Ok::<(), Box<dyn Error + Send + Sync>>(())
|
||||||
}).await??;
|
}).await??;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for email module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_email_module() {
|
fn test_email_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic email module test");
|
assert!(true, "Basic email module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_email_send() {
|
fn test_email_send() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
123
src/email/mod.rs
123
src/email/mod.rs
|
|
@ -1,16 +1,13 @@
|
||||||
use crate::{config::EmailConfig, shared::state::AppState};
|
use crate::{config::EmailConfig, shared::state::AppState};
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use actix_web::error::ErrorInternalServerError;
|
use actix_web::error::ErrorInternalServerError;
|
||||||
use actix_web::http::header::ContentType;
|
use actix_web::http::header::ContentType;
|
||||||
use actix_web::{web, HttpResponse, Result};
|
use actix_web::{web, HttpResponse, Result};
|
||||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use imap::types::Seq;
|
use imap::types::Seq;
|
||||||
use mailparse::{parse_mail, MailHeaderMap};
|
use mailparse::{parse_mail, MailHeaderMap};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct EmailResponse {
|
pub struct EmailResponse {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -22,7 +19,6 @@ pub struct EmailResponse {
|
||||||
read: bool,
|
read: bool,
|
||||||
labels: Vec<String>,
|
labels: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body: &str) {
|
async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body: &str) {
|
||||||
let email = Message::builder()
|
let email = Message::builder()
|
||||||
.from(config.from.parse().unwrap())
|
.from(config.from.parse().unwrap())
|
||||||
|
|
@ -30,9 +26,7 @@ async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.body(body.to_string())
|
.body(body.to_string())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let creds = Credentials::new(config.username.clone(), config.password.clone());
|
let creds = Credentials::new(config.username.clone(), config.password.clone());
|
||||||
|
|
||||||
SmtpTransport::relay(&config.server)
|
SmtpTransport::relay(&config.server)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.port(config.port)
|
.port(config.port)
|
||||||
|
|
@ -41,7 +35,6 @@ async fn internal_send_email(config: &EmailConfig, to: &str, subject: &str, body
|
||||||
.send(&email)
|
.send(&email)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/emails/list")]
|
#[actix_web::get("/emails/list")]
|
||||||
pub async fn list_emails(
|
pub async fn list_emails(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
|
|
@ -50,61 +43,41 @@ pub async fn list_emails(
|
||||||
.config
|
.config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
||||||
|
|
||||||
// Establish connection
|
|
||||||
let tls = native_tls::TlsConnector::builder().build().map_err(|e| {
|
let tls = native_tls::TlsConnector::builder().build().map_err(|e| {
|
||||||
ErrorInternalServerError(format!("Failed to create TLS connector: {:?}", e))
|
ErrorInternalServerError(format!("Failed to create TLS connector: {:?}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let client = imap::connect(
|
let client = imap::connect(
|
||||||
(_config.email.server.as_str(), 993),
|
(_config.email.server.as_str(), 993),
|
||||||
_config.email.server.as_str(),
|
_config.email.server.as_str(),
|
||||||
&tls,
|
&tls,
|
||||||
)
|
)
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Failed to connect to IMAP: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Failed to connect to IMAP: {:?}", e)))?;
|
||||||
|
|
||||||
// Login
|
|
||||||
let mut session = client
|
let mut session = client
|
||||||
.login(&_config.email.username, &_config.email.password)
|
.login(&_config.email.username, &_config.email.password)
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Login failed: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Login failed: {:?}", e)))?;
|
||||||
|
|
||||||
// Select INBOX
|
|
||||||
session
|
session
|
||||||
.select("INBOX")
|
.select("INBOX")
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Failed to select INBOX: {:?}", e)))?;
|
||||||
|
|
||||||
// Search for all messages
|
|
||||||
let messages = session
|
let messages = session
|
||||||
.search("ALL")
|
.search("ALL")
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Failed to search emails: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Failed to search emails: {:?}", e)))?;
|
||||||
|
|
||||||
let mut email_list = Vec::new();
|
let mut email_list = Vec::new();
|
||||||
|
|
||||||
// Get last 20 messages
|
|
||||||
let recent_messages: Vec<_> = messages.iter().cloned().collect();
|
let recent_messages: Vec<_> = messages.iter().cloned().collect();
|
||||||
let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect();
|
let recent_messages: Vec<Seq> = recent_messages.into_iter().rev().take(20).collect();
|
||||||
for seq in recent_messages {
|
for seq in recent_messages {
|
||||||
// Fetch the entire message (headers + body)
|
|
||||||
let fetch_result = session.fetch(seq.to_string(), "RFC822");
|
let fetch_result = session.fetch(seq.to_string(), "RFC822");
|
||||||
let messages = fetch_result
|
let messages = fetch_result
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Failed to fetch email: {:?}", e)))?;
|
||||||
|
|
||||||
for msg in messages.iter() {
|
for msg in messages.iter() {
|
||||||
let body = msg
|
let body = msg
|
||||||
.body()
|
.body()
|
||||||
.ok_or_else(|| ErrorInternalServerError("No body found"))?;
|
.ok_or_else(|| ErrorInternalServerError("No body found"))?;
|
||||||
|
|
||||||
// Parse the complete email message
|
|
||||||
let parsed = parse_mail(body)
|
let parsed = parse_mail(body)
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Failed to parse email: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Failed to parse email: {:?}", e)))?;
|
||||||
|
|
||||||
// Extract headers
|
|
||||||
let headers = parsed.get_headers();
|
let headers = parsed.get_headers();
|
||||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||||
let from = headers.get_first_value("From").unwrap_or_default();
|
let from = headers.get_first_value("From").unwrap_or_default();
|
||||||
let date = headers.get_first_value("Date").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
|
let body_text = if let Some(body_part) = parsed
|
||||||
.subparts
|
.subparts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -114,18 +87,13 @@ pub async fn list_emails(
|
||||||
} else {
|
} else {
|
||||||
parsed.get_body().unwrap_or_default()
|
parsed.get_body().unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create preview
|
|
||||||
let preview = body_text.lines().take(3).collect::<Vec<_>>().join(" ");
|
let preview = body_text.lines().take(3).collect::<Vec<_>>().join(" ");
|
||||||
let preview_truncated = if preview.len() > 150 {
|
let preview_truncated = if preview.len() > 150 {
|
||||||
format!("{}...", &preview[..150])
|
format!("{}...", &preview[..150])
|
||||||
} else {
|
} else {
|
||||||
preview
|
preview
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse From field
|
|
||||||
let (from_name, from_email) = parse_from_field(&from);
|
let (from_name, from_email) = parse_from_field(&from);
|
||||||
|
|
||||||
email_list.push(EmailResponse {
|
email_list.push(EmailResponse {
|
||||||
id: seq.to_string(),
|
id: seq.to_string(),
|
||||||
name: from_name,
|
name: from_name,
|
||||||
|
|
@ -146,15 +114,11 @@ pub async fn list_emails(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session
|
session
|
||||||
.logout()
|
.logout()
|
||||||
.map_err(|e| ErrorInternalServerError(format!("Failed to logout: {:?}", e)))?;
|
.map_err(|e| ErrorInternalServerError(format!("Failed to logout: {:?}", e)))?;
|
||||||
|
|
||||||
Ok(web::Json(email_list))
|
Ok(web::Json(email_list))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to parse From field
|
|
||||||
fn parse_from_field(from: &str) -> (String, String) {
|
fn parse_from_field(from: &str) -> (String, String) {
|
||||||
if let Some(start) = from.find('<') {
|
if let Some(start) = from.find('<') {
|
||||||
if let Some(end) = 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())
|
("Unknown".to_string(), from.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct SaveDraftRequest {
|
pub struct SaveDraftRequest {
|
||||||
pub to: String,
|
pub to: String,
|
||||||
|
|
@ -173,26 +136,22 @@ pub struct SaveDraftRequest {
|
||||||
pub cc: Option<String>,
|
pub cc: Option<String>,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct SaveDraftResponse {
|
pub struct SaveDraftResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub draft_id: Option<String>,
|
pub draft_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct GetLatestEmailRequest {
|
pub struct GetLatestEmailRequest {
|
||||||
pub from_email: String,
|
pub from_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct LatestEmailResponse {
|
pub struct LatestEmailResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub email_text: Option<String>,
|
pub email_text: Option<String>,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/emails/save_draft")]
|
#[actix_web::post("/emails/save_draft")]
|
||||||
pub async fn save_draft(
|
pub async fn save_draft(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
|
|
@ -202,7 +161,6 @@ pub async fn save_draft(
|
||||||
.config
|
.config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
||||||
|
|
||||||
match save_email_draft(&config.email, &draft_data).await {
|
match save_email_draft(&config.email, &draft_data).await {
|
||||||
Ok(draft_id) => Ok(web::Json(SaveDraftResponse {
|
Ok(draft_id) => Ok(web::Json(SaveDraftResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -216,32 +174,23 @@ pub async fn save_draft(
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_email_draft(
|
pub async fn save_email_draft(
|
||||||
email_config: &EmailConfig,
|
email_config: &EmailConfig,
|
||||||
draft_data: &SaveDraftRequest,
|
draft_data: &SaveDraftRequest,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
// Establish connection
|
|
||||||
let tls = native_tls::TlsConnector::builder().build()?;
|
let tls = native_tls::TlsConnector::builder().build()?;
|
||||||
let client = imap::connect(
|
let client = imap::connect(
|
||||||
(email_config.server.as_str(), 993),
|
(email_config.server.as_str(), 993),
|
||||||
email_config.server.as_str(),
|
email_config.server.as_str(),
|
||||||
&tls,
|
&tls,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Login
|
|
||||||
let mut session = client
|
let mut session = client
|
||||||
.login(&email_config.username, &email_config.password)
|
.login(&email_config.username, &email_config.password)
|
||||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||||
|
|
||||||
// Select or create Drafts folder
|
|
||||||
if session.select("Drafts").is_err() {
|
if session.select("Drafts").is_err() {
|
||||||
// Try to create Drafts folder if it doesn't exist
|
|
||||||
session.create("Drafts")?;
|
session.create("Drafts")?;
|
||||||
session.select("Drafts")?;
|
session.select("Drafts")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create email message
|
|
||||||
let cc_header = draft_data
|
let cc_header = draft_data
|
||||||
.cc
|
.cc
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|
@ -257,68 +206,43 @@ pub async fn save_email_draft(
|
||||||
chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S +0000"),
|
chrono::Utc::now().format("%a, %d %b %Y %H:%M:%S +0000"),
|
||||||
draft_data.text
|
draft_data.text
|
||||||
);
|
);
|
||||||
|
|
||||||
// Append to Drafts folder
|
|
||||||
session.append("Drafts", &email_message)?;
|
session.append("Drafts", &email_message)?;
|
||||||
|
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
|
|
||||||
Ok(chrono::Utc::now().timestamp().to_string())
|
Ok(chrono::Utc::now().timestamp().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_latest_email_from_sender(
|
async fn fetch_latest_email_from_sender(
|
||||||
email_config: &EmailConfig,
|
email_config: &EmailConfig,
|
||||||
from_email: &str,
|
from_email: &str,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
// Establish connection
|
|
||||||
let tls = native_tls::TlsConnector::builder().build()?;
|
let tls = native_tls::TlsConnector::builder().build()?;
|
||||||
let client = imap::connect(
|
let client = imap::connect(
|
||||||
(email_config.server.as_str(), 993),
|
(email_config.server.as_str(), 993),
|
||||||
email_config.server.as_str(),
|
email_config.server.as_str(),
|
||||||
&tls,
|
&tls,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Login
|
|
||||||
let mut session = client
|
let mut session = client
|
||||||
.login(&email_config.username, &email_config.password)
|
.login(&email_config.username, &email_config.password)
|
||||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||||
|
|
||||||
// Try to select Archive folder first, then fall back to INBOX
|
|
||||||
if session.select("Archive").is_err() {
|
if session.select("Archive").is_err() {
|
||||||
session.select("INBOX")?;
|
session.select("INBOX")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for emails from the specified sender
|
|
||||||
let search_query = format!("FROM \"{}\"", from_email);
|
let search_query = format!("FROM \"{}\"", from_email);
|
||||||
let messages = session.search(&search_query)?;
|
let messages = session.search(&search_query)?;
|
||||||
|
|
||||||
if messages.is_empty() {
|
if messages.is_empty() {
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
return Err(format!("No emails found from {}", from_email).into());
|
return Err(format!("No emails found from {}", from_email).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the latest message (highest sequence number)
|
|
||||||
let latest_seq = messages.iter().max().unwrap();
|
let latest_seq = messages.iter().max().unwrap();
|
||||||
|
|
||||||
// Fetch the entire message
|
|
||||||
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
||||||
|
|
||||||
let mut email_text = String::new();
|
let mut email_text = String::new();
|
||||||
|
|
||||||
for msg in messages.iter() {
|
for msg in messages.iter() {
|
||||||
let body = msg.body().ok_or("No body found in email")?;
|
let body = msg.body().ok_or("No body found in email")?;
|
||||||
|
|
||||||
// Parse the complete email message
|
|
||||||
let parsed = parse_mail(body)?;
|
let parsed = parse_mail(body)?;
|
||||||
|
|
||||||
// Extract headers
|
|
||||||
let headers = parsed.get_headers();
|
let headers = parsed.get_headers();
|
||||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||||
let from = headers.get_first_value("From").unwrap_or_default();
|
let from = headers.get_first_value("From").unwrap_or_default();
|
||||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||||
let to = headers.get_first_value("To").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
|
let body_text = if let Some(body_part) = parsed
|
||||||
.subparts
|
.subparts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -328,25 +252,19 @@ async fn fetch_latest_email_from_sender(
|
||||||
} else {
|
} else {
|
||||||
parsed.get_body().unwrap_or_default()
|
parsed.get_body().unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the email text ready for reply with headers
|
|
||||||
email_text = format!(
|
email_text = format!(
|
||||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
||||||
from, to, date, subject, body_text
|
from, to, date, subject, body_text
|
||||||
);
|
);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
|
|
||||||
if email_text.is_empty() {
|
if email_text.is_empty() {
|
||||||
Err("Failed to extract email content".into())
|
Err("Failed to extract email content".into())
|
||||||
} else {
|
} else {
|
||||||
Ok(email_text)
|
Ok(email_text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/emails/get_latest_from")]
|
#[actix_web::post("/emails/get_latest_from")]
|
||||||
pub async fn get_latest_email_from(
|
pub async fn get_latest_email_from(
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
|
|
@ -356,7 +274,6 @@ pub async fn get_latest_email_from(
|
||||||
.config
|
.config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
.ok_or_else(|| ErrorInternalServerError("Configuration not available"))?;
|
||||||
|
|
||||||
match fetch_latest_email_from_sender(&config.email, &request.from_email).await {
|
match fetch_latest_email_from_sender(&config.email, &request.from_email).await {
|
||||||
Ok(email_text) => Ok(web::Json(LatestEmailResponse {
|
Ok(email_text) => Ok(web::Json(LatestEmailResponse {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -376,59 +293,39 @@ pub async fn get_latest_email_from(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_latest_sent_to(
|
pub async fn fetch_latest_sent_to(
|
||||||
email_config: &EmailConfig,
|
email_config: &EmailConfig,
|
||||||
to_email: &str,
|
to_email: &str,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
// Establish connection
|
|
||||||
let tls = native_tls::TlsConnector::builder().build()?;
|
let tls = native_tls::TlsConnector::builder().build()?;
|
||||||
let client = imap::connect(
|
let client = imap::connect(
|
||||||
(email_config.server.as_str(), 993),
|
(email_config.server.as_str(), 993),
|
||||||
email_config.server.as_str(),
|
email_config.server.as_str(),
|
||||||
&tls,
|
&tls,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Login
|
|
||||||
let mut session = client
|
let mut session = client
|
||||||
.login(&email_config.username, &email_config.password)
|
.login(&email_config.username, &email_config.password)
|
||||||
.map_err(|e| format!("Login failed: {:?}", e))?;
|
.map_err(|e| format!("Login failed: {:?}", e))?;
|
||||||
|
|
||||||
// Try to select Archive folder first, then fall back to INBOX
|
|
||||||
if session.select("Sent").is_err() {
|
if session.select("Sent").is_err() {
|
||||||
session.select("Sent Items")?;
|
session.select("Sent Items")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search for emails from the specified sender
|
|
||||||
let search_query = format!("TO \"{}\"", to_email);
|
let search_query = format!("TO \"{}\"", to_email);
|
||||||
let messages = session.search(&search_query)?;
|
let messages = session.search(&search_query)?;
|
||||||
|
|
||||||
if messages.is_empty() {
|
if messages.is_empty() {
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
return Err(format!("No emails found to {}", to_email).into());
|
return Err(format!("No emails found to {}", to_email).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the latest message (highest sequence number)
|
|
||||||
let latest_seq = messages.iter().max().unwrap();
|
let latest_seq = messages.iter().max().unwrap();
|
||||||
|
|
||||||
// Fetch the entire message
|
|
||||||
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
let messages = session.fetch(latest_seq.to_string(), "RFC822")?;
|
||||||
|
|
||||||
let mut email_text = String::new();
|
let mut email_text = String::new();
|
||||||
|
|
||||||
for msg in messages.iter() {
|
for msg in messages.iter() {
|
||||||
let body = msg.body().ok_or("No body found in email")?;
|
let body = msg.body().ok_or("No body found in email")?;
|
||||||
|
|
||||||
// Parse the complete email message
|
|
||||||
let parsed = parse_mail(body)?;
|
let parsed = parse_mail(body)?;
|
||||||
|
|
||||||
// Extract headers
|
|
||||||
let headers = parsed.get_headers();
|
let headers = parsed.get_headers();
|
||||||
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
let subject = headers.get_first_value("Subject").unwrap_or_default();
|
||||||
let from = headers.get_first_value("From").unwrap_or_default();
|
let from = headers.get_first_value("From").unwrap_or_default();
|
||||||
let date = headers.get_first_value("Date").unwrap_or_default();
|
let date = headers.get_first_value("Date").unwrap_or_default();
|
||||||
let to = headers.get_first_value("To").unwrap_or_default();
|
let to = headers.get_first_value("To").unwrap_or_default();
|
||||||
|
|
||||||
if !to
|
if !to
|
||||||
.trim()
|
.trim()
|
||||||
.to_lowercase()
|
.to_lowercase()
|
||||||
|
|
@ -436,7 +333,6 @@ pub async fn fetch_latest_sent_to(
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Extract body text (handles both simple and multipart emails)
|
|
||||||
let body_text = if let Some(body_part) = parsed
|
let body_text = if let Some(body_part) = parsed
|
||||||
.subparts
|
.subparts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -446,52 +342,38 @@ pub async fn fetch_latest_sent_to(
|
||||||
} else {
|
} else {
|
||||||
parsed.get_body().unwrap_or_default()
|
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" {
|
if !body_text.trim().is_empty() && body_text != "No readable content found" {
|
||||||
// Format the email text ready for reply with headers
|
|
||||||
email_text = format!(
|
email_text = format!(
|
||||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n{}\n\n--- Reply Above This Line ---\n\n",
|
||||||
from, to, date, subject, body_text.trim()
|
from, to, date, subject, body_text.trim()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Still provide headers even if body is empty
|
|
||||||
email_text = format!(
|
email_text = format!(
|
||||||
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n[No readable content]\n\n--- Reply Above This Line ---\n\n",
|
"--- Original Message ---\nFrom: {}\nTo: {}\nDate: {}\nSubject: {}\n\n[No readable content]\n\n--- Reply Above This Line ---\n\n",
|
||||||
from, to, date, subject
|
from, to, date, subject
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
session.logout()?;
|
session.logout()?;
|
||||||
|
|
||||||
// Always return something, even if it's just headers
|
|
||||||
if email_text.is_empty() {
|
if email_text.is_empty() {
|
||||||
Err("Failed to extract email content".into())
|
Err("Failed to extract email content".into())
|
||||||
} else {
|
} else {
|
||||||
Ok(email_text)
|
Ok(email_text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/emails/send")]
|
#[actix_web::post("/emails/send")]
|
||||||
pub async fn send_email(
|
pub async fn send_email(
|
||||||
payload: web::Json<(String, String, String)>,
|
payload: web::Json<(String, String, String)>,
|
||||||
state: web::Data<AppState>,
|
state: web::Data<AppState>,
|
||||||
) -> Result<HttpResponse, actix_web::Error> {
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
let (to, subject, body) = payload.into_inner();
|
let (to, subject, body) = payload.into_inner();
|
||||||
|
|
||||||
info!("To: {}", to);
|
info!("To: {}", to);
|
||||||
info!("Subject: {}", subject);
|
info!("Subject: {}", subject);
|
||||||
info!("Body: {}", body);
|
info!("Body: {}", body);
|
||||||
|
|
||||||
// Send via SMTP
|
|
||||||
internal_send_email(&state.config.clone().unwrap().email, &to, &subject, &body).await;
|
internal_send_email(&state.config.clone().unwrap().email, &to, &subject, &body).await;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
Ok(HttpResponse::Ok().finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/campaigns/{campaign_id}/click/{email}")]
|
#[actix_web::get("/campaigns/{campaign_id}/click/{email}")]
|
||||||
pub async fn save_click(
|
pub async fn save_click(
|
||||||
path: web::Path<(String, String)>,
|
path: web::Path<(String, String)>,
|
||||||
|
|
@ -499,7 +381,6 @@ pub async fn save_click(
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let (campaign_id, email) = path.into_inner();
|
let (campaign_id, email) = path.into_inner();
|
||||||
use crate::shared::models::clicks;
|
use crate::shared::models::clicks;
|
||||||
|
|
||||||
let _ = diesel::insert_into(clicks::table)
|
let _ = diesel::insert_into(clicks::table)
|
||||||
.values((
|
.values((
|
||||||
clicks::campaign_id.eq(campaign_id),
|
clicks::campaign_id.eq(campaign_id),
|
||||||
|
|
@ -510,7 +391,6 @@ pub async fn save_click(
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(clicks::updated_at.eq(diesel::dsl::now))
|
.set(clicks::updated_at.eq(diesel::dsl::now))
|
||||||
.execute(&state.conn);
|
.execute(&state.conn);
|
||||||
|
|
||||||
let pixel = [
|
let pixel = [
|
||||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
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,
|
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
|
||||||
0xAE, 0x42, 0x60, 0x82,
|
0xAE, 0x42, 0x60, 0x82,
|
||||||
];
|
];
|
||||||
|
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.content_type(ContentType::png())
|
.content_type(ContentType::png())
|
||||||
.body(pixel.to_vec())
|
.body(pixel.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/campaigns/{campaign_id}/emails")]
|
#[actix_web::get("/campaigns/{campaign_id}/emails")]
|
||||||
pub async fn get_emails(path: web::Path<String>, state: web::Data<AppState>) -> String {
|
pub async fn get_emails(path: web::Path<String>, state: web::Data<AppState>) -> String {
|
||||||
let campaign_id = path.into_inner();
|
let campaign_id = path.into_inner();
|
||||||
use crate::shared::models::clicks::dsl::*;
|
use crate::shared::models::clicks::dsl::*;
|
||||||
|
|
||||||
let rows = clicks
|
let rows = clicks
|
||||||
.filter(campaign_id.eq(campaign_id))
|
.filter(campaign_id.eq(campaign_id))
|
||||||
.select(email)
|
.select(email)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for file module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_file_module() {
|
fn test_file_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic file module test");
|
assert!(true, "Basic file module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_file_operations() {
|
fn test_file_operations() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use aws_sdk_s3::Client;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tokio_stream::StreamExt as TokioStreamExt;
|
use tokio_stream::StreamExt as TokioStreamExt;
|
||||||
|
|
||||||
#[post("/files/upload/{folder_path}")]
|
#[post("/files/upload/{folder_path}")]
|
||||||
pub async fn upload_file(
|
pub async fn upload_file(
|
||||||
folder_path: web::Path<String>,
|
folder_path: web::Path<String>,
|
||||||
|
|
@ -17,7 +16,6 @@ pub async fn upload_file(
|
||||||
let mut temp_file = NamedTempFile::new().map_err(|e| {
|
let mut temp_file = NamedTempFile::new().map_err(|e| {
|
||||||
actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e))
|
actix_web::error::ErrorInternalServerError(format!("Failed to create temp file: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut file_name: Option<String> = None;
|
let mut file_name: Option<String> = None;
|
||||||
while let Some(mut field) = payload.try_next().await? {
|
while let Some(mut field) = payload.try_next().await? {
|
||||||
if let Some(disposition) = field.content_disposition() {
|
if let Some(disposition) = field.content_disposition() {
|
||||||
|
|
@ -25,7 +23,6 @@ pub async fn upload_file(
|
||||||
file_name = Some(name.to_string());
|
file_name = Some(name.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(chunk) = field.try_next().await? {
|
while let Some(chunk) = field.try_next().await? {
|
||||||
temp_file.write_all(&chunk).map_err(|e| {
|
temp_file.write_all(&chunk).map_err(|e| {
|
||||||
actix_web::error::ErrorInternalServerError(format!(
|
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 file_name = file_name.unwrap_or_else(|| "unnamed_file".to_string());
|
||||||
let temp_file_path = temp_file.into_temp_path();
|
let temp_file_path = temp_file.into_temp_path();
|
||||||
|
|
||||||
let client = state.get_ref().drive.as_ref().ok_or_else(|| {
|
let client = state.get_ref().drive.as_ref().ok_or_else(|| {
|
||||||
actix_web::error::ErrorInternalServerError("S3 client is not initialized")
|
actix_web::error::ErrorInternalServerError("S3 client is not initialized")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let s3_key = format!("{}/{}", folder_path, file_name);
|
let s3_key = format!("{}/{}", folder_path, file_name);
|
||||||
|
|
||||||
match upload_to_s3(client, &state.get_ref().bucket_name, &s3_key, &temp_file_path).await {
|
match upload_to_s3(client, &state.get_ref().bucket_name, &s3_key, &temp_file_path).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let _ = std::fs::remove_file(&temp_file_path);
|
let _ = std::fs::remove_file(&temp_file_path);
|
||||||
|
|
@ -62,7 +55,6 @@ pub async fn upload_file(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upload_to_s3(
|
async fn upload_to_s3(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
|
|
|
||||||
|
|
@ -6,31 +6,24 @@ use std::sync::Arc;
|
||||||
use log::{info, error};
|
use log::{info, error};
|
||||||
use tokio;
|
use tokio;
|
||||||
use reqwest;
|
use reqwest;
|
||||||
|
|
||||||
use actix_web::{post, web, HttpResponse, Result};
|
use actix_web::{post, web, HttpResponse, Result};
|
||||||
|
|
||||||
#[post("/api/chat/completions")]
|
#[post("/api/chat/completions")]
|
||||||
pub async fn chat_completions_local(
|
pub async fn chat_completions_local(
|
||||||
_data: web::Data<AppState>,
|
_data: web::Data<AppState>,
|
||||||
_payload: web::Json<serde_json::Value>,
|
_payload: web::Json<serde_json::Value>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
// Placeholder implementation
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "chat_completions_local not implemented" })))
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "chat_completions_local not implemented" })))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/api/embeddings")]
|
#[post("/api/embeddings")]
|
||||||
pub async fn embeddings_local(
|
pub async fn embeddings_local(
|
||||||
_data: web::Data<AppState>,
|
_data: web::Data<AppState>,
|
||||||
_payload: web::Json<serde_json::Value>,
|
_payload: web::Json<serde_json::Value>,
|
||||||
) -> Result<HttpResponse> {
|
) -> Result<HttpResponse> {
|
||||||
// Placeholder implementation
|
|
||||||
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "embeddings_local not implemented" })))
|
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "embeddings_local not implemented" })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn ensure_llama_servers_running(
|
pub async fn ensure_llama_servers_running(
|
||||||
app_state: Arc<AppState>
|
app_state: Arc<AppState>
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Get all config values before starting async operations
|
|
||||||
let config_values = {
|
let config_values = {
|
||||||
let conn_arc = app_state.conn.clone();
|
let conn_arc = app_state.conn.clone();
|
||||||
let default_bot_id = tokio::task::spawn_blocking(move || {
|
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;
|
let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_server_path) = config_values;
|
||||||
|
|
||||||
info!("Starting LLM servers...");
|
info!("Starting LLM servers...");
|
||||||
info!("Configuration:");
|
info!("Configuration:");
|
||||||
info!(" LLM URL: {}", llm_url);
|
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!(" LLM Model: {}", llm_model);
|
||||||
info!(" Embedding Model: {}", embedding_model);
|
info!(" Embedding Model: {}", embedding_model);
|
||||||
info!(" LLM Server Path: {}", llm_server_path);
|
info!(" LLM Server Path: {}", llm_server_path);
|
||||||
|
|
||||||
// Restart any existing llama-server processes
|
|
||||||
info!("Restarting any existing llama-server processes...");
|
info!("Restarting any existing llama-server processes...");
|
||||||
if let Err(e) = tokio::process::Command::new("sh")
|
if let Err(e) = tokio::process::Command::new("sh")
|
||||||
.arg("-c")
|
.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;
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
info!("Existing llama-server processes terminated (if any)");
|
info!("Existing llama-server processes terminated (if any)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if servers are already running
|
|
||||||
let llm_running = is_server_running(&llm_url).await;
|
let llm_running = is_server_running(&llm_url).await;
|
||||||
let embedding_running = is_server_running(&embedding_url).await;
|
let embedding_running = is_server_running(&embedding_url).await;
|
||||||
|
|
||||||
if llm_running && embedding_running {
|
if llm_running && embedding_running {
|
||||||
info!("Both LLM and Embedding servers are already running");
|
info!("Both LLM and Embedding servers are already running");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start servers that aren't running
|
|
||||||
let mut tasks = vec![];
|
let mut tasks = vec![];
|
||||||
|
|
||||||
if !llm_running && !llm_model.is_empty() {
|
if !llm_running && !llm_model.is_empty() {
|
||||||
info!("Starting LLM server...");
|
info!("Starting LLM server...");
|
||||||
tasks.push(tokio::spawn(start_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() {
|
} else if llm_model.is_empty() {
|
||||||
info!("LLM_MODEL not set, skipping LLM server");
|
info!("LLM_MODEL not set, skipping LLM server");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !embedding_running && !embedding_model.is_empty() {
|
if !embedding_running && !embedding_model.is_empty() {
|
||||||
info!("Starting Embedding server...");
|
info!("Starting Embedding server...");
|
||||||
tasks.push(tokio::spawn(start_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() {
|
} else if embedding_model.is_empty() {
|
||||||
info!("EMBEDDING_MODEL not set, skipping Embedding server");
|
info!("EMBEDDING_MODEL not set, skipping Embedding server");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all server startup tasks
|
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
task.await??;
|
task.await??;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for servers to be ready with verbose logging
|
|
||||||
info!("Waiting for servers to become ready...");
|
info!("Waiting for servers to become ready...");
|
||||||
|
|
||||||
let mut llm_ready = llm_running || llm_model.is_empty();
|
let mut llm_ready = llm_running || llm_model.is_empty();
|
||||||
let mut embedding_ready = embedding_running || embedding_model.is_empty();
|
let mut embedding_ready = embedding_running || embedding_model.is_empty();
|
||||||
|
|
||||||
let mut attempts = 0;
|
let mut attempts = 0;
|
||||||
let max_attempts = 60; // 2 minutes total
|
let max_attempts = 60;
|
||||||
|
|
||||||
while attempts < max_attempts && (!llm_ready || !embedding_ready) {
|
while attempts < max_attempts && (!llm_ready || !embedding_ready) {
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
info!("Checking server health (attempt {}/{})...", attempts + 1, max_attempts);
|
info!("Checking server health (attempt {}/{})...", attempts + 1, max_attempts);
|
||||||
|
|
||||||
if !llm_ready && !llm_model.is_empty() {
|
if !llm_ready && !llm_model.is_empty() {
|
||||||
if is_server_running(&llm_url).await {
|
if is_server_running(&llm_url).await {
|
||||||
info!("LLM server ready at {}", llm_url);
|
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");
|
info!("LLM server not ready yet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !embedding_ready && !embedding_model.is_empty() {
|
if !embedding_ready && !embedding_model.is_empty() {
|
||||||
if is_server_running(&embedding_url).await {
|
if is_server_running(&embedding_url).await {
|
||||||
info!("Embedding server ready at {}", embedding_url);
|
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");
|
info!("Embedding server not ready yet");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
|
|
||||||
if attempts % 10 == 0 {
|
if attempts % 10 == 0 {
|
||||||
info!("Still waiting for servers... (attempt {}/{})", attempts, max_attempts);
|
info!("Still waiting for servers... (attempt {}/{})", attempts, max_attempts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if llm_ready && embedding_ready {
|
if llm_ready && embedding_ready {
|
||||||
info!("All llama.cpp servers are ready and responding!");
|
info!("All llama.cpp servers are ready and responding!");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -166,7 +136,6 @@ let (_default_bot_id, llm_url, llm_model, embedding_url, embedding_model, llm_se
|
||||||
Err(error_msg.into())
|
Err(error_msg.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_server_running(url: &str) -> bool {
|
pub async fn is_server_running(url: &str) -> bool {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
match client.get(&format!("{}/health", url)).send().await {
|
match client.get(&format!("{}/health", url)).send().await {
|
||||||
|
|
@ -174,7 +143,6 @@ pub async fn is_server_running(url: &str) -> bool {
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_llm_server(
|
pub async fn start_llm_server(
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
llama_cpp_path: String,
|
llama_cpp_path: String,
|
||||||
|
|
@ -182,20 +150,16 @@ pub async fn start_llm_server(
|
||||||
url: String,
|
url: String,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let port = url.split(':').last().unwrap_or("8081");
|
let port = url.split(':').last().unwrap_or("8081");
|
||||||
|
|
||||||
std::env::set_var("OMP_NUM_THREADS", "20");
|
std::env::set_var("OMP_NUM_THREADS", "20");
|
||||||
std::env::set_var("OMP_PLACES", "cores");
|
std::env::set_var("OMP_PLACES", "cores");
|
||||||
std::env::set_var("OMP_PROC_BIND", "close");
|
std::env::set_var("OMP_PROC_BIND", "close");
|
||||||
|
|
||||||
let conn = app_state.conn.clone();
|
let conn = app_state.conn.clone();
|
||||||
let config_manager = ConfigManager::new(conn.clone());
|
let config_manager = ConfigManager::new(conn.clone());
|
||||||
|
|
||||||
let mut conn = conn.get().unwrap();
|
let mut conn = conn.get().unwrap();
|
||||||
let default_bot_id = bots.filter(name.eq("default"))
|
let default_bot_id = bots.filter(name.eq("default"))
|
||||||
.select(id)
|
.select(id)
|
||||||
.first::<uuid::Uuid>(&mut *conn)
|
.first::<uuid::Uuid>(&mut *conn)
|
||||||
.unwrap_or_else(|_| uuid::Uuid::nil());
|
.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 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 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());
|
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 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 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());
|
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!(
|
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 {}",
|
"-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
|
model_path, port, gpu_layers
|
||||||
|
|
@ -213,7 +175,6 @@ pub async fn start_llm_server(
|
||||||
if !reasoning_format.is_empty() {
|
if !reasoning_format.is_empty() {
|
||||||
args.push_str(&format!(" --reasoning-format {}", reasoning_format));
|
args.push_str(&format!(" --reasoning-format {}", reasoning_format));
|
||||||
}
|
}
|
||||||
|
|
||||||
if n_moe != "0" {
|
if n_moe != "0" {
|
||||||
args.push_str(&format!(" --n-cpu-moe {}", n_moe));
|
args.push_str(&format!(" --n-cpu-moe {}", n_moe));
|
||||||
}
|
}
|
||||||
|
|
@ -226,14 +187,12 @@ pub async fn start_llm_server(
|
||||||
if mlock == "true" {
|
if mlock == "true" {
|
||||||
args.push_str(" --mlock");
|
args.push_str(" --mlock");
|
||||||
}
|
}
|
||||||
|
|
||||||
if no_mmap == "true" {
|
if no_mmap == "true" {
|
||||||
args.push_str(" --no-mmap");
|
args.push_str(" --no-mmap");
|
||||||
}
|
}
|
||||||
if n_predict != "0" {
|
if n_predict != "0" {
|
||||||
args.push_str(&format!(" --n-predict {}", n_predict));
|
args.push_str(&format!(" --n-predict {}", n_predict));
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let mut cmd = tokio::process::Command::new("cmd");
|
let mut cmd = tokio::process::Command::new("cmd");
|
||||||
cmd.arg("/C").arg(format!(
|
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);
|
info!("Executing LLM server command: cd {} && ./llama-server {} --verbose", llama_cpp_path, args);
|
||||||
cmd.spawn()?;
|
cmd.spawn()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_embedding_server(
|
pub async fn start_embedding_server(
|
||||||
llama_cpp_path: String,
|
llama_cpp_path: String,
|
||||||
model_path: String,
|
model_path: String,
|
||||||
url: String,
|
url: String,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let port = url.split(':').last().unwrap_or("8082");
|
let port = url.split(':').last().unwrap_or("8082");
|
||||||
|
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let mut cmd = tokio::process::Command::new("cmd");
|
let mut cmd = tokio::process::Command::new("cmd");
|
||||||
cmd.arg("/c").arg(format!(
|
cmd.arg("/c").arg(format!(
|
||||||
|
|
@ -277,6 +233,5 @@ pub async fn start_embedding_server(
|
||||||
));
|
));
|
||||||
cmd.spawn()?;
|
cmd.spawn()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@ use async_trait::async_trait;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
|
||||||
pub mod local;
|
pub mod local;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait LLMProvider: Send + Sync {
|
pub trait LLMProvider: Send + Sync {
|
||||||
async fn generate(
|
async fn generate(
|
||||||
|
|
@ -13,14 +10,12 @@ pub trait LLMProvider: Send + Sync {
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
config: &Value,
|
config: &Value,
|
||||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
|
||||||
async fn generate_stream(
|
async fn generate_stream(
|
||||||
&self,
|
&self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
config: &Value,
|
config: &Value,
|
||||||
tx: mpsc::Sender<String>,
|
tx: mpsc::Sender<String>,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||||
|
|
||||||
async fn summarize(
|
async fn summarize(
|
||||||
&self,
|
&self,
|
||||||
text: &str,
|
text: &str,
|
||||||
|
|
@ -29,29 +24,25 @@ pub trait LLMProvider: Send + Sync {
|
||||||
self.generate(&prompt, &serde_json::json!({"max_tokens": 500}))
|
self.generate(&prompt, &serde_json::json!({"max_tokens": 500}))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cancel_job(
|
async fn cancel_job(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OpenAIClient {
|
pub struct OpenAIClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
api_key: String,
|
api_key: String,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenAIClient {
|
impl OpenAIClient {
|
||||||
pub fn new(api_key: String, base_url: Option<String>) -> Self {
|
pub fn new(api_key: String, base_url: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: reqwest::Client::new(),
|
client: reqwest::Client::new(),
|
||||||
api_key,
|
api_key,
|
||||||
base_url: base_url.unwrap_or_else(|| "http://localhost:8081/v1".to_string()),
|
base_url: base_url.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl LLMProvider for OpenAIClient {
|
impl LLMProvider for OpenAIClient {
|
||||||
async fn generate(
|
async fn generate(
|
||||||
|
|
@ -61,7 +52,7 @@ impl LLMProvider for OpenAIClient {
|
||||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
.post(&format!("{}/v1/chat/completions", self.base_url))
|
.post(&format!("{}/v1/chat/completions/", self.base_url))
|
||||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
.json(&serde_json::json!({
|
.json(&serde_json::json!({
|
||||||
"model": "gpt-3.5-turbo",
|
"model": "gpt-3.5-turbo",
|
||||||
|
|
@ -70,24 +61,18 @@ impl LLMProvider for OpenAIClient {
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let result: Value = response.json().await?;
|
let result: Value = response.json().await?;
|
||||||
let raw_content = result["choices"][0]["message"]["content"]
|
let raw_content = result["choices"][0]["message"]["content"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
// Define the end token we want to skip up to. Adjust the token string if needed.
|
|
||||||
let end_token = "final<|message|>";
|
let end_token = "final<|message|>";
|
||||||
let content = if let Some(pos) = raw_content.find(end_token) {
|
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()
|
raw_content[(pos + end_token.len())..].to_string()
|
||||||
} else {
|
} else {
|
||||||
// If the token is not found, return the full content.
|
|
||||||
raw_content.to_string()
|
raw_content.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_stream(
|
async fn generate_stream(
|
||||||
&self,
|
&self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
|
|
@ -106,14 +91,11 @@ impl LLMProvider for OpenAIClient {
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut stream = response.bytes_stream();
|
let mut stream = response.bytes_stream();
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
|
|
||||||
while let Some(chunk) = stream.next().await {
|
while let Some(chunk) = stream.next().await {
|
||||||
let chunk = chunk?;
|
let chunk = chunk?;
|
||||||
let chunk_str = String::from_utf8_lossy(&chunk);
|
let chunk_str = String::from_utf8_lossy(&chunk);
|
||||||
|
|
||||||
for line in chunk_str.lines() {
|
for line in chunk_str.lines() {
|
||||||
if line.starts_with("data: ") && !line.contains("[DONE]") {
|
if line.starts_with("data: ") && !line.contains("[DONE]") {
|
||||||
if let Ok(data) = serde_json::from_str::<Value>(&line[6..]) {
|
if let Ok(data) = serde_json::from_str::<Value>(&line[6..]) {
|
||||||
|
|
@ -125,16 +107,12 @@ impl LLMProvider for OpenAIClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn cancel_job(
|
async fn cancel_job(
|
||||||
&self,
|
&self,
|
||||||
_session_id: &str,
|
_session_id: &str,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// OpenAI doesn't support job cancellation
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
use super::ModelHandler;
|
use super::ModelHandler;
|
||||||
use regex;
|
use regex;
|
||||||
|
|
||||||
pub struct DeepseekR3Handler;
|
pub struct DeepseekR3Handler;
|
||||||
|
|
||||||
impl ModelHandler for DeepseekR3Handler {
|
impl ModelHandler for DeepseekR3Handler {
|
||||||
fn is_analysis_complete(&self, buffer: &str) -> bool {
|
fn is_analysis_complete(&self, buffer: &str) -> bool {
|
||||||
buffer.contains("</think>")
|
buffer.contains("</think>")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_content(&self, content: &str) -> String {
|
fn process_content(&self, content: &str) -> String {
|
||||||
let re = regex::Regex::new(r"(?s)<think>.*?</think>").unwrap();
|
let re = regex::Regex::new(r"(?s)<think>.*?</think>").unwrap();
|
||||||
re.replace_all(content, "").to_string()
|
re.replace_all(content, "").to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
||||||
buffer.contains("<think>")
|
buffer.contains("<think>")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,21 @@
|
||||||
use super::ModelHandler;
|
use super::ModelHandler;
|
||||||
|
|
||||||
pub struct GptOss120bHandler {
|
pub struct GptOss120bHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GptOss120bHandler {
|
impl GptOss120bHandler {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ModelHandler for GptOss120bHandler {
|
impl ModelHandler for GptOss120bHandler {
|
||||||
fn is_analysis_complete(&self, buffer: &str) -> bool {
|
fn is_analysis_complete(&self, buffer: &str) -> bool {
|
||||||
// GPT-120B uses explicit end marker
|
|
||||||
buffer.contains("**end**")
|
buffer.contains("**end**")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_content(&self, content: &str) -> String {
|
fn process_content(&self, content: &str) -> String {
|
||||||
// Remove both start and end markers from final output
|
|
||||||
content.replace("**start**", "")
|
content.replace("**start**", "")
|
||||||
.replace("**end**", "")
|
.replace("**end**", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
||||||
// GPT-120B uses explicit start marker
|
|
||||||
buffer.contains("**start**")
|
buffer.contains("**start**")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
use super::ModelHandler;
|
use super::ModelHandler;
|
||||||
|
|
||||||
pub struct GptOss20bHandler;
|
pub struct GptOss20bHandler;
|
||||||
|
|
||||||
impl ModelHandler for GptOss20bHandler {
|
impl ModelHandler for GptOss20bHandler {
|
||||||
fn is_analysis_complete(&self, buffer: &str) -> bool {
|
fn is_analysis_complete(&self, buffer: &str) -> bool {
|
||||||
buffer.ends_with("final")
|
buffer.ends_with("final")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_content(&self, content: &str) -> String {
|
fn process_content(&self, content: &str) -> String {
|
||||||
if let Some(pos) = content.find("final") {
|
if let Some(pos) = content.find("final") {
|
||||||
content[..pos].to_string()
|
content[..pos].to_string()
|
||||||
|
|
@ -14,7 +11,6 @@ impl ModelHandler for GptOss20bHandler {
|
||||||
content.to_string()
|
content.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
fn has_analysis_markers(&self, buffer: &str) -> bool {
|
||||||
buffer.contains("analysis<|message|>")
|
buffer.contains("analysis<|message|>")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,32 @@
|
||||||
//! Tests for LLM models module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_llm_models_module() {
|
fn test_llm_models_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic LLM models module test");
|
assert!(true, "Basic LLM models module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deepseek_r3_process_content() {
|
fn test_deepseek_r3_process_content() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
let handler = DeepseekR3Handler;
|
let handler = DeepseekR3Handler;
|
||||||
let input = r#"<think>
|
let input = r#"<think>
|
||||||
Alright, I need to help the user revise their resume entry. Let me read what they provided first.
|
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.
|
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.
|
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.
|
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.
|
||||||
</think>
|
</think>
|
||||||
|
|
||||||
" Auxiliom has been updated last week! New release.""#;
|
" Auxiliom has been updated last week! New release.""#;
|
||||||
|
|
||||||
let expected = r#"" 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);
|
let result = handler.process_content(input);
|
||||||
assert_eq!(result, expected);
|
assert_eq!(result, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gpt_oss_20b() {
|
fn test_gpt_oss_20b() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "GPT OSS 20B placeholder test");
|
assert!(true, "GPT OSS 20B placeholder test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gpt_oss_120b() {
|
fn test_gpt_oss_120b() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,13 @@
|
||||||
//! Module for handling model-specific behavior and token processing
|
|
||||||
|
|
||||||
pub mod gpt_oss_20b;
|
pub mod gpt_oss_20b;
|
||||||
pub mod deepseek_r3;
|
pub mod deepseek_r3;
|
||||||
pub mod gpt_oss_120b;
|
pub mod gpt_oss_120b;
|
||||||
|
|
||||||
|
|
||||||
/// Trait for model-specific token processing
|
|
||||||
pub trait ModelHandler: Send + Sync {
|
pub trait ModelHandler: Send + Sync {
|
||||||
/// Check if the analysis buffer indicates completion
|
|
||||||
fn is_analysis_complete(&self, buffer: &str) -> bool;
|
fn is_analysis_complete(&self, buffer: &str) -> bool;
|
||||||
|
|
||||||
/// Process the content, removing any model-specific tokens
|
|
||||||
fn process_content(&self, content: &str) -> String;
|
fn process_content(&self, content: &str) -> String;
|
||||||
|
|
||||||
/// Check if the buffer contains analysis start markers
|
|
||||||
fn has_analysis_markers(&self, buffer: &str) -> bool;
|
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<dyn ModelHandler> {
|
pub fn get_handler(model_path: &str) -> Box<dyn ModelHandler> {
|
||||||
let path = model_path.to_lowercase();
|
let path = model_path.to_lowercase();
|
||||||
|
|
||||||
if path.contains("deepseek") {
|
if path.contains("deepseek") {
|
||||||
Box::new(deepseek_r3::DeepseekR3Handler)
|
Box::new(deepseek_r3::DeepseekR3Handler)
|
||||||
} else if path.contains("120b") {
|
} else if path.contains("120b") {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
//! Tests for the main application module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_main() {
|
fn test_main() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
// Basic test that main.rs compiles and has expected components
|
|
||||||
assert!(true, "Basic sanity check");
|
assert!(true, "Basic sanity check");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for meet module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_meet_module() {
|
fn test_meet_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic meet module test");
|
assert!(true, "Basic meet module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_meeting_scheduling() {
|
fn test_meeting_scheduling() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use actix_web::{web, HttpResponse, Result};
|
use actix_web::{web, HttpResponse, Result};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
#[actix_web::post("/api/voice/start")]
|
#[actix_web::post("/api/voice/start")]
|
||||||
async fn voice_start(
|
async fn voice_start(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
|
|
@ -20,7 +18,6 @@ async fn voice_start(
|
||||||
"Voice session start request - session: {}, user: {}",
|
"Voice session start request - session: {}, user: {}",
|
||||||
session_id, user_id
|
session_id, user_id
|
||||||
);
|
);
|
||||||
|
|
||||||
match data
|
match data
|
||||||
.voice_adapter
|
.voice_adapter
|
||||||
.start_voice_session(session_id, user_id)
|
.start_voice_session(session_id, user_id)
|
||||||
|
|
@ -43,7 +40,6 @@ async fn voice_start(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::post("/api/voice/stop")]
|
#[actix_web::post("/api/voice/stop")]
|
||||||
async fn voice_stop(
|
async fn voice_stop(
|
||||||
data: web::Data<AppState>,
|
data: web::Data<AppState>,
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,25 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use sysinfo::{System};
|
use sysinfo::{System};
|
||||||
|
|
||||||
/// System monitoring data
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct SystemMetrics {
|
pub struct SystemMetrics {
|
||||||
pub gpu_usage: Option<f32>,
|
pub gpu_usage: Option<f32>,
|
||||||
pub cpu_usage: f32,
|
pub cpu_usage: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets current system metrics
|
|
||||||
pub fn get_system_metrics(_current_tokens: usize, _max_tokens: usize) -> Result<SystemMetrics> {
|
pub fn get_system_metrics(_current_tokens: usize, _max_tokens: usize) -> Result<SystemMetrics> {
|
||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
sys.refresh_cpu_usage();
|
sys.refresh_cpu_usage();
|
||||||
|
|
||||||
// Get CPU usage (average across all cores)
|
|
||||||
let cpu_usage = sys.global_cpu_usage();
|
let cpu_usage = sys.global_cpu_usage();
|
||||||
|
|
||||||
// Get GPU usage if available
|
|
||||||
let gpu_usage = if has_nvidia_gpu() {
|
let gpu_usage = if has_nvidia_gpu() {
|
||||||
get_gpu_utilization()?.get("gpu").copied()
|
get_gpu_utilization()?.get("gpu").copied()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Ok(SystemMetrics {
|
Ok(SystemMetrics {
|
||||||
gpu_usage,
|
gpu_usage,
|
||||||
cpu_usage,
|
cpu_usage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if NVIDIA GPU is available
|
|
||||||
pub fn has_nvidia_gpu() -> bool {
|
pub fn has_nvidia_gpu() -> bool {
|
||||||
match std::process::Command::new("nvidia-smi")
|
match std::process::Command::new("nvidia-smi")
|
||||||
.arg("--query-gpu=utilization.gpu")
|
.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<HashMap<String, f32>> {
|
pub fn get_gpu_utilization() -> Result<HashMap<String, f32>> {
|
||||||
let output = std::process::Command::new("nvidia-smi")
|
let output = std::process::Command::new("nvidia-smi")
|
||||||
.arg("--query-gpu=utilization.gpu,utilization.memory")
|
.arg("--query-gpu=utilization.gpu,utilization.memory")
|
||||||
.arg("--format=csv,noheader,nounits")
|
.arg("--format=csv,noheader,nounits")
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow::anyhow!("Failed to query GPU utilization"));
|
return Err(anyhow::anyhow!("Failed to query GPU utilization"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let output_str = String::from_utf8(output.stdout)?;
|
let output_str = String::from_utf8(output.stdout)?;
|
||||||
let mut util = HashMap::new();
|
let mut util = HashMap::new();
|
||||||
|
|
||||||
for line in output_str.lines() {
|
for line in output_str.lines() {
|
||||||
let parts: Vec<&str> = line.split(',').collect();
|
let parts: Vec<&str> = line.split(',').collect();
|
||||||
if parts.len() >= 2 {
|
if parts.len() >= 2 {
|
||||||
|
|
@ -72,6 +55,5 @@ pub fn get_gpu_utilization() -> Result<HashMap<String, f32>> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(util)
|
Ok(util)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,15 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use crate::package_manager::{get_all_components, InstallMode, PackageManager};
|
use crate::package_manager::{get_all_components, InstallMode, PackageManager};
|
||||||
|
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
print_usage();
|
print_usage();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = &args[1];
|
let command = &args[1];
|
||||||
|
|
||||||
match command.as_str() {
|
match command.as_str() {
|
||||||
"start" => {
|
"start" => {
|
||||||
let mode = if args.contains(&"--container".to_string()) {
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
|
|
@ -27,10 +22,8 @@ pub async fn run() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pm = PackageManager::new(mode, tenant)?;
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
println!("Starting all installed components...");
|
println!("Starting all installed components...");
|
||||||
|
|
||||||
let components = get_all_components();
|
let components = get_all_components();
|
||||||
for component in components {
|
for component in components {
|
||||||
if pm.is_installed(component.name) {
|
if pm.is_installed(component.name) {
|
||||||
|
|
@ -44,27 +37,19 @@ pub async fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
"stop" => {
|
"stop" => {
|
||||||
println!("Stopping all components...");
|
println!("Stopping all components...");
|
||||||
|
|
||||||
// Stop components gracefully
|
|
||||||
let components = get_all_components();
|
let components = get_all_components();
|
||||||
for component in components {
|
for component in components {
|
||||||
let _ = Command::new("pkill").arg("-f").arg(component.termination_command).output();
|
let _ = Command::new("pkill").arg("-f").arg(component.termination_command).output();
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("✓ BotServer components stopped");
|
println!("✓ BotServer components stopped");
|
||||||
}
|
}
|
||||||
"restart" => {
|
"restart" => {
|
||||||
println!("Restarting BotServer...");
|
println!("Restarting BotServer...");
|
||||||
|
|
||||||
// Stop
|
|
||||||
let components = get_all_components();
|
let components = get_all_components();
|
||||||
for component in components {
|
for component in components {
|
||||||
let _ = Command::new("pkill").arg("-f").arg(component.termination_command).output();
|
let _ = Command::new("pkill").arg("-f").arg(component.termination_command).output();
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
// Start
|
|
||||||
let mode = if args.contains(&"--container".to_string()) {
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
InstallMode::Container
|
InstallMode::Container
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -75,16 +60,13 @@ pub async fn run() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pm = PackageManager::new(mode, tenant)?;
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
|
|
||||||
let components = get_all_components();
|
let components = get_all_components();
|
||||||
for component in components {
|
for component in components {
|
||||||
if pm.is_installed(component.name) {
|
if pm.is_installed(component.name) {
|
||||||
let _ = pm.start(component.name);
|
let _ = pm.start(component.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("✓ BotServer restarted");
|
println!("✓ BotServer restarted");
|
||||||
}
|
}
|
||||||
"install" => {
|
"install" => {
|
||||||
|
|
@ -92,7 +74,6 @@ pub async fn run() -> Result<()> {
|
||||||
eprintln!("Usage: botserver install <component> [--container] [--tenant <name>]");
|
eprintln!("Usage: botserver install <component> [--container] [--tenant <name>]");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let component = &args[2];
|
let component = &args[2];
|
||||||
let mode = if args.contains(&"--container".to_string()) {
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
InstallMode::Container
|
InstallMode::Container
|
||||||
|
|
@ -104,7 +85,6 @@ pub async fn run() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pm = PackageManager::new(mode, tenant)?;
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
pm.install(component).await?;
|
pm.install(component).await?;
|
||||||
println!("✓ Component '{}' installed successfully", component);
|
println!("✓ Component '{}' installed successfully", component);
|
||||||
|
|
@ -114,7 +94,6 @@ pub async fn run() -> Result<()> {
|
||||||
eprintln!("Usage: botserver remove <component> [--container] [--tenant <name>]");
|
eprintln!("Usage: botserver remove <component> [--container] [--tenant <name>]");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let component = &args[2];
|
let component = &args[2];
|
||||||
let mode = if args.contains(&"--container".to_string()) {
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
InstallMode::Container
|
InstallMode::Container
|
||||||
|
|
@ -126,7 +105,6 @@ pub async fn run() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pm = PackageManager::new(mode, tenant)?;
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
pm.remove(component)?;
|
pm.remove(component)?;
|
||||||
println!("✓ Component '{}' removed successfully", component);
|
println!("✓ Component '{}' removed successfully", component);
|
||||||
|
|
@ -142,7 +120,6 @@ pub async fn run() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pm = PackageManager::new(mode, tenant)?;
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
println!("Available components:");
|
println!("Available components:");
|
||||||
for component in pm.list() {
|
for component in pm.list() {
|
||||||
|
|
@ -159,7 +136,6 @@ pub async fn run() -> Result<()> {
|
||||||
eprintln!("Usage: botserver status <component> [--container] [--tenant <name>]");
|
eprintln!("Usage: botserver status <component> [--container] [--tenant <name>]");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let component = &args[2];
|
let component = &args[2];
|
||||||
let mode = if args.contains(&"--container".to_string()) {
|
let mode = if args.contains(&"--container".to_string()) {
|
||||||
InstallMode::Container
|
InstallMode::Container
|
||||||
|
|
@ -171,7 +147,6 @@ pub async fn run() -> Result<()> {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let pm = PackageManager::new(mode, tenant)?;
|
let pm = PackageManager::new(mode, tenant)?;
|
||||||
if pm.is_installed(component) {
|
if pm.is_installed(component) {
|
||||||
println!("✓ Component '{}' is installed", component);
|
println!("✓ Component '{}' is installed", component);
|
||||||
|
|
@ -187,10 +162,8 @@ pub async fn run() -> Result<()> {
|
||||||
print_usage();
|
print_usage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_usage() {
|
fn print_usage() {
|
||||||
println!("BotServer Package Manager\n\nUSAGE:\n botserver <command> [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver start\n botserver stop\n botserver restart\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list");
|
println!("BotServer Package Manager\n\nUSAGE:\n botserver <command> [options]\n\nCOMMANDS:\n start Start all installed components\n stop Stop all running components\n restart Restart all components\n install <component> Install component\n remove <component> Remove component\n list List all components\n status <component> Check component status\n\nOPTIONS:\n --container Use container mode (LXC)\n --tenant <name> Specify tenant (default: 'default')\n\nCOMPONENTS:\n Required: drive cache tables llm\n Optional: email proxy directory alm alm-ci dns webmail meeting table-editor doc-editor desktop devtools bot system vector-db host\n\nEXAMPLES:\n botserver start\n botserver stop\n botserver restart\n botserver install email\n botserver install email --container --tenant myorg\n botserver remove email\n botserver list");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ComponentConfig {
|
pub struct ComponentConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
||||||
|
|
@ -9,48 +9,40 @@ use reqwest::Client;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
impl PackageManager {
|
impl PackageManager {
|
||||||
pub async fn install(&self, component_name: &str) -> Result<()> {
|
pub async fn install(&self, component_name: &str) -> Result<()> {
|
||||||
let component = self
|
let component = self
|
||||||
.components
|
.components
|
||||||
.get(component_name)
|
.get(component_name)
|
||||||
.context(format!("Component '{}' not found", component_name))?;
|
.context(format!("Component '{}' not found", component_name))?;
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Starting installation of component '{}' in {:?} mode",
|
"Starting installation of component '{}' in {:?} mode",
|
||||||
component_name,
|
component_name,
|
||||||
self.mode
|
self.mode
|
||||||
);
|
);
|
||||||
|
|
||||||
for dep in &component.dependencies {
|
for dep in &component.dependencies {
|
||||||
if !self.is_installed(dep) {
|
if !self.is_installed(dep) {
|
||||||
warn!("Installing missing dependency: {}", dep);
|
warn!("Installing missing dependency: {}", dep);
|
||||||
Box::pin(self.install(dep)).await?;
|
Box::pin(self.install(dep)).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.mode {
|
match self.mode {
|
||||||
InstallMode::Local => self.install_local(component).await?,
|
InstallMode::Local => self.install_local(component).await?,
|
||||||
InstallMode::Container => self.install_container(component)?,
|
InstallMode::Container => self.install_container(component)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Component '{}' installation completed successfully",
|
"Component '{}' installation completed successfully",
|
||||||
component_name
|
component_name
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn install_local(&self, component: &ComponentConfig) -> Result<()> {
|
pub async fn install_local(&self, component: &ComponentConfig) -> Result<()> {
|
||||||
trace!(
|
trace!(
|
||||||
"Installing component '{}' locally to {}",
|
"Installing component '{}' locally to {}",
|
||||||
component.name,
|
component.name,
|
||||||
self.base_path.display()
|
self.base_path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
self.create_directories(&component.name)?;
|
self.create_directories(&component.name)?;
|
||||||
|
|
||||||
let (pre_cmds, post_cmds) = match self.os_type {
|
let (pre_cmds, post_cmds) = match self.os_type {
|
||||||
OsType::Linux => (
|
OsType::Linux => (
|
||||||
&component.pre_install_cmds_linux,
|
&component.pre_install_cmds_linux,
|
||||||
|
|
@ -65,10 +57,8 @@ impl PackageManager {
|
||||||
&component.post_install_cmds_windows,
|
&component.post_install_cmds_windows,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.run_commands(pre_cmds, "local", &component.name)?;
|
self.run_commands(pre_cmds, "local", &component.name)?;
|
||||||
self.install_system_packages(component)?;
|
self.install_system_packages(component)?;
|
||||||
|
|
||||||
if let Some(url) = &component.download_url {
|
if let Some(url) = &component.download_url {
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
let name = component.name.clone();
|
let name = component.name.clone();
|
||||||
|
|
@ -76,8 +66,6 @@ impl PackageManager {
|
||||||
self.download_and_install(&url, &name, binary_name.as_deref())
|
self.download_and_install(&url, &name, binary_name.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process additional data downloads with progress bar
|
|
||||||
if !component.data_download_list.is_empty() {
|
if !component.data_download_list.is_empty() {
|
||||||
for url in &component.data_download_list {
|
for url in &component.data_download_list {
|
||||||
let filename = url.split('/').last().unwrap_or("download.tmp");
|
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?;
|
utils::download_file(url, output_path.to_str().unwrap()).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.run_commands(post_cmds, "local", &component.name)?;
|
self.run_commands(post_cmds, "local", &component.name)?;
|
||||||
trace!(
|
trace!(
|
||||||
"Component '{}' installation completed successfully",
|
"Component '{}' installation completed successfully",
|
||||||
component.name
|
component.name
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_container(&self, component: &ComponentConfig) -> Result<()> {
|
pub fn install_container(&self, component: &ComponentConfig) -> Result<()> {
|
||||||
let container_name = format!("{}-{}", self.tenant, component.name);
|
let container_name = format!("{}-{}", self.tenant, component.name);
|
||||||
|
|
||||||
let output = Command::new("lxc")
|
let output = Command::new("lxc")
|
||||||
.args(&[
|
.args(&[
|
||||||
"launch",
|
"launch",
|
||||||
|
|
@ -107,17 +91,14 @@ impl PackageManager {
|
||||||
"security.privileged=true",
|
"security.privileged=true",
|
||||||
])
|
])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"LXC container creation failed: {}",
|
"LXC container creation failed: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(15));
|
std::thread::sleep(std::time::Duration::from_secs(15));
|
||||||
self.exec_in_container(&container_name, "mkdir -p /opt/gbo/{bin,data,conf,logs}")?;
|
self.exec_in_container(&container_name, "mkdir -p /opt/gbo/{bin,data,conf,logs}")?;
|
||||||
|
|
||||||
let (pre_cmds, post_cmds) = match self.os_type {
|
let (pre_cmds, post_cmds) = match self.os_type {
|
||||||
OsType::Linux => (
|
OsType::Linux => (
|
||||||
&component.pre_install_cmds_linux,
|
&component.pre_install_cmds_linux,
|
||||||
|
|
@ -132,15 +113,12 @@ impl PackageManager {
|
||||||
&component.post_install_cmds_windows,
|
&component.post_install_cmds_windows,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.run_commands(pre_cmds, &container_name, &component.name)?;
|
self.run_commands(pre_cmds, &container_name, &component.name)?;
|
||||||
|
|
||||||
let packages = match self.os_type {
|
let packages = match self.os_type {
|
||||||
OsType::Linux => &component.linux_packages,
|
OsType::Linux => &component.linux_packages,
|
||||||
OsType::MacOS => &component.macos_packages,
|
OsType::MacOS => &component.macos_packages,
|
||||||
OsType::Windows => &component.windows_packages,
|
OsType::Windows => &component.windows_packages,
|
||||||
};
|
};
|
||||||
|
|
||||||
if !packages.is_empty() {
|
if !packages.is_empty() {
|
||||||
let pkg_list = packages.join(" ");
|
let pkg_list = packages.join(" ");
|
||||||
self.exec_in_container(
|
self.exec_in_container(
|
||||||
|
|
@ -148,7 +126,6 @@ impl PackageManager {
|
||||||
&format!("apt-get update && apt-get install -y {}", pkg_list),
|
&format!("apt-get update && apt-get install -y {}", pkg_list),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(url) = &component.download_url {
|
if let Some(url) = &component.download_url {
|
||||||
self.download_in_container(
|
self.download_in_container(
|
||||||
&container_name,
|
&container_name,
|
||||||
|
|
@ -157,15 +134,12 @@ impl PackageManager {
|
||||||
component.binary_name.as_deref(),
|
component.binary_name.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.run_commands(post_cmds, &container_name, &component.name)?;
|
self.run_commands(post_cmds, &container_name, &component.name)?;
|
||||||
|
|
||||||
self.exec_in_container(
|
self.exec_in_container(
|
||||||
&container_name,
|
&container_name,
|
||||||
"useradd --system --no-create-home --shell /bin/false gbuser",
|
"useradd --system --no-create-home --shell /bin/false gbuser",
|
||||||
)?;
|
)?;
|
||||||
self.mount_container_directories(&container_name, &component.name)?;
|
self.mount_container_directories(&container_name, &component.name)?;
|
||||||
|
|
||||||
if !component.exec_cmd.is_empty() {
|
if !component.exec_cmd.is_empty() {
|
||||||
self.create_container_service(
|
self.create_container_service(
|
||||||
&container_name,
|
&container_name,
|
||||||
|
|
@ -174,37 +148,30 @@ impl PackageManager {
|
||||||
&component.env_vars,
|
&component.env_vars,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setup_port_forwarding(&container_name, &component.ports)?;
|
self.setup_port_forwarding(&container_name, &component.ports)?;
|
||||||
trace!(
|
trace!(
|
||||||
"Container installation of '{}' completed in {}",
|
"Container installation of '{}' completed in {}",
|
||||||
component.name,
|
component.name,
|
||||||
container_name
|
container_name
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&self, component_name: &str) -> Result<()> {
|
pub fn remove(&self, component_name: &str) -> Result<()> {
|
||||||
let component = self
|
let component = self
|
||||||
.components
|
.components
|
||||||
.get(component_name)
|
.get(component_name)
|
||||||
.context(format!("Component '{}' not found", component_name))?;
|
.context(format!("Component '{}' not found", component_name))?;
|
||||||
|
|
||||||
match self.mode {
|
match self.mode {
|
||||||
InstallMode::Local => self.remove_local(component)?,
|
InstallMode::Local => self.remove_local(component)?,
|
||||||
InstallMode::Container => self.remove_container(component)?,
|
InstallMode::Container => self.remove_container(component)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_local(&self, component: &ComponentConfig) -> Result<()> {
|
pub fn remove_local(&self, component: &ComponentConfig) -> Result<()> {
|
||||||
let bin_path = self.base_path.join("bin").join(&component.name);
|
let bin_path = self.base_path.join("bin").join(&component.name);
|
||||||
let _ = std::fs::remove_dir_all(bin_path);
|
let _ = std::fs::remove_dir_all(bin_path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_container(&self, component: &ComponentConfig) -> Result<()> {
|
pub fn remove_container(&self, component: &ComponentConfig) -> Result<()> {
|
||||||
let container_name = format!("{}-{}", self.tenant, component.name);
|
let container_name = format!("{}-{}", self.tenant, component.name);
|
||||||
let _ = Command::new("lxc")
|
let _ = Command::new("lxc")
|
||||||
|
|
@ -213,21 +180,17 @@ impl PackageManager {
|
||||||
let output = Command::new("lxc")
|
let output = Command::new("lxc")
|
||||||
.args(&["delete", &container_name])
|
.args(&["delete", &container_name])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!(
|
warn!(
|
||||||
"Container deletion had issues: {}",
|
"Container deletion had issues: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list(&self) -> Vec<String> {
|
pub fn list(&self) -> Vec<String> {
|
||||||
self.components.keys().cloned().collect()
|
self.components.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_installed(&self, component_name: &str) -> bool {
|
pub fn is_installed(&self, component_name: &str) -> bool {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
InstallMode::Local => {
|
InstallMode::Local => {
|
||||||
|
|
@ -240,17 +203,14 @@ impl PackageManager {
|
||||||
.args(&["list", &container_name, "--format=json"])
|
.args(&["list", &container_name, "--format=json"])
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
!output_str.contains("\"name\":\"") || output_str.contains("\"status\":\"Stopped\"")
|
!output_str.contains("\"name\":\"") || output_str.contains("\"status\":\"Stopped\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_directories(&self, component: &str) -> Result<()> {
|
pub fn create_directories(&self, component: &str) -> Result<()> {
|
||||||
let dirs = ["bin", "data", "conf", "logs"];
|
let dirs = ["bin", "data", "conf", "logs"];
|
||||||
for dir in &dirs {
|
for dir in &dirs {
|
||||||
|
|
@ -260,37 +220,30 @@ impl PackageManager {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_system_packages(&self, component: &ComponentConfig) -> Result<()> {
|
pub fn install_system_packages(&self, component: &ComponentConfig) -> Result<()> {
|
||||||
let packages = match self.os_type {
|
let packages = match self.os_type {
|
||||||
OsType::Linux => &component.linux_packages,
|
OsType::Linux => &component.linux_packages,
|
||||||
OsType::MacOS => &component.macos_packages,
|
OsType::MacOS => &component.macos_packages,
|
||||||
OsType::Windows => &component.windows_packages,
|
OsType::Windows => &component.windows_packages,
|
||||||
};
|
};
|
||||||
|
|
||||||
if packages.is_empty() {
|
if packages.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Installing {} system packages for component '{}'",
|
"Installing {} system packages for component '{}'",
|
||||||
packages.len(),
|
packages.len(),
|
||||||
component.name
|
component.name
|
||||||
);
|
);
|
||||||
|
|
||||||
match self.os_type {
|
match self.os_type {
|
||||||
OsType::Linux => {
|
OsType::Linux => {
|
||||||
let output = Command::new("apt-get").args(&["update"]).output()?;
|
let output = Command::new("apt-get").args(&["update"]).output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!("apt-get update had issues");
|
warn!("apt-get update had issues");
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = Command::new("apt-get")
|
let output = Command::new("apt-get")
|
||||||
.args(&["install", "-y"])
|
.args(&["install", "-y"])
|
||||||
.args(packages)
|
.args(packages)
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!("Some packages may have failed to install");
|
warn!("Some packages may have failed to install");
|
||||||
}
|
}
|
||||||
|
|
@ -300,7 +253,6 @@ impl PackageManager {
|
||||||
.args(&["install"])
|
.args(&["install"])
|
||||||
.args(packages)
|
.args(packages)
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!("Homebrew installation had warnings");
|
warn!("Homebrew installation had warnings");
|
||||||
}
|
}
|
||||||
|
|
@ -309,10 +261,8 @@ impl PackageManager {
|
||||||
warn!("Windows package installation not implemented");
|
warn!("Windows package installation not implemented");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_and_install(
|
pub async fn download_and_install(
|
||||||
&self,
|
&self,
|
||||||
url: &str,
|
url: &str,
|
||||||
|
|
@ -321,21 +271,17 @@ impl PackageManager {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let bin_path = self.base_path.join("bin").join(component);
|
let bin_path = self.base_path.join("bin").join(component);
|
||||||
std::fs::create_dir_all(&bin_path)?;
|
std::fs::create_dir_all(&bin_path)?;
|
||||||
|
|
||||||
let filename = url.split('/').last().unwrap_or("download.tmp");
|
let filename = url.split('/').last().unwrap_or("download.tmp");
|
||||||
let temp_file = if filename.starts_with('/') {
|
let temp_file = if filename.starts_with('/') {
|
||||||
PathBuf::from(filename)
|
PathBuf::from(filename)
|
||||||
} else {
|
} else {
|
||||||
bin_path.join(filename)
|
bin_path.join(filename)
|
||||||
};
|
};
|
||||||
|
|
||||||
self.download_with_reqwest(url, &temp_file, component)
|
self.download_with_reqwest(url, &temp_file, component)
|
||||||
.await?;
|
.await?;
|
||||||
self.handle_downloaded_file(&temp_file, &bin_path, binary_name)?;
|
self.handle_downloaded_file(&temp_file, &bin_path, binary_name)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_with_reqwest(
|
pub async fn download_with_reqwest(
|
||||||
&self,
|
&self,
|
||||||
url: &str,
|
url: &str,
|
||||||
|
|
@ -344,14 +290,11 @@ impl PackageManager {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
const MAX_RETRIES: u32 = 3;
|
const MAX_RETRIES: u32 = 3;
|
||||||
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2);
|
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_secs(2);
|
||||||
|
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.user_agent("botserver-package-manager/1.0")
|
.user_agent("botserver-package-manager/1.0")
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
|
|
||||||
for attempt in 0..=MAX_RETRIES {
|
for attempt in 0..=MAX_RETRIES {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
trace!(
|
trace!(
|
||||||
|
|
@ -362,7 +305,6 @@ impl PackageManager {
|
||||||
);
|
);
|
||||||
std::thread::sleep(RETRY_DELAY * attempt);
|
std::thread::sleep(RETRY_DELAY * attempt);
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.attempt_reqwest_download(&client, url, temp_file).await {
|
match self.attempt_reqwest_download(&client, url, temp_file).await {
|
||||||
Ok(_size) => {
|
Ok(_size) => {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
|
|
@ -377,7 +319,6 @@ impl PackageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
Err(anyhow::anyhow!(
|
||||||
"Failed to download {} after {} attempts. Last error: {}",
|
"Failed to download {} after {} attempts. Last error: {}",
|
||||||
component,
|
component,
|
||||||
|
|
@ -385,7 +326,6 @@ impl PackageManager {
|
||||||
last_error.unwrap()
|
last_error.unwrap()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn attempt_reqwest_download(
|
pub async fn attempt_reqwest_download(
|
||||||
&self,
|
&self,
|
||||||
_client: &Client,
|
_client: &Client,
|
||||||
|
|
@ -396,12 +336,10 @@ impl PackageManager {
|
||||||
utils::download_file(url, output_path)
|
utils::download_file(url, output_path)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to download file using shared utility: {}", e))?;
|
.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 metadata = std::fs::metadata(temp_file).context("Failed to get file metadata")?;
|
||||||
let size = metadata.len();
|
let size = metadata.len();
|
||||||
Ok(size)
|
Ok(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_downloaded_file(
|
pub fn handle_downloaded_file(
|
||||||
&self,
|
&self,
|
||||||
temp_file: &PathBuf,
|
temp_file: &PathBuf,
|
||||||
|
|
@ -412,12 +350,10 @@ impl PackageManager {
|
||||||
if metadata.len() == 0 {
|
if metadata.len() == 0 {
|
||||||
return Err(anyhow::anyhow!("Downloaded file is empty"));
|
return Err(anyhow::anyhow!("Downloaded file is empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_extension = temp_file
|
let file_extension = temp_file
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|ext| ext.to_str())
|
.and_then(|ext| ext.to_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
match file_extension {
|
match file_extension {
|
||||||
"gz" | "tgz" => {
|
"gz" | "tgz" => {
|
||||||
self.extract_tar_gz(temp_file, bin_path)?;
|
self.extract_tar_gz(temp_file, bin_path)?;
|
||||||
|
|
@ -435,44 +371,36 @@ impl PackageManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_tar_gz(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> {
|
pub fn extract_tar_gz(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> {
|
||||||
let output = Command::new("tar")
|
let output = Command::new("tar")
|
||||||
.current_dir(bin_path)
|
.current_dir(bin_path)
|
||||||
.args(&["-xzf", temp_file.to_str().unwrap(), "--strip-components=1"])
|
.args(&["-xzf", temp_file.to_str().unwrap(), "--strip-components=1"])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"tar extraction failed: {}",
|
"tar extraction failed: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::remove_file(temp_file)?;
|
std::fs::remove_file(temp_file)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_zip(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> {
|
pub fn extract_zip(&self, temp_file: &PathBuf, bin_path: &PathBuf) -> Result<()> {
|
||||||
let output = Command::new("unzip")
|
let output = Command::new("unzip")
|
||||||
.current_dir(bin_path)
|
.current_dir(bin_path)
|
||||||
.args(&["-o", "-q", temp_file.to_str().unwrap()])
|
.args(&["-o", "-q", temp_file.to_str().unwrap()])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"unzip extraction failed: {}",
|
"unzip extraction failed: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::remove_file(temp_file)?;
|
std::fs::remove_file(temp_file)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn install_binary(
|
pub fn install_binary(
|
||||||
&self,
|
&self,
|
||||||
temp_file: &PathBuf,
|
temp_file: &PathBuf,
|
||||||
|
|
@ -484,7 +412,6 @@ impl PackageManager {
|
||||||
self.make_executable(&final_path)?;
|
self.make_executable(&final_path)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_executable(&self, path: &PathBuf) -> Result<()> {
|
pub fn make_executable(&self, path: &PathBuf) -> Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
|
@ -495,8 +422,6 @@ impl PackageManager {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn run_commands(&self, commands: &[String], target: &str, component: &str) -> Result<()> {
|
pub fn run_commands(&self, commands: &[String], target: &str, component: &str) -> Result<()> {
|
||||||
let bin_path = if target == "local" {
|
let bin_path = if target == "local" {
|
||||||
self.base_path.join("bin").join(component)
|
self.base_path.join("bin").join(component)
|
||||||
|
|
@ -518,14 +443,12 @@ impl PackageManager {
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from("/opt/gbo/logs")
|
PathBuf::from("/opt/gbo/logs")
|
||||||
};
|
};
|
||||||
|
|
||||||
for cmd in commands {
|
for cmd in commands {
|
||||||
let rendered_cmd = cmd
|
let rendered_cmd = cmd
|
||||||
.replace("{{BIN_PATH}}", &bin_path.to_string_lossy())
|
.replace("{{BIN_PATH}}", &bin_path.to_string_lossy())
|
||||||
.replace("{{DATA_PATH}}", &data_path.to_string_lossy())
|
.replace("{{DATA_PATH}}", &data_path.to_string_lossy())
|
||||||
.replace("{{CONF_PATH}}", &conf_path.to_string_lossy())
|
.replace("{{CONF_PATH}}", &conf_path.to_string_lossy())
|
||||||
.replace("{{LOGS_PATH}}", &logs_path.to_string_lossy());
|
.replace("{{LOGS_PATH}}", &logs_path.to_string_lossy());
|
||||||
|
|
||||||
if target == "local" {
|
if target == "local" {
|
||||||
trace!("Executing command: {}", rendered_cmd);
|
trace!("Executing command: {}", rendered_cmd);
|
||||||
let child = Command::new("bash")
|
let child = Command::new("bash")
|
||||||
|
|
@ -535,14 +458,12 @@ impl PackageManager {
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("Failed to spawn command for component '{}'", component)
|
format!("Failed to spawn command for component '{}'", component)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let output = child.wait_with_output().with_context(|| {
|
let output = child.wait_with_output().with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Failed while waiting for command to finish for component '{}'",
|
"Failed while waiting for command to finish for component '{}'",
|
||||||
component
|
component
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
error!(
|
error!(
|
||||||
"Command had non-zero exit: {}",
|
"Command had non-zero exit: {}",
|
||||||
|
|
@ -553,25 +474,20 @@ impl PackageManager {
|
||||||
self.exec_in_container(target, &rendered_cmd)?;
|
self.exec_in_container(target, &rendered_cmd)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec_in_container(&self, container: &str, command: &str) -> Result<()> {
|
pub fn exec_in_container(&self, container: &str, command: &str) -> Result<()> {
|
||||||
let output = Command::new("lxc")
|
let output = Command::new("lxc")
|
||||||
.args(&["exec", container, "--", "bash", "-c", command])
|
.args(&["exec", container, "--", "bash", "-c", command])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!(
|
warn!(
|
||||||
"Container command failed: {}",
|
"Container command failed: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_in_container(
|
pub fn download_in_container(
|
||||||
&self,
|
&self,
|
||||||
container: &str,
|
container: &str,
|
||||||
|
|
@ -581,7 +497,6 @@ impl PackageManager {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let download_cmd = format!("wget -O /tmp/download.tmp {}", url);
|
let download_cmd = format!("wget -O /tmp/download.tmp {}", url);
|
||||||
self.exec_in_container(container, &download_cmd)?;
|
self.exec_in_container(container, &download_cmd)?;
|
||||||
|
|
||||||
if url.ends_with(".tar.gz") || url.ends_with(".tgz") {
|
if url.ends_with(".tar.gz") || url.ends_with(".tgz") {
|
||||||
self.exec_in_container(container, "tar -xzf /tmp/download.tmp -C /opt/gbo/bin")?;
|
self.exec_in_container(container, "tar -xzf /tmp/download.tmp -C /opt/gbo/bin")?;
|
||||||
} else if url.ends_with(".zip") {
|
} else if url.ends_with(".zip") {
|
||||||
|
|
@ -593,25 +508,19 @@ impl PackageManager {
|
||||||
);
|
);
|
||||||
self.exec_in_container(container, &mv_cmd)?;
|
self.exec_in_container(container, &mv_cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.exec_in_container(container, "rm -f /tmp/download.tmp")?;
|
self.exec_in_container(container, "rm -f /tmp/download.tmp")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mount_container_directories(&self, container: &str, component: &str) -> Result<()> {
|
pub fn mount_container_directories(&self, container: &str, component: &str) -> Result<()> {
|
||||||
let host_base = format!("/opt/gbo/tenants/{}/{}", self.tenant, component);
|
let host_base = format!("/opt/gbo/tenants/{}/{}", self.tenant, component);
|
||||||
|
|
||||||
for dir in &["data", "conf", "logs"] {
|
for dir in &["data", "conf", "logs"] {
|
||||||
let host_path = format!("{}/{}", host_base, dir);
|
let host_path = format!("{}/{}", host_base, dir);
|
||||||
std::fs::create_dir_all(&host_path)?;
|
std::fs::create_dir_all(&host_path)?;
|
||||||
|
|
||||||
let device_name = format!("{}-{}", component, dir);
|
let device_name = format!("{}-{}", component, dir);
|
||||||
let container_path = format!("/opt/gbo/{}", dir);
|
let container_path = format!("/opt/gbo/{}", dir);
|
||||||
|
|
||||||
let _ = Command::new("lxc")
|
let _ = Command::new("lxc")
|
||||||
.args(&["config", "device", "remove", container, &device_name])
|
.args(&["config", "device", "remove", container, &device_name])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let output = Command::new("lxc")
|
let output = Command::new("lxc")
|
||||||
.args(&[
|
.args(&[
|
||||||
"config",
|
"config",
|
||||||
|
|
@ -624,11 +533,9 @@ impl PackageManager {
|
||||||
&format!("path={}", container_path),
|
&format!("path={}", container_path),
|
||||||
])
|
])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!("Failed to mount {} in container {}", dir, container);
|
warn!("Failed to mount {} in container {}", dir, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Mounted {} to {} in container {}",
|
"Mounted {} to {} in container {}",
|
||||||
host_path,
|
host_path,
|
||||||
|
|
@ -636,10 +543,8 @@ impl PackageManager {
|
||||||
container
|
container
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_container_service(
|
pub fn create_container_service(
|
||||||
&self,
|
&self,
|
||||||
container: &str,
|
container: &str,
|
||||||
|
|
@ -652,7 +557,6 @@ impl PackageManager {
|
||||||
.replace("{{DATA_PATH}}", "/opt/gbo/data")
|
.replace("{{DATA_PATH}}", "/opt/gbo/data")
|
||||||
.replace("{{CONF_PATH}}", "/opt/gbo/conf")
|
.replace("{{CONF_PATH}}", "/opt/gbo/conf")
|
||||||
.replace("{{LOGS_PATH}}", "/opt/gbo/logs");
|
.replace("{{LOGS_PATH}}", "/opt/gbo/logs");
|
||||||
|
|
||||||
let mut env_section = String::new();
|
let mut env_section = String::new();
|
||||||
for (key, value) in env_vars {
|
for (key, value) in env_vars {
|
||||||
let rendered_value = value
|
let rendered_value = value
|
||||||
|
|
@ -662,15 +566,12 @@ impl PackageManager {
|
||||||
.replace("{{LOGS_PATH}}", "/opt/gbo/logs");
|
.replace("{{LOGS_PATH}}", "/opt/gbo/logs");
|
||||||
env_section.push_str(&format!("Environment={}={}\n", key, rendered_value));
|
env_section.push_str(&format!("Environment={}={}\n", key, rendered_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
let service_content = format!(
|
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",
|
"[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
|
component, env_section, rendered_cmd
|
||||||
);
|
);
|
||||||
|
|
||||||
let service_file = format!("/tmp/{}.service", component);
|
let service_file = format!("/tmp/{}.service", component);
|
||||||
std::fs::write(&service_file, &service_content)?;
|
std::fs::write(&service_file, &service_content)?;
|
||||||
|
|
||||||
let output = Command::new("lxc")
|
let output = Command::new("lxc")
|
||||||
.args(&[
|
.args(&[
|
||||||
"file",
|
"file",
|
||||||
|
|
@ -679,33 +580,26 @@ impl PackageManager {
|
||||||
&format!("{}/etc/systemd/system/{}.service", container, component),
|
&format!("{}/etc/systemd/system/{}.service", container, component),
|
||||||
])
|
])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!("Failed to push service file to container");
|
warn!("Failed to push service file to container");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.exec_in_container(container, "systemctl daemon-reload")?;
|
self.exec_in_container(container, "systemctl daemon-reload")?;
|
||||||
self.exec_in_container(container, &format!("systemctl enable {}", component))?;
|
self.exec_in_container(container, &format!("systemctl enable {}", component))?;
|
||||||
self.exec_in_container(container, &format!("systemctl start {}", component))?;
|
self.exec_in_container(container, &format!("systemctl start {}", component))?;
|
||||||
|
|
||||||
std::fs::remove_file(&service_file)?;
|
std::fs::remove_file(&service_file)?;
|
||||||
trace!(
|
trace!(
|
||||||
"Created and started service in container {}: {}",
|
"Created and started service in container {}: {}",
|
||||||
container,
|
container,
|
||||||
component
|
component
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup_port_forwarding(&self, container: &str, ports: &[u16]) -> Result<()> {
|
pub fn setup_port_forwarding(&self, container: &str, ports: &[u16]) -> Result<()> {
|
||||||
for port in ports {
|
for port in ports {
|
||||||
let device_name = format!("port-{}", port);
|
let device_name = format!("port-{}", port);
|
||||||
|
|
||||||
let _ = Command::new("lxc")
|
let _ = Command::new("lxc")
|
||||||
.args(&["config", "device", "remove", container, &device_name])
|
.args(&["config", "device", "remove", container, &device_name])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
let output = Command::new("lxc")
|
let output = Command::new("lxc")
|
||||||
.args(&[
|
.args(&[
|
||||||
"config",
|
"config",
|
||||||
|
|
@ -718,18 +612,15 @@ impl PackageManager {
|
||||||
&format!("connect=tcp:127.0.0.1:{}", port),
|
&format!("connect=tcp:127.0.0.1:{}", port),
|
||||||
])
|
])
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
warn!("Failed to setup port forwarding for port {}", port);
|
warn!("Failed to setup port forwarding for port {}", port);
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Port forwarding configured: {} -> container {}",
|
"Port forwarding configured: {} -> container {}",
|
||||||
port,
|
port,
|
||||||
container
|
container
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,24 @@
|
||||||
pub mod component;
|
pub mod component;
|
||||||
pub mod installer;
|
pub mod installer;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
|
|
||||||
pub use installer::PackageManager;
|
pub use installer::PackageManager;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod facade;
|
pub mod facade;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum InstallMode {
|
pub enum InstallMode {
|
||||||
Local,
|
Local,
|
||||||
Container,
|
Container,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum OsType {
|
pub enum OsType {
|
||||||
Linux,
|
Linux,
|
||||||
MacOS,
|
MacOS,
|
||||||
Windows,
|
Windows,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ComponentInfo {
|
pub struct ComponentInfo {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub termination_command: &'static str,
|
pub termination_command: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_all_components() -> Vec<ComponentInfo> {
|
pub fn get_all_components() -> Vec<ComponentInfo> {
|
||||||
vec![
|
vec![
|
||||||
ComponentInfo {
|
ComponentInfo {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::package_manager::OsType;
|
use crate::package_manager::OsType;
|
||||||
|
|
||||||
pub fn detect_os() -> OsType {
|
pub fn detect_os() -> OsType {
|
||||||
if cfg!(target_os = "linux") {
|
if cfg!(target_os = "linux") {
|
||||||
OsType::Linux
|
OsType::Linux
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,22 @@
|
||||||
//! Tests for package manager module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_package_manager_module() {
|
fn test_package_manager_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic package manager module test");
|
assert!(true, "Basic package manager module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_interface() {
|
fn test_cli_interface() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "CLI interface placeholder test");
|
assert!(true, "CLI interface placeholder test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_component_management() {
|
fn test_component_management() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Component management placeholder test");
|
assert!(true, "Component management placeholder test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_os_specific() {
|
fn test_os_specific() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,20 @@
|
||||||
use boa_engine::{Context, JsValue, Source};
|
use boa_engine::{Context, JsValue, Source};
|
||||||
|
|
||||||
fn compile_riot_component(riot_code: &str) -> Result<JsValue, Box<dyn std::error::Error>> {
|
fn compile_riot_component(riot_code: &str) -> Result<JsValue, Box<dyn std::error::Error>> {
|
||||||
let mut context = Context::default();
|
let mut context = Context::default();
|
||||||
|
let compiler = include_str!("riot_compiler.js");
|
||||||
let compiler = include_str!("riot_compiler.js"); // Your Riot compiler logic
|
|
||||||
|
|
||||||
context.eval(Source::from_bytes(compiler))?;
|
context.eval(Source::from_bytes(compiler))?;
|
||||||
|
|
||||||
let result = context.eval(Source::from_bytes(&format!(
|
let result = context.eval(Source::from_bytes(&format!(
|
||||||
"compileRiot(`{}`)",
|
"compileRiot(`{}`)",
|
||||||
riot_code.replace('`', "\\`")
|
riot_code.replace('`', "\\`")
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let riot_component = r#"
|
let riot_component = r#"
|
||||||
<todo-item>
|
<todo-item>
|
||||||
<h3>{ props.title }</h3>
|
<h3>{ props.title }</h3>
|
||||||
<input if="{ !props.done }" type="checkbox" onclick="{ toggle }">
|
<input if="{ !props.done }" type="checkbox" onclick="{ toggle }">
|
||||||
<span if="{ props.done }">✓ Done</span>
|
<span if="{ props.done }">✓ Done</span>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
toggle() {
|
toggle() {
|
||||||
|
|
@ -31,7 +24,6 @@ fn main() {
|
||||||
</script>
|
</script>
|
||||||
</todo-item>
|
</todo-item>
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
match compile_riot_component(riot_component) {
|
match compile_riot_component(riot_component) {
|
||||||
Ok(compiled) => println!("Compiled: {:?}", compiled),
|
Ok(compiled) => println!("Compiled: {:?}", compiled),
|
||||||
Err(e) => eprintln!("Compilation failed: {}", e),
|
Err(e) => eprintln!("Compilation failed: {}", e),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for Riot compiler module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_riot_compiler_module() {
|
fn test_riot_compiler_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic Riot compiler module test");
|
assert!(true, "Basic Riot compiler module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_compilation() {
|
fn test_compilation() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -285,8 +285,8 @@ impl SessionManager {
|
||||||
let mut history: Vec<(String, String)> = Vec::new();
|
let mut history: Vec<(String, String)> = Vec::new();
|
||||||
for (other_role, content) in messages {
|
for (other_role, content) in messages {
|
||||||
let role_str = match other_role {
|
let role_str = match other_role {
|
||||||
1 => "user".to_string(),
|
1 => "human".to_string(),
|
||||||
2 => "assistant".to_string(),
|
2 => "bot".to_string(),
|
||||||
3 => "system".to_string(),
|
3 => "system".to_string(),
|
||||||
9 => "compact".to_string(),
|
9 => "compact".to_string(),
|
||||||
_ => "unknown".to_string(),
|
_ => "unknown".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for session module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_session_module() {
|
fn test_session_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic session module test");
|
assert!(true, "Basic session module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_session_management() {
|
fn test_session_management() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ use chrono::{DateTime, Utc};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum TriggerKind {
|
pub enum TriggerKind {
|
||||||
Scheduled = 0,
|
Scheduled = 0,
|
||||||
|
|
@ -11,7 +9,6 @@ pub enum TriggerKind {
|
||||||
TableInsert = 2,
|
TableInsert = 2,
|
||||||
TableDelete = 3,
|
TableDelete = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TriggerKind {
|
impl TriggerKind {
|
||||||
pub fn _from_i32(value: i32) -> Option<Self> {
|
pub fn _from_i32(value: i32) -> Option<Self> {
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -23,7 +20,6 @@ impl TriggerKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Queryable, Serialize, Deserialize, Identifiable)]
|
#[derive(Debug, Queryable, Serialize, Deserialize, Identifiable)]
|
||||||
#[diesel(table_name = system_automations)]
|
#[diesel(table_name = system_automations)]
|
||||||
pub struct Automation {
|
pub struct Automation {
|
||||||
|
|
@ -36,7 +32,6 @@ pub struct Automation {
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
pub last_triggered: Option<chrono::DateTime<chrono::Utc>>,
|
pub last_triggered: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Selectable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Selectable)]
|
||||||
#[diesel(table_name = user_sessions)]
|
#[diesel(table_name = user_sessions)]
|
||||||
pub struct UserSession {
|
pub struct UserSession {
|
||||||
|
|
@ -49,10 +44,6 @@ pub struct UserSession {
|
||||||
pub created_at: chrono::DateTime<Utc>,
|
pub created_at: chrono::DateTime<Utc>,
|
||||||
pub updated_at: chrono::DateTime<Utc>,
|
pub updated_at: chrono::DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct UserMessage {
|
pub struct UserMessage {
|
||||||
pub bot_id: String,
|
pub bot_id: String,
|
||||||
|
|
@ -65,13 +56,11 @@ pub struct UserMessage {
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
pub context_name: Option<String>,
|
pub context_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Suggestion {
|
pub struct Suggestion {
|
||||||
pub text: String, // The button text that will be sent as message
|
pub text: String,
|
||||||
pub context: String, // The context name to set when clicked
|
pub context: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct BotResponse {
|
pub struct BotResponse {
|
||||||
pub bot_id: String,
|
pub bot_id: String,
|
||||||
|
|
@ -87,7 +76,6 @@ pub struct BotResponse {
|
||||||
pub context_length: usize,
|
pub context_length: usize,
|
||||||
pub context_max_length: usize,
|
pub context_max_length: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotResponse {
|
impl BotResponse {
|
||||||
pub fn from_string_ids(
|
pub fn from_string_ids(
|
||||||
bot_id: &str,
|
bot_id: &str,
|
||||||
|
|
@ -112,7 +100,6 @@ impl BotResponse {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Insertable)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable, Insertable)]
|
||||||
#[diesel(table_name = bot_memories)]
|
#[diesel(table_name = bot_memories)]
|
||||||
pub struct BotMemory {
|
pub struct BotMemory {
|
||||||
|
|
@ -123,7 +110,6 @@ pub struct BotMemory {
|
||||||
pub created_at: chrono::DateTime<Utc>,
|
pub created_at: chrono::DateTime<Utc>,
|
||||||
pub updated_at: chrono::DateTime<Utc>,
|
pub updated_at: chrono::DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod schema {
|
pub mod schema {
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
organizations (org_id) {
|
organizations (org_id) {
|
||||||
|
|
@ -133,7 +119,6 @@ pub mod schema {
|
||||||
created_at -> Timestamptz,
|
created_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
bots (id) {
|
bots (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -149,7 +134,6 @@ pub mod schema {
|
||||||
tenant_id -> Nullable<Uuid>,
|
tenant_id -> Nullable<Uuid>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
system_automations (id) {
|
system_automations (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -162,7 +146,6 @@ pub mod schema {
|
||||||
last_triggered -> Nullable<Timestamptz>,
|
last_triggered -> Nullable<Timestamptz>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
user_sessions (id) {
|
user_sessions (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -175,7 +158,6 @@ pub mod schema {
|
||||||
updated_at -> Timestamptz,
|
updated_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
message_history (id) {
|
message_history (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -188,7 +170,6 @@ pub mod schema {
|
||||||
created_at -> Timestamptz,
|
created_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
users (id) {
|
users (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -200,7 +181,6 @@ pub mod schema {
|
||||||
updated_at -> Timestamptz,
|
updated_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
clicks (id) {
|
clicks (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -209,7 +189,6 @@ pub mod schema {
|
||||||
updated_at -> Timestamptz,
|
updated_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
bot_memories (id) {
|
bot_memories (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
|
|
@ -220,7 +199,6 @@ pub mod schema {
|
||||||
updated_at -> Timestamptz,
|
updated_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
kb_documents (id) {
|
kb_documents (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
|
|
@ -238,7 +216,6 @@ pub mod schema {
|
||||||
updated_at -> Text,
|
updated_at -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
basic_tools (id) {
|
basic_tools (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
|
|
@ -255,7 +232,6 @@ pub mod schema {
|
||||||
updated_at -> Text,
|
updated_at -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
kb_collections (id) {
|
kb_collections (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
|
|
@ -270,7 +246,6 @@ pub mod schema {
|
||||||
updated_at -> Text,
|
updated_at -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
user_kb_associations (id) {
|
user_kb_associations (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
|
|
@ -283,7 +258,6 @@ pub mod schema {
|
||||||
updated_at -> Text,
|
updated_at -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
session_tool_associations (id) {
|
session_tool_associations (id) {
|
||||||
id -> Text,
|
id -> Text,
|
||||||
|
|
@ -292,21 +266,17 @@ pub mod schema {
|
||||||
added_at -> Text,
|
added_at -> Text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
bot_configuration (id) {
|
bot_configuration (id) {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
bot_id -> Uuid,
|
bot_id -> Uuid,
|
||||||
config_key -> Text,
|
config_key -> Text,
|
||||||
config_value -> Text,
|
config_value -> Text,
|
||||||
|
|
||||||
is_encrypted -> Bool,
|
is_encrypted -> Bool,
|
||||||
|
|
||||||
config_type -> Text,
|
config_type -> Text,
|
||||||
created_at -> Timestamptz,
|
created_at -> Timestamptz,
|
||||||
updated_at -> Timestamptz,
|
updated_at -> Timestamptz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use schema::*;
|
pub use schema::*;
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
// @generated automatically by Diesel CLI.
|
|
||||||
|
|
@ -1,28 +1,22 @@
|
||||||
//! Tests for shared module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_shared_module() {
|
fn test_shared_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic shared module test");
|
assert!(true, "Basic shared module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_models() {
|
fn test_models() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Models placeholder test");
|
assert!(true, "Models placeholder test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_state() {
|
fn test_state() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "State placeholder test");
|
assert!(true, "State placeholder test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils() {
|
fn test_utils() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,12 @@ use tokio::io::AsyncWriteExt;
|
||||||
use aws_sdk_s3::{Client as S3Client, config::Builder as S3ConfigBuilder};
|
use aws_sdk_s3::{Client as S3Client, config::Builder as S3ConfigBuilder};
|
||||||
use aws_config::BehaviorVersion;
|
use aws_config::BehaviorVersion;
|
||||||
use crate::config::DriveConfig;
|
use crate::config::DriveConfig;
|
||||||
|
|
||||||
pub async fn create_s3_operator(config: &DriveConfig) -> Result<S3Client, Box<dyn std::error::Error>> {
|
pub async fn create_s3_operator(config: &DriveConfig) -> Result<S3Client, Box<dyn std::error::Error>> {
|
||||||
let endpoint = if !config.server.ends_with('/') {
|
let endpoint = if !config.server.ends_with('/') {
|
||||||
format!("{}/", config.server)
|
format!("{}/", config.server)
|
||||||
} else {
|
} else {
|
||||||
config.server.clone()
|
config.server.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let base_config = aws_config::defaults(BehaviorVersion::latest())
|
let base_config = aws_config::defaults(BehaviorVersion::latest())
|
||||||
.endpoint_url(endpoint)
|
.endpoint_url(endpoint)
|
||||||
.region("auto")
|
.region("auto")
|
||||||
|
|
@ -38,14 +36,11 @@ pub async fn create_s3_operator(config: &DriveConfig) -> Result<S3Client, Box<dy
|
||||||
)
|
)
|
||||||
.load()
|
.load()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let s3_config = S3ConfigBuilder::from(&base_config)
|
let s3_config = S3ConfigBuilder::from(&base_config)
|
||||||
.force_path_style(true)
|
.force_path_style(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Ok(S3Client::from_conf(s3_config))
|
Ok(S3Client::from_conf(s3_config))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
pub fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
||||||
match value {
|
match value {
|
||||||
Value::Null => Dynamic::UNIT,
|
Value::Null => Dynamic::UNIT,
|
||||||
|
|
@ -72,7 +67,6 @@ pub fn json_value_to_dynamic(value: &Value) -> Dynamic {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_array(value: Dynamic) -> Array {
|
pub fn to_array(value: Dynamic) -> Array {
|
||||||
if value.is_array() {
|
if value.is_array() {
|
||||||
value.cast::<Array>()
|
value.cast::<Array>()
|
||||||
|
|
@ -82,7 +76,6 @@ pub fn to_array(value: Dynamic) -> Array {
|
||||||
Array::from([value])
|
Array::from([value])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn download_file(url: &str, output_path: &str) -> Result<(), anyhow::Error> {
|
pub async fn download_file(url: &str, output_path: &str) -> Result<(), anyhow::Error> {
|
||||||
let url = url.to_string();
|
let url = url.to_string();
|
||||||
let output_path = output_path.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?
|
download_handle.await?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
pub fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn Error>> {
|
||||||
let parts: Vec<&str> = filter_str.split('=').collect();
|
let parts: Vec<&str> = filter_str.split('=').collect();
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
|
|
@ -132,27 +124,22 @@ pub fn parse_filter(filter_str: &str) -> Result<(String, Vec<String>), Box<dyn E
|
||||||
}
|
}
|
||||||
Ok((format!("{} = $1", column), vec![value.to_string()]))
|
Ok((format!("{} = $1", column), vec![value.to_string()]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn estimate_token_count(text: &str) -> usize {
|
pub fn estimate_token_count(text: &str) -> usize {
|
||||||
let char_count = text.chars().count();
|
let char_count = text.chars().count();
|
||||||
(char_count / 4).max(1)
|
(char_count / 4).max(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn establish_pg_connection() -> Result<PgConnection> {
|
pub fn establish_pg_connection() -> Result<PgConnection> {
|
||||||
let database_url = std::env::var("DATABASE_URL").unwrap();
|
let database_url = std::env::var("DATABASE_URL").unwrap();
|
||||||
PgConnection::establish(&database_url)
|
PgConnection::establish(&database_url)
|
||||||
.with_context(|| format!("Failed to connect to database at {}", database_url))
|
.with_context(|| format!("Failed to connect to database at {}", database_url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||||
pub fn create_conn() -> Result<DbPool, r2d2::Error> {
|
pub fn create_conn() -> Result<DbPool, r2d2::Error> {
|
||||||
let database_url = std::env::var("DATABASE_URL")
|
let database_url = std::env::var("DATABASE_URL")
|
||||||
.unwrap_or_else(|_| "postgres://gbuser:@localhost:5432/botserver".to_string());
|
.unwrap();
|
||||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
Pool::builder().build(manager)
|
Pool::builder().build(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
|
pub fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
|
||||||
if let Some(stripped) = url.strip_prefix("postgres://") {
|
if let Some(stripped) = url.strip_prefix("postgres://") {
|
||||||
let parts: Vec<&str> = stripped.split('@').collect();
|
let parts: Vec<&str> = stripped.split('@').collect();
|
||||||
|
|
@ -173,11 +160,5 @@ pub fn parse_database_url(url: &str) -> (String, String, String, u32, String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(
|
("".to_string(), "".to_string(), "".to_string(), 5432, "".to_string())
|
||||||
"gbuser".to_string(),
|
|
||||||
"".to_string(),
|
|
||||||
"localhost".to_string(),
|
|
||||||
5432,
|
|
||||||
"botserver".to_string(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,9 @@
|
||||||
//! Common test utilities for the botserver project
|
|
||||||
|
|
||||||
use std::sync::Once;
|
use std::sync::Once;
|
||||||
|
|
||||||
static INIT: Once = Once::new();
|
static INIT: Once = Once::new();
|
||||||
|
|
||||||
/// Setup function to be called at the beginning of each test module
|
|
||||||
pub fn setup() {
|
pub fn setup() {
|
||||||
INIT.call_once(|| {
|
INIT.call_once(|| {
|
||||||
// Initialize any test configuration here
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple assertion macro for better test error messages
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! assert_ok {
|
macro_rules! assert_ok {
|
||||||
($expr:expr) => {
|
($expr:expr) => {
|
||||||
|
|
@ -21,8 +13,6 @@ macro_rules! assert_ok {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple assertion macro for error cases
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! assert_err {
|
macro_rules! assert_err {
|
||||||
($expr:expr) => {
|
($expr:expr) => {
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,19 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tauri::{Emitter, Window};
|
use tauri::{Emitter, Window};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct FileItem {
|
pub struct FileItem {
|
||||||
name: String,
|
name: String,
|
||||||
path: String,
|
path: String,
|
||||||
is_dir: bool,
|
is_dir: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
let base_path = Path::new(path);
|
let base_path = Path::new(path);
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
|
||||||
if !base_path.exists() {
|
if !base_path.exists() {
|
||||||
return Err("Path does not exist".into());
|
return Err("Path does not exist".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? {
|
for entry in fs::read_dir(base_path).map_err(|e| e.to_string())? {
|
||||||
let entry = entry.map_err(|e| e.to_string())?;
|
let entry = entry.map_err(|e| e.to_string())?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
@ -27,15 +23,12 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
files.push(FileItem {
|
files.push(FileItem {
|
||||||
name,
|
name,
|
||||||
path: path.to_str().unwrap_or("").to_string(),
|
path: path.to_str().unwrap_or("").to_string(),
|
||||||
is_dir: path.is_dir(),
|
is_dir: path.is_dir(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort directories first, then files
|
|
||||||
files.sort_by(|a, b| {
|
files.sort_by(|a, b| {
|
||||||
if a.is_dir && !b.is_dir {
|
if a.is_dir && !b.is_dir {
|
||||||
std::cmp::Ordering::Less
|
std::cmp::Ordering::Less
|
||||||
|
|
@ -45,51 +38,39 @@ pub fn list_files(path: &str) -> Result<Vec<FileItem>, String> {
|
||||||
a.name.cmp(&b.name)
|
a.name.cmp(&b.name)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn upload_file(window: Window, src_path: String, dest_path: String) -> Result<(), String> {
|
pub async fn upload_file(window: Window, src_path: String, dest_path: String) -> Result<(), String> {
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
let src = PathBuf::from(&src_path);
|
let src = PathBuf::from(&src_path);
|
||||||
let dest_dir = PathBuf::from(&dest_path);
|
let dest_dir = PathBuf::from(&dest_path);
|
||||||
let dest = dest_dir.join(src.file_name().ok_or("Invalid source file")?);
|
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() {
|
if !dest_dir.exists() {
|
||||||
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
|
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 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 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 file_size = source_file.metadata().map_err(|e| e.to_string())?.len();
|
||||||
let mut buffer = [0; 8192];
|
let mut buffer = [0; 8192];
|
||||||
let mut total_read = 0;
|
let mut total_read = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
|
let bytes_read = source_file.read(&mut buffer).map_err(|e| e.to_string())?;
|
||||||
if bytes_read == 0 {
|
if bytes_read == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
dest_file
|
dest_file
|
||||||
.write_all(&buffer[..bytes_read])
|
.write_all(&buffer[..bytes_read])
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
total_read += bytes_read as u64;
|
total_read += bytes_read as u64;
|
||||||
|
|
||||||
let progress = (total_read as f64 / file_size as f64) * 100.0;
|
let progress = (total_read as f64 / file_size as f64) * 100.0;
|
||||||
window
|
window
|
||||||
.emit("upload_progress", progress)
|
.emit("upload_progress", progress)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
pub fn create_folder(path: String, name: String) -> Result<(), String> {
|
||||||
let full_path = Path::new(&path).join(&name);
|
let full_path = Path::new(&path).join(&name);
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@ use std::time::{Duration, Instant};
|
||||||
use notify_rust::Notification;
|
use notify_rust::Notification;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
// App state
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AppState {
|
struct AppState {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -26,13 +24,11 @@ struct AppState {
|
||||||
show_about_dialog: bool,
|
show_about_dialog: bool,
|
||||||
current_screen: Screen,
|
current_screen: Screen,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Screen {
|
enum Screen {
|
||||||
Main,
|
Main,
|
||||||
Status,
|
Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct RcloneConfig {
|
struct RcloneConfig {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -41,7 +37,6 @@ struct RcloneConfig {
|
||||||
access_key: String,
|
access_key: String,
|
||||||
secret_key: String,
|
secret_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct SyncStatus {
|
struct SyncStatus {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -51,7 +46,6 @@ struct SyncStatus {
|
||||||
errors: usize,
|
errors: usize,
|
||||||
last_updated: String,
|
last_updated: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
NameChanged(String),
|
NameChanged(String),
|
||||||
|
|
@ -67,15 +61,12 @@ enum Message {
|
||||||
BackToMain,
|
BackToMain,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
dioxus_desktop::launch(app);
|
dioxus_desktop::launch(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app(cx: Scope) -> Element {
|
fn app(cx: Scope) -> Element {
|
||||||
let window = use_window();
|
let window = use_window();
|
||||||
window.set_inner_size(LogicalSize::new(800, 600));
|
window.set_inner_size(LogicalSize::new(800, 600));
|
||||||
|
|
||||||
let state = use_ref(cx, || AppState {
|
let state = use_ref(cx, || AppState {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
access_key: String::new(),
|
access_key: String::new(),
|
||||||
|
|
@ -88,27 +79,20 @@ fn app(cx: Scope) -> Element {
|
||||||
show_about_dialog: false,
|
show_about_dialog: false,
|
||||||
current_screen: Screen::Main,
|
current_screen: Screen::Main,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Monitor sync status
|
|
||||||
use_future( async move {
|
use_future( async move {
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut last_check = Instant::now();
|
let mut last_check = Instant::now();
|
||||||
let check_interval = Duration::from_secs(5);
|
let check_interval = Duration::from_secs(5);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
if !*state.read().sync_active.lock().unwrap() {
|
if !*state.read().sync_active.lock().unwrap() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if last_check.elapsed() < check_interval {
|
if last_check.elapsed() < check_interval {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_check = Instant::now();
|
last_check = Instant::now();
|
||||||
|
|
||||||
match read_rclone_configs() {
|
match read_rclone_configs() {
|
||||||
Ok(configs) => {
|
Ok(configs) => {
|
||||||
let mut new_statuses = Vec::new();
|
let mut new_statuses = Vec::new();
|
||||||
|
|
@ -126,11 +110,9 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
div {
|
div {
|
||||||
class: "app",
|
class: "app",
|
||||||
// Main menu bar
|
|
||||||
div {
|
div {
|
||||||
class: "menu-bar",
|
class: "menu-bar",
|
||||||
button {
|
button {
|
||||||
|
|
@ -142,8 +124,6 @@ fn app(cx: Scope) -> Element {
|
||||||
"About"
|
"About"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main content
|
|
||||||
{match state.read().current_screen {
|
{match state.read().current_screen {
|
||||||
Screen::Main => rsx! {
|
Screen::Main => rsx! {
|
||||||
div {
|
div {
|
||||||
|
|
@ -189,8 +169,6 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Config dialog
|
|
||||||
if state.read().show_config_dialog {
|
if state.read().show_config_dialog {
|
||||||
div {
|
div {
|
||||||
class: "dialog",
|
class: "dialog",
|
||||||
|
|
@ -223,8 +201,6 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// About dialog
|
|
||||||
if state.read().show_about_dialog {
|
if state.read().show_about_dialog {
|
||||||
div {
|
div {
|
||||||
class: "dialog",
|
class: "dialog",
|
||||||
|
|
@ -240,34 +216,27 @@ fn app(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save sync configuration
|
|
||||||
fn save_config(state: &UseRef<AppState>) {
|
fn save_config(state: &UseRef<AppState>) {
|
||||||
if state.read().name.is_empty() || state.read().access_key.is_empty() || state.read().secret_key.is_empty() {
|
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());
|
state.write_with(|state| state.status_text = "All fields are required!".to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_config = RcloneConfig {
|
let new_config = RcloneConfig {
|
||||||
name: state.read().name.clone(),
|
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(),
|
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(),
|
access_key: state.read().access_key.clone(),
|
||||||
secret_key: state.read().secret_key.clone(),
|
secret_key: state.read().secret_key.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = save_rclone_config(&new_config) {
|
if let Err(e) = save_rclone_config(&new_config) {
|
||||||
state.write_with(|state| state.status_text = format!("Failed to save config: {}", e));
|
state.write_with(|state| state.status_text = format!("Failed to save config: {}", e));
|
||||||
} else {
|
} else {
|
||||||
state.write_with(|state| state.status_text = "New sync saved!".to_string());
|
state.write_with(|state| state.status_text = "New sync saved!".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start sync process
|
|
||||||
fn start_sync(state: &UseRef<AppState>) {
|
fn start_sync(state: &UseRef<AppState>) {
|
||||||
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
|
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
|
||||||
processes.clear();
|
processes.clear();
|
||||||
|
|
||||||
match read_rclone_configs() {
|
match read_rclone_configs() {
|
||||||
Ok(configs) => {
|
Ok(configs) => {
|
||||||
for config in configs {
|
for config in configs {
|
||||||
|
|
@ -282,8 +251,6 @@ fn start_sync(state: &UseRef<AppState>) {
|
||||||
Err(e) => state.write_with(|state| state.status_text = format!("Failed to read configurations: {}", e)),
|
Err(e) => state.write_with(|state| state.status_text = format!("Failed to read configurations: {}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop sync process
|
|
||||||
fn stop_sync(state: &UseRef<AppState>) {
|
fn stop_sync(state: &UseRef<AppState>) {
|
||||||
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
|
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
|
||||||
for child in processes.iter_mut() {
|
for child in processes.iter_mut() {
|
||||||
|
|
@ -293,47 +260,38 @@ fn stop_sync(state: &UseRef<AppState>) {
|
||||||
state.write_with(|state| *state.sync_active.lock().unwrap() = false);
|
state.write_with(|state| *state.sync_active.lock().unwrap() = false);
|
||||||
state.write_with(|state| state.status_text = "Sync stopped.".to_string());
|
state.write_with(|state| state.status_text = "Sync stopped.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility functions (rclone, notifications, etc.)
|
|
||||||
fn save_rclone_config(config: &RcloneConfig) -> Result<(), String> {
|
fn save_rclone_config(config: &RcloneConfig) -> Result<(), String> {
|
||||||
let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_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 config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf");
|
||||||
|
|
||||||
let mut file = OpenOptions::new()
|
let mut file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(&config_path)
|
.open(&config_path)
|
||||||
.map_err(|e| format!("Failed to open config file: {}", e))?;
|
.map_err(|e| format!("Failed to open config file: {}", e))?;
|
||||||
|
|
||||||
writeln!(file, "[{}]", config.name)
|
writeln!(file, "[{}]", config.name)
|
||||||
.and_then(|_| writeln!(file, "type = s3"))
|
.and_then(|_| writeln!(file, "type = s3"))
|
||||||
.and_then(|_| writeln!(file, "provider = Other"))
|
.and_then(|_| writeln!(file, "provider = Other"))
|
||||||
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
|
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
|
||||||
.and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_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"))
|
.and_then(|_| writeln!(file, "acl = private"))
|
||||||
.map_err(|e| format!("Failed to write config: {}", e))
|
.map_err(|e| format!("Failed to write config: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_rclone_configs() -> Result<Vec<RcloneConfig>, String> {
|
fn read_rclone_configs() -> Result<Vec<RcloneConfig>, String> {
|
||||||
let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_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 config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf");
|
||||||
|
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = File::open(&config_path).map_err(|e| format!("Failed to open config file: {}", e))?;
|
let file = File::open(&config_path).map_err(|e| format!("Failed to open config file: {}", e))?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let mut configs = Vec::new();
|
let mut configs = Vec::new();
|
||||||
let mut current_config: Option<RcloneConfig> = None;
|
let mut current_config: Option<RcloneConfig> = None;
|
||||||
|
|
||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
let line = line.map_err(|e| format!("Failed to read line: {}", e))?;
|
let line = line.map_err(|e| format!("Failed to read line: {}", e))?;
|
||||||
if line.is_empty() || line.starts_with('#') {
|
if line.is_empty() || line.starts_with('#') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
if let Some(config) = current_config.take() {
|
if let Some(config) = current_config.take() {
|
||||||
configs.push(config);
|
configs.push(config);
|
||||||
|
|
@ -341,7 +299,7 @@ fn read_rclone_configs() -> Result<Vec<RcloneConfig>, String> {
|
||||||
let name = line[1..line.len()-1].to_string();
|
let name = line[1..line.len()-1].to_string();
|
||||||
current_config = Some(RcloneConfig {
|
current_config = Some(RcloneConfig {
|
||||||
name: name.clone(),
|
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(),
|
local_path: Path::new(&home_dir).join("General Bots").join(&name).to_string_lossy().to_string(),
|
||||||
access_key: String::new(),
|
access_key: String::new(),
|
||||||
secret_key: String::new(),
|
secret_key: String::new(),
|
||||||
|
|
@ -358,20 +316,16 @@ fn read_rclone_configs() -> Result<Vec<RcloneConfig>, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(config) = current_config {
|
if let Some(config) = current_config {
|
||||||
configs.push(config);
|
configs.push(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(configs)
|
Ok(configs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_sync(config: &RcloneConfig) -> Result<Child, std::io::Error> {
|
fn run_sync(config: &RcloneConfig) -> Result<Child, std::io::Error> {
|
||||||
let local_path = Path::new(&config.local_path);
|
let local_path = Path::new(&config.local_path);
|
||||||
if !local_path.exists() {
|
if !local_path.exists() {
|
||||||
create_dir_all(local_path)?;
|
create_dir_all(local_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcCommand::new("rclone")
|
ProcCommand::new("rclone")
|
||||||
.arg("sync")
|
.arg("sync")
|
||||||
.arg(&config.remote_path)
|
.arg(&config.remote_path)
|
||||||
|
|
@ -383,7 +337,6 @@ fn run_sync(config: &RcloneConfig) -> Result<Child, std::io::Error> {
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.spawn()
|
.spawn()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
||||||
let output = ProcCommand::new("rclone")
|
let output = ProcCommand::new("rclone")
|
||||||
.arg("rc")
|
.arg("rc")
|
||||||
|
|
@ -391,11 +344,9 @@ fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
||||||
.arg("--json")
|
.arg("--json")
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
|
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
|
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = String::from_utf8_lossy(&output.stdout);
|
let json = String::from_utf8_lossy(&output.stdout);
|
||||||
let parsed: Result<Value, _> = serde_json::from_str(&json);
|
let parsed: Result<Value, _> = serde_json::from_str(&json);
|
||||||
match parsed {
|
match parsed {
|
||||||
|
|
@ -403,7 +354,6 @@ fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
||||||
let transferred = value.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0);
|
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 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 speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
|
|
||||||
let status = if errors > 0 {
|
let status = if errors > 0 {
|
||||||
"Error occurred".to_string()
|
"Error occurred".to_string()
|
||||||
} else if speed > 0.0 {
|
} else if speed > 0.0 {
|
||||||
|
|
@ -413,7 +363,6 @@ fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
||||||
} else {
|
} else {
|
||||||
"Initializing".to_string()
|
"Initializing".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(SyncStatus {
|
Ok(SyncStatus {
|
||||||
name: remote_name.to_string(),
|
name: remote_name.to_string(),
|
||||||
status,
|
status,
|
||||||
|
|
@ -426,12 +375,10 @@ fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
||||||
Err(e) => Err(format!("Failed to parse rclone status: {}", e)),
|
Err(e) => Err(format!("Failed to parse rclone status: {}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_bytes(bytes: u64) -> String {
|
fn format_bytes(bytes: u64) -> String {
|
||||||
const KB: u64 = 1024;
|
const KB: u64 = 1024;
|
||||||
const MB: u64 = KB * 1024;
|
const MB: u64 = KB * 1024;
|
||||||
const GB: u64 = MB * 1024;
|
const GB: u64 = MB * 1024;
|
||||||
|
|
||||||
if bytes >= GB {
|
if bytes >= GB {
|
||||||
format!("{:.2} GB", bytes as f64 / GB as f64)
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||||
} else if bytes >= MB {
|
} else if bytes >= MB {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
pub mod drive;
|
pub mod drive;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
|
@ -2,19 +2,16 @@ use ratatui::{
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
widgets::{Block, Borders, Gauge},
|
widgets::{Block, Borders, Gauge},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct StreamProgress {
|
pub struct StreamProgress {
|
||||||
pub progress: f64,
|
pub progress: f64,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_progress_bar(progress: &StreamProgress) -> Gauge {
|
pub fn render_progress_bar(progress: &StreamProgress) -> Gauge {
|
||||||
let color = if progress.progress >= 1.0 {
|
let color = if progress.progress >= 1.0 {
|
||||||
Color::Green
|
Color::Green
|
||||||
} else {
|
} else {
|
||||||
Color::Blue
|
Color::Blue
|
||||||
};
|
};
|
||||||
|
|
||||||
Gauge::default()
|
Gauge::default()
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ use std::path::Path;
|
||||||
use std::fs::{OpenOptions, create_dir_all};
|
use std::fs::{OpenOptions, create_dir_all};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RcloneConfig {
|
pub struct RcloneConfig {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -15,7 +13,6 @@ pub struct RcloneConfig {
|
||||||
access_key: String,
|
access_key: String,
|
||||||
secret_key: String,
|
secret_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SyncStatus {
|
pub struct SyncStatus {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -25,40 +22,34 @@ pub struct SyncStatus {
|
||||||
errors: usize,
|
errors: usize,
|
||||||
last_updated: String,
|
last_updated: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct AppState {
|
pub(crate) struct AppState {
|
||||||
pub sync_processes: Mutex<Vec<std::process::Child>>,
|
pub sync_processes: Mutex<Vec<std::process::Child>>,
|
||||||
pub sync_active: Mutex<bool>,
|
pub sync_active: Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn save_config(config: RcloneConfig) -> Result<(), String> {
|
pub fn save_config(config: RcloneConfig) -> Result<(), String> {
|
||||||
let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_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 config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf");
|
||||||
|
|
||||||
let mut file = OpenOptions::new()
|
let mut file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(&config_path)
|
.open(&config_path)
|
||||||
.map_err(|e| format!("Failed to open config file: {}", e))?;
|
.map_err(|e| format!("Failed to open config file: {}", e))?;
|
||||||
|
|
||||||
writeln!(file, "[{}]", config.name)
|
writeln!(file, "[{}]", config.name)
|
||||||
.and_then(|_| writeln!(file, "type = s3"))
|
.and_then(|_| writeln!(file, "type = s3"))
|
||||||
.and_then(|_| writeln!(file, "provider = Other"))
|
.and_then(|_| writeln!(file, "provider = Other"))
|
||||||
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
|
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
|
||||||
.and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_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"))
|
.and_then(|_| writeln!(file, "acl = private"))
|
||||||
.map_err(|e| format!("Failed to write config: {}", e))
|
.map_err(|e| format!("Failed to write config: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result<(), String> {
|
pub fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result<(), String> {
|
||||||
let local_path = Path::new(&config.local_path);
|
let local_path = Path::new(&config.local_path);
|
||||||
if !local_path.exists() {
|
if !local_path.exists() {
|
||||||
create_dir_all(local_path).map_err(|e| format!("Failed to create local path: {}", e))?;
|
create_dir_all(local_path).map_err(|e| format!("Failed to create local path: {}", e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let child = Command::new("rclone")
|
let child = Command::new("rclone")
|
||||||
.arg("sync")
|
.arg("sync")
|
||||||
.arg(&config.remote_path)
|
.arg(&config.remote_path)
|
||||||
|
|
@ -70,12 +61,10 @@ pub fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to start rclone: {}", e))?;
|
.map_err(|e| format!("Failed to start rclone: {}", e))?;
|
||||||
|
|
||||||
state.sync_processes.lock().unwrap().push(child);
|
state.sync_processes.lock().unwrap().push(child);
|
||||||
*state.sync_active.lock().unwrap() = true;
|
*state.sync_active.lock().unwrap() = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
pub fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
||||||
let mut processes = state.sync_processes.lock().unwrap();
|
let mut processes = state.sync_processes.lock().unwrap();
|
||||||
|
|
@ -86,7 +75,6 @@ pub fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
||||||
*state.sync_active.lock().unwrap() = false;
|
*state.sync_active.lock().unwrap() = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||||
let output = Command::new("rclone")
|
let output = Command::new("rclone")
|
||||||
|
|
@ -95,19 +83,15 @@ pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||||
.arg("--json")
|
.arg("--json")
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
|
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
|
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let json = String::from_utf8_lossy(&output.stdout);
|
let json = String::from_utf8_lossy(&output.stdout);
|
||||||
let value: serde_json::Value = serde_json::from_str(&json)
|
let value: serde_json::Value = serde_json::from_str(&json)
|
||||||
.map_err(|e| format!("Failed to parse rclone status: {}", e))?;
|
.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 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 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 speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
|
|
||||||
let status = if errors > 0 {
|
let status = if errors > 0 {
|
||||||
"Error occurred".to_string()
|
"Error occurred".to_string()
|
||||||
} else if speed > 0.0 {
|
} else if speed > 0.0 {
|
||||||
|
|
@ -117,7 +101,6 @@ pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||||
} else {
|
} else {
|
||||||
"Initializing".to_string()
|
"Initializing".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(SyncStatus {
|
Ok(SyncStatus {
|
||||||
name: remote_name,
|
name: remote_name,
|
||||||
status,
|
status,
|
||||||
|
|
@ -127,12 +110,10 @@ pub fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||||
last_updated: chrono::Local::now().format("%H:%M:%S").to_string(),
|
last_updated: chrono::Local::now().format("%H:%M:%S").to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_bytes(bytes: u64) -> String {
|
pub fn format_bytes(bytes: u64) -> String {
|
||||||
const KB: u64 = 1024;
|
const KB: u64 = 1024;
|
||||||
const MB: u64 = KB * 1024;
|
const MB: u64 = KB * 1024;
|
||||||
const GB: u64 = MB * 1024;
|
const GB: u64 = MB * 1024;
|
||||||
|
|
||||||
if bytes >= GB {
|
if bytes >= GB {
|
||||||
format!("{:.2} GB", bytes as f64 / GB as f64)
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||||
} else if bytes >= MB {
|
} else if bytes >= MB {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,17 @@
|
||||||
//! Tests for UI module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_module() {
|
fn test_ui_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic UI module test");
|
assert!(true, "Basic UI module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_drive_ui() {
|
fn test_drive_ui() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Drive UI placeholder test");
|
assert!(true, "Drive UI placeholder test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sync_ui() {
|
fn test_sync_ui() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ use crate::shared::state::AppState;
|
||||||
use crate::shared::models::BotResponse;
|
use crate::shared::models::BotResponse;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct ChatPanel {
|
pub struct ChatPanel {
|
||||||
pub messages: Vec<String>,
|
pub messages: Vec<String>,
|
||||||
pub input_buffer: String,
|
pub input_buffer: String,
|
||||||
|
|
@ -12,7 +11,6 @@ pub struct ChatPanel {
|
||||||
pub user_id: Uuid,
|
pub user_id: Uuid,
|
||||||
pub response_rx: Option<mpsc::Receiver<BotResponse>>,
|
pub response_rx: Option<mpsc::Receiver<BotResponse>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatPanel {
|
impl ChatPanel {
|
||||||
pub fn new(_app_state: Arc<AppState>) -> Self {
|
pub fn new(_app_state: Arc<AppState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -23,26 +21,20 @@ impl ChatPanel {
|
||||||
response_rx: None,
|
response_rx: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_char(&mut self, c: char) {
|
pub fn add_char(&mut self, c: char) {
|
||||||
self.input_buffer.push(c);
|
self.input_buffer.push(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(&mut self) {
|
pub fn backspace(&mut self) {
|
||||||
self.input_buffer.pop();
|
self.input_buffer.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_message(&mut self, bot_name: &str, app_state: &Arc<AppState>) -> Result<()> {
|
pub async fn send_message(&mut self, bot_name: &str, app_state: &Arc<AppState>) -> Result<()> {
|
||||||
if self.input_buffer.trim().is_empty() {
|
if self.input_buffer.trim().is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = self.input_buffer.clone();
|
let message = self.input_buffer.clone();
|
||||||
self.messages.push(format!("You: {}", message));
|
self.messages.push(format!("You: {}", message));
|
||||||
self.input_buffer.clear();
|
self.input_buffer.clear();
|
||||||
|
|
||||||
let bot_id = self.get_bot_id(bot_name, app_state).await?;
|
let bot_id = self.get_bot_id(bot_name, app_state).await?;
|
||||||
|
|
||||||
let user_message = crate::shared::models::UserMessage {
|
let user_message = crate::shared::models::UserMessage {
|
||||||
bot_id: bot_id.to_string(),
|
bot_id: bot_id.to_string(),
|
||||||
user_id: self.user_id.to_string(),
|
user_id: self.user_id.to_string(),
|
||||||
|
|
@ -54,16 +46,12 @@ impl ChatPanel {
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
context_name: None,
|
context_name: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel::<BotResponse>(100);
|
let (tx, rx) = mpsc::channel::<BotResponse>(100);
|
||||||
self.response_rx = Some(rx);
|
self.response_rx = Some(rx);
|
||||||
|
|
||||||
let orchestrator = crate::bot::BotOrchestrator::new(app_state.clone());
|
let orchestrator = crate::bot::BotOrchestrator::new(app_state.clone());
|
||||||
let _ = orchestrator.stream_response(user_message, tx).await;
|
let _ = orchestrator.stream_response(user_message, tx).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn poll_response(&mut self, _bot_name: &str) -> Result<()> {
|
pub async fn poll_response(&mut self, _bot_name: &str) -> Result<()> {
|
||||||
if let Some(rx) = &mut self.response_rx {
|
if let Some(rx) = &mut self.response_rx {
|
||||||
while let Ok(response) = rx.try_recv() {
|
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));
|
self.messages.push(format!("Bot: {}", response.content));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.is_complete && response.content.is_empty() {
|
if response.is_complete && response.content.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -86,34 +73,27 @@ let _ = orchestrator.stream_response(user_message, tx).await;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_bot_id(&self, bot_name: &str, app_state: &Arc<AppState>) -> Result<Uuid> {
|
async fn get_bot_id(&self, bot_name: &str, app_state: &Arc<AppState>) -> Result<Uuid> {
|
||||||
use crate::shared::models::schema::bots::dsl::*;
|
use crate::shared::models::schema::bots::dsl::*;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
let mut conn = app_state.conn.get().unwrap();
|
let mut conn = app_state.conn.get().unwrap();
|
||||||
let bot_id = bots
|
let bot_id = bots
|
||||||
.filter(name.eq(bot_name))
|
.filter(name.eq(bot_name))
|
||||||
.select(id)
|
.select(id)
|
||||||
.first::<Uuid>(&mut *conn)?;
|
.first::<Uuid>(&mut *conn)?;
|
||||||
|
|
||||||
Ok(bot_id)
|
Ok(bot_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self) -> String {
|
pub fn render(&self) -> String {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
lines.push("╔═══════════════════════════════════════╗".to_string());
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
lines.push("║ CONVERSATION ║".to_string());
|
lines.push("║ CONVERSATION ║".to_string());
|
||||||
lines.push("╚═══════════════════════════════════════╝".to_string());
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
|
|
||||||
let visible_start = if self.messages.len() > 15 {
|
let visible_start = if self.messages.len() > 15 {
|
||||||
self.messages.len() - 15
|
self.messages.len() - 15
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
for msg in &self.messages[visible_start..] {
|
for msg in &self.messages[visible_start..] {
|
||||||
if msg.starts_with("You: ") {
|
if msg.starts_with("You: ") {
|
||||||
lines.push(format!(" {}", msg));
|
lines.push(format!(" {}", msg));
|
||||||
|
|
@ -123,13 +103,11 @@ let _ = orchestrator.stream_response(user_message, tx).await;
|
||||||
lines.push(format!(" {}", msg));
|
lines.push(format!(" {}", msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push("─────────────────────────────────────────".to_string());
|
lines.push("─────────────────────────────────────────".to_string());
|
||||||
lines.push(format!(" > {}_", self.input_buffer));
|
lines.push(format!(" > {}_", self.input_buffer));
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push(" Enter: Send | Tab: Switch Panel".to_string());
|
lines.push(" Enter: Send | Tab: Switch Panel".to_string());
|
||||||
|
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
file_path: String,
|
file_path: String,
|
||||||
bucket: String,
|
bucket: String,
|
||||||
|
|
@ -11,7 +10,6 @@ pub struct Editor {
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
modified: bool,
|
modified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub async fn load(app_state: &Arc<AppState>, bucket: &str, path: &str) -> Result<Self> {
|
pub async fn load(app_state: &Arc<AppState>, bucket: &str, path: &str) -> Result<Self> {
|
||||||
let content = if let Some(drive) = &app_state.drive {
|
let content = if let Some(drive) = &app_state.drive {
|
||||||
|
|
@ -25,7 +23,6 @@ impl Editor {
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
file_path: format!("{}/{}", bucket, path),
|
file_path: format!("{}/{}", bucket, path),
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
|
|
@ -36,7 +33,6 @@ impl Editor {
|
||||||
modified: false,
|
modified: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&mut self, app_state: &Arc<AppState>) -> Result<()> {
|
pub async fn save(&mut self, app_state: &Arc<AppState>) -> Result<()> {
|
||||||
if let Some(drive) = &app_state.drive {
|
if let Some(drive) = &app_state.drive {
|
||||||
drive.put_object()
|
drive.put_object()
|
||||||
|
|
@ -49,11 +45,9 @@ impl Editor {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn file_path(&self) -> &str {
|
pub fn file_path(&self) -> &str {
|
||||||
&self.file_path
|
&self.file_path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self, cursor_blink: bool) -> String {
|
pub fn render(&self, cursor_blink: bool) -> String {
|
||||||
let lines: Vec<&str> = self.content.lines().collect();
|
let lines: Vec<&str> = self.content.lines().collect();
|
||||||
let total_lines = lines.len().max(1);
|
let total_lines = lines.len().max(1);
|
||||||
|
|
@ -64,41 +58,33 @@ impl Editor {
|
||||||
.last()
|
.last()
|
||||||
.map(|line| line.len())
|
.map(|line| line.len())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let start = self.scroll_offset;
|
let start = self.scroll_offset;
|
||||||
let end = (start + visible_lines).min(total_lines);
|
let end = (start + visible_lines).min(total_lines);
|
||||||
|
|
||||||
let mut display_lines = Vec::new();
|
let mut display_lines = Vec::new();
|
||||||
for i in start..end {
|
for i in start..end {
|
||||||
let line_num = i + 1;
|
let line_num = i + 1;
|
||||||
let line_content = if i < lines.len() { lines[i] } else { "" };
|
let line_content = if i < lines.len() { lines[i] } else { "" };
|
||||||
let is_cursor_line = i == cursor_line;
|
let is_cursor_line = i == cursor_line;
|
||||||
|
|
||||||
let cursor_indicator = if is_cursor_line && cursor_blink {
|
let cursor_indicator = if is_cursor_line && cursor_blink {
|
||||||
let spaces = " ".repeat(cursor_col);
|
let spaces = " ".repeat(cursor_col);
|
||||||
format!("{}█", spaces)
|
format!("{}█", spaces)
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
display_lines.push(format!(" {:4} │ {}{}", line_num, line_content, cursor_indicator));
|
display_lines.push(format!(" {:4} │ {}{}", line_num, line_content, cursor_indicator));
|
||||||
}
|
}
|
||||||
|
|
||||||
if display_lines.is_empty() {
|
if display_lines.is_empty() {
|
||||||
let cursor_indicator = if cursor_blink { "█" } else { "" };
|
let cursor_indicator = if cursor_blink { "█" } else { "" };
|
||||||
display_lines.push(format!(" 1 │ {}", cursor_indicator));
|
display_lines.push(format!(" 1 │ {}", cursor_indicator));
|
||||||
}
|
}
|
||||||
|
|
||||||
display_lines.push("".to_string());
|
display_lines.push("".to_string());
|
||||||
display_lines.push("─────────────────────────────────────────────────────────────".to_string());
|
display_lines.push("─────────────────────────────────────────────────────────────".to_string());
|
||||||
let status = if self.modified { "MODIFIED" } else { "SAVED" };
|
let status = if self.modified { "MODIFIED" } else { "SAVED" };
|
||||||
display_lines.push(format!(" {} {} │ Line: {}, Col: {}",
|
display_lines.push(format!(" {} {} │ Line: {}, Col: {}",
|
||||||
status, self.file_path, cursor_line + 1, cursor_col + 1));
|
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.push(" Ctrl+S: Save │ Ctrl+W: Close │ Esc: Close without saving".to_string());
|
||||||
|
|
||||||
display_lines.join("\n")
|
display_lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_up(&mut self) {
|
pub fn move_up(&mut self) {
|
||||||
if let Some(prev_line_end) = self.content[..self.cursor_pos].rfind('\n') {
|
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') {
|
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) {
|
pub fn move_down(&mut self) {
|
||||||
if let Some(next_line_start) = self.content[self.cursor_pos..].find('\n') {
|
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);
|
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) {
|
pub fn move_left(&mut self) {
|
||||||
if self.cursor_pos > 0 {
|
if self.cursor_pos > 0 {
|
||||||
self.cursor_pos -= 1;
|
self.cursor_pos -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_right(&mut self) {
|
pub fn move_right(&mut self) {
|
||||||
if self.cursor_pos < self.content.len() {
|
if self.cursor_pos < self.content.len() {
|
||||||
self.cursor_pos += 1;
|
self.cursor_pos += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_char(&mut self, c: char) {
|
pub fn insert_char(&mut self, c: char) {
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
self.content.insert(self.cursor_pos, c);
|
self.content.insert(self.cursor_pos, c);
|
||||||
self.cursor_pos += 1;
|
self.cursor_pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(&mut self) {
|
pub fn backspace(&mut self) {
|
||||||
if self.cursor_pos > 0 {
|
if self.cursor_pos > 0 {
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
|
|
@ -153,7 +134,6 @@ impl Editor {
|
||||||
self.cursor_pos -= 1;
|
self.cursor_pos -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_newline(&mut self) {
|
pub fn insert_newline(&mut self) {
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
self.content.insert(self.cursor_pos, '\n');
|
self.content.insert(self.cursor_pos, '\n');
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum TreeNode {
|
pub enum TreeNode {
|
||||||
Bucket { name: String },
|
Bucket { name: String },
|
||||||
Folder { bucket: String, path: String },
|
Folder { bucket: String, path: String },
|
||||||
File { bucket: String, path: String },
|
File { bucket: String, path: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileTree {
|
pub struct FileTree {
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
items: Vec<(String, TreeNode)>,
|
items: Vec<(String, TreeNode)>,
|
||||||
|
|
@ -16,7 +14,6 @@ pub struct FileTree {
|
||||||
current_bucket: Option<String>,
|
current_bucket: Option<String>,
|
||||||
current_path: Vec<String>,
|
current_path: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTree {
|
impl FileTree {
|
||||||
pub fn new(app_state: Arc<AppState>) -> Self {
|
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -27,12 +24,10 @@ impl FileTree {
|
||||||
current_path: Vec::new(),
|
current_path: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_root(&mut self) -> Result<()> {
|
pub async fn load_root(&mut self) -> Result<()> {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
self.current_bucket = None;
|
self.current_bucket = None;
|
||||||
self.current_path.clear();
|
self.current_path.clear();
|
||||||
|
|
||||||
if let Some(drive) = &self.app_state.drive {
|
if let Some(drive) = &self.app_state.drive {
|
||||||
let result = drive.list_buckets().send().await;
|
let result = drive.list_buckets().send().await;
|
||||||
match result {
|
match result {
|
||||||
|
|
@ -53,27 +48,23 @@ impl FileTree {
|
||||||
} else {
|
} else {
|
||||||
self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() }));
|
self.items.push(("✗ Drive not connected".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.items.is_empty() {
|
if self.items.is_empty() {
|
||||||
self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() }));
|
self.items.push(("(no buckets found)".to_string(), TreeNode::Bucket { name: String::new() }));
|
||||||
}
|
}
|
||||||
self.selected = 0;
|
self.selected = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enter_bucket(&mut self, bucket: String) -> Result<()> {
|
pub async fn enter_bucket(&mut self, bucket: String) -> Result<()> {
|
||||||
self.current_bucket = Some(bucket.clone());
|
self.current_bucket = Some(bucket.clone());
|
||||||
self.current_path.clear();
|
self.current_path.clear();
|
||||||
self.load_bucket_contents(&bucket, "").await
|
self.load_bucket_contents(&bucket, "").await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn enter_folder(&mut self, bucket: String, path: String) -> Result<()> {
|
pub async fn enter_folder(&mut self, bucket: String, path: String) -> Result<()> {
|
||||||
self.current_bucket = Some(bucket.clone());
|
self.current_bucket = Some(bucket.clone());
|
||||||
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|s| !s.is_empty()).collect();
|
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.current_path = parts.iter().map(|s| s.to_string()).collect();
|
||||||
self.load_bucket_contents(&bucket, &path).await
|
self.load_bucket_contents(&bucket, &path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn go_up(&mut self) -> bool {
|
pub fn go_up(&mut self) -> bool {
|
||||||
if self.current_path.is_empty() {
|
if self.current_path.is_empty() {
|
||||||
if self.current_bucket.is_some() {
|
if self.current_bucket.is_some() {
|
||||||
|
|
@ -85,7 +76,6 @@ impl FileTree {
|
||||||
self.current_path.pop();
|
self.current_path.pop();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_current(&mut self) -> Result<()> {
|
pub async fn refresh_current(&mut self) -> Result<()> {
|
||||||
if let Some(bucket) = &self.current_bucket.clone() {
|
if let Some(bucket) = &self.current_bucket.clone() {
|
||||||
let path = self.current_path.join("/");
|
let path = self.current_path.join("/");
|
||||||
|
|
@ -94,14 +84,12 @@ impl FileTree {
|
||||||
self.load_root().await
|
self.load_root().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_bucket_contents(&mut self, bucket: &str, prefix: &str) -> Result<()> {
|
async fn load_bucket_contents(&mut self, bucket: &str, prefix: &str) -> Result<()> {
|
||||||
self.items.clear();
|
self.items.clear();
|
||||||
self.items.push(("⬆️ .. (go back)".to_string(), TreeNode::Folder {
|
self.items.push(("⬆️ .. (go back)".to_string(), TreeNode::Folder {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
path: "..".to_string(),
|
path: "..".to_string(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if let Some(drive) = &self.app_state.drive {
|
if let Some(drive) = &self.app_state.drive {
|
||||||
let normalized_prefix = if prefix.is_empty() {
|
let normalized_prefix = if prefix.is_empty() {
|
||||||
String::new()
|
String::new()
|
||||||
|
|
@ -110,10 +98,8 @@ impl FileTree {
|
||||||
} else {
|
} else {
|
||||||
format!("{}/", prefix)
|
format!("{}/", prefix)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut continuation_token = None;
|
let mut continuation_token = None;
|
||||||
let mut all_keys = Vec::new();
|
let mut all_keys = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut request = drive.list_objects_v2().bucket(bucket);
|
let mut request = drive.list_objects_v2().bucket(bucket);
|
||||||
if !normalized_prefix.is_empty() {
|
if !normalized_prefix.is_empty() {
|
||||||
|
|
@ -122,39 +108,31 @@ impl FileTree {
|
||||||
if let Some(token) = continuation_token {
|
if let Some(token) = continuation_token {
|
||||||
request = request.continuation_token(token);
|
request = request.continuation_token(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = request.send().await?;
|
let result = request.send().await?;
|
||||||
|
|
||||||
for obj in result.contents() {
|
for obj in result.contents() {
|
||||||
if let Some(key) = obj.key() {
|
if let Some(key) = obj.key() {
|
||||||
all_keys.push(key.to_string());
|
all_keys.push(key.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !result.is_truncated.unwrap_or(false) {
|
if !result.is_truncated.unwrap_or(false) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
continuation_token = result.next_continuation_token;
|
continuation_token = result.next_continuation_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut folders = std::collections::HashSet::new();
|
let mut folders = std::collections::HashSet::new();
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
|
||||||
for key in all_keys {
|
for key in all_keys {
|
||||||
if key == normalized_prefix {
|
if key == normalized_prefix {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) {
|
let relative = if !normalized_prefix.is_empty() && key.starts_with(&normalized_prefix) {
|
||||||
&key[normalized_prefix.len()..]
|
&key[normalized_prefix.len()..]
|
||||||
} else {
|
} else {
|
||||||
&key
|
&key
|
||||||
};
|
};
|
||||||
|
|
||||||
if relative.is_empty() {
|
if relative.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(slash_pos) = relative.find('/') {
|
if let Some(slash_pos) = relative.find('/') {
|
||||||
let folder_name = &relative[..slash_pos];
|
let folder_name = &relative[..slash_pos];
|
||||||
if !folder_name.is_empty() {
|
if !folder_name.is_empty() {
|
||||||
|
|
@ -164,7 +142,6 @@ impl FileTree {
|
||||||
files.push((relative.to_string(), key.clone()));
|
files.push((relative.to_string(), key.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut folder_vec: Vec<String> = folders.into_iter().collect();
|
let mut folder_vec: Vec<String> = folders.into_iter().collect();
|
||||||
folder_vec.sort();
|
folder_vec.sort();
|
||||||
for folder_name in folder_vec {
|
for folder_name in folder_vec {
|
||||||
|
|
@ -179,7 +156,6 @@ impl FileTree {
|
||||||
path: full_path,
|
path: full_path,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
files.sort_by(|(a, _), (b, _)| a.cmp(b));
|
files.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||||
for (name, full_path) in files {
|
for (name, full_path) in files {
|
||||||
let icon = if name.ends_with(".bas") {
|
let icon = if name.ends_with(".bas") {
|
||||||
|
|
@ -202,37 +178,30 @@ impl FileTree {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.items.len() == 1 {
|
if self.items.len() == 1 {
|
||||||
self.items.push(("(empty folder)".to_string(), TreeNode::Folder {
|
self.items.push(("(empty folder)".to_string(), TreeNode::Folder {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
path: String::new(),
|
path: String::new(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.selected = 0;
|
self.selected = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_items(&self) -> &[(String, TreeNode)] {
|
pub fn render_items(&self) -> &[(String, TreeNode)] {
|
||||||
&self.items
|
&self.items
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selected_index(&self) -> usize {
|
pub fn selected_index(&self) -> usize {
|
||||||
self.selected
|
self.selected
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected_node(&self) -> Option<&TreeNode> {
|
pub fn get_selected_node(&self) -> Option<&TreeNode> {
|
||||||
self.items.get(self.selected).map(|(_, node)| node)
|
self.items.get(self.selected).map(|(_, node)| node)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_selected_bot(&self) -> Option<String> {
|
pub fn get_selected_bot(&self) -> Option<String> {
|
||||||
if let Some(bucket) = &self.current_bucket {
|
if let Some(bucket) = &self.current_bucket {
|
||||||
if bucket.ends_with(".gbai") {
|
if bucket.ends_with(".gbai") {
|
||||||
return Some(bucket.trim_end_matches(".gbai").to_string());
|
return Some(bucket.trim_end_matches(".gbai").to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((_, node)) = self.items.get(self.selected) {
|
if let Some((_, node)) = self.items.get(self.selected) {
|
||||||
match node {
|
match node {
|
||||||
TreeNode::Bucket { name } => {
|
TreeNode::Bucket { name } => {
|
||||||
|
|
@ -243,16 +212,13 @@ impl FileTree {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_up(&mut self) {
|
pub fn move_up(&mut self) {
|
||||||
if self.selected > 0 {
|
if self.selected > 0 {
|
||||||
self.selected -= 1;
|
self.selected -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_down(&mut self) {
|
pub fn move_down(&mut self) {
|
||||||
if self.selected < self.items.len().saturating_sub(1) {
|
if self.selected < self.items.len().saturating_sub(1) {
|
||||||
self.selected += 1;
|
self.selected += 1;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use log::{Log, Metadata, LevelFilter, Record, SetLoggerError};
|
use log::{Log, Metadata, LevelFilter, Record, SetLoggerError};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
|
||||||
pub struct LogPanel {
|
pub struct LogPanel {
|
||||||
logs: Vec<String>,
|
logs: Vec<String>,
|
||||||
max_logs: usize,
|
max_logs: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogPanel {
|
impl LogPanel {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -14,14 +12,12 @@ impl LogPanel {
|
||||||
max_logs: 1000,
|
max_logs: 1000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_log(&mut self, entry: &str) {
|
pub fn add_log(&mut self, entry: &str) {
|
||||||
if self.logs.len() >= self.max_logs {
|
if self.logs.len() >= self.max_logs {
|
||||||
self.logs.remove(0);
|
self.logs.remove(0);
|
||||||
}
|
}
|
||||||
self.logs.push(entry.to_string());
|
self.logs.push(entry.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&self) -> String {
|
pub fn render(&self) -> String {
|
||||||
let visible_logs = if self.logs.len() > 10 {
|
let visible_logs = if self.logs.len() > 10 {
|
||||||
&self.logs[self.logs.len() - 10..]
|
&self.logs[self.logs.len() - 10..]
|
||||||
|
|
@ -31,17 +27,14 @@ impl LogPanel {
|
||||||
visible_logs.join("\n")
|
visible_logs.join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UiLogger {
|
pub struct UiLogger {
|
||||||
log_panel: Arc<Mutex<LogPanel>>,
|
log_panel: Arc<Mutex<LogPanel>>,
|
||||||
filter: LevelFilter,
|
filter: LevelFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Log for UiLogger {
|
impl Log for UiLogger {
|
||||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||||
metadata.level() <= self.filter
|
metadata.level() <= self.filter
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &Record) {
|
fn log(&self, record: &Record) {
|
||||||
if self.enabled(record.metadata()) {
|
if self.enabled(record.metadata()) {
|
||||||
let timestamp = Local::now().format("%H:%M:%S");
|
let timestamp = Local::now().format("%H:%M:%S");
|
||||||
|
|
@ -58,10 +51,8 @@ impl Log for UiLogger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&self) {}
|
fn flush(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_logger(log_panel: Arc<Mutex<LogPanel>>) -> Result<(), SetLoggerError> {
|
pub fn init_logger(log_panel: Arc<Mutex<LogPanel>>) -> Result<(), SetLoggerError> {
|
||||||
let logger = Box::new(UiLogger {
|
let logger = Box::new(UiLogger {
|
||||||
log_panel,
|
log_panel,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ use file_tree::{FileTree, TreeNode};
|
||||||
use log_panel::{init_logger, LogPanel};
|
use log_panel::{init_logger, LogPanel};
|
||||||
use status_panel::StatusPanel;
|
use status_panel::StatusPanel;
|
||||||
use chat_panel::ChatPanel;
|
use chat_panel::ChatPanel;
|
||||||
|
|
||||||
pub struct XtreeUI {
|
pub struct XtreeUI {
|
||||||
app_state: Option<Arc<AppState>>,
|
app_state: Option<Arc<AppState>>,
|
||||||
file_tree: Option<FileTree>,
|
file_tree: Option<FileTree>,
|
||||||
|
|
@ -40,7 +39,6 @@ should_quit: bool,
|
||||||
progress_channel: Option<Arc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<crate::BootstrapProgress>>>>,
|
progress_channel: Option<Arc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<crate::BootstrapProgress>>>>,
|
||||||
bootstrap_status: String,
|
bootstrap_status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
enum ActivePanel {
|
enum ActivePanel {
|
||||||
FileTree,
|
FileTree,
|
||||||
|
|
@ -49,7 +47,6 @@ Status,
|
||||||
Logs,
|
Logs,
|
||||||
Chat,
|
Chat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl XtreeUI {
|
impl XtreeUI {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let log_panel = Arc::new(Mutex::new(LogPanel::new()));
|
let log_panel = Arc::new(Mutex::new(LogPanel::new()));
|
||||||
|
|
@ -66,11 +63,9 @@ progress_channel: None,
|
||||||
bootstrap_status: "Initializing...".to_string(),
|
bootstrap_status: "Initializing...".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_progress_channel(&mut self, rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<crate::BootstrapProgress>>>) {
|
pub fn set_progress_channel(&mut self, rx: Arc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<crate::BootstrapProgress>>>) {
|
||||||
self.progress_channel = Some(rx);
|
self.progress_channel = Some(rx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_app_state(&mut self, app_state: Arc<AppState>) {
|
pub fn set_app_state(&mut self, app_state: Arc<AppState>) {
|
||||||
self.file_tree = Some(FileTree::new(app_state.clone()));
|
self.file_tree = Some(FileTree::new(app_state.clone()));
|
||||||
self.status_panel = Some(StatusPanel::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.active_panel = ActivePanel::FileTree;
|
||||||
self.bootstrap_status = "Ready".to_string();
|
self.bootstrap_status = "Ready".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_ui(&mut self) -> Result<()> {
|
pub fn start_ui(&mut self) -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
|
||||||
|
|
@ -98,13 +92,12 @@ execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
fn run_event_loop(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
||||||
let mut last_update = std::time::Instant::now();
|
let mut last_update = std::time::Instant::now();
|
||||||
let update_interval = std::time::Duration::from_millis(1000);
|
let update_interval = std::time::Duration::from_millis(1000);
|
||||||
let mut cursor_blink = false;
|
let mut cursor_blink = false;
|
||||||
let mut last_blink = std::time::Instant::now();
|
let mut last_blink = std::time::Instant::now();
|
||||||
let rt = tokio::runtime::Runtime::new()?; // create runtime once
|
let rt = tokio::runtime::Runtime::new()?;
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref progress_rx) = self.progress_channel {
|
if let Some(ref progress_rx) = self.progress_channel {
|
||||||
if let Ok(mut rx) = progress_rx.try_lock() {
|
if let Ok(mut rx) = progress_rx.try_lock() {
|
||||||
|
|
@ -148,7 +141,6 @@ break;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(&mut self, f: &mut Frame, cursor_blink: bool) {
|
fn render(&mut self, f: &mut Frame, cursor_blink: bool) {
|
||||||
let bg = Color::Rgb(0, 30, 100);
|
let bg = Color::Rgb(0, 30, 100);
|
||||||
let border_active = Color::Rgb(85, 255, 255);
|
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);
|
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) {
|
fn render_header(&self, f: &mut Frame, area: Rect, _bg: Color, title_bg: Color, title_fg: Color) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.style(Style::default().bg(title_bg));
|
.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) {
|
fn render_loading(&self, f: &mut Frame, bg: Color, text: Color, border: Color, title_bg: Color, title_fg: Color) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
|
|
@ -267,7 +257,6 @@ let paragraph = Paragraph::new(loading_text)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
f.render_widget(paragraph, center);
|
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) {
|
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 {
|
if let Some(file_tree) = &self.file_tree {
|
||||||
let items = file_tree.render_items();
|
let items = file_tree.render_items();
|
||||||
|
|
@ -296,7 +285,6 @@ let list = List::new(list_items).block(block);
|
||||||
f.render_widget(list, area);
|
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) {
|
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 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 {
|
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 });
|
.wrap(Wrap { trim: false });
|
||||||
f.render_widget(paragraph, area);
|
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) {
|
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 is_active = self.active_panel == ActivePanel::Editor;
|
||||||
let border_color = if is_active { border_active } else { border_inactive };
|
let border_color = if is_active { border_active } else { border_inactive };
|
||||||
|
|
@ -347,7 +334,6 @@ let paragraph = Paragraph::new(content)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
f.render_widget(paragraph, area);
|
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) {
|
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 {
|
if let Some(chat_panel) = &self.chat_panel {
|
||||||
let is_active = self.active_panel == ActivePanel::Chat;
|
let is_active = self.active_panel == ActivePanel::Chat;
|
||||||
|
|
@ -376,7 +362,6 @@ let paragraph = Paragraph::new(content)
|
||||||
f.render_widget(paragraph, area);
|
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) {
|
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_panel = self.log_panel.try_lock();
|
||||||
let log_lines = if let Ok(panel) = log_panel {
|
let log_lines = if let Ok(panel) = log_panel {
|
||||||
|
|
@ -402,7 +387,6 @@ let paragraph = Paragraph::new(log_lines)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result<()> {
|
async fn handle_input(&mut self, key: KeyCode, modifiers: KeyModifiers) -> Result<()> {
|
||||||
if modifiers.contains(KeyModifiers::CONTROL) {
|
if modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
match key {
|
match key {
|
||||||
|
|
@ -550,7 +534,6 @@ _ => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_tree_enter(&mut self) -> Result<()> {
|
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(file_tree), Some(app_state)) = (&mut self.file_tree, &self.app_state) {
|
||||||
if let Some(node) = file_tree.get_selected_node().cloned() {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_data(&mut self) -> Result<()> {
|
async fn update_data(&mut self) -> Result<()> {
|
||||||
if let Some(status_panel) = &mut self.status_panel {
|
if let Some(status_panel) = &mut self.status_panel {
|
||||||
status_panel.update().await?;
|
status_panel.update().await?;
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,12 @@ use crate::shared::state::AppState;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
|
|
||||||
pub struct StatusPanel {
|
pub struct StatusPanel {
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
last_update: std::time::Instant,
|
last_update: std::time::Instant,
|
||||||
cached_content: String,
|
cached_content: String,
|
||||||
system: System,
|
system: System,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusPanel {
|
impl StatusPanel {
|
||||||
pub fn new(app_state: Arc<AppState>) -> Self {
|
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -22,47 +20,37 @@ impl StatusPanel {
|
||||||
system: System::new_all(),
|
system: System::new_all(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&mut self) -> Result<(), std::io::Error> {
|
pub async fn update(&mut self) -> Result<(), std::io::Error> {
|
||||||
if self.last_update.elapsed() < std::time::Duration::from_secs(1) {
|
if self.last_update.elapsed() < std::time::Duration::from_secs(1) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.system.refresh_all();
|
self.system.refresh_all();
|
||||||
|
|
||||||
self.cached_content = String::new();
|
self.cached_content = String::new();
|
||||||
self.last_update = std::time::Instant::now();
|
self.last_update = std::time::Instant::now();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(&mut self, selected_bot: Option<String>) -> String {
|
pub fn render(&mut self, selected_bot: Option<String>) -> String {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
self.system.refresh_all();
|
self.system.refresh_all();
|
||||||
|
|
||||||
lines.push("╔═══════════════════════════════════════╗".to_string());
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
lines.push("║ SYSTEM METRICS ║".to_string());
|
lines.push("║ SYSTEM METRICS ║".to_string());
|
||||||
lines.push("╚═══════════════════════════════════════╝".to_string());
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
|
|
||||||
let system_metrics = match nvidia::get_system_metrics(0, 0) {
|
let system_metrics = match nvidia::get_system_metrics(0, 0) {
|
||||||
Ok(metrics) => metrics,
|
Ok(metrics) => metrics,
|
||||||
Err(_) => nvidia::SystemMetrics::default(),
|
Err(_) => nvidia::SystemMetrics::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20);
|
let cpu_bar = Self::create_progress_bar(system_metrics.cpu_usage, 20);
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
" CPU: {:5.1}% {}",
|
" CPU: {:5.1}% {}",
|
||||||
system_metrics.cpu_usage, cpu_bar
|
system_metrics.cpu_usage, cpu_bar
|
||||||
));
|
));
|
||||||
|
|
||||||
if let Some(gpu_usage) = system_metrics.gpu_usage {
|
if let Some(gpu_usage) = system_metrics.gpu_usage {
|
||||||
let gpu_bar = Self::create_progress_bar(gpu_usage, 20);
|
let gpu_bar = Self::create_progress_bar(gpu_usage, 20);
|
||||||
lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar));
|
lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar));
|
||||||
} else {
|
} else {
|
||||||
lines.push(" GPU: Not available".to_string());
|
lines.push(" GPU: Not available".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_mem = self.system.total_memory() as f32 / 1024.0 / 1024.0 / 1024.0;
|
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 used_mem = self.system.used_memory() as f32 / 1024.0 / 1024.0 / 1024.0;
|
||||||
let mem_percentage = (used_mem / total_mem) * 100.0;
|
let mem_percentage = (used_mem / total_mem) * 100.0;
|
||||||
|
|
@ -71,20 +59,17 @@ impl StatusPanel {
|
||||||
" MEM: {:5.1}% {} ({:.1}/{:.1} GB)",
|
" MEM: {:5.1}% {} ({:.1}/{:.1} GB)",
|
||||||
mem_percentage, mem_bar, used_mem, total_mem
|
mem_percentage, mem_bar, used_mem, total_mem
|
||||||
));
|
));
|
||||||
|
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push("╔═══════════════════════════════════════╗".to_string());
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
lines.push("║ COMPONENTS STATUS ║".to_string());
|
lines.push("║ COMPONENTS STATUS ║".to_string());
|
||||||
lines.push("╚═══════════════════════════════════════╝".to_string());
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
|
|
||||||
let components = vec![
|
let components = vec![
|
||||||
("Tables", "postgres", "5432"),
|
("Tables", "postgres", "5432"),
|
||||||
("Cache", "valkey-server", "6379"),
|
("Cache", "valkey-server", "6379"),
|
||||||
("Drive", "minio", "9000"),
|
("Drive", "minio", "9000"),
|
||||||
("LLM", "llama-server", "8081"),
|
("LLM", "llama-server", "8081"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (comp_name, process, port) in components {
|
for (comp_name, process, port) in components {
|
||||||
let status = if Self::check_component_running(process) {
|
let status = if Self::check_component_running(process) {
|
||||||
format!("🟢 ONLINE [Port: {}]", port)
|
format!("🟢 ONLINE [Port: {}]", port)
|
||||||
|
|
@ -93,13 +78,11 @@ impl StatusPanel {
|
||||||
};
|
};
|
||||||
lines.push(format!(" {:<10} {}", comp_name, status));
|
lines.push(format!(" {:<10} {}", comp_name, status));
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push("╔═══════════════════════════════════════╗".to_string());
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
lines.push("║ ACTIVE BOTS ║".to_string());
|
lines.push("║ ACTIVE BOTS ║".to_string());
|
||||||
lines.push("╚═══════════════════════════════════════╝".to_string());
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
|
|
||||||
if let Ok(mut conn) = self.app_state.conn.get() {
|
if let Ok(mut conn) = self.app_state.conn.get() {
|
||||||
match bots
|
match bots
|
||||||
.filter(is_active.eq(true))
|
.filter(is_active.eq(true))
|
||||||
|
|
@ -121,30 +104,24 @@ impl StatusPanel {
|
||||||
" "
|
" "
|
||||||
};
|
};
|
||||||
lines.push(format!(" {} 🤖 {}", marker, bot_name));
|
lines.push(format!(" {} 🤖 {}", marker, bot_name));
|
||||||
|
|
||||||
if let Some(ref selected) = selected_bot {
|
if let Some(ref selected) = selected_bot {
|
||||||
if selected == &bot_name {
|
if selected == &bot_name {
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push(" ┌─ Bot Configuration ─────────┐".to_string());
|
lines.push(" ┌─ Bot Configuration ─────────┐".to_string());
|
||||||
|
|
||||||
let config_manager =
|
let config_manager =
|
||||||
ConfigManager::new(self.app_state.conn.clone());
|
ConfigManager::new(self.app_state.conn.clone());
|
||||||
|
|
||||||
let llm_model = config_manager
|
let llm_model = config_manager
|
||||||
.get_config(&bot_id, "llm-model", None)
|
.get_config(&bot_id, "llm-model", None)
|
||||||
.unwrap_or_else(|_| "N/A".to_string());
|
.unwrap_or_else(|_| "N/A".to_string());
|
||||||
lines.push(format!(" Model: {}", llm_model));
|
lines.push(format!(" Model: {}", llm_model));
|
||||||
|
|
||||||
let ctx_size = config_manager
|
let ctx_size = config_manager
|
||||||
.get_config(&bot_id, "llm-server-ctx-size", None)
|
.get_config(&bot_id, "llm-server-ctx-size", None)
|
||||||
.unwrap_or_else(|_| "N/A".to_string());
|
.unwrap_or_else(|_| "N/A".to_string());
|
||||||
lines.push(format!(" Context: {}", ctx_size));
|
lines.push(format!(" Context: {}", ctx_size));
|
||||||
|
|
||||||
let temp = config_manager
|
let temp = config_manager
|
||||||
.get_config(&bot_id, "llm-temperature", None)
|
.get_config(&bot_id, "llm-temperature", None)
|
||||||
.unwrap_or_else(|_| "N/A".to_string());
|
.unwrap_or_else(|_| "N/A".to_string());
|
||||||
lines.push(format!(" Temp: {}", temp));
|
lines.push(format!(" Temp: {}", temp));
|
||||||
|
|
||||||
lines.push(" └─────────────────────────────┘".to_string());
|
lines.push(" └─────────────────────────────┘".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,12 +135,10 @@ impl StatusPanel {
|
||||||
} else {
|
} else {
|
||||||
lines.push(" Database locked".to_string());
|
lines.push(" Database locked".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("".to_string());
|
lines.push("".to_string());
|
||||||
lines.push("╔═══════════════════════════════════════╗".to_string());
|
lines.push("╔═══════════════════════════════════════╗".to_string());
|
||||||
lines.push("║ SESSIONS ║".to_string());
|
lines.push("║ SESSIONS ║".to_string());
|
||||||
lines.push("╚═══════════════════════════════════════╝".to_string());
|
lines.push("╚═══════════════════════════════════════╝".to_string());
|
||||||
|
|
||||||
let session_count = self
|
let session_count = self
|
||||||
.app_state
|
.app_state
|
||||||
.response_channels
|
.response_channels
|
||||||
|
|
@ -171,10 +146,8 @@ impl StatusPanel {
|
||||||
.map(|channels| channels.len())
|
.map(|channels| channels.len())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
lines.push(format!(" Active Sessions: {}", session_count));
|
lines.push(format!(" Active Sessions: {}", session_count));
|
||||||
|
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_progress_bar(percentage: f32, width: usize) -> String {
|
fn create_progress_bar(percentage: f32, width: usize) -> String {
|
||||||
let filled = (percentage / 100.0 * width as f32).round() as usize;
|
let filled = (percentage / 100.0 * width as f32).round() as usize;
|
||||||
let empty = width.saturating_sub(filled);
|
let empty = width.saturating_sub(filled);
|
||||||
|
|
@ -182,7 +155,6 @@ impl StatusPanel {
|
||||||
let empty_chars = "░".repeat(empty);
|
let empty_chars = "░".repeat(empty);
|
||||||
format!("[{}{}]", filled_chars, empty_chars)
|
format!("[{}{}]", filled_chars, empty_chars)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_component_running(process_name: &str) -> bool {
|
pub fn check_component_running(process_name: &str) -> bool {
|
||||||
std::process::Command::new("pgrep")
|
std::process::Command::new("pgrep")
|
||||||
.arg("-f")
|
.arg("-f")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[actix_web::get("/")]
|
#[actix_web::get("/")]
|
||||||
async fn index() -> Result<HttpResponse> {
|
async fn index() -> Result<HttpResponse> {
|
||||||
match fs::read_to_string("web/html/index.html") {
|
match fs::read_to_string("web/html/index.html") {
|
||||||
|
|
@ -12,7 +11,6 @@ async fn index() -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/{botname}")]
|
#[actix_web::get("/{botname}")]
|
||||||
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
let botname = req.match_info().query("botname");
|
let botname = req.match_info().query("botname");
|
||||||
|
|
@ -25,7 +23,6 @@ async fn bot_index(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::get("/static/{filename:.*}")]
|
#[actix_web::get("/static/{filename:.*}")]
|
||||||
async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
|
async fn static_files(req: HttpRequest) -> Result<HttpResponse> {
|
||||||
let filename = req.match_info().query("filename");
|
let filename = req.match_info().query("filename");
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,12 @@
|
||||||
//! Tests for web server module
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::tests::test_util;
|
use crate::tests::test_util;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_web_server_module() {
|
fn test_web_server_module() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
assert!(true, "Basic web server module test");
|
assert!(true, "Basic web server module test");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_server_routes() {
|
fn test_server_routes() {
|
||||||
test_util::setup();
|
test_util::setup();
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ llm-key,none
|
||||||
llm-url,http://localhost:8081
|
llm-url,http://localhost:8081
|
||||||
llm-model,../../../../data/llm/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf
|
llm-model,../../../../data/llm/DeepSeek-R1-Distill-Qwen-1.5B-Q3_K_M.gguf
|
||||||
|
|
||||||
|
prompt-compact,4
|
||||||
|
|
||||||
mcp-server,false
|
mcp-server,false
|
||||||
|
|
||||||
embedding-url,http://localhost:8082
|
embedding-url,http://localhost:8082
|
||||||
|
|
|
||||||
|
Loading…
Add table
Reference in a new issue