From e0cabbd142de8ca7cc041e16822c9eaa8e3f8533 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 14 Aug 2024 22:44:44 -0700 Subject: [PATCH] Make WorkflowStepResolution an entity (#16268) This PR is just a refactor, to pave the way toward adding a view for workflow step resolution. The entity carries the state of the tool call's streaming output. Release Notes: - N/A --- crates/assistant/src/assistant.rs | 2 + crates/assistant/src/assistant_panel.rs | 18 +- crates/assistant/src/context.rs | 762 ++------------------ crates/assistant/src/context_inspector.rs | 14 +- crates/assistant/src/workflow.rs | 672 +++++++++++++++++ crates/language_model/src/language_model.rs | 10 + 6 files changed, 753 insertions(+), 725 deletions(-) create mode 100644 crates/assistant/src/workflow.rs diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 28be7e98b9..e7adff1286 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -13,6 +13,7 @@ mod slash_command; pub mod slash_command_settings; mod streaming_diff; mod terminal_inline_assistant; +mod workflow; pub use assistant_panel::{AssistantPanel, AssistantPanelEvent}; use assistant_settings::AssistantSettings; @@ -43,6 +44,7 @@ use slash_command::{ use std::sync::Arc; pub(crate) use streaming_diff::*; use util::ResultExt; +pub use workflow::*; use crate::slash_command_settings::SlashCommandSettings; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index f1041d1f12..6101403c03 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1884,9 +1884,8 @@ impl ContextEditor { range: Range, cx: &mut ViewContext, ) { - self.context.update(cx, |context, cx| { - context.resolve_workflow_step(range, self.project.clone(), cx) - }); + self.context + .update(cx, |context, cx| context.resolve_workflow_step(range, cx)); } fn stop_workflow_step(&mut self, range: Range, cx: &mut ViewContext) { @@ -2010,19 +2009,16 @@ impl ContextEditor { .text_for_range(step.tagged_range.clone()) .collect::() )); - match &step.status { - crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep { - title, - suggestions, - }) => { + match &step.resolution.read(cx).result { + Some(Ok(ResolvedWorkflowStep { title, suggestions })) => { output.push_str("Resolution:\n"); output.push_str(&format!(" {:?}\n", title)); output.push_str(&format!(" {:?}\n", suggestions)); } - crate::WorkflowStepStatus::Pending(_) => { + None => { output.push_str("Resolution: Pending\n"); } - crate::WorkflowStepStatus::Error(error) => { + Some(Err(error)) => { writeln!(output, "Resolution: Error\n{:?}", error).unwrap(); } } @@ -2485,7 +2481,7 @@ impl ContextEditor { return; }; - let resolved_step = step.status.into_resolved(); + let resolved_step = step.resolution.read(cx).result.clone(); if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) { existing_step.resolved_step = resolved_step; if let Some(debug) = self.debug_inspector.as_mut() { diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index 14791d934a..4d41f54a2c 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -1,6 +1,6 @@ use crate::{ - prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantPanel, InlineAssistId, - InlineAssistant, MessageId, MessageStatus, + prompts::PromptBuilder, slash_command::SlashCommandLine, workflow::WorkflowStepResolution, + MessageId, MessageStatus, }; use anyhow::{anyhow, Context as _, Result}; use assistant_slash_command::{ @@ -9,34 +9,25 @@ use assistant_slash_command::{ use client::{self, proto, telemetry::Telemetry}; use clock::ReplicaId; use collections::{HashMap, HashSet}; -use editor::Editor; use fs::{Fs, RemoveOptions}; -use futures::{ - future::{self, Shared}, - stream::FuturesUnordered, - FutureExt, StreamExt, -}; +use futures::{future::Shared, stream::FuturesUnordered, FutureExt, StreamExt}; use gpui::{ - AppContext, Context as _, EventEmitter, Image, Model, ModelContext, RenderImage, Subscription, - Task, UpdateGlobal, View, WeakView, + AppContext, Context as _, EventEmitter, Image, Model, ModelContext, RenderImage, SharedString, + Subscription, Task, }; -use language::{ - AnchorRangeExt, Bias, Buffer, BufferSnapshot, LanguageRegistry, OffsetRangeExt, ParseStatus, - Point, ToOffset, -}; +use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset}; use language_model::{ LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelTool, Role, + Role, }; use open_ai::Model as OpenAiModel; use paths::{context_images_dir, contexts_dir}; use project::Project; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ - cmp::{self, Ordering}, + cmp::Ordering, collections::hash_map, fmt::Debug, iter, mem, @@ -46,10 +37,8 @@ use std::{ time::{Duration, Instant}, }; use telemetry_events::AssistantKind; -use ui::{SharedString, WindowContext}; use util::{post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; -use workspace::Workspace; #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct ContextId(String); @@ -408,250 +397,17 @@ struct PendingCompletion { #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] pub struct SlashCommandId(clock::Lamport); -#[derive(Debug)] pub struct WorkflowStep { pub tagged_range: Range, - pub status: WorkflowStepStatus, + pub resolution: Model, + pub _task: Option>, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ResolvedWorkflowStep { - pub title: String, - pub suggestions: HashMap, Vec>, -} - -pub enum WorkflowStepStatus { - Pending(Task>), - Resolved(ResolvedWorkflowStep), - Error(Arc), -} - -impl WorkflowStepStatus { - pub fn into_resolved(&self) -> Option>> { - match self { - WorkflowStepStatus::Resolved(resolved) => Some(Ok(resolved.clone())), - WorkflowStepStatus::Error(error) => Some(Err(error.clone())), - WorkflowStepStatus::Pending(_) => None, - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct WorkflowSuggestionGroup { - pub context_range: Range, - pub suggestions: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum WorkflowSuggestion { - Update { - range: Range, - description: String, - }, - CreateFile { - description: String, - }, - InsertSiblingBefore { - position: language::Anchor, - description: String, - }, - InsertSiblingAfter { - position: language::Anchor, - description: String, - }, - PrependChild { - position: language::Anchor, - description: String, - }, - AppendChild { - position: language::Anchor, - description: String, - }, - Delete { - range: Range, - }, -} - -impl WorkflowSuggestion { - pub fn range(&self) -> Range { - match self { - WorkflowSuggestion::Update { range, .. } => range.clone(), - WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX, - WorkflowSuggestion::InsertSiblingBefore { position, .. } - | WorkflowSuggestion::InsertSiblingAfter { position, .. } - | WorkflowSuggestion::PrependChild { position, .. } - | WorkflowSuggestion::AppendChild { position, .. } => *position..*position, - WorkflowSuggestion::Delete { range } => range.clone(), - } - } - - pub fn description(&self) -> Option<&str> { - match self { - WorkflowSuggestion::Update { description, .. } - | WorkflowSuggestion::CreateFile { description } - | WorkflowSuggestion::InsertSiblingBefore { description, .. } - | WorkflowSuggestion::InsertSiblingAfter { description, .. } - | WorkflowSuggestion::PrependChild { description, .. } - | WorkflowSuggestion::AppendChild { description, .. } => Some(description), - WorkflowSuggestion::Delete { .. } => None, - } - } - - fn description_mut(&mut self) -> Option<&mut String> { - match self { - WorkflowSuggestion::Update { description, .. } - | WorkflowSuggestion::CreateFile { description } - | WorkflowSuggestion::InsertSiblingBefore { description, .. } - | WorkflowSuggestion::InsertSiblingAfter { description, .. } - | WorkflowSuggestion::PrependChild { description, .. } - | WorkflowSuggestion::AppendChild { description, .. } => Some(description), - WorkflowSuggestion::Delete { .. } => None, - } - } - - fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool { - let range = self.range(); - let other_range = other.range(); - - // Don't merge if we don't contain the other suggestion. - if range.start.cmp(&other_range.start, buffer).is_gt() - || range.end.cmp(&other_range.end, buffer).is_lt() - { - return false; - } - - if let Some(description) = self.description_mut() { - if let Some(other_description) = other.description() { - description.push('\n'); - description.push_str(other_description); - } - } - true - } - - pub fn show( - &self, - editor: &View, - excerpt_id: editor::ExcerptId, - workspace: &WeakView, - assistant_panel: &View, - cx: &mut WindowContext, - ) -> Option { - let mut initial_transaction_id = None; - let initial_prompt; - let suggestion_range; - let buffer = editor.read(cx).buffer().clone(); - let snapshot = buffer.read(cx).snapshot(cx); - - match self { - WorkflowSuggestion::Update { range, description } => { - initial_prompt = description.clone(); - suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? - ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?; - } - WorkflowSuggestion::CreateFile { description } => { - initial_prompt = description.clone(); - suggestion_range = editor::Anchor::min()..editor::Anchor::min(); - } - WorkflowSuggestion::InsertSiblingBefore { - position, - description, - } => { - let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; - initial_prompt = description.clone(); - suggestion_range = buffer.update(cx, |buffer, cx| { - buffer.start_transaction(cx); - let line_start = buffer.insert_empty_line(position, true, true, cx); - initial_transaction_id = buffer.end_transaction(cx); - buffer.refresh_preview(cx); - - let line_start = buffer.read(cx).anchor_before(line_start); - line_start..line_start - }); - } - WorkflowSuggestion::InsertSiblingAfter { - position, - description, - } => { - let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; - initial_prompt = description.clone(); - suggestion_range = buffer.update(cx, |buffer, cx| { - buffer.start_transaction(cx); - let line_start = buffer.insert_empty_line(position, true, true, cx); - initial_transaction_id = buffer.end_transaction(cx); - buffer.refresh_preview(cx); - - let line_start = buffer.read(cx).anchor_before(line_start); - line_start..line_start - }); - } - WorkflowSuggestion::PrependChild { - position, - description, - } => { - let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; - initial_prompt = description.clone(); - suggestion_range = buffer.update(cx, |buffer, cx| { - buffer.start_transaction(cx); - let line_start = buffer.insert_empty_line(position, false, true, cx); - initial_transaction_id = buffer.end_transaction(cx); - buffer.refresh_preview(cx); - - let line_start = buffer.read(cx).anchor_before(line_start); - line_start..line_start - }); - } - WorkflowSuggestion::AppendChild { - position, - description, - } => { - let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; - initial_prompt = description.clone(); - suggestion_range = buffer.update(cx, |buffer, cx| { - buffer.start_transaction(cx); - let line_start = buffer.insert_empty_line(position, true, false, cx); - initial_transaction_id = buffer.end_transaction(cx); - buffer.refresh_preview(cx); - - let line_start = buffer.read(cx).anchor_before(line_start); - line_start..line_start - }); - } - WorkflowSuggestion::Delete { range } => { - initial_prompt = "Delete".to_string(); - suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? - ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?; - } - } - - InlineAssistant::update_global(cx, |inline_assistant, cx| { - Some(inline_assistant.suggest_assist( - editor, - suggestion_range, - initial_prompt, - initial_transaction_id, - Some(workspace.clone()), - Some(assistant_panel), - cx, - )) - }) - } -} - -impl Debug for WorkflowStepStatus { +impl Debug for WorkflowStep { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - WorkflowStepStatus::Pending(_) => write!(f, "WorkflowStepStatus::Pending"), - WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f - .debug_struct("WorkflowStepStatus::Resolved") - .field("title", title) - .field("suggestions", suggestions) - .finish(), - WorkflowStepStatus::Error(error) => f - .debug_tuple("WorkflowStepStatus::Error") - .field(error) - .finish(), - } + f.debug_struct("WorkflowStep") + .field("tagged_range", &self.tagged_range) + .finish_non_exhaustive() } } @@ -1082,6 +838,14 @@ impl Context { &self.buffer } + pub fn project(&self) -> Option> { + self.project.clone() + } + + pub fn prompt_builder(&self) -> Arc { + self.prompt_builder.clone() + } + pub fn path(&self) -> Option<&Path> { self.path.as_deref() } @@ -1308,12 +1072,7 @@ impl Context { start_ix..end_ix } - fn parse_workflow_steps_in_range( - &mut self, - range: Range, - project: Model, - cx: &mut ModelContext, - ) { + fn parse_workflow_steps_in_range(&mut self, range: Range, cx: &mut ModelContext) { let mut new_edit_steps = Vec::new(); let mut edits = Vec::new(); @@ -1356,8 +1115,11 @@ impl Context { new_edit_steps.push(( ix, WorkflowStep { + resolution: cx.new_model(|_| { + WorkflowStepResolution::new(tagged_range.clone()) + }), tagged_range, - status: WorkflowStepStatus::Pending(Task::ready(None)), + _task: None, }, )); } @@ -1374,7 +1136,7 @@ impl Context { let step_range = step.tagged_range.clone(); updated.push(step_range.clone()); self.workflow_steps.insert(index, step); - self.resolve_workflow_step(step_range, project.clone(), cx); + self.resolve_workflow_step(step_range, cx); } // Delete tags, making sure we don't accidentally invalidate @@ -1387,7 +1149,6 @@ impl Context { pub fn resolve_workflow_step( &mut self, tagged_range: Range, - project: Model, cx: &mut ModelContext, ) { let Ok(step_index) = self @@ -1397,152 +1158,22 @@ impl Context { return; }; - let mut request = self.to_completion_request(cx); - let Some(edit_step) = self.workflow_steps.get_mut(step_index) else { - return; - }; - - if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() { - let step_text = self - .buffer - .read(cx) - .text_for_range(tagged_range.clone()) - .collect::(); - - let tagged_range = tagged_range.clone(); - edit_step.status = WorkflowStepStatus::Pending(cx.spawn(|this, mut cx| { - async move { - let result = async { - let mut prompt = this.update(&mut cx, |this, _| { - this.prompt_builder.generate_step_resolution_prompt() - })??; - prompt.push_str(&step_text); - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![prompt.into()], - }); - - // Invoke the model to get its edit suggestions for this workflow step. - let resolution = model - .use_tool::(request, &cx) - .await?; - - // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code. - let suggestion_tasks: Vec<_> = resolution - .suggestions - .iter() - .map(|suggestion| suggestion.resolve(project.clone(), cx.clone())) - .collect(); - - // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges. - let suggestions = future::join_all(suggestion_tasks) - .await - .into_iter() - .filter_map(|task| task.log_err()) - .collect::>(); - - let mut suggestions_by_buffer = HashMap::default(); - for (buffer, suggestion) in suggestions { - suggestions_by_buffer - .entry(buffer) - .or_insert_with(Vec::new) - .push(suggestion); - } - - let mut suggestion_groups_by_buffer = HashMap::default(); - for (buffer, mut suggestions) in suggestions_by_buffer { - let mut suggestion_groups = Vec::::new(); - let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; - // Sort suggestions by their range so that earlier, larger ranges come first - suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot)); - - // Merge overlapping suggestions - suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot)); - - // Create context ranges for each suggestion - for suggestion in suggestions { - let context_range = { - let suggestion_point_range = - suggestion.range().to_point(&snapshot); - let start_row = - suggestion_point_range.start.row.saturating_sub(5); - let end_row = cmp::min( - suggestion_point_range.end.row + 5, - snapshot.max_point().row, - ); - let start = snapshot.anchor_before(Point::new(start_row, 0)); - let end = snapshot.anchor_after(Point::new( - end_row, - snapshot.line_len(end_row), - )); - start..end - }; - - if let Some(last_group) = suggestion_groups.last_mut() { - if last_group - .context_range - .end - .cmp(&context_range.start, &snapshot) - .is_ge() - { - // Merge with the previous group if context ranges overlap - last_group.context_range.end = context_range.end; - last_group.suggestions.push(suggestion); - } else { - // Create a new group - suggestion_groups.push(WorkflowSuggestionGroup { - context_range, - suggestions: vec![suggestion], - }); - } - } else { - // Create the first group - suggestion_groups.push(WorkflowSuggestionGroup { - context_range, - suggestions: vec![suggestion], - }); - } - } - - suggestion_groups_by_buffer.insert(buffer, suggestion_groups); - } - - Ok((resolution.step_title, suggestion_groups_by_buffer)) - }; - - let result = result.await; - this.update(&mut cx, |this, cx| { - let step_index = this - .workflow_steps - .binary_search_by(|step| { - step.tagged_range.cmp(&tagged_range, this.buffer.read(cx)) - }) - .map_err(|_| anyhow!("edit step not found"))?; - if let Some(edit_step) = this.workflow_steps.get_mut(step_index) { - edit_step.status = match result { - Ok((title, suggestions)) => { - WorkflowStepStatus::Resolved(ResolvedWorkflowStep { - title, - suggestions, - }) - } - Err(error) => WorkflowStepStatus::Error(Arc::new(error)), - }; - cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range)); - cx.notify(); - } - anyhow::Ok(()) - })? - } - .log_err() - })); - } else { - edit_step.status = WorkflowStepStatus::Error(Arc::new(anyhow!("no active model"))); - } - - cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range)); + cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range.clone())); cx.notify(); + + let task = self.workflow_steps[step_index] + .resolution + .update(cx, |resolution, cx| resolution.resolve(self, cx)); + self.workflow_steps[step_index]._task = task.map(|task| { + cx.spawn(|this, mut cx| async move { + task.await; + this.update(&mut cx, |_, cx| { + cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range)); + cx.notify(); + }) + .ok(); + }) + }); } pub fn pending_command_for_position( @@ -1762,11 +1393,10 @@ impl Context { ); message_start_offset..message_new_end_offset }); - if let Some(project) = this.project.clone() { - // Use `inclusive = false` as edits might occur at the end of a parsed step. - this.prune_invalid_workflow_steps(false, cx); - this.parse_workflow_steps_in_range(message_range, project, cx); - } + + // Use `inclusive = false` as edits might occur at the end of a parsed step. + this.prune_invalid_workflow_steps(false, cx); + this.parse_workflow_steps_in_range(message_range, cx); cx.emit(ContextEvent::StreamedCompletion); Some(()) @@ -2752,7 +2382,9 @@ pub struct SavedContextMetadata { #[cfg(test)] mod tests { use super::*; - use crate::{assistant_panel, prompt_library, slash_command::file_command, MessageId}; + use crate::{ + assistant_panel, prompt_library, slash_command::file_command, workflow::tool, MessageId, + }; use assistant_slash_command::{ArgumentCompletion, SlashCommand}; use fs::FakeFs; use gpui::{AppContext, TestAppContext, WeakView}; @@ -3392,10 +3024,10 @@ mod tests { .iter() .map(|step| { let buffer = context.buffer.read(cx); - let status = match &step.status { - WorkflowStepStatus::Pending(_) => WorkflowStepTestStatus::Pending, - WorkflowStepStatus::Resolved { .. } => WorkflowStepTestStatus::Resolved, - WorkflowStepStatus::Error(_) => WorkflowStepTestStatus::Error, + let status = match &step.resolution.read(cx).result { + None => WorkflowStepTestStatus::Pending, + Some(Ok(_)) => WorkflowStepTestStatus::Resolved, + Some(Err(_)) => WorkflowStepTestStatus::Error, }; (step.tagged_range.to_point(buffer), status) }) @@ -3798,289 +3430,3 @@ mod tests { } } } - -mod tool { - use gpui::AsyncAppContext; - use project::ProjectPath; - - use super::*; - - #[derive(Debug, Serialize, Deserialize, JsonSchema)] - pub struct WorkflowStepResolution { - /// An extremely short title for the edit step represented by these operations. - pub step_title: String, - /// A sequence of operations to apply to the codebase. - /// When multiple operations are required for a step, be sure to include multiple operations in this list. - pub suggestions: Vec, - } - - impl LanguageModelTool for WorkflowStepResolution { - fn name() -> String { - "edit".into() - } - - fn description() -> String { - "suggest edits to one or more locations in the codebase".into() - } - } - - /// A description of an operation to apply to one location in the codebase. - /// - /// This object represents a single edit operation that can be performed on a specific file - /// in the codebase. It encapsulates both the location (file path) and the nature of the - /// edit to be made. - /// - /// # Fields - /// - /// * `path`: A string representing the file path where the edit operation should be applied. - /// This path is relative to the root of the project or repository. - /// - /// * `kind`: An enum representing the specific type of edit operation to be performed. - /// - /// # Usage - /// - /// `EditOperation` is used within a code editor to represent and apply - /// programmatic changes to source code. It provides a structured way to describe - /// edits for features like refactoring tools or AI-assisted coding suggestions. - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] - pub struct WorkflowSuggestion { - /// The path to the file containing the relevant operation - pub path: String, - #[serde(flatten)] - pub kind: WorkflowSuggestionKind, - } - - impl WorkflowSuggestion { - pub(super) async fn resolve( - &self, - project: Model, - mut cx: AsyncAppContext, - ) -> Result<(Model, super::WorkflowSuggestion)> { - let path = self.path.clone(); - let kind = self.kind.clone(); - let buffer = project - .update(&mut cx, |project, cx| { - let project_path = project - .find_project_path(Path::new(&path), cx) - .or_else(|| { - // If we couldn't find a project path for it, put it in the active worktree - // so that when we create the buffer, it can be saved. - let worktree = project - .active_entry() - .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) - .or_else(|| project.worktrees(cx).next())?; - let worktree = worktree.read(cx); - - Some(ProjectPath { - worktree_id: worktree.id(), - path: Arc::from(Path::new(&path)), - }) - }) - .with_context(|| format!("worktree not found for {:?}", path))?; - anyhow::Ok(project.open_buffer(project_path, cx)) - })?? - .await?; - - let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?; - while *parse_status.borrow() != ParseStatus::Idle { - parse_status.changed().await?; - } - - let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; - let outline = snapshot.outline(None).context("no outline for buffer")?; - - let suggestion; - match kind { - WorkflowSuggestionKind::Update { - symbol, - description, - } => { - let symbol = outline - .find_most_similar(&symbol) - .with_context(|| format!("symbol not found: {:?}", symbol))? - .to_point(&snapshot); - let start = symbol - .annotation_range - .map_or(symbol.range.start, |range| range.start); - let start = Point::new(start.row, 0); - let end = Point::new( - symbol.range.end.row, - snapshot.line_len(symbol.range.end.row), - ); - let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - suggestion = super::WorkflowSuggestion::Update { range, description }; - } - WorkflowSuggestionKind::Create { description } => { - suggestion = super::WorkflowSuggestion::CreateFile { description }; - } - WorkflowSuggestionKind::InsertSiblingBefore { - symbol, - description, - } => { - let symbol = outline - .find_most_similar(&symbol) - .with_context(|| format!("symbol not found: {:?}", symbol))? - .to_point(&snapshot); - let position = snapshot.anchor_before( - symbol - .annotation_range - .map_or(symbol.range.start, |annotation_range| { - annotation_range.start - }), - ); - suggestion = super::WorkflowSuggestion::InsertSiblingBefore { - position, - description, - }; - } - WorkflowSuggestionKind::InsertSiblingAfter { - symbol, - description, - } => { - let symbol = outline - .find_most_similar(&symbol) - .with_context(|| format!("symbol not found: {:?}", symbol))? - .to_point(&snapshot); - let position = snapshot.anchor_after(symbol.range.end); - suggestion = super::WorkflowSuggestion::InsertSiblingAfter { - position, - description, - }; - } - WorkflowSuggestionKind::PrependChild { - symbol, - description, - } => { - if let Some(symbol) = symbol { - let symbol = outline - .find_most_similar(&symbol) - .with_context(|| format!("symbol not found: {:?}", symbol))? - .to_point(&snapshot); - - let position = snapshot.anchor_after( - symbol - .body_range - .map_or(symbol.range.start, |body_range| body_range.start), - ); - suggestion = super::WorkflowSuggestion::PrependChild { - position, - description, - }; - } else { - suggestion = super::WorkflowSuggestion::PrependChild { - position: language::Anchor::MIN, - description, - }; - } - } - WorkflowSuggestionKind::AppendChild { - symbol, - description, - } => { - if let Some(symbol) = symbol { - let symbol = outline - .find_most_similar(&symbol) - .with_context(|| format!("symbol not found: {:?}", symbol))? - .to_point(&snapshot); - - let position = snapshot.anchor_before( - symbol - .body_range - .map_or(symbol.range.end, |body_range| body_range.end), - ); - suggestion = super::WorkflowSuggestion::AppendChild { - position, - description, - }; - } else { - suggestion = super::WorkflowSuggestion::PrependChild { - position: language::Anchor::MAX, - description, - }; - } - } - WorkflowSuggestionKind::Delete { symbol } => { - let symbol = outline - .find_most_similar(&symbol) - .with_context(|| format!("symbol not found: {:?}", symbol))? - .to_point(&snapshot); - let start = symbol - .annotation_range - .map_or(symbol.range.start, |range| range.start); - let start = Point::new(start.row, 0); - let end = Point::new( - symbol.range.end.row, - snapshot.line_len(symbol.range.end.row), - ); - let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - suggestion = super::WorkflowSuggestion::Delete { range }; - } - } - - Ok((buffer, suggestion)) - } - } - - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] - #[serde(tag = "kind")] - pub enum WorkflowSuggestionKind { - /// Rewrites the specified symbol entirely based on the given description. - /// This operation completely replaces the existing symbol with new content. - Update { - /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// The path should uniquely identify the symbol within the containing file. - symbol: String, - /// A brief description of the transformation to apply to the symbol. - description: String, - }, - /// Creates a new file with the given path based on the provided description. - /// This operation adds a new file to the codebase. - Create { - /// A brief description of the file to be created. - description: String, - }, - /// Inserts a new symbol based on the given description before the specified symbol. - /// This operation adds new content immediately preceding an existing symbol. - InsertSiblingBefore { - /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// The new content will be inserted immediately before this symbol. - symbol: String, - /// A brief description of the new symbol to be inserted. - description: String, - }, - /// Inserts a new symbol based on the given description after the specified symbol. - /// This operation adds new content immediately following an existing symbol. - InsertSiblingAfter { - /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// The new content will be inserted immediately after this symbol. - symbol: String, - /// A brief description of the new symbol to be inserted. - description: String, - }, - /// Inserts a new symbol as a child of the specified symbol at the start. - /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided). - PrependChild { - /// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// If provided, the new content will be inserted as the first child of this symbol. - /// If not provided, the new content will be inserted at the top of the file. - symbol: Option, - /// A brief description of the new symbol to be inserted. - description: String, - }, - /// Inserts a new symbol as a child of the specified symbol at the end. - /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided). - AppendChild { - /// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - /// If provided, the new content will be inserted as the last child of this symbol. - /// If not provided, the new content will be applied at the bottom of the file. - symbol: Option, - /// A brief description of the new symbol to be inserted. - description: String, - }, - /// Deletes the specified symbol from the containing file. - Delete { - /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. - symbol: String, - }, - } -} diff --git a/crates/assistant/src/context_inspector.rs b/crates/assistant/src/context_inspector.rs index 5b7c1a1485..340411bbbe 100644 --- a/crates/assistant/src/context_inspector.rs +++ b/crates/assistant/src/context_inspector.rs @@ -44,6 +44,7 @@ impl ContextInspector { self.activate_for_step(range.clone(), cx); } } + fn crease_content( context: &Model, range: StepRange, @@ -52,8 +53,8 @@ impl ContextInspector { use std::fmt::Write; let step = context.read(cx).workflow_step_for_range(range)?; let mut output = String::from("\n\n"); - match &step.status { - crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => { + match &step.resolution.read(cx).result { + Some(Ok(ResolvedWorkflowStep { title, suggestions })) => { writeln!(output, "Resolution:").ok()?; writeln!(output, " {title:?}").ok()?; if suggestions.is_empty() { @@ -75,17 +76,18 @@ impl ContextInspector { } } } - crate::WorkflowStepStatus::Pending(_) => { - writeln!(output, "Resolution: Pending").ok()?; - } - crate::WorkflowStepStatus::Error(error) => { + Some(Err(error)) => { writeln!(output, "Resolution: Error").ok()?; writeln!(output, "{error:?}").ok()?; } + None => { + writeln!(output, "Resolution: Pending").ok()?; + } } Some(output.into()) } + pub(crate) fn activate_for_step(&mut self, range: StepRange, cx: &mut WindowContext<'_>) { let text = Self::crease_content(&self.context, range.clone(), cx) .unwrap_or_else(|| Arc::from("Error fetching debug info")); diff --git a/crates/assistant/src/workflow.rs b/crates/assistant/src/workflow.rs new file mode 100644 index 0000000000..5a208434c2 --- /dev/null +++ b/crates/assistant/src/workflow.rs @@ -0,0 +1,672 @@ +use crate::{AssistantPanel, Context, InlineAssistId, InlineAssistant}; +use anyhow::{anyhow, Error, Result}; +use collections::HashMap; +use editor::Editor; +use futures::future; +use gpui::{Model, ModelContext, Task, UpdateGlobal as _, View, WeakView, WindowContext}; +use language::{Anchor, Buffer, BufferSnapshot}; +use language_model::{LanguageModelRegistry, LanguageModelRequestMessage, Role}; +use project::Project; +use rope::Point; +use serde::{Deserialize, Serialize}; +use smol::stream::StreamExt; +use std::{cmp, ops::Range, sync::Arc}; +use text::{AnchorRangeExt as _, OffsetRangeExt as _}; +use util::ResultExt as _; +use workspace::Workspace; + +pub struct WorkflowStepResolution { + tagged_range: Range, + output: String, + pub result: Option>>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ResolvedWorkflowStep { + pub title: String, + pub suggestions: HashMap, Vec>, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WorkflowSuggestionGroup { + pub context_range: Range, + pub suggestions: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum WorkflowSuggestion { + Update { + range: Range, + description: String, + }, + CreateFile { + description: String, + }, + InsertSiblingBefore { + position: language::Anchor, + description: String, + }, + InsertSiblingAfter { + position: language::Anchor, + description: String, + }, + PrependChild { + position: language::Anchor, + description: String, + }, + AppendChild { + position: language::Anchor, + description: String, + }, + Delete { + range: Range, + }, +} + +impl WorkflowStepResolution { + pub fn new(range: Range) -> Self { + Self { + tagged_range: range, + output: String::new(), + result: None, + } + } + + pub fn resolve( + &mut self, + context: &Context, + cx: &mut ModelContext, + ) -> Option> { + let project = context.project()?; + let context_buffer = context.buffer().clone(); + let prompt_builder = context.prompt_builder(); + let mut request = context.to_completion_request(cx); + let model = LanguageModelRegistry::read_global(cx).active_model(); + let step_text = context_buffer + .read(cx) + .text_for_range(self.tagged_range.clone()) + .collect::(); + + Some(cx.spawn(|this, mut cx| async move { + let result = async { + let Some(model) = model else { + return Err(anyhow!("no model selected")); + }; + + this.update(&mut cx, |this, cx| { + this.output.clear(); + this.result = None; + cx.notify(); + })?; + + let mut prompt = prompt_builder.generate_step_resolution_prompt()?; + prompt.push_str(&step_text); + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![prompt.into()], + }); + + // Invoke the model to get its edit suggestions for this workflow step. + let mut stream = model + .use_tool_stream::(request, &cx) + .await?; + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + this.update(&mut cx, |this, cx| { + this.output.push_str(&chunk); + cx.notify(); + })?; + } + + let resolution = this.update(&mut cx, |this, _| { + serde_json::from_str::(&this.output) + })??; + + // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code. + let suggestion_tasks: Vec<_> = resolution + .suggestions + .iter() + .map(|suggestion| suggestion.resolve(project.clone(), cx.clone())) + .collect(); + + // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges. + let suggestions = future::join_all(suggestion_tasks) + .await + .into_iter() + .filter_map(|task| task.log_err()) + .collect::>(); + + let mut suggestions_by_buffer = HashMap::default(); + for (buffer, suggestion) in suggestions { + suggestions_by_buffer + .entry(buffer) + .or_insert_with(Vec::new) + .push(suggestion); + } + + let mut suggestion_groups_by_buffer = HashMap::default(); + for (buffer, mut suggestions) in suggestions_by_buffer { + let mut suggestion_groups = Vec::::new(); + let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; + // Sort suggestions by their range so that earlier, larger ranges come first + suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot)); + + // Merge overlapping suggestions + suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot)); + + // Create context ranges for each suggestion + for suggestion in suggestions { + let context_range = { + let suggestion_point_range = suggestion.range().to_point(&snapshot); + let start_row = suggestion_point_range.start.row.saturating_sub(5); + let end_row = cmp::min( + suggestion_point_range.end.row + 5, + snapshot.max_point().row, + ); + let start = snapshot.anchor_before(Point::new(start_row, 0)); + let end = snapshot + .anchor_after(Point::new(end_row, snapshot.line_len(end_row))); + start..end + }; + + if let Some(last_group) = suggestion_groups.last_mut() { + if last_group + .context_range + .end + .cmp(&context_range.start, &snapshot) + .is_ge() + { + // Merge with the previous group if context ranges overlap + last_group.context_range.end = context_range.end; + last_group.suggestions.push(suggestion); + } else { + // Create a new group + suggestion_groups.push(WorkflowSuggestionGroup { + context_range, + suggestions: vec![suggestion], + }); + } + } else { + // Create the first group + suggestion_groups.push(WorkflowSuggestionGroup { + context_range, + suggestions: vec![suggestion], + }); + } + } + + suggestion_groups_by_buffer.insert(buffer, suggestion_groups); + } + + Ok((resolution.step_title, suggestion_groups_by_buffer)) + }; + + let result = result.await; + this.update(&mut cx, |this, cx| { + this.result = Some(match result { + Ok((title, suggestions)) => Ok(ResolvedWorkflowStep { title, suggestions }), + Err(error) => Err(Arc::new(error)), + }); + cx.notify(); + }) + .ok(); + })) + } +} + +impl WorkflowSuggestion { + pub fn range(&self) -> Range { + match self { + WorkflowSuggestion::Update { range, .. } => range.clone(), + WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX, + WorkflowSuggestion::InsertSiblingBefore { position, .. } + | WorkflowSuggestion::InsertSiblingAfter { position, .. } + | WorkflowSuggestion::PrependChild { position, .. } + | WorkflowSuggestion::AppendChild { position, .. } => *position..*position, + WorkflowSuggestion::Delete { range } => range.clone(), + } + } + + pub fn description(&self) -> Option<&str> { + match self { + WorkflowSuggestion::Update { description, .. } + | WorkflowSuggestion::CreateFile { description } + | WorkflowSuggestion::InsertSiblingBefore { description, .. } + | WorkflowSuggestion::InsertSiblingAfter { description, .. } + | WorkflowSuggestion::PrependChild { description, .. } + | WorkflowSuggestion::AppendChild { description, .. } => Some(description), + WorkflowSuggestion::Delete { .. } => None, + } + } + + fn description_mut(&mut self) -> Option<&mut String> { + match self { + WorkflowSuggestion::Update { description, .. } + | WorkflowSuggestion::CreateFile { description } + | WorkflowSuggestion::InsertSiblingBefore { description, .. } + | WorkflowSuggestion::InsertSiblingAfter { description, .. } + | WorkflowSuggestion::PrependChild { description, .. } + | WorkflowSuggestion::AppendChild { description, .. } => Some(description), + WorkflowSuggestion::Delete { .. } => None, + } + } + + fn try_merge(&mut self, other: &Self, buffer: &BufferSnapshot) -> bool { + let range = self.range(); + let other_range = other.range(); + + // Don't merge if we don't contain the other suggestion. + if range.start.cmp(&other_range.start, buffer).is_gt() + || range.end.cmp(&other_range.end, buffer).is_lt() + { + return false; + } + + if let Some(description) = self.description_mut() { + if let Some(other_description) = other.description() { + description.push('\n'); + description.push_str(other_description); + } + } + true + } + + pub fn show( + &self, + editor: &View, + excerpt_id: editor::ExcerptId, + workspace: &WeakView, + assistant_panel: &View, + cx: &mut WindowContext, + ) -> Option { + let mut initial_transaction_id = None; + let initial_prompt; + let suggestion_range; + let buffer = editor.read(cx).buffer().clone(); + let snapshot = buffer.read(cx).snapshot(cx); + + match self { + WorkflowSuggestion::Update { range, description } => { + initial_prompt = description.clone(); + suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? + ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?; + } + WorkflowSuggestion::CreateFile { description } => { + initial_prompt = description.clone(); + suggestion_range = editor::Anchor::min()..editor::Anchor::min(); + } + WorkflowSuggestion::InsertSiblingBefore { + position, + description, + } => { + let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; + initial_prompt = description.clone(); + suggestion_range = buffer.update(cx, |buffer, cx| { + buffer.start_transaction(cx); + let line_start = buffer.insert_empty_line(position, true, true, cx); + initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); + + let line_start = buffer.read(cx).anchor_before(line_start); + line_start..line_start + }); + } + WorkflowSuggestion::InsertSiblingAfter { + position, + description, + } => { + let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; + initial_prompt = description.clone(); + suggestion_range = buffer.update(cx, |buffer, cx| { + buffer.start_transaction(cx); + let line_start = buffer.insert_empty_line(position, true, true, cx); + initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); + + let line_start = buffer.read(cx).anchor_before(line_start); + line_start..line_start + }); + } + WorkflowSuggestion::PrependChild { + position, + description, + } => { + let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; + initial_prompt = description.clone(); + suggestion_range = buffer.update(cx, |buffer, cx| { + buffer.start_transaction(cx); + let line_start = buffer.insert_empty_line(position, false, true, cx); + initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); + + let line_start = buffer.read(cx).anchor_before(line_start); + line_start..line_start + }); + } + WorkflowSuggestion::AppendChild { + position, + description, + } => { + let position = snapshot.anchor_in_excerpt(excerpt_id, *position)?; + initial_prompt = description.clone(); + suggestion_range = buffer.update(cx, |buffer, cx| { + buffer.start_transaction(cx); + let line_start = buffer.insert_empty_line(position, true, false, cx); + initial_transaction_id = buffer.end_transaction(cx); + buffer.refresh_preview(cx); + + let line_start = buffer.read(cx).anchor_before(line_start); + line_start..line_start + }); + } + WorkflowSuggestion::Delete { range } => { + initial_prompt = "Delete".to_string(); + suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)? + ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?; + } + } + + InlineAssistant::update_global(cx, |inline_assistant, cx| { + Some(inline_assistant.suggest_assist( + editor, + suggestion_range, + initial_prompt, + initial_transaction_id, + Some(workspace.clone()), + Some(assistant_panel), + cx, + )) + }) + } +} + +pub mod tool { + use std::path::Path; + + use super::*; + use anyhow::Context as _; + use gpui::AsyncAppContext; + use language::ParseStatus; + use language_model::LanguageModelTool; + use project::ProjectPath; + use schemars::JsonSchema; + + #[derive(Debug, Serialize, Deserialize, JsonSchema)] + pub struct WorkflowStepResolution { + /// An extremely short title for the edit step represented by these operations. + pub step_title: String, + /// A sequence of operations to apply to the codebase. + /// When multiple operations are required for a step, be sure to include multiple operations in this list. + pub suggestions: Vec, + } + + impl LanguageModelTool for WorkflowStepResolution { + fn name() -> String { + "edit".into() + } + + fn description() -> String { + "suggest edits to one or more locations in the codebase".into() + } + } + + /// A description of an operation to apply to one location in the codebase. + /// + /// This object represents a single edit operation that can be performed on a specific file + /// in the codebase. It encapsulates both the location (file path) and the nature of the + /// edit to be made. + /// + /// # Fields + /// + /// * `path`: A string representing the file path where the edit operation should be applied. + /// This path is relative to the root of the project or repository. + /// + /// * `kind`: An enum representing the specific type of edit operation to be performed. + /// + /// # Usage + /// + /// `EditOperation` is used within a code editor to represent and apply + /// programmatic changes to source code. It provides a structured way to describe + /// edits for features like refactoring tools or AI-assisted coding suggestions. + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] + pub struct WorkflowSuggestion { + /// The path to the file containing the relevant operation + pub path: String, + #[serde(flatten)] + pub kind: WorkflowSuggestionKind, + } + + impl WorkflowSuggestion { + pub(super) async fn resolve( + &self, + project: Model, + mut cx: AsyncAppContext, + ) -> Result<(Model, super::WorkflowSuggestion)> { + let path = self.path.clone(); + let kind = self.kind.clone(); + let buffer = project + .update(&mut cx, |project, cx| { + let project_path = project + .find_project_path(Path::new(&path), cx) + .or_else(|| { + // If we couldn't find a project path for it, put it in the active worktree + // so that when we create the buffer, it can be saved. + let worktree = project + .active_entry() + .and_then(|entry_id| project.worktree_for_entry(entry_id, cx)) + .or_else(|| project.worktrees(cx).next())?; + let worktree = worktree.read(cx); + + Some(ProjectPath { + worktree_id: worktree.id(), + path: Arc::from(Path::new(&path)), + }) + }) + .with_context(|| format!("worktree not found for {:?}", path))?; + anyhow::Ok(project.open_buffer(project_path, cx)) + })?? + .await?; + + let mut parse_status = buffer.read_with(&cx, |buffer, _cx| buffer.parse_status())?; + while *parse_status.borrow() != ParseStatus::Idle { + parse_status.changed().await?; + } + + let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?; + let outline = snapshot.outline(None).context("no outline for buffer")?; + + let suggestion; + match kind { + WorkflowSuggestionKind::Update { + symbol, + description, + } => { + let symbol = outline + .find_most_similar(&symbol) + .with_context(|| format!("symbol not found: {:?}", symbol))? + .to_point(&snapshot); + let start = symbol + .annotation_range + .map_or(symbol.range.start, |range| range.start); + let start = Point::new(start.row, 0); + let end = Point::new( + symbol.range.end.row, + snapshot.line_len(symbol.range.end.row), + ); + let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); + suggestion = super::WorkflowSuggestion::Update { range, description }; + } + WorkflowSuggestionKind::Create { description } => { + suggestion = super::WorkflowSuggestion::CreateFile { description }; + } + WorkflowSuggestionKind::InsertSiblingBefore { + symbol, + description, + } => { + let symbol = outline + .find_most_similar(&symbol) + .with_context(|| format!("symbol not found: {:?}", symbol))? + .to_point(&snapshot); + let position = snapshot.anchor_before( + symbol + .annotation_range + .map_or(symbol.range.start, |annotation_range| { + annotation_range.start + }), + ); + suggestion = super::WorkflowSuggestion::InsertSiblingBefore { + position, + description, + }; + } + WorkflowSuggestionKind::InsertSiblingAfter { + symbol, + description, + } => { + let symbol = outline + .find_most_similar(&symbol) + .with_context(|| format!("symbol not found: {:?}", symbol))? + .to_point(&snapshot); + let position = snapshot.anchor_after(symbol.range.end); + suggestion = super::WorkflowSuggestion::InsertSiblingAfter { + position, + description, + }; + } + WorkflowSuggestionKind::PrependChild { + symbol, + description, + } => { + if let Some(symbol) = symbol { + let symbol = outline + .find_most_similar(&symbol) + .with_context(|| format!("symbol not found: {:?}", symbol))? + .to_point(&snapshot); + + let position = snapshot.anchor_after( + symbol + .body_range + .map_or(symbol.range.start, |body_range| body_range.start), + ); + suggestion = super::WorkflowSuggestion::PrependChild { + position, + description, + }; + } else { + suggestion = super::WorkflowSuggestion::PrependChild { + position: language::Anchor::MIN, + description, + }; + } + } + WorkflowSuggestionKind::AppendChild { + symbol, + description, + } => { + if let Some(symbol) = symbol { + let symbol = outline + .find_most_similar(&symbol) + .with_context(|| format!("symbol not found: {:?}", symbol))? + .to_point(&snapshot); + + let position = snapshot.anchor_before( + symbol + .body_range + .map_or(symbol.range.end, |body_range| body_range.end), + ); + suggestion = super::WorkflowSuggestion::AppendChild { + position, + description, + }; + } else { + suggestion = super::WorkflowSuggestion::PrependChild { + position: language::Anchor::MAX, + description, + }; + } + } + WorkflowSuggestionKind::Delete { symbol } => { + let symbol = outline + .find_most_similar(&symbol) + .with_context(|| format!("symbol not found: {:?}", symbol))? + .to_point(&snapshot); + let start = symbol + .annotation_range + .map_or(symbol.range.start, |range| range.start); + let start = Point::new(start.row, 0); + let end = Point::new( + symbol.range.end.row, + snapshot.line_len(symbol.range.end.row), + ); + let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); + suggestion = super::WorkflowSuggestion::Delete { range }; + } + } + + Ok((buffer, suggestion)) + } + } + + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] + #[serde(tag = "kind")] + pub enum WorkflowSuggestionKind { + /// Rewrites the specified symbol entirely based on the given description. + /// This operation completely replaces the existing symbol with new content. + Update { + /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. + /// The path should uniquely identify the symbol within the containing file. + symbol: String, + /// A brief description of the transformation to apply to the symbol. + description: String, + }, + /// Creates a new file with the given path based on the provided description. + /// This operation adds a new file to the codebase. + Create { + /// A brief description of the file to be created. + description: String, + }, + /// Inserts a new symbol based on the given description before the specified symbol. + /// This operation adds new content immediately preceding an existing symbol. + InsertSiblingBefore { + /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. + /// The new content will be inserted immediately before this symbol. + symbol: String, + /// A brief description of the new symbol to be inserted. + description: String, + }, + /// Inserts a new symbol based on the given description after the specified symbol. + /// This operation adds new content immediately following an existing symbol. + InsertSiblingAfter { + /// A fully-qualified reference to the symbol, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. + /// The new content will be inserted immediately after this symbol. + symbol: String, + /// A brief description of the new symbol to be inserted. + description: String, + }, + /// Inserts a new symbol as a child of the specified symbol at the start. + /// This operation adds new content as the first child of an existing symbol (or file if no symbol is provided). + PrependChild { + /// An optional fully-qualified reference to the symbol after the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. + /// If provided, the new content will be inserted as the first child of this symbol. + /// If not provided, the new content will be inserted at the top of the file. + symbol: Option, + /// A brief description of the new symbol to be inserted. + description: String, + }, + /// Inserts a new symbol as a child of the specified symbol at the end. + /// This operation adds new content as the last child of an existing symbol (or file if no symbol is provided). + AppendChild { + /// An optional fully-qualified reference to the symbol before the code you want to insert, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. + /// If provided, the new content will be inserted as the last child of this symbol. + /// If not provided, the new content will be applied at the bottom of the file. + symbol: Option, + /// A brief description of the new symbol to be inserted. + description: String, + }, + /// Deletes the specified symbol from the containing file. + Delete { + /// An fully-qualified reference to the symbol to be deleted, e.g. `mod foo impl Bar pub fn baz` instead of just `fn baz`. + symbol: String, + }, + } +} diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index f0a5754518..9c8f9c3a9f 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -99,6 +99,16 @@ impl dyn LanguageModel { Ok(serde_json::from_str(&response)?) } } + + pub fn use_tool_stream( + &self, + request: LanguageModelRequest, + cx: &AsyncAppContext, + ) -> BoxFuture<'static, Result>>> { + let schema = schemars::schema_for!(T); + let schema_json = serde_json::to_value(&schema).unwrap(); + self.use_any_tool(request, T::name(), T::description(), schema_json, cx) + } } pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {