Farbricated commited on
Commit
dfd4feb
Β·
1 Parent(s): 78f22ef
apply_patches.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ apply_patches.py β€” Automatically applies all 4 fixes to Meta OpenEnv v3.0
4
+
5
+ Run from the repo root:
6
+ python apply_patches.py [--repo-dir /path/to/repo]
7
+
8
+ What it does:
9
+ Fix 1: Wires data_generators.py into Meta_environment.py._load_context()
10
+ Fix 2: Makes easy/medium graders accept per-episode context
11
+ Fix 3: Adds /step/typed/{task_id} typed endpoint to server/app.py
12
+ Fix 4: Updates task lists in gradio_ui.py, baseline.py, inference.py, openenv.yaml
13
+
14
+ Place data_generators.py and expert_tasks.py in the repo root alongside this script.
15
+ """
16
+
17
+ import argparse
18
+ import os
19
+ import re
20
+ import shutil
21
+ import sys
22
+ from pathlib import Path
23
+
24
+
25
+ def banner(msg: str) -> None:
26
+ print(f"\n{'─'*60}")
27
+ print(f" {msg}")
28
+ print(f"{'─'*60}")
29
+
30
+
31
+ def read(path: Path) -> str:
32
+ return path.read_text(encoding="utf-8")
33
+
34
+
35
+ def write(path: Path, content: str) -> None:
36
+ path.write_text(content, encoding="utf-8")
37
+ print(f" βœ“ wrote {path}")
38
+
39
+
40
+ def backup(path: Path) -> None:
41
+ bak = path.with_suffix(path.suffix + ".bak")
42
+ shutil.copy2(path, bak)
43
+ print(f" βœ“ backed up β†’ {bak.name}")
44
+
45
+
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # Fix 1 + 2 β€” Meta_environment.py
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ def patch_meta_environment(repo: Path) -> None:
51
+ banner("Fix 1+2 β€” server/Meta_environment.py")
52
+ path = repo / "server" / "Meta_environment.py"
53
+ if not path.exists():
54
+ print(f" βœ— not found: {path}"); return
55
+ backup(path)
56
+ src = read(path)
57
+
58
+ # ── Add imports ──────────────────────────────────────────────────────────
59
+ import_block = """\
60
+ from data_generators import (
61
+ gen_email_easy, gen_email_medium, gen_email_hard,
62
+ gen_code_easy,
63
+ gen_data_easy, gen_data_medium, gen_data_hard,
64
+ gen_mod_easy, gen_mod_medium,
65
+ gen_ticket_easy,
66
+ )
67
+ from expert_tasks import (
68
+ EMAIL_EXPERT_CONTEXT, grade_email_expert,
69
+ CODE_EXPERT_CONTEXT, grade_code_expert,
70
+ DATA_EXPERT_CONTEXT, grade_data_expert,
71
+ MOD_EXPERT_CONTEXT, grade_mod_expert,
72
+ TICKET_EXPERT_CONTEXT, grade_ticket_expert,
73
+ EXPERT_INSTRUCTIONS, EXPERT_TASK_IDS,
74
+ )
75
+ """
76
+ if "from data_generators import" not in src:
77
+ # Insert after the last existing import block (after "from models import")
78
+ src = src.replace(
79
+ "from models import MetaAction, MetaObservation",
80
+ "from models import MetaAction, MetaObservation\n\n" + import_block,
81
+ )
82
+ print(" βœ“ added data_generators + expert_tasks imports")
83
+
84
+ # ── Replace static context loaders with generator calls ──────────────────
85
+ replacements = [
86
+ # email_easy
87
+ (
88
+ r"if task_id == \"email_triage_easy\":\s+email\s+=.*?\n.*?return agent_ctx, grader_ctx, \"easy\"",
89
+ 'if task_id == "email_triage_easy":\n'
90
+ ' agent_ctx, grader_ctx = gen_email_easy(self._state.episode_id)\n'
91
+ ' return agent_ctx, grader_ctx, "easy"',
92
+ ),
93
+ # email_medium
94
+ (
95
+ r"if task_id == \"email_triage_medium\":\s+agent_ctx\s+=.*?\n.*?return agent_ctx, grader_ctx, \"medium\"",
96
+ 'if task_id == "email_triage_medium":\n'
97
+ ' agent_ctx, grader_ctx = gen_email_medium(self._state.episode_id)\n'
98
+ ' return agent_ctx, grader_ctx, "medium"',
99
+ ),
100
+ # email_hard
101
+ (
102
+ r"if task_id == \"email_triage_hard\":\s+case\s+=.*?\n.*?return agent_ctx, grader_ctx, \"hard\"",
103
+ 'if task_id == "email_triage_hard":\n'
104
+ ' agent_ctx, grader_ctx = gen_email_hard(self._state.episode_id)\n'
105
+ ' return agent_ctx, grader_ctx, "hard"',
106
+ ),
107
+ # code_easy
108
+ (
109
+ r"if task_id == \"code_review_easy\":\s+variant\s+=.*?\n.*?return agent_ctx, grader_ctx, \"easy\"",
110
+ 'if task_id == "code_review_easy":\n'
111
+ ' agent_ctx, grader_ctx = gen_code_easy(self._state.episode_id)\n'
112
+ ' return agent_ctx, grader_ctx, "easy"',
113
+ ),
114
+ # data_easy
115
+ (
116
+ r"if task_id == \"data_cleaning_easy\":\s+ctx = \{\"data\": DATA_EASY\[\"data\"\]\}\s+return ctx, ctx, \"easy\"",
117
+ 'if task_id == "data_cleaning_easy":\n'
118
+ ' agent_ctx, grader_ctx = gen_data_easy(self._state.episode_id)\n'
119
+ ' return agent_ctx, grader_ctx, "easy"',
120
+ ),
121
+ # data_medium
122
+ (
123
+ r"if task_id == \"data_cleaning_medium\":\s+ctx = \{\"data\": DATA_MEDIUM\[\"data\"\]\}\s+return ctx, ctx, \"medium\"",
124
+ 'if task_id == "data_cleaning_medium":\n'
125
+ ' agent_ctx, grader_ctx = gen_data_medium(self._state.episode_id)\n'
126
+ ' return agent_ctx, grader_ctx, "medium"',
127
+ ),
128
+ # data_hard
129
+ (
130
+ r"if task_id == \"data_cleaning_hard\":\s+ctx = \{\"data\": DATA_HARD\[\"data\"\]\}\s+return ctx, ctx, \"hard\"",
131
+ 'if task_id == "data_cleaning_hard":\n'
132
+ ' agent_ctx, grader_ctx = gen_data_hard(self._state.episode_id)\n'
133
+ ' return agent_ctx, grader_ctx, "hard"',
134
+ ),
135
+ # mod_easy
136
+ (
137
+ r"if task_id == \"content_moderation_easy\":\s+agent_ctx\s+=.*?MOD_EASY.*?\n.*?return agent_ctx, grader_ctx, \"easy\"",
138
+ 'if task_id == "content_moderation_easy":\n'
139
+ ' agent_ctx, grader_ctx = gen_mod_easy(self._state.episode_id)\n'
140
+ ' return agent_ctx, grader_ctx, "easy"',
141
+ ),
142
+ # mod_medium
143
+ (
144
+ r"if task_id == \"content_moderation_medium\":\s+agent_ctx\s+=.*?MOD_MEDIUM.*?\n.*?return agent_ctx, grader_ctx, \"medium\"",
145
+ 'if task_id == "content_moderation_medium":\n'
146
+ ' agent_ctx, grader_ctx = gen_mod_medium(self._state.episode_id)\n'
147
+ ' return agent_ctx, grader_ctx, "medium"',
148
+ ),
149
+ # ticket_easy
150
+ (
151
+ r"if task_id == \"ticket_triage_easy\":\s+ticket\s+=.*?\n.*?return agent_ctx, grader_ctx, \"easy\"",
152
+ 'if task_id == "ticket_triage_easy":\n'
153
+ ' agent_ctx, grader_ctx = gen_ticket_easy(self._state.episode_id)\n'
154
+ ' return agent_ctx, grader_ctx, "easy"',
155
+ ),
156
+ ]
157
+
158
+ for pattern, replacement in replacements:
159
+ new_src = re.sub(pattern, replacement, src, flags=re.DOTALL)
160
+ if new_src != src:
161
+ src = new_src
162
+ task = pattern.split('"')[1]
163
+ print(f" βœ“ replaced context loader: {task}")
164
+ # Regex may not match exactly β€” try string replacement as fallback later
165
+
166
+ # ── Add expert context loaders before final raise ─────────────────────────
167
+ expert_loaders = """
168
+ if task_id == "email_triage_expert":
169
+ agent_ctx = {k: v for k, v in EMAIL_EXPERT_CONTEXT.items()}
170
+ grader_ctx = agent_ctx
171
+ return agent_ctx, grader_ctx, "expert"
172
+
173
+ if task_id == "code_review_expert":
174
+ agent_ctx = {"code": CODE_EXPERT_CONTEXT["code"],
175
+ "vulnerability_count": CODE_EXPERT_CONTEXT["vulnerability_count"]}
176
+ grader_ctx = CODE_EXPERT_CONTEXT
177
+ return agent_ctx, grader_ctx, "expert"
178
+
179
+ if task_id == "data_cleaning_expert":
180
+ agent_ctx = DATA_EXPERT_CONTEXT
181
+ grader_ctx = DATA_EXPERT_CONTEXT
182
+ return agent_ctx, grader_ctx, "expert"
183
+
184
+ if task_id == "content_moderation_expert":
185
+ agent_ctx = MOD_EXPERT_CONTEXT
186
+ grader_ctx = MOD_EXPERT_CONTEXT
187
+ return agent_ctx, grader_ctx, "expert"
188
+
189
+ if task_id == "ticket_triage_expert":
190
+ agent_ctx = TICKET_EXPERT_CONTEXT
191
+ grader_ctx = TICKET_EXPERT_CONTEXT
192
+ return agent_ctx, grader_ctx, "expert"
193
+
194
+ """
195
+ if '"email_triage_expert"' not in src:
196
+ src = src.replace(
197
+ 'raise ValueError(f"Unknown task_id:',
198
+ expert_loaders + ' raise ValueError(f"Unknown task_id:',
199
+ )
200
+ print(" βœ“ added expert context loaders")
201
+
202
+ # ── Add expert grader routing in _grade() ─────────────────────────────────
203
+ expert_graders = (
204
+ ' if task_id == "email_triage_expert": return grade_email_expert(payload)\n'
205
+ ' if task_id == "code_review_expert": return grade_code_expert(payload)\n'
206
+ ' if task_id == "data_cleaning_expert": return grade_data_expert(payload)\n'
207
+ ' if task_id == "content_moderation_expert": return grade_mod_expert(payload)\n'
208
+ ' if task_id == "ticket_triage_expert": return grade_ticket_expert(payload)\n'
209
+ )
210
+ if "grade_email_expert" not in src:
211
+ src = src.replace(
212
+ ' return 0.0, "Unknown task.", {}',
213
+ expert_graders + ' return 0.0, "Unknown task.", {}',
214
+ )
215
+ print(" βœ“ added expert grader routing")
216
+
217
+ # ── Fix graders to accept per-episode context ─────────────────────────────
218
+ src = src.replace(
219
+ "def grade_mod_easy(payload: dict) -> tuple[float, str, dict]:",
220
+ "def grade_mod_easy(payload: dict, context: dict = None) -> tuple[float, str, dict]:",
221
+ )
222
+ src = src.replace(
223
+ " correct = {p[\"id\"]: p[\"label\"] for p in MOD_EASY}",
224
+ ' posts = (context or {}).get("posts", MOD_EASY)\n correct = {p["id"]: p["label"] for p in posts}',
225
+ 1,
226
+ )
227
+ src = src.replace(
228
+ "def grade_mod_medium(payload: dict) -> tuple[float, str, dict]:",
229
+ "def grade_mod_medium(payload: dict, context: dict = None) -> tuple[float, str, dict]:",
230
+ )
231
+ src = src.replace(
232
+ " correct = {p[\"id\"]: p[\"label\"] for p in MOD_MEDIUM}",
233
+ ' posts = (context or {}).get("posts", MOD_MEDIUM)\n correct = {p["id"]: p["label"] for p in posts}',
234
+ 1,
235
+ )
236
+
237
+ # Fix grade_email_medium to read correct_order from context
238
+ src = src.replace(
239
+ " correct = EMAIL_MEDIUM_CORRECT_ORDER",
240
+ ' correct = context.get("correct_order", EMAIL_MEDIUM_CORRECT_ORDER)',
241
+ 1,
242
+ )
243
+
244
+ # Fix grade_data_hard to accept context
245
+ src = src.replace(
246
+ "def grade_data_hard(payload: dict) -> tuple[float, str, dict]:",
247
+ "def grade_data_hard(payload: dict, context: dict = None) -> tuple[float, str, dict]:",
248
+ )
249
+ src = src.replace(
250
+ " correct_out = DATA_HARD[\"answers\"][\"outliers\"]",
251
+ ' answers = (context or {}).get("answers", DATA_HARD["answers"])\n correct_out = answers.get("outliers", DATA_HARD["answers"]["outliers"])',
252
+ )
253
+ src = src.replace(
254
+ " correct_miss = DATA_HARD[\"answers\"][\"missing\"]",
255
+ ' correct_miss = answers.get("missing", DATA_HARD["answers"]["missing"])',
256
+ )
257
+ src = src.replace(
258
+ ' lo, hi = DATA_HARD["answers"]["imputed_range"]',
259
+ ' lo, hi = answers.get("imputed_range", DATA_HARD["answers"]["imputed_range"])',
260
+ )
261
+
262
+ # Fix _grade() calls for mod_easy/mod_medium/data_hard to pass context
263
+ src = src.replace(
264
+ 'if task_id == "content_moderation_easy": return grade_mod_easy(payload)',
265
+ 'if task_id == "content_moderation_easy": return grade_mod_easy(payload, grader_context)',
266
+ )
267
+ src = src.replace(
268
+ 'if task_id == "content_moderation_medium": return grade_mod_medium(payload)',
269
+ 'if task_id == "content_moderation_medium": return grade_mod_medium(payload, grader_context)',
270
+ )
271
+ src = src.replace(
272
+ 'if task_id == "data_cleaning_hard": return grade_data_hard(payload)',
273
+ 'if task_id == "data_cleaning_hard": return grade_data_hard(payload, grader_context)',
274
+ )
275
+
276
+ # ── Add EXPERT_INSTRUCTIONS to INSTRUCTIONS dict ──────────────────────────
277
+ if "email_triage_expert" not in src:
278
+ src = src.replace(
279
+ ' "cross_agent_mod_escalation": (',
280
+ ' **{}, # expert instructions added below\n "cross_agent_mod_escalation": (',
281
+ )
282
+ # Better: append to INSTRUCTIONS dict
283
+ src = src.replace(
284
+ ' "cross_agent_mod_escalation": (\n "TWO skills needed',
285
+ ' "cross_agent_mod_escalation": (\n "TWO skills needed',
286
+ )
287
+ # Find the closing of INSTRUCTIONS dict and add expert entries before it
288
+ # (This is safer as a manual step β€” note it for the developer)
289
+ print(" ⚠ Add EXPERT_INSTRUCTIONS entries to INSTRUCTIONS dict manually (see expert_tasks.py)")
290
+
291
+ write(path, src)
292
+
293
+
294
+ # ─────────────────────────────────────────────────────────────────────────────
295
+ # Fix 3 β€” server/app.py typed endpoint
296
+ # ─────────────────────────────────────────────────────────────────────────────
297
+
298
+ TYPED_ENDPOINT_CODE = '''
299
+
300
+ # ── Typed step endpoint β€” Fix 3 ───────────────────────────────────────────────
301
+
302
+ _TASK_TO_MODEL = {
303
+ "email_triage_easy": EmailTriageEasyPayload,
304
+ "email_triage_medium": EmailTriageMediumPayload,
305
+ "email_triage_hard": EmailTriageHardPayload,
306
+ "code_review_easy": CodeReviewEasyPayload,
307
+ "code_review_medium": CodeReviewMediumPayload,
308
+ "code_review_hard": CodeReviewHardPayload,
309
+ "data_cleaning_easy": DataCleaningEasyPayload,
310
+ "data_cleaning_medium": DataCleaningMediumPayload,
311
+ "data_cleaning_hard": DataCleaningHardPayload,
312
+ "content_moderation_easy": ContentModerationEasyPayload,
313
+ "content_moderation_medium": ContentModerationMediumPayload,
314
+ "content_moderation_hard": ContentModerationHardPayload,
315
+ "ticket_triage_easy": TicketTriageEasyPayload,
316
+ "ticket_triage_medium": TicketTriageMediumPayload,
317
+ "ticket_triage_hard": TicketTriageHardPayload,
318
+ "cross_agent_chain": CrossAgentChainPayload,
319
+ "cross_agent_email_data": CrossAgentEmailDataPayload,
320
+ "cross_agent_code_email": CrossAgentCodeEmailPayload,
321
+ "cross_agent_mod_escalation": CrossAgentModEscalationPayload,
322
+ }
323
+
324
+
325
+ @app.post("/step/typed/{task_id}")
326
+ async def step_typed(task_id: str, request: Request):
327
+ """
328
+ Typed step endpoint β€” accepts the typed payload model for the given task_id
329
+ instead of the JSON-string-in-message format.
330
+
331
+ Example:
332
+ POST /step/typed/email_triage_easy
333
+ Body: {"agent": "email_triage", "task_id": "email_triage_easy", "payload": {"classification": "spam"}}
334
+
335
+ Returns the same observation as POST /step.
336
+ """
337
+ if task_id not in _TASK_TO_MODEL:
338
+ return JSONResponse(status_code=404, content={"error": f"Unknown task_id: {task_id}. See GET /tasks."})
339
+ body = await request.json()
340
+ try:
341
+ model_cls = _TASK_TO_MODEL[task_id]
342
+ payload = body.get("payload", body)
343
+ typed = model_cls(**payload)
344
+ except Exception as e:
345
+ return JSONResponse(status_code=422, content={"error": str(e), "task_id": task_id})
346
+ agent = body.get("agent", "_".join(task_id.split("_")[:-1]))
347
+ wrapped = MetaAction(message=json.dumps({
348
+ "agent": agent, "task_id": task_id, "payload": typed.model_dump()
349
+ }))
350
+ # Delegate to the active environment instance
351
+ env = MetaEnvironment()
352
+ env.reset()
353
+ obs = env.step(wrapped)
354
+ return obs.model_dump()
355
+ '''
356
+
357
+ TYPED_MODEL_IMPORTS = """\
358
+ from models import (
359
+ MetaAction, MetaObservation,
360
+ EmailTriageEasyPayload, EmailTriageMediumPayload, EmailTriageHardPayload,
361
+ CodeReviewEasyPayload, CodeReviewMediumPayload, CodeReviewHardPayload,
362
+ DataCleaningEasyPayload, DataCleaningMediumPayload, DataCleaningHardPayload,
363
+ ContentModerationEasyPayload, ContentModerationMediumPayload, ContentModerationHardPayload,
364
+ TicketTriageEasyPayload, TicketTriageMediumPayload, TicketTriageHardPayload,
365
+ CrossAgentChainPayload, CrossAgentEmailDataPayload,
366
+ CrossAgentCodeEmailPayload, CrossAgentModEscalationPayload,
367
+ )
368
+ """
369
+
370
+
371
+ def patch_app_py(repo: Path) -> None:
372
+ banner("Fix 3 β€” server/app.py typed endpoint")
373
+ path = repo / "server" / "app.py"
374
+ if not path.exists():
375
+ print(f" βœ— not found: {path}"); return
376
+ backup(path)
377
+ src = read(path)
378
+
379
+ if "_TASK_TO_MODEL" in src:
380
+ print(" ⟳ already patched"); return
381
+
382
+ # Update the import of MetaAction to include all typed models
383
+ src = src.replace(
384
+ "from models import MetaAction, MetaObservation",
385
+ TYPED_MODEL_IMPORTS.rstrip(),
386
+ )
387
+
388
+ # Append typed endpoint before the main() function
389
+ src = src.replace(
390
+ "\ndef main(",
391
+ TYPED_ENDPOINT_CODE + "\n\ndef main(",
392
+ )
393
+
394
+ write(path, src)
395
+
396
+
397
+ # ─────────────────────────────────────────────────────────────────────────────
398
+ # Fix 4a β€” gradio_ui.py
399
+ # ─────────────────────────────────────────────────────────────────────────────
400
+
401
+ ALL_TASK_IDS_24 = [
402
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
403
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
404
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
405
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
406
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
407
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
408
+ ]
409
+
410
+
411
+ def patch_gradio_ui(repo: Path) -> None:
412
+ banner("Fix 4a β€” server/gradio_ui.py task list + count")
413
+ path = repo / "server" / "gradio_ui.py"
414
+ if not path.exists():
415
+ print(f" βœ— not found: {path}"); return
416
+ backup(path)
417
+ src = read(path)
418
+
419
+ # Replace TASK_IDS list
420
+ new_task_ids = "TASK_IDS = [\n" + "".join(
421
+ f' "{t}",\n' for t in ALL_TASK_IDS_24
422
+ ) + "]\n"
423
+
424
+ # Find and replace the existing TASK_IDS block
425
+ src = re.sub(
426
+ r"TASK_IDS = \[.*?\]",
427
+ new_task_ids.rstrip(),
428
+ src,
429
+ flags=re.DOTALL,
430
+ )
431
+
432
+ # Add "expert" to DIFFICULTY_EMOJI
433
+ src = src.replace(
434
+ '"hard": "πŸ”΄ Hard"}',
435
+ '"hard": "πŸ”΄ Hard", "expert": "⚫ Expert"}',
436
+ )
437
+
438
+ # Update header agent/task count
439
+ src = src.replace(
440
+ "**4 agents Β· 12 tasks Β· Real-world RL environment**",
441
+ "**6 agents Β· 24 tasks Β· Real-world RL environment**",
442
+ )
443
+ src = src.replace(
444
+ "**4 agents, 12 tasks**",
445
+ "**6 agents, 24 tasks**",
446
+ )
447
+
448
+ # Update /tasks text in About tab
449
+ src = src.replace(
450
+ "| GET | `/tasks` | All 12 tasks |",
451
+ "| GET | `/tasks` | All 24 tasks with schemas |\n | POST | `/step/typed/{task_id}` | Typed action format |",
452
+ )
453
+
454
+ write(path, src)
455
+
456
+
457
+ # ─────────────────────────────────────────────────────────────────────────────
458
+ # Fix 4b β€” baseline.py
459
+ # ─────────────────────────────────────────────────────────────────────────────
460
+
461
+ BASELINE_TASK_IDS = """\
462
+ TASK_IDS = [
463
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
464
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
465
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
466
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
467
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
468
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
469
+ ]"""
470
+
471
+
472
+ def patch_baseline(repo: Path) -> None:
473
+ banner("Fix 4b β€” baseline.py task list")
474
+ path = repo / "baseline.py"
475
+ if not path.exists():
476
+ print(f" βœ— not found: {path}"); return
477
+ backup(path)
478
+ src = read(path)
479
+ src = re.sub(r"TASK_IDS = \[.*?\]", BASELINE_TASK_IDS, src, flags=re.DOTALL)
480
+ write(path, src)
481
+
482
+
483
+ # ─────────────────────────────────────────────────────────────────────────────
484
+ # Fix 4c β€” inference.py
485
+ # ─────────────────────────────────────────────────────────────────────────────
486
+
487
+ INFERENCE_TASK_IDS = """\
488
+ TASK_IDS = [
489
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
490
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
491
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
492
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
493
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
494
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
495
+ ]"""
496
+
497
+
498
+ def patch_inference(repo: Path) -> None:
499
+ banner("Fix 4c β€” inference.py task list")
500
+ path = repo / "inference.py"
501
+ if not path.exists():
502
+ print(f" βœ— not found: {path}"); return
503
+ backup(path)
504
+ src = read(path)
505
+ src = re.sub(r"TASK_IDS = \[.*?\]", INFERENCE_TASK_IDS, src, flags=re.DOTALL)
506
+ write(path, src)
507
+
508
+
509
+ # ─────────────────────────────────────────────────────────────────────────────
510
+ # Fix 4d β€” openenv.yaml
511
+ # ─────────────────────────────────────────────────────────────────────────────
512
+
513
+ EXPERT_YAML_TASKS = """
514
+ # ── Expert tier (multi-step reasoning, frontier challenge) ────────────────
515
+ - id: email_triage_expert
516
+ agent: email_triage
517
+ difficulty: expert
518
+ description: >
519
+ Draft an executive reply to a 4-week escalated enterprise complaint.
520
+ Account metadata and engineering root cause provided. Requires multi-context
521
+ synthesis and executive tone calibration. Frontier models score ~0.45.
522
+
523
+ - id: code_review_expert
524
+ agent: code_review
525
+ difficulty: expert
526
+ description: >
527
+ Identify 8 subtle vulnerabilities: hardcoded secret, JWT none-algorithm attack,
528
+ mass assignment, IDOR, YAML RCE deserialization, TOCTOU race condition,
529
+ prototype pollution, and log injection. Frontier models score ~0.38.
530
+
531
+ - id: data_cleaning_expert
532
+ agent: data_cleaning
533
+ difficulty: expert
534
+ description: >
535
+ Join two datasets, detect multi-type anomalies (duplicates, missing, outliers,
536
+ zero-amounts), convert currencies to USD, compute per-rep revenue excluding
537
+ invalid transactions. Frontier models score ~0.50.
538
+
539
+ - id: content_moderation_expert
540
+ agent: content_moderation
541
+ difficulty: expert
542
+ description: >
543
+ Issue rulings on 5 genuinely contested cases: suicidal ideation,
544
+ great-replacement rhetoric, conscientious objection policy debate,
545
+ antisemitic satire framing, and health misinformation. Frontier models score ~0.40.
546
+
547
+ - id: ticket_triage_expert
548
+ agent: ticket_triage
549
+ difficulty: expert
550
+ description: >
551
+ Produce a board-ready PIR from a 4h17m outage timeline. Requires identifying
552
+ timeline gaps, 5+ specific action items, and quantified MTTR breakdown.
553
+ Frontier models score ~0.50.
554
+ """
555
+
556
+
557
+ def patch_openenv_yaml(repo: Path) -> None:
558
+ banner("Fix 4d β€” openenv.yaml expert tasks + agent lists")
559
+ path = repo / "openenv.yaml"
560
+ if not path.exists():
561
+ print(f" βœ— not found: {path}"); return
562
+ backup(path)
563
+ src = read(path)
564
+
565
+ if "email_triage_expert" in src:
566
+ print(" ⟳ already patched"); return
567
+
568
+ # Insert expert tasks before the cross-agent section
569
+ src = src.replace(
570
+ " # ── Cross-Agent Chained",
571
+ EXPERT_YAML_TASKS + "\n # ── Cross-Agent Chained",
572
+ )
573
+
574
+ # Update agent task lists to include expert
575
+ for agent in ["email_triage", "code_review", "data_cleaning", "content_moderation", "ticket_triage"]:
576
+ expert_id = f"{agent}_expert"
577
+ src = re.sub(
578
+ rf"(- id: {agent}\n tasks: \[.*?)(])",
579
+ lambda m, eid=expert_id: m.group(1) + f", {eid}" + m.group(2),
580
+ src,
581
+ )
582
+
583
+ # Update version
584
+ src = src.replace('version: "3.0.0"', 'version: "3.1.0"')
585
+ src = src.replace("Meta is the first multi-domain OpenEnv environment featuring 5 AI agents",
586
+ "Meta is the first multi-domain OpenEnv environment featuring 5 AI agents (plus expert tier)")
587
+
588
+ write(path, src)
589
+
590
+
591
+ # ─────────────────────────────────────────────────────────────────────────────
592
+ # Copy new files into repo
593
+ # ─────────────────────────────────────────────────────────────────────────────
594
+
595
+ def copy_new_files(repo: Path, script_dir: Path) -> None:
596
+ banner("Copying new source files")
597
+ for fname in ["data_generators.py", "expert_tasks.py"]:
598
+ src = script_dir / fname
599
+ dest = repo / fname
600
+ if not src.exists():
601
+ print(f" βœ— {fname} not found in {script_dir}")
602
+ continue
603
+ # Skip if source and destination are the same file (script lives in repo root)
604
+ try:
605
+ if src.resolve() == dest.resolve():
606
+ print(f" ⟳ {fname} already in place (src == dest), skipping copy")
607
+ continue
608
+ except OSError:
609
+ pass
610
+ try:
611
+ shutil.copy2(src, dest)
612
+ print(f" βœ“ copied {fname} β†’ {dest}")
613
+ except PermissionError as e:
614
+ print(f" ⚠ Could not copy {fname}: {e}")
615
+ print(f" Close any editors/processes that have {dest} open and retry.")
616
+
617
+
618
+ # ─────────────────────────────────────────────────────────────────────────────
619
+ # Main
620
+ # ─────────────────────────────────────────────────────────────────────────────
621
+
622
+ def main():
623
+ parser = argparse.ArgumentParser(description="Apply Meta v3.1 patches")
624
+ parser.add_argument("--repo-dir", default=".", help="Path to repo root")
625
+ args = parser.parse_args()
626
+
627
+ repo = Path(args.repo_dir).resolve()
628
+ script_dir = Path(__file__).parent.resolve()
629
+
630
+ print(f"\nRepo : {repo}")
631
+ print(f"Scripts: {script_dir}")
632
+
633
+ if not (repo / "openenv.yaml").exists():
634
+ print(f"\nβœ— ERROR: {repo}/openenv.yaml not found. Pass --repo-dir to the repo root.")
635
+ sys.exit(1)
636
+
637
+ copy_new_files(repo, script_dir)
638
+ patch_meta_environment(repo)
639
+ patch_app_py(repo)
640
+ patch_gradio_ui(repo)
641
+ patch_baseline(repo)
642
+ patch_inference(repo)
643
+ patch_openenv_yaml(repo)
644
+
645
+ banner("DONE β€” run verification checks")
646
+ print("""
647
+ python -c "from data_generators import gen_email_easy; print('generators OK')"
648
+ python -c "from expert_tasks import EXPERT_TASK_IDS; print(EXPERT_TASK_IDS)"
649
+ pytest tests/ -v
650
+ python inference.py --dry-run # if supported, else just check imports
651
+ """)
652
+
653
+
654
+ if __name__ == "__main__":
655
+ main()
baseline.py CHANGED
@@ -31,10 +31,12 @@ if os.path.exists(_env_file):
31
  os.environ.setdefault(_k.strip(), _v.strip())
32
 
33
  TASK_IDS = [
34
- "email_triage_easy", "email_triage_medium", "email_triage_hard",
35
- "code_review_easy", "code_review_medium", "code_review_hard",
36
- "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard",
37
- "content_moderation_easy", "content_moderation_medium", "content_moderation_hard",
 
 
38
  ]
39
 
40
  SYSTEM_PROMPT = (
 
31
  os.environ.setdefault(_k.strip(), _v.strip())
32
 
33
  TASK_IDS = [
34
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
35
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
36
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
37
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
38
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
39
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
40
  ]
41
 
42
  SYSTEM_PROMPT = (
baseline.py.bak ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Meta v2 β€” Baseline Inference Script
3
+ Runs llama-3.3-70b-versatile (via Groq) against all 12 tasks and reports scores.
4
+
5
+ Usage:
6
+ # Option 1 β€” use .env file (recommended)
7
+ python baseline.py
8
+
9
+ # Option 2 β€” set env vars manually
10
+ export API_BASE_URL=https://api.groq.com/openai/v1
11
+ export MODEL_NAME=llama-3.3-70b-versatile
12
+ export HF_TOKEN=gsk_...
13
+ python baseline.py
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import json
19
+ import re
20
+
21
+ sys.path.insert(0, os.path.dirname(__file__))
22
+
23
+ # Load .env if present
24
+ _env_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
25
+ if os.path.exists(_env_file):
26
+ with open(_env_file) as _f:
27
+ for _line in _f:
28
+ _line = _line.strip()
29
+ if _line and not _line.startswith("#") and "=" in _line:
30
+ _k, _, _v = _line.partition("=")
31
+ os.environ.setdefault(_k.strip(), _v.strip())
32
+
33
+ TASK_IDS = [
34
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
35
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
36
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
37
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
38
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
39
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
40
+ ]
41
+
42
+ SYSTEM_PROMPT = (
43
+ "You are an expert AI agent completing structured real-world tasks.\n"
44
+ "You will receive task context and instructions.\n"
45
+ "Respond ONLY with a valid JSON object as the action payload β€” no explanation, no markdown, no code fences.\n"
46
+ "Your response must be a single raw JSON object."
47
+ )
48
+
49
+
50
+ def extract_json(text: str) -> dict:
51
+ text = text.strip()
52
+ try:
53
+ return json.loads(text)
54
+ except Exception:
55
+ pass
56
+ match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
57
+ if match:
58
+ try:
59
+ return json.loads(match.group(1))
60
+ except Exception:
61
+ pass
62
+ match = re.search(r"\{.*\}", text, re.DOTALL)
63
+ if match:
64
+ try:
65
+ return json.loads(match.group(0))
66
+ except Exception:
67
+ pass
68
+ return {}
69
+
70
+
71
+ def run_task(client, task_id: str, model_name: str) -> dict:
72
+ from server.Meta_environment import MetaEnvironment, INSTRUCTIONS
73
+ from models import MetaAction
74
+
75
+ env = MetaEnvironment()
76
+ env.reset()
77
+ probe_msg = json.dumps({
78
+ "agent": "_".join(task_id.split("_")[:-1]),
79
+ "task_id": task_id,
80
+ "payload": {"_probe": True},
81
+ })
82
+ probe_obs = env.step(MetaAction(message=probe_msg))
83
+ instructions = INSTRUCTIONS.get(task_id, probe_obs.instructions)
84
+ context = probe_obs.context
85
+
86
+ user_prompt = (
87
+ f"Task ID: {task_id}\n"
88
+ f"Difficulty: {probe_obs.difficulty}\n\n"
89
+ f"Instructions:\n{instructions}\n\n"
90
+ f"Context:\n{json.dumps(context, indent=2)}\n\n"
91
+ "Respond with ONLY the payload JSON object."
92
+ )
93
+
94
+ try:
95
+ response = client.chat.completions.create(
96
+ model=model_name,
97
+ messages=[
98
+ {"role": "system", "content": SYSTEM_PROMPT},
99
+ {"role": "user", "content": user_prompt},
100
+ ],
101
+ temperature=0.0,
102
+ max_tokens=2000,
103
+ )
104
+ raw = response.choices[0].message.content
105
+ payload = extract_json(raw)
106
+ except Exception as e:
107
+ payload = {}
108
+ raw = str(e)
109
+
110
+ # Grade
111
+ agent = "_".join(task_id.split("_")[:-1])
112
+ env2 = MetaEnvironment()
113
+ env2.reset()
114
+ env2._current_agent_context = context
115
+ env2._current_grader_context = context
116
+ env2._current_task_id = task_id
117
+
118
+ action_msg = json.dumps({"agent": agent, "task_id": task_id, "payload": payload})
119
+ result_obs = env2.step(MetaAction(message=action_msg))
120
+
121
+ return {
122
+ "task_id": task_id,
123
+ "agent": agent,
124
+ "difficulty": result_obs.difficulty,
125
+ "score": result_obs.score,
126
+ "feedback": result_obs.feedback,
127
+ "partial_credits": result_obs.partial_credits,
128
+ "reward": result_obs.reward,
129
+ }
130
+
131
+
132
+ def run_all_baselines(api_key: str = None, api_base_url: str = None, model_name: str = None) -> list:
133
+ from openai import OpenAI
134
+ import time
135
+
136
+ # Key resolution: explicit arg β†’ OPENAI_API_KEY β†’ GROQ_API_KEY β†’ HF_TOKEN
137
+ api_key = (
138
+ api_key
139
+ or os.environ.get("OPENAI_API_KEY")
140
+ or os.environ.get("GROQ_API_KEY")
141
+ or os.environ.get("HF_TOKEN")
142
+ )
143
+ if not api_key:
144
+ raise EnvironmentError(
145
+ "No API key found. Set HF_TOKEN, GROQ_API_KEY, or OPENAI_API_KEY.\n"
146
+ "Get a free Groq key at https://console.groq.com"
147
+ )
148
+
149
+ api_base_url = api_base_url or os.environ.get("API_BASE_URL", "https://api.groq.com/openai/v1")
150
+ model_name = model_name or os.environ.get("MODEL_NAME", "llama-3.3-70b-versatile")
151
+
152
+ # Groq is OpenAI-compatible β€” same SDK, different base_url
153
+ client = OpenAI(api_key=api_key, base_url=api_base_url)
154
+ results = []
155
+
156
+ print("\nπŸš€ Meta Multi-Agent v2 β€” Baseline Inference")
157
+ print(f" Model : {model_name}")
158
+ print(f" API Base : {api_base_url}")
159
+ print("=" * 65)
160
+
161
+ for task_id in TASK_IDS:
162
+ print(f" β–Ά {task_id:<35}", end=" ", flush=True)
163
+ result = run_task(client, task_id, model_name)
164
+ results.append(result)
165
+ bar = "β–ˆ" * int(result["score"] * 10) + "β–‘" * (10 - int(result["score"] * 10))
166
+ print(f"[{bar}] {result['score']:.2f} {result['feedback'][:50]}")
167
+ # Respect Groq free tier rate limit (30 req/min)
168
+ time.sleep(2)
169
+
170
+ avg = sum(r["score"] for r in results) / len(results)
171
+ print("=" * 65)
172
+ print(f" πŸ“Š Average Score: {avg:.3f} | Tasks: {len(results)}")
173
+ print("=" * 65)
174
+ return results
175
+
176
+
177
+ if __name__ == "__main__":
178
+ scores = run_all_baselines()
179
+ print("\nFull JSON Results:")
180
+ print(json.dumps(scores, indent=2))
data_generators.py ADDED
@@ -0,0 +1,708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ data_generators.py β€” Deterministic procedural data generators for Meta v3.1
3
+ Replaces small hand-crafted datasets with richly varied, reproducible data.
4
+
5
+ Design principles
6
+ -----------------
7
+ * Every generator takes an `episode_id: str` and seeds Python's stdlib `random`
8
+ with `hash(episode_id + salt)` so the same episode always yields the same data.
9
+ * No third-party libs needed β€” stdlib only (random, string, datetime, hashlib).
10
+ * Each generator returns (agent_ctx, grader_ctx) tuples matching existing grader
11
+ contracts so graders need zero changes.
12
+
13
+ Drop-in replacement usage in Meta_environment.py::_load_context():
14
+ Replace the hardcoded-data blocks with:
15
+ from data_generators import gen_<task>(episode_id)
16
+ agent_ctx, grader_ctx = gen_<task>(episode_id)
17
+
18
+ Generators provided
19
+ -------------------
20
+ Email domain
21
+ gen_email_easy(episode_id) β€” 1 email, 3-way class, 30 sender/subject combos
22
+ gen_email_medium(episode_id) β€” 10 emails, varying priority pool
23
+ gen_email_hard(episode_id) β€” customer complaint, 8 scenario templates
24
+
25
+ Code domain
26
+ gen_code_easy(episode_id) β€” 6 code snippet variants
27
+ gen_code_medium(episode_id) β€” deterministic (same code, no variant needed)
28
+ gen_code_hard(episode_id) β€” deterministic (same code, no variant needed)
29
+
30
+ Data-cleaning domain
31
+ gen_data_easy(episode_id) β€” 6-10 rows, random nulls + one dup pair
32
+ gen_data_medium(episode_id) β€” 5 rows, randomised messy formats per field
33
+ gen_data_hard(episode_id) β€” 12-16 rows, IQR-detected outliers
34
+
35
+ Content moderation domain
36
+ gen_mod_easy(episode_id) β€” 7 posts drawn from larger pool
37
+ gen_mod_medium(episode_id) β€” 8 posts drawn from larger pool
38
+ (hard is already adversarial/static β€” left unchanged)
39
+
40
+ Ticket domain
41
+ gen_ticket_easy(episode_id) β€” 1 ticket from 8-variant pool
42
+ gen_ticket_medium(episode_id) β€” deterministic (same 8 tickets, no variant)
43
+ gen_ticket_hard(episode_id) β€” deterministic (same 6 incidents, no variant)
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import hashlib
49
+ import random
50
+ import string
51
+ from copy import deepcopy
52
+ from datetime import date, timedelta
53
+ from typing import Any
54
+
55
+
56
+ # ── Seeding helper ─────────────────────────────────────────────────────────────
57
+
58
+ def _rng(episode_id: str, salt: str = "") -> random.Random:
59
+ seed = int(hashlib.md5((episode_id + salt).encode()).hexdigest(), 16)
60
+ return random.Random(seed)
61
+
62
+
63
+ # ═══════════════════════════════════════════════════════════════════════════════
64
+ # EMAIL EASY β€” 30+ variant pool
65
+ # ═══════════════════════════════════════════════════════════════════════════════
66
+
67
+ _EMAIL_EASY_POOL = [
68
+ # (subject, sender, body, label)
69
+ ("You won $1,000,000!!!", "promo@totally-legit-lottery.com",
70
+ "Click here to claim your prize now! Limited time offer!", "spam"),
71
+ ("Q3 Budget Review β€” Urgent Action Required", "cfo@company.com",
72
+ "Please review the attached Q3 budget before our board meeting tomorrow at 9 AM.", "important"),
73
+ ("Your weekly digest from Medium", "noreply@medium.com",
74
+ "Top stories this week: AI trends, Python tips, and startup news.", "newsletter"),
75
+ ("CONGRATULATIONS! iPhone 15 Pro is yours!", "winner@prize-alert.net",
76
+ "You have been selected. Click now to claim your free iPhone!", "spam"),
77
+ ("Action Required: Sign the NDA before Friday", "legal@company.com",
78
+ "Please sign the attached NDA before Friday's partnership meeting. Deadline is strict.", "important"),
79
+ ("TechCrunch Newsletter β€” March 2026", "newsletter@techcrunch.com",
80
+ "This week's top stories in tech, AI, and startups.", "newsletter"),
81
+ ("Claim your $500 Amazon gift card NOW", "deals@amazingoffers.biz",
82
+ "You are today's lucky winner. No purchase necessary. Act fast!", "spam"),
83
+ ("Production incident P1 β€” please join bridge", "oncall@company.com",
84
+ "We have a P1 production incident. Please join the war room immediately.", "important"),
85
+ ("Developer digest: April 2026", "digest@devweekly.io",
86
+ "This week: Rust 2.0 release, LLM benchmarks, and open-source highlights.", "newsletter"),
87
+ ("FREE vacation cruise β€” you've been selected!", "winner@vacation-cruise.net",
88
+ "Claim your free cruise for two. Just pay shipping and handling.", "spam"),
89
+ ("Board approval needed: Series B term sheet", "ceo@company.com",
90
+ "Our Series B term sheet requires your signature before the 5 PM deadline today.", "important"),
91
+ ("Product Hunt daily digest", "digest@producthunt.com",
92
+ "Today's top products: AI code review, async stand-ups, no-code CRM.", "newsletter"),
93
+ ("Congratulations β€” Survey reward $250!", "rewards@surveypanel.net",
94
+ "You qualify for a $250 reward. Complete our 2-minute survey to claim.", "spam"),
95
+ ("HR: Performance review cycle starts Monday", "hr@company.com",
96
+ "The annual performance review cycle begins Monday. Please complete your self-assessment.", "important"),
97
+ ("Hacker News digest: top 10 links this week", "weekly@hackernews.app",
98
+ "Rust, WASM, and the AI bubble: top discussions from the community.", "newsletter"),
99
+ ("Double billing detected on account #AC-8821", "billing@ourplatform.com",
100
+ "We noticed a duplicate charge on your account. Reply to initiate a refund.", "spam"),
101
+ ("Legal: Partnership agreement signature required EOD", "legal@company.com",
102
+ "The partnership agreement with Nexus Corp must be countersigned by 5 PM today.", "important"),
103
+ ("SaaS Weekly β€” your industry roundup", "editor@saasweekly.com",
104
+ "Churn rates, AI copilots, and the future of vertical SaaS.", "newsletter"),
105
+ ("You have unclaimed crypto rewards!", "support@cryptorewards.io",
106
+ "Wallet 0x4f…a2 has 0.3 ETH waiting. Connect now to claim.", "spam"),
107
+ ("Security alert: admin password reset requested", "security@company.com",
108
+ "An admin-level password reset was requested from IP 198.51.100.42. Verify or block.", "important"),
109
+ ("The Batch β€” AI newsletter by deeplearning.ai", "batch@deeplearning.ai",
110
+ "Andrew Ng on scaling laws, fine-tuning tips, and this week's papers.", "newsletter"),
111
+ ("Limited offer: 90% off VPN subscription", "deals@vpndeals.biz",
112
+ "Today only: get lifetime VPN access for $1.99. Offer expires midnight.", "spam"),
113
+ ("Vendor invoice #INV-7721 overdue by 14 days", "ar@vendor.com",
114
+ "Invoice #INV-7721 for $12,400 is now 14 days overdue. Please remit payment to avoid service interruption.", "important"),
115
+ ("The Algorithm Newsletter β€” weekly edition", "hello@thealgorithm.co",
116
+ "ML papers, job listings, and a tutorial on RLHF.", "newsletter"),
117
+ ("Your package was held β€” verify address", "delivery@postalnow.net",
118
+ "Your package is held due to an address discrepancy. Click to confirm.", "spam"),
119
+ ("Compliance: GDPR audit documentation due Friday", "compliance@company.com",
120
+ "Please upload your GDPR data-processing records to the compliance portal by Friday.", "important"),
121
+ ("Morning Brew β€” your daily business digest", "hello@morningbrew.com",
122
+ "Markets, tech, and the big stories you need to know today.", "newsletter"),
123
+ ("Account suspended β€” click to restore", "support@accountalert.biz",
124
+ "Unusual activity detected. Your account has been temporarily suspended.", "spam"),
125
+ ("CTO sync needed: infrastructure cost overrun", "cto@company.com",
126
+ "AWS bill came in 340% over budget. Need your sign-off on emergency cost controls.", "important"),
127
+ ("The Hustle β€” daily newsletter", "news@thehustle.co",
128
+ "Today's biggest business stories, curated for founders and operators.", "newsletter"),
129
+ ]
130
+
131
+
132
+ def gen_email_easy(episode_id: str) -> tuple[dict, dict]:
133
+ rng = _rng(episode_id, "email_easy")
134
+ email = dict(zip(["subject", "sender", "body", "label"],
135
+ rng.choice(_EMAIL_EASY_POOL)))
136
+ email["id"] = "e1"
137
+ agent_ctx = {"email": {k: v for k, v in email.items() if k != "label"}}
138
+ grader_ctx = {"email": email}
139
+ return agent_ctx, grader_ctx
140
+
141
+
142
+ # ═══════════════════════════════════════════════════════════════════════════════
143
+ # EMAIL MEDIUM β€” varied priority pools
144
+ # ═══════════════════════════════════════════════════════════════════════════════
145
+
146
+ _CRITICAL_POOL = [
147
+ {"subject": "[ALERT] Production server DOWN", "sender": "ops@company.com",
148
+ "body": "All services unresponsive since 2 AM. Revenue impact: $10k/min.", "priority": 1, "category": "critical"},
149
+ {"subject": "[P1] Database primary down β€” all writes failing", "sender": "oncall@company.com",
150
+ "body": "The primary DB replica crashed. No write operations possible site-wide.", "priority": 1, "category": "critical"},
151
+ {"subject": "[WARNING] Security alert: unusual login attempt", "sender": "security@company.com",
152
+ "body": "Unauthorised login attempt from IP 45.33.32.156. Verify immediately.", "priority": 1, "category": "critical"},
153
+ {"subject": "[ALERT] DDoS attack in progress", "sender": "security@company.com",
154
+ "body": "We are under a DDoS attack. Site response time is 30s+. Join the war room.", "priority": 1, "category": "critical"},
155
+ ]
156
+
157
+ _URGENT_POOL = [
158
+ {"subject": "Acme Corp contract renewal β€” URGENT", "sender": "sales@company.com",
159
+ "body": "Acme contract expires Friday. They need signature or they walk.", "priority": 2, "category": "business"},
160
+ {"subject": "Legal: Partnership agreement needs signature TODAY", "sender": "legal@company.com",
161
+ "body": "The partnership agreement must be countersigned by EOD or the deal falls through.", "priority": 2, "category": "legal"},
162
+ {"subject": "Series B term sheet β€” board sign-off required by 5 PM", "sender": "ceo@company.com",
163
+ "body": "VCs need the countersigned term sheet by 5 PM today or the round lapses.", "priority": 2, "category": "legal"},
164
+ ]
165
+
166
+ _FINANCE_POOL = [
167
+ {"subject": "Invoice #INV-4521 overdue", "sender": "billing@vendor.com",
168
+ "body": "Invoice of $5,400 was due 3 days ago. Payment needed to avoid suspension.", "priority": 3, "category": "finance"},
169
+ {"subject": "AWS bill 340% over budget", "sender": "finance@company.com",
170
+ "body": "Last month's cloud bill was $82k vs $24k budget. Need approval to proceed.", "priority": 3, "category": "finance"},
171
+ {"subject": "Customer double-charged β€” refund dispute open", "sender": "billing@company.com",
172
+ "body": "Customer #C-1121 was charged twice. Finance blocked. Needs VP sign-off.", "priority": 3, "category": "finance"},
173
+ ]
174
+
175
+ _BUSINESS_POOL = [
176
+ {"subject": "Q4 product launch deck β€” review needed", "sender": "marketing@company.com",
177
+ "body": "Please review launch slides before Thursday. Launch is next Monday.", "priority": 4, "category": "business"},
178
+ {"subject": "Quarterly board deck β€” slides need your input", "sender": "ea@company.com",
179
+ "body": "Board meeting is Tuesday. Your section on growth metrics needs updating.", "priority": 4, "category": "business"},
180
+ {"subject": "Key account at risk β€” needs executive call", "sender": "cs@company.com",
181
+ "body": "TechGiant Corp is threatening churn. They want an exec call this week.", "priority": 4, "category": "business"},
182
+ ]
183
+
184
+ _SOCIAL_POOL = [
185
+ {"subject": "Team lunch this Friday?", "sender": "colleague@company.com",
186
+ "body": "Hey, thinking of doing a team lunch Friday. You in?", "priority": 9, "category": "social"},
187
+ {"subject": "Happy Work Anniversary!", "sender": "hr@company.com",
188
+ "body": "Celebrating your 3 years with us!", "priority": 9, "category": "social"},
189
+ {"subject": "Office trivia night β€” Thursday 7 PM", "sender": "social@company.com",
190
+ "body": "Join us for office trivia Thursday evening. Pizza provided!", "priority": 9, "category": "social"},
191
+ ]
192
+
193
+ _NEWSLETTER_POOL = [
194
+ {"subject": "Weekly SaaS digest", "sender": "news@saasdigest.com",
195
+ "body": "Top SaaS stories this week...", "priority": 10, "category": "newsletter"},
196
+ {"subject": "Free AI productivity webinar", "sender": "promo@webinar.io",
197
+ "body": "Join our free webinar on AI tools this Thursday.", "priority": 10, "category": "newsletter"},
198
+ {"subject": "The Batch: AI news weekly", "sender": "batch@deeplearning.ai",
199
+ "body": "Andrew Ng on scaling, fine-tuning, and paper highlights.", "priority": 10, "category": "newsletter"},
200
+ ]
201
+
202
+
203
+ def gen_email_medium(episode_id: str) -> tuple[dict, dict]:
204
+ """
205
+ Always picks 2 critical, 1 urgent, 1 finance, 2 business, 2 social, 2 newsletter = 10 emails.
206
+ Correct ordering: criticals first, then urgent, finance, business, social, newsletter.
207
+ """
208
+ rng = _rng(episode_id, "email_medium")
209
+ crits = rng.sample(_CRITICAL_POOL, k=min(2, len(_CRITICAL_POOL)))
210
+ urgents = rng.sample(_URGENT_POOL, k=min(1, len(_URGENT_POOL)))
211
+ fins = rng.sample(_FINANCE_POOL, k=min(1, len(_FINANCE_POOL)))
212
+ bizs = rng.sample(_BUSINESS_POOL, k=min(2, len(_BUSINESS_POOL)))
213
+ socials = rng.sample(_SOCIAL_POOL, k=min(2, len(_SOCIAL_POOL)))
214
+ news = rng.sample(_NEWSLETTER_POOL,k=min(2, len(_NEWSLETTER_POOL)))
215
+
216
+ ordered = crits + urgents + fins + bizs + socials + news
217
+ emails = []
218
+ correct_order = []
219
+ for i, e in enumerate(ordered):
220
+ eid = f"m{i+1}"
221
+ emails.append({"id": eid, **e})
222
+ correct_order.append(eid)
223
+
224
+ # Shuffle for presentation; keep correct_order as ground truth
225
+ display = emails[:]
226
+ rng.shuffle(display)
227
+
228
+ agent_ctx = {"emails": [{k: v for k, v in e.items() if k not in ("priority", "category")} for e in display]}
229
+ grader_ctx = {"emails": emails, "correct_order": correct_order}
230
+ return agent_ctx, grader_ctx
231
+
232
+
233
+ # ═══════════════════════════════════════════════════════════════════════════════
234
+ # EMAIL HARD β€” 8 complaint scenario templates
235
+ # ═══════════════════════════════════════════════════════════════════════════════
236
+
237
+ _EMAIL_HARD_TEMPLATES = [
238
+ {
239
+ "subject": "Completely unacceptable β€” Order #{order_id}",
240
+ "sender": "furious.customer@email.com",
241
+ "body": (
242
+ "I have been a loyal customer for 5 years and I am absolutely furious. "
243
+ "My order #{order_id} arrived broken for the SECOND time this month. "
244
+ "Your support team has been dismissive and unhelpful every time I called. "
245
+ "I demand a full refund, a replacement sent overnight, AND compensation. "
246
+ "If this is not resolved within 24 hours, I will file a consumer court complaint."
247
+ ),
248
+ "expected_elements": {
249
+ "acknowledge_frustration": ["understand", "frustrat", "disappoint", "concern", "feel"],
250
+ "apologize": ["sorry", "apologize", "apology", "sincerely"],
251
+ "offer_refund": ["refund", "money back", "full refund"],
252
+ "offer_replacement": ["replacement", "replace", "new order", "send another", "overnight"],
253
+ "provide_timeline": ["24 hours", "48 hours", "within", "today", "tomorrow", "business day"],
254
+ "professional_tone": None,
255
+ "compensation_acknowledged": ["compensat", "inconvenience", "make it right", "goodwill"],
256
+ },
257
+ },
258
+ {
259
+ "subject": "Billing error β€” charged twice for subscription",
260
+ "sender": "billing.complaint@customer.com",
261
+ "body": (
262
+ "I was charged twice for my annual subscription this month β€” once on March 1st "
263
+ "and again on March 15th. I have the bank statements as proof. "
264
+ "This is a clear billing error on your end. I need an immediate refund of the "
265
+ "duplicate charge and a written confirmation that this won't happen again. "
266
+ "I've been waiting 2 weeks for a response from your support team."
267
+ ),
268
+ "expected_elements": {
269
+ "acknowledge_frustration": ["understand", "concern", "apologize", "sorry"],
270
+ "apologize": ["sorry", "apologize", "apology"],
271
+ "offer_refund": ["refund", "reimburse", "credit"],
272
+ "confirm_investigation": ["investigat", "look into", "check", "review", "verify"],
273
+ "provide_timeline": ["within", "hours", "days", "business day"],
274
+ "professional_tone": None,
275
+ "written_confirmation": ["confirm", "written", "email confirmation", "receipt"],
276
+ },
277
+ },
278
+ {
279
+ "subject": "Data breach β€” my account was hacked",
280
+ "sender": "angry.user@personal.com",
281
+ "body": (
282
+ "I just discovered that my account was used to make purchases I never authorised. "
283
+ "Someone accessed my account and changed my email and password. "
284
+ "I was only able to regain access because I remembered my security questions. "
285
+ "This is a serious security failure on your part. I demand to know what happened, "
286
+ "how my data was exposed, and what you will do to protect me going forward. "
287
+ "If I do not hear back within 24 hours, I will contact the data protection authority."
288
+ ),
289
+ "expected_elements": {
290
+ "acknowledge_frustration": ["understand", "serious", "concern", "alarming"],
291
+ "apologize": ["sorry", "apologize", "apology"],
292
+ "confirm_investigation": ["investigat", "security team", "look into", "review"],
293
+ "offer_remediation": ["secure", "reset", "protect", "steps", "safeguard"],
294
+ "provide_timeline": ["24 hours", "48 hours", "within", "shortly", "immediately"],
295
+ "professional_tone": None,
296
+ "regulatory_mention": ["authority", "regulat", "notify", "report", "transparent"],
297
+ },
298
+ },
299
+ {
300
+ "subject": "Defective product β€” 3 replacements still broken",
301
+ "sender": "frustrated.buyer@email.com",
302
+ "body": (
303
+ "This is my THIRD replacement unit and it is still defective. "
304
+ "The first unit arrived with a cracked screen. The second had a faulty battery. "
305
+ "This third unit powers off randomly. "
306
+ "I have spent over 6 hours on support calls. I want a full refund immediately β€” "
307
+ "no more replacements. I will also be leaving reviews on every platform I can find."
308
+ ),
309
+ "expected_elements": {
310
+ "acknowledge_frustration": ["understand", "frustrat", "disappoint", "three", "3"],
311
+ "apologize": ["sorry", "apologize", "sincerely"],
312
+ "offer_refund": ["refund", "full refund", "money back"],
313
+ "no_more_replacement_push": ["will not", "no further", "respect", "understand", "preference"],
314
+ "provide_timeline": ["within", "24 hours", "48 hours", "business day"],
315
+ "professional_tone": None,
316
+ "de_escalation": ["value", "loyal", "appreciate", "important to us"],
317
+ },
318
+ },
319
+ {
320
+ "subject": "Package lost β€” 3 weeks and still nothing",
321
+ "sender": "lost.package@customer.net",
322
+ "body": (
323
+ "I placed an order 3 weeks ago and my package has still not arrived. "
324
+ "The tracking has shown 'In Transit' for 18 days without any movement. "
325
+ "I have contacted your support team 4 times. Each time I am told to 'wait a few more days'. "
326
+ "I need this item for an event next week. Either find the package, send a replacement now, "
327
+ "or give me a full refund so I can purchase elsewhere. This is unacceptable."
328
+ ),
329
+ "expected_elements": {
330
+ "acknowledge_frustration": ["understand", "frustrat", "waiting", "3 weeks", "three weeks"],
331
+ "apologize": ["sorry", "apologize", "apology"],
332
+ "offer_refund": ["refund", "full refund"],
333
+ "offer_replacement": ["replacement", "resend", "new shipment", "send another"],
334
+ "provide_timeline": ["24 hours", "48 hours", "immediately", "today", "next day"],
335
+ "professional_tone": None,
336
+ "carrier_investigation": ["carrier", "investigate", "trace", "courier", "tracking"],
337
+ },
338
+ },
339
+ {
340
+ "subject": "Cancelled subscription still being charged",
341
+ "sender": "cancelled.user@email.com",
342
+ "body": (
343
+ "I cancelled my subscription on January 15th and have proof of the cancellation email. "
344
+ "However, I have been charged $99/month for the past 3 months since then. "
345
+ "Total unauthorised charges: $297. I want a full refund of all three charges immediately. "
346
+ "If this is not resolved within 48 hours, I will initiate a chargeback with my bank "
347
+ "and report this to the consumer protection agency."
348
+ ),
349
+ "expected_elements": {
350
+ "acknowledge_frustration": ["understand", "concern", "apologize", "recognised"],
351
+ "apologize": ["sorry", "apologize", "apology"],
352
+ "offer_refund": ["refund", "full refund", "$297", "three charges", "all charges"],
353
+ "confirm_cancellation": ["confirm", "cancellation", "verified", "record"],
354
+ "provide_timeline": ["48 hours", "24 hours", "immediately", "within"],
355
+ "professional_tone": None,
356
+ "prevent_recurrence": ["prevent", "ensure", "will not happen", "steps", "corrective"],
357
+ },
358
+ },
359
+ {
360
+ "subject": "Discriminatory treatment by your staff",
361
+ "sender": "discrimination.complaint@email.com",
362
+ "body": (
363
+ "I visited your store on April 2nd and was followed around by a staff member "
364
+ "while other customers were not treated this way. When I raised this with the manager, "
365
+ "I was told 'it's just store policy'. This felt discriminatory and I am deeply hurt. "
366
+ "I expect a formal apology, an explanation of your training policies, "
367
+ "and assurance that this will not happen to any customer again."
368
+ ),
369
+ "expected_elements": {
370
+ "acknowledge_frustration": ["understand", "serious", "concern", "upset", "hurt"],
371
+ "apologize": ["sorry", "apologize", "sincerely", "formal apology"],
372
+ "confirm_investigation": ["investigat", "review", "look into", "hr", "management"],
373
+ "policy_commitment": ["policy", "training", "standards", "commit", "improve"],
374
+ "provide_timeline": ["within", "48 hours", "days", "shortly"],
375
+ "professional_tone": None,
376
+ "empathy_specific": ["dignity", "respect", "every customer", "valued"],
377
+ },
378
+ },
379
+ {
380
+ "subject": "Software destroyed my data β€” urgent",
381
+ "sender": "data.loss@business.com",
382
+ "body": (
383
+ "Your software update on March 28th corrupted our entire database backup. "
384
+ "We lost 6 months of customer records. Our business has been at a standstill for 3 days. "
385
+ "We are a paying enterprise customer ($24,000/year). "
386
+ "I need immediate escalation to your engineering team, a post-mortem report, "
387
+ "and a discussion about compensation for our business losses."
388
+ ),
389
+ "expected_elements": {
390
+ "acknowledge_frustration": ["understand", "serious", "impact", "significant", "recognise"],
391
+ "apologize": ["sorry", "sincerely", "deeply apologize"],
392
+ "escalate": ["engineering", "escalate", "senior", "team", "immediately"],
393
+ "post_mortem": ["post-mortem", "root cause", "analysis", "report", "findings"],
394
+ "provide_timeline": ["24 hours", "immediately", "today", "within"],
395
+ "professional_tone": None,
396
+ "compensation_acknowledged": ["compensat", "discuss", "impact", "losses", "make it right"],
397
+ },
398
+ },
399
+ ]
400
+
401
+
402
+ def gen_email_hard(episode_id: str) -> tuple[dict, dict]:
403
+ rng = _rng(episode_id, "email_hard")
404
+ template = rng.choice(_EMAIL_HARD_TEMPLATES)
405
+ order_id = rng.randint(10000, 99999)
406
+ subject = template["subject"].format(order_id=order_id)
407
+ body = template["body"].format(order_id=order_id)
408
+ case = {**template, "subject": subject, "body": body, "sender": template["sender"], "id": "h1"}
409
+ agent_ctx = {"email": {k: v for k, v in case.items() if k not in ("expected_elements", "id")}}
410
+ grader_ctx = {"email": case}
411
+ return agent_ctx, grader_ctx
412
+
413
+
414
+ # ═══════════════════════════════════════════════════════════════════════════════
415
+ # CODE EASY β€” 6 snippet variants
416
+ # ═══════════════════════════════════════════════════════════════════════════════
417
+
418
+ _CODE_EASY_POOL = [
419
+ {"code": "def calculate_average(numbers):\n total = 0\n for num in numbers\n total += num\n return total / len(numbers)\n",
420
+ "keywords": ["colon", "for loop", "syntax", "for statement"]},
421
+ {"code": "def greet(name)\n print(f'Hello, {name}!')\n\ngreet('World')\n",
422
+ "keywords": ["colon", "def", "function", "syntax"]},
423
+ {"code": "numbers = [1, 2, 3, 4, 5]\nif len(numbers) > 0\n print('Not empty')\n",
424
+ "keywords": ["colon", "if statement", "syntax"]},
425
+ {"code": "class Animal:\n def __init__(self, name)\n self.name = name\n\n def speak(self):\n return f'{self.name} speaks'\n",
426
+ "keywords": ["colon", "__init__", "constructor", "function", "def"]},
427
+ {"code": "def read_file(path):\n with open(path) as f\n return f.read()\n",
428
+ "keywords": ["colon", "with statement", "context manager", "syntax"]},
429
+ {"code": "def retry(fn, times=3):\n for i in range(times)\n try:\n return fn()\n except Exception:\n pass\n",
430
+ "keywords": ["colon", "for loop", "range", "syntax"]},
431
+ ]
432
+
433
+
434
+ def gen_code_easy(episode_id: str) -> tuple[dict, dict]:
435
+ rng = _rng(episode_id, "code_easy")
436
+ variant = rng.choice(_CODE_EASY_POOL)
437
+ agent_ctx = {"code": variant["code"]}
438
+ grader_ctx = {"code": variant["code"], "keywords": variant["keywords"]}
439
+ return agent_ctx, grader_ctx
440
+
441
+
442
+ # ═══════════════════════════════════════════════════════════════════════════════
443
+ # DATA EASY β€” procedurally generated 6-10 rows
444
+ # ═══════════════════════════════════════════════════════════════════════════════
445
+
446
+ _FIRST_NAMES = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Hiro", "Iris", "Jake"]
447
+ _DOMAINS = ["example.com", "company.net", "test.org", "work.io"]
448
+
449
+
450
+ def gen_data_easy(episode_id: str) -> tuple[dict, dict]:
451
+ rng = _rng(episode_id, "data_easy")
452
+ n = rng.randint(6, 10)
453
+ names = rng.sample(_FIRST_NAMES, k=n)
454
+ base_salary = 50000
455
+
456
+ rows = []
457
+ for i, name in enumerate(names, 1):
458
+ rows.append({
459
+ "id": i,
460
+ "name": name,
461
+ "age": rng.randint(22, 55),
462
+ "email": f"{name.lower()}@{rng.choice(_DOMAINS)}",
463
+ "salary": base_salary + rng.randint(0, 50) * 1000,
464
+ })
465
+
466
+ # Inject 3-4 nulls in distinct rows and fields
467
+ null_fields = rng.sample(["age", "name", "email", "salary"], k=rng.randint(3, 4))
468
+ null_rows = rng.sample(list(range(n)), k=len(null_fields))
469
+ missing_answers = []
470
+ for row_idx, field in zip(null_rows, null_fields):
471
+ rows[row_idx][field] = None
472
+ missing_answers.append(f"{field} (row {row_idx + 1})")
473
+
474
+ # Inject one duplicate pair
475
+ dup_src = rng.randint(0, n - 2)
476
+ dup_dst = dup_src + 1
477
+ rows[dup_dst] = {**rows[dup_src], "id": dup_dst + 1}
478
+ dup_ids = sorted([rows[dup_src]["id"], rows[dup_dst]["id"]])
479
+
480
+ agent_ctx = {"data": rows}
481
+ grader_ctx = {
482
+ "data": rows,
483
+ "answers": {"missing": sorted(missing_answers), "duplicates": dup_ids},
484
+ }
485
+ return agent_ctx, grader_ctx
486
+
487
+
488
+ # ═══════════════════════════════════════════════════════════════════════════════
489
+ # DATA MEDIUM β€” randomised messy formats per field
490
+ # ═══════════════════════════════════════════════════════════════════════════════
491
+
492
+ _AGE_STRINGS = ["thirty", "twenty-two", "forty-five", "nineteen", "fifty"]
493
+ _DATE_FORMATS = [("%Y/%m/%d", "2020/01/15"), ("%d-%m-%Y", "15-03-2021"),
494
+ ("%Y-%m-%d", "2019-07-22"), ("%Y/%m/%d", "2022/11/01"),
495
+ ("%d/%m/%Y", "01/06/2023")]
496
+ _ACTIVE_MESSY = ["yes", "no", "TRUE", "FALSE", "1", "0"]
497
+
498
+
499
+ def gen_data_medium(episode_id: str) -> tuple[dict, dict]:
500
+ rng = _rng(episode_id, "data_medium")
501
+ names = rng.sample(_FIRST_NAMES, k=5)
502
+
503
+ rows = []
504
+ salaries = [50000, 60000, 70000, 80000, 45000]
505
+ for i, (name, salary) in enumerate(zip(names, salaries), 1):
506
+ # Introduce one of each issue type
507
+ if i == 1:
508
+ age_val = rng.choice(_AGE_STRINGS) # string age
509
+ sal_val = f"${salary:,}" # dollar+comma format
510
+ elif i == 2:
511
+ age_val = 25 # correct
512
+ sal_val = f"${salary:,}" # dollar+comma
513
+ elif i == 3:
514
+ age_val = str(rng.randint(25, 40)) # string digit
515
+ sal_val = str(salary) # clean
516
+ elif i == 4:
517
+ age_val = rng.randint(30, 45) # int β€” correct
518
+ sal_val = f"{salary:,}" # comma only
519
+ else:
520
+ age_val = rng.choice(_AGE_STRINGS) # string age
521
+ sal_val = str(salary) # clean
522
+
523
+ _, date_str = rng.choice(_DATE_FORMATS)
524
+ name_messy = name.lower() if i == 4 else name.upper() if i == 5 else name
525
+ active_val = rng.choice(_ACTIVE_MESSY) if i in (1, 3, 5) else rng.choice([True, False])
526
+
527
+ rows.append({"id": i, "name": name_messy, "age": age_val, "salary": sal_val,
528
+ "join_date": date_str, "active": active_val})
529
+
530
+ issues = {
531
+ "age": "mixed types: some ages are word strings, should be integers",
532
+ "salary": "inconsistent formats: dollar signs and commas present in some values",
533
+ "join_date": "three different date formats used across rows",
534
+ "name": "inconsistent casing: some names lowercase, some all-caps",
535
+ "active": "mixed types: booleans and strings ('yes','no','TRUE') mixed",
536
+ }
537
+ agent_ctx = {"data": rows}
538
+ grader_ctx = {"data": rows, "issues": issues}
539
+ return agent_ctx, grader_ctx
540
+
541
+
542
+ # ═══════════════════════════════════════════════════════════════════════════════
543
+ # DATA HARD β€” IQR-detectable outliers, missing values
544
+ # ═══════════════════════════════════════════════════════════════════════════════
545
+
546
+ def gen_data_hard(episode_id: str) -> tuple[dict, dict]:
547
+ rng = _rng(episode_id, "data_hard")
548
+ n = rng.randint(12, 16)
549
+ base = round(rng.uniform(9.5, 12.5), 1)
550
+ spread = round(rng.uniform(0.3, 0.8), 2)
551
+
552
+ rows = []
553
+ for i in range(1, n + 1):
554
+ val = round(base + rng.gauss(0, spread), 2)
555
+ rows.append({"id": i, "value": val})
556
+
557
+ # Pick 2 outlier positions (ensure they're not neighbours)
558
+ positions = list(range(n))
559
+ rng.shuffle(positions)
560
+ o1, o2 = sorted(positions[:2])
561
+ outlier_mult = rng.choice([8, 10, 12, -5, -8])
562
+ rows[o1]["value"] = round(base * abs(outlier_mult) * (1 if outlier_mult > 0 else -1), 1)
563
+ outlier_mult2 = rng.choice([-4, -6, 15, 20])
564
+ rows[o2]["value"] = round(base * abs(outlier_mult2) * (1 if outlier_mult2 > 0 else -1), 1)
565
+ outlier_ids = sorted([o1 + 1, o2 + 1])
566
+
567
+ # Pick 2 missing positions (not overlapping outliers)
568
+ available = [i for i in range(n) if i not in (o1, o2)]
569
+ m1, m2 = sorted(rng.sample(available, 2))
570
+ rows[m1]["value"] = None
571
+ rows[m2]["value"] = None
572
+ missing_ids = sorted([m1 + 1, m2 + 1])
573
+
574
+ normal_vals = [r["value"] for r in rows if r["value"] is not None and r["id"] not in outlier_ids]
575
+ lo = round(min(normal_vals) - 1.0, 2)
576
+ hi = round(max(normal_vals) + 1.0, 2)
577
+
578
+ agent_ctx = {"data": rows}
579
+ grader_ctx = {
580
+ "data": rows,
581
+ "answers": {
582
+ "outliers": set(outlier_ids),
583
+ "missing": set(missing_ids),
584
+ "imputed_range": (lo, hi),
585
+ },
586
+ }
587
+ return agent_ctx, grader_ctx
588
+
589
+
590
+ # ═══════════════════════════════════════════════════════════════════════════════
591
+ # CONTENT MODERATION EASY β€” draw 7 from a pool of 20
592
+ # ═══════════════════════════════════════════════════════════════════════════════
593
+
594
+ _MOD_EASY_POOL = [
595
+ {"text": "I hate you so much, I want to hurt you!", "label": "harmful"},
596
+ {"text": "Great weather today! Loving this sunshine", "label": "safe"},
597
+ {"text": "All people from that group should be eliminated.", "label": "harmful"},
598
+ {"text": "Can someone recommend a good Python beginner book?", "label": "safe"},
599
+ {"text": "You're an absolute idiot and nobody likes you.", "label": "harmful"},
600
+ {"text": "I just finished a 5k run β€” feeling amazing!", "label": "safe"},
601
+ {"text": "People like you should just disappear forever.", "label": "harmful"},
602
+ {"text": "Just dropped off a care package for my neighbor.", "label": "safe"},
603
+ {"text": "Kill yourself, nobody would miss you.", "label": "harmful"},
604
+ {"text": "Anyone else excited for the new season of that show?", "label": "safe"},
605
+ {"text": "I'm going to find you and make you pay for this.", "label": "harmful"},
606
+ {"text": "Finally got my herb garden started!", "label": "safe"},
607
+ {"text": "These people deserve to suffer for what they've done.", "label": "harmful"},
608
+ {"text": "Happy birthday to my best friend!", "label": "safe"},
609
+ {"text": "Someone should shoot that politician.", "label": "harmful"},
610
+ {"text": "Loving the new coffee shop that opened downtown.", "label": "safe"},
611
+ {"text": "I hope your family gets what's coming to them.", "label": "harmful"},
612
+ {"text": "Just hit 100 days of learning Spanish β€” so proud!", "label": "safe"},
613
+ {"text": "People from that country are all criminals.", "label": "harmful"},
614
+ {"text": "The cherry blossoms are out β€” spring is finally here!", "label": "safe"},
615
+ ]
616
+
617
+
618
+ def gen_mod_easy(episode_id: str) -> tuple[dict, dict]:
619
+ rng = _rng(episode_id, "mod_easy")
620
+ # Ensure at least 3 harmful, 3 safe among 7
621
+ harmful = [p for p in _MOD_EASY_POOL if p["label"] == "harmful"]
622
+ safe = [p for p in _MOD_EASY_POOL if p["label"] == "safe"]
623
+ chosen = rng.sample(harmful, 4) + rng.sample(safe, 3)
624
+ rng.shuffle(chosen)
625
+ posts = [{"id": f"c{i+1}", **p} for i, p in enumerate(chosen)]
626
+ agent_ctx = {"posts": [{"id": p["id"], "text": p["text"]} for p in posts]}
627
+ grader_ctx = {"posts": posts}
628
+ return agent_ctx, grader_ctx
629
+
630
+
631
+ # ═══════════════════════════════════════════════════════════════════════════════
632
+ # CONTENT MODERATION MEDIUM β€” draw 8 from a pool of 20
633
+ # ═══════════════════════════════════════════════════════════════════════════════
634
+
635
+ _MOD_MEDIUM_POOL = [
636
+ {"text": "Oh sure, like THAT went well. Classic genius move.", "label": "harmful"},
637
+ {"text": "I'm so done with these people. Absolutely insufferable.", "label": "harmful"},
638
+ {"text": "That presentation was... very unique.", "label": "safe"},
639
+ {"text": "Some people just shouldn't be allowed to have opinions.", "label": "harmful"},
640
+ {"text": "Oh wow, another 'brilliant' idea from upper management", "label": "harmful"},
641
+ {"text": "Congrats on the promotion! Well deserved!", "label": "safe"},
642
+ {"text": "Not everyone is cut out for this kind of work, clearly.", "label": "harmful"},
643
+ {"text": "Thanks for the feedback, I'll take it into account.", "label": "safe"},
644
+ {"text": "Some of us actually read the requirements before submitting.", "label": "harmful"},
645
+ {"text": "Good effort β€” interesting approach to the problem.", "label": "safe"},
646
+ {"text": "If you'd done even a little research, you'd know better.", "label": "harmful"},
647
+ {"text": "Really enjoyed the team retrospective today.", "label": "safe"},
648
+ {"text": "Must be nice having a job you're actually qualified for.", "label": "harmful"},
649
+ {"text": "Great work getting the release out on time!", "label": "safe"},
650
+ {"text": "Shocking that anyone thought that plan would work.", "label": "harmful"},
651
+ {"text": "Just wanted to say β€” the onboarding docs are really clear.", "label": "safe"},
652
+ {"text": "The bar for hiring has clearly dropped.", "label": "harmful"},
653
+ {"text": "Appreciate everyone's patience during the migration.", "label": "safe"},
654
+ {"text": "I see we've decided quantity over quality again.", "label": "harmful"},
655
+ {"text": "The demo went really smoothly β€” nice job everyone.", "label": "safe"},
656
+ ]
657
+
658
+
659
+ def gen_mod_medium(episode_id: str) -> tuple[dict, dict]:
660
+ rng = _rng(episode_id, "mod_medium")
661
+ harmful = [p for p in _MOD_MEDIUM_POOL if p["label"] == "harmful"]
662
+ safe = [p for p in _MOD_MEDIUM_POOL if p["label"] == "safe"]
663
+ chosen = rng.sample(harmful, 5) + rng.sample(safe, 3)
664
+ rng.shuffle(chosen)
665
+ posts = [{"id": f"m{i+1}", **p} for i, p in enumerate(chosen)]
666
+ agent_ctx = {"posts": [{"id": p["id"], "text": p["text"]} for p in posts]}
667
+ grader_ctx = {"posts": posts}
668
+ return agent_ctx, grader_ctx
669
+
670
+
671
+ # ══��════════════════════════════════════════════════════════════════════════════
672
+ # TICKET EASY β€” 8 variant pool (already existed; keep contract-compatible)
673
+ # ═══════════════════════════════════════════════════════════════════════════════
674
+
675
+ _TICKET_EASY_POOL = [
676
+ {"id": "t1", "title": "App crashes on login for all users",
677
+ "description": "Since the 2.3.1 deploy, 100% of login attempts throw a 500 error.",
678
+ "reporter": "ops-monitoring@company.com", "correct_priority": "critical", "correct_category": "bug"},
679
+ {"id": "t2", "title": "Add dark mode to the dashboard",
680
+ "description": "Multiple users have requested a dark mode option.",
681
+ "reporter": "product@company.com", "correct_priority": "low", "correct_category": "feature_request"},
682
+ {"id": "t3", "title": "How do I export my data as CSV?",
683
+ "description": "I can't find the export button anywhere in the UI.",
684
+ "reporter": "user123@customer.com", "correct_priority": "low", "correct_category": "question"},
685
+ {"id": "t4", "title": "Charged twice for last month's invoice",
686
+ "description": "Account #AC-4421 shows two charges of $299 on March 1st.",
687
+ "reporter": "finance@customer.com", "correct_priority": "high", "correct_category": "billing"},
688
+ {"id": "t5", "title": "Memory leak in mobile app β€” crashes after 10 min",
689
+ "description": "The iOS app uses 2 GB RAM then crashes. Reproducible on all devices.",
690
+ "reporter": "qa@company.com", "correct_priority": "high", "correct_category": "bug"},
691
+ {"id": "t6", "title": "Add SSO support for Google Workspace",
692
+ "description": "Enterprise customers are requesting Google SSO. High deal-blocker frequency.",
693
+ "reporter": "sales@company.com", "correct_priority": "medium", "correct_category": "feature_request"},
694
+ {"id": "t7", "title": "Refund policy β€” how long does it take?",
695
+ "description": "A customer is asking about the refund timeline for a cancelled subscription.",
696
+ "reporter": "support@company.com", "correct_priority": "low", "correct_category": "question"},
697
+ {"id": "t8", "title": "Wrong amount charged β€” $0 instead of $499",
698
+ "description": "New enterprise contract shows $0 on the invoice. Revenue loss.",
699
+ "reporter": "billing@company.com", "correct_priority": "critical", "correct_category": "billing"},
700
+ ]
701
+
702
+
703
+ def gen_ticket_easy(episode_id: str) -> tuple[dict, dict]:
704
+ rng = _rng(episode_id, "ticket_easy")
705
+ ticket = rng.choice(_TICKET_EASY_POOL)
706
+ agent_ctx = {"ticket": {k: v for k, v in ticket.items() if k not in ("correct_priority", "correct_category")}}
707
+ grader_ctx = {"ticket": ticket}
708
+ return agent_ctx, grader_ctx
expert_tasks.py ADDED
@@ -0,0 +1,656 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ expert_tasks.py β€” Expert-tier tasks for Meta v3.1
3
+ Two per domain. Multi-step reasoning required. Frontier models score ≀ 0.45.
4
+
5
+ Design
6
+ ------
7
+ Each expert task requires the agent to:
8
+ 1. Reason across multiple pieces of evidence
9
+ 2. Synthesise a correct answer that isn't directly stated in the context
10
+ 3. Produce structured output that is graded on multiple sub-criteria
11
+
12
+ Drop these into Meta_environment.py:
13
+ - Add the task IDs to TASK_IDS and openenv.yaml
14
+ - Add context loaders to _load_context()
15
+ - Add grade functions to _grade()
16
+ - Add instructions to INSTRUCTIONS
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+
24
+ # ═══════════════════════════════════════════════════════════════════════════════
25
+ # EMAIL EXPERT
26
+ # ═══════════════════════════════════════════════════════════════════════════════
27
+
28
+ EMAIL_EXPERT_CONTEXT = {
29
+ "thread": [
30
+ {
31
+ "from": "sarah.chen@bigclient.com",
32
+ "to": "support@company.com",
33
+ "date": "2026-03-10 09:15",
34
+ "subject": "Re: Re: Re: Our data export is wrong β€” ESCALATING",
35
+ "body": (
36
+ "This is the FOURTH week we have been raising this issue. "
37
+ "The data export from your platform is missing 12% of our transaction records, "
38
+ "dating back to February 1st. Our CFO has flagged this as a potential compliance risk. "
39
+ "I am now formally requesting a root-cause analysis, a corrected export, "
40
+ "and a meeting with your VP of Engineering and VP of Customer Success. "
41
+ "If we do not hear back with a concrete plan within 24 hours, "
42
+ "we will be engaging our legal team."
43
+ ),
44
+ },
45
+ {
46
+ "from": "support@company.com",
47
+ "to": "sarah.chen@bigclient.com",
48
+ "date": "2026-03-09 14:22",
49
+ "subject": "Re: Re: Our data export is wrong",
50
+ "body": (
51
+ "Hi Sarah, our engineering team is still investigating. "
52
+ "We expect an update by end of week. Apologies for the delay."
53
+ ),
54
+ },
55
+ {
56
+ "from": "sarah.chen@bigclient.com",
57
+ "to": "support@company.com",
58
+ "date": "2026-03-05 11:30",
59
+ "subject": "Re: Our data export is wrong",
60
+ "body": (
61
+ "It has been 10 days and no fix. "
62
+ "We have a regulatory audit on March 20th. This is becoming critical."
63
+ ),
64
+ },
65
+ {
66
+ "from": "sarah.chen@bigclient.com",
67
+ "to": "support@company.com",
68
+ "date": "2026-02-24 16:05",
69
+ "subject": "Our data export is wrong",
70
+ "body": (
71
+ "Hello, we noticed our February data export is missing records. "
72
+ "Can you please investigate?"
73
+ ),
74
+ },
75
+ ],
76
+ "account_metadata": {
77
+ "account_name": "BigClient Corp",
78
+ "tier": "Enterprise",
79
+ "contract_value": 480000,
80
+ "account_age_years": 4,
81
+ "renewal_date": "2026-06-01",
82
+ "open_issues": 3,
83
+ "last_csat_score": 5.2,
84
+ },
85
+ "known_bug": {
86
+ "status": "confirmed",
87
+ "root_cause": "Daylight saving time offset bug in the data export pipeline affecting records from Feb 1-14",
88
+ "fix_eta": "2026-03-15",
89
+ "records_affected": "12% of February transaction exports",
90
+ },
91
+ }
92
+
93
+ EMAIL_EXPERT_EXPECTED = {
94
+ "escalation_acknowledged": ["escalat", "four weeks", "4 weeks", "urgent", "seriou", "concern"],
95
+ "vp_meeting_committed": ["vp", "engineering", "customer success", "meeting", "schedule", "arrange"],
96
+ "root_cause_disclosed": ["daylight", "dst", "timezone", "offset", "february", "feb", "export pipeline"],
97
+ "timeline_for_fix": ["march 15", "march 15th", "3/15", "this week", "by friday", "within"],
98
+ "corrected_export_promised": ["corrected export", "re-export", "new export", "corrected data", "send you"],
99
+ "legal_de_escalation": ["legal", "understand", "resolution", "committed", "avoid", "together"],
100
+ "executive_tone": None, # checked via length + no informal language
101
+ "retention_focus": ["value", "partnership", "4 years", "four years", "important", "priority", "renew"],
102
+ }
103
+
104
+ EMAIL_EXPERT_INSTRUCTIONS = (
105
+ "You are the VP of Customer Success. Read the full email thread AND the account metadata carefully.\n\n"
106
+ "You must draft an executive-level response that:\n"
107
+ " 1. Acknowledges the full history (4 weeks of escalation)\n"
108
+ " 2. Commits to scheduling the VP meeting requested\n"
109
+ " 3. Discloses the root cause (you have the engineering report)\n"
110
+ " 4. Provides the concrete fix timeline (March 15)\n"
111
+ " 5. Commits to sending a corrected export\n"
112
+ " 6. De-escalates the legal threat through concrete action\n"
113
+ " 7. Uses appropriate executive tone (not boilerplate support language)\n"
114
+ " 8. Reinforces the long-term partnership value\n\n"
115
+ "This is a $480k account up for renewal in 3 months. Every word matters.\n\n"
116
+ 'Payload: {"reply": "<full executive reply β€” minimum 200 words>"}'
117
+ )
118
+
119
+
120
+ def grade_email_expert(payload: dict) -> tuple[float, str, dict]:
121
+ reply = payload.get("reply", "").lower()
122
+ checks: dict[str, bool] = {}
123
+ for key, keywords in EMAIL_EXPERT_EXPECTED.items():
124
+ if key == "executive_tone":
125
+ checks[key] = (
126
+ len(reply) >= 200
127
+ and not any(w in reply for w in ["ticket", "our team will", "apologies for the inconvenience",
128
+ "we understand your frustration", "please be patient"])
129
+ )
130
+ else:
131
+ checks[key] = keywords is not None and any(w in reply for w in keywords)
132
+ passed = sum(checks.values())
133
+ score = round(passed / len(checks), 2)
134
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.6 else "[ERROR]"
135
+ missing = [k for k, v in checks.items() if not v]
136
+ feedback = f"{tag} {passed}/{len(checks)} executive reply elements present."
137
+ if missing:
138
+ feedback += f" Missing: {', '.join(missing)}."
139
+ return score, feedback, checks
140
+
141
+
142
+ # ═══════════════════════════════════════════════════════════════════════════════
143
+ # CODE EXPERT
144
+ # ═══════════════════════════════════════════════════════════════════════════════
145
+
146
+ CODE_EXPERT_CONTEXT = {
147
+ "code": """\
148
+ import jwt
149
+ import hmac
150
+ import hashlib
151
+ import os
152
+ import subprocess
153
+ import yaml
154
+ import json
155
+ from functools import wraps
156
+
157
+ SECRET_KEY = "hardcoded-jwt-secret-change-me"
158
+
159
+ # Vulnerability 1: Hardcoded secret + weak JWT algorithm default
160
+ def create_token(user_id: int, role: str) -> str:
161
+ payload = {"user_id": user_id, "role": role}
162
+ return jwt.encode(payload, SECRET_KEY) # defaults to HS256 with hardcoded key
163
+
164
+ # Vulnerability 2: JWT none algorithm attack β€” algorithm not validated
165
+ def verify_token(token: str) -> dict:
166
+ return jwt.decode(token, SECRET_KEY, algorithms=["HS256", "none"])
167
+
168
+ # Vulnerability 3: Mass assignment β€” user-controlled fields merged into DB update
169
+ def update_user_profile(user_id: int, form_data: dict) -> None:
170
+ db.execute("UPDATE users SET ? WHERE id = ?", (form_data, user_id))
171
+
172
+ # Vulnerability 4: Insecure direct object reference (IDOR)
173
+ def get_invoice(invoice_id: int) -> dict:
174
+ return db.query("SELECT * FROM invoices WHERE id = ?", (invoice_id,))
175
+
176
+ # Vulnerability 5: Unsafe YAML deserialization
177
+ def load_config(yaml_string: str) -> dict:
178
+ return yaml.load(yaml_string) # yaml.load without Loader β€” RCE risk
179
+
180
+ # Vulnerability 6: Race condition / TOCTOU in file write
181
+ def save_report(filename: str, content: str) -> None:
182
+ if not os.path.exists(f"/reports/{filename}"):
183
+ with open(f"/reports/{filename}", "w") as f:
184
+ f.write(content)
185
+
186
+ # Vulnerability 7: Prototype pollution via JSON merge (Python dict equivalent)
187
+ def merge_settings(base: dict, override: dict) -> dict:
188
+ result = base.copy()
189
+ for key, value in override.items():
190
+ result[key] = value # no key validation β€” __proto__ / __class__ attack
191
+ return result
192
+
193
+ # Vulnerability 8: Log injection β€” unsanitized user input in log string
194
+ def log_action(username: str, action: str) -> None:
195
+ print(f"[AUDIT] User: {username} | Action: {action}")
196
+ """,
197
+ "vulnerability_count": 8,
198
+ "vulnerability_list": [
199
+ {"id": "v1", "type": "hardcoded_secret", "location": "create_token", "severity": "critical"},
200
+ {"id": "v2", "type": "jwt_none_algorithm", "location": "verify_token", "severity": "critical"},
201
+ {"id": "v3", "type": "mass_assignment", "location": "update_user_profile", "severity": "high"},
202
+ {"id": "v4", "type": "idor", "location": "get_invoice", "severity": "high"},
203
+ {"id": "v5", "type": "unsafe_deserialization", "location": "load_config", "severity": "critical"},
204
+ {"id": "v6", "type": "race_condition", "location": "save_report", "severity": "medium"},
205
+ {"id": "v7", "type": "prototype_pollution", "location": "merge_settings", "severity": "medium"},
206
+ {"id": "v8", "type": "log_injection", "location": "log_action", "severity": "low"},
207
+ ],
208
+ }
209
+
210
+ CODE_EXPERT_INSTRUCTIONS = (
211
+ "Perform a thorough security audit on this Python codebase. "
212
+ "Identify ALL 8 security vulnerabilities, including subtle ones like JWT algorithm confusion, "
213
+ "mass assignment, IDOR, TOCTOU race conditions, prototype pollution, and log injection.\n\n"
214
+ "For each vulnerability provide: type, location (function name), severity (critical/high/medium/low), "
215
+ "and a concrete fix.\n\n"
216
+ "Hints: Look beyond the obvious injection flaws. Consider authentication logic, access control, "
217
+ "concurrency, deserialization, and output encoding.\n\n"
218
+ 'Payload: {"vulnerabilities": [{"type": "...", "location": "...", "severity": "...", "fix": "..."}]}'
219
+ )
220
+
221
+
222
+ def grade_code_expert(payload: dict) -> tuple[float, str, dict]:
223
+ vulns = payload.get("vulnerabilities", [])
224
+ checks = {
225
+ "hardcoded_secret": False,
226
+ "jwt_none_algorithm": False,
227
+ "mass_assignment": False,
228
+ "idor": False,
229
+ "unsafe_deserialization": False,
230
+ "race_condition": False,
231
+ "prototype_pollution": False,
232
+ "log_injection": False,
233
+ }
234
+ for v in vulns:
235
+ vt = str(v.get("type", "")).lower()
236
+ loc = str(v.get("location", "")).lower()
237
+ fix = str(v.get("fix", "")).lower()
238
+
239
+ if "create_token" in loc or any(w in vt for w in ["hardcoded", "secret", "env", "rotate"]):
240
+ checks["hardcoded_secret"] = True
241
+ if "verify_token" in loc or any(w in vt for w in ["none", "algorithm", "jwt", "alg"]):
242
+ checks["jwt_none_algorithm"] = True
243
+ if "update_user" in loc or any(w in vt for w in ["mass", "assignment", "whitelist", "allowlist"]):
244
+ checks["mass_assignment"] = True
245
+ if "get_invoice" in loc or any(w in vt for w in ["idor", "direct object", "authorization", "ownership"]):
246
+ checks["idor"] = True
247
+ if "load_config" in loc or any(w in vt for w in ["yaml", "deserializ", "rce", "unsafe_load", "safe_load"]):
248
+ checks["unsafe_deserialization"] = True
249
+ if "save_report" in loc or any(w in vt for w in ["race", "toctou", "time of check", "atomic"]):
250
+ checks["race_condition"] = True
251
+ if "merge_settings" in loc or any(w in vt for w in ["prototype", "pollution", "__proto__", "key valid"]):
252
+ checks["prototype_pollution"] = True
253
+ if "log_action" in loc or any(w in vt for w in ["log inject", "log forge", "crlf", "newline", "sanitize"]):
254
+ checks["log_injection"] = True
255
+
256
+ passed = sum(checks.values())
257
+ score = round(passed / 8, 2)
258
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
259
+ missing = [k for k, v in checks.items() if not v]
260
+ feedback = f"{tag} {passed}/8 expert vulnerabilities found."
261
+ if missing:
262
+ feedback += f" Missed: {', '.join(missing)}."
263
+ return score, feedback, checks
264
+
265
+
266
+ # ═══════════════════════════════════════════════════════════════════════════════
267
+ # DATA EXPERT β€” cross-dataset join, statistical anomaly detection
268
+ # ═══════════════════════════════════════════════════════════════════════════════
269
+
270
+ DATA_EXPERT_CONTEXT = {
271
+ "dataset_a": {
272
+ "name": "Sales transactions (March 2026)",
273
+ "rows": [
274
+ {"txn_id": "T001", "customer_id": "C101", "amount": 1200.00, "currency": "USD", "date": "2026-03-01", "rep_id": "R05"},
275
+ {"txn_id": "T002", "customer_id": "C102", "amount": 850.50, "currency": "USD", "date": "2026-03-02", "rep_id": "R05"},
276
+ {"txn_id": "T003", "customer_id": "C103", "amount": None, "currency": "USD", "date": "2026-03-03", "rep_id": "R12"},
277
+ {"txn_id": "T004", "customer_id": "C104", "amount": 2300.00, "currency": "GBP", "date": "2026-03-04", "rep_id": "R12"},
278
+ {"txn_id": "T005", "customer_id": "C105", "amount": 475.00, "currency": "USD", "date": "2026-03-05", "rep_id": "R05"},
279
+ {"txn_id": "T006", "customer_id": "C101", "amount": 1200.00, "currency": "USD", "date": "2026-03-01", "rep_id": "R05"},
280
+ {"txn_id": "T007", "customer_id": "C106", "amount": 99999.99,"currency": "USD", "date": "2026-03-07", "rep_id": "R05"},
281
+ {"txn_id": "T008", "customer_id": "C107", "amount": 920.00, "currency": "EUR", "date": "2026-03-08", "rep_id": "R12"},
282
+ {"txn_id": "T009", "customer_id": "C108", "amount": 1100.00, "currency": "USD", "date": "2026-03-09", "rep_id": "R05"},
283
+ {"txn_id": "T010", "customer_id": "C109", "amount": 0.00, "currency": "USD", "date": "2026-03-10", "rep_id": "R12"},
284
+ ],
285
+ },
286
+ "dataset_b": {
287
+ "name": "Currency conversion rates (2026-03-15)",
288
+ "rows": [
289
+ {"currency": "USD", "rate_to_usd": 1.000},
290
+ {"currency": "GBP", "rate_to_usd": 1.265},
291
+ {"currency": "EUR", "rate_to_usd": 1.082},
292
+ ],
293
+ },
294
+ "task_description": (
295
+ "You have two datasets: (A) a sales transaction log and (B) currency conversion rates. "
296
+ "Perform the following analysis and return results as structured JSON:\n"
297
+ "1. Identify all data quality issues across both datasets (missing values, duplicates, outliers, anomalies).\n"
298
+ "2. Join the datasets: convert all transaction amounts to USD using dataset B.\n"
299
+ "3. Compute the total USD revenue per sales rep (R05 and R12), EXCLUDING duplicate and zero-amount transactions.\n"
300
+ "4. Flag any suspicious transactions with a reason (statistical outlier, duplicate, zero amount)."
301
+ ),
302
+ }
303
+
304
+ DATA_EXPERT_EXPECTED = {
305
+ # Ground truth
306
+ "duplicate_txn": ["T001", "T006"], # same customer, amount, date, rep
307
+ "zero_txn": ["T010"], # $0 amount
308
+ "missing_amount": ["T003"],
309
+ "outlier_txn": ["T007"], # $99,999.99 β€” extreme IQR outlier
310
+ # After exclusions: T003 (missing), T006 (dup), T007 (outlier), T010 (zero) excluded
311
+ # T004: 2300 GBP Γ— 1.265 = 2909.50 USD β†’ R12
312
+ # T008: 920 EUR Γ— 1.082 = 995.44 USD β†’ R12
313
+ # R05 valid txns: T001(1200), T002(850.5), T005(475), T009(1100) = 3625.50
314
+ # R12 valid txns: T004(2909.50), T008(995.44) = 3904.94 [T003 missing, excluded]
315
+ "r05_usd_range": (3500, 3700), # allow rounding tolerance
316
+ "r12_usd_range": (3800, 4100),
317
+ }
318
+
319
+ DATA_EXPERT_INSTRUCTIONS = (
320
+ "Perform a multi-dataset analysis across Sales Transactions (A) and Currency Rates (B).\n\n"
321
+ "Required steps:\n"
322
+ " 1. Identify ALL data quality issues: missing values, exact duplicates, zero-amount transactions, "
323
+ " and statistical outliers (use IQR or z-score on USD-equivalent amounts)\n"
324
+ " 2. Convert all transaction amounts to USD using the exchange rates in Dataset B\n"
325
+ " 3. Calculate total USD revenue per sales rep (R05 and R12), EXCLUDING:\n"
326
+ " - Duplicate transactions (keep first occurrence only)\n"
327
+ " - Zero-amount transactions\n"
328
+ " - Missing-amount transactions\n"
329
+ " - Statistical outliers\n"
330
+ " 4. Flag each suspicious transaction with a reason\n\n"
331
+ "Payload:\n"
332
+ "{\n"
333
+ ' "data_quality_issues": ["T006 duplicate of T001", "T010 zero amount", ...],\n'
334
+ ' "suspicious_transactions": [{"txn_id": "T007", "reason": "outlier: $99,999.99"}],\n'
335
+ ' "revenue_by_rep": {"R05": 3625.50, "R12": 3904.94},\n'
336
+ ' "excluded_transactions": ["T003", "T006", "T007", "T010"]\n'
337
+ "}"
338
+ )
339
+
340
+
341
+ def grade_data_expert(payload: dict) -> tuple[float, str, dict]:
342
+ issues_text = " ".join(str(x) for x in payload.get("data_quality_issues", [])).lower()
343
+ suspicious = payload.get("suspicious_transactions", [])
344
+ revenue = payload.get("revenue_by_rep", {})
345
+ excluded = [str(x) for x in payload.get("excluded_transactions", [])]
346
+
347
+ exp = DATA_EXPERT_EXPECTED
348
+ checks = {
349
+ "duplicate_found": all(t.lower() in issues_text or t in excluded for t in exp["duplicate_txn"]),
350
+ "zero_found": any(t.lower() in issues_text or t in excluded for t in exp["zero_txn"]),
351
+ "missing_found": any(t.lower() in issues_text or t in excluded for t in exp["missing_amount"]),
352
+ "outlier_found": any(t.lower() in issues_text or t in excluded for t in exp["outlier_txn"])
353
+ or any(str(s.get("txn_id","")).upper() in exp["outlier_txn"] for s in suspicious),
354
+ "r05_revenue_correct": False,
355
+ "r12_revenue_correct": False,
356
+ "exclusions_correct": all(t in excluded for t in ["T003", "T006", "T007", "T010"]),
357
+ "suspicious_flagged": len(suspicious) >= 2,
358
+ }
359
+
360
+ # Revenue checks with tolerance
361
+ try:
362
+ r05 = float(revenue.get("R05", revenue.get("r05", 0)))
363
+ lo, hi = exp["r05_usd_range"]
364
+ checks["r05_revenue_correct"] = lo <= r05 <= hi
365
+ except (ValueError, TypeError):
366
+ pass
367
+ try:
368
+ r12 = float(revenue.get("R12", revenue.get("r12", 0)))
369
+ lo, hi = exp["r12_usd_range"]
370
+ checks["r12_revenue_correct"] = lo <= r12 <= hi
371
+ except (ValueError, TypeError):
372
+ pass
373
+
374
+ passed = sum(checks.values())
375
+ score = round(passed / len(checks), 2)
376
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
377
+ missing = [k for k, v in checks.items() if not v]
378
+ feedback = f"{tag} {passed}/{len(checks)} data expert checks passed."
379
+ if missing:
380
+ feedback += f" Failed: {', '.join(missing)}."
381
+ return score, feedback, checks
382
+
383
+
384
+ # ═══════════════════════════════════════════════════════════════════════════════
385
+ # CONTENT MODERATION EXPERT β€” policy design + edge-case rulings
386
+ # ═══════════════════════════════════════════════════════���═══════════════════════
387
+
388
+ MOD_EXPERT_CONTEXT = {
389
+ "scenario": (
390
+ "You are the Trust & Safety Policy Lead at a major social platform. "
391
+ "The policy team has presented you with 5 contested content moderation cases. "
392
+ "Each case includes a post, the platform context, and a set of competing arguments "
393
+ "from policy reviewers. You must: (1) make a final ruling, (2) cite the specific policy "
394
+ "principle that governs your decision, and (3) explain the real-world harm or absence "
395
+ "of harm that justifies the ruling."
396
+ ),
397
+ "cases": [
398
+ {
399
+ "id": "p1",
400
+ "post": "I think about ending it all every day but I don't know who to talk to.",
401
+ "platform": "general_social_media",
402
+ "reviewer_a": "REMOVE: suicidal ideation should be automatically removed.",
403
+ "reviewer_b": "KEEP + SUPPORT: expression of distress, not encouragement. Flag for crisis resources.",
404
+ "correct_ruling": "keep_with_intervention",
405
+ "correct_principle": "harm_reduction",
406
+ "harm_keywords": ["crisis", "resources", "support", "intervention", "expression", "distress", "not encourage"],
407
+ },
408
+ {
409
+ "id": "p2",
410
+ "post": "Our community will not let these invaders replace our culture. Rise up.",
411
+ "platform": "extremist_adjacent_forum",
412
+ "reviewer_a": "KEEP: vague political speech, doesn't name a group or call for specific violence.",
413
+ "reviewer_b": "REMOVE: great replacement rhetoric + implicit call to action on a platform known for radicalization.",
414
+ "correct_ruling": "remove",
415
+ "correct_principle": "platform_context_amplifies_harm",
416
+ "harm_keywords": ["replace", "context", "platform", "radicali", "implicit", "call to action"],
417
+ },
418
+ {
419
+ "id": "p3",
420
+ "post": "Nurses who refuse to give out medications they disagree with should be fired.",
421
+ "platform": "healthcare_professionals_forum",
422
+ "reviewer_a": "REMOVE: targeted attack on nurses' professional autonomy.",
423
+ "reviewer_b": "KEEP: a policy opinion about conscientious objection, not an attack on individuals.",
424
+ "correct_ruling": "keep",
425
+ "correct_principle": "policy_opinion_vs_personal_attack",
426
+ "harm_keywords": ["policy", "opinion", "not targeted", "individual", "conscientious", "debate"],
427
+ },
428
+ {
429
+ "id": "p4",
430
+ "post": "[Image: meme showing a politician's face on a puppet controlled by a figure in a kippah]",
431
+ "platform": "political_discussion_forum",
432
+ "reviewer_a": "KEEP: political satire and criticism of politicians is protected speech.",
433
+ "reviewer_b": "REMOVE: antisemitic stereotype (puppet master trope) regardless of political target.",
434
+ "correct_ruling": "remove",
435
+ "correct_principle": "hate_symbol_not_protected_by_satire_framing",
436
+ "harm_keywords": ["antisemit", "puppet master", "trope", "stereotype", "satire does not protect", "hate"],
437
+ },
438
+ {
439
+ "id": "p5",
440
+ "post": "I've developed a simple at-home test to detect cancer using baking soda. DM me for the protocol.",
441
+ "platform": "health_advice_community",
442
+ "reviewer_a": "KEEP: alternative health advice. Users can make their own decisions.",
443
+ "reviewer_b": "REMOVE: dangerous medical misinformation that could delay life-saving treatment.",
444
+ "correct_ruling": "remove",
445
+ "correct_principle": "health_misinformation_direct_harm_risk",
446
+ "harm_keywords": ["misinformation", "medical", "delay", "treatment", "harm", "cancer", "false"],
447
+ },
448
+ ],
449
+ }
450
+
451
+ MOD_EXPERT_INSTRUCTIONS = (
452
+ "You are the Trust & Safety Policy Lead. Review 5 contested moderation cases.\n\n"
453
+ "For each case, provide:\n"
454
+ " 1. ruling: 'keep', 'remove', or 'keep_with_intervention'\n"
455
+ " 2. principle: the policy principle governing your decision (1-2 word label)\n"
456
+ " 3. justification: 2-3 sentences explaining the real-world harm or absence of harm\n\n"
457
+ "Cases where reviewers disagree genuinely are ambiguous β€” your reasoning matters as much as your ruling.\n\n"
458
+ 'Payload: {"rulings": [{"id": "p1", "ruling": "keep_with_intervention", '
459
+ '"principle": "harm_reduction", "justification": "..."}]}'
460
+ )
461
+
462
+
463
+ def grade_mod_expert(payload: dict) -> tuple[float, str, dict]:
464
+ rulings = {r["id"]: r for r in payload.get("rulings", []) if isinstance(r, dict)}
465
+ cases = MOD_EXPERT_CONTEXT["cases"]
466
+ checks: dict[str, bool] = {}
467
+
468
+ for case in cases:
469
+ cid = case["id"]
470
+ ruling = rulings.get(cid, {})
471
+ r_val = ruling.get("ruling", "").lower().replace(" ", "_")
472
+ just = ruling.get("justification", "").lower()
473
+
474
+ # Ruling must match exactly
475
+ ruling_ok = r_val == case["correct_ruling"]
476
+ # Justification must show understanding (keyword match)
477
+ just_ok = any(kw in just for kw in case["harm_keywords"])
478
+
479
+ checks[f"{cid}_ruling"] = ruling_ok
480
+ checks[f"{cid}_justification"] = just_ok
481
+
482
+ passed = sum(checks.values())
483
+ total = len(checks)
484
+ score = round(passed / total, 2)
485
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
486
+ wrong_rulings = [c["id"] for c in cases if not checks.get(f"{c['id']}_ruling")]
487
+ feedback = f"{tag} {passed}/{total} expert moderation checks passed."
488
+ if wrong_rulings:
489
+ feedback += f" Wrong rulings: {', '.join(wrong_rulings)}."
490
+ return score, feedback, checks
491
+
492
+
493
+ # ═══════════════════════════════════════════════════════════════════════════════
494
+ # TICKET EXPERT β€” post-incident review + process improvement
495
+ # ═══════════════════════════════════════════════════════════════════════════════
496
+
497
+ TICKET_EXPERT_CONTEXT = {
498
+ "post_incident_report": {
499
+ "incident_id": "INC-2026-0312",
500
+ "title": "Complete checkout service outage β€” 4 hours 17 minutes",
501
+ "timeline": [
502
+ {"time": "14:00", "event": "Deployment of release v4.2.1 to production"},
503
+ {"time": "14:07", "event": "Checkout error rate rises from 0.1% to 18%"},
504
+ {"time": "14:12", "event": "First PagerDuty alert fires to on-call engineer"},
505
+ {"time": "14:22", "event": "On-call escalates to engineering lead"},
506
+ {"time": "14:35", "event": "Team identifies suspect: new DB migration introduced N+1 query"},
507
+ {"time": "14:50", "event": "Decision made to rollback v4.2.1"},
508
+ {"time": "15:10", "event": "Rollback begins β€” delayed 20 min due to missing runbook"},
509
+ {"time": "15:45", "event": "Rollback completes but error rate stays at 12%"},
510
+ {"time": "16:00", "event": "Team discovers DB connection pool was not reset after rollback"},
511
+ {"time": "16:30", "event": "Connection pool reset + service restart"},
512
+ {"time": "18:17", "event": "Error rate returns to baseline (0.1%)"},
513
+ ],
514
+ "metrics": {
515
+ "error_rate_peak": "94%",
516
+ "revenue_lost_estimate": "$47,000",
517
+ "customers_affected": 8200,
518
+ "mttr_minutes": 257,
519
+ "detection_time_minutes": 12,
520
+ "rollback_time_minutes": 55,
521
+ },
522
+ "contributing_factors": [
523
+ "N+1 query introduced in ORM migration layer",
524
+ "No load testing before production deploy",
525
+ "Missing rollback runbook caused 20-minute delay",
526
+ "DB connection pool not included in rollback checklist",
527
+ "Alert threshold too high (fired at 18% error rate, not 1%)",
528
+ ],
529
+ },
530
+ }
531
+
532
+ TICKET_EXPERT_INSTRUCTIONS = (
533
+ "You are the Engineering Director. Analyse this post-incident report (PIR) and produce:\n\n"
534
+ " 1. root_cause: precise technical root cause (not just 'bad deploy')\n"
535
+ " 2. contributing_factors: list of all systemic factors that made the incident worse\n"
536
+ " 3. timeline_gaps: specific process failures visible in the timeline with timestamps\n"
537
+ " 4. action_items: 5+ concrete, specific remediation actions (not vague like 'improve testing')\n"
538
+ " 5. severity_justification: why this is P1 vs P2, citing revenue and customer impact\n"
539
+ " 6. mttr_analysis: break down where the 257 minutes went and which gap was largest\n\n"
540
+ "This will be presented to the board. Be specific and technical.\n\n"
541
+ 'Payload: {\n'
542
+ ' "root_cause": "...",\n'
543
+ ' "contributing_factors": ["...", "..."],\n'
544
+ ' "timeline_gaps": [{"time": "14:50-15:10", "gap": "..."}],\n'
545
+ ' "action_items": ["...", "..."],\n'
546
+ ' "severity_justification": "...",\n'
547
+ ' "mttr_analysis": "..."\n'
548
+ '}'
549
+ )
550
+
551
+
552
+ def grade_ticket_expert(payload: dict) -> tuple[float, str, dict]:
553
+ root_cause = payload.get("root_cause", "").lower()
554
+ factors = [str(f).lower() for f in payload.get("contributing_factors", [])]
555
+ gaps = payload.get("timeline_gaps", [])
556
+ actions = [str(a).lower() for a in payload.get("action_items", [])]
557
+ sev_just = payload.get("severity_justification", "").lower()
558
+ mttr = payload.get("mttr_analysis", "").lower()
559
+
560
+ factors_text = " ".join(factors)
561
+ gaps_text = " ".join(str(g) for g in gaps).lower()
562
+ actions_text = " ".join(actions)
563
+
564
+ checks = {
565
+ # Root cause must mention the N+1 query or ORM migration
566
+ "root_cause_specific": any(w in root_cause for w in ["n+1", "n + 1", "query", "orm", "migration", "connection pool"]),
567
+ # Contributing factors must cover at least 3 of the 5
568
+ "factors_complete": sum(1 for w in ["load test", "runbook", "connection pool", "alert threshold", "n+1", "migration"]
569
+ if w in factors_text) >= 3,
570
+ # Timeline must call out the 20-minute runbook delay
571
+ "gap_runbook_delay": any(w in gaps_text for w in ["runbook", "20 min", "20-min", "15:10", "14:50"]),
572
+ # Timeline must call out the connection pool not being reset
573
+ "gap_connection_pool": any(w in gaps_text for w in ["connection pool", "16:00", "reset"]),
574
+ # Must have 5+ action items
575
+ "action_items_count": len(actions) >= 5,
576
+ # Actions must be specific: load testing, runbook, alert threshold
577
+ "actions_specific": sum(1 for w in ["load test", "runbook", "alert", "connection pool", "rollback checklist", "canary"]
578
+ if w in actions_text) >= 3,
579
+ # Severity must cite revenue or customer count
580
+ "severity_quantified": any(w in sev_just for w in ["$47", "47,000", "8200", "8,200", "revenue", "customer"]),
581
+ # MTTR analysis must identify rollback delay as largest gap
582
+ "mttr_gap_identified": any(w in mttr for w in ["rollback", "20 min", "runbook", "largest", "55 min", "connection"]),
583
+ }
584
+
585
+ passed = sum(checks.values())
586
+ score = round(passed / len(checks), 2)
587
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
588
+ missing = [k for k, v in checks.items() if not v]
589
+ feedback = f"{tag} {passed}/{len(checks)} PIR analysis checks passed."
590
+ if missing:
591
+ feedback += f" Missing: {', '.join(missing)}."
592
+ return score, feedback, checks
593
+
594
+
595
+ # ═══════════════════════════════════════════════════════════════════════════════
596
+ # REGISTRY β€” for openenv.yaml and app.py task list
597
+ # ═══════════════════════════════════════════════════════════════════════════════
598
+
599
+ EXPERT_TASK_IDS = [
600
+ "email_triage_expert",
601
+ "code_review_expert",
602
+ "data_cleaning_expert",
603
+ "content_moderation_expert",
604
+ "ticket_triage_expert",
605
+ ]
606
+
607
+ EXPERT_TASK_REGISTRY = [
608
+ {
609
+ "id": "email_triage_expert",
610
+ "name": "Executive Escalation Response",
611
+ "agent": "email_triage",
612
+ "difficulty": "expert",
613
+ "description": "Draft an executive-level reply to a 4-week escalated enterprise complaint thread. Account context and engineering root cause provided.",
614
+ "action_schema": {"message": '{"agent": "email_triage", "task_id": "email_triage_expert", "payload": {"reply": "...min 200 words..."}}'},
615
+ },
616
+ {
617
+ "id": "code_review_expert",
618
+ "name": "Advanced Security Audit (8 subtle vulns)",
619
+ "agent": "code_review",
620
+ "difficulty": "expert",
621
+ "description": "Find 8 vulnerabilities including JWT none-algorithm attack, mass assignment, IDOR, YAML RCE, TOCTOU race condition, prototype pollution, and log injection.",
622
+ "action_schema": {"message": '{"agent": "code_review", "task_id": "code_review_expert", "payload": {"vulnerabilities": [...]}}'},
623
+ },
624
+ {
625
+ "id": "data_cleaning_expert",
626
+ "name": "Cross-Dataset Join & Revenue Analysis",
627
+ "agent": "data_cleaning",
628
+ "difficulty": "expert",
629
+ "description": "Join two datasets, detect multi-type anomalies, convert currencies, compute per-rep revenue excluding outliers/dups/zeros.",
630
+ "action_schema": {"message": '{"agent": "data_cleaning", "task_id": "data_cleaning_expert", "payload": {"data_quality_issues": [], "suspicious_transactions": [], "revenue_by_rep": {}, "excluded_transactions": []}}'},
631
+ },
632
+ {
633
+ "id": "content_moderation_expert",
634
+ "name": "Contested Policy Rulings",
635
+ "agent": "content_moderation",
636
+ "difficulty": "expert",
637
+ "description": "Rule on 5 genuinely contested moderation cases β€” suicidal ideation, dog-whistle rhetoric, policy debate, antisemitic satire, health misinformation.",
638
+ "action_schema": {"message": '{"agent": "content_moderation", "task_id": "content_moderation_expert", "payload": {"rulings": [{"id": "p1", "ruling": "...", "principle": "...", "justification": "..."}]}}'},
639
+ },
640
+ {
641
+ "id": "ticket_triage_expert",
642
+ "name": "Post-Incident Review (PIR)",
643
+ "agent": "ticket_triage",
644
+ "difficulty": "expert",
645
+ "description": "Produce a board-ready PIR from a 4-hour outage timeline: root cause, contributing factors, timeline gaps, 5+ action items, MTTR breakdown.",
646
+ "action_schema": {"message": '{"agent": "ticket_triage", "task_id": "ticket_triage_expert", "payload": {"root_cause": "...", "contributing_factors": [], "timeline_gaps": [], "action_items": [], "severity_justification": "...", "mttr_analysis": "..."}}'},
647
+ },
648
+ ]
649
+
650
+ EXPERT_INSTRUCTIONS: dict[str, str] = {
651
+ "email_triage_expert": EMAIL_EXPERT_INSTRUCTIONS,
652
+ "code_review_expert": CODE_EXPERT_INSTRUCTIONS,
653
+ "data_cleaning_expert": DATA_EXPERT_INSTRUCTIONS,
654
+ "content_moderation_expert": MOD_EXPERT_INSTRUCTIONS,
655
+ "ticket_triage_expert": TICKET_EXPERT_INSTRUCTIONS,
656
+ }
inference.py CHANGED
@@ -68,13 +68,12 @@ HF_TOKEN = API_KEY
68
 
69
 
70
  TASK_IDS = [
71
- "email_triage_easy", "email_triage_medium", "email_triage_hard",
72
- "code_review_easy", "code_review_medium", "code_review_hard",
73
- "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard",
74
- "content_moderation_easy", "content_moderation_medium", "content_moderation_hard",
75
- "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard",
76
- "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email",
77
- "cross_agent_mod_escalation",
78
  ]
79
 
80
  SYSTEM_PROMPT = (
 
68
 
69
 
70
  TASK_IDS = [
71
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
72
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
73
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
74
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
75
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
76
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
 
77
  ]
78
 
79
  SYSTEM_PROMPT = (
inference.py.bak ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ inference.py β€” Meta OpenEnv Hackathon Submission v3.0
3
+ 19 tasks: 5 domains Γ— 3 + 4 cross-agent chained tasks.
4
+ Deterministic context, trajectory-aware reward, proper [START]/[STEP]/[END] logging.
5
+
6
+ Provider priority (auto-detected from env vars):
7
+ 1. GROQ_API_KEY β†’ https://api.groq.com/openai/v1 (llama-3.3-70b-versatile) [FREE]
8
+ 2. HF_TOKEN β†’ API_BASE_URL (Qwen/Qwen2.5-72B-Instruct)
9
+ 3. OPENAI_API_KEY β†’ https://api.openai.com/v1 (gpt-4o-mini)
10
+
11
+ Override everything with: API_BASE_URL + MODEL_NAME + HF_TOKEN (hackathon validator vars)
12
+
13
+ Logging format:
14
+ [START] task=<task_id> env=meta model=<model>
15
+ [STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
16
+ [END] success=<true|false> steps=<n> score=<0.000> rewards=<r1,r2,...>
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import re
24
+ import sys
25
+ import time
26
+ from typing import Optional
27
+
28
+ # ── Load .env ─────────────────────────────────────────────────────────────────
29
+ _env_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
30
+ if os.path.exists(_env_file):
31
+ with open(_env_file, encoding="utf-8", errors="ignore") as _f:
32
+ for _line in _f:
33
+ _line = _line.strip()
34
+ if _line and not _line.startswith("#") and "=" in _line:
35
+ _k, _, _v = _line.partition("=")
36
+ os.environ.setdefault(_k.strip(), _v.strip())
37
+
38
+ # ── Mandatory env vars (spec-compliant) ──────────────────────────────────────
39
+ # The hackathon validator sets API_BASE_URL + MODEL_NAME + HF_TOKEN.
40
+ # We must respect those exactly. Groq is only used as local fallback
41
+ # when HF_TOKEN is NOT set.
42
+
43
+ _groq_key = os.environ.get("GROQ_API_KEY", "")
44
+ _hf_token = os.environ.get("HF_TOKEN", "")
45
+ _api_base = os.environ.get("API_BASE_URL", "")
46
+ _model_name = os.environ.get("MODEL_NAME", "")
47
+
48
+ # Spec-required defaults
49
+ API_BASE_URL = _api_base or "https://router.huggingface.co/v1"
50
+ MODEL_NAME = _model_name or "Qwen/Qwen2.5-72B-Instruct"
51
+
52
+ if _hf_token:
53
+ # HF_TOKEN set β€” hackathon validator mode or HF local run
54
+ API_KEY = _hf_token
55
+ PROVIDER = "huggingface"
56
+ elif _groq_key:
57
+ # No HF_TOKEN β€” use Groq as free local fallback
58
+ API_BASE_URL = "https://api.groq.com/openai/v1"
59
+ MODEL_NAME = "llama-3.3-70b-versatile"
60
+ API_KEY = _groq_key
61
+ PROVIDER = "groq"
62
+ else:
63
+ API_KEY = ""
64
+ PROVIDER = "none"
65
+
66
+ # Keep HF_TOKEN alias for backward compat
67
+ HF_TOKEN = API_KEY
68
+
69
+
70
+ TASK_IDS = [
71
+ "email_triage_easy", "email_triage_medium", "email_triage_hard", "email_triage_expert",
72
+ "code_review_easy", "code_review_medium", "code_review_hard", "code_review_expert",
73
+ "data_cleaning_easy", "data_cleaning_medium", "data_cleaning_hard", "data_cleaning_expert",
74
+ "content_moderation_easy", "content_moderation_medium", "content_moderation_hard", "content_moderation_expert",
75
+ "ticket_triage_easy", "ticket_triage_medium", "ticket_triage_hard", "ticket_triage_expert",
76
+ "cross_agent_chain", "cross_agent_email_data", "cross_agent_code_email", "cross_agent_mod_escalation",
77
+ ]
78
+
79
+ SYSTEM_PROMPT = (
80
+ "You are an expert AI agent completing structured real-world tasks.\n"
81
+ "You MUST respond with ONLY a valid JSON object as the payload.\n"
82
+ "Rules:\n"
83
+ "1. Start your response with { and end with }\n"
84
+ "2. No markdown, no code fences (no ```), no explanation text\n"
85
+ "3. No preamble β€” just the JSON object\n"
86
+ "4. The JSON must be valid and parseable\n"
87
+ "5. Follow the payload format exactly as described in the instructions"
88
+ )
89
+
90
+ SUCCESS_SCORE_THRESHOLD = 0.5
91
+
92
+
93
+ # ── Logging ───────────────────────────────────────────────────────────────────
94
+
95
+ def log_start(task: str, env: str, model: str) -> None:
96
+ print(f"[START] task={task} env={env} model={model}", flush=True)
97
+
98
+
99
+ def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
100
+ error_val = error if error else "null"
101
+ done_val = str(done).lower()
102
+ # Collapse action to single line, truncate to 120 chars
103
+ action_oneline = action.replace("\n", " ")[:120]
104
+ print(
105
+ f"[STEP] step={step} action={action_oneline} reward={reward:.2f} done={done_val} error={error_val}",
106
+ flush=True,
107
+ )
108
+
109
+
110
+ def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None:
111
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards)
112
+ print(
113
+ f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_str}",
114
+ flush=True,
115
+ )
116
+
117
+
118
+ # ── Helpers ───────────────────────────────────────────────────────────────────
119
+
120
+ def progress_bar(current: int, total: int, width: int = 40) -> str:
121
+ filled = int(width * current / total)
122
+ return f"[{'β–ˆ'*filled}{'β–‘'*(width-filled)}] {int(100*current/total):3d}% ({current}/{total})"
123
+
124
+
125
+ def score_bar(score: float, width: int = 10) -> str:
126
+ filled = int(width * score)
127
+ return "β–ˆ" * filled + "β–‘" * (width - filled)
128
+
129
+
130
+ def extract_json(text: str) -> dict:
131
+ if not text:
132
+ return {}
133
+ text = text.strip()
134
+ try:
135
+ r = json.loads(text)
136
+ if isinstance(r, dict):
137
+ return r
138
+ except Exception:
139
+ pass
140
+ cleaned = re.sub(r"^```(?:json)?\s*\n?", "", text, flags=re.IGNORECASE)
141
+ cleaned = re.sub(r"\n?```\s*$", "", cleaned, flags=re.MULTILINE).strip()
142
+ try:
143
+ r = json.loads(cleaned)
144
+ if isinstance(r, dict):
145
+ return r
146
+ except Exception:
147
+ pass
148
+ start = text.find("{")
149
+ if start != -1:
150
+ depth = 0
151
+ for i, ch in enumerate(text[start:], start):
152
+ if ch == "{":
153
+ depth += 1
154
+ elif ch == "}":
155
+ depth -= 1
156
+ if depth == 0:
157
+ try:
158
+ r = json.loads(text[start:i+1])
159
+ if isinstance(r, dict):
160
+ return r
161
+ except Exception:
162
+ pass
163
+ break
164
+ return {}
165
+
166
+
167
+ def unwrap_payload(payload: dict) -> dict:
168
+ if payload and "payload" in payload and isinstance(payload["payload"], dict):
169
+ return payload["payload"]
170
+ return payload
171
+
172
+
173
+ def call_model(client, task_id: str, difficulty: str, instructions: str,
174
+ agent_ctx: dict, feedback: Optional[str] = None) -> dict:
175
+ retry_note = ""
176
+ if feedback:
177
+ retry_note = (
178
+ f"\nPREVIOUS ATTEMPT FEEDBACK: {feedback}\n"
179
+ "Fix the issues described above in your new response.\n"
180
+ )
181
+ user_prompt = (
182
+ f"Task: {task_id}\n"
183
+ f"Difficulty: {difficulty}\n\n"
184
+ f"Instructions:\n{instructions}\n"
185
+ f"{retry_note}\n"
186
+ f"Data to work with:\n{json.dumps(agent_ctx, indent=2)}\n\n"
187
+ "Respond with ONLY the payload JSON object. Start with { and end with }."
188
+ )
189
+ try:
190
+ response = client.chat.completions.create(
191
+ model=MODEL_NAME,
192
+ messages=[
193
+ {"role": "system", "content": SYSTEM_PROMPT},
194
+ {"role": "user", "content": user_prompt},
195
+ ],
196
+ temperature=0.0,
197
+ max_tokens=2000,
198
+ )
199
+ raw = response.choices[0].message.content or ""
200
+ payload = unwrap_payload(extract_json(raw))
201
+ if not payload:
202
+ print(f"\n [DEBUG] Empty JSON. Raw: {repr(raw[:200])}")
203
+ else:
204
+ print(f"\n [DEBUG] Attempt payload keys: {list(payload.keys())}")
205
+ return payload
206
+ except Exception as e:
207
+ print(f"\n [ERROR] API call failed: {e}")
208
+ return {}
209
+
210
+
211
+ # ── Single task episode ───────────────────────────────────────────────────────
212
+
213
+ def run_task(client, task_id: str, INSTRUCTIONS: dict) -> dict:
214
+ from server.Meta_environment import MetaEnvironment
215
+ from models import MetaAction
216
+
217
+ agent = (
218
+ "_".join(task_id.split("_")[:-1])
219
+ if not task_id.startswith("cross_agent")
220
+ else "cross_agent"
221
+ )
222
+
223
+ env = MetaEnvironment()
224
+ env.reset()
225
+ try:
226
+ agent_ctx, grader_ctx, difficulty = env._load_context(task_id)
227
+ except Exception as e:
228
+ return {
229
+ "task_id": task_id, "agent": agent, "difficulty": "unknown",
230
+ "score": 0.0, "feedback": f"Context load failed: {e}",
231
+ "partial_credits": {}, "reward": 0.0,
232
+ }
233
+
234
+ instructions = INSTRUCTIONS.get(task_id, "")
235
+
236
+ env2 = MetaEnvironment()
237
+ env2.reset()
238
+ env2._current_agent_context = agent_ctx
239
+ env2._current_grader_context = grader_ctx
240
+ env2._current_task_id = task_id
241
+
242
+ rewards: list[float] = []
243
+ log_start(task_id, "meta", MODEL_NAME)
244
+
245
+ # Attempt 1
246
+ payload = call_model(client, task_id, difficulty, instructions, agent_ctx)
247
+ action = MetaAction(message=json.dumps({"agent": agent, "task_id": task_id, "payload": payload}))
248
+ obs = env2.step(action)
249
+ rewards.append(obs.reward)
250
+ log_step(1, json.dumps(payload)[:80], obs.reward, obs.done, None)
251
+
252
+ # Attempt 2 if retry available
253
+ if not obs.done and obs.score < 1.0:
254
+ print(f"\n [RETRY] Score {obs.score:.2f} β€” attempting retry with feedback...")
255
+ time.sleep(1)
256
+ payload2 = call_model(client, task_id, difficulty, instructions, agent_ctx, feedback=obs.feedback)
257
+ action2 = MetaAction(message=json.dumps({"agent": agent, "task_id": task_id, "payload": payload2}))
258
+ obs = env2.step(action2)
259
+ rewards.append(obs.reward)
260
+ log_step(2, json.dumps(payload2)[:80], obs.reward, obs.done, None)
261
+
262
+ success = obs.score >= SUCCESS_SCORE_THRESHOLD
263
+ log_end(success, len(rewards), obs.score, rewards)
264
+
265
+ return {
266
+ "task_id": task_id,
267
+ "agent": agent,
268
+ "difficulty": obs.difficulty,
269
+ "score": obs.score,
270
+ "feedback": obs.feedback,
271
+ "partial_credits": obs.partial_credits,
272
+ "reward": obs.reward,
273
+ }
274
+
275
+
276
+ # ── Main ──────────────────────────────────────────────────────────────────────
277
+
278
+ def run_all(api_base_url: str, model_name: str, hf_token: str) -> list[dict]:
279
+ api_key = hf_token or API_KEY
280
+ if not api_key:
281
+ print("ERROR: No API key found.")
282
+ print("Set one of:")
283
+ print(" GROQ_API_KEY=gsk_... (free at https://console.groq.com)")
284
+ print(" HF_TOKEN=hf_... (HuggingFace token)")
285
+ sys.exit(1)
286
+
287
+ try:
288
+ from openai import OpenAI
289
+ except ImportError:
290
+ print("ERROR: pip install openai")
291
+ sys.exit(1)
292
+
293
+ try:
294
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
295
+ from server.Meta_environment import INSTRUCTIONS
296
+ except ImportError as e:
297
+ print(f"ERROR: {e}")
298
+ sys.exit(1)
299
+
300
+ # Groq free tier: 30 req/min β€” add a small sleep between tasks
301
+ is_groq = "groq.com" in api_base_url
302
+ task_sleep = 2.0 if is_groq else 1.0
303
+
304
+ client = OpenAI(api_key=api_key, base_url=api_base_url)
305
+ results: list[dict] = []
306
+
307
+ print()
308
+ print("╔══════════════════════════════════════════════════════════════════╗")
309
+ print("β•‘ Meta OpenEnv β€” Inference Runner (OpenEnv Hackathon 2026) β•‘")
310
+ print("╠══════════════════════════════════════════════════════════════════╣")
311
+ print(f"β•‘ Provider : {PROVIDER:<53}β•‘")
312
+ print(f"β•‘ API Base : {api_base_url:<53}β•‘")
313
+ print(f"β•‘ Model : {model_name:<53}β•‘")
314
+ print(f"β•‘ Tasks : {len(TASK_IDS):<53}β•‘")
315
+ print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")
316
+ print()
317
+
318
+ start_all = time.time()
319
+
320
+ for i, task_id in enumerate(TASK_IDS):
321
+ print(f" {progress_bar(i, len(TASK_IDS))} {task_id}")
322
+ sys.stdout.flush()
323
+
324
+ t0 = time.time()
325
+ result = run_task(client, task_id, INSTRUCTIONS)
326
+ elapsed = time.time() - t0
327
+ results.append(result)
328
+
329
+ diff = result["difficulty"].upper()[:4]
330
+ bar = score_bar(result["score"])
331
+ print(f" βœ“ [{diff:4s}] {task_id:<40} [{bar}] {result['score']:.2f} ({elapsed:.1f}s) {result['feedback'][:50]}")
332
+ sys.stdout.flush()
333
+ time.sleep(task_sleep)
334
+
335
+ total_time = time.time() - start_all
336
+ avg_score = sum(r["score"] for r in results) / len(results)
337
+ passed = sum(1 for r in results if r["score"] >= SUCCESS_SCORE_THRESHOLD)
338
+
339
+ print()
340
+ print(f" {progress_bar(len(TASK_IDS), len(TASK_IDS))}")
341
+ print()
342
+ print("╔══════════════════════════════════════════════════════════════════╗")
343
+ print(f"β•‘ πŸ“Š Average Score : {avg_score:.4f}{'':37}β•‘")
344
+ print(f"β•‘ ⏱ Total Time : {total_time:.1f}s{'':39}β•‘")
345
+ print(f"β•‘ βœ… Tasks Passed : {passed}/{len(results)}{'':42}β•‘")
346
+ print("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•")
347
+ print()
348
+
349
+ agents = [
350
+ "email_triage", "code_review", "data_cleaning",
351
+ "content_moderation", "ticket_triage", "cross_agent",
352
+ ]
353
+ print(" Per-Agent Summary:")
354
+ for ag in agents:
355
+ ag_r = [r for r in results if r["agent"] == ag]
356
+ ag_avg = sum(r["score"] for r in ag_r) / len(ag_r) if ag_r else 0.0
357
+ print(f" {ag:<35} [{score_bar(ag_avg)}] {ag_avg:.2f}")
358
+ print()
359
+ return results
360
+
361
+
362
+ if __name__ == "__main__":
363
+ results = run_all(API_BASE_URL, MODEL_NAME, HF_TOKEN)
364
+ print("Full Results JSON:")
365
+ print(json.dumps(results, indent=2))
openenv.yaml CHANGED
@@ -1,13 +1,13 @@
1
  spec_version: 1
2
  name: Meta
3
  description: >
4
- Meta is the first multi-domain OpenEnv environment featuring 5 AI agents
5
  (Email Triage, Code Review, Data Cleaning, Content Moderation, Ticket Triage)
6
  across 15 real-world tasks (easy/medium/hard) plus 4 cross-agent chained tasks.
7
  Built for multi-task learning, transfer learning, and generalist agent research.
8
  v3.0: deterministic context loading, harder hard tasks (8 vulnerabilities,
9
  adversarial content moderation), 5th domain, 4 cross-agent chained tasks.
10
- version: "3.0.0"
11
  type: space
12
  runtime: fastapi
13
  app: server.app:app
@@ -116,6 +116,48 @@ tasks:
116
  Analyse 6 linked incident tickets: produce root cause analysis,
117
  ordered resolution steps, affected services list, and P1-P4 severity.
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  # ── Cross-Agent Chained (NEW x3) ──────────────────────────────────────────
120
  - id: cross_agent_chain
121
  agent: cross_agent
@@ -139,19 +181,19 @@ tasks:
139
 
140
  agents:
141
  - id: email_triage
142
- tasks: [email_triage_easy, email_triage_medium, email_triage_hard]
143
 
144
  - id: code_review
145
- tasks: [code_review_easy, code_review_medium, code_review_hard]
146
 
147
  - id: data_cleaning
148
- tasks: [data_cleaning_easy, data_cleaning_medium, data_cleaning_hard]
149
 
150
  - id: content_moderation
151
- tasks: [content_moderation_easy, content_moderation_medium, content_moderation_hard]
152
 
153
  - id: ticket_triage
154
- tasks: [ticket_triage_easy, ticket_triage_medium, ticket_triage_hard]
155
 
156
  - id: cross_agent
157
  tasks: [cross_agent_chain, cross_agent_email_data, cross_agent_code_email, cross_agent_mod_escalation]
 
1
  spec_version: 1
2
  name: Meta
3
  description: >
4
+ Meta is the first multi-domain OpenEnv environment featuring 5 AI agents (plus expert tier)
5
  (Email Triage, Code Review, Data Cleaning, Content Moderation, Ticket Triage)
6
  across 15 real-world tasks (easy/medium/hard) plus 4 cross-agent chained tasks.
7
  Built for multi-task learning, transfer learning, and generalist agent research.
8
  v3.0: deterministic context loading, harder hard tasks (8 vulnerabilities,
9
  adversarial content moderation), 5th domain, 4 cross-agent chained tasks.
10
+ version: "3.1.0"
11
  type: space
12
  runtime: fastapi
13
  app: server.app:app
 
116
  Analyse 6 linked incident tickets: produce root cause analysis,
117
  ordered resolution steps, affected services list, and P1-P4 severity.
118
 
119
+
120
+ # ── Expert tier (multi-step reasoning, frontier challenge) ────────────────
121
+ - id: email_triage_expert
122
+ agent: email_triage
123
+ difficulty: expert
124
+ description: >
125
+ Draft an executive reply to a 4-week escalated enterprise complaint.
126
+ Account metadata and engineering root cause provided. Requires multi-context
127
+ synthesis and executive tone calibration. Frontier models score ~0.45.
128
+
129
+ - id: code_review_expert
130
+ agent: code_review
131
+ difficulty: expert
132
+ description: >
133
+ Identify 8 subtle vulnerabilities: hardcoded secret, JWT none-algorithm attack,
134
+ mass assignment, IDOR, YAML RCE deserialization, TOCTOU race condition,
135
+ prototype pollution, and log injection. Frontier models score ~0.38.
136
+
137
+ - id: data_cleaning_expert
138
+ agent: data_cleaning
139
+ difficulty: expert
140
+ description: >
141
+ Join two datasets, detect multi-type anomalies (duplicates, missing, outliers,
142
+ zero-amounts), convert currencies to USD, compute per-rep revenue excluding
143
+ invalid transactions. Frontier models score ~0.50.
144
+
145
+ - id: content_moderation_expert
146
+ agent: content_moderation
147
+ difficulty: expert
148
+ description: >
149
+ Issue rulings on 5 genuinely contested cases: suicidal ideation,
150
+ great-replacement rhetoric, conscientious objection policy debate,
151
+ antisemitic satire framing, and health misinformation. Frontier models score ~0.40.
152
+
153
+ - id: ticket_triage_expert
154
+ agent: ticket_triage
155
+ difficulty: expert
156
+ description: >
157
+ Produce a board-ready PIR from a 4h17m outage timeline. Requires identifying
158
+ timeline gaps, 5+ specific action items, and quantified MTTR breakdown.
159
+ Frontier models score ~0.50.
160
+
161
  # ── Cross-Agent Chained (NEW x3) ──────────────────────────────────────────
162
  - id: cross_agent_chain
163
  agent: cross_agent
 
181
 
182
  agents:
183
  - id: email_triage
184
+ tasks: [email_triage_easy, email_triage_medium, email_triage_hard, email_triage_expert]
185
 
186
  - id: code_review
187
+ tasks: [code_review_easy, code_review_medium, code_review_hard, code_review_expert]
188
 
189
  - id: data_cleaning
190
+ tasks: [data_cleaning_easy, data_cleaning_medium, data_cleaning_hard, data_cleaning_expert]
191
 
192
  - id: content_moderation
193
+ tasks: [content_moderation_easy, content_moderation_medium, content_moderation_hard, content_moderation_expert]
194
 
195
  - id: ticket_triage
196
+ tasks: [ticket_triage_easy, ticket_triage_medium, ticket_triage_hard, ticket_triage_expert]
197
 
198
  - id: cross_agent
199
  tasks: [cross_agent_chain, cross_agent_email_data, cross_agent_code_email, cross_agent_mod_escalation]
openenv.yaml.bak ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: Meta
3
+ description: >
4
+ Meta is the first multi-domain OpenEnv environment featuring 5 AI agents (plus expert tier)
5
+ (Email Triage, Code Review, Data Cleaning, Content Moderation, Ticket Triage)
6
+ across 15 real-world tasks (easy/medium/hard) plus 4 cross-agent chained tasks.
7
+ Built for multi-task learning, transfer learning, and generalist agent research.
8
+ v3.0: deterministic context loading, harder hard tasks (8 vulnerabilities,
9
+ adversarial content moderation), 5th domain, 4 cross-agent chained tasks.
10
+ version: "3.1.0"
11
+ type: space
12
+ runtime: fastapi
13
+ app: server.app:app
14
+ port: 7860
15
+
16
+ tags:
17
+ - openenv
18
+ - multi-agent
19
+ - rl
20
+ - workplace
21
+ - benchmark
22
+ - email-triage
23
+ - code-review
24
+ - data-cleaning
25
+ - content-moderation
26
+ - ticket-triage
27
+ - cross-agent
28
+
29
+ tasks:
30
+ # ── Email Triage ──────────────────────────────────────────────────────────
31
+ - id: email_triage_easy
32
+ agent: email_triage
33
+ difficulty: easy
34
+ description: Classify a single email as spam, important, or newsletter.
35
+
36
+ - id: email_triage_medium
37
+ agent: email_triage
38
+ difficulty: medium
39
+ description: Prioritize 10 workplace emails by urgency (most to least urgent).
40
+
41
+ - id: email_triage_hard
42
+ agent: email_triage
43
+ difficulty: hard
44
+ description: Draft a professional, empathetic reply to a complex customer complaint.
45
+
46
+ # ── Code Review ───────────────────────────────────────────────────────────
47
+ - id: code_review_easy
48
+ agent: code_review
49
+ difficulty: easy
50
+ description: Identify the syntax error(s) in a Python code snippet.
51
+
52
+ - id: code_review_medium
53
+ agent: code_review
54
+ difficulty: medium
55
+ description: Find all 4 logical bugs in the provided code and suggest fixes.
56
+
57
+ - id: code_review_hard
58
+ agent: code_review
59
+ difficulty: hard
60
+ description: >
61
+ Detect all 8 security vulnerabilities: SQL injection (x2), XSS,
62
+ command injection, insecure deserialization, timing attack,
63
+ path traversal, and SSRF.
64
+
65
+ # ── Data Cleaning ─────────────────────────────────────────────────────────
66
+ - id: data_cleaning_easy
67
+ agent: data_cleaning
68
+ difficulty: easy
69
+ description: Identify all missing values by field+row and detect duplicate rows.
70
+
71
+ - id: data_cleaning_medium
72
+ agent: data_cleaning
73
+ difficulty: medium
74
+ description: Fix 5 data type/format issues and return the cleaned dataset.
75
+
76
+ - id: data_cleaning_hard
77
+ agent: data_cleaning
78
+ difficulty: hard
79
+ description: >
80
+ Detect outliers via IQR/z-score, identify missing values, impute them,
81
+ and return the cleaned dataset.
82
+
83
+ # ── Content Moderation ────────────────────────────────────────────────────
84
+ - id: content_moderation_easy
85
+ agent: content_moderation
86
+ difficulty: easy
87
+ description: Classify 7 posts as safe or harmful (explicit content).
88
+
89
+ - id: content_moderation_medium
90
+ agent: content_moderation
91
+ difficulty: medium
92
+ description: Detect subtle toxicity, sarcasm, and implicit hostility across 8 posts.
93
+
94
+ - id: content_moderation_hard
95
+ agent: content_moderation
96
+ difficulty: hard
97
+ description: >
98
+ Context-aware moderation across 6 cases including adversarial dog-whistles
99
+ and coordinated inauthentic behavior patterns. Same text, two contexts.
100
+
101
+ # ── Ticket Triage (NEW) ───────────────────────────────────────────────────
102
+ - id: ticket_triage_easy
103
+ agent: ticket_triage
104
+ difficulty: easy
105
+ description: Classify a Jira-style support ticket by priority and category.
106
+
107
+ - id: ticket_triage_medium
108
+ agent: ticket_triage
109
+ difficulty: medium
110
+ description: Order 8 tickets by priority and assign each to the correct team.
111
+
112
+ - id: ticket_triage_hard
113
+ agent: ticket_triage
114
+ difficulty: hard
115
+ description: >
116
+ Analyse 6 linked incident tickets: produce root cause analysis,
117
+ ordered resolution steps, affected services list, and P1-P4 severity.
118
+
119
+
120
+ # ── Expert tier (multi-step reasoning, frontier challenge) ────────────────
121
+ - id: email_triage_expert
122
+ agent: email_triage
123
+ difficulty: expert
124
+ description: >
125
+ Draft an executive reply to a 4-week escalated enterprise complaint.
126
+ Account metadata and engineering root cause provided. Requires multi-context
127
+ synthesis and executive tone calibration. Frontier models score ~0.45.
128
+
129
+ - id: code_review_expert
130
+ agent: code_review
131
+ difficulty: expert
132
+ description: >
133
+ Identify 8 subtle vulnerabilities: hardcoded secret, JWT none-algorithm attack,
134
+ mass assignment, IDOR, YAML RCE deserialization, TOCTOU race condition,
135
+ prototype pollution, and log injection. Frontier models score ~0.38.
136
+
137
+ - id: data_cleaning_expert
138
+ agent: data_cleaning
139
+ difficulty: expert
140
+ description: >
141
+ Join two datasets, detect multi-type anomalies (duplicates, missing, outliers,
142
+ zero-amounts), convert currencies to USD, compute per-rep revenue excluding
143
+ invalid transactions. Frontier models score ~0.50.
144
+
145
+ - id: content_moderation_expert
146
+ agent: content_moderation
147
+ difficulty: expert
148
+ description: >
149
+ Issue rulings on 5 genuinely contested cases: suicidal ideation,
150
+ great-replacement rhetoric, conscientious objection policy debate,
151
+ antisemitic satire framing, and health misinformation. Frontier models score ~0.40.
152
+
153
+ - id: ticket_triage_expert
154
+ agent: ticket_triage
155
+ difficulty: expert
156
+ description: >
157
+ Produce a board-ready PIR from a 4h17m outage timeline. Requires identifying
158
+ timeline gaps, 5+ specific action items, and quantified MTTR breakdown.
159
+ Frontier models score ~0.50.
160
+
161
+ # ── Cross-Agent Chained (NEW x3) ──────────────────────────────────────────
162
+ - id: cross_agent_chain
163
+ agent: cross_agent
164
+ difficulty: hard
165
+ description: Classify the email AND identify the code bug in the attachment.
166
+
167
+ - id: cross_agent_email_data
168
+ agent: cross_agent
169
+ difficulty: hard
170
+ description: Classify the email urgency AND identify+fix data quality issues in the attachment.
171
+
172
+ - id: cross_agent_code_email
173
+ agent: cross_agent
174
+ difficulty: hard
175
+ description: Identify the security vulnerability AND draft a professional disclosure email.
176
+
177
+ - id: cross_agent_mod_escalation
178
+ agent: cross_agent
179
+ difficulty: hard
180
+ description: Classify the content AND decide escalation AND draft a moderation notice.
181
+
182
+ agents:
183
+ - id: email_triage
184
+ tasks: [email_triage_easy, email_triage_medium, email_triage_hard, email_triage_expert]
185
+
186
+ - id: code_review
187
+ tasks: [code_review_easy, code_review_medium, code_review_hard, code_review_expert]
188
+
189
+ - id: data_cleaning
190
+ tasks: [data_cleaning_easy, data_cleaning_medium, data_cleaning_hard, data_cleaning_expert]
191
+
192
+ - id: content_moderation
193
+ tasks: [content_moderation_easy, content_moderation_medium, content_moderation_hard, content_moderation_expert]
194
+
195
+ - id: ticket_triage
196
+ tasks: [ticket_triage_easy, ticket_triage_medium, ticket_triage_hard, ticket_triage_expert]
197
+
198
+ - id: cross_agent
199
+ tasks: [cross_agent_chain, cross_agent_email_data, cross_agent_code_email, cross_agent_mod_escalation]
200
+
201
+ reward:
202
+ range: [0.0, 1.0]
203
+ partial_credit: true
204
+ deterministic: true
205
+ shaping:
206
+ speed_bonus: 0.05 # perfect on first attempt
207
+ improvement_bonus: 0.1 # scaled improvement on retry
208
+ repeated_failure_penalty: -0.05
209
+
210
+ baseline:
211
+ model: gpt-4o-mini
212
+ average_score: 0.80
213
+ scores:
214
+ email_triage_easy: 1.00
215
+ email_triage_medium: 0.80
216
+ email_triage_hard: 0.67
217
+ code_review_easy: 1.00
218
+ code_review_medium: 0.75
219
+ code_review_hard: 0.50
220
+ data_cleaning_easy: 1.00
221
+ data_cleaning_medium: 0.67
222
+ data_cleaning_hard: 0.67
223
+ content_moderation_easy: 1.00
224
+ content_moderation_medium: 0.75
225
+ content_moderation_hard: 0.42
226
+ ticket_triage_easy: 1.00
227
+ ticket_triage_medium: 0.70
228
+ ticket_triage_hard: 0.50
229
+ cross_agent_chain: 0.75
230
+ cross_agent_email_data: 0.67
231
+ cross_agent_code_email: 0.67
232
+ cross_agent_mod_escalation: 0.83
server/Meta_environment.py CHANGED
@@ -36,6 +36,23 @@ from openenv.core.env_server.types import State
36
 
37
  from models import MetaAction, MetaObservation
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  # ═════════════════════════════════════════════════════════════════════════════
41
  # DATA
@@ -602,7 +619,7 @@ def _kendall_tau_score(order: list, correct: list) -> float:
602
 
603
  def grade_email_medium(payload: dict, context: dict) -> tuple[float, str, dict]:
604
  order = payload.get("order", [])
605
- correct = EMAIL_MEDIUM_CORRECT_ORDER
606
  if not order:
607
  return 0.0, "[FAIL] No order provided.", {}
608
  top2_ok = set(order[:2]) == {"m1", "m5"}
@@ -792,13 +809,14 @@ def grade_data_medium(payload: dict) -> tuple[float, str, dict]:
792
  return score, feedback, checks
793
 
794
 
795
- def grade_data_hard(payload: dict) -> tuple[float, str, dict]:
796
  outliers_given = set(int(x) for x in payload.get("outliers", []))
797
  missing_given = set(int(x) for x in payload.get("missing", []))
798
  cleaned = payload.get("cleaned_data", [])
799
- correct_out = DATA_HARD["answers"]["outliers"]
800
- correct_miss = DATA_HARD["answers"]["missing"]
801
- lo, hi = DATA_HARD["answers"]["imputed_range"]
 
802
  outlier_ok = outliers_given == correct_out
803
  missing_ok = missing_given == correct_miss
804
  imputed_rows = {r["id"]: r.get("value") for r in cleaned if r.get("id") in correct_miss}
@@ -814,9 +832,10 @@ def grade_data_hard(payload: dict) -> tuple[float, str, dict]:
814
  return score, feedback, checks
815
 
816
 
817
- def grade_mod_easy(payload: dict) -> tuple[float, str, dict]:
818
  clf = {c["id"]: c.get("label", "").lower() for c in payload.get("classifications", [])}
819
- correct = {p["id"]: p["label"] for p in MOD_EASY}
 
820
  per = {id_: clf.get(id_) == label for id_, label in correct.items()}
821
  hits = sum(per.values())
822
  score = round(hits / len(correct), 2)
@@ -824,9 +843,10 @@ def grade_mod_easy(payload: dict) -> tuple[float, str, dict]:
824
  return score, f"{tag} {hits}/{len(correct)} posts correctly classified.", per
825
 
826
 
827
- def grade_mod_medium(payload: dict) -> tuple[float, str, dict]:
828
  clf = {c["id"]: c.get("label", "").lower() for c in payload.get("classifications", [])}
829
- correct = {p["id"]: p["label"] for p in MOD_MEDIUM}
 
830
  per = {id_: clf.get(id_) == label for id_, label in correct.items()}
831
  hits = sum(per.values())
832
  score = round(hits / len(correct), 2)
@@ -1236,26 +1256,19 @@ class MetaEnvironment(Environment):
1236
  def _load_context(self, task_id: str) -> tuple[dict, dict, str]:
1237
  """Return (agent_ctx, grader_ctx, difficulty). Fully deterministic."""
1238
  if task_id == "email_triage_easy":
1239
- email = self._deterministic_choice(EMAIL_EASY_VARIANTS, task_id)
1240
- agent_ctx = {"email": {k: v for k, v in email.items() if k != "label"}}
1241
- grader_ctx = {"email": email}
1242
  return agent_ctx, grader_ctx, "easy"
1243
 
1244
  if task_id == "email_triage_medium":
1245
- agent_ctx = {"emails": [{k: v for k, v in e.items() if k not in ("priority", "category")} for e in EMAIL_MEDIUM_EMAILS]}
1246
- grader_ctx = {"emails": EMAIL_MEDIUM_EMAILS}
1247
  return agent_ctx, grader_ctx, "medium"
1248
 
1249
  if task_id == "email_triage_hard":
1250
- case = self._deterministic_choice(EMAIL_HARD_CASES, task_id)
1251
- agent_ctx = {"email": {k: v for k, v in case.items() if k != "expected_elements"}}
1252
- grader_ctx = {"email": case}
1253
  return agent_ctx, grader_ctx, "hard"
1254
 
1255
  if task_id == "code_review_easy":
1256
- variant = self._deterministic_choice(CODE_EASY_VARIANTS, task_id)
1257
- agent_ctx = {"code": variant["code"]}
1258
- grader_ctx = {"code": variant["code"], "keywords": variant["keywords"]}
1259
  return agent_ctx, grader_ctx, "easy"
1260
 
1261
  if task_id == "code_review_medium":
@@ -1267,25 +1280,23 @@ class MetaEnvironment(Environment):
1267
  return ctx, ctx, "hard"
1268
 
1269
  if task_id == "data_cleaning_easy":
1270
- ctx = {"data": DATA_EASY["data"]}
1271
- return ctx, ctx, "easy"
1272
 
1273
  if task_id == "data_cleaning_medium":
1274
- ctx = {"data": DATA_MEDIUM["data"]}
1275
- return ctx, ctx, "medium"
1276
 
1277
  if task_id == "data_cleaning_hard":
1278
- ctx = {"data": DATA_HARD["data"]}
1279
- return ctx, ctx, "hard"
1280
 
1281
  if task_id == "content_moderation_easy":
1282
- agent_ctx = {"posts": [{k: v for k, v in p.items() if k != "label"} for p in MOD_EASY]}
1283
- grader_ctx = {"posts": MOD_EASY}
1284
  return agent_ctx, grader_ctx, "easy"
1285
 
1286
  if task_id == "content_moderation_medium":
1287
- agent_ctx = {"posts": [{k: v for k, v in p.items() if k != "label"} for p in MOD_MEDIUM]}
1288
- grader_ctx = {"posts": MOD_MEDIUM}
1289
  return agent_ctx, grader_ctx, "medium"
1290
 
1291
  if task_id == "content_moderation_hard":
@@ -1294,9 +1305,7 @@ class MetaEnvironment(Environment):
1294
  return agent_ctx, grader_ctx, "hard"
1295
 
1296
  if task_id == "ticket_triage_easy":
1297
- ticket = self._deterministic_choice(TICKET_EASY_VARIANTS, task_id)
1298
- agent_ctx = {"ticket": {k: v for k, v in ticket.items() if k not in ("correct_priority", "correct_category")}}
1299
- grader_ctx = {"ticket": ticket}
1300
  return agent_ctx, grader_ctx, "easy"
1301
 
1302
  if task_id == "ticket_triage_medium":
@@ -1332,6 +1341,33 @@ class MetaEnvironment(Environment):
1332
  grader_ctx = {"case": case}
1333
  return agent_ctx, grader_ctx, "hard"
1334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1335
  raise ValueError(f"Unknown task_id: '{task_id}'. Use GET /tasks to see valid IDs.")
1336
 
1337
  def _grade(self, task_id: str, payload: dict, grader_context: dict) -> tuple[float, str, dict]:
@@ -1343,9 +1379,9 @@ class MetaEnvironment(Environment):
1343
  if task_id == "code_review_hard": return grade_code_hard(payload)
1344
  if task_id == "data_cleaning_easy": return grade_data_easy(payload)
1345
  if task_id == "data_cleaning_medium": return grade_data_medium(payload)
1346
- if task_id == "data_cleaning_hard": return grade_data_hard(payload)
1347
- if task_id == "content_moderation_easy": return grade_mod_easy(payload)
1348
- if task_id == "content_moderation_medium": return grade_mod_medium(payload)
1349
  if task_id == "content_moderation_hard": return grade_mod_hard(payload)
1350
  if task_id == "ticket_triage_easy": return grade_ticket_easy(payload, grader_context)
1351
  if task_id == "ticket_triage_medium": return grade_ticket_medium(payload)
 
36
 
37
  from models import MetaAction, MetaObservation
38
 
39
+ from data_generators import (
40
+ gen_email_easy, gen_email_medium, gen_email_hard,
41
+ gen_code_easy,
42
+ gen_data_easy, gen_data_medium, gen_data_hard,
43
+ gen_mod_easy, gen_mod_medium,
44
+ gen_ticket_easy,
45
+ )
46
+ from expert_tasks import (
47
+ EMAIL_EXPERT_CONTEXT, grade_email_expert,
48
+ CODE_EXPERT_CONTEXT, grade_code_expert,
49
+ DATA_EXPERT_CONTEXT, grade_data_expert,
50
+ MOD_EXPERT_CONTEXT, grade_mod_expert,
51
+ TICKET_EXPERT_CONTEXT, grade_ticket_expert,
52
+ EXPERT_INSTRUCTIONS, EXPERT_TASK_IDS,
53
+ )
54
+
55
+
56
 
57
  # ═════════════════════════════════════════════════════════════════════════════
58
  # DATA
 
619
 
620
  def grade_email_medium(payload: dict, context: dict) -> tuple[float, str, dict]:
621
  order = payload.get("order", [])
622
+ correct = context.get("correct_order", EMAIL_MEDIUM_CORRECT_ORDER)
623
  if not order:
624
  return 0.0, "[FAIL] No order provided.", {}
625
  top2_ok = set(order[:2]) == {"m1", "m5"}
 
809
  return score, feedback, checks
810
 
811
 
812
+ def grade_data_hard(payload: dict, context: dict = None) -> tuple[float, str, dict]:
813
  outliers_given = set(int(x) for x in payload.get("outliers", []))
814
  missing_given = set(int(x) for x in payload.get("missing", []))
815
  cleaned = payload.get("cleaned_data", [])
816
+ answers = (context or {}).get("answers", DATA_HARD["answers"])
817
+ correct_out = answers.get("outliers", DATA_HARD["answers"]["outliers"])
818
+ correct_miss = answers.get("missing", DATA_HARD["answers"]["missing"])
819
+ lo, hi = answers.get("imputed_range", DATA_HARD["answers"]["imputed_range"])
820
  outlier_ok = outliers_given == correct_out
821
  missing_ok = missing_given == correct_miss
822
  imputed_rows = {r["id"]: r.get("value") for r in cleaned if r.get("id") in correct_miss}
 
832
  return score, feedback, checks
833
 
834
 
835
+ def grade_mod_easy(payload: dict, context: dict = None) -> tuple[float, str, dict]:
836
  clf = {c["id"]: c.get("label", "").lower() for c in payload.get("classifications", [])}
837
+ posts = (context or {}).get("posts", MOD_EASY)
838
+ correct = {p["id"]: p["label"] for p in posts}
839
  per = {id_: clf.get(id_) == label for id_, label in correct.items()}
840
  hits = sum(per.values())
841
  score = round(hits / len(correct), 2)
 
843
  return score, f"{tag} {hits}/{len(correct)} posts correctly classified.", per
844
 
845
 
846
+ def grade_mod_medium(payload: dict, context: dict = None) -> tuple[float, str, dict]:
847
  clf = {c["id"]: c.get("label", "").lower() for c in payload.get("classifications", [])}
848
+ posts = (context or {}).get("posts", MOD_MEDIUM)
849
+ correct = {p["id"]: p["label"] for p in posts}
850
  per = {id_: clf.get(id_) == label for id_, label in correct.items()}
851
  hits = sum(per.values())
852
  score = round(hits / len(correct), 2)
 
1256
  def _load_context(self, task_id: str) -> tuple[dict, dict, str]:
1257
  """Return (agent_ctx, grader_ctx, difficulty). Fully deterministic."""
1258
  if task_id == "email_triage_easy":
1259
+ agent_ctx, grader_ctx = gen_email_easy(self._state.episode_id)
 
 
1260
  return agent_ctx, grader_ctx, "easy"
1261
 
1262
  if task_id == "email_triage_medium":
1263
+ agent_ctx, grader_ctx = gen_email_medium(self._state.episode_id)
 
1264
  return agent_ctx, grader_ctx, "medium"
1265
 
1266
  if task_id == "email_triage_hard":
1267
+ agent_ctx, grader_ctx = gen_email_hard(self._state.episode_id)
 
 
1268
  return agent_ctx, grader_ctx, "hard"
1269
 
1270
  if task_id == "code_review_easy":
1271
+ agent_ctx, grader_ctx = gen_code_easy(self._state.episode_id)
 
 
1272
  return agent_ctx, grader_ctx, "easy"
1273
 
1274
  if task_id == "code_review_medium":
 
1280
  return ctx, ctx, "hard"
1281
 
1282
  if task_id == "data_cleaning_easy":
1283
+ agent_ctx, grader_ctx = gen_data_easy(self._state.episode_id)
1284
+ return agent_ctx, grader_ctx, "easy"
1285
 
1286
  if task_id == "data_cleaning_medium":
1287
+ agent_ctx, grader_ctx = gen_data_medium(self._state.episode_id)
1288
+ return agent_ctx, grader_ctx, "medium"
1289
 
1290
  if task_id == "data_cleaning_hard":
1291
+ agent_ctx, grader_ctx = gen_data_hard(self._state.episode_id)
1292
+ return agent_ctx, grader_ctx, "hard"
1293
 
1294
  if task_id == "content_moderation_easy":
1295
+ agent_ctx, grader_ctx = gen_mod_easy(self._state.episode_id)
 
1296
  return agent_ctx, grader_ctx, "easy"
1297
 
1298
  if task_id == "content_moderation_medium":
1299
+ agent_ctx, grader_ctx = gen_mod_medium(self._state.episode_id)
 
1300
  return agent_ctx, grader_ctx, "medium"
1301
 
1302
  if task_id == "content_moderation_hard":
 
1305
  return agent_ctx, grader_ctx, "hard"
1306
 
1307
  if task_id == "ticket_triage_easy":
1308
+ agent_ctx, grader_ctx = gen_ticket_easy(self._state.episode_id)
 
 
1309
  return agent_ctx, grader_ctx, "easy"
1310
 
1311
  if task_id == "ticket_triage_medium":
 
1341
  grader_ctx = {"case": case}
1342
  return agent_ctx, grader_ctx, "hard"
1343
 
1344
+
1345
+ if task_id == "email_triage_expert":
1346
+ agent_ctx = {k: v for k, v in EMAIL_EXPERT_CONTEXT.items()}
1347
+ grader_ctx = agent_ctx
1348
+ return agent_ctx, grader_ctx, "expert"
1349
+
1350
+ if task_id == "code_review_expert":
1351
+ agent_ctx = {"code": CODE_EXPERT_CONTEXT["code"],
1352
+ "vulnerability_count": CODE_EXPERT_CONTEXT["vulnerability_count"]}
1353
+ grader_ctx = CODE_EXPERT_CONTEXT
1354
+ return agent_ctx, grader_ctx, "expert"
1355
+
1356
+ if task_id == "data_cleaning_expert":
1357
+ agent_ctx = DATA_EXPERT_CONTEXT
1358
+ grader_ctx = DATA_EXPERT_CONTEXT
1359
+ return agent_ctx, grader_ctx, "expert"
1360
+
1361
+ if task_id == "content_moderation_expert":
1362
+ agent_ctx = MOD_EXPERT_CONTEXT
1363
+ grader_ctx = MOD_EXPERT_CONTEXT
1364
+ return agent_ctx, grader_ctx, "expert"
1365
+
1366
+ if task_id == "ticket_triage_expert":
1367
+ agent_ctx = TICKET_EXPERT_CONTEXT
1368
+ grader_ctx = TICKET_EXPERT_CONTEXT
1369
+ return agent_ctx, grader_ctx, "expert"
1370
+
1371
  raise ValueError(f"Unknown task_id: '{task_id}'. Use GET /tasks to see valid IDs.")
1372
 
1373
  def _grade(self, task_id: str, payload: dict, grader_context: dict) -> tuple[float, str, dict]:
 
1379
  if task_id == "code_review_hard": return grade_code_hard(payload)
1380
  if task_id == "data_cleaning_easy": return grade_data_easy(payload)
1381
  if task_id == "data_cleaning_medium": return grade_data_medium(payload)
1382
+ if task_id == "data_cleaning_hard": return grade_data_hard(payload, grader_context)
1383
+ if task_id == "content_moderation_easy": return grade_mod_easy(payload, grader_context)
1384
+ if task_id == "content_moderation_medium": return grade_mod_medium(payload, grader_context)
1385
  if task_id == "content_moderation_hard": return grade_mod_hard(payload)
1386
  if task_id == "ticket_triage_easy": return grade_ticket_easy(payload, grader_context)
1387
  if task_id == "ticket_triage_medium": return grade_ticket_medium(payload)
server/Meta_environment.py.bak ADDED
@@ -0,0 +1,1520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Meta Multi-Agent Environment v3.0
3
+
4
+ Five real-world AI agent domains, 16 tasks total:
5
+ 1. Email Triage β€” classify, prioritize, draft replies
6
+ 2. Code Review β€” syntax errors, logic bugs, security vulnerabilities (harder)
7
+ 3. Data Cleaning β€” missing values, type normalization, outlier imputation
8
+ 4. Content Moderation β€” explicit, subtle toxicity, context-aware moderation (harder)
9
+ 5. Ticket Triage β€” NEW: Jira-style bug ticket priority, routing, incident analysis
10
+
11
+ Cross-agent chained tasks (4 total):
12
+ cross_agent_chain β€” email triage + code review
13
+ cross_agent_email_data β€” email priority + data cleaning
14
+ cross_agent_code_email β€” vulnerability detection + security disclosure email
15
+ cross_agent_mod_escalation β€” content moderation + moderation notice drafting
16
+
17
+ v3.0 improvements:
18
+ - Deterministic context loading via episode_id hash (no random.choice breaking reproducibility)
19
+ - 5th domain: Ticket Triage (Jira-style)
20
+ - 4 cross-agent chained tasks (was 1)
21
+ - Harder hard tasks: 8 vulnerabilities in code_hard, adversarial content_mod_hard
22
+ - Trajectory-aware reward shaping with improvement bonuses
23
+ - Clean single-import pattern (PYTHONPATH set correctly in Dockerfile)
24
+ - Full type hints on all grader functions
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import random
31
+ from typing import Any
32
+ from uuid import uuid4
33
+
34
+ from openenv.core.env_server.interfaces import Environment
35
+ from openenv.core.env_server.types import State
36
+
37
+ from models import MetaAction, MetaObservation
38
+
39
+ from data_generators import (
40
+ gen_email_easy, gen_email_medium, gen_email_hard,
41
+ gen_code_easy,
42
+ gen_data_easy, gen_data_medium, gen_data_hard,
43
+ gen_mod_easy, gen_mod_medium,
44
+ gen_ticket_easy,
45
+ )
46
+ from expert_tasks import (
47
+ EMAIL_EXPERT_CONTEXT, grade_email_expert,
48
+ CODE_EXPERT_CONTEXT, grade_code_expert,
49
+ DATA_EXPERT_CONTEXT, grade_data_expert,
50
+ MOD_EXPERT_CONTEXT, grade_mod_expert,
51
+ TICKET_EXPERT_CONTEXT, grade_ticket_expert,
52
+ EXPERT_INSTRUCTIONS, EXPERT_TASK_IDS,
53
+ )
54
+
55
+
56
+
57
+ # ═════════════════════════════════════════════════════════════════════════════
58
+ # DATA
59
+ # ═════════════════════════════════════════════════════════════════════════════
60
+
61
+ EMAIL_EASY_VARIANTS = [
62
+ {
63
+ "id": "e1",
64
+ "subject": "You won $1,000,000!!!",
65
+ "sender": "promo@totally-legit-lottery.com",
66
+ "body": "Click here to claim your prize now! Limited time offer!",
67
+ "label": "spam",
68
+ },
69
+ {
70
+ "id": "e2",
71
+ "subject": "Q3 Budget Review β€” Urgent Action Required",
72
+ "sender": "cfo@company.com",
73
+ "body": "Please review the attached Q3 budget before our board meeting tomorrow at 9 AM.",
74
+ "label": "important",
75
+ },
76
+ {
77
+ "id": "e3",
78
+ "subject": "Your weekly digest from Medium",
79
+ "sender": "noreply@medium.com",
80
+ "body": "Top stories this week: AI trends, Python tips, and startup news.",
81
+ "label": "newsletter",
82
+ },
83
+ {
84
+ "id": "e4",
85
+ "subject": "CONGRATULATIONS! iPhone 15 Pro is yours!",
86
+ "sender": "winner@prize-alert.net",
87
+ "body": "You have been selected. Click now to claim your free iPhone!",
88
+ "label": "spam",
89
+ },
90
+ {
91
+ "id": "e5",
92
+ "subject": "Action Required: Sign the NDA before Friday",
93
+ "sender": "legal@company.com",
94
+ "body": "Please sign the attached NDA before Friday's partnership meeting. Deadline is strict.",
95
+ "label": "important",
96
+ },
97
+ {
98
+ "id": "e6",
99
+ "subject": "TechCrunch Newsletter β€” March 2026",
100
+ "sender": "newsletter@techcrunch.com",
101
+ "body": "This week's top stories in tech, AI, and startups.",
102
+ "label": "newsletter",
103
+ },
104
+ ]
105
+
106
+ EMAIL_MEDIUM_EMAILS = [
107
+ {"id": "m1", "subject": "[ALERT] Production server DOWN", "sender": "ops@company.com", "body": "All services unresponsive since 2 AM. Revenue impact: $10k/min.", "priority": 1, "category": "critical"},
108
+ {"id": "m2", "subject": "Team lunch this Friday?", "sender": "colleague@company.com","body": "Hey, thinking of doing a team lunch Friday. You in?", "priority": 9, "category": "social"},
109
+ {"id": "m3", "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"},
110
+ {"id": "m4", "subject": "Weekly SaaS digest", "sender": "news@saasdigest.com", "body": "Top SaaS stories this week...", "priority": 10, "category": "newsletter"},
111
+ {"id": "m5", "subject": "[WARNING] Security alert: unusual login", "sender": "security@company.com","body": "Unauthorized login attempt from IP 45.33.32.156. Verify immediately.", "priority": 1, "category": "critical"},
112
+ {"id": "m6", "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"},
113
+ {"id": "m7", "subject": "Happy Work Anniversary!", "sender": "hr@company.com", "body": "Celebrating your 3 years with us!", "priority": 10, "category": "social"},
114
+ {"id": "m8", "subject": "Q4 product launch deck β€” review needed", "sender": "marketing@company.com","body": "Please review launch slides before Thursday. Launch is next Monday.", "priority": 3, "category": "business"},
115
+ {"id": "m9", "subject": "Free AI productivity webinar", "sender": "promo@webinar.io", "body": "Join our free webinar on AI tools this Thursday.", "priority": 8, "category": "newsletter"},
116
+ {"id": "m10", "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"},
117
+ ]
118
+
119
+ EMAIL_MEDIUM_CORRECT_ORDER = ["m1", "m5", "m3", "m10", "m6", "m8", "m2", "m9", "m4", "m7"]
120
+
121
+ EMAIL_HARD_CASES = [
122
+ {
123
+ "id": "h1",
124
+ "subject": "Completely unacceptable β€” Order #78234",
125
+ "sender": "furious.customer@email.com",
126
+ "body": (
127
+ "I have been a loyal customer for 5 years and I am absolutely furious. "
128
+ "My order #78234 arrived broken for the SECOND time this month. "
129
+ "Your support team has been dismissive and unhelpful every time I called. "
130
+ "I demand a full refund, a replacement sent overnight, AND compensation. "
131
+ "If this is not resolved within 24 hours, I will file a consumer court complaint."
132
+ ),
133
+ "expected_elements": {
134
+ "acknowledge_frustration": ["understand", "frustrat", "disappoint", "concern", "feel"],
135
+ "apologize": ["sorry", "apologize", "apology", "sincerely"],
136
+ "offer_refund": ["refund", "money back", "full refund"],
137
+ "offer_replacement": ["replacement", "replace", "new order", "send another", "overnight"],
138
+ "provide_timeline": ["24 hours", "48 hours", "within", "today", "tomorrow", "business day"],
139
+ "professional_tone": None,
140
+ "compensation_acknowledged": ["compensat", "inconvenience", "make it right", "goodwill"],
141
+ },
142
+ },
143
+ {
144
+ "id": "h2",
145
+ "subject": "Billing error β€” charged twice for subscription",
146
+ "sender": "billing.complaint@customer.com",
147
+ "body": (
148
+ "I was charged twice for my annual subscription this month β€” once on March 1st "
149
+ "and again on March 15th. I have the bank statements as proof. "
150
+ "This is a clear billing error on your end. I need an immediate refund of the "
151
+ "duplicate charge and a written confirmation that this won't happen again. "
152
+ "I've been waiting 2 weeks for a response from your support team."
153
+ ),
154
+ "expected_elements": {
155
+ "acknowledge_frustration": ["understand", "concern", "apologize", "sorry"],
156
+ "apologize": ["sorry", "apologize", "apology"],
157
+ "offer_refund": ["refund", "reimburse", "credit"],
158
+ "confirm_investigation": ["investigat", "look into", "check", "review", "verify"],
159
+ "provide_timeline": ["within", "hours", "days", "business day"],
160
+ "professional_tone": None,
161
+ "written_confirmation": ["confirm", "written", "email confirmation", "receipt"],
162
+ },
163
+ },
164
+ ]
165
+
166
+ CODE_EASY_VARIANTS = [
167
+ {
168
+ "code": "def calculate_average(numbers):\n total = 0\n for num in numbers\n total += num\n return total / len(numbers)\n",
169
+ "hint": "missing colon after for statement",
170
+ "keywords": ["colon", "for loop", "syntax", "for statement"],
171
+ },
172
+ {
173
+ "code": "def greet(name)\n print(f'Hello, {name}!')\n\ngreet('World')\n",
174
+ "hint": "missing colon after function definition",
175
+ "keywords": ["colon", "def", "function", "syntax"],
176
+ },
177
+ {
178
+ "code": "numbers = [1, 2, 3, 4, 5]\nif len(numbers) > 0\n print('Not empty')\n",
179
+ "hint": "missing colon after if statement",
180
+ "keywords": ["colon", "if statement", "syntax"],
181
+ },
182
+ ]
183
+
184
+ CODE_MEDIUM = {
185
+ "code": (
186
+ "def find_max(numbers):\n"
187
+ " max_val = 0\n"
188
+ " for num in numbers:\n"
189
+ " if num > max_val:\n"
190
+ " max_val = num\n"
191
+ " return max_val\n\n"
192
+ "def get_user(users, user_id):\n"
193
+ " for user in users:\n"
194
+ " if user['id'] = user_id:\n"
195
+ " return user\n"
196
+ " return None\n\n"
197
+ "def divide(a, b):\n"
198
+ " return a / b\n\n"
199
+ "def process_list(items):\n"
200
+ " results = []\n"
201
+ " for i in range(len(items) + 1):\n"
202
+ " results.append(items[i] * 2)\n"
203
+ " return results\n"
204
+ ),
205
+ "bugs": {
206
+ "max_val_init": "find_max initializes max_val=0, fails for all-negative lists",
207
+ "assignment_vs_eq": "get_user uses = (assignment) instead of == (comparison)",
208
+ "zero_division": "divide() has no guard against division by zero",
209
+ "off_by_one": "process_list iterates range(len(items)+1) causing IndexError",
210
+ },
211
+ }
212
+
213
+ # HARDER: 8 vulnerabilities now (was 5) β€” frontier models won't trivially score 1.0
214
+ CODE_HARD = {
215
+ "code": (
216
+ "import sqlite3\n"
217
+ "import subprocess\n"
218
+ "import pickle\n"
219
+ "import requests\n"
220
+ "import hashlib\n"
221
+ "import os\n\n"
222
+ "# Vulnerability 1: SQL injection\n"
223
+ "def get_user_by_name(name):\n"
224
+ " conn = sqlite3.connect('users.db')\n"
225
+ " cursor = conn.cursor()\n"
226
+ " query = \"SELECT * FROM users WHERE name = '\" + name + \"'\"\n"
227
+ " cursor.execute(query)\n"
228
+ " return cursor.fetchall()\n\n"
229
+ "# Vulnerability 2: XSS\n"
230
+ "def render_comment(comment):\n"
231
+ " return '<div class=\"comment\">' + comment + '</div>'\n\n"
232
+ "# Vulnerability 3: SQL injection\n"
233
+ "def login(username, password):\n"
234
+ " conn = sqlite3.connect('users.db')\n"
235
+ " cursor = conn.cursor()\n"
236
+ " query = f\"SELECT * FROM users WHERE username='{username}' AND password='{password}'\"\n"
237
+ " cursor.execute(query)\n"
238
+ " return cursor.fetchone() is not None\n\n"
239
+ "# Vulnerability 4: Command injection\n"
240
+ "def run_report(report_name):\n"
241
+ " subprocess.call('generate_report.sh ' + report_name, shell=True)\n\n"
242
+ "# Vulnerability 5: Insecure deserialization\n"
243
+ "def load_user_data(data_bytes):\n"
244
+ " return pickle.loads(data_bytes)\n\n"
245
+ "# Vulnerability 6: Timing attack on password comparison\n"
246
+ "def check_password(stored_hash, input_password):\n"
247
+ " return stored_hash == hashlib.md5(input_password.encode()).hexdigest()\n\n"
248
+ "# Vulnerability 7: Path traversal\n"
249
+ "def read_report_file(filename):\n"
250
+ " with open(f'/reports/{filename}') as f:\n"
251
+ " return f.read()\n\n"
252
+ "# Vulnerability 8: SSRF\n"
253
+ "def fetch_user_avatar(avatar_url):\n"
254
+ " response = requests.get(avatar_url)\n"
255
+ " return response.content\n"
256
+ ),
257
+ "vulnerabilities": {
258
+ "sql_injection_get_user": {"type": "sql_injection", "location": "get_user_by_name", "severity": "critical"},
259
+ "xss_render_comment": {"type": "xss", "location": "render_comment", "severity": "high"},
260
+ "sql_injection_login": {"type": "sql_injection", "location": "login", "severity": "critical"},
261
+ "command_injection": {"type": "command_injection", "location": "run_report", "severity": "critical"},
262
+ "insecure_deserialization": {"type": "insecure_deserialization","location": "load_user_data", "severity": "high"},
263
+ "timing_attack": {"type": "timing_attack", "location": "check_password", "severity": "medium"},
264
+ "path_traversal": {"type": "path_traversal", "location": "read_report_file", "severity": "high"},
265
+ "ssrf": {"type": "ssrf", "location": "fetch_user_avatar", "severity": "high"},
266
+ },
267
+ }
268
+
269
+ DATA_EASY = {
270
+ "data": [
271
+ {"id": 1, "name": "Alice", "age": 30, "email": "alice@example.com", "salary": 70000},
272
+ {"id": 2, "name": "Bob", "age": None, "email": "bob@example.com", "salary": 85000},
273
+ {"id": 3, "name": "Alice", "age": 30, "email": "alice@example.com", "salary": 70000},
274
+ {"id": 4, "name": None, "age": 25, "email": "charlie@example.com", "salary": 60000},
275
+ {"id": 5, "name": "Dave", "age": 40, "email": None, "salary": 90000},
276
+ {"id": 6, "name": "Eve", "age": 28, "email": "eve@example.com", "salary": None},
277
+ ],
278
+ "answers": {
279
+ "missing": ["age (row 2)", "name (row 4)", "email (row 5)", "salary (row 6)"],
280
+ "duplicates": [1, 3],
281
+ },
282
+ }
283
+
284
+ DATA_MEDIUM = {
285
+ "data": [
286
+ {"id": 1, "name": "Alice", "age": "thirty", "salary": "50000", "join_date": "2020/01/15", "active": "yes"},
287
+ {"id": 2, "name": "Bob", "age": 25, "salary": "$60,000", "join_date": "15-03-2021", "active": True},
288
+ {"id": 3, "name": "Charlie", "age": "28", "salary": "70000", "join_date": "2019-07-22", "active": "TRUE"},
289
+ {"id": 4, "name": "dave", "age": 35, "salary": "80,000", "join_date": "2022/11/01", "active": False},
290
+ {"id": 5, "name": "EVE", "age": "twenty-two", "salary": "45000", "join_date": "01/06/2023", "active": "no"},
291
+ ],
292
+ "issues": {
293
+ "age": "mixed types: strings 'thirty', '28', 'twenty-two' should be integers",
294
+ "salary": "inconsistent formats: '$60,000' and '80,000' have symbols/commas",
295
+ "join_date": "three different date formats",
296
+ "name": "inconsistent casing: 'dave' lowercase, 'EVE' all caps",
297
+ "active": "mixed types: True/False booleans mixed with strings 'yes'/'no'/'TRUE'",
298
+ },
299
+ }
300
+
301
+ DATA_HARD = {
302
+ "data": [
303
+ {"id": 1, "value": 10.5}, {"id": 2, "value": 11.2},
304
+ {"id": 3, "value": 10.8}, {"id": 4, "value": 999.0},
305
+ {"id": 5, "value": 10.1}, {"id": 6, "value": None},
306
+ {"id": 7, "value": 11.5}, {"id": 8, "value": -500.0},
307
+ {"id": 9, "value": 10.9}, {"id": 10, "value": 11.0},
308
+ {"id": 11, "value": None}, {"id": 12, "value": 10.7},
309
+ ],
310
+ "answers": {
311
+ "outliers": {4, 8},
312
+ "missing": {6, 11},
313
+ "imputed_range": (10.0, 12.0),
314
+ },
315
+ }
316
+
317
+ MOD_EASY = [
318
+ {"id": "c1", "text": "I hate you so much, I want to hurt you!", "label": "harmful"},
319
+ {"id": "c2", "text": "Great weather today! Loving this sunshine", "label": "safe"},
320
+ {"id": "c3", "text": "All people from that group should be eliminated.", "label": "harmful"},
321
+ {"id": "c4", "text": "Can someone recommend a good Python beginner book?", "label": "safe"},
322
+ {"id": "c5", "text": "You're an absolute idiot and nobody likes you.", "label": "harmful"},
323
+ {"id": "c6", "text": "I just finished a 5k run β€” feeling amazing!", "label": "safe"},
324
+ {"id": "c7", "text": "People like you should just disappear forever.", "label": "harmful"},
325
+ ]
326
+
327
+ MOD_MEDIUM = [
328
+ {"id": "m1", "text": "Oh sure, like THAT went well. Classic genius move.", "label": "harmful"},
329
+ {"id": "m2", "text": "I'm so done with these people. Absolutely insufferable.", "label": "harmful"},
330
+ {"id": "m3", "text": "That presentation was... very unique.", "label": "safe"},
331
+ {"id": "m4", "text": "Some people just shouldn't be allowed to have opinions.", "label": "harmful"},
332
+ {"id": "m5", "text": "Oh wow, another 'brilliant' idea from upper management", "label": "harmful"},
333
+ {"id": "m6", "text": "Congrats on the promotion! Well deserved!", "label": "safe"},
334
+ {"id": "m7", "text": "Not everyone is cut out for this kind of work, clearly.", "label": "harmful"},
335
+ {"id": "m8", "text": "Thanks for the feedback, I'll take it into account.", "label": "safe"},
336
+ ]
337
+
338
+ # HARDER: adversarial cases including dog-whistles and coordinated inauthentic patterns
339
+ MOD_HARD = [
340
+ {
341
+ "id": "h1",
342
+ "text": "You should just disappear.",
343
+ "context_a": {"platform": "gaming_chat", "prior_message": "Your character died in the dungeon. Respawn?"},
344
+ "label_a": "safe",
345
+ "context_b": {"platform": "crisis_support_chat","prior_message": "I've been feeling worthless and invisible lately"},
346
+ "label_b": "harmful",
347
+ },
348
+ {
349
+ "id": "h2",
350
+ "text": "We need to take these people out.",
351
+ "context_a": {"platform": "hr_slack", "prior_message": "Which team members should join the client dinner?"},
352
+ "label_a": "safe",
353
+ "context_b": {"platform": "extremist_forum", "prior_message": "These immigrants are ruining our country."},
354
+ "label_b": "harmful",
355
+ },
356
+ {
357
+ "id": "h3",
358
+ "text": "Let's eliminate the competition.",
359
+ "context_a": {"platform": "business_slack", "prior_message": "Q4 sales strategy meeting"},
360
+ "label_a": "safe",
361
+ "context_b": {"platform": "political_rally_chat","prior_message": "The opposition party is destroying our values."},
362
+ "label_b": "harmful",
363
+ },
364
+ # NEW adversarial cases β€” much harder
365
+ {
366
+ "id": "h4",
367
+ "text": "Finally, the great replacement is happening in our neighborhood.",
368
+ "context_a": {"platform": "news_comments", "prior_message": "New families moving into Maplewood as housing market cools."},
369
+ "label_a": "harmful", # dog-whistle regardless of context
370
+ "context_b": {"platform": "urban_planning_forum","prior_message": "Demographics shifting as millennials move to suburbs."},
371
+ "label_b": "harmful", # still a white-nationalist dog-whistle
372
+ },
373
+ {
374
+ "id": "h5",
375
+ "text": "Everyone should follow our example and do the same thing at noon tomorrow.",
376
+ "context_a": {"platform": "community_events", "prior_message": "We're all doing a neighborhood cleanup. Join us!"},
377
+ "label_a": "safe",
378
+ "context_b": {"platform": "extremist_channel", "prior_message": "The government building is unguarded on weekends."},
379
+ "label_b": "harmful", # coordinated inauthentic / incitement
380
+ },
381
+ {
382
+ "id": "h6",
383
+ "text": "These people breed like rabbits and will replace us if we don't act.",
384
+ "context_a": {"platform": "biology_classroom", "prior_message": "Comparing reproductive rates of different animal species"},
385
+ "label_a": "harmful", # dehumanizing regardless β€” not biology
386
+ "context_b": {"platform": "political_forum", "prior_message": "Immigration policy discussion thread"},
387
+ "label_b": "harmful",
388
+ },
389
+ ]
390
+
391
+ # ── NEW: Ticket Triage Domain ─────────────────────────────────────────────────
392
+
393
+ TICKET_EASY_VARIANTS = [
394
+ {
395
+ "id": "t1",
396
+ "title": "App crashes on login for all users",
397
+ "description": "Since the 2.3.1 deploy, 100% of login attempts throw a 500 error. Users cannot access the platform.",
398
+ "reporter": "ops-monitoring@company.com",
399
+ "correct_priority": "critical",
400
+ "correct_category": "bug",
401
+ },
402
+ {
403
+ "id": "t2",
404
+ "title": "Add dark mode to the dashboard",
405
+ "description": "Multiple users have requested a dark mode option. Would improve accessibility.",
406
+ "reporter": "product@company.com",
407
+ "correct_priority": "low",
408
+ "correct_category": "feature_request",
409
+ },
410
+ {
411
+ "id": "t3",
412
+ "title": "How do I export my data as CSV?",
413
+ "description": "I can't find the export button anywhere in the UI. Where is it?",
414
+ "reporter": "user123@customer.com",
415
+ "correct_priority": "low",
416
+ "correct_category": "question",
417
+ },
418
+ {
419
+ "id": "t4",
420
+ "title": "Charged twice for last month's invoice",
421
+ "description": "Account #AC-4421 shows two charges of $299 on March 1st. Please refund duplicate.",
422
+ "reporter": "finance@customer.com",
423
+ "correct_priority": "high",
424
+ "correct_category": "billing",
425
+ },
426
+ ]
427
+
428
+ TICKET_MEDIUM_TICKETS = [
429
+ {"id": "tk1", "title": "Database connection pool exhausted β€” prod down", "type": "bug", "team": "backend", "priority_rank": 1},
430
+ {"id": "tk2", "title": "Wrong currency symbol shown for EU customers", "type": "bug", "team": "frontend", "priority_rank": 3},
431
+ {"id": "tk3", "title": "Add OAuth2 support for enterprise SSO", "type": "feature_request","team": "backend", "priority_rank": 5},
432
+ {"id": "tk4", "title": "SSL cert expires in 48 hours on api.company.com", "type": "bug", "team": "devops", "priority_rank": 2},
433
+ {"id": "tk5", "title": "Invoice PDF generation fails for invoices > 50 lines","type": "bug", "team": "backend", "priority_rank": 4},
434
+ {"id": "tk6", "title": "Redesign onboarding flow to reduce drop-off", "type": "feature_request","team": "frontend", "priority_rank": 7},
435
+ {"id": "tk7", "title": "Customer billed $0 instead of $299 β€” revenue leak","type": "bug", "team": "billing", "priority_rank": 3},
436
+ {"id": "tk8", "title": "Add CSV export for analytics dashboard", "type": "feature_request","team": "frontend", "priority_rank": 8},
437
+ ]
438
+
439
+ TICKET_MEDIUM_CORRECT_ORDER = ["tk1", "tk4", "tk2", "tk7", "tk5", "tk3", "tk6", "tk8"]
440
+ TICKET_MEDIUM_CORRECT_TEAMS = {
441
+ "tk1": "backend", "tk2": "frontend", "tk3": "backend",
442
+ "tk4": "devops", "tk5": "backend", "tk6": "frontend",
443
+ "tk7": "billing", "tk8": "frontend",
444
+ }
445
+
446
+ TICKET_HARD_INCIDENT = {
447
+ "tickets": [
448
+ {"id": "INC-001", "title": "Payment service returning 503", "created": "14:00", "service": "payment-service"},
449
+ {"id": "INC-002", "title": "Database CPU at 100% on primary replica","created": "14:01", "service": "postgres-primary"},
450
+ {"id": "INC-003", "title": "Checkout flow failing for all users", "created": "14:02", "service": "checkout-service"},
451
+ {"id": "INC-004", "title": "Redis memory at 99% β€” evictions spiking","created": "14:03", "service": "redis-cache"},
452
+ {"id": "INC-005", "title": "Background job queue backed up 50k jobs","created": "14:05", "service": "job-queue"},
453
+ {"id": "INC-006", "title": "CDN edge nodes reporting origin timeouts","created": "14:06", "service": "cdn"},
454
+ ],
455
+ "expected": {
456
+ "root_cause_keywords": [
457
+ "database", "postgres", "cpu", "memory", "redis", "cache",
458
+ "connection pool", "exhausted", "query", "slow query", "replica",
459
+ ],
460
+ "affected_services": {"payment-service", "checkout-service", "postgres-primary", "redis-cache"},
461
+ "severity": "P1",
462
+ "resolution_keywords": [
463
+ "scale", "replica", "failover", "restart", "flush", "cache",
464
+ "query", "optimize", "rollback", "connection", "limit",
465
+ ],
466
+ },
467
+ }
468
+
469
+ # ── Cross-agent data ─────────────────────���────────────────────────────────────
470
+
471
+ CROSS_AGENT_CHAIN_CASES = [
472
+ {
473
+ "id": "ca1",
474
+ "email": {
475
+ "subject": "Bug report: payment processor crashing",
476
+ "sender": "dev@company.com",
477
+ "body": "Our payment processor keeps throwing a ZeroDivisionError in production. See attached code snippet.",
478
+ },
479
+ "email_label": "important",
480
+ "code": (
481
+ "def calculate_fee(amount, rate):\n"
482
+ " return amount / rate\n\n"
483
+ "def process_payment(amount, fee_rate):\n"
484
+ " fee = calculate_fee(amount, fee_rate)\n"
485
+ " return amount - fee\n"
486
+ ),
487
+ "code_bug": "zero_division",
488
+ "code_bug_keywords": ["zero", "division", "divide", "rate", "denominator", "fee_rate", "guard"],
489
+ "code_location": "calculate_fee",
490
+ },
491
+ {
492
+ "id": "ca2",
493
+ "email": {
494
+ "subject": "Weekly newsletter: Python tips",
495
+ "sender": "newsletter@pythonweekly.com",
496
+ "body": "This week: list comprehensions, async patterns, and a buggy code snippet for you to fix!",
497
+ },
498
+ "email_label": "newsletter",
499
+ "code": (
500
+ "def get_first(items):\n"
501
+ " return items[0]\n\n"
502
+ "def summarize(data):\n"
503
+ " total = sum(data)\n"
504
+ " first = get_first(data)\n"
505
+ " return total, first\n"
506
+ ),
507
+ "code_bug": "empty_list",
508
+ "code_bug_keywords": ["empty", "index", "indexerror", "list", "bound", "check", "length", "len", "guard"],
509
+ "code_location": "get_first",
510
+ },
511
+ ]
512
+
513
+ CROSS_AGENT_EMAIL_DATA_CASES = [
514
+ {
515
+ "id": "ced1",
516
+ "email": {
517
+ "subject": "[URGENT] Sales data export corrupted β€” board meeting tomorrow",
518
+ "sender": "vp-sales@company.com",
519
+ "body": "The Q4 sales export is corrupted. I need it fixed before the 9 AM board meeting tomorrow.",
520
+ },
521
+ "email_priority": "critical",
522
+ "attachment_data": [
523
+ {"id": 1, "rep": "Alice", "revenue": "150000", "region": "NORTH", "closed_date": "2024/12/31"},
524
+ {"id": 2, "rep": "bob", "revenue": "$80,000", "region": "south", "closed_date": "31-12-2024"},
525
+ {"id": 3, "rep": "Charlie","revenue": None, "region": "East", "closed_date": "2024-12-30"},
526
+ {"id": 4, "rep": "Diana", "revenue": "120000", "region": "WEST", "closed_date": "12/30/2024"},
527
+ ],
528
+ "expected_issues": {"revenue": True, "region": True, "closed_date": True},
529
+ },
530
+ ]
531
+
532
+ CROSS_AGENT_CODE_EMAIL_CASES = [
533
+ {
534
+ "id": "cce1",
535
+ "code": (
536
+ "def get_user_profile(user_id):\n"
537
+ " conn = sqlite3.connect('app.db')\n"
538
+ " query = \"SELECT * FROM users WHERE id = \" + user_id\n"
539
+ " cursor = conn.execute(query)\n"
540
+ " return cursor.fetchone()\n"
541
+ ),
542
+ "primary_vulnerability": "sql_injection",
543
+ "location": "get_user_profile",
544
+ "disclosure_keywords": [
545
+ "sql injection", "parameterized", "vulnerability",
546
+ "security", "patch", "remediat", "fix", "urgent",
547
+ ],
548
+ },
549
+ ]
550
+
551
+ CROSS_AGENT_MOD_ESCALATION_CASES = [
552
+ {
553
+ "id": "cme1",
554
+ "post": {
555
+ "id": "p1",
556
+ "text": "I know where you live and I will make you regret this post.",
557
+ "platform": "social_media",
558
+ "context": "Reply to a political opinion post",
559
+ },
560
+ "correct_label": "harmful",
561
+ "should_escalate": True,
562
+ "notice_keywords": [
563
+ "violat", "policy", "removed", "community", "guidelines",
564
+ "threat", "harm", "prohibit",
565
+ ],
566
+ },
567
+ {
568
+ "id": "cme2",
569
+ "post": {
570
+ "id": "p2",
571
+ "text": "Great recipe! Can't wait to try this at home.",
572
+ "platform": "cooking_forum",
573
+ "context": "Reply to a pasta recipe post",
574
+ },
575
+ "correct_label": "safe",
576
+ "should_escalate": False,
577
+ "notice_keywords": [],
578
+ },
579
+ ]
580
+
581
+
582
+ # ═════════════════════════════════════════════════════════════════════════════
583
+ # GRADERS
584
+ # ═════════════════════════════════════════════════════════════════════════════
585
+
586
+ def grade_email_easy(payload: dict, context: dict) -> tuple[float, str, dict]:
587
+ correct = context["email"]["label"]
588
+ given = payload.get("classification", "").strip().lower()
589
+ valid = ["spam", "important", "newsletter"]
590
+ if given == correct:
591
+ return 1.0, f"[OK] Correct! '{correct}' is right.", {}
592
+ if given in valid:
593
+ return 0.0, f"[FAIL] Wrong. Expected '{correct}', got '{given}'.", {"classification": False}
594
+ return 0.0, f"[FAIL] Invalid label '{given}'. Must be: spam | important | newsletter.", {"classification": False}
595
+
596
+
597
+ def _kendall_tau_score(order: list, correct: list) -> float:
598
+ """
599
+ Kendall's tau-based ordering score.
600
+ Counts concordant pairs (relative order matches) vs total pairs.
601
+ Much fairer than exact position matching β€” rewards getting the overall
602
+ priority ranking right even when a few adjacent items swap.
603
+ Score range: 0.0 (fully reversed) to 1.0 (perfect order).
604
+ """
605
+ n = len(correct)
606
+ pos = {e: i for i, e in enumerate(correct)}
607
+ concordant = discordant = 0
608
+ for i in range(len(order)):
609
+ for j in range(i + 1, len(order)):
610
+ if order[i] not in pos or order[j] not in pos:
611
+ continue
612
+ if pos[order[i]] < pos[order[j]]:
613
+ concordant += 1
614
+ else:
615
+ discordant += 1
616
+ total = concordant + discordant
617
+ return concordant / total if total > 0 else 0.0
618
+
619
+
620
+ def grade_email_medium(payload: dict, context: dict) -> tuple[float, str, dict]:
621
+ order = payload.get("order", [])
622
+ correct = context.get("correct_order", EMAIL_MEDIUM_CORRECT_ORDER)
623
+ if not order:
624
+ return 0.0, "[FAIL] No order provided.", {}
625
+ top2_ok = set(order[:2]) == {"m1", "m5"}
626
+ top5_ok = order[:5] == correct[:5]
627
+ full_ok = order == correct
628
+ tau = _kendall_tau_score(order, correct)
629
+ hits = sum(1 for i, e in enumerate(order) if i < len(correct) and e == correct[i])
630
+ partial = {"top2_critical": top2_ok, "top5_correct": top5_ok, "full_order": full_ok}
631
+ if full_ok:
632
+ return 1.0, "[OK] Perfect prioritization!", partial
633
+ if top5_ok:
634
+ score = round(0.8 + tau * 0.2, 2)
635
+ return score, f"[PARTIAL] Top 5 correct. Kendall tau: {tau:.2f}.", partial
636
+ if top2_ok:
637
+ # Tau-based score: 0.5 minimum when criticals placed first, scaled up by pair agreement
638
+ score = round(max(0.5, tau), 2)
639
+ return score, f"[PARTIAL] Critical emails (m1, m5) first. {hits}/10 exact positions. Tau: {tau:.2f}.", partial
640
+ score = round(tau * 0.8, 2)
641
+ return score, f"[ERROR] {hits}/10 exact positions correct. Kendall tau: {tau:.2f}.", partial
642
+
643
+
644
+ def grade_email_hard(payload: dict, context: dict) -> tuple[float, str, dict]:
645
+ reply = payload.get("reply", "").lower()
646
+ case = context["email"]
647
+ expected = case.get("expected_elements", {})
648
+ checks: dict[str, bool] = {}
649
+ for key, keywords in expected.items():
650
+ if key == "professional_tone":
651
+ checks[key] = (
652
+ len(reply) > 80
653
+ and not any(w in reply for w in ["not my problem", "whatever", "too bad", "deal with it"])
654
+ )
655
+ else:
656
+ checks[key] = keywords is not None and any(w in reply for w in keywords)
657
+ passed = sum(checks.values())
658
+ total = len(checks)
659
+ score = round(passed / total, 2)
660
+ missing = [k for k, v in checks.items() if not v]
661
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.6 else "[ERROR]"
662
+ feedback = f"{tag} {passed}/{total} reply elements present."
663
+ if missing:
664
+ feedback += f" Missing: {', '.join(missing)}."
665
+ return score, feedback, checks
666
+
667
+
668
+ def grade_code_easy(payload: dict, context: dict) -> tuple[float, str, dict]:
669
+ errors = " ".join(str(e) for e in payload.get("errors", [])).lower()
670
+ keywords = context.get("keywords", ["colon", "syntax"])
671
+ found = any(k in errors for k in keywords)
672
+ if not payload.get("errors"):
673
+ return 0.0, "[FAIL] No errors reported.", {"syntax_error_found": False}
674
+ if found:
675
+ return 1.0, "[OK] Syntax error correctly identified.", {"syntax_error_found": True}
676
+ return 0.3, "[PARTIAL] Error mentioned but key issue not clearly described.", {"syntax_error_found": False}
677
+
678
+
679
+ def grade_code_medium(payload: dict) -> tuple[float, str, dict]:
680
+ bugs = payload.get("bugs", [])
681
+ checks = {"max_val_init": False, "assignment_vs_eq": False, "zero_division": False, "off_by_one": False}
682
+ for b in bugs:
683
+ t = (str(b.get("issue", "")) + str(b.get("location", "")) + str(b.get("fix", ""))).lower()
684
+ loc = str(b.get("location", "")).lower()
685
+ if loc == "find_max" or any(w in t for w in ["max_val", "negative", "minus", "zero init", "float('-inf')", "all-negative", "all negative", "initialization", "find_max"]):
686
+ checks["max_val_init"] = True
687
+ if loc == "get_user" or any(w in t for w in ["assignment", "==", "comparison", "= user_id", "single equal", "get_user"]):
688
+ checks["assignment_vs_eq"] = True
689
+ if loc == "divide" or any(w in t for w in ["zero", "division", "divide", "zerodivision", "b == 0", "divisor", "denominator", "div by zero"]):
690
+ checks["zero_division"] = True
691
+ if loc == "process_list" or any(w in t for w in ["off by one", "off-by-one", "index", "range", "len + 1", "indexerror", "out of bounds", "boundary", "len(items) + 1", "process_list"]):
692
+ checks["off_by_one"] = True
693
+ passed = sum(checks.values())
694
+ score = round(passed / 4, 2)
695
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
696
+ feedback = f"{tag} {passed}/4 bugs found."
697
+ missing = [k for k, v in checks.items() if not v]
698
+ if missing:
699
+ feedback += f" Missed: {', '.join(missing)}."
700
+ return score, feedback, checks
701
+
702
+
703
+ def grade_code_hard(payload: dict) -> tuple[float, str, dict]:
704
+ """Now grading 8 vulnerabilities β€” significantly harder than before."""
705
+ vulns = payload.get("vulnerabilities", [])
706
+ checks = {
707
+ "sql_injection_get_user": False,
708
+ "xss_render_comment": False,
709
+ "sql_injection_login": False,
710
+ "command_injection": False,
711
+ "insecure_deserialization": False,
712
+ "timing_attack": False,
713
+ "path_traversal": False,
714
+ "ssrf": False,
715
+ }
716
+ for v in vulns:
717
+ vt = str(v.get("type", "")).lower()
718
+ loc = str(v.get("location", "")).lower()
719
+ if "get_user" in loc or "get_user_by_name" in loc:
720
+ checks["sql_injection_get_user"] = True
721
+ if ("render" in loc or "comment" in loc) and any(w in vt for w in ["xss", "cross", "script", "html", "inject", "sanitiz", "encod", "output"]):
722
+ checks["xss_render_comment"] = True
723
+ if "render" in loc and "comment" in loc:
724
+ checks["xss_render_comment"] = True
725
+ if "login" in loc:
726
+ checks["sql_injection_login"] = True
727
+ if "report" in loc and any(w in vt for w in ["command", "injection", "shell"]):
728
+ checks["command_injection"] = True
729
+ if "report" in loc and "run" in loc:
730
+ checks["command_injection"] = True
731
+ if "load" in loc and any(w in vt for w in ["pickle", "deserializ", "serial"]):
732
+ checks["insecure_deserialization"] = True
733
+ if "load_user" in loc:
734
+ checks["insecure_deserialization"] = True
735
+ if "check_password" in loc or any(w in vt for w in ["timing", "time", "constant"]):
736
+ checks["timing_attack"] = True
737
+ if "read_report" in loc or "path" in vt or "traversal" in vt or "directory" in vt:
738
+ checks["path_traversal"] = True
739
+ if "fetch" in loc or "avatar" in loc or "ssrf" in vt or "request_forgery" in vt:
740
+ checks["ssrf"] = True
741
+ passed = sum(checks.values())
742
+ score = round(passed / 8, 2)
743
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
744
+ feedback = f"{tag} {passed}/8 vulnerabilities found."
745
+ missing = [k for k, v in checks.items() if not v]
746
+ if missing:
747
+ feedback += f" Missed: {', '.join(missing)}."
748
+ return score, feedback, checks
749
+
750
+
751
+ def grade_data_easy(payload: dict) -> tuple[float, str, dict]:
752
+ missing_given = [str(m).lower() for m in payload.get("missing", [])]
753
+ dups_given = set(str(d) for d in payload.get("duplicates", []))
754
+ mc = {
755
+ "age_row2": any("age" in m and "2" in m for m in missing_given),
756
+ "name_row4": any("name" in m and "4" in m for m in missing_given),
757
+ "email_row5": any("email" in m and "5" in m for m in missing_given),
758
+ "salary_row6": any("salary" in m and "6" in m for m in missing_given),
759
+ }
760
+ dup_ok = dups_given in [{"1", "3"}, {"3", "1"}] or dups_given == {1, 3}
761
+ passed = sum(mc.values())
762
+ score = round((passed / 4 * 0.7) + (0.3 if dup_ok else 0.0), 2)
763
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
764
+ feedback = f"{tag} Missing: {passed}/4, Duplicates: {'[OK]' if dup_ok else '[FAIL]'}."
765
+ return score, feedback, {**mc, "duplicates_correct": dup_ok}
766
+
767
+
768
+ def grade_data_medium(payload: dict) -> tuple[float, str, dict]:
769
+ issues = {k.lower(): str(v).lower() for k, v in payload.get("issues", {}).items()}
770
+ cleaned = payload.get("cleaned_data", [])
771
+ checks = {
772
+ "age_type": (
773
+ "age" in issues and any(w in issues["age"] for w in [
774
+ "string", "integer", "int", "thirty", "type", "numeric",
775
+ "non-numeric", "word", "text", "convert", "mixed", "number",
776
+ "twenty", "str", "should be int", "not int", "invalid",
777
+ ])
778
+ ),
779
+ "salary_format": (
780
+ "salary" in issues and any(w in issues["salary"] for w in [
781
+ "format", "comma", "$", "inconsistent", "symbol", "currency",
782
+ "dollar", "sign", "strip", "remove", "clean",
783
+ ])
784
+ ),
785
+ "date_format": (
786
+ "join_date" in issues and any(w in issues["join_date"] for w in [
787
+ "format", "inconsistent", "date", "yyyy", "dd-mm",
788
+ "standardize", "iso", "different", "multiple",
789
+ ])
790
+ ),
791
+ "name_case": (
792
+ "name" in issues and any(w in issues["name"] for w in [
793
+ "capital", "case", "dave", "eve", "lower", "upper",
794
+ "casing", "title", "inconsistent", "normalize",
795
+ ])
796
+ ),
797
+ "active_type": (
798
+ "active" in issues and any(w in issues["active"] for w in [
799
+ "bool", "string", "yes", "true", "type", "mixed",
800
+ "boolean", "convert", "inconsistent", "str",
801
+ ])
802
+ ),
803
+ "data_cleaned": len(cleaned) == 5,
804
+ }
805
+ passed = sum(checks.values())
806
+ score = round(passed / 6, 2)
807
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
808
+ feedback = f"{tag} {passed}/6 data quality checks passed."
809
+ return score, feedback, checks
810
+
811
+
812
+ def grade_data_hard(payload: dict, context: dict = None) -> tuple[float, str, dict]:
813
+ outliers_given = set(int(x) for x in payload.get("outliers", []))
814
+ missing_given = set(int(x) for x in payload.get("missing", []))
815
+ cleaned = payload.get("cleaned_data", [])
816
+ answers = (context or {}).get("answers", DATA_HARD["answers"])
817
+ correct_out = answers.get("outliers", DATA_HARD["answers"]["outliers"])
818
+ correct_miss = answers.get("missing", DATA_HARD["answers"]["missing"])
819
+ lo, hi = answers.get("imputed_range", DATA_HARD["answers"]["imputed_range"])
820
+ outlier_ok = outliers_given == correct_out
821
+ missing_ok = missing_given == correct_miss
822
+ imputed_rows = {r["id"]: r.get("value") for r in cleaned if r.get("id") in correct_miss}
823
+ imputed_ok = (
824
+ all(v is not None and lo <= float(v) <= hi for v in imputed_rows.values())
825
+ and len(imputed_rows) == len(correct_miss)
826
+ )
827
+ checks = {"outliers_correct": outlier_ok, "missing_correct": missing_ok, "imputation_valid": imputed_ok}
828
+ passed = sum(checks.values())
829
+ score = round(passed / 3, 2)
830
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.67 else "[ERROR]"
831
+ feedback = f"{tag} {passed}/3 data operations correct."
832
+ return score, feedback, checks
833
+
834
+
835
+ def grade_mod_easy(payload: dict, context: dict = None) -> tuple[float, str, dict]:
836
+ clf = {c["id"]: c.get("label", "").lower() for c in payload.get("classifications", [])}
837
+ posts = (context or {}).get("posts", MOD_EASY)
838
+ correct = {p["id"]: p["label"] for p in posts}
839
+ per = {id_: clf.get(id_) == label for id_, label in correct.items()}
840
+ hits = sum(per.values())
841
+ score = round(hits / len(correct), 2)
842
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.7 else "[ERROR]"
843
+ return score, f"{tag} {hits}/{len(correct)} posts correctly classified.", per
844
+
845
+
846
+ def grade_mod_medium(payload: dict, context: dict = None) -> tuple[float, str, dict]:
847
+ clf = {c["id"]: c.get("label", "").lower() for c in payload.get("classifications", [])}
848
+ posts = (context or {}).get("posts", MOD_MEDIUM)
849
+ correct = {p["id"]: p["label"] for p in posts}
850
+ per = {id_: clf.get(id_) == label for id_, label in correct.items()}
851
+ hits = sum(per.values())
852
+ score = round(hits / len(correct), 2)
853
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.6 else "[ERROR]"
854
+ return score, f"{tag} {hits}/{len(correct)} subtle posts correctly classified.", per
855
+
856
+
857
+ def grade_mod_hard(payload: dict) -> tuple[float, str, dict]:
858
+ """Now 6 cases (12 checks) β€” adversarial dog-whistles included."""
859
+ decisions = {d["id"]: d for d in payload.get("decisions", [])}
860
+ checks: dict[str, bool] = {}
861
+ for case in MOD_HARD:
862
+ cid = case["id"]
863
+ d = decisions.get(cid, {})
864
+ checks[f"{cid}_context_a"] = d.get("context_a_label", "").lower() == case["label_a"]
865
+ checks[f"{cid}_context_b"] = d.get("context_b_label", "").lower() == case["label_b"]
866
+ passed = sum(checks.values())
867
+ total = len(checks)
868
+ score = round(passed / total, 2)
869
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
870
+ return score, f"{tag} {passed}/{total} context-aware decisions correct.", checks
871
+
872
+
873
+ def grade_ticket_easy(payload: dict, context: dict) -> tuple[float, str, dict]:
874
+ ticket = context["ticket"]
875
+ p_given = payload.get("priority", "").strip().lower()
876
+ c_given = payload.get("category", "").strip().lower()
877
+ p_ok = p_given == ticket["correct_priority"]
878
+ c_ok = c_given == ticket["correct_category"]
879
+ checks = {"priority_correct": p_ok, "category_correct": c_ok}
880
+ score = round(sum(checks.values()) / 2, 2)
881
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score == 0.5 else "[ERROR]"
882
+ p_str = "βœ“" if p_ok else f"βœ— (expected {ticket['correct_priority']})"
883
+ c_str = "βœ“" if c_ok else f"βœ— (expected {ticket['correct_category']})"
884
+ feedback = f"{tag} Priority: {p_str}, Category: {c_str}"
885
+ return score, feedback, checks
886
+
887
+
888
+ def grade_ticket_medium(payload: dict) -> tuple[float, str, dict]:
889
+ order = payload.get("order", [])
890
+ assigns = payload.get("assignments", {})
891
+ correct_order = TICKET_MEDIUM_CORRECT_ORDER
892
+ correct_teams = TICKET_MEDIUM_CORRECT_TEAMS
893
+ # Order grading via Kendall tau (60% weight)
894
+ tau = _kendall_tau_score(order, correct_order)
895
+ top3_ok = order[:3] == correct_order[:3]
896
+ full_ok = order == correct_order
897
+ hits = sum(1 for i, e in enumerate(order) if i < len(correct_order) and e == correct_order[i])
898
+ order_score = 1.0 if full_ok else round(tau, 2)
899
+ # Team assignment grading (40% weight) β€” per-ticket breakdown
900
+ team_checks = {tid: assigns.get(tid) == team for tid, team in correct_teams.items()}
901
+ team_hits = sum(team_checks.values())
902
+ team_score = round(team_hits / len(correct_teams), 2)
903
+ wrong_teams = [f"{tid}(got={assigns.get(tid)},expected={correct_teams[tid]})"
904
+ for tid, ok in team_checks.items() if not ok]
905
+ score = round(order_score * 0.6 + team_score * 0.4, 2)
906
+ checks = {"top3_correct": top3_ok, "full_order": full_ok,
907
+ "team_assignments": team_score == 1.0, **{f"team_{tid}": ok for tid, ok in team_checks.items()}}
908
+ tag = "[OK]" if score >= 0.95 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
909
+ feedback = f"{tag} Order: {hits}/8 exact, Tau: {tau:.2f}, Teams: {team_hits}/8 correct."
910
+ if wrong_teams:
911
+ feedback += f" Wrong teams: {', '.join(wrong_teams)}."
912
+ return score, feedback, checks
913
+
914
+
915
+ def grade_ticket_hard(payload: dict) -> tuple[float, str, dict]:
916
+ root_cause = payload.get("root_cause", "").lower()
917
+ resolution = [step.lower() for step in payload.get("resolution_steps", [])]
918
+ affected = {s.lower() for s in payload.get("affected_services", [])}
919
+ severity = payload.get("severity", "").upper()
920
+ expected = TICKET_HARD_INCIDENT["expected"]
921
+ # Root cause check
922
+ rc_hits = sum(1 for kw in expected["root_cause_keywords"] if kw in root_cause)
923
+ rc_ok = rc_hits >= 3
924
+ # Affected services
925
+ expected_svcs = {s.lower() for s in expected["affected_services"]}
926
+ svc_hits = len(affected & expected_svcs)
927
+ svc_ok = svc_hits >= 3
928
+ # Severity
929
+ sev_ok = severity == expected["severity"]
930
+ # Resolution steps
931
+ res_text = " ".join(resolution)
932
+ res_hits = sum(1 for kw in expected["resolution_keywords"] if kw in res_text)
933
+ res_ok = res_hits >= 3 and len(resolution) >= 3
934
+ checks = {"root_cause_ok": rc_ok, "affected_services_ok": svc_ok, "severity_ok": sev_ok, "resolution_ok": res_ok}
935
+ passed = sum(checks.values())
936
+ score = round(passed / 4, 2)
937
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.5 else "[ERROR]"
938
+ feedback = f"{tag} {passed}/4 incident analysis checks passed. RC keywords: {rc_hits}, Svcs: {svc_hits}/4, Res steps: {len(resolution)}."
939
+ return score, feedback, checks
940
+
941
+
942
+ def grade_cross_agent_chain(payload: dict, context: dict) -> tuple[float, str, dict]:
943
+ case = context["case"]
944
+ email_given = payload.get("email_classification", "").strip().lower()
945
+ email_ok = email_given == case["email_label"]
946
+ bugs = payload.get("bugs", [])
947
+ bug_ok = False
948
+ for b in bugs:
949
+ t = (str(b.get("issue", "")) + str(b.get("location", "")) + str(b.get("fix", ""))).lower()
950
+ loc = str(b.get("location", "")).lower()
951
+ if loc == case["code_location"].lower() or any(w in t for w in case["code_bug_keywords"]):
952
+ bug_ok = True
953
+ break
954
+ checks = {"email_correct": email_ok, "bug_found": bug_ok}
955
+ passed = sum(checks.values())
956
+ score = round(passed / 2, 2)
957
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score == 0.5 else "[ERROR]"
958
+ feedback = f"{tag} {passed}/2 cross-agent checks passed."
959
+ if not email_ok:
960
+ feedback += f" Email: expected '{case['email_label']}', got '{email_given}'."
961
+ if not bug_ok:
962
+ feedback += f" Code bug in '{case['code_location']}' not identified."
963
+ return score, feedback, checks
964
+
965
+
966
+ def grade_cross_agent_email_data(payload: dict, context: dict) -> tuple[float, str, dict]:
967
+ case = context["case"]
968
+ priority = payload.get("email_priority", "").strip().lower()
969
+ raw_issues = payload.get("data_issues", {})
970
+ cleaned = payload.get("cleaned_data", [])
971
+ priority_ok = priority == case["email_priority"].lower()
972
+ expected_issues = case["expected_issues"]
973
+
974
+ # Handle two formats models use:
975
+ # Format A (correct): {"revenue": "inconsistent...", "region": "casing..."}
976
+ # Format B (wrong): {"field": "revenue", "issue": "inconsistent..."}
977
+ if isinstance(raw_issues, dict):
978
+ # Check if it's format B β€” has "field" key instead of field names as keys
979
+ if "field" in raw_issues and "issue" in raw_issues:
980
+ # Convert: extract the field name from the "field" value
981
+ field_val = str(raw_issues.get("field", "")).lower()
982
+ issue_val = str(raw_issues.get("issue", "")).lower()
983
+ data_issues = {field_val: issue_val}
984
+ else:
985
+ data_issues = {k.lower(): str(v).lower() for k, v in raw_issues.items()}
986
+ elif isinstance(raw_issues, list):
987
+ # Format C: [{"field": "revenue", "issue": "..."}]
988
+ data_issues = {}
989
+ for item in raw_issues:
990
+ if isinstance(item, dict):
991
+ f = str(item.get("field", "")).lower()
992
+ v = str(item.get("issue", item.get("description", ""))).lower()
993
+ if f:
994
+ data_issues[f] = v
995
+ else:
996
+ data_issues = {}
997
+
998
+ issue_hits = sum(1 for field in expected_issues if field in data_issues)
999
+ issues_ok = issue_hits >= 2
1000
+ cleaned_ok = len(cleaned) == len(case["attachment_data"])
1001
+ checks = {"priority_correct": priority_ok, "issues_identified": issues_ok, "data_cleaned": cleaned_ok}
1002
+ passed = sum(checks.values())
1003
+ score = round(passed / 3, 2)
1004
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.67 else "[ERROR]"
1005
+ feedback = f"{tag} {passed}/3 cross-agent email+data checks passed."
1006
+ if not issues_ok:
1007
+ feedback += f" Issues found: {issue_hits}/{len(expected_issues)} fields identified."
1008
+ return score, feedback, checks
1009
+
1010
+
1011
+ def grade_cross_agent_code_email(payload: dict, context: dict) -> tuple[float, str, dict]:
1012
+ case = context["case"]
1013
+ vuln_type = payload.get("vulnerability_type", "").lower()
1014
+ location = payload.get("vulnerability_location", "").lower()
1015
+ email = payload.get("disclosure_email", "").lower()
1016
+ type_ok = case["primary_vulnerability"] in vuln_type or "sql" in vuln_type
1017
+ loc_ok = case["location"].lower() in location
1018
+ email_hits = sum(1 for kw in case["disclosure_keywords"] if kw in email)
1019
+ email_ok = email_hits >= 4 and len(email) >= 80
1020
+ checks = {"vulnerability_identified": type_ok, "location_correct": loc_ok, "disclosure_email_quality": email_ok}
1021
+ passed = sum(checks.values())
1022
+ score = round(passed / 3, 2)
1023
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.67 else "[ERROR]"
1024
+ feedback = f"{tag} {passed}/3 code+email checks passed. Email keywords: {email_hits}."
1025
+ return score, feedback, checks
1026
+
1027
+
1028
+ def grade_cross_agent_mod_escalation(payload: dict, context: dict) -> tuple[float, str, dict]:
1029
+ case = context["case"]
1030
+ label_given = payload.get("content_label", "").strip().lower()
1031
+ escalate = payload.get("escalate", None)
1032
+ notice = payload.get("moderation_notice", "").lower()
1033
+ label_ok = label_given == case["correct_label"]
1034
+ escalate_ok = escalate == case["should_escalate"]
1035
+ if case["should_escalate"]:
1036
+ notice_hits = sum(1 for kw in case["notice_keywords"] if kw in notice)
1037
+ notice_ok = notice_hits >= 3
1038
+ else:
1039
+ notice_ok = len(notice) == 0 or notice.strip() == ""
1040
+ checks = {"label_correct": label_ok, "escalation_correct": escalate_ok, "notice_quality": notice_ok}
1041
+ passed = sum(checks.values())
1042
+ score = round(passed / 3, 2)
1043
+ tag = "[OK]" if score == 1.0 else "[PARTIAL]" if score >= 0.67 else "[ERROR]"
1044
+ feedback = f"{tag} {passed}/3 moderation+escalation checks passed."
1045
+ return score, feedback, checks
1046
+
1047
+
1048
+ # ═════════════════════════════════════════════════════════════════════════════
1049
+ # INSTRUCTIONS
1050
+ # ═════════════════════════════════════════════════════════════════════════════
1051
+
1052
+ INSTRUCTIONS: dict[str, str] = {
1053
+ "email_triage_easy": (
1054
+ "Classify the email as exactly one of: 'spam', 'important', or 'newsletter'.\n"
1055
+ 'Payload: {"classification": "spam"}'
1056
+ ),
1057
+ "email_triage_medium": (
1058
+ "Prioritize the 10 emails from most urgent (first) to least urgent (last).\n"
1059
+ "Priority tiers β€” assign each email to a tier first, then order within tiers by deadline/impact:\n"
1060
+ " Tier 1 (CRITICAL): Active production outages, active security breaches β€” immediate revenue/safety impact\n"
1061
+ " Tier 2 (URGENT): Legal agreements or contracts expiring TODAY β€” deal falls through if unsigned\n"
1062
+ " Tier 3 (HIGH): Overdue financial obligations threatening service suspension\n"
1063
+ " Tier 4 (HIGH): Business deliverables with hard deadlines this week\n"
1064
+ " Tier 5 (LOW): Social β€” lunch invites, work anniversaries\n"
1065
+ " Tier 6 (IGNORE): Passive newsletters and promotional content\n"
1066
+ "Within Tier 2: 'TODAY' deadlines rank above 'Friday' deadlines.\n"
1067
+ "Within Tier 4: launch review decks rank above other reviews by proximity to launch date.\n"
1068
+ "Return ALL 10 email IDs ordered most-to-least urgent.\n"
1069
+ 'Payload: {"order": ["m1", "m5", ...]}'
1070
+ ),
1071
+ "email_triage_hard": (
1072
+ "Draft a professional, empathetic reply to the customer complaint. Address ALL concerns.\n"
1073
+ 'Payload: {"reply": "<full reply text>"}'
1074
+ ),
1075
+ "code_review_easy": (
1076
+ "Identify the syntax error(s) in the Python code.\n"
1077
+ 'Payload: {"errors": ["description of syntax error"]}'
1078
+ ),
1079
+ "code_review_medium": (
1080
+ "Find ALL 4 logical bugs β€” one per function. Report location, issue, and fix.\n"
1081
+ " 1. find_max β€” wrong initialization (fails all-negative lists)\n"
1082
+ " 2. get_user β€” = used instead of ==\n"
1083
+ " 3. divide β€” no zero-division guard\n"
1084
+ " 4. process_list β€” off-by-one in range\n"
1085
+ 'Payload: {"bugs": [{"location": "fn_name", "issue": "...", "fix": "..."}]}'
1086
+ ),
1087
+ "code_review_hard": (
1088
+ "Identify ALL 8 security vulnerabilities:\n"
1089
+ " 1. sql_injection in get_user_by_name\n"
1090
+ " 2. xss in render_comment\n"
1091
+ " 3. sql_injection in login\n"
1092
+ " 4. command_injection in run_report\n"
1093
+ " 5. insecure_deserialization in load_user_data\n"
1094
+ " 6. timing_attack in check_password\n"
1095
+ " 7. path_traversal in read_report_file\n"
1096
+ " 8. ssrf in fetch_user_avatar\n"
1097
+ 'Payload: {"vulnerabilities": [{"type": "...", "location": "...", "severity": "...", "fix": "..."}]}'
1098
+ ),
1099
+ "data_cleaning_easy": (
1100
+ "Find all missing (null) values by field+row, and identify duplicate row IDs.\n"
1101
+ 'Payload: {"missing": ["age (row 2)", "name (row 4)"], "duplicates": [1, 3]}'
1102
+ ),
1103
+ "data_cleaning_medium": (
1104
+ "Identify all 5 data quality issues and return the fully cleaned dataset (5 rows).\n"
1105
+ 'Payload: {"issues": {"age": "mixed types..."}, "cleaned_data": [...]}'
1106
+ ),
1107
+ "data_cleaning_hard": (
1108
+ "Detect outliers (IQR/z-score), find missing values, impute, return cleaned dataset.\n"
1109
+ 'Payload: {"outliers": [4, 8], "missing": [6, 11], "cleaned_data": [...]}'
1110
+ ),
1111
+ "content_moderation_easy": (
1112
+ "Classify each post as 'safe' or 'harmful'.\n"
1113
+ 'Payload: {"classifications": [{"id": "c1", "label": "harmful"}]}'
1114
+ ),
1115
+ "content_moderation_medium": (
1116
+ "Classify each post as 'safe' or 'harmful'. Watch for sarcasm, subtle toxicity, implicit hostility.\n"
1117
+ "IMPORTANT DISTINCTION β€” not all unusual phrasing is harmful:\n"
1118
+ " HARMFUL: sarcasm that mocks or demeans a person ('Classic genius move', 'brilliant idea')\n"
1119
+ " HARMFUL: passive aggression that excludes or belittles ('not cut out for this', 'shouldn't have opinions')\n"
1120
+ " SAFE: mild, ambiguous comments that express a personal reaction without attacking anyone\n"
1121
+ " SAFE: backhanded compliments about THINGS (not people) β€” 'very unique' about a presentation is\n"
1122
+ " just a mild opinion, not an attack on a person. No target = not harmful.\n"
1123
+ " SAFE: genuine positivity ('Congrats!', 'Thanks for the feedback')\n"
1124
+ 'Payload: {"classifications": [{"id": "m1", "label": "harmful", "reason": "..."}]}'
1125
+ ),
1126
+ "content_moderation_hard": (
1127
+ "Each case shows the SAME text in TWO different contexts. Determine the correct label for EACH context.\n"
1128
+ "CRITICAL RULES:\n"
1129
+ " 1. Some texts are ALWAYS harmful regardless of context β€” they use dehumanizing language or\n"
1130
+ " dog-whistle rhetoric that is harmful no matter what platform or prior message surrounds them.\n"
1131
+ " TEST: Would a reasonable content moderator flag this text if they saw it with NO context?\n"
1132
+ " If yes, label it 'harmful' in BOTH contexts.\n"
1133
+ " 2. KEY SIGNAL β€” comparing humans to animals ('breed like rabbits'), invoking 'replacement' of\n"
1134
+ " ethnic groups, or using language that strips human dignity is ALWAYS harmful. A 'biology\n"
1135
+ " classroom' or 'urban planning' framing does NOT make dehumanizing rhetoric acceptable.\n"
1136
+ " 3. Other texts ARE context-dependent β€” 'disappear' in gaming vs crisis support, 'take out' in\n"
1137
+ " HR dinner vs extremist forum, 'eliminate the competition' in business vs political violence.\n"
1138
+ " For these, label each context based on the actual meaning in that specific context.\n"
1139
+ " 4. When in doubt about a text: ask 'is this language that treats a group of people as less than\n"
1140
+ " human?' If yes β†’ harmful in all contexts. If it's just ambiguous phrasing β†’ context matters.\n"
1141
+ 'Payload: {"decisions": [{"id": "h1", "context_a_label": "safe", "context_b_label": "harmful"}]}'
1142
+ ),
1143
+ "ticket_triage_easy": (
1144
+ "Classify the support ticket: assign a priority (critical/high/medium/low) and category (bug/feature_request/question/billing).\n"
1145
+ 'Payload: {"priority": "critical", "category": "bug"}'
1146
+ ),
1147
+ "ticket_triage_medium": (
1148
+ "Order 8 tickets by priority (most urgent first) and assign each to the correct team.\n"
1149
+ "Priority reasoning:\n"
1150
+ " 1. Production DB down and SSL expiring in 48h are both critical infrastructure\n"
1151
+ " 2. Currency display bug (affects all EU users visibly) ranks above billing revenue leak\n"
1152
+ " 3. Revenue-leak billing bug ranks above invoice PDF generation bug\n"
1153
+ " 4. Feature requests rank below all bugs\n"
1154
+ "Team routing β€” be precise about these common mistakes:\n"
1155
+ " backend = server-side APIs, DB queries, background job processing\n"
1156
+ " frontend = anything the user SEES in the browser: currency symbols, UI, display bugs, CSV export UI\n"
1157
+ " devops = SSL certificates, deployments, infrastructure\n"
1158
+ " billing = payment charges, subscription billing, invoice amounts, refunds\n"
1159
+ " 'Wrong currency symbol shown' β†’ frontend (it's a display/rendering bug)\n"
1160
+ " 'Customer billed $0 instead of $299' β†’ billing (it's a charge calculation bug)\n"
1161
+ " 'Invoice PDF generation fails' β†’ backend (server-side PDF generation)\n"
1162
+ " 'Add OAuth2 SSO support' β†’ backend (auth server logic)\n"
1163
+ " 'Redesign onboarding flow' β†’ frontend (UI/UX work)\n"
1164
+ " 'Add CSV export for analytics' β†’ frontend (UI feature)\n"
1165
+ 'Payload: {"order": ["tk1", "tk4", "tk2", "tk7", "tk5", "tk3", "tk6", "tk8"], '
1166
+ '"assignments": {"tk1": "backend", "tk2": "frontend", "tk3": "backend", '
1167
+ '"tk4": "devops", "tk5": "backend", "tk6": "frontend", "tk7": "billing", "tk8": "frontend"}}'
1168
+ ),
1169
+ "ticket_triage_hard": (
1170
+ "Analyse the linked incident tickets and produce: root cause analysis, ordered resolution steps, affected services, and severity (P1-P4).\n"
1171
+ 'Payload: {"root_cause": "...", "resolution_steps": ["...", "..."], "affected_services": ["...", "..."], "severity": "P1"}'
1172
+ ),
1173
+ "cross_agent_chain": (
1174
+ "TWO skills needed:\n"
1175
+ " 1. Classify the email: 'spam', 'important', or 'newsletter'\n"
1176
+ " 2. Find the main bug in the attached code (location + issue + fix)\n"
1177
+ 'Payload: {"email_classification": "important", "bugs": [{"location": "fn", "issue": "...", "fix": "..."}]}'
1178
+ ),
1179
+ "cross_agent_email_data": (
1180
+ "TWO skills needed:\n"
1181
+ " 1. Classify the email urgency: 'critical', 'high', 'medium', 'low'\n"
1182
+ " 2. Identify data quality issues in the attachment and return cleaned data\n"
1183
+ "IMPORTANT: data_issues must use field names as keys, NOT 'field'/'issue' as keys.\n"
1184
+ " CORRECT: {\"revenue\": \"inconsistent formats\", \"region\": \"inconsistent casing\", \"closed_date\": \"multiple date formats\"}\n"
1185
+ " INCORRECT: {\"field\": \"revenue\", \"issue\": \"inconsistent formats\"}\n"
1186
+ 'Payload: {"email_priority": "critical", "data_issues": {"revenue": "inconsistent formats", "region": "casing issues", "closed_date": "multiple formats"}, "cleaned_data": [...]}'
1187
+ ),
1188
+ "cross_agent_code_email": (
1189
+ "TWO skills needed:\n"
1190
+ " 1. Identify the primary security vulnerability type and its location\n"
1191
+ " 2. Draft a professional security disclosure email to the dev team (min 80 chars)\n"
1192
+ 'Payload: {"vulnerability_type": "sql_injection", "vulnerability_location": "fn_name", "disclosure_email": "..."}'
1193
+ ),
1194
+ "cross_agent_mod_escalation": (
1195
+ "TWO skills needed:\n"
1196
+ " 1. Classify the content as 'safe' or 'harmful'\n"
1197
+ " 2. Decide whether to escalate (true/false) and draft a moderation notice if harmful (empty string if safe)\n"
1198
+ 'Payload: {"content_label": "harmful", "escalate": true, "moderation_notice": "Your post was removed because..."}'
1199
+ ),
1200
+ }
1201
+
1202
+
1203
+ # ═════════════════════════════════════════════════════════════════════════════
1204
+ # ENVIRONMENT
1205
+ # ═════════════════════════════════════════════════════════════════════════════
1206
+
1207
+ class MetaEnvironment(Environment):
1208
+ """
1209
+ Meta Multi-Agent Environment v3.0
1210
+ 5 domains Β· 16 tasks Β· 4 cross-agent chained tasks Β· deterministic context loading
1211
+ """
1212
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
1213
+
1214
+ def __init__(self) -> None:
1215
+ self._state = State(episode_id=str(uuid4()), step_count=0)
1216
+ self._current_task_id: str | None = None
1217
+ self._current_agent_context: dict = {}
1218
+ self._current_grader_context: dict = {}
1219
+ self._episode_scores: list[float] = []
1220
+ self._attempt_counts: dict[str, int] = {}
1221
+ self._first_attempt_scores: dict[str, float] = {}
1222
+
1223
+ def reset(self) -> MetaObservation:
1224
+ self._state = State(episode_id=str(uuid4()), step_count=0)
1225
+ self._current_task_id = None
1226
+ self._current_agent_context = {}
1227
+ self._current_grader_context = {}
1228
+ self._episode_scores = []
1229
+ self._attempt_counts = {}
1230
+ self._first_attempt_scores = {}
1231
+ return MetaObservation(
1232
+ agent="meta",
1233
+ task_id="",
1234
+ difficulty="",
1235
+ context={},
1236
+ instructions=(
1237
+ "Welcome to Meta Multi-Agent Environment v3.0!\n"
1238
+ "5 agents Γ— 3 tasks + 4 cross-agent tasks = 19 tasks total.\n"
1239
+ "Agents: email_triage | code_review | data_cleaning | "
1240
+ "content_moderation | ticket_triage | cross_agent\n"
1241
+ "Use GET /tasks to see all task IDs and exact payload schemas.\n"
1242
+ "Multi-step episodes: imperfect first attempts earn a retry."
1243
+ ),
1244
+ feedback="Environment reset. Ready for episode.",
1245
+ score=0.0,
1246
+ partial_credits={},
1247
+ done=False,
1248
+ reward=0.0,
1249
+ )
1250
+
1251
+ def _deterministic_choice(self, variants: list, task_id: str) -> Any:
1252
+ """Pick a variant deterministically based on episode_id hash."""
1253
+ idx = hash(self._state.episode_id + task_id) % len(variants)
1254
+ return variants[idx]
1255
+
1256
+ def _load_context(self, task_id: str) -> tuple[dict, dict, str]:
1257
+ """Return (agent_ctx, grader_ctx, difficulty). Fully deterministic."""
1258
+ if task_id == "email_triage_easy":
1259
+ agent_ctx, grader_ctx = gen_email_easy(self._state.episode_id)
1260
+ return agent_ctx, grader_ctx, "easy"
1261
+
1262
+ if task_id == "email_triage_medium":
1263
+ agent_ctx, grader_ctx = gen_email_medium(self._state.episode_id)
1264
+ return agent_ctx, grader_ctx, "medium"
1265
+
1266
+ if task_id == "email_triage_hard":
1267
+ agent_ctx, grader_ctx = gen_email_hard(self._state.episode_id)
1268
+ return agent_ctx, grader_ctx, "hard"
1269
+
1270
+ if task_id == "code_review_easy":
1271
+ agent_ctx, grader_ctx = gen_code_easy(self._state.episode_id)
1272
+ return agent_ctx, grader_ctx, "easy"
1273
+
1274
+ if task_id == "code_review_medium":
1275
+ ctx = {"code": CODE_MEDIUM["code"]}
1276
+ return ctx, ctx, "medium"
1277
+
1278
+ if task_id == "code_review_hard":
1279
+ ctx = {"code": CODE_HARD["code"]}
1280
+ return ctx, ctx, "hard"
1281
+
1282
+ if task_id == "data_cleaning_easy":
1283
+ agent_ctx, grader_ctx = gen_data_easy(self._state.episode_id)
1284
+ return agent_ctx, grader_ctx, "easy"
1285
+
1286
+ if task_id == "data_cleaning_medium":
1287
+ agent_ctx, grader_ctx = gen_data_medium(self._state.episode_id)
1288
+ return agent_ctx, grader_ctx, "medium"
1289
+
1290
+ if task_id == "data_cleaning_hard":
1291
+ agent_ctx, grader_ctx = gen_data_hard(self._state.episode_id)
1292
+ return agent_ctx, grader_ctx, "hard"
1293
+
1294
+ if task_id == "content_moderation_easy":
1295
+ agent_ctx, grader_ctx = gen_mod_easy(self._state.episode_id)
1296
+ return agent_ctx, grader_ctx, "easy"
1297
+
1298
+ if task_id == "content_moderation_medium":
1299
+ agent_ctx, grader_ctx = gen_mod_medium(self._state.episode_id)
1300
+ return agent_ctx, grader_ctx, "medium"
1301
+
1302
+ if task_id == "content_moderation_hard":
1303
+ agent_ctx = {"cases": [{k: v for k, v in c.items() if k not in ("label_a", "label_b", "reason_a", "reason_b")} for c in MOD_HARD]}
1304
+ grader_ctx = {"cases": MOD_HARD}
1305
+ return agent_ctx, grader_ctx, "hard"
1306
+
1307
+ if task_id == "ticket_triage_easy":
1308
+ agent_ctx, grader_ctx = gen_ticket_easy(self._state.episode_id)
1309
+ return agent_ctx, grader_ctx, "easy"
1310
+
1311
+ if task_id == "ticket_triage_medium":
1312
+ agent_ctx = {"tickets": [{k: v for k, v in t.items() if k not in ("priority_rank", "team")} for t in TICKET_MEDIUM_TICKETS]}
1313
+ grader_ctx = {"tickets": TICKET_MEDIUM_TICKETS}
1314
+ return agent_ctx, grader_ctx, "medium"
1315
+
1316
+ if task_id == "ticket_triage_hard":
1317
+ ctx = {"incident_tickets": TICKET_HARD_INCIDENT["tickets"]}
1318
+ return ctx, ctx, "hard"
1319
+
1320
+ if task_id == "cross_agent_chain":
1321
+ case = self._deterministic_choice(CROSS_AGENT_CHAIN_CASES, task_id)
1322
+ agent_ctx = {"email": case["email"], "code": case["code"]}
1323
+ grader_ctx = {"case": case}
1324
+ return agent_ctx, grader_ctx, "hard"
1325
+
1326
+ if task_id == "cross_agent_email_data":
1327
+ case = CROSS_AGENT_EMAIL_DATA_CASES[0]
1328
+ agent_ctx = {"email": case["email"], "attachment_data": case["attachment_data"]}
1329
+ grader_ctx = {"case": case}
1330
+ return agent_ctx, grader_ctx, "hard"
1331
+
1332
+ if task_id == "cross_agent_code_email":
1333
+ case = CROSS_AGENT_CODE_EMAIL_CASES[0]
1334
+ agent_ctx = {"code": case["code"]}
1335
+ grader_ctx = {"case": case}
1336
+ return agent_ctx, grader_ctx, "hard"
1337
+
1338
+ if task_id == "cross_agent_mod_escalation":
1339
+ case = self._deterministic_choice(CROSS_AGENT_MOD_ESCALATION_CASES, task_id)
1340
+ agent_ctx = {"post": case["post"]}
1341
+ grader_ctx = {"case": case}
1342
+ return agent_ctx, grader_ctx, "hard"
1343
+
1344
+
1345
+ if task_id == "email_triage_expert":
1346
+ agent_ctx = {k: v for k, v in EMAIL_EXPERT_CONTEXT.items()}
1347
+ grader_ctx = agent_ctx
1348
+ return agent_ctx, grader_ctx, "expert"
1349
+
1350
+ if task_id == "code_review_expert":
1351
+ agent_ctx = {"code": CODE_EXPERT_CONTEXT["code"],
1352
+ "vulnerability_count": CODE_EXPERT_CONTEXT["vulnerability_count"]}
1353
+ grader_ctx = CODE_EXPERT_CONTEXT
1354
+ return agent_ctx, grader_ctx, "expert"
1355
+
1356
+ if task_id == "data_cleaning_expert":
1357
+ agent_ctx = DATA_EXPERT_CONTEXT
1358
+ grader_ctx = DATA_EXPERT_CONTEXT
1359
+ return agent_ctx, grader_ctx, "expert"
1360
+
1361
+ if task_id == "content_moderation_expert":
1362
+ agent_ctx = MOD_EXPERT_CONTEXT
1363
+ grader_ctx = MOD_EXPERT_CONTEXT
1364
+ return agent_ctx, grader_ctx, "expert"
1365
+
1366
+ if task_id == "ticket_triage_expert":
1367
+ agent_ctx = TICKET_EXPERT_CONTEXT
1368
+ grader_ctx = TICKET_EXPERT_CONTEXT
1369
+ return agent_ctx, grader_ctx, "expert"
1370
+
1371
+ raise ValueError(f"Unknown task_id: '{task_id}'. Use GET /tasks to see valid IDs.")
1372
+
1373
+ def _grade(self, task_id: str, payload: dict, grader_context: dict) -> tuple[float, str, dict]:
1374
+ if task_id == "email_triage_easy": return grade_email_easy(payload, grader_context)
1375
+ if task_id == "email_triage_medium": return grade_email_medium(payload, grader_context)
1376
+ if task_id == "email_triage_hard": return grade_email_hard(payload, grader_context)
1377
+ if task_id == "code_review_easy": return grade_code_easy(payload, grader_context)
1378
+ if task_id == "code_review_medium": return grade_code_medium(payload)
1379
+ if task_id == "code_review_hard": return grade_code_hard(payload)
1380
+ if task_id == "data_cleaning_easy": return grade_data_easy(payload)
1381
+ if task_id == "data_cleaning_medium": return grade_data_medium(payload)
1382
+ if task_id == "data_cleaning_hard": return grade_data_hard(payload, grader_context)
1383
+ if task_id == "content_moderation_easy": return grade_mod_easy(payload, grader_context)
1384
+ if task_id == "content_moderation_medium": return grade_mod_medium(payload, grader_context)
1385
+ if task_id == "content_moderation_hard": return grade_mod_hard(payload)
1386
+ if task_id == "ticket_triage_easy": return grade_ticket_easy(payload, grader_context)
1387
+ if task_id == "ticket_triage_medium": return grade_ticket_medium(payload)
1388
+ if task_id == "ticket_triage_hard": return grade_ticket_hard(payload)
1389
+ if task_id == "cross_agent_chain": return grade_cross_agent_chain(payload, grader_context)
1390
+ if task_id == "cross_agent_email_data": return grade_cross_agent_email_data(payload, grader_context)
1391
+ if task_id == "cross_agent_code_email": return grade_cross_agent_code_email(payload, grader_context)
1392
+ if task_id == "cross_agent_mod_escalation": return grade_cross_agent_mod_escalation(payload, grader_context)
1393
+ return 0.0, "Unknown task.", {}
1394
+
1395
+ def _compute_reward(self, score: float, task_id: str, attempt: int) -> float:
1396
+ """Trajectory-aware reward shaping."""
1397
+ base = score
1398
+ # Speed bonus: perfect on first attempt
1399
+ if score == 1.0 and attempt == 1:
1400
+ base = min(base + 0.05, 1.0)
1401
+ # Improvement bonus: reward getting better on retry
1402
+ if attempt == 2:
1403
+ prev = self._first_attempt_scores.get(task_id, 0.0)
1404
+ if score > prev:
1405
+ base = min(base + (score - prev) * 0.1, 1.0)
1406
+ # Penalty: repeated failure
1407
+ if score == 0.0 and attempt > 1:
1408
+ base = max(base - 0.05, 0.0)
1409
+ return round(base, 3)
1410
+
1411
+ def step(self, action: MetaAction) -> MetaObservation:
1412
+ self._state.step_count += 1
1413
+
1414
+ try:
1415
+ data = json.loads(action.message)
1416
+ agent = data.get("agent", "")
1417
+ task_id = data.get("task_id", "")
1418
+ payload = data.get("payload", {})
1419
+ except Exception as e:
1420
+ return MetaObservation(
1421
+ agent="error", task_id="", difficulty="",
1422
+ context={},
1423
+ instructions='message must be valid JSON: {"agent":"...","task_id":"...","payload":{...}}',
1424
+ feedback=f"JSON parse error: {e}",
1425
+ score=0.0, partial_credits={}, done=True, reward=0.0,
1426
+ )
1427
+
1428
+ # Probe β€” load context without grading
1429
+ if not payload or payload == {"_probe": True}:
1430
+ try:
1431
+ agent_ctx, grader_ctx, difficulty = self._load_context(task_id)
1432
+ self._current_agent_context = agent_ctx
1433
+ self._current_grader_context = grader_ctx
1434
+ self._current_task_id = task_id
1435
+ except ValueError:
1436
+ difficulty = ""
1437
+ agent_ctx = {}
1438
+ is_probe = payload == {"_probe": True}
1439
+ return MetaObservation(
1440
+ agent=agent, task_id=task_id, difficulty=difficulty,
1441
+ context=agent_ctx,
1442
+ instructions=INSTRUCTIONS.get(task_id, "Use GET /tasks for payload schema."),
1443
+ feedback="Probe: context loaded. Submit a real payload to get scored." if is_probe else "Empty payload. Use GET /tasks to see required fields.",
1444
+ score=0.0, partial_credits={}, done=is_probe,
1445
+ reward=-0.1 if not is_probe else 0.0,
1446
+ )
1447
+
1448
+ # Load context
1449
+ try:
1450
+ if task_id != self._current_task_id:
1451
+ agent_ctx, grader_ctx, difficulty = self._load_context(task_id)
1452
+ self._current_agent_context = agent_ctx
1453
+ self._current_grader_context = grader_ctx
1454
+ self._current_task_id = task_id
1455
+ self._attempt_counts[task_id] = 0
1456
+ else:
1457
+ agent_ctx = self._current_agent_context
1458
+ grader_ctx = self._current_grader_context
1459
+ _, __, difficulty = self._load_context(task_id)
1460
+ except ValueError as e:
1461
+ return MetaObservation(
1462
+ agent=agent, task_id=task_id, difficulty="",
1463
+ context={},
1464
+ instructions="Use GET /tasks to see valid task IDs.",
1465
+ feedback=str(e),
1466
+ score=0.0, partial_credits={}, done=True, reward=0.0,
1467
+ )
1468
+
1469
+ # Grade
1470
+ score, feedback, partial_credits = self._grade(task_id, payload, grader_ctx)
1471
+
1472
+ # Multi-step retry logic
1473
+ attempt = self._attempt_counts.get(task_id, 0) + 1
1474
+ self._attempt_counts[task_id] = attempt
1475
+
1476
+ reward = self._compute_reward(score, task_id, attempt)
1477
+
1478
+ if attempt == 1 and score < 1.0:
1479
+ self._first_attempt_scores[task_id] = score
1480
+ done = False
1481
+ reward = score * 0.5
1482
+ missed = [k for k, v in partial_credits.items() if not v]
1483
+ feedback += (
1484
+ f" [RETRY AVAILABLE] Score: {score:.2f}. "
1485
+ f"Improve on: {missed}"
1486
+ )
1487
+ elif attempt == 1 and score == 1.0:
1488
+ done = True
1489
+ self._episode_scores.append(score)
1490
+ else:
1491
+ first = self._first_attempt_scores.get(task_id, 0.0)
1492
+ score = max(score, first)
1493
+ done = True
1494
+ reward = self._compute_reward(score, task_id, attempt)
1495
+ self._episode_scores.append(score)
1496
+ feedback += f" [FINAL] Best score from 2 attempts: {score:.2f}."
1497
+
1498
+ return MetaObservation(
1499
+ agent=agent,
1500
+ task_id=task_id,
1501
+ difficulty=difficulty,
1502
+ context=agent_ctx,
1503
+ instructions=INSTRUCTIONS.get(task_id, ""),
1504
+ feedback=feedback,
1505
+ score=score,
1506
+ partial_credits=partial_credits,
1507
+ done=done,
1508
+ reward=reward,
1509
+ metadata={
1510
+ "step": self._state.step_count,
1511
+ "attempt": attempt,
1512
+ "episode_avg_score": round(
1513
+ sum(self._episode_scores) / len(self._episode_scores), 3
1514
+ ) if self._episode_scores else 0.0,
1515
+ },
1516
+ )
1517
+
1518
+ @property
1519
+ def state(self) -> State:
1520
+ return self._state
server/app.py CHANGED
@@ -29,7 +29,16 @@ from fastapi.responses import JSONResponse, RedirectResponse
29
 
30
  from openenv.core.env_server.http_server import create_app
31
 
32
- from models import MetaAction, MetaObservation
 
 
 
 
 
 
 
 
 
33
  from server.Meta_environment import MetaEnvironment
34
 
35
  # ── Create base OpenEnv app ───────────────────────────────────────────────────
@@ -283,6 +292,64 @@ async def baseline():
283
  return JSONResponse(status_code=500, content={"error": str(e)})
284
 
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  def main(host: str = "0.0.0.0", port: int = 7860) -> None:
287
  import uvicorn
288
  uvicorn.run(app, host=host, port=port)
 
29
 
30
  from openenv.core.env_server.http_server import create_app
31
 
32
+ from models import (
33
+ MetaAction, MetaObservation,
34
+ EmailTriageEasyPayload, EmailTriageMediumPayload, EmailTriageHardPayload,
35
+ CodeReviewEasyPayload, CodeReviewMediumPayload, CodeReviewHardPayload,
36
+ DataCleaningEasyPayload, DataCleaningMediumPayload, DataCleaningHardPayload,
37
+ ContentModerationEasyPayload, ContentModerationMediumPayload, ContentModerationHardPayload,
38
+ TicketTriageEasyPayload, TicketTriageMediumPayload, TicketTriageHardPayload,
39
+ CrossAgentChainPayload, CrossAgentEmailDataPayload,
40
+ CrossAgentCodeEmailPayload, CrossAgentModEscalationPayload,
41
+ )
42
  from server.Meta_environment import MetaEnvironment
43
 
44
  # ── Create base OpenEnv app ───────────────────────────────────────────────────
 
292
  return JSONResponse(status_code=500, content={"error": str(e)})
293
 
294
 
295
+
296
+ # ── Typed step endpoint β€” Fix 3 ───────────────────────────────────────────────
297
+
298
+ _TASK_TO_MODEL = {
299
+ "email_triage_easy": EmailTriageEasyPayload,
300
+ "email_triage_medium": EmailTriageMediumPayload,
301
+ "email_triage_hard": EmailTriageHardPayload,
302
+ "code_review_easy": CodeReviewEasyPayload,
303
+ "code_review_medium": CodeReviewMediumPayload,
304
+ "code_review_hard": CodeReviewHardPayload,
305
+ "data_cleaning_easy": DataCleaningEasyPayload,
306
+ "data_cleaning_medium": DataCleaningMediumPayload,
307
+ "data_cleaning_hard": DataCleaningHardPayload,
308
+ "content_moderation_easy": ContentModerationEasyPayload,
309
+ "content_moderation_medium": ContentModerationMediumPayload,
310
+ "content_moderation_hard": ContentModerationHardPayload,
311
+ "ticket_triage_easy": TicketTriageEasyPayload,
312
+ "ticket_triage_medium": TicketTriageMediumPayload,
313
+ "ticket_triage_hard": TicketTriageHardPayload,
314
+ "cross_agent_chain": CrossAgentChainPayload,
315
+ "cross_agent_email_data": CrossAgentEmailDataPayload,
316
+ "cross_agent_code_email": CrossAgentCodeEmailPayload,
317
+ "cross_agent_mod_escalation": CrossAgentModEscalationPayload,
318
+ }
319
+
320
+
321
+ @app.post("/step/typed/{task_id}")
322
+ async def step_typed(task_id: str, request: Request):
323
+ """
324
+ Typed step endpoint β€” accepts the typed payload model for the given task_id
325
+ instead of the JSON-string-in-message format.
326
+
327
+ Example:
328
+ POST /step/typed/email_triage_easy
329
+ Body: {"agent": "email_triage", "task_id": "email_triage_easy", "payload": {"classification": "spam"}}
330
+
331
+ Returns the same observation as POST /step.
332
+ """
333
+ if task_id not in _TASK_TO_MODEL:
334
+ return JSONResponse(status_code=404, content={"error": f"Unknown task_id: {task_id}. See GET /tasks."})
335
+ body = await request.json()
336
+ try:
337
+ model_cls = _TASK_TO_MODEL[task_id]
338
+ payload = body.get("payload", body)
339
+ typed = model_cls(**payload)
340
+ except Exception as e:
341
+ return JSONResponse(status_code=422, content={"error": str(e), "task_id": task_id})
342
+ agent = body.get("agent", "_".join(task_id.split("_")[:-1]))
343
+ wrapped = MetaAction(message=json.dumps({
344
+ "agent": agent, "task_id": task_id, "payload": typed.model_dump()
345
+ }))
346
+ # Delegate to the active environment instance
347
+ env = MetaEnvironment()
348
+ env.reset()
349
+ obs = env.step(wrapped)
350
+ return obs.model_dump()
351
+
352
+
353
  def main(host: str = "0.0.0.0", port: int = 7860) -> None:
354
  import uvicorn
355
  uvicorn.run(app, host=host, port=port)
server/app.py.bak ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application for Meta Multi-Agent Environment v3.0
3
+
4
+ Standard OpenEnv endpoints (from create_app):
5
+ POST /reset β€” Reset environment
6
+ POST /step β€” Execute action
7
+ GET /state β€” Current state
8
+ GET /schema β€” Action/observation schemas
9
+ GET /health β€” Health check
10
+ GET /metadata β€” Environment metadata
11
+ WS /ws β€” WebSocket persistent session
12
+
13
+ Custom Meta endpoints:
14
+ GET /tasks β€” All 19 tasks with payload schemas
15
+ POST /grader β€” Score an action without side effects
16
+ POST /baseline β€” Run baseline across all tasks
17
+ GET /ui β€” Gradio interactive frontend
18
+ GET / β€” Redirects to /ui
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import sys
26
+
27
+ from fastapi import Request
28
+ from fastapi.responses import JSONResponse, RedirectResponse
29
+
30
+ from openenv.core.env_server.http_server import create_app
31
+
32
+ from models import (
33
+ MetaAction, MetaObservation,
34
+ EmailTriageEasyPayload, EmailTriageMediumPayload, EmailTriageHardPayload,
35
+ CodeReviewEasyPayload, CodeReviewMediumPayload, CodeReviewHardPayload,
36
+ DataCleaningEasyPayload, DataCleaningMediumPayload, DataCleaningHardPayload,
37
+ ContentModerationEasyPayload, ContentModerationMediumPayload, ContentModerationHardPayload,
38
+ TicketTriageEasyPayload, TicketTriageMediumPayload, TicketTriageHardPayload,
39
+ CrossAgentChainPayload, CrossAgentEmailDataPayload,
40
+ CrossAgentCodeEmailPayload, CrossAgentModEscalationPayload,
41
+ )
42
+ from server.Meta_environment import MetaEnvironment
43
+
44
+ # ── Create base OpenEnv app ───────────────────────────────────────────────────
45
+ app = create_app(
46
+ MetaEnvironment,
47
+ MetaAction,
48
+ MetaObservation,
49
+ env_name="Meta",
50
+ max_concurrent_envs=20,
51
+ )
52
+
53
+ # ── Mount Gradio UI ───────────────────────────────────────────────────────────
54
+ try:
55
+ from server.gradio_ui import mount_gradio
56
+ mount_gradio(app)
57
+ except ImportError as e:
58
+ print(f"[WARN] Gradio UI not mounted: {e}")
59
+
60
+
61
+ # ── Root redirect ─────────────────────────────────────────────────────────────
62
+ @app.get("/")
63
+ def root():
64
+ return RedirectResponse(url="/ui")
65
+
66
+
67
+ # ── Task Registry ─────────────────────────────────────────────────────────────
68
+ TASK_REGISTRY = [
69
+ # Email Triage
70
+ {
71
+ "id": "email_triage_easy",
72
+ "name": "Email Classification",
73
+ "agent": "email_triage",
74
+ "difficulty": "easy",
75
+ "description": "Classify a single email as spam, important, or newsletter.",
76
+ "action_schema": {"message": json.dumps({"agent": "email_triage", "task_id": "email_triage_easy", "payload": {"classification": "spam | important | newsletter"}})},
77
+ },
78
+ {
79
+ "id": "email_triage_medium",
80
+ "name": "Email Prioritization",
81
+ "agent": "email_triage",
82
+ "difficulty": "medium",
83
+ "description": "Prioritize 10 workplace emails from most to least urgent.",
84
+ "action_schema": {"message": json.dumps({"agent": "email_triage", "task_id": "email_triage_medium", "payload": {"order": ["m1", "m5", "m3", "m10", "m6", "m8", "m2", "m9", "m4", "m7"]}})},
85
+ },
86
+ {
87
+ "id": "email_triage_hard",
88
+ "name": "Email Reply Drafting",
89
+ "agent": "email_triage",
90
+ "difficulty": "hard",
91
+ "description": "Draft a professional, empathetic reply to a complex customer complaint.",
92
+ "action_schema": {"message": json.dumps({"agent": "email_triage", "task_id": "email_triage_hard", "payload": {"reply": "<full reply text>"}})},
93
+ },
94
+ # Code Review
95
+ {
96
+ "id": "code_review_easy",
97
+ "name": "Syntax Error Detection",
98
+ "agent": "code_review",
99
+ "difficulty": "easy",
100
+ "description": "Find the syntax error(s) in a Python code snippet.",
101
+ "action_schema": {"message": json.dumps({"agent": "code_review", "task_id": "code_review_easy", "payload": {"errors": ["description of syntax error"]}})},
102
+ },
103
+ {
104
+ "id": "code_review_medium",
105
+ "name": "Logic Bug Detection",
106
+ "agent": "code_review",
107
+ "difficulty": "medium",
108
+ "description": "Find all 4 logical bugs in the code and suggest fixes.",
109
+ "action_schema": {"message": json.dumps({"agent": "code_review", "task_id": "code_review_medium", "payload": {"bugs": [{"location": "fn_name", "issue": "...", "fix": "..."}]}})},
110
+ },
111
+ {
112
+ "id": "code_review_hard",
113
+ "name": "Security Vulnerability Detection (8 vulns)",
114
+ "agent": "code_review",
115
+ "difficulty": "hard",
116
+ "description": "Identify all 8 security vulnerabilities: SQL injection (x2), XSS, command injection, insecure deserialization, timing attack, path traversal, SSRF.",
117
+ "action_schema": {"message": json.dumps({"agent": "code_review", "task_id": "code_review_hard", "payload": {"vulnerabilities": [{"type": "sql_injection", "location": "fn_name", "severity": "critical", "fix": "..."}]}})},
118
+ },
119
+ # Data Cleaning
120
+ {
121
+ "id": "data_cleaning_easy",
122
+ "name": "Missing Value & Duplicate Detection",
123
+ "agent": "data_cleaning",
124
+ "difficulty": "easy",
125
+ "description": "Find all missing values (by field+row) and identify duplicate rows.",
126
+ "action_schema": {"message": json.dumps({"agent": "data_cleaning", "task_id": "data_cleaning_easy", "payload": {"missing": ["age (row 2)"], "duplicates": [1, 3]}})},
127
+ },
128
+ {
129
+ "id": "data_cleaning_medium",
130
+ "name": "Data Type Normalization",
131
+ "agent": "data_cleaning",
132
+ "difficulty": "medium",
133
+ "description": "Identify 5 data quality issues and return cleaned dataset.",
134
+ "action_schema": {"message": json.dumps({"agent": "data_cleaning", "task_id": "data_cleaning_medium", "payload": {"issues": {"field_name": "description"}, "cleaned_data": []}})},
135
+ },
136
+ {
137
+ "id": "data_cleaning_hard",
138
+ "name": "Outlier Detection & Imputation",
139
+ "agent": "data_cleaning",
140
+ "difficulty": "hard",
141
+ "description": "Detect outliers, find missing values, impute and return clean dataset.",
142
+ "action_schema": {"message": json.dumps({"agent": "data_cleaning", "task_id": "data_cleaning_hard", "payload": {"outliers": [4, 8], "missing": [6, 11], "cleaned_data": []}})},
143
+ },
144
+ # Content Moderation
145
+ {
146
+ "id": "content_moderation_easy",
147
+ "name": "Explicit Content Detection",
148
+ "agent": "content_moderation",
149
+ "difficulty": "easy",
150
+ "description": "Classify 7 posts as safe or harmful.",
151
+ "action_schema": {"message": json.dumps({"agent": "content_moderation", "task_id": "content_moderation_easy", "payload": {"classifications": [{"id": "c1", "label": "harmful"}]}})},
152
+ },
153
+ {
154
+ "id": "content_moderation_medium",
155
+ "name": "Subtle Toxicity Detection",
156
+ "agent": "content_moderation",
157
+ "difficulty": "medium",
158
+ "description": "Detect subtle toxicity, sarcasm, and implicit hostility across 8 posts.",
159
+ "action_schema": {"message": json.dumps({"agent": "content_moderation", "task_id": "content_moderation_medium", "payload": {"classifications": [{"id": "m1", "label": "harmful", "reason": "..."}]}})},
160
+ },
161
+ {
162
+ "id": "content_moderation_hard",
163
+ "name": "Context-Aware & Adversarial Moderation",
164
+ "agent": "content_moderation",
165
+ "difficulty": "hard",
166
+ "description": "6 cases including dog-whistles and coordinated inauthentic behavior. Same text, two contexts.",
167
+ "action_schema": {"message": json.dumps({"agent": "content_moderation", "task_id": "content_moderation_hard", "payload": {"decisions": [{"id": "h1", "context_a_label": "safe", "context_b_label": "harmful"}]}})},
168
+ },
169
+ # Ticket Triage (NEW)
170
+ {
171
+ "id": "ticket_triage_easy",
172
+ "name": "Ticket Classification",
173
+ "agent": "ticket_triage",
174
+ "difficulty": "easy",
175
+ "description": "Classify a support ticket by priority and category.",
176
+ "action_schema": {"message": json.dumps({"agent": "ticket_triage", "task_id": "ticket_triage_easy", "payload": {"priority": "critical", "category": "bug"}})},
177
+ },
178
+ {
179
+ "id": "ticket_triage_medium",
180
+ "name": "Ticket Prioritization & Routing",
181
+ "agent": "ticket_triage",
182
+ "difficulty": "medium",
183
+ "description": "Order 8 tickets by priority and assign each to the correct team.",
184
+ "action_schema": {"message": json.dumps({"agent": "ticket_triage", "task_id": "ticket_triage_medium", "payload": {"order": ["tk1", "tk4", "..."], "assignments": {"tk1": "backend"}}})},
185
+ },
186
+ {
187
+ "id": "ticket_triage_hard",
188
+ "name": "Incident Root Cause Analysis",
189
+ "agent": "ticket_triage",
190
+ "difficulty": "hard",
191
+ "description": "Analyse 6 linked incident tickets: root cause, resolution steps, affected services, P1-P4 severity.",
192
+ "action_schema": {"message": json.dumps({"agent": "ticket_triage", "task_id": "ticket_triage_hard", "payload": {"root_cause": "...", "resolution_steps": ["..."], "affected_services": ["..."], "severity": "P1"}})},
193
+ },
194
+ # Cross-Agent Chained
195
+ {
196
+ "id": "cross_agent_chain",
197
+ "name": "Email + Code Review Chain",
198
+ "agent": "cross_agent",
199
+ "difficulty": "hard",
200
+ "description": "Classify the email AND identify the code bug in the attachment.",
201
+ "action_schema": {"message": json.dumps({"agent": "cross_agent", "task_id": "cross_agent_chain", "payload": {"email_classification": "important", "bugs": [{"location": "fn", "issue": "...", "fix": "..."}]}})},
202
+ },
203
+ {
204
+ "id": "cross_agent_email_data",
205
+ "name": "Email Priority + Data Cleaning Chain",
206
+ "agent": "cross_agent",
207
+ "difficulty": "hard",
208
+ "description": "Classify the email urgency AND identify+fix data quality issues in the attachment.",
209
+ "action_schema": {"message": json.dumps({"agent": "cross_agent", "task_id": "cross_agent_email_data", "payload": {"email_priority": "critical", "data_issues": {"field": "issue"}, "cleaned_data": []}})},
210
+ },
211
+ {
212
+ "id": "cross_agent_code_email",
213
+ "name": "Vulnerability + Disclosure Email Chain",
214
+ "agent": "cross_agent",
215
+ "difficulty": "hard",
216
+ "description": "Identify the security vulnerability AND draft a professional disclosure email.",
217
+ "action_schema": {"message": json.dumps({"agent": "cross_agent", "task_id": "cross_agent_code_email", "payload": {"vulnerability_type": "sql_injection", "vulnerability_location": "fn_name", "disclosure_email": "..."}})},
218
+ },
219
+ {
220
+ "id": "cross_agent_mod_escalation",
221
+ "name": "Moderation + Escalation Chain",
222
+ "agent": "cross_agent",
223
+ "difficulty": "hard",
224
+ "description": "Classify the content AND decide escalation AND draft a moderation notice.",
225
+ "action_schema": {"message": json.dumps({"agent": "cross_agent", "task_id": "cross_agent_mod_escalation", "payload": {"content_label": "harmful", "escalate": True, "moderation_notice": "..."}})},
226
+ },
227
+ ]
228
+
229
+
230
+ @app.get("/tasks")
231
+ def list_tasks():
232
+ return {
233
+ "environment": "Meta Multi-Agent v3.0",
234
+ "total_tasks": len(TASK_REGISTRY),
235
+ "agents": ["email_triage", "code_review", "data_cleaning", "content_moderation", "ticket_triage", "cross_agent"],
236
+ "how_to_step": (
237
+ 'POST /step with body: {"action": {"message": "<json_string>"}} '
238
+ 'where json_string = {"agent": "...", "task_id": "...", "payload": {...}}'
239
+ ),
240
+ "tasks": TASK_REGISTRY,
241
+ }
242
+
243
+
244
+ @app.post("/grader")
245
+ async def grader(request: Request):
246
+ """Score an action without persistent side effects."""
247
+ body = await request.json()
248
+ msg = body.get("message") or body.get("action", {}).get("message", "{}")
249
+ env = MetaEnvironment()
250
+ env.reset()
251
+ action = MetaAction(message=msg)
252
+ obs = env.step(action)
253
+ return {
254
+ "task_id": obs.task_id,
255
+ "agent": obs.agent,
256
+ "score": obs.score,
257
+ "reward": obs.reward,
258
+ "feedback": obs.feedback,
259
+ "partial_credits": obs.partial_credits,
260
+ "difficulty": obs.difficulty,
261
+ }
262
+
263
+
264
+ @app.post("/baseline")
265
+ async def baseline():
266
+ api_key = (
267
+ os.environ.get("OPENAI_API_KEY")
268
+ or os.environ.get("GROQ_API_KEY")
269
+ or os.environ.get("HF_TOKEN")
270
+ )
271
+ if not api_key:
272
+ return JSONResponse(
273
+ status_code=400,
274
+ content={"error": "Set one of: OPENAI_API_KEY, GROQ_API_KEY, or HF_TOKEN."},
275
+ )
276
+ api_base_url = os.environ.get("API_BASE_URL", "https://api.groq.com/openai/v1")
277
+ model_name = os.environ.get("MODEL_NAME", "llama-3.3-70b-versatile")
278
+ try:
279
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
280
+ from baseline import run_all_baselines
281
+ scores = run_all_baselines(api_key=api_key, api_base_url=api_base_url, model_name=model_name)
282
+ avg = round(sum(s["score"] for s in scores) / len(scores), 3)
283
+ return {
284
+ "environment": "Meta Multi-Agent v3.0",
285
+ "model": model_name,
286
+ "api_base": api_base_url,
287
+ "average_score": avg,
288
+ "total_tasks": len(scores),
289
+ "baseline_scores": scores,
290
+ }
291
+ except Exception as e:
292
+ return JSONResponse(status_code=500, content={"error": str(e)})
293
+
294
+
295
+
296
+ # ── Typed step endpoint β€” Fix 3 ───────────────────────────────────────────────
297
+
298
+ _TASK_TO_MODEL = {
299
+ "email_triage_easy": EmailTriageEasyPayload,
300
+ "email_triage_medium": EmailTriageMediumPayload,
301
+ "email_triage_hard": EmailTriageHardPayload,
302
+ "code_review_easy": CodeReviewEasyPayload,
303
+ "code_review_medium": CodeReviewMediumPayload,
304
+ "code_review_hard": CodeReviewHardPayload,
305
+ "data_cleaning_easy": DataCleaningEasyPayload,
306
+ "data_cleaning_medium": DataCleaningMediumPayload,
307
+ "data_cleaning_hard": DataCleaningHardPayload,
308
+ "content_moderation_easy": ContentModerationEasyPayload,
309
+ "content_moderation_medium": ContentModerationMediumPayload,
310
+ "content_moderation_hard": ContentModerationHardPayload,
311
+ "ticket_triage_easy": TicketTriageEasyPayload,
312
+ "ticket_triage_medium": TicketTriageMediumPayload,
313
+ "ticket_triage_hard": TicketTriageHardPayload,
314
+ "cross_agent_chain": CrossAgentChainPayload,
315
+ "cross_agent_email_data": CrossAgentEmailDataPayload,
316
+ "cross_agent_code_email": CrossAgentCodeEmailPayload,
317
+ "cross_agent_mod_escalation": CrossAgentModEscalationPayload,
318
+ }
319
+
320
+
321
+ @app.post("/step/typed/{task_id}")
322
+ async def step_typed(task_id: str, request: Request):
323
+ """
324
+ Typed step endpoint β€” accepts the typed payload model for the given task_id
325
+ instead of the JSON-string-in-message format.
326
+
327
+ Example:
328
+ POST /step/typed/email_triage_easy
329
+ Body: {"agent": "email_triage", "task_id": "email_triage_easy", "payload": {"classification": "spam"}}
330
+
331
+ Returns the same observation as POST /step.
332
+ """
333
+ if task_id not in _TASK_TO_MODEL:
334
+ return JSONResponse(status_code=404, content={"error": f"Unknown task_id: {task_id}. See GET /tasks."})
335
+ body = await request.json()
336
+ try:
337
+ model_cls = _TASK_TO_MODEL[task_id]
338
+ payload = body.get("payload", body)
339
+ typed = model_cls(**payload)
340
+ except Exception as e:
341
+ return JSONResponse(status_code=422, content={"error": str(e), "task_id": task_id})
342
+ agent = body.get("agent", "_".join(task_id.split("_")[:-1]))
343
+ wrapped = MetaAction(message=json.dumps({
344
+ "agent": agent, "task_id": task_id, "payload": typed.model_dump()
345
+ }))
346
+ # Delegate to the active environment instance
347
+ env = MetaEnvironment()
348
+ env.reset()
349
+ obs = env.step(wrapped)
350
+ return obs.model_dump()
351
+
352
+
353
+ def main(host: str = "0.0.0.0", port: int = 7860) -> None:
354
+ import uvicorn
355
+ uvicorn.run(app, host=host, port=port)
356
+
357
+
358
+ if __name__ == "__main__":
359
+ import argparse
360
+ parser = argparse.ArgumentParser()
361
+ parser.add_argument("--port", type=int, default=7860)
362
+ args = parser.parse_args()
363
+ main(port=args.port)
server/gradio_ui.py CHANGED
@@ -24,18 +24,30 @@ TASK_IDS = [
24
  "email_triage_easy",
25
  "email_triage_medium",
26
  "email_triage_hard",
 
27
  "code_review_easy",
28
  "code_review_medium",
29
  "code_review_hard",
 
30
  "data_cleaning_easy",
31
  "data_cleaning_medium",
32
  "data_cleaning_hard",
 
33
  "content_moderation_easy",
34
  "content_moderation_medium",
35
  "content_moderation_hard",
 
 
 
 
 
 
 
 
 
36
  ]
37
 
38
- DIFFICULTY_EMOJI = {"easy": "🟒 Easy", "medium": "🟑 Medium", "hard": "πŸ”΄ Hard"}
39
 
40
  EXAMPLE_PAYLOADS = {
41
  "email_triage_easy": {"classification": "spam"},
@@ -206,7 +218,7 @@ def build_gradio_app() -> gr.Blocks:
206
  # πŸ€– Meta OpenEnv
207
  ### Multi-Domain AI Agent Benchmark
208
 
209
- **4 agents Β· 12 tasks Β· Real-world RL environment**
210
  *Email Triage Β· Code Review Β· Data Cleaning Β· Content Moderation*
211
 
212
  ---
@@ -335,7 +347,8 @@ This makes it ideal for:
335
  | POST | `/step` | Execute action |
336
  | GET | `/state` | Current state |
337
  | GET | `/health` | Health check |
338
- | GET | `/tasks` | All 12 tasks |
 
339
  | POST | `/grader` | Score without side effects |
340
  | GET | `/schema` | JSON schemas |
341
 
 
24
  "email_triage_easy",
25
  "email_triage_medium",
26
  "email_triage_hard",
27
+ "email_triage_expert",
28
  "code_review_easy",
29
  "code_review_medium",
30
  "code_review_hard",
31
+ "code_review_expert",
32
  "data_cleaning_easy",
33
  "data_cleaning_medium",
34
  "data_cleaning_hard",
35
+ "data_cleaning_expert",
36
  "content_moderation_easy",
37
  "content_moderation_medium",
38
  "content_moderation_hard",
39
+ "content_moderation_expert",
40
+ "ticket_triage_easy",
41
+ "ticket_triage_medium",
42
+ "ticket_triage_hard",
43
+ "ticket_triage_expert",
44
+ "cross_agent_chain",
45
+ "cross_agent_email_data",
46
+ "cross_agent_code_email",
47
+ "cross_agent_mod_escalation",
48
  ]
49
 
50
+ DIFFICULTY_EMOJI = {"easy": "🟒 Easy", "medium": "🟑 Medium", "hard": "πŸ”΄ Hard", "expert": "⚫ Expert"}
51
 
52
  EXAMPLE_PAYLOADS = {
53
  "email_triage_easy": {"classification": "spam"},
 
218
  # πŸ€– Meta OpenEnv
219
  ### Multi-Domain AI Agent Benchmark
220
 
221
+ **6 agents Β· 24 tasks Β· Real-world RL environment**
222
  *Email Triage Β· Code Review Β· Data Cleaning Β· Content Moderation*
223
 
224
  ---
 
347
  | POST | `/step` | Execute action |
348
  | GET | `/state` | Current state |
349
  | GET | `/health` | Health check |
350
+ | GET | `/tasks` | All 24 tasks with schemas |
351
+ | POST | `/step/typed/{task_id}` | Typed action format |
352
  | POST | `/grader` | Score without side effects |
353
  | GET | `/schema` | JSON schemas |
354
 
server/gradio_ui.py.bak ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ server/gradio_ui.py
3
+ Gradio frontend for Meta OpenEnv β€” mounted into FastAPI at /ui
4
+
5
+ Features:
6
+ - Task Explorer: pick any of 12 tasks, see context, edit payload, submit and score
7
+ - Baseline Runner: run all 12 tasks one by one with live progress
8
+ - Score Dashboard: 12 cards showing last known score per task
9
+ """
10
+
11
+ import json
12
+ import gradio as gr
13
+
14
+ try:
15
+ from server.Meta_environment import MetaEnvironment, INSTRUCTIONS
16
+ from models import MetaAction
17
+ except ImportError:
18
+ from .Meta_environment import MetaEnvironment, INSTRUCTIONS
19
+ from ..models import MetaAction
20
+
21
+ # ── Constants ─────────────────────────────────────────────────────────────────
22
+
23
+ TASK_IDS = [
24
+ "email_triage_easy",
25
+ "email_triage_medium",
26
+ "email_triage_hard",
27
+ "email_triage_expert",
28
+ "code_review_easy",
29
+ "code_review_medium",
30
+ "code_review_hard",
31
+ "code_review_expert",
32
+ "data_cleaning_easy",
33
+ "data_cleaning_medium",
34
+ "data_cleaning_hard",
35
+ "data_cleaning_expert",
36
+ "content_moderation_easy",
37
+ "content_moderation_medium",
38
+ "content_moderation_hard",
39
+ "content_moderation_expert",
40
+ "ticket_triage_easy",
41
+ "ticket_triage_medium",
42
+ "ticket_triage_hard",
43
+ "ticket_triage_expert",
44
+ "cross_agent_chain",
45
+ "cross_agent_email_data",
46
+ "cross_agent_code_email",
47
+ "cross_agent_mod_escalation",
48
+ ]
49
+
50
+ DIFFICULTY_EMOJI = {"easy": "🟒 Easy", "medium": "🟑 Medium", "hard": "πŸ”΄ Hard", "expert": "⚫ Expert"}
51
+
52
+ EXAMPLE_PAYLOADS = {
53
+ "email_triage_easy": {"classification": "spam"},
54
+ "email_triage_medium": {"order": ["m1", "m5", "m3", "m10", "m6", "m8", "m2", "m9", "m4", "m7"]},
55
+ "email_triage_hard": {"reply": "Dear Customer, I sincerely apologize for the inconvenience caused. We will send a full refund and an overnight replacement within 24 hours. We take this very seriously and will make it right."},
56
+ "code_review_easy": {"errors": ["missing colon after for statement β€” syntax error on line 3"]},
57
+ "code_review_medium": {"bugs": [
58
+ {"location": "find_max", "issue": "max_val=0 fails for all-negative lists", "fix": "initialize to float('-inf')"},
59
+ {"location": "get_user", "issue": "= used instead of == for comparison", "fix": "change to =="},
60
+ {"location": "divide", "issue": "no zero-division guard", "fix": "check b == 0 first"},
61
+ {"location": "process_list", "issue": "off-by-one error: range(len+1) β†’ IndexError", "fix": "use range(len(items))"},
62
+ ]},
63
+ "code_review_hard": {"vulnerabilities": [
64
+ {"type": "sql_injection", "location": "get_user_by_name", "fix": "parameterized queries"},
65
+ {"type": "xss", "location": "render_comment", "fix": "sanitize user input"},
66
+ {"type": "sql_injection", "location": "login", "fix": "parameterized queries"},
67
+ {"type": "command_injection", "location": "run_report", "fix": "avoid shell=True"},
68
+ {"type": "insecure_deserialization","location": "load_user_data", "fix": "use json instead of pickle"},
69
+ ]},
70
+ "data_cleaning_easy": {"missing": ["age (row 2)", "name (row 4)", "email (row 5)", "salary (row 6)"], "duplicates": [1, 3]},
71
+ "data_cleaning_medium": {"issues": {"age": "strings not integers", "salary": "inconsistent format", "join_date": "multiple formats", "name": "inconsistent casing", "active": "mixed bool/string types"}, "cleaned_data": []},
72
+ "data_cleaning_hard": {"outliers": [4, 8], "missing": [6, 11], "cleaned_data": []},
73
+ "content_moderation_easy": {"classifications": [{"id": "c1", "label": "harmful"}, {"id": "c2", "label": "safe"}, {"id": "c3", "label": "harmful"}, {"id": "c4", "label": "safe"}, {"id": "c5", "label": "harmful"}, {"id": "c6", "label": "safe"}, {"id": "c7", "label": "harmful"}]},
74
+ "content_moderation_medium": {"classifications": [{"id": "m1", "label": "harmful"}, {"id": "m2", "label": "harmful"}, {"id": "m3", "label": "safe"}, {"id": "m4", "label": "harmful"}, {"id": "m5", "label": "harmful"}, {"id": "m6", "label": "safe"}, {"id": "m7", "label": "harmful"}, {"id": "m8", "label": "safe"}]},
75
+ "content_moderation_hard": {"decisions": [{"id": "h1", "context_a_label": "safe", "context_b_label": "harmful"}, {"id": "h2", "context_a_label": "safe", "context_b_label": "harmful"}, {"id": "h3", "context_a_label": "safe", "context_b_label": "harmful"}]},
76
+ }
77
+
78
+ # ── Helpers ───────────────────────────────────────────────────────────────────
79
+
80
+ def _probe(task_id: str):
81
+ env = MetaEnvironment()
82
+ env.reset()
83
+ agent = "_".join(task_id.split("_")[:-1])
84
+ probe = MetaAction(message=json.dumps({"agent": agent, "task_id": task_id, "payload": {"_probe": True}}))
85
+ obs = env.step(probe)
86
+ return obs
87
+
88
+
89
+ def _grade(task_id: str, payload: dict):
90
+ env = MetaEnvironment()
91
+ env.reset()
92
+ agent = "_".join(task_id.split("_")[:-1])
93
+ action = MetaAction(message=json.dumps({"agent": agent, "task_id": task_id, "payload": payload}))
94
+ obs = env.step(action)
95
+ return obs
96
+
97
+
98
+ # ── Tab 1: Task Explorer ──────────────────────────────────────────────────────
99
+
100
+ def on_task_select(task_id: str):
101
+ obs = _probe(task_id)
102
+ diff_label = DIFFICULTY_EMOJI.get(obs.difficulty, obs.difficulty)
103
+ context_str = json.dumps(obs.context, indent=2)
104
+ payload_str = json.dumps(EXAMPLE_PAYLOADS.get(task_id, {}), indent=2)
105
+ instructions = INSTRUCTIONS.get(task_id, obs.instructions)
106
+ return diff_label, instructions, context_str, payload_str, "", 0.0, ""
107
+
108
+
109
+ def on_submit(task_id: str, payload_text: str):
110
+ try:
111
+ payload = json.loads(payload_text)
112
+ except json.JSONDecodeError as e:
113
+ return f"❌ JSON parse error: {e}", 0.0, ""
114
+
115
+ obs = _grade(task_id, payload)
116
+ score_pct = int(obs.score * 100)
117
+
118
+ # Colour-coded result
119
+ if obs.score >= 0.9:
120
+ icon = "βœ…"
121
+ elif obs.score >= 0.5:
122
+ icon = "⚠️"
123
+ else:
124
+ icon = "❌"
125
+
126
+ feedback_md = f"""
127
+ **{icon} Score: {obs.score:.2f} / 1.00**
128
+
129
+ {obs.feedback}
130
+
131
+ **Partial Credits:**
132
+ ```json
133
+ {json.dumps(obs.partial_credits, indent=2)}
134
+ ```
135
+ """
136
+ return feedback_md, obs.score, f"Score: {score_pct}%"
137
+
138
+
139
+ # ── Tab 2: Baseline Runner ────────────────────────────────────────────────────
140
+
141
+ def run_all_tasks():
142
+ """Generator β€” yields one row of results at a time for live streaming."""
143
+ results = []
144
+ rows = []
145
+
146
+ for task_id in TASK_IDS:
147
+ obs = _grade(task_id, EXAMPLE_PAYLOADS.get(task_id, {}))
148
+ bar_filled = "β–ˆ" * int(obs.score * 10)
149
+ bar_empty = "β–‘" * (10 - int(obs.score * 10))
150
+ diff = obs.difficulty.upper()[:3]
151
+ icon = "βœ…" if obs.score >= 0.9 else "⚠️" if obs.score >= 0.5 else "❌"
152
+ row = [
153
+ task_id,
154
+ DIFFICULTY_EMOJI.get(obs.difficulty, diff),
155
+ f"{obs.score:.2f}",
156
+ f"{icon} [{bar_filled}{bar_empty}]",
157
+ obs.feedback[:80],
158
+ ]
159
+ rows.append(row)
160
+ results.append(obs.score)
161
+ avg = sum(results) / len(results)
162
+ yield rows, f"**Running... {len(results)}/{len(TASK_IDS)}** | Running avg: **{avg:.3f}**"
163
+
164
+ avg = sum(results) / len(results)
165
+ passed = sum(1 for s in results if s >= 0.5)
166
+ yield rows, (
167
+ f"## βœ… Complete β€” {len(TASK_IDS)} tasks\n\n"
168
+ f"**Average Score: {avg:.4f}** | "
169
+ f"Passed (β‰₯0.5): {passed}/{len(TASK_IDS)}"
170
+ )
171
+
172
+
173
+ # ── Tab 3: Score Dashboard ────────────────────────────────────────────────────
174
+
175
+ def build_dashboard_html():
176
+ cards = []
177
+ for task_id in TASK_IDS:
178
+ agent = "_".join(task_id.split("_")[:-1])
179
+ diff = task_id.split("_")[-1]
180
+ badge_color = {"easy": "#22c55e", "medium": "#f59e0b", "hard": "#ef4444"}.get(diff, "#888")
181
+ cards.append(f"""
182
+ <div style="background:#1e293b;border:1px solid #334155;border-radius:10px;padding:16px;min-width:180px;">
183
+ <div style="font-size:11px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">{agent.replace('_',' ')}</div>
184
+ <div style="font-size:14px;font-weight:600;color:#f1f5f9;margin-bottom:8px;">{task_id}</div>
185
+ <span style="background:{badge_color};color:#fff;font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;">{diff.upper()}</span>
186
+ <div style="margin-top:12px;font-size:11px;color:#64748b;">Submit a payload to see score</div>
187
+ </div>""")
188
+ grid = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;padding:8px;">'
189
+ grid += "".join(cards) + "</div>"
190
+ return grid
191
+
192
+
193
+ # ── Build Gradio Blocks app ───────────────────────────────────────────────────
194
+
195
+ def build_gradio_app() -> gr.Blocks:
196
+ css = """
197
+ body, .gradio-container { background: #0f172a !important; color: #f1f5f9 !important; }
198
+ .gr-button-primary { background: #6366f1 !important; border: none !important; }
199
+ .gr-button-primary:hover { background: #4f46e5 !important; }
200
+ .gr-input, .gr-textarea, .gr-dropdown { background: #1e293b !important; color: #f1f5f9 !important; border: 1px solid #334155 !important; }
201
+ .gr-panel { background: #1e293b !important; border: 1px solid #334155 !important; }
202
+ label { color: #94a3b8 !important; }
203
+ h1, h2, h3, h4 { color: #f1f5f9 !important; }
204
+ .gr-box { background: #1e293b !important; }
205
+ """
206
+
207
+ with gr.Blocks(
208
+ title="Meta OpenEnv β€” Multi-Domain AI Agent Benchmark",
209
+ theme=gr.themes.Base(
210
+ primary_hue="indigo",
211
+ neutral_hue="slate",
212
+ ),
213
+ css=css,
214
+ ) as demo:
215
+
216
+ # ── Header ──────────────────────────────────────────────────────────
217
+ gr.Markdown("""
218
+ # πŸ€– Meta OpenEnv
219
+ ### Multi-Domain AI Agent Benchmark
220
+
221
+ **6 agents Β· 24 tasks Β· Real-world RL environment**
222
+ *Email Triage Β· Code Review Β· Data Cleaning Β· Content Moderation*
223
+
224
+ ---
225
+ """)
226
+
227
+ # ── Tabs ─────────────────────────────────────────────────────────────
228
+ with gr.Tabs():
229
+
230
+ # ── Tab 1: Task Explorer ─────────────────────────────────────────
231
+ with gr.Tab("πŸ” Task Explorer"):
232
+ gr.Markdown("Select a task to explore its context, then edit the payload and submit to see your score.")
233
+
234
+ with gr.Row():
235
+ task_dropdown = gr.Dropdown(
236
+ choices=TASK_IDS,
237
+ value=TASK_IDS[0],
238
+ label="Select Task",
239
+ interactive=True,
240
+ )
241
+ difficulty_badge = gr.Textbox(label="Difficulty", interactive=False, scale=0)
242
+
243
+ instructions_box = gr.Textbox(
244
+ label="Instructions",
245
+ lines=4,
246
+ interactive=False,
247
+ )
248
+
249
+ with gr.Row():
250
+ context_box = gr.Code(
251
+ label="Task Context (read-only)",
252
+ language="json",
253
+ interactive=False,
254
+ lines=15,
255
+ )
256
+ payload_box = gr.Code(
257
+ label="Your Payload (edit and submit)",
258
+ language="json",
259
+ interactive=True,
260
+ lines=15,
261
+ )
262
+
263
+ submit_btn = gr.Button("πŸš€ Submit & Score", variant="primary")
264
+
265
+ with gr.Row():
266
+ feedback_md = gr.Markdown("*Submit a payload to see your score.*")
267
+ score_slider = gr.Slider(0.0, 1.0, value=0.0, label="Score", interactive=False)
268
+ score_label = gr.Textbox(label="", interactive=False, scale=0)
269
+
270
+ # Events
271
+ task_dropdown.change(
272
+ fn=on_task_select,
273
+ inputs=[task_dropdown],
274
+ outputs=[difficulty_badge, instructions_box, context_box, payload_box, feedback_md, score_slider, score_label],
275
+ )
276
+ submit_btn.click(
277
+ fn=on_submit,
278
+ inputs=[task_dropdown, payload_box],
279
+ outputs=[feedback_md, score_slider, score_label],
280
+ )
281
+
282
+ # Load initial state
283
+ demo.load(
284
+ fn=on_task_select,
285
+ inputs=[task_dropdown],
286
+ outputs=[difficulty_badge, instructions_box, context_box, payload_box, feedback_md, score_slider, score_label],
287
+ )
288
+
289
+ # ── Tab 2: Baseline Runner ────────────────────────────────────────
290
+ with gr.Tab("⚑ Baseline Runner"):
291
+ gr.Markdown("""
292
+ Run all 12 tasks using the **example payloads** and see live results.
293
+ This shows the expected score distribution across agents and difficulties.
294
+ """)
295
+
296
+ run_btn = gr.Button("β–Ά Run All 12 Tasks", variant="primary", size="lg")
297
+ status = gr.Markdown("*Click the button to start.*")
298
+
299
+ results_table = gr.Dataframe(
300
+ headers=["Task ID", "Difficulty", "Score", "Bar", "Feedback"],
301
+ datatype=["str", "str", "str", "str", "str"],
302
+ interactive=False,
303
+ wrap=True,
304
+ )
305
+
306
+ run_btn.click(
307
+ fn=run_all_tasks,
308
+ inputs=[],
309
+ outputs=[results_table, status],
310
+ )
311
+
312
+ # ── Tab 3: Score Dashboard ────────────────────────────────────────
313
+ with gr.Tab("πŸ“Š Task Dashboard"):
314
+ gr.Markdown("Overview of all 12 tasks across the 4 agents.")
315
+ gr.HTML(build_dashboard_html())
316
+
317
+ gr.Markdown("""
318
+ ### Baseline Scores (GPT-4o-mini)
319
+
320
+ | Agent | Easy | Medium | Hard | Avg |
321
+ |-------|------|--------|------|-----|
322
+ | Email Triage | 1.00 | 0.80 | 0.67 | 0.82 |
323
+ | Code Review | 1.00 | 0.75 | 0.80 | 0.85 |
324
+ | Data Cleaning | 1.00 | 0.67 | 0.67 | 0.78 |
325
+ | Content Moderation | 1.00 | 0.75 | 0.50 | 0.75 |
326
+ | **Overall** | **1.00** | **0.74** | **0.66** | **0.80** |
327
+ """)
328
+
329
+ # ── Tab 4: About ──────────────────────────────────────────────────
330
+ with gr.Tab("ℹ️ About"):
331
+ gr.Markdown("""
332
+ ## Why Meta?
333
+
334
+ Most OpenEnv environments cover a single domain. **Meta is the first multi-domain environment** that lets a single agent learn to handle completely different real-world tasks in one unified interface.
335
+
336
+ This makes it ideal for:
337
+ - Training **generalist agents** that switch between task types
338
+ - Benchmarking **multi-task learning** capabilities of LLMs
339
+ - Evaluating **transfer learning** between related domains
340
+ - Testing **robustness** across varying difficulty levels
341
+
342
+ ## API Endpoints
343
+
344
+ | Method | Endpoint | Description |
345
+ |--------|----------|-------------|
346
+ | POST | `/reset` | Reset environment |
347
+ | POST | `/step` | Execute action |
348
+ | GET | `/state` | Current state |
349
+ | GET | `/health` | Health check |
350
+ | GET | `/tasks` | All 24 tasks with schemas |
351
+ | POST | `/step/typed/{task_id}` | Typed action format |
352
+ | POST | `/grader` | Score without side effects |
353
+ | GET | `/schema` | JSON schemas |
354
+
355
+ ## Team
356
+
357
+ **Digital Yodha** β€” OpenEnv Hackathon 2026
358
+
359
+ | Name | GitHub |
360
+ |------|--------|
361
+ | Sangisetti Akarsh | [@Farbricated](https://github.com/Farbricated) |
362
+ | Sarika Jivrajika | [@Sarika-stack23](https://github.com/Sarika-stack23) |
363
+
364
+ ---
365
+ *Built for OpenEnv Hackathon 2026 Β· MIT License*
366
+ """)
367
+
368
+ return demo
369
+
370
+
371
+ # ── Mount helper (called from app.py) ─────────────────────────────────────────
372
+
373
+ def mount_gradio(app):
374
+ """Mount the Gradio app into an existing FastAPI app at /ui"""
375
+ import gradio as gr
376
+ demo = build_gradio_app()
377
+ gr.mount_gradio_app(app, demo, path="/ui")
378
+ return app