Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/goose-cli/src/recipes/github_recipe.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use anyhow::{anyhow, Result};
use console::style;
use goose::recipe::template_recipe::parse_recipe_content;
use goose::recipe::RECIPE_FILE_EXTENSIONS;
use serde::{Deserialize, Serialize};

use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS;
use goose::recipe::read_recipe_file_content::RecipeFile;
use std::env;
use std::fs;
Expand Down
2 changes: 0 additions & 2 deletions crates/goose-cli/src/recipes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ use goose::recipe::Recipe;
use serde_json::Value;
use std::collections::HashMap;

pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];

fn create_user_prompt_callback() -> impl Fn(&str, &str) -> Result<String> {
|key: &str, description: &str| -> Result<String> {
let input_value =
Expand Down
172 changes: 19 additions & 153 deletions crates/goose-cli/src/recipes/search_recipe.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use goose::config::Config;
use goose::recipe::read_recipe_file_content::{read_recipe_file, RecipeFile};
use goose::recipe::template_recipe::parse_recipe_content;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use crate::recipes::recipe::RECIPE_FILE_EXTENSIONS;
use goose::recipe::read_recipe_file_content::RecipeFile;

use super::github_recipe::{
list_github_recipes, retrieve_recipe_from_github, RecipeInfo, RecipeSource,
GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY,
};

const GOOSE_RECIPE_PATH_ENV_VAR: &str = "GOOSE_RECIPE_PATH";
use goose::recipe::search_local_recipes::{discover_local_recipes, retrieve_local_recipe_file};

pub fn retrieve_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
if RECIPE_FILE_EXTENSIONS
.iter()
.any(|ext| recipe_name.ends_with(&format!(".{}", ext)))
{
let path = PathBuf::from(recipe_name);
return read_recipe_file(path);
}
if is_file_path(recipe_name) || is_file_name(recipe_name) {
return Err(anyhow!(
"Recipe file {} is not a json or yaml file",
recipe_name
));
}
retrieve_recipe_from_local_path(recipe_name).or_else(|e| {
retrieve_local_recipe_file(recipe_name).or_else(|e| {
if let Some(recipe_repo_full_name) = configured_github_recipe_repo() {
retrieve_recipe_from_github(recipe_name, &recipe_repo_full_name)
} else {
Expand All @@ -38,60 +18,6 @@ pub fn retrieve_recipe_file(recipe_name: &str) -> Result<RecipeFile> {
})
}

fn is_file_path(recipe_name: &str) -> bool {
recipe_name.contains('/')
|| recipe_name.contains('\\')
|| recipe_name.starts_with('~')
|| recipe_name.starts_with('.')
}

fn is_file_name(recipe_name: &str) -> bool {
Path::new(recipe_name).extension().is_some()
}

fn read_recipe_in_dir(dir: &Path, recipe_name: &str) -> Result<RecipeFile> {
for ext in RECIPE_FILE_EXTENSIONS {
let recipe_path = dir.join(format!("{}.{}", recipe_name, ext));
if let Ok(result) = read_recipe_file(recipe_path) {
return Ok(result);
}
}
Err(anyhow!(format!(
"No {}.yaml or {}.json recipe file found in directory: {}",
recipe_name,
recipe_name,
dir.display()
)))
}

fn retrieve_recipe_from_local_path(recipe_name: &str) -> Result<RecipeFile> {
let mut search_dirs = vec![PathBuf::from(".")];
if let Ok(recipe_path_env) = env::var(GOOSE_RECIPE_PATH_ENV_VAR) {
let path_separator = if cfg!(windows) { ';' } else { ':' };
let recipe_path_env_dirs: Vec<PathBuf> = recipe_path_env
.split(path_separator)
.map(PathBuf::from)
.collect();
search_dirs.extend(recipe_path_env_dirs);
}
for dir in &search_dirs {
if let Ok(result) = read_recipe_in_dir(dir, recipe_name) {
return Ok(result);
}
}
let search_dirs_str = search_dirs
.iter()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>()
.join(":");
Err(anyhow!(
"ℹ️ Failed to retrieve {}.yaml or {}.json in {}",
recipe_name,
recipe_name,
search_dirs_str
))
}

fn configured_github_recipe_repo() -> Option<String> {
let config = Config::global();
match config.get_param(GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY) {
Expand All @@ -106,7 +32,21 @@ pub fn list_available_recipes() -> Result<Vec<RecipeInfo>> {

// Search local recipes
if let Ok(local_recipes) = discover_local_recipes() {
recipes.extend(local_recipes);
recipes.extend(local_recipes.into_iter().map(|(path, recipe)| {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();

RecipeInfo {
name,
source: RecipeSource::Local,
path: path.to_string_lossy().to_string(),
title: Some(recipe.title),
description: Some(recipe.description),
}
}));
}

// Search GitHub recipes if configured
Expand All @@ -118,77 +58,3 @@ pub fn list_available_recipes() -> Result<Vec<RecipeInfo>> {

Ok(recipes)
}

fn discover_local_recipes() -> Result<Vec<RecipeInfo>> {
let mut recipes = Vec::new();
let mut search_dirs = vec![PathBuf::from(".")];

// Add GOOSE_RECIPE_PATH directories
if let Ok(recipe_path_env) = env::var(GOOSE_RECIPE_PATH_ENV_VAR) {
let path_separator = if cfg!(windows) { ';' } else { ':' };
let recipe_path_env_dirs: Vec<PathBuf> = recipe_path_env
.split(path_separator)
.map(PathBuf::from)
.collect();
search_dirs.extend(recipe_path_env_dirs);
}

for dir in search_dirs {
if let Ok(dir_recipes) = scan_directory_for_recipes(&dir) {
recipes.extend(dir_recipes);
}
}

Ok(recipes)
}

fn scan_directory_for_recipes(dir: &Path) -> Result<Vec<RecipeInfo>> {
let mut recipes = Vec::new();

if !dir.exists() || !dir.is_dir() {
return Ok(recipes);
}

for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();

if path.is_file() {
if let Some(extension) = path.extension() {
if RECIPE_FILE_EXTENSIONS.contains(&extension.to_string_lossy().as_ref()) {
if let Ok(recipe_info) = create_local_recipe_info(&path) {
recipes.push(recipe_info);
}
}
}
}
}

Ok(recipes)
}

fn create_local_recipe_info(path: &Path) -> Result<RecipeInfo> {
let content = fs::read_to_string(path)?;
let recipe_dir = path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_string_lossy()
.to_string();
let (recipe, _) = parse_recipe_content(&content, recipe_dir)?;

let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();

let path_str = path.to_string_lossy().to_string();

Ok(RecipeInfo {
name,
source: RecipeSource::Local,
path: path_str,
title: Some(recipe.title),
description: Some(recipe.description),
})
}
4 changes: 2 additions & 2 deletions crates/goose-server/src/routes/recipe_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::PathBuf;

use anyhow::Result;

use goose::recipe::recipe_library::list_all_recipes_from_library;
use goose::recipe::search_local_recipes::discover_local_recipes;
use goose::recipe::Recipe;

use std::path::Path;
Expand All @@ -29,7 +29,7 @@ fn short_id_from_path(path: &str) -> String {
}

pub fn get_all_recipes_manifests() -> Result<Vec<RecipeManifestWithPath>> {
let recipes_with_path = list_all_recipes_from_library()?;
let recipes_with_path = discover_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())
Expand Down
10 changes: 10 additions & 0 deletions crates/goose/src/recipe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
use std::path::Path;

use crate::agents::extension::ExtensionConfig;
use crate::agents::types::RetryConfig;
use crate::recipe::read_recipe_file_content::read_recipe_file;
use crate::utils::contains_unicode_tags;
use serde::de::Deserializer;
use serde::{Deserialize, Serialize};
Expand All @@ -13,9 +15,11 @@ use utoipa::ToSchema;
pub mod build_recipe;
pub mod read_recipe_file_content;
pub mod recipe_library;
pub mod search_local_recipes;
pub mod template_recipe;

pub const BUILT_IN_RECIPE_DIR_PARAM: &str = "recipe_dir";
pub const RECIPE_FILE_EXTENSIONS: &[&str] = &["yaml", "json"];

fn default_version() -> String {
"1.0.0".to_string()
Expand Down Expand Up @@ -308,6 +312,12 @@ impl Recipe {
retry: None,
}
}

pub fn from_file_path(file_path: &Path) -> Result<Self> {
let file = read_recipe_file(file_path)?;
Self::from_content(&file.content)
}

pub fn from_content(content: &str) -> Result<Self> {
let recipe: Recipe =
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(content) {
Expand Down
47 changes: 2 additions & 45 deletions crates/goose/src/recipe/recipe_library.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,9 @@
use crate::config::APP_STRATEGY;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider just merging this with search_local_recipes and renaming that last one to local_recipes and just do all the local_recipe work there

use crate::recipe::read_recipe_file_content::read_recipe_file;
use crate::recipe::search_local_recipes::{discover_local_recipes, get_recipe_library_dir};
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()
Expand Down Expand Up @@ -87,7 +44,7 @@ pub fn save_recipe_to_file(
default_file_path
}
};
let all_recipes = list_all_recipes_from_library()?;
let all_recipes = discover_local_recipes()?;

for (existing_path, existing_recipe) in &all_recipes {
if existing_recipe.title == recipe.title && existing_path != &file_path_value {
Expand Down
Loading
Loading