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 {
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<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
/// 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<String>,
}
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 {
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 {
// 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::<YAML>::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<StaticPromptFrontmatter> {
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::<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 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::<YAML>::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();

View File

@ -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())),
)
}
}

View File

@ -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::<Vec<_>>();
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,