Spaces:
Running
Running
Download src/miniwebgen/sampler.py from sparklabuofu/MiniWeb-demo: direct link, hf CLI and curl.
- Browser
- Download file 19 kB
-
https://huggingface.co/spaces/sparklabuofu/MiniWeb-demo/resolve/main/src/miniwebgen/sampler.py
- Command line
-
hf download hf://spaces/sparklabuofu/MiniWeb-demo/src/miniwebgen/sampler.py
-
curl -L -o sampler.py https://huggingface.co/spaces/sparklabuofu/MiniWeb-demo/resolve/main/src/miniwebgen/sampler.py
19 kB
| """Stage 2 β parameter sampler. Programmatically picks (site, target element, argument, | |
| start state) tuples; diversity is enforced HERE, not by the suggester. | |
| * train sites only, and only sites whose macro_locations entry lists the macro and whose | |
| site map has a usable (probed) control of the macro's kind; | |
| * stratified round-robin across those sites (seeded order), per-site cap, least-used | |
| control first within a site, options drawn without replacement; | |
| * dedup key (site, target element, argument) β element = the control's backend identity | |
| "METHOD path param", stable across re-crawls; keys kept in earlier runs are excluded; | |
| * start state: `fresh` (the control's page), `preset` (another control on the same listing | |
| already set via the start URL) or `mid_chain` (a preceding macro on the same page is | |
| executed first and its actions become the history). The target check then also | |
| requires that earlier state to survive (`keep`); | |
| * every tuple is dry-run with privileged calls in a throw-away session: the check must | |
| FAIL on the start state (not vacuous) and PASS after applying the target (solvable). | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import random | |
| import re | |
| from urllib.parse import urlencode | |
| from miniwebgen import browser as B | |
| from miniwebgen import checks, config, kinds, sitemap | |
| PHRASINGS = ("imperative", "goal", "conversational") | |
| def element_key(ctrl): | |
| """The control's backend identity (stable across re-crawls), as its kind defines it.""" | |
| return kinds.of(ctrl).element_key(ctrl) | |
| def dedup_key(site, ctrl, option): | |
| return [site, element_key(ctrl), kinds.of(ctrl).dedup_value(option)] | |
| def _options(ctrl, macro=None): | |
| return kinds.of(ctrl).arguments_for(ctrl, macro) if macro else kinds.of(ctrl).arguments(ctrl) | |
| def _ctrl_view(ctrl): | |
| return kinds.of(ctrl).view(ctrl) | |
| def candidate_sites(macro, train, sitemaps): | |
| from miniweb.annotation.macro_locations import MACRO_LOCATIONS | |
| out, dropped = {}, {} | |
| # a macro no train site lists (its demos are all on held-out sites) may use any train site | |
| # whose site map has a control serving it | |
| unlisted = not any(macro in (MACRO_LOCATIONS.get(s) or {}) for s in train) | |
| for site in train: | |
| if not unlisted and macro not in (MACRO_LOCATIONS.get(site) or {}): | |
| continue | |
| ctrls = [c for c in sitemap.usable_for(sitemaps.get(site), macro) if kinds.of(c).has_arguments(c)] | |
| if ctrls: | |
| out[site] = ctrls | |
| else: | |
| dropped[site] = "no usable control in site map" if sitemaps.get(site) else "no site map" | |
| return out, dropped | |
| def _companions(site_ctrls, ctrl): | |
| """Other usable controls on the same page that hit the same endpoint (for preset / mid-chain).""" | |
| out = [] | |
| if not kinds.of(ctrl).chainable: | |
| return out | |
| for c in site_ctrls: | |
| if c is ctrl or c["page"] != ctrl["page"] or not c.get("usable") or not kinds.of(c).chainable: | |
| continue | |
| if c["signature"]["path"] != ctrl["signature"]["path"] or c["signature"].get("param") == ctrl["signature"].get("param"): | |
| continue | |
| if _options(c): | |
| out.append(c) | |
| return out | |
| def propose(macro, n, *, train, sitemaps, seed=0, per_site_cap=2, mid_chain_share=0.3, preset_share=0.15, | |
| exclude_keys=(), prior_site_counts=None, fill=False): | |
| """Up to `n` unvalidated tuples (the sampler over-draws; the dry run filters). | |
| `prior_site_counts` {site: n} = trajectories already kept for this macro by earlier runs; | |
| they count against the per-site cap. Start kinds follow quotas: every prefix of the list | |
| holds ~mid_chain_share mid-chain and ~preset_share preset tuples (where the page allows).""" | |
| rng = random.Random(f"{seed}:{macro}") | |
| cands, dropped = candidate_sites(macro, train, sitemaps) | |
| sites = sorted(cands) | |
| rng.shuffle(sites) | |
| used_keys = {tuple(k) for k in exclude_keys} | |
| per_site, ctrl_use, out = dict(prior_site_counts or {}), {}, [] | |
| kinds = {"mid_chain": 0, "preset": 0} | |
| all_ctrls = {s: [c for c in sitemaps[s]["controls"] if c.get("usable")] for s in sites} | |
| progress, cap = True, per_site_cap | |
| while len(out) < n and (progress or (fill and cap == per_site_cap)): | |
| if not progress: # fill: the even cap left the macro short; sites with more take it | |
| cap = 10 ** 6 | |
| progress = False | |
| for site in sites: | |
| if len(out) >= n: | |
| break | |
| if per_site.get(site, 0) >= cap: | |
| continue | |
| ctrls = sorted(cands[site], key=lambda c: (ctrl_use.get(c["control_id"], 0), rng.random())) | |
| pick = None | |
| for ctrl in ctrls: | |
| opts = [o for o in _options(ctrl, macro) if tuple(dedup_key(site, ctrl, o)) not in used_keys] | |
| if opts: | |
| pick = (ctrl, rng.choice(opts)) | |
| break | |
| if not pick: | |
| continue | |
| ctrl, opt = pick | |
| used_keys.add(tuple(dedup_key(site, ctrl, opt))) | |
| per_site[site] = per_site.get(site, 0) + 1 | |
| ctrl_use[ctrl["control_id"]] = ctrl_use.get(ctrl["control_id"], 0) + 1 | |
| i = len(out) + 1 | |
| want = ("mid_chain" if kinds["mid_chain"] < mid_chain_share * i else | |
| "preset" if kinds["preset"] < preset_share * i else "fresh") | |
| t = _make(macro, site, ctrl, opt, all_ctrls[site], rng, want) | |
| if t["start"]["kind"] != "mid_chain" and want == "mid_chain" and kinds["preset"] < preset_share * i: | |
| t = _make(macro, site, ctrl, opt, all_ctrls[site], rng, "preset") | |
| kinds[t["start"]["kind"]] = kinds.get(t["start"]["kind"], 0) + 1 | |
| out.append(t) | |
| progress = True | |
| return out, dropped | |
| _LO = re.compile(r"\b(min|minimum|from|low|lower|start)|\bat least\b") | |
| _HI = re.compile(r"\b(max|maximum|high|upper|end|until)|\b(to|at most|up to)\b") | |
| def _bound(ctrl): | |
| """'lo' / 'hi' for one end of a numeric range pair (Min Price / Max Price, price_from / price_to), else None.""" | |
| s = re.sub(r"[_\-\[\]]", " ", f"{ctrl.get('name') or ''} {ctrl.get('label') or ''}").lower() | |
| lo, hi = bool(_LO.search(s)), bool(_HI.search(s)) | |
| return "lo" if lo and not hi else "hi" if hi and not lo else None | |
| def _consistent(ctrl, opt, other, o2): | |
| """A preset / prefix on the other end of the same range must leave it non-empty: min < max. 2026-09-29: a | |
| preset Min Price above the task's Max Price gave an empty listing and a self-contradicting task.""" | |
| a, b = _bound(ctrl), _bound(other) | |
| if not a or not b or a == b: | |
| return True | |
| try: | |
| v, w = float(opt["value"]), float(o2["value"]) | |
| except (TypeError, ValueError, KeyError): | |
| return True | |
| return v < w if a == "lo" else w < v | |
| def _make(macro, site, ctrl, opt, site_ctrls, rng, want="fresh"): | |
| start = {"kind": "fresh", "url": ctrl["page"], "keep": {}} | |
| comps = [c for c in _companions(site_ctrls, ctrl) if any(_consistent(ctrl, opt, c, o) for o in _options(c))] | |
| if comps and want == "mid_chain": | |
| other = rng.choice(comps) | |
| o2 = rng.choice([o for o in _options(other) if _consistent(ctrl, opt, other, o)]) | |
| start = {"kind": "mid_chain", "url": ctrl["page"], "keep": {other["signature"]["param"]: o2["value"]}, | |
| "prefix": {"macro": kinds.macro_of(other), "control": _ctrl_view(other), | |
| "option": o2}} | |
| elif comps and want == "preset" and ctrl["page"].rstrip("/") == ctrl["signature"]["path"].rstrip("/"): | |
| other = rng.choice([c for c in comps if c["signature"]["method"] == "GET"] or comps) | |
| o2 = rng.choice([o for o in _options(other) if _consistent(ctrl, opt, other, o)]) | |
| if other["signature"]["method"] == "GET": | |
| q = {other["signature"]["param"]: o2["value"]} | |
| start = {"kind": "preset", "url": ctrl["page"] + "?" + urlencode(q), "keep": q, | |
| "preset": {"control": _ctrl_view(other), "option": o2}} | |
| key = dedup_key(site, ctrl, opt) | |
| pid = "p-" + hashlib.sha1(json.dumps([macro, key, start["kind"], start.get("keep")]).encode()).hexdigest()[:10] | |
| return {"param_id": pid, "macro": macro, "site": site, "control": _ctrl_view(ctrl), "option": opt, | |
| **({"op": opt["op"]} if opt.get("op") else {}), | |
| "start": start, "phrasing": rng.choice(PHRASINGS), "dedup_key": key, | |
| "check": kinds.of(ctrl).check(ctrl, opt, keep=start["keep"])} | |
| # ββ feasibility dry run (privileged) ββββββββββββββββββββββββββββββββββββββββββ | |
| def _pin_status(chk, entries): | |
| """Record the status class the successful request had (see checks._status_class_ok).""" | |
| if chk.get("kind") != "request": | |
| return | |
| ok = [e for e in entries if checks._accepted(e) and (e.get("method") or "GET").upper() == chk["method"] | |
| and checks._path(e.get("path")) == checks._path(chk["path"]) | |
| and checks._carries(e, {**chk.get("params", {}), **chk.get("keep", {})})] | |
| if ok and str(ok[-1].get("status", ""))[:1] == "3": | |
| chk["status"] = "3xx" | |
| def _weak_check(t, before, after=(), changes=None): | |
| """A check that can pass without the task being done (the flash judge caught these, 2026-09-28): | |
| None when the check is sound, else the reason to drop the task.""" | |
| chk = t["check"] | |
| if chk.get("kind") != "request": | |
| return None | |
| want = {**chk.get("params", {}), **chk.get("keep", {})} | |
| # a rejected login / registration re-renders the form (200) and changes nothing; other POSTs (tools, | |
| # joins, shares, uploads) legitimately answer 200 without a data change | |
| if chk["method"] == "POST" and not chk.get("response") and \ | |
| (t.get("macro") in ("sign_by_text", "authenticate_by_form", "create_by_form", "toggle_relationship") | |
| or re.search(r"/(login|signin|sign-in|register|signup|sign-up)\b", chk["path"])): | |
| hits = [e for e in after if checks._accepted(e) and (e.get("method") or "").upper() == "POST" | |
| and checks._path(e.get("path")) == checks._path(chk["path"]) and checks._carries(e, want)] | |
| redirected = any(str(e.get("status", ""))[:1] == "3" for e in hits) | |
| if hits and not redirected and changes is not None and not changes: | |
| return ("weak check: the submission shows no success (no redirect, no data change): a rejected " | |
| "form re-renders the same way (invalid login, username taken)") | |
| if chk["method"] == "GET" and not want and "/api/" in chk["path"]: | |
| return "weak check: a parameterless background API request (polling happens without the action)" | |
| if chk["method"] == "GET" and not want and any(checks._path(e.get("path")) == checks._path(chk["path"]) for e in before): | |
| return "weak check: the start state already makes this request" | |
| from miniwebgen import formvalues as FV | |
| typed = {FV.field_key(f) for f in (t["control"].get("fields") or []) | |
| if (f.get("type") or "text") in ("text", "textarea", "search", "email", "")} | |
| texts = [v for k, v in ((t.get("option") or {}).get("values") or {}).items() | |
| if k in typed and isinstance(v, str) and len(v.strip()) > 12 and " " in v.strip()] | |
| carried = json.dumps(want, ensure_ascii=False) | |
| missing = [v for v in texts if v.strip() not in carried] | |
| if missing: | |
| return f"weak check: typed text never reaches the checked request ({missing[0][:40]!r})" | |
| return None | |
| def _oracle_once(t, base, b): | |
| """One privileged run of an oracle step in a fresh session: its evidence | |
| {entry, requests, changes, clipboard, downloads}.""" | |
| ctx = B.new_context(b) | |
| page = ctx.new_page() | |
| downloads = [] | |
| page.on("download", lambda dl: downloads.append({"filename": dl.suggested_filename, "url": dl.url})) | |
| try: | |
| kinds.of(t["control"]).setup(ctx, base, t["control"], t["option"]) | |
| B.goto_start(page, base + t["start"]["url"]) | |
| before = B.session_log(ctx, base) | |
| where = kinds.of(t["control"]).changes_site(t) | |
| ch0 = B.session_changes(ctx, base, where) or [] | |
| ctrl = t["control"] | {"form": {"submit": t["control"].get("apply_button")}} if t["control"]["kind"] != "form" else dict(t["control"]) | |
| e, _k, _m, _new = sitemap.apply_privileged(page, ctrl, t["option"], base, ctx, len(before)) | |
| after = B.session_log(ctx, base) | |
| ch1 = B.session_changes(ctx, base, where) or [] | |
| return {"entry": e, "requests": after[len(before):], "changes": [c for c in ch1 if c not in ch0], | |
| "clipboard": B.clipboard_writes(ctx, base), "downloads": downloads} | |
| finally: | |
| ctx.close() | |
| def _stabilize(chk, changes2): | |
| """Keep only the expected fields a second, independent run reproduces (drops per-request | |
| values such as confirmation codes). None if an expected change did not recur at all.""" | |
| out = [] | |
| for want in chk["expect"]: | |
| same = [c for c in changes2 if c["collection"] == want["collection"] and c["op"] == want["op"] | |
| and (want["item_id"] is None or str(c["item_id"]) == str(want["item_id"]))] | |
| if not same: | |
| return None | |
| best = max(same, key=lambda c: sum(checks._essential(c).get(k) == v for k, v in want["fields"].items())) | |
| ess = checks._essential(best) | |
| out.append(dict(want, fields={k: v for k, v in want["fields"].items() if ess.get(k) == v})) | |
| return dict(chk, expect=out) | |
| def dry_run_oracle(t, base, b): | |
| """(feasible, detail) for kinds whose check is recorded: run the step twice (fresh sessions), | |
| record what it changed (else the request it sent), keep what both runs agree on.""" | |
| kind = kinds.of(t["control"]) | |
| ev1 = _oracle_once(t, base, b) | |
| req1, ch1 = ev1["requests"], ev1["changes"] | |
| chk = kind.observed_check(ev1, t["site"], t["control"]) | |
| if chk is None: | |
| return False, "the step changed nothing observable (no data change, request, clipboard or download)" | |
| if chk["kind"] != "state": | |
| t["check"] = chk | |
| ok, detail = checks.evaluate(chk, req1, ch1, ev1["clipboard"]) | |
| return (True, detail) if ok else (False, "the recorded check does not pass its own run: " + detail) | |
| ch2 = _oracle_once(t, base, b)["changes"] | |
| stable = _stabilize(chk, ch2) | |
| if stable is None: | |
| return False, "the step's data change did not recur in a second run (unstable)" | |
| t["check"] = stable | |
| ok1, detail = checks.evaluate(stable, req1, ch1) | |
| ok2, detail2 = checks.evaluate(stable, [], ch2) | |
| if ok1 and ok2: | |
| return True, detail | |
| return False, "the recorded check does not pass its own runs: " + (detail if not ok1 else detail2) | |
| def dry_run(t, base, b): | |
| """(feasible, detail). Privileged select_option/clicks in a throw-away session.""" | |
| try: | |
| own = kinds.of(t["control"]).dry_run(t, base, b) | |
| except Exception as exc: | |
| return False, f"dry-run error {type(exc).__name__}: {str(exc)[:160]}" | |
| if own is not None: | |
| return own | |
| if getattr(kinds.of(t["control"]), "oracle", False): | |
| try: | |
| return dry_run_oracle(t, base, b) | |
| except Exception as exc: | |
| return False, f"dry-run error {type(exc).__name__}: {str(exc)[:160]}" | |
| ctx = B.new_context(b) | |
| page = ctx.new_page() | |
| try: | |
| kinds.of(t["control"]).setup(ctx, base, t["control"], t["option"]) | |
| B.goto_start(page, base + t["start"]["url"]) | |
| if t["start"]["kind"] == "mid_chain": | |
| pre = t["start"]["prefix"] | |
| n0 = len(B.session_log(ctx, base)) | |
| e, _k, _m, _new = sitemap.apply_privileged(page, pre["control"] | {"form": {"submit": pre["control"].get("apply_button")}}, | |
| pre["option"], base, ctx, n0) | |
| if not e: | |
| return False, "prefix step produced no request" | |
| before = B.session_log(ctx, base) | |
| ok0, _ = checks.evaluate(t["check"], before) | |
| if ok0: | |
| return False, "vacuous: the start state already satisfies the check" | |
| ctrl = t["control"] | {"form": {"submit": t["control"].get("apply_button")}} | |
| if ctrl["kind"] in ("link", "chips"): | |
| ctrl["key"] = ctrl["signature"]["param"] | |
| e, _k, _m, _new = sitemap.apply_privileged(page, ctrl, t["option"], base, ctx, len(before)) | |
| after = B.session_log(ctx, base) | |
| ok1, detail = checks.evaluate(t["check"], after) | |
| if not ok1: | |
| return False, "not solvable by applying the control: " + detail | |
| weak = _weak_check(t, before, after, B.session_changes(ctx, base)) | |
| if weak: | |
| return False, weak | |
| _pin_status(t["check"], after) | |
| return True, detail | |
| except Exception as exc: | |
| return False, f"dry-run error {type(exc).__name__}: {str(exc)[:160]}" | |
| finally: | |
| ctx.close() | |
| def sample(macro, n, *, train, sitemaps, bases, workers=4, seed=0, overdraw=3, log=print, **kw): | |
| """(feasible[:n], infeasible, dropped sites, spare feasible) β fewer than n if candidates run out.""" | |
| import queue | |
| import threading | |
| proposed, dropped = propose(macro, n * overdraw, train=train, sitemaps=sitemaps, seed=seed, **kw) | |
| # keep the stratified order: validate in order, stop once n are feasible | |
| results = [None] * len(proposed) | |
| todo = queue.Queue() | |
| for i, t in enumerate(proposed): | |
| todo.put(i) | |
| lock = threading.Lock() | |
| state = {"ok": 0} | |
| def worker(wi): | |
| with B.browser() as b: | |
| while True: | |
| with lock: | |
| if state["ok"] >= n: | |
| return | |
| try: | |
| i = todo.get_nowait() | |
| except queue.Empty: | |
| return | |
| ok, detail = dry_run(proposed[i], bases[wi % len(bases)], b) | |
| results[i] = (ok, detail) | |
| with lock: | |
| state["ok"] += ok | |
| threads = [threading.Thread(target=worker, args=(w,), daemon=True) for w in range(max(1, workers))] | |
| for th in threads: | |
| th.start() | |
| for th in threads: | |
| th.join() | |
| feasible, infeasible = [], [] | |
| for t, r in zip(proposed, results): | |
| if r is None: | |
| continue | |
| t["feasibility"] = r[1] | |
| (feasible if r[0] else infeasible).append(t) | |
| # workers validate in parallel, so a few extra feasible tuples may exist: keep the first n | |
| # in stratified order, the rest are returned as `spare` (feasible, unused) | |
| feasible, spare = feasible[:n], feasible[n:] | |
| log(f"sample {macro}: validated {len(feasible) + len(spare) + len(infeasible)}, feasible " | |
| f"{len(feasible) + len(spare)} (using {len(feasible)}), infeasible {len(infeasible)}, " | |
| f"sites dropped {len(dropped)}") | |
| return feasible, infeasible, dropped, spare | |