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");
// 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<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
/// 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",
];

View file

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