mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Lifei/pass param to goose recipe (#2395)
This commit is contained in:
Generated
+1
@@ -2484,6 +2484,7 @@ dependencies = [
|
||||
"mcp-client",
|
||||
"mcp-core",
|
||||
"mcp-server",
|
||||
"minijinja",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"regex",
|
||||
|
||||
@@ -52,6 +52,7 @@ shlex = "1.3.0"
|
||||
async-trait = "0.1.86"
|
||||
base64 = "0.22.1"
|
||||
regex = "1.11.1"
|
||||
minijinja = "2.8.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["wincred"] }
|
||||
|
||||
@@ -59,6 +59,13 @@ fn extract_identifier(identifier: Identifier) -> session::Identifier {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_val(s: &str) -> Result<(String, String), String> {
|
||||
match s.split_once('=') {
|
||||
Some((key, value)) => Ok((key.to_string(), value.to_string())),
|
||||
None => Err(format!("invalid KEY=VALUE: {}", s)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SessionCommand {
|
||||
#[command(about = "List all available sessions")]
|
||||
@@ -271,6 +278,16 @@ enum Command {
|
||||
)]
|
||||
recipe: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "KEY=VALUE",
|
||||
help = "Dynamic parameters (e.g., --params username=alice --params channel_name=goose-channel)",
|
||||
long_help = "Key-value parameters to pass to the recipe file. Can be specified multiple times.",
|
||||
action = clap::ArgAction::Append,
|
||||
value_parser = parse_key_val,
|
||||
)]
|
||||
params: Vec<(String, String)>,
|
||||
|
||||
/// Continue in interactive mode after processing input
|
||||
#[arg(
|
||||
short = 's',
|
||||
@@ -414,7 +431,7 @@ pub async fn cli() -> Result<()> {
|
||||
}
|
||||
None => {
|
||||
// Run session command by default
|
||||
let mut session = build_session(SessionBuilderConfig {
|
||||
let mut session: crate::Session = build_session(SessionBuilderConfig {
|
||||
identifier: identifier.map(extract_identifier),
|
||||
resume,
|
||||
extensions,
|
||||
@@ -445,6 +462,7 @@ pub async fn cli() -> Result<()> {
|
||||
extensions,
|
||||
remote_extensions,
|
||||
builtins,
|
||||
params,
|
||||
}) => {
|
||||
let input_config = match (instructions, input_text, recipe) {
|
||||
(Some(file), _, _) if file == "-" => {
|
||||
@@ -479,7 +497,7 @@ pub async fn cli() -> Result<()> {
|
||||
additional_system_prompt: None,
|
||||
},
|
||||
(_, _, Some(file)) => {
|
||||
let recipe = load_recipe(&file, true).unwrap_or_else(|err| {
|
||||
let recipe = load_recipe(&file, true, Some(params)).unwrap_or_else(|err| {
|
||||
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::recipe::load_recipe;
|
||||
/// Result indicating success or failure
|
||||
pub fn handle_validate<P: AsRef<Path>>(file_path: P) -> Result<()> {
|
||||
// Load and validate the recipe file
|
||||
match load_recipe(&file_path, false) {
|
||||
match load_recipe(&file_path, false, None) {
|
||||
Ok(_) => {
|
||||
println!("{} recipe file is valid", style("✓").green().bold());
|
||||
Ok(())
|
||||
@@ -39,7 +39,7 @@ pub fn handle_validate<P: AsRef<Path>>(file_path: P) -> Result<()> {
|
||||
/// Result indicating success or failure
|
||||
pub fn handle_deeplink<P: AsRef<Path>>(file_path: P) -> Result<()> {
|
||||
// Load the recipe file first to validate it
|
||||
match load_recipe(&file_path, false) {
|
||||
match load_recipe(&file_path, false, None) {
|
||||
Ok(recipe) => {
|
||||
if let Ok(recipe_json) = serde_json::to_string(&recipe) {
|
||||
let deeplink = base64::engine::general_purpose::STANDARD.encode(recipe_json);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::{Context, Result};
|
||||
use console::style;
|
||||
use std::path::Path;
|
||||
|
||||
use goose::recipe::Recipe;
|
||||
use minijinja::UndefinedBehavior;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
/// Loads and validates a recipe from a YAML or JSON file
|
||||
///
|
||||
@@ -10,6 +10,7 @@ use goose::recipe::Recipe;
|
||||
///
|
||||
/// * `path` - Path to the recipe file (YAML or JSON)
|
||||
/// * `log` - whether to log information about the recipe or not
|
||||
/// * `params` - optional parameters to render the recipe with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
@@ -22,7 +23,11 @@ use goose::recipe::Recipe;
|
||||
/// - The file can't be read
|
||||
/// - The YAML/JSON is invalid
|
||||
/// - The required fields are missing
|
||||
pub fn load_recipe<P: AsRef<Path>>(path: P, log: bool) -> Result<Recipe> {
|
||||
pub fn load_recipe<P: AsRef<Path>>(
|
||||
path: P,
|
||||
log: bool,
|
||||
params: Option<Vec<(String, String)>>,
|
||||
) -> Result<Recipe> {
|
||||
let path = path.as_ref();
|
||||
|
||||
// Check if file exists
|
||||
@@ -32,13 +37,18 @@ pub fn load_recipe<P: AsRef<Path>>(path: P, log: bool) -> Result<Recipe> {
|
||||
// Read file content
|
||||
let content = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read recipe file: {}", path.display()))?;
|
||||
// Check if any parameters were provided
|
||||
let rendered_content = match params {
|
||||
None => content,
|
||||
Some(params) => render_content_with_params(&content, ¶ms)?,
|
||||
};
|
||||
|
||||
// Determine file format based on extension and parse accordingly
|
||||
let recipe: Recipe = if let Some(extension) = path.extension() {
|
||||
match extension.to_str().unwrap_or("").to_lowercase().as_str() {
|
||||
"json" => serde_json::from_str(&content)
|
||||
"json" => serde_json::from_str(&rendered_content)
|
||||
.with_context(|| format!("Failed to parse JSON recipe file: {}", path.display()))?,
|
||||
"yaml" => serde_yaml::from_str(&content)
|
||||
"yaml" => serde_yaml::from_str(&rendered_content)
|
||||
.with_context(|| format!("Failed to parse YAML recipe file: {}", path.display()))?,
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -68,3 +78,60 @@ pub fn load_recipe<P: AsRef<Path>>(path: P, log: bool) -> Result<Recipe> {
|
||||
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
fn render_content_with_params(content: &str, params: &[(String, String)]) -> Result<String> {
|
||||
// Turn params into HashMap
|
||||
let param_map: HashMap<String, String> = params.iter().cloned().collect();
|
||||
|
||||
// Create a minijinja environment and context
|
||||
let mut env = minijinja::Environment::new();
|
||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||
let template = env.template_from_str(content)
|
||||
.map_err(|_| anyhow::anyhow!("Failed to render recipe, please check if the recipe has proper syntax for variables: eg: {{ variable_name }}"))?;
|
||||
|
||||
// Render the template with the parameters
|
||||
template.render(param_map).map_err(|_| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to render the recipe - please check if all required parameters are provided"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_render_content_with_params() {
|
||||
// Test basic parameter substitution
|
||||
let content = "Hello {{ name }}!";
|
||||
let params = vec![("name".to_string(), "World".to_string())];
|
||||
let result = render_content_with_params(content, ¶ms).unwrap();
|
||||
assert_eq!(result, "Hello World!");
|
||||
|
||||
// Test multiple parameters
|
||||
let content = "{{ greeting }} {{ name }}!";
|
||||
let params = vec![
|
||||
("greeting".to_string(), "Hi".to_string()),
|
||||
("name".to_string(), "Alice".to_string()),
|
||||
];
|
||||
let result = render_content_with_params(content, ¶ms).unwrap();
|
||||
assert_eq!(result, "Hi Alice!");
|
||||
|
||||
// Test missing parameter results in error
|
||||
let content = "Hello {{ missing }}!";
|
||||
let params = vec![];
|
||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("please check if all required parameters"));
|
||||
|
||||
// Test invalid template syntax results in error
|
||||
let content = "Hello {{ unclosed";
|
||||
let params = vec![];
|
||||
let err = render_content_with_params(content, ¶ms).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("please check if the recipe has proper syntax"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user