Create / edit recipe form unification and improvements (#4693)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user