hopefully a bit better tool call detection

This commit is contained in:
Dhanji Prasanna
2025-10-02 10:27:58 +10:00
parent 9638f40cfb
commit e324ddd99d
4 changed files with 194 additions and 19 deletions

1
Cargo.lock generated
View File

@@ -1015,6 +1015,7 @@ dependencies = [
"g3-execution",
"g3-providers",
"rand",
"regex",
"reqwest",
"serde",
"serde_json",

View File

@@ -307,7 +307,31 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co
if e.to_string().contains("cancelled") {
output.print("⚠️ Operation cancelled by user");
} else {
// Enhanced error logging with detailed information
error!("=== TASK EXECUTION ERROR ===");
error!("Error: {}", e);
// Log error chain
let mut source = e.source();
let mut depth = 1;
while let Some(err) = source {
error!(" Caused by [{}]: {}", depth, err);
source = err.source();
depth += 1;
}
// Log additional context
error!("Task input: {}", input);
error!("Error type: {}", std::any::type_name_of_val(&e));
// Display user-friendly error message
output.print(&format!("❌ Error: {}", e));
// If it's a stream error, provide helpful guidance
if e.to_string().contains("No response received") {
output.print("💡 This may be a temporary issue. Please try again or check the logs for more details.");
output.print(" Log files are saved in the 'logs/' directory.");
}
}
}
}

View File

@@ -22,3 +22,4 @@ tokio-util = "0.7"
futures-util = "0.3"
chrono = { version = "0.4", features = ["serde"] }
rand = "0.8"
regex = "1.0"

View File

