File size: 43,233 Bytes
dfd4feb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
"""
data_generators.py β€” Deterministic procedural data generators for Meta v3.1
Replaces small hand-crafted datasets with richly varied, reproducible data.

Design principles
-----------------
* Every generator takes an `episode_id: str` and seeds Python's stdlib `random`
  with `hash(episode_id + salt)` so the same episode always yields the same data.
* No third-party libs needed β€” stdlib only (random, string, datetime, hashlib).
* Each generator returns (agent_ctx, grader_ctx) tuples matching existing grader
  contracts so graders need zero changes.

Drop-in replacement usage in Meta_environment.py::_load_context():
    Replace the hardcoded-data blocks with:
        from data_generators import gen_<task>(episode_id)
        agent_ctx, grader_ctx = gen_<task>(episode_id)

Generators provided
-------------------
Email domain
    gen_email_easy(episode_id)     β€” 1 email, 3-way class, 30 sender/subject combos
    gen_email_medium(episode_id)   β€” 10 emails, varying priority pool
    gen_email_hard(episode_id)     β€” customer complaint, 8 scenario templates

Code domain
    gen_code_easy(episode_id)      β€” 6 code snippet variants
    gen_code_medium(episode_id)    β€” deterministic (same code, no variant needed)
    gen_code_hard(episode_id)      β€” deterministic (same code, no variant needed)

Data-cleaning domain
    gen_data_easy(episode_id)      β€” 6-10 rows, random nulls + one dup pair
    gen_data_medium(episode_id)    β€” 5 rows, randomised messy formats per field
    gen_data_hard(episode_id)      β€” 12-16 rows, IQR-detected outliers

Content moderation domain
    gen_mod_easy(episode_id)       β€” 7 posts drawn from larger pool
    gen_mod_medium(episode_id)     β€” 8 posts drawn from larger pool
    (hard is already adversarial/static β€” left unchanged)

Ticket domain
    gen_ticket_easy(episode_id)    β€” 1 ticket from 8-variant pool
    gen_ticket_medium(episode_id)  β€” deterministic (same 8 tickets, no variant)
    gen_ticket_hard(episode_id)    β€” deterministic (same 6 incidents, no variant)
"""

from __future__ import annotations

import hashlib
import random
import string
from copy import deepcopy
from datetime import date, timedelta
from typing import Any


# ── Seeding helper ─────────────────────────────────────────────────────────────

def _rng(episode_id: str, salt: str = "") -> random.Random:
    seed = int(hashlib.md5((episode_id + salt).encode()).hexdigest(), 16)
    return random.Random(seed)


# ═══════════════════════════════════════════════════════════════════════════════
# EMAIL EASY  β€” 30+ variant pool
# ═══════════════════════════════════════════════════════════════════════════════

