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
pull/1/head
sigoden 1 year ago
parent 9e8a5481cf
commit 627a2d08ce

@ -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:"

69
Cargo.lock generated

@ -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"

@ -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

@ -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.

@ -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)]

@ -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.");

Loading…
Cancel
Save