use std::collections::HashMap; use std::fs; use std::hash::DefaultHasher; use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::sync::Arc; use crate::routes::errors::ErrorResponse; use crate::state::AppState; use anyhow::Result; use axum::http::StatusCode; use goose::agents::Agent; use goose::prompt_template::render_global_file; use goose::recipe::build_recipe::{build_recipe_from_template, RecipeError}; use goose::recipe::local_recipes::{get_recipe_library_dir, list_local_recipes}; use goose::recipe::validate_recipe::validate_recipe_template_from_content; use goose::recipe::Recipe; use serde::Serialize; use serde_json::Value; use tracing::error; use utoipa::ToSchema; pub struct RecipeValidationError { pub status: StatusCode, pub message: String, } #[derive(Debug, Serialize, ToSchema)] pub struct RecipeManifest { pub id: String, pub recipe: Recipe, #[schema(value_type = String)] pub file_path: PathBuf, pub last_modified: String, pub schedule_cron: Option, pub slash_command: Option, } pub fn short_id_from_path(path: &str) -> String { let mut hasher = DefaultHasher::new(); path.hash(&mut hasher); let h = hasher.finish(); format!("{:016x}", h) } pub fn get_all_recipes_manifests() -> Result> { let recipes_with_path = list_local_recipes()?; 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::::from(m.modified().unwrap()).to_rfc3339()) else { continue; }; let manifest_with_path = RecipeManifest { id: short_id_from_path(file_path.to_string_lossy().as_ref()), recipe, file_path, last_modified, schedule_cron: None, slash_command: None, }; 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) } pub fn validate_recipe(recipe: &Recipe) -> Result<(), RecipeValidationError> { let recipe_yaml = recipe.to_yaml().map_err(|err| { let message = err.to_string(); error!("Failed to serialize recipe for validation: {}", message); RecipeValidationError { status: StatusCode::BAD_REQUEST, message, } })?; validate_recipe_template_from_content(&recipe_yaml, None).map_err(|err| { let message = err.to_string(); error!("Recipe validation failed: {}", message); RecipeValidationError { status: StatusCode::BAD_REQUEST, message, } })?; Ok(()) } pub async fn get_recipe_file_path_by_id( state: &AppState, id: &str, ) -> Result { let cached_path = { let map = state.recipe_file_hash_map.lock().await; map.get(id).cloned() }; if let Some(path) = cached_path { return Ok(path); } let recipe_manifest_with_paths = get_all_recipes_manifests().unwrap_or_default(); let mut recipe_file_hash_map = HashMap::new(); let mut resolved_path: Option = None; for recipe_manifest_with_path in &recipe_manifest_with_paths { if recipe_manifest_with_path.id == id { resolved_path = Some(recipe_manifest_with_path.file_path.clone()); } recipe_file_hash_map.insert( recipe_manifest_with_path.id.clone(), recipe_manifest_with_path.file_path.clone(), ); } state.set_recipe_file_hash_map(recipe_file_hash_map).await; resolved_path.ok_or_else(|| ErrorResponse { message: format!("Recipe not found: {}", id), status: StatusCode::NOT_FOUND, }) } pub async fn load_recipe_by_id(state: &AppState, id: &str) -> Result { let path = get_recipe_file_path_by_id(state, id).await?; Recipe::from_file_path(&path).map_err(|err| ErrorResponse { message: format!("Failed to load recipe: {}", err), status: StatusCode::INTERNAL_SERVER_ERROR, }) } pub async fn build_recipe_with_parameter_values( original_recipe: &Recipe, user_recipe_values: HashMap, ) -> Result> { let recipe_content = original_recipe.to_yaml()?; let recipe_dir = get_recipe_library_dir(true); let params = user_recipe_values.into_iter().collect(); let recipe = match build_recipe_from_template( recipe_content, &recipe_dir, params, None:: Result>, ) { Ok(recipe) => Some(recipe), Err(RecipeError::MissingParams { .. }) => None, Err(e) => return Err(anyhow::anyhow!(e)), }; Ok(recipe) } pub async fn apply_recipe_to_agent( agent: &Arc, recipe: &Recipe, include_final_output_tool: bool, ) -> Option { agent .apply_recipe_components( recipe.sub_recipes.clone(), recipe.response.clone(), include_final_output_tool, ) .await; recipe.instructions.as_ref().map(|instructions| { let mut context: HashMap<&str, Value> = HashMap::new(); context.insert("recipe_instructions", Value::String(instructions.clone())); render_global_file("desktop_recipe_instruction.md", &context).expect("Prompt should render") }) }