Create / edit recipe form unification and improvements (#4693)

This commit is contained in:
Zane
2025-10-05 09:00:59 -07:00
committed by GitHub
parent ebb3888ff1
commit f4d79404ee
55 changed files with 4486 additions and 3074 deletions
+1 -1
View File
@@ -278,7 +278,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
process::exit(1);
});
// Handle session file resolution and resuming
// Handle session resolution and resuming
let session_id: Option<String> = if session_config.no_session {
None
} else if session_config.resume {
+2 -1
View File
@@ -6,7 +6,6 @@ use goose::config::ExtensionEntry;
use goose::conversation::Conversation;
use goose::permission::permission_confirmation::PrincipalType;
use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata};
use goose::session::{Session, SessionInsights};
use rmcp::model::{
Annotations, Content, EmbeddedResource, Icon, ImageContent, JsonObject, RawAudioContent,
@@ -353,6 +352,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::session::get_session_insights,
super::routes::session::update_session_description,
super::routes::session::delete_session,
super::routes::session::update_session_user_recipe_values,
super::routes::schedule::create_schedule,
super::routes::schedule::list_schedules,
super::routes::schedule::delete_schedule,
@@ -391,6 +391,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::context::ContextManageResponse,
super::routes::session::SessionListResponse,
super::routes::session::UpdateSessionDescriptionRequest,
super::routes::session::UpdateSessionUserRecipeValuesRequest,
Message,
MessageContent,
MessageMetadata,
+32 -21
View File
@@ -4,10 +4,10 @@ use std::sync::Arc;
use axum::routing::get;
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use goose::conversation::{message::Message, Conversation};
use goose::recipe::recipe_library;
use goose::recipe::Recipe;
use goose::recipe_deeplink;
use goose::session::SessionManager;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
@@ -18,16 +18,10 @@ use crate::state::AppState;
#[derive(Debug, Deserialize, ToSchema)]
pub struct CreateRecipeRequest {
messages: Vec<Message>,
// Required metadata
title: String,
description: String,
session_id: String,
// Optional fields
#[serde(default)]
activities: Option<Vec<String>>,
#[serde(default)]
author: Option<AuthorRequest>,
session_id: String,
}
#[derive(Debug, Deserialize, ToSchema)]
@@ -127,25 +121,38 @@ async fn create_recipe(
Json(request): Json<CreateRecipeRequest>,
) -> Result<Json<CreateRecipeResponse>, StatusCode> {
tracing::info!(
"Recipe creation request received with {} messages",
request.messages.len()
"Recipe creation request received for session_id: {}",
request.session_id
);
// Load messages from session
let session = match SessionManager::get_session(&request.session_id, true).await {
Ok(session) => session,
Err(e) => {
tracing::error!("Failed to get session: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
let conversation = match session.conversation {
Some(conversation) => conversation,
None => {
let error_message = "Session has no conversation".to_string();
let error_response = CreateRecipeResponse {
recipe: None,
error: Some(error_message),
};
return Ok(Json(error_response));
}
};
let agent = state.get_agent_for_route(request.session_id).await?;
// Create base recipe from agent state and messages
let recipe_result = agent
.create_recipe(Conversation::new_unvalidated(request.messages))
.await;
let recipe_result = agent.create_recipe(conversation).await;
match recipe_result {
Ok(mut recipe) => {
recipe.title = request.title;
recipe.description = request.description;
if request.activities.is_some() {
recipe.activities = request.activities
};
if let Some(author_req) = request.author {
recipe.author = Some(goose::recipe::Author {
contact: author_req.contact,
@@ -160,7 +167,11 @@ async fn create_recipe(
}
Err(e) => {
tracing::error!("Error details: {:?}", e);
Err(StatusCode::BAD_REQUEST)
let error_response = CreateRecipeResponse {
recipe: None,
error: Some(format!("Failed to create recipe: {}", e)),
};
Ok(Json(error_response))
}
}
}
@@ -241,7 +252,7 @@ async fn scan_recipe(
async fn list_recipes(
State(state): State<Arc<AppState>>,
) -> Result<Json<ListRecipeResponse>, StatusCode> {
let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap();
let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap_or_default();
let mut recipe_file_hash_map = HashMap::new();
let recipe_manifest_responses = recipe_manifest_with_paths
.iter()
+44
View File
@@ -8,6 +8,7 @@ use axum::{
use goose::session::session_manager::SessionInsights;
use goose::session::{Session, SessionManager};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use utoipa::ToSchema;
@@ -25,6 +26,13 @@ pub struct UpdateSessionDescriptionRequest {
description: String,
}
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSessionUserRecipeValuesRequest {
/// Recipe parameter values entered by the user
user_recipe_values: HashMap<String, String>,
}
const MAX_DESCRIPTION_LENGTH: usize = 200;
#[utoipa::path(
@@ -128,6 +136,38 @@ async fn update_session_description(
Ok(StatusCode::OK)
}
#[utoipa::path(
put,
path = "/sessions/{session_id}/user_recipe_values",
request_body = UpdateSessionUserRecipeValuesRequest,
params(
("session_id" = String, Path, description = "Unique identifier for the session")
),
responses(
(status = 200, description = "Session user recipe values updated successfully"),
(status = 401, description = "Unauthorized - Invalid or missing API key"),
(status = 404, description = "Session not found"),
(status = 500, description = "Internal server error")
),
security(
("api_key" = [])
),
tag = "Session Management"
)]
// Update session user recipe parameter values
async fn update_session_user_recipe_values(
Path(session_id): Path<String>,
Json(request): Json<UpdateSessionUserRecipeValuesRequest>,
) -> Result<StatusCode, StatusCode> {
SessionManager::update_session(&session_id)
.user_recipe_values(Some(request.user_recipe_values))
.apply()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::OK)
}
#[utoipa::path(
delete,
path = "/sessions/{session_id}",
@@ -169,5 +209,9 @@ pub fn routes(state: Arc<AppState>) -> Router {
"/sessions/{session_id}/description",
put(update_session_description),
)
.route(
"/sessions/{session_id}/user_recipe_values",
put(update_session_user_recipe_values),
)
.with_state(state)
}
+24 -2
View File
@@ -1609,9 +1609,31 @@ impl Agent {
extension_configs.len()
);
let (title, description) =
if let Ok(json_content) = serde_json::from_str::<Value>(&clean_content) {
let title = json_content
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("Custom recipe from chat")
.to_string();
let description = json_content
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("a custom recipe instance from this chat session")
.to_string();
(title, description)
} else {
(
"Custom recipe from chat".to_string(),
"a custom recipe instance from this chat session".to_string(),
)
};
let recipe = Recipe::builder()
.title("Custom recipe from chat")
.description("a custom recipe instance from this chat session")
.title(title)
.description(description)
.instructions(instructions)
.activities(activities)
.extensions(extension_configs)
@@ -330,6 +330,7 @@ mod tests {
extension_data: extension_data::ExtensionData::new(),
conversation: Some(conversation),
message_count,
user_recipe_values: None,
}
}
+7 -4
View File
@@ -1,13 +1,16 @@
Based on our conversation so far, could you create:
1. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with. Make the instructions generic, and higher-level so that can be re-used across various similar tasks. Pay special attention if any output styles or formats are requested (and make it clear), and note any non standard tools used or required.
1. A concise title (5-10 words) that captures the main topic or task
2. A brief description (1-2 sentences) that summarizes what this recipe helps with
3. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with. Make the instructions generic, and higher-level so that can be re-used across various similar tasks. Pay special attention if any output styles or formats are requested (and make it clear), and note any non standard tools used or required.
4. A list of 3-5 example activities (as a few words each at most) that would be relevant to this topic
2. A list of 3-5 example activities (as a few words each at most) that would be relevant to this topic
Format your response in _VALID_ json, with one key being `instructions` which contains a string, and the other key `activities` as an array of strings.
Format your response in _VALID_ json, with keys being `title`, `description`, `instructions` (string), and `activities` (array of strings).
For example, perhaps we have been discussing fruit and you might write:
{
"title": "Fruit Information Assistant",
"description": "A recipe for finding and sharing information about different types of fruit.",
"instructions": "Using web searches we find pictures of fruit, and always check what language to reply in.",
"activities": [
"Show pics of apples",
+47 -6
View File
@@ -11,6 +11,7 @@ use rmcp::model::Role;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -18,7 +19,7 @@ use tokio::sync::OnceCell;
use tracing::{info, warn};
use utoipa::ToSchema;
const CURRENT_SCHEMA_VERSION: i32 = 1;
const CURRENT_SCHEMA_VERSION: i32 = 2;
static SESSION_STORAGE: OnceCell<Arc<SessionStorage>> = OnceCell::const_new();
@@ -39,6 +40,7 @@ pub struct Session {
pub accumulated_output_tokens: Option<i32>,
pub schedule_id: Option<String>,
pub recipe: Option<Recipe>,
pub user_recipe_values: Option<HashMap<String, String>>,
pub conversation: Option<Conversation>,
pub message_count: usize,
}
@@ -56,6 +58,7 @@ pub struct SessionUpdateBuilder {
accumulated_output_tokens: Option<Option<i32>>,
schedule_id: Option<Option<String>>,
recipe: Option<Option<Recipe>>,
user_recipe_values: Option<Option<HashMap<String, String>>>,
}
#[derive(Serialize, ToSchema, Debug)]
@@ -82,6 +85,7 @@ impl SessionUpdateBuilder {
accumulated_output_tokens: None,
schedule_id: None,
recipe: None,
user_recipe_values: None,
}
}
@@ -140,6 +144,14 @@ impl SessionUpdateBuilder {
self
}
pub fn user_recipe_values(
mut self,
user_recipe_values: Option<HashMap<String, String>>,
) -> Self {
self.user_recipe_values = Some(user_recipe_values);
self
}
pub async fn apply(self) -> Result<()> {
SessionManager::apply_update(self).await
}
@@ -265,6 +277,7 @@ impl Default for Session {
accumulated_output_tokens: None,
schedule_id: None,
recipe: None,
user_recipe_values: None,
conversation: None,
message_count: 0,
}
@@ -285,6 +298,10 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
let recipe_json: Option<String> = row.try_get("recipe_json")?;
let recipe = recipe_json.and_then(|json| serde_json::from_str(&json).ok());
let user_recipe_values_json: Option<String> = row.try_get("user_recipe_values_json")?;
let user_recipe_values =
user_recipe_values_json.and_then(|json| serde_json::from_str(&json).ok());
Ok(Session {
id: row.try_get("id")?,
working_dir: PathBuf::from(row.try_get::<String, _>("working_dir")?),
@@ -301,6 +318,7 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
accumulated_output_tokens: row.try_get("accumulated_output_tokens")?,
schedule_id: row.try_get("schedule_id")?,
recipe,
user_recipe_values,
conversation: None,
message_count: row.try_get("message_count").unwrap_or(0) as usize,
})
@@ -386,7 +404,8 @@ impl SessionStorage {
accumulated_input_tokens INTEGER,
accumulated_output_tokens INTEGER,
schedule_id TEXT,
recipe_json TEXT
recipe_json TEXT,
user_recipe_values_json TEXT
)
"#,
)
@@ -472,14 +491,19 @@ impl SessionStorage {
None => None,
};
let user_recipe_values_json = match &session.user_recipe_values {
Some(user_recipe_values) => Some(serde_json::to_string(user_recipe_values)?),
None => None,
};
sqlx::query(
r#"
INSERT INTO sessions (
id, description, working_dir, created_at, updated_at, extension_data,
total_tokens, input_tokens, output_tokens,
accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens,
schedule_id, recipe_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
schedule_id, recipe_json, user_recipe_values_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(&session.id)
@@ -496,6 +520,7 @@ impl SessionStorage {
.bind(session.accumulated_output_tokens)
.bind(&session.schedule_id)
.bind(recipe_json)
.bind(user_recipe_values_json)
.execute(&self.pool)
.await?;
@@ -572,6 +597,15 @@ impl SessionStorage {
.execute(&self.pool)
.await?;
}
2 => {
sqlx::query(
r#"
ALTER TABLE sessions ADD COLUMN user_recipe_values_json TEXT
"#,
)
.execute(&self.pool)
.await?;
}
_ => {
anyhow::bail!("Unknown migration version: {}", version);
}
@@ -612,7 +646,7 @@ impl SessionStorage {
SELECT id, working_dir, description, created_at, updated_at, extension_data,
total_tokens, input_tokens, output_tokens,
accumulated_total_tokens, accumulated_input_tokens, accumulated_output_tokens,
schedule_id, recipe_json
schedule_id, recipe_json, user_recipe_values_json
FROM sessions
WHERE id = ?
"#,
@@ -669,6 +703,7 @@ impl SessionStorage {
);
add_update!(builder.schedule_id, "schedule_id");
add_update!(builder.recipe, "recipe_json");
add_update!(builder.user_recipe_values, "user_recipe_values_json");
if updates.is_empty() {
return Ok(());
@@ -715,6 +750,12 @@ impl SessionStorage {
let recipe_json = recipe.map(|r| serde_json::to_string(&r)).transpose()?;
q = q.bind(recipe_json);
}
if let Some(user_recipe_values) = builder.user_recipe_values {
let user_recipe_values_json = user_recipe_values
.map(|urv| serde_json::to_string(&urv))
.transpose()?;
q = q.bind(user_recipe_values_json);
}
q = q.bind(&builder.session_id);
q.execute(&self.pool).await?;
@@ -805,7 +846,7 @@ impl SessionStorage {
SELECT s.id, s.working_dir, s.description, s.created_at, s.updated_at, s.extension_data,
s.total_tokens, s.input_tokens, s.output_tokens,
s.accumulated_total_tokens, s.accumulated_input_tokens, s.accumulated_output_tokens,
s.schedule_id, s.recipe_json,
s.schedule_id, s.recipe_json, s.user_recipe_values_json,
COUNT(m.id) as message_count
FROM sessions s
INNER JOIN messages m ON s.id = m.session_id
+1
View File
@@ -395,5 +395,6 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) ->
updated_at: Default::default(),
conversation: None,
message_count,
user_recipe_values: None,
}
}