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");
|
||||
// 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 ¤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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue