fix: Improve tool call handling and BASIC SELECT/CASE conversion
All checks were successful
BotServer CI / build (push) Successful in 9m34s
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:
parent
4ca7e5da40
commit
c264ad1294
2 changed files with 163 additions and 9 deletions
101
src/basic/mod.rs
101
src/basic/mod.rs
|
|
@ -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 ¤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
|
/// 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",
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue