MiniWeb-demo / src /miniwebgen /formvalues.py
sparklabuofu's picture
Update the demo
22de727 verified
Raw History Blame Contribute Delete
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