Download tools/evaluate_mind2web.py from INLEVEL9/Vons: direct link, hf CLI and curl.
- Browser
- Download file 7.97 kB
-
https://huggingface.co/INLEVEL9/Vons/resolve/main/tools/evaluate_mind2web.py
- Command line
-
hf download hf://INLEVEL9/Vons/tools/evaluate_mind2web.py
-
curl -L -o evaluate_mind2web.py https://huggingface.co/INLEVEL9/Vons/resolve/main/tools/evaluate_mind2web.py
7.97 kB
| """Run the restricted Mind2Web candidate/selection evaluation. | |
| The input rows must be normalized JSONL rows accepted by | |
| ``Mind2WebExample.from_mapping``. Predictions are intentionally a separate | |
| JSONL file so retrieval misses and missing selections remain observable. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| from collections.abc import Mapping, Sequence | |
| from pathlib import Path | |
| from typing import Any, NamedTuple | |
| from vons.mind2web import Mind2WebExample, evaluate_mind2web, task_bootstrap_intervals | |
| def _read_jsonl(path: Path) -> list[dict[str, Any]]: | |
| rows: list[dict[str, Any]] = [] | |
| with path.open(encoding="utf-8") as handle: | |
| for line_number, line in enumerate(handle, start=1): | |
| if not line.strip(): | |
| continue | |
| value = json.loads(line) | |
| if not isinstance(value, Mapping): | |
| raise TypeError(f"{path}:{line_number}: expected a JSON object") | |
| rows.append(dict(value)) | |
| return rows | |
| def _file_sha256(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def _string_sequence(value: Any, label: str) -> tuple[str, ...]: | |
| if isinstance(value, (str, bytes, bytearray)) or not isinstance(value, Sequence): | |
| raise TypeError(f"{label} must be a JSON list") | |
| values: list[str] = [] | |
| for item in value: | |
| candidate = item.get("id") if isinstance(item, Mapping) else item | |
| if not isinstance(candidate, str) or not candidate: | |
| raise TypeError(f"{label} ids must be non-empty strings") | |
| values.append(candidate) | |
| if len(values) != len(set(values)): | |
| raise ValueError(f"{label} ids must be unique") | |
| return tuple(values) | |
| class PredictionBundle(NamedTuple): | |
| generated_candidates: dict[str, tuple[str, ...]] | |
| selections: dict[str, str | None] | |
| declared_k: int | None | |
| def load_predictions(path: Path) -> PredictionBundle: | |
| generated_candidates: dict[str, tuple[str, ...]] = {} | |
| selections: dict[str, str | None] = {} | |
| declared_ks: set[int] = set() | |
| missing_k = False | |
| for row in _read_jsonl(path): | |
| example_id = row.get("id") | |
| if not isinstance(example_id, str) or not example_id: | |
| raise ValueError(f"{path}: prediction requires id") | |
| key = example_id | |
| if key in generated_candidates: | |
| raise ValueError(f"{path}: duplicate prediction id {key}") | |
| if "generated_candidates" not in row: | |
| raise ValueError(f"{path}: prediction requires generated_candidates") | |
| generated_candidates[key] = _string_sequence(row["generated_candidates"], "generated_candidates") | |
| selection = row.get("selection", row.get("selected_id")) | |
| selections[key] = None if selection in (None, "") else str(selection) | |
| row_k = row.get("k") | |
| if row_k is None: | |
| missing_k = True | |
| elif isinstance(row_k, bool) or not isinstance(row_k, int) or row_k <= 0: | |
| raise ValueError(f"{path}: prediction k must be a positive integer") | |
| else: | |
| declared_ks.add(row_k) | |
| if declared_ks and missing_k: | |
| raise ValueError(f"{path}: every prediction row must declare k when any row does") | |
| if len(declared_ks) > 1: | |
| raise ValueError(f"{path}: prediction rows must use one k, found {sorted(declared_ks)}") | |
| return PredictionBundle( | |
| generated_candidates=generated_candidates, | |
| selections=selections, | |
| declared_k=next(iter(declared_ks), None), | |
| ) | |
| def evaluate_files( | |
| rows_path: Path, | |
| predictions_path: Path, | |
| ks: Sequence[int], | |
| split: str | None = None, | |
| ) -> dict[str, Any]: | |
| raw_rows = _read_jsonl(rows_path) | |
| if split is not None: | |
| raw_rows = [row for row in raw_rows if row.get("split") == split] | |
| examples = [Mind2WebExample.from_mapping(row) for row in raw_rows] | |
| if len({example.example_id for example in examples}) != len(examples): | |
| raise ValueError(f"{rows_path}: duplicate gold row ids") | |
| predictions = load_predictions(predictions_path) | |
| generated_candidates = predictions.generated_candidates | |
| example_ids = {row.example_id for row in examples} | |
| unknown_prediction_ids = sorted(set(generated_candidates) - example_ids) | |
| if unknown_prediction_ids: | |
| raise ValueError(f"predictions contain unknown example ids: {unknown_prediction_ids[:5]}") | |
| if not ks or any(k <= 0 for k in ks): | |
| raise ValueError("ks must contain positive integers") | |
| if len(set(ks)) != len(ks): | |
| raise ValueError("ks must not contain duplicates") | |
| if predictions.declared_k is not None and tuple(ks) != (predictions.declared_k,): | |
| raise ValueError( | |
| f"predictions are k={predictions.declared_k}; evaluate exactly that single k, not {list(ks)}" | |
| ) | |
| selection_is_k_specific = predictions.declared_k is not None or len(ks) == 1 | |
| selections = predictions.selections if selection_is_k_specific else {} | |
| metric_mappings: list[dict[str, Any]] = [] | |
| bootstrap_by_k: dict[str, dict[str, Any]] = {} | |
| for k in ks: | |
| metric = evaluate_mind2web(examples, generated_candidates, selections, k=k) | |
| mapping = metric.to_mapping() | |
| if not selection_is_k_specific: | |
| mapping["selection_accuracy_given_recall"] = None | |
| mapping["complete_case_selection_accuracy_given_recall"] = None | |
| mapping["selection_metrics_status"] = "not_evaluated_recall_only" | |
| metric_mappings.append(mapping) | |
| bootstrap = task_bootstrap_intervals(examples, generated_candidates, selections, k=k) | |
| if not selection_is_k_specific: | |
| bootstrap["selection_accuracy_given_recall_task_macro_ci95"] = None | |
| bootstrap_by_k[str(k)] = bootstrap | |
| return { | |
| "schema": "vons.mind2web-evaluation/v1", | |
| "rows_path": str(rows_path), | |
| "predictions_path": str(predictions_path), | |
| "rows": len(examples), | |
| "prediction_rows": len(generated_candidates), | |
| "split": split, | |
| "input_sha256": { | |
| "rows": _file_sha256(rows_path), | |
| "predictions": _file_sha256(predictions_path), | |
| }, | |
| "prediction_k": predictions.declared_k, | |
| "metrics_by_k": metric_mappings, | |
| "task_bootstrap_by_k": bootstrap_by_k, | |
| "selection_evaluation": { | |
| "ranking_and_selection_predictions_supplied_once": True, | |
| "selection_reused_across_k": False, | |
| "selection_metrics_valid": selection_is_k_specific, | |
| "interpretation": ( | |
| "Selection is evaluated only at the declared single prediction k. " | |
| "When multiple k values are requested without a declared k, metrics are recall-only; " | |
| "separate k-specific prediction files are required for selection." | |
| ), | |
| }, | |
| "scope": "candidate recall and candidate-in-set selection only; no browser task success", | |
| } | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--rows", type=Path, required=True, help="normalized Mind2Web rows JSONL") | |
| parser.add_argument("--predictions", type=Path, required=True, help="retriever/selector predictions JSONL") | |
| parser.add_argument("--output", type=Path, required=True, help="evaluation report JSON") | |
| parser.add_argument("--split", help="evaluate only rows whose normalized split matches this value") | |
| parser.add_argument("--k", type=int, nargs="+", default=[5, 10, 20, 32]) | |
| args = parser.parse_args() | |
| report = evaluate_files(args.rows, args.predictions, args.k, split=args.split) | |
| args.output.parent.mkdir(parents=True, exist_ok=True) | |
| args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") | |
| if __name__ == "__main__": | |
| main() | |