From 86bb4cad8eeca129ad1264f1c19caa1856188eaa Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 5 Apr 2026 19:11:04 -0300 Subject: [PATCH] fix(botserver): Handle TOOL_EXEC message type for direct tool execution without KB/LLM --- src/basic/keywords/add_suggestion.rs | 9 +-- src/basic/mod.rs | 2 + src/core/bot/mod.rs | 97 ++++++++++++++++++++++++++++ src/core/bot/tool_executor.rs | 24 +++++++ 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/basic/keywords/add_suggestion.rs b/src/basic/keywords/add_suggestion.rs index 2f501ad7..0a2f9910 100644 --- a/src/basic/keywords/add_suggestion.rs +++ b/src/basic/keywords/add_suggestion.rs @@ -323,18 +323,19 @@ fn add_tool_suggestion( let redis_key = format!("suggestions:{}:{}", user_session.bot_id, user_session.id); info!("Adding suggestion to Redis key: {}", redis_key); - // Create action object and serialize it to JSON string + let prompt_for_params = params.is_some() && !params.as_ref().unwrap().is_empty(); let action_obj = json!({ "type": "invoke_tool", "tool": tool_name, "params": params, - "prompt_for_params": params.is_none() + "prompt_for_params": prompt_for_params }); - let action_str = action_obj.to_string(); let suggestion = json!({ + "type": "invoke_tool", "text": button_text, - "action": action_str + "tool": tool_name, + "action": action_obj }); let mut conn = match get_redis_connection(cache_client) { diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 9cb12885..5bdc2caf 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -589,6 +589,8 @@ impl ScriptService { trimmed.starts_with("PARAM\t") || trimmed.starts_with("DESCRIPTION ") || trimmed.starts_with("DESCRIPTION\t") || + trimmed.starts_with("REM ") || + trimmed.starts_with("REM\t") || trimmed.starts_with('\'') || // BASIC comment lines trimmed.starts_with('#') || // Hash comment lines trimmed.is_empty()) diff --git a/src/core/bot/mod.rs b/src/core/bot/mod.rs index 40f67bac..f0dadc0c 100644 --- a/src/core/bot/mod.rs +++ b/src/core/bot/mod.rs @@ -448,6 +448,102 @@ impl BotOrchestrator { let session_id = Uuid::parse_str(&message.session_id)?; let message_content = message.content.clone(); + // Handle direct tool execution via TOOL_EXEC message type (invisible to user) + if message.message_type == MessageType::TOOL_EXEC { + let tool_name = message_content.trim(); + if !tool_name.is_empty() { + info!("[TOOL_EXEC] Direct tool execution: {}", tool_name); + + // Get bot name from bot_id + let bot_name = if let Ok(bot_uuid) = Uuid::parse_str(&message.bot_id) { + let conn = self.state.conn.get().ok(); + conn.and_then(|mut db_conn| { + use crate::core::shared::models::schema::bots::dsl::*; + bots.filter(id.eq(bot_uuid)) + .select(name) + .first::(&mut db_conn) + .ok() + }).unwrap_or_else(|| "default".to_string()) + } else { + "default".to_string() + }; + + let tool_result = ToolExecutor::execute_tool_by_name( + &self.state, + &bot_name, + tool_name, + &session_id, + &user_id, + ).await; + + let response_content = if tool_result.success { + tool_result.result + } else { + format!("Erro ao executar '{}': {}", tool_name, tool_result.error.unwrap_or_default()) + }; + + let final_response = BotResponse { + bot_id: message.bot_id.clone(), + user_id: message.user_id.clone(), + session_id: message.session_id.clone(), + channel: message.channel.clone(), + content: response_content, + message_type: MessageType::BOT_RESPONSE, + stream_token: None, + is_complete: true, + suggestions: vec![], + context_name: None, + context_length: 0, + context_max_length: 0, + }; + + let _ = response_tx.send(final_response).await; + return Ok(()); + } + } + + // Legacy: Handle direct tool invocation via __TOOL__: prefix + if message_content.starts_with("__TOOL__:") { + let tool_name = message_content.trim_start_matches("__TOOL__:").trim(); + if !tool_name.is_empty() { + info!("Direct tool invocation via WS: {}", tool_name); + + let tool_result = ToolExecutor::execute_tool_by_name( + &self.state, + &message.bot_id, + tool_name, + &session_id, + &user_id, + ).await; + + let response_content = if tool_result.success { + tool_result.result + } else { + format!("Erro ao executar tool '{}': {}", tool_name, tool_result.error.unwrap_or_default()) + }; + + let final_response = BotResponse { + bot_id: message.bot_id.clone(), + user_id: message.user_id.clone(), + session_id: message.session_id.clone(), + channel: message.channel.clone(), + content: response_content, + message_type: MessageType::BOT_RESPONSE, + stream_token: None, + is_complete: true, + suggestions: vec![], + context_name: None, + context_length: 0, + context_max_length: 0, + }; + + if let Err(e) = response_tx.send(final_response).await { + error!("Failed to send tool response: {}", e); + } + return Ok(()); + } + } + // If a HEAR is blocking the script thread for this session, deliver the input // directly and return — the script continues from where it paused. if crate::basic::keywords::hearing::deliver_hear_input( @@ -675,6 +771,7 @@ impl BotOrchestrator { return Ok(()); } + // Inject KB context for normal messages if let Some(kb_manager) = self.state.kb_manager.as_ref() { let context = crate::core::bot::kb_context::KbInjectionContext { session_id, diff --git a/src/core/bot/tool_executor.rs b/src/core/bot/tool_executor.rs index 7dcfec48..69640fe2 100644 --- a/src/core/bot/tool_executor.rs +++ b/src/core/bot/tool_executor.rs @@ -364,6 +364,30 @@ impl ToolExecutor { // Fallback to source path for error messages (even if it doesn't exist) source_path } + + /// Execute a tool directly by name (without going through LLM) + pub async fn execute_tool_by_name( + state: &Arc, + bot_name: &str, + tool_name: &str, + session_id: &Uuid, + user_id: &Uuid, + ) -> ToolExecutionResult { + let tool_call_id = format!("direct_{}", Uuid::new_v4()); + + info!( + "[TOOL_EXEC] Direct tool invocation: '{}' for bot '{}', session '{}'", + tool_name, bot_name, session_id + ); + + let tool_call = ParsedToolCall { + id: tool_call_id.clone(), + tool_name: tool_name.to_string(), + arguments: Value::Object(serde_json::Map::new()), + }; + + Self::execute_tool_call(state, bot_name, &tool_call, session_id, user_id).await + } } #[cfg(test)]