diff --git a/crates/goose-cli/.gitignore b/crates/goose-cli/.gitignore new file mode 100644 index 0000000000..94a2ce173a --- /dev/null +++ b/crates/goose-cli/.gitignore @@ -0,0 +1,3 @@ + +.goosehints +.goose \ No newline at end of file diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 97309df21d..885593dd4e 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -26,4 +26,6 @@ tokio = { version = "1.0", features = ["full"] } futures = "0.3" serde = { version = "1.0", features = ["derive"] } # For serialization serde_yaml = "0.9" -dirs = "4.0" \ No newline at end of file +dirs = "4.0" +strum = "0.26" +strum_macros = "0.26" \ No newline at end of file diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 0e5eef2550..e44b053f1d 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -1,70 +1,82 @@ -use cliclack::input; +use crate::commands::expected_config::{get_recommended_models, RecommendedModels}; +use crate::inputs::inputs::get_user_input; +use crate::profile::profile::Profile; +use crate::profile::profile_handler::{find_existing_profile, profile_path, save_profile}; +use crate::profile::provider_helper::{get_provider_type, select_provider_lists, set_provider_config, PROVIDER_OPEN_AI}; use console::style; -use ctrlc; -use serde::{Deserialize, Serialize}; +use goose::providers::factory; +use goose::providers::types::message::Message; use std::error::Error; -use std::fs::{create_dir_all, File}; -use std::io::Write; -use std::path::PathBuf; +use cliclack::spinner; +use goose::providers::configs::ProviderConfig; -#[derive(Serialize, Deserialize)] -pub struct ConfigOptions { - pub provider: Option, - pub host: Option, - pub token: Option, - pub processor: Option, - pub accelerator: Option, -} +pub async fn handle_configure(provided_profile_name: Option) -> Result<(), Box> { + cliclack::intro(style(" configure-goose ").on_cyan().black())?; + println!("We are helping you configure your Goose CLI profile."); + let profile_name = provided_profile_name.unwrap_or_else(|| { + get_user_input("Enter profile name:", "default").unwrap() + }); + let existing_profile_result = get_existing_profile(&profile_name); + let existing_profile = existing_profile_result.as_ref(); -pub fn handle_configure(options: ConfigOptions) -> Result<(), Box> { - ctrlc::set_handler(move || {}).expect("setting Ctrl-C handler"); - - cliclack::clear_screen()?; - - cliclack::intro(style(" create-app ").on_cyan().black())?; - let provider = prompt(options.provider, "Enter provider name:"); - let host = prompt(options.host, "Enter host URL:"); - let token = prompt(options.token, "Enter token:"); - let processor = prompt(options.processor, "Enter processor:"); - let accelerator = prompt(options.accelerator, "Enter accelerator:"); - - let final_config = ConfigOptions { - provider: Some(provider), - host: Some(host), - token: Some(token), - processor: Some(processor), - accelerator: Some(accelerator), + let provider_name = select_provider(existing_profile); + let recommended_models = get_recommended_models(&provider_name); + let processor = set_processor(existing_profile, &recommended_models)?; + let accelerator = set_accelerator(existing_profile, &recommended_models)?; + let provider_config = set_provider_config(&provider_name, processor.clone()); + let profile = Profile { + provider: provider_name.to_string(), + processor: processor.clone(), + accelerator, }; - match save_to_yaml(&final_config) { - Ok(path) => println!("\nConfiguration saved to: {:?}", path), - Err(e) => eprintln!("Failed to save configuration: {}", e), + match save_profile(profile_name.as_str(), profile) { + Ok(()) => println!("\nProfile saved to: {:?}", profile_path()?), + Err(e) => println!("Failed to save profile: {}", e), } + check_configuration(provider_name, provider_config).await?; Ok(()) } -// Helper function to prompt the user -fn prompt(value: Option, message: &str) -> String { - value.unwrap_or_else(|| input(message).interact().expect("Failed to get input")) +async fn check_configuration(provider_name: &str, provider_config: ProviderConfig) -> Result<(), Box> { + let spin = spinner(); + spin.start("Now let's check your configuration..."); + let provider = factory::get_provider(get_provider_type(provider_name), provider_config).unwrap(); + let message = Message::user("Please give a nice welcome messsage (one sentence) and let them know they are all set to use this agent ").unwrap(); + let result = provider.complete( + "You are an AI agent called Goose. You use tools of connected systems to solve problems.", + &[message], &[]).await?; + spin.stop(result.0.text()); + Ok(()) } -fn save_to_yaml(config: &ConfigOptions) -> Result> { - // Locate the config directory - let mut path = dirs::home_dir().ok_or("Failed to find home directory")?; - path.push(".config"); - path.push("goose"); - - // TODO: set to profile1.yaml temporarily to avoid overriting the existing config - path.push("profile1.yaml"); - - // Ensure the ~/.config/goose directory exists - if let Some(parent) = path.parent() { - create_dir_all(parent)?; // Create the directory if it doesn't exist +fn get_existing_profile(profile_name: &String) -> Option { + let existing_profile_result = find_existing_profile(profile_name.as_str()); + if existing_profile_result.is_some() { + println!("Profile already exists. We are going to overwriting the existing profile..."); + } else { + println!("We are creating a new profile..."); } - - // Serialize the configuration to YAML and save it to the file - let yaml_string = serde_yaml::to_string(config)?; - let mut file = File::create(&path)?; - file.write_all(yaml_string.as_bytes())?; - - Ok(path) + existing_profile_result } + +fn set_processor(existing_profile: Option<&Profile>, recommended_models: &RecommendedModels) -> Result> { + let default_processor_value = existing_profile + .map_or(recommended_models.processor, |profile| profile.processor.as_str()); + let processor = get_user_input("Enter processor:", default_processor_value)?; + Ok(processor) +} + +fn set_accelerator(existing_profile: Option<&Profile>, recommended_models: &RecommendedModels) -> Result> { + let default_accelerator_value = existing_profile + .map_or(recommended_models.accelerator, |profile| profile.accelerator.as_str()); + let processor = get_user_input("Enter accelerator:", default_accelerator_value)?; + Ok(processor) +} + +fn select_provider(existing_profile: Option<&Profile>) -> &str { + let default_value = existing_profile + .map_or(PROVIDER_OPEN_AI, |profile| profile.provider.as_str()); + cliclack::select("Select provider:") + .initial_value(default_value).items(&select_provider_lists()).interact().unwrap() +} + diff --git a/crates/goose-cli/src/commands/expected_config.rs b/crates/goose-cli/src/commands/expected_config.rs new file mode 100644 index 0000000000..8bf7c6af7c --- /dev/null +++ b/crates/goose-cli/src/commands/expected_config.rs @@ -0,0 +1,21 @@ +// This is a temporary file to simulate some configuration data from the backend + +use crate::profile::provider_helper::PROVIDER_OPEN_AI; + +pub struct RecommendedModels { + pub processor: &'static str, + pub accelerator: &'static str, +} +pub fn get_recommended_models(provider_name: &str) -> RecommendedModels { + if provider_name == PROVIDER_OPEN_AI { + RecommendedModels { + processor: "gpt-4o", + accelerator: "gpt-4o-mini", + } + } else { + RecommendedModels { + processor: "claude-3-5-sonnet-2", + accelerator: "claude-3-5-sonnet-2", + } + } +} \ No newline at end of file diff --git a/crates/goose-cli/src/commands/mod.rs b/crates/goose-cli/src/commands/mod.rs index 87064d9a93..b90ad2f44d 100644 --- a/crates/goose-cli/src/commands/mod.rs +++ b/crates/goose-cli/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod configure; pub mod version; +pub mod expected_config; \ No newline at end of file diff --git a/crates/goose-cli/src/commands/version.rs b/crates/goose-cli/src/commands/version.rs index 632c8bacd2..6d71cc4008 100644 --- a/crates/goose-cli/src/commands/version.rs +++ b/crates/goose-cli/src/commands/version.rs @@ -1,3 +1,3 @@ pub fn print_version() { println!(env!("CARGO_PKG_VERSION")) -} +} \ No newline at end of file diff --git a/crates/goose-cli/src/inputs/inputs.rs b/crates/goose-cli/src/inputs/inputs.rs new file mode 100644 index 0000000000..967b41d057 --- /dev/null +++ b/crates/goose-cli/src/inputs/inputs.rs @@ -0,0 +1,22 @@ +use cliclack::{input, password}; + +pub fn get_env_value_or_input(env_name: &str, input_prompt: &str, mask: bool) -> String { + if let Ok(value) = std::env::var(env_name) { + return value; + } + + let input_value = if mask { + password(input_prompt).mask('▪').interact().unwrap() + } else { + input(input_prompt).interact().unwrap() + }; + + std::env::set_var(env_name, &input_value); + input_value +} + +pub fn get_user_input(message: &str, default_value: &str) -> std::io::Result { + input(message) + .default_input(default_value) + .interact() +} \ No newline at end of file diff --git a/crates/goose-cli/src/inputs/mod.rs b/crates/goose-cli/src/inputs/mod.rs new file mode 100644 index 0000000000..ee594c8f9d --- /dev/null +++ b/crates/goose-cli/src/inputs/mod.rs @@ -0,0 +1 @@ +pub mod inputs; \ No newline at end of file diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 9584d5c3ef..194b603475 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -1,4 +1,6 @@ mod commands; +mod profile; +mod inputs; use anyhow::Result; use bat::PrettyPrinter; @@ -6,18 +8,17 @@ use clap::{Parser, Subcommand}; use cliclack::{input, spinner}; use console::style; use futures::StreamExt; +use goose::providers::factory::ProviderType; +use commands::configure::handle_configure; +use commands::version::print_version; use goose::agent::Agent; use goose::developer::DeveloperSystem; use goose::providers::configs::OpenAiProviderConfig; use goose::providers::configs::{DatabricksProviderConfig, ProviderConfig}; use goose::providers::factory; -use goose::providers::factory::ProviderType; use goose::providers::types::message::Message; -use commands::configure::{handle_configure, ConfigOptions}; -use commands::version::print_version; - #[derive(Parser)] #[command(author, about, long_about = None)] struct Cli { @@ -59,27 +60,8 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Configure the provider and default systems Configure { - /// Optional provider name; prompted if not provided - #[arg(long)] - provider: Option, - - /// Optional host URL; prompted if not provided - #[arg(long)] - host: Option, - - /// Optional token; prompted if not provided - #[arg(long)] - token: Option, - - /// Optional processor; prompted if not provided - #[arg(long)] - processor: Option, - - /// Optional accelerator; prompted if not provided - #[arg(long)] - accelerator: Option, + profile_name: Option, }, /// Start or resume sessions with an optional session name Session { @@ -106,26 +88,14 @@ async fn main() -> Result<()> { } match cli.command { - Some(Command::Configure { - provider, - host, - token, - processor, - accelerator, - }) => { - let options = ConfigOptions { - provider, - host, - token, - processor, - accelerator, - }; - let _ = handle_configure(options); + Some(Command::Configure {profile_name}) => { + let _ = handle_configure(profile_name).await; return Ok(()); } Some(Command::Session { session_name }) => { - let session_name = session_name - .unwrap_or_else(|| input("Session name:").placeholder("").interact().unwrap()); + let session_name = session_name.unwrap_or_else(|| { + input("Session name:").placeholder("").interact().unwrap() + }); println!("Session name: {}", session_name); return Ok(()); } @@ -220,4 +190,4 @@ fn create_provider_config(cli: &Cli) -> ProviderConfig { max_tokens: cli.max_tokens, }), } -} +} \ No newline at end of file diff --git a/crates/goose-cli/src/profile/mod.rs b/crates/goose-cli/src/profile/mod.rs new file mode 100644 index 0000000000..091728fcf1 --- /dev/null +++ b/crates/goose-cli/src/profile/mod.rs @@ -0,0 +1,3 @@ +pub mod profile; +pub mod profile_handler; +pub mod provider_helper; \ No newline at end of file diff --git a/crates/goose-cli/src/profile/profile.rs b/crates/goose-cli/src/profile/profile.rs new file mode 100644 index 0000000000..bf6c17e5a3 --- /dev/null +++ b/crates/goose-cli/src/profile/profile.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug)] +pub struct Profile { + pub provider: String, + pub processor: String, + pub accelerator: String, +} + +#[derive(Serialize, Deserialize)] +pub struct Profiles { + pub profile_items: std::collections::HashMap +} diff --git a/crates/goose-cli/src/profile/profile_handler.rs b/crates/goose-cli/src/profile/profile_handler.rs new file mode 100644 index 0000000000..41e81d020e --- /dev/null +++ b/crates/goose-cli/src/profile/profile_handler.rs @@ -0,0 +1,57 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fs::{create_dir_all, File}; +use std::io::Write; +use std::path::PathBuf; +use crate::profile::profile::Profile; + +// TODO: set to profile-1.0.yaml temporarily to avoid overriting the existing config +pub const PROFILE_CONFIG_PATH: &str = ".config/goose/profile-1.0.yaml"; + +fn save_profiles_to_file(profiles: &HashMap) -> Result<(), Box> { + let path = profile_path()?; + + if let Some(parent) = path.parent() { + create_dir_all(parent)?; + } + + let yaml_string = serde_yaml::to_string(profiles)?; + let mut file = File::create(&path)?; + file.write_all(yaml_string.as_bytes())?; + Ok(()) +} + +pub fn profile_path() -> Result> { + let mut path = dirs::home_dir().ok_or("Failed to find home directory")?; + path.push(PROFILE_CONFIG_PATH); + Ok(path) +} + +pub fn save_profile(profile_name: &str, new_profile: Profile) -> Result<(), Box> { + let mut profiles = load_profiles().unwrap(); + profiles.insert(profile_name.to_string(), new_profile); + let _ = save_profiles_to_file(&profiles); + Ok(()) +} + +fn profile_file_exists() -> bool { + profile_path().unwrap().exists() +} +pub fn load_profiles() -> Result, Box> { + let path = profile_path()?; + if !path.exists() { + return Ok(HashMap::new()); + } + let file = File::open(&path)?; + let profiles: HashMap = serde_yaml::from_reader(file)?; + Ok(profiles) +} + +pub fn find_existing_profile(profile_name: &str) -> Option { + if profile_file_exists() { + let profiles = load_profiles().unwrap(); + profiles.get(profile_name).cloned() + } else { + None + } +} \ No newline at end of file diff --git a/crates/goose-cli/src/profile/provider_helper.rs b/crates/goose-cli/src/profile/provider_helper.rs new file mode 100644 index 0000000000..e8d7624ac6 --- /dev/null +++ b/crates/goose-cli/src/profile/provider_helper.rs @@ -0,0 +1,45 @@ +use goose::providers::factory::ProviderType; +use strum::IntoEnumIterator; +use goose::providers::configs::{DatabricksProviderConfig, OpenAiProviderConfig, ProviderConfig}; +use crate::inputs::inputs::get_env_value_or_input; + +pub const PROVIDER_OPEN_AI: &str = "openai"; +pub const PROVIDER_DATABRICKS: &str = "databricks"; + +pub fn select_provider_lists() -> Vec<(&'static str, String, &'static str)> { + ProviderType::iter() + .map(|provider| { + match provider { + ProviderType::OpenAi => (PROVIDER_OPEN_AI, PROVIDER_OPEN_AI.to_string(), "Recommended"), + ProviderType::Databricks => (PROVIDER_DATABRICKS, PROVIDER_DATABRICKS.to_string(), ""), + } + }).collect() +} + +pub fn set_provider_config(provider_name: &str, processor: String) -> ProviderConfig { + match provider_name.to_lowercase().as_str() { + PROVIDER_OPEN_AI => ProviderConfig::OpenAi(OpenAiProviderConfig { + host: "https://api.openai.com".to_string(), + api_key: get_env_value_or_input("OPENAI_API_KEY", "Please enter your OpenAI API key:", true), + model: processor, + temperature: None, + max_tokens: None, + }), + PROVIDER_DATABRICKS => ProviderConfig::Databricks(DatabricksProviderConfig { + host: get_env_value_or_input("DATABRICKS_HOST", "Please enter your Databricks host:", false), + token: get_env_value_or_input("DATABRICKS_TOKEN", "Please enter your Databricks token:", true), + model: processor, + temperature: None, + max_tokens: None, + }), + _ => panic!("Invalid provider name"), + } +} + +pub fn get_provider_type(provider_name: &str) -> ProviderType { + match provider_name.to_lowercase().as_str() { + PROVIDER_OPEN_AI => ProviderType::OpenAi, + PROVIDER_DATABRICKS => ProviderType::Databricks, + _ => panic!("Invalid provider name"), + } +} \ No newline at end of file diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 3508f6979c..dabaf445a7 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -20,6 +20,8 @@ uuid = { version = "1.0", features = ["v4"] } regex = "1.11.1" async-trait = "0.1" async-stream = "0.3" +strum = "0.26" +strum_macros = "0.26" tera = "1.20.0" tokenizers = "0.20.3" include_dir = "0.7.4" diff --git a/crates/goose/src/providers/factory.rs b/crates/goose/src/providers/factory.rs index ed4e34fa4a..de41700aff 100644 --- a/crates/goose/src/providers/factory.rs +++ b/crates/goose/src/providers/factory.rs @@ -2,7 +2,9 @@ use super::{ base::Provider, configs::ProviderConfig, databricks::DatabricksProvider, openai::OpenAiProvider, }; use anyhow::Error; +use strum_macros::{EnumIter}; +#[derive(EnumIter, Debug)] pub enum ProviderType { OpenAi, Databricks,