Extract common renderings (#432)

This commit is contained in:
Jarrod Sibbison
2024-12-10 09:34:10 +11:00
committed by GitHub
parent 942c5916b8
commit 8cc96703a9
4 changed files with 311 additions and 405 deletions
+1
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
use goose::models::message::Message;
pub mod cliclack;
pub mod renderer;
pub mod rustyline;
pub mod thinking;
+24 -150
View File
@@ -1,14 +1,14 @@
use std::{
collections::HashMap,
io::{self, Write},
};
use std::collections::HashMap;
use anyhow::Result;
use bat::WrappingMode;
use cliclack::{input, set_theme, spinner, Theme as CliclackTheme, ThemeState};
use goose::models::message::{Message, MessageContent, ToolRequest, ToolResponse};
use goose::models::message::Message;
use super::{thinking::get_random_thinking_message, Input, InputType, Prompt, Theme};
use super::{
renderer::{render, BashDeveloperSystemRenderer, DefaultRenderer, ToolRenderer},
thinking::get_random_thinking_message,
Input, InputType, Prompt, Theme,
};
pub struct CliclackPrompt {
spinner: cliclack::ProgressBar,
@@ -24,164 +24,38 @@ enum InputMode {
impl CliclackPrompt {
pub fn new() -> Self {
// // Load highlighting assets
// let assets = HighlightingAssets::from_binary();
// // Fetch and list all available themes
// let themes = assets.themes();
// for theme_name in themes {
// println!("{}", theme_name);
// }
// // List all available syntaxes (languages)
// let syntax_set = assets.get_syntaxes().unwrap();
// for syntax in syntax_set {
// println!("{}", syntax.name);
// }
set_theme(PromptTheme);
let mut renderers: HashMap<String, Box<dyn ToolRenderer>> = HashMap::new();
let default_renderer = DefaultRenderer;
renderers.insert(default_renderer.tool_name(), Box::new(default_renderer));
let bash_dev_system_renderer = BashDeveloperSystemRenderer;
renderers.insert(
bash_dev_system_renderer.tool_name(),
Box::new(bash_dev_system_renderer),
);
CliclackPrompt {
spinner: spinner(),
input_mode: InputMode::Multiline,
theme: Theme::Dark,
input_mode: InputMode::Singleline,
theme: std::env::var("GOOSE_CLI_THEME")
.ok()
.map(|val| {
if val.eq_ignore_ascii_case("light") {
Theme::Light
} else {
Theme::Dark
}
})
.unwrap_or(Theme::Dark),
renderers,
}
}
}
/// Implement the ToolRenderer trait for each tool that you want to render in the prompt.
trait ToolRenderer {
fn tool_name(&self) -> String;
fn request(&self, tool_request: &ToolRequest, theme: &str);
fn response(&self, tool_response: &ToolResponse, theme: &str);
}
struct DefaultRenderer;
impl ToolRenderer for DefaultRenderer {
fn tool_name(&self) -> String {
"default".to_string()
}
fn request(&self, tool_request: &ToolRequest, theme: &str) {
match &tool_request.tool_call {
Ok(call) => {
print_tool_request(
&serde_json::to_string_pretty(&call.arguments).unwrap(),
theme,
&call.name,
);
}
Err(e) => print(&e.to_string(), theme),
}
}
fn response(&self, tool_response: &ToolResponse, theme: &str) {
match &tool_response.tool_result {
Ok(output) => {
let output_value = serde_json::to_string_pretty(output).unwrap();
// For pure text responses, strip the quotes and replace escaped newlines. Eg. bash responses
let unquoted = output_value.trim_matches('"');
let formatted = unquoted.replace("\\n", "\n");
let language = if formatted.starts_with("{") {
"JSON"
} else {
"Markdown"
};
print_tool_response(&formatted, theme, language);
}
Err(e) => print(&e.to_string(), theme),
}
}
}
fn print_tool_request(content: &str, theme: &str, tool_name: &str) {
bat::PrettyPrinter::new()
.input(
bat::Input::from_bytes(content.as_bytes()).name(format!("Tool Request: {}", tool_name)),
)
.theme(theme)
.language("JSON")
.grid(true)
.header(true)
.wrapping_mode(WrappingMode::Character)
.print()
.unwrap();
}
fn print_tool_response(content: &str, theme: &str, language: &str) {
bat::PrettyPrinter::new()
.input(bat::Input::from_bytes(content.as_bytes()).name("Tool Response:"))
.theme(theme)
.language(language)
.grid(true)
.header(true)
.wrapping_mode(WrappingMode::Character)
.print()
.unwrap();
}
fn print(content: &str, theme: &str) {
bat::PrettyPrinter::new()
.input(bat::Input::from_bytes(content.as_bytes()))
.theme(theme)
.language("Markdown")
.wrapping_mode(WrappingMode::Character)
.print()
.unwrap();
}
fn print_newline() {
println!();
}
impl Prompt for CliclackPrompt {
fn render(&mut self, message: Box<Message>) {
let theme = match self.theme {
Theme::Light => "GitHub",
Theme::Dark => "zenburn",
};
let mut last_tool_name: &str = "default";
for message_content in &message.content {
match message_content {
MessageContent::Text(text) => print(&text.text, theme),
MessageContent::ToolRequest(tool_request) => match &tool_request.tool_call {
Ok(call) => {
last_tool_name = &call.name;
self.renderers
.get(&call.name)
.or_else(|| self.renderers.get("default"))
.unwrap()
.request(tool_request, theme);
}
Err(_) => self
.renderers
.get("default")
.unwrap()
.request(tool_request, theme),
},
MessageContent::ToolResponse(tool_response) => self
.renderers
.get(last_tool_name)
.or_else(|| self.renderers.get("default"))
.unwrap()
.response(tool_response, theme),
MessageContent::Image(image) => {
println!("Image: [data: {}, type: {}]", image.data, image.mime_type);
}
}
}
print_newline();
io::stdout().flush().expect("Failed to flush stdout");
render(message, &self.theme, self.renderers.clone());
}
fn show_busy(&mut self) {
+268
View File
@@ -0,0 +1,268 @@
use std::collections::HashMap;
use std::io::{self, Write};
use bat::WrappingMode;
use console::style;
use goose::models::message::{Message, MessageContent, ToolRequest, ToolResponse};
use goose::models::role::Role;
use goose::models::{content::Content, tool::ToolCall};
use serde_json::Value;
use super::Theme;
const MAX_STRING_LENGTH: usize = 40;
const INDENT: &str = " ";
/// Implement the ToolRenderer trait for each tool that you want to render in the prompt.
pub trait ToolRenderer: ToolRendererClone {
fn tool_name(&self) -> String;
fn request(&self, tool_request: &ToolRequest, theme: &str);
fn response(&self, tool_response: &ToolResponse, theme: &str);
}
// Helper trait for cloning boxed ToolRenderer objects
pub trait ToolRendererClone {
fn clone_box(&self) -> Box<dyn ToolRenderer>;
}
// Implement the helper trait for any type that implements ToolRenderer and Clone
impl<T> ToolRendererClone for T
where
T: 'static + ToolRenderer + Clone,
{
fn clone_box(&self) -> Box<dyn ToolRenderer> {
Box::new(self.clone())
}
}
// Make Box<dyn ToolRenderer> clonable
impl Clone for Box<dyn ToolRenderer> {
fn clone(&self) -> Box<dyn ToolRenderer> {
self.clone_box()
}
}
#[derive(Clone)]
pub struct DefaultRenderer;
impl ToolRenderer for DefaultRenderer {
fn tool_name(&self) -> String {
"default".to_string()
}
fn request(&self, tool_request: &ToolRequest, theme: &str) {
match &tool_request.tool_call {
Ok(call) => {
default_print_request_header(call);
// Format and print the parameters
print_params(&call.arguments, 0);
print_newline();
}
Err(e) => print_markdown(&e.to_string(), theme),
}
}
fn response(&self, tool_response: &ToolResponse, theme: &str) {
default_response_renderer(tool_response, theme);
}
}
#[derive(Clone)]
pub struct BashDeveloperSystemRenderer;
impl ToolRenderer for BashDeveloperSystemRenderer {
fn tool_name(&self) -> String {
"DeveloperSystem__bash".to_string()
}
fn request(&self, tool_request: &ToolRequest, theme: &str) {
match &tool_request.tool_call {
Ok(call) => {
default_print_request_header(call);
match call.arguments.get("command") {
Some(Value::String(s)) => {
println!("{}: {}", style("command").dim(), style(s).green());
}
_ => print_params(&call.arguments, 0),
}
print_newline();
}
Err(e) => print_markdown(&e.to_string(), theme),
}
}
fn response(&self, tool_response: &ToolResponse, theme: &str) {
default_response_renderer(tool_response, theme);
}
}
pub fn render(
message: Box<Message>,
theme: &Theme,
renderers: HashMap<String, Box<dyn ToolRenderer>>,
) {
let theme = match theme {
Theme::Light => "GitHub",
Theme::Dark => "zenburn",
};
let mut last_tool_name: &str = "default";
for message_content in &message.content {
match message_content {
MessageContent::Text(text) => print_markdown(&text.text, theme),
MessageContent::ToolRequest(tool_request) => match &tool_request.tool_call {
Ok(call) => {
last_tool_name = &call.name;
renderers
.get(&call.name)
.or_else(|| renderers.get("default"))
.unwrap()
.request(tool_request, theme);
}
Err(_) => renderers
.get("default")
.unwrap()
.request(tool_request, theme),
},
MessageContent::ToolResponse(tool_response) => renderers
.get(last_tool_name)
.or_else(|| renderers.get("default"))
.unwrap()
.response(tool_response, theme),
MessageContent::Image(image) => {
println!("Image: [data: {}, type: {}]", image.data, image.mime_type);
}
}
}
print_newline();
io::stdout().flush().expect("Failed to flush stdout");
}
pub fn default_response_renderer(tool_response: &ToolResponse, theme: &str) {
match &tool_response.tool_result {
Ok(contents) => {
for content in contents {
if content
.audience()
.is_some_and(|audience| !audience.contains(&Role::User))
{
continue;
}
let min_priority = std::env::var("GOOSE_CLI_MIN_PRIORITY")
.ok()
.and_then(|val| val.parse::<f32>().ok())
.unwrap_or(0.0);
if content
.priority()
.is_some_and(|priority| priority <= min_priority)
{
continue;
}
if let Content::Text(text) = content {
print_markdown(&text.text, theme);
}
}
}
Err(e) => print_markdown(&e.to_string(), theme),
}
}
pub fn default_print_request_header(call: &ToolCall) {
// Print the tool name with an emoji
let parts: Vec<_> = call.name.split("__").collect();
let tool_header = format!(
"─── {} | {} ──────────────────────────",
style(parts.get(1).unwrap_or(&"unknown")),
style(parts.first().unwrap_or(&"unknown")).magenta().dim(),
);
print_newline();
println!("{}", tool_header);
}
pub fn print_markdown(content: &str, theme: &str) {
bat::PrettyPrinter::new()
.input(bat::Input::from_bytes(content.as_bytes()))
.theme(theme)
.language("Markdown")
.wrapping_mode(WrappingMode::Character)
.print()
.unwrap();
}
/// Format and print parameters recursively with proper indentation and colors
pub fn print_params(value: &Value, depth: usize) {
let indent = INDENT.repeat(depth);
match value {
Value::Object(map) => {
for (key, val) in map {
match val {
Value::Object(_) => {
println!("{}{}:", indent, style(key).dim());
print_params(val, depth + 1);
}
Value::Array(arr) => {
println!("{}{}:", indent, style(key).dim());
for item in arr.iter() {
println!("{}{}- ", indent, INDENT);
print_params(item, depth + 2);
}
}
Value::String(s) => {
if s.len() > MAX_STRING_LENGTH {
println!("{}{}: {}", indent, style(key).dim(), style("...").dim());
} else {
println!("{}{}: {}", indent, style(key).dim(), style(s).green());
}
}
Value::Number(n) => {
println!("{}{}: {}", indent, style(key).dim(), style(n).blue());
}
Value::Bool(b) => {
println!("{}{}: {}", indent, style(key).dim(), style(b).blue());
}
Value::Null => {
println!("{}{}: {}", indent, style(key).dim(), style("null").dim());
}
}
}
}
Value::Array(arr) => {
for (i, item) in arr.iter().enumerate() {
println!("{}{}.", indent, i + 1);
print_params(item, depth + 1);
}
}
Value::String(s) => {
if s.len() > MAX_STRING_LENGTH {
println!(
"{}{}",
indent,
style(format!("[REDACTED: {} chars]", s.len())).yellow()
);
} else {
println!("{}{}", indent, style(s).green());
}
}
Value::Number(n) => {
println!("{}{}", indent, style(n).yellow());
}
Value::Bool(b) => {
println!("{}{}", indent, style(b).yellow());
}
Value::Null => {
println!("{}{}", indent, style("null").dim());
}
}
}
pub fn print_newline() {
println!();
}
+18 -255
View File
@@ -1,22 +1,16 @@
use std::{
collections::HashMap,
io::{self, Write},
use std::collections::HashMap;
use super::{
renderer::{render, BashDeveloperSystemRenderer, DefaultRenderer, ToolRenderer},
thinking::get_random_thinking_message,
Input, InputType, Prompt, Theme,
};
use anyhow::Result;
use bat::WrappingMode;
use cliclack::spinner;
use console::style;
use goose::models::message::{Message, MessageContent, ToolRequest, ToolResponse};
use goose::models::role::Role;
use goose::models::{content::Content, tool::ToolCall};
use serde_json::Value;
use super::{thinking::get_random_thinking_message, Input, InputType, Prompt, Theme};
use goose::models::message::Message;
const PROMPT: &str = "\x1b[1m\x1b[38;5;30m( O)> \x1b[0m";
const MAX_STRING_LENGTH: usize = 40;
const INDENT: &str = " ";
pub struct RustylinePrompt {
spinner: cliclack::ProgressBar,
@@ -26,21 +20,6 @@ pub struct RustylinePrompt {
impl RustylinePrompt {
pub fn new() -> Self {
// // Load highlighting assets
// let assets = HighlightingAssets::from_binary();
// // Fetch and list all available themes
// let themes = assets.themes();
// for theme_name in themes {
// println!("{}", theme_name);
// }
// // List all available syntaxes (languages)
// let syntax_set = assets.get_syntaxes().unwrap();
// for syntax in syntax_set {
// println!("{}", syntax.name);
// }
let mut renderers: HashMap<String, Box<dyn ToolRenderer>> = HashMap::new();
let default_renderer = DefaultRenderer;
renderers.insert(default_renderer.tool_name(), Box::new(default_renderer));
@@ -52,240 +31,24 @@ impl RustylinePrompt {
RustylinePrompt {
spinner: spinner(),
theme: Theme::Dark,
theme: std::env::var("GOOSE_CLI_THEME")
.ok()
.map(|val| {
if val.eq_ignore_ascii_case("light") {
Theme::Light
} else {
Theme::Dark
}
})
.unwrap_or(Theme::Dark),
renderers,
}
}
}
/// Implement the ToolRenderer trait for each tool that you want to render in the prompt.
trait ToolRenderer {
fn tool_name(&self) -> String;
fn request(&self, tool_request: &ToolRequest, theme: &str);
fn response(&self, tool_response: &ToolResponse, theme: &str);
}
struct DefaultRenderer;
impl ToolRenderer for DefaultRenderer {
fn tool_name(&self) -> String {
"default".to_string()
}
fn request(&self, tool_request: &ToolRequest, theme: &str) {
match &tool_request.tool_call {
Ok(call) => {
default_print_request_header(call);
// Format and print the parameters
print_params(&call.arguments, 0);
print_newline();
}
Err(e) => print(&e.to_string(), theme),
}
}
fn response(&self, tool_response: &ToolResponse, theme: &str) {
default_response_renderer(tool_response, theme);
}
}
fn default_response_renderer(tool_response: &ToolResponse, theme: &str) {
match &tool_response.tool_result {
Ok(contents) => {
for content in contents {
if content
.audience()
.is_some_and(|audience| !audience.contains(&Role::User))
{
continue;
}
if content.priority().is_some_and(|priority| priority == 0.0) {
continue;
}
if let Content::Text(text) = content {
print_markdown(&text.text, theme);
}
}
}
Err(e) => print(&e.to_string(), theme),
}
}
fn default_print_request_header(call: &ToolCall) {
// Print the tool name with an emoji
let parts: Vec<_> = call.name.split("__").collect();
let tool_header = format!(
"─── {} | {} ──────────────────────────",
style(parts.get(1).unwrap_or(&"unknown")),
style(parts.first().unwrap_or(&"unknown")).magenta().dim(),
);
print_newline();
println!("{}", tool_header);
}
struct BashDeveloperSystemRenderer;
impl ToolRenderer for BashDeveloperSystemRenderer {
fn tool_name(&self) -> String {
"DeveloperSystem__bash".to_string()
}
fn request(&self, tool_request: &ToolRequest, theme: &str) {
match &tool_request.tool_call {
Ok(call) => {
default_print_request_header(call);
match call.arguments.get("command") {
Some(Value::String(s)) => {
println!("{}: {}", style("command").dim(), style(s).green());
}
_ => print_params(&call.arguments, 0),
}
print_newline();
}
Err(e) => print(&e.to_string(), theme),
}
}
fn response(&self, tool_response: &ToolResponse, theme: &str) {
default_response_renderer(tool_response, theme);
}
}
fn print_markdown(content: &str, theme: &str) {
bat::PrettyPrinter::new()
.input(bat::Input::from_bytes(content.as_bytes()))
.theme(theme)
.language("Markdown")
.wrapping_mode(WrappingMode::Character)
.print()
.unwrap();
}
fn print(content: &str, theme: &str) {
bat::PrettyPrinter::new()
.input(bat::Input::from_bytes(content.as_bytes()))
.theme(theme)
.language("Markdown")
.wrapping_mode(WrappingMode::Character)
.print()
.unwrap();
}
/// Format and print parameters recursively with proper indentation and colors
fn print_params(value: &Value, depth: usize) {
let indent = INDENT.repeat(depth);
match value {
Value::Object(map) => {
for (key, val) in map {
match val {
Value::Object(_) => {
println!("{}{}:", indent, style(key).dim());
print_params(val, depth + 1);
}
Value::Array(arr) => {
println!("{}{}:", indent, style(key).dim());
for item in arr.iter() {
println!("{}{}- ", indent, INDENT);
print_params(item, depth + 2);
}
}
Value::String(s) => {
if s.len() > MAX_STRING_LENGTH {
println!("{}{}: {}", indent, style(key).dim(), style("...").dim());
} else {
println!("{}{}: {}", indent, style(key).dim(), style(s).green());
}
}
Value::Number(n) => {
println!("{}{}: {}", indent, style(key).dim(), style(n).blue());
}
Value::Bool(b) => {
println!("{}{}: {}", indent, style(key).dim(), style(b).blue());
}
Value::Null => {
println!("{}{}: {}", indent, style(key).dim(), style("null").dim());
}
}
}
}
Value::Array(arr) => {
for (i, item) in arr.iter().enumerate() {
println!("{}{}.", indent, i + 1);
print_params(item, depth + 1);
}
}
Value::String(s) => {
if s.len() > MAX_STRING_LENGTH {
println!(
"{}{}",
indent,
style(format!("[REDACTED: {} chars]", s.len())).yellow()
);
} else {
println!("{}{}", indent, style(s).green());
}
}
Value::Number(n) => {
println!("{}{}", indent, style(n).yellow());
}
Value::Bool(b) => {
println!("{}{}", indent, style(b).yellow());
}
Value::Null => {
println!("{}{}", indent, style("null").dim());
}
}
}
fn print_newline() {
println!();
}
impl Prompt for RustylinePrompt {
fn render(&mut self, message: Box<Message>) {
let theme = match self.theme {
Theme::Light => "GitHub",
Theme::Dark => "zenburn",
};
let mut last_tool_name: &str = "default";
for message_content in &message.content {
match message_content {
MessageContent::Text(text) => print_markdown(&text.text, theme),
MessageContent::ToolRequest(tool_request) => match &tool_request.tool_call {
Ok(call) => {
last_tool_name = &call.name;
self.renderers
.get(&call.name)
.or_else(|| self.renderers.get("default"))
.unwrap()
.request(tool_request, theme);
}
Err(_) => self
.renderers
.get("default")
.unwrap()
.request(tool_request, theme),
},
MessageContent::ToolResponse(tool_response) => self
.renderers
.get(last_tool_name)
.or_else(|| self.renderers.get("default"))
.unwrap()
.response(tool_response, theme),
MessageContent::Image(image) => {
println!("Image: [data: {}, type: {}]", image.data, image.mime_type);
}
}
}
print_newline();
io::stdout().flush().expect("Failed to flush stdout");
render(message, &self.theme, self.renderers.clone());
}
fn show_busy(&mut self) {