From dc5ea050aa7d1b6e12d67832889d167a9cae3dd2 Mon Sep 17 00:00:00 2001 From: itzlambda Date: Mon, 30 Jun 2025 18:56:18 +0530 Subject: [PATCH 01/15] feat(templates): implement TemplateStore for TOML template management --- crates/rullm-cli/src/constants.rs | 1 + crates/rullm-cli/src/main.rs | 1 + crates/rullm-cli/src/templates.rs | 346 ++++++++++++++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 crates/rullm-cli/src/templates.rs diff --git a/crates/rullm-cli/src/constants.rs b/crates/rullm-cli/src/constants.rs index 2766cd93..b0f35475 100644 --- a/crates/rullm-cli/src/constants.rs +++ b/crates/rullm-cli/src/constants.rs @@ -2,4 +2,5 @@ pub const CONFIG_FILE_NAME: &str = "config.toml"; pub const MODEL_FILE_NAME: &str = "models.json"; pub const ALIASES_CONFIG_FILE: &str = "aliases.toml"; pub const KEYS_CONFIG_FILE: &str = "keys.toml"; +pub const TEMPLATES_DIR_NAME: &str = "templates"; pub const BINARY_NAME: &str = env!("CARGO_BIN_NAME"); diff --git a/crates/rullm-cli/src/main.rs b/crates/rullm-cli/src/main.rs index 9b3a6a05..e7037971 100644 --- a/crates/rullm-cli/src/main.rs +++ b/crates/rullm-cli/src/main.rs @@ -10,6 +10,7 @@ mod config; mod constants; mod output; mod provider; +mod templates; use anyhow::Result; use args::{Cli, CliConfig}; diff --git a/crates/rullm-cli/src/templates.rs b/crates/rullm-cli/src/templates.rs new file mode 100644 index 00000000..c4c146a1 --- /dev/null +++ b/crates/rullm-cli/src/templates.rs @@ -0,0 +1,346 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::constants::TEMPLATES_DIR_NAME; + +/// A template for LLM queries with placeholder support +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Template { + /// Name of the template + pub name: String, + /// System prompt/message (optional) + pub system_prompt: Option, + /// User prompt template with {{placeholder}} syntax + pub user_prompt: String, + /// Default values for placeholders + #[serde(default)] + pub defaults: HashMap, + /// Description of the template (optional) + pub description: Option, +} + +impl Template { + pub fn new(name: String, user_prompt: String) -> Self { + Self { + name, + system_prompt: None, + user_prompt, + defaults: HashMap::new(), + description: None, + } + } + + /// Render the template by replacing placeholders with provided values + /// Returns an error if any required placeholders are missing + pub fn render(&self, params: &HashMap) -> Result { + let mut rendered_user = self.user_prompt.clone(); + let mut rendered_system = self.system_prompt.clone(); + let mut missing_placeholders = Vec::new(); + + // Extract all placeholders from user prompt + let user_placeholders = extract_placeholders(&self.user_prompt); + + // Extract placeholders from system prompt if it exists + let system_placeholders = if let Some(ref system) = self.system_prompt { + extract_placeholders(system) + } else { + Vec::new() + }; + + // Combine all unique placeholders + let mut all_placeholders = user_placeholders; + for ph in system_placeholders { + if !all_placeholders.contains(&ph) { + all_placeholders.push(ph); + } + } + + // Replace placeholders + for placeholder in &all_placeholders { + let value = params + .get(placeholder) + .or_else(|| self.defaults.get(placeholder)); + + match value { + Some(val) => { + let pattern = format!("{{{{{}}}}}", placeholder); + rendered_user = rendered_user.replace(&pattern, val); + if let Some(ref mut system) = rendered_system { + *system = system.replace(&pattern, val); + } + } + None => { + missing_placeholders.push(placeholder.clone()); + } + } + } + + if !missing_placeholders.is_empty() { + return Err(anyhow::anyhow!( + "Missing required placeholders: {}", + missing_placeholders.join(", ") + )); + } + + Ok(RenderedTemplate { + system_prompt: rendered_system, + user_prompt: rendered_user, + }) + } + + /// Get all placeholders required by this template + pub fn get_placeholders(&self) -> Vec { + let mut placeholders = extract_placeholders(&self.user_prompt); + + if let Some(ref system) = self.system_prompt { + let system_placeholders = extract_placeholders(system); + for ph in system_placeholders { + if !placeholders.contains(&ph) { + placeholders.push(ph); + } + } + } + + placeholders + } +} + +/// A rendered template ready for use +#[derive(Debug)] +pub struct RenderedTemplate { + pub system_prompt: Option, + pub user_prompt: String, +} + +/// Store for managing templates +pub struct TemplateStore { + templates_dir: PathBuf, + templates: HashMap, +} + +impl TemplateStore { + /// Create a new TemplateStore with the given base directory + pub fn new(base_path: &Path) -> Self { + let templates_dir = base_path.join(TEMPLATES_DIR_NAME); + Self { + templates_dir, + templates: HashMap::new(), + } + } + + /// Load all templates from the templates directory + pub fn load(&mut self) -> Result<()> { + self.templates.clear(); + + // Create templates directory if it doesn't exist + if !self.templates_dir.exists() { + fs::create_dir_all(&self.templates_dir) + .context("Failed to create templates directory")?; + return Ok(()); + } + + // Read all .toml files in the templates directory + let entries = + fs::read_dir(&self.templates_dir).context("Failed to read templates directory")?; + + for entry in entries { + let entry = entry.context("Failed to read directory entry")?; + let path = entry.path(); + + if path.extension().and_then(|s| s.to_str()) == Some("toml") { + match self.load_template_file(&path) { + Ok(template) => { + self.templates.insert(template.name.clone(), template); + } + Err(e) => { + eprintln!("Warning: Failed to load template {:?}: {}", path, e); + } + } + } + } + + Ok(()) + } + + /// Load a single template file + fn load_template_file(&self, path: &Path) -> Result