From 627a2d08ce0932241a21a281712e58427c15d478 Mon Sep 17 00:00:00 2001 From: sigoden Date: Fri, 3 Mar 2023 09:38:53 +0800 Subject: [PATCH] refactor: config path and config data use yaml other than toml for configuration. use $AICHAT_CONFIG_DIR other than $HOME for config dir. split roles to seperate config file --- .aichat.example.toml | 7 ----- Cargo.lock | 69 ++++++++++++-------------------------------- Cargo.toml | 2 +- README.md | 35 ++++++++++++++-------- src/config.rs | 50 ++++++++++++++++++++++++++++---- src/main.rs | 28 ++++-------------- 6 files changed, 92 insertions(+), 99 deletions(-) delete mode 100644 .aichat.example.toml diff --git a/.aichat.example.toml b/.aichat.example.toml deleted file mode 100644 index 0cb5910..0000000 --- a/.aichat.example.toml +++ /dev/null @@ -1,7 +0,0 @@ -api_key = "" # Request via https://platform.openai.com/account/api-keys -temperature = 1.0 # optional, see https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature -proxy = "socks5://127.0.0.1:1080" # optional, set proxy server. e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080 - -[[roles]] -name = "javascript-console" -prompt = "I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. First sentense will append to prompt:" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1498741..f400f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,8 +17,8 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_yaml", "tokio", - "toml", ] [[package]] @@ -1168,24 +1168,28 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.1" +name = "serde_urlencoded" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ + "form_urlencoded", + "itoa", + "ryu", "serde", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "serde_yaml" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "8fb06d4b6cdaef0e0c51fa881acb721bed3c924cfaa71d9c94a3b771dfdf6567" dependencies = [ - "form_urlencoded", + "indexmap", "itoa", "ryu", "serde", + "unsafe-libyaml", ] [[package]] @@ -1423,40 +1427,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "tower-service" version = "0.3.2" @@ -1522,6 +1492,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unsafe-libyaml" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7ed8ba44ca06be78ea1ad2c3682a43349126c8818054231ee6f4748012aed2" + [[package]] name = "url" version = "2.3.1" @@ -1795,15 +1771,6 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" -[[package]] -name = "winnow" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf09497b8f8b5ac5d3bb4d05c0a99be20f26fd3d5f2db7b0716e946d5103658" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 6441939..5038caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,8 @@ reedline = "0.16.0" reqwest = { version = "0.11.14", features = ["json", "stream", "socks"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" +serde_yaml = "0.9.17" tokio = { version = "1.26.0", features = ["full"] } -toml = "0.7.2" [profile.release] lto = true diff --git a/README.md b/README.md index d03b3ef..95052b2 100644 --- a/README.md +++ b/README.md @@ -21,27 +21,38 @@ Download from [Github Releases](https://github.com/sigoden/aichat/releases), unz ## Config -When starting for the first time, aichat will prompt to set `api_key`, after setting, it will automatically create the configuration file at `$HOME/.aichat.toml`. Of course, you can also manually set the configuration file. +When starting for the first time, aichat will prompt to set `api_key`, after setting, it will automatically create the configuration file. Of course, you can also manually set the configuration file. -```toml -api_key = "" # Request via https://platform.openai.com/account/api-keys -temperature = 1.0 # optional, see https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature -save_path = "/tmp/AICHAT.md" # optional, Specify a file path to save chat messages to -proxy = "socks5://127.0.0.1:1080" # optional, set proxy server. e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080 +```yaml +api_key: "" # Request via https://platform.openai.com/account/api-keys +temperature: 1.0 # optional, see https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature +save: true # optional, If set to true, aichat will save chat messages to message.md +proxy: "socks5://127.0.0.1:1080" # optional, set proxy server. e.g. http://127.0.0.1:8080 or socks5://127.0.0.1:1080 ``` -> We provide a [sample configuration file](.aichat.example.toml). +The default config dir is as follows, You can override config dir with `AICHAT_CONFIG_DIR` environment variable. + +- Linux: `/home/alice/.config/aichat` +- Windows: `C:\Users\Alice\AppData\Roaming\aichat` +- MacOS: `/Users/Alice/Library/Application Support` + +Depending on your configuration, aichat may generate the following files in the config dir: + +- `config.yaml`: the config file. +- `roles.yaml`: the roles definition file. +- `history.txt`: the repl history file. +- `messages.md`: the chat messages storage file. ## Roles We can let ChatGPT play a certain role through `prompt` to make it better generate what we want. See [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) for details. -In aichat, we can predefine a batch of roles in the configuration. For example, we define a javascript-console role as follows. +In aichat, we can predefine a batch of roles in `rules.yaml`. For example, we define a javascript-console role as follows. -```toml -[[roles]] -name = "javascript-console" -prompt = "I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. My first command is:" +```yaml +- name: javascript-console + prompt: > + I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. My first command is: ``` Let ChaGPT answer questions in the role of a javascript-console. diff --git a/src/config.rs b/src/config.rs index c99a6ef..3c2c633 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,26 +1,33 @@ use std::{ - fs::read_to_string, + env, + fs::{self, read_to_string}, path::{Path, PathBuf}, }; use anyhow::{anyhow, Result}; use serde::Deserialize; +pub const CONFIG_FILE_NAME: &str = "config.yaml"; +pub const ROLES_FILE_NAME: &str = "roles.yaml"; +pub const HISTORY_FILE_NAME: &str = "history.txt"; +pub const MESSAGE_FILE_NAME: &str = "messages.md"; + #[derive(Debug, Clone, Deserialize)] pub struct Config { /// Openai api key pub api_key: String, /// What sampling temperature to use, between 0 and 2 pub temperature: Option, - /// Specify a file path to save chat messages to - pub save_path: Option, + /// Whether to persistently save chat messages + #[serde(default)] + pub save: bool, /// Set proxy pub proxy: Option, /// Used only for debugging #[serde(default)] pub dry_run: bool, /// Predefined roles - #[serde(default)] + #[serde(default, skip_serializing)] pub roles: Vec, } @@ -28,10 +35,41 @@ impl Config { pub fn init(path: &Path) -> Result { let content = read_to_string(path) .map_err(|err| anyhow!("Failed to load config at {}, {err}", path.display()))?; - let config: Config = - toml::from_str(&content).map_err(|err| anyhow!("Invalid config, {err}"))?; + let mut config: Config = + serde_yaml::from_str(&content).map_err(|err| anyhow!("Invalid config, {err}"))?; + config.load_roles()?; Ok(config) } + pub fn local_file(name: &str) -> Result { + let env_name = format!( + "{}_CONFIG_DIR", + env!("CARGO_CRATE_NAME").to_ascii_uppercase() + ); + let mut path = match env::var(env_name) { + Ok(v) => PathBuf::from(v), + Err(_) => dirs::config_dir().ok_or_else(|| anyhow!("Not found config dir"))?, + }; + path.push(env!("CARGO_CRATE_NAME")); + if !path.exists() { + fs::create_dir_all(&path).map_err(|err| { + anyhow!("Failed to create config dir at {}, {err}", path.display()) + })?; + } + path.push(name); + Ok(path) + } + fn load_roles(&mut self) -> Result<()> { + let path = Self::local_file(ROLES_FILE_NAME)?; + if !path.exists() { + return Ok(()); + } + let content = read_to_string(&path) + .map_err(|err| anyhow!("Failed to load roles at {}, {err}", path.display()))?; + let roles: Vec = + serde_yaml::from_str(&content).map_err(|err| anyhow!("Invalid roles config, {err}"))?; + self.roles = roles; + Ok(()) + } } #[derive(Debug, Clone, Deserialize)] diff --git a/src/main.rs b/src/main.rs index 18be063..0f19adf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,10 @@ mod config; use std::fs::{File, OpenOptions}; use std::io::{stdout, Write}; use std::path::Path; -use std::path::PathBuf; use std::process::exit; use std::time::Duration; -use config::{Config, Role}; +use config::{Config, Role, CONFIG_FILE_NAME, HISTORY_FILE_NAME, MESSAGE_FILE_NAME}; use anyhow::{anyhow, Result}; use clap::{Arg, ArgAction, Command}; @@ -77,7 +76,7 @@ fn start() -> Result<()> { .collect::>() .join(" ") }); - let config_path = get_config_path()?; + let config_path = Config::local_file(CONFIG_FILE_NAME)?; if !config_path.exists() && text.is_none() { create_config_file(&config_path)?; } @@ -137,7 +136,7 @@ fn run_repl( ]), ); let history = Box::new( - FileBackedHistory::with_file(1000, get_history_path()?) + FileBackedHistory::with_file(1000, Config::local_file(HISTORY_FILE_NAME)?) .map_err(|err| anyhow!("Failed to setup history file, {err}"))?, ); let edit_mode = Box::new(Emacs::new(keybindings)); @@ -150,17 +149,12 @@ fn run_repl( let mut trigged_ctrlc = false; let mut output = String::new(); let mut role: Option = None; - let mut save_file: Option = if let Some(path) = &config.save_path { + let mut save_file: Option = if config.save { let file = OpenOptions::new() .create(true) .append(true) - .open(path) - .map_err(|err| { - anyhow!( - "Failed to create/append save_file at {}, {err}", - path.display() - ) - })?; + .open(Config::local_file(MESSAGE_FILE_NAME)?) + .map_err(|err| anyhow!("Failed to create/append save_file, {err}"))?; Some(file) } else { None @@ -440,16 +434,6 @@ fn dump(text: T, newlines: usize) { stdout().flush().unwrap(); } -fn get_config_path() -> Result { - let config_dir = dirs::home_dir().ok_or_else(|| anyhow!("No home dir"))?; - Ok(config_dir.join(format!(".{}.toml", env!("CARGO_CRATE_NAME")))) -} - -fn get_history_path() -> Result { - let config_dir = dirs::home_dir().ok_or_else(|| anyhow!("No home dir"))?; - Ok(config_dir.join(format!(".{}_history", env!("CARGO_CRATE_NAME")))) -} - fn print_repl_title() { println!("Welcome to aichat {}", env!("CARGO_PKG_VERSION")); println!("Type \".help\" for more information.");