File size: 17,029 Bytes
e6ce96e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#!/usr/bin/env python3
"""
Build (or refresh) parameter_pools.json by calling the live EC2 application APIs.

Usage:
    python scripts/build_parameter_pools.py --host ec2-16-59-2-56.us-east-2.compute.amazonaws.com
    python scripts/build_parameter_pools.py --host localhost   # if running directly on EC2
    python scripts/build_parameter_pools.py --host <IP> --output parameter_pools.json

Requirements: pip install requests
"""

import argparse
import json
import sys
from datetime import date
from pathlib import Path

try:
    import requests
    requests.packages.urllib3.disable_warnings()
except ImportError:
    print("pip install requests")
    sys.exit(1)

# ── Config ────────────────────────────────────────────────────────────────────
PORTS = {
    "shopping":       7770,
    "shopping_admin": 7780,
    "forum":          9999,
    "wikipedia":      8888,
}

ADMIN_USER = "admin"
ADMIN_PASS = "admin1234"

# Wikipedia articles to verify exist in the ZIM snapshot
WIKIPEDIA_TITLES = [
    ("Python (programming language)", "Python programming language",   "Python_(programming_language)"),
    ("Albert Einstein",               "Albert Einstein",               "Albert_Einstein"),
    ("World War II",                  "World War II",                  "World_War_II"),
    ("Photosynthesis",                "Photosynthesis",                "Photosynthesis"),
    ("Marie Curie",                   "Marie Curie",                   "Marie_Curie"),
    ("Moon",                          "Moon",                          "Moon"),
    ("JavaScript",                    "JavaScript",                    "JavaScript"),
    ("Eiffel Tower",                  "Eiffel Tower",                  "Eiffel_Tower"),
    ("Black hole",                    "Black hole",                    "Black_hole"),
    ("Charles Darwin",                "Charles Darwin",                "Charles_Darwin"),
    ("Artificial intelligence",       "Artificial intelligence",       "Artificial_intelligence"),
    ("DNA",                           "DNA",                           "DNA"),
    ("Mount Everest",                 "Mount Everest",                 "Mount_Everest"),
    ("Isaac Newton",                  "Isaac Newton",                  "Isaac_Newton"),
    ("Solar System",                  "Solar System",                  "Solar_System"),
    ("Great Wall of China",           "Great Wall of China",           "Great_Wall_of_China"),
    ("William Shakespeare",           "William Shakespeare",           "William_Shakespeare"),
    ("Amazon River",                  "Amazon River",                  "Amazon_River"),
    ("Quantum mechanics",             "Quantum mechanics",             "Quantum_mechanics"),
    ("Napoleon",                      "Napoleon",                      "Napoleon"),
]

# Post titles generated for template_5 β€” not fetched from the live app
FORUM_POST_TITLES = [
    "Thoughts on the latest developments in AI safety",
    "Best practices for remote work in 2026",
    "How do you stay motivated when learning a new skill?",
    "What are your favourite open-source projects right now?",
    "Underrated books that changed how you think",
    "Tips for beginner photographers β€” what I wish I knew",
    "The most interesting science paper I read this week",
    "Ask me anything about Python performance tuning",
    "Weekly discussion: what are you building this month?",
    "Hidden gems in streaming music you should know about",
    "Travel destinations that are worth the hype",
    "How to cook a perfect risotto β€” my method after 10 attempts",
    "What sport have you picked up recently and why?",
    "Recommend a documentary that genuinely surprised you",
    "Discussion: is functional programming overrated?",
    "Things that made you better at managing personal finance",
    "The weirdest film you watched and actually enjoyed",
    "My experience switching from VS Code to a different editor",
    "Why I started journaling and what changed",
    "Gaming setup upgrades that actually made a difference",
]

# ── Helpers ───────────────────────────────────────────────────────────────────

def base_url(host: str, app: str) -> str:
    return f"http://{host}:{PORTS[app]}"


