From a9c6e435f7191e428c250103ebb17f229adb0415 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 1 Aug 2024 15:56:17 +0200 Subject: [PATCH] Bundle editing workflow prompt as a read-only built-in prompt (#15615) Built-in prompts can still be removed from the default prompt, but they can't be edited and are automatically updated with new Zed releases. Release Notes: - N/A --------- Co-authored-by: Antonio --- assets/prompts/edit_workflow.md | 87 +++++++++++ .../{operations.md => step_resolution.md} | 0 crates/assistant/src/context.rs | 2 +- crates/assistant/src/prompt_library.rs | 135 +++++++++++++++--- crates/assistant/src/system_prompts/edits.md | 86 ----------- 5 files changed, 207 insertions(+), 103 deletions(-) create mode 100644 assets/prompts/edit_workflow.md rename assets/prompts/{operations.md => step_resolution.md} (100%) delete mode 100644 crates/assistant/src/system_prompts/edits.md diff --git a/assets/prompts/edit_workflow.md b/assets/prompts/edit_workflow.md new file mode 100644 index 0000000000000000000000000000000000000000..4675aa921bcfc32f23e88c146a4d5ffbbd3310c6 --- /dev/null +++ b/assets/prompts/edit_workflow.md @@ -0,0 +1,87 @@ +Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc. +Surround each distinct step in a XML tag. + +Here's an example of a user-assistant dialog with step numbers and a follow-up question in Rust: + +1. User: I want to create a User struct with name and age fields, and a method to print the user's information. Can you help me with that? +A: Certainly! Let's create a User struct with the specified fields and a method to print the information: +Define the User struct +```rust +struct User { + name: String, + age: u32, +} +``` + +Implement methods for the User struct +```rust +impl User { + fn new(name: String, age: u32) -> Self { + User { name, age } + } + fn print_info(&self) { + println!("Name: {}, Age: {}", self.name, self.age); + } +} +``` + + +This code defines a User struct and implements two methods: +- `new`: a constructor to create a new User +- `print_info`: a method to print the user's information + +2. User: Great! Now, how can I create a User instance and call the print_info method? +A: You can create a User instance and call the print_info method like this: + +Create a User instance and call print_info +```rust +fn main() { + let user = User::new(String::from("Alice"), 30); + user.print_info(); +} +``` + + +This will output: `Name: Alice, Age: 30` + +3. User: Can you add an email field to the User struct and update the print_info method to include it? +A: Certainly! Let's modify the User struct and update the print_info method: + +Add email field to User struct +```rust +struct User { + name: String, + age: u32, + email: String, +} +``` + + +Update the new method and print_info method +```rust +impl User { + fn new(name: String, age: u32, email: String) -> Self { + User { name, age, email } + } + fn print_info(&self) { + println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email); + } +} +``` + + +Update the main function +```rust +fn main() { + let user = User::new(String::from("Alice"), 30, String::from("alice@example.com")); + user.print_info(); +} +``` + + +This will now output: `Name: Alice, Age: 30, Email: alice@example.com` +The changes include: +1. Adding the `email` field to the User struct +2. Updating the `new` method to accept an email parameter +3. Modifying the `print_info` method to include the email +4. Updating the main function to provide an email when creating a User instance diff --git a/assets/prompts/operations.md b/assets/prompts/step_resolution.md similarity index 100% rename from assets/prompts/operations.md rename to assets/prompts/step_resolution.md diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f6666a823cd2a499108424f7fd5e61be76bcb5c9..6e1dfcac93da414895144eb4e5047a2c9cfd9380 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1386,7 +1386,7 @@ impl Context { async move { let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?; - let mut prompt = prompt_store.operations_prompt(); + let mut prompt = prompt_store.step_resolution_prompt(); prompt.push_str(&step_text); request.messages.push(LanguageModelRequestMessage { diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs index 1e15576fe4b9575fbff67bd9c6cc5ab61cb0cef4..a121a5b3169180ee3b264f1c114a8d3f0d5c80a6 100644 --- a/crates/assistant/src/prompt_library.rs +++ b/crates/assistant/src/prompt_library.rs @@ -66,6 +66,11 @@ pub fn init(cx: &mut AppContext) { cx.set_global(GlobalPromptStore(prompt_store_future)) } +const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!( + "This prompt supports special functionality.\n", + "It's read-only, but you can remove it from your default prompt." +); + /// This function opens a new prompt library window if one doesn't exist already. /// If one exists, it brings it to the foreground. /// @@ -233,15 +238,29 @@ impl PickerDelegate for PromptPickerDelegate { .end_hover_slot( h_flex() .gap_2() - .child( + .child(if prompt_id.is_built_in() { + div() + .id("built-in-prompt") + .child(Icon::new(IconName::FileLock).color(Color::Muted)) + .tooltip(move |cx| { + Tooltip::with_meta( + "Built-in prompt", + None, + BUILT_IN_TOOLTIP_TEXT, + cx, + ) + }) + .into_any() + } else { IconButton::new("delete-prompt", IconName::Trash) .icon_color(Color::Muted) .shape(IconButtonShape::Square) .tooltip(move |cx| Tooltip::text("Delete Prompt", cx)) .on_click(cx.listener(move |_, _, cx| { cx.emit(PromptPickerEvent::Deleted { prompt_id }) - })), - ) + })) + .into_any_element() + }) .child( IconButton::new("toggle-default-prompt", IconName::Sparkle) .selected(default) @@ -354,6 +373,10 @@ impl PromptLibrary { pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext) { const SAVE_THROTTLE: Duration = Duration::from_millis(500); + if prompt_id.is_built_in() { + return; + } + let prompt_metadata = self.store.metadata(prompt_id).unwrap(); let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap(); let title = prompt_editor.title_editor.read(cx).text(cx); @@ -463,6 +486,7 @@ impl PromptLibrary { let mut editor = Editor::auto_width(cx); editor.set_placeholder_text("Untitled", cx); editor.set_text(prompt_metadata.title.unwrap_or_default(), cx); + editor.set_read_only(true); editor }); let body_editor = cx.new_view(|cx| { @@ -474,6 +498,7 @@ impl PromptLibrary { }); let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_read_only(true); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); editor.set_show_wrap_guides(false, cx); @@ -943,7 +968,23 @@ impl PromptLibrary { ) }, )) - .child( + .child(if prompt_id.is_built_in() { + div() + .id("built-in-prompt") + .child( + Icon::new(IconName::FileLock) + .color(Color::Muted), + ) + .tooltip(move |cx| { + Tooltip::with_meta( + "Built-in prompt", + None, + BUILT_IN_TOOLTIP_TEXT, + cx, + ) + }) + .into_any() + } else { IconButton::new( "delete-prompt", IconName::Trash, @@ -961,8 +1002,9 @@ impl PromptLibrary { }) .on_click(|_, cx| { cx.dispatch_action(Box::new(DeletePrompt)); - }), - ) + }) + .into_any_element() + }) .child( IconButton::new( "duplicate-prompt", @@ -1062,7 +1104,6 @@ pub struct PromptMetadata { pub title: Option, pub default: bool, pub saved_at: DateTime, - pub built_in: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -1078,6 +1119,10 @@ impl PromptId { uuid: Uuid::new_v4(), } } + + pub fn is_built_in(&self) -> bool { + !matches!(self, PromptId::User { .. }) + } } pub struct PromptStore { @@ -1163,13 +1208,17 @@ impl PromptStore { let metadata_cache = MetadataCache::from_db(metadata, &txn)?; txn.commit()?; - Ok(PromptStore { + let store = PromptStore { executor, env: db_env, metadata_cache: RwLock::new(metadata_cache), metadata, bodies, - }) + }; + + store.save_built_in_prompts().log_err(); + + Ok(store) } }) } @@ -1237,7 +1286,6 @@ impl PromptStore { title: metadata_v1.title.clone(), default: metadata_v1.default, saved_at: metadata_v1.saved_at, - built_in: false, }, )?; bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?; @@ -1346,12 +1394,15 @@ impl PromptStore { default: bool, body: Rope, ) -> Task> { + if id.is_built_in() { + return Task::ready(Err(anyhow!("built-in prompts cannot be saved"))); + } + let prompt_metadata = PromptMetadata { id, title, default, saved_at: Utc::now(), - built_in: false, }; self.metadata_cache.write().insert(prompt_metadata.clone()); @@ -1371,20 +1422,72 @@ impl PromptStore { }) } + fn save_built_in_prompts(&self) -> Result<()> { + self.save_built_in_prompt( + PromptId::EditWorkflow, + "Built-in: Editing Workflow", + "prompts/edit_workflow.md", + )?; + Ok(()) + } + + /// Write a built-in prompt to the database, preserving the value of the default field + /// if a prompt with this id already exists. This method blocks. + fn save_built_in_prompt( + &self, + id: PromptId, + title: impl Into, + body_path: &str, + ) -> Result<()> { + let mut metadata_cache = self.metadata_cache.write(); + let existing_metadata = metadata_cache.metadata_by_id.get(&id).cloned(); + + let prompt_metadata = PromptMetadata { + id, + title: Some(title.into()), + default: existing_metadata.map_or(true, |m| m.default), + saved_at: Utc::now(), + }; + + metadata_cache.insert(prompt_metadata.clone()); + + let db_connection = self.env.clone(); + let bodies = self.bodies; + let metadata_db = self.metadata; + + let mut txn = db_connection.write_txn()?; + metadata_db.put(&mut txn, &id, &prompt_metadata)?; + + let body = String::from_utf8(Assets.load(body_path)?.unwrap().to_vec())?; + bodies.put(&mut txn, &id, &body)?; + + txn.commit()?; + Ok(()) + } + fn save_metadata( &self, id: PromptId, - title: Option, + mut title: Option, default: bool, ) -> Task> { + let mut cache = self.metadata_cache.write(); + + if id.is_built_in() { + title = cache + .metadata_by_id + .get(&id) + .and_then(|metadata| metadata.title.clone()); + } + let prompt_metadata = PromptMetadata { id, title, default, saved_at: Utc::now(), - built_in: false, }; - self.metadata_cache.write().insert(prompt_metadata.clone()); + + cache.insert(prompt_metadata.clone()); let db_connection = self.env.clone(); let metadata = self.metadata; @@ -1402,10 +1505,10 @@ impl PromptStore { self.metadata_cache.read().metadata.first().cloned() } - pub fn operations_prompt(&self) -> String { + pub fn step_resolution_prompt(&self) -> String { String::from_utf8( Assets - .load("prompts/operations.md") + .load("prompts/step_resolution.md") .unwrap() .unwrap() .to_vec(), diff --git a/crates/assistant/src/system_prompts/edits.md b/crates/assistant/src/system_prompts/edits.md deleted file mode 100644 index f3b77332edf9f63f66f23fff3dbf9e293af74de4..0000000000000000000000000000000000000000 --- a/crates/assistant/src/system_prompts/edits.md +++ /dev/null @@ -1,86 +0,0 @@ -When the user asks you to suggest edits for a buffer, use a strict template consisting of: - -* A markdown code block with the file path as the language identifier. -* The original code that should be replaced -* A separator line (`---`) -* The new text that should replace the original lines - -Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits. - -## Example - -If you have a buffer with the following lines: - -```path/to/file.rs -fn quicksort(arr: &mut [i32]) { - if arr.len() <= 1 { - return; - } - let pivot_index = partition(arr); - let (left, right) = arr.split_at_mut(pivot_index); - quicksort(left); - quicksort(&mut right[1..]); -} - -fn partition(arr: &mut [i32]) -> usize { - let last_index = arr.len() - 1; - let pivot = arr[last_index]; - let mut i = 0; - for j in 0..last_index { - if arr[j] <= pivot { - arr.swap(i, j); - i += 1; - } - } - arr.swap(i, last_index); - i -} -``` - -And you want to replace the for loop inside `partition`, output the following. - -```edit path/to/file.rs -for j in 0..last_index { - if arr[j] <= pivot { - arr.swap(i, j); - i += 1; - } -} ---- -let mut j = 0; -while j < last_index { - if arr[j] <= pivot { - arr.swap(i, j); - i += 1; - } - j += 1; -} -``` - -If you wanted to insert comments above the partition function, output the following: - -```edit path/to/file.rs -fn partition(arr: &mut [i32]) -> usize { ---- -// A helper function used for quicksort. -fn partition(arr: &mut [i32]) -> usize { -``` - -If you wanted to delete the partition function, output the following: - -```edit path/to/file.rs -fn partition(arr: &mut [i32]) -> usize { - let last_index = arr.len() - 1; - let pivot = arr[last_index]; - let mut i = 0; - for j in 0..last_index { - if arr[j] <= pivot { - arr.swap(i, j); - i += 1; - } - } - arr.swap(i, last_index); - i -} ---- -```