Spaces:
Running
Running
File size: 9,851 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 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 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """Element side file β what every recorded action targeted. PRIVILEGED: it sits next to a
trajectory for analysis/debugging and is never exported (export.py never reads it).
episodes/<task>/a<n>/elements.json every attempt (kept or failure pool), by recording index
accepted/<task>/elements.json aligned with trajectory.json: `history[k]` <-> history[k],
`steps[k]` <-> steps[k] (same `i` and `screenshot`)
One entry per step (FORMAT miniwebgen.elements/v1):
i, span, screenshot, type recording index, prefix|target, frame before the action, action type
element the targeted element, or for key/type the focused one:
role_in_action target | focused | scroll_at | scroll_target | drag_source
selector the dom query the script used to find it ({css, text, tag}), if any
css_path unique CSS path computed at action time
tag, role, accessible_name (ARIA: aria-labelledby/aria-label/<label>/alt/title/text),
label, context_label (a short heading/label just before it, when not programmatic),
text (trimmed), id, name, type
box [x, y, w, h] viewport pixels just before the action
url page URL before the action
point the actual (jittered) mouse point, or null
scroll {window: [sx, sy], container: {css_path, scroll_top, scroll_left} | null}
at_point element under `point` when it is not the target itself (else null)
select for <select> targets: {before, after: {index, value, label}, reloaded}
drag {start, end, end_point, waypoints} for drags
Top level: format, complete (false for backfills), missing (what a backfill could not recover),
source (recorded | backfill).
"""
from __future__ import annotations
import json
from pathlib import Path
FORMAT = "datagen.elements/v1" # a stored-data format id (kept on the 2026-10-08 move)
_CTX_KEYS = ("element", "url", "point", "scroll", "at_point", "select", "drag")
def entries_from_steps(steps):
out = []
for s in steps:
ctx = s.get("el") or {}
out.append({"i": s["i"], "span": s.get("span", "target"), "screenshot": s["screenshot"], "type": s["type"],
**{k: ctx.get(k) for k in _CTX_KEYS}})
return out
def write_attempt(attempt_dir, steps):
"""elements.json for one executed attempt (written by the executor)."""
doc = {"format": FORMAT, "source": "recorded", "complete": True, "missing": [],
"steps": entries_from_steps(steps)}
(Path(attempt_dir) / "elements.json").write_text(json.dumps(doc, indent=1, ensure_ascii=False, default=str))
return doc
def for_trajectory(attempt_doc, traj):
"""Split an attempt's element entries into history/steps aligned with trajectory.json."""
by_i = {e["i"]: e for e in attempt_doc["steps"]}
out = {"format": FORMAT, "id": traj["id"], "source": attempt_doc.get("source"),
"complete": attempt_doc.get("complete", False), "missing": attempt_doc.get("missing", [])}
for part in ("history", "steps"):
rows = []
for st in traj[part]:
e = dict(by_i.get(st["i"]) or {"i": st["i"], "missing": True})
e["screenshot"] = st["screenshot"] # the trajectory's own frame path
rows.append(e)
out[part] = rows
return out
# ββ backfill from what older runs logged (no browser, no LLM) ββββββββββββββββ
MISSING_V0 = [
"selector: the script's dom query was not logged β css_path is inferred from the task spec only where the "
"logged tag/name/id/text unambiguously match the target control or its apply button, else null",
"scroll: window/container scroll position was not logged",
"at_point: the element under the jittered click point was not logged (the hit-test only guaranteed it was the "
"target or its descendant)",
"accessible_name: approximated by the logged label (or text for non-selects); context_label for a select "
"comes from the site map's label of the matched control",
"element for scroll steps: the scroll target was not logged (element is null)",
"element for key/type steps: the focused element was not logged β inferred as the <select> clicked just before "
"in the same span, else null",
"select before/after: inferred β before from the select's logged text at the opening click, after (on the "
"committing Enter) from the task spec, confirmed against the server log",
"text: logged trimmed to 80 characters",
]
def _span_spec(task, span):
if span == "prefix" and task["start"].get("prefix"):
pre = task["start"]["prefix"]
return pre["control"], pre["option"]
return task["control"], task["option"]
def _opt_by_label(ctrl, label):
for o in ctrl.get("options") or []:
if (o.get("text") or "").strip() == (label or "").strip():
return {"index": o["index"], "value": o["value"], "label": o["text"]}
return {"index": None, "value": None, "label": label} if label else None
def _confirmed(entries, param, value):
return any(str((e.get("query") or {}).get(param, "")).strip().casefold() == str(value).strip().casefold()
or (isinstance(e.get("body"), dict) and str(e["body"].get(param, "")).strip().casefold()
== str(value).strip().casefold()) for e in entries)
def _infer_css(tgt, ctrl):
tag = tgt.get("tag")
if tag == "select" and ctrl.get("kind") == "select":
css = ctrl.get("css") or ""
if (tgt.get("id") and tgt["id"] in css) or (tgt.get("name") and tgt["name"] in css) or \
(tgt.get("label") and tgt.get("label") == ctrl.get("label")):
return css
b = ctrl.get("apply_button") if ctrl.get("apply") == "button" else None
if b and tag in ("button", "input") and (tgt.get("text") or "").strip() == (b.get("text") or "").strip():
return b.get("css")
return None
def backfill_attempt(attempt_dir, task):
"""Best-effort elements.json from an attempt recorded before element capture existed.
Uses only attempt.json (logged targets, points, urls, drag paths), the task spec and the
server log. complete=false; `missing` lists what could not be recovered."""
d = Path(attempt_dir)
rec = json.loads((d / "attempt.json").read_text())
log = json.loads((d / "server_log.json").read_text()) if (d / "server_log.json").exists() else []
rows, last_select = [], {}
for s in rec.get("steps", []):
span = s.get("span", "target")
ctrl, option = _span_spec(task, span)
tgt = s.get("target") or {}
row = {"i": s["i"], "span": span, "screenshot": s["screenshot"], "type": s["type"],
"element": None, "url": s.get("url"), "point": [s["x"], s["y"]] if "x" in s and s["type"] != "key" else None,
"scroll": None, "at_point": None, "select": None, "drag": None}
if tgt:
css = _infer_css(tgt, ctrl)
is_select = tgt.get("tag") == "select" # a select's logged text is its selected option, not a name
row["element"] = {"role_in_action": "scroll_target" if s["type"] == "scroll" else "target",
"selector": None, "css_path": css, "css_path_inferred": bool(css),
"tag": tgt.get("tag"), "role": tgt.get("role") or "",
"accessible_name": tgt.get("label") or ("" if is_select else tgt.get("text") or ""),
"label": tgt.get("label") or "",
"context_label": (ctrl.get("label") or "") if (is_select and css == ctrl.get("css")) else "",
"text": tgt.get("text") or "",
"id": tgt.get("id") or "", "name": tgt.get("name") or "", "type": "",
"box": tgt.get("box") or []}
if tgt.get("tag") == "select" and s["type"] in ("click", "double_click"):
row["select"] = {"before": _opt_by_label(ctrl, tgt.get("text")), "after": None, "inferred": True}
last_select[span] = (row["element"], s.get("url"))
elif s["type"] in ("click", "double_click"):
last_select.pop(span, None)
elif s["type"] in ("key", "type") and span in last_select and last_select[span][1] == s.get("url"):
el = dict(last_select[span][0], role_in_action="focused", inferred=True)
row["element"] = el
if s["type"] == "key" and s.get("key") == "Enter":
param = (ctrl.get("signature") or {}).get("param")
row["select"] = {"before": None, "after": {"index": option["index"], "value": option["value"],
"label": option["text"]},
"inferred": True, "confirmed_by_server_log": bool(param and _confirmed(log, param, option["value"]))}
last_select.pop(span, None)
if s["type"] == "drag":
row["drag"] = {"start": row["element"], "end": None, "end_point": [s.get("x2"), s.get("y2")],
"waypoints": s.get("path") or []}
rows.append(row)
doc = {"format": FORMAT, "source": "backfill", "complete": False, "missing": MISSING_V0, "steps": rows}
(d / "elements.json").write_text(json.dumps(doc, indent=1, ensure_ascii=False, default=str))
return doc
def load_attempt(attempt_dir):
p = Path(attempt_dir) / "elements.json"
return json.loads(p.read_text()) if p.exists() else None
|