def get_admin_token(host: str) -> str:
    url = f"{base_url(host, 'shopping_admin')}/rest/V1/integration/admin/token"
    resp = requests.post(url, json={"username": ADMIN_USER, "password": ADMIN_PASS}, timeout=10)
    resp.raise_for_status()
    return resp.json()


def admin_get(host: str, path: str, token: str, params: dict = None):
    url = f"{base_url(host, 'shopping_admin')}{path}"
    resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}, params=params, timeout=15)
    resp.raise_for_status()
    return resp.json()


# ── Pool builders ─────────────────────────────────────────────────────────────

def build_category_pool(host: str, token: str) -> list:
    """Template 1: leaf categories from GET /rest/V1/categories/list."""
    data = admin_get(host, "/rest/V1/categories/list", token, params={"searchCriteria[pageSize]": 500})
    items = data.get("items", [])
    pool = []
    for item in items:
        # include all named categories; caller can filter to leaf nodes if needed
        if item.get("name") and item.get("id"):
            pool.append({"name": item["name"], "category_id": item["id"]})
    return pool


def build_product_pool(host: str, token: str, max_items: int = 50) -> list:
    """Templates 3 & 6: simple, in-stock products."""
    data = admin_get(host, "/rest/V1/products", token, params={
        "searchCriteria[filterGroups][0][filters][0][field]": "type_id",
        "searchCriteria[filterGroups][0][filters][0][value]": "simple",
        "searchCriteria[filterGroups][0][filters][0][conditionType]": "eq",
        "searchCriteria[pageSize]": max_items,
    })
    items = data.get("items", [])
    pool = []
    for item in items:
        name = item.get("name", "").strip()
        sku  = item.get("sku", "").strip()
        if name and sku:
            pool.append({"name": name, "sku": sku})
    return pool


def build_wikipedia_pool(host: str) -> list:
    """Template 2: verify known articles exist in the ZIM snapshot."""
    base = base_url(host, "wikipedia")
    verified = []
    for display, search_query, expected_slug in WIKIPEDIA_TITLES:
        check_url = f"{base}/wikipedia_en_all_maxi_2022-05/A/{expected_slug}"
        try:
            r = requests.head(check_url, timeout=8, allow_redirects=True)
            if r.status_code == 200:
                verified.append({
                    "display": display,
                    "search_query": search_query,
                    "expected_slug": expected_slug,
                })
            else:
                print(f"  [wikipedia] WARNING: {expected_slug} β†’ HTTP {r.status_code}, skipping")
        except Exception as e:
            print(f"  [wikipedia] WARNING: could not reach {check_url}: {e}")
    return verified


def build_forum_category_pool(host: str) -> list:
    """Templates 4 & 5: forum slugs with at least one submission."""
    base = base_url(host, "forum")
    pool = []
    page = 1
    while True:
        try:
            r = requests.get(f"{base}/api/forums", params={"page": page}, timeout=10)
            r.raise_for_status()
            data = r.json()
        except Exception as e:
            print(f"  [forum] WARNING: could not reach forums API: {e}")
            break
        items = data if isinstance(data, list) else data.get("items", data.get("forums", []))
        if not items:
            break
        for item in items:
            name = item.get("name") or item.get("forum_name") or item.get("normalizedName")
            display = item.get("title") or item.get("displayName") or name
            if name:
                pool.append({"forum_name": name, "display_name": display or name})
        if len(items) < 20:
            break
        page += 1
    # deduplicate by forum_name
    seen = set()
    deduped = []
    for entry in pool:
        if entry["forum_name"] not in seen:
            seen.add(entry["forum_name"])
            deduped.append(entry)
    return deduped


# ── Template 7 pool ───────────────────────────────────────────────────────────

