File size: 9,409 Bytes
adf912b
 
 
 
8086c56
454b3e6
adf912b
 
454b3e6
adf912b
 
 
454b3e6
adf912b
8086c56
 
454b3e6
 
 
 
 
adf912b
 
 
 
 
 
 
 
 
 
 
 
 
8086c56
 
 
 
 
 
 
 
454b3e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
adf912b
8086c56
 
 
 
adf912b
 
 
 
8086c56
 
adf912b
 
454b3e6
 
 
 
 
 
 
 
 
 
 
 
adf912b
 
 
454b3e6
adf912b
 
 
 
 
 
 
8086c56
 
 
 
 
 
 
 
 
 
 
adf912b
 
 
 
 
454b3e6
adf912b
8086c56
adf912b
8086c56
 
 
 
 
adf912b
454b3e6
 
 
 
 
 
 
 
 
 
adf912b
 
 
8086c56
adf912b
 
454b3e6
adf912b
 
 
 
 
 
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
143
144
145
"""cases.jsonl + pages.jsonl -> tokenized training items (train split) and eval cases (held-out pages).

    python finetune/build_items.py out/pages.jsonl out/cases.jsonl out/
"""
import json, random, os, sys
from array import array
import torch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common_ft import build_request, gold_for, goal_done_question
from transformers import AutoTokenizer
from laya.common import QTYPES, build_sequence, render_options

MAX_LEN, HEAD_MAX_LEN = int(os.environ.get('LAYA_MAXLEN', '1024')), int(os.environ.get('LAYA_HEAD', '512'))
EVAL_EVERY = 5   # pages with index % 5 == 0 are held out
MAX_TARGETS = int(os.environ.get('MAX_TARGETS', '40'))
FINAL_P = float(os.environ.get('FINAL_P', '0.2'))   # share of target items built like the server's final round
# NOUL=1: also emit the yes/no question "is the whole goal visibly done on this page?" (common_ft.GOAL_DONE).
# yes = states whose gold is DONE; no = every mid-trajectory state (incl. webgym's results-shown-but-filter-missing
# states).  NOUL_NEG_P keeps the negatives at roughly 60:40 (no-heavy, to counter the DONE-too-early bias).
NOUL = os.environ.get('NOUL') == '1'
NOUL_NEG_P = float(os.environ.get('NOUL_NEG_P', '0.3'))


