"""Validate dataset JSONL files against tools.json and SPEC.md conventions. Usage: python3 validate.py file1.jsonl [file2.jsonl ...] """ import json import re import sys from pathlib import Path TOOLS = {t["function"]["name"]: t["function"] for t in json.loads((Path(__file__).parent / "tools.json").read_text())} TIME_RE = re.compile(r"^([01]\d|2[0-3]):[0-5]\d$") PY_TYPES = {"string": str, "integer": int, "boolean": bool} def check_call(call): errs = [] name = call.get("name") if name not in TOOLS: return [f"unknown function {name!r}"] args = call.get("arguments") if not isinstance(args, dict): return ["arguments must be an object"] params = TOOLS[name]["parameters"] props = params["properties"] for req in params["required"]: if req not in args: errs.append(f"{name}: missing required arg {req!r}") for k, v in args.items(): if k not in props: errs.append(f"{name}: unknown arg {k!r}") continue spec = props[k] typ = PY_TYPES[spec["type"]] if not isinstance(v, typ) or (typ is int and isinstance(v, bool)): errs.append(f"{name}.{k}: expected {spec['type']}, got {v!r}") continue if "enum" in spec and v not in spec["enum"]: errs.append(f"{name}.{k}: {v!r} not in {spec['enum']}") if k == "time" and not TIME_RE.match(v): errs.append(f"{name}.time: {v!r} is not HH:MM 24h") if k == "level" and not 0 <= v <= 100: errs.append(f"{name}.level out of range: {v}") if k in ("duration_seconds", "duration_minutes", "delay_minutes") and v <= 0: errs.append(f"{name}.{k} must be positive: {v}") if typ is str and not v.strip(): errs.append(f"{name}.{k}: empty string") if k == "date" and v != v.lower(): errs.append(f"{name}.date must be lowercase: {v!r}") if name in ("set_volume", "set_brightness") and len(args) != 1: errs.append(f"{name}: exactly one of level/action required") if name == "make_call" and args.get("video") is False: errs.append("make_call: omit video instead of false") return errs def validate(path): errors, n = [], 0 for i, line in enumerate(Path(path).read_text().splitlines(), 1): if not line.strip(): continue n += 1 try: rec = json.loads(line) except json.JSONDecodeError as e: errors.append(f"{path}:{i}: bad JSON: {e}") continue errs = [] if not isinstance(rec.get("text"), str) or not rec["text"].strip(): errs.append("missing text") if rec.get("lang") not in ("en", "hi-en"): errs.append(f"bad lang {rec.get('lang')!r}") calls = rec.get("calls") if not isinstance(calls, list): errs.append("calls must be a list") calls = [] if not calls and not (isinstance(rec.get("reply"), str) and rec["reply"].strip()): errs.append("no-call record needs a reply") for c in calls: errs += check_call(c) errors += [f"{path}:{i}: {e}" for e in errs] return n, errors if __name__ == "__main__": total_err = 0 for p in sys.argv[1:]: n, errs = validate(p) total_err += len(errs) print(f"{p}: {n} records, {len(errs)} errors") for e in errs[:50]: print(" " + e) sys.exit(1 if total_err else 0)