Detailed changes
@@ -73,6 +73,7 @@ pub enum PromptFormat {
MarkedExcerpt,
LabeledSections,
NumLinesUniDiff,
+ OldTextNewText,
/// Prompt format intended for use via zeta_cli
OnlySnippets,
}
@@ -100,6 +101,7 @@ impl std::fmt::Display for PromptFormat {
PromptFormat::LabeledSections => write!(f, "Labeled Sections"),
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
PromptFormat::NumLinesUniDiff => write!(f, "Numbered Lines / Unified Diff"),
+ PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
}
}
}
@@ -100,6 +100,54 @@ const UNIFIED_DIFF_REMINDER: &str = indoc! {"
to uniquely identify it amongst all excerpts of code provided.
"};
+const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
+ # Instructions
+
+ You are an edit prediction agent in a code editor.
+ Your job is to predict the next edit that the user will make,
+ based on their last few edits and their current cursor location.
+
+ # Output Format
+
+ You must briefly explain your understanding of the user's goal, in one
+ or two sentences, and then specify their next edit, using the following
+ XML format:
+
+ <edits path="my-project/src/myapp/cli.py">
+ <old_text>
+ OLD TEXT 1 HERE
+ </old_text>
+ <new_text>
+ NEW TEXT 1 HERE
+ </new_text>
+
+ <old_text>
+ OLD TEXT 1 HERE
+ </old_text>
+ <new_text>
+ NEW TEXT 1 HERE
+ </new_text>
+ </edits>
+
+ - Specify the file to edit using the `path` attribute.
+ - Use `<old_text>` and `<new_text>` tags to replace content
+ - `<old_text>` must exactly match existing file content, including indentation
+ - `<old_text>` cannot be empty
+ - Do not escape quotes, newlines, or other characters within tags
+ - Always close all tags properly
+ - Don't include the <|user_cursor|> marker in your output.
+
+ # Edit History:
+
+"#};
+
+const OLD_TEXT_NEW_TEXT_REMINDER: &str = indoc! {r#"
+ ---
+
+ Remember that the edits in the edit history have already been deployed.
+ The files are currently as shown in the Code Excerpts section.
+"#};
+
pub fn build_prompt(
request: &predict_edits_v3::PredictEditsRequest,
) -> Result<(String, SectionLabels)> {
@@ -121,7 +169,9 @@ pub fn build_prompt(
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
),
],
- PromptFormat::LabeledSections | PromptFormat::NumLinesUniDiff => {
+ PromptFormat::LabeledSections
+ | PromptFormat::NumLinesUniDiff
+ | PromptFormat::OldTextNewText => {
vec![(request.cursor_point, CURSOR_MARKER)]
}
PromptFormat::OnlySnippets => vec![],
@@ -131,6 +181,7 @@ pub fn build_prompt(
PromptFormat::MarkedExcerpt => MARKED_EXCERPT_INSTRUCTIONS.to_string(),
PromptFormat::LabeledSections => LABELED_SECTIONS_INSTRUCTIONS.to_string(),
PromptFormat::NumLinesUniDiff => NUMBERED_LINES_INSTRUCTIONS.to_string(),
+ PromptFormat::OldTextNewText => XML_TAGS_INSTRUCTIONS.to_string(),
PromptFormat::OnlySnippets => String::new(),
};
@@ -186,6 +237,9 @@ pub fn build_prompt(
PromptFormat::NumLinesUniDiff => {
prompt.push_str(UNIFIED_DIFF_REMINDER);
}
+ PromptFormat::OldTextNewText => {
+ prompt.push_str(OLD_TEXT_NEW_TEXT_REMINDER);
+ }
_ => {}
}
@@ -611,6 +665,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
match self.request.prompt_format {
PromptFormat::MarkedExcerpt
| PromptFormat::OnlySnippets
+ | PromptFormat::OldTextNewText
| PromptFormat::NumLinesUniDiff => {
if range.start.0 > 0 && !skipped_last_snippet {
output.push_str("…\n");
@@ -0,0 +1,197 @@
+use anyhow::{Context as _, Result, anyhow};
+use language::{Anchor, BufferSnapshot, OffsetRangeExt as _, TextBufferSnapshot};
+use std::ops::Range;
+use std::path::Path;
+use std::sync::Arc;
+
+pub async fn parse_xml_edits<'a>(
+ mut input: &'a str,
+ get_buffer: impl Fn(&Path) -> Option<(&'a BufferSnapshot, &'a [Range<Anchor>])> + Send,
+) -> Result<(&'a BufferSnapshot, Vec<(Range<Anchor>, Arc<str>)>)> {
+ let edits_tag = parse_tag(&mut input, "edits")?.context("No edits tag")?;
+
+ input = edits_tag.body;
+
+ let file_path = edits_tag
+ .attributes
+ .trim_start()
+ .strip_prefix("path")
+ .context("no file attribute on edits tag")?
+ .trim_end()
+ .strip_prefix('=')
+ .context("no value for path attribute")?
+ .trim()
+ .trim_start_matches('"')
+ .trim_end_matches('"');
+
+ let (buffer, context_ranges) = get_buffer(file_path.as_ref())
+ .with_context(|| format!("no buffer for file {file_path}"))?;
+
+ let mut edits = vec![];
+ while let Some(old_text_tag) = parse_tag(&mut input, "old_text")? {
+ let new_text_tag =
+ parse_tag(&mut input, "new_text")?.context("no new_text tag following old_text")?;
+ edits.extend(resolve_new_text_old_text_in_buffer(
+ new_text_tag.body,
+ old_text_tag.body,
+ buffer,
+ context_ranges,
+ )?);
+ }
+
+ Ok((buffer, edits))
+}
+
+fn resolve_new_text_old_text_in_buffer(
+ new_text: &str,
+ old_text: &str,
+ buffer: &TextBufferSnapshot,
+ ranges: &[Range<Anchor>],
+) -> Result<impl Iterator<Item = (Range<Anchor>, Arc<str>)>, anyhow::Error> {
+ let context_offset = if old_text.is_empty() {
+ Ok(0)
+ } else {
+ let mut offset = None;
+ for range in ranges {
+ let range = range.to_offset(buffer);
+ let text = buffer.text_for_range(range.clone()).collect::<String>();
+ for (match_offset, _) in text.match_indices(old_text) {
+ if offset.is_some() {
+ anyhow::bail!("old_text is not unique enough:\n{}", old_text);
+ }
+ offset = Some(range.start + match_offset);
+ }
+ }
+ offset.ok_or_else(|| anyhow!("Failed to match old_text:\n{}", old_text))
+ }?;
+
+ let edits_within_hunk = language::text_diff(&old_text, &new_text);
+ Ok(edits_within_hunk
+ .into_iter()
+ .map(move |(inner_range, inner_text)| {
+ (
+ buffer.anchor_after(context_offset + inner_range.start)
+ ..buffer.anchor_before(context_offset + inner_range.end),
+ inner_text,
+ )
+ }))
+}
+
+struct ParsedTag<'a> {
+ attributes: &'a str,
+ body: &'a str,
+}
+
+fn parse_tag<'a>(input: &mut &'a str, tag: &str) -> Result<Option<ParsedTag<'a>>> {
+ let open_tag = format!("<{}", tag);
+ let close_tag = format!("</{}>", tag);
+ let Some(start_ix) = input.find(&open_tag) else {
+ return Ok(None);
+ };
+ let start_ix = start_ix + open_tag.len();
+ let closing_bracket_ix = start_ix
+ + input[start_ix..]
+ .find('>')
+ .with_context(|| format!("missing > after {tag}"))?;
+ let attributes = &input[start_ix..closing_bracket_ix].trim();
+ let end_ix = closing_bracket_ix
+ + input[closing_bracket_ix..]
+ .find(&close_tag)
+ .with_context(|| format!("no `{close_tag}` tag"))?;
+ let body = &input[closing_bracket_ix + '>'.len_utf8()..end_ix];
+ let body = body.strip_prefix('\n').unwrap_or(body);
+ *input = &input[end_ix + close_tag.len()..];
+ Ok(Some(ParsedTag { attributes, body }))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::TestAppContext;
+ use indoc::indoc;
+ use language::Point;
+ use project::{FakeFs, Project};
+ use serde_json::json;
+ use settings::SettingsStore;
+ use util::path;
+
+ #[test]
+ fn test_parse_tags() {
+ let mut input = indoc! {r#"
+ Prelude
+ <tag attr="foo">
+ tag value
+ </tag>
+ "# };
+ let parsed = parse_tag(&mut input, "tag").unwrap().unwrap();
+ assert_eq!(parsed.attributes, "attr=\"foo\"");
+ assert_eq!(parsed.body, "tag value\n");
+ assert_eq!(input, "\n");
+ }
+
+ #[gpui::test]
+ async fn test_parse_xml_edits(cx: &mut TestAppContext) {
+ let fs = init_test(cx);
+
+ let buffer_1_text = indoc! {r#"
+ one two three four
+ five six seven eight
+ nine ten eleven twelve
+ "# };
+
+ fs.insert_tree(
+ path!("/root"),
+ json!({
+ "file1": buffer_1_text,
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer(path!("/root/file1"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+ let edits = indoc! {r#"
+ <edits path="root/file1">
+ <old_text>
+ five six seven eight
+ </old_text>
+ <new_text>
+ five SIX seven eight!
+ </new_text>
+ </edits>
+ "#};
+
+ let (buffer, edits) = parse_xml_edits(edits, |_path| {
+ Some((&buffer_snapshot, &[(Anchor::MIN..Anchor::MAX)] as &[_]))
+ })
+ .await
+ .unwrap();
+
+ let edits = edits
+ .into_iter()
+ .map(|(range, text)| (range.to_point(&buffer), text))
+ .collect::<Vec<_>>();
+ assert_eq!(
+ edits,
+ &[
+ (Point::new(1, 5)..Point::new(1, 8), "SIX".into()),
+ (Point::new(1, 20)..Point::new(1, 20), "!".into())
+ ]
+ );
+ }
+
+ fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ });
+
+ FakeFs::new(cx.background_executor.clone())
+ }
+}
@@ -47,6 +47,7 @@ mod prediction;
mod provider;
pub mod retrieval_search;
pub mod udiff;
+mod xml_edits;
use crate::merge_excerpts::merge_excerpts;
use crate::prediction::EditPrediction;
@@ -948,8 +949,9 @@ impl Zeta {
llm_token,
app_version,
#[cfg(feature = "llm-response-cache")]
- llm_response_cache
- ).await;
+ llm_response_cache,
+ )
+ .await;
let request_time = chrono::Utc::now() - before_request;
log::trace!("Got edit prediction response");
@@ -969,7 +971,7 @@ impl Zeta {
let (res, usage) = response?;
let request_id = EditPredictionId(res.id.clone().into());
let Some(mut output_text) = text_from_response(res) else {
- return Ok((None, usage))
+ return Ok((None, usage));
};
if output_text.contains(CURSOR_MARKER) {
@@ -977,20 +979,25 @@ impl Zeta {
output_text = output_text.replace(CURSOR_MARKER, "");
}
+ let get_buffer_from_context = |path: &Path| {
+ included_files
+ .iter()
+ .find_map(|(_, buffer, probe_path, ranges)| {
+ if probe_path.as_ref() == path {
+ Some((buffer, ranges.as_slice()))
+ } else {
+ None
+ }
+ })
+ };
+
let (edited_buffer_snapshot, edits) = match options.prompt_format {
PromptFormat::NumLinesUniDiff => {
- crate::udiff::parse_diff(&output_text, |path| {
- included_files
- .iter()
- .find_map(|(_, buffer, probe_path, ranges)| {
- if probe_path.as_ref() == path {
- Some((buffer, ranges.as_slice()))
- } else {
- None
- }
- })
- })
- .await?
+ crate::udiff::parse_diff(&output_text, get_buffer_from_context).await?
+ }
+ PromptFormat::OldTextNewText => {
+ crate::xml_edits::parse_xml_edits(&output_text, get_buffer_from_context)
+ .await?
}
_ => {
bail!("unsupported prompt format {}", options.prompt_format)
@@ -1006,9 +1013,17 @@ impl Zeta {
None
}
})
- .context("Failed to find buffer in included_buffers, even though we just found the snapshot")?;
-
- anyhow::Ok((Some((request_id, edited_buffer, edited_buffer_snapshot.clone(), edits)), usage))
+ .context("Failed to find buffer in included_buffers")?;
+
+ anyhow::Ok((
+ Some((
+ request_id,
+ edited_buffer,
+ edited_buffer_snapshot.clone(),
+ edits,
+ )),
+ usage,
+ ))
}
});
@@ -1387,7 +1402,8 @@ impl Zeta {
continue;
}
- let input: SearchToolInput = serde_json::from_str(&function.arguments)?;
+ let input: SearchToolInput = serde_json::from_str(&function.arguments)
+ .with_context(|| format!("invalid search json {}", &function.arguments))?;
queries.extend(input.queries);
}
@@ -1447,6 +1463,16 @@ impl Zeta {
})
}
+ pub fn set_context(
+ &mut self,
+ project: Entity<Project>,
+ context: HashMap<Entity<Buffer>, Vec<Range<Anchor>>>,
+ ) {
+ if let Some(zeta_project) = self.projects.get_mut(&project.entity_id()) {
+ zeta_project.context = Some(context);
+ }
+ }
+
fn gather_nearby_diagnostics(
cursor_offset: usize,
diagnostic_sets: &[(LanguageServerId, DiagnosticSet)],
@@ -24,6 +24,8 @@ pub struct EvaluateArguments {
skip_cache: bool,
#[arg(long, value_enum, default_value_t = PromptFormat::default())]
prompt_format: PromptFormat,
+ #[arg(long)]
+ use_expected_context: bool,
}
pub async fn run_evaluate(
@@ -39,6 +41,7 @@ pub async fn run_evaluate(
&path,
args.skip_cache,
args.prompt_format,
+ args.use_expected_context,
app_state.clone(),
cx,
)
@@ -63,13 +66,21 @@ pub async fn run_evaluate_one(
example_path: &Path,
skip_cache: bool,
prompt_format: PromptFormat,
+ use_expected_context: bool,
app_state: Arc<ZetaCliAppState>,
cx: &mut AsyncApp,
) -> Result<EvaluationResult> {
let example = NamedExample::load(&example_path).unwrap();
- let predictions = zeta2_predict(example.clone(), skip_cache, prompt_format, &app_state, cx)
- .await
- .unwrap();
+ let predictions = zeta2_predict(
+ example.clone(),
+ skip_cache,
+ prompt_format,
+ use_expected_context,
+ &app_state,
+ cx,
+ )
+ .await
+ .unwrap();
let evaluation_result = evaluate(&example.example, &predictions);
@@ -171,6 +171,7 @@ enum PromptFormat {
OnlySnippets,
#[default]
NumberedLines,
+ OldTextNewText,
}
impl Into<predict_edits_v3::PromptFormat> for PromptFormat {
@@ -180,6 +181,7 @@ impl Into<predict_edits_v3::PromptFormat> for PromptFormat {
Self::LabeledSections => predict_edits_v3::PromptFormat::LabeledSections,
Self::OnlySnippets => predict_edits_v3::PromptFormat::OnlySnippets,
Self::NumberedLines => predict_edits_v3::PromptFormat::NumLinesUniDiff,
+ Self::OldTextNewText => predict_edits_v3::PromptFormat::OldTextNewText,
}
}
}
@@ -1,20 +1,23 @@
use crate::PromptFormat;
-use crate::example::{ActualExcerpt, NamedExample};
+use crate::example::{ActualExcerpt, ExpectedExcerpt, NamedExample};
use crate::headless::ZetaCliAppState;
use crate::paths::{CACHE_DIR, LOGS_DIR};
use ::serde::Serialize;
use anyhow::{Result, anyhow};
use clap::Args;
+use collections::HashMap;
use gpui::http_client::Url;
+use language::{Anchor, Buffer, Point};
// use cloud_llm_client::predict_edits_v3::PromptFormat;
use cloud_zeta2_prompt::{CURSOR_MARKER, write_codeblock};
use futures::StreamExt as _;
-use gpui::{AppContext, AsyncApp};
+use gpui::{AppContext, AsyncApp, Entity};
use project::Project;
use serde::Deserialize;
use std::cell::Cell;
use std::fs;
use std::io::Write;
+use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
@@ -25,6 +28,8 @@ use zeta2::LlmResponseCache;
pub struct PredictArguments {
#[arg(long, value_enum, default_value_t = PromptFormat::default())]
prompt_format: PromptFormat,
+ #[arg(long)]
+ use_expected_context: bool,
#[clap(long, short, value_enum, default_value_t = PredictionsOutputFormat::Md)]
format: PredictionsOutputFormat,
example_path: PathBuf,
@@ -38,15 +43,23 @@ pub enum PredictionsOutputFormat {
Md,
Diff,
}
+
pub async fn run_zeta2_predict(
args: PredictArguments,
app_state: &Arc<ZetaCliAppState>,
cx: &mut AsyncApp,
) {
let example = NamedExample::load(args.example_path).unwrap();
- let result = zeta2_predict(example, args.skip_cache, args.prompt_format, &app_state, cx)
- .await
- .unwrap();
+ let result = zeta2_predict(
+ example,
+ args.skip_cache,
+ args.prompt_format,
+ args.use_expected_context,
+ &app_state,
+ cx,
+ )
+ .await
+ .unwrap();
result.write(args.format, std::io::stdout()).unwrap();
}
@@ -58,6 +71,7 @@ pub async fn zeta2_predict(
example: NamedExample,
skip_cache: bool,
prompt_format: PromptFormat,
+ use_expected_context: bool,
app_state: &Arc<ZetaCliAppState>,
cx: &mut AsyncApp,
) -> Result<PredictionDetails> {
@@ -126,14 +140,13 @@ pub async fn zeta2_predict(
let debug_task = cx.background_spawn({
let result = result.clone();
async move {
- let mut context_retrieval_started_at = None;
- let mut context_retrieval_finished_at = None;
+ let mut start_time = None;
let mut search_queries_generated_at = None;
let mut search_queries_executed_at = None;
while let Some(event) = debug_rx.next().await {
match event {
zeta2::ZetaDebugInfo::ContextRetrievalStarted(info) => {
- context_retrieval_started_at = Some(info.timestamp);
+ start_time = Some(info.timestamp);
fs::write(LOGS_DIR.join("search_prompt.md"), &info.search_prompt)?;
}
zeta2::ZetaDebugInfo::SearchQueriesGenerated(info) => {
@@ -146,11 +159,10 @@ pub async fn zeta2_predict(
zeta2::ZetaDebugInfo::SearchQueriesExecuted(info) => {
search_queries_executed_at = Some(info.timestamp);
}
- zeta2::ZetaDebugInfo::ContextRetrievalFinished(info) => {
- context_retrieval_finished_at = Some(info.timestamp);
- }
+ zeta2::ZetaDebugInfo::ContextRetrievalFinished(_info) => {}
zeta2::ZetaDebugInfo::EditPredictionRequested(request) => {
let prediction_started_at = Instant::now();
+ start_time.get_or_insert(prediction_started_at);
fs::write(
LOGS_DIR.join("prediction_prompt.md"),
&request.local_prompt.unwrap_or_default(),
@@ -190,15 +202,16 @@ pub async fn zeta2_predict(
let mut result = result.lock().unwrap();
- result.planning_search_time = search_queries_generated_at.unwrap()
- - context_retrieval_started_at.unwrap();
- result.running_search_time = search_queries_executed_at.unwrap()
- - search_queries_generated_at.unwrap();
- result.filtering_search_time = context_retrieval_finished_at.unwrap()
- - search_queries_executed_at.unwrap();
+ if !use_expected_context {
+ result.planning_search_time =
+ Some(search_queries_generated_at.unwrap() - start_time.unwrap());
+ result.running_search_time = Some(
+ search_queries_executed_at.unwrap()
+ - search_queries_generated_at.unwrap(),
+ );
+ }
result.prediction_time = prediction_finished_at - prediction_started_at;
- result.total_time =
- prediction_finished_at - context_retrieval_started_at.unwrap();
+ result.total_time = prediction_finished_at - start_time.unwrap();
break;
}
@@ -208,13 +221,42 @@ pub async fn zeta2_predict(
}
});
- zeta.update(cx, |zeta, cx| {
+ zeta.update(cx, |zeta, _cx| {
let mut options = zeta.options().clone();
options.prompt_format = prompt_format.into();
zeta.set_options(options);
- zeta.refresh_context(project.clone(), cursor_buffer.clone(), cursor_anchor, cx)
- })?
- .await?;
+ })?;
+
+ if use_expected_context {
+ let context_excerpts_tasks = example
+ .example
+ .expected_context
+ .iter()
+ .flat_map(|section| {
+ section.alternatives[0].excerpts.iter().map(|excerpt| {
+ resolve_context_entry(project.clone(), excerpt.clone(), cx.clone())
+ })
+ })
+ .collect::<Vec<_>>();
+ let context_excerpts_vec = futures::future::try_join_all(context_excerpts_tasks).await?;
+
+ let mut context_excerpts = HashMap::default();
+ for (buffer, mut excerpts) in context_excerpts_vec {
+ context_excerpts
+ .entry(buffer)
+ .or_insert(Vec::new())
+ .append(&mut excerpts);
+ }
+
+ zeta.update(cx, |zeta, _cx| {
+ zeta.set_context(project.clone(), context_excerpts)
+ })?;
+ } else {
+ zeta.update(cx, |zeta, cx| {
+ zeta.refresh_context(project.clone(), cursor_buffer.clone(), cursor_anchor, cx)
+ })?
+ .await?;
+ }
let prediction = zeta
.update(cx, |zeta, cx| {
@@ -242,6 +284,38 @@ pub async fn zeta2_predict(
anyhow::Ok(result)
}
+async fn resolve_context_entry(
+ project: Entity<Project>,
+ excerpt: ExpectedExcerpt,
+ mut cx: AsyncApp,
+) -> Result<(Entity<Buffer>, Vec<Range<Anchor>>)> {
+ let buffer = project
+ .update(&mut cx, |project, cx| {
+ let project_path = project.find_project_path(&excerpt.path, cx).unwrap();
+ project.open_buffer(project_path, cx)
+ })?
+ .await?;
+
+ let ranges = buffer.read_with(&mut cx, |buffer, _| {
+ let full_text = buffer.text();
+ let offset = full_text
+ .find(&excerpt.text)
+ .expect("Expected context not found");
+ let point = buffer.offset_to_point(offset);
+ excerpt
+ .required_lines
+ .iter()
+ .map(|line| {
+ let row = point.row + line.0;
+ let range = Point::new(row, 0)..Point::new(row + 1, 0);
+ buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
+ })
+ .collect()
+ })?;
+
+ Ok((buffer, ranges))
+}
+
struct Cache {
skip_cache: bool,
}
@@ -292,9 +366,8 @@ pub struct PredictionDetails {
pub diff: String,
pub excerpts: Vec<ActualExcerpt>,
pub excerpts_text: String, // TODO: contains the worktree root path. Drop this field and compute it on the fly
- pub planning_search_time: Duration,
- pub filtering_search_time: Duration,
- pub running_search_time: Duration,
+ pub planning_search_time: Option<Duration>,
+ pub running_search_time: Option<Duration>,
pub prediction_time: Duration,
pub total_time: Duration,
}
@@ -311,8 +384,7 @@ impl PredictionDetails {
}
pub fn to_markdown(&self) -> String {
- let inference_time =
- self.planning_search_time + self.filtering_search_time + self.prediction_time;
+ let inference_time = self.planning_search_time.unwrap_or_default() + self.prediction_time;
format!(
"## Excerpts\n\n\
@@ -322,16 +394,14 @@ impl PredictionDetails {
## Time\n\n\
Planning searches: {}ms\n\
Running searches: {}ms\n\
- Filtering context results: {}ms\n\
Making Prediction: {}ms\n\n\
-------------------\n\n\
Total: {}ms\n\
Inference: {}ms ({:.2}%)\n",
self.excerpts_text,
self.diff,
- self.planning_search_time.as_millis(),
- self.running_search_time.as_millis(),
- self.filtering_search_time.as_millis(),
+ self.planning_search_time.unwrap_or_default().as_millis(),
+ self.running_search_time.unwrap_or_default().as_millis(),
self.prediction_time.as_millis(),
self.total_time.as_millis(),
inference_time.as_millis(),