mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
fix: backwards compatible parsing recipe file (#5020)
This commit is contained in:
@@ -24,57 +24,6 @@ fn default_version() -> String {
|
||||
"1.0.0".to_string()
|
||||
}
|
||||
|
||||
/// A Recipe represents a personalized, user-generated agent configuration that defines
|
||||
/// specific behaviors and capabilities within the goose system.
|
||||
///
|
||||
/// # Fields
|
||||
///
|
||||
/// ## Required Fields
|
||||
/// * `version` - Semantic version of the Recipe file format (defaults to "1.0.0")
|
||||
/// * `title` - Short, descriptive name of the Recipe
|
||||
/// * `description` - Detailed description explaining the Recipe's purpose and functionality
|
||||
/// * `Instructions` - Instructions that defines the Recipe's behavior
|
||||
///
|
||||
/// ## Optional Fields
|
||||
/// * `prompt` - the initial prompt to the session to start with
|
||||
/// * `extensions` - List of extension configurations required by the Recipe
|
||||
/// * `context` - Supplementary context information for the Recipe
|
||||
/// * `activities` - Activity labels that appear when loading the Recipe
|
||||
/// * `author` - Information about the Recipe's creator and metadata
|
||||
/// * `parameters` - Additional parameters for the Recipe
|
||||
/// * `response` - Response configuration including JSON schema validation
|
||||
/// * `retry` - Retry configuration for automated validation and recovery
|
||||
/// # Example
|
||||
///
|
||||
///
|
||||
/// use goose::recipe::Recipe;
|
||||
///
|
||||
/// // Using the builder pattern
|
||||
/// let recipe = Recipe::builder()
|
||||
/// .title("Example Agent")
|
||||
/// .description("An example Recipe configuration")
|
||||
/// .instructions("Act as a helpful assistant")
|
||||
/// .build()
|
||||
/// .expect("Missing required fields");
|
||||
///
|
||||
/// // Or using struct initialization
|
||||
/// let recipe = Recipe {
|
||||
/// version: "1.0.0".to_string(),
|
||||
/// title: "Example Agent".to_string(),
|
||||
/// description: "An example Recipe configuration".to_string(),
|
||||
/// instructions: Some("Act as a helpful assistant".to_string()),
|
||||
/// prompt: None,
|
||||
/// extensions: None,
|
||||
/// context: None,
|
||||
/// activities: None,
|
||||
/// author: None,
|
||||
/// settings: None,
|
||||
/// parameters: None,
|
||||
/// response: None,
|
||||
/// sub_recipes: None,
|
||||
/// retry: None,
|
||||
/// };
|
||||
///
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct Recipe {
|
||||
// Required fields
|
||||
@@ -279,20 +228,6 @@ impl Recipe {
|
||||
false
|
||||
}
|
||||
|
||||
/// Creates a new RecipeBuilder to construct a Recipe instance
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
///
|
||||
/// use goose::recipe::Recipe;
|
||||
///
|
||||
/// let recipe = Recipe::builder()
|
||||
/// .title("My Recipe")
|
||||
/// .description("A helpful assistant")
|
||||
/// .instructions("Act as a helpful assistant")
|
||||
/// .build()
|
||||
/// .expect("Failed to build Recipe: missing required fields");
|
||||
///
|
||||
pub fn builder() -> RecipeBuilder {
|
||||
RecipeBuilder {
|
||||
version: default_version(),
|
||||
@@ -318,24 +253,34 @@ impl Recipe {
|
||||
}
|
||||
|
||||
pub fn from_content(content: &str) -> Result<Self> {
|
||||
let recipe: Recipe =
|
||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(content) {
|
||||
if let Some(nested_recipe) = json_value.get("recipe") {
|
||||
serde_json::from_value(nested_recipe.clone())?
|
||||
} else {
|
||||
serde_json::from_str(content)?
|
||||
// Parse using YAML parser (since JSON is a subset of YAML, this handles both)
|
||||
let mut value: serde_yaml::Value = serde_yaml::from_str(content)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse recipe content as YAML/JSON: {}", e))?;
|
||||
|
||||
// Handle nested legacy recipe format
|
||||
if let Some(nested_recipe) = value.get("recipe") {
|
||||
value = nested_recipe.clone();
|
||||
}
|
||||
|
||||
if let Some(extensions) = value
|
||||
.get_mut("extensions")
|
||||
.and_then(|v| v.as_sequence_mut())
|
||||
{
|
||||
for ext in extensions.iter_mut() {
|
||||
if let Some(obj) = ext.as_mapping_mut() {
|
||||
if let Some(desc) = obj.get("description") {
|
||||
if desc.is_null() || desc.as_str().is_some_and(|s| s.is_empty()) {
|
||||
if let Some(name) = obj.get("name").and_then(|n| n.as_str()) {
|
||||
obj.insert("description".into(), name.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Ok(yaml_value) = serde_yaml::from_str::<serde_yaml::Value>(content) {
|
||||
if let Some(nested_recipe) = yaml_value.get("recipe") {
|
||||
serde_yaml::from_value(nested_recipe.clone())?
|
||||
} else {
|
||||
serde_yaml::from_str(content)?
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unsupported format. Expected JSON or YAML."
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let recipe: Recipe = serde_yaml::from_value(value)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize recipe: {}", e))?;
|
||||
|
||||
if let Some(ref retry_config) = recipe.retry {
|
||||
if let Err(validation_error) = retry_config.validate() {
|
||||
@@ -351,25 +296,21 @@ impl Recipe {
|
||||
}
|
||||
|
||||
impl RecipeBuilder {
|
||||
/// Sets the version of the Recipe
|
||||
pub fn version(mut self, version: impl Into<String>) -> Self {
|
||||
self.version = version.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the title of the Recipe (required)
|
||||
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the description of the Recipe (required)
|
||||
pub fn description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the instructions for the Recipe (required)
|
||||
pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
|
||||
self.instructions = Some(instructions.into());
|
||||
self
|
||||
@@ -380,13 +321,11 @@ impl RecipeBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the extensions for the Recipe
|
||||
pub fn extensions(mut self, extensions: Vec<ExtensionConfig>) -> Self {
|
||||
self.extensions = Some(extensions);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the context for the Recipe
|
||||
pub fn context(mut self, context: Vec<String>) -> Self {
|
||||
self.context = Some(context);
|
||||
self
|
||||
@@ -397,19 +336,16 @@ impl RecipeBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the activities for the Recipe
|
||||
pub fn activities(mut self, activities: Vec<String>) -> Self {
|
||||
self.activities = Some(activities);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the author information for the Recipe
|
||||
pub fn author(mut self, author: Author) -> Self {
|
||||
self.author = Some(author);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the parameters for the Recipe
|
||||
pub fn parameters(mut self, parameters: Vec<RecipeParameter>) -> Self {
|
||||
self.parameters = Some(parameters);
|
||||
self
|
||||
@@ -425,15 +361,11 @@ impl RecipeBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the retry configuration for the Recipe
|
||||
pub fn retry(mut self, retry: RetryConfig) -> Self {
|
||||
self.retry = Some(retry);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the Recipe instance
|
||||
///
|
||||
/// Returns an error if any required fields are missing
|
||||
pub fn build(self) -> Result<Recipe, &'static str> {
|
||||
let title = self.title.ok_or("Title is required")?;
|
||||
let description = self.description.ok_or("Description is required")?;
|
||||
@@ -815,4 +747,40 @@ isGlobal: true"#;
|
||||
recipe.prompt = Some(format!("prompt{}", '\u{E0042}'));
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_content_with_null_description() {
|
||||
let content = r#"{
|
||||
"version": "1.0.0",
|
||||
"title": "Test Recipe",
|
||||
"description": "A test recipe",
|
||||
"instructions": "Test instructions",
|
||||
"extensions": [
|
||||
{
|
||||
"type": "stdio",
|
||||
"name": "test_extension",
|
||||
"cmd": "test_cmd",
|
||||
"args": [],
|
||||
"timeout": 300,
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let recipe = Recipe::from_content(content).unwrap();
|
||||
|
||||
assert!(recipe.extensions.is_some());
|
||||
let extensions = recipe.extensions.unwrap();
|
||||
assert_eq!(extensions.len(), 1);
|
||||
|
||||
if let ExtensionConfig::Stdio {
|
||||
name, description, ..
|
||||
} = &extensions[0]
|
||||
{
|
||||
assert_eq!(name, "test_extension");
|
||||
assert_eq!(description, "test_extension");
|
||||
} else {
|
||||
panic!("Expected Stdio extension");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3425,7 +3425,6 @@
|
||||
},
|
||||
"Recipe": {
|
||||
"type": "object",
|
||||
"description": "A Recipe represents a personalized, user-generated agent configuration that defines\nspecific behaviors and capabilities within the goose system.\n\n# Fields\n\n## Required Fields\n* `version` - Semantic version of the Recipe file format (defaults to \"1.0.0\")\n* `title` - Short, descriptive name of the Recipe\n* `description` - Detailed description explaining the Recipe's purpose and functionality\n* `Instructions` - Instructions that defines the Recipe's behavior\n\n## Optional Fields\n* `prompt` - the initial prompt to the session to start with\n* `extensions` - List of extension configurations required by the Recipe\n* `context` - Supplementary context information for the Recipe\n* `activities` - Activity labels that appear when loading the Recipe\n* `author` - Information about the Recipe's creator and metadata\n* `parameters` - Additional parameters for the Recipe\n* `response` - Response configuration including JSON schema validation\n* `retry` - Retry configuration for automated validation and recovery\n# Example\n\n\nuse goose::recipe::Recipe;\n\n// Using the builder pattern\nlet recipe = Recipe::builder()\n.title(\"Example Agent\")\n.description(\"An example Recipe configuration\")\n.instructions(\"Act as a helpful assistant\")\n.build()\n.expect(\"Missing required fields\");\n\n// Or using struct initialization\nlet recipe = Recipe {\nversion: \"1.0.0\".to_string(),\ntitle: \"Example Agent\".to_string(),\ndescription: \"An example Recipe configuration\".to_string(),\ninstructions: Some(\"Act as a helpful assistant\".to_string()),\nprompt: None,\nextensions: None,\ncontext: None,\nactivities: None,\nauthor: None,\nsettings: None,\nparameters: None,\nresponse: None,\nsub_recipes: None,\nretry: None,\n};\n",
|
||||
"required": [
|
||||
"title",
|
||||
"description"
|
||||
|
||||
@@ -547,59 +547,6 @@ export type RawTextContent = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Recipe represents a personalized, user-generated agent configuration that defines
|
||||
* specific behaviors and capabilities within the goose system.
|
||||
*
|
||||
* # Fields
|
||||
*
|
||||
* ## Required Fields
|
||||
* * `version` - Semantic version of the Recipe file format (defaults to "1.0.0")
|
||||
* * `title` - Short, descriptive name of the Recipe
|
||||
* * `description` - Detailed description explaining the Recipe's purpose and functionality
|
||||
* * `Instructions` - Instructions that defines the Recipe's behavior
|
||||
*
|
||||
* ## Optional Fields
|
||||
* * `prompt` - the initial prompt to the session to start with
|
||||
* * `extensions` - List of extension configurations required by the Recipe
|
||||
* * `context` - Supplementary context information for the Recipe
|
||||
* * `activities` - Activity labels that appear when loading the Recipe
|
||||
* * `author` - Information about the Recipe's creator and metadata
|
||||
* * `parameters` - Additional parameters for the Recipe
|
||||
* * `response` - Response configuration including JSON schema validation
|
||||
* * `retry` - Retry configuration for automated validation and recovery
|
||||
* # Example
|
||||
*
|
||||
*
|
||||
* use goose::recipe::Recipe;
|
||||
*
|
||||
* // Using the builder pattern
|
||||
* let recipe = Recipe::builder()
|
||||
* .title("Example Agent")
|
||||
* .description("An example Recipe configuration")
|
||||
* .instructions("Act as a helpful assistant")
|
||||
* .build()
|
||||
* .expect("Missing required fields");
|
||||
*
|
||||
* // Or using struct initialization
|
||||
* let recipe = Recipe {
|
||||
* version: "1.0.0".to_string(),
|
||||
* title: "Example Agent".to_string(),
|
||||
* description: "An example Recipe configuration".to_string(),
|
||||
* instructions: Some("Act as a helpful assistant".to_string()),
|
||||
* prompt: None,
|
||||
* extensions: None,
|
||||
* context: None,
|
||||
* activities: None,
|
||||
* author: None,
|
||||
* settings: None,
|
||||
* parameters: None,
|
||||
* response: None,
|
||||
* sub_recipes: None,
|
||||
* retry: None,
|
||||
* };
|
||||
*
|
||||
*/
|
||||
export type Recipe = {
|
||||
activities?: Array<string> | null;
|
||||
author?: Author | null;
|
||||
|
||||
Reference in New Issue
Block a user