Lifei/UI parameter input (#5222)
This commit is contained in:
@@ -9,9 +9,7 @@ use goose::config::Config;
|
||||
use goose::recipe::build_recipe::{
|
||||
apply_values_to_parameters, build_recipe_from_template, RecipeError,
|
||||
};
|
||||
use goose::recipe::read_recipe_file_content::RecipeFile;
|
||||
use goose::recipe::template_recipe::render_recipe_for_preview;
|
||||
use goose::recipe::validate_recipe::validate_recipe_parameters;
|
||||
use goose::recipe::validate_recipe::parse_and_validate_parameters;
|
||||
use goose::recipe::Recipe;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -23,19 +21,16 @@ fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_recipe_file_with_dir(recipe_name: &str) -> Result<(RecipeFile, String)> {
|
||||
let recipe_file = load_recipe_file(recipe_name)?;
|
||||
let recipe_dir_str = recipe_file
|
||||
.parent_dir
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Error getting recipe directory"))?
|
||||
.to_string();
|
||||
Ok((recipe_file, recipe_dir_str))
|
||||
}
|
||||
|
||||
pub fn load_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<Recipe> {
|
||||
let recipe_file = load_recipe_file(recipe_name)?;
|
||||
match build_recipe_from_template(recipe_file, params, Some(create_user_prompt_callback())) {
|
||||
let recipe_content = recipe_file.content;
|
||||
let recipe_dir = recipe_file.parent_dir;
|
||||
match build_recipe_from_template(
|
||||
recipe_content,
|
||||
&recipe_dir,
|
||||
params,
|
||||
Some(create_user_prompt_callback()),
|
||||
) {
|
||||
Ok(recipe) => {
|
||||
let secret_requirements = discover_recipe_secrets(&recipe);
|
||||
if let Err(e) = collect_missing_secrets(&secret_requirements) {
|
||||
@@ -132,10 +127,12 @@ pub fn render_recipe_as_yaml(recipe_name: &str, params: Vec<(String, String)>) -
|
||||
}
|
||||
|
||||
pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Result<()> {
|
||||
let (recipe_file, recipe_dir_str) = load_recipe_file_with_dir(recipe_name)?;
|
||||
let recipe_file = load_recipe_file(recipe_name)?;
|
||||
let recipe_dir_str = recipe_file.parent_dir.display().to_string();
|
||||
let recipe_file_content = &recipe_file.content;
|
||||
let recipe_parameters =
|
||||
validate_recipe_parameters(recipe_file_content, Some(recipe_dir_str.clone()))?;
|
||||
let recipe_template =
|
||||
parse_and_validate_parameters(recipe_file_content, Some(recipe_dir_str.clone()))?;
|
||||
let recipe_parameters = recipe_template.parameters.clone();
|
||||
|
||||
let (params_for_template, missing_params) = apply_values_to_parameters(
|
||||
¶ms,
|
||||
@@ -143,12 +140,7 @@ pub fn explain_recipe(recipe_name: &str, params: Vec<(String, String)>) -> Resul
|
||||
&recipe_dir_str,
|
||||
None::<fn(&str, &str) -> Result<String>>,
|
||||
)?;
|
||||
let recipe = render_recipe_for_preview(
|
||||
recipe_file_content,
|
||||
Some(recipe_dir_str.clone()),
|
||||
¶ms_for_template,
|
||||
)?;
|
||||
print_recipe_explanation(&recipe);
|
||||
print_recipe_explanation(&recipe_template);
|
||||
print_required_parameters_for_template(params_for_template, missing_params);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -344,11 +344,9 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::start_agent,
|
||||
super::routes::agent::resume_agent,
|
||||
super::routes::agent::get_tools,
|
||||
super::routes::agent::add_sub_recipes,
|
||||
super::routes::agent::extend_prompt,
|
||||
super::routes::agent::update_from_session,
|
||||
super::routes::agent::update_agent_provider,
|
||||
super::routes::agent::update_router_tool_selector,
|
||||
super::routes::agent::update_session_config,
|
||||
super::routes::reply::confirm_permission,
|
||||
super::routes::reply::reply,
|
||||
super::routes::context::manage_context,
|
||||
@@ -400,6 +398,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::session::SessionListResponse,
|
||||
super::routes::session::UpdateSessionDescriptionRequest,
|
||||
super::routes::session::UpdateSessionUserRecipeValuesRequest,
|
||||
super::routes::session::UpdateSessionUserRecipeValuesResponse,
|
||||
Message,
|
||||
MessageContent,
|
||||
MessageMetadata,
|
||||
@@ -479,16 +478,12 @@ derive_utoipa!(Icon as IconSchema);
|
||||
goose::recipe::SubRecipe,
|
||||
goose::agents::types::RetryConfig,
|
||||
goose::agents::types::SuccessCheck,
|
||||
super::routes::agent::AddSubRecipesRequest,
|
||||
super::routes::agent::AddSubRecipesResponse,
|
||||
super::routes::agent::ExtendPromptRequest,
|
||||
super::routes::agent::ExtendPromptResponse,
|
||||
super::routes::agent::UpdateProviderRequest,
|
||||
super::routes::agent::SessionConfigRequest,
|
||||
super::routes::agent::GetToolsQuery,
|
||||
super::routes::agent::UpdateRouterToolSelectorRequest,
|
||||
super::routes::agent::StartAgentRequest,
|
||||
super::routes::agent::ResumeAgentRequest,
|
||||
super::routes::agent::UpdateFromSessionRequest,
|
||||
super::routes::setup::SetupResponse,
|
||||
))
|
||||
)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::{load_recipe_by_id, validate_recipe};
|
||||
use crate::routes::recipe_utils::{
|
||||
apply_recipe_to_agent, build_recipe_with_parameter_values, load_recipe_by_id, validate_recipe,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
@@ -9,44 +11,30 @@ use axum::{
|
||||
};
|
||||
use goose::config::PermissionManager;
|
||||
|
||||
use goose::config::Config;
|
||||
use goose::model::ModelConfig;
|
||||
use goose::prompt_template::render_global_file;
|
||||
use goose::providers::create;
|
||||
use goose::recipe::{Recipe, Response};
|
||||
use goose::recipe::Recipe;
|
||||
use goose::recipe_deeplink;
|
||||
use goose::session::{Session, SessionManager};
|
||||
use goose::{
|
||||
agents::{extension::ToolInfo, extension_manager::get_parameter_names},
|
||||
config::permission::PermissionLevel,
|
||||
};
|
||||
use goose::{config::Config, recipe::SubRecipe};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct ExtendPromptRequest {
|
||||
extension: String,
|
||||
pub struct UpdateFromSessionRequest {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct ExtendPromptResponse {
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct AddSubRecipesRequest {
|
||||
sub_recipes: Vec<SubRecipe>,
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, utoipa::ToSchema)]
|
||||
pub struct AddSubRecipesResponse {
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateProviderRequest {
|
||||
provider: String,
|
||||
@@ -54,12 +42,6 @@ pub struct UpdateProviderRequest {
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct SessionConfigRequest {
|
||||
response: Option<Response>,
|
||||
session_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, utoipa::ToSchema)]
|
||||
pub struct GetToolsQuery {
|
||||
extension_name: Option<String>,
|
||||
@@ -109,7 +91,7 @@ async fn start_agent(
|
||||
recipe_deeplink,
|
||||
} = payload;
|
||||
|
||||
let resolved_recipe = if let Some(deeplink) = recipe_deeplink {
|
||||
let original_recipe = if let Some(deeplink) = recipe_deeplink {
|
||||
match recipe_deeplink::decode(&deeplink) {
|
||||
Ok(recipe) => Some(recipe),
|
||||
Err(err) => {
|
||||
@@ -129,7 +111,7 @@ async fn start_agent(
|
||||
recipe
|
||||
};
|
||||
|
||||
if let Some(ref recipe) = resolved_recipe {
|
||||
if let Some(ref recipe) = original_recipe {
|
||||
if let Err(err) = validate_recipe(recipe) {
|
||||
return Err(ErrorResponse {
|
||||
message: err.message,
|
||||
@@ -151,7 +133,7 @@ async fn start_agent(
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(recipe) = resolved_recipe {
|
||||
if let Some(recipe) = original_recipe {
|
||||
SessionManager::update_session(&session.id)
|
||||
.recipe(Some(recipe))
|
||||
.apply()
|
||||
@@ -207,40 +189,61 @@ async fn resume_agent(
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/add_sub_recipes",
|
||||
request_body = AddSubRecipesRequest,
|
||||
path = "/agent/update_from_session",
|
||||
request_body = UpdateFromSessionRequest,
|
||||
responses(
|
||||
(status = 200, description = "Added sub recipes to agent successfully", body = AddSubRecipesResponse),
|
||||
(status = 200, description = "Update agent from session data successfully"),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
),
|
||||
)]
|
||||
async fn add_sub_recipes(
|
||||
async fn update_from_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<AddSubRecipesRequest>,
|
||||
) -> Result<Json<AddSubRecipesResponse>, StatusCode> {
|
||||
let agent = state.get_agent_for_route(payload.session_id).await?;
|
||||
agent.add_sub_recipes(payload.sub_recipes.clone()).await;
|
||||
Ok(Json(AddSubRecipesResponse { success: true }))
|
||||
}
|
||||
Json(payload): Json<UpdateFromSessionRequest>,
|
||||
) -> Result<StatusCode, ErrorResponse> {
|
||||
let agent = state
|
||||
.get_agent_for_route(payload.session_id.clone())
|
||||
.await
|
||||
.map_err(|status| ErrorResponse {
|
||||
message: format!("Failed to get agent: {}", status),
|
||||
status,
|
||||
})?;
|
||||
let session = SessionManager::get_session(&payload.session_id, false)
|
||||
.await
|
||||
.map_err(|err| ErrorResponse {
|
||||
message: format!("Failed to get session: {}", err),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
let context: HashMap<&str, Value> = HashMap::new();
|
||||
let desktop_prompt =
|
||||
render_global_file("desktop_prompt.md", &context).expect("Prompt should render");
|
||||
let mut update_prompt = desktop_prompt;
|
||||
if let Some(recipe) = session.recipe {
|
||||
match build_recipe_with_parameter_values(
|
||||
&recipe,
|
||||
session.user_recipe_values.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(recipe)) => {
|
||||
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await {
|
||||
update_prompt = prompt;
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Recipe has missing parameters - use default prompt
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(ErrorResponse {
|
||||
message: e.to_string(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
agent.extend_system_prompt(update_prompt).await;
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/prompt",
|
||||
request_body = ExtendPromptRequest,
|
||||
responses(
|
||||
(status = 200, description = "Extended system prompt successfully", body = ExtendPromptResponse),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
),
|
||||
)]
|
||||
async fn extend_prompt(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ExtendPromptRequest>,
|
||||
) -> Result<Json<ExtendPromptResponse>, StatusCode> {
|
||||
let agent = state.get_agent_for_route(payload.session_id).await?;
|
||||
agent.extend_system_prompt(payload.extension.clone()).await;
|
||||
Ok(Json(ExtendPromptResponse { success: true }))
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -378,46 +381,16 @@ async fn update_router_tool_selector(
|
||||
))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/agent/session_config",
|
||||
request_body = SessionConfigRequest,
|
||||
responses(
|
||||
(status = 200, description = "Session config updated successfully", body = String),
|
||||
(status = 401, description = "Unauthorized - invalid secret key"),
|
||||
(status = 424, description = "Agent not initialized"),
|
||||
(status = 500, description = "Internal server error")
|
||||
)
|
||||
)]
|
||||
async fn update_session_config(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<SessionConfigRequest>,
|
||||
) -> Result<Json<String>, StatusCode> {
|
||||
let agent = state.get_agent_for_route(payload.session_id).await?;
|
||||
if let Some(response) = payload.response {
|
||||
agent.add_final_output_tool(response).await;
|
||||
|
||||
tracing::info!("Added final output tool with response config");
|
||||
Ok(Json(
|
||||
"Session config updated with final output tool".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(Json("Nothing provided to update.".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/agent/start", post(start_agent))
|
||||
.route("/agent/resume", post(resume_agent))
|
||||
.route("/agent/prompt", post(extend_prompt))
|
||||
.route("/agent/tools", get(get_tools))
|
||||
.route("/agent/update_provider", post(update_agent_provider))
|
||||
.route(
|
||||
"/agent/update_router_tool_selector",
|
||||
post(update_router_tool_selector),
|
||||
)
|
||||
.route("/agent/session_config", post(update_session_config))
|
||||
.route("/agent/add_sub_recipes", post(add_sub_recipes))
|
||||
.route("/agent/update_from_session", post(update_from_session))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -382,7 +382,6 @@ fn ensure_recipe_valid(recipe: &Recipe) -> Result<(), ErrorResponse> {
|
||||
status: err.status,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,21 @@ use std::fs;
|
||||
use std::hash::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use axum::http::StatusCode;
|
||||
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::state::AppState;
|
||||
use goose::recipe::local_recipes::list_local_recipes;
|
||||
use goose::agents::Agent;
|
||||
use goose::prompt_template::render_global_file;
|
||||
use goose::recipe::build_recipe::{build_recipe_from_template, RecipeError};
|
||||
use goose::recipe::local_recipes::{get_recipe_library_dir, list_local_recipes};
|
||||
use goose::recipe::validate_recipe::validate_recipe_template_from_content;
|
||||
use goose::recipe::Recipe;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
use serde_yaml;
|
||||
use tracing::error;
|
||||
|
||||
pub struct RecipeValidationError {
|
||||
@@ -58,7 +63,7 @@ pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||
}
|
||||
|
||||
pub fn validate_recipe(recipe: &Recipe) -> Result<(), RecipeValidationError> {
|
||||
let recipe_json = serde_json::to_string(recipe).map_err(|err| {
|
||||
let recipe_yaml = serde_yaml::to_string(recipe).map_err(|err| {
|
||||
let message = err.to_string();
|
||||
error!("Failed to serialize recipe for validation: {}", message);
|
||||
RecipeValidationError {
|
||||
@@ -67,7 +72,7 @@ pub fn validate_recipe(recipe: &Recipe) -> Result<(), RecipeValidationError> {
|
||||
}
|
||||
})?;
|
||||
|
||||
validate_recipe_template_from_content(&recipe_json, None).map_err(|err| {
|
||||
validate_recipe_template_from_content(&recipe_yaml, None).map_err(|err| {
|
||||
let message = err.to_string();
|
||||
error!("Recipe validation failed: {}", message);
|
||||
RecipeValidationError {
|
||||
@@ -122,3 +127,48 @@ pub async fn load_recipe_by_id(state: &AppState, id: &str) -> Result<Recipe, Err
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn build_recipe_with_parameter_values(
|
||||
original_recipe: &Recipe,
|
||||
user_recipe_values: HashMap<String, String>,
|
||||
) -> Result<Option<Recipe>> {
|
||||
let recipe_content = serde_yaml::to_string(&original_recipe)?;
|
||||
|
||||
let recipe_dir = get_recipe_library_dir(true);
|
||||
let params = user_recipe_values.into_iter().collect();
|
||||
|
||||
let recipe = match build_recipe_from_template(
|
||||
recipe_content,
|
||||
&recipe_dir,
|
||||
params,
|
||||
None::<fn(&str, &str) -> Result<String, anyhow::Error>>,
|
||||
) {
|
||||
Ok(recipe) => Some(recipe),
|
||||
Err(RecipeError::MissingParams { .. }) => None,
|
||||
Err(e) => return Err(anyhow::anyhow!(e)),
|
||||
};
|
||||
|
||||
Ok(recipe)
|
||||
}
|
||||
|
||||
pub async fn apply_recipe_to_agent(
|
||||
agent: &Arc<Agent>,
|
||||
recipe: &Recipe,
|
||||
include_final_output_tool: bool,
|
||||
) -> Option<String> {
|
||||
if let Some(sub_recipes) = &recipe.sub_recipes {
|
||||
agent.add_sub_recipes(sub_recipes.clone()).await;
|
||||
}
|
||||
|
||||
if include_final_output_tool {
|
||||
if let Some(response) = &recipe.response {
|
||||
agent.add_final_output_tool(response.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
recipe.instructions.as_ref().map(|instructions| {
|
||||
let mut context: HashMap<&str, Value> = HashMap::new();
|
||||
context.insert("recipe_instructions", Value::String(instructions.clone()));
|
||||
render_global_file("desktop_recipe_instruction.md", &context).expect("Prompt should render")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::{apply_recipe_to_agent, build_recipe_with_parameter_values};
|
||||
use crate::state::AppState;
|
||||
use axum::extract::State;
|
||||
use axum::routing::post;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
@@ -6,6 +9,7 @@ use axum::{
|
||||
routing::{delete, get, put},
|
||||
Json, Router,
|
||||
};
|
||||
use goose::recipe::Recipe;
|
||||
use goose::session::session_manager::SessionInsights;
|
||||
use goose::session::{Session, SessionManager};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -34,6 +38,11 @@ pub struct UpdateSessionUserRecipeValuesRequest {
|
||||
user_recipe_values: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct UpdateSessionUserRecipeValuesResponse {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportSessionRequest {
|
||||
@@ -151,10 +160,10 @@ async fn update_session_description(
|
||||
("session_id" = String, Path, description = "Unique identifier for the session")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Session user recipe values updated successfully"),
|
||||
(status = 200, description = "Session user recipe values updated successfully", body = UpdateSessionUserRecipeValuesResponse),
|
||||
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||
(status = 404, description = "Session not found"),
|
||||
(status = 500, description = "Internal server error")
|
||||
(status = 404, description = "Session not found", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
@@ -163,16 +172,54 @@ async fn update_session_description(
|
||||
)]
|
||||
// Update session user recipe parameter values
|
||||
async fn update_session_user_recipe_values(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(request): Json<UpdateSessionUserRecipeValuesRequest>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
) -> Result<Json<UpdateSessionUserRecipeValuesResponse>, ErrorResponse> {
|
||||
SessionManager::update_session(&session_id)
|
||||
.user_recipe_values(Some(request.user_recipe_values))
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
.map_err(|err| ErrorResponse {
|
||||
message: err.to_string(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
let session = SessionManager::get_session(&session_id, false)
|
||||
.await
|
||||
.map_err(|err| ErrorResponse {
|
||||
message: err.to_string(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
})?;
|
||||
let recipe = session.recipe.ok_or_else(|| ErrorResponse {
|
||||
message: "Recipe not found".to_string(),
|
||||
status: StatusCode::NOT_FOUND,
|
||||
})?;
|
||||
|
||||
let user_recipe_values = session.user_recipe_values.unwrap_or_default();
|
||||
match build_recipe_with_parameter_values(&recipe, user_recipe_values).await {
|
||||
Ok(Some(recipe)) => {
|
||||
let agent = state
|
||||
.get_agent_for_route(session_id.clone())
|
||||
.await
|
||||
.map_err(|status| ErrorResponse {
|
||||
message: format!("Failed to get agent: {}", status),
|
||||
status,
|
||||
})?;
|
||||
if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, false).await {
|
||||
agent.extend_system_prompt(prompt).await;
|
||||
}
|
||||
Ok(Json(UpdateSessionUserRecipeValuesResponse { recipe }))
|
||||
}
|
||||
Ok(None) => Err(ErrorResponse {
|
||||
message: "Missing required parameters".to_string(),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
}),
|
||||
Err(e) => Err(ErrorResponse {
|
||||
message: e.to_string(),
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
|
||||
@@ -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