def build_admin_product_pool() -> list:
    """Template 7: fully generated SKU/price/name tuples. No API call needed."""
    specs = [
        ("HAR-TEST-001", 19.99,  "HAR Training Widget Alpha"),
        ("HAR-TEST-002", 34.50,  "HAR Training Widget Beta"),
        ("HAR-TEST-003", 9.99,   "HAR Economy Pack"),
        ("HAR-TEST-004", 49.00,  "HAR Premium Kit"),
        ("HAR-TEST-005", 7.75,   "HAR Starter Bundle"),
        ("HAR-TEST-006", 129.00, "HAR Deluxe Set"),
        ("HAR-TEST-007", 22.00,  "HAR Standard Unit"),
        ("HAR-TEST-008", 14.95,  "HAR Basic Module"),
        ("HAR-TEST-009", 59.99,  "HAR Advanced Pack"),
        ("HAR-TEST-010", 3.50,   "HAR Mini Component"),
        ("HAR-TEST-011", 89.00,  "HAR Pro Edition"),
        ("HAR-TEST-012", 11.25,  "HAR Lite Version"),
        ("HAR-TEST-013", 199.99, "HAR Enterprise Module"),
        ("HAR-TEST-014", 6.00,   "HAR Sample Item"),
        ("HAR-TEST-015", 45.00,  "HAR Mid-Range Pack"),
        ("HAR-TEST-016", 25.00,  "HAR Core Component"),
        ("HAR-TEST-017", 75.00,  "HAR Extended Kit"),
        ("HAR-TEST-018", 18.50,  "HAR Value Bundle"),
        ("HAR-TEST-019", 99.00,  "HAR Complete Suite"),
        ("HAR-TEST-020", 2.99,   "HAR Micro Unit"),
    ]
    return [{"sku": sku, "price": price, "product_name": name} for sku, price, name in specs]


# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Build HARvestGym parameter pools from live EC2 apps.")
    parser.add_argument("--host",   default="ec2-16-59-2-56.us-east-2.compute.amazonaws.com")
    parser.add_argument("--output", default="parameter_pools.json")
    args = parser.parse_args()

    host   = args.host
    output = Path(args.output)

    print(f"Building parameter pools β€” host: {host}\n")

    # Admin token (needed for Shopping endpoints)
    print("[1/7] Fetching admin token...")
    token = get_admin_token(host)
    print("      OK\n")

    # Template 1 β€” category pool
    print("[2/7] Template 1: Shopping categories...")
    cat_pool = build_category_pool(host, token)
    print(f"      {len(cat_pool)} categories found\n")

    # Templates 3 & 6 β€” product pool
    print("[3/7] Templates 3 & 6: Shopping products (simple, in-stock)...")
    prod_pool = build_product_pool(host, token)
    print(f"      {len(prod_pool)} products found\n")

    # Template 2 β€” Wikipedia
    print("[4/7] Template 2: Verifying Wikipedia articles...")
    wiki_pool = build_wikipedia_pool(host)
    print(f"      {len(wiki_pool)} articles verified\n")

    # Templates 4 & 5 β€” Forum categories
    print("[5/7] Templates 4 & 5: Forum categories...")
    forum_pool = build_forum_category_pool(host)
    # template_5 category pool excludes any image-only forums β€” same list for now
    forum_pool_t5 = forum_pool
    print(f"      {len(forum_pool)} forums found\n")

    # Template 5 β€” post titles (static)
    print("[6/7] Template 5: Post titles (static list, no API call)...")
    print(f"      {len(FORUM_POST_TITLES)} titles loaded\n")

    # Template 7 β€” admin product specs (static)
    print("[7/7] Template 7: Admin product specs (generated, no API call)...")
    admin_pool = build_admin_product_pool()
    print(f"      {len(admin_pool)} product specs loaded\n")

    # ── Assemble output ───────────────────────────────────────────────────────
    pools = {
        "_meta": {
            "description": "Static parameter pools for the 7 HARvestGym task templates.",
            "generated_at": str(date.today()),
            "generated_from_host": host,
            "how_to_refresh": "python scripts/build_parameter_pools.py --host <EC2_HOST>",
            "source_apps": {
                "shopping":       f"http://{host}:{PORTS['shopping']}/",
                "shopping_admin": f"http://{host}:{PORTS['shopping_admin']}/admin",
                "forum":          f"http://{host}:{PORTS['forum']}/",
                "wikipedia":      f"http://{host}:{PORTS['wikipedia']}/",
            },
        },
        "template_1": {
            "description": "List products in category {category_name}",
            "tier": "Easy",
            "app": "shopping",
            "slots": ["category_name"],
            "source_endpoint": "GET /rest/V1/categories/list?searchCriteria[pageSize]=500",
            "note": "Only leaf categories are meaningful for product listing tasks. category_id is stored for grader use β€” not exposed in the task string.",
            "pool": {"category_name": cat_pool},
        },
        "template_2": {
            "description": "Retrieve article summary for {title}",
            "tier": "Easy",
            "app": "wikipedia",
            "slots": ["title"],
            "source_endpoint": "HEAD /wikipedia_en_all_maxi_2022-05/A/{slug} (verification only)",
            "note": "expected_slug is stored for grader verification. The agent must derive the slug independently via GET /search.",
            "pool": {"title": wiki_pool},
        },
        "template_3": {
            "description": "Add {product_name} to a guest cart",
            "tier": "Medium",
            "app": "shopping",
            "slots": ["product_name"],
            "source_endpoint": "GET /rest/V1/products?searchCriteria[pageSize]=50 (simple, in-stock only)",
            "note": "SKU stored for grader use β€” agent must independently discover it via product search.",
            "pool": {"product_name": prod_pool},
        },
        "template_4": {
            "description": "Retrieve all posts in {forum_category} (authed)",
            "tier": "Medium",
            "app": "forum",
            "slots": ["forum_category"],
            "source_endpoint": "GET /api/forums?page=1",
            "note": "forum_name is the URL slug; display_name is the human-readable label.",
            "pool": {"forum_category": forum_pool},
        },
        "template_5": {
            "description": "Create a post titled {title} in {category}",
            "tier": "Hard",
            "app": "forum",
            "slots": ["title", "category"],
            "source_endpoint": "GET /api/forums?page=1 (for category); titles are generated",
            "note": "title and category are sampled independently. category list excludes any image-only forums.",
            "pool": {
                "title":    FORUM_POST_TITLES,
                "category": forum_pool_t5,
            },
        },
        "template_6": {
            "description": "Guest checkout for {product_name}",
            "tier": "Hard",
            "app": "shopping",
            "slots": ["product_name"],
            "source_endpoint": "GET /rest/V1/products?searchCriteria[pageSize]=50 (same pool as template_3)",
            "note": "Guest checkout email is always test@example.com (STATIC). Grader queries /rest/V1/orders by email to confirm order creation.",
            "pool": {"product_name": prod_pool},
        },
        "template_7": {
            "description": "Create a new product with SKU {sku}, price {price}",
            "tier": "Hard",
            "app": "shopping_admin",
            "slots": ["sku", "price", "product_name"],
            "source_endpoint": "Fully generated β€” SKUs follow HAR-XXXXX pattern, no collision with existing catalog.",
            "note": "All slots sampled together as a product_spec tuple. attribute_set_id=4 (Default) is STATIC. Grader calls GET /rest/V1/products/{sku} to verify creation.",
            "pool": {"product_spec": admin_pool},
        },
    }

    output.write_text(json.dumps(pools, indent=2))
    print(f"Written to {output}  ({output.stat().st_size:,} bytes)")

    # Summary
    print("\n=== POOL SUMMARY ===")
    for tid in [k for k in pools if k.startswith("template")]:
        t = pools[tid]
        counts = {slot: len(vals) for slot, vals in t["pool"].items()}
        print(f"  {tid}: {counts}")


if __name__ == "__main__":
    main()