summaryrefslogtreecommitdiffstats
path: root/src/repl
diff options
context:
space:
mode:
Diffstat (limited to 'src/repl')
-rw-r--r--src/repl/mod.rs671
1 files changed, 339 insertions, 332 deletions
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index 136d113..87befd3 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -7,7 +7,9 @@ use self::highlighter::ReplHighlighter;
use self::prompt::ReplPrompt;
use crate::client::{call_chat_completions, call_chat_completions_streaming};
-use crate::config::{AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags};
+use crate::config::{
+ macro_execute, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags,
+};
use crate::render::render_error;
use crate::utils::{
abortable_run_with_spinner, create_abort_signal, set_text, temp_file, AbortSignal,
@@ -26,7 +28,7 @@ use std::{env, process};
const MENU_NAME: &str = "completion_menu";
lazy_static::lazy_static! {
- static ref REPL_COMMANDS: [ReplCommand; 34] = [
+ static ref REPL_COMMANDS: [ReplCommand; 35] = [
ReplCommand::new(".help", "Show this help message", AssertState::pass()),
ReplCommand::new(".info", "View system info", AssertState::pass()),
ReplCommand::new(".model", "Change the current LLM", AssertState::pass()),
@@ -147,6 +149,11 @@ lazy_static::lazy_static! {
AssertState::TrueFalse(StateFlags::RAG, StateFlags::AGENT),
),
ReplCommand::new(
+ ".macro",
+ "Execute a macro",
+ AssertState::pass()
+ ),
+ ReplCommand::new(
".file",
"Include files, directories, URLs or commands",
AssertState::pass()
@@ -192,7 +199,13 @@ impl Repl {
if AssertState::False(StateFlags::AGENT | StateFlags::RAG)
.assert(self.config.read().state())
{
- self.banner();
+ print!(
+ r#"Welcome to {} {}
+Type ".help" for additional help.
+"#,
+ env!("CARGO_CRATE_NAME"),
+ env!("CARGO_PKG_VERSION"),
+ )
}
loop {
@@ -203,7 +216,7 @@ impl Repl {
match sig {
Ok(Signal::Success(line)) => {
self.abort_signal.reset();
- match self.handle(&line).await {
+ match run_repl_command(&self.config, self.abort_signal.clone(), &line).await {
Ok(exit) => {
if exit {
break;
@@ -230,313 +243,6 @@ impl Repl {
Ok(())
}
- async fn handle(&self, mut line: &str) -> Result<bool> {
- if let Ok(Some(captures)) = MULTILINE_RE.captures(line) {
- if let Some(text_match) = captures.get(1) {
- line = text_match.as_str();
- }
- }
- match parse_command(line) {
- Some((cmd, args)) => match cmd {
- ".help" => {
- dump_repl_help();
- }
- ".info" => match args {
- Some("role") => {
- let info = self.config.read().role_info()?;
- print!("{}", info);
- }
- Some("session") => {
- let info = self.config.read().session_info()?;
- print!("{}", info);
- }
- Some("rag") => {
- let info = self.config.read().rag_info()?;
- print!("{}", info);
- }
- Some("agent") => {
- let info = self.config.read().agent_info()?;
- print!("{}", info);
- }
- Some(_) => unknown_command()?,
- None => {
- let output = self.config.read().sysinfo()?;
- print!("{}", output);
- }
- },
- ".model" => match args {
- Some(name) => {
- self.config.write().set_model(name)?;
- }
- None => println!("Usage: .model <name>"),
- },
- ".prompt" => match args {
- Some(text) => {
- self.config.write().use_prompt(text)?;
- }
- None => println!("Usage: .prompt <text>..."),
- },
- ".role" => match args {
- Some(args) => match args.split_once(['\n', ' ']) {
- Some((name, text)) => {
- let role = self.config.read().retrieve_role(name.trim())?;
- let input = Input::from_str(&self.config, text, Some(role));
- ask(&self.config, self.abort_signal.clone(), input, false).await?;
- }
- None => {
- let name = args;
- if Config::has_role(name) {
- self.config.write().use_role(name)?;
- } else {
- self.config.write().new_role(name)?;
- }
- }
- },
- None => println!(
- r#"Usage:
- .role <name> # If the role exists, switch to it; otherwise, create a new role
- .role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
- ),
- },
- ".session" => {
- self.config.write().use_session(args)?;
- Config::maybe_autoname_session(self.config.clone());
- }
- ".rag" => {
- Config::use_rag(&self.config, args, self.abort_signal.clone()).await?;
- }
- ".agent" => match split_args(args) {
- Some((agent_name, session_name)) => {
- Config::use_agent(
- &self.config,
- agent_name,
- session_name,
- self.abort_signal.clone(),
- )
- .await?;
- }
- None => println!(r#"Usage: .agent <agent-name> [session-name]"#),
- },
- ".starter" => match args {
- Some(value) => {
- let input = Input::from_str(&self.config, value, None);
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- None => {
- let banner = self.config.read().agent_banner()?;
- self.config.read().print_markdown(&banner)?;
- }
- },
- ".variable" => match args {
- Some(args) => {
- self.config.write().set_agent_variable(args)?;
- }
- _ => {
- println!("Usage: .variable <key> <value>")
- }
- },
- ".save" => match split_args(args) {
- Some(("role", name)) => {
- self.config.write().save_role(name)?;
- }
- Some(("session", name)) => {
- self.config.write().save_session(name)?;
- }
- _ => {
- println!(r#"Usage: .save <role|session> [name]"#)
- }
- },
- ".edit" => match args {
- Some("role") => {
- self.config.write().edit_role()?;
- }
- Some("session") => {
- self.config.write().edit_session()?;
- }
- Some("rag-docs") => {
- Config::edit_rag_docs(&self.config, self.abort_signal.clone()).await?;
- }
- _ => {
- println!(r#"Usage: .edit <role|session|rag-docs>"#)
- }
- },
- ".compress" => match args {
- Some("session") => {
- abortable_run_with_spinner(
- Config::compress_session(&self.config),
- "Compressing",
- self.abort_signal.clone(),
- )
- .await?;
- println!("✓ Successfully compressed the session.");
- }
- _ => {
- println!(r#"Usage: .compress session"#)
- }
- },
- ".empty" => match args {
- Some("session") => {
- self.config.write().empty_session()?;
- }
- _ => {
- println!(r#"Usage: .empty session"#)
- }
- },
- ".rebuild" => match args {
- Some("rag") => {
- Config::rebuild_rag(&self.config, self.abort_signal.clone()).await?;
- }
- _ => {
- println!(r#"Usage: .rebuild rag"#)
- }
- },
- ".sources" => match args {
- Some("rag") => {
- let output = Config::rag_sources(&self.config)?;
- println!("{}", output);
- }
- _ => {
- println!(r#"Usage: .sources rag"#)
- }
- },
- ".file" => match args {
- Some(args) => {
- let (files, text) = split_files_text(args, cfg!(windows));
- let input = Input::from_files_with_spinner(
- &self.config,
- text,
- files,
- None,
- self.abort_signal.clone(),
- )
- .await?;
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- None => println!(
- r#"Usage: .file <file|dir|url|%%|cmd>... [-- <text>...]
-
-.file /tmp/file.txt
-.file src/ Cargo.toml -- analyze
-.file https://example.com/file.txt -- summarize
-.file https://example.com/image.png -- recognize text
-.file %% -- translate last reply to english
-.file `git diff` -- Generate git commit message"#
- ),
- },
- ".continue" => {
- let LastMessage {
- mut input, output, ..
- } = match self
- .config
- .read()
- .last_message
- .as_ref()
- .filter(|v| v.continuous && !v.output.is_empty())
- .cloned()
- {
- Some(v) => v,
- None => bail!("Unable to continue the response"),
- };
- input.set_continue_output(&output);
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- ".regenerate" => {
- let LastMessage { mut input, .. } = match self
- .config
- .read()
- .last_message
- .as_ref()
- .filter(|v| v.continuous)
- .cloned()
- {
- Some(v) => v,
- None => bail!("Unable to regenerate the response"),
- };
- input.set_regenerate();
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- ".set" => match args {
- Some(args) => {
- Config::update(&self.config, args)?;
- }
- _ => {
- println!("Usage: .set <key> <value>...")
- }
- },
- ".delete" => match args {
- Some(args) => {
- Config::delete(&self.config, args)?;
- }
- _ => {
- println!("Usage: .delete <role|session|rag|agent-data>")
- }
- },
- ".copy" => {
- let output = match self
- .config
- .read()
- .last_message
- .as_ref()
- .filter(|v| v.continuous && !v.output.is_empty())
- .map(|v| v.output.clone())
- {
- Some(v) => v,
- None => bail!("No chat response to copy"),
- };
- self.copy(&output)
- .with_context(|| "Failed to copy the last chat response")?;
- }
- ".exit" => match args {
- Some("role") => {
- self.config.write().exit_role()?;
- }
- Some("session") => {
- if self.config.read().agent.is_some() {
- self.config.write().exit_agent_session()?;
- } else {
- self.config.write().exit_session()?;
- }
- }
- Some("rag") => {
- self.config.write().exit_rag()?;
- }
- Some("agent") => {
- self.config.write().exit_agent()?;
- }
- Some(_) => unknown_command()?,
- None => {
- return Ok(true);
- }
- },
- ".clear" => match args {
- Some("messages") => {
- bail!("Use '.empty session' instead");
- }
- _ => unknown_command()?,
- },
- _ => unknown_command()?,
- },
- None => {
- let input = Input::from_str(&self.config, line, None);
- ask(&self.config, self.abort_signal.clone(), input, true).await?;
- }
- }
-
- println!();
-
- Ok(false)
- }
-
- fn banner(&self) {
- let name = env!("CARGO_CRATE_NAME");
- let version = env!("CARGO_PKG_VERSION");
- print!(
- r#"Welcome to {name} {version}
-Type ".help" for additional help.
-"#
- )
- }
-
fn create_editor(config: &GlobalConfig) -> Result<Reedline> {
let completer = ReplCompleter::new(config);
let highlighter = ReplHighlighter::new(config);
@@ -602,14 +308,6 @@ Type ".help" for additional help.
let completion_menu = ColumnarMenu::default().with_name(MENU_NAME);
ReedlineMenu::EngineCompleter(Box::new(completion_menu))
}
-
- fn copy(&self, text: &str) -> Result<()> {
- if text.is_empty() {
- bail!("No text to copy")
- }
- set_text(text)?;
- Ok(())
- }
}
#[derive(Debug, Clone)]
@@ -647,6 +345,314 @@ impl Validator for ReplValidator {
}
}
+pub async fn run_repl_command(
+ config: &GlobalConfig,
+ abort_signal: AbortSignal,
+ mut line: &str,
+) -> Result<bool> {
+ if let Ok(Some(captures)) = MULTILINE_RE.captures(line) {
+ if let Some(text_match) = captures.get(1) {
+ line = text_match.as_str();
+ }
+ }
+ match parse_command(line) {
+ Some((cmd, args)) => match cmd {
+ ".help" => {
+ dump_repl_help();
+ }
+ ".info" => match args {
+ Some("role") => {
+ let info = config.read().role_info()?;
+ print!("{}", info);
+ }
+ Some("session") => {
+ let info = config.read().session_info()?;
+ print!("{}", info);
+ }
+ Some("rag") => {
+ let info = config.read().rag_info()?;
+ print!("{}", info);
+ }
+ Some("agent") => {
+ let info = config.read().agent_info()?;
+ print!("{}", info);
+ }
+ Some(_) => unknown_command()?,
+ None => {
+ let output = config.read().sysinfo()?;
+ print!("{}", output);
+ }
+ },
+ ".model" => match args {
+ Some(name) => {
+ config.write().set_model(name)?;
+ }
+ None => println!("Usage: .model <name>"),
+ },
+ ".prompt" => match args {
+ Some(text) => {
+ config.write().use_prompt(text)?;
+ }
+ None => println!("Usage: .prompt <text>..."),
+ },
+ ".role" => match args {
+ Some(args) => match args.split_once(['\n', ' ']) {
+ Some((name, text)) => {
+ let role = config.read().retrieve_role(name.trim())?;
+ let input = Input::from_str(config, text, Some(role));
+ ask(config, abort_signal.clone(), input, false).await?;
+ }
+ None => {
+ let name = args;
+ if !Config::has_role(name) {
+ config.write().new_role(name)?;
+ }
+ config.write().use_role(name)?;
+ }
+ },
+ None => println!(
+ r#"Usage:
+ .role <name> # If the role exists, switch to it; otherwise, create a new role
+ .role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
+ ),
+ },
+ ".session" => {
+ config.write().use_session(args)?;
+ Config::maybe_autoname_session(config.clone());
+ }
+ ".rag" => {
+ Config::use_rag(config, args, abort_signal.clone()).await?;
+ }
+ ".agent" => match split_first_arg(args) {
+ Some((agent_name, session_name)) => {
+ Config::use_agent(config, agent_name, session_name, abort_signal.clone())
+ .await?;
+ }
+ None => println!(r#"Usage: .agent <agent-name> [session-name]"#),
+ },
+ ".starter" => match args {
+ Some(value) => {
+ let input = Input::from_str(config, value, None);
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ None => {
+ let banner = config.read().agent_banner()?;
+ config.read().print_markdown(&banner)?;
+ }
+ },
+ ".variable" => match args {
+ Some(args) => {
+ config.write().set_agent_variable(args)?;
+ }
+ _ => {
+ println!("Usage: .variable <key> <value>")
+ }
+ },
+ ".save" => match split_first_arg(args) {
+ Some(("role", name)) => {
+ config.write().save_role(name)?;
+ }
+ Some(("session", name)) => {
+ config.write().save_session(name)?;
+ }
+ _ => {
+ println!(r#"Usage: .save <role|session> [name]"#)
+ }
+ },
+ ".edit" => {
+ if config.read().macro_flag {
+ bail!("Cannot perform this operation because you are in a macro")
+ }
+ match args {
+ Some("role") => {
+ config.write().edit_role()?;
+ }
+ Some("session") => {
+ config.write().edit_session()?;
+ }
+ Some("rag-docs") => {
+ Config::edit_rag_docs(config, abort_signal.clone()).await?;
+ }
+ _ => {
+ println!(r#"Usage: .edit <role|session|rag-docs>"#)
+ }
+ }
+ }
+ ".compress" => match args {
+ Some("session") => {
+ abortable_run_with_spinner(
+ Config::compress_session(config),
+ "Compressing",
+ abort_signal.clone(),
+ )
+ .await?;
+ println!("✓ Successfully compressed the session.");
+ }
+ _ => {
+ println!(r#"Usage: .compress session"#)
+ }
+ },
+ ".empty" => match args {
+ Some("session") => {
+ config.write().empty_session()?;
+ }
+ _ => {
+ println!(r#"Usage: .empty session"#)
+ }
+ },
+ ".rebuild" => match args {
+ Some("rag") => {
+ Config::rebuild_rag(config, abort_signal.clone()).await?;
+ }
+ _ => {
+ println!(r#"Usage: .rebuild rag"#)
+ }
+ },
+ ".sources" => match args {
+ Some("rag") => {
+ let output = Config::rag_sources(config)?;
+ println!("{}", output);
+ }
+ _ => {
+ println!(r#"Usage: .sources rag"#)
+ }
+ },
+ ".macro" => match split_first_arg(args) {
+ Some((name, extra)) => {
+ if !Config::has_macro(name) && extra.is_none() {
+ config.write().new_macro(name)?;
+ } else {
+ macro_execute(config, name, extra, abort_signal.clone()).await?;
+ }
+ }
+ None => println!("Usage: .macro <name> <text>..."),
+ },
+ ".file" => match args {
+ Some(args) => {
+ let (files, text) = split_params_text(args, cfg!(windows));
+ let input = Input::from_files_with_spinner(
+ config,
+ text,
+ files,
+ None,
+ abort_signal.clone(),
+ )
+ .await?;
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ None => println!(
+ r#"Usage: .file <file|dir|url|%%|cmd>... [-- <text>...]
+
+.file /tmp/file.txt
+.file src/ Cargo.toml -- analyze
+.file https://example.com/file.txt -- summarize
+.file https://example.com/image.png -- recognize text
+.file %% -- translate last reply to english
+.file `git diff` -- Generate git commit message"#
+ ),
+ },
+ ".continue" => {
+ let LastMessage {
+ mut input, output, ..
+ } = match config
+ .read()
+ .last_message
+ .as_ref()
+ .filter(|v| v.continuous && !v.output.is_empty())
+ .cloned()
+ {
+ Some(v) => v,
+ None => bail!("Unable to continue the response"),
+ };
+ input.set_continue_output(&output);
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ ".regenerate" => {
+ let LastMessage { mut input, .. } = match config
+ .read()
+ .last_message
+ .as_ref()
+ .filter(|v| v.continuous)
+ .cloned()
+ {
+ Some(v) => v,
+ None => bail!("Unable to regenerate the response"),
+ };
+ input.set_regenerate();
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ ".set" => match args {
+ Some(args) => {
+ Config::update(config, args)?;
+ }
+ _ => {
+ println!("Usage: .set <key> <value>...")
+ }
+ },
+ ".delete" => match args {
+ Some(args) => {
+ Config::delete(config, args)?;
+ }
+ _ => {
+ println!("Usage: .delete <role|session|rag|macro|agent-data>")
+ }
+ },
+ ".copy" => {
+ let output = match config
+ .read()
+ .last_message
+ .as_ref()
+ .filter(|v| v.continuous && !v.output.is_empty())
+ .map(|v| v.output.clone())
+ {
+ Some(v) => v,
+ None => bail!("No chat response to copy"),
+ };
+ set_text(&output).context("Failed to copy the last chat response")?;
+ }
+ ".exit" => match args {
+ Some("role") => {
+ config.write().exit_role()?;
+ }
+ Some("session") => {
+ if config.read().agent.is_some() {
+ config.write().exit_agent_session()?;
+ } else {
+ config.write().exit_session()?;
+ }
+ }
+ Some("rag") => {
+ config.write().exit_rag()?;
+ }
+ Some("agent") => {
+ config.write().exit_agent()?;
+ }
+ Some(_) => unknown_command()?,
+ None => {
+ return Ok(true);
+ }
+ },
+ ".clear" => match args {
+ Some("messages") => {
+ bail!("Use '.empty session' instead");
+ }
+ _ => unknown_command()?,
+ },
+ _ => unknown_command()?,
+ },
+ None => {
+ let input = Input::from_str(config, line, None);
+ ask(config, abort_signal.clone(), input, true).await?;
+ }
+ }
+
+ if !config.read().macro_flag {
+ println!();
+ }
+
+ Ok(false)
+}
+
#[async_recursion::async_recursion]
async fn ask(
config: &GlobalConfig,
@@ -720,14 +726,14 @@ fn parse_command(line: &str) -> Option<(&str, Option<&str>)> {
}
}
-fn split_args(args: Option<&str>) -> Option<(&str, Option<&str>)> {
+fn split_first_arg(args: Option<&str>) -> Option<(&str, Option<&str>)> {
args.map(|v| match v.split_once(' ') {
Some((subcmd, args)) => (subcmd, Some(args.trim())),
None => (v, None),
})
}
-fn split_files_text(line: &str, is_win: bool) -> (Vec<String>, &str) {
+pub fn split_params_text(line: &str, is_win: bool) -> (Vec<String>, &str) {
let mut words = Vec::new();
let mut word = String::new();
let mut unbalance: Option<char> = None;
@@ -823,44 +829,45 @@ mod tests {
}
#[test]
- fn test_split_files_text() {
+ fn test_split_params_text() {
+ assert_eq!(split_params_text("", false), (vec![], ""));
assert_eq!(
- split_files_text("file.txt", false),
+ split_params_text("file.txt", false),
(vec!["file.txt".into()], "")
);
assert_eq!(
- split_files_text("file.txt --", false),
+ split_params_text("file.txt --", false),
(vec!["file.txt".into()], "")
);
assert_eq!(
- split_files_text("file.txt -- hello", false),
+ split_params_text("file.txt -- hello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text("file.txt -- \thello", false),
+ split_params_text("file.txt -- \thello", false),
(vec!["file.txt".into()], "\thello")
);
assert_eq!(
- split_files_text("file.txt --\nhello", false),
+ split_params_text("file.txt --\nhello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text("file.txt --\r\nhello", false),
+ split_params_text("file.txt --\r\nhello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text("file.txt --\rhello", false),
+ split_params_text("file.txt --\rhello", false),
(vec!["file.txt".into()], "hello")
);
assert_eq!(
- split_files_text(r#"file1.txt 'file2.txt' "file3.txt""#, false),
+ split_params_text(r#"file1.txt 'file2.txt' "file3.txt""#, false),
(
vec!["file1.txt".into(), "file2.txt".into(), "file3.txt".into()],
""
)
);
assert_eq!(
- split_files_text(r#"./file1.txt 'file1 - Copy.txt' file\ 2.txt"#, false),
+ split_params_text(r#"./file1.txt 'file1 - Copy.txt' file\ 2.txt"#, false),
(
vec![
"./file1.txt".into(),
@@ -871,7 +878,7 @@ mod tests {
)
);
assert_eq!(
- split_files_text(r#".\file.txt C:\dir\file.txt"#, true),
+ split_params_text(r#".\file.txt C:\dir\file.txt"#, true),
(vec![".\\file.txt".into(), "C:\\dir\\file.txt".into()], "")
);
}