"""Go-Browse (apurvaga/go-browse-wa-raw, MIT; BrowserGym trajectories on the WebArena sites, each labelled success/fail) -> laya cases, mainly for the DONE decision. python finetune/convert_gobrowse.py [shards=0-191] [keep_shards=0] Shards are streamed one at a time through the mirror (mdl, no proxy), converted and deleted. successful trajectories: every step is an action case (click/fill/press Enter/scroll/select) and the final send_msg_to_user step is a DONE case (the task really is done there -> also a noul "yes"); failed trajectories: only the state where the agent stopped is kept, as a noul-only "not done" hard negative (`noul_only`: no operation/target labels -- a failed run's actions are not known to be right). Page = the step's axtree_visible_only_txt: `[bid] role 'name', props` lines -> candidates (interactive roles) + text. """ import json, os, re, subprocess, sys from collections import defaultdict TMP = os.path.expanduser("~/data/gobrowse") INTERACTIVE = {"link", "button", "textbox", "searchbox", "combobox", "checkbox", "radio", "switch", "tab", "menuitem", "menuitemradio", "menuitemcheckbox", "option", "listbox", "spinbutton", "treeitem", "gridcell", "row"} LINE = re.compile(r"^(\t*)\[(\w+)\] (\w+) '((?:[^'\\]|\\.)*)'(.*)$") TEXT_LINE = re.compile(r"^\t*(StaticText|heading|paragraph|cell|columnheader|listitem|LabelText) '((?:[^'\\]|\\.)*)'") def parse_axtree(ax): """(url, title, elements [(bid, role, name, props)], text)""" url = title = ""; els, text = [], [] for ln in (ax or "").splitlines(): if ln.startswith("RootWebArea"): m = re.match(r"RootWebArea '((?:[^'\\]|\\.)*)'.*url='([^']*)'", ln) if m: title, url = m.group(1), m.group(2) continue m = LINE.match(ln) if m: bid, role, name, props = m.group(2), m.group(3), m.group(4), m.group(5) els.append((bid, role, name, props)) if role in ("StaticText", "heading", "paragraph", "cell", "columnheader", "listitem", "LabelText") and name.strip(): text.append(name.strip()) continue t = TEXT_LINE.match(ln) if t and t.group(2).strip(): text.append(t.group(2).strip()) return url, title, els, "\n".join(dict.fromkeys(text))[:6000] def page_obj(url, title, els, text): acts, seen = [], set() for bid, role, name, props in els: if role not in INTERACTIVE or bid in seen: continue seen.add(bid) label = name.strip() or role base = {"node": bid, "label": label[:120], "role": role} for k in ("checked", "selected", "expanded"): m = re.search(rf"{k}=(\w+)", props) if m: base[k] = m.group(1).lower() if role in ("textbox", "searchbox") or (role == "combobox" and "hasPopup" not in props): v = re.search(r"value='((?:[^'\\]|\\.)*)'", props) acts.append({**base, "id": f"fill:{bid}", "kind": "fill", "value": v.group(1) if v else "", "current_value": v.group(1) if v else ""}) acts.append({**base, "id": f"click:{bid}", "kind": "click"}) if len(acts) >= 250: break acts.append({"id": "scroll_down", "kind": "scroll", "label": "Scroll down", "delta": 560}) return {"url": url, "title": title, "text": text, "actions": acts} def gold_of(parsed, acts): """(gold_op, gold_id, kind, text) from a BrowserGym action string, or None.""" a = (parsed or "").strip() if a.startswith("send_msg_to_user") or a.startswith("report_infeasible"): return ("DONE", "DONE", "done", None) if a.startswith("send_msg_to_user") else None m = re.match(r"(click|dblclick)\('(\w+)'", a) if m and any(x["id"] == f"click:{m.group(2)}" for x in acts): return "CLICK", f"click:{m.group(2)}", "click", None m = re.match(r"fill\('(\w+)',\s*['\"](.*)['\"]\)", a, re.S) if m and any(x["id"] == f"fill:{m.group(1)}" for x in acts): return "TYPE_TEXT", f"fill:{m.group(1)}", "fill", m.group(2) if re.match(r"(press|keyboard_press)\(.*Enter", a): return "PRESS_ENTER", "press_enter", "key", None m = re.match(r"scroll\(\s*-?\d+\s*,\s*(-?\d+)", a) if m and int(m.group(1)) > 0: return "SCROLL_DOWN", "scroll_down", "scroll", None return None def hist_entry(parsed, acts): g = gold_of(parsed, acts) lab = next((x["label"] for x in acts if g and x["id"] == g[1]), (parsed or "")[:60]) return {"action": lab[:80], "kind": g[2] if g else "click", "text": g[3] if g else None, "page_changed": True} SEEN = set() def emit(out, case, stats, tag): """Write a case once: the raw data repeats trajectories, which would multiply identical states.""" import hashlib h = hashlib.md5(json.dumps([case["goal"], case["gold_op"], case.get("gold_id"), case["url"], case["page_obj"]["text"][:800], len(case["history"])], ensure_ascii=False).encode()).hexdigest() if h in SEEN: stats["dup"] += 1; return SEEN.add(h); out.write(json.dumps(case, ensure_ascii=False) + "\n"); stats[tag] += 1 def convert_shard(path, out, stats): import pyarrow.parquet as pq f = pq.ParquetFile(path) trajs = defaultdict(list) for rg in range(f.num_row_groups): for r in f.read_row_group(rg, columns=["__key__", "json"]).to_pylist(): j = r["json"]; td = j.get("traj_data") or {}; sd = j["step_data"] key = (j.get("graph_data", {}).get("root_url"), td.get("goal"), td.get("traj_num"), r["__key__"].split("-")[0]) trajs[key].append((sd.get("step_number") or 0, sd, td)) for key, steps in trajs.items(): steps.sort(key=lambda s: s[0]); td = steps[0][2] goal = (td.get("goal") or "").strip(); ok = str(td.get("success")) == "True" and float(td.get("reward") or 0) > 0 if not goal: continue stats["traj_ok" if ok else "traj_fail"] += 1 hist = [] for n, sd, _ in steps: url, title, els, text = parse_axtree(sd["obs"].get("axtree_visible_only_txt")) pg = page_obj(url, title, els, text) g = gold_of(sd.get("parsed_action"), pg["actions"]) if pg["actions"][:-1] == []: hist.append(hist_entry(sd.get("parsed_action"), pg["actions"])); continue base = {"page": -1, "url": url, "title": title, "goal": goal, "history": hist[-10:], "source": "gobrowse", "website": re.sub(r"https?://([^/:]+).*", r"\1", url) + ":" + str(key[0])[-12:], "page_obj": pg} if ok and g: if g[0] == "PRESS_ENTER": pg["actions"].insert(-1, {"id": "press_enter", "kind": "key", "label": "Press Enter in the focused text field (submit it)", "key": "Enter"}) lab = next((x["label"] for x in pg["actions"] if x["id"] == g[1]), "") emit(out, {**base, "gold_op": g[0], "gold_id": g[1], "kind": g[2], "label": lab, "gold_text": g[3]}, stats, g[0]) elif not ok and g and g[0] == "DONE": # the failed run stopped here believing it was done: a "not done" hard negative for the completion head emit(out, {**base, "gold_op": "NOT_DONE", "gold_id": None, "kind": "done", "label": "", "noul_only": True}, stats, "noul_neg") hist.append(hist_entry(sd.get("parsed_action"), pg["actions"])) def main(): out_path = sys.argv[1] lo, hi = (map(int, sys.argv[2].split("-")) if len(sys.argv) > 2 else (0, 191)) keep = len(sys.argv) > 3 and sys.argv[3] == "1" stats = defaultdict(int) with open(out_path, "a") as out: for i in range(lo, hi + 1): name = f"data/train-{i:05d}-of-00192.parquet"; path = os.path.join(TMP, name) if not os.path.exists(path): subprocess.run(["mdl", "data", "apurvaga/go-browse-wa-raw", "-i", name, "-d", TMP, "--src", "hf"], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if not os.path.exists(path): stats["shard_missing"] += 1; continue convert_shard(path, out, stats); out.flush() if not keep: os.remove(path) print(f" shard {i}: {dict(stats)}", flush=True) print("done", dict(stats), "->", out_path) if __name__ == "__main__": main()