feat: add ADD_SWITCHER keyword with underscore preprocessing
All checks were successful
BotServer CI/CD / build (push) Successful in 3m25s

Implement ADD_SWITCHER keyword following the same pattern as ADD_SUGGESTION_TOOL:
- Created switcher.rs module with add_switcher_keyword() and clear_switchers_keyword()
- Added preprocessing to convert "ADD SWITCHER" to "ADD_SWITCHER"
- Added to keyword patterns and get_all_keywords()
- Stores switcher suggestions in Redis with type "switcher" and action "switch_context"
- Supports both "ADD SWITCHER" and "ADD_SWITCHER" syntax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-04-13 15:47:21 -03:00
parent 498c771d7b
commit 6d987c0eea
4 changed files with 170 additions and 1 deletions

View file

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

View file

@ -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<String> {
"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(),

View file

@ -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<redis::Client>) -> Option<redis::Connection> {
let timeout = Duration::from_millis(50);
cache_client.get_connection_with_timeout(timeout).ok()
}
pub fn clear_switchers_keyword(
state: Arc<AppState>,
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<i64, redis::RedisError> =
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<AppState>,
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<redis::Client>>,
user_session: &UserSession,
switcher_name: &str,
button_text: &str,
) -> Result<(), Box<rhai::EvalAltResult>> {
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<i64, redis::RedisError> = 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");
}
}

View file

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