File size: 4,672 Bytes
d2a6765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b8930b3
d2a6765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa6cf50
 
 
 
 
 
 
 
 
 
d2a6765
 
 
b8930b3
 
 
 
 
 
 
 
 
 
 
f1a9398
 
 
 
 
 
 
 
 
 
d2a6765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa6cf50
d2a6765
 
 
 
 
 
 
 
 
 
 
f1a9398
b8930b3
 
 
 
f1a9398
d2a6765
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""Command-line entry point for the two-phase evaluation harness.

Usage::

    # Phase 1 -- runs the model, spends quota; start with a small slice.
    uv run python -m eval.run_eval predict --dataset sroie --limit 20

    # Phase 2 -- offline; recompute metrics and sweep as often as you like.
    uv run python -m eval.run_eval score --dataset sroie

The predict phase caches results under ``eval/cache/<dataset>/`` and is
idempotent (already-cached ids are skipped unless ``--overwrite``). The score
phase reads that cache and prints the metrics tables and threshold sweep; it
never re-runs inference.
"""

from __future__ import annotations

import argparse
import logging
import sys
from pathlib import Path

from eval.cache import DEFAULT_CACHE_BASE
from eval.predict import run_predict
from eval.score import build_report, format_report
from eval.splits import SPLIT_NAMES


def _add_common(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--dataset", default="sroie", help="Dataset adapter name (default: sroie).")
    parser.add_argument(
        "--cache-base",
        type=Path,
        default=DEFAULT_CACHE_BASE,
        help="Root cache directory (default: eval/cache).",
    )


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="eval.run_eval", description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)

    predict = subparsers.add_parser("predict", help="Run the model over a slice and cache results.")
    _add_common(predict)
    predict.add_argument(
        "--limit",
        type=int,
        default=20,
        help="Number of examples to process (the held-out slice size; default: 20).",
    )
    predict.add_argument(
        "--overwrite",
        action="store_true",
        help="Re-process and overwrite already-cached examples.",
    )
    predict.add_argument(
        "--retry-errors",
        action="store_true",
        help=(
            "Re-predict ONLY cached entries whose 'error' field is set, leaving "
            "every successful prediction byte-identical. Use after a quota or "
            "network outage. Unlike --overwrite this never re-runs a document "
            "that already succeeded, so frozen predictions stay comparable."
        ),
    )

    score = subparsers.add_parser("score", help="Compute metrics + sweep from the cache (offline).")
    _add_common(score)
    score.add_argument(
        "--split",
        choices=SPLIT_NAMES,
        default="all",
        help=(
            "Which cached documents to score: 'tuning' (the ids pinned in "
            "eval/splits/<dataset>_tuning.json, used to fit the operating point), "
            "'heldout' (everything else), or 'all'. Report tuning and held-out "
            "separately; a combined number is contaminated by the tuning slice."
        ),
    )
    score.add_argument(
        "--revalidate",
        action="store_true",
        help=(
            "Score under CURRENT validation/scoring rules by recomputing them from the "
            "cached predicted documents, instead of the scalars frozen in at predict "
            "time. Still offline (no API calls). Drift between the two is reported "
            "either way."
        ),
    )

    return parser


def main(argv: list[str] | None = None) -> int:
    """Run the CLI.

    Args:
        argv: Argument list (defaults to ``sys.argv[1:]``).

    Returns:
        Process exit code (0 on success).
    """
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )
    args = _build_parser().parse_args(argv)

    if args.command == "predict":
        stats = run_predict(
            args.dataset,
            args.limit,
            cache_base=args.cache_base,
            overwrite=args.overwrite,
            retry_errors=args.retry_errors,
        )
        print(
            f"\nPredict complete for {stats.dataset}: "
            f"processed={stats.processed} skipped={stats.skipped} "
            f"accepted={stats.accepted} review={stats.review} errors={stats.errors} "
            f"failed={stats.failed}\n"
            f"Now run: uv run python -m eval.run_eval score --dataset {stats.dataset}"
        )
        return 0

    if args.command == "score":
        report = build_report(
            args.dataset,
            cache_base=args.cache_base,
            revalidate=args.revalidate,
            split=args.split,
        )
        print(format_report(report))
        return 0

    return 1  # pragma: no cover -- argparse enforces a valid subcommand.


if __name__ == "__main__":
    sys.exit(main())