_EMAIL_EASY_POOL = [
    # (subject, sender, body, label)
    ("You won $1,000,000!!!", "promo@totally-legit-lottery.com",
     "Click here to claim your prize now! Limited time offer!", "spam"),
    ("Q3 Budget Review β€” Urgent Action Required", "cfo@company.com",
     "Please review the attached Q3 budget before our board meeting tomorrow at 9 AM.", "important"),
    ("Your weekly digest from Medium", "noreply@medium.com",
     "Top stories this week: AI trends, Python tips, and startup news.", "newsletter"),
    ("CONGRATULATIONS! iPhone 15 Pro is yours!", "winner@prize-alert.net",
     "You have been selected. Click now to claim your free iPhone!", "spam"),
    ("Action Required: Sign the NDA before Friday", "legal@company.com",
     "Please sign the attached NDA before Friday's partnership meeting. Deadline is strict.", "important"),
    ("TechCrunch Newsletter β€” March 2026", "newsletter@techcrunch.com",
     "This week's top stories in tech, AI, and startups.", "newsletter"),
    ("Claim your $500 Amazon gift card NOW", "deals@amazingoffers.biz",
     "You are today's lucky winner. No purchase necessary. Act fast!", "spam"),
    ("Production incident P1 β€” please join bridge", "oncall@company.com",
     "We have a P1 production incident. Please join the war room immediately.", "important"),
    ("Developer digest: April 2026", "digest@devweekly.io",
     "This week: Rust 2.0 release, LLM benchmarks, and open-source highlights.", "newsletter"),
    ("FREE vacation cruise β€” you've been selected!", "winner@vacation-cruise.net",
     "Claim your free cruise for two. Just pay shipping and handling.", "spam"),
    ("Board approval needed: Series B term sheet", "ceo@company.com",
     "Our Series B term sheet requires your signature before the 5 PM deadline today.", "important"),
    ("Product Hunt daily digest", "digest@producthunt.com",
     "Today's top products: AI code review, async stand-ups, no-code CRM.", "newsletter"),
    ("Congratulations β€” Survey reward $250!", "rewards@surveypanel.net",
     "You qualify for a $250 reward. Complete our 2-minute survey to claim.", "spam"),
    ("HR: Performance review cycle starts Monday", "hr@company.com",
     "The annual performance review cycle begins Monday. Please complete your self-assessment.", "important"),
    ("Hacker News digest: top 10 links this week", "weekly@hackernews.app",
     "Rust, WASM, and the AI bubble: top discussions from the community.", "newsletter"),
    ("Double billing detected on account #AC-8821", "billing@ourplatform.com",
     "We noticed a duplicate charge on your account. Reply to initiate a refund.", "spam"),
    ("Legal: Partnership agreement signature required EOD", "legal@company.com",
     "The partnership agreement with Nexus Corp must be countersigned by 5 PM today.", "important"),
    ("SaaS Weekly β€” your industry roundup", "editor@saasweekly.com",
     "Churn rates, AI copilots, and the future of vertical SaaS.", "newsletter"),
    ("You have unclaimed crypto rewards!", "support@cryptorewards.io",
     "Wallet 0x4f…a2 has 0.3 ETH waiting. Connect now to claim.", "spam"),
    ("Security alert: admin password reset requested", "security@company.com",
     "An admin-level password reset was requested from IP 198.51.100.42. Verify or block.", "important"),
    ("The Batch β€” AI newsletter by deeplearning.ai", "batch@deeplearning.ai",
     "Andrew Ng on scaling laws, fine-tuning tips, and this week's papers.", "newsletter"),
    ("Limited offer: 90% off VPN subscription", "deals@vpndeals.biz",
     "Today only: get lifetime VPN access for $1.99. Offer expires midnight.", "spam"),
    ("Vendor invoice #INV-7721 overdue by 14 days", "ar@vendor.com",
     "Invoice #INV-7721 for $12,400 is now 14 days overdue. Please remit payment to avoid service interruption.", "important"),
    ("The Algorithm Newsletter β€” weekly edition", "hello@thealgorithm.co",
     "ML papers, job listings, and a tutorial on RLHF.", "newsletter"),
    ("Your package was held β€” verify address", "delivery@postalnow.net",
     "Your package is held due to an address discrepancy. Click to confirm.", "spam"),
    ("Compliance: GDPR audit documentation due Friday", "compliance@company.com",
     "Please upload your GDPR data-processing records to the compliance portal by Friday.", "important"),
    ("Morning Brew β€” your daily business digest", "hello@morningbrew.com",
     "Markets, tech, and the big stories you need to know today.", "newsletter"),
    ("Account suspended β€” click to restore", "support@accountalert.biz",
     "Unusual activity detected. Your account has been temporarily suspended.", "spam"),
    ("CTO sync needed: infrastructure cost overrun", "cto@company.com",
     "AWS bill came in 340% over budget. Need your sign-off on emergency cost controls.", "important"),
    ("The Hustle β€” daily newsletter", "news@thehustle.co",
     "Today's biggest business stories, curated for founders and operators.", "newsletter"),
]


def gen_email_easy(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "email_easy")
    email = dict(zip(["subject", "sender", "body", "label"],
                     rng.choice(_EMAIL_EASY_POOL)))
    email["id"] = "e1"
    agent_ctx  = {"email": {k: v for k, v in email.items() if k != "label"}}
    grader_ctx = {"email": email}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# EMAIL MEDIUM  β€” varied priority pools
# ═══════════════════════════════════════════════════════════════════════════════

_CRITICAL_POOL = [
    {"subject": "[ALERT] Production server DOWN", "sender": "ops@company.com",
     "body": "All services unresponsive since 2 AM. Revenue impact: $10k/min.", "priority": 1, "category": "critical"},
    {"subject": "[P1] Database primary down β€” all writes failing", "sender": "oncall@company.com",
     "body": "The primary DB replica crashed. No write operations possible site-wide.", "priority": 1, "category": "critical"},
    {"subject": "[WARNING] Security alert: unusual login attempt", "sender": "security@company.com",
     "body": "Unauthorised login attempt from IP 45.33.32.156. Verify immediately.", "priority": 1, "category": "critical"},
    {"subject": "[ALERT] DDoS attack in progress", "sender": "security@company.com",
     "body": "We are under a DDoS attack. Site response time is 30s+. Join the war room.", "priority": 1, "category": "critical"},
]

