Lifei/UI parameter input (#5222)

This commit is contained in:
Lifei Zhou
2025-10-20 10:05:54 +11:00
committed by GitHub
parent 6c3e07e9c7
commit 8b53a5696e
27 changed files with 717 additions and 1250 deletions
+3 -8
View File
@@ -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,
))
)]
+62 -89
View File
@@ -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)
}
-1
View File
@@ -382,7 +382,6 @@ fn ensure_recipe_valid(recipe: &Recipe) -> Result<(), ErrorResponse> {
status: err.status,
});
}
Ok(())
}
+54 -4
View File
@@ -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")
})
}
+53 -6
View File
@@ -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(