Allow /workflow and step resolution prompts to be overridden (#15892)

This will help us as we hit issues with the /workflow and step
resolution. We can override the baked-in prompts and make tweaks, then
import our refinements back into the source tree when we're ready.

Release Notes:

- N/A
This commit is contained in:
Nathan Sobo
2024-08-06 21:47:42 -06:00
committed by GitHub
parent c8f1358629
commit 990774247e
14 changed files with 196 additions and 104 deletions
+28 -14
View File
@@ -25,6 +25,7 @@ use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
pub(crate) use model_selector::*;
pub use prompts::PromptBuilder;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
@@ -163,7 +164,7 @@ impl Assistant {
}
}
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<PromptBuilder> {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
@@ -196,19 +197,25 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
prompt_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
register_slash_commands(cx);
assistant_panel::init(cx);
if let Some(prompt_builder) = prompts::PromptBuilder::new(Some((fs.clone(), cx))).log_err() {
let prompt_builder = Arc::new(prompt_builder);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx);
}
let prompt_builder = prompts::PromptBuilder::new(Some((fs.clone(), cx)))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
IndexedDocsRegistry::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -226,6 +233,8 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
});
})
.detach();
prompt_builder
}
fn init_language_model_settings(cx: &mut AppContext) {
@@ -256,7 +265,7 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
});
}
fn register_slash_commands(cx: &mut AppContext) {
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
@@ -270,7 +279,12 @@ fn register_slash_commands(cx: &mut AppContext) {
slash_command_registry.register_command(now_command::NowSlashCommand, true);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
slash_command_registry.register_command(workflow_command::WorkflowSlashCommand, true);
if let Some(prompt_builder) = prompt_builder {
slash_command_registry.register_command(
workflow_command::WorkflowSlashCommand::new(prompt_builder),
true,
);
}
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
}
+5 -1
View File
@@ -2,6 +2,7 @@ use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count,
prompt_library::open_prompt_library,
prompts::PromptBuilder,
slash_command::{
default_command::DefaultSlashCommand,
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
@@ -315,14 +316,17 @@ impl PickerDelegate for SavedContextPickerDelegate {
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
prompt_builder: Arc<PromptBuilder>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let context_store = workspace
.update(&mut cx, |workspace, cx| {
ContextStore::new(workspace.project().clone(), cx)
let project = workspace.project().clone();
ContextStore::new(project, prompt_builder.clone(), cx)
})?
.await?;
workspace.update(&mut cx, |workspace, cx| {
// TODO: deserialize state.
cx.new_view(|cx| Self::new(workspace, context_store, cx))
+42 -11
View File
@@ -1,5 +1,5 @@
use crate::{
prompt_library::PromptStore, slash_command::SlashCommandLine, AssistantPanel, InitialInsertion,
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantPanel, InitialInsertion,
InlineAssistId, InlineAssistant, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
@@ -611,6 +611,7 @@ pub struct Context {
language_registry: Arc<LanguageRegistry>,
workflow_steps: Vec<WorkflowStep>,
project: Option<Model<Project>>,
prompt_builder: Arc<PromptBuilder>,
}
impl EventEmitter<ContextEvent> for Context {}
@@ -620,6 +621,7 @@ impl Context {
language_registry: Arc<LanguageRegistry>,
project: Option<Model<Project>>,
telemetry: Option<Arc<Telemetry>>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
) -> Self {
Self::new(
@@ -627,17 +629,20 @@ impl Context {
ReplicaId::default(),
language::Capability::ReadWrite,
language_registry,
prompt_builder,
project,
telemetry,
cx,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
id: ContextId,
replica_id: ReplicaId,
capability: language::Capability,
language_registry: Arc<LanguageRegistry>,
prompt_builder: Arc<PromptBuilder>,
project: Option<Model<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
@@ -680,6 +685,7 @@ impl Context {
project,
language_registry,
workflow_steps: Vec::new(),
prompt_builder,
};
let first_message_id = MessageId(clock::Lamport {
@@ -749,6 +755,7 @@ impl Context {
saved_context: SavedContext,
path: PathBuf,
language_registry: Arc<LanguageRegistry>,
prompt_builder: Arc<PromptBuilder>,
project: Option<Model<Project>>,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
@@ -759,6 +766,7 @@ impl Context {
ReplicaId::default(),
language::Capability::ReadWrite,
language_registry,
prompt_builder,
project,
telemetry,
cx,
@@ -1246,9 +1254,9 @@ impl Context {
cx.spawn(|this, mut cx| {
async move {
let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?;
let mut prompt = prompt_store.step_resolution_prompt()?;
let mut prompt = this.update(&mut cx, |this, _| {
this.prompt_builder.generate_step_resolution_prompt()
})??;
prompt.push_str(&step_text);
request.messages.push(LanguageModelRequestMessage {
@@ -2448,8 +2456,9 @@ mod tests {
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let context = cx.new_model(|cx| Context::local(registry, None, None, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -2580,7 +2589,9 @@ mod tests {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let context = cx.new_model(|cx| Context::local(registry, None, None, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -2673,7 +2684,9 @@ mod tests {
cx.set_global(settings_store);
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let context = cx.new_model(|cx| Context::local(registry, None, None, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -2778,7 +2791,10 @@ mod tests {
slash_command_registry.register_command(active_command::ActiveSlashCommand, false);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let context = cx.new_model(|cx| Context::local(registry.clone(), None, None, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(registry.clone(), None, None, prompt_builder.clone(), cx)
});
let output_ranges = Rc::new(RefCell::new(HashSet::default()));
context.update(cx, |_, cx| {
@@ -2905,7 +2921,16 @@ mod tests {
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
// Create a new context
let context = cx.new_model(|cx| Context::local(registry.clone(), Some(project), None, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(
registry.clone(),
Some(project),
None,
prompt_builder.clone(),
cx,
)
});
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
// Simulate user input
@@ -3070,7 +3095,10 @@ mod tests {
cx.update(LanguageModelRegistry::test);
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let context = cx.new_model(|cx| Context::local(registry.clone(), None, None, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new_model(|cx| {
Context::local(registry.clone(), None, None, prompt_builder.clone(), cx)
});
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
let message_1 = context.update(cx, |context, cx| {
@@ -3109,6 +3137,7 @@ mod tests {
serialized_context,
Default::default(),
registry.clone(),
prompt_builder.clone(),
None,
None,
cx,
@@ -3158,6 +3187,7 @@ mod tests {
let num_peers = rng.gen_range(min_peers..=max_peers);
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
let context = cx.new_model(|cx| {
Context::new(
@@ -3165,6 +3195,7 @@ mod tests {
i as ReplicaId,
language::Capability::ReadWrite,
registry.clone(),
prompt_builder.clone(),
None,
None,
cx,
+16 -3
View File
@@ -1,6 +1,6 @@
use crate::{
Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
SavedContext, SavedContextMetadata,
};
use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
@@ -52,6 +52,7 @@ pub struct ContextStore {
project_is_shared: bool,
client_subscription: Option<client::Subscription>,
_project_subscriptions: Vec<gpui::Subscription>,
prompt_builder: Arc<PromptBuilder>,
}
pub enum ContextStoreEvent {
@@ -82,7 +83,11 @@ impl ContextHandle {
}
impl ContextStore {
pub fn new(project: Model<Project>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
pub fn new(
project: Model<Project>,
prompt_builder: Arc<PromptBuilder>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
let fs = project.read(cx).fs().clone();
let languages = project.read(cx).languages().clone();
let telemetry = project.read(cx).client().telemetry().clone();
@@ -117,6 +122,7 @@ impl ContextStore {
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
prompt_builder,
};
this.handle_project_changed(project, cx);
this.synchronize_contexts(cx);
@@ -334,6 +340,7 @@ impl ContextStore {
self.languages.clone(),
Some(self.project.clone()),
Some(self.telemetry.clone()),
self.prompt_builder.clone(),
cx,
)
});
@@ -358,6 +365,7 @@ impl ContextStore {
let language_registry = self.languages.clone();
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let prompt_builder = self.prompt_builder.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
let response = request.await?;
@@ -369,6 +377,7 @@ impl ContextStore {
replica_id,
capability,
language_registry,
prompt_builder,
Some(project),
Some(telemetry),
cx,
@@ -417,6 +426,7 @@ impl ContextStore {
SavedContext::from_json(&saved_context)
}
});
let prompt_builder = self.prompt_builder.clone();
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
@@ -425,6 +435,7 @@ impl ContextStore {
saved_context,
path.clone(),
languages,
prompt_builder,
Some(project),
Some(telemetry),
cx,
@@ -493,6 +504,7 @@ impl ContextStore {
project_id,
context_id: context_id.to_proto(),
});
let prompt_builder = self.prompt_builder.clone();
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
@@ -502,6 +514,7 @@ impl ContextStore {
replica_id,
capability,
language_registry,
prompt_builder,
Some(project),
Some(telemetry),
cx,
+2 -14
View File
@@ -2,7 +2,6 @@ use crate::{
slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssist, InlineAssistant,
};
use anyhow::{anyhow, Result};
use assets::Assets;
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet};
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
@@ -12,8 +11,8 @@ use futures::{
};
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds,
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
@@ -1466,17 +1465,6 @@ impl PromptStore {
fn first(&self) -> Option<PromptMetadata> {
self.metadata_cache.read().metadata.first().cloned()
}
pub fn step_resolution_prompt(&self) -> Result<String> {
let path = "prompts/step_resolution.md";
Ok(String::from_utf8(
Assets
.load(path)?
.ok_or_else(|| anyhow!("{path} not found"))?
.to_vec(),
)?)
}
}
/// Wraps a shared future to a prompt store so it can be assigned as a context global.
+21 -15
View File
@@ -125,22 +125,20 @@ impl PromptBuilder {
}
fn register_templates(handlebars: &mut Handlebars) -> Result<(), Box<TemplateError>> {
let content_prompt = Assets::get("prompts/content_prompt.hbs")
.expect("Content prompt template not found")
.data;
let terminal_assistant_prompt = Assets::get("prompts/terminal_assistant_prompt.hbs")
.expect("Terminal assistant prompt template not found")
.data;
let mut register_template = |id: &str| {
let prompt = Assets::get(&format!("prompts/{}.hbs", id))
.unwrap_or_else(|| panic!("{} prompt template not found", id))
.data;
handlebars
.register_template_string(id, String::from_utf8_lossy(&prompt))
.map_err(Box::new)
};
register_template("content_prompt")?;
register_template("terminal_assistant_prompt")?;
register_template("edit_workflow")?;
register_template("step_resolution")?;
handlebars
.register_template_string("content_prompt", String::from_utf8_lossy(&content_prompt))
.map_err(Box::new)?;
handlebars
.register_template_string(
"terminal_assistant_prompt",
String::from_utf8_lossy(&terminal_assistant_prompt),
)
.map_err(Box::new)?;
Ok(())
}
@@ -236,4 +234,12 @@ impl PromptBuilder {
.lock()
.render("terminal_assistant_prompt", &context)
}
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("edit_workflow", &())
}
pub fn generate_step_resolution_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("step_resolution", &())
}
}
@@ -1,18 +1,27 @@
use std::sync::atomic::AtomicBool;
use crate::prompts::PromptBuilder;
use std::sync::Arc;
use anyhow::{Context as _, Result};
use assets::Assets;
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{AppContext, AssetSource, Task, WeakView};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use text::LineEnding;
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct WorkflowSlashCommand;
pub(crate) struct WorkflowSlashCommand {
prompt_builder: Arc<PromptBuilder>,
}
impl WorkflowSlashCommand {
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
Self { prompt_builder }
}
}
impl SlashCommand for WorkflowSlashCommand {
fn name(&self) -> String {
@@ -46,26 +55,22 @@ impl SlashCommand for WorkflowSlashCommand {
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
_cx: &mut WindowContext,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let mut text = match Assets
.load("prompts/edit_workflow.md")
.and_then(|prompt| prompt.context("prompts/edit_workflow.md not found"))
{
Ok(prompt) => String::from_utf8_lossy(&prompt).into_owned(),
Err(error) => return Task::ready(Err(error)),
};
LineEnding::normalize(&mut text);
let range = 0..text.len();
let prompt_builder = self.prompt_builder.clone();
cx.spawn(|_cx| async move {
let text = prompt_builder.generate_workflow_prompt()?;
let range = 0..text.len();
Task::ready(Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Route,
label: "Workflow".into(),
}],
run_commands_in_text: false,
}))
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Route,
label: "Workflow".into(),
}],
run_commands_in_text: false,
})
})
}
}