Lifei/UI parameter input (#5222)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
You are being accessed through the Goose Desktop application.
|
||||
|
||||
The user is interacting with you through a graphical user interface with the following features:
|
||||
- A chat interface where messages are displayed in a conversation format
|
||||
- Support for markdown formatting in your responses
|
||||
- Support for code blocks with syntax highlighting
|
||||
- Tool use messages are included in the chat but outputs may need to be expanded
|
||||
|
||||
The user can add extensions for you through the "Settings" page, which is available in the menu
|
||||
on the top right of the window. There is a section on that page for extensions, and it links to
|
||||
the registry.
|
||||
|
||||
Some extensions are builtin, such as Developer and Memory, while
|
||||
3rd party extensions can be browsed at https://block.github.io/goose/v1/extensions/.
|
||||
@@ -0,0 +1,15 @@
|
||||
You are a helpful agent.
|
||||
You are being accessed through the Goose Desktop application, pre configured with instructions as requested by a human.
|
||||
|
||||
The user is interacting with you through a graphical user interface with the following features:
|
||||
- A chat interface where messages are displayed in a conversation format
|
||||
- Support for markdown formatting in your responses
|
||||
- Support for code blocks with syntax highlighting
|
||||
- Tool use messages are included in the chat but outputs may need to be expanded
|
||||
|
||||
It is VERY IMPORTANT that you take note of the provided instructions, also check if a style of output is requested and always do your best to adhere to it.
|
||||
You can also validate your output after you have generated it to ensure it meets the requirements of the user.
|
||||
There may be (but not always) some tools mentioned in the instructions which you can check are available to this instance of goose (and try to help the user if they are not or find alternatives).
|
||||
|
||||
IMPORTANT instructions for you to operate as agent:
|
||||
{{recipe_instructions}}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::recipe::read_recipe_file_content::{read_parameter_file_content, RecipeFile};
|
||||
use crate::recipe::read_recipe_file_content::read_parameter_file_content;
|
||||
use crate::recipe::template_recipe::render_recipe_content_with_params;
|
||||
use crate::recipe::validate_recipe::validate_recipe_template_from_content;
|
||||
use crate::recipe::{
|
||||
@@ -19,33 +19,26 @@ pub enum RecipeError {
|
||||
RecipeParsing { source: anyhow::Error },
|
||||
}
|
||||
|
||||
pub fn render_recipe_template<F>(
|
||||
recipe_file: RecipeFile,
|
||||
fn render_recipe_template<F>(
|
||||
recipe_content: String,
|
||||
recipe_dir: &Path,
|
||||
params: Vec<(String, String)>,
|
||||
user_prompt_fn: Option<F>,
|
||||
) -> Result<(String, Vec<String>)>
|
||||
where
|
||||
F: Fn(&str, &str) -> Result<String, anyhow::Error>,
|
||||
{
|
||||
let RecipeFile {
|
||||
content: recipe_file_content,
|
||||
parent_dir: recipe_parent_dir,
|
||||
..
|
||||
} = recipe_file;
|
||||
let recipe_dir_str = recipe_parent_dir
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Error getting recipe directory"))?;
|
||||
let recipe_parameters = validate_recipe_template_from_content(
|
||||
&recipe_file_content,
|
||||
Some(recipe_dir_str.to_string()),
|
||||
)?
|
||||
.parameters;
|
||||
let recipe_dir_str = recipe_dir.display().to_string();
|
||||
|
||||
let recipe_parameters =
|
||||
validate_recipe_template_from_content(&recipe_content, Some(recipe_dir_str.clone()))?
|
||||
.parameters;
|
||||
|
||||
let (params_for_template, missing_params) =
|
||||
apply_values_to_parameters(¶ms, recipe_parameters, recipe_dir_str, user_prompt_fn)?;
|
||||
apply_values_to_parameters(¶ms, recipe_parameters, &recipe_dir_str, user_prompt_fn)?;
|
||||
|
||||
let rendered_content = if missing_params.is_empty() {
|
||||
render_recipe_content_with_params(&recipe_file_content, ¶ms_for_template)?
|
||||
render_recipe_content_with_params(&recipe_content, ¶ms_for_template)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
@@ -54,16 +47,16 @@ where
|
||||
}
|
||||
|
||||
pub fn build_recipe_from_template<F>(
|
||||
recipe_file: RecipeFile,
|
||||
recipe_content: String,
|
||||
recipe_dir: &Path,
|
||||
params: Vec<(String, String)>,
|
||||
user_prompt_fn: Option<F>,
|
||||
) -> Result<Recipe, RecipeError>
|
||||
where
|
||||
F: Fn(&str, &str) -> Result<String, anyhow::Error>,
|
||||
{
|
||||
let recipe_parent_dir = recipe_file.parent_dir.clone();
|
||||
let (rendered_content, missing_params) =
|
||||
render_recipe_template(recipe_file, params.clone(), user_prompt_fn)
|
||||
render_recipe_template(recipe_content, recipe_dir, params.clone(), user_prompt_fn)
|
||||
.map_err(|source| RecipeError::TemplateRendering { source })?;
|
||||
|
||||
if !missing_params.is_empty() {
|
||||
@@ -77,8 +70,7 @@ where
|
||||
|
||||
if let Some(ref mut sub_recipes) = recipe.sub_recipes {
|
||||
for sub_recipe in sub_recipes {
|
||||
if let Ok(resolved_path) = resolve_sub_recipe_path(&sub_recipe.path, &recipe_parent_dir)
|
||||
{
|
||||
if let Ok(resolved_path) = resolve_sub_recipe_path(&sub_recipe.path, recipe_dir) {
|
||||
sub_recipe.path = resolved_path;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +82,7 @@ where
|
||||
pub fn apply_values_to_parameters<F>(
|
||||
user_params: &[(String, String)],
|
||||
recipe_parameters: Option<Vec<RecipeParameter>>,
|
||||
recipe_parent_dir: &str,
|
||||
recipe_dir: &str,
|
||||
user_prompt_fn: Option<F>,
|
||||
) -> Result<(HashMap<String, String>, Vec<String>)>
|
||||
where
|
||||
@@ -99,7 +91,7 @@ where
|
||||
let mut param_map: HashMap<String, String> = user_params.iter().cloned().collect();
|
||||
param_map.insert(
|
||||
BUILT_IN_RECIPE_DIR_PARAM.to_string(),
|
||||
recipe_parent_dir.to_string(),
|
||||
recipe_dir.to_string(),
|
||||
);
|
||||
let mut missing_params: Vec<String> = Vec::new();
|
||||
for param in recipe_parameters.unwrap_or_default() {
|
||||
|
||||
@@ -3,12 +3,13 @@ use crate::recipe::build_recipe::{
|
||||
};
|
||||
use crate::recipe::read_recipe_file_content::RecipeFile;
|
||||
use crate::recipe::{RecipeParameterInputType, RecipeParameterRequirement};
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
const NO_USER_PROMPT: Option<fn(&str, &str) -> Result<String, anyhow::Error>> = None;
|
||||
|
||||
fn setup_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile) {
|
||||
fn setup_recipe_file(instructions_and_parameters: &str) -> (TempDir, String, PathBuf) {
|
||||
let recipe_content = format!(
|
||||
r#"{{
|
||||
"version": "1.0.0",
|
||||
@@ -22,14 +23,10 @@ fn setup_recipe_file(instructions_and_parameters: &str) -> (TempDir, RecipeFile)
|
||||
let recipe_path = temp_dir.path().join("test_recipe.json");
|
||||
|
||||
std::fs::write(&recipe_path, recipe_content).unwrap();
|
||||
let recipe_dir = temp_dir.path().to_path_buf();
|
||||
let recipe_content = std::fs::read_to_string(&recipe_path).unwrap();
|
||||
|
||||
let recipe_file = RecipeFile {
|
||||
content: std::fs::read_to_string(&recipe_path).unwrap(),
|
||||
parent_dir: temp_dir.path().to_path_buf(),
|
||||
file_path: recipe_path,
|
||||
};
|
||||
|
||||
(temp_dir, recipe_file)
|
||||
(temp_dir, recipe_content, recipe_dir)
|
||||
}
|
||||
|
||||
fn setup_test_file(temp_dir: &TempDir, filename: &str, content: &str) -> std::path::PathBuf {
|
||||
@@ -101,10 +98,11 @@ fn test_build_recipe_from_template_success() {
|
||||
}
|
||||
]"#;
|
||||
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![("my_name".to_string(), "value".to_string())];
|
||||
let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap();
|
||||
let recipe =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, params, NO_USER_PROMPT).unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
@@ -134,10 +132,11 @@ fn test_build_recipe_from_template_success_variable_in_prompt() {
|
||||
}
|
||||
]"#;
|
||||
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![("my_name".to_string(), "value".to_string())];
|
||||
let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap();
|
||||
let recipe =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, params, NO_USER_PROMPT).unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
@@ -165,9 +164,10 @@ fn test_build_recipe_from_template_wrong_parameters_in_recipe_file() {
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let build_recipe_result = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT);
|
||||
let build_recipe_result =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, Vec::new(), NO_USER_PROMPT);
|
||||
assert!(build_recipe_result.is_err());
|
||||
let err = build_recipe_result.unwrap_err();
|
||||
println!("{}", err);
|
||||
@@ -203,10 +203,11 @@ fn test_build_recipe_from_template_with_default_values_in_recipe_file() {
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
let params = vec![("param_without_default".to_string(), "value1".to_string())];
|
||||
|
||||
let recipe = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT).unwrap();
|
||||
let recipe =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, params, NO_USER_PROMPT).unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
@@ -229,9 +230,11 @@ fn test_build_recipe_from_template_optional_parameters_with_empty_default_values
|
||||
"default": ""
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
|
||||
let recipe =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, Vec::new(), NO_USER_PROMPT)
|
||||
.unwrap();
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe");
|
||||
assert_eq!(recipe.instructions.unwrap(), "Test instructions with ");
|
||||
@@ -249,9 +252,10 @@ fn test_build_recipe_from_template_optional_parameters_without_default_values_in
|
||||
"description": "A test parameter"
|
||||
}
|
||||
]"#;
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let build_recipe_result = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT);
|
||||
let build_recipe_result =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, Vec::new(), NO_USER_PROMPT);
|
||||
assert!(build_recipe_result.is_err());
|
||||
let err = build_recipe_result.unwrap_err();
|
||||
println!("{}", err);
|
||||
@@ -276,9 +280,10 @@ fn test_build_recipe_from_template_wrong_input_type_in_recipe_file() {
|
||||
}
|
||||
]"#;
|
||||
let params = vec![("param".to_string(), "value".to_string())];
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let build_recipe_result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
let build_recipe_result =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, params, NO_USER_PROMPT);
|
||||
assert!(build_recipe_result.is_err());
|
||||
let err = build_recipe_result.unwrap_err();
|
||||
match err {
|
||||
@@ -296,9 +301,11 @@ fn test_build_recipe_from_template_success_without_parameters() {
|
||||
let instructions_and_parameters = r#"
|
||||
"instructions": "Test instructions"
|
||||
"#;
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
|
||||
let recipe =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, Vec::new(), NO_USER_PROMPT)
|
||||
.unwrap();
|
||||
assert_eq!(recipe.instructions.unwrap(), "Test instructions");
|
||||
assert!(recipe.parameters.is_none());
|
||||
}
|
||||
@@ -306,9 +313,10 @@ fn test_build_recipe_from_template_success_without_parameters() {
|
||||
#[test]
|
||||
fn test_build_recipe_from_template_missing_prompt_and_instructions() {
|
||||
let instructions_and_parameters = "";
|
||||
let (_temp_dir, recipe_file) = setup_recipe_file(instructions_and_parameters);
|
||||
let (_temp_dir, recipe_content, recipe_dir) = setup_recipe_file(instructions_and_parameters);
|
||||
|
||||
let build_recipe_result = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT);
|
||||
let build_recipe_result =
|
||||
build_recipe_from_template(recipe_content, &recipe_dir, Vec::new(), NO_USER_PROMPT);
|
||||
assert!(build_recipe_result.is_err());
|
||||
let err = build_recipe_result.unwrap_err();
|
||||
println!("{}", err);
|
||||
@@ -366,8 +374,13 @@ fn test_template_inheritance() {
|
||||
("is_enabled".to_string(), "true".to_string()),
|
||||
];
|
||||
|
||||
let parent_recipe =
|
||||
build_recipe_from_template(parent_recipe_file, params.clone(), NO_USER_PROMPT).unwrap();
|
||||
let parent_recipe = build_recipe_from_template(
|
||||
parent_recipe_file.content,
|
||||
&parent_recipe_file.parent_dir,
|
||||
params.clone(),
|
||||
NO_USER_PROMPT,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parent_recipe.description, "Parent recipe");
|
||||
assert_eq!(
|
||||
parent_recipe.prompt.unwrap(),
|
||||
@@ -380,8 +393,13 @@ fn test_template_inheritance() {
|
||||
"is_enabled"
|
||||
);
|
||||
|
||||
let child_recipe =
|
||||
build_recipe_from_template(child_recipe_file, params, NO_USER_PROMPT).unwrap();
|
||||
let child_recipe = build_recipe_from_template(
|
||||
child_recipe_file.content,
|
||||
&child_recipe_file.parent_dir,
|
||||
params,
|
||||
NO_USER_PROMPT,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(child_recipe.title, "Parent");
|
||||
assert_eq!(child_recipe.description, "Parent recipe");
|
||||
assert_eq!(
|
||||
@@ -467,7 +485,13 @@ instructions: Child instructions
|
||||
file_path: main_recipe_path,
|
||||
};
|
||||
|
||||
let recipe = build_recipe_from_template(recipe_file, Vec::new(), NO_USER_PROMPT).unwrap();
|
||||
let recipe = build_recipe_from_template(
|
||||
recipe_file.content,
|
||||
&recipe_file.parent_dir,
|
||||
Vec::new(),
|
||||
NO_USER_PROMPT,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(recipe.title, "Main Recipe");
|
||||
assert!(recipe.sub_recipes.is_some());
|
||||
@@ -505,7 +529,12 @@ parameters:
|
||||
"FILE_PARAM".to_string(),
|
||||
test_file_path.to_string_lossy().to_string(),
|
||||
)];
|
||||
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
let result = build_recipe_from_template(
|
||||
recipe_file.content,
|
||||
&recipe_file.parent_dir,
|
||||
params,
|
||||
NO_USER_PROMPT,
|
||||
);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let recipe = result.unwrap();
|
||||
@@ -530,7 +559,12 @@ parameters:
|
||||
"FILE_PARAM".to_string(),
|
||||
"/nonexistent/path/file.txt".to_string(),
|
||||
)];
|
||||
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
let result = build_recipe_from_template(
|
||||
recipe_file.content,
|
||||
&recipe_file.parent_dir,
|
||||
params,
|
||||
NO_USER_PROMPT,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
if let Err(RecipeError::TemplateRendering { source }) = result {
|
||||
@@ -553,7 +587,12 @@ parameters:
|
||||
let (_temp_dir, recipe_file) = setup_yaml_recipe_file(instructions_and_parameters);
|
||||
|
||||
let params = vec![];
|
||||
let result = build_recipe_from_template(recipe_file, params, NO_USER_PROMPT);
|
||||
let result = build_recipe_from_template(
|
||||
recipe_file.content,
|
||||
&recipe_file.parent_dir,
|
||||
params,
|
||||
NO_USER_PROMPT,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
if let Err(RecipeError::TemplateRendering { source }) = result {
|
||||
|
||||
@@ -150,6 +150,11 @@ fn get_env_with_template_variables(
|
||||
Ok((env, template_variables))
|
||||
}
|
||||
|
||||
fn uses_template_inheritance(content: &str) -> bool {
|
||||
let re = Regex::new(r"\{%-?\s*(extends|include)").unwrap();
|
||||
re.is_match(content)
|
||||
}
|
||||
|
||||
pub fn parse_recipe_content(
|
||||
content: &str,
|
||||
recipe_dir: Option<String>,
|
||||
@@ -163,46 +168,23 @@ pub fn parse_recipe_content(
|
||||
UndefinedBehavior::Lenient,
|
||||
)?;
|
||||
let template = env.get_template(CURRENT_TEMPLATE_NAME).unwrap();
|
||||
let rendered_content = template
|
||||
.render(())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse the recipe {}", e))?;
|
||||
let recipe = Recipe::from_content(&rendered_content)?;
|
||||
|
||||
// Detect if template uses inheritance or includes
|
||||
let recipe_content = if uses_template_inheritance(&preprocessed_content) {
|
||||
// Must render to resolve inheritance
|
||||
template
|
||||
.render(())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse the recipe {}", e))?
|
||||
} else {
|
||||
// Preserve conditionals and variables as-is
|
||||
preprocessed_content
|
||||
};
|
||||
|
||||
let recipe = Recipe::from_content(&recipe_content)?;
|
||||
// return recipe (without loading any variables) and the variable names that are in the recipe
|
||||
Ok((recipe, template_variables))
|
||||
}
|
||||
|
||||
// render the recipe for validation, deeplink and explain, etc.
|
||||
pub fn render_recipe_for_preview(
|
||||
content: &str,
|
||||
recipe_dir: Option<String>,
|
||||
params: &HashMap<String, String>,
|
||||
) -> Result<Recipe> {
|
||||
// Pre-process template variables to handle invalid variable names
|
||||
let preprocessed_content = preprocess_template_variables(content)?;
|
||||
|
||||
let (env, template_variables) = get_env_with_template_variables(
|
||||
&preprocessed_content,
|
||||
recipe_dir,
|
||||
UndefinedBehavior::Lenient,
|
||||
)?;
|
||||
let template = env.get_template(CURRENT_TEMPLATE_NAME).unwrap();
|
||||
// if the variables are not provided, the template will be rendered with the variables, otherwise it will keep the variables as is
|
||||
let mut ctx = preserve_vars(&template_variables).clone();
|
||||
ctx.extend(params.clone());
|
||||
let rendered_content = template
|
||||
.render(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse the recipe {}", e))?;
|
||||
Recipe::from_content(&rendered_content)
|
||||
}
|
||||
|
||||
fn preserve_vars(variables: &HashSet<String>) -> HashMap<String, String> {
|
||||
let mut context = HashMap::<String, String>::new();
|
||||
for template_var in variables {
|
||||
context.insert(template_var.clone(), format!("{{{{ {} }}}}", template_var));
|
||||
}
|
||||
context
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod render_content_with_params_tests {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
use crate::recipe::read_recipe_file_content::RecipeFile;
|
||||
use crate::recipe::template_recipe::{parse_recipe_content, render_recipe_for_preview};
|
||||
use crate::recipe::template_recipe::parse_recipe_content;
|
||||
use crate::recipe::{
|
||||
Recipe, RecipeParameter, RecipeParameterInputType, RecipeParameterRequirement,
|
||||
BUILT_IN_RECIPE_DIR_PARAM,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn validate_recipe_parameters(
|
||||
pub fn parse_and_validate_parameters(
|
||||
recipe_file_content: &str,
|
||||
recipe_dir_str: Option<String>,
|
||||
) -> Result<Option<Vec<RecipeParameter>>> {
|
||||
) -> Result<Recipe> {
|
||||
let (recipe_template, template_variables) =
|
||||
parse_recipe_content(recipe_file_content, recipe_dir_str)?;
|
||||
let recipe_parameters = recipe_template.parameters;
|
||||
validate_optional_parameters(&recipe_parameters)?;
|
||||
validate_parameters_in_template(&recipe_parameters, &template_variables)?;
|
||||
Ok(recipe_parameters)
|
||||
let recipe_parameters = &recipe_template.parameters;
|
||||
validate_optional_parameters(recipe_parameters)?;
|
||||
validate_parameters_in_template(recipe_parameters, &template_variables)?;
|
||||
Ok(recipe_template)
|
||||
}
|
||||
|
||||
fn validate_json_schema(schema: &serde_json::Value) -> Result<()> {
|
||||
@@ -40,8 +40,8 @@ pub fn validate_recipe_template_from_content(
|
||||
recipe_content: &str,
|
||||
recipe_dir: Option<String>,
|
||||
) -> Result<Recipe> {
|
||||
validate_recipe_parameters(recipe_content, recipe_dir.clone())?;
|
||||
let recipe = render_recipe_for_preview(recipe_content, recipe_dir, &HashMap::new())?;
|
||||
parse_and_validate_parameters(recipe_content, recipe_dir.clone())?;
|
||||
let (recipe, _) = parse_recipe_content(recipe_content, recipe_dir)?;
|
||||
|
||||
validate_prompt_or_instructions(&recipe)?;
|
||||
if let Some(response) = &recipe.response {
|
||||
@@ -154,3 +154,47 @@ fn validate_optional_parameters(parameters: &Option<Vec<RecipeParameter>>) -> Re
|
||||
Err(anyhow::anyhow!("Optional parameters missing default values in the recipe: {}. Please provide defaults.", optional_params_without_default_values.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_recipe_template_from_content_success() {
|
||||
let recipe_content = r#"
|
||||
version: 1.0.0
|
||||
title: Test Recipe
|
||||
description: A test recipe for validation
|
||||
instructions: Test instructions with {{ user_role }}
|
||||
prompt: |
|
||||
{% if user_role in ["Director, Account Management", "Senior Director, Account Management"] %}
|
||||
- Focus on strategic planning and organizational performance
|
||||
{% else %}
|
||||
- Provide foundational account management guidance
|
||||
{% endif %}
|
||||
parameters:
|
||||
- key: user_role
|
||||
input_type: string
|
||||
requirement: required
|
||||
description: A test parameter
|
||||
"#;
|
||||
|
||||
let result = validate_recipe_template_from_content(recipe_content, None);
|
||||
if let Err(e) = &result {
|
||||
eprintln!("Validation error: {}", e);
|
||||
eprintln!("Error chain:");
|
||||
let mut source = e.source();
|
||||
while let Some(err) = source {
|
||||
eprintln!(" Caused by: {}", err);
|
||||
source = err.source();
|
||||
}
|
||||
}
|
||||
assert!(result.is_ok(), "Validation failed: {:?}", result.err());
|
||||
|
||||
let recipe = result.unwrap();
|
||||
assert_eq!(recipe.title, "Test Recipe");
|
||||
assert_eq!(recipe.description, "A test recipe for validation");
|
||||
assert!(recipe.instructions.is_some());
|
||||
println!("Recipe: {:?}", recipe.prompt);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user