_URGENT_POOL = [
    {"subject": "Acme Corp contract renewal β€” URGENT", "sender": "sales@company.com",
     "body": "Acme contract expires Friday. They need signature or they walk.", "priority": 2, "category": "business"},
    {"subject": "Legal: Partnership agreement needs signature TODAY", "sender": "legal@company.com",
     "body": "The partnership agreement must be countersigned by EOD or the deal falls through.", "priority": 2, "category": "legal"},
    {"subject": "Series B term sheet β€” board sign-off required by 5 PM", "sender": "ceo@company.com",
     "body": "VCs need the countersigned term sheet by 5 PM today or the round lapses.", "priority": 2, "category": "legal"},
]

_FINANCE_POOL = [
    {"subject": "Invoice #INV-4521 overdue", "sender": "billing@vendor.com",
     "body": "Invoice of $5,400 was due 3 days ago. Payment needed to avoid suspension.", "priority": 3, "category": "finance"},
    {"subject": "AWS bill 340% over budget", "sender": "finance@company.com",
     "body": "Last month's cloud bill was $82k vs $24k budget. Need approval to proceed.", "priority": 3, "category": "finance"},
    {"subject": "Customer double-charged β€” refund dispute open", "sender": "billing@company.com",
     "body": "Customer #C-1121 was charged twice. Finance blocked. Needs VP sign-off.", "priority": 3, "category": "finance"},
]

_BUSINESS_POOL = [
    {"subject": "Q4 product launch deck β€” review needed", "sender": "marketing@company.com",
     "body": "Please review launch slides before Thursday. Launch is next Monday.", "priority": 4, "category": "business"},
    {"subject": "Quarterly board deck β€” slides need your input", "sender": "ea@company.com",
     "body": "Board meeting is Tuesday. Your section on growth metrics needs updating.", "priority": 4, "category": "business"},
    {"subject": "Key account at risk β€” needs executive call", "sender": "cs@company.com",
     "body": "TechGiant Corp is threatening churn. They want an exec call this week.", "priority": 4, "category": "business"},
]

_SOCIAL_POOL = [
    {"subject": "Team lunch this Friday?", "sender": "colleague@company.com",
     "body": "Hey, thinking of doing a team lunch Friday. You in?", "priority": 9, "category": "social"},
    {"subject": "Happy Work Anniversary!", "sender": "hr@company.com",
     "body": "Celebrating your 3 years with us!", "priority": 9, "category": "social"},
    {"subject": "Office trivia night β€” Thursday 7 PM", "sender": "social@company.com",
     "body": "Join us for office trivia Thursday evening. Pizza provided!", "priority": 9, "category": "social"},
]

_NEWSLETTER_POOL = [
    {"subject": "Weekly SaaS digest", "sender": "news@saasdigest.com",
     "body": "Top SaaS stories this week...", "priority": 10, "category": "newsletter"},
    {"subject": "Free AI productivity webinar", "sender": "promo@webinar.io",
     "body": "Join our free webinar on AI tools this Thursday.", "priority": 10, "category": "newsletter"},
    {"subject": "The Batch: AI news weekly", "sender": "batch@deeplearning.ai",
     "body": "Andrew Ng on scaling, fine-tuning, and paper highlights.", "priority": 10, "category": "newsletter"},
]


def gen_email_medium(episode_id: str) -> tuple[dict, dict]:
    """
    Always picks 2 critical, 1 urgent, 1 finance, 2 business, 2 social, 2 newsletter = 10 emails.
    Correct ordering: criticals first, then urgent, finance, business, social, newsletter.
    """
    rng = _rng(episode_id, "email_medium")
    crits   = rng.sample(_CRITICAL_POOL, k=min(2, len(_CRITICAL_POOL)))
    urgents = rng.sample(_URGENT_POOL,   k=min(1, len(_URGENT_POOL)))
    fins    = rng.sample(_FINANCE_POOL,  k=min(1, len(_FINANCE_POOL)))
    bizs    = rng.sample(_BUSINESS_POOL, k=min(2, len(_BUSINESS_POOL)))
    socials = rng.sample(_SOCIAL_POOL,   k=min(2, len(_SOCIAL_POOL)))
    news    = rng.sample(_NEWSLETTER_POOL,k=min(2, len(_NEWSLETTER_POOL)))

    ordered = crits + urgents + fins + bizs + socials + news
    emails = []
    correct_order = []
    for i, e in enumerate(ordered):
        eid = f"m{i+1}"
        emails.append({"id": eid, **e})
        correct_order.append(eid)

    # Shuffle for presentation; keep correct_order as ground truth
    display = emails[:]
    rng.shuffle(display)

    agent_ctx  = {"emails": [{k: v for k, v in e.items() if k not in ("priority", "category")} for e in display]}
    grader_ctx = {"emails": emails, "correct_order": correct_order}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# EMAIL HARD  β€” 8 complaint scenario templates
