assistant2: Return no-edits response to architect model (#27200)
Sometimes the editor model returns no search/replace blocks. This usually happens when the architect model calls the edit tool before reading any files. When this happens, we'll now return the raw response from the editor model to the architect model so it can recover accordingly. Release Notes: - N/A
This commit is contained in:
parent
c60a7034c8
commit
6408ae81d1
1 changed files with 205 additions and 155 deletions
|
@ -145,22 +145,30 @@ impl Tool for EditFilesTool {
|
||||||
|
|
||||||
struct EditToolRequest {
|
struct EditToolRequest {
|
||||||
parser: EditActionParser,
|
parser: EditActionParser,
|
||||||
output: String,
|
editor_response: EditorResponse,
|
||||||
changed_buffers: HashSet<Entity<language::Buffer>>,
|
|
||||||
bad_searches: Vec<BadSearch>,
|
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
action_log: Entity<ActionLog>,
|
action_log: Entity<ActionLog>,
|
||||||
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
enum EditorResponse {
|
||||||
enum DiffResult {
|
/// The editor model hasn't produced any actions yet.
|
||||||
BadSearch(BadSearch),
|
/// If we don't have any by the end, we'll return its message to the architect model.
|
||||||
Diff(language::Diff),
|
Message(String),
|
||||||
|
/// The editor model produced at least one action.
|
||||||
|
Actions {
|
||||||
|
applied: Vec<AppliedAction>,
|
||||||
|
search_errors: Vec<SearchError>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppliedAction {
|
||||||
|
source: String,
|
||||||
|
buffer: Entity<language::Buffer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum BadSearch {
|
enum SearchError {
|
||||||
NoMatch {
|
NoMatch {
|
||||||
file_path: String,
|
file_path: String,
|
||||||
search: String,
|
search: String,
|
||||||
|
@ -226,10 +234,7 @@ impl EditToolRequest {
|
||||||
|
|
||||||
let mut request = Self {
|
let mut request = Self {
|
||||||
parser: EditActionParser::new(),
|
parser: EditActionParser::new(),
|
||||||
// we start with the success header so we don't need to shift the output in the common case
|
editor_response: EditorResponse::Message(String::with_capacity(256)),
|
||||||
output: Self::SUCCESS_OUTPUT_HEADER.to_string(),
|
|
||||||
changed_buffers: HashSet::default(),
|
|
||||||
bad_searches: Vec::new(),
|
|
||||||
action_log,
|
action_log,
|
||||||
project,
|
project,
|
||||||
tool_log,
|
tool_log,
|
||||||
|
@ -246,6 +251,12 @@ impl EditToolRequest {
|
||||||
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
|
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
|
||||||
let new_actions = self.parser.parse_chunk(chunk);
|
let new_actions = self.parser.parse_chunk(chunk);
|
||||||
|
|
||||||
|
if let EditorResponse::Message(ref mut message) = self.editor_response {
|
||||||
|
if new_actions.is_empty() {
|
||||||
|
message.push_str(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((ref log, req_id)) = self.tool_log {
|
if let Some((ref log, req_id)) = self.tool_log {
|
||||||
log.update(cx, |log, cx| {
|
log.update(cx, |log, cx| {
|
||||||
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
|
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
|
||||||
|
@ -276,6 +287,11 @@ impl EditToolRequest {
|
||||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
enum DiffResult {
|
||||||
|
Diff(language::Diff),
|
||||||
|
SearchError(SearchError),
|
||||||
|
}
|
||||||
|
|
||||||
let result = match action {
|
let result = match action {
|
||||||
EditAction::Replace {
|
EditAction::Replace {
|
||||||
old,
|
old,
|
||||||
|
@ -285,7 +301,39 @@ impl EditToolRequest {
|
||||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||||
|
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.spawn(Self::replace_diff(old, new, file_path, snapshot))
|
.spawn(async move {
|
||||||
|
if snapshot.is_empty() {
|
||||||
|
let exists = snapshot
|
||||||
|
.file()
|
||||||
|
.map_or(false, |file| file.disk_state().exists());
|
||||||
|
|
||||||
|
let error = SearchError::EmptyBuffer {
|
||||||
|
file_path: file_path.display().to_string(),
|
||||||
|
exists,
|
||||||
|
search: old,
|
||||||
|
};
|
||||||
|
|
||||||
|
return anyhow::Ok(DiffResult::SearchError(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let replace_result =
|
||||||
|
// Try to match exactly
|
||||||
|
replace_exact(&old, &new, &snapshot)
|
||||||
|
.await
|
||||||
|
// If that fails, try being flexible about indentation
|
||||||
|
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
|
||||||
|
|
||||||
|
let Some(diff) = replace_result else {
|
||||||
|
let error = SearchError::NoMatch {
|
||||||
|
search: old,
|
||||||
|
file_path: file_path.display().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(DiffResult::SearchError(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DiffResult::Diff(diff))
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
|
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
|
||||||
|
@ -296,134 +344,144 @@ impl EditToolRequest {
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
DiffResult::BadSearch(invalid_replace) => {
|
DiffResult::SearchError(error) => {
|
||||||
self.bad_searches.push(invalid_replace);
|
self.push_search_error(error);
|
||||||
}
|
}
|
||||||
DiffResult::Diff(diff) => {
|
DiffResult::Diff(diff) => {
|
||||||
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
|
||||||
|
|
||||||
write!(&mut self.output, "\n\n{}", source)?;
|
self.push_applied_action(AppliedAction { source, buffer });
|
||||||
self.changed_buffers.insert(buffer);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn replace_diff(
|
fn push_search_error(&mut self, error: SearchError) {
|
||||||
old: String,
|
match &mut self.editor_response {
|
||||||
new: String,
|
EditorResponse::Message(_) => {
|
||||||
file_path: std::path::PathBuf,
|
self.editor_response = EditorResponse::Actions {
|
||||||
snapshot: language::BufferSnapshot,
|
applied: Vec::new(),
|
||||||
) -> Result<DiffResult> {
|
search_errors: vec![error],
|
||||||
if snapshot.is_empty() {
|
|
||||||
let exists = snapshot
|
|
||||||
.file()
|
|
||||||
.map_or(false, |file| file.disk_state().exists());
|
|
||||||
|
|
||||||
return Ok(DiffResult::BadSearch(BadSearch::EmptyBuffer {
|
|
||||||
file_path: file_path.display().to_string(),
|
|
||||||
exists,
|
|
||||||
search: old,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let result =
|
|
||||||
// Try to match exactly
|
|
||||||
replace_exact(&old, &new, &snapshot)
|
|
||||||
.await
|
|
||||||
// If that fails, try being flexible about indentation
|
|
||||||
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
|
|
||||||
|
|
||||||
let Some(diff) = result else {
|
|
||||||
return anyhow::Ok(DiffResult::BadSearch(BadSearch::NoMatch {
|
|
||||||
search: old,
|
|
||||||
file_path: file_path.display().to_string(),
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
anyhow::Ok(DiffResult::Diff(diff))
|
EditorResponse::Actions { search_errors, .. } => {
|
||||||
|
search_errors.push(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SUCCESS_OUTPUT_HEADER: &str = "Successfully applied. Here's a list of changes:";
|
fn push_applied_action(&mut self, action: AppliedAction) {
|
||||||
const ERROR_OUTPUT_HEADER_NO_EDITS: &str = "I couldn't apply any edits!";
|
match &mut self.editor_response {
|
||||||
const ERROR_OUTPUT_HEADER_WITH_EDITS: &str =
|
EditorResponse::Message(_) => {
|
||||||
"Errors occurred. First, here's a list of the edits we managed to apply:";
|
self.editor_response = EditorResponse::Actions {
|
||||||
|
applied: vec![action],
|
||||||
|
search_errors: Vec::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
EditorResponse::Actions { applied, .. } => {
|
||||||
|
applied.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
|
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
|
||||||
let changed_buffer_count = self.changed_buffers.len();
|
match self.editor_response {
|
||||||
|
EditorResponse::Message(message) => Err(anyhow!(
|
||||||
|
"No edits were applied! You might need to provide more context.\n\n{}",
|
||||||
|
message
|
||||||
|
)),
|
||||||
|
EditorResponse::Actions {
|
||||||
|
applied,
|
||||||
|
search_errors,
|
||||||
|
} => {
|
||||||
|
let mut output = String::with_capacity(1024);
|
||||||
|
|
||||||
// Save each buffer once at the end
|
let parse_errors = self.parser.errors();
|
||||||
for buffer in &self.changed_buffers {
|
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
|
||||||
|
|
||||||
|
if has_errors {
|
||||||
|
let error_count = search_errors.len() + parse_errors.len();
|
||||||
|
|
||||||
|
if applied.is_empty() {
|
||||||
|
writeln!(
|
||||||
|
&mut output,
|
||||||
|
"{} errors occurred! No edits were applied.",
|
||||||
|
error_count,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
writeln!(
|
||||||
|
&mut output,
|
||||||
|
"{} errors occurred, but {} edits were correctly applied.",
|
||||||
|
error_count,
|
||||||
|
applied.len(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
&mut output,
|
||||||
|
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
|
||||||
|
applied.len()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
&mut output,
|
||||||
|
"Successfully applied! Here's a list of applied edits:"
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut changed_buffers = HashSet::default();
|
||||||
|
|
||||||
|
for action in applied {
|
||||||
|
changed_buffers.insert(action.buffer);
|
||||||
|
write!(&mut output, "\n\n{}", action.source)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for buffer in &changed_buffers {
|
||||||
self.project
|
self.project
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.action_log
|
self.action_log
|
||||||
.update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx))
|
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
let errors = self.parser.errors();
|
if !search_errors.is_empty() {
|
||||||
|
|
||||||
if errors.is_empty() && self.bad_searches.is_empty() {
|
|
||||||
if changed_buffer_count == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"The instructions didn't lead to any changes. You might need to consult the file contents first."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.output)
|
|
||||||
} else {
|
|
||||||
let mut output = self.output;
|
|
||||||
|
|
||||||
if output.is_empty() {
|
|
||||||
output.replace_range(
|
|
||||||
0..Self::SUCCESS_OUTPUT_HEADER.len(),
|
|
||||||
Self::ERROR_OUTPUT_HEADER_NO_EDITS,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
output.replace_range(
|
|
||||||
0..Self::SUCCESS_OUTPUT_HEADER.len(),
|
|
||||||
Self::ERROR_OUTPUT_HEADER_WITH_EDITS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.bad_searches.is_empty() {
|
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"\n\n# {} SEARCH/REPLACE block(s) failed to match:\n",
|
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
|
||||||
self.bad_searches.len()
|
search_errors.len()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for bad_search in self.bad_searches {
|
for error in search_errors {
|
||||||
match bad_search {
|
match error {
|
||||||
BadSearch::NoMatch { file_path, search } => {
|
SearchError::NoMatch { file_path, search } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"## No exact match in: `{}`\n```\n{}\n```\n",
|
"### No exact match in: `{}`\n```\n{}\n```\n",
|
||||||
file_path, search,
|
file_path, search,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
BadSearch::EmptyBuffer {
|
SearchError::EmptyBuffer {
|
||||||
file_path,
|
file_path,
|
||||||
exists: true,
|
exists: true,
|
||||||
search,
|
search,
|
||||||
} => {
|
} => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"## No match because `{}` is empty:\n```\n{}\n```\n",
|
"### No match because `{}` is empty:\n```\n{}\n```\n",
|
||||||
file_path, search,
|
file_path, search,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
BadSearch::EmptyBuffer {
|
SearchError::EmptyBuffer {
|
||||||
file_path,
|
file_path,
|
||||||
exists: false,
|
exists: false,
|
||||||
search,
|
search,
|
||||||
} => {
|
} => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"## No match because `{}` does not exist:\n```\n{}\n```\n",
|
"### No match because `{}` does not exist:\n```\n{}\n```\n",
|
||||||
file_path, search,
|
file_path, search,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
@ -436,37 +494,29 @@ impl EditToolRequest {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if !parse_errors.is_empty() {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"\n\n# {} SEARCH/REPLACE blocks failed to parse:",
|
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
|
||||||
errors.len()
|
parse_errors.len()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for error in errors {
|
for error in parse_errors {
|
||||||
writeln!(&mut output, "- {}", error)?;
|
writeln!(&mut output, "- {}", error)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed_buffer_count > 0 {
|
if has_errors {
|
||||||
writeln!(
|
writeln!(&mut output,
|
||||||
&mut output,
|
"\n\nYou can fix errors by running the tool again. You can include instructions, \
|
||||||
"\n\nThe other SEARCH/REPLACE blocks were applied successfully. Do not re-send them!",
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeln!(
|
|
||||||
&mut output,
|
|
||||||
"{}You can fix errors by running the tool again. You can include instructions, \
|
|
||||||
but errors are part of the conversation so you don't need to repeat them.",
|
but errors are part of the conversation so you don't need to repeat them.",
|
||||||
if changed_buffer_count == 0 {
|
|
||||||
"\n\n"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Err(anyhow!(output))
|
Err(anyhow!(output))
|
||||||
|
} else {
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue