File size: 5,140 Bytes
22de727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Per-skill results of MiniWeb runs: task success, then the share of primitive instances passed per skill family and
per primitive, by the VLM judge and by the task verifiers, and optionally the failed instances with the judge's reason.

A run is an output folder of `python -m miniwebagent.evaluate --out <run>`: results.jsonl (or results_regraded.jsonl)
holds each task's verifier verdicts, judge.jsonl the judge's (python src/miniweb/evaluation/judge_runs.py --runs <run>).

    python scripts/skill_report.py runs/my_agent
    python scripts/skill_report.py runs/agent_a runs/agent_b --by primitive
    python scripts/skill_report.py runs/my_agent --failures 20 --family "Drag & gesture"
"""
import argparse, ast, collections, json, os, sys

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src"))
from miniwebgen.viewer import FAMILIES  # noqa: E402

FAMILY = {m: f for f, ms in FAMILIES.items() for m in ms}
ORDER = list(FAMILIES)
FINAL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "final_set.yaml")


def final_primitives():
    """The paper's 37 primitives (data/final_set.yaml); instances of the 9 dropped ones are left out by default."""
    import yaml
    return set(yaml.safe_load(open(FINAL))["primitives"]) if os.path.exists(FINAL) else None


def rows(path):
    return [json.loads(line) for line in open(path) if line.strip()] if os.path.exists(path) else []


def load(run):
    res = rows(os.path.join(run, "results_regraded.jsonl")) or rows(os.path.join(run, "results.jsonl"))
    verifier = {}
    for r in res:
        d = r.get("detail")
        d = ast.literal_eval(d) if isinstance(d, str) and d.startswith("{") else (d if isinstance(d, dict) else {})
        verifier[r["task_id"]] = (bool(r.get("ok")), {k: bool(v) for k, v in d.items()})
    judge = {}
    for r in rows(os.path.join(run, "judge.jsonl")):
        inst = {k: (bool(v.get("passed")), v.get("why", "")) for k, v in (r.get("instances") or {}).items()}
        judge[r["task_id"]] = (bool(r.get("passed")), inst)
    return verifier, judge


def pct(a, n):
    return f"{100 * a / n:5.1f}%" if n else "    -"


def main():
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("runs", nargs="+")
    ap.add_argument("--by", choices=["family", "primitive"], default="family")
    ap.add_argument("--failures", type=int, default=0, help="list this many failed instances (judge's reason)")
    ap.add_argument("--family", help="with --failures: only this family")
    ap.add_argument("--all-primitives", action="store_true",
                    help="also count the 9 primitives dropped from the final set (the paper's tables leave them out)")
    a = ap.parse_args()
    keep = None if a.all_primitives else final_primitives()
    for run in a.runs:
        verifier, judge = load(run)
        tasks = sorted(set(verifier) | set(judge))
        print(f"\n{run}: {len(tasks)} tasks")
        if judge:
            print(f"  task success, judge     {pct(sum(p for p, _ in judge.values()), len(judge))}  ({len(judge)} judged)")
        if verifier:
            print(f"  task success, verifiers {pct(sum(p for p, _ in verifier.values()), len(verifier))}  ({len(verifier)} graded)")
        stat = collections.defaultdict(lambda: [0, 0, 0, 0])     # key -> judge pass, judge n, verifier pass, verifier n
        fails = []
        for t in tasks:
            for inst, (ok, why) in (judge.get(t, (None, {}))[1]).items():
                m = inst.split("#")[0]
                if keep and m not in keep:
                    continue
                key = FAMILY.get(m, "Other") if a.by == "family" else m
                stat[key][0] += ok; stat[key][1] += 1
                if not ok:
                    fails.append((t, inst, FAMILY.get(m, "Other"), why))
            for inst, ok in (verifier.get(t, (None, {}))[1]).items():
                m = inst.split("#")[0]
                if keep and m not in keep:
                    continue
                key = FAMILY.get(m, "Other") if a.by == "family" else m
                stat[key][2] += ok; stat[key][3] += 1
        keys = [k for k in ORDER if k in stat] + sorted(k for k in stat if k not in ORDER) if a.by == "family" else \
            sorted(stat, key=lambda k: (stat[k][0] / stat[k][1]) if stat[k][1] else 2)
        print(f"  {'instances passed by ' + a.by:34s} {'n':>5s}  {'judge':>6s}  {'verifier':>8s}")
        for k in keys:
            jp, jn, vp, vn = stat[k]
            print(f"  {k:34s} {max(jn, vn):5d}  {pct(jp, jn):>6s}  {pct(vp, vn):>8s}")
        jp, jn, vp, vn = (sum(v[i] for v in stat.values()) for i in range(4))
        print(f"  {'all instances':34s} {max(jn, vn):5d}  {pct(jp, jn):>6s}  {pct(vp, vn):>8s}")
        if a.failures:
            shown = [f for f in fails if not a.family or f[2] == a.family][:a.failures]
            print(f"\n  failed instances{' (' + a.family + ')' if a.family else ''}: {len(shown)} shown")
            for t, inst, fam, why in shown:
                print(f"  - {t} / {inst} [{fam}]: {why[:160]}")


if __name__ == "__main__":
    main()