# ═══════════════════════════════════════════════════════════════════════════════

_EMAIL_HARD_TEMPLATES = [
    {
        "subject": "Completely unacceptable β€” Order #{order_id}",
        "sender": "furious.customer@email.com",
        "body": (
            "I have been a loyal customer for 5 years and I am absolutely furious. "
            "My order #{order_id} arrived broken for the SECOND time this month. "
            "Your support team has been dismissive and unhelpful every time I called. "
            "I demand a full refund, a replacement sent overnight, AND compensation. "
            "If this is not resolved within 24 hours, I will file a consumer court complaint."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "frustrat", "disappoint", "concern", "feel"],
            "apologize": ["sorry", "apologize", "apology", "sincerely"],
            "offer_refund": ["refund", "money back", "full refund"],
            "offer_replacement": ["replacement", "replace", "new order", "send another", "overnight"],
            "provide_timeline": ["24 hours", "48 hours", "within", "today", "tomorrow", "business day"],
            "professional_tone": None,
            "compensation_acknowledged": ["compensat", "inconvenience", "make it right", "goodwill"],
        },
    },
    {
        "subject": "Billing error β€” charged twice for subscription",
        "sender": "billing.complaint@customer.com",
        "body": (
            "I was charged twice for my annual subscription this month β€” once on March 1st "
            "and again on March 15th. I have the bank statements as proof. "
            "This is a clear billing error on your end. I need an immediate refund of the "
            "duplicate charge and a written confirmation that this won't happen again. "
            "I've been waiting 2 weeks for a response from your support team."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "concern", "apologize", "sorry"],
            "apologize": ["sorry", "apologize", "apology"],
            "offer_refund": ["refund", "reimburse", "credit"],
            "confirm_investigation": ["investigat", "look into", "check", "review", "verify"],
            "provide_timeline": ["within", "hours", "days", "business day"],
            "professional_tone": None,
            "written_confirmation": ["confirm", "written", "email confirmation", "receipt"],
        },
    },
    {
        "subject": "Data breach β€” my account was hacked",
        "sender": "angry.user@personal.com",
        "body": (
            "I just discovered that my account was used to make purchases I never authorised. "
            "Someone accessed my account and changed my email and password. "
            "I was only able to regain access because I remembered my security questions. "
            "This is a serious security failure on your part. I demand to know what happened, "
            "how my data was exposed, and what you will do to protect me going forward. "
            "If I do not hear back within 24 hours, I will contact the data protection authority."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "serious", "concern", "alarming"],
            "apologize": ["sorry", "apologize", "apology"],
            "confirm_investigation": ["investigat", "security team", "look into", "review"],
            "offer_remediation": ["secure", "reset", "protect", "steps", "safeguard"],
            "provide_timeline": ["24 hours", "48 hours", "within", "shortly", "immediately"],
            "professional_tone": None,
            "regulatory_mention": ["authority", "regulat", "notify", "report", "transparent"],
        },
    },
    {
        "subject": "Defective product β€” 3 replacements still broken",
        "sender": "frustrated.buyer@email.com",
        "body": (
            "This is my THIRD replacement unit and it is still defective. "
            "The first unit arrived with a cracked screen. The second had a faulty battery. "
            "This third unit powers off randomly. "
            "I have spent over 6 hours on support calls. I want a full refund immediately β€” "
            "no more replacements. I will also be leaving reviews on every platform I can find."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "frustrat", "disappoint", "three", "3"],
            "apologize": ["sorry", "apologize", "sincerely"],
            "offer_refund": ["refund", "full refund", "money back"],
            "no_more_replacement_push": ["will not", "no further", "respect", "understand", "preference"],
            "provide_timeline": ["within", "24 hours", "48 hours", "business day"],
            "professional_tone": None,
            "de_escalation": ["value", "loyal", "appreciate", "important to us"],
        },
    },
    {
        "subject": "Package lost β€” 3 weeks and still nothing",
        "sender": "lost.package@customer.net",
        "body": (
            "I placed an order 3 weeks ago and my package has still not arrived. "
            "The tracking has shown 'In Transit' for 18 days without any movement. "
            "I have contacted your support team 4 times. Each time I am told to 'wait a few more days'. "
            "I need this item for an event next week. Either find the package, send a replacement now, "
            "or give me a full refund so I can purchase elsewhere. This is unacceptable."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "frustrat", "waiting", "3 weeks", "three weeks"],
            "apologize": ["sorry", "apologize", "apology"],
            "offer_refund": ["refund", "full refund"],
            "offer_replacement": ["replacement", "resend", "new shipment", "send another"],
            "provide_timeline": ["24 hours", "48 hours", "immediately", "today", "next day"],
            "professional_tone": None,
            "carrier_investigation": ["carrier", "investigate", "trace", "courier", "tracking"],
        },
    },
    {
        "subject": "Cancelled subscription still being charged",
        "sender": "cancelled.user@email.com",
        "body": (
            "I cancelled my subscription on January 15th and have proof of the cancellation email. "
            "However, I have been charged $99/month for the past 3 months since then. "
            "Total unauthorised charges: $297. I want a full refund of all three charges immediately. "
            "If this is not resolved within 48 hours, I will initiate a chargeback with my bank "
            "and report this to the consumer protection agency."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "concern", "apologize", "recognised"],
            "apologize": ["sorry", "apologize", "apology"],
            "offer_refund": ["refund", "full refund", "$297", "three charges", "all charges"],
            "confirm_cancellation": ["confirm", "cancellation", "verified", "record"],
            "provide_timeline": ["48 hours", "24 hours", "immediately", "within"],
            "professional_tone": None,
            "prevent_recurrence": ["prevent", "ensure", "will not happen", "steps", "corrective"],
        },
    },
    {
        "subject": "Discriminatory treatment by your staff",
        "sender": "discrimination.complaint@email.com",
        "body": (
            "I visited your store on April 2nd and was followed around by a staff member "
            "while other customers were not treated this way. When I raised this with the manager, "
            "I was told 'it's just store policy'. This felt discriminatory and I am deeply hurt. "
            "I expect a formal apology, an explanation of your training policies, "
            "and assurance that this will not happen to any customer again."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "serious", "concern", "upset", "hurt"],
            "apologize": ["sorry", "apologize", "sincerely", "formal apology"],
            "confirm_investigation": ["investigat", "review", "look into", "hr", "management"],
            "policy_commitment": ["policy", "training", "standards", "commit", "improve"],
            "provide_timeline": ["within", "48 hours", "days", "shortly"],
            "professional_tone": None,
            "empathy_specific": ["dignity", "respect", "every customer", "valued"],
        },
    },
    {
        "subject": "Software destroyed my data β€” urgent",
        "sender": "data.loss@business.com",
        "body": (
            "Your software update on March 28th corrupted our entire database backup. "
            "We lost 6 months of customer records. Our business has been at a standstill for 3 days. "
            "We are a paying enterprise customer ($24,000/year). "
            "I need immediate escalation to your engineering team, a post-mortem report, "
            "and a discussion about compensation for our business losses."
        ),
        "expected_elements": {
            "acknowledge_frustration": ["understand", "serious", "impact", "significant", "recognise"],
            "apologize": ["sorry", "sincerely", "deeply apologize"],
            "escalate": ["engineering", "escalate", "senior", "team", "immediately"],
            "post_mortem": ["post-mortem", "root cause", "analysis", "report", "findings"],
            "provide_timeline": ["24 hours", "immediately", "today", "within"],
            "professional_tone": None,
            "compensation_acknowledged": ["compensat", "discuss", "impact", "losses", "make it right"],
        },
    },
]


