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
This commit is contained in:
Nate Butler 2024-05-24 19:15:02 -04:00 committed by GitHub
parent 461e7d00a6
commit 800c1ba916
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 112 deletions

View File

@ -19,7 +19,7 @@ pub struct StaticPromptFrontmatter {
impl Default for StaticPromptFrontmatter { impl Default for StaticPromptFrontmatter {
fn default() -> Self { fn default() -> Self {
Self { Self {
title: "New Prompt".to_string(), title: "Untitled Prompt".to_string(),
version: "1.0".to_string(), version: "1.0".to_string(),
author: "No Author".to_string(), author: "No Author".to_string(),
languages: vec!["*".to_string()], languages: vec!["*".to_string()],
@ -28,37 +28,7 @@ impl Default for StaticPromptFrontmatter {
} }
} }
impl StaticPromptFrontmatter { /// A static prompt that can be loaded into the prompt library
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<SharedString> {
// self.languages
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.dependencies
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
}
/// A statuc prompt that can be loaded into the prompt library
/// from Markdown with a frontmatter header /// from Markdown with a frontmatter header
/// ///
/// Examples: /// Examples:
@ -92,95 +62,69 @@ impl StaticPromptFrontmatter {
/// ``` /// ```
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPrompt { pub struct StaticPrompt {
#[serde(skip)]
metadata: StaticPromptFrontmatter,
content: String, content: String,
file_name: Option<String>, file_name: Option<String>,
} }
impl StaticPrompt { impl StaticPrompt {
pub fn new(content: String) -> Self { pub fn new(content: String, file_name: Option<String>) -> Self {
let matter = Matter::<YAML>::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 { StaticPrompt {
content, content,
file_name: None, file_name,
metadata,
} }
} }
pub fn title(&self) -> Option<SharedString> {
self.metadata().map(|m| m.title())
}
// pub fn version(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.version())
// }
// pub fn author(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.author())
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.metadata().map(|m| m.languages()).unwrap_or_default()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.metadata()
// .map(|m| m.dependencies())
// .unwrap_or_default()
// }
// pub fn load(fs: Arc<Fs>, file_name: String) -> anyhow::Result<Self> {
// todo!()
// }
// pub fn save(&self, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
// pub fn rename(&self, new_file_name: String, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
} }
impl StaticPrompt { impl StaticPrompt {
// pub fn update(&mut self, contents: String) -> &mut Self {
// self.content = contents;
// self
// }
/// Sets the file name of the prompt /// 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.file_name = Some(file_name);
self 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 /// Returns the prompt's content
pub fn content(&self) -> &String { pub fn content(&self) -> &String {
&self.content &self.content
} }
fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> {
let matter = Matter::<YAML>::new(); /// Returns the prompt's metadata
let result = matter.parse(self.content.as_str()); pub fn _metadata(&self) -> &StaticPromptFrontmatter {
match result.data { &self.metadata
Some(data) => {
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
let body = result.content;
Ok((front_matter, body))
}
None => Err(anyhow::anyhow!("Failed to parse frontmatter")),
}
} }
pub fn metadata(&self) -> Option<StaticPromptFrontmatter> { /// Returns the prompt's title
self.parse().ok().map(|(front_matter, _)| front_matter) pub fn title(&self) -> SharedString {
self.metadata.title.clone().into()
}
pub fn body(&self) -> String {
let matter = Matter::<YAML>::new();
let result = matter.parse(self.content.as_str());
result.content.clone()
} }
} }

View File

@ -2,6 +2,7 @@ use anyhow::Context;
use collections::HashMap; use collections::HashMap;
use fs::Fs; use fs::Fs;
use gray_matter::{engine::YAML, Matter};
use parking_lot::RwLock; use parking_lot::RwLock;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smol::stream::StreamExt; use smol::stream::StreamExt;
@ -119,6 +120,17 @@ impl PromptLibrary {
while let Some(prompt_path) = prompt_paths.next().await { while let Some(prompt_path) = prompt_paths.next().await {
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?; 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 if !fs.is_file(&prompt_path).await
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md") || prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
@ -130,13 +142,17 @@ impl PromptLibrary {
.load(&prompt_path) .load(&prompt_path)
.await .await
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?; .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() { // Check that the prompt is valid
let file_name = file_name.to_string_lossy().into_owned(); let matter = Matter::<YAML>::new();
static_prompt.file_name(file_name); 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 state = self.state.get_mut();
let id = Uuid::new_v4(); let id = Uuid::new_v4();

View File

@ -321,7 +321,7 @@ impl PickerDelegate for PromptManagerDelegate {
.inset(true) .inset(true)
.spacing(ListItemSpacing::Sparse) .spacing(ListItemSpacing::Sparse)
.selected(selected) .selected(selected)
.child(Label::new(prompt.title().unwrap_or_default().clone())), .child(Label::new(prompt.title())),
) )
} }
} }

View File

@ -43,12 +43,7 @@ impl SlashCommand for PromptSlashCommand {
.prompts() .prompts()
.into_iter() .into_iter()
.enumerate() .enumerate()
.filter_map(|(ix, prompt)| { .map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.1.title().to_string()))
prompt
.1
.title()
.map(|title| StringMatchCandidate::new(ix, title.into()))
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = fuzzy::match_strings( let matches = fuzzy::match_strings(
&candidates, &candidates,
@ -86,11 +81,10 @@ impl SlashCommand for PromptSlashCommand {
let prompt = library let prompt = library
.prompts() .prompts()
.into_iter() .into_iter()
.filter_map(|prompt| prompt.1.title().map(|title| (title, prompt))) .find(|prompt| &prompt.1.title().to_string() == &title)
.find(|(t, _)| t == &title)
.with_context(|| format!("no prompt found with title {:?}", title))? .with_context(|| format!("no prompt found with title {:?}", title))?
.1; .1;
Ok(prompt.1.content().to_owned()) Ok(prompt.body())
}); });
SlashCommandInvocation { SlashCommandInvocation {
output, output,