Merge branch 'evals' of https://github.com/buster-so/buster into evals

This commit is contained in:
Nate Kelley 2025-04-10 12:27:26 -06:00
commit ab61b21f48
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 192 additions and 84 deletions

View File

@ -13,13 +13,15 @@ use std::time::Instant;
use thiserror::Error;
use tokio::sync::broadcast;
use uuid::Uuid;
use std::process::Command;
// --- Agent Imports ---
use agents::{AgentError, AgentExt, AgentThread, BusterCliAgent};
// Ratatui / Crossterm related imports
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind,
MouseEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
@ -105,6 +107,15 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> {
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "<unknown>".to_string());
// --- Git Repository Check ---
let git_check = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.output(); // Use output to capture status and stderr/stdout if needed
if git_check.is_err() || !git_check.unwrap().status.success() {
println!("{}", colored::Colorize::yellow("Warning: Buster operates best in a git repository."));
}
// --- Get Credentials ---
let (base_url, api_key) = get_api_credentials(&args)?;
if api_key.is_none() {
@ -177,69 +188,83 @@ pub async fn run_chat(args: ChatArgs) -> Result<()> {
.unwrap_or_else(|| std::time::Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
// Quit handlers
if key.modifiers.contains(event::KeyModifiers::CONTROL)
&& key.code == KeyCode::Char('c')
{
app_state.should_quit = true;
continue;
}
if key.code == KeyCode::Esc {
app_state.should_quit = true;
continue;
}
if app_state.input == "/exit" && key.code == KeyCode::Enter {
app_state.should_quit = true;
continue;
}
match event::read()? {
Event::Key(key) => {
if key.kind == KeyEventKind::Press {
// Quit handlers
if key.modifiers.contains(event::KeyModifiers::CONTROL)
&& key.code == KeyCode::Char('c')
{
app_state.should_quit = true;
continue;
}
if key.code == KeyCode::Esc {
app_state.should_quit = true;
continue;
}
if app_state.input == "/exit" && key.code == KeyCode::Enter {
app_state.should_quit = true;
continue;
}
// Check if input allowed
let can_input =
!app_state.is_agent_processing && app_state.active_tool_calls.is_empty();
// Check if input allowed
let can_input =
!app_state.is_agent_processing && app_state.active_tool_calls.is_empty();
if can_input {
match key.code {
KeyCode::Enter => {
// Submit message & trigger agent processing
app_state.submit_message();
if can_input {
match key.code {
KeyCode::Enter => {
// Submit message & trigger agent processing
app_state.submit_message();
// Get the agent receiver immediately
// The agent's run method should quickly setup the stream
// and return the receiver before heavy processing.
match cli_agent.run(&mut app_state.agent_thread, &cwd).await {
Ok(rx) => {
agent_rx = Some(rx); // Start polling this receiver
}
Err(e) => {
// Report error via AppState
app_state.process_agent_message(Err(AgentError(format!(
"Failed to start agent: {}",
e
))));
// Get the agent receiver immediately
// The agent's run method should quickly setup the stream
// and return the receiver before heavy processing.
match cli_agent.run(&mut app_state.agent_thread, &cwd).await {
Ok(rx) => {
agent_rx = Some(rx); // Start polling this receiver
}
Err(e) => {
// Report error via AppState
app_state.process_agent_message(Err(AgentError(format!(
"Failed to start agent: {}",
e
))));
}
}
}
KeyCode::Char(c) => {
app_state.input.push(c);
}
KeyCode::Backspace => {
app_state.input.pop();
}
KeyCode::Up => app_state.scroll_up(),
KeyCode::Down => app_state.scroll_down(),
_ => {} // Ignore other keys when input is enabled
}
KeyCode::Char(c) => {
app_state.input.push(c);
} else {
// Handle scrolling even when input is disabled
match key.code {
KeyCode::Up => app_state.scroll_up(),
KeyCode::Down => app_state.scroll_down(),
_ => {} // Ignore other input
}
KeyCode::Backspace => {
app_state.input.pop();
}
KeyCode::Up => app_state.scroll_up(),
KeyCode::Down => app_state.scroll_down(),
_ => {} // Ignore other keys when input is enabled
}
} else {
// Handle scrolling even when input is disabled
match key.code {
KeyCode::Up => app_state.scroll_up(),
KeyCode::Down => app_state.scroll_down(),
_ => {} // Ignore other input
}
}
}
Event::Mouse(mouse_event) => {
match mouse_event.kind {
MouseEventKind::ScrollUp => {
app_state.scroll_up();
}
MouseEventKind::ScrollDown => {
app_state.scroll_down();
}
_ => {} // Ignore other mouse events (clicks, moves, etc.)
}
}
_ => {} // Ignore other event types (like resize for now)
}
}

View File

@ -1,6 +1,26 @@
use agents::{AgentError, AgentThread};
use litellm::{AgentMessage, MessageProgress, ToolCall};
use uuid::Uuid;
use std::time::Instant;
use serde_json;
use serde_json::Value;
use serde::{Deserialize, Serialize};
// --- Structs for specific tool results (add more as needed) ---
#[derive(Serialize, Deserialize, Debug)]
struct ListDirectoryEntry {
name: String,
path: String,
is_dir: bool,
size: Option<u64>,
// modified_at: Option<String>, // Ignoring for now
}
#[derive(Serialize, Deserialize, Debug)]
struct ListDirectoryResult {
entries: Vec<ListDirectoryEntry>,
}
// --- Application State Structs ---
#[derive(Debug, Clone)]
pub struct ActiveToolCall {
@ -195,10 +215,17 @@ impl AppState {
for tc in calls {
if !existing_tool_ids.contains(&tc.id) {
// Format arguments nicely
let args_json = serde_json::to_string_pretty(&tc.function.arguments)
.unwrap_or_else(|_| "<failed to format args>".to_string());
self.messages.push(AgentMessage::Developer {
id: Some(tc.id.clone()),
content: format!("Executing: {}...", tc.function.name),
name: Some("Tool".to_string()),
content: format!(
"Executing: {}\nArgs:\n{}",
tc.function.name,
args_json
),
name: Some("Tool Call".to_string()), // Change name for clarity
});
}
}
@ -293,10 +320,17 @@ impl AppState {
for tc in calls {
if !existing_tool_ids.contains(&tc.id) {
// Format arguments nicely
let args_json = serde_json::to_string_pretty(&tc.function.arguments)
.unwrap_or_else(|_| "<failed to format args>".to_string());
self.messages.push(AgentMessage::Developer {
id: Some(tc.id.clone()),
content: format!("Executing: {}...", tc.function.name), // Or maybe "Called:"?
name: Some("Tool".to_string()),
content: format!(
"Executing: {}\nArgs:\n{}",
tc.function.name,
args_json
),
name: Some("Tool Call".to_string()), // Change name for clarity
});
}
}
@ -321,16 +355,37 @@ impl AppState {
// Update the placeholder message in the main history
for msg in self.messages.iter_mut() {
if let AgentMessage::Developer {
id: msg_id,
content: msg_content,
..
} = msg
{
if let AgentMessage::Developer { id: msg_id, content: msg_content, name: msg_name, .. } = msg {
if msg_id.as_ref() == Some(&tool_call_id) {
*msg_name = Some(format!("{} Result", name)); // Update name to indicate result
*msg_content = match progress {
MessageProgress::InProgress => format!("Running {}: {}...", name, content), // Show partial content?
MessageProgress::Complete => format!("Result ({}): {}", name, content),
MessageProgress::InProgress => {
// Try to show formatted partial content if possible, else raw
format!("Running {}: {}...", name, content)
},
MessageProgress::Complete => {
// Attempt to parse and format known tool outputs
match name.as_str() {
"list_directory" => {
match serde_json::from_str::<ListDirectoryResult>(&content) {
Ok(parsed_result) => {
let mut formatted = String::from("Entries:\n");
for entry in parsed_result.entries {
formatted.push_str(&format!(
" - {} ({})\n",
entry.name,
if entry.is_dir { "directory" } else { "file" }
));
}
formatted.trim_end().to_string() // Remove trailing newline
}
Err(_) => format!("Result ({}):\n{}", name, content), // Fallback to raw JSON on parse error
}
}
// Add more known tool formatters here
_ => format!("Result ({}):\n{}", name, content), // Default: show raw JSON
}
},
};
found_message = true;
break;

View File

@ -85,7 +85,9 @@ fn render_welcome(frame: &mut Frame, area: Rect, cwd: &str) {
fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) {
use litellm::{AgentMessage, MessageProgress};
let mut message_lines: Vec<Line> = Vec::new();
for msg in app.messages.iter() {
let num_messages = app.messages.len();
for (msg_index, msg) in app.messages.iter().enumerate() {
match msg {
AgentMessage::User { content, .. } => {
message_lines.push(Line::from(vec![
@ -95,32 +97,54 @@ fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) {
}
AgentMessage::Assistant { content, progress, name, .. } => {
let is_in_progress = *progress == MessageProgress::InProgress;
let prefix = Span::styled(
format!(
"{} {}: ",
if is_in_progress { "" } else { "" },
name.as_deref().unwrap_or("Assistant")
),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
let is_last_message = msg_index == num_messages - 1;
let prev_msg_was_tool_result = if msg_index > 0 {
matches!(app.messages.get(msg_index - 1), Some(AgentMessage::Developer { .. }))
} else {
false
};
// Omit prefix if it's the last message AND the previous was a tool result
let show_prefix = !(is_last_message && prev_msg_was_tool_result && !is_in_progress);
let prefix = if show_prefix {
Some(Span::styled(
format!(
"{} {}: ",
if is_in_progress { "" } else { "" },
name.as_deref().unwrap_or("Assistant")
),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
} else {
None // Don't show prefix
};
if let Some(c) = content {
let lines: Vec<&str> = c.split('\n').collect();
for (i, line_content) in lines.iter().enumerate() {
let line_span = Span::styled(*line_content, Style::default().fg(Color::White));
if i == 0 {
message_lines.push(Line::from(vec![prefix.clone(), line_span]));
if let Some(p) = prefix.clone() {
message_lines.push(Line::from(vec![p, line_span]));
} else {
// No prefix, just the content line
message_lines.push(Line::from(line_span));
}
} else {
// Indent subsequent lines (relative to prefix or start)
let indent = if prefix.is_some() { " " } else { "" };
message_lines.push(Line::from(vec![
Span::raw(" "),
Span::raw(indent),
line_span,
]));
}
}
} else {
message_lines.push(Line::from(prefix));
} else if let Some(p) = prefix {
// Show prefix even if content is None (e.g., for InProgress)
message_lines.push(Line::from(p));
}
if !is_in_progress {
@ -168,7 +192,11 @@ fn render_messages(frame: &mut Frame, app: &AppState, area: Rect) {
let content_height = message_lines.len() as u16;
let view_height = area.height;
let max_scroll = content_height.saturating_sub(view_height);
let current_scroll = app.scroll_offset.min(max_scroll);
let current_scroll = if app.scroll_offset == 0 {
max_scroll
} else {
app.scroll_offset.min(max_scroll)
};
let messages_widget = Paragraph::new(message_lines)
.scroll((current_scroll, 0))
@ -247,7 +275,7 @@ fn render_input(frame: &mut Frame, app: &AppState, area: Rect) {
// --- Cursor ---
if !is_input_disabled {
frame.set_cursor(
area.x + input_prefix.len() as u16 + app.input.chars().count() as u16,
area.x + 1 + input_prefix.len() as u16 + app.input.chars().count() as u16, // +1 for left border
area.y + 1,
)
}