def gen_email_hard(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "email_hard")
    template = rng.choice(_EMAIL_HARD_TEMPLATES)
    order_id = rng.randint(10000, 99999)
    subject  = template["subject"].format(order_id=order_id)
    body     = template["body"].format(order_id=order_id)
    case = {**template, "subject": subject, "body": body, "sender": template["sender"], "id": "h1"}
    agent_ctx  = {"email": {k: v for k, v in case.items() if k not in ("expected_elements", "id")}}
    grader_ctx = {"email": case}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# CODE EASY  β€” 6 snippet variants
# ═══════════════════════════════════════════════════════════════════════════════

_CODE_EASY_POOL = [
    {"code": "def calculate_average(numbers):\n    total = 0\n    for num in numbers\n        total += num\n    return total / len(numbers)\n",
     "keywords": ["colon", "for loop", "syntax", "for statement"]},
    {"code": "def greet(name)\n    print(f'Hello, {name}!')\n\ngreet('World')\n",
     "keywords": ["colon", "def", "function", "syntax"]},
    {"code": "numbers = [1, 2, 3, 4, 5]\nif len(numbers) > 0\n    print('Not empty')\n",
     "keywords": ["colon", "if statement", "syntax"]},
    {"code": "class Animal:\n    def __init__(self, name)\n        self.name = name\n\n    def speak(self):\n        return f'{self.name} speaks'\n",
     "keywords": ["colon", "__init__", "constructor", "function", "def"]},
    {"code": "def read_file(path):\n    with open(path) as f\n        return f.read()\n",
     "keywords": ["colon", "with statement", "context manager", "syntax"]},
    {"code": "def retry(fn, times=3):\n    for i in range(times)\n        try:\n            return fn()\n        except Exception:\n            pass\n",
     "keywords": ["colon", "for loop", "range", "syntax"]},
]


