Lifei/v1.0 gooose configure cli (#245)

Implemented goose configure cli
This commit is contained in:
Lifei Zhou
2024-11-17 20:05:50 +11:00
committed by GitHub
parent 18113507f6
commit 06a9be4a0e
15 changed files with 256 additions and 101 deletions
+3
View File
@@ -0,0 +1,3 @@
.goosehints
.goose
+3 -1
View File
@@ -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"
dirs = "4.0"
strum = "0.26"
strum_macros = "0.26"
+69 -57
View File
@@ -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<String>,
pub host: Option<String>,
pub token: Option<String>,
pub processor: Option<String>,
pub accelerator: Option<String>,
}
pub async fn handle_configure(provided_profile_name: Option<String>) -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
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<String>, 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<dyn Error>> {
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<PathBuf, Box<dyn Error>> {
// 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<Profile> {
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<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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()
}
@@ -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",
}
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod configure;
pub mod version;
pub mod expected_config;
+1 -1
View File
@@ -1,3 +1,3 @@
pub fn print_version() {
println!(env!("CARGO_PKG_VERSION"))
}
}
+22
View File
@@ -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<String> {
input(message)
.default_input(default_value)
.interact()
}
+1
View File
@@ -0,0 +1 @@
pub mod inputs;
+12 -42
View File
@@ -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<String>,
/// Optional host URL; prompted if not provided
#[arg(long)]
host: Option<String>,
/// Optional token; prompted if not provided
#[arg(long)]
token: Option<String>,
/// Optional processor; prompted if not provided
#[arg(long)]
processor: Option<String>,
/// Optional accelerator; prompted if not provided
#[arg(long)]
accelerator: Option<String>,
profile_name: Option<String>,
},
/// 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,
}),
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod profile;
pub mod profile_handler;
pub mod provider_helper;
+14
View File
@@ -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<String, Profile>
}
@@ -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<String, Profile>) -> Result<(), Box<dyn Error>> {
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<PathBuf, Box<dyn Error>> {
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<dyn Error>> {
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<HashMap<String, Profile>, Box<dyn Error>> {
let path = profile_path()?;
if !path.exists() {
return Ok(HashMap::new());
}
let file = File::open(&path)?;
let profiles: HashMap<String, Profile> = serde_yaml::from_reader(file)?;
Ok(profiles)
}
pub fn find_existing_profile(profile_name: &str) -> Option<Profile> {
if profile_file_exists() {
let profiles = load_profiles().unwrap();
profiles.get(profile_name).cloned()
} else {
None
}
}
@@ -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"),
}
}
+2
View File
@@ -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"
+2
View File
@@ -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,