mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
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::list_recipes,
|
||||
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_tetrate_setup,
|
||||
),
|
||||
@@ -448,6 +450,10 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::recipe::RecipeManifestResponse,
|
||||
super::routes::recipe::ListRecipeResponse,
|
||||
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::Author,
|
||||
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 config_management;
|
||||
pub mod context;
|
||||
pub mod errors;
|
||||
pub mod extension;
|
||||
pub mod health;
|
||||
pub mod recipe;
|
||||
|
||||
@@ -5,12 +5,14 @@ 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 serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::get_all_recipes_manifests;
|
||||
use crate::state::AppState;
|
||||
|
||||
@@ -72,11 +74,25 @@ pub struct ScanRecipeResponse {
|
||||
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)]
|
||||
pub struct RecipeManifestResponse {
|
||||
name: String,
|
||||
#[serde(rename = "isGlobal")]
|
||||
is_global: bool,
|
||||
recipe: Recipe,
|
||||
#[serde(rename = "lastModified")]
|
||||
last_modified: String,
|
||||
@@ -235,7 +251,6 @@ async fn list_recipes(
|
||||
recipe_file_hash_map.insert(id.clone(), file_path);
|
||||
RecipeManifestResponse {
|
||||
name: recipe_manifest_with_path.name.clone(),
|
||||
is_global: recipe_manifest_with_path.is_global,
|
||||
recipe: recipe_manifest_with_path.recipe.clone(),
|
||||
id: id.clone(),
|
||||
last_modified: recipe_manifest_with_path.last_modified.clone(),
|
||||
@@ -278,6 +293,57 @@ async fn delete_recipe(
|
||||
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 {
|
||||
Router::new()
|
||||
.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/list", get(list_recipes))
|
||||
.route("/recipes/delete", post(delete_recipe))
|
||||
.route("/recipes/save", post(save_recipe))
|
||||
.route("/recipes/parse", post(parse_recipe))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@ use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use etcetera::{choose_app_strategy, AppStrategy};
|
||||
|
||||
use goose::config::APP_STRATEGY;
|
||||
use goose::recipe::read_recipe_file_content::read_recipe_file;
|
||||
use goose::recipe::recipe_library::list_all_recipes_from_library;
|
||||
use goose::recipe::Recipe;
|
||||
|
||||
use std::path::Path;
|
||||
@@ -18,7 +16,6 @@ use utoipa::ToSchema;
|
||||
pub struct RecipeManifestWithPath {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub is_global: bool,
|
||||
pub recipe: Recipe,
|
||||
pub file_path: PathBuf,
|
||||
pub last_modified: String,
|
||||
@@ -31,59 +28,31 @@ fn short_id_from_path(path: &str) -> String {
|
||||
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>> {
|
||||
let current_dir = std::env::current_dir()?;
|
||||
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 recipes_with_path = list_all_recipes_from_library()?;
|
||||
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)?);
|
||||
recipe_manifests_with_path.extend(load_recipes_from_path(&global_recipe_path, true)?);
|
||||
let manifest_with_path = RecipeManifestWithPath {
|
||||
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));
|
||||
|
||||
Ok(recipe_manifests_with_path)
|
||||
@@ -93,8 +62,6 @@ pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
struct RecipeManifestMetadata {
|
||||
pub name: String,
|
||||
#[serde(rename = "isGlobal")]
|
||||
pub is_global: bool,
|
||||
}
|
||||
|
||||
impl RecipeManifestMetadata {
|
||||
@@ -129,6 +96,5 @@ recipe: recipe_content
|
||||
let result = RecipeManifestMetadata::from_yaml_file(&file_path).unwrap();
|
||||
|
||||
assert_eq!(result.name, "Test Recipe");
|
||||
assert!(result.is_global);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use utoipa::ToSchema;
|
||||
|
||||
pub mod build_recipe;
|
||||
pub mod read_recipe_file_content;
|
||||
pub mod recipe_library;
|
||||
pub mod template_recipe;
|
||||
|
||||
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": {
|
||||
"post": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -3300,7 +3408,6 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"isGlobal",
|
||||
"recipe",
|
||||
"lastModified",
|
||||
"id"
|
||||
@@ -3309,9 +3416,6 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isGlobal": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastModified": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
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';
|
||||
|
||||
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>) => {
|
||||
return (options.client ?? _heyApiClient).post<ScanRecipeResponses, unknown, ThrowOnError>({
|
||||
url: '/recipes/scan',
|
||||
|
||||
@@ -448,6 +448,14 @@ export type ModelInfo = {
|
||||
supports_cache_control?: boolean | null;
|
||||
};
|
||||
|
||||
export type ParseRecipeRequest = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ParseRecipeResponse = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
export type PermissionConfirmationRequest = {
|
||||
action: string;
|
||||
id: string;
|
||||
@@ -616,7 +624,6 @@ export type Recipe = {
|
||||
|
||||
export type RecipeManifestResponse = {
|
||||
id: string;
|
||||
isGlobal: boolean;
|
||||
lastModified: string;
|
||||
name: string;
|
||||
recipe: Recipe;
|
||||
@@ -695,6 +702,12 @@ export type RunNowResponse = {
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
export type SaveRecipeRequest = {
|
||||
id?: string | null;
|
||||
is_global?: boolean | null;
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
export type ScanRecipeRequest = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
@@ -1784,6 +1797,64 @@ export type 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 = {
|
||||
body: ScanRecipeRequest;
|
||||
path?: never;
|
||||
|
||||
@@ -119,17 +119,13 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
||||
return recipe as Recipe;
|
||||
};
|
||||
|
||||
const validateTitleUniqueness = async (
|
||||
title: string,
|
||||
isGlobal: boolean
|
||||
): Promise<string | undefined> => {
|
||||
const validateTitleUniqueness = async (title: string): Promise<string | undefined> => {
|
||||
if (!title.trim()) return undefined;
|
||||
|
||||
try {
|
||||
const existingRecipes = await listSavedRecipes();
|
||||
const titleExists = existingRecipes.some(
|
||||
(recipe) =>
|
||||
recipe.recipe.title?.toLowerCase() === title.toLowerCase() && recipe.isGlobal === isGlobal
|
||||
(recipe) => recipe.recipe.title?.toLowerCase() === title.toLowerCase()
|
||||
);
|
||||
|
||||
if (titleExists) {
|
||||
@@ -171,10 +167,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
||||
|
||||
recipe.title = value.recipeTitle.trim();
|
||||
|
||||
const titleValidationError = await validateTitleUniqueness(
|
||||
value.recipeTitle.trim(),
|
||||
value.global
|
||||
);
|
||||
const titleValidationError = await validateTitleUniqueness(value.recipeTitle.trim());
|
||||
if (titleValidationError) {
|
||||
throw new Error(titleValidationError);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user