mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 22:16:19 +00:00
* feat(memory): add DeerMem scope-isolation benchmark Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> * fix(benchmark): grade persisted summaries and retry failed extractions --------- Signed-off-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> Co-authored-by: PeaceMaker-best <221849497+PeaceMaker-best@users.noreply.github.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
237 lines
12 KiB
Python
237 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from deerflow.agents.memory.backends.deermem.deermem.core.paths import DEFAULT_AGENT_BUCKET
|
|
from scripts.benchmark.deermem_scope_isolation import runner
|
|
from scripts.benchmark.deermem_scope_isolation.contract import load_protocol
|
|
from scripts.benchmark.deermem_scope_isolation.grading import grade_routing_row, grade_semantic_rows
|
|
from scripts.benchmark.deermem_scope_isolation.report import RowIntegrityError, build_report, collect_rows, write_report
|
|
from scripts.benchmark.deermem_scope_isolation.runner import ROOT, ensure_run_identity, run
|
|
|
|
MANIFEST = ROOT / "manifest.json"
|
|
|
|
|
|
def test_contract_is_versioned_and_canaries_are_unique() -> None:
|
|
protocol = load_protocol(MANIFEST)
|
|
|
|
assert protocol.protocol_id == "deermem-scope-isolation-v1"
|
|
assert len(protocol.semantic_cases) == 6
|
|
canaries = [canary for case in protocol.semantic_cases for canary in (*case.expected_persisted_canaries, *case.expected_rejected_canaries, *case.expected_removed_canaries)] + [protocol.routing_case.canary]
|
|
assert len(canaries) == len(set(canaries))
|
|
|
|
|
|
def test_contract_rejects_duplicate_canary(tmp_path: Path) -> None:
|
|
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
canary = manifest["semantic_cases"][0]["expected_persisted_canaries"][0]
|
|
case = manifest["semantic_cases"][1]
|
|
case["expected_rejected_canaries"] = [canary]
|
|
case["messages"][0]["content"] += f" {canary}"
|
|
case["offline_output"]["newFacts"][0]["content"] += f" {canary}"
|
|
path = tmp_path / "manifest.json"
|
|
path.write_text(json.dumps(manifest), encoding="utf-8")
|
|
|
|
with pytest.raises(ValueError, match="canary .* is reused"):
|
|
load_protocol(path)
|
|
|
|
|
|
def test_offline_runner_executes_production_scope_and_routing_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
protocol = load_protocol(MANIFEST)
|
|
monkeypatch.setattr(
|
|
"scripts.benchmark.deermem_scope_isolation.runner.build_llm",
|
|
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("offline mode must not build a provider model")),
|
|
)
|
|
|
|
result = run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
marker = json.loads((tmp_path / "run.json").read_text(encoding="utf-8"))
|
|
semantic, routing = collect_rows(protocol, tmp_path, marker)
|
|
|
|
assert result.executed == 7
|
|
assert all(row["update_succeeded"] for row in semantic)
|
|
assert grade_semantic_rows(semantic)["durable_retention_rate"] == 1.0
|
|
assert grade_semantic_rows(semantic)["unsafe_persistence_rate"] == 0.0
|
|
assert grade_semantic_rows(semantic)["atomic_correction_success_rate"] == 1.0
|
|
assert routing is not None
|
|
assert routing["checked_scopes"]["default"]["agent_name"] == DEFAULT_AGENT_BUCKET
|
|
assert grade_routing_row(routing)["cross_agent_contamination_rate"] == 0.0
|
|
assert grade_routing_row(routing)["cross_user_contamination_rate"] == 0.0
|
|
assert grade_routing_row(routing)["custom_agent_bootstrap_success"] is True
|
|
|
|
|
|
def test_resume_reuses_protocol_bound_rows_and_rejects_changed_manifest(tmp_path: Path) -> None:
|
|
protocol = load_protocol(MANIFEST)
|
|
first = run(protocol, manifest_path=MANIFEST, output_dir=tmp_path / "run", mode="offline")
|
|
second = run(protocol, manifest_path=MANIFEST, output_dir=tmp_path / "run", mode="offline")
|
|
|
|
assert first.executed == 7
|
|
assert second.executed == 0
|
|
assert second.reused == 7
|
|
|
|
changed = tmp_path / "changed.json"
|
|
raw = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
raw["protocol_id"] = "changed"
|
|
changed.write_text(json.dumps(raw), encoding="utf-8")
|
|
with pytest.raises(ValueError, match="different protocol"):
|
|
ensure_run_identity(tmp_path / "run", mode="offline", protocol=load_protocol(changed), manifest_path=changed, settings=None)
|
|
|
|
|
|
def test_report_recomputes_metrics_and_rejects_tampered_outcome(tmp_path: Path) -> None:
|
|
protocol = load_protocol(MANIFEST)
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
marker = json.loads((tmp_path / "run.json").read_text(encoding="utf-8"))
|
|
|
|
report = build_report(protocol, tmp_path, marker)
|
|
|
|
assert report["semantic_model_quality"]["durable_retention_rate"] == 1.0
|
|
assert report["deterministic_identity_routing"]["cross_agent_contamination_rate"] == 0.0
|
|
|
|
path = tmp_path / "rows" / "durable-preference.json"
|
|
row = json.loads(path.read_text(encoding="utf-8"))
|
|
row["expected_persisted_canaries"] = []
|
|
path.write_text(json.dumps(row), encoding="utf-8")
|
|
with pytest.raises(RowIntegrityError, match="result-integrity"):
|
|
collect_rows(protocol, tmp_path, marker)
|
|
|
|
|
|
def test_contract_rejects_reused_routing_canary(tmp_path):
|
|
raw = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
raw["routing_case"]["canary"] = raw["semantic_cases"][0]["expected_persisted_canaries"][0]
|
|
path = tmp_path / "manifest.json"
|
|
path.write_text(json.dumps(raw), encoding="utf-8")
|
|
with pytest.raises(ValueError, match="routing canary must be unique"):
|
|
load_protocol(path)
|
|
|
|
|
|
@pytest.mark.parametrize("group,section", [("user", "workContext"), ("user", "personalContext"), ("user", "topOfMind"), ("history", "recentMonths"), ("history", "earlierContext"), ("history", "longTermBackground")])
|
|
@pytest.mark.parametrize("scope,unsafe", [("user", True), ("project", False)])
|
|
def test_semantic_verdict_checks_persisted_summaries(tmp_path, group, section, scope, unsafe):
|
|
protocol = load_protocol(MANIFEST)
|
|
case = protocol.semantic_cases[1]
|
|
canary = case.expected_rejected_canaries[0]
|
|
output = {"user": {}, "history": {}, "newFacts": [], "factsToRemove": []}
|
|
output[group][section] = {"shouldUpdate": True, "summary": canary, "scope": scope, "authority": "descriptive"}
|
|
protocol = replace(protocol, semantic_cases=(replace(case, offline_output=output),))
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
marker = json.loads((tmp_path / "run.json").read_text(encoding="utf-8"))
|
|
semantic, _ = collect_rows(protocol, tmp_path, marker)
|
|
assert semantic[0]["rejected_canaries_present"] == ([canary] if unsafe else [])
|
|
assert grade_semantic_rows(semantic)["unsafe_persistence_rate"] == float(unsafe)
|
|
|
|
|
|
def test_missing_live_key_fails_before_model_construction(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("DEERMEM_BENCH_TEST_KEY", raising=False)
|
|
monkeypatch.setattr(runner, "build_llm", lambda *_: pytest.fail("model construction reached without a key"))
|
|
settings = runner.LiveSettings("openai", "test", 0.0, "DEERMEM_BENCH_TEST_KEY", None)
|
|
with pytest.raises(ValueError, match="required API key"):
|
|
run(load_protocol(MANIFEST), manifest_path=MANIFEST, output_dir=tmp_path, mode="live", settings=settings)
|
|
|
|
|
|
def test_report_refuses_to_overwrite_evidence(tmp_path):
|
|
protocol = load_protocol(MANIFEST)
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
report = write_report(protocol, manifest_path=MANIFEST, output_dir=tmp_path)
|
|
original = report.read_bytes()
|
|
with pytest.raises(FileExistsError, match="refusing to overwrite"):
|
|
write_report(protocol, manifest_path=MANIFEST, output_dir=tmp_path)
|
|
assert report.read_bytes() == original
|
|
|
|
|
|
@pytest.mark.parametrize("failure", ["provider", "json"])
|
|
def test_failed_live_extraction_is_not_sealed_and_resume_retries(tmp_path, monkeypatch, failure):
|
|
protocol = load_protocol(MANIFEST)
|
|
settings = runner.LiveSettings("openai", "test", 0.0, "DEERMEM_BENCH_TEST_KEY", None)
|
|
monkeypatch.setenv(settings.api_key_env, "synthetic-test-key")
|
|
cases = iter(enumerate(protocol.semantic_cases))
|
|
|
|
def fail_invoke(*args, **kwargs):
|
|
if failure == "provider":
|
|
raise TimeoutError("synthetic provider timeout")
|
|
return SimpleNamespace(content="not JSON")
|
|
|
|
def build(*args):
|
|
index, case = next(cases)
|
|
return SimpleNamespace(invoke=fail_invoke) if index == 1 else runner._StaticModel(case.offline_output)
|
|
|
|
monkeypatch.setattr(runner, "build_llm", build)
|
|
with pytest.raises(RuntimeError, match="project-constraint"):
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="live", settings=settings)
|
|
assert sorted(path.stem for path in (tmp_path / "rows").glob("*.json")) == ["durable-preference"]
|
|
with pytest.raises(RowIntegrityError, match="missing or invalid"):
|
|
write_report(protocol, manifest_path=MANIFEST, output_dir=tmp_path)
|
|
remaining = iter(protocol.semantic_cases[1:])
|
|
monkeypatch.setattr(runner, "build_llm", lambda *_: runner._StaticModel(next(remaining).offline_output))
|
|
resumed = run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="live", settings=settings)
|
|
assert (resumed.reused, resumed.executed) == (1, 5)
|
|
write_report(protocol, manifest_path=MANIFEST, output_dir=tmp_path)
|
|
|
|
|
|
def test_unsuccessful_rows_are_rejected_and_not_reused(tmp_path):
|
|
protocol = load_protocol(MANIFEST)
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
path = tmp_path / "rows" / "project-constraint.json"
|
|
row = json.loads(path.read_text(encoding="utf-8"))
|
|
row["update_succeeded"] = False
|
|
path.write_text(json.dumps(runner._seal_row(row)), encoding="utf-8")
|
|
with pytest.raises(RowIntegrityError, match="failed"):
|
|
write_report(protocol, manifest_path=MANIFEST, output_dir=tmp_path)
|
|
with pytest.raises(ValueError, match="failed"):
|
|
grade_semantic_rows([row])
|
|
resumed = run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
assert (resumed.executed, resumed.reused) == (1, 6)
|
|
|
|
|
|
def test_legacy_fact_only_rows_are_not_reported_or_reused(tmp_path):
|
|
protocol = load_protocol(MANIFEST)
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
path = tmp_path / "rows" / "project-constraint.json"
|
|
row = json.loads(path.read_text(encoding="utf-8"))
|
|
row["schema_version"] = 1
|
|
path.write_text(json.dumps(runner._seal_row(row)), encoding="utf-8")
|
|
with pytest.raises(RowIntegrityError, match="unsupported benchmark row"):
|
|
write_report(protocol, manifest_path=MANIFEST, output_dir=tmp_path)
|
|
resumed = run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
assert (resumed.executed, resumed.reused) == (1, 6)
|
|
|
|
|
|
def test_routing_does_not_treat_shared_summaries_as_agent_leaks(tmp_path, monkeypatch):
|
|
protocol = load_protocol(MANIFEST)
|
|
canary = protocol.routing_case.canary
|
|
original_invoke = runner._StaticModel.invoke
|
|
|
|
def invoke(self, prompt, config=None):
|
|
response = original_invoke(self, prompt, config)
|
|
output = json.loads(response.content)
|
|
if any(canary in fact["content"] for fact in output["newFacts"]):
|
|
output["user"]["workContext"] = {"shouldUpdate": True, "summary": canary, "scope": "user", "authority": "descriptive"}
|
|
response.content = json.dumps(output)
|
|
return response
|
|
|
|
monkeypatch.setattr(runner._StaticModel, "invoke", invoke)
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
marker = json.loads((tmp_path / "run.json").read_text(encoding="utf-8"))
|
|
_, routing = collect_rows(protocol, tmp_path, marker)
|
|
assert routing["selected_present"] is True
|
|
assert routing["default_present"] is False
|
|
assert routing["other_agent_present"] is False
|
|
assert routing["other_user_present"] is False
|
|
|
|
|
|
def test_storage_failure_does_not_produce_a_semantic_row(tmp_path, monkeypatch):
|
|
protocol = load_protocol(MANIFEST)
|
|
finalize = runner.MemoryUpdater._finalize_update
|
|
|
|
def fail_save(*args, **kwargs):
|
|
raise OSError("synthetic storage failure")
|
|
|
|
monkeypatch.setattr(runner.MemoryUpdater, "_finalize_update", fail_save)
|
|
with pytest.raises(RuntimeError, match="durable-preference"):
|
|
run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline")
|
|
assert not (tmp_path / "rows").exists()
|
|
monkeypatch.setattr(runner.MemoryUpdater, "_finalize_update", finalize)
|
|
assert run(protocol, manifest_path=MANIFEST, output_dir=tmp_path, mode="offline").executed == 7
|