def gen_code_easy(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "code_easy")
    variant    = rng.choice(_CODE_EASY_POOL)
    agent_ctx  = {"code": variant["code"]}
    grader_ctx = {"code": variant["code"], "keywords": variant["keywords"]}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# DATA EASY  β€” procedurally generated 6-10 rows
# ═══════════════════════════════════════════════════════════════════════════════

_FIRST_NAMES = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Hiro", "Iris", "Jake"]
_DOMAINS = ["example.com", "company.net", "test.org", "work.io"]


def gen_data_easy(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "data_easy")
    n = rng.randint(6, 10)
    names = rng.sample(_FIRST_NAMES, k=n)
    base_salary = 50000

    rows = []
    for i, name in enumerate(names, 1):
        rows.append({
            "id": i,
            "name": name,
            "age": rng.randint(22, 55),
            "email": f"{name.lower()}@{rng.choice(_DOMAINS)}",
            "salary": base_salary + rng.randint(0, 50) * 1000,
        })

    # Inject 3-4 nulls in distinct rows and fields
    null_fields = rng.sample(["age", "name", "email", "salary"], k=rng.randint(3, 4))
    null_rows   = rng.sample(list(range(n)), k=len(null_fields))
    missing_answers = []
    for row_idx, field in zip(null_rows, null_fields):
        rows[row_idx][field] = None
        missing_answers.append(f"{field} (row {row_idx + 1})")

    # Inject one duplicate pair
    dup_src = rng.randint(0, n - 2)
    dup_dst = dup_src + 1
    rows[dup_dst] = {**rows[dup_src], "id": dup_dst + 1}
    dup_ids = sorted([rows[dup_src]["id"], rows[dup_dst]["id"]])

    agent_ctx  = {"data": rows}
    grader_ctx = {
        "data": rows,
        "answers": {"missing": sorted(missing_answers), "duplicates": dup_ids},
    }
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# DATA MEDIUM  β€” randomised messy formats per field
# ═══════════════════════════════════════════════════════════════════════════════

_AGE_STRINGS   = ["thirty", "twenty-two", "forty-five", "nineteen", "fifty"]
_DATE_FORMATS  = [("%Y/%m/%d", "2020/01/15"), ("%d-%m-%Y", "15-03-2021"),
                  ("%Y-%m-%d", "2019-07-22"), ("%Y/%m/%d", "2022/11/01"),
                  ("%d/%m/%Y", "01/06/2023")]
_ACTIVE_MESSY  = ["yes", "no", "TRUE", "FALSE", "1", "0"]


def gen_data_medium(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "data_medium")
    names = rng.sample(_FIRST_NAMES, k=5)

    rows = []
    salaries = [50000, 60000, 70000, 80000, 45000]
    for i, (name, salary) in enumerate(zip(names, salaries), 1):
        # Introduce one of each issue type
        if i == 1:
            age_val = rng.choice(_AGE_STRINGS)        # string age
            sal_val = f"${salary:,}"                   # dollar+comma format
        elif i == 2:
            age_val = 25                               # correct
            sal_val = f"${salary:,}"                   # dollar+comma
        elif i == 3:
            age_val = str(rng.randint(25, 40))         # string digit
            sal_val = str(salary)                      # clean
        elif i == 4:
            age_val = rng.randint(30, 45)              # int β€” correct
            sal_val = f"{salary:,}"                    # comma only
        else:
            age_val = rng.choice(_AGE_STRINGS)         # string age
            sal_val = str(salary)                      # clean

        _, date_str = rng.choice(_DATE_FORMATS)
        name_messy = name.lower() if i == 4 else name.upper() if i == 5 else name
        active_val = rng.choice(_ACTIVE_MESSY) if i in (1, 3, 5) else rng.choice([True, False])

        rows.append({"id": i, "name": name_messy, "age": age_val, "salary": sal_val,
                     "join_date": date_str, "active": active_val})

    issues = {
        "age":       "mixed types: some ages are word strings, should be integers",
        "salary":    "inconsistent formats: dollar signs and commas present in some values",
        "join_date": "three different date formats used across rows",
        "name":      "inconsistent casing: some names lowercase, some all-caps",
        "active":    "mixed types: booleans and strings ('yes','no','TRUE') mixed",
    }
    agent_ctx  = {"data": rows}
    grader_ctx = {"data": rows, "issues": issues}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# DATA HARD  β€” IQR-detectable outliers, missing values
# ═══════════════════════════════════════════════════════════════════════════════

