mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* eval(memory): scaffold reproducible eviction evaluation * refactor(eval): align with benchmark layout * eval(memory): add deterministic QA grading Implement the disclosed deterministic-overlap-v1 grader as a pure offline module. Grading is blind by construction: grade_answer() accepts only the prediction and reference strings, never a policy identity. The undisclosed stopword list is committed as a fixed part of this grader version; yes/no/not are deliberately excluded because negation can be the entire answer. Before freezing, the grader locally reproduced all 90 historical (prediction, grade) pairs disclosed in #4789 with zero mismatches and no post-hoc tuning. validate-contracts now rejects a config whose qa.grader_version does not match the committed grader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): add environment-configured QA runner Add the exact answer-prompt renderer (retained facts sorted by ID, CURRENT DATE line omitted when absent), an OpenAI-compatible provider adapter configured only through the environment variable names pinned in the config, and a resumable run-qa command that calls both policies with identical versioned settings. Each row persists as its own file on success, so a partial paid run resumes without repeating completed calls; qa_run.json binds an output directory to one config identity. Row files and errors carry predictions and non-secret metadata only -- never questions, references, memory text, credentials, or response headers. All tests are offline via mocked transports; run-qa fails fast before touching the dataset when the provider environment is missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): add blind QA grading report and paired statistics Add grade-qa: it recomputes the deterministic selector output, rejects any answer row whose kept facts, capacity, or policy disagree with it, grades every prediction through the policy-blind grade_answer(prediction, reference) call, and only then joins grades back through stable row IDs. Published artifacts are qa.rows.jsonl (graded rows with non-secret metadata), qa.summary.json (accuracy by source/scenario/policy; official and synthetic suites never folded together), and qa.stats.json (exact paired McNemar and seeded paired bootstrap difference for the official, synthetic, and overall suites using the pinned statistics parameters). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): pin the official DeepSeek model ID The historical protocol recorded the answer model with an aggregator-style namespace (deepseek/deepseek-v4-flash). The live run calls the same underlying model (DeepSeek-V4-Flash-0731, released before the historical run) directly through DeepSeek's official OpenAI-compatible API, whose canonical ID is deepseek-v4-flash. The served model is recorded from the provider response in every answer row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): publish paired eviction QA results Publish the equal-budget live QA artifacts for pr4789-reproduction-v1: provenance, 90 graded rows, per-scenario summary, and paired statistics. At capacity 7 with identical settings, confidence answers 24/45 and hybrid-v1 40/45 (official 23/40 vs 35/40, exact McNemar p=0.0042; overall p=0.0004). The noisy-signal control is the one scenario where hybrid-v1 scored below the baseline (10/10 vs 8/10) and is reported separately. The offline suite now verifies the published statistics are recomputable from the published rows and that the artifacts carry no dataset text or credentials. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(eval): decompose the noisy-signal QA cell Both policies retained the support fact in all ten noisy-signal cases, so the two rows hybrid-v1 lost are grader phrasing boundaries (verbose numeric answers rejected by the numeric-conflict rule), not eviction failures. Documented from the published rows; the grader stays frozen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): address review hardening findings - ignore the responses/ directory the runner actually writes instead of the stale provider-responses/ entry - cover the official selection-rule recomputation with direct synthetic tests: matching manifests pass, rule-breaking IDs and missing eligible rows fail, and every published exclusion is load-bearing - align the report docstring and README with the statistics contract: the summary never folds sources; the explicitly labeled overall suite is reported alongside the separate official and synthetic suites - recompute the published bootstrap intervals (not only McNemar) in the published-results test - wire required_policy_version to the production EVICTION_POLICY_HYBRID_V1 constant so validate-contracts rejects policy drift Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): restore historical evidence rendering and harden resume identity Address both blocking findings from the #4789 artifact cross-check. The evidence renderer now emits the historical SESSION {id} AT {date} line instead of the divergent bracket format. The byte representation is protocol-critical: the witness record 35a27287 renders at 697 characters again, stays inside the 700-character distractor-bank bound, and 60d45044 leaves the bank, restoring row-level pool reproduction. Deterministic capacity-7 retention is unchanged at 27/45 vs 45/45. qa_run.json now binds a run directory to the SHA-256 of all five protocol inputs (config, both manifests, answer prompt, dataset) and names the changed artifact when it refuses to resume. Stored rows are reused only when row identity, policy, capacity, kept facts, and the request fingerprint recomputed from the current task all match; the disclosed probe (changed message under the same config) is now a regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): republish QA results under the historical protocol Replace the published artifacts with the fresh equal-budget run executed at 497ff3d0 under the restored historical evidence rendering; the earlier run under the divergent rendering is discarded entirely. At capacity 7 with identical settings, confidence answers 24/45 and hybrid-v1 38/45 (official 23/40 vs 33/40, exact McNemar p=0.0129; overall p=0.0013). The confidence control is the one scenario below baseline for hybrid-v1 (8/10 vs 6/10); both lost rows retained the support fact and are grader phrasing/abstention boundaries, documented from the published rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): adopt the historical fact IDs and prompt serialization Pool facts now carry the historical protocol IDs (gold_{case} for the support fact, d_{case}_{index}_{source} for distractors in bank-draw order), and the rendered STORED MEMORY joins fact blocks with a blank line. Sorting by these IDs reproduces the historical selection tie-break: witness case 41698283 at capacity 7 again keeps the 58bf7951 distractor and evicts 001be529 under both policies. Deterministic capacity-7 retention is unchanged at 27/45 vs 45/45. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): republish QA results under the historical serialization Replace the published artifacts with the fresh equal-budget run executed at 01f99d61 under the historical fact IDs and prompt serialization; earlier runs under divergent serializations are discarded entirely. At capacity 7 with identical settings, confidence answers 24/45 and hybrid-v1 40/45 (official 23/40 vs 35/40, exact McNemar p=0.0018; overall p=0.0001). The single row below baseline (1cea1afa, confidence-control) retained its support fact; the model abstained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * eval(memory): self-certify the publish path and pin offset coverage grade-qa now verifies (read-only) that the run marker's five protocol artifact hashes match the current inputs, rebuilds every answer task, and rejects any stored row whose request fingerprint does not match the task recomputed from the current protocol — the staleness class that previously required an out-of-band cross-check to detect. Verified end-to-end against the published run: all 90 rows pass and regrade to byte-identical artifacts, while a tampered fingerprint is refused by row ID. The distractor offset derivation and wraparound selection are now pinned by unit tests with hardcoded indices, including a wrapping offset, so a digest-slice or modulus regression can no longer stay green offline. Closes both non-blocking suggestions from the re-review. * fix(bench): bind persisted answer rows to their expected case identity Grading derived the reference case from the stored row's embedded case_id, so reassigning a valid row to another valid case passed every integrity check while silently changing the published grade. The resume path had the same gap: _row_matches_task() never compared case_id, source, or scenario. The recomputed task is now authoritative in both paths: grade_answer_rows() resolves the reference case from the expected PolicyResult and rejects any mismatch in the persisted row_id/case_id/source/scenario, and _row_matches_task() checks the same identity fields so a reassigned row is re-run instead of reused. Regressions tamper each field individually and exercise both paths. * docs(bench): document where to download the pinned LongMemEval file The README named the dataset but never said it lives on Hugging Face or how to fetch the pinned revision, so a reviewer could not run the offline commands. Add the direct download URL, the expected SHA-256, and the mirror and huggingface-cli alternatives; the CLI still never downloads anything. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
95 lines
3.8 KiB
Python
95 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts.benchmark.deermem_eviction.config import load_evaluation_config
|
|
from scripts.benchmark.deermem_eviction.grading import GRADER_VERSION, OVERLAP_THRESHOLD, grade_answer, normalize_answer
|
|
|
|
EVAL_ROOT = Path(__file__).parents[1] / "scripts" / "benchmark" / "deermem_eviction"
|
|
|
|
|
|
def test_config_pins_the_committed_grader_version() -> None:
|
|
config = load_evaluation_config(EVAL_ROOT / "configs" / "pr4789-reproduction-v1.yaml")
|
|
assert config.qa.grader_version == GRADER_VERSION == "deterministic-overlap-v1"
|
|
|
|
|
|
def test_grader_is_blind_by_construction() -> None:
|
|
parameters = inspect.signature(grade_answer).parameters
|
|
assert list(parameters) == ["prediction", "reference"]
|
|
|
|
|
|
def test_normalization_lowercases_strips_and_maps_number_words() -> None:
|
|
assert normalize_answer("Seven WEEKS!") == ["7", "weeks"]
|
|
assert normalize_answer("fifteen") == ["15"]
|
|
assert normalize_answer("eleven") == ["eleven"]
|
|
assert normalize_answer("70-200mm zoom lens") == ["70", "200mm", "zoom", "lens"]
|
|
assert normalize_answer(" \t\n ") == []
|
|
|
|
|
|
def test_empty_and_insufficient_predictions_are_rejected() -> None:
|
|
empty = grade_answer("", "3 weeks")
|
|
assert not empty.correct
|
|
assert empty.rule == "empty-prediction"
|
|
assert not grade_answer(" ", "3 weeks").correct
|
|
result = grade_answer("INSUFFICIENT.", "3 weeks")
|
|
assert not result.correct
|
|
assert result.rule == "insufficient"
|
|
assert not grade_answer("insufficient", "3 weeks").correct
|
|
|
|
|
|
def test_empty_reference_is_a_contract_error() -> None:
|
|
with pytest.raises(ValueError):
|
|
grade_answer("3 weeks", " . ")
|
|
|
|
|
|
def test_exact_match_ignores_case_punctuation_and_number_words() -> None:
|
|
assert grade_answer("Every week.", "every week").rule == "exact"
|
|
assert grade_answer("NO", "no").rule == "exact"
|
|
assert grade_answer("seven", "7").rule == "exact"
|
|
|
|
|
|
def test_substring_matches_are_token_level_and_bidirectional() -> None:
|
|
assert grade_answer("132 points", "132").rule == "substring"
|
|
assert grade_answer("132", "132 points").rule == "substring"
|
|
assert grade_answer("Ford F-150 pickup truck.", "Ford F-150").rule == "substring"
|
|
assert grade_answer("Ford F-150 pickup truck.", "a Ford F-150").correct
|
|
assert not grade_answer("5", "25").correct
|
|
assert not grade_answer("no", "north").correct
|
|
|
|
|
|
def test_conflicting_numeric_answers_are_rejected() -> None:
|
|
result = grade_answer("5", "3 weeks")
|
|
assert not result.correct
|
|
assert result.rule == "numeric-conflict"
|
|
assert not grade_answer("12 weeks", "8 weeks").correct
|
|
|
|
|
|
def test_numbers_inside_an_explicit_reference_range_are_accepted() -> None:
|
|
reference = "ranging from 5 to 10 hours"
|
|
assert grade_answer("7", reference).rule == "numeric-range"
|
|
assert grade_answer("5 hours", reference).rule == "numeric-range"
|
|
assert grade_answer("10", reference).correct
|
|
assert not grade_answer("4", reference).correct
|
|
assert not grade_answer("11", reference).correct
|
|
assert grade_answer("7", "ranging from 5 dollars to 10 dollars").rule == "numeric-range"
|
|
|
|
|
|
def test_overlap_requires_sixty_percent_in_both_directions() -> None:
|
|
assert OVERLAP_THRESHOLD == 0.6
|
|
accepted = grade_answer("under my bed", "under the bed")
|
|
assert accepted.correct
|
|
assert accepted.rule == "overlap-accept"
|
|
assert grade_answer("red kite string", "red kite ribbon").correct
|
|
assert not grade_answer("red kite", "red balloon ribbon string flag").correct
|
|
assert not grade_answer("50mm prime lens", "70-200mm zoom lens").correct
|
|
assert not grade_answer("YES", "NO").correct
|
|
|
|
|
|
def test_all_stopword_predictions_cannot_pass_overlap() -> None:
|
|
result = grade_answer("of the", "3 weeks")
|
|
assert not result.correct
|
|
assert result.rule == "overlap-reject"
|