Lifei/create save recipe to file (#4895)
This commit is contained in:
@@ -368,6 +368,8 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::recipe::scan_recipe,
|
super::routes::recipe::scan_recipe,
|
||||||
super::routes::recipe::list_recipes,
|
super::routes::recipe::list_recipes,
|
||||||
super::routes::recipe::delete_recipe,
|
super::routes::recipe::delete_recipe,
|
||||||
|
super::routes::recipe::save_recipe,
|
||||||
|
super::routes::recipe::parse_recipe,
|
||||||
super::routes::setup::start_openrouter_setup,
|
super::routes::setup::start_openrouter_setup,
|
||||||
super::routes::setup::start_tetrate_setup,
|
super::routes::setup::start_tetrate_setup,
|
||||||
),
|
),
|
||||||
@@ -448,6 +450,10 @@ derive_utoipa!(Icon as IconSchema);
|
|||||||
super::routes::recipe::RecipeManifestResponse,
|
super::routes::recipe::RecipeManifestResponse,
|
||||||
super::routes::recipe::ListRecipeResponse,
|
super::routes::recipe::ListRecipeResponse,
|
||||||
super::routes::recipe::DeleteRecipeRequest,
|
super::routes::recipe::DeleteRecipeRequest,
|
||||||
|
super::routes::recipe::SaveRecipeRequest,
|
||||||
|
super::routes::errors::ErrorResponse,
|
||||||
|
super::routes::recipe::ParseRecipeRequest,
|
||||||
|
super::routes::recipe::ParseRecipeResponse,
|
||||||
goose::recipe::Recipe,
|
goose::recipe::Recipe,
|
||||||
goose::recipe::Author,
|
goose::recipe::Author,
|
||||||
goose::recipe::Settings,
|
goose::recipe::Settings,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct ErrorResponse {
|
||||||
|
pub message: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub status: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ErrorResponse {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let body = Json(serde_json::json!({
|
||||||
|
"message": self.message,
|
||||||
|
}));
|
||||||
|
|
||||||
|
(self.status, body).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod agent;
|
|||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod config_management;
|
pub mod config_management;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
pub mod errors;
|
||||||
pub mod extension;
|
pub mod extension;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod recipe;
|
pub mod recipe;
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ use std::sync::Arc;
|
|||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
|
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
|
||||||
use goose::conversation::{message::Message, Conversation};
|
use goose::conversation::{message::Message, Conversation};
|
||||||
|
use goose::recipe::recipe_library;
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
use goose::recipe_deeplink;
|
use goose::recipe_deeplink;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::routes::errors::ErrorResponse;
|
||||||
use crate::routes::recipe_utils::get_all_recipes_manifests;
|
use crate::routes::recipe_utils::get_all_recipes_manifests;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
@@ -72,11 +74,25 @@ pub struct ScanRecipeResponse {
|
|||||||
has_security_warnings: bool,
|
has_security_warnings: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct SaveRecipeRequest {
|
||||||
|
recipe: Recipe,
|
||||||
|
id: Option<String>,
|
||||||
|
is_global: Option<bool>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
|
pub struct ParseRecipeRequest {
|
||||||
|
pub content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
|
pub struct ParseRecipeResponse {
|
||||||
|
pub recipe: Recipe,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, ToSchema)]
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
pub struct RecipeManifestResponse {
|
pub struct RecipeManifestResponse {
|
||||||
name: String,
|
name: String,
|
||||||
#[serde(rename = "isGlobal")]
|
|
||||||
is_global: bool,
|
|
||||||
recipe: Recipe,
|
recipe: Recipe,
|
||||||
#[serde(rename = "lastModified")]
|
#[serde(rename = "lastModified")]
|
||||||
last_modified: String,
|
last_modified: String,
|
||||||
@@ -235,7 +251,6 @@ async fn list_recipes(
|
|||||||
recipe_file_hash_map.insert(id.clone(), file_path);
|
recipe_file_hash_map.insert(id.clone(), file_path);
|
||||||
RecipeManifestResponse {
|
RecipeManifestResponse {
|
||||||
name: recipe_manifest_with_path.name.clone(),
|
name: recipe_manifest_with_path.name.clone(),
|
||||||
is_global: recipe_manifest_with_path.is_global,
|
|
||||||
recipe: recipe_manifest_with_path.recipe.clone(),
|
recipe: recipe_manifest_with_path.recipe.clone(),
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
last_modified: recipe_manifest_with_path.last_modified.clone(),
|
last_modified: recipe_manifest_with_path.last_modified.clone(),
|
||||||
@@ -278,6 +293,57 @@ async fn delete_recipe(
|
|||||||
StatusCode::NO_CONTENT
|
StatusCode::NO_CONTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/recipes/save",
|
||||||
|
request_body = SaveRecipeRequest,
|
||||||
|
responses(
|
||||||
|
(status = 204, description = "Recipe saved to file successfully"),
|
||||||
|
(status = 401, description = "Unauthorized - Invalid or missing API key"),
|
||||||
|
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||||
|
),
|
||||||
|
tag = "Recipe Management"
|
||||||
|
)]
|
||||||
|
async fn save_recipe(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(request): Json<SaveRecipeRequest>,
|
||||||
|
) -> Result<StatusCode, ErrorResponse> {
|
||||||
|
let file_path = match request.id {
|
||||||
|
Some(id) => state.recipe_file_hash_map.lock().await.get(&id).cloned(),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match recipe_library::save_recipe_to_file(request.recipe, request.is_global, file_path) {
|
||||||
|
Ok(_) => Ok(StatusCode::NO_CONTENT),
|
||||||
|
Err(e) => Err(ErrorResponse {
|
||||||
|
message: e.to_string(),
|
||||||
|
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/recipes/parse",
|
||||||
|
request_body = ParseRecipeRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Recipe parsed successfully", body = ParseRecipeResponse),
|
||||||
|
(status = 400, description = "Bad request - Invalid recipe format", body = ErrorResponse),
|
||||||
|
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||||
|
),
|
||||||
|
tag = "Recipe Management"
|
||||||
|
)]
|
||||||
|
async fn parse_recipe(
|
||||||
|
Json(request): Json<ParseRecipeRequest>,
|
||||||
|
) -> Result<Json<ParseRecipeResponse>, ErrorResponse> {
|
||||||
|
let recipe = Recipe::from_content(&request.content).map_err(|e| ErrorResponse {
|
||||||
|
message: format!("Invalid recipe format: {}", e),
|
||||||
|
status: StatusCode::BAD_REQUEST,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Json(ParseRecipeResponse { recipe }))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes(state: Arc<AppState>) -> Router {
|
pub fn routes(state: Arc<AppState>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/recipes/create", post(create_recipe))
|
.route("/recipes/create", post(create_recipe))
|
||||||
@@ -286,6 +352,8 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
|||||||
.route("/recipes/scan", post(scan_recipe))
|
.route("/recipes/scan", post(scan_recipe))
|
||||||
.route("/recipes/list", get(list_recipes))
|
.route("/recipes/list", get(list_recipes))
|
||||||
.route("/recipes/delete", post(delete_recipe))
|
.route("/recipes/delete", post(delete_recipe))
|
||||||
|
.route("/recipes/save", post(save_recipe))
|
||||||
|
.route("/recipes/parse", post(parse_recipe))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ use std::hash::{Hash, Hasher};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use etcetera::{choose_app_strategy, AppStrategy};
|
|
||||||
|
|
||||||
use goose::config::APP_STRATEGY;
|
use goose::recipe::recipe_library::list_all_recipes_from_library;
|
||||||
use goose::recipe::read_recipe_file_content::read_recipe_file;
|
|
||||||
use goose::recipe::Recipe;
|
use goose::recipe::Recipe;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
@@ -18,7 +16,6 @@ use utoipa::ToSchema;
|
|||||||
pub struct RecipeManifestWithPath {
|
pub struct RecipeManifestWithPath {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub is_global: bool,
|
|
||||||
pub recipe: Recipe,
|
pub recipe: Recipe,
|
||||||
pub file_path: PathBuf,
|
pub file_path: PathBuf,
|
||||||
pub last_modified: String,
|
pub last_modified: String,
|
||||||
@@ -31,59 +28,31 @@ fn short_id_from_path(path: &str) -> String {
|
|||||||
format!("{:016x}", h)
|
format!("{:016x}", h)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_recipes_from_path(path: &PathBuf, is_global: bool) -> Result<Vec<RecipeManifestWithPath>> {
|
|
||||||
let mut recipe_manifests_with_path = Vec::new();
|
|
||||||
if path.exists() {
|
|
||||||
for entry in fs::read_dir(path)? {
|
|
||||||
let path = entry?.path();
|
|
||||||
if path.extension() == Some("yaml".as_ref()) {
|
|
||||||
let Ok(recipe_file) = read_recipe_file(path.clone()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(recipe) = Recipe::from_content(&recipe_file.content) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let Ok(last_modified) = fs::metadata(path.clone()).map(|m| {
|
|
||||||
chrono::DateTime::<chrono::Utc>::from(m.modified().unwrap()).to_rfc3339()
|
|
||||||
}) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let recipe_metadata =
|
|
||||||
RecipeManifestMetadata::from_yaml_file(&path).unwrap_or_else(|_| {
|
|
||||||
RecipeManifestMetadata {
|
|
||||||
name: recipe.title.clone(),
|
|
||||||
is_global,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let manifest_with_path = RecipeManifestWithPath {
|
|
||||||
id: short_id_from_path(recipe_file.file_path.to_string_lossy().as_ref()),
|
|
||||||
name: recipe_metadata.name,
|
|
||||||
is_global: recipe_metadata.is_global,
|
|
||||||
recipe,
|
|
||||||
file_path: recipe_file.file_path,
|
|
||||||
last_modified,
|
|
||||||
};
|
|
||||||
recipe_manifests_with_path.push(manifest_with_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(recipe_manifests_with_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||||
let current_dir = std::env::current_dir()?;
|
let recipes_with_path = list_all_recipes_from_library()?;
|
||||||
let local_recipe_path = current_dir.join(".goose/recipes");
|
|
||||||
|
|
||||||
let global_recipe_path = choose_app_strategy(APP_STRATEGY.clone())
|
|
||||||
.expect("goose requires a home dir")
|
|
||||||
.config_dir()
|
|
||||||
.join("recipes");
|
|
||||||
|
|
||||||
let mut recipe_manifests_with_path = Vec::new();
|
let mut recipe_manifests_with_path = Vec::new();
|
||||||
|
for (file_path, recipe) in recipes_with_path {
|
||||||
|
let Ok(last_modified) = fs::metadata(file_path.clone())
|
||||||
|
.map(|m| chrono::DateTime::<chrono::Utc>::from(m.modified().unwrap()).to_rfc3339())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let recipe_metadata =
|
||||||
|
RecipeManifestMetadata::from_yaml_file(&file_path).unwrap_or_else(|_| {
|
||||||
|
RecipeManifestMetadata {
|
||||||
|
name: recipe.title.clone(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
recipe_manifests_with_path.extend(load_recipes_from_path(&local_recipe_path, false)?);
|
let manifest_with_path = RecipeManifestWithPath {
|
||||||
recipe_manifests_with_path.extend(load_recipes_from_path(&global_recipe_path, true)?);
|
id: short_id_from_path(file_path.to_string_lossy().as_ref()),
|
||||||
|
name: recipe_metadata.name,
|
||||||
|
recipe,
|
||||||
|
file_path,
|
||||||
|
last_modified,
|
||||||
|
};
|
||||||
|
recipe_manifests_with_path.push(manifest_with_path);
|
||||||
|
}
|
||||||
recipe_manifests_with_path.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
|
recipe_manifests_with_path.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
|
||||||
|
|
||||||
Ok(recipe_manifests_with_path)
|
Ok(recipe_manifests_with_path)
|
||||||
@@ -93,8 +62,6 @@ pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
|||||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||||
struct RecipeManifestMetadata {
|
struct RecipeManifestMetadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(rename = "isGlobal")]
|
|
||||||
pub is_global: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecipeManifestMetadata {
|
impl RecipeManifestMetadata {
|
||||||
@@ -129,6 +96,5 @@ recipe: recipe_content
|
|||||||
let result = RecipeManifestMetadata::from_yaml_file(&file_path).unwrap();
|
let result = RecipeManifestMetadata::from_yaml_file(&file_path).unwrap();
|
||||||
|
|
||||||
assert_eq!(result.name, "Test Recipe");
|
assert_eq!(result.name, "Test Recipe");
|
||||||
assert!(result.is_global);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use utoipa::ToSchema;
|
|||||||
|
|
||||||
pub mod build_recipe;
|
pub mod build_recipe;
|
||||||
pub mod read_recipe_file_content;
|
pub mod read_recipe_file_content;
|
||||||
|
pub mod recipe_library;
|
||||||
pub mod template_recipe;
|
pub mod template_recipe;
|
||||||
|
|
||||||
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
|
pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
use crate::config::APP_STRATEGY;
|
||||||
|
use crate::recipe::read_recipe_file_content::read_recipe_file;
|
||||||
|
use crate::recipe::Recipe;
|
||||||
|
use anyhow::Result;
|
||||||
|
use etcetera::{choose_app_strategy, AppStrategy};
|
||||||
|
use serde_yaml;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn get_recipe_library_dir(is_global: bool) -> PathBuf {
|
||||||
|
if is_global {
|
||||||
|
choose_app_strategy(APP_STRATEGY.clone())
|
||||||
|
.expect("goose requires a home dir")
|
||||||
|
.config_dir()
|
||||||
|
.join("recipes")
|
||||||
|
} else {
|
||||||
|
std::env::current_dir().unwrap().join(".goose/recipes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_recipes_from_library(is_global: bool) -> Result<Vec<(PathBuf, Recipe)>> {
|
||||||
|
let path = get_recipe_library_dir(is_global);
|
||||||
|
let mut recipes_with_path = Vec::new();
|
||||||
|
if path.exists() {
|
||||||
|
for entry in fs::read_dir(path)? {
|
||||||
|
let path = entry?.path();
|
||||||
|
let extension = path.extension();
|
||||||
|
|
||||||
|
if extension == Some("yaml".as_ref()) || extension == Some("json".as_ref()) {
|
||||||
|
let Ok(recipe_file) = read_recipe_file(path.clone()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(recipe) = Recipe::from_content(&recipe_file.content) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
recipes_with_path.push((path, recipe));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(recipes_with_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_all_recipes_from_library() -> Result<Vec<(PathBuf, Recipe)>> {
|
||||||
|
let mut recipes_with_path = Vec::new();
|
||||||
|
recipes_with_path.extend(list_recipes_from_library(true)?);
|
||||||
|
recipes_with_path.extend(list_recipes_from_library(false)?);
|
||||||
|
Ok(recipes_with_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_recipe_filename(title: &str) -> String {
|
||||||
|
let base_name = title
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphanumeric() || c.is_whitespace() || *c == '-')
|
||||||
|
.collect::<String>()
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
let filename = if base_name.is_empty() {
|
||||||
|
"untitled-recipe".to_string()
|
||||||
|
} else {
|
||||||
|
base_name
|
||||||
|
};
|
||||||
|
format!("{}.yaml", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_recipe_to_file(
|
||||||
|
recipe: Recipe,
|
||||||
|
is_global: Option<bool>,
|
||||||
|
file_path: Option<PathBuf>,
|
||||||
|
) -> anyhow::Result<PathBuf> {
|
||||||
|
let is_global_value = is_global.unwrap_or(true);
|
||||||
|
|
||||||
|
let default_file_path =
|
||||||
|
get_recipe_library_dir(is_global_value).join(generate_recipe_filename(&recipe.title));
|
||||||
|
|
||||||
|
let file_path_value = match file_path {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
if default_file_path.exists() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Recipe file already exists at: {:?}",
|
||||||
|
default_file_path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
default_file_path
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let all_recipes = list_all_recipes_from_library()?;
|
||||||
|
|
||||||
|
for (existing_path, existing_recipe) in &all_recipes {
|
||||||
|
if existing_recipe.title == recipe.title && existing_path != &file_path_value {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Recipe with title '{}' already exists",
|
||||||
|
recipe.title
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let yaml_content = serde_yaml::to_string(&recipe)?;
|
||||||
|
fs::write(&file_path_value, yaml_content)?;
|
||||||
|
Ok(file_path_value)
|
||||||
|
}
|
||||||
+127
-4
@@ -1090,6 +1090,92 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/recipes/parse": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Recipe Management"
|
||||||
|
],
|
||||||
|
"operationId": "parse_recipe",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ParseRecipeRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Recipe parsed successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ParseRecipeResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid recipe format",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/recipes/save": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Recipe Management"
|
||||||
|
],
|
||||||
|
"operationId": "save_recipe",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SaveRecipeRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Recipe saved to file successfully"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - Invalid or missing API key"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/recipes/scan": {
|
"/recipes/scan": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -2984,6 +3070,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ParseRecipeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"content"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ParseRecipeResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"recipe"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"recipe": {
|
||||||
|
"$ref": "#/components/schemas/Recipe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"PermissionConfirmationRequest": {
|
"PermissionConfirmationRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3300,7 +3408,6 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"isGlobal",
|
|
||||||
"recipe",
|
"recipe",
|
||||||
"lastModified",
|
"lastModified",
|
||||||
"id"
|
"id"
|
||||||
@@ -3309,9 +3416,6 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isGlobal": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"lastModified": {
|
"lastModified": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -3517,6 +3621,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SaveRecipeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"recipe"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"is_global": {
|
||||||
|
"type": "boolean",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"recipe": {
|
||||||
|
"$ref": "#/components/schemas/Recipe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ScanRecipeRequest": {
|
"ScanRecipeRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Options as ClientOptions, TDataShape, Client } from './client';
|
import type { Options as ClientOptions, TDataShape, Client } from './client';
|
||||||
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionInsightsData, GetSessionInsightsResponses, GetSessionInsightsErrors, DeleteSessionData, DeleteSessionResponses, DeleteSessionErrors, GetSessionData, GetSessionResponses, GetSessionErrors, UpdateSessionDescriptionData, UpdateSessionDescriptionResponses, UpdateSessionDescriptionErrors, StatusData, StatusResponses } from './types.gen';
|
import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, ResumeAgentData, ResumeAgentResponses, ResumeAgentErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, StartAgentData, StartAgentResponses, StartAgentErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, GetProviderModelsData, GetProviderModelsResponses, GetProviderModelsErrors, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, DeleteRecipeData, DeleteRecipeResponses, DeleteRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ListRecipesData, ListRecipesResponses, ListRecipesErrors, ParseRecipeData, ParseRecipeResponses, ParseRecipeErrors, SaveRecipeData, SaveRecipeResponses, SaveRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionInsightsData, GetSessionInsightsResponses, GetSessionInsightsErrors, DeleteSessionData, DeleteSessionResponses, DeleteSessionErrors, GetSessionData, GetSessionResponses, GetSessionErrors, UpdateSessionDescriptionData, UpdateSessionDescriptionResponses, UpdateSessionDescriptionErrors, StatusData, StatusResponses } from './types.gen';
|
||||||
import { client as _heyApiClient } from './client.gen';
|
import { client as _heyApiClient } from './client.gen';
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||||
@@ -328,6 +328,28 @@ export const listRecipes = <ThrowOnError extends boolean = false>(options?: Opti
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseRecipe = <ThrowOnError extends boolean = false>(options: Options<ParseRecipeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<ParseRecipeResponses, ParseRecipeErrors, ThrowOnError>({
|
||||||
|
url: '/recipes/parse',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveRecipe = <ThrowOnError extends boolean = false>(options: Options<SaveRecipeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? _heyApiClient).post<SaveRecipeResponses, SaveRecipeErrors, ThrowOnError>({
|
||||||
|
url: '/recipes/save',
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const scanRecipe = <ThrowOnError extends boolean = false>(options: Options<ScanRecipeData, ThrowOnError>) => {
|
export const scanRecipe = <ThrowOnError extends boolean = false>(options: Options<ScanRecipeData, ThrowOnError>) => {
|
||||||
return (options.client ?? _heyApiClient).post<ScanRecipeResponses, unknown, ThrowOnError>({
|
return (options.client ?? _heyApiClient).post<ScanRecipeResponses, unknown, ThrowOnError>({
|
||||||
url: '/recipes/scan',
|
url: '/recipes/scan',
|
||||||
|
|||||||
@@ -448,6 +448,14 @@ export type ModelInfo = {
|
|||||||
supports_cache_control?: boolean | null;
|
supports_cache_control?: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ParseRecipeRequest = {
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseRecipeResponse = {
|
||||||
|
recipe: Recipe;
|
||||||
|
};
|
||||||
|
|
||||||
export type PermissionConfirmationRequest = {
|
export type PermissionConfirmationRequest = {
|
||||||
action: string;
|
action: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -616,7 +624,6 @@ export type Recipe = {
|
|||||||
|
|
||||||
export type RecipeManifestResponse = {
|
export type RecipeManifestResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
isGlobal: boolean;
|
|
||||||
lastModified: string;
|
lastModified: string;
|
||||||
name: string;
|
name: string;
|
||||||
recipe: Recipe;
|
recipe: Recipe;
|
||||||
@@ -695,6 +702,12 @@ export type RunNowResponse = {
|
|||||||
session_id: string;
|
session_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SaveRecipeRequest = {
|
||||||
|
id?: string | null;
|
||||||
|
is_global?: boolean | null;
|
||||||
|
recipe: Recipe;
|
||||||
|
};
|
||||||
|
|
||||||
export type ScanRecipeRequest = {
|
export type ScanRecipeRequest = {
|
||||||
recipe: Recipe;
|
recipe: Recipe;
|
||||||
};
|
};
|
||||||
@@ -1784,6 +1797,64 @@ export type ListRecipesResponses = {
|
|||||||
|
|
||||||
export type ListRecipesResponse = ListRecipesResponses[keyof ListRecipesResponses];
|
export type ListRecipesResponse = ListRecipesResponses[keyof ListRecipesResponses];
|
||||||
|
|
||||||
|
export type ParseRecipeData = {
|
||||||
|
body: ParseRecipeRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/recipes/parse';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseRecipeErrors = {
|
||||||
|
/**
|
||||||
|
* Bad request - Invalid recipe format
|
||||||
|
*/
|
||||||
|
400: ErrorResponse;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: ErrorResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseRecipeError = ParseRecipeErrors[keyof ParseRecipeErrors];
|
||||||
|
|
||||||
|
export type ParseRecipeResponses = {
|
||||||
|
/**
|
||||||
|
* Recipe parsed successfully
|
||||||
|
*/
|
||||||
|
200: ParseRecipeResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParseRecipeResponse2 = ParseRecipeResponses[keyof ParseRecipeResponses];
|
||||||
|
|
||||||
|
export type SaveRecipeData = {
|
||||||
|
body: SaveRecipeRequest;
|
||||||
|
path?: never;
|
||||||
|
query?: never;
|
||||||
|
url: '/recipes/save';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SaveRecipeErrors = {
|
||||||
|
/**
|
||||||
|
* Unauthorized - Invalid or missing API key
|
||||||
|
*/
|
||||||
|
401: unknown;
|
||||||
|
/**
|
||||||
|
* Internal server error
|
||||||
|
*/
|
||||||
|
500: ErrorResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SaveRecipeError = SaveRecipeErrors[keyof SaveRecipeErrors];
|
||||||
|
|
||||||
|
export type SaveRecipeResponses = {
|
||||||
|
/**
|
||||||
|
* Recipe saved to file successfully
|
||||||
|
*/
|
||||||
|
204: void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SaveRecipeResponse = SaveRecipeResponses[keyof SaveRecipeResponses];
|
||||||
|
|
||||||
export type ScanRecipeData = {
|
export type ScanRecipeData = {
|
||||||
body: ScanRecipeRequest;
|
body: ScanRecipeRequest;
|
||||||
path?: never;
|
path?: never;
|
||||||
|
|||||||
@@ -119,17 +119,13 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
return recipe as Recipe;
|
return recipe as Recipe;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateTitleUniqueness = async (
|
const validateTitleUniqueness = async (title: string): Promise<string | undefined> => {
|
||||||
title: string,
|
|
||||||
isGlobal: boolean
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
if (!title.trim()) return undefined;
|
if (!title.trim()) return undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingRecipes = await listSavedRecipes();
|
const existingRecipes = await listSavedRecipes();
|
||||||
const titleExists = existingRecipes.some(
|
const titleExists = existingRecipes.some(
|
||||||
(recipe) =>
|
(recipe) => recipe.recipe.title?.toLowerCase() === title.toLowerCase()
|
||||||
recipe.recipe.title?.toLowerCase() === title.toLowerCase() && recipe.isGlobal === isGlobal
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (titleExists) {
|
if (titleExists) {
|
||||||
@@ -171,10 +167,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
|||||||
|
|
||||||
recipe.title = value.recipeTitle.trim();
|
recipe.title = value.recipeTitle.trim();
|
||||||
|
|
||||||
const titleValidationError = await validateTitleUniqueness(
|
const titleValidationError = await validateTitleUniqueness(value.recipeTitle.trim());
|
||||||
value.recipeTitle.trim(),
|
|
||||||
value.global
|
|
||||||
);
|
|
||||||
if (titleValidationError) {
|
if (titleValidationError) {
|
||||||
throw new Error(titleValidationError);
|
throw new Error(titleValidationError);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user