Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 15 additions & 23 deletions crates/goose-cli/src/recipes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -132,23 +127,20 @@ 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(
&params,
recipe_parameters,
&recipe_dir_str,
None::<fn(&str, &str) -> Result<String>>,
)?;
let recipe = render_recipe_for_preview(
recipe_file_content,
Some(recipe_dir_str.clone()),
&params_for_template,
)?;
print_recipe_explanation(&recipe);
print_recipe_explanation(&recipe_template);
print_required_parameters_for_template(params_for_template, missing_params);

Ok(())
Expand Down
11 changes: 3 additions & 8 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
))
)]
Expand Down
151 changes: 62 additions & 89 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -9,57 +11,37 @@ 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,
model: Option<String>,
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>,
Expand Down Expand Up @@ -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) => {
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
}
1 change: 0 additions & 1 deletion crates/goose-server/src/routes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,6 @@ fn ensure_recipe_valid(recipe: &Recipe) -> Result<(), ErrorResponse> {
status: err.status,
});
}

Ok(())
}

Expand Down
Loading
Loading