summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.aichat.example.toml7
-rw-r--r--Cargo.lock69
-rw-r--r--Cargo.toml2
-rw-r--r--README.md35
-rw-r--r--src/config.rs50
-rw-r--r--src/main.rs28
6 files changed, 92 insertions, 99 deletions
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 = "<YOUR SECRET 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]]
@@ -1424,40 +1428,6 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1523,6 +1493,12 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1796,15 +1772,6 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
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 = "<YOUR SECRET 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: "<YOUR SECRET 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<f64>,
- /// Specify a file path to save chat messages to
- pub save_path: Option<PathBuf>,
+ /// Whether to persistently save chat messages
+ #[serde(default)]
+ pub save: bool,
/// Set proxy
pub proxy: Option<String>,
/// Used only for debugging
#[serde(default)]
pub dry_run: bool,
/// Predefined roles
- #[serde(default)]
+ #[serde(default, skip_serializing)]
pub roles: Vec<Role>,
}
@@ -28,10 +35,41 @@ impl Config {
pub fn init(path: &Path) -> Result<Config> {
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<PathBuf> {
+ 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<Role> =
+ 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::<Vec<String>>()
.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<Role> = None;
- let mut save_file: Option<File> = if let Some(path) = &config.save_path {
+ let mut save_file: Option<File> = 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<T: ToString>(text: T, newlines: usize) {
stdout().flush().unwrap();
}
-fn get_config_path() -> Result<PathBuf> {
- 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<PathBuf> {
- 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.");