diff options
Diffstat (limited to 'src/config')
| -rw-r--r-- | src/config/agent.rs | 2 | ||||
| -rw-r--r-- | src/config/input.rs | 6 | ||||
| -rw-r--r-- | src/config/mod.rs | 310 | ||||
| -rw-r--r-- | src/config/session.rs | 3 |
4 files changed, 249 insertions, 72 deletions
diff --git a/src/config/agent.rs b/src/config/agent.rs index 731dd91..533e7a9 100644 --- a/src/config/agent.rs +++ b/src/config/agent.rs @@ -68,7 +68,7 @@ impl Agent { let rag = if rag_path.exists() { Some(Arc::new(Rag::load(config, DEFAULT_AGENT_NAME, &rag_path)?)) - } else if !definition.documents.is_empty() && !config.read().cli_info_flag { + } else if !definition.documents.is_empty() && !config.read().info_flag { let mut ans = false; if *IS_STDOUT_TERMINAL { ans = Confirm::new("The agent has the documents, init RAG?") diff --git a/src/config/input.rs b/src/config/input.rs index 70a01cf..c21bc62 100644 --- a/src/config/input.rs +++ b/src/config/input.rs @@ -27,8 +27,8 @@ pub struct Input { medias: Vec<String>, data_urls: HashMap<String, String>, tool_calls: Option<MessageContentToolCalls>, - rag_name: Option<String>, role: Role, + rag_name: Option<String>, with_session: bool, with_agent: bool, } @@ -47,8 +47,8 @@ impl Input { medias: Default::default(), data_urls: Default::default(), tool_calls: None, - rag_name: None, role, + rag_name: None, with_session, with_agent, } @@ -128,8 +128,8 @@ impl Input { medias, data_urls, tool_calls: Default::default(), - rag_name: None, role, + rag_name: None, with_session, with_agent, }) diff --git a/src/config/mod.rs b/src/config/mod.rs index 975c8ea..7e2f275 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -17,6 +17,7 @@ use crate::client::{ use crate::function::{FunctionDeclaration, Functions, ToolResult}; use crate::rag::Rag; use crate::render::{MarkdownRender, RenderOptions}; +use crate::repl::{run_repl_command, split_params_text}; use crate::utils::*; use anyhow::{anyhow, bail, Context, Result}; @@ -49,6 +50,7 @@ const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.t const CONFIG_FILE_NAME: &str = "config.yaml"; const ROLES_DIR_NAME: &str = "roles"; +const MACROS_DIR_NAME: &str = "macros"; const ENV_FILE_NAME: &str = ".env"; const MESSAGES_FILE_NAME: &str = "messages.md"; const SESSIONS_DIR_NAME: &str = "sessions"; @@ -142,13 +144,12 @@ pub struct Config { pub clients: Vec<ClientConfig>, #[serde(skip)] - pub role: Option<Role>, - #[serde(skip)] - pub session: Option<Session>, + pub macro_flag: bool, #[serde(skip)] - pub rag: Option<Arc<Rag>>, + pub info_flag: bool, #[serde(skip)] - pub agent: Option<Agent>, + pub cli_agent_variables: Option<AgentVariables>, + #[serde(skip)] pub model: Model, #[serde(skip)] @@ -159,9 +160,13 @@ pub struct Config { pub last_message: Option<LastMessage>, #[serde(skip)] - pub cli_info_flag: bool, + pub role: Option<Role>, #[serde(skip)] - pub cli_agent_variables: Option<AgentVariables>, + pub session: Option<Session>, + #[serde(skip)] + pub rag: Option<Arc<Rag>>, + #[serde(skip)] + pub agent: Option<Agent>, } impl Default for Config { @@ -212,17 +217,19 @@ impl Default for Config { clients: vec![], - role: None, - session: None, - rag: None, - agent: None, + macro_flag: false, + info_flag: false, + cli_agent_variables: None, + model: Default::default(), functions: Default::default(), working_mode: WorkingMode::Cmd, last_message: None, - cli_info_flag: false, - cli_agent_variables: None, + role: None, + session: None, + rag: None, + agent: None, } } } @@ -296,6 +303,17 @@ impl Config { Self::roles_dir().join(format!("{name}.md")) } + pub fn macros_dir() -> PathBuf { + match env::var(get_env_name("macros_dir")) { + Ok(value) => PathBuf::from(value), + Err(_) => Self::local_path(MACROS_DIR_NAME), + } + } + + pub fn macro_file(name: &str) -> PathBuf { + Self::macros_dir().join(format!("{name}.yaml")) + } + pub fn env_file() -> PathBuf { match env::var(get_env_name("env_file")) { Ok(value) => PathBuf::from(value), @@ -537,23 +555,16 @@ impl Config { let role = self.extract_role(); let mut items = vec![ ("model", role.model().id()), + ("temperature", format_option_value(&role.temperature())), + ("top_p", format_option_value(&role.top_p())), + ("use_tools", format_option_value(&role.use_tools())), ( "max_output_tokens", self.model .max_tokens_param() .map(|v| format!("{v} (current model)")) - .unwrap_or_else(|| "-".into()), + .unwrap_or_else(|| "null".into()), ), - ("temperature", format_option_value(&role.temperature())), - ("top_p", format_option_value(&role.top_p())), - ("dry_run", self.dry_run.to_string()), - ("stream", self.stream.to_string()), - ("save", self.save.to_string()), - ("keybindings", self.keybindings.clone()), - ("wrap", wrap), - ("wrap_code", self.wrap_code.to_string()), - ("function_calling", self.function_calling.to_string()), - ("use_tools", format_option_value(&role.use_tools())), ("save_session", format_option_value(&self.save_session)), ("compress_threshold", self.compress_threshold.to_string()), ( @@ -561,6 +572,13 @@ impl Config { format_option_value(&rag_reranker_model), ), ("rag_top_k", rag_top_k.to_string()), + ("dry_run", self.dry_run.to_string()), + ("function_calling", self.function_calling.to_string()), + ("stream", self.stream.to_string()), + ("save", self.save.to_string()), + ("keybindings", self.keybindings.clone()), + ("wrap", wrap), + ("wrap_code", self.wrap_code.to_string()), ("highlight", self.highlight.to_string()), ("light_theme", self.light_theme.to_string()), ("config_file", display_path(&Self::config_file())), @@ -568,6 +586,7 @@ impl Config { ("roles_dir", display_path(&Self::roles_dir())), ("sessions_dir", display_path(&self.sessions_dir())), ("rags_dir", display_path(&Self::rags_dir())), + ("macros_dir", display_path(&Self::macros_dir())), ("functions_dir", display_path(&Self::functions_dir())), ("messages_file", display_path(&self.messages_file())), ]; @@ -590,10 +609,6 @@ impl Config { let key = parts[0]; let value = parts[1]; match key { - "max_output_tokens" => { - let value = parse_value(value)?; - config.write().set_max_output_tokens(value); - } "temperature" => { let value = parse_value(value)?; config.write().set_temperature(value); @@ -602,29 +617,14 @@ impl Config { let value = parse_value(value)?; config.write().set_top_p(value); } - "dry_run" => { - let value = value.parse().with_context(|| "Invalid value")?; - config.write().dry_run = value; - } - "stream" => { - let value = value.parse().with_context(|| "Invalid value")?; - config.write().stream = value; - } - "save" => { - let value = value.parse().with_context(|| "Invalid value")?; - config.write().save = value; - } - "function_calling" => { - let value = value.parse().with_context(|| "Invalid value")?; - if value && config.write().functions.is_empty() { - bail!("Function calling cannot be enabled because no functions are installed.") - } - config.write().function_calling = value; - } "use_tools" => { let value = parse_value(value)?; config.write().set_use_tools(value); } + "max_output_tokens" => { + let value = parse_value(value)?; + config.write().set_max_output_tokens(value); + } "save_session" => { let value = parse_value(value)?; config.write().set_save_session(value); @@ -641,6 +641,25 @@ impl Config { let value = value.parse().with_context(|| "Invalid value")?; Self::set_rag_top_k(config, value)?; } + "dry_run" => { + let value = value.parse().with_context(|| "Invalid value")?; + config.write().dry_run = value; + } + "function_calling" => { + let value = value.parse().with_context(|| "Invalid value")?; + if value && config.write().functions.is_empty() { + bail!("Function calling cannot be enabled because no functions are installed.") + } + config.write().function_calling = value; + } + "stream" => { + let value = value.parse().with_context(|| "Invalid value")?; + config.write().stream = value; + } + "save" => { + let value = value.parse().with_context(|| "Invalid value")?; + config.write().save = value; + } "highlight" => { let value = value.parse().with_context(|| "Invalid value")?; config.write().highlight = value; @@ -655,6 +674,7 @@ impl Config { "role" => (Self::roles_dir(), Some(".md")), "session" => (config.read().sessions_dir(), Some(".yaml")), "rag" => (Self::rags_dir(), Some(".yaml")), + "macro" => (Self::macros_dir(), Some(".yaml")), "agent-data" => (Self::agents_data_dir(), None), _ => bail!("Unknown kind '{kind}'"), }; @@ -887,31 +907,38 @@ impl Config { } pub fn new_role(&mut self, name: &str) -> Result<()> { + if self.macro_flag { + bail!("No role"); + } let ans = Confirm::new("Create a new role?") .with_default(true) .prompt()?; if ans { self.upsert_role(name)?; + } else { + bail!("No role"); } Ok(()) } pub fn edit_role(&mut self) -> Result<()> { + let role_name; if let Some(session) = self.session.as_ref() { if let Some(name) = session.role_name().map(|v| v.to_string()) { if session.is_empty() { - self.upsert_role(&name) + role_name = Some(name); } else { bail!("Cannot perform this operation because you are in a non-empty session") } } else { bail!("No role") } - } else if let Some(name) = self.role.as_ref().map(|v| v.name().to_string()) { - self.upsert_role(&name) } else { - bail!("No role") + role_name = self.role.as_ref().map(|v| v.name().to_string()); } + let name = role_name.ok_or_else(|| anyhow!("No role"))?; + self.upsert_role(&name)?; + self.use_role(&name) } pub fn upsert_role(&mut self, name: &str) -> Result<()> { @@ -921,7 +948,6 @@ impl Config { ensure_parent_exists(&role_path)?; let editor = self.editor()?; edit_file(&editor, &role_path)?; - self.use_role(name)?; Ok(()) } @@ -1444,9 +1470,13 @@ impl Config { bail!("Already in a agent, please run '.exit agent' first to exit the current agent."); } let agent = Agent::init(config, agent_name, abort_signal).await?; - let session = session_name - .map(|v| v.to_string()) - .or_else(|| agent.agent_prelude().map(|v| v.to_string())); + let session = session_name.map(|v| v.to_string()).or_else(|| { + if config.read().macro_flag { + None + } else { + agent.agent_prelude().map(|v| v.to_string()) + } + }); config.write().rag = agent.rag(); config.write().agent = Some(agent); if let Some(session) = session { @@ -1516,8 +1546,43 @@ impl Config { Ok(()) } + pub fn list_macros() -> Vec<String> { + list_file_names(Self::macros_dir(), ".yaml") + } + + pub fn load_macro(name: &str) -> Result<Macro> { + let path = Self::macro_file(name); + let err = || format!("Failed to load macro '{name}' at '{}'", path.display()); + let content = read_to_string(&path).with_context(err)?; + let value: Macro = serde_yaml::from_str(&content).with_context(err)?; + Ok(value) + } + + pub fn has_macro(name: &str) -> bool { + let names = Self::list_macros(); + names.contains(&name.to_string()) + } + + pub fn new_macro(&mut self, name: &str) -> Result<()> { + if self.macro_flag { + bail!("No macro"); + } + let ans = Confirm::new("Create a new macro?") + .with_default(true) + .prompt()?; + if ans { + let macro_path = Self::macro_file(name); + ensure_parent_exists(¯o_path)?; + let editor = self.editor()?; + edit_file(&editor, ¯o_path)?; + } else { + bail!("No macro"); + } + Ok(()) + } + pub fn apply_prelude(&mut self) -> Result<()> { - if !self.state().is_empty() { + if self.macro_flag || !self.state().is_empty() { return Ok(()); } let prelude = match self.working_mode { @@ -1672,6 +1737,7 @@ impl Config { } ".rag" => map_completion_values(Self::list_rags()), ".agent" => map_completion_values(list_agents()), + ".macro" => map_completion_values(Self::list_macros()), ".starter" => match &self.agent { Some(agent) => map_completion_values(agent.conversation_staters().to_vec()), None => vec![], @@ -1686,18 +1752,18 @@ impl Config { }, ".set" => { let mut values = vec![ - "max_output_tokens", "temperature", "top_p", - "dry_run", - "stream", - "save", - "function_calling", "use_tools", "save_session", "compress_threshold", "rag_reranker_model", "rag_top_k", + "max_output_tokens", + "dry_run", + "function_calling", + "stream", + "save", "highlight", ]; values.sort_unstable(); @@ -1706,7 +1772,9 @@ impl Config { .map(|v| (format!("{v} "), None)) .collect() } - ".delete" => map_completion_values(vec!["role", "session", "rag", "agent-data"]), + ".delete" => { + map_completion_values(vec!["role", "session", "rag", "macro", "agent-data"]) + } _ => vec![], }; filter = args[0] @@ -2009,11 +2077,11 @@ impl Config { let new_variables = Agent::init_agent_variables( agent.defined_variables(), &config_variables, - self.cli_info_flag, + self.info_flag, )?; agent.set_shared_variables(new_variables); } - if !self.cli_info_flag { + if !self.info_flag { agent.update_shared_dynamic_instructions(false)?; } Ok(()) @@ -2035,7 +2103,7 @@ impl Config { let new_variables = Agent::init_agent_variables( agent.defined_variables(), &config_variables, - self.cli_info_flag, + self.info_flag, )?; agent.set_shared_variables(new_variables.clone()); new_variables @@ -2043,7 +2111,7 @@ impl Config { shared_variables }; agent.set_session_variables(session_variables); - if !self.cli_info_flag { + if !self.info_flag { agent.update_session_dynamic_instructions(None)?; } session.sync_agent(agent); @@ -2319,6 +2387,106 @@ impl WorkingMode { } } +#[async_recursion::async_recursion] +pub async fn macro_execute( + config: &GlobalConfig, + name: &str, + args: Option<&str>, + abort_signal: AbortSignal, +) -> Result<()> { + let macro_value = Config::load_macro(name)?; + let (mut new_args, text) = split_params_text(args.unwrap_or_default(), cfg!(windows)); + if !text.is_empty() { + new_args.push(text.to_string()); + } + let variables = macro_value + .resolve_variables(&new_args) + .map_err(|err| anyhow!("{err}. Usage: {}", macro_value.usage(name)))?; + let role = config.read().extract_role(); + let mut config = config.read().clone(); + config.temperature = role.temperature(); + config.top_p = role.top_p(); + config.use_tools = role.use_tools().clone(); + config.macro_flag = true; + config.model = role.model().clone(); + config.role = None; + config.session = None; + config.rag = None; + config.agent = None; + config.discontinuous_last_message(); + let config = Arc::new(RwLock::new(config)); + config.write().macro_flag = true; + for step in ¯o_value.steps { + let command = Macro::interpolate_command(step, &variables); + println!(">> {}", multiline_text(&command)); + run_repl_command(&config, abort_signal.clone(), &command).await?; + } + Ok(()) +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Macro { + #[serde(default)] + pub variables: Vec<MacroVariable>, + pub steps: Vec<String>, +} + +impl Macro { + pub fn resolve_variables(&self, args: &[String]) -> Result<IndexMap<String, String>> { + let mut output = IndexMap::new(); + for (i, variable) in self.variables.iter().enumerate() { + let value = if variable.rest && i == self.variables.len() - 1 { + if args.len() > i { + Some(args[i..].join(" ")) + } else { + variable.default.clone() + } + } else { + args.get(i) + .map(|v| v.to_string()) + .or_else(|| variable.default.clone()) + }; + let value = + value.ok_or_else(|| anyhow!("Missing value for variable '{}'", variable.name))?; + output.insert(variable.name.clone(), value); + } + Ok(output) + } + + pub fn usage(&self, name: &str) -> String { + let mut parts = vec![name.to_string()]; + for (i, variable) in self.variables.iter().enumerate() { + let part = match ( + variable.rest && i == self.variables.len() - 1, + variable.default.is_some(), + ) { + (true, true) => format!("[{}]...", variable.name), + (true, false) => format!("<{}>...", variable.name), + (false, true) => format!("[{}]", variable.name), + (false, false) => format!("<{}>", variable.name), + }; + parts.push(part); + } + parts.join(" ") + } + + pub fn interpolate_command(command: &str, variables: &IndexMap<String, String>) -> String { + let mut output = command.to_string(); + for (key, value) in variables { + output = output.replace(&format!("{{{{{key}}}}}"), value); + } + output + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct MacroVariable { + pub name: String, + #[serde(default)] + pub rest: bool, + pub default: Option<String>, +} + #[derive(Debug, Clone)] pub struct LastMessage { pub input: Input, @@ -2487,3 +2655,13 @@ where config.write().rag = Some(Arc::new(rag)); Ok(()) } + +fn format_option_value<T>(value: &Option<T>) -> String +where + T: std::fmt::Display, +{ + match value { + Some(value) => value.to_string(), + None => "null".to_string(), + } +} diff --git a/src/config/session.rs b/src/config/session.rs index 227a23c..13af751 100644 --- a/src/config/session.rs +++ b/src/config/session.rs @@ -234,8 +234,7 @@ impl Session { } MessageRole::User => { lines.push(format!( - "{}){}", - self.name, + ">> {}", message.content.render_input(resolve_url_fn, agent_info) )); } |