def gen_data_hard(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "data_hard")
    n = rng.randint(12, 16)
    base = round(rng.uniform(9.5, 12.5), 1)
    spread = round(rng.uniform(0.3, 0.8), 2)

    rows = []
    for i in range(1, n + 1):
        val = round(base + rng.gauss(0, spread), 2)
        rows.append({"id": i, "value": val})

    # Pick 2 outlier positions (ensure they're not neighbours)
    positions = list(range(n))
    rng.shuffle(positions)
    o1, o2 = sorted(positions[:2])
    outlier_mult = rng.choice([8, 10, 12, -5, -8])
    rows[o1]["value"] = round(base * abs(outlier_mult) * (1 if outlier_mult > 0 else -1), 1)
    outlier_mult2 = rng.choice([-4, -6, 15, 20])
    rows[o2]["value"] = round(base * abs(outlier_mult2) * (1 if outlier_mult2 > 0 else -1), 1)
    outlier_ids = sorted([o1 + 1, o2 + 1])

    # Pick 2 missing positions (not overlapping outliers)
    available = [i for i in range(n) if i not in (o1, o2)]
    m1, m2 = sorted(rng.sample(available, 2))
    rows[m1]["value"] = None
    rows[m2]["value"] = None
    missing_ids = sorted([m1 + 1, m2 + 1])

    normal_vals = [r["value"] for r in rows if r["value"] is not None and r["id"] not in outlier_ids]
    lo = round(min(normal_vals) - 1.0, 2)
    hi = round(max(normal_vals) + 1.0, 2)

    agent_ctx  = {"data": rows}
    grader_ctx = {
        "data": rows,
        "answers": {
            "outliers":      set(outlier_ids),
            "missing":       set(missing_ids),
            "imputed_range": (lo, hi),
        },
    }
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# CONTENT MODERATION EASY  β€” draw 7 from a pool of 20
# ═══════════════════════════════════════════════════════════════════════════════

_MOD_EASY_POOL = [
    {"text": "I hate you so much, I want to hurt you!",                      "label": "harmful"},
    {"text": "Great weather today! Loving this sunshine",                     "label": "safe"},
    {"text": "All people from that group should be eliminated.",              "label": "harmful"},
    {"text": "Can someone recommend a good Python beginner book?",            "label": "safe"},
    {"text": "You're an absolute idiot and nobody likes you.",               "label": "harmful"},
    {"text": "I just finished a 5k run β€” feeling amazing!",                  "label": "safe"},
    {"text": "People like you should just disappear forever.",               "label": "harmful"},
    {"text": "Just dropped off a care package for my neighbor.",             "label": "safe"},
    {"text": "Kill yourself, nobody would miss you.",                        "label": "harmful"},
    {"text": "Anyone else excited for the new season of that show?",         "label": "safe"},
    {"text": "I'm going to find you and make you pay for this.",             "label": "harmful"},
    {"text": "Finally got my herb garden started!",                          "label": "safe"},
    {"text": "These people deserve to suffer for what they've done.",        "label": "harmful"},
    {"text": "Happy birthday to my best friend!",                            "label": "safe"},
    {"text": "Someone should shoot that politician.",                        "label": "harmful"},
    {"text": "Loving the new coffee shop that opened downtown.",             "label": "safe"},
    {"text": "I hope your family gets what's coming to them.",               "label": "harmful"},
    {"text": "Just hit 100 days of learning Spanish β€” so proud!",           "label": "safe"},
    {"text": "People from that country are all criminals.",                  "label": "harmful"},
    {"text": "The cherry blossoms are out β€” spring is finally here!",       "label": "safe"},
]


def gen_mod_easy(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "mod_easy")
    # Ensure at least 3 harmful, 3 safe among 7
    harmful = [p for p in _MOD_EASY_POOL if p["label"] == "harmful"]
    safe    = [p for p in _MOD_EASY_POOL if p["label"] == "safe"]
    chosen  = rng.sample(harmful, 4) + rng.sample(safe, 3)
    rng.shuffle(chosen)
    posts = [{"id": f"c{i+1}", **p} for i, p in enumerate(chosen)]
    agent_ctx  = {"posts": [{"id": p["id"], "text": p["text"]} for p in posts]}
    grader_ctx = {"posts": posts}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# CONTENT MODERATION MEDIUM  β€” draw 8 from a pool of 20
# ═══════════════════════════════════════════════════════════════════════════════

