fix(agent): improve ReAct parser and tool schemas for better LLM compatibility

- Fix ACTION_INPUT regex to properly capture multiline JSON responses
  - Changed from stopping at first newline to capturing all remaining text
  - Resolves parsing errors when LLM generates formatted JSON with line breaks

- Enhance tool schemas with detailed descriptions and parameter specifications
  - Add comprehensive Message schema for generate_text tool
  - Clarify distinction between resources/get (file read) and resources/list (directory listing)
  - Include clear usage guidance in tool descriptions

- Set default model to llama3.2:latest instead of invalid "ollama"

- Add parse error debugging to help troubleshoot LLM response issues

The agent infrastructure now correctly handles multiline tool arguments and
provides better guidance to LLMs through improved tool schemas. Remaining
errors are due to LLM quality (model making poor tool choices or generating
malformed responses), not infrastructure bugs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-09 19:43:07 +02:00
parent 05e90d3e2b
commit 33d11ae223
25 changed files with 1348 additions and 121 deletions

View File

@@ -51,6 +51,15 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
0
};
// Calculate agent actions panel height (similar to thinking)
let actions_height = if let Some(actions) = app.agent_actions() {
let content_width = available_width.saturating_sub(4);
let visual_lines = calculate_wrapped_line_count(actions.lines(), content_width);
(visual_lines as u16).min(6) + 2 // +2 for borders, max 6 lines
} else {
0
};
let mut constraints = vec![
Constraint::Length(4), // Header
Constraint::Min(8), // Messages
@@ -59,6 +68,10 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
if thinking_height > 0 {
constraints.push(Constraint::Length(thinking_height)); // Thinking
}
// Insert agent actions panel after thinking (if any)
if actions_height > 0 {
constraints.push(Constraint::Length(actions_height)); // Agent actions
}
constraints.push(Constraint::Length(input_height)); // Input
constraints.push(Constraint::Length(5)); // System/Status output (3 lines content + 2 borders)
@@ -80,6 +93,11 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &mut ChatApp) {
render_thinking(frame, layout[idx], app);
idx += 1;
}
// Render agent actions panel if present
if actions_height > 0 {
render_agent_actions(frame, layout[idx], app);
idx += 1;
}
render_input(frame, layout[idx], app);
idx += 1;
@@ -898,6 +916,191 @@ fn render_thinking(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
}
}
// Render a panel displaying the latest ReAct agent actions (thought/action/observation).
// Color-coded: THOUGHT (blue), ACTION (yellow), OBSERVATION (green)
fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
let theme = app.theme().clone();
if let Some(actions) = app.agent_actions().cloned() {
let viewport_height = area.height.saturating_sub(2) as usize; // subtract borders
let content_width = area.width.saturating_sub(4);
// Parse and color-code ReAct components
let mut lines: Vec<Line> = Vec::new();
for line in actions.lines() {
let line_trimmed = line.trim();
// Detect ReAct components and apply color coding
if line_trimmed.starts_with("THOUGHT:") {
// Blue for THOUGHT
let thought_content = line_trimmed.strip_prefix("THOUGHT:").unwrap_or("").trim();
let wrapped = wrap(thought_content, content_width as usize);
// First line with label
if let Some(first) = wrapped.first() {
lines.push(Line::from(vec![
Span::styled(
"THOUGHT: ",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
Span::styled(first.to_string(), Style::default().fg(Color::Blue)),
]));
}
// Continuation lines
for chunk in wrapped.iter().skip(1) {
lines.push(Line::from(Span::styled(
format!(" {}", chunk),
Style::default().fg(Color::Blue),
)));
}
} else if line_trimmed.starts_with("ACTION:") {
// Yellow for ACTION
let action_content = line_trimmed.strip_prefix("ACTION:").unwrap_or("").trim();
lines.push(Line::from(vec![
Span::styled(
"ACTION: ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled(
action_content,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
]));
} else if line_trimmed.starts_with("ACTION_INPUT:") {
// Cyan for ACTION_INPUT
let input_content = line_trimmed
.strip_prefix("ACTION_INPUT:")
.unwrap_or("")
.trim();
let wrapped = wrap(input_content, content_width as usize);
if let Some(first) = wrapped.first() {
lines.push(Line::from(vec![
Span::styled(
"ACTION_INPUT: ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(first.to_string(), Style::default().fg(Color::Cyan)),
]));
}
for chunk in wrapped.iter().skip(1) {
lines.push(Line::from(Span::styled(
format!(" {}", chunk),
Style::default().fg(Color::Cyan),
)));
}
} else if line_trimmed.starts_with("OBSERVATION:") {
// Green for OBSERVATION
let obs_content = line_trimmed
.strip_prefix("OBSERVATION:")
.unwrap_or("")
.trim();
let wrapped = wrap(obs_content, content_width as usize);
if let Some(first) = wrapped.first() {
lines.push(Line::from(vec![
Span::styled(
"OBSERVATION: ",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
),
Span::styled(first.to_string(), Style::default().fg(Color::Green)),
]));
}
for chunk in wrapped.iter().skip(1) {
lines.push(Line::from(Span::styled(
format!(" {}", chunk),
Style::default().fg(Color::Green),
)));
}
} else if line_trimmed.starts_with("FINAL_ANSWER:") {
// Magenta for FINAL_ANSWER
let answer_content = line_trimmed
.strip_prefix("FINAL_ANSWER:")
.unwrap_or("")
.trim();
let wrapped = wrap(answer_content, content_width as usize);
if let Some(first) = wrapped.first() {
lines.push(Line::from(vec![
Span::styled(
"FINAL_ANSWER: ",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
),
Span::styled(
first.to_string(),
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
),
]));
}
for chunk in wrapped.iter().skip(1) {
lines.push(Line::from(Span::styled(
format!(" {}", chunk),
Style::default().fg(Color::Magenta),
)));
}
} else if !line_trimmed.is_empty() {
// Regular text
let wrapped = wrap(line_trimmed, content_width as usize);
for chunk in wrapped {
lines.push(Line::from(Span::styled(
chunk.into_owned(),
Style::default().fg(theme.text),
)));
}
} else {
// Empty line
lines.push(Line::from(""));
}
}
// Highlight border if this panel is focused
let border_color = if matches!(app.focused_panel(), FocusedPanel::Thinking) {
// Reuse the same focus logic; could add a dedicated enum variant later.
theme.focused_panel_border
} else {
theme.unfocused_panel_border
};
let paragraph = Paragraph::new(lines)
.style(Style::default().bg(theme.background))
.block(
Block::default()
.title(Span::styled(
" 🤖 Agent Actions ",
Style::default()
.fg(theme.thinking_panel_title)
.add_modifier(Modifier::ITALIC),
))
.borders(Borders::ALL)
.border_style(Style::default().fg(border_color))
.style(Style::default().bg(theme.background).fg(theme.text)),
)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
_ = viewport_height;
}
}
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
let theme = app.theme();
let title = match app.mode() {
@@ -1068,17 +1271,35 @@ fn render_status(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
let help_text = "i:Input :m:Model :n:New :c:Clear :h:Help q:Quit";
let spans = vec![
Span::styled(
format!(" {} ", mode_text),
let mut spans = vec![Span::styled(
format!(" {} ", mode_text),
Style::default()
.fg(theme.background)
.bg(mode_bg_color)
.add_modifier(Modifier::BOLD),
)];
// Add agent status indicator if agent mode is active
if app.is_agent_running() {
spans.push(Span::styled(
" 🤖 AGENT RUNNING ",
Style::default()
.fg(theme.background)
.bg(mode_bg_color)
.fg(Color::Black)
.bg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled(" ", Style::default().fg(theme.text)),
Span::styled(help_text, Style::default().fg(theme.info)),
];
));
} else if app.is_agent_mode() {
spans.push(Span::styled(
" 🤖 AGENT MODE ",
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD),
));
}
spans.push(Span::styled(" ", Style::default().fg(theme.text)));
spans.push(Span::styled(help_text, Style::default().fg(theme.info)));
let paragraph = Paragraph::new(Line::from(spans))
.alignment(Alignment::Left)