Spaces:
Sleeping
Sleeping
DayalGupta03 commited on
Commit Β·
78d0a45
1
Parent(s): 2a39e79
phase 1 and phase 2a
Browse files- Dockerfile +6 -0
- README.md +7 -7
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/baseline_inference.cpython-313.pyc +0 -0
- __pycache__/content_moderation_env.cpython-313.pyc +0 -0
- _add_scenarios.py +94 -0
- app.py +51 -0
- content_moderation_env.py +155 -124
- inference.py +331 -0
- moderation_benchmark.json +462 -31
- openenv.yaml +30 -12
- requirements.txt +1 -0
Dockerfile
CHANGED
|
@@ -21,6 +21,7 @@ RUN pip install --no-cache-dir --upgrade pip \
|
|
| 21 |
|
| 22 |
# ββ application files βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
COPY --chown=appuser:appuser . .
|
|
|
|
| 24 |
|
| 25 |
USER appuser
|
| 26 |
|
|
@@ -31,4 +32,9 @@ ENV GRADIO_SERVER_PORT=7860
|
|
| 31 |
|
| 32 |
# ββ startup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
EXPOSE 7860
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
CMD ["python", "app.py"]
|
|
|
|
| 21 |
|
| 22 |
# ββ application files βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
COPY --chown=appuser:appuser . .
|
| 24 |
+
COPY --chown=appuser:appuser inference.py .
|
| 25 |
|
| 26 |
USER appuser
|
| 27 |
|
|
|
|
| 32 |
|
| 33 |
# ββ startup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
EXPOSE 7860
|
| 35 |
+
|
| 36 |
+
# ββ health check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s \
|
| 38 |
+
CMD curl -f http://localhost:7860/ || exit 1
|
| 39 |
+
|
| 40 |
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -172,11 +172,11 @@ Expected output:
|
|
| 172 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
TIER N MEAN PERFECT ZERO
|
| 174 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 175 |
-
easy 20 0.
|
| 176 |
-
medium 20 0.
|
| 177 |
-
hard 20 0.
|
| 178 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 179 |
-
OVERALL 60 0.
|
| 180 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
```
|
| 182 |
|
|
@@ -235,9 +235,9 @@ content-moderation-env/
|
|
| 235 |
| Tier | Mean Reward | Perfect (1.0) | Zero (0.0) |
|
| 236 |
|------|-------------|----------------|------------|
|
| 237 |
| easy | 0.750 | 12/20 | 2/20 |
|
| 238 |
-
| medium | 0.575 |
|
| 239 |
-
| hard | 0.220 | 1/20 |
|
| 240 |
-
| **Overall** | **0.515** | **
|
| 241 |
|
| 242 |
> π‘ The lexical baseline performs well on easy text patterns but struggles with policy-contextual decisions (medium) and severity rating (hard). An LLM agent should significantly outperform these scores. All numbers are reproducible with `--seed 42`.
|
| 243 |
|
|
|
|
| 172 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 173 |
TIER N MEAN PERFECT ZERO
|
| 174 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 175 |
+
easy 20 0.750 12 2
|
| 176 |
+
medium 20 0.575 7 3
|
| 177 |
+
hard 20 0.220 1 8
|
| 178 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 179 |
+
OVERALL 60 0.515 20 13
|
| 180 |
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
```
|
| 182 |
|
|
|
|
| 235 |
| Tier | Mean Reward | Perfect (1.0) | Zero (0.0) |
|
| 236 |
|------|-------------|----------------|------------|
|
| 237 |
| easy | 0.750 | 12/20 | 2/20 |
|
| 238 |
+
| medium | 0.575 | 7/20 | 3/20 |
|
| 239 |
+
| hard | 0.220 | 1/20 | 8/20 |
|
| 240 |
+
| **Overall** | **0.515** | **20/60** | **13/60** |
|
| 241 |
|
| 242 |
> π‘ The lexical baseline performs well on easy text patterns but struggles with policy-contextual decisions (medium) and severity rating (hard). An LLM agent should significantly outperform these scores. All numbers are reproducible with `--seed 42`.
|
| 243 |
|
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (17.2 kB). View file
|
|
|
__pycache__/baseline_inference.cpython-313.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
__pycache__/content_moderation_env.cpython-313.pyc
ADDED
|
Binary file (18 kB). View file
|
|
|
_add_scenarios.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Appends 15 new scenarios to moderation_benchmark.json"""
|
| 2 |
+
import json, pathlib
|
| 3 |
+
|
| 4 |
+
HERE = pathlib.Path(__file__).parent
|
| 5 |
+
JSON_PATH = HERE / "moderation_benchmark.json"
|
| 6 |
+
|
| 7 |
+
HARD_RUBRIC = {"label_correct": 0.4, "action_correct": 0.4, "severity_within_1": 0.2}
|
| 8 |
+
EASY_RUBRIC = {"label_correct": 0.5, "action_correct": 0.5}
|
| 9 |
+
|
| 10 |
+
NEW = [
|
| 11 |
+
{"id":"scen_hard_21","tier":"hard",
|
| 12 |
+
"state":{"text":"Oh sure, let's just give everyone guns, that'll definitely solve all our problems. What could possibly go wrong?",
|
| 13 |
+
"audio_transcript":None,"visual_tags":["political_content"],"previous_flags":0,"platform_policy":"moderate"},
|
| 14 |
+
"ground_truth":{"label":"misleading","action":"warn","severity":2,"rationale":"Sarcastic political speech. Warn for framing, not toxic."},
|
| 15 |
+
"score_rubric":HARD_RUBRIC},
|
| 16 |
+
{"id":"scen_hard_22","tier":"hard",
|
| 17 |
+
"state":{"text":"Just sharing my thoughts on today's news. Very interesting developments!",
|
| 18 |
+
"audio_transcript":None,"visual_tags":["known_bot_network","copied_profile_picture","bulk_posting_pattern"],"previous_flags":8,"platform_policy":"strict"},
|
| 19 |
+
"ground_truth":{"label":"spam","action":"shadowban","severity":4,"rationale":"Benign text but metadata signals coordinated inauthentic behavior."},
|
| 20 |
+
"score_rubric":HARD_RUBRIC},
|
| 21 |
+
{"id":"scen_hard_23","tier":"hard",
|
| 22 |
+
"state":{"text":"For acute overdose cases, administer 0.4-2mg naloxone IV. Repeat every 2-3 minutes if no response. Max 10mg before reassessing.",
|
| 23 |
+
"audio_transcript":"Medical grand rounds recording, hospital setting",
|
| 24 |
+
"visual_tags":["medical_setting","professional_attire","hospital_environment"],"previous_flags":0,"platform_policy":"moderate"},
|
| 25 |
+
"ground_truth":{"label":"safe","action":"allow","severity":1,"rationale":"Clinical medical discussion confirmed by audio and visual context."},
|
| 26 |
+
"score_rubric":HARD_RUBRIC},
|
| 27 |
+
{"id":"scen_hard_24","tier":"hard",
|
| 28 |
+
"state":{"text":"Just a reminder that this public figure's home address and daily schedule are publicly available on their website.",
|
| 29 |
+
"audio_transcript":None,"visual_tags":["doxxing_pattern","personal_information"],"previous_flags":3,"platform_policy":"strict"},
|
| 30 |
+
"ground_truth":{"label":"toxic","action":"escalate","severity":5,"rationale":"Doxxing pattern with prior violations. Facilitates harassment."},
|
| 31 |
+
"score_rubric":HARD_RUBRIC},
|
| 32 |
+
{"id":"scen_hard_25","tier":"hard",
|
| 33 |
+
"state":{"text":"These people are ruining everything. You know what needs to happen.",
|
| 34 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":12,"platform_policy":"strict"},
|
| 35 |
+
"ground_truth":{"label":"toxic","action":"escalate","severity":5,"rationale":"Implied threat. 12 prior violations makes this critical."},
|
| 36 |
+
"score_rubric":HARD_RUBRIC},
|
| 37 |
+
{"id":"scen_hard_26","tier":"hard",
|
| 38 |
+
"state":{"text":"BREAKING: Scientists discover eating tide pods cures all diseases. Big Pharma doesn't want you to know!",
|
| 39 |
+
"audio_transcript":None,"visual_tags":["satire_account_verified","parody_label"],"previous_flags":0,"platform_policy":"lenient"},
|
| 40 |
+
"ground_truth":{"label":"misleading","action":"warn","severity":2,"rationale":"Satire account with verified parody label on lenient platform."},
|
| 41 |
+
"score_rubric":HARD_RUBRIC},
|
| 42 |
+
{"id":"scen_hard_27","tier":"hard",
|
| 43 |
+
"state":{"text":"Estos inmigrantes estan destruyendo nuestro pais. Hay que hacer algo.",
|
| 44 |
+
"audio_transcript":None,"visual_tags":["translated_content","nationalist_imagery"],"previous_flags":2,"platform_policy":"strict"},
|
| 45 |
+
"ground_truth":{"label":"toxic","action":"remove","severity":4,"rationale":"Xenophobic content in Spanish: immigrants destroying country, something must be done."},
|
| 46 |
+
"score_rubric":HARD_RUBRIC},
|
| 47 |
+
{"id":"scen_hard_28","tier":"hard",
|
| 48 |
+
"state":{"text":"I need help. I can't take it anymore. Does anyone know what happens if you take a whole bottle of sleeping pills?",
|
| 49 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":0,"platform_policy":"strict"},
|
| 50 |
+
"ground_truth":{"label":"toxic","action":"escalate","severity":5,"rationale":"Crisis signal. Escalate to human review immediately."},
|
| 51 |
+
"score_rubric":HARD_RUBRIC},
|
| 52 |
+
{"id":"scen_hard_29","tier":"hard",
|
| 53 |
+
"state":{"text":"Honest review after 6 months: This product changed my life. Lost 40 pounds, skin cleared up. DM me for my full routine!",
|
| 54 |
+
"audio_transcript":None,"visual_tags":["before_after_image","affiliate_link_detected"],"previous_flags":0,"platform_policy":"moderate"},
|
| 55 |
+
"ground_truth":{"label":"spam","action":"remove","severity":3,"rationale":"Affiliate marketing disguised as personal story."},
|
| 56 |
+
"score_rubric":HARD_RUBRIC},
|
| 57 |
+
{"id":"scen_hard_30","tier":"hard",
|
| 58 |
+
"state":{"text":"I absolutely despise how this game is designed. The devs should be ashamed of themselves.",
|
| 59 |
+
"audio_transcript":None,"visual_tags":["gaming_community","product_review"],"previous_flags":1,"platform_policy":"lenient"},
|
| 60 |
+
"ground_truth":{"label":"safe","action":"allow","severity":1,"rationale":"Strong opinion on lenient platform. Valid product criticism."},
|
| 61 |
+
"score_rubric":HARD_RUBRIC},
|
| 62 |
+
# 5 easy shadowban
|
| 63 |
+
{"id":"scen_easy_21","tier":"easy",
|
| 64 |
+
"state":{"text":"Buy cheap followers! 1000 followers for $5! DM NOW! Limited offer!",
|
| 65 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":4,"platform_policy":"moderate"},
|
| 66 |
+
"ground_truth":{"label":"spam","action":"shadowban"},"score_rubric":EASY_RUBRIC},
|
| 67 |
+
{"id":"scen_easy_22","tier":"easy",
|
| 68 |
+
"state":{"text":"Work from home and earn $500/day! No experience needed! Click my bio link!",
|
| 69 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":3,"platform_policy":"moderate"},
|
| 70 |
+
"ground_truth":{"label":"spam","action":"shadowban"},"score_rubric":EASY_RUBRIC},
|
| 71 |
+
{"id":"scen_easy_23","tier":"easy",
|
| 72 |
+
"state":{"text":"FREE crypto giveaway! Send 0.1 BTC get 1 BTC back! Official promotion!",
|
| 73 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":5,"platform_policy":"strict"},
|
| 74 |
+
"ground_truth":{"label":"spam","action":"shadowban"},"score_rubric":EASY_RUBRIC},
|
| 75 |
+
{"id":"scen_easy_24","tier":"easy",
|
| 76 |
+
"state":{"text":"Lose 30 pounds in 30 days with our miracle pill! Doctor approved! No diet needed!",
|
| 77 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":3,"platform_policy":"moderate"},
|
| 78 |
+
"ground_truth":{"label":"spam","action":"shadowban"},"score_rubric":EASY_RUBRIC},
|
| 79 |
+
{"id":"scen_easy_25","tier":"easy",
|
| 80 |
+
"state":{"text":"HOT SINGLES IN YOUR AREA! Click now! Real people waiting! Free tonight only!",
|
| 81 |
+
"audio_transcript":None,"visual_tags":[],"previous_flags":4,"platform_policy":"strict"},
|
| 82 |
+
"ground_truth":{"label":"spam","action":"shadowban"},"score_rubric":EASY_RUBRIC},
|
| 83 |
+
]
|
| 84 |
+
|
| 85 |
+
data = json.loads(JSON_PATH.read_text(encoding="utf-8"))
|
| 86 |
+
existing = {s["id"] for s in data}
|
| 87 |
+
added = 0
|
| 88 |
+
for s in NEW:
|
| 89 |
+
if s["id"] not in existing:
|
| 90 |
+
data.append(s)
|
| 91 |
+
added += 1
|
| 92 |
+
|
| 93 |
+
JSON_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
| 94 |
+
print(f"Added {added}. Total: {len(data)}")
|
app.py
CHANGED
|
@@ -324,5 +324,56 @@ ContentModerationEnv Β· OpenEnv v1.0 Β· MIT License
|
|
| 324 |
</p>
|
| 325 |
""")
|
| 326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
if __name__ == "__main__":
|
| 328 |
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
|
|
|
|
| 324 |
</p>
|
| 325 |
""")
|
| 326 |
|
| 327 |
+
|
| 328 |
+
# ββ OpenEnv HTTP API routes βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 329 |
+
# Added to the Gradio FastAPI instance so POST /reset returns HTTP 200,
|
| 330 |
+
# satisfying the HF Space validator check.
|
| 331 |
+
|
| 332 |
+
from fastapi.responses import JSONResponse
|
| 333 |
+
from fastapi import Request
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
@demo.app.post("/reset")
|
| 337 |
+
async def api_reset(request: Request):
|
| 338 |
+
"""POST /reset β initial observation, HTTP 200"""
|
| 339 |
+
try:
|
| 340 |
+
body: dict = {}
|
| 341 |
+
if request.headers.get("content-type", "").startswith("application/json"):
|
| 342 |
+
body = await request.json()
|
| 343 |
+
except Exception:
|
| 344 |
+
body = {}
|
| 345 |
+
scenario_id = body.get("scenario_id", None) if isinstance(body, dict) else None
|
| 346 |
+
try:
|
| 347 |
+
state = env.reset(scenario_id=scenario_id)
|
| 348 |
+
return JSONResponse({"state": state, "status": "ok"})
|
| 349 |
+
except Exception as exc:
|
| 350 |
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
@demo.app.post("/step")
|
| 354 |
+
async def api_step(request: Request):
|
| 355 |
+
"""POST /step β takes action dict, returns result"""
|
| 356 |
+
try:
|
| 357 |
+
body: dict = await request.json()
|
| 358 |
+
except Exception:
|
| 359 |
+
body = {}
|
| 360 |
+
action = body.get("action", {}) if isinstance(body, dict) else {}
|
| 361 |
+
try:
|
| 362 |
+
result = env.step(action)
|
| 363 |
+
return JSONResponse(result)
|
| 364 |
+
except Exception as exc:
|
| 365 |
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@demo.app.get("/state")
|
| 369 |
+
async def api_state():
|
| 370 |
+
"""GET /state β current environment state"""
|
| 371 |
+
try:
|
| 372 |
+
state = env.state()
|
| 373 |
+
return JSONResponse({"state": state, "status": "ok"})
|
| 374 |
+
except Exception as exc:
|
| 375 |
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
if __name__ == "__main__":
|
| 379 |
demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
|
content_moderation_env.py
CHANGED
|
@@ -6,13 +6,13 @@ against a pre-built scenario benchmark.
|
|
| 6 |
|
| 7 |
Usage:
|
| 8 |
env = ContentModerationEnv("moderation_benchmark.json")
|
| 9 |
-
state = env.reset() #
|
| 10 |
-
state = env.reset(scenario_id="scen_hard_7") #
|
| 11 |
result = env.step({
|
| 12 |
"label": "toxic",
|
| 13 |
"action": "escalate",
|
| 14 |
-
"severity": 5,
|
| 15 |
-
"rationale": "This is doxxing ..."
|
| 16 |
})
|
| 17 |
env.render()
|
| 18 |
"""
|
|
@@ -38,12 +38,12 @@ def _load_scenarios(path: str) -> Dict[str, dict]:
|
|
| 38 |
|
| 39 |
def _compute_reward(action: dict, scenario: dict) -> tuple[float, dict]:
|
| 40 |
"""
|
| 41 |
-
Deterministic scorer.
|
| 42 |
|
| 43 |
Returns
|
| 44 |
-------
|
| 45 |
-
reward : float
|
| 46 |
-
breakdown : dict
|
| 47 |
"""
|
| 48 |
gt = scenario["ground_truth"]
|
| 49 |
rubric = scenario["score_rubric"]
|
|
@@ -52,19 +52,24 @@ def _compute_reward(action: dict, scenario: dict) -> tuple[float, dict]:
|
|
| 52 |
breakdown: dict = {}
|
| 53 |
reward = 0.0
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
label_score = rubric["label_correct"] if label_ok else 0.0
|
| 58 |
breakdown["label_correct"] = label_score
|
| 59 |
reward += label_score
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
action_ok =
|
| 63 |
action_score = rubric["action_correct"] if action_ok else 0.0
|
| 64 |
breakdown["action_correct"] = action_score
|
| 65 |
reward += action_score
|
| 66 |
|
| 67 |
-
#
|
| 68 |
if tier == "hard" and "severity_within_1" in rubric:
|
| 69 |
gt_severity = int(gt.get("severity", 0))
|
| 70 |
pred_severity = action.get("severity")
|
|
@@ -76,8 +81,36 @@ def _compute_reward(action: dict, scenario: dict) -> tuple[float, dict]:
|
|
| 76 |
breakdown["severity_within_1"] = sev_score
|
| 77 |
reward += sev_score
|
| 78 |
|
| 79 |
-
#
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
return reward, breakdown
|
| 82 |
|
| 83 |
|
|
@@ -95,9 +128,13 @@ class ContentModerationEnv:
|
|
| 95 |
Path to the JSON file containing benchmark scenarios.
|
| 96 |
seed : int | None
|
| 97 |
Optional RNG seed for reproducible random episode selection.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
"""
|
| 99 |
|
| 100 |
-
# Supported labels and actions (for validation)
|
| 101 |
VALID_LABELS = {"safe", "toxic", "spam", "misleading"}
|
| 102 |
VALID_ACTIONS = {"allow", "warn", "remove", "shadowban", "escalate"}
|
| 103 |
|
|
@@ -116,7 +153,12 @@ class ContentModerationEnv:
|
|
| 116 |
self._last_breakdown: dict = {}
|
| 117 |
self._step_count: int = 0
|
| 118 |
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
def reset(self, scenario_id: Optional[str] = None) -> dict:
|
| 122 |
"""
|
|
@@ -125,30 +167,43 @@ class ContentModerationEnv:
|
|
| 125 |
Parameters
|
| 126 |
----------
|
| 127 |
scenario_id : str | None
|
| 128 |
-
If provided, loads that specific scenario
|
| 129 |
-
If None,
|
|
|
|
| 130 |
|
| 131 |
Returns
|
| 132 |
-------
|
| 133 |
-
state : dict
|
| 134 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
if scenario_id is not None:
|
|
|
|
| 136 |
if scenario_id not in self._scenarios:
|
| 137 |
-
available = ", ".join(self._scenario_ids[:5]) + "
|
| 138 |
raise ValueError(
|
| 139 |
f"Unknown scenario_id {scenario_id!r}. "
|
| 140 |
f"Available (sample): {available}"
|
| 141 |
)
|
| 142 |
-
|
| 143 |
else:
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
return deepcopy(self._current_scenario["state"])
|
| 153 |
|
| 154 |
def step(self, action: dict) -> dict:
|
|
@@ -159,20 +214,20 @@ class ContentModerationEnv:
|
|
| 159 |
----------
|
| 160 |
action : dict
|
| 161 |
{
|
| 162 |
-
"label":
|
| 163 |
-
"action":
|
| 164 |
-
"severity":
|
| 165 |
-
"rationale": str
|
| 166 |
}
|
| 167 |
|
| 168 |
Returns
|
| 169 |
-------
|
| 170 |
result : dict
|
| 171 |
{
|
| 172 |
-
"state": dict,
|
| 173 |
-
"reward": float,
|
| 174 |
-
"done": bool,
|
| 175 |
-
"info": dict
|
| 176 |
}
|
| 177 |
"""
|
| 178 |
if self._current_scenario is None or self._done:
|
|
@@ -182,7 +237,7 @@ class ContentModerationEnv:
|
|
| 182 |
if not isinstance(action, dict):
|
| 183 |
raise TypeError(f"action must be a dict, got {type(action).__name__!r}")
|
| 184 |
|
| 185 |
-
# Validate keys (warn but don't hard-fail,
|
| 186 |
warnings: List[str] = []
|
| 187 |
label = str(action.get("label", "")).strip().lower()
|
| 188 |
act = str(action.get("action", "")).strip().lower()
|
|
@@ -191,25 +246,40 @@ class ContentModerationEnv:
|
|
| 191 |
if act not in self.VALID_ACTIONS:
|
| 192 |
warnings.append(f"Unknown action {act!r}; valid: {sorted(self.VALID_ACTIONS)}")
|
| 193 |
|
| 194 |
-
|
|
|
|
|
|
|
| 195 |
|
| 196 |
self._last_reward = reward
|
| 197 |
self._last_breakdown = breakdown
|
| 198 |
-
self.
|
| 199 |
self._step_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
result = {
|
| 202 |
-
"state":
|
| 203 |
"reward": reward,
|
| 204 |
-
"done":
|
| 205 |
"info": {
|
| 206 |
-
"scenario_id":
|
| 207 |
-
"tier":
|
| 208 |
-
"
|
| 209 |
-
"
|
| 210 |
-
"
|
|
|
|
|
|
|
|
|
|
| 211 |
"submitted_action": deepcopy(action),
|
| 212 |
-
"warnings":
|
| 213 |
},
|
| 214 |
}
|
| 215 |
return result
|
|
@@ -221,14 +291,7 @@ class ContentModerationEnv:
|
|
| 221 |
return deepcopy(self._current_scenario["state"])
|
| 222 |
|
| 223 |
def render(self, mode: str = "text") -> None:
|
| 224 |
-
"""
|
| 225 |
-
Pretty-print the current scenario and last step result.
|
| 226 |
-
|
| 227 |
-
Parameters
|
| 228 |
-
----------
|
| 229 |
-
mode : str
|
| 230 |
-
Only "text" is supported.
|
| 231 |
-
"""
|
| 232 |
if self._current_scenario is None:
|
| 233 |
print("[ContentModerationEnv] No active scenario. Call reset() first.")
|
| 234 |
return
|
|
@@ -237,18 +300,18 @@ class ContentModerationEnv:
|
|
| 237 |
gt = sc["ground_truth"]
|
| 238 |
st = sc["state"]
|
| 239 |
|
| 240 |
-
sep = "
|
| 241 |
print(sep)
|
| 242 |
-
print(f" Scenario : {sc['id']}
|
| 243 |
print(sep)
|
| 244 |
print(f" Text : {textwrap.shorten(st['text'], 80)}")
|
| 245 |
if st.get("audio_transcript"):
|
| 246 |
print(f" Audio : {textwrap.shorten(st['audio_transcript'], 80)}")
|
| 247 |
if st.get("visual_tags"):
|
| 248 |
print(f" Visual : {', '.join(st['visual_tags'])}")
|
| 249 |
-
print(f" Prev flags: {st['previous_flags']}
|
| 250 |
print()
|
| 251 |
-
print(f" Ground truth
|
| 252 |
if "severity" in gt:
|
| 253 |
print(f" severity={gt['severity']}", end="")
|
| 254 |
print()
|
|
@@ -259,9 +322,12 @@ class ContentModerationEnv:
|
|
| 259 |
if self._done and self._last_breakdown:
|
| 260 |
print(f" Last reward : {self._last_reward:.3f}")
|
| 261 |
print(f" Breakdown : {self._last_breakdown}")
|
|
|
|
|
|
|
|
|
|
| 262 |
print(sep)
|
| 263 |
|
| 264 |
-
#
|
| 265 |
|
| 266 |
@property
|
| 267 |
def scenario_ids(self) -> List[str]:
|
|
@@ -272,13 +338,19 @@ class ContentModerationEnv:
|
|
| 272 |
def num_scenarios(self) -> int:
|
| 273 |
return len(self._scenarios)
|
| 274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
def __repr__(self) -> str:
|
| 276 |
active = self._current_scenario["id"] if self._current_scenario else "None"
|
| 277 |
return (
|
| 278 |
f"ContentModerationEnv("
|
| 279 |
f"scenarios={self.num_scenarios}, "
|
| 280 |
f"active={active!r}, "
|
| 281 |
-
f"done={self._done}
|
|
|
|
| 282 |
)
|
| 283 |
|
| 284 |
|
|
@@ -287,83 +359,42 @@ class ContentModerationEnv:
|
|
| 287 |
# ---------------------------------------------------------------------------
|
| 288 |
|
| 289 |
def main():
|
| 290 |
-
# Resolve path relative to this file's directory
|
| 291 |
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 292 |
scenarios_path = os.path.join(base_dir, "moderation_benchmark.json")
|
| 293 |
|
| 294 |
print("=" * 62)
|
| 295 |
-
print(" ContentModerationEnv
|
| 296 |
print("=" * 62)
|
| 297 |
|
| 298 |
env = ContentModerationEnv(scenarios_path, seed=42)
|
| 299 |
print(f"Loaded {env.num_scenarios} scenarios.\n")
|
| 300 |
|
| 301 |
-
#
|
| 302 |
-
print("
|
| 303 |
state = env.reset(scenario_id="scen_easy_2")
|
| 304 |
-
print(f" State text: {state['text']!r}")
|
| 305 |
-
|
| 306 |
result = env.step({"label": "toxic", "action": "remove"})
|
| 307 |
-
print(f" Reward
|
| 308 |
-
print(f" Breakdown: {result['info']['score_breakdown']}")
|
| 309 |
-
env.render()
|
| 310 |
|
| 311 |
-
#
|
| 312 |
-
print("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
env.reset(scenario_id="scen_easy_2")
|
| 314 |
-
result = env.step({"label": "
|
| 315 |
-
print(f" Reward
|
| 316 |
-
print(f" Breakdown: {result['info']['score_breakdown']}")
|
| 317 |
-
|
| 318 |
-
# ββ Episode 3: Medium tier, partial βββββββββββββββββββββββββββββββββββββ
|
| 319 |
-
print("\nβΆ Episode 3 β Medium tier, scen_medium_5 (correct label, wrong action)")
|
| 320 |
-
env.reset(scenario_id="scen_medium_5")
|
| 321 |
-
result = env.step({"label": "toxic", "action": "warn"}) # GT action=shadowban
|
| 322 |
-
print(f" Reward : {result['reward']} (expected 0.5 β action wrong)")
|
| 323 |
-
print(f" Breakdown: {result['info']['score_breakdown']}")
|
| 324 |
-
print(f" Ground truth: {result['info']['ground_truth']}")
|
| 325 |
-
|
| 326 |
-
# ββ Episode 4: Hard tier, near miss on severity ββββββββββββββββββββββββββ
|
| 327 |
-
print("\nβΆ Episode 4 β Hard tier, scen_hard_1 (severity Β±1)")
|
| 328 |
-
env.reset(scenario_id="scen_hard_1")
|
| 329 |
-
result = env.step({
|
| 330 |
-
"label": "toxic",
|
| 331 |
-
"action": "escalate",
|
| 332 |
-
"severity": 4, # GT=5, within Β±1 β gets severity credit
|
| 333 |
-
"rationale": "Coordinated threat with prior violations."
|
| 334 |
-
})
|
| 335 |
-
print(f" Reward : {result['reward']} (expected 1.0 β all correct, sev within 1)")
|
| 336 |
print(f" Breakdown: {result['info']['score_breakdown']}")
|
| 337 |
|
| 338 |
-
# ββ Episode 5: Hard tier, severity too far off βββββββββββββββββββββββββ
|
| 339 |
-
print("\nβΆ Episode 5 β Hard tier, scen_hard_1 (severity off by 2)")
|
| 340 |
-
env.reset(scenario_id="scen_hard_1")
|
| 341 |
-
result = env.step({
|
| 342 |
-
"label": "toxic",
|
| 343 |
-
"action": "escalate",
|
| 344 |
-
"severity": 2, # GT=5, off by 3 β no severity credit
|
| 345 |
-
"rationale": "Some concern."
|
| 346 |
-
})
|
| 347 |
-
print(f" Reward : {result['reward']} (expected 0.8 β sev wrong)")
|
| 348 |
-
print(f" Breakdown: {result['info']['score_breakdown']}")
|
| 349 |
-
|
| 350 |
-
# ββ Episode 6: Random episode ββββββββββββββββββββββββββββββββββοΏ½οΏ½βββββββββ
|
| 351 |
-
print("\nβΆ Episode 6 β Random scenario, dummy all-wrong action")
|
| 352 |
-
state = env.reset()
|
| 353 |
-
print(f" Current scenario: {env}")
|
| 354 |
-
result = env.step({"label": "safe", "action": "allow", "severity": 1})
|
| 355 |
-
print(f" Reward : {result['reward']}")
|
| 356 |
-
env.render()
|
| 357 |
-
|
| 358 |
-
# ββ Step guard: calling step() twice without reset βββββββββββββββββββββββ
|
| 359 |
-
print("\nβΆ Episode 7 β Guard: step() on done env raises RuntimeError")
|
| 360 |
-
try:
|
| 361 |
-
env.step({"label": "safe", "action": "allow"})
|
| 362 |
-
except RuntimeError as e:
|
| 363 |
-
print(f" β Caught expected error: {e}")
|
| 364 |
-
|
| 365 |
-
print("\nβ
All episodes completed successfully.")
|
| 366 |
-
|
| 367 |
|
| 368 |
if __name__ == "__main__":
|
| 369 |
main()
|
|
|
|
| 6 |
|
| 7 |
Usage:
|
| 8 |
env = ContentModerationEnv("moderation_benchmark.json")
|
| 9 |
+
state = env.reset() # 3-post queue episode
|
| 10 |
+
state = env.reset(scenario_id="scen_hard_7") # single-step (backward compat)
|
| 11 |
result = env.step({
|
| 12 |
"label": "toxic",
|
| 13 |
"action": "escalate",
|
| 14 |
+
"severity": 5,
|
| 15 |
+
"rationale": "This is doxxing ..."
|
| 16 |
})
|
| 17 |
env.render()
|
| 18 |
"""
|
|
|
|
| 38 |
|
| 39 |
def _compute_reward(action: dict, scenario: dict) -> tuple[float, dict]:
|
| 40 |
"""
|
| 41 |
+
Deterministic scorer with penalty support.
|
| 42 |
|
| 43 |
Returns
|
| 44 |
-------
|
| 45 |
+
reward : float -- -0.3 ... 1.0 (penalties may make it negative)
|
| 46 |
+
breakdown : dict -- per-component scores for debugging
|
| 47 |
"""
|
| 48 |
gt = scenario["ground_truth"]
|
| 49 |
rubric = scenario["score_rubric"]
|
|
|
|
| 52 |
breakdown: dict = {}
|
| 53 |
reward = 0.0
|
| 54 |
|
| 55 |
+
# -- Label ------------------------------------------------------------------
|
| 56 |
+
label_submitted = str(action.get("label", "")).strip().lower()
|
| 57 |
+
action_submitted = str(action.get("action", "")).strip().lower()
|
| 58 |
+
gt_label = str(gt.get("label", "")).strip().lower()
|
| 59 |
+
gt_action = str(gt.get("action", "")).strip().lower()
|
| 60 |
+
|
| 61 |
+
label_ok = label_submitted == gt_label
|
| 62 |
label_score = rubric["label_correct"] if label_ok else 0.0
|
| 63 |
breakdown["label_correct"] = label_score
|
| 64 |
reward += label_score
|
| 65 |
|
| 66 |
+
# -- Action -----------------------------------------------------------------
|
| 67 |
+
action_ok = action_submitted == gt_action
|
| 68 |
action_score = rubric["action_correct"] if action_ok else 0.0
|
| 69 |
breakdown["action_correct"] = action_score
|
| 70 |
reward += action_score
|
| 71 |
|
| 72 |
+
# -- Severity (hard tier only) ----------------------------------------------
|
| 73 |
if tier == "hard" and "severity_within_1" in rubric:
|
| 74 |
gt_severity = int(gt.get("severity", 0))
|
| 75 |
pred_severity = action.get("severity")
|
|
|
|
| 81 |
breakdown["severity_within_1"] = sev_score
|
| 82 |
reward += sev_score
|
| 83 |
|
| 84 |
+
# -- Penalties --------------------------------------------------------------
|
| 85 |
+
|
| 86 |
+
# Penalty 1: False positive escalation (-0.25)
|
| 87 |
+
# Removing/escalating content the GT says is safe is documented real-world harm.
|
| 88 |
+
if gt_label == "safe" and action_submitted in {"escalate", "remove"}:
|
| 89 |
+
breakdown["false_positive_penalty"] = -0.25
|
| 90 |
+
reward += -0.25
|
| 91 |
+
|
| 92 |
+
# Penalty 2: Missed critical threat (-0.20)
|
| 93 |
+
# Missing a severity-5 threat is the worst moderation failure.
|
| 94 |
+
if (
|
| 95 |
+
gt_label == "toxic"
|
| 96 |
+
and int(gt.get("severity", 0)) == 5
|
| 97 |
+
and action_submitted in {"allow", "warn"}
|
| 98 |
+
):
|
| 99 |
+
breakdown["missed_critical_threat_penalty"] = -0.20
|
| 100 |
+
reward += -0.20
|
| 101 |
+
|
| 102 |
+
# Penalty 3: Invalid label (-0.10)
|
| 103 |
+
if label_submitted not in {"safe", "toxic", "spam", "misleading"}:
|
| 104 |
+
breakdown["invalid_label_penalty"] = -0.10
|
| 105 |
+
reward += -0.10
|
| 106 |
+
|
| 107 |
+
# Penalty 4: Invalid action (-0.10)
|
| 108 |
+
if action_submitted not in {"allow", "warn", "remove", "shadowban", "escalate"}:
|
| 109 |
+
breakdown["invalid_action_penalty"] = -0.10
|
| 110 |
+
reward += -0.10
|
| 111 |
+
|
| 112 |
+
# Clamp to [-0.3, 1.0]
|
| 113 |
+
reward = max(-0.3, min(1.0, round(reward, 6)))
|
| 114 |
return reward, breakdown
|
| 115 |
|
| 116 |
|
|
|
|
| 128 |
Path to the JSON file containing benchmark scenarios.
|
| 129 |
seed : int | None
|
| 130 |
Optional RNG seed for reproducible random episode selection.
|
| 131 |
+
|
| 132 |
+
Episode modes
|
| 133 |
+
-------------
|
| 134 |
+
Queue mode : reset() -- 3-post episode (1 easy+1 med+1 hard)
|
| 135 |
+
Single-step : reset(scenario_id="...") -- backward-compatible, done=True after 1 step
|
| 136 |
"""
|
| 137 |
|
|
|
|
| 138 |
VALID_LABELS = {"safe", "toxic", "spam", "misleading"}
|
| 139 |
VALID_ACTIONS = {"allow", "warn", "remove", "shadowban", "escalate"}
|
| 140 |
|
|
|
|
| 153 |
self._last_breakdown: dict = {}
|
| 154 |
self._step_count: int = 0
|
| 155 |
|
| 156 |
+
# Queue episode state
|
| 157 |
+
self._queue: List[dict] = []
|
| 158 |
+
self._queue_index: int = 0
|
| 159 |
+
self._episode_rewards: List[float] = []
|
| 160 |
+
|
| 161 |
+
# -- Core API ---------------------------------------------------------------
|
| 162 |
|
| 163 |
def reset(self, scenario_id: Optional[str] = None) -> dict:
|
| 164 |
"""
|
|
|
|
| 167 |
Parameters
|
| 168 |
----------
|
| 169 |
scenario_id : str | None
|
| 170 |
+
If provided, loads that specific scenario (single-step mode,
|
| 171 |
+
backward compatible). If None, samples a mixed-tier queue of
|
| 172 |
+
3 scenarios (1 easy + 1 medium + 1 hard) for a multi-step episode.
|
| 173 |
|
| 174 |
Returns
|
| 175 |
-------
|
| 176 |
+
state : dict -- the first scenario's observation
|
| 177 |
"""
|
| 178 |
+
self._episode_rewards = []
|
| 179 |
+
self._queue_index = 0
|
| 180 |
+
self._last_reward = 0.0
|
| 181 |
+
self._last_breakdown = {}
|
| 182 |
+
self._step_count = 0
|
| 183 |
+
self._done = False
|
| 184 |
+
|
| 185 |
if scenario_id is not None:
|
| 186 |
+
# -- Single-step mode (backward compatible) -------------------------
|
| 187 |
if scenario_id not in self._scenarios:
|
| 188 |
+
available = ", ".join(self._scenario_ids[:5]) + " ..."
|
| 189 |
raise ValueError(
|
| 190 |
f"Unknown scenario_id {scenario_id!r}. "
|
| 191 |
f"Available (sample): {available}"
|
| 192 |
)
|
| 193 |
+
self._queue = [deepcopy(self._scenarios[scenario_id])]
|
| 194 |
else:
|
| 195 |
+
# -- Queue mode: 1 easy + 1 medium + 1 hard ------------------------
|
| 196 |
+
easy_ids = [i for i in self._scenario_ids if i.startswith("scen_easy_")]
|
| 197 |
+
medium_ids = [i for i in self._scenario_ids if i.startswith("scen_medium_")]
|
| 198 |
+
hard_ids = [i for i in self._scenario_ids if i.startswith("scen_hard_")]
|
| 199 |
+
sampled = [
|
| 200 |
+
self._rng.choice(easy_ids),
|
| 201 |
+
self._rng.choice(medium_ids),
|
| 202 |
+
self._rng.choice(hard_ids),
|
| 203 |
+
]
|
| 204 |
+
self._queue = [deepcopy(self._scenarios[sid]) for sid in sampled]
|
| 205 |
+
|
| 206 |
+
self._current_scenario = self._queue[0]
|
| 207 |
return deepcopy(self._current_scenario["state"])
|
| 208 |
|
| 209 |
def step(self, action: dict) -> dict:
|
|
|
|
| 214 |
----------
|
| 215 |
action : dict
|
| 216 |
{
|
| 217 |
+
"label": str, # required: safe/toxic/spam/misleading
|
| 218 |
+
"action": str, # required: allow/warn/remove/shadowban/escalate
|
| 219 |
+
"severity": int, # optional; scored in hard tier (1-5)
|
| 220 |
+
"rationale": str # optional; not scored
|
| 221 |
}
|
| 222 |
|
| 223 |
Returns
|
| 224 |
-------
|
| 225 |
result : dict
|
| 226 |
{
|
| 227 |
+
"state": dict, -- next observation (next queued post, or current if done)
|
| 228 |
+
"reward": float, -- -0.3 ... 1.0 (with penalty support)
|
| 229 |
+
"done": bool, -- False until final post in queue is processed
|
| 230 |
+
"info": dict -- breakdown + ground truth + queue metadata
|
| 231 |
}
|
| 232 |
"""
|
| 233 |
if self._current_scenario is None or self._done:
|
|
|
|
| 237 |
if not isinstance(action, dict):
|
| 238 |
raise TypeError(f"action must be a dict, got {type(action).__name__!r}")
|
| 239 |
|
| 240 |
+
# Validate keys (warn but don't hard-fail, allows partial submissions)
|
| 241 |
warnings: List[str] = []
|
| 242 |
label = str(action.get("label", "")).strip().lower()
|
| 243 |
act = str(action.get("action", "")).strip().lower()
|
|
|
|
| 246 |
if act not in self.VALID_ACTIONS:
|
| 247 |
warnings.append(f"Unknown action {act!r}; valid: {sorted(self.VALID_ACTIONS)}")
|
| 248 |
|
| 249 |
+
# Capture scenario being processed BEFORE advancing queue
|
| 250 |
+
processed_scenario = self._current_scenario
|
| 251 |
+
reward, breakdown = _compute_reward(action, processed_scenario)
|
| 252 |
|
| 253 |
self._last_reward = reward
|
| 254 |
self._last_breakdown = breakdown
|
| 255 |
+
self._episode_rewards.append(reward)
|
| 256 |
self._step_count += 1
|
| 257 |
+
self._queue_index += 1
|
| 258 |
+
|
| 259 |
+
# Advance queue or mark episode done
|
| 260 |
+
if self._queue_index >= len(self._queue):
|
| 261 |
+
self._done = True
|
| 262 |
+
next_state = deepcopy(processed_scenario["state"])
|
| 263 |
+
else:
|
| 264 |
+
self._done = False
|
| 265 |
+
self._current_scenario = self._queue[self._queue_index]
|
| 266 |
+
next_state = deepcopy(self._current_scenario["state"])
|
| 267 |
|
| 268 |
result = {
|
| 269 |
+
"state": next_state,
|
| 270 |
"reward": reward,
|
| 271 |
+
"done": self._done,
|
| 272 |
"info": {
|
| 273 |
+
"scenario_id": processed_scenario["id"],
|
| 274 |
+
"tier": processed_scenario["tier"],
|
| 275 |
+
"queue_position": self._queue_index,
|
| 276 |
+
"queue_length": len(self._queue),
|
| 277 |
+
"episode_rewards": list(self._episode_rewards),
|
| 278 |
+
"ground_truth": deepcopy(processed_scenario["ground_truth"]),
|
| 279 |
+
"score_rubric": deepcopy(processed_scenario["score_rubric"]),
|
| 280 |
+
"score_breakdown": breakdown,
|
| 281 |
"submitted_action": deepcopy(action),
|
| 282 |
+
"warnings": warnings,
|
| 283 |
},
|
| 284 |
}
|
| 285 |
return result
|
|
|
|
| 291 |
return deepcopy(self._current_scenario["state"])
|
| 292 |
|
| 293 |
def render(self, mode: str = "text") -> None:
|
| 294 |
+
"""Pretty-print the current scenario and last step result."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
if self._current_scenario is None:
|
| 296 |
print("[ContentModerationEnv] No active scenario. Call reset() first.")
|
| 297 |
return
|
|
|
|
| 300 |
gt = sc["ground_truth"]
|
| 301 |
st = sc["state"]
|
| 302 |
|
| 303 |
+
sep = "-" * 62
|
| 304 |
print(sep)
|
| 305 |
+
print(f" Scenario : {sc['id']} | Tier: {sc['tier'].upper()}")
|
| 306 |
print(sep)
|
| 307 |
print(f" Text : {textwrap.shorten(st['text'], 80)}")
|
| 308 |
if st.get("audio_transcript"):
|
| 309 |
print(f" Audio : {textwrap.shorten(st['audio_transcript'], 80)}")
|
| 310 |
if st.get("visual_tags"):
|
| 311 |
print(f" Visual : {', '.join(st['visual_tags'])}")
|
| 312 |
+
print(f" Prev flags: {st['previous_flags']} | Policy: {st['platform_policy']}")
|
| 313 |
print()
|
| 314 |
+
print(f" Ground truth > label={gt['label']} action={gt['action']}", end="")
|
| 315 |
if "severity" in gt:
|
| 316 |
print(f" severity={gt['severity']}", end="")
|
| 317 |
print()
|
|
|
|
| 322 |
if self._done and self._last_breakdown:
|
| 323 |
print(f" Last reward : {self._last_reward:.3f}")
|
| 324 |
print(f" Breakdown : {self._last_breakdown}")
|
| 325 |
+
if self._episode_rewards:
|
| 326 |
+
ep_total = sum(self._episode_rewards)
|
| 327 |
+
print(f" Episode rewards: {self._episode_rewards} (total={ep_total:.3f})")
|
| 328 |
print(sep)
|
| 329 |
|
| 330 |
+
# -- Convenience ------------------------------------------------------------
|
| 331 |
|
| 332 |
@property
|
| 333 |
def scenario_ids(self) -> List[str]:
|
|
|
|
| 338 |
def num_scenarios(self) -> int:
|
| 339 |
return len(self._scenarios)
|
| 340 |
|
| 341 |
+
@property
|
| 342 |
+
def episode_rewards(self) -> List[float]:
|
| 343 |
+
"""All rewards collected in the current episode so far."""
|
| 344 |
+
return list(self._episode_rewards)
|
| 345 |
+
|
| 346 |
def __repr__(self) -> str:
|
| 347 |
active = self._current_scenario["id"] if self._current_scenario else "None"
|
| 348 |
return (
|
| 349 |
f"ContentModerationEnv("
|
| 350 |
f"scenarios={self.num_scenarios}, "
|
| 351 |
f"active={active!r}, "
|
| 352 |
+
f"done={self._done}, "
|
| 353 |
+
f"queue={self._queue_index}/{len(self._queue)})"
|
| 354 |
)
|
| 355 |
|
| 356 |
|
|
|
|
| 359 |
# ---------------------------------------------------------------------------
|
| 360 |
|
| 361 |
def main():
|
|
|
|
| 362 |
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 363 |
scenarios_path = os.path.join(base_dir, "moderation_benchmark.json")
|
| 364 |
|
| 365 |
print("=" * 62)
|
| 366 |
+
print(" ContentModerationEnv -- Demo")
|
| 367 |
print("=" * 62)
|
| 368 |
|
| 369 |
env = ContentModerationEnv(scenarios_path, seed=42)
|
| 370 |
print(f"Loaded {env.num_scenarios} scenarios.\n")
|
| 371 |
|
| 372 |
+
# -- Single-step mode (backward compat) ------------------------------------
|
| 373 |
+
print("Episode 1 -- single-step (scen_easy_2, perfect)")
|
| 374 |
state = env.reset(scenario_id="scen_easy_2")
|
|
|
|
|
|
|
| 375 |
result = env.step({"label": "toxic", "action": "remove"})
|
| 376 |
+
print(f" Reward: {result['reward']} done={result['done']}\n")
|
|
|
|
|
|
|
| 377 |
|
| 378 |
+
# -- Multi-step queue mode ------------------------------------------------
|
| 379 |
+
print("Episode 2 -- queue mode (3 posts, mixed tiers)")
|
| 380 |
+
state = env.reset()
|
| 381 |
+
step_n = 0
|
| 382 |
+
while True:
|
| 383 |
+
step_n += 1
|
| 384 |
+
result = env.step({"label": "safe", "action": "allow", "severity": 1})
|
| 385 |
+
print(f" Step {step_n}: reward={result['reward']} done={result['done']}")
|
| 386 |
+
if result["done"]:
|
| 387 |
+
break
|
| 388 |
+
state = result["state"]
|
| 389 |
+
print(f" Episode rewards: {env.episode_rewards}\n")
|
| 390 |
+
|
| 391 |
+
# -- Penalty test ----------------------------------------------------------
|
| 392 |
+
print("Episode 3 -- penalty (escalating safe content)")
|
| 393 |
env.reset(scenario_id="scen_easy_2")
|
| 394 |
+
result = env.step({"label": "safe", "action": "escalate"})
|
| 395 |
+
print(f" Reward: {result['reward']} (expect < 0)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
print(f" Breakdown: {result['info']['score_breakdown']}")
|
| 397 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
if __name__ == "__main__":
|
| 400 |
main()
|
inference.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference.py β OpenAI LLM agent for ContentModerationEnv
|
| 3 |
+
=========================================================
|
| 4 |
+
Hackathon-compliant inference script for the OpenEnv Content Moderation
|
| 5 |
+
benchmark. Uses the OpenAI client to drive an LLM agent through 3 task
|
| 6 |
+
episodes (easy / medium / hard moderation), then emits the exact stdout
|
| 7 |
+
format required for automated evaluation scoring.
|
| 8 |
+
|
| 9 |
+
Credentials (read from environment variables):
|
| 10 |
+
API_BASE_URL β LLM API endpoint (default: https://api.openai.com/v1)
|
| 11 |
+
MODEL_NAME β model identifier (default: gpt-4o-mini)
|
| 12 |
+
HF_TOKEN β API key (falls back to OPENAI_API_KEY)
|
| 13 |
+
|
| 14 |
+
Stdout format (zero deviation allowed):
|
| 15 |
+
[START] task=<name> env=content_moderation model=<model>
|
| 16 |
+
[STEP] step=<n> action=<json> reward=<0.00> done=<true|false> error=<msg|null>
|
| 17 |
+
[END] success=<true|false> steps=<n> rewards=<r1,r2,...>
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
import json
|
| 21 |
+
import os
|
| 22 |
+
import sys
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from typing import Dict, List, Optional
|
| 25 |
+
|
| 26 |
+
from openai import OpenAI
|
| 27 |
+
|
| 28 |
+
# ββ local import ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
+
SCRIPT_DIR = Path(__file__).parent
|
| 30 |
+
sys.path.insert(0, str(SCRIPT_DIR))
|
| 31 |
+
from content_moderation_env import ContentModerationEnv
|
| 32 |
+
|
| 33 |
+
# ββ Credentials βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
+
API_BASE_URL: str = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
|
| 35 |
+
MODEL_NAME: str = os.getenv("MODEL_NAME", "gpt-4o-mini")
|
| 36 |
+
API_KEY: Optional[str] = os.getenv("HF_TOKEN") or os.getenv("OPENAI_API_KEY")
|
| 37 |
+
|
| 38 |
+
# ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
SCENARIOS_PATH = SCRIPT_DIR / "moderation_benchmark.json"
|
| 40 |
+
ENV_NAME = "content_moderation"
|
| 41 |
+
|
| 42 |
+
# Tasks and their scenario ID slices (5 scenarios each)
|
| 43 |
+
TASKS = [
|
| 44 |
+
{
|
| 45 |
+
"name": "easy_moderation",
|
| 46 |
+
"tier": "easy",
|
| 47 |
+
"scenario_ids": [f"scen_easy_{i}" for i in range(1, 6)],
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"name": "medium_moderation",
|
| 51 |
+
"tier": "medium",
|
| 52 |
+
"scenario_ids": [f"scen_medium_{i}" for i in range(1, 6)],
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"name": "hard_moderation",
|
| 56 |
+
"tier": "hard",
|
| 57 |
+
"scenario_ids": [f"scen_hard_{i}" for i in range(1, 6)],
|
| 58 |
+
},
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
# Fallback action used when the LLM response cannot be parsed
|
| 62 |
+
FALLBACK_ACTION: Dict = {"label": "safe", "action": "allow"}
|
| 63 |
+
|
| 64 |
+
# ββ Logging helpers (exact stdout format) βββββββββββββββββββββββββββββββββββββ
|
| 65 |
+
|
| 66 |
+
def log_start(task_name: str, model: str) -> None:
|
| 67 |
+
"""Emit [START] line."""
|
| 68 |
+
print(f"[START] task={task_name} env={ENV_NAME} model={model}", flush=True)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def log_step(
|
| 72 |
+
step: int,
|
| 73 |
+
action: Dict,
|
| 74 |
+
reward: float,
|
| 75 |
+
done: bool,
|
| 76 |
+
error: Optional[str],
|
| 77 |
+
) -> None:
|
| 78 |
+
"""Emit [STEP] line.
|
| 79 |
+
|
| 80 |
+
Rules:
|
| 81 |
+
- action serialised as compact JSON (no spaces)
|
| 82 |
+
- reward: exactly 2 decimal places
|
| 83 |
+
- done: lowercase string 'true' or 'false'
|
| 84 |
+
- error: raw string or the literal word null (not quoted 'null', not None)
|
| 85 |
+
"""
|
| 86 |
+
action_str = json.dumps(action, separators=(",", ":"))
|
| 87 |
+
reward_str = f"{reward:.2f}"
|
| 88 |
+
done_str = "true" if done else "false"
|
| 89 |
+
error_str = error if error is not None else "null"
|
| 90 |
+
print(
|
| 91 |
+
f"[STEP] step={step} action={action_str} reward={reward_str} "
|
| 92 |
+
f"done={done_str} error={error_str}",
|
| 93 |
+
flush=True,
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def log_end(success: bool, steps: int, rewards: List[float]) -> None:
|
| 98 |
+
"""Emit [END] line.
|
| 99 |
+
|
| 100 |
+
Rules:
|
| 101 |
+
- success: lowercase string 'true' or 'false'
|
| 102 |
+
- rewards: comma-separated, each exactly 2 decimal places
|
| 103 |
+
"""
|
| 104 |
+
success_str = "true" if success else "false"
|
| 105 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 106 |
+
print(
|
| 107 |
+
f"[END] success={success_str} steps={steps} rewards={rewards_str}",
|
| 108 |
+
flush=True,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ββ Prompt builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 113 |
+
|
| 114 |
+
_SYSTEM_PROMPT = """\
|
| 115 |
+
You are an expert content moderation agent for a social media platform.
|
| 116 |
+
You will be shown a piece of content and must make a moderation decision.
|
| 117 |
+
|
| 118 |
+
You must respond with ONLY a valid JSON object, nothing else.
|
| 119 |
+
No explanation, no markdown, no code blocks. Just the raw JSON.
|
| 120 |
+
|
| 121 |
+
For easy and medium scenarios respond with:
|
| 122 |
+
{"label": "<label>", "action": "<action>"}
|
| 123 |
+
|
| 124 |
+
For hard scenarios also include severity:
|
| 125 |
+
{"label": "<label>", "action": "<action>", "severity": <1-5>, "rationale": "<brief reason>"}
|
| 126 |
+
|
| 127 |
+
Valid labels: safe, toxic, spam, misleading
|
| 128 |
+
Valid actions: allow, warn, remove, shadowban, escalate
|
| 129 |
+
severity: integer 1 (mild) to 5 (critical) β include only for hard tier\
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def build_prompt(state: Dict, tier: str) -> str:
|
| 134 |
+
"""Build the user-turn content from an environment state dict."""
|
| 135 |
+
audio = state.get("audio_transcript") or "None"
|
| 136 |
+
vtags = state.get("visual_tags") or []
|
| 137 |
+
vtags_str = ", ".join(vtags) if vtags else "None"
|
| 138 |
+
flags = state.get("previous_flags", 0)
|
| 139 |
+
policy = state.get("platform_policy", "moderate")
|
| 140 |
+
|
| 141 |
+
return (
|
| 142 |
+
f"Content to moderate:\n"
|
| 143 |
+
f"Text: {state['text']}\n"
|
| 144 |
+
f"Audio transcript: {audio}\n"
|
| 145 |
+
f"Visual tags: {vtags_str}\n"
|
| 146 |
+
f"Previous violations by this account: {flags}\n"
|
| 147 |
+
f"Platform policy: {policy}\n"
|
| 148 |
+
f"Tier: {tier}\n\n"
|
| 149 |
+
f"Make your moderation decision."
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# ββ LLM action getter βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 154 |
+
|
| 155 |
+
def get_llm_action(
|
| 156 |
+
client: OpenAI,
|
| 157 |
+
state: Dict,
|
| 158 |
+
tier: str,
|
| 159 |
+
) -> tuple[Dict, Optional[str]]:
|
| 160 |
+
"""
|
| 161 |
+
Call the LLM and parse its JSON response into an action dict.
|
| 162 |
+
|
| 163 |
+
Returns
|
| 164 |
+
-------
|
| 165 |
+
action : dict β parsed action (or FALLBACK_ACTION on failure)
|
| 166 |
+
error : str | None β error message if something went wrong, else None
|
| 167 |
+
"""
|
| 168 |
+
user_content = build_prompt(state, tier)
|
| 169 |
+
try:
|
| 170 |
+
response = client.chat.completions.create(
|
| 171 |
+
model=MODEL_NAME,
|
| 172 |
+
messages=[
|
| 173 |
+
{"role": "system", "content": _SYSTEM_PROMPT},
|
| 174 |
+
{"role": "user", "content": user_content},
|
| 175 |
+
],
|
| 176 |
+
temperature=0.1,
|
| 177 |
+
max_tokens=150,
|
| 178 |
+
)
|
| 179 |
+
raw = response.choices[0].message.content or ""
|
| 180 |
+
raw = raw.strip()
|
| 181 |
+
|
| 182 |
+
# Strip markdown fences if the model returns them despite instructions
|
| 183 |
+
if raw.startswith("```"):
|
| 184 |
+
lines = raw.splitlines()
|
| 185 |
+
# Remove opening fence (```json or ```)
|
| 186 |
+
lines = lines[1:] if lines else lines
|
| 187 |
+
# Remove closing fence
|
| 188 |
+
if lines and lines[-1].strip() == "```":
|
| 189 |
+
lines = lines[:-1]
|
| 190 |
+
raw = "\n".join(lines).strip()
|
| 191 |
+
|
| 192 |
+
parsed = json.loads(raw)
|
| 193 |
+
|
| 194 |
+
# Validate required keys; fall back silently if malformed
|
| 195 |
+
label = str(parsed.get("label", "")).strip().lower()
|
| 196 |
+
action = str(parsed.get("action", "")).strip().lower()
|
| 197 |
+
|
| 198 |
+
valid_labels = {"safe", "toxic", "spam", "misleading"}
|
| 199 |
+
valid_actions = {"allow", "warn", "remove", "shadowban", "escalate"}
|
| 200 |
+
|
| 201 |
+
if label not in valid_labels:
|
| 202 |
+
label = FALLBACK_ACTION["label"]
|
| 203 |
+
if action not in valid_actions:
|
| 204 |
+
action = FALLBACK_ACTION["action"]
|
| 205 |
+
|
| 206 |
+
result: Dict = {"label": label, "action": action}
|
| 207 |
+
|
| 208 |
+
# Include severity for hard tier (or if the model provided it)
|
| 209 |
+
if "severity" in parsed:
|
| 210 |
+
try:
|
| 211 |
+
sev = int(parsed["severity"])
|
| 212 |
+
sev = max(1, min(5, sev))
|
| 213 |
+
result["severity"] = sev
|
| 214 |
+
except (ValueError, TypeError):
|
| 215 |
+
pass
|
| 216 |
+
|
| 217 |
+
# Include rationale if provided (not scored, but useful for logging)
|
| 218 |
+
if "rationale" in parsed and isinstance(parsed["rationale"], str):
|
| 219 |
+
result["rationale"] = parsed["rationale"][:500] # cap length
|
| 220 |
+
|
| 221 |
+
return result, None
|
| 222 |
+
|
| 223 |
+
except Exception as exc: # noqa: BLE001
|
| 224 |
+
# Return fallback and surface the error message in the [STEP] log
|
| 225 |
+
return dict(FALLBACK_ACTION), str(exc)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# ββ Task runner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 229 |
+
|
| 230 |
+
def run_task(
|
| 231 |
+
client: OpenAI,
|
| 232 |
+
env: ContentModerationEnv,
|
| 233 |
+
task_name: str,
|
| 234 |
+
scenario_ids: List[str],
|
| 235 |
+
tier: str,
|
| 236 |
+
) -> None:
|
| 237 |
+
"""
|
| 238 |
+
Run one complete task episode and emit [START] / [STEP]* / [END] lines.
|
| 239 |
+
|
| 240 |
+
Each scenario is executed in single-step mode (scenario_id=...) for
|
| 241 |
+
reproducibility. The while-not-done loop handles the queue episode
|
| 242 |
+
structure, where done=False signals more posts remain in the episode.
|
| 243 |
+
|
| 244 |
+
Parameters
|
| 245 |
+
----------
|
| 246 |
+
client : OpenAI client instance
|
| 247 |
+
env : initialised ContentModerationEnv
|
| 248 |
+
task_name : name string used in log lines
|
| 249 |
+
scenario_ids : list of scenario IDs to include in this task
|
| 250 |
+
tier : "easy" | "medium" | "hard"
|
| 251 |
+
"""
|
| 252 |
+
rewards: List[float] = []
|
| 253 |
+
steps_taken = 0
|
| 254 |
+
success = False
|
| 255 |
+
|
| 256 |
+
log_start(task_name, MODEL_NAME)
|
| 257 |
+
|
| 258 |
+
try:
|
| 259 |
+
for sid in scenario_ids:
|
| 260 |
+
# Single-step mode per scenario (reproducible, deterministic order)
|
| 261 |
+
try:
|
| 262 |
+
state = env.reset(scenario_id=sid)
|
| 263 |
+
except Exception as exc: # noqa: BLE001
|
| 264 |
+
# Can't even reset β log a zero-reward step and move on
|
| 265 |
+
steps_taken += 1
|
| 266 |
+
rewards.append(0.0)
|
| 267 |
+
log_step(steps_taken, dict(FALLBACK_ACTION), 0.0, True,
|
| 268 |
+
f"reset_error:{exc}")
|
| 269 |
+
continue
|
| 270 |
+
|
| 271 |
+
done = False
|
| 272 |
+
while not done:
|
| 273 |
+
step_error: Optional[str] = None
|
| 274 |
+
|
| 275 |
+
# Get LLM decision (returns tuple: action, error_or_None)
|
| 276 |
+
action, llm_error = get_llm_action(client, state, tier)
|
| 277 |
+
if llm_error:
|
| 278 |
+
step_error = f"llm_error:{llm_error}"
|
| 279 |
+
|
| 280 |
+
# Submit to environment
|
| 281 |
+
reward = 0.0
|
| 282 |
+
try:
|
| 283 |
+
result = env.step(action)
|
| 284 |
+
reward = result["reward"]
|
| 285 |
+
done = result["done"]
|
| 286 |
+
# Advance state if episode continues
|
| 287 |
+
if not done:
|
| 288 |
+
state = result["state"]
|
| 289 |
+
except Exception as exc: # noqa: BLE001
|
| 290 |
+
done = True # treat step failure as terminal
|
| 291 |
+
step_error = (step_error or "") + f" step_error:{exc}"
|
| 292 |
+
|
| 293 |
+
steps_taken += 1
|
| 294 |
+
rewards.append(reward)
|
| 295 |
+
log_step(steps_taken, action, reward, done, step_error)
|
| 296 |
+
|
| 297 |
+
mean_reward = sum(rewards) / len(rewards) if rewards else 0.0
|
| 298 |
+
success = mean_reward >= 0.1
|
| 299 |
+
|
| 300 |
+
except Exception as exc: # noqa: BLE001
|
| 301 |
+
print(f"[DEBUG] Task fatal error: {exc}", flush=True)
|
| 302 |
+
|
| 303 |
+
finally:
|
| 304 |
+
log_end(success, steps_taken, rewards)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 308 |
+
|
| 309 |
+
def main() -> None:
|
| 310 |
+
"""Entry point β run all 3 task episodes."""
|
| 311 |
+
if not API_KEY:
|
| 312 |
+
# Still emit output so the format check passes; auth will fail at LLM call
|
| 313 |
+
sys.stderr.write(
|
| 314 |
+
"WARNING: No API key found. Set HF_TOKEN or OPENAI_API_KEY.\n"
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY or "no-key")
|
| 318 |
+
env = ContentModerationEnv(str(SCENARIOS_PATH), seed=42)
|
| 319 |
+
|
| 320 |
+
for task in TASKS:
|
| 321 |
+
run_task(
|
| 322 |
+
client=client,
|
| 323 |
+
env=env,
|
| 324 |
+
task_name=task["name"],
|
| 325 |
+
scenario_ids=task["scenario_ids"],
|
| 326 |
+
tier=task["tier"],
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
if __name__ == "__main__":
|
| 331 |
+
main()
|
moderation_benchmark.json
CHANGED
|
@@ -385,7 +385,10 @@
|
|
| 385 |
"state": {
|
| 386 |
"text": "I'm telling you, the election results were definitely swapped at the last minute by a shadow group.",
|
| 387 |
"audio_transcript": "Yeah, and I have the proof right here in this blurry photo.",
|
| 388 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 389 |
"previous_flags": 1,
|
| 390 |
"platform_policy": "strict"
|
| 391 |
},
|
|
@@ -442,7 +445,10 @@
|
|
| 442 |
"state": {
|
| 443 |
"text": "Everyone should check out this new dietary supplement, it literally cures aging.",
|
| 444 |
"audio_transcript": "I've been taking it for two days and I feel 20 years younger already.",
|
| 445 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 446 |
"previous_flags": 0,
|
| 447 |
"platform_policy": "strict"
|
| 448 |
},
|
|
@@ -480,7 +486,10 @@
|
|
| 480 |
"state": {
|
| 481 |
"text": "Check out this leaked video of the upcoming game! [LINK]",
|
| 482 |
"audio_transcript": null,
|
| 483 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 484 |
"previous_flags": 0,
|
| 485 |
"platform_policy": "strict"
|
| 486 |
},
|
|
@@ -537,7 +546,10 @@
|
|
| 537 |
"state": {
|
| 538 |
"text": "Hey check out my new stream! I'm playing some rare classic games today.",
|
| 539 |
"audio_transcript": "Join me as I beat this world record live!",
|
| 540 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 541 |
"previous_flags": 3,
|
| 542 |
"platform_policy": "strict"
|
| 543 |
},
|
|
@@ -556,7 +568,10 @@
|
|
| 556 |
"state": {
|
| 557 |
"text": "Did you hear that the local water supply has high levels of mercury? Be careful!",
|
| 558 |
"audio_transcript": "My neighbor's dog got sick from drinking tap water yesterday.",
|
| 559 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 560 |
"previous_flags": 0,
|
| 561 |
"platform_policy": "strict"
|
| 562 |
},
|
|
@@ -613,7 +628,10 @@
|
|
| 613 |
"state": {
|
| 614 |
"text": "Don't believe the mainstream media about the virus, it's just a hoax to control us.",
|
| 615 |
"audio_transcript": "It's all a conspiracy, stay safe by ignoring the experts.",
|
| 616 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 617 |
"previous_flags": 1,
|
| 618 |
"platform_policy": "strict"
|
| 619 |
},
|
|
@@ -670,7 +688,10 @@
|
|
| 670 |
"state": {
|
| 671 |
"text": "Hey look at my new profile picture, I look so cool!",
|
| 672 |
"audio_transcript": null,
|
| 673 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 674 |
"previous_flags": 0,
|
| 675 |
"platform_policy": "moderate"
|
| 676 |
},
|
|
@@ -689,7 +710,10 @@
|
|
| 689 |
"state": {
|
| 690 |
"text": "I'm selling these amazing weight loss pills, DM for details!",
|
| 691 |
"audio_transcript": "I lost 15 pounds in a week without dieting!",
|
| 692 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 693 |
"previous_flags": 0,
|
| 694 |
"platform_policy": "strict"
|
| 695 |
},
|
|
@@ -708,7 +732,10 @@
|
|
| 708 |
"state": {
|
| 709 |
"text": "I think we need a revolution to reset the system and get things back on track.",
|
| 710 |
"audio_transcript": "Things have gone too far and peaceful methods aren't working.",
|
| 711 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 712 |
"previous_flags": 0,
|
| 713 |
"platform_policy": "moderate"
|
| 714 |
},
|
|
@@ -727,7 +754,10 @@
|
|
| 727 |
"state": {
|
| 728 |
"text": "This person is a convicted criminal and should be harassed until they leave town.",
|
| 729 |
"audio_transcript": null,
|
| 730 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 731 |
"previous_flags": 0,
|
| 732 |
"platform_policy": "strict"
|
| 733 |
},
|
|
@@ -746,7 +776,9 @@
|
|
| 746 |
"state": {
|
| 747 |
"text": "My cat is the cutest in the world and I will fight anyone who says otherwise.",
|
| 748 |
"audio_transcript": null,
|
| 749 |
-
"visual_tags": [
|
|
|
|
|
|
|
| 750 |
"previous_flags": 0,
|
| 751 |
"platform_policy": "lenient"
|
| 752 |
},
|
|
@@ -765,7 +797,11 @@
|
|
| 765 |
"state": {
|
| 766 |
"text": "We're cleaning house tonight. If you're on the list, you know what's coming.",
|
| 767 |
"audio_transcript": "Don't bother calling the cops, they're on our side. Just be ready for the knock.",
|
| 768 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
"previous_flags": 5,
|
| 770 |
"platform_policy": "strict"
|
| 771 |
},
|
|
@@ -787,7 +823,10 @@
|
|
| 787 |
"state": {
|
| 788 |
"text": "I'm just saying that some groups are biologically predisposed to be less intelligent.",
|
| 789 |
"audio_transcript": "It's just science, we can't ignore the data just because it's uncomfortable.",
|
| 790 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 791 |
"previous_flags": 1,
|
| 792 |
"platform_policy": "moderate"
|
| 793 |
},
|
|
@@ -809,7 +848,10 @@
|
|
| 809 |
"state": {
|
| 810 |
"text": "Looking for fellow enthusiasts for this rare historical collection.",
|
| 811 |
"audio_transcript": "It's getting harder to find authentic pieces these days without being tracked.",
|
| 812 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 813 |
"previous_flags": 2,
|
| 814 |
"platform_policy": "strict"
|
| 815 |
},
|
|
@@ -831,7 +873,10 @@
|
|
| 831 |
"state": {
|
| 832 |
"text": "The local clinic is handing out tracking chips disguised as vaccines.",
|
| 833 |
"audio_transcript": "My cousin worked there and saw the crates coming in from the government. Stay away!",
|
| 834 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 835 |
"previous_flags": 3,
|
| 836 |
"platform_policy": "strict"
|
| 837 |
},
|
|
@@ -853,7 +898,11 @@
|
|
| 853 |
"state": {
|
| 854 |
"text": "I'm done with this. Everyone I loved is gone. Goodbye.",
|
| 855 |
"audio_transcript": "Seriously, don't try to stop me this time. I have the pills right here.",
|
| 856 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
"previous_flags": 0,
|
| 858 |
"platform_policy": "strict"
|
| 859 |
},
|
|
@@ -875,7 +924,10 @@
|
|
| 875 |
"state": {
|
| 876 |
"text": "Check out these new 'energy supplements' for kids! Keeps them focused all day.",
|
| 877 |
"audio_transcript": "It's basically legal speed for second graders, they love the taste too.",
|
| 878 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 879 |
"previous_flags": 0,
|
| 880 |
"platform_policy": "strict"
|
| 881 |
},
|
|
@@ -897,7 +949,10 @@
|
|
| 897 |
"state": {
|
| 898 |
"text": "Here's the address of that journalist who keeps lying about us: 123 Pine St. See you there.",
|
| 899 |
"audio_transcript": "We need to make sure they never write another word. Bring whatever you need.",
|
| 900 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 901 |
"previous_flags": 1,
|
| 902 |
"platform_policy": "strict"
|
| 903 |
},
|
|
@@ -919,7 +974,10 @@
|
|
| 919 |
"state": {
|
| 920 |
"text": "The latest study proves that people with this genetic marker are more prone to violence.",
|
| 921 |
"audio_transcript": "It's just objective data. We should probably keep an eye on these neighborhoods.",
|
| 922 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 923 |
"previous_flags": 2,
|
| 924 |
"platform_policy": "moderate"
|
| 925 |
},
|
|
@@ -941,7 +999,10 @@
|
|
| 941 |
"state": {
|
| 942 |
"text": "I'm just asking questions. Why did the museum suddenly close exactly when the report was released?",
|
| 943 |
"audio_transcript": "Maybe it's because they're hiding the evidence of the hidden tunnels.",
|
| 944 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 945 |
"previous_flags": 4,
|
| 946 |
"platform_policy": "moderate"
|
| 947 |
},
|
|
@@ -963,7 +1024,10 @@
|
|
| 963 |
"state": {
|
| 964 |
"text": "Someone really should take care of this problem before I have to do it myself.",
|
| 965 |
"audio_transcript": "Next time I see them, they're going to regret ever coming to this neighborhood.",
|
| 966 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 967 |
"previous_flags": 0,
|
| 968 |
"platform_policy": "strict"
|
| 969 |
},
|
|
@@ -985,7 +1049,10 @@
|
|
| 985 |
"state": {
|
| 986 |
"text": "If you follow this link, you'll see the truth the government is trying to bury.",
|
| 987 |
"audio_transcript": "Use a VPN, they're tracking everyone who visits this page. The report is 100% real.",
|
| 988 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 989 |
"previous_flags": 5,
|
| 990 |
"platform_policy": "strict"
|
| 991 |
},
|
|
@@ -1007,7 +1074,10 @@
|
|
| 1007 |
"state": {
|
| 1008 |
"text": "Everyone buy this stock now! I have inside info it's going to 100x tomorrow!",
|
| 1009 |
"audio_transcript": "Put your entire savings in, you won't regret it. Trust the process.",
|
| 1010 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1011 |
"previous_flags": 1,
|
| 1012 |
"platform_policy": "strict"
|
| 1013 |
},
|
|
@@ -1029,7 +1099,10 @@
|
|
| 1029 |
"state": {
|
| 1030 |
"text": "I'm not saying it didn't happen, I'm just saying the numbers don't add up.",
|
| 1031 |
"audio_transcript": "6 million? Based on the records, it seems much more likely to be 100k maximum.",
|
| 1032 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1033 |
"previous_flags": 0,
|
| 1034 |
"platform_policy": "strict"
|
| 1035 |
},
|
|
@@ -1051,7 +1124,10 @@
|
|
| 1051 |
"state": {
|
| 1052 |
"text": "They're coming for your kids, and they're using the school lunch program to do it.",
|
| 1053 |
"audio_transcript": "It's a biological attack on our future. We have to shut down all schools now.",
|
| 1054 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1055 |
"previous_flags": 2,
|
| 1056 |
"platform_policy": "strict"
|
| 1057 |
},
|
|
@@ -1095,7 +1171,10 @@
|
|
| 1095 |
"state": {
|
| 1096 |
"text": "Look at these 'volunteers' at the polling station. Probably illegals voting.",
|
| 1097 |
"audio_transcript": "I'm going down there with my group to challenge every single one of them.",
|
| 1098 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1099 |
"previous_flags": 1,
|
| 1100 |
"platform_policy": "strict"
|
| 1101 |
},
|
|
@@ -1117,7 +1196,10 @@
|
|
| 1117 |
"state": {
|
| 1118 |
"text": "This app is great, but did you know you can unlock premium features by entering this code?",
|
| 1119 |
"audio_transcript": "Just follow the prompts and it'll give you full access for free. Totally safe.",
|
| 1120 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1121 |
"previous_flags": 3,
|
| 1122 |
"platform_policy": "moderate"
|
| 1123 |
},
|
|
@@ -1139,7 +1221,10 @@
|
|
| 1139 |
"state": {
|
| 1140 |
"text": "Why do these people keep coming here and taking our resources? We should send them back.",
|
| 1141 |
"audio_transcript": "They're like an infestation. We need to cleanse this town.",
|
| 1142 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1143 |
"previous_flags": 0,
|
| 1144 |
"platform_policy": "strict"
|
| 1145 |
},
|
|
@@ -1161,7 +1246,10 @@
|
|
| 1161 |
"state": {
|
| 1162 |
"text": "The earth is actually flat and the ice wall is guarded by armed penguins.",
|
| 1163 |
"audio_transcript": "No seriously, my friend saw a penguin with a rifle last year.",
|
| 1164 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1165 |
"previous_flags": 10,
|
| 1166 |
"platform_policy": "lenient"
|
| 1167 |
},
|
|
@@ -1183,7 +1271,10 @@
|
|
| 1183 |
"state": {
|
| 1184 |
"text": "Look at what this public figure just did! Absolutely disgusting! [LINK]",
|
| 1185 |
"audio_transcript": "They're part of the ritual sacrifices at that pizza place. Spread the word!",
|
| 1186 |
-
"visual_tags": [
|
|
|
|
|
|
|
|
|
|
| 1187 |
"previous_flags": 2,
|
| 1188 |
"platform_policy": "strict"
|
| 1189 |
},
|
|
@@ -1198,5 +1289,345 @@
|
|
| 1198 |
"action_correct": 0.4,
|
| 1199 |
"severity_within_1": 0.2
|
| 1200 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
}
|
| 1202 |
-
]
|
|
|
|
| 385 |
"state": {
|
| 386 |
"text": "I'm telling you, the election results were definitely swapped at the last minute by a shadow group.",
|
| 387 |
"audio_transcript": "Yeah, and I have the proof right here in this blurry photo.",
|
| 388 |
+
"visual_tags": [
|
| 389 |
+
"political_graphic",
|
| 390 |
+
"blurry_document"
|
| 391 |
+
],
|
| 392 |
"previous_flags": 1,
|
| 393 |
"platform_policy": "strict"
|
| 394 |
},
|
|
|
|
| 445 |
"state": {
|
| 446 |
"text": "Everyone should check out this new dietary supplement, it literally cures aging.",
|
| 447 |
"audio_transcript": "I've been taking it for two days and I feel 20 years younger already.",
|
| 448 |
+
"visual_tags": [
|
| 449 |
+
"supplement_bottle",
|
| 450 |
+
"health_claims"
|
| 451 |
+
],
|
| 452 |
"previous_flags": 0,
|
| 453 |
"platform_policy": "strict"
|
| 454 |
},
|
|
|
|
| 486 |
"state": {
|
| 487 |
"text": "Check out this leaked video of the upcoming game! [LINK]",
|
| 488 |
"audio_transcript": null,
|
| 489 |
+
"visual_tags": [
|
| 490 |
+
"copyrighted_material",
|
| 491 |
+
"game_footage"
|
| 492 |
+
],
|
| 493 |
"previous_flags": 0,
|
| 494 |
"platform_policy": "strict"
|
| 495 |
},
|
|
|
|
| 546 |
"state": {
|
| 547 |
"text": "Hey check out my new stream! I'm playing some rare classic games today.",
|
| 548 |
"audio_transcript": "Join me as I beat this world record live!",
|
| 549 |
+
"visual_tags": [
|
| 550 |
+
"video_game",
|
| 551 |
+
"gaming_setup"
|
| 552 |
+
],
|
| 553 |
"previous_flags": 3,
|
| 554 |
"platform_policy": "strict"
|
| 555 |
},
|
|
|
|
| 568 |
"state": {
|
| 569 |
"text": "Did you hear that the local water supply has high levels of mercury? Be careful!",
|
| 570 |
"audio_transcript": "My neighbor's dog got sick from drinking tap water yesterday.",
|
| 571 |
+
"visual_tags": [
|
| 572 |
+
"faucet",
|
| 573 |
+
"water_glass"
|
| 574 |
+
],
|
| 575 |
"previous_flags": 0,
|
| 576 |
"platform_policy": "strict"
|
| 577 |
},
|
|
|
|
| 628 |
"state": {
|
| 629 |
"text": "Don't believe the mainstream media about the virus, it's just a hoax to control us.",
|
| 630 |
"audio_transcript": "It's all a conspiracy, stay safe by ignoring the experts.",
|
| 631 |
+
"visual_tags": [
|
| 632 |
+
"news_broadcast",
|
| 633 |
+
"overlay_text"
|
| 634 |
+
],
|
| 635 |
"previous_flags": 1,
|
| 636 |
"platform_policy": "strict"
|
| 637 |
},
|
|
|
|
| 688 |
"state": {
|
| 689 |
"text": "Hey look at my new profile picture, I look so cool!",
|
| 690 |
"audio_transcript": null,
|
| 691 |
+
"visual_tags": [
|
| 692 |
+
"personat_photo",
|
| 693 |
+
"landscape"
|
| 694 |
+
],
|
| 695 |
"previous_flags": 0,
|
| 696 |
"platform_policy": "moderate"
|
| 697 |
},
|
|
|
|
| 710 |
"state": {
|
| 711 |
"text": "I'm selling these amazing weight loss pills, DM for details!",
|
| 712 |
"audio_transcript": "I lost 15 pounds in a week without dieting!",
|
| 713 |
+
"visual_tags": [
|
| 714 |
+
"unlabeled_pills",
|
| 715 |
+
"before_after_photos"
|
| 716 |
+
],
|
| 717 |
"previous_flags": 0,
|
| 718 |
"platform_policy": "strict"
|
| 719 |
},
|
|
|
|
| 732 |
"state": {
|
| 733 |
"text": "I think we need a revolution to reset the system and get things back on track.",
|
| 734 |
"audio_transcript": "Things have gone too far and peaceful methods aren't working.",
|
| 735 |
+
"visual_tags": [
|
| 736 |
+
"political_protest",
|
| 737 |
+
"flag"
|
| 738 |
+
],
|
| 739 |
"previous_flags": 0,
|
| 740 |
"platform_policy": "moderate"
|
| 741 |
},
|
|
|
|
| 754 |
"state": {
|
| 755 |
"text": "This person is a convicted criminal and should be harassed until they leave town.",
|
| 756 |
"audio_transcript": null,
|
| 757 |
+
"visual_tags": [
|
| 758 |
+
"mugshot",
|
| 759 |
+
"court_records"
|
| 760 |
+
],
|
| 761 |
"previous_flags": 0,
|
| 762 |
"platform_policy": "strict"
|
| 763 |
},
|
|
|
|
| 776 |
"state": {
|
| 777 |
"text": "My cat is the cutest in the world and I will fight anyone who says otherwise.",
|
| 778 |
"audio_transcript": null,
|
| 779 |
+
"visual_tags": [
|
| 780 |
+
"cute_cat"
|
| 781 |
+
],
|
| 782 |
"previous_flags": 0,
|
| 783 |
"platform_policy": "lenient"
|
| 784 |
},
|
|
|
|
| 797 |
"state": {
|
| 798 |
"text": "We're cleaning house tonight. If you're on the list, you know what's coming.",
|
| 799 |
"audio_transcript": "Don't bother calling the cops, they're on our side. Just be ready for the knock.",
|
| 800 |
+
"visual_tags": [
|
| 801 |
+
"baseboard_mask",
|
| 802 |
+
"clenched_fist",
|
| 803 |
+
"street_sign"
|
| 804 |
+
],
|
| 805 |
"previous_flags": 5,
|
| 806 |
"platform_policy": "strict"
|
| 807 |
},
|
|
|
|
| 823 |
"state": {
|
| 824 |
"text": "I'm just saying that some groups are biologically predisposed to be less intelligent.",
|
| 825 |
"audio_transcript": "It's just science, we can't ignore the data just because it's uncomfortable.",
|
| 826 |
+
"visual_tags": [
|
| 827 |
+
"pseudo-science_chart",
|
| 828 |
+
"phrenology_diagram"
|
| 829 |
+
],
|
| 830 |
"previous_flags": 1,
|
| 831 |
"platform_policy": "moderate"
|
| 832 |
},
|
|
|
|
| 848 |
"state": {
|
| 849 |
"text": "Looking for fellow enthusiasts for this rare historical collection.",
|
| 850 |
"audio_transcript": "It's getting harder to find authentic pieces these days without being tracked.",
|
| 851 |
+
"visual_tags": [
|
| 852 |
+
"prohibited_historical_symbols",
|
| 853 |
+
"military_memorabilia"
|
| 854 |
+
],
|
| 855 |
"previous_flags": 2,
|
| 856 |
"platform_policy": "strict"
|
| 857 |
},
|
|
|
|
| 873 |
"state": {
|
| 874 |
"text": "The local clinic is handing out tracking chips disguised as vaccines.",
|
| 875 |
"audio_transcript": "My cousin worked there and saw the crates coming in from the government. Stay away!",
|
| 876 |
+
"visual_tags": [
|
| 877 |
+
"clinic_building",
|
| 878 |
+
"government_logo"
|
| 879 |
+
],
|
| 880 |
"previous_flags": 3,
|
| 881 |
"platform_policy": "strict"
|
| 882 |
},
|
|
|
|
| 898 |
"state": {
|
| 899 |
"text": "I'm done with this. Everyone I loved is gone. Goodbye.",
|
| 900 |
"audio_transcript": "Seriously, don't try to stop me this time. I have the pills right here.",
|
| 901 |
+
"visual_tags": [
|
| 902 |
+
"pill_bottle",
|
| 903 |
+
"room_darkness",
|
| 904 |
+
"farewell_note"
|
| 905 |
+
],
|
| 906 |
"previous_flags": 0,
|
| 907 |
"platform_policy": "strict"
|
| 908 |
},
|
|
|
|
| 924 |
"state": {
|
| 925 |
"text": "Check out these new 'energy supplements' for kids! Keeps them focused all day.",
|
| 926 |
"audio_transcript": "It's basically legal speed for second graders, they love the taste too.",
|
| 927 |
+
"visual_tags": [
|
| 928 |
+
"child_holding_bottle",
|
| 929 |
+
"neon_candy_look"
|
| 930 |
+
],
|
| 931 |
"previous_flags": 0,
|
| 932 |
"platform_policy": "strict"
|
| 933 |
},
|
|
|
|
| 949 |
"state": {
|
| 950 |
"text": "Here's the address of that journalist who keeps lying about us: 123 Pine St. See you there.",
|
| 951 |
"audio_transcript": "We need to make sure they never write another word. Bring whatever you need.",
|
| 952 |
+
"visual_tags": [
|
| 953 |
+
"residential_building",
|
| 954 |
+
"location_pin"
|
| 955 |
+
],
|
| 956 |
"previous_flags": 1,
|
| 957 |
"platform_policy": "strict"
|
| 958 |
},
|
|
|
|
| 974 |
"state": {
|
| 975 |
"text": "The latest study proves that people with this genetic marker are more prone to violence.",
|
| 976 |
"audio_transcript": "It's just objective data. We should probably keep an eye on these neighborhoods.",
|
| 977 |
+
"visual_tags": [
|
| 978 |
+
"scientific_graph",
|
| 979 |
+
"neighborhood_map"
|
| 980 |
+
],
|
| 981 |
"previous_flags": 2,
|
| 982 |
"platform_policy": "moderate"
|
| 983 |
},
|
|
|
|
| 999 |
"state": {
|
| 1000 |
"text": "I'm just asking questions. Why did the museum suddenly close exactly when the report was released?",
|
| 1001 |
"audio_transcript": "Maybe it's because they're hiding the evidence of the hidden tunnels.",
|
| 1002 |
+
"visual_tags": [
|
| 1003 |
+
"museum_entrance_closed",
|
| 1004 |
+
"document_screenshot"
|
| 1005 |
+
],
|
| 1006 |
"previous_flags": 4,
|
| 1007 |
"platform_policy": "moderate"
|
| 1008 |
},
|
|
|
|
| 1024 |
"state": {
|
| 1025 |
"text": "Someone really should take care of this problem before I have to do it myself.",
|
| 1026 |
"audio_transcript": "Next time I see them, they're going to regret ever coming to this neighborhood.",
|
| 1027 |
+
"visual_tags": [
|
| 1028 |
+
"person_pacing",
|
| 1029 |
+
"baseball_bat"
|
| 1030 |
+
],
|
| 1031 |
"previous_flags": 0,
|
| 1032 |
"platform_policy": "strict"
|
| 1033 |
},
|
|
|
|
| 1049 |
"state": {
|
| 1050 |
"text": "If you follow this link, you'll see the truth the government is trying to bury.",
|
| 1051 |
"audio_transcript": "Use a VPN, they're tracking everyone who visits this page. The report is 100% real.",
|
| 1052 |
+
"visual_tags": [
|
| 1053 |
+
"redact_document",
|
| 1054 |
+
"eye_icon"
|
| 1055 |
+
],
|
| 1056 |
"previous_flags": 5,
|
| 1057 |
"platform_policy": "strict"
|
| 1058 |
},
|
|
|
|
| 1074 |
"state": {
|
| 1075 |
"text": "Everyone buy this stock now! I have inside info it's going to 100x tomorrow!",
|
| 1076 |
"audio_transcript": "Put your entire savings in, you won't regret it. Trust the process.",
|
| 1077 |
+
"visual_tags": [
|
| 1078 |
+
"stock_chart",
|
| 1079 |
+
"money_bag"
|
| 1080 |
+
],
|
| 1081 |
"previous_flags": 1,
|
| 1082 |
"platform_policy": "strict"
|
| 1083 |
},
|
|
|
|
| 1099 |
"state": {
|
| 1100 |
"text": "I'm not saying it didn't happen, I'm just saying the numbers don't add up.",
|
| 1101 |
"audio_transcript": "6 million? Based on the records, it seems much more likely to be 100k maximum.",
|
| 1102 |
+
"visual_tags": [
|
| 1103 |
+
"archival_photos",
|
| 1104 |
+
"revisionist_text"
|
| 1105 |
+
],
|
| 1106 |
"previous_flags": 0,
|
| 1107 |
"platform_policy": "strict"
|
| 1108 |
},
|
|
|
|
| 1124 |
"state": {
|
| 1125 |
"text": "They're coming for your kids, and they're using the school lunch program to do it.",
|
| 1126 |
"audio_transcript": "It's a biological attack on our future. We have to shut down all schools now.",
|
| 1127 |
+
"visual_tags": [
|
| 1128 |
+
"school_cafeteria",
|
| 1129 |
+
"biohazard_symbol"
|
| 1130 |
+
],
|
| 1131 |
"previous_flags": 2,
|
| 1132 |
"platform_policy": "strict"
|
| 1133 |
},
|
|
|
|
| 1171 |
"state": {
|
| 1172 |
"text": "Look at these 'volunteers' at the polling station. Probably illegals voting.",
|
| 1173 |
"audio_transcript": "I'm going down there with my group to challenge every single one of them.",
|
| 1174 |
+
"visual_tags": [
|
| 1175 |
+
"voter_lineup",
|
| 1176 |
+
"concealed_firearm_bulge"
|
| 1177 |
+
],
|
| 1178 |
"previous_flags": 1,
|
| 1179 |
"platform_policy": "strict"
|
| 1180 |
},
|
|
|
|
| 1196 |
"state": {
|
| 1197 |
"text": "This app is great, but did you know you can unlock premium features by entering this code?",
|
| 1198 |
"audio_transcript": "Just follow the prompts and it'll give you full access for free. Totally safe.",
|
| 1199 |
+
"visual_tags": [
|
| 1200 |
+
"phishing_page_screenshot",
|
| 1201 |
+
"input_box"
|
| 1202 |
+
],
|
| 1203 |
"previous_flags": 3,
|
| 1204 |
"platform_policy": "moderate"
|
| 1205 |
},
|
|
|
|
| 1221 |
"state": {
|
| 1222 |
"text": "Why do these people keep coming here and taking our resources? We should send them back.",
|
| 1223 |
"audio_transcript": "They're like an infestation. We need to cleanse this town.",
|
| 1224 |
+
"visual_tags": [
|
| 1225 |
+
"dehumanizing_imagery",
|
| 1226 |
+
"neighborhood_park"
|
| 1227 |
+
],
|
| 1228 |
"previous_flags": 0,
|
| 1229 |
"platform_policy": "strict"
|
| 1230 |
},
|
|
|
|
| 1246 |
"state": {
|
| 1247 |
"text": "The earth is actually flat and the ice wall is guarded by armed penguins.",
|
| 1248 |
"audio_transcript": "No seriously, my friend saw a penguin with a rifle last year.",
|
| 1249 |
+
"visual_tags": [
|
| 1250 |
+
"badly_photoshopped_penguin",
|
| 1251 |
+
"infinite_plane_diagram"
|
| 1252 |
+
],
|
| 1253 |
"previous_flags": 10,
|
| 1254 |
"platform_policy": "lenient"
|
| 1255 |
},
|
|
|
|
| 1271 |
"state": {
|
| 1272 |
"text": "Look at what this public figure just did! Absolutely disgusting! [LINK]",
|
| 1273 |
"audio_transcript": "They're part of the ritual sacrifices at that pizza place. Spread the word!",
|
| 1274 |
+
"visual_tags": [
|
| 1275 |
+
"celebrity_photo",
|
| 1276 |
+
"altered_background"
|
| 1277 |
+
],
|
| 1278 |
"previous_flags": 2,
|
| 1279 |
"platform_policy": "strict"
|
| 1280 |
},
|
|
|
|
| 1289 |
"action_correct": 0.4,
|
| 1290 |
"severity_within_1": 0.2
|
| 1291 |
}
|
| 1292 |
+
},
|
| 1293 |
+
{
|
| 1294 |
+
"id": "scen_hard_21",
|
| 1295 |
+
"tier": "hard",
|
| 1296 |
+
"state": {
|
| 1297 |
+
"text": "Oh sure, let's just give everyone guns, that'll definitely solve all our problems. What could possibly go wrong?",
|
| 1298 |
+
"audio_transcript": null,
|
| 1299 |
+
"visual_tags": [
|
| 1300 |
+
"political_content"
|
| 1301 |
+
],
|
| 1302 |
+
"previous_flags": 0,
|
| 1303 |
+
"platform_policy": "moderate"
|
| 1304 |
+
},
|
| 1305 |
+
"ground_truth": {
|
| 1306 |
+
"label": "misleading",
|
| 1307 |
+
"action": "warn",
|
| 1308 |
+
"severity": 2,
|
| 1309 |
+
"rationale": "Sarcastic political speech. Warn for framing, not toxic."
|
| 1310 |
+
},
|
| 1311 |
+
"score_rubric": {
|
| 1312 |
+
"label_correct": 0.4,
|
| 1313 |
+
"action_correct": 0.4,
|
| 1314 |
+
"severity_within_1": 0.2
|
| 1315 |
+
}
|
| 1316 |
+
},
|
| 1317 |
+
{
|
| 1318 |
+
"id": "scen_hard_22",
|
| 1319 |
+
"tier": "hard",
|
| 1320 |
+
"state": {
|
| 1321 |
+
"text": "Just sharing my thoughts on today's news. Very interesting developments!",
|
| 1322 |
+
"audio_transcript": null,
|
| 1323 |
+
"visual_tags": [
|
| 1324 |
+
"known_bot_network",
|
| 1325 |
+
"copied_profile_picture",
|
| 1326 |
+
"bulk_posting_pattern"
|
| 1327 |
+
],
|
| 1328 |
+
"previous_flags": 8,
|
| 1329 |
+
"platform_policy": "strict"
|
| 1330 |
+
},
|
| 1331 |
+
"ground_truth": {
|
| 1332 |
+
"label": "spam",
|
| 1333 |
+
"action": "shadowban",
|
| 1334 |
+
"severity": 4,
|
| 1335 |
+
"rationale": "Benign text but metadata signals coordinated inauthentic behavior."
|
| 1336 |
+
},
|
| 1337 |
+
"score_rubric": {
|
| 1338 |
+
"label_correct": 0.4,
|
| 1339 |
+
"action_correct": 0.4,
|
| 1340 |
+
"severity_within_1": 0.2
|
| 1341 |
+
}
|
| 1342 |
+
},
|
| 1343 |
+
{
|
| 1344 |
+
"id": "scen_hard_23",
|
| 1345 |
+
"tier": "hard",
|
| 1346 |
+
"state": {
|
| 1347 |
+
"text": "For acute overdose cases, administer 0.4-2mg naloxone IV. Repeat every 2-3 minutes if no response. Max 10mg before reassessing.",
|
| 1348 |
+
"audio_transcript": "Medical grand rounds recording, hospital setting",
|
| 1349 |
+
"visual_tags": [
|
| 1350 |
+
"medical_setting",
|
| 1351 |
+
"professional_attire",
|
| 1352 |
+
"hospital_environment"
|
| 1353 |
+
],
|
| 1354 |
+
"previous_flags": 0,
|
| 1355 |
+
"platform_policy": "moderate"
|
| 1356 |
+
},
|
| 1357 |
+
"ground_truth": {
|
| 1358 |
+
"label": "safe",
|
| 1359 |
+
"action": "allow",
|
| 1360 |
+
"severity": 1,
|
| 1361 |
+
"rationale": "Clinical medical discussion confirmed by audio and visual context."
|
| 1362 |
+
},
|
| 1363 |
+
"score_rubric": {
|
| 1364 |
+
"label_correct": 0.4,
|
| 1365 |
+
"action_correct": 0.4,
|
| 1366 |
+
"severity_within_1": 0.2
|
| 1367 |
+
}
|
| 1368 |
+
},
|
| 1369 |
+
{
|
| 1370 |
+
"id": "scen_hard_24",
|
| 1371 |
+
"tier": "hard",
|
| 1372 |
+
"state": {
|
| 1373 |
+
"text": "Just a reminder that this public figure's home address and daily schedule are publicly available on their website.",
|
| 1374 |
+
"audio_transcript": null,
|
| 1375 |
+
"visual_tags": [
|
| 1376 |
+
"doxxing_pattern",
|
| 1377 |
+
"personal_information"
|
| 1378 |
+
],
|
| 1379 |
+
"previous_flags": 3,
|
| 1380 |
+
"platform_policy": "strict"
|
| 1381 |
+
},
|
| 1382 |
+
"ground_truth": {
|
| 1383 |
+
"label": "toxic",
|
| 1384 |
+
"action": "escalate",
|
| 1385 |
+
"severity": 5,
|
| 1386 |
+
"rationale": "Doxxing pattern with prior violations. Facilitates harassment."
|
| 1387 |
+
},
|
| 1388 |
+
"score_rubric": {
|
| 1389 |
+
"label_correct": 0.4,
|
| 1390 |
+
"action_correct": 0.4,
|
| 1391 |
+
"severity_within_1": 0.2
|
| 1392 |
+
}
|
| 1393 |
+
},
|
| 1394 |
+
{
|
| 1395 |
+
"id": "scen_hard_25",
|
| 1396 |
+
"tier": "hard",
|
| 1397 |
+
"state": {
|
| 1398 |
+
"text": "These people are ruining everything. You know what needs to happen.",
|
| 1399 |
+
"audio_transcript": null,
|
| 1400 |
+
"visual_tags": [],
|
| 1401 |
+
"previous_flags": 12,
|
| 1402 |
+
"platform_policy": "strict"
|
| 1403 |
+
},
|
| 1404 |
+
"ground_truth": {
|
| 1405 |
+
"label": "toxic",
|
| 1406 |
+
"action": "escalate",
|
| 1407 |
+
"severity": 5,
|
| 1408 |
+
"rationale": "Implied threat. 12 prior violations makes this critical."
|
| 1409 |
+
},
|
| 1410 |
+
"score_rubric": {
|
| 1411 |
+
"label_correct": 0.4,
|
| 1412 |
+
"action_correct": 0.4,
|
| 1413 |
+
"severity_within_1": 0.2
|
| 1414 |
+
}
|
| 1415 |
+
},
|
| 1416 |
+
{
|
| 1417 |
+
"id": "scen_hard_26",
|
| 1418 |
+
"tier": "hard",
|
| 1419 |
+
"state": {
|
| 1420 |
+
"text": "BREAKING: Scientists discover eating tide pods cures all diseases. Big Pharma doesn't want you to know!",
|
| 1421 |
+
"audio_transcript": null,
|
| 1422 |
+
"visual_tags": [
|
| 1423 |
+
"satire_account_verified",
|
| 1424 |
+
"parody_label"
|
| 1425 |
+
],
|
| 1426 |
+
"previous_flags": 0,
|
| 1427 |
+
"platform_policy": "lenient"
|
| 1428 |
+
},
|
| 1429 |
+
"ground_truth": {
|
| 1430 |
+
"label": "misleading",
|
| 1431 |
+
"action": "warn",
|
| 1432 |
+
"severity": 2,
|
| 1433 |
+
"rationale": "Satire account with verified parody label on lenient platform."
|
| 1434 |
+
},
|
| 1435 |
+
"score_rubric": {
|
| 1436 |
+
"label_correct": 0.4,
|
| 1437 |
+
"action_correct": 0.4,
|
| 1438 |
+
"severity_within_1": 0.2
|
| 1439 |
+
}
|
| 1440 |
+
},
|
| 1441 |
+
{
|
| 1442 |
+
"id": "scen_hard_27",
|
| 1443 |
+
"tier": "hard",
|
| 1444 |
+
"state": {
|
| 1445 |
+
"text": "Estos inmigrantes estan destruyendo nuestro pais. Hay que hacer algo.",
|
| 1446 |
+
"audio_transcript": null,
|
| 1447 |
+
"visual_tags": [
|
| 1448 |
+
"translated_content",
|
| 1449 |
+
"nationalist_imagery"
|
| 1450 |
+
],
|
| 1451 |
+
"previous_flags": 2,
|
| 1452 |
+
"platform_policy": "strict"
|
| 1453 |
+
},
|
| 1454 |
+
"ground_truth": {
|
| 1455 |
+
"label": "toxic",
|
| 1456 |
+
"action": "remove",
|
| 1457 |
+
"severity": 4,
|
| 1458 |
+
"rationale": "Xenophobic content in Spanish: immigrants destroying country, something must be done."
|
| 1459 |
+
},
|
| 1460 |
+
"score_rubric": {
|
| 1461 |
+
"label_correct": 0.4,
|
| 1462 |
+
"action_correct": 0.4,
|
| 1463 |
+
"severity_within_1": 0.2
|
| 1464 |
+
}
|
| 1465 |
+
},
|
| 1466 |
+
{
|
| 1467 |
+
"id": "scen_hard_28",
|
| 1468 |
+
"tier": "hard",
|
| 1469 |
+
"state": {
|
| 1470 |
+
"text": "I need help. I can't take it anymore. Does anyone know what happens if you take a whole bottle of sleeping pills?",
|
| 1471 |
+
"audio_transcript": null,
|
| 1472 |
+
"visual_tags": [],
|
| 1473 |
+
"previous_flags": 0,
|
| 1474 |
+
"platform_policy": "strict"
|
| 1475 |
+
},
|
| 1476 |
+
"ground_truth": {
|
| 1477 |
+
"label": "toxic",
|
| 1478 |
+
"action": "escalate",
|
| 1479 |
+
"severity": 5,
|
| 1480 |
+
"rationale": "Crisis signal. Escalate to human review immediately."
|
| 1481 |
+
},
|
| 1482 |
+
"score_rubric": {
|
| 1483 |
+
"label_correct": 0.4,
|
| 1484 |
+
"action_correct": 0.4,
|
| 1485 |
+
"severity_within_1": 0.2
|
| 1486 |
+
}
|
| 1487 |
+
},
|
| 1488 |
+
{
|
| 1489 |
+
"id": "scen_hard_29",
|
| 1490 |
+
"tier": "hard",
|
| 1491 |
+
"state": {
|
| 1492 |
+
"text": "Honest review after 6 months: This product changed my life. Lost 40 pounds, skin cleared up. DM me for my full routine!",
|
| 1493 |
+
"audio_transcript": null,
|
| 1494 |
+
"visual_tags": [
|
| 1495 |
+
"before_after_image",
|
| 1496 |
+
"affiliate_link_detected"
|
| 1497 |
+
],
|
| 1498 |
+
"previous_flags": 0,
|
| 1499 |
+
"platform_policy": "moderate"
|
| 1500 |
+
},
|
| 1501 |
+
"ground_truth": {
|
| 1502 |
+
"label": "spam",
|
| 1503 |
+
"action": "remove",
|
| 1504 |
+
"severity": 3,
|
| 1505 |
+
"rationale": "Affiliate marketing disguised as personal story."
|
| 1506 |
+
},
|
| 1507 |
+
"score_rubric": {
|
| 1508 |
+
"label_correct": 0.4,
|
| 1509 |
+
"action_correct": 0.4,
|
| 1510 |
+
"severity_within_1": 0.2
|
| 1511 |
+
}
|
| 1512 |
+
},
|
| 1513 |
+
{
|
| 1514 |
+
"id": "scen_hard_30",
|
| 1515 |
+
"tier": "hard",
|
| 1516 |
+
"state": {
|
| 1517 |
+
"text": "I absolutely despise how this game is designed. The devs should be ashamed of themselves.",
|
| 1518 |
+
"audio_transcript": null,
|
| 1519 |
+
"visual_tags": [
|
| 1520 |
+
"gaming_community",
|
| 1521 |
+
"product_review"
|
| 1522 |
+
],
|
| 1523 |
+
"previous_flags": 1,
|
| 1524 |
+
"platform_policy": "lenient"
|
| 1525 |
+
},
|
| 1526 |
+
"ground_truth": {
|
| 1527 |
+
"label": "safe",
|
| 1528 |
+
"action": "allow",
|
| 1529 |
+
"severity": 1,
|
| 1530 |
+
"rationale": "Strong opinion on lenient platform. Valid product criticism."
|
| 1531 |
+
},
|
| 1532 |
+
"score_rubric": {
|
| 1533 |
+
"label_correct": 0.4,
|
| 1534 |
+
"action_correct": 0.4,
|
| 1535 |
+
"severity_within_1": 0.2
|
| 1536 |
+
}
|
| 1537 |
+
},
|
| 1538 |
+
{
|
| 1539 |
+
"id": "scen_easy_21",
|
| 1540 |
+
"tier": "easy",
|
| 1541 |
+
"state": {
|
| 1542 |
+
"text": "Buy cheap followers! 1000 followers for $5! DM NOW! Limited offer!",
|
| 1543 |
+
"audio_transcript": null,
|
| 1544 |
+
"visual_tags": [],
|
| 1545 |
+
"previous_flags": 4,
|
| 1546 |
+
"platform_policy": "moderate"
|
| 1547 |
+
},
|
| 1548 |
+
"ground_truth": {
|
| 1549 |
+
"label": "spam",
|
| 1550 |
+
"action": "shadowban"
|
| 1551 |
+
},
|
| 1552 |
+
"score_rubric": {
|
| 1553 |
+
"label_correct": 0.5,
|
| 1554 |
+
"action_correct": 0.5
|
| 1555 |
+
}
|
| 1556 |
+
},
|
| 1557 |
+
{
|
| 1558 |
+
"id": "scen_easy_22",
|
| 1559 |
+
"tier": "easy",
|
| 1560 |
+
"state": {
|
| 1561 |
+
"text": "Work from home and earn $500/day! No experience needed! Click my bio link!",
|
| 1562 |
+
"audio_transcript": null,
|
| 1563 |
+
"visual_tags": [],
|
| 1564 |
+
"previous_flags": 3,
|
| 1565 |
+
"platform_policy": "moderate"
|
| 1566 |
+
},
|
| 1567 |
+
"ground_truth": {
|
| 1568 |
+
"label": "spam",
|
| 1569 |
+
"action": "shadowban"
|
| 1570 |
+
},
|
| 1571 |
+
"score_rubric": {
|
| 1572 |
+
"label_correct": 0.5,
|
| 1573 |
+
"action_correct": 0.5
|
| 1574 |
+
}
|
| 1575 |
+
},
|
| 1576 |
+
{
|
| 1577 |
+
"id": "scen_easy_23",
|
| 1578 |
+
"tier": "easy",
|
| 1579 |
+
"state": {
|
| 1580 |
+
"text": "FREE crypto giveaway! Send 0.1 BTC get 1 BTC back! Official promotion!",
|
| 1581 |
+
"audio_transcript": null,
|
| 1582 |
+
"visual_tags": [],
|
| 1583 |
+
"previous_flags": 5,
|
| 1584 |
+
"platform_policy": "strict"
|
| 1585 |
+
},
|
| 1586 |
+
"ground_truth": {
|
| 1587 |
+
"label": "spam",
|
| 1588 |
+
"action": "shadowban"
|
| 1589 |
+
},
|
| 1590 |
+
"score_rubric": {
|
| 1591 |
+
"label_correct": 0.5,
|
| 1592 |
+
"action_correct": 0.5
|
| 1593 |
+
}
|
| 1594 |
+
},
|
| 1595 |
+
{
|
| 1596 |
+
"id": "scen_easy_24",
|
| 1597 |
+
"tier": "easy",
|
| 1598 |
+
"state": {
|
| 1599 |
+
"text": "Lose 30 pounds in 30 days with our miracle pill! Doctor approved! No diet needed!",
|
| 1600 |
+
"audio_transcript": null,
|
| 1601 |
+
"visual_tags": [],
|
| 1602 |
+
"previous_flags": 3,
|
| 1603 |
+
"platform_policy": "moderate"
|
| 1604 |
+
},
|
| 1605 |
+
"ground_truth": {
|
| 1606 |
+
"label": "spam",
|
| 1607 |
+
"action": "shadowban"
|
| 1608 |
+
},
|
| 1609 |
+
"score_rubric": {
|
| 1610 |
+
"label_correct": 0.5,
|
| 1611 |
+
"action_correct": 0.5
|
| 1612 |
+
}
|
| 1613 |
+
},
|
| 1614 |
+
{
|
| 1615 |
+
"id": "scen_easy_25",
|
| 1616 |
+
"tier": "easy",
|
| 1617 |
+
"state": {
|
| 1618 |
+
"text": "HOT SINGLES IN YOUR AREA! Click now! Real people waiting! Free tonight only!",
|
| 1619 |
+
"audio_transcript": null,
|
| 1620 |
+
"visual_tags": [],
|
| 1621 |
+
"previous_flags": 4,
|
| 1622 |
+
"platform_policy": "strict"
|
| 1623 |
+
},
|
| 1624 |
+
"ground_truth": {
|
| 1625 |
+
"label": "spam",
|
| 1626 |
+
"action": "shadowban"
|
| 1627 |
+
},
|
| 1628 |
+
"score_rubric": {
|
| 1629 |
+
"label_correct": 0.5,
|
| 1630 |
+
"action_correct": 0.5
|
| 1631 |
+
}
|
| 1632 |
}
|
| 1633 |
+
]
|
openenv.yaml
CHANGED
|
@@ -26,7 +26,7 @@ tasks:
|
|
| 26 |
- name: action_correct
|
| 27 |
weight: 0.5
|
| 28 |
description: Moderation action matches ground truth (allow/warn/remove/shadowban/escalate)
|
| 29 |
-
num_scenarios:
|
| 30 |
|
| 31 |
- id: medium_moderation
|
| 32 |
name: Medium Content Moderation
|
|
@@ -63,10 +63,9 @@ tasks:
|
|
| 63 |
- name: severity_within_1
|
| 64 |
weight: 0.2
|
| 65 |
description: Predicted severity within Β±1 of ground truth (1-5 scale)
|
| 66 |
-
num_scenarios:
|
| 67 |
|
| 68 |
# ββ Observation space βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
-
observation_space:
|
| 70 |
type: dict
|
| 71 |
fields:
|
| 72 |
text:
|
|
@@ -113,39 +112,58 @@ action_space:
|
|
| 113 |
|
| 114 |
# ββ Reward function βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 115 |
reward:
|
| 116 |
-
range: [0.
|
| 117 |
type: deterministic
|
| 118 |
description: >
|
| 119 |
Partial-credit scoring. Each correct component (label, action, severity)
|
| 120 |
contributes its rubric weight to the total. Severity is only scored in
|
| 121 |
-
hard tier and receives credit within
|
|
|
|
| 122 |
partial_progress: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
# ββ API spec ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 125 |
api:
|
| 126 |
reset:
|
| 127 |
signature: "reset(scenario_id: str | None = None) -> dict"
|
| 128 |
description: >
|
| 129 |
-
Begin a new episode.
|
| 130 |
-
|
|
|
|
| 131 |
step:
|
| 132 |
signature: "step(action: dict) -> dict"
|
| 133 |
description: >
|
| 134 |
Submit a moderation decision. Returns {state, reward, done, info}.
|
| 135 |
-
|
| 136 |
state:
|
| 137 |
signature: "state() -> dict"
|
| 138 |
description: Return current observation without stepping.
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
# ββ Baseline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 141 |
baseline:
|
| 142 |
-
script:
|
| 143 |
agent: rule_based_lexical
|
| 144 |
reproducibility_seed: 42
|
| 145 |
expected_scores:
|
| 146 |
-
easy:
|
| 147 |
-
medium:
|
| 148 |
-
hard:
|
|
|
|
| 149 |
|
| 150 |
# ββ Deployment ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 151 |
deployment:
|
|
|
|
| 26 |
- name: action_correct
|
| 27 |
weight: 0.5
|
| 28 |
description: Moderation action matches ground truth (allow/warn/remove/shadowban/escalate)
|
| 29 |
+
num_scenarios: 25
|
| 30 |
|
| 31 |
- id: medium_moderation
|
| 32 |
name: Medium Content Moderation
|
|
|
|
| 63 |
- name: severity_within_1
|
| 64 |
weight: 0.2
|
| 65 |
description: Predicted severity within Β±1 of ground truth (1-5 scale)
|
| 66 |
+
num_scenarios: 30
|
| 67 |
|
| 68 |
# ββ Observation space βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 69 |
type: dict
|
| 70 |
fields:
|
| 71 |
text:
|
|
|
|
| 112 |
|
| 113 |
# ββ Reward function βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 114 |
reward:
|
| 115 |
+
range: [-0.3, 1.0]
|
| 116 |
type: deterministic
|
| 117 |
description: >
|
| 118 |
Partial-credit scoring. Each correct component (label, action, severity)
|
| 119 |
contributes its rubric weight to the total. Severity is only scored in
|
| 120 |
+
hard tier and receives credit within +/-1 of ground truth.
|
| 121 |
+
Penalty rewards may push the score below 0.0.
|
| 122 |
partial_progress: true
|
| 123 |
+
penalties:
|
| 124 |
+
false_positive_escalation: -0.25 # escalating/removing GT-safe content
|
| 125 |
+
missed_critical_threat: -0.20 # allowing/warning on GT-toxic severity-5
|
| 126 |
+
invalid_label: -0.10 # label not in {safe,toxic,spam,misleading}
|
| 127 |
+
invalid_action: -0.10 # action not in {allow,warn,remove,shadowban,escalate}
|
| 128 |
|
| 129 |
# ββ API spec ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 130 |
api:
|
| 131 |
reset:
|
| 132 |
signature: "reset(scenario_id: str | None = None) -> dict"
|
| 133 |
description: >
|
| 134 |
+
Begin a new episode. If scenario_id is provided, loads that specific
|
| 135 |
+
scenario (single-step mode, backward compatible). If None, samples a
|
| 136 |
+
mixed-tier queue of 3 scenarios (1 easy + 1 medium + 1 hard).
|
| 137 |
step:
|
| 138 |
signature: "step(action: dict) -> dict"
|
| 139 |
description: >
|
| 140 |
Submit a moderation decision. Returns {state, reward, done, info}.
|
| 141 |
+
done=False until all posts in the queue are processed.
|
| 142 |
state:
|
| 143 |
signature: "state() -> dict"
|
| 144 |
description: Return current observation without stepping.
|
| 145 |
|
| 146 |
+
episode_structure:
|
| 147 |
+
mode: queue
|
| 148 |
+
queue_length: 3
|
| 149 |
+
composition: "1 easy + 1 medium + 1 hard per episode"
|
| 150 |
+
single_step_mode: "available via reset(scenario_id=...)"
|
| 151 |
+
|
| 152 |
+
http_endpoints:
|
| 153 |
+
- "POST /reset -> {state, status} HTTP 200"
|
| 154 |
+
- "POST /step -> {state, reward, done, info} HTTP 200"
|
| 155 |
+
- "GET /state -> {state, status} HTTP 200"
|
| 156 |
+
|
| 157 |
# ββ Baseline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 158 |
baseline:
|
| 159 |
+
script: inference.py
|
| 160 |
agent: rule_based_lexical
|
| 161 |
reproducibility_seed: 42
|
| 162 |
expected_scores:
|
| 163 |
+
easy: 0.750 # lexical baseline β most easy cases caught
|
| 164 |
+
medium: 0.575 # context nuance is lost without LLM
|
| 165 |
+
hard: 0.220 # severity rating fails completely
|
| 166 |
+
overall: 0.515 # weighted mean across all 60 scenarios
|
| 167 |
|
| 168 |
# ββ Deployment ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 169 |
deployment:
|
requirements.txt
CHANGED
|
@@ -2,3 +2,4 @@ anthropic>=0.34.0
|
|
| 2 |
google-genai>=0.7.0
|
| 3 |
pydantic>=2.0
|
| 4 |
gradio>=4.40.0
|
|
|
|
|
|
| 2 |
google-genai>=0.7.0
|
| 3 |
pydantic>=2.0
|
| 4 |
gradio>=4.40.0
|
| 5 |
+
openai>=1.0.0
|