_MOD_MEDIUM_POOL = [
    {"text": "Oh sure, like THAT went well. Classic genius move.",              "label": "harmful"},
    {"text": "I'm so done with these people. Absolutely insufferable.",         "label": "harmful"},
    {"text": "That presentation was... very unique.",                           "label": "safe"},
    {"text": "Some people just shouldn't be allowed to have opinions.",        "label": "harmful"},
    {"text": "Oh wow, another 'brilliant' idea from upper management",         "label": "harmful"},
    {"text": "Congrats on the promotion! Well deserved!",                      "label": "safe"},
    {"text": "Not everyone is cut out for this kind of work, clearly.",        "label": "harmful"},
    {"text": "Thanks for the feedback, I'll take it into account.",            "label": "safe"},
    {"text": "Some of us actually read the requirements before submitting.",   "label": "harmful"},
    {"text": "Good effort β€” interesting approach to the problem.",             "label": "safe"},
    {"text": "If you'd done even a little research, you'd know better.",       "label": "harmful"},
    {"text": "Really enjoyed the team retrospective today.",                   "label": "safe"},
    {"text": "Must be nice having a job you're actually qualified for.",       "label": "harmful"},
    {"text": "Great work getting the release out on time!",                    "label": "safe"},
    {"text": "Shocking that anyone thought that plan would work.",             "label": "harmful"},
    {"text": "Just wanted to say β€” the onboarding docs are really clear.",    "label": "safe"},
    {"text": "The bar for hiring has clearly dropped.",                        "label": "harmful"},
    {"text": "Appreciate everyone's patience during the migration.",           "label": "safe"},
    {"text": "I see we've decided quantity over quality again.",               "label": "harmful"},
    {"text": "The demo went really smoothly β€” nice job everyone.",            "label": "safe"},
]


def gen_mod_medium(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "mod_medium")
    harmful = [p for p in _MOD_MEDIUM_POOL if p["label"] == "harmful"]
    safe    = [p for p in _MOD_MEDIUM_POOL if p["label"] == "safe"]
    chosen  = rng.sample(harmful, 5) + rng.sample(safe, 3)
    rng.shuffle(chosen)
    posts = [{"id": f"m{i+1}", **p} for i, p in enumerate(chosen)]
    agent_ctx  = {"posts": [{"id": p["id"], "text": p["text"]} for p in posts]}
    grader_ctx = {"posts": posts}
    return agent_ctx, grader_ctx


# ═══════════════════════════════════════════════════════════════════════════════
# TICKET EASY  β€” 8 variant pool (already existed; keep contract-compatible)
# ═══════════════════════════════════════════════════════════════════════════════

_TICKET_EASY_POOL = [
    {"id": "t1", "title": "App crashes on login for all users",
     "description": "Since the 2.3.1 deploy, 100% of login attempts throw a 500 error.",
     "reporter": "ops-monitoring@company.com", "correct_priority": "critical", "correct_category": "bug"},
    {"id": "t2", "title": "Add dark mode to the dashboard",
     "description": "Multiple users have requested a dark mode option.",
     "reporter": "product@company.com", "correct_priority": "low", "correct_category": "feature_request"},
    {"id": "t3", "title": "How do I export my data as CSV?",
     "description": "I can't find the export button anywhere in the UI.",
     "reporter": "user123@customer.com", "correct_priority": "low", "correct_category": "question"},
    {"id": "t4", "title": "Charged twice for last month's invoice",
     "description": "Account #AC-4421 shows two charges of $299 on March 1st.",
     "reporter": "finance@customer.com", "correct_priority": "high", "correct_category": "billing"},
    {"id": "t5", "title": "Memory leak in mobile app β€” crashes after 10 min",
     "description": "The iOS app uses 2 GB RAM then crashes. Reproducible on all devices.",
     "reporter": "qa@company.com", "correct_priority": "high", "correct_category": "bug"},
    {"id": "t6", "title": "Add SSO support for Google Workspace",
     "description": "Enterprise customers are requesting Google SSO. High deal-blocker frequency.",
     "reporter": "sales@company.com", "correct_priority": "medium", "correct_category": "feature_request"},
    {"id": "t7", "title": "Refund policy β€” how long does it take?",
     "description": "A customer is asking about the refund timeline for a cancelled subscription.",
     "reporter": "support@company.com", "correct_priority": "low", "correct_category": "question"},
    {"id": "t8", "title": "Wrong amount charged β€” $0 instead of $499",
     "description": "New enterprise contract shows $0 on the invoice. Revenue loss.",
     "reporter": "billing@company.com", "correct_priority": "critical", "correct_category": "billing"},
]


def gen_ticket_easy(episode_id: str) -> tuple[dict, dict]:
    rng = _rng(episode_id, "ticket_easy")
    ticket     = rng.choice(_TICKET_EASY_POOL)
    agent_ctx  = {"ticket": {k: v for k, v in ticket.items() if k not in ("correct_priority", "correct_category")}}
    grader_ctx = {"ticket": ticket}
    return agent_ctx, grader_ctx