@@ -7,6 +7,7 @@ use anyhow::Result;
use g3_config::Config;
use g3_execution::CodeExecutor;
use g3_providers::{CompletionRequest, Message, MessageRole, ProviderRegistry, Tool};
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::time::{Duration, Instant};
@@ -993,6 +994,13 @@ The tool will execute immediately and you'll receive the result (success or erro
if attempt > 1 {
info!("Stream started successfully after {} attempts", attempt);
}
debug!("Stream started successfully");
debug!(
"Request had {} messages, tools={}, max_tokens={:?}",
request.messages.len(),
request.tools.is_some(),
request.max_tokens
);
return Ok(stream);
}
Err(e) if attempt < MAX_ATTEMPTS => {
@@ -1194,10 +1202,20 @@ The tool will execute immediately and you'll receive the result (success or erro
.unwrap_or_else(|_| "Failed to serialize request".to_string()),
);
// Log initial request details
debug!("Starting stream with provider={}, model={}, messages={}, tools={}, max_tokens={:?}",
provider.name(),
provider.model(),
request.messages.len(),
request.tools.is_some(),
request.max_tokens
);
// Try to get stream with retry logic
let mut stream = match self.stream_with_retry(&request, &error_context).await {
Ok(s) => s,
Err(e) => {
error!("Failed to start stream: {}", e);
// Additional retry for "busy" errors on subsequent iterations
if iteration_count > 1 && e.to_string().contains("busy") {
warn!(
@@ -1210,6 +1228,7 @@ The tool will execute immediately and you'll receive the result (success or erro
Ok(s) => s,
Err(e2) => {
error!("Failed to start stream after retry: {}", e2);
error_context.clone().log_error(&e2);
return Err(e2);
}
}
@@ -1223,10 +1242,25 @@ The tool will execute immediately and you'll receive the result (success or erro
let mut current_response = String::new();
let mut tool_executed = false;
let mut chunks_received = 0;
let mut raw_chunks: Vec<String> = Vec::new(); // Store raw chunks for debugging
let mut _last_error: Option<String> = None;
while let Some(chunk_result) = stream.next().await {
match chunk_result {
Ok(chunk) => {
// Store raw chunk for debugging (limit to first 20 and last 5)
if chunks_received < 20 || chunk.finished {
raw_chunks.push(format!(
"Chunk #{}: content={:?}, finished={}, tool_calls={:?}",
chunks_received + 1,
chunk.content,
chunk.finished,
chunk.tool_calls
));
} else if raw_chunks.len() == 20 {
raw_chunks.push("... (chunks 21+ omitted for brevity) ...".to_string());
}
// Record time to first token
if first_token_time.is_none() && !chunk.content.is_empty() {
first_token_time = Some(stream_start.elapsed());
@@ -1234,7 +1268,19 @@ The tool will execute immediately and you'll receive the result (success or erro
chunks_received += 1;
if chunks_received == 1 {
debug!("First chunk received: content_len={}, finished={}", chunk.content.len(), chunk.finished);
debug!(
"First chunk received: content_len={}, finished={}",
chunk.content.len(),
chunk.finished
);
}
// Log raw chunk data for debugging
if chunks_received <= 5 || chunk.finished {
debug!(
"Chunk #{}: content={:?}, finished={}, tool_calls={:?}",
chunks_received, chunk.content, chunk.finished, chunk.tool_calls
);
}
// Process chunk with the new parser
@@ -1456,6 +1502,90 @@ The tool will execute immediately and you'll receive the result (success or erro
// No tools were executed in this iteration
// Check if we got any response at all
if current_response.is_empty() && full_response.is_empty() {
// Log detailed error information before failing
error!(
"=== STREAM ERROR: No content or tool calls received ==="
);
error!("Iteration: {}/{}", iteration_count, MAX_ITERATIONS);
error!(
"Provider: {} (model: {})",
provider.name(),
provider.model()
);
error!("Chunks received: {}", chunks_received);
error!("Parser state:");
error!(" - Text buffer length: {}", parser.text_buffer_len());
error!(
" - Text buffer content: {:?}",
parser.get_text_content()
);
error!(" - Native tool calls: {:?}", parser.native_tool_calls);
error!(" - Message stopped: {}", parser.is_message_stopped());
error!(" - In JSON tool call: {}", parser.in_json_tool_call);
error!(" - JSON tool start: {:?}", parser.json_tool_start);
error!("Request details:");
error!(" - Messages count: {}", request.messages.len());
error!(" - Has tools: {}", request.tools.is_some());
error!(" - Max tokens: {:?}", request.max_tokens);
error!(" - Temperature: {:?}", request.temperature);
error!(" - Stream: {}", request.stream);
// Log raw chunks received
error!("Raw chunks received ({} total):", chunks_received);
for (i, chunk_str) in raw_chunks.iter().take(25).enumerate() {
error!(" [{}] {}", i, chunk_str);
}
// Log the full request JSON
match serde_json::to_string_pretty(&request) {
Ok(json) => {
error!("Full request JSON:\n{}", json);
}
Err(e) => {
error!("Failed to serialize request: {}", e);
}
}
// Log last user message for context
if let Some(last_user_msg) = request
.messages
.iter()
.rev()
.find(|m| matches!(m.role, MessageRole::User))
{
error!(
"Last user message: {}",
if last_user_msg.content.len() > 500 {
format!(
"{}... (truncated)",
&last_user_msg.content[..500]
)
} else {
last_user_msg.content.clone()
}
);
}
// Log context window state
error!("Context window state:");
error!(
" - Used tokens: {}/{}",
self.context_window.used_tokens,
self.context_window.total_tokens
);
error!(
" - Percentage used: {:.1}%",
self.context_window.percentage_used()
);
error!(
" - Conversation history length: {}",
self.context_window.conversation_history.len()
);
// Log session info
error!("Session ID: {:?}", self.session_id);
error!("=== END STREAM ERROR ===");
// No response received - this is an error condition
warn!("Stream finished without any content or tool calls");
warn!("Chunks received: {}", chunks_received);
@@ -1470,19 +1600,34 @@ The tool will execute immediately and you'll receive the result (success or erro
}
println!();
let ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed());
let ttft =
first_token_time.unwrap_or_else(|| stream_start.elapsed());
return Ok((full_response, ttft));
}
break; // Tool was executed, break to continue outer loop
}
}
Err(e) => {
error!("Streaming error: {}", e);
// Capture detailed streaming error information
let error_details =
format!("Streaming error at chunk {}: {}", chunks_received + 1, e);
error!("{}", error_details);
error!("Error type: {}", std::any::type_name_of_val(&e));
error!("Parser state at error: text_buffer_len={}, native_tool_calls={}, message_stopped={}",
parser.text_buffer_len(), parser.native_tool_calls.len(), parser.is_message_stopped());
// Store the error for potential logging later
_last_error = Some(error_details);
if tool_executed {
warn!("Stream error after tool execution, attempting to continue");
break; // Break to outer loop to start new stream
} else {
// Log raw chunks before failing
error!("Fatal streaming error. Raw chunks received before error:");
for chunk_str in raw_chunks.iter().take(10) {
error!(" {}", chunk_str);
}
return Err(e);
}
}
@@ -1492,7 +1637,10 @@ The tool will execute immediately and you'll receive the result (success or erro
// If we get here and no tool was executed, we're done
if !tool_executed {
if current_response.is_empty() && full_response.is_empty() {
warn!("Loop exited without any response after {} iterations", iteration_count);
warn!(
"Loop exited without any response after {} iterations",
iteration_count
);
} else {
full_response.push_str(&current_response);
println!();
@@ -2099,7 +2247,8 @@ fn filter_json_tool_calls(content: &str) -> String {
}
// Check if this looks like the start of a JSON tool call (larger chunks)
if trimmed.starts_with('{') && (trimmed.contains("tool") || trimmed.contains('"')) {
let pattern = Regex::new(r#"\{\s*"tool"\s*:"#).unwrap();
if pattern.is_match(trimmed) {
// This might be the start of a JSON tool call
// Enter suppression mode preemptively
debug!("Detected potential JSON tool call start - entering suppression mode");