def main():
    pages_f, cases_f, out = sys.argv[1:4]
    pages = [json.loads(l) for l in open(pages_f)]
    cases = [json.loads(l) for l in open(cases_f)]
    for extra in sys.argv[4:]:
        cases += [json.loads(l) for l in open(extra)]
    snap = os.environ.get("LAYA_BASE")
    tok = AutoTokenizer.from_pretrained(os.path.join(snap, "tokenizer"))
    items, n_skip, ev = [], 0, []
    import hashlib
    STOPS = [" Stop when it is open.", " Stop once that page is visible.", " Then stop.", " Finish when it has loaded.", ""]
    # evaluation hygiene: never train on a suite task's start page (suite A and B start URLs), whatever the source
    import re as _re
    def _norm(u): return (u or "").split("#")[0].rstrip("/").replace("://www.", "://")
    suite_urls = set()
    for f in ("apps/browser_suite.py", "apps/browser_suite_b.py"):
        if os.path.exists(f):
            suite_urls |= {_norm(u) for u in _re.findall(r'\("[a-z0-9-]+", "(https?://[^"]+)"', open(f).read())}
    n_suite = 0
    if os.environ.get("LAYA_FMT") == "v6":
        # v6 history carries each action's result = the page the NEXT step starts on (same trajectory, one more step)
        traj = lambda c: (c.get("source"), c.get("task_id") or c.get("goal"), c.get("website"))
        at = {}
        for c in cases:
            pg = c.get("page_obj") or (pages[c["page"]] if c.get("page", -1) >= 0 else {})
            at.setdefault((traj(c), len(c.get("history") or [])), (pg.get("url") or c.get("url"), pg.get("title") or c.get("title")))
        filled = 0
        for c in cases:
            for i, h in enumerate(c.get("history") or []):
                if not h.get("url") and (traj(c), i + 1) in at:
                    h["url"], h["title"] = at[(traj(c), i + 1)]; filled += 1
        print("v6: history steps given their result page:", filled, flush=True)
    keep = float(os.environ.get("CASE_KEEP", "1"))   # CASE_KEEP=p: build from a random p of the cases (smaller, faster)
    if keep < 1:
        cases = [c for c in cases if int(hashlib.md5((c["goal"] + str(len(c.get("history") or []))).encode()).hexdigest()[:8], 16) / 0xffffffff < keep]
        print("CASE_KEEP", keep, "->", len(cases), "cases", flush=True)
    for c in cases:
        if _norm(c.get("url") or (c.get("page_obj") or {}).get("url")) in suite_urls or (c.get("page", -1) >= 0 and _norm(pages[c["page"]]["url"]) in suite_urls):
            n_suite += 1; continue
        if c.get("source") == "dagger":
            continue
        if c["kind"] == "done" and "page_obj" not in c:
            continue  # template DONE cases leak phrasing; real ones come from make_done_cases.py
        page = c.get("page_obj") or pages[c["page"]]
        h = int(hashlib.md5(c["goal"].encode()).hexdigest(), 16)
        is_sub = str(c.get("skill", "")).endswith("@sub") or c.get("skill") == "subgoal_done"
        goal = c["goal"] + ("" if is_sub or c.get("source") == "webgym" else STOPS[h % len(STOPS)])
        c = {**c, "goal": goal}
        state, questions, targets, controls = build_request(page, goal, c.get("history", []))
        if c.get("noul_only"):
            # a state where a failed run wrongly stopped: only the completion question, answered "not done"
            if NOUL and not ((hashlib.md5(c["website"].encode()).digest()[0] % EVAL_EVERY == 0)):
                nq = goal_done_question(goal)
                qq = {"t": "noul", "ins": json.dumps(nq["instructions"]), "crit": None}
                seq, markers = build_sequence(tok, state, qq, MAX_LEN, HEAD_MAX_LEN)
                if len(markers) == 2:
                    items.extend([{"ids": array("i", seq), "markers": markers, "qtype": QTYPES["noul"], "target": [1.0, 0.0], "label": 0,
                                   "qid": "goal_done", "gold_op": "NOT_DONE", "src": c.get("source")}] * 2)
            elif NOUL:
                ev.append({**c, "gold_index": None})
            continue
        gop, gidx = gold_for(c, targets, controls)
        if gop is None:
            n_skip += 1; continue
        held = (hashlib.md5(c["website"].encode()).digest()[0] % EVAL_EVERY == 0) if c.get("source") in ("mind2web", "nnetnav", "webchain", "gobrowse") else (c["page"] % EVAL_EVERY == 0)
        if held:
            ev.append({**c, "gold_index": gidx}); continue
        golds = {"operation": gop}
        if gidx is not None:
            golds[gop.lower() + "_target"] = gidx
        for qid, gold in golds.items():
            q = questions[qid]; keys = list(q["criteria"])
            rs = random.Random(h + len(keys) + len(qid))
            if qid != "operation" and len(keys) > 8 and rs.random() < FINAL_P:
                width = rs.randint(2, 6)          # the server's final round: only the chunk winners (2-6 options)
            else:
                width = MAX_TARGETS
            if qid != "operation" and len(keys) > width:
                # inference splits wide choices into chunks of <= MAX_TARGETS (systemone_server MAXOPT); train on the same
                # widths, so option labels are never truncated to a few tokens: gold + a random sample, page order kept
                keep = set(rs.sample([k for k in keys if k != gold], width - 1)) | {gold}
                keys = [k for k in keys if k in keep]
                q = {**q, "criteria": {k: q["criteria"][k] for k in keys}}
            target = [1.0 if k == gold else 0.0 for k in keys]
            qq = {"t": "choice", "ins": json.dumps(q["instructions"]), "crit": q["criteria"]}
            seq, markers = build_sequence(tok, state, qq, MAX_LEN, HEAD_MAX_LEN)
            if len(markers) != len(render_options(qq)):
                n_skip += 1; continue
            item = {"ids": array("i", seq), "markers": markers, "qtype": QTYPES["choice"], "target": target, "label": keys.index(gold), "qid": qid, "gold_op": gop, "src": c.get("source")}
            # class balance: CLICK dominates the operation question, so repeat the rare operations
            reps = {"DONE": 4, "TYPE_TEXT": 3, "SELECT": 3, "PRESS_ENTER": 4, "SCROLL_DOWN": 2}.get(gop, 1) if qid == "operation" else 1
            if c.get("source") == "dagger":
                reps *= 5
            if c.get("source") == "webgym" or str(c.get("skill", "")).endswith("@sub"):
                reps = 1      # webgym already pairs every DONE with its "keep going" twin; no op-level oversampling
            if c.get("source") == "rollout":
                reps *= 3   # scripted scroll / search-submit / select trajectories: the skills the model lacked   # on-policy teacher corrections from real tasks: few but exactly where the policy fails
            items.extend([item] * reps)
        if NOUL:
            yes = gop == "DONE"
            rn = random.Random(h + 7)
            if yes or rn.random() < NOUL_NEG_P:
                nq = goal_done_question(goal)
                qq = {"t": "noul", "ins": json.dumps(nq["instructions"]), "crit": None}
                seq, markers = build_sequence(tok, state, qq, MAX_LEN, HEAD_MAX_LEN)
                if len(markers) == 2:
                    items.append({"ids": array("i", seq), "markers": markers, "qtype": QTYPES["noul"], "target": [0.0, 1.0] if yes else [1.0, 0.0],
                                  "label": int(yes), "qid": "goal_done", "gold_op": gop, "src": c.get("source")})
    torch.save(items, os.path.join(out, "train_items.pt"))
    with open(os.path.join(out, "eval_cases.jsonl"), "w") as f:
        for c in ev: f.write(json.dumps(c, ensure_ascii=False) + "\n")
    print(f"dropped {n_suite} cases on suite start pages; dagger cases are never used")
    lens = [len(i["ids"]) for i in items]
    import collections
    print("goal_done (noul) items:", collections.Counter(i["label"] for i in items if i["qid"] == "goal_done"))
    print("operation label counts:", dict(collections.Counter(i["gold_op"] for i in items if i["qid"] == "operation")))
    print(f"train items {len(items)} (op {sum(i['qid']=='operation' for i in items)}, target {sum(i['qid']!='operation' for i in items)}), "
          f"eval cases {len(ev)}, skipped {n_skip}, seq len mean {sum(lens)/len(lens):.0f} max {max(lens)}")

if __name__ == "__main__":
    main()