-
Notifications
You must be signed in to change notification settings - Fork 2.7k
fix: backwards compatible parsing recipe file #5020
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,57 +21,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 | ||
|
|
@@ -276,20 +225,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(), | ||
|
|
@@ -309,24 +244,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)? | ||
| } | ||
| } 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)? | ||
| // 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") { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ugh
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is legacy format that we have to maintain :( |
||
| 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 { | ||
| 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() { | ||
|
|
@@ -342,25 +287,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 | ||
|
|
@@ -371,13 +312,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 | ||
|
|
@@ -388,19 +327,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 | ||
|
|
@@ -416,15 +352,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")?; | ||
|
|
@@ -806,4 +738,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"); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for deleting this!