ep: Use programmatic scores as a signal for repair (#48478)

Also, lots of updates to the repair prompt and the ability to keep
previous predictions without having to rewrite them.

Release Notes:

- N/A
This commit is contained in:
Oleksiy Syvokon authored and GitHub committed 2026-02-05 14:07:04 +00:00
1 parent 8c047d9469
commit 044dca12b7
4 files changed
+210 -56

No files matched your search

@@ -421,7 +421,7 @@ pub fn extract_cursor_excerpt_from_example(example: &Example) -> Option<String>
Some(result)
}
fn extract_last_codeblock(text: &str) -> String {
pub(crate) fn extract_last_codeblock(text: &str) -> String {
let mut last_block = None;
let mut search_start = 0;
+4
View File
@@ -1068,6 +1068,10 @@ fn main() {
score::write_summary_json(&examples, summary_path)?;
}
}
Command::Repair(args) => {
let examples = finished_examples.lock().unwrap();
repair::print_report(&examples, args.confidence_threshold);
}
_ => (),
};
@@ -13,11 +13,21 @@ A previous model generated a prediction that was judged to have issues. Your job
## Rules
- **NEVER undo or revert the user's recent edits.** Examine the diff in the edit history carefully:
- If a line was removed (starts with `-`), do NOT restore that content—even if the code now appears incomplete or broken without it
- If a line was added (starts with `+`), do NOT delete or significantly modify it
- If code appears broken or incomplete after the user's edit, output `NO_EDITS` rather than "fixing" it by reverting
- Only add NEW content that extends the user's work forward; never restore what they removed
- **Key test**: if your prediction would make the code more similar to what it was BEFORE the user's edit, output `NO_EDITS` instead
- **Never assume a deletion was accidental.** Even if removing content breaks the code, breaks a pattern, or leaves text looking "incomplete", respect it. The user may be mid-rewrite. Do NOT "complete" partial text by restoring what was deleted.
- Do not just mechanically apply patterns - reason about what changes make sense given the context and the programmer's apparent goals.
- Do not just fix syntax errors - look for the broader refactoring pattern and apply it systematically throughout the code.
- Keep existing formatting unless it's absolutely necessary
- When edit history and surrounding code suggest different edits, prioritize the most recent edits in the history as they best reflect current intent.
- When uncertain, predict only the minimal, high-confidence portion of the edit. Prefer a small, correct prediction over a large, speculative one.
- Don't write a lot of code if you're not sure what to do
- Do not delete or remove text that was just added in the edit history. If a recent edit introduces incomplete or incorrect code, finish or fix it in place, or simply do nothing rather than removing it. Only remove a recent edit if the history explicitly shows the user undoing it themselves.
- Do not delete or remove text that was just added in the edit history. If a recent edit introduces incomplete or incorrect code, finish or fix it in place, or simply output `NO_EDITS` rather than removing it. Only remove a recent edit if the history explicitly shows the user undoing it themselves.
- Treat partial text at or near the cursor as the beginning of something the user is actively typing. Complete the code the user appears to be creating based on context.
# Input Format
@@ -29,7 +39,7 @@ You will be provided with:
- Within the user's current file, there is an *editable region* delimited by the `<|editable_region_start|>` and `<|editable_region_end|>` tags. You can only predict edits in this region.
- The `<|user_cursor|>` tag marks the user's current cursor position, as it stands after the last edit in the history.
4. The *previous prediction* that was generated and needs improvement.
5. *Quality assessment feedback* explaining why the previous prediction was problematic.
5. *Quality feedback* explaining why the previous prediction was problematic.
# Output Format
@@ -37,6 +47,30 @@ You will be provided with:
- Output the entire editable region, applying the edits that you predict the user will make next.
- If you're unsure about some portion of the next edit, you may still predict the surrounding code (such as a function definition, `for` loop, etc) and place the `<|user_cursor|>` within it for the user to fill in.
- Wrap the edited code in a codeblock with exactly five backticks.
- There are two special outputs for when you don't want to generate a new prediction. **These have different meanings — use the correct one:**
1. **`NO_EDITS`** — The code is already complete and correct as-is. No edits should be made at all. The editable region should remain unchanged. Use this when:
- The code needs no modifications whatsoever
- Any prediction would revert or undo the user's intentional changes
- You are unsure what edit to make and prefer to do nothing
`````
NO_EDITS
`````
2. **`KEEP_PREVIOUS`** — The previous prediction was actually correct and should be used as-is. Use this when:
- After reviewing the quality feedback, you determine the previous prediction is good
- You cannot find a meaningful improvement over the previous prediction
- The quality feedback was too cautious and the previous prediction correctly addresses the user's intent
`````
KEEP_PREVIOUS
`````
**Important:** `NO_EDITS` and `KEEP_PREVIOUS` are NOT interchangeable.
- `NO_EDITS` means "make zero changes to the code" (empty prediction).
- `KEEP_PREVIOUS` means "the previous prediction is correct, use it" (reuse the previous prediction).
- If you believe the previous prediction was correct, you MUST use `KEEP_PREVIOUS`, not `NO_EDITS`. Using `NO_EDITS` would discard the previous prediction entirely.
# 1. User Edits History
@@ -60,12 +94,10 @@ The previous model generated the following edit (in word-diff format):
{actual_patch_word_diff}
`````
# 5. Quality Assessment Feedback
# 5. Quality Feedback
- **Reverts user edits**: {reverts_edits}
- **Confidence score**: {confidence}/5
- **Reasoning**: {qa_reasoning}
{quality_feedback}
# Your Improved Prediction
Based on the feedback above, generate an improved prediction. Address the issues identified in the quality assessment.
Based on the feedback above, generate an improved prediction. Address the issues identified in the quality feedback. If the previous prediction was actually correct, output `KEEP_PREVIOUS`. If no edits should be made at all, output `NO_EDITS`.
+166 -48
View File
@@ -1,14 +1,16 @@
//! Repair predictions that received poor QA scores.
//! Repair predictions that received poor quality signals.
//!
//! This module takes examples with predictions and QA feedback, identifies
//! predictions that need improvement (based on reverts_edits or low confidence),
//! and uses an LLM to generate improved predictions.
//! This module takes examples with predictions, identifies predictions that need
//! improvement, and uses an LLM to generate improved predictions. It supports
//! two sources of quality signals:
//! - QA feedback (reverts_edits or low confidence)
//! - Computed scores when QA is unavailable (high reversal_ratio or wrong_editable_region)
use crate::{
BatchProvider, PredictionProvider,
anthropic_client::AnthropicClient,
example::{Example, ExamplePrediction},
format_prompt::{TeacherPrompt, extract_cursor_excerpt_from_example},
format_prompt::{TeacherPrompt, extract_cursor_excerpt_from_example, extract_last_codeblock},
openai_client::OpenAiClient,
parse_output::run_parse_output,
paths::LLM_CACHE_DB,
@@ -18,6 +20,46 @@ use crate::{
use anyhow::{Context as _, Result};
use std::sync::OnceLock;
const KEEP_PREVIOUS: &str = "KEEP_PREVIOUS";
/// Print a summary report of repair results across all examples.
pub fn print_report(examples: &[Example], confidence_threshold: u8) {
let total = examples.len();
let mut no_repair_needed = 0;
let mut repaired = 0;
let mut repair_failed = 0;
for example in examples {
if !needs_repair(example, confidence_threshold) {
no_repair_needed += 1;
continue;
}
if has_successful_repair(example) {
repaired += 1;
} else {
repair_failed += 1;
}
}
let needed_repair = total - no_repair_needed;
eprintln!();
eprintln!("Repair summary ({total} examples):");
eprintln!(
" {no_repair_needed}/{total} didn't need repair (confidence > {confidence_threshold})"
);
if needed_repair > 0 {
eprintln!(" {needed_repair}/{total} needed repair:");
if repaired > 0 {
eprintln!(" {repaired} repaired successfully");
}
if repair_failed > 0 {
eprintln!(" {repair_failed} failed to repair");
}
}
}
/// Arguments for the repair command.
#[derive(Debug, Clone, clap::Args)]
pub struct RepairArgs {
@@ -41,18 +83,77 @@ fn model_for_backend(backend: BatchProvider) -> &'static str {
}
}
/// Build the quality feedback string from QA results.
fn build_qa_feedback(example: &Example) -> Option<String> {
let qa = example.qa.first()?.as_ref()?;
let qa_reasoning = qa.reasoning.as_deref().unwrap_or("No reasoning provided");
let reverts_edits = qa
.reverts_edits
.map_or("unknown", |v| if v { "yes" } else { "no" });
let confidence = qa
.confidence
.map_or("unknown".to_string(), |v| v.to_string());
Some(format!(
"- **Reverts user edits**: {reverts_edits}\n\
- **Confidence score**: {confidence}/5\n\
- **Reasoning**: {qa_reasoning}"
))
}
/// Build the quality feedback string from computed scores when QA is unavailable.
fn build_score_feedback(example: &Example) -> Option<String> {
let score = example.score.first()?;
let mut issues = Vec::new();
if score.reversal_ratio > 0.9 {
issues.push(format!(
"Automated analysis detected a high reversal ratio ({:.2}), which suggests this \
prediction may be reverting changes the user intentionally made. Double-check that \
the prediction doesn't undo the user's recent edits. If the prediction is actually \
fine and the edits are intentional completions rather than reversals, keep it as-is. \
If it truly reverts the user's changes, generate an improved prediction that \
continues the user's intent instead.",
score.reversal_ratio
));
}
if score.wrong_editable_region == Some(true) {
issues.push(
"Automated analysis detected that the prediction may be modifying code outside \
the expected editable region, or producing changes misaligned with the editable \
region boundaries. Make sure the prediction only modifies code within the editable \
region and is properly aligned."
.to_string(),
);
}
if issues.is_empty() {
return None;
}
let mut feedback = String::from(
"No human quality assessment is available, but automated scoring flagged potential issues:\n\n",
);
for issue in &issues {
feedback.push_str(&format!("- {issue}\n"));
}
feedback.push_str(
"\nRemember: if the previous prediction was actually correct, output `KEEP_PREVIOUS`. \
If no edits should be made at all and you are unsure how to improve it, output `NO_EDITS`.",
);
Some(feedback)
}
/// Build the repair prompt for an example that needs improvement.
pub fn build_repair_prompt(example: &Example) -> Result<String> {
let prediction = example
.predictions
.first()
.context("no predictions available")?;
let qa = example
.qa
.first()
.context("no QA results available")?
.as_ref()
.context("QA result is None")?;
let prompt_inputs = example
.prompt_inputs
.as_ref()
@@ -62,6 +163,10 @@ pub fn build_repair_prompt(example: &Example) -> Result<String> {
.as_ref()
.context("no actual_patch available (run predict first)")?;
let quality_feedback = build_qa_feedback(example)
.or_else(|| build_score_feedback(example))
.context("no quality feedback available (need either QA results or computed scores)")?;
let actual_patch_word_diff = unified_to_word_diff(actual_patch);
let mut edit_history = String::new();
@@ -88,37 +193,39 @@ pub fn build_repair_prompt(example: &Example) -> Result<String> {
let cursor_excerpt =
extract_cursor_excerpt_from_example(example).context("failed to extract cursor excerpt")?;
let qa_reasoning = qa.reasoning.as_deref().unwrap_or("No reasoning provided");
let reverts_edits = qa
.reverts_edits
.map_or("unknown", |v| if v { "yes" } else { "no" });
let confidence = qa
.confidence
.map_or("unknown".to_string(), |v| v.to_string());
let prompt_template = crate::prompt_assets::get_prompt("repair.md");
Ok(prompt_template
.replace("{edit_history}", &edit_history)
.replace("{context}", &context)
.replace("{cursor_excerpt}", &cursor_excerpt)
.replace("{actual_patch_word_diff}", &actual_patch_word_diff)
.replace("{reverts_edits}", reverts_edits)
.replace("{confidence}", &confidence)
.replace("{qa_reasoning}", qa_reasoning))
.replace("{quality_feedback}", &quality_feedback))
}
/// Check if an example needs repair based on QA feedback.
/// Check if an example needs repair based on QA feedback or computed scores.
pub fn needs_repair(example: &Example, confidence_threshold: u8) -> bool {
let Some(qa) = example.qa.first().and_then(|q| q.as_ref()) else {
return false;
};
// Check QA-based signals first.
if let Some(qa) = example.qa.first().and_then(|q| q.as_ref()) {
if qa.reverts_edits == Some(true) {
return true;
}
if qa.reverts_edits == Some(true) {
return true;
if let Some(confidence) = qa.confidence {
if confidence <= confidence_threshold {
return true;
}
}
return false;
}
if let Some(confidence) = qa.confidence {
if confidence <= confidence_threshold {
// When QA is unavailable, fall back to computed score signals.
if let Some(score) = example.score.first() {
if score.reversal_ratio > 0.9 {
return true;
}
if score.wrong_editable_region == Some(true) {
return true;
}
}
@@ -163,10 +270,6 @@ pub async fn run_repair(
anyhow::bail!("no predictions available (run predict first)");
}
if example.qa.is_empty() {
anyhow::bail!("no QA results available (run qa first)");
}
let step_progress = example_progress.start(Step::Repair);
let model = model_for_backend(args.backend);
@@ -251,22 +354,37 @@ pub async fn run_repair(
}
};
let parse_result = TeacherPrompt::parse(example, &response);
let err = parse_result
.as_ref()
.err()
.map(|e| format!("Failed to parse repair response: {}", e));
let last_codeblock = extract_last_codeblock(&response);
if last_codeblock.trim() == KEEP_PREVIOUS {
let original = example
.predictions
.first()
.context("no original prediction to keep")?;
example.predictions.push(ExamplePrediction {
actual_patch: original.actual_patch.clone(),
actual_output: response,
actual_cursor: original.actual_cursor.clone(),
error: None,
provider: PredictionProvider::Repair,
});
} else {
let parse_result = TeacherPrompt::parse(example, &response);
let err = parse_result
.as_ref()
.err()
.map(|e| format!("Failed to parse repair response: {}", e));
let (actual_patch, actual_cursor) = parse_result.ok().unzip();
let actual_cursor = actual_cursor.flatten();
let (actual_patch, actual_cursor) = parse_result.ok().unzip();
let actual_cursor = actual_cursor.flatten();
example.predictions.push(ExamplePrediction {
actual_patch,
actual_output: response,
actual_cursor,
error: err,
provider: PredictionProvider::Repair,
});
example.predictions.push(ExamplePrediction {
actual_patch,
actual_output: response,
actual_cursor,
error: err,
provider: PredictionProvider::Repair,
});
}
Ok(())
}