User configurable templates (#6420)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-01-20 17:15:52 -05:00
committed by GitHub
parent 5a04ee8700
commit 2f083b827a
23 changed files with 1052 additions and 307 deletions
+8
View File
@@ -353,6 +353,10 @@ derive_utoipa!(Icon as IconSchema);
super::routes::config_management::check_provider,
super::routes::config_management::set_config_provider,
super::routes::config_management::get_pricing,
super::routes::prompts::get_prompts,
super::routes::prompts::get_prompt,
super::routes::prompts::save_prompt,
super::routes::prompts::reset_prompt,
super::routes::agent::start_agent,
super::routes::agent::resume_agent,
super::routes::agent::stop_agent,
@@ -427,6 +431,10 @@ derive_utoipa!(Icon as IconSchema);
super::routes::config_management::PricingQuery,
super::routes::config_management::PricingResponse,
super::routes::config_management::PricingData,
super::routes::prompts::PromptsListResponse,
super::routes::prompts::PromptContentResponse,
super::routes::prompts::SavePromptRequest,
goose::prompt_template::Template,
super::routes::action_required::ConfirmToolActionRequest,
super::routes::reply::ChatRequest,
super::routes::session::ImportSessionRequest,
+3 -3
View File
@@ -18,7 +18,7 @@ use goose::agents::ExtensionConfig;
use goose::config::resolve_extensions_for_new_session;
use goose::config::{Config, GooseMode};
use goose::model::ModelConfig;
use goose::prompt_template::render_global_file;
use goose::prompt_template::render_template;
use goose::providers::create;
use goose::recipe::Recipe;
use goose::recipe_deeplink;
@@ -418,7 +418,7 @@ async fn update_from_session(
})?;
let context: HashMap<&str, Value> = HashMap::new();
let desktop_prompt =
render_global_file("desktop_prompt.md", &context).expect("Prompt should render");
render_template("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(
@@ -691,7 +691,7 @@ async fn restart_agent_internal(
let context: HashMap<&str, Value> = HashMap::new();
let desktop_prompt =
render_global_file("desktop_prompt.md", &context).expect("Prompt should render");
render_template("desktop_prompt.md", &context).expect("Prompt should render");
let mut update_prompt = desktop_prompt;
if let Some(ref recipe) = session.recipe {
+2
View File
@@ -5,6 +5,7 @@ pub mod config_management;
pub mod errors;
pub mod mcp_app_proxy;
pub mod mcp_ui_proxy;
pub mod prompts;
pub mod recipe;
pub mod recipe_utils;
pub mod reply;
@@ -29,6 +30,7 @@ pub fn configure(state: Arc<crate::state::AppState>, secret_key: String) -> Rout
.merge(agent::routes(state.clone()))
.merge(audio::routes(state.clone()))
.merge(config_management::routes(state.clone()))
.merge(prompts::routes())
.merge(recipe::routes(state.clone()))
.merge(session::routes(state.clone()))
.merge(schedule::routes(state.clone()))
+133
View File
@@ -0,0 +1,133 @@
use axum::{
extract::Path,
routing::{delete, get, put},
Json, Router,
};
use goose::prompt_template::{
get_template, list_templates, reset_template, save_template, Template,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
pub struct PromptsListResponse {
pub prompts: Vec<Template>,
}
#[derive(Serialize, ToSchema)]
pub struct PromptContentResponse {
pub name: String,
pub content: String,
pub default_content: String,
pub is_customized: bool,
}
#[derive(Deserialize, ToSchema)]
pub struct SavePromptRequest {
pub content: String,
}
#[utoipa::path(
get,
path = "/config/prompts",
responses(
(status = 200, description = "List of all available prompts", body = PromptsListResponse)
)
)]
pub async fn get_prompts() -> Json<PromptsListResponse> {
Json(PromptsListResponse {
prompts: list_templates(),
})
}
#[utoipa::path(
get,
path = "/config/prompts/{name}",
params(
("name" = String, Path, description = "Prompt template name (e.g., system.md)")
),
responses(
(status = 200, description = "Prompt content retrieved successfully", body = PromptContentResponse),
(status = 404, description = "Prompt not found")
)
)]
pub async fn get_prompt(
Path(name): Path<String>,
) -> Result<Json<PromptContentResponse>, StatusCode> {
let template = get_template(&name).ok_or(StatusCode::NOT_FOUND)?;
let content = template
.user_content
.as_ref()
.unwrap_or(&template.default_content);
Ok(Json(PromptContentResponse {
name: template.name,
content: content.clone(),
default_content: template.default_content,
is_customized: template.is_customized,
}))
}
#[utoipa::path(
put,
path = "/config/prompts/{name}",
params(
("name" = String, Path, description = "Prompt template name (e.g., system.md)")
),
request_body = SavePromptRequest,
responses(
(status = 200, description = "Prompt saved successfully", body = String),
(status = 404, description = "Prompt not found"),
(status = 500, description = "Failed to save prompt")
)
)]
pub async fn save_prompt(
Path(name): Path<String>,
Json(request): Json<SavePromptRequest>,
) -> Result<Json<String>, StatusCode> {
save_template(&name, &request.content).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StatusCode::NOT_FOUND
} else {
tracing::error!("Failed to save prompt {}: {}", name, e);
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
Ok(Json(format!("Saved prompt: {}", name)))
}
#[utoipa::path(
delete,
path = "/config/prompts/{name}",
params(
("name" = String, Path, description = "Prompt template name (e.g., system.md)")
),
responses(
(status = 200, description = "Prompt reset to default successfully", body = String),
(status = 404, description = "Prompt not found"),
(status = 500, description = "Failed to reset prompt")
)
)]
pub async fn reset_prompt(Path(name): Path<String>) -> Result<Json<String>, StatusCode> {
reset_template(&name).map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
StatusCode::NOT_FOUND
} else {
tracing::error!("Failed to reset prompt {}: {}", name, e);
StatusCode::INTERNAL_SERVER_ERROR
}
})?;
Ok(Json(format!("Reset prompt to default: {}", name)))
}
pub fn routes() -> Router {
Router::new()
.route("/config/prompts", get(get_prompts))
.route("/config/prompts/{name}", get(get_prompt))
.route("/config/prompts/{name}", put(save_prompt))
.route("/config/prompts/{name}", delete(reset_prompt))
}
@@ -10,7 +10,7 @@ use crate::state::AppState;
use anyhow::Result;
use axum::http::StatusCode;
use goose::agents::Agent;
use goose::prompt_template::render_global_file;
use goose::prompt_template::render_template;
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;
@@ -173,6 +173,6 @@ pub async fn apply_recipe_to_agent(
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")
render_template("desktop_recipe_instruction.md", &context).expect("Prompt should render")
})
}