Files
goose/crates/goose-cli/src/recipes/extract_from_cli.rs
2025-07-22 10:49:21 +10:00

247 lines
8.7 KiB
Rust

use std::path::PathBuf;
use anyhow::{anyhow, Result};
use goose::recipe::SubRecipe;
use crate::recipes::print_recipe::print_recipe_info;
use crate::recipes::recipe::load_recipe;
use crate::recipes::search_recipe::retrieve_recipe_file;
use crate::{
cli::{InputConfig, RecipeInfo},
session::SessionSettings,
};
pub fn extract_recipe_info_from_cli(
recipe_name: String,
params: Vec<(String, String)>,
additional_sub_recipes: Vec<String>,
) -> Result<(InputConfig, RecipeInfo)> {
let recipe = load_recipe(&recipe_name, params.clone()).unwrap_or_else(|err| {
eprintln!("{}: {}", console::style("Error").red().bold(), err);
std::process::exit(1);
});
print_recipe_info(&recipe, params);
let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default();
if !additional_sub_recipes.is_empty() {
for sub_recipe_name in additional_sub_recipes {
match retrieve_recipe_file(&sub_recipe_name) {
Ok(recipe_file) => {
let name = extract_recipe_name(&sub_recipe_name);
let recipe_file_path = recipe_file.file_path;
let additional_sub_recipe = SubRecipe {
path: recipe_file_path.to_string_lossy().to_string(),
name,
values: None,
sequential_when_repeated: true,
};
all_sub_recipes.push(additional_sub_recipe);
}
Err(e) => {
return Err(anyhow!(
"Could not retrieve sub-recipe '{}': {}",
sub_recipe_name,
e
));
}
}
}
}
let input_config = InputConfig {
contents: recipe.prompt.filter(|s| !s.trim().is_empty()),
extensions_override: recipe.extensions,
additional_system_prompt: recipe.instructions,
};
let recipe_info = RecipeInfo {
session_settings: recipe.settings.map(|s| SessionSettings {
goose_provider: s.goose_provider,
goose_model: s.goose_model,
temperature: s.temperature,
}),
sub_recipes: Some(all_sub_recipes),
final_output_response: recipe.response,
retry_config: recipe.retry,
};
Ok((input_config, recipe_info))
}
fn extract_recipe_name(recipe_identifier: &str) -> String {
// If it's a path (contains / or \), extract the file stem
if recipe_identifier.contains('/') || recipe_identifier.contains('\\') {
PathBuf::from(recipe_identifier)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string()
} else {
// If it's just a name (like "weekly-updates"), use it directly
recipe_identifier.to_string()
}
}
#[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, recipe_info) =
extract_recipe_info_from_cli(recipe_name, params, Vec::new()).unwrap();
let settings = recipe_info.session_settings;
let sub_recipes = recipe_info.sub_recipes;
let response = recipe_info.final_output_response;
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());
assert!(response.is_some());
let response = response.unwrap();
assert_eq!(
response.json_schema,
Some(serde_json::json!({
"type": "object",
"properties": {
"result": {"type": "string"}
}
}))
);
}
#[test]
fn test_extract_recipe_info_from_cli_with_additional_sub_recipes() {
let (_temp_dir, recipe_path) = create_recipe();
// Create actual sub-recipe files in the temp directory
std::fs::create_dir_all(_temp_dir.path().join("path/to")).unwrap();
std::fs::create_dir_all(_temp_dir.path().join("another")).unwrap();
let sub_recipe1_path = _temp_dir.path().join("path/to/sub_recipe1.yaml");
let sub_recipe2_path = _temp_dir.path().join("another/sub_recipe2.yaml");
std::fs::write(&sub_recipe1_path, "title: Sub Recipe 1").unwrap();
std::fs::write(&sub_recipe2_path, "title: Sub Recipe 2").unwrap();
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![
sub_recipe1_path.to_string_lossy().to_string(),
sub_recipe2_path.to_string_lossy().to_string(),
];
let (input_config, recipe_info) =
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes).unwrap();
let settings = recipe_info.session_settings;
let sub_recipes = recipe_info.sub_recipes;
let response = recipe_info.final_output_response;
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,
sub_recipe1_path
.canonicalize()
.unwrap()
.to_string_lossy()
.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,
sub_recipe2_path
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string()
);
assert_eq!(sub_recipes[2].name, "sub_recipe2".to_string());
assert!(sub_recipes[2].values.is_none());
assert!(response.is_some());
let response = response.unwrap();
assert_eq!(
response.json_schema,
Some(serde_json::json!({
"type": "object",
"properties": {
"result": {"type": "string"}
}
}))
);
}
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
response:
json_schema:
type: object
properties:
result:
type: string
"#;
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();
let canonical_recipe_path = recipe_path.canonicalize().unwrap();
(temp_dir, canonical_recipe_path)
}
}