From c264ad1294a4b62db9ad007c5675b66f48165a30 Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Mon, 16 Feb 2026 09:30:19 +0000 Subject: [PATCH] fix: Improve tool call handling and BASIC SELECT/CASE conversion - Add tool_call_buffer to accumulate JSON chunks across multiple LLM responses - Handle incomplete tool call JSON that spans multiple chunks - Convert SELECT...CASE/END SELECT to Rhai match expressions - Fix NOT IN operator conversion to !in for IF conditions --- src/basic/mod.rs | 101 +++++++++++++++++++++++++++++++++++++++++++- src/core/bot/mod.rs | 71 ++++++++++++++++++++++++++++--- 2 files changed, 163 insertions(+), 9 deletions(-) diff --git a/src/basic/mod.rs b/src/basic/mod.rs index 0ba93249..05cd9b4c 100644 --- a/src/basic/mod.rs +++ b/src/basic/mod.rs @@ -595,6 +595,8 @@ impl ScriptService { info!("[TOOL] Preprocessed tool script for Rhai compilation"); // Convert IF ... THEN / END IF to if ... { } let script = Self::convert_if_then_syntax(&script); + // Convert SELECT ... CASE / END SELECT to match expressions + let script = Self::convert_select_case_syntax(&script); // Convert BASIC keywords to lowercase (but preserve variable casing) let script = Self::convert_keywords_to_lowercase(&script); // Save to file for debugging @@ -662,9 +664,11 @@ impl ScriptService { None => continue, // Skip invalid IF statement }; let condition = &trimmed[3..then_pos].trim(); + // Convert BASIC "NOT IN" to Rhai "!in" + let condition = condition.replace(" NOT IN ", " !in ").replace(" not in ", " !in "); log::info!("[TOOL] Converting IF statement: condition='{}'", condition); result.push_str("if "); - result.push_str(condition); + result.push_str(&condition); result.push_str(" {\n"); if_stack.push(true); continue; @@ -803,12 +807,105 @@ impl ScriptService { result } + /// Convert BASIC SELECT ... CASE / END SELECT to Rhai match expressions + /// Transforms: SELECT var ... CASE "value" ... END SELECT + /// Into: match var { "value" => { ... } ... } + fn convert_select_case_syntax(script: &str) -> String { + let mut result = String::new(); + let mut lines: Vec<&str> = script.lines().collect(); + let mut i = 0; + + log::info!("[TOOL] Converting SELECT/CASE syntax"); + + while i < lines.len() { + let trimmed = lines[i].trim(); + let upper = trimmed.to_uppercase(); + + // Detect SELECT statement (e.g., "SELECT tipoMissa") + if upper.starts_with("SELECT ") && !upper.contains(" THEN") { + // Extract the variable being selected + let select_var = trimmed[7..].trim(); // Skip "SELECT " + log::info!("[TOOL] Converting SELECT statement for variable: '{}'", select_var); + + // Start match expression + result.push_str(&format!("match {} {{\n", select_var)); + + // Skip the SELECT line + i += 1; + + // Process CASE statements until END SELECT + let mut current_case_body: Vec = Vec::new(); + let mut in_case = false; + + while i < lines.len() { + let case_trimmed = lines[i].trim(); + let case_upper = case_trimmed.to_uppercase(); + + if case_upper == "END SELECT" { + // Close any open case + if in_case { + for body_line in ¤t_case_body { + result.push_str(" "); + result.push_str(body_line); + result.push('\n'); + } + current_case_body.clear(); + in_case = false; + } + // Close the match expression + result.push_str("}\n"); + i += 1; + break; + } else if case_upper.starts_with("CASE ") { + // Close previous case if any + if in_case { + for body_line in ¤t_case_body { + result.push_str(" "); + result.push_str(body_line); + result.push('\n'); + } + current_case_body.clear(); + } + + // Extract the case value (handle both CASE "value" and CASE value) + let case_value = if case_trimmed[5..].trim().starts_with('"') { + // CASE "value" format + case_trimmed[5..].trim().to_string() + } else { + // CASE value format (variable/enum) + format!("\"{}\"", case_trimmed[5..].trim()) + }; + + result.push_str(&format!(" {} => {{\n", case_value)); + in_case = true; + i += 1; + } else { + // Collect body lines for the current case + if in_case { + current_case_body.push(lines[i].to_string()); + } + i += 1; + } + } + + continue; + } + + // Not a SELECT statement - just copy the line + result.push_str(lines[i]); + result.push('\n'); + i += 1; + } + + result + } + /// Convert BASIC keywords to lowercase without touching variables /// This is a simplified version of normalize_variables_to_lowercase for tools fn convert_keywords_to_lowercase(script: &str) -> String { let keywords = [ "IF", "THEN", "ELSE", "END IF", "FOR", "NEXT", "WHILE", "WEND", - "DO", "LOOP", "RETURN", "EXIT", "SELECT", "CASE", "END SELECT", + "DO", "LOOP", "RETURN", "EXIT", "WITH", "END WITH", "AND", "OR", "NOT", "MOD", "DIM", "AS", "NEW", "FUNCTION", "SUB", "CALL", ]; diff --git a/src/core/bot/mod.rs b/src/core/bot/mod.rs index dffb1ee5..32b2341a 100644 --- a/src/core/bot/mod.rs +++ b/src/core/bot/mod.rs @@ -643,6 +643,7 @@ impl BotOrchestrator { let mut full_response = String::new(); let mut analysis_buffer = String::new(); let mut in_analysis = false; + let mut tool_call_buffer = String::new(); // Accumulate potential tool call JSON chunks let handler = llm_models::get_handler(&model); info!("[STREAM_START] Entering stream processing loop for model: {}", model); @@ -676,17 +677,35 @@ impl BotOrchestrator { trace!("Received LLM chunk: {:?}", chunk); // ===== GENERIC TOOL EXECUTION ===== - // Check if this chunk contains a tool call (works with all LLM providers) - if let Some(tool_call) = ToolExecutor::parse_tool_call(&chunk) { + // Add chunk to tool_call_buffer and try to parse + // Tool calls arrive as JSON that can span multiple chunks + let looks_like_json = chunk.trim().starts_with('{') || chunk.trim().starts_with('[') || + tool_call_buffer.contains('{') || tool_call_buffer.contains('['); + + let chunk_in_tool_buffer = if looks_like_json { + tool_call_buffer.push_str(&chunk); + true + } else { + false + }; + + // Try to parse tool call from accumulated buffer + let tool_call = if chunk_in_tool_buffer { + ToolExecutor::parse_tool_call(&tool_call_buffer) + } else { + None + }; + + if let Some(tc) = tool_call { info!( "[TOOL_CALL] Detected tool '{}' from LLM, executing...", - tool_call.tool_name + tc.tool_name ); let execution_result = ToolExecutor::execute_tool_call( &self.state, &bot_name_for_context, - &tool_call, + &tc, &session_id, &user_id, ) @@ -695,7 +714,7 @@ impl BotOrchestrator { if execution_result.success { info!( "[TOOL_EXEC] Tool '{}' executed successfully: {}", - tool_call.tool_name, execution_result.result + tc.tool_name, execution_result.result ); // Send tool execution result to user @@ -721,13 +740,13 @@ impl BotOrchestrator { } else { error!( "[TOOL_EXEC] Tool '{}' execution failed: {:?}", - tool_call.tool_name, execution_result.error + tc.tool_name, execution_result.error ); // Send error to user let error_msg = format!( "Erro ao executar ferramenta '{}': {:?}", - tool_call.tool_name, + tc.tool_name, execution_result.error ); @@ -753,9 +772,47 @@ impl BotOrchestrator { } // Don't add tool_call JSON to full_response or analysis_buffer + // Clear the tool_call_buffer since we found and executed a tool call + tool_call_buffer.clear(); // Continue to next chunk continue; } + + // Clear tool_call_buffer if it's getting too large and no tool call was found + // This prevents memory issues from accumulating JSON fragments + if tool_call_buffer.len() > 10000 { + // Flush accumulated content to client since it's too large to be a tool call + info!("[TOOL_EXEC] Flushing tool_call_buffer (too large, assuming not a tool call)"); + full_response.push_str(&tool_call_buffer); + + let 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: tool_call_buffer.clone(), + message_type: MessageType::BOT_RESPONSE, + stream_token: None, + is_complete: false, + suggestions: Vec::new(), + context_name: None, + context_length: 0, + context_max_length: 0, + }; + + tool_call_buffer.clear(); + + if response_tx.send(response).await.is_err() { + warn!("Response channel closed"); + break; + } + } + + // If this chunk was added to tool_call_buffer and no tool call was found yet, + // skip processing (it's part of an incomplete tool call JSON) + if chunk_in_tool_buffer && tool_call_buffer.len() <= 10000 { + continue; + } // ===== END TOOL EXECUTION ===== analysis_buffer.push_str(&chunk);