refactor: Use openapi for recipe endpoint types and in frontend (#3548)
This commit is contained in:
@@ -318,7 +318,10 @@ derive_utoipa!(Annotations as AnnotationsSchema);
|
||||
super::routes::schedule::unpause_schedule,
|
||||
super::routes::schedule::kill_running_job,
|
||||
super::routes::schedule::inspect_running_job,
|
||||
super::routes::schedule::sessions_handler
|
||||
super::routes::schedule::sessions_handler,
|
||||
super::routes::recipe::create_recipe,
|
||||
super::routes::recipe::encode_recipe,
|
||||
super::routes::recipe::decode_recipe
|
||||
),
|
||||
components(schemas(
|
||||
super::routes::config_management::UpsertConfigQuery,
|
||||
@@ -375,6 +378,21 @@ derive_utoipa!(Annotations as AnnotationsSchema);
|
||||
super::routes::schedule::ListSchedulesResponse,
|
||||
super::routes::schedule::SessionsQuery,
|
||||
super::routes::schedule::SessionDisplayInfo,
|
||||
super::routes::recipe::CreateRecipeRequest,
|
||||
super::routes::recipe::AuthorRequest,
|
||||
super::routes::recipe::CreateRecipeResponse,
|
||||
super::routes::recipe::EncodeRecipeRequest,
|
||||
super::routes::recipe::EncodeRecipeResponse,
|
||||
super::routes::recipe::DecodeRecipeRequest,
|
||||
super::routes::recipe::DecodeRecipeResponse,
|
||||
goose::recipe::Recipe,
|
||||
goose::recipe::Author,
|
||||
goose::recipe::Settings,
|
||||
goose::recipe::RecipeParameter,
|
||||
goose::recipe::RecipeParameterInputType,
|
||||
goose::recipe::RecipeParameterRequirement,
|
||||
goose::recipe::Response,
|
||||
goose::recipe::SubRecipe,
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -5,10 +5,11 @@ use goose::message::Message;
|
||||
use goose::recipe::Recipe;
|
||||
use goose::recipe_deeplink;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CreateRecipeRequest {
|
||||
messages: Vec<Message>,
|
||||
// Required metadata
|
||||
@@ -21,7 +22,7 @@ pub struct CreateRecipeRequest {
|
||||
author: Option<AuthorRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct AuthorRequest {
|
||||
#[serde(default)]
|
||||
contact: Option<String>,
|
||||
@@ -29,33 +30,45 @@ pub struct AuthorRequest {
|
||||
metadata: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct CreateRecipeResponse {
|
||||
recipe: Option<Recipe>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct EncodeRecipeRequest {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct EncodeRecipeResponse {
|
||||
deeplink: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct DecodeRecipeRequest {
|
||||
deeplink: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct DecodeRecipeResponse {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
/// Create a Recipe configuration from the current state of an agent
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/create",
|
||||
request_body = CreateRecipeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe created successfully", body = CreateRecipeResponse),
|
||||
(status = 400, description = "Bad request"),
|
||||
(status = 412, description = "Precondition failed - Agent not available"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
/// Create a Recipe configuration from the current session
|
||||
async fn create_recipe(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<CreateRecipeRequest>,
|
||||
@@ -105,6 +118,16 @@ async fn create_recipe(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/encode",
|
||||
request_body = EncodeRecipeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe encoded successfully", body = EncodeRecipeResponse),
|
||||
(status = 400, description = "Bad request")
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn encode_recipe(
|
||||
Json(request): Json<EncodeRecipeRequest>,
|
||||
) -> Result<Json<EncodeRecipeResponse>, StatusCode> {
|
||||
@@ -117,6 +140,16 @@ async fn encode_recipe(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/decode",
|
||||
request_body = DecodeRecipeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe decoded successfully", body = DecodeRecipeResponse),
|
||||
(status = 400, description = "Bad request")
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn decode_recipe(
|
||||
Json(request): Json<DecodeRecipeRequest>,
|
||||
) -> Result<Json<DecodeRecipeResponse>, StatusCode> {
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::fmt;
|
||||
use crate::agents::extension::ExtensionConfig;
|
||||
use serde::de::Deserializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub mod build_recipe;
|
||||
pub mod read_recipe_file_content;
|
||||
@@ -67,7 +68,7 @@ fn default_version() -> String {
|
||||
/// sub_recipes: None,
|
||||
/// };
|
||||
///
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct Recipe {
|
||||
// Required fields
|
||||
#[serde(default = "default_version")]
|
||||
@@ -110,7 +111,7 @@ pub struct Recipe {
|
||||
pub sub_recipes: Option<Vec<SubRecipe>>, // sub-recipes for the recipe
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct Author {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contact: Option<String>, // creator/contact information of the recipe
|
||||
@@ -119,7 +120,7 @@ pub struct Author {
|
||||
pub metadata: Option<String>, // any additional metadata for the author
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct Settings {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub goose_provider: Option<String>,
|
||||
@@ -131,13 +132,13 @@ pub struct Settings {
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct Response {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub json_schema: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct SubRecipe {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
@@ -172,7 +173,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RecipeParameterRequirement {
|
||||
Required,
|
||||
@@ -190,7 +191,7 @@ impl fmt::Display for RecipeParameterRequirement {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RecipeParameterInputType {
|
||||
String,
|
||||
@@ -210,7 +211,7 @@ impl fmt::Display for RecipeParameterInputType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct RecipeParameter {
|
||||
pub key: String,
|
||||
pub input_type: RecipeParameterInputType,
|
||||
|
||||
@@ -499,6 +499,112 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/recipes/create": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Recipe Management"
|
||||
],
|
||||
"summary": "Create a Recipe configuration from the current session",
|
||||
"operationId": "create_recipe",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateRecipeRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Recipe created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request"
|
||||
},
|
||||
"412": {
|
||||
"description": "Precondition failed - Agent not available"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/recipes/decode": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Recipe Management"
|
||||
],
|
||||
"operationId": "decode_recipe",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DecodeRecipeRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Recipe decoded successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DecodeRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/recipes/encode": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Recipe Management"
|
||||
],
|
||||
"operationId": "encode_recipe",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EncodeRecipeRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Recipe encoded successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EncodeRecipeResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/schedule/create": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -945,6 +1051,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Author": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contact": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"AuthorRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contact": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ConfigKey": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -1131,6 +1263,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"messages",
|
||||
"title",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"activities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"author": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AuthorRequest"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateRecipeResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"recipe": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateScheduleRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -1154,6 +1340,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DecodeRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"deeplink"
|
||||
],
|
||||
"properties": {
|
||||
"deeplink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DecodeRecipeResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"recipe"
|
||||
],
|
||||
"properties": {
|
||||
"recipe": {
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedResource": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -1172,6 +1380,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EncodeRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"recipe"
|
||||
],
|
||||
"properties": {
|
||||
"recipe": {
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EncodeRecipeResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"deeplink"
|
||||
],
|
||||
"properties": {
|
||||
"deeplink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Envs": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -1951,6 +2181,137 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Recipe": {
|
||||
"type": "object",
|
||||
"description": "A Recipe represents a personalized, user-generated agent configuration that defines\nspecific behaviors and capabilities within the Goose system.\n\n# Fields\n\n## Required Fields\n* `version` - Semantic version of the Recipe file format (defaults to \"1.0.0\")\n* `title` - Short, descriptive name of the Recipe\n* `description` - Detailed description explaining the Recipe's purpose and functionality\n* `Instructions` - Instructions that defines the Recipe's behavior\n\n## Optional Fields\n* `prompt` - the initial prompt to the session to start with\n* `extensions` - List of extension configurations required by the Recipe\n* `context` - Supplementary context information for the Recipe\n* `activities` - Activity labels that appear when loading the Recipe\n* `author` - Information about the Recipe's creator and metadata\n* `parameters` - Additional parameters for the Recipe\n* `response` - Response configuration including JSON schema validation\n\n# Example\n\n\nuse goose::recipe::Recipe;\n\n// Using the builder pattern\nlet recipe = Recipe::builder()\n.title(\"Example Agent\")\n.description(\"An example Recipe configuration\")\n.instructions(\"Act as a helpful assistant\")\n.build()\n.expect(\"Missing required fields\");\n\n// Or using struct initialization\nlet recipe = Recipe {\nversion: \"1.0.0\".to_string(),\ntitle: \"Example Agent\".to_string(),\ndescription: \"An example Recipe configuration\".to_string(),\ninstructions: Some(\"Act as a helpful assistant\".to_string()),\nprompt: None,\nextensions: None,\ncontext: None,\nactivities: None,\nauthor: None,\nsettings: None,\nparameters: None,\nresponse: None,\nsub_recipes: None,\n};\n",
|
||||
"required": [
|
||||
"title",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"activities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"author": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Author"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"context": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ExtensionConfig"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"instructions": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"parameters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RecipeParameter"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"response": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Response"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"settings": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Settings"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"sub_recipes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SubRecipe"
|
||||
},
|
||||
"nullable": true
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RecipeParameter": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"key",
|
||||
"input_type",
|
||||
"requirement",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_type": {
|
||||
"$ref": "#/components/schemas/RecipeParameterInputType"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"requirement": {
|
||||
"$ref": "#/components/schemas/RecipeParameterRequirement"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RecipeParameterInputType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"date",
|
||||
"file"
|
||||
]
|
||||
},
|
||||
"RecipeParameterRequirement": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"required",
|
||||
"optional",
|
||||
"user_prompt"
|
||||
]
|
||||
},
|
||||
"RedactedThinkingContent": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2004,6 +2365,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"json_schema": {
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Role": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2273,6 +2642,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"goose_model": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"goose_provider": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"format": "float",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SubRecipe": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"sequential_when_repeated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"values": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"SummarizationRequested": {
|
||||
"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 '@hey-api/client-fetch';
|
||||
import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RecoverConfigData, RecoverConfigResponse, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ValidateConfigData, ValidateConfigResponse, ConfirmPermissionData, ManageContextData, ManageContextResponse, CreateScheduleData, CreateScheduleResponse, DeleteScheduleData, DeleteScheduleResponse, ListSchedulesData, ListSchedulesResponse2, UpdateScheduleData, UpdateScheduleResponse, InspectRunningJobData, InspectRunningJobResponse, KillRunningJobData, PauseScheduleData, PauseScheduleResponse, RunNowHandlerData, RunNowHandlerResponse, SessionsHandlerData, SessionsHandlerResponse, UnpauseScheduleData, UnpauseScheduleResponse, ListSessionsData, ListSessionsResponse, GetSessionHistoryData, GetSessionHistoryResponse } from './types.gen';
|
||||
import type { GetToolsData, GetToolsResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RecoverConfigData, RecoverConfigResponse, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ValidateConfigData, ValidateConfigResponse, ConfirmPermissionData, ManageContextData, ManageContextResponse, CreateRecipeData, CreateRecipeResponse2, DecodeRecipeData, DecodeRecipeResponse2, EncodeRecipeData, EncodeRecipeResponse2, CreateScheduleData, CreateScheduleResponse, DeleteScheduleData, DeleteScheduleResponse, ListSchedulesData, ListSchedulesResponse2, UpdateScheduleData, UpdateScheduleResponse, InspectRunningJobData, InspectRunningJobResponse, KillRunningJobData, PauseScheduleData, PauseScheduleResponse, RunNowHandlerData, RunNowHandlerResponse, SessionsHandlerData, SessionsHandlerResponse, UnpauseScheduleData, UnpauseScheduleResponse, ListSessionsData, ListSessionsResponse, GetSessionHistoryData, GetSessionHistoryResponse } from './types.gen';
|
||||
import { client as _heyApiClient } from './client.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
|
||||
@@ -158,6 +158,42 @@ export const manageContext = <ThrowOnError extends boolean = false>(options: Opt
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a Recipe configuration from the current session
|
||||
*/
|
||||
export const createRecipe = <ThrowOnError extends boolean = false>(options: Options<CreateRecipeData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<CreateRecipeResponse2, unknown, ThrowOnError>({
|
||||
url: '/recipes/create',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const decodeRecipe = <ThrowOnError extends boolean = false>(options: Options<DecodeRecipeData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<DecodeRecipeResponse2, unknown, ThrowOnError>({
|
||||
url: '/recipes/decode',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const encodeRecipe = <ThrowOnError extends boolean = false>(options: Options<EncodeRecipeData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<EncodeRecipeResponse2, unknown, ThrowOnError>({
|
||||
url: '/recipes/encode',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const createSchedule = <ThrowOnError extends boolean = false>(options: Options<CreateScheduleData, ThrowOnError>) => {
|
||||
return (options.client ?? _heyApiClient).post<CreateScheduleResponse, unknown, ThrowOnError>({
|
||||
url: '/schedule/create',
|
||||
|
||||
@@ -6,6 +6,16 @@ export type Annotations = {
|
||||
timestamp?: string;
|
||||
};
|
||||
|
||||
export type Author = {
|
||||
contact?: string | null;
|
||||
metadata?: string | null;
|
||||
};
|
||||
|
||||
export type AuthorRequest = {
|
||||
contact?: string | null;
|
||||
metadata?: string | null;
|
||||
};
|
||||
|
||||
export type ConfigKey = {
|
||||
default?: string | null;
|
||||
name: string;
|
||||
@@ -71,6 +81,19 @@ export type ContextManageResponse = {
|
||||
tokenCounts: Array<number>;
|
||||
};
|
||||
|
||||
export type CreateRecipeRequest = {
|
||||
activities?: Array<string> | null;
|
||||
author?: AuthorRequest | null;
|
||||
description: string;
|
||||
messages: Array<Message>;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type CreateRecipeResponse = {
|
||||
error?: string | null;
|
||||
recipe?: Recipe | null;
|
||||
};
|
||||
|
||||
export type CreateScheduleRequest = {
|
||||
cron: string;
|
||||
execution_mode?: string | null;
|
||||
@@ -78,11 +101,27 @@ export type CreateScheduleRequest = {
|
||||
recipe_source: string;
|
||||
};
|
||||
|
||||
export type DecodeRecipeRequest = {
|
||||
deeplink: string;
|
||||
};
|
||||
|
||||
export type DecodeRecipeResponse = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
export type EmbeddedResource = {
|
||||
annotations?: Annotations;
|
||||
resource: ResourceContents;
|
||||
};
|
||||
|
||||
export type EncodeRecipeRequest = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
export type EncodeRecipeResponse = {
|
||||
deeplink: string;
|
||||
};
|
||||
|
||||
export type Envs = {
|
||||
[key: string]: string;
|
||||
};
|
||||
@@ -273,6 +312,10 @@ export type ModelInfo = {
|
||||
* Cost per token for output (optional)
|
||||
*/
|
||||
output_token_cost?: number | null;
|
||||
/**
|
||||
* Whether this model supports cache control
|
||||
*/
|
||||
supports_cache_control?: boolean | null;
|
||||
};
|
||||
|
||||
export type PermissionConfirmationRequest = {
|
||||
@@ -333,6 +376,86 @@ export type ProvidersResponse = {
|
||||
providers: Array<ProviderDetails>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Recipe represents a personalized, user-generated agent configuration that defines
|
||||
* specific behaviors and capabilities within the Goose system.
|
||||
*
|
||||
* # Fields
|
||||
*
|
||||
* ## Required Fields
|
||||
* * `version` - Semantic version of the Recipe file format (defaults to "1.0.0")
|
||||
* * `title` - Short, descriptive name of the Recipe
|
||||
* * `description` - Detailed description explaining the Recipe's purpose and functionality
|
||||
* * `Instructions` - Instructions that defines the Recipe's behavior
|
||||
*
|
||||
* ## Optional Fields
|
||||
* * `prompt` - the initial prompt to the session to start with
|
||||
* * `extensions` - List of extension configurations required by the Recipe
|
||||
* * `context` - Supplementary context information for the Recipe
|
||||
* * `activities` - Activity labels that appear when loading the Recipe
|
||||
* * `author` - Information about the Recipe's creator and metadata
|
||||
* * `parameters` - Additional parameters for the Recipe
|
||||
* * `response` - Response configuration including JSON schema validation
|
||||
*
|
||||
* # Example
|
||||
*
|
||||
*
|
||||
* use goose::recipe::Recipe;
|
||||
*
|
||||
* // Using the builder pattern
|
||||
* let recipe = Recipe::builder()
|
||||
* .title("Example Agent")
|
||||
* .description("An example Recipe configuration")
|
||||
* .instructions("Act as a helpful assistant")
|
||||
* .build()
|
||||
* .expect("Missing required fields");
|
||||
*
|
||||
* // Or using struct initialization
|
||||
* let recipe = Recipe {
|
||||
* version: "1.0.0".to_string(),
|
||||
* title: "Example Agent".to_string(),
|
||||
* description: "An example Recipe configuration".to_string(),
|
||||
* instructions: Some("Act as a helpful assistant".to_string()),
|
||||
* prompt: None,
|
||||
* extensions: None,
|
||||
* context: None,
|
||||
* activities: None,
|
||||
* author: None,
|
||||
* settings: None,
|
||||
* parameters: None,
|
||||
* response: None,
|
||||
* sub_recipes: None,
|
||||
* };
|
||||
*
|
||||
*/
|
||||
export type Recipe = {
|
||||
activities?: Array<string> | null;
|
||||
author?: Author | null;
|
||||
context?: Array<string> | null;
|
||||
description: string;
|
||||
extensions?: Array<ExtensionConfig> | null;
|
||||
instructions?: string | null;
|
||||
parameters?: Array<RecipeParameter> | null;
|
||||
prompt?: string | null;
|
||||
response?: Response | null;
|
||||
settings?: Settings | null;
|
||||
sub_recipes?: Array<SubRecipe> | null;
|
||||
title: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type RecipeParameter = {
|
||||
default?: string | null;
|
||||
description: string;
|
||||
input_type: RecipeParameterInputType;
|
||||
key: string;
|
||||
requirement: RecipeParameterRequirement;
|
||||
};
|
||||
|
||||
export type RecipeParameterInputType = 'string' | 'number' | 'boolean' | 'date' | 'file';
|
||||
|
||||
export type RecipeParameterRequirement = 'required' | 'optional' | 'user_prompt';
|
||||
|
||||
export type RedactedThinkingContent = {
|
||||
data: string;
|
||||
};
|
||||
@@ -347,6 +470,10 @@ export type ResourceContents = {
|
||||
uri: string;
|
||||
};
|
||||
|
||||
export type Response = {
|
||||
json_schema?: unknown;
|
||||
};
|
||||
|
||||
export type Role = string;
|
||||
|
||||
export type RunNowResponse = {
|
||||
@@ -460,6 +587,21 @@ export type SessionsQuery = {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
goose_model?: string | null;
|
||||
goose_provider?: string | null;
|
||||
temperature?: number | null;
|
||||
};
|
||||
|
||||
export type SubRecipe = {
|
||||
name: string;
|
||||
path: string;
|
||||
sequential_when_repeated?: boolean;
|
||||
values?: {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type SummarizationRequested = {
|
||||
msg: string;
|
||||
};
|
||||
@@ -994,6 +1136,83 @@ export type ManageContextResponses = {
|
||||
|
||||
export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses];
|
||||
|
||||
export type CreateRecipeData = {
|
||||
body: CreateRecipeRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/recipes/create';
|
||||
};
|
||||
|
||||
export type CreateRecipeErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: unknown;
|
||||
/**
|
||||
* Precondition failed - Agent not available
|
||||
*/
|
||||
412: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type CreateRecipeResponses = {
|
||||
/**
|
||||
* Recipe created successfully
|
||||
*/
|
||||
200: CreateRecipeResponse;
|
||||
};
|
||||
|
||||
export type CreateRecipeResponse2 = CreateRecipeResponses[keyof CreateRecipeResponses];
|
||||
|
||||
export type DecodeRecipeData = {
|
||||
body: DecodeRecipeRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/recipes/decode';
|
||||
};
|
||||
|
||||
export type DecodeRecipeErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: unknown;
|
||||
};
|
||||
|
||||
export type DecodeRecipeResponses = {
|
||||
/**
|
||||
* Recipe decoded successfully
|
||||
*/
|
||||
200: DecodeRecipeResponse;
|
||||
};
|
||||
|
||||
export type DecodeRecipeResponse2 = DecodeRecipeResponses[keyof DecodeRecipeResponses];
|
||||
|
||||
export type EncodeRecipeData = {
|
||||
body: EncodeRecipeRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/recipes/encode';
|
||||
};
|
||||
|
||||
export type EncodeRecipeErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: unknown;
|
||||
};
|
||||
|
||||
export type EncodeRecipeResponses = {
|
||||
/**
|
||||
* Recipe encoded successfully
|
||||
*/
|
||||
200: EncodeRecipeResponse;
|
||||
};
|
||||
|
||||
export type EncodeRecipeResponse2 = EncodeRecipeResponses[keyof EncodeRecipeResponses];
|
||||
|
||||
export type CreateScheduleData = {
|
||||
body: CreateScheduleRequest;
|
||||
path?: never;
|
||||
|
||||
@@ -163,8 +163,12 @@ export default function RecipesView({ _onLoadRecipe }: RecipesViewProps = {}) {
|
||||
}
|
||||
const recipe = await decodeRecipe(recipeEncoded);
|
||||
|
||||
if (!recipe.title || !recipe.description || !recipe.instructions) {
|
||||
throw new Error('Recipe is missing required fields (title, description, instructions)');
|
||||
if (!recipe.title || !recipe.description) {
|
||||
throw new Error('Recipe is missing required fields (title, description)');
|
||||
}
|
||||
|
||||
if (!recipe.instructions && !recipe.prompt) {
|
||||
throw new Error('Recipe must have either instructions or prompt');
|
||||
}
|
||||
|
||||
return recipe;
|
||||
|
||||
@@ -48,11 +48,13 @@ interface CleanExtension {
|
||||
bundled?: boolean;
|
||||
}
|
||||
|
||||
// TODO: This 'Recipe' interface should be converted to match the OpenAPI spec for Recipe
|
||||
// once we have separated the recipe from the schedule in the frontend.
|
||||
// Interface for clean recipe in YAML
|
||||
interface CleanRecipe {
|
||||
title: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
instructions?: string;
|
||||
prompt?: string;
|
||||
activities?: string[];
|
||||
extensions?: CleanExtension[];
|
||||
@@ -131,9 +133,12 @@ function recipeToYaml(recipe: Recipe, executionMode: ExecutionMode): string {
|
||||
const cleanRecipe: CleanRecipe = {
|
||||
title: recipe.title,
|
||||
description: recipe.description,
|
||||
instructions: recipe.instructions,
|
||||
};
|
||||
|
||||
if (recipe.instructions) {
|
||||
cleanRecipe.instructions = recipe.instructions;
|
||||
}
|
||||
|
||||
if (recipe.prompt) {
|
||||
cleanRecipe.prompt = recipe.prompt;
|
||||
}
|
||||
@@ -211,7 +216,7 @@ function recipeToYaml(recipe: Recipe, executionMode: ExecutionMode): string {
|
||||
}
|
||||
|
||||
// Add common optional fields
|
||||
if (ext.env_keys && ext.env_keys.length > 0) {
|
||||
if ('env_keys' in ext && ext.env_keys && ext.env_keys.length > 0) {
|
||||
cleanExt.env_keys = ext.env_keys;
|
||||
}
|
||||
|
||||
@@ -244,7 +249,10 @@ function recipeToYaml(recipe: Recipe, executionMode: ExecutionMode): string {
|
||||
}
|
||||
|
||||
if (recipe.author) {
|
||||
cleanRecipe.author = recipe.author;
|
||||
cleanRecipe.author = {
|
||||
contact: recipe.author.contact || undefined,
|
||||
metadata: recipe.author.metadata || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Add schedule configuration based on execution mode
|
||||
|
||||
+97
-101
@@ -1,39 +1,36 @@
|
||||
import { Message } from '../types/message';
|
||||
import { getApiUrl } from '../config';
|
||||
import { FullExtensionConfig } from '../extensions';
|
||||
import { safeJsonParse } from '../utils/jsonUtils';
|
||||
import {
|
||||
createRecipe as apiCreateRecipe,
|
||||
encodeRecipe as apiEncodeRecipe,
|
||||
decodeRecipe as apiDecodeRecipe,
|
||||
} from '../api';
|
||||
import type {
|
||||
CreateRecipeRequest as ApiCreateRecipeRequest,
|
||||
CreateRecipeResponse as ApiCreateRecipeResponse,
|
||||
RecipeParameter,
|
||||
Message as ApiMessage,
|
||||
Role,
|
||||
MessageContent,
|
||||
} from '../api';
|
||||
import type { Message as FrontendMessage } from '../types/message';
|
||||
|
||||
export interface Parameter {
|
||||
key: string;
|
||||
description: string;
|
||||
input_type: string;
|
||||
default?: string;
|
||||
requirement: 'required' | 'optional' | 'user_prompt';
|
||||
}
|
||||
|
||||
export interface Recipe {
|
||||
title: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
prompt?: string;
|
||||
activities?: string[];
|
||||
parameters?: Parameter[];
|
||||
author?: {
|
||||
contact?: string;
|
||||
metadata?: string;
|
||||
};
|
||||
extensions?: FullExtensionConfig[];
|
||||
goosehints?: string;
|
||||
context?: string[];
|
||||
profile?: string;
|
||||
mcps?: number;
|
||||
// Re-export OpenAPI types with frontend-specific additions
|
||||
export type Parameter = RecipeParameter;
|
||||
export type Recipe = import('../api').Recipe & {
|
||||
// TODO: Separate these from the raw recipe type
|
||||
// Properties added for scheduled execution
|
||||
scheduledJobId?: string;
|
||||
isScheduledExecution?: boolean;
|
||||
}
|
||||
// TODO: Separate these from the raw recipe type
|
||||
// Legacy frontend properties (not in OpenAPI schema)
|
||||
profile?: string;
|
||||
goosehints?: string;
|
||||
mcps?: number;
|
||||
};
|
||||
|
||||
// Create frontend-compatible type that accepts frontend Message until we can refactor.
|
||||
export interface CreateRecipeRequest {
|
||||
messages: Message[];
|
||||
// TODO: Fix this type to match Message OpenAPI spec
|
||||
messages: FrontendMessage[];
|
||||
title: string;
|
||||
description: string;
|
||||
activities?: string[];
|
||||
@@ -43,98 +40,97 @@ export interface CreateRecipeRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateRecipeResponse {
|
||||
recipe: Recipe | null;
|
||||
error: string | null;
|
||||
export type CreateRecipeResponse = ApiCreateRecipeResponse;
|
||||
|
||||
function convertFrontendMessageToApiMessage(frontendMessage: FrontendMessage): ApiMessage {
|
||||
// TODO: Fix this type to match Message OpenAPI spec
|
||||
return {
|
||||
id: frontendMessage.id,
|
||||
role: frontendMessage.role as Role,
|
||||
content: frontendMessage.content.map((content) => ({
|
||||
...content,
|
||||
// Convert toolCall to match API expectations
|
||||
...(content.type === 'toolRequest' && 'toolCall' in content
|
||||
? {
|
||||
toolCall: content.toolCall as unknown as { [key: string]: unknown },
|
||||
}
|
||||
: {}),
|
||||
})) as MessageContent[],
|
||||
created: frontendMessage.created,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRecipe(request: CreateRecipeRequest): Promise<CreateRecipeResponse> {
|
||||
const url = getApiUrl('/recipes/create');
|
||||
console.log('Creating recipe at:', url);
|
||||
console.log('Request:', JSON.stringify(request, null, 2));
|
||||
console.log('Creating recipe with request:', JSON.stringify(request, null, 2));
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
try {
|
||||
const apiRequest: ApiCreateRecipeRequest = {
|
||||
messages: request.messages.map(convertFrontendMessageToApiMessage),
|
||||
title: request.title,
|
||||
description: request.description,
|
||||
activities: request.activities || undefined,
|
||||
author: request.author
|
||||
? {
|
||||
contact: request.author.contact || undefined,
|
||||
metadata: request.author.metadata || undefined,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Failed to create recipe:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
const response = await apiCreateRecipe({
|
||||
body: apiRequest,
|
||||
});
|
||||
throw new Error(`Failed to create recipe: ${response.statusText} (${errorText})`);
|
||||
|
||||
if (!response.data) {
|
||||
throw new Error('No data returned from API');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to create recipe:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return safeJsonParse<CreateRecipeResponse>(response, 'Server failed to create recipe:');
|
||||
}
|
||||
|
||||
export interface EncodeRecipeRequest {
|
||||
recipe: Recipe;
|
||||
}
|
||||
|
||||
export interface EncodeRecipeResponse {
|
||||
deeplink: string;
|
||||
}
|
||||
|
||||
export interface DecodeRecipeRequest {
|
||||
deeplink: string;
|
||||
}
|
||||
|
||||
export interface DecodeRecipeResponse {
|
||||
recipe: Recipe;
|
||||
}
|
||||
|
||||
export async function encodeRecipe(recipe: Recipe): Promise<string> {
|
||||
const url = getApiUrl('/recipes/encode');
|
||||
try {
|
||||
const response = await apiEncodeRecipe({
|
||||
body: { recipe },
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ recipe } as EncodeRecipeRequest),
|
||||
});
|
||||
if (!response.data) {
|
||||
throw new Error('No data returned from API');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to encode recipe: ${response.status} ${response.statusText}`);
|
||||
return response.data.deeplink;
|
||||
} catch (error) {
|
||||
console.error('Failed to encode recipe:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const data: EncodeRecipeResponse = await response.json();
|
||||
return data.deeplink;
|
||||
}
|
||||
|
||||
export async function decodeRecipe(deeplink: string): Promise<Recipe> {
|
||||
const url = getApiUrl('/recipes/decode');
|
||||
|
||||
console.log('Decoding recipe from deeplink:', deeplink);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ deeplink } as DecodeRecipeRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to decode deeplink:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
try {
|
||||
const response = await apiDecodeRecipe({
|
||||
body: { deeplink },
|
||||
});
|
||||
throw new Error(`Failed to decode deeplink: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data: DecodeRecipeResponse = await response.json();
|
||||
if (!data.recipe) {
|
||||
console.error('Decoded recipe is null:', data);
|
||||
throw new Error('Decoded recipe is null');
|
||||
if (!response.data) {
|
||||
throw new Error('No data returned from API');
|
||||
}
|
||||
|
||||
if (!response.data.recipe) {
|
||||
console.error('Decoded recipe is null:', response.data);
|
||||
throw new Error('Decoded recipe is null');
|
||||
}
|
||||
|
||||
return response.data.recipe as Recipe;
|
||||
} catch (error) {
|
||||
console.error('Failed to decode deeplink:', error);
|
||||
throw error;
|
||||
}
|
||||
return data.recipe;
|
||||
}
|
||||
|
||||
export async function generateDeepLink(recipe: Recipe): Promise<string> {
|
||||
|
||||
@@ -100,8 +100,12 @@ export async function saveRecipe(recipe: Recipe, options: SaveRecipeOptions): Pr
|
||||
}
|
||||
|
||||
// Validate recipe has required fields
|
||||
if (!recipe.title || !recipe.description || !recipe.instructions) {
|
||||
throw new Error('Recipe is missing required fields (title, description, instructions)');
|
||||
if (!recipe.title || !recipe.description) {
|
||||
throw new Error('Recipe is missing required fields (title, description)');
|
||||
}
|
||||
|
||||
if (!recipe.instructions && !recipe.prompt) {
|
||||
throw new Error('Recipe must have either instructions or prompt');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -142,14 +146,14 @@ export async function loadRecipe(recipeName: string, isGlobal: boolean): Promise
|
||||
}
|
||||
|
||||
// Validate the loaded recipe has required fields
|
||||
if (
|
||||
!savedRecipe.recipe.title ||
|
||||
!savedRecipe.recipe.description ||
|
||||
!savedRecipe.recipe.instructions
|
||||
) {
|
||||
if (!savedRecipe.recipe.title || !savedRecipe.recipe.description) {
|
||||
throw new Error('Loaded recipe is missing required fields');
|
||||
}
|
||||
|
||||
if (!savedRecipe.recipe.instructions && !savedRecipe.recipe.prompt) {
|
||||
throw new Error('Loaded recipe must have either instructions or prompt');
|
||||
}
|
||||
|
||||
return savedRecipe.recipe;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user