fix: Improve tool call handling and BASIC SELECT/CASE conversion
All checks were successful
BotServer CI / build (push) Successful in 9m34s

- 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
This commit is contained in:
Rodrigo Rodriguez 2026-02-16 09:30:19 +00:00
parent 4ca7e5da40
commit c264ad1294
2 changed files with 163 additions and 9 deletions

View file

@ -595,6 +595,8 @@ impl ScriptService {
info!("[TOOL] Preprocessed tool script for Rhai compilation"); info!("[TOOL] Preprocessed tool script for Rhai compilation");
// Convert IF ... THEN / END IF to if ... { } // Convert IF ... THEN / END IF to if ... { }
let script = Self::convert_if_then_syntax(&script); 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) // Convert BASIC keywords to lowercase (but preserve variable casing)
let script = Self::convert_keywords_to_lowercase(&script); let script = Self::convert_keywords_to_lowercase(&script);
// Save to file for debugging // Save to file for debugging
@ -662,9 +664,11 @@ impl ScriptService {
None => continue, // Skip invalid IF statement None => continue, // Skip invalid IF statement
}; };
let condition = &trimmed[3..then_pos].trim(); 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); log::info!("[TOOL] Converting IF statement: condition='{}'", condition);
result.push_str("if "); result.push_str("if ");
result.push_str(condition); result.push_str(&condition);
result.push_str(" {\n"); result.push_str(" {\n");
if_stack.push(true); if_stack.push(true);
continue; continue;
@ -803,12 +807,105 @@ impl ScriptService {
result 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<String> = 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 &current_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 &current_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 /// Convert BASIC keywords to lowercase without touching variables
/// This is a simplified version of normalize_variables_to_lowercase for tools /// This is a simplified version of normalize_variables_to_lowercase for tools
fn convert_keywords_to_lowercase(script: &str) -> String { fn convert_keywords_to_lowercase(script: &str) -> String {
let keywords = [ let keywords = [
"IF", "THEN", "ELSE", "END IF", "FOR", "NEXT", "WHILE", "WEND", "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", "WITH", "END WITH", "AND", "OR", "NOT", "MOD",
"DIM", "AS", "NEW", "FUNCTION", "SUB", "CALL", "DIM", "AS", "NEW", "FUNCTION", "SUB", "CALL",
]; ];

View file

@ -643,6 +643,7 @@ impl BotOrchestrator {
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;
let mut tool_call_buffer = String::new(); // Accumulate potential tool call JSON chunks
let handler = llm_models::get_handler(&model); let handler = llm_models::get_handler(&model);
info!("[STREAM_START] Entering stream processing loop for model: {}", model); info!("[STREAM_START] Entering stream processing loop for model: {}", model);
@ -676,17 +677,35 @@ impl BotOrchestrator {
trace!("Received LLM chunk: {:?}", chunk); trace!("Received LLM chunk: {:?}", chunk);
// ===== GENERIC TOOL EXECUTION ===== // ===== GENERIC TOOL EXECUTION =====
// Check if this chunk contains a tool call (works with all LLM providers) // Add chunk to tool_call_buffer and try to parse
if let Some(tool_call) = ToolExecutor::parse_tool_call(&chunk) { // 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!( info!(
"[TOOL_CALL] Detected tool '{}' from LLM, executing...", "[TOOL_CALL] Detected tool '{}' from LLM, executing...",
tool_call.tool_name tc.tool_name
); );
let execution_result = ToolExecutor::execute_tool_call( let execution_result = ToolExecutor::execute_tool_call(
&self.state, &self.state,
&bot_name_for_context, &bot_name_for_context,
&tool_call, &tc,
&session_id, &session_id,
&user_id, &user_id,
) )
@ -695,7 +714,7 @@ impl BotOrchestrator {
if execution_result.success { if execution_result.success {
info!( info!(
"[TOOL_EXEC] Tool '{}' executed successfully: {}", "[TOOL_EXEC] Tool '{}' executed successfully: {}",
tool_call.tool_name, execution_result.result tc.tool_name, execution_result.result
); );
// Send tool execution result to user // Send tool execution result to user
@ -721,13 +740,13 @@ impl BotOrchestrator {
} else { } else {
error!( error!(
"[TOOL_EXEC] Tool '{}' execution failed: {:?}", "[TOOL_EXEC] Tool '{}' execution failed: {:?}",
tool_call.tool_name, execution_result.error tc.tool_name, execution_result.error
); );
// Send error to user // Send error to user
let error_msg = format!( let error_msg = format!(
"Erro ao executar ferramenta '{}': {:?}", "Erro ao executar ferramenta '{}': {:?}",
tool_call.tool_name, tc.tool_name,
execution_result.error execution_result.error
); );
@ -753,9 +772,47 @@ impl BotOrchestrator {
} }
// Don't add tool_call JSON to full_response or analysis_buffer // 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 to next chunk
continue; 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 ===== // ===== END TOOL EXECUTION =====
analysis_buffer.push_str(&chunk); analysis_buffer.push_str(&chunk);