541 lines · 19.1 KB · Rust
RawDownload
use crate::{    PredictArgs, PredictionProvider,    example::{Example, ExampleScore},    format_prompt::TeacherPrompt,    headless::EpAppState,    metrics,    parse_output::parse_prediction_output,    predict::run_prediction,    progress::{ExampleProgress, Step},    reversal_tracking,};use anyhow::Context as _;use edit_prediction::udiff::{apply_diff_to_string, apply_diff_to_string_with_hunk_offset};use gpui::AsyncApp;use serde::Serialize;use std::fs::File;use std::io::BufWriter;use std::path::Path;use std::sync::Arc; pub async fn run_scoring(    example: &mut Example,    args: &PredictArgs,    app_state: Arc<EpAppState>,    example_progress: &ExampleProgress,    cx: AsyncApp,) -> anyhow::Result<()> {    run_prediction(example, args, app_state, example_progress, cx).await?;     let progress = example_progress.start(Step::Score);     progress.set_substatus("applying patches");    let original_text = &example        .prompt_inputs        .as_ref()        .context("prompt_inputs is required for scoring - run prediction first or ensure JSON includes prompt_inputs")?        .content;    let expected_patches_with_cursors = example.spec.expected_patches_with_cursor_positions();     let expected_texts: Vec<String> = expected_patches_with_cursors        .iter()        .map(|(patch, _)| {            apply_diff_to_string(patch, original_text)                .with_context(|| format!("Expected patch did not apply for {}", example.spec.name))        })        .collect::<Result<Vec<_>, _>>()?;     // For Teacher prompts, we need to extract the editable region to properly compute cursor offsets.    // The actual_cursor_offset from Teacher is relative to the editable region, while the expected    // cursor from the patch is relative to the hunk. We need to apply the patch to the editable    // region to find where the hunk matched, then compute the expected cursor position.    let old_editable_region = if let Some(p) = example.prompt.as_ref() {        if matches!(            p.provider,            PredictionProvider::Teacher(_) | PredictionProvider::TeacherNonBatching(_)        ) {            Some(                TeacherPrompt::extract_editable_region(&p.input)?                    .replace(TeacherPrompt::USER_CURSOR_MARKER, ""),            )        } else {            None        }    } else {        None    };     let zero_scores = ExampleScore {        delta_chr_f: 0.0,        braces_disbalance: 0,        exact_lines_tp: 0,        exact_lines_fp: 0,        exact_lines_fn: 0,        reversal_ratio: 0.0,        cursor_distance: None,        cursor_exact_match: None,    };     let prompt_inputs = example.prompt_inputs.as_ref().unwrap();    let cursor_path = example.spec.cursor_path.as_ref();     progress.set_substatus("computing metrics");    let mut scores = vec![];    for prediction in &example.predictions {        let actual_patch = prediction.actual_patch.clone().or_else(|| {            parse_prediction_output(example, &prediction.actual_output, prediction.provider)                .ok()                .map(|(patch, _)| patch)        });         let Some(actual_patch) = actual_patch else {            scores.push(zero_scores.clone());            continue;        };         let actual_text = match apply_diff_to_string(&actual_patch, original_text) {            Ok(text) => text,            Err(_) => {                scores.push(zero_scores.clone());                continue;            }        };         let mut best_delta_chr_f = 0.0f32;        let mut best_expected_cursor: Option<usize> = None;        let mut best_patch_idx: Option<usize> = None;         for (idx, expected) in expected_texts.iter().enumerate() {            let delta_chr_f = metrics::delta_chr_f(original_text, expected, &actual_text) as f32;            if delta_chr_f > best_delta_chr_f {                best_delta_chr_f = delta_chr_f;                best_patch_idx = Some(idx);            }        }         if let Some(idx) = best_patch_idx {            // Get the raw cursor offset from the expected patch (relative to hunk new text)            let expected_cursor_in_patch = expected_patches_with_cursors                .get(idx)                .and_then(|(_, cursor)| *cursor);             // For Teacher prompts, we need to apply the patch to the editable region            // to find where the hunk matched, then compute the actual cursor position            if let (Some(editable_region), Some(cursor_in_patch)) =                (&old_editable_region, expected_cursor_in_patch)            {                let (patch, _) = &expected_patches_with_cursors[idx];                if let Ok((_, hunk_offset)) =                    apply_diff_to_string_with_hunk_offset(patch, editable_region)                {                    let hunk_start = hunk_offset.unwrap_or(0);                    best_expected_cursor = Some(hunk_start + cursor_in_patch);                }            } else {                // For non-Teacher prompts or if we can't compute, use raw offset                best_expected_cursor = expected_cursor_in_patch;            }        }         let disbalance_before = metrics::braces_disbalance(&original_text);        let disbalance_after = metrics::braces_disbalance(&actual_text);        let braces_disbalance = disbalance_after.saturating_sub(disbalance_before);        if braces_disbalance > 0 {            std::fs::write(                "/tmp/unbalanced-count.before",                disbalance_before.to_string(),            )            .ok();            std::fs::write("/tmp/unbalanced-count.after", disbalance_after.to_string()).ok();            std::fs::write("/tmp/unbalanced-text.before", &original_text).ok();            std::fs::write("/tmp/unbalanced-text.after", &actual_text).ok();        }         // Compute exact lines match against best matching expected patch        let best_exact_lines = expected_patches_with_cursors            .iter()            .map(|(expected_patch, _)| metrics::exact_lines_match(expected_patch, &actual_patch))            .max_by_key(|m| m.true_positives)            .unwrap_or_default();         // Compute reversal ratio        let reversal_ratio = reversal_tracking::compute_prediction_reversal_ratio(            prompt_inputs,            &actual_text,            cursor_path,        );         // Compute cursor position metrics        let (cursor_distance, cursor_exact_match) =            compute_cursor_metrics(best_expected_cursor, prediction.actual_cursor_offset);         scores.push(ExampleScore {            delta_chr_f: best_delta_chr_f,            braces_disbalance,            exact_lines_tp: best_exact_lines.true_positives,            exact_lines_fp: best_exact_lines.false_positives,            exact_lines_fn: best_exact_lines.false_negatives,            reversal_ratio,            cursor_distance,            cursor_exact_match,        });    }     example.score = scores;    Ok(())} fn compute_cursor_metrics(    expected_cursor: Option<usize>,    actual_cursor: Option<usize>,) -> (Option<usize>, Option<bool>) {    match (expected_cursor, actual_cursor) {        (Some(expected), Some(actual)) => {            let distance = expected.abs_diff(actual);            let exact_match = expected == actual;            (Some(distance), Some(exact_match))        }        (None, None) => {            // Neither has cursor position - skip cursor scoring            (None, None)        }        (Some(_), None) | (None, Some(_)) => {            // Only one has cursor position - count as miss            (None, Some(false))        }    }} pub fn print_report(examples: &[Example]) {    use crate::metrics::ClassificationMetrics;     const LINE_WIDTH: usize = 94;    let separator = "─".repeat(LINE_WIDTH);     println!("{}", separator);    println!(        "{:<40} {:>8} {:>5} {:>7} {:>7} {:>7} {:>7} {:>6}",        "Example", "DeltaChrF", "Brace", "F1", "Revert", "QaRev", "QaConf", "Cursor"    );    println!("{}", separator);     let mut all_delta_chr_f_scores = Vec::new();    let mut all_reversal_ratios = Vec::new();    let mut braces_disbalance_sum: usize = 0;    let mut total_exact_lines = ClassificationMetrics::default();    let mut total_scores: usize = 0;    let mut qa_reverts_count: usize = 0;    let mut qa_reverts_total: usize = 0;    let mut qa_confidence_sum: u64 = 0;    let mut qa_confidence_count: usize = 0;    let mut cursor_exact_matches: usize = 0;    let mut cursor_total: usize = 0;    let mut cursor_distance_sum: usize = 0;    let mut cursor_distance_count: usize = 0;     for example in examples {        for (score_idx, score) in example.score.iter().enumerate() {            let exact_lines = ClassificationMetrics {                true_positives: score.exact_lines_tp,                false_positives: score.exact_lines_fp,                false_negatives: score.exact_lines_fn,            };             // Get QA results for this prediction if available            let qa_result = example.qa.get(score_idx).and_then(|q| q.as_ref());            let qa_reverts_str = qa_result                .and_then(|q| q.reverts_edits)                .map(|v| if v { "yes" } else { "no" })                .unwrap_or("-");            let qa_conf_str = qa_result                .and_then(|q| q.confidence)                .map(|v| format!("{}", v))                .unwrap_or("-".to_string());             // Format cursor metric            let cursor_str = match (score.cursor_exact_match, score.cursor_distance) {                (Some(true), _) => "✓".to_string(),                (Some(false), Some(dist)) => format!("±{}", dist),                (Some(false), None) => "✗".to_string(),                (None, _) => "-".to_string(),            };             println!(                "{:<40} {:>8.2} {:>5} {:>6.1}% {:>6.1}% {:>7} {:>7} {:>6}",                truncate_name(&example.spec.name, 40),                score.delta_chr_f,                score.braces_disbalance,                exact_lines.f1() * 100.0,                score.reversal_ratio * 100.0,                qa_reverts_str,                qa_conf_str,                cursor_str            );             all_delta_chr_f_scores.push(score.delta_chr_f);            all_reversal_ratios.push(score.reversal_ratio);            total_scores += 1;            braces_disbalance_sum += score.braces_disbalance;            total_exact_lines.true_positives += score.exact_lines_tp;            total_exact_lines.false_positives += score.exact_lines_fp;            total_exact_lines.false_negatives += score.exact_lines_fn;             // Accumulate QA metrics            if let Some(qa) = qa_result {                if let Some(reverts) = qa.reverts_edits {                    qa_reverts_total += 1;                    if reverts {                        qa_reverts_count += 1;                    }                }                if let Some(conf) = qa.confidence {                    qa_confidence_sum += conf as u64;                    qa_confidence_count += 1;                }            }             // Accumulate cursor metrics            if let Some(exact_match) = score.cursor_exact_match {                cursor_total += 1;                if exact_match {                    cursor_exact_matches += 1;                }            }            if let Some(dist) = score.cursor_distance {                cursor_distance_sum += dist;                cursor_distance_count += 1;            }        }    }     println!("{}", separator);     if !all_delta_chr_f_scores.is_empty() {        let avg_delta_chr_f: f32 =            all_delta_chr_f_scores.iter().sum::<f32>() / all_delta_chr_f_scores.len() as f32;        let avg_reversal_ratio: f32 =            all_reversal_ratios.iter().sum::<f32>() / all_reversal_ratios.len() as f32;        let braces_disbalance_avg: f32 = braces_disbalance_sum as f32 / total_scores as f32;         let qa_reverts_str = if qa_reverts_total > 0 {            format!(                "{:.1}%",                qa_reverts_count as f32 / qa_reverts_total as f32 * 100.0            )        } else {            "-".to_string()        };        let qa_conf_str = if qa_confidence_count > 0 {            format!(                "{:.1}",                qa_confidence_sum as f32 / qa_confidence_count as f32            )        } else {            "-".to_string()        };        let cursor_str = if cursor_total > 0 {            format!(                "{:.0}%",                cursor_exact_matches as f32 / cursor_total as f32 * 100.0            )        } else {            "-".to_string()        };        let avg_cursor_distance = if cursor_distance_count > 0 {            Some(cursor_distance_sum as f32 / cursor_distance_count as f32)        } else {            None        };         println!(            "{:<40} {:>8.2} {:>5.1} {:>6.1}% {:>6.1}% {:>7} {:>7} {:>6}",            "TOTAL / AVERAGE",            avg_delta_chr_f,            braces_disbalance_avg,            total_exact_lines.f1() * 100.0,            avg_reversal_ratio * 100.0,            qa_reverts_str,            qa_conf_str,            cursor_str        );        println!("{}", separator);         // Print additional cursor metrics if available        if let Some(avg_dist) = avg_cursor_distance {            println!(                "Cursor: {}/{} exact matches ({:.0}%), avg distance: {:.1} bytes",                cursor_exact_matches,                cursor_total,                cursor_exact_matches as f32 / cursor_total as f32 * 100.0,                avg_dist            );        }    }     println!("\n");} fn truncate_name(name: &str, max_len: usize) -> String {    if name.len() <= max_len {        name.to_string()    } else {        format!("{}...", &name[..max_len - 3])    }} #[derive(Serialize)]pub struct SummaryJson {    pub total_examples: usize,    pub avg_delta_chr_f: f32,    pub avg_braces_disbalance: f32,    pub exact_lines_true_positives: usize,    pub exact_lines_false_positives: usize,    pub exact_lines_false_negatives: usize,    pub exact_lines_precision: f64,    pub exact_lines_recall: f64,    pub exact_lines_f1: f64,    pub avg_reversal_ratio: f32,    #[serde(skip_serializing_if = "Option::is_none")]    pub qa_avg_reverts_edits: Option<f32>,    #[serde(skip_serializing_if = "Option::is_none")]    pub qa_avg_confidence: Option<f32>,    #[serde(skip_serializing_if = "Option::is_none")]    pub cursor_exact_match_rate: Option<f32>,    #[serde(skip_serializing_if = "Option::is_none")]    pub cursor_avg_distance: Option<f32>,    #[serde(skip_serializing_if = "Option::is_none")]    pub cursor_total_evaluated: Option<usize>,} pub fn compute_summary(examples: &[Example]) -> SummaryJson {    use crate::metrics::ClassificationMetrics;     let mut all_delta_chr_f_scores = Vec::new();    let mut all_reversal_ratios = Vec::new();    let mut braces_disbalance_sum: usize = 0;    let mut total_exact_lines = ClassificationMetrics::default();    let mut total_scores: usize = 0;    let mut qa_reverts_count: usize = 0;    let mut qa_reverts_total: usize = 0;    let mut qa_confidence_sum: u64 = 0;    let mut qa_confidence_count: usize = 0;    let mut cursor_exact_matches: usize = 0;    let mut cursor_total: usize = 0;    let mut cursor_distance_sum: usize = 0;    let mut cursor_distance_count: usize = 0;     for example in examples {        for (score_idx, score) in example.score.iter().enumerate() {            all_delta_chr_f_scores.push(score.delta_chr_f);            all_reversal_ratios.push(score.reversal_ratio);            total_scores += 1;            braces_disbalance_sum += score.braces_disbalance;            total_exact_lines.true_positives += score.exact_lines_tp;            total_exact_lines.false_positives += score.exact_lines_fp;            total_exact_lines.false_negatives += score.exact_lines_fn;             // Accumulate QA metrics            if let Some(Some(qa)) = example.qa.get(score_idx) {                if let Some(reverts) = qa.reverts_edits {                    qa_reverts_total += 1;                    if reverts {                        qa_reverts_count += 1;                    }                }                if let Some(conf) = qa.confidence {                    qa_confidence_sum += conf as u64;                    qa_confidence_count += 1;                }            }             // Accumulate cursor metrics            if let Some(exact_match) = score.cursor_exact_match {                cursor_total += 1;                if exact_match {                    cursor_exact_matches += 1;                }            }            if let Some(dist) = score.cursor_distance {                cursor_distance_sum += dist;                cursor_distance_count += 1;            }        }    }     let avg_delta_chr_f = if all_delta_chr_f_scores.is_empty() {        0.0    } else {        all_delta_chr_f_scores.iter().sum::<f32>() / all_delta_chr_f_scores.len() as f32    };     let avg_reversal_ratio = if all_reversal_ratios.is_empty() {        0.0    } else {        all_reversal_ratios.iter().sum::<f32>() / all_reversal_ratios.len() as f32    };     let avg_braces_disbalance = if total_scores == 0 {        0.0    } else {        braces_disbalance_sum as f32 / total_scores as f32    };     let qa_avg_reverts_edits = if qa_reverts_total > 0 {        Some(qa_reverts_count as f32 / qa_reverts_total as f32)    } else {        None    };     let qa_avg_confidence = if qa_confidence_count > 0 {        Some(qa_confidence_sum as f32 / qa_confidence_count as f32)    } else {        None    };     let cursor_exact_match_rate = if cursor_total > 0 {        Some(cursor_exact_matches as f32 / cursor_total as f32)    } else {        None    }; 

Lines 1–500 of 541.