mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 06:34:26 +01:00
feat: additional sub recipes via command line (#3163)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3512,6 +3512,7 @@ dependencies = [
|
|||||||
"clap 4.5.31",
|
"clap 4.5.31",
|
||||||
"cliclack",
|
"cliclack",
|
||||||
"console",
|
"console",
|
||||||
|
"dirs 5.0.1",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures",
|
"futures",
|
||||||
"goose",
|
"goose",
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ regex = "1.11.1"
|
|||||||
minijinja = { version = "2.10.2", features = ["loader"] }
|
minijinja = { version = "2.10.2", features = ["loader"] }
|
||||||
nix = { version = "0.30.1", features = ["process", "signal"] }
|
nix = { version = "0.30.1", features = ["process", "signal"] }
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
|
dirs = "5.0"
|
||||||
# Web server dependencies
|
# Web server dependencies
|
||||||
axum = { version = "0.8.1", features = ["ws", "macros"] }
|
axum = { version = "0.8.1", features = ["ws", "macros"] }
|
||||||
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ use crate::commands::schedule::{
|
|||||||
};
|
};
|
||||||
use crate::commands::session::{handle_session_list, handle_session_remove};
|
use crate::commands::session::{handle_session_list, handle_session_remove};
|
||||||
use crate::logging::setup_logging;
|
use crate::logging::setup_logging;
|
||||||
use crate::recipes::recipe::{
|
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
||||||
explain_recipe_with_parameters, load_recipe_as_template, load_recipe_content_as_template,
|
use crate::recipes::recipe::{explain_recipe_with_parameters, load_recipe_content_as_template};
|
||||||
};
|
|
||||||
use crate::session;
|
use crate::session;
|
||||||
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
|
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
|
||||||
use goose_bench::bench_config::BenchRunConfig;
|
use goose_bench::bench_config::BenchRunConfig;
|
||||||
@@ -519,6 +518,16 @@ enum Command {
|
|||||||
hide = true
|
hide = true
|
||||||
)]
|
)]
|
||||||
scheduled_job_id: Option<String>,
|
scheduled_job_id: Option<String>,
|
||||||
|
|
||||||
|
/// Additional sub-recipe file paths
|
||||||
|
#[arg(
|
||||||
|
long = "sub-recipe",
|
||||||
|
value_name = "FILE",
|
||||||
|
help = "Path to a sub-recipe YAML file (can be specified multiple times)",
|
||||||
|
long_help = "Specify paths to sub-recipe YAML files that contain additional recipe configuration or instructions to be used alongside the main recipe. Can be specified multiple times to include multiple sub-recipes.",
|
||||||
|
action = clap::ArgAction::Append
|
||||||
|
)]
|
||||||
|
additional_sub_recipes: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Recipe utilities for validation and deeplinking
|
/// Recipe utilities for validation and deeplinking
|
||||||
@@ -593,10 +602,10 @@ enum CliProviderVariant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InputConfig {
|
pub struct InputConfig {
|
||||||
contents: Option<String>,
|
pub contents: Option<String>,
|
||||||
extensions_override: Option<Vec<ExtensionConfig>>,
|
pub extensions_override: Option<Vec<ExtensionConfig>>,
|
||||||
additional_system_prompt: Option<String>,
|
pub additional_system_prompt: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cli() -> Result<()> {
|
pub async fn cli() -> Result<()> {
|
||||||
@@ -725,15 +734,14 @@ pub async fn cli() -> Result<()> {
|
|||||||
render_recipe,
|
render_recipe,
|
||||||
scheduled_job_id,
|
scheduled_job_id,
|
||||||
quiet,
|
quiet,
|
||||||
|
additional_sub_recipes,
|
||||||
}) => {
|
}) => {
|
||||||
let (input_config, session_settings, sub_recipes) = match (
|
let (input_config, session_settings, sub_recipes) = match (
|
||||||
instructions,
|
instructions,
|
||||||
input_text,
|
input_text,
|
||||||
recipe,
|
recipe,
|
||||||
explain,
|
|
||||||
render_recipe,
|
|
||||||
) {
|
) {
|
||||||
(Some(file), _, _, _, _) if file == "-" => {
|
(Some(file), _, _) if file == "-" => {
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
std::io::stdin()
|
std::io::stdin()
|
||||||
.read_to_string(&mut input)
|
.read_to_string(&mut input)
|
||||||
@@ -749,7 +757,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
(Some(file), _, _, _, _) => {
|
(Some(file), _, _) => {
|
||||||
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
|
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Instruction file not found — did you mean to use goose run --text?\n{}",
|
"Instruction file not found — did you mean to use goose run --text?\n{}",
|
||||||
@@ -767,7 +775,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
(_, Some(text), _, _, _) => (
|
(_, Some(text), _) => (
|
||||||
InputConfig {
|
InputConfig {
|
||||||
contents: Some(text),
|
contents: Some(text),
|
||||||
extensions_override: None,
|
extensions_override: None,
|
||||||
@@ -776,7 +784,7 @@ pub async fn cli() -> Result<()> {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
(_, _, Some(recipe_name), explain, render_recipe) => {
|
(_, _, Some(recipe_name)) => {
|
||||||
if explain {
|
if explain {
|
||||||
explain_recipe_with_parameters(&recipe_name, params)?;
|
explain_recipe_with_parameters(&recipe_name, params)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -790,26 +798,9 @@ pub async fn cli() -> Result<()> {
|
|||||||
println!("{}", recipe);
|
println!("{}", recipe);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let recipe =
|
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes)?
|
||||||
load_recipe_as_template(&recipe_name, params).unwrap_or_else(|err| {
|
|
||||||
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
|
||||||
std::process::exit(1);
|
|
||||||
});
|
|
||||||
(
|
|
||||||
InputConfig {
|
|
||||||
contents: recipe.prompt,
|
|
||||||
extensions_override: recipe.extensions,
|
|
||||||
additional_system_prompt: recipe.instructions,
|
|
||||||
},
|
|
||||||
recipe.settings.map(|s| SessionSettings {
|
|
||||||
goose_provider: s.goose_provider,
|
|
||||||
goose_model: s.goose_model,
|
|
||||||
temperature: s.temperature,
|
|
||||||
}),
|
|
||||||
recipe.sub_recipes,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
(None, None, None, _, _) => {
|
(None, None, None) => {
|
||||||
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
161
crates/goose-cli/src/recipes/extract_from_cli.rs
Normal file
161
crates/goose-cli/src/recipes/extract_from_cli.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use goose::recipe::SubRecipe;
|
||||||
|
|
||||||
|
use crate::{cli::InputConfig, recipes::recipe::load_recipe_as_template, session::SessionSettings};
|
||||||
|
|
||||||
|
pub fn extract_recipe_info_from_cli(
|
||||||
|
recipe_name: String,
|
||||||
|
params: Vec<(String, String)>,
|
||||||
|
additional_sub_recipes: Vec<String>,
|
||||||
|
) -> Result<(InputConfig, Option<SessionSettings>, Option<Vec<SubRecipe>>)> {
|
||||||
|
let recipe = load_recipe_as_template(&recipe_name, params).unwrap_or_else(|err| {
|
||||||
|
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default();
|
||||||
|
if !additional_sub_recipes.is_empty() {
|
||||||
|
additional_sub_recipes.iter().for_each(|sub_recipe_path| {
|
||||||
|
let path = convert_path(sub_recipe_path);
|
||||||
|
let name = path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("unknown")
|
||||||
|
.to_string();
|
||||||
|
let additional_sub_recipe: SubRecipe = SubRecipe {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
name,
|
||||||
|
values: None,
|
||||||
|
};
|
||||||
|
all_sub_recipes.push(additional_sub_recipe);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
InputConfig {
|
||||||
|
contents: recipe.prompt,
|
||||||
|
extensions_override: recipe.extensions,
|
||||||
|
additional_system_prompt: recipe.instructions,
|
||||||
|
},
|
||||||
|
recipe.settings.map(|s| SessionSettings {
|
||||||
|
goose_provider: s.goose_provider,
|
||||||
|
goose_model: s.goose_model,
|
||||||
|
temperature: s.temperature,
|
||||||
|
}),
|
||||||
|
Some(all_sub_recipes),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_path(path: &str) -> PathBuf {
|
||||||
|
if let Some(stripped) = path.strip_prefix("~/") {
|
||||||
|
if let Some(home_dir) = dirs::home_dir() {
|
||||||
|
return home_dir.join(stripped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathBuf::from(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_recipe_info_from_cli_basic() {
|
||||||
|
let (_temp_dir, recipe_path) = create_recipe();
|
||||||
|
let params = vec![("name".to_string(), "my_value".to_string())];
|
||||||
|
let recipe_name = recipe_path.to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
let (input_config, settings, sub_recipes) =
|
||||||
|
extract_recipe_info_from_cli(recipe_name, params, Vec::new()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
input_config.additional_system_prompt,
|
||||||
|
Some("test_instructions my_value".to_string())
|
||||||
|
);
|
||||||
|
assert!(input_config.extensions_override.is_none());
|
||||||
|
|
||||||
|
assert!(settings.is_some());
|
||||||
|
let settings = settings.unwrap();
|
||||||
|
assert_eq!(settings.goose_provider, Some("test_provider".to_string()));
|
||||||
|
assert_eq!(settings.goose_model, Some("test_model".to_string()));
|
||||||
|
assert_eq!(settings.temperature, Some(0.7));
|
||||||
|
|
||||||
|
assert!(sub_recipes.is_some());
|
||||||
|
let sub_recipes = sub_recipes.unwrap();
|
||||||
|
assert!(sub_recipes.len() == 1);
|
||||||
|
assert_eq!(sub_recipes[0].path, "existing_sub_recipe.yaml".to_string());
|
||||||
|
assert_eq!(sub_recipes[0].name, "existing_sub_recipe".to_string());
|
||||||
|
assert!(sub_recipes[0].values.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_recipe_info_from_cli_with_additional_sub_recipes() {
|
||||||
|
let (_temp_dir, recipe_path) = create_recipe();
|
||||||
|
let params = vec![("name".to_string(), "my_value".to_string())];
|
||||||
|
let recipe_name = recipe_path.to_str().unwrap().to_string();
|
||||||
|
let additional_sub_recipes = vec![
|
||||||
|
"path/to/sub_recipe1.yaml".to_string(),
|
||||||
|
"another/sub_recipe2.yaml".to_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let (input_config, settings, sub_recipes) =
|
||||||
|
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
input_config.additional_system_prompt,
|
||||||
|
Some("test_instructions my_value".to_string())
|
||||||
|
);
|
||||||
|
assert!(input_config.extensions_override.is_none());
|
||||||
|
|
||||||
|
assert!(settings.is_some());
|
||||||
|
let settings = settings.unwrap();
|
||||||
|
assert_eq!(settings.goose_provider, Some("test_provider".to_string()));
|
||||||
|
assert_eq!(settings.goose_model, Some("test_model".to_string()));
|
||||||
|
assert_eq!(settings.temperature, Some(0.7));
|
||||||
|
|
||||||
|
assert!(sub_recipes.is_some());
|
||||||
|
let sub_recipes = sub_recipes.unwrap();
|
||||||
|
assert!(sub_recipes.len() == 3);
|
||||||
|
assert_eq!(sub_recipes[0].path, "existing_sub_recipe.yaml".to_string());
|
||||||
|
assert_eq!(sub_recipes[0].name, "existing_sub_recipe".to_string());
|
||||||
|
assert!(sub_recipes[0].values.is_none());
|
||||||
|
assert_eq!(sub_recipes[1].path, "path/to/sub_recipe1.yaml".to_string());
|
||||||
|
assert_eq!(sub_recipes[1].name, "sub_recipe1".to_string());
|
||||||
|
assert!(sub_recipes[1].values.is_none());
|
||||||
|
assert_eq!(sub_recipes[2].path, "another/sub_recipe2.yaml".to_string());
|
||||||
|
assert_eq!(sub_recipes[2].name, "sub_recipe2".to_string());
|
||||||
|
assert!(sub_recipes[2].values.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_recipe() -> (TempDir, PathBuf) {
|
||||||
|
let test_recipe_content = r#"
|
||||||
|
title: test_recipe
|
||||||
|
description: A test recipe
|
||||||
|
instructions: test_instructions {{name}}
|
||||||
|
prompt: test_prompt
|
||||||
|
parameters:
|
||||||
|
- key: name
|
||||||
|
description: name
|
||||||
|
input_type: string
|
||||||
|
requirement: required
|
||||||
|
settings:
|
||||||
|
goose_provider: test_provider
|
||||||
|
goose_model: test_model
|
||||||
|
temperature: 0.7
|
||||||
|
sub_recipes:
|
||||||
|
- path: existing_sub_recipe.yaml
|
||||||
|
name: existing_sub_recipe
|
||||||
|
"#;
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let recipe_path: std::path::PathBuf = temp_dir.path().join("test_recipe.yaml");
|
||||||
|
|
||||||
|
std::fs::write(&recipe_path, test_recipe_content).unwrap();
|
||||||
|
(temp_dir, recipe_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod extract_from_cli;
|
||||||
pub mod github_recipe;
|
pub mod github_recipe;
|
||||||
pub mod print_recipe;
|
pub mod print_recipe;
|
||||||
pub mod recipe;
|
pub mod recipe;
|
||||||
|
|||||||
Reference in New Issue
Block a user