mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-18 14:44:21 +01:00
feat: additional sub recipes via command line (#3163)
This commit is contained in:
@@ -59,6 +59,7 @@ regex = "1.11.1"
|
||||
minijinja = { version = "2.10.2", features = ["loader"] }
|
||||
nix = { version = "0.30.1", features = ["process", "signal"] }
|
||||
tar = "0.4"
|
||||
dirs = "5.0"
|
||||
# Web server dependencies
|
||||
axum = { version = "0.8.1", features = ["ws", "macros"] }
|
||||
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::logging::setup_logging;
|
||||
use crate::recipes::recipe::{
|
||||
explain_recipe_with_parameters, load_recipe_as_template, load_recipe_content_as_template,
|
||||
};
|
||||
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
||||
use crate::recipes::recipe::{explain_recipe_with_parameters, load_recipe_content_as_template};
|
||||
use crate::session;
|
||||
use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
|
||||
use goose_bench::bench_config::BenchRunConfig;
|
||||
@@ -519,6 +518,16 @@ enum Command {
|
||||
hide = true
|
||||
)]
|
||||
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
|
||||
@@ -593,10 +602,10 @@ enum CliProviderVariant {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InputConfig {
|
||||
contents: Option<String>,
|
||||
extensions_override: Option<Vec<ExtensionConfig>>,
|
||||
additional_system_prompt: Option<String>,
|
||||
pub struct InputConfig {
|
||||
pub contents: Option<String>,
|
||||
pub extensions_override: Option<Vec<ExtensionConfig>>,
|
||||
pub additional_system_prompt: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn cli() -> Result<()> {
|
||||
@@ -725,15 +734,14 @@ pub async fn cli() -> Result<()> {
|
||||
render_recipe,
|
||||
scheduled_job_id,
|
||||
quiet,
|
||||
additional_sub_recipes,
|
||||
}) => {
|
||||
let (input_config, session_settings, sub_recipes) = match (
|
||||
instructions,
|
||||
input_text,
|
||||
recipe,
|
||||
explain,
|
||||
render_recipe,
|
||||
) {
|
||||
(Some(file), _, _, _, _) if file == "-" => {
|
||||
(Some(file), _, _) if file == "-" => {
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_to_string(&mut input)
|
||||
@@ -749,7 +757,7 @@ pub async fn cli() -> Result<()> {
|
||||
None,
|
||||
)
|
||||
}
|
||||
(Some(file), _, _, _, _) => {
|
||||
(Some(file), _, _) => {
|
||||
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
|
||||
eprintln!(
|
||||
"Instruction file not found — did you mean to use goose run --text?\n{}",
|
||||
@@ -767,7 +775,7 @@ pub async fn cli() -> Result<()> {
|
||||
None,
|
||||
)
|
||||
}
|
||||
(_, Some(text), _, _, _) => (
|
||||
(_, Some(text), _) => (
|
||||
InputConfig {
|
||||
contents: Some(text),
|
||||
extensions_override: None,
|
||||
@@ -776,7 +784,7 @@ pub async fn cli() -> Result<()> {
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(_, _, Some(recipe_name), explain, render_recipe) => {
|
||||
(_, _, Some(recipe_name)) => {
|
||||
if explain {
|
||||
explain_recipe_with_parameters(&recipe_name, params)?;
|
||||
return Ok(());
|
||||
@@ -790,26 +798,9 @@ pub async fn cli() -> Result<()> {
|
||||
println!("{}", recipe);
|
||||
return Ok(());
|
||||
}
|
||||
let recipe =
|
||||
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,
|
||||
)
|
||||
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes)?
|
||||
}
|
||||
(None, None, None, _, _) => {
|
||||
(None, None, None) => {
|
||||
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
||||
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 print_recipe;
|
||||
pub mod recipe;
|
||||
|
||||
Reference in New Issue
Block a user