Spaces:
Running
Running
Download src/miniwebgen/formvalues.py from sparklabuofu/MiniWeb-demo: direct link, hf CLI and curl.
- Browser
- Download file 8.04 kB
-
https://huggingface.co/spaces/sparklabuofu/MiniWeb-demo/resolve/main/src/miniwebgen/formvalues.py
- Command line
-
hf download hf://spaces/sparklabuofu/MiniWeb-demo/src/miniwebgen/formvalues.py
-
curl -L -o formvalues.py https://huggingface.co/spaces/sparklabuofu/MiniWeb-demo/resolve/main/src/miniwebgen/formvalues.py
8.04 kB
| """Field values for generated form tasks (privileged, parameter stage). | |
| The sampler picks WHICH form; this module proposes WHAT to enter: K diverse, realistic value | |
| sets per form, written once by an LLM from the form's fields (labels, types, options, current | |
| values) and a little site context, cached on disk. Sign-in forms take real accounts from the | |
| site's users table instead. Every set is validated by the dry run (the site must accept it). | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import re | |
| from miniwebgen import config | |
| CACHE = config.DATAGEN_DIR / "formvalues" | |
| SYSTEM = ("You fill in web forms for realistic test tasks. Propose DIFFERENT value sets a real user of this " | |
| "site might submit. Use only values the form can accept: for fields with options pick one of the " | |
| "options exactly; dates as YYYY-MM-DD (bookings/deadlines in the future: 2026-10 to 2027-06), times " | |
| "as HH:MM, numbers plain; emails @example.com unless an existing person below fits. For an edit or " | |
| "settings form change 1–3 fields from their current values and repeat the others unchanged. Never " | |
| "invent other people when the context lists who exists. Reply ONLY JSON.") | |
| def field_key(f): | |
| return f.get("name") or f["css"] | |
| def _people(site, limit=15): | |
| """A few accounts of the site (privileged, base data; users tables are small).""" | |
| try: | |
| from miniweb.app import db | |
| rows = db.query(site, "users", limit=limit) | |
| except Exception: | |
| return [] | |
| keep = ("username", "name", "display_name", "full_name", "email", "handle") | |
| return [{k: r.get(k) for k in keep if r.get(k)} for r in rows if isinstance(r, dict)] | |
| def records(site, tables=10, per=25): | |
| """A few real records of the site (id + title/name per table; base data, bounded queries) so | |
| values that must exist — a meeting code, an order number — can be chosen, not invented.""" | |
| try: | |
| from miniweb.app import db | |
| prefix = site.replace("-", "_") + "_" | |
| names = [r["name"] for r in db.execute( | |
| "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE ? ORDER BY name LIMIT ?", | |
| (prefix + "%", tables * 3))] | |
| except Exception: | |
| return [] | |
| out = [] | |
| for t in names: | |
| if t.endswith("_users") or len(out) >= tables * per: | |
| continue | |
| try: | |
| cols = [c["name"] for c in db.execute(f"PRAGMA table_info({t})")] | |
| label = next((c for c in ("title", "name", "subject", "code", "number", "label") if c in cols), None) | |
| if "id" not in cols: | |
| continue | |
| rows = db.execute(f"SELECT id{', ' + label if label else ''} FROM {t} ORDER BY id LIMIT ?", (per,)) | |
| except Exception: | |
| continue | |
| out += [{"collection": t[len(prefix):], "id": r["id"], **({label: r[label]} if label else {})} for r in rows] | |
| return out | |
| def accounts(site, limit=20): | |
| """Sign-in credentials from the site's users table: [{login, email, password, name}].""" | |
| try: | |
| from miniweb.app import db | |
| rows = db.query(site, "users", limit=limit) | |
| except Exception: | |
| return [] | |
| out = [] | |
| for r in rows or []: | |
| pw = r.get("password") or r.get("pass") or r.get("pwd") | |
| if not pw or not isinstance(pw, str) or len(pw) > 40 or pw.startswith(("$2", "pbkdf2", "sha")): | |
| continue # hashed: the task could not state it | |
| login = r.get("username") or r.get("user") or r.get("handle") or r.get("email") | |
| if login: | |
| out.append({"login": str(login), "email": str(r.get("email") or ""), "password": pw, | |
| "name": str(r.get("name") or r.get("display_name") or r.get("full_name") or login)}) | |
| return out | |
| def _key(ctrl): | |
| ident = json.dumps([ctrl.get("site_id"), ctrl.get("page"), ctrl.get("role"), | |
| [(f.get("name"), f.get("type")) for f in ctrl.get("fields", [])]], sort_keys=True) | |
| return hashlib.sha1(ident.encode()).hexdigest()[:16] | |
| K = 12 # value sets per form (a cache written with fewer is topped up with new, different ones) | |
| def value_sets(ctrl, site, k=K, model=None, force=False, grounded=False): | |
| """[{values: {field_key: value}, summary}] for a form control (cached). `grounded`: also show the | |
| site's own records so values that must exist are picked from them.""" | |
| path = CACHE / site / f"{_key(ctrl)}.json" | |
| have = [] if force or not path.exists() else json.loads(path.read_text()) | |
| if have and (len(have) >= k or ctrl.get("_topped_up") == k): | |
| return have | |
| out, seen = list(have), {json.dumps(s["values"], sort_keys=True) for s in have} | |
| for _ in range(8): # batches: one call returns ~a dozen sets at most | |
| if len(out) >= k: | |
| break | |
| more = _generate(ctrl, site, min(15, k - len(out)), model, grounded or ctrl.get("grounded"), avoid=out) | |
| fresh = [s for s in more if json.dumps(s["values"], sort_keys=True) not in seen] | |
| if not fresh: | |
| break | |
| seen |= {json.dumps(s["values"], sort_keys=True) for s in fresh} | |
| out += fresh | |
| ctrl["_topped_up"] = k | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(json.dumps(out, indent=1, ensure_ascii=False)) | |
| return out | |
| def _generate(ctrl, site, k, model=None, grounded=False, avoid=()): | |
| if k <= 0: | |
| return [] | |
| from miniweb.annotation import macros as registry | |
| from helpers.llm import call_llm | |
| fields = [{"key": field_key(f), "label": f.get("label") or f.get("placeholder") or f.get("name"), "type": f["type"], | |
| "current": f.get("value") or None, "required": f.get("required", False), | |
| **({"options": [o["text"] for o in f.get("options", [])][:30]} if f.get("options") else {}), | |
| **({"min": f.get("min"), "max": f.get("max")} if f.get("min") or f.get("max") else {})} | |
| for f in ctrl["fields"]] | |
| prompt = json.dumps({"site": ctrl.get("site_name") or site, "page": ctrl.get("page_title"), "form": ctrl.get("heading"), | |
| "purpose": registry.describe(ctrl["macro"]).get("description"), "submit_button": ctrl["submit"].get("text"), | |
| "fields": fields, "people_on_this_site": _people(site), "page_context": ctrl.get("context", [])[:25], | |
| **({"records_on_this_site": records(site), | |
| "note": "a value that refers to something (a code, an id, a number) must be one of these"} | |
| if grounded else {}), | |
| **({"already_used_do_not_repeat": [x["values"] for x in avoid][-40:]} if avoid else {}), | |
| "output": {"sets": [{"values": {"<field key>": "<value>"}, | |
| "summary": "one short line: what this submission does"}]}, | |
| "n_sets": k}, ensure_ascii=False) | |
| raw = call_llm(prompt, system=SYSTEM, json_mode=True, model=model or config.MODEL_SUGGEST, max_tokens=6000, | |
| temperature=0.9) | |
| try: | |
| sets = json.loads(raw or "").get("sets") or [] | |
| except (ValueError, AttributeError): | |
| sets = [] | |
| keys = {field_key(f) for f in ctrl["fields"]} | |
| clean = [] | |
| for s in sets: | |
| vals = {k2: str(v) for k2, v in (s.get("values") or {}).items() if k2 in keys and v not in (None, "")} | |
| if vals: | |
| clean.append({"values": vals, "summary": str(s.get("summary") or "")[:200]}) | |
| return clean | |
| def option_for(field, value): | |
| """The option of a select/radio field matching a value (by text, then value).""" | |
| v = re.sub(r"\s+", " ", str(value)).strip().casefold() | |
| for o in field.get("options") or []: | |
| if str(o.get("text", "")).strip().casefold() == v: | |
| return o | |
| for o in field.get("options") or []: | |
| if str(o.get("value", "")).strip().casefold() == v: | |
| return o | |
| return None | |