From a033fdfb023de904d95f18ba7198564daac7e03a Mon Sep 17 00:00:00 2001 From: Jaden Earl Date: Thu, 2 Jul 2026 09:10:27 -0600 Subject: [PATCH] Record fetching backend per URL in run reports Add a "backend" field to each entry in reports/.json: the fetcher recorded on the stored capture (set for stored/deduped/ cache_hit outcomes, "" when nothing was fetched, e.g. errors). This lets downstream tooling attribute per-domain cost by backend without changing the cost estimation itself. Co-Authored-By: Claude Fable 5 --- .../test_source_archive/test_reports.py | 38 +++++++++++++++++++ .../source_archive/reports.py | 14 ++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/code_tests/unit_tests/test_agents_and_tools/test_source_archive/test_reports.py b/code_tests/unit_tests/test_agents_and_tools/test_source_archive/test_reports.py index a3248fa8..52c9d841 100644 --- a/code_tests/unit_tests/test_agents_and_tools/test_source_archive/test_reports.py +++ b/code_tests/unit_tests/test_agents_and_tools/test_source_archive/test_reports.py @@ -1,6 +1,12 @@ from __future__ import annotations +import json + from forecasting_tools.agents_and_tools.source_archive.config import ArchiveConfig +from forecasting_tools.agents_and_tools.source_archive.models import ( + StoredCapture, + url_hash, +) from forecasting_tools.agents_and_tools.source_archive.pipeline import ( CaptureOutcome, PipelineSummary, @@ -12,6 +18,12 @@ from forecasting_tools.agents_and_tools.source_archive.storage import LocalBlobStore +def _stored(url: str, fetcher: str) -> StoredCapture: + return StoredCapture( + url=url, url_hash=url_hash(url), content_hash="c1", fetcher=fetcher + ) + + def test_run_report_roundtrip_canonicalizes(tmp_path): store = LocalBlobStore(tmp_path) config = ArchiveConfig(s3_prefix="t") @@ -29,6 +41,32 @@ def test_run_report_roundtrip_canonicalizes(tmp_path): assert out["https://b.test/q"] == "error" +def test_run_report_records_backend_per_url(tmp_path): + store = LocalBlobStore(tmp_path) + config = ArchiveConfig(s3_prefix="t") + summary = PipelineSummary( + outcomes=[ + CaptureOutcome( + url="https://a.test/p", + status="stored", + stored=_stored("https://a.test/p", "firecrawl"), + ), + CaptureOutcome( + url="https://c.test/r", + status="cache_hit", + stored=_stored("https://c.test/r", "playwright"), + ), + CaptureOutcome(url="https://b.test/q", status="error", reason="cloudflare"), + ] + ) + key = write_run_report(store, "r1", summary, config) + + rows = {r["url"]: r for r in json.loads(store.get(key).decode("utf-8"))} + assert rows["https://a.test/p"]["backend"] == "firecrawl" + assert rows["https://c.test/r"]["backend"] == "playwright" + assert rows["https://b.test/q"]["backend"] == "" # nothing fetched + + def test_captured_status_wins_over_failure(tmp_path): store = LocalBlobStore(tmp_path) config = ArchiveConfig(s3_prefix="t") diff --git a/forecasting_tools/agents_and_tools/source_archive/reports.py b/forecasting_tools/agents_and_tools/source_archive/reports.py index ba75b4b6..37e8a2cd 100644 --- a/forecasting_tools/agents_and_tools/source_archive/reports.py +++ b/forecasting_tools/agents_and_tools/source_archive/reports.py @@ -35,9 +35,19 @@ def report_key(run_id: str, config: ArchiveConfig) -> str: def write_run_report( store: BlobStore, run_id: str, summary, config: ArchiveConfig ) -> str: - """Persist a run's per-URL outcomes; ``summary`` is a ``PipelineSummary``.""" + """Persist a run's per-URL outcomes; ``summary`` is a ``PipelineSummary``. + + ``backend`` is the fetcher that produced the capture (from the stored + capture, so it is set for stored/deduped/cache_hit outcomes and ``""`` + when unknown, e.g. errors) — it enables per-domain cost attribution. + """ rows = [ - {"url": o.url, "status": o.status, "reason": getattr(o, "reason", "")} + { + "url": o.url, + "status": o.status, + "reason": getattr(o, "reason", ""), + "backend": o.stored.fetcher if o.stored is not None else "", + } for o in summary.outcomes ] key = report_key(run_id, config)