Lifei/UI parameter input (#5222)
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user