cq-test / scripts /scavenger_hunt /generator.py
BhargavMN
data set for fine tuning
0acfbcd
Raw
History Blame Contribute Delete
8.47 kB
r"""
generator.py β€” scavenger_hunt dataset generator
One Gemini call per example. Failed examples retry up to 2x with the
validation errors fed back into the prompt. Arithmetic fields are
auto-fixed before validation so the model is never penalised for math.
Saves after every successful example (safe to Ctrl-C and resume).
Usage (PowerShell / Windows):
$env:GEMINI_API_KEY = "your-key"
python generator.py --n 10 --output ..\..\app\data\scavenger_hunt\dataset.json
Usage (bash / Mac / Linux):
export GEMINI_API_KEY="your-key"
python3 generator.py --n 10 --output ../../app/data/scavenger_hunt/dataset.json
Flags:
--n INT examples to generate this run (default 10)
--output PATH output JSON file (default app/data/scavenger_hunt/dataset.json)
--sleep FLOAT seconds between API calls (default 8)
--seed INT fix random seed for sampling (optional)
--mock skip Gemini, use built-in stub (pipeline smoke-test)
"""
import os, re, json, time, random, logging, argparse, sys
from sampler import sample_input, build_prompt
from validator import validate_example, auto_fix
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "common"))
from cq_common import has_risk_indicators
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("cq")
DEFAULT_OUTPUT = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)), "..", "..", "app", "data", "scavenger_hunt", "dataset.json"))
# ── JSON extraction ───────────────────────────────────────────────────────────
def extract_json(text: str) -> dict:
"""Pull JSON object from model output, stripping markdown fences if present."""
text = re.sub(r"^```(?:json)?\s*", "", text.strip())
text = re.sub(r"\s*```$", "", text)
start, end = text.find("{"), text.rfind("}")
if start == -1 or end == -1:
raise ValueError("no JSON object found in model output")
return json.loads(text[start : end + 1])
# ── Gemini backend ────────────────────────────────────────────────────────────
def call_gemini(prompt: str) -> str:
from google import genai
from google.genai import types
client = genai.Client() # reads GEMINI_API_KEY
resp = client.models.generate_content(
model = "gemini-2.5-flash",
contents = prompt,
config = types.GenerateContentConfig(
response_mime_type = "application/json",
temperature = 0.8,
),
)
return resp.text
# ── Mock backend (no API needed) ─────────────────────────────────────────────
def call_mock(prompt: str) -> str:
"""Deterministic stub β€” builds a valid-ish output from info already in the prompt."""
import re as _re
def _find(pattern, default):
m = _re.search(pattern, prompt)
return m.group(1) if m else default
n_tasks = int(_find(r"exactly (\d+)", "4"))
method = _find(r'scoring_method\s*:\s*"(\w+)"', "point_accumulation")
dur = int(_find(r"time_limit_minutes\s*:\s*(\d+)", "60"))
tb_raw = _find(r"time_bonus_per_minute\s*:\s*(\d+|null)", "null")
tb = None if tb_raw == "null" else int(tb_raw)
agg_raw = _find(r'team_aggregation\s*:\s*(null|"[^"]+")', "null")
agg = None if agg_raw == "null" else agg_raw.strip('"')
bonus_ok = "bonus_task_eligible : true" in prompt
# tags from prompt
tm = _re.search(r'"landscape_tags":\s*\[([^\]]+)\]', prompt)
tags = [t.strip().strip('"') for t in tm.group(1).split(",")] if tm else ["plaza_or_square"]
# difficulty mix
if "ZERO hard tasks" in prompt:
diffs = ["easy"] * n_tasks
elif "β‰₯40 % hard" in prompt or ">=40% hard" in prompt:
n_hard = max(2, round(n_tasks * 0.45))
n_med = max(1, round(n_tasks * 0.35))
n_easy = max(0, n_tasks - n_hard - n_med)
diffs = (["hard"] * n_hard + ["medium"] * n_med + ["easy"] * n_easy)[:n_tasks]
else:
diffs = (["medium", "easy"] * n_tasks)[:n_tasks]
pts_map = {"easy": 10, "medium": 20, "hard": 30}
types_cycle = ["observe_and_answer","find_and_photograph","reach_and_verify",
"social_interaction","timed_challenge","collect_and_return"]
per_task = max(3, int(dur * 0.78) // n_tasks)
tasks = []
for i in range(n_tasks):
tag = tags[i % len(tags)]
description = f"Within the zone characterised by {tag.replace('_',' ')}, locate and document the most structurally distinct feature visible from a public pedestrian path."
flags = ["uneven paving may be slippery when wet", "watch for cyclists sharing the path"] if has_risk_indicators(description) else []
tasks.append({
"task_id": f"T{i+1:02d}",
"title": f"Zone Study {i+1}",
"description": description,
"landscape_tags_used": [tag],
"task_type": types_cycle[i % len(types_cycle)],
"difficulty_contribution": diffs[i],
"points": pts_map[diffs[i]],
"completion_proof": "photograph submitted via app",
"estimated_time_minutes": per_task,
"hints": {
"hint_1": "Scan the area from open space.",
"hint_2": f"Move toward the {tag.replace('_',' ')} zone and compare features.",
"hint_3": f"Within the {tag.replace('_',' ')} zone, find the feature that differs most from its neighbours in scale or colour.",
},
"safety_flags": flags,
})
bonus = (
{"description": "Reach the highest publicly accessible viewpoint in the area and return before the window closes.",
"points": 50, "risk": "βˆ’20 points if not completed within 15 minutes",
"completion_proof": "geotagged photo from viewpoint"}
if bonus_ok else
{"description": None, "points": None, "risk": None, "completion_proof": None}
)
out = {
"game_title": "The Urban Field Survey",
"rules": {
"objective": "Score the most points before the time limit expires.",
"scoring_method": method,
"task_reveal_mode": "sequential",
"team_rules": "Teams move together and submit jointly." if agg else None,
"time_limit_minutes": dur,
"disqualification_conditions": ["entering an exclusion zone","using prohibited transport"],
},
"safety_constraints": {
"exclusion_zones": ["private_property","active_roadway","construction_sites","restricted_government_buildings"],
"physical_limits": ["no climbing","no jumping","no water entry","no entering buildings"],
"adult_supervision_required": False,
"notes": "Standard urban precautions apply.",
},
"tasks": tasks,
"task_count": n_tasks,
"total_possible_points": 0, # auto_fix will correct
"max_deductible_points": 0,
"minimum_possible_points": 0,
"bonus_task_eligible": bonus_ok,
"bonus_task": bonus,
"scoring_summary": {
"base_points_available": 0,
"time_bonus_per_minute_early": tb,
"hint_cost_tier_1": 5,
"hint_cost_tier_2": 10,
"team_aggregation_method": agg,
"winning_condition_detail": "The competitor with the highest total points when the time limit expires wins; ties broken by earliest final task submission.",
},
"estimated_total_time_minutes": 0,
"quality_score": 4.0,
}
return json.dumps(out)
from cli import run_cli # noqa: E402
if __name__ == "__main__":
run_cli("scavenger_hunt", sample_input, build_prompt, auto_fix,
validate_example, DEFAULT_OUTPUT, call_mock)