Spaces:
Running
Running
File size: 8,044 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 | """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
|