MiniWeb-demo / src /miniwebgen /executor.py
sparklabuofu's picture
Update the demo
22de727 verified
Raw History Blame Contribute Delete
12 kB
"""Stage 4 — executor. A coding LLM writes a script against the restricted wrapper
(src/miniwebgen/actions.py API_DOC) using the macro guide and privileged DOM facts; the harness
runs it in a fresh session and records only screenshots + pixel actions.
Per attempt (fresh browser context = fresh Flask session):
goto start URL (the only navigation) -> [prefix step script] -> target step script
-> final screenshot -> backend gate (checks.gate over the session request log)
A failed attempt (policy violation, script error, gate failure) is kept with its reason —
it is a DPO negative — and the next attempt's prompt carries the error and the page state.
"""
from __future__ import annotations
import json
import random
import re
import time
import traceback
from pathlib import Path
from miniwebgen import browser as B
from miniwebgen import checks, config, elements, kinds
from miniwebgen.actions import (API_DOC, Act, ActionError, Dom, Expect, PolicyViolation, Recorder, Session,
run_script)
from miniwebgen.driver import PlaywrightDriver
SYSTEM = ("You are a browser-automation engineer. You write short, robust Python scripts that complete one "
"UI step using ONLY the API below. The script's actions are recorded as a demonstration, so act "
"like a careful human: scroll to things before using them, one purposeful action at a time, no "
"unnecessary clicks.\n\n" + API_DOC)
def _clip(s, n):
s = str(s or "")
return s if len(s) <= n else s[:n] + "…"
def step_spec(step):
lines = [f"MACRO: {step['macro']}", f"STEP: {step['subtask']}", f"(full task: {step['instruction']})"]
op = step["control"].get("opener")
if op:
lines.append(f"OPEN FIRST: the control is revealed by clicking css={op['css']!r} text={op.get('text')!r} "
f"(a dialog, tab or panel) — click it, then re-query the control (task['opener_css'])")
lines += kinds.of(step["control"]).step_spec(step)
lines.append(f"BACKEND PROOF checked by expect.backend(): {checks.describe(step['check'])}")
return "\n".join(lines)
def script_task(step):
"""The `task` dict a script sees: the exact targets, so selectors are never re-typed."""
t = kinds.of(step["control"]).script_task(step)
if step["control"].get("opener"):
t["opener_css"] = step["control"]["opener"]["css"]
return t
def page_state(driver, step):
"""Privileged facts about the live page for the prompt."""
facts = [f"URL: {driver.url()}", f"VIEWPORT: {driver.viewport()[0]}x{driver.viewport()[1]}"]
try:
hits = kinds.of(step["control"]).locate(driver, step)
for _k, info in hits[:2]:
vw, vh = driver.viewport()
b = info.get("box") or [0, 0, 0, 0]
on = info.get("visible") and b[1] >= 0 and b[1] + b[3] <= vh
facts.append(f"TARGET NOW: <{info['tag']}> visible={info.get('visible')} on_screen={bool(on)} "
f"box=({b[0]:.0f},{b[1]:.0f},{b[2]:.0f},{b[3]:.0f})"
+ (f" selected={info.get('selected_text')!r}" if info["tag"] == "select" else ""))
if not hits:
facts.append("TARGET NOW: not found with that selector/text — inspect the outline")
except Exception as exc:
facts.append(f"TARGET NOW: lookup failed ({exc})")
facts.append("PAGE OUTLINE (first elements):\n" + _clip(driver.outline(60), 3500))
return "\n".join(facts)
def _code(raw):
m = re.search(r"```(?:python)?\s*\n(.*?)```", raw or "", re.S)
return (m.group(1) if m else (raw or "")).strip()
def write_script(step, guide_txt, state, feedback=None, model=config.MODEL_EXECUTOR):
from helpers.llm import call_llm
example = kinds.of(step["control"]).example
prompt = (f"GUIDE for {step['macro']} (distilled from human demonstrations):\n{guide_txt}\n\n"
f"{step_spec(step)}\n\nLIVE PAGE:\n{state}\n\nEXAMPLE (different page):\n```python\n{example}\n```\n")
if feedback:
prompt += f"\nA PREVIOUS ATTEMPT FAILED:\n{feedback}\nFix the cause.\n"
prompt += (f"\nThe script receives task = {json.dumps(script_task(step), ensure_ascii=False)}\n"
"Use task[...] for selectors/texts verbatim — never rewrite a selector. After any action that may "
"reload the page (committing a choice, clicking Apply/a link), re-query elements with dom.one(...) "
"before using them again.\nWrite the script now, in one ```python block. End it with expect.backend().")
raw = call_llm(prompt, system=SYSTEM, model=model, max_tokens=8000, temperature=0.3)
return _code(raw)
def _step_of(task, which):
if which == "prefix":
pre = task["start"]["prefix"]
return {"macro": pre["macro"], "subtask": task.get("prefix_subtask") or "", "instruction": task["instruction"],
"control": pre["control"], "option": pre["option"], "check": pre["check"]}
return {"macro": task["macro"], "subtask": task["subtask"], "instruction": task["instruction"],
"control": task["control"], "option": task["option"], "check": task["check"]}
def run_attempt(task, base, b, out_dir, n, guides, feedback=None, model=config.MODEL_EXECUTOR, log=print):
"""One attempt in a fresh session. Returns the attempt record (also written to disk)."""
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
rng = random.Random(f"{task['task_id']}:{n}")
ctx = B.new_context(b)
page = ctx.new_page()
vp = config.VIEWPORT
driver = PlaywrightDriver(page, vp)
rec = Recorder(out_dir, vp)
logs, entries = [], []
record = {"attempt": n, "task_id": task["task_id"], "ok": False, "executed": False, "stage": None,
"error": None, "scripts": {}, "t0": time.time()}
try:
kinds.of(task["control"]).setup(ctx, base, task["control"], task["option"])
B.goto_start(page, base + task["start"]["url"])
record["start_url"] = page.url
which_steps = (["prefix"] if task["start"]["kind"] == "mid_chain" else []) + ["target"]
for which in which_steps:
step = _step_of(task, which)
rec.span = which
record["stage"] = which
state = page_state(driver, step)
fb = (feedback or {}).get(which)
code = write_script(step, guides[step["macro"]], state, feedback=fb, model=model)
record["scripts"][which] = code
(out_dir / f"{which}.py").write_text(code)
if not code:
raise RuntimeError("the LLM returned no script")
session = Session(driver, rec, rng, max_actions=30, time_budget=180)
chk = step["check"]
session.files = kinds.of(step["control"]).files(step)
session.upload_input = step["control"]["css"] if step["control"]["kind"] == "upload" else None
session.open_urls = kinds.of(step["control"]).open_urls(step)
session.forbidden_keys = kinds.of(step["control"]).forbidden_keys(step)
expect = Expect(session, lambda chk=chk: checks.evaluate(
chk, B.session_log(ctx, base), B.session_changes(ctx, base) if _needs_changes(chk) else None,
B.clipboard_writes(ctx, base) if chk.get("kind") == "clipboard" else None, rec.answer, rec.seen))
run_script(code, act=Act(session), dom=Dom(session), expect=expect, task=script_task(step),
log=lambda m: logs.append(f"[{which}] {m}"))
record["executed"] = True
record["stage"] = "backend"
except PolicyViolation as exc:
record["error"] = f"policy violation: {exc}"
except AssertionError as exc:
record["error"] = f"assertion failed: {str(exc)[:400]}"
except ActionError as exc:
record["error"] = f"action refused: {exc}"
except Exception as exc:
tb = traceback.extract_tb(exc.__traceback__)
where = next((f"script line {f.lineno}" for f in reversed(tb) if f.filename == "<executor-script>"), "")
record["error"] = f"{type(exc).__name__}: {str(exc)[:300]} {where}".strip()
finally:
try:
record["final_screenshot"] = rec.final(driver.screenshot())
record["final_url"] = driver.page.url
html = driver.page.content()
(out_dir / "final.html").write_text(html[:400000])
except Exception:
pass
entries = B.session_log(ctx, base)
changes = B.session_changes(ctx, base)
records = B.session_record(ctx, base)
record["dialogs"], record["downloads"] = driver.dialogs, driver.downloads
try:
ctx.close()
except Exception:
pass
(out_dir / "server_log.json").write_text(json.dumps(entries, indent=1, default=str))
(out_dir / "data_changes.json").write_text(json.dumps(changes, indent=1, default=str))
clip = [e.get("value") for e in records if e.get("action") == "clipboard_write"]
copies = [{"type": "action", "action": "clipboard_write", "target": "", "value": v} for v in clip]
ok, detail, report = checks.gate(task["check"], task["verifier"], entries, action_events(rec.steps) + copies,
changes, clip, rec.answer, rec.seen)
record["answer"] = rec.answer
record["backend"] = {"ok": ok, "detail": detail, "verify_task": report.get("by_macro")}
record["ok"] = bool(record["executed"] and ok)
if record["executed"] and not ok:
record["error"] = f"backend check failed: {detail}"
record["steps"] = rec.steps
elements.write_attempt(out_dir, rec.steps) # privileged element side file (every attempt)
record["logs"] = logs[-50:]
record["duration_s"] = round(time.time() - record.pop("t0"), 1)
(out_dir / "attempt.json").write_text(json.dumps(record, indent=1, default=str))
return record
def _needs_changes(chk):
return chk.get("kind") == "state" or (chk.get("kind") == "answer" and (chk.get("requires") or {}).get("kind") == "state")
def action_events(steps):
"""Recorded pixel actions as human-schema action events (for verify_task / the judge digest)."""
out = []
for s in steps:
tgt = s.get("target") or {}
desc = f"{tgt.get('tag', '')} '{tgt.get('label') or tgt.get('text') or tgt.get('name') or ''}'".strip()
ev = {"type": "action", "action": s["type"], "target": desc if tgt else "", "url": s.get("url"),
"timestamp": s.get("t")}
if s["type"] == "type":
ev["value"] = s["text"]
elif s["type"] == "key":
ev["value"] = s["key"]
elif s["type"] == "scroll":
ev["value"] = f"dy={s.get('dy')}"
elif s["type"] == "answer":
ev["value"] = s["text"]
out.append(ev)
return out
def execute_task(task, base, b, episodes_dir, guides, attempts=3, model=config.MODEL_EXECUTOR, log=print):
"""Up to `attempts` fresh attempts; stops at the first that passes the backend gate."""
tdir = Path(episodes_dir) / task["task_id"]
records, feedback = [], None
for n in range(1, attempts + 1):
rec = run_attempt(task, base, b, tdir / f"a{n}", n, guides, feedback=feedback, model=model, log=log)
rec["dir"] = str((tdir / f"a{n}").relative_to(config.ROOT))
records.append(rec)
log(f" {task['task_id']} a{n}: {'OK' if rec['ok'] else 'FAIL'} "
f"({len(rec['steps'])} actions, {rec['duration_s']}s) {rec.get('error') or ''}"[:220])
if rec["ok"]:
break
stage = rec.get("stage") if rec.get("stage") in ("prefix", "target") else "target"
prev = rec["scripts"].get(stage, "")
feedback = {stage: f"error: {rec.get('error')}\nscript was:\n```python\n{prev}\n```\n"
f"actions it recorded: {[s['type'] for s in rec['steps'] if s.get('span') == stage]}"}
return records