From 800c1ba91663ea7a810e8e8755b357f5e3b55a2f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Fri, 24 May 2024 19:15:02 -0400 Subject: [PATCH] Rework prompt frontmatter (#12262) Moved some things around so prompts now always have front-matter to return, either by creating a prompt with default front-matter, or bailing earlier on importing the prompt to the library. In the future we'll improve visibility of malformed prompts in the `prompts` folder in the prompt manager UI. Fixes: - Prompts inserted with the `/prompt` command now only include their body, not the entire file including metadata. - Prompts with an invalid title will now show "Untitled prompt" instead of an empty line. Release Notes: - N/A --- crates/assistant/src/prompts/prompt.rs | 140 ++++++------------ .../assistant/src/prompts/prompt_library.rs | 24 ++- .../assistant/src/prompts/prompt_manager.rs | 2 +- .../src/slash_command/prompt_command.rs | 12 +- 4 files changed, 66 insertions(+), 112 deletions(-) diff --git a/crates/assistant/src/prompts/prompt.rs b/crates/assistant/src/prompts/prompt.rs index f3dfd8aa96..f24bb7c965 100644 --- a/crates/assistant/src/prompts/prompt.rs +++ b/crates/assistant/src/prompts/prompt.rs @@ -19,7 +19,7 @@ pub struct StaticPromptFrontmatter { impl Default for StaticPromptFrontmatter { fn default() -> Self { Self { - title: "New Prompt".to_string(), + title: "Untitled Prompt".to_string(), version: "1.0".to_string(), author: "No Author".to_string(), languages: vec!["*".to_string()], @@ -28,37 +28,7 @@ impl Default for StaticPromptFrontmatter { } } -impl StaticPromptFrontmatter { - pub fn title(&self) -> SharedString { - self.title.clone().into() - } - - // pub fn version(&self) -> SharedString { - // self.version.clone().into() - // } - - // pub fn author(&self) -> SharedString { - // self.author.clone().into() - // } - - // pub fn languages(&self) -> Vec { - // self.languages - // .clone() - // .into_iter() - // .map(|s| s.into()) - // .collect() - // } - - // pub fn dependencies(&self) -> Vec { - // self.dependencies - // .clone() - // .into_iter() - // .map(|s| s.into()) - // .collect() - // } -} - -/// A statuc prompt that can be loaded into the prompt library +/// A static prompt that can be loaded into the prompt library /// from Markdown with a frontmatter header /// /// Examples: @@ -92,95 +62,69 @@ impl StaticPromptFrontmatter { /// ``` #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct StaticPrompt { + #[serde(skip)] + metadata: StaticPromptFrontmatter, content: String, file_name: Option, } impl StaticPrompt { - pub fn new(content: String) -> Self { + pub fn new(content: String, file_name: Option) -> Self { + let matter = Matter::::new(); + let result = matter.parse(&content); + + let metadata = result + .data + .map_or_else( + || Err(anyhow::anyhow!("Failed to parse frontmatter")), + |data| { + let front_matter: StaticPromptFrontmatter = data.deserialize()?; + Ok(front_matter) + }, + ) + .unwrap_or_else(|e| { + if let Some(file_name) = &file_name { + log::error!("Failed to parse frontmatter for {}: {}", file_name, e); + } else { + log::error!("Failed to parse frontmatter: {}", e); + } + StaticPromptFrontmatter::default() + }); + StaticPrompt { content, - file_name: None, + file_name, + metadata, } } - - pub fn title(&self) -> Option { - self.metadata().map(|m| m.title()) - } - - // pub fn version(&self) -> Option { - // self.metadata().map(|m| m.version()) - // } - - // pub fn author(&self) -> Option { - // self.metadata().map(|m| m.author()) - // } - - // pub fn languages(&self) -> Vec { - // self.metadata().map(|m| m.languages()).unwrap_or_default() - // } - - // pub fn dependencies(&self) -> Vec { - // self.metadata() - // .map(|m| m.dependencies()) - // .unwrap_or_default() - // } - - // pub fn load(fs: Arc, file_name: String) -> anyhow::Result { - // todo!() - // } - - // pub fn save(&self, fs: Arc) -> anyhow::Result<()> { - // todo!() - // } - - // pub fn rename(&self, new_file_name: String, fs: Arc) -> anyhow::Result<()> { - // todo!() - // } } impl StaticPrompt { - // pub fn update(&mut self, contents: String) -> &mut Self { - // self.content = contents; - // self - // } - /// Sets the file name of the prompt - pub fn file_name(&mut self, file_name: String) -> &mut Self { + pub fn _file_name(&mut self, file_name: String) -> &mut Self { self.file_name = Some(file_name); self } - /// Sets the file name of the prompt based on the title - // pub fn file_name_from_title(&mut self) -> &mut Self { - // if let Some(title) = self.title() { - // let file_name = title.to_lowercase().replace(" ", "_"); - // if !file_name.is_empty() { - // self.file_name = Some(file_name); - // } - // } - // self - // } - /// Returns the prompt's content pub fn content(&self) -> &String { &self.content } - fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> { - let matter = Matter::::new(); - let result = matter.parse(self.content.as_str()); - match result.data { - Some(data) => { - let front_matter: StaticPromptFrontmatter = data.deserialize()?; - let body = result.content; - Ok((front_matter, body)) - } - None => Err(anyhow::anyhow!("Failed to parse frontmatter")), - } + + /// Returns the prompt's metadata + pub fn _metadata(&self) -> &StaticPromptFrontmatter { + &self.metadata } - pub fn metadata(&self) -> Option { - self.parse().ok().map(|(front_matter, _)| front_matter) + /// Returns the prompt's title + pub fn title(&self) -> SharedString { + self.metadata.title.clone().into() + } + + pub fn body(&self) -> String { + let matter = Matter::::new(); + let result = matter.parse(self.content.as_str()); + result.content.clone() } } diff --git a/crates/assistant/src/prompts/prompt_library.rs b/crates/assistant/src/prompts/prompt_library.rs index cdef98b452..d0e73ccb0f 100644 --- a/crates/assistant/src/prompts/prompt_library.rs +++ b/crates/assistant/src/prompts/prompt_library.rs @@ -2,6 +2,7 @@ use anyhow::Context; use collections::HashMap; use fs::Fs; +use gray_matter::{engine::YAML, Matter}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use smol::stream::StreamExt; @@ -119,6 +120,17 @@ impl PromptLibrary { while let Some(prompt_path) = prompt_paths.next().await { let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?; + let file_name_lossy = if prompt_path.file_name().is_some() { + Some( + prompt_path + .file_name() + .unwrap() + .to_string_lossy() + .to_string(), + ) + } else { + None + }; if !fs.is_file(&prompt_path).await || prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md") @@ -130,13 +142,17 @@ impl PromptLibrary { .load(&prompt_path) .await .with_context(|| format!("Failed to load prompt {:?}", prompt_path))?; - let mut static_prompt = StaticPrompt::new(json); - if let Some(file_name) = prompt_path.file_name() { - let file_name = file_name.to_string_lossy().into_owned(); - static_prompt.file_name(file_name); + // Check that the prompt is valid + let matter = Matter::::new(); + let result = matter.parse(&json); + if result.data.is_none() { + log::warn!("Invalid prompt: {:?}", prompt_path); + continue; } + let static_prompt = StaticPrompt::new(json, file_name_lossy.clone()); + let state = self.state.get_mut(); let id = Uuid::new_v4(); diff --git a/crates/assistant/src/prompts/prompt_manager.rs b/crates/assistant/src/prompts/prompt_manager.rs index b48f238535..cfaacaeb09 100644 --- a/crates/assistant/src/prompts/prompt_manager.rs +++ b/crates/assistant/src/prompts/prompt_manager.rs @@ -321,7 +321,7 @@ impl PickerDelegate for PromptManagerDelegate { .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) - .child(Label::new(prompt.title().unwrap_or_default().clone())), + .child(Label::new(prompt.title())), ) } } diff --git a/crates/assistant/src/slash_command/prompt_command.rs b/crates/assistant/src/slash_command/prompt_command.rs index f3aef558d2..37b73013da 100644 --- a/crates/assistant/src/slash_command/prompt_command.rs +++ b/crates/assistant/src/slash_command/prompt_command.rs @@ -43,12 +43,7 @@ impl SlashCommand for PromptSlashCommand { .prompts() .into_iter() .enumerate() - .filter_map(|(ix, prompt)| { - prompt - .1 - .title() - .map(|title| StringMatchCandidate::new(ix, title.into())) - }) + .map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.1.title().to_string())) .collect::>(); let matches = fuzzy::match_strings( &candidates, @@ -86,11 +81,10 @@ impl SlashCommand for PromptSlashCommand { let prompt = library .prompts() .into_iter() - .filter_map(|prompt| prompt.1.title().map(|title| (title, prompt))) - .find(|(t, _)| t == &title) + .find(|prompt| &prompt.1.title().to_string() == &title) .with_context(|| format!("no prompt found with title {:?}", title))? .1; - Ok(prompt.1.content().to_owned()) + Ok(prompt.body()) }); SlashCommandInvocation { output,