|
|
|
@ -23,11 +23,14 @@ use tokio::runtime::Runtime;
|
|
|
|
|
|
|
|
|
|
const API_URL: &str = "https://api.openai.com/v1/chat/completions";
|
|
|
|
|
const MODEL: &str = "gpt-3.5-turbo";
|
|
|
|
|
const HELP: &str = r###".exit Exit the REPL.
|
|
|
|
|
.help Print this help message.
|
|
|
|
|
.role Specify the role that the AI will play.
|
|
|
|
|
|
|
|
|
|
Press Ctrl+C to abort current chat, Ctrl+D to exit the REPL"###;
|
|
|
|
|
const REPL_COMMANDS: [(&str, &str); 6] = [
|
|
|
|
|
(".clear", "Clear the screen"),
|
|
|
|
|
(".clear-history", "Clear the history"),
|
|
|
|
|
(".exit", " Exit the REPL"),
|
|
|
|
|
(".help", "Print this help message"),
|
|
|
|
|
(".history", "Print the history"),
|
|
|
|
|
(".role", "Specify the role that the AI will play"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
if let Err(err) = start() {
|
|
|
|
@ -93,8 +96,37 @@ fn start() -> Result<()> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run_repl(runtime: Runtime, client: Client, config: Config, role: Option<String>) -> Result<()> {
|
|
|
|
|
println!("Welcome to aichat {}", env!("CARGO_PKG_VERSION"));
|
|
|
|
|
println!("Type \".help\" for more information.");
|
|
|
|
|
print_repl_title();
|
|
|
|
|
let mut commands: Vec<String> = REPL_COMMANDS
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(v, _)| v.to_string())
|
|
|
|
|
.collect();
|
|
|
|
|
commands.extend(config.roles.iter().map(|v| format!(".role {}", v.name)));
|
|
|
|
|
let mut completer = DefaultCompleter::with_inclusions(&['.', '-']).set_min_word_len(2);
|
|
|
|
|
completer.insert(commands.clone());
|
|
|
|
|
let completer = Box::new(completer);
|
|
|
|
|
let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu"));
|
|
|
|
|
let mut keybindings = default_emacs_keybindings();
|
|
|
|
|
keybindings.add_binding(
|
|
|
|
|
KeyModifiers::NONE,
|
|
|
|
|
KeyCode::Tab,
|
|
|
|
|
ReedlineEvent::UntilFound(vec![
|
|
|
|
|
ReedlineEvent::Menu("completion_menu".to_string()),
|
|
|
|
|
ReedlineEvent::MenuNext,
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
let history = Box::new(
|
|
|
|
|
FileBackedHistory::with_file(1000, get_history_path()?)
|
|
|
|
|
.map_err(|err| anyhow!("Failed to setup history file, {err}"))?,
|
|
|
|
|
);
|
|
|
|
|
let edit_mode = Box::new(Emacs::new(keybindings));
|
|
|
|
|
let mut line_editor = Reedline::create()
|
|
|
|
|
.with_completer(completer)
|
|
|
|
|
.with_history(history)
|
|
|
|
|
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
|
|
|
|
|
.with_edit_mode(edit_mode);
|
|
|
|
|
let prompt = DefaultPrompt::new(DefaultPromptSegment::Empty, DefaultPromptSegment::Empty);
|
|
|
|
|
|
|
|
|
|
let send_line = |line: String| -> Result<()> {
|
|
|
|
|
if line.is_empty() {
|
|
|
|
|
return Ok(());
|
|
|
|
@ -104,8 +136,7 @@ fn run_repl(runtime: Runtime, client: Client, config: Config, role: Option<Strin
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let handle_line = |line: String| -> Result<bool> {
|
|
|
|
|
let handle_line = |line_editor: &mut Reedline, line: String| -> Result<bool> {
|
|
|
|
|
if line.starts_with('.') {
|
|
|
|
|
let (name, args) = match line.split_once(' ') {
|
|
|
|
|
Some((head, tail)) => (head, Some(tail.trim())),
|
|
|
|
@ -116,7 +147,19 @@ fn run_repl(runtime: Runtime, client: Client, config: Config, role: Option<Strin
|
|
|
|
|
return Ok(true);
|
|
|
|
|
}
|
|
|
|
|
".help" => {
|
|
|
|
|
dump(HELP);
|
|
|
|
|
dump(get_repl_help());
|
|
|
|
|
}
|
|
|
|
|
".clear" => {
|
|
|
|
|
line_editor.clear_scrollback()?;
|
|
|
|
|
}
|
|
|
|
|
".clear-history" => {
|
|
|
|
|
let history = Box::new(line_editor.history_mut());
|
|
|
|
|
history
|
|
|
|
|
.clear()
|
|
|
|
|
.map_err(|err| anyhow!("Failed to clear history, {err}"))?;
|
|
|
|
|
}
|
|
|
|
|
".history" => {
|
|
|
|
|
line_editor.print_history()?;
|
|
|
|
|
}
|
|
|
|
|
".role" => match args {
|
|
|
|
|
Some(name) => match config.roles.iter().find(|v| v.name == name) {
|
|
|
|
@ -137,40 +180,14 @@ fn run_repl(runtime: Runtime, client: Client, config: Config, role: Option<Strin
|
|
|
|
|
Ok(false)
|
|
|
|
|
};
|
|
|
|
|
if let Some(name) = role {
|
|
|
|
|
handle_line(format!("role {name}"))?;
|
|
|
|
|
handle_line(&mut line_editor, format!("role {name}"))?;
|
|
|
|
|
}
|
|
|
|
|
let mut commands = vec![".help".into(), ".exit".into(), ".role".into()];
|
|
|
|
|
commands.extend(config.roles.iter().map(|v| format!(".role {}", v.name)));
|
|
|
|
|
let mut completer = DefaultCompleter::with_inclusions(&['.']).set_min_word_len(2);
|
|
|
|
|
completer.insert(commands.clone());
|
|
|
|
|
let completer = Box::new(completer);
|
|
|
|
|
let completion_menu = Box::new(ColumnarMenu::default().with_name("completion_menu"));
|
|
|
|
|
let mut keybindings = default_emacs_keybindings();
|
|
|
|
|
keybindings.add_binding(
|
|
|
|
|
KeyModifiers::NONE,
|
|
|
|
|
KeyCode::Tab,
|
|
|
|
|
ReedlineEvent::UntilFound(vec![
|
|
|
|
|
ReedlineEvent::Menu("completion_menu".to_string()),
|
|
|
|
|
ReedlineEvent::MenuNext,
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
let history = Box::new(
|
|
|
|
|
FileBackedHistory::with_file(1000, get_history_path()?)
|
|
|
|
|
.map_err(|err| anyhow!("Failed to setup history file, {err}"))?,
|
|
|
|
|
);
|
|
|
|
|
let edit_mode = Box::new(Emacs::new(keybindings));
|
|
|
|
|
let mut line_editor = Reedline::create()
|
|
|
|
|
.with_completer(completer)
|
|
|
|
|
.with_history(history)
|
|
|
|
|
.with_menu(ReedlineMenu::EngineCompleter(completion_menu))
|
|
|
|
|
.with_edit_mode(edit_mode);
|
|
|
|
|
let prompt = DefaultPrompt::new(DefaultPromptSegment::Empty, DefaultPromptSegment::Empty);
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
let sig = line_editor.read_line(&prompt);
|
|
|
|
|
match sig {
|
|
|
|
|
Ok(Signal::Success(line)) => {
|
|
|
|
|
let quit = handle_line(line)?;
|
|
|
|
|
let quit = handle_line(&mut line_editor, line)?;
|
|
|
|
|
if quit {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
@ -309,3 +326,17 @@ 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.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_repl_help() -> String {
|
|
|
|
|
let head = REPL_COMMANDS
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(name, desc)| format!("{name:<15} {desc}"))
|
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
|
.join("\n");
|
|
|
|
|
format!("{head}\n\nPress Ctrl+C/Ctrl+D to exit the REPL")
|
|
|
|
|
}
|
|
|
|
|