use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use crossbeam_channel::Receiver;
use rig_core::tool::{ToolErrorKind, ToolExecutionError, ToolResult};
use rig_ecs::stage::{AgentPass, IfStopped, Outcome, Stage};
use rig_ecs::tasks::Wake;
use rig_tools::{Live, OutputLimit};
#[derive(Component, Clone, Debug, Default, PartialEq, Eq)]
pub struct ToolProgress(pub String);
#[derive(Component, Debug)]
pub struct CallOutput {
lines: Receiver<String>,
kept: String,
cut_lines: usize,
}
impl CallOutput {
pub fn new(wake: &Wake) -> (Self, Live) {
let (sender, lines) = crossbeam_channel::unbounded();
let wake = wake.clone();
let live = Live::new(move |text: &str| {
if sender.send(text.to_owned()).is_ok() {
wake.wake();
}
});
let output = Self {
lines,
kept: String::new(),
cut_lines: 0,
};
(output, live)
}
pub fn text(&self) -> String {
let (kept, dropped) = OutputLimit::default().tail(&self.kept);
match self.cut_lines + dropped {
0 => kept.to_owned(),
cut => format!("[{cut} earlier lines cut]\n{kept}"),
}
}
fn read(&mut self) -> bool {
let mut came = false;
for piece in self.lines.try_iter() {
self.kept.push_str(&piece);
came = true;
}
let cut = OutputLimit::default();
if self.kept.len() > 2 * cut.bytes {
let (kept, dropped) = cut.tail(&self.kept);
self.cut_lines += dropped;
self.kept = kept.to_owned();
}
came
}
fn if_stopped(&self) -> IfStopped {
let mut text = self.text();
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
text.push_str("[stopped before it finished]");
let stopped =
ToolExecutionError::new(ToolErrorKind::Cancelled, "stopped before it finished")
.with_model_feedback(text);
IfStopped(Outcome::from(ToolResult::failed(stopped)))
}
}
pub(crate) struct ProgressPlugin;
impl Plugin for ProgressPlugin {
fn build(&self, app: &mut App) {
app.add_systems(AgentPass, follow_call_output.in_set(Stage::Arrive));
}
}
fn follow_call_output(
mut calls: Query<(Entity, &mut CallOutput), Without<Outcome>>,
mut commands: Commands,
) {
for (call, mut output) in &mut calls {
if !output.read() {
continue;
}
let last = output
.kept
.lines()
.rev()
.find(|line| !line.trim().is_empty());
let progress = ToolProgress(last.unwrap_or_default().trim_end().to_owned());
let stopped = output.if_stopped();
commands.entity(call).try_insert((progress, stopped));
}
}
#[cfg(test)]
mod tests;