laya-browser / code /finetune /webgym /attrib_gym.py
cklxx's picture
v19s: WebChain real-site trajectories, format v5, webgym x7 + DAgger, harness fixes; replaces v17s
454b3e6 verified
Raw History Blame Contribute Delete
6.14 kB
"""Failure attribution on HELD-OUT webgym seeds: the real agent drives (as in eval_gym), a shadow expert labels every
state the agent visits, and each failed episode is attributed to its FIRST divergence from the expert.
python finetune/webgym/attrib_gym.py [n_per_kind=10] [kinds=all] [max_steps=60]
env: ATTRIB_OUT=attrib.json (per-episode steps); S1_URL / JEV_* as in eval_gym
Category of a step = what the expert is doing there: the widget of the sub-goal's field (date:picker, count:stepper,
combo:..., toggle, choice, ...), or submit / sort / filter / page / open / done. A divergence is
wrong_target the model acts on another element than the expert
wrong_text same field, different text
premature_done the model says DONE / BLOCKED before the task is done
missed_done the task is done and the model keeps acting
The expert's action is never executed: this measures the policy as deployed, only labelled.
"""
import json, os, random, sys, time
from collections import Counter, defaultdict
sys.path.insert(0, "/home/ckl/projects/S/jev-ultrafast")
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import eval_gym as G # sets the jev env (and S1_URL override)
import expert as E
import spec as S
from dagger_gym import expert_gold
from jev_ultrafast import Agent
def category(sp, key, gold_op):
if gold_op == "DONE":
return "done"
if key is None:
return "recover"
f = next((x for x in sp.get("fields", []) if x["key"] == key), None)
if f:
return f"{f['w']}:{f.get('impl', '-')}"
for k in ("submit", "sort", "filter", "page", "open", "q", "go"):
if key.startswith(k):
return k
return key
def run(kind, seed, max_steps):
sp = S.make(kind, seed)
steps, first, status, meta = [], None, "error", None
try:
with Agent(f"{G.BASE}/t/{kind}/{seed}", sp["goal"]) as agent:
b = agent.browser
ex = E.Expert(sp, b, random.Random(seed))
while agent.state["status"] not in ("done", "blocked") and len(agent.state["history"]) < max_steps:
page = agent.state["page"]
info = E.status(b); st = info.get("st") or {}; m = info.get("meta")
if st and "open" not in st: st["open"] = []
try:
gold_op, ga, gtext, cur = expert_gold(ex, b, sp, page, st, m)
key = sp["subgoals"][cur][0] if cur is not None else None
except E.Stop as e:
gold_op, ga, gtext, key = "STOP", None, None, None
n_hist = len(agent.state["history"])
agent.command("tick")
d = agent.state["decisions"][-1]["choice"] if agent.state["decisions"] else None
if len(agent.state["history"]) == n_hist and agent.state["status"] not in ("done", "blocked"):
continue # stale / rejected decision: nothing was executed, decide again
h = agent.state["history"][-1] if len(agent.state["history"]) > n_hist else None
cat = category(sp, key, gold_op)
if gold_op == "STOP":
kind_ = None
elif gold_op == "DONE":
kind_ = None if d == "DONE" else "missed_done"
elif d in ("DONE", "BLOCKED"):
kind_ = "premature_done"
elif d != ga["id"]:
kind_ = "wrong_target"
elif gtext and h and (h.get("text") or "").strip().lower() != str(gtext).strip().lower() and cat.split(":")[0] not in ("combo",):
kind_ = "wrong_text"
else:
kind_ = None
row = {"i": len(steps), "cat": cat, "gold": gold_op, "gold_label": (ga or {}).get("label", "")[:60], "gold_text": gtext,
"model": d, "model_label": (h or {}).get("action", d)[:60] if h else d, "model_text": (h or {}).get("text"), "div": kind_}
steps.append(row)
if kind_ and first is None:
first = row
status = agent.state["status"]
time.sleep(0.8)
meta = json.loads(b.evaluate("JSON.stringify(window.__gym ? window.__gym.meta : null)") or "null")
except Exception as e:
status = f"error:{type(e).__name__}:{str(e)[:60]}"
ok = G.success(sp, "", meta)
return {"kind": kind, "seed": seed, "ok": ok, "status": status, "steps": steps, "first": first, "goal": sp["goal"]}
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 10
kinds = (sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] != "all" else ",".join(S.KINDS)).split(",")
max_steps = int(sys.argv[3]) if len(sys.argv) > 3 else 60
rows = []
for kind in kinds:
for i in range(n):
r = run(kind, 900000 + 10 * i, max_steps); rows.append(r)
f = r["first"] or {}
print(f"{'PASS' if r['ok'] else 'FAIL'} {kind:10s} {r['seed']} steps={len(r['steps']):2d} {r['status'][:20]:20s} "
f"first={f.get('div')}@{f.get('cat')} gold='{f.get('gold_label', '')[:30]}' model='{str(f.get('model_label'))[:30]}'", flush=True)
json.dump(rows, open(os.environ.get("ATTRIB_OUT", "/tmp/attrib.json"), "w"), indent=1)
fails = [r for r in rows if not r["ok"]]
print(f"== success {len(rows) - len(fails)}/{len(rows)}")
print("== failed episodes by FIRST divergence (category / kind):")
for (c, k), v in Counter(((r["first"] or {}).get("cat", "none"), (r["first"] or {}).get("div", "none")) for r in fails).most_common():
print(f" {v:3d} {c:22s} {k}")
agg = defaultdict(lambda: [0, 0])
for r in rows:
for s in r["steps"]:
if s["gold"] != "STOP":
agg[s["cat"]][0] += s["div"] is None; agg[s["cat"]][1] += 1
print("== per-step agreement with the expert, by category:")
for c, (a, t) in sorted(agg.items(), key=lambda x: x[1][0] / max(1, x[1][1])):
print(f" {c:22s} {a:4d}/{t:<4d} {a / max(1, t):.2f}")
if __name__ == "__main__":
main()