diff --git a/src/basic/compiler/mod.rs b/src/basic/compiler/mod.rs index ceca63ec..378a9504 100644 --- a/src/basic/compiler/mod.rs +++ b/src/basic/compiler/mod.rs @@ -493,7 +493,8 @@ impl BasicCompiler { .replace("GROUP BY", "GROUP_BY") .replace("ADD SUGGESTION TOOL", "ADD_SUGGESTION_TOOL") .replace("ADD SUGGESTION TEXT", "ADD_SUGGESTION_TEXT") - .replace("ADD SUGGESTION", "ADD_SUGGESTION"); + .replace("ADD SUGGESTION", "ADD_SUGGESTION") + .replace("ADD SWITCHER", "ADD_SWITCHER"); if normalized.starts_with("SET SCHEDULE") || trimmed.starts_with("SET SCHEDULE") { has_schedule = true; let parts: Vec<&str> = normalized.split('"').collect(); diff --git a/src/basic/keywords/mod.rs b/src/basic/keywords/mod.rs index 0c56f1ef..08f09372 100644 --- a/src/basic/keywords/mod.rs +++ b/src/basic/keywords/mod.rs @@ -5,6 +5,8 @@ pub mod add_bot; pub mod add_member; #[cfg(feature = "chat")] pub mod add_suggestion; +#[cfg(feature = "chat")] +pub mod switcher; pub mod agent_reflection; #[cfg(feature = "llm")] pub mod ai_tools; @@ -203,6 +205,9 @@ pub fn get_all_keywords() -> Vec { "SMS".to_string(), "ADD SUGGESTION".to_string(), "ADD_SUGGESTION_TOOL".to_string(), + "ADD SWITCHER".to_string(), + "ADD_SWITCHER".to_string(), + "CLEAR SWITCHERS".to_string(), "CLEAR SUGGESTIONS".to_string(), "ADD TOOL".to_string(), "CLEAR TOOLS".to_string(), diff --git a/src/basic/keywords/switcher.rs b/src/basic/keywords/switcher.rs new file mode 100644 index 00000000..d1d8538c --- /dev/null +++ b/src/basic/keywords/switcher.rs @@ -0,0 +1,155 @@ +use crate::core::shared::models::UserSession; +use crate::core::shared::state::AppState; +use log::{error, trace}; +use rhai::{Dynamic, Engine}; +use serde_json::json; +use std::sync::Arc; +use std::time::Duration; + +fn get_redis_connection(cache_client: &Arc) -> Option { + let timeout = Duration::from_millis(50); + cache_client.get_connection_with_timeout(timeout).ok() +} + +pub fn clear_switchers_keyword( + state: Arc, + user_session: UserSession, + engine: &mut Engine, +) { + let cache = state.cache.clone(); + + engine + .register_custom_syntax(["CLEAR", "SWITCHERS"], true, move |_context, _inputs| { + if let Some(cache_client) = &cache { + let redis_key = format!("suggestions:{}:{}", user_session.bot_id, user_session.id); + let mut conn = match get_redis_connection(cache_client) { + Some(conn) => conn, + None => { + trace!("Cache not ready, skipping clear switchers"); + return Ok(Dynamic::UNIT); + } + }; + + let result: Result = + redis::cmd("DEL").arg(&redis_key).query(&mut conn); + + match result { + Ok(deleted) => { + trace!( + "Cleared {} switchers from session {}", + deleted, + user_session.id + ); + } + Err(e) => error!("Failed to clear switchers from Redis: {}", e), + } + } else { + trace!("No cache configured, switchers not cleared"); + } + + Ok(Dynamic::UNIT) + }) + .expect("valid syntax registration"); +} + +pub fn add_switcher_keyword( + state: Arc, + user_session: UserSession, + engine: &mut Engine, +) { + let cache = state.cache.clone(); + + // ADD_SWITCHER "switcher_name" as "button text" + // Note: compiler converts AS -> as (lowercase keywords), so we use lowercase here + engine + .register_custom_syntax( + ["ADD_SWITCHER", "$expr$", "as", "$expr$"], + true, + move |context, inputs| { + let switcher_name = context.eval_expression_tree(&inputs[0])?.to_string(); + let button_text = context.eval_expression_tree(&inputs[1])?.to_string(); + + add_switcher( + cache.as_ref(), + &user_session, + &switcher_name, + &button_text, + )?; + + Ok(Dynamic::UNIT) + }, + ) + .expect("valid syntax registration"); +} + +fn add_switcher( + cache: Option<&Arc>, + user_session: &UserSession, + switcher_name: &str, + button_text: &str, +) -> Result<(), Box> { + trace!( + "ADD_SWITCHER called: switcher={}, button={}", + switcher_name, + button_text + ); + + if let Some(cache_client) = cache { + let redis_key = format!("suggestions:{}:{}", user_session.bot_id, user_session.id); + + let suggestion = json!({ + "type": "switcher", + "switcher": switcher_name, + "text": button_text, + "action": { + "type": "switch_context", + "switcher": switcher_name + } + }); + + let mut conn = match get_redis_connection(cache_client) { + Some(conn) => conn, + None => { + trace!("Cache not ready, skipping add switcher"); + return Ok(()); + } + }; + + let _: Result = redis::cmd("SADD") + .arg(&redis_key) + .arg(suggestion.to_string()) + .query(&mut conn); + + trace!( + "Added switcher suggestion '{}' to session {}", + switcher_name, + user_session.id + ); + } else { + trace!("No cache configured, switcher suggestion not added"); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + #[test] + fn test_switcher_json() { + let suggestion = json!({ + "type": "switcher", + "switcher": "mode_switcher", + "text": "Switch Mode", + "action": { + "type": "switch_context", + "switcher": "mode_switcher" + } + }); + + assert_eq!(suggestion["type"], "switcher"); + assert_eq!(suggestion["action"]["type"], "switch_context"); + assert_eq!(suggestion["switcher"], "mode_switcher"); + } +} diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 38297dbc..74cb7345 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -27,6 +27,8 @@ use self::keywords::add_bot::register_bot_keywords; use self::keywords::add_member::add_member_keyword; #[cfg(feature = "chat")] use self::keywords::add_suggestion::add_suggestion_keyword; +#[cfg(feature = "chat")] +use self::keywords::switcher::{add_switcher_keyword, clear_switchers_keyword}; #[cfg(feature = "llm")] use self::keywords::ai_tools::register_ai_tools_keywords; use self::keywords::bot_memory::{get_bot_memory_keyword, set_bot_memory_keyword}; @@ -157,12 +159,16 @@ impl ScriptService { set_user_keyword(state.clone(), user.clone(), &mut engine); #[cfg(feature = "chat")] clear_suggestions_keyword(state.clone(), user.clone(), &mut engine); + #[cfg(feature = "chat")] + clear_switchers_keyword(state.clone(), user.clone(), &mut engine); use_tool_keyword(state.clone(), user.clone(), &mut engine); clear_tools_keyword(state.clone(), user.clone(), &mut engine); clear_websites_keyword(state.clone(), user.clone(), &mut engine); #[cfg(feature = "chat")] add_suggestion_keyword(state.clone(), user.clone(), &mut engine); #[cfg(feature = "chat")] + add_switcher_keyword(state.clone(), user.clone(), &mut engine); + #[cfg(feature = "chat")] add_member_keyword(state.clone(), user.clone(), &mut engine); #[cfg(feature = "chat")] register_bot_keywords(&state, &user, &mut engine); @@ -1313,6 +1319,7 @@ impl ScriptService { (r#"ADD_SUGGESTION_TOOL"#, 2, 2, vec!["tool", "text"]), (r#"ADD_SUGGESTION_TEXT"#, 2, 2, vec!["value", "text"]), (r#"ADD_SUGGESTION(?!\\s+TOOL|\\s+TEXT|_)"#, 2, 2, vec!["context", "text"]), + (r#"ADD_SWITCHER"#, 2, 2, vec!["switcher", "text"]), (r#"ADD\\s+MEMBER"#, 2, 2, vec!["name", "role"]), // CREATE family @@ -1344,6 +1351,7 @@ impl ScriptService { if trimmed_upper.contains("ADD_SUGGESTION_TOOL") || trimmed_upper.contains("ADD_SUGGESTION_TEXT") || trimmed_upper.starts_with("ADD_SUGGESTION_") || + trimmed_upper.contains("ADD_SWITCHER") || trimmed_upper.starts_with("ADD_MEMBER") || (trimmed_upper.starts_with("USE_") && trimmed.contains('(')) { // Keep original line and add semicolon if needed