Somuai12 commited on
Commit
8cd3fa7
·
0 Parent(s):

Initial commit of PolicyEvolverEnv - Meta Hackathon

Browse files
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ .env
6
+ .git/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ outputs/
11
+ baseline_results.json
12
+ .dockerignore
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ env.bak/
11
+ venv.bak/
12
+ *.log
13
+ *results.json
14
+ .DS_Store
15
+ .env
README.md ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PolicyEvolverEnv
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 8000
8
+ ---
9
+ # PolicyEvolverEnv
10
+
11
+ **PolicyEvolverEnv** is an OpenEnv-compliant reinforcement learning environment designed for the Meta × PyTorch × Scaler Hackathon.
12
+
13
+ ## Environment Description & Motivation
14
+ PolicyEvolverEnv is a real-world governance sandbox where an AI agent learns to **design and evolve governance policies** through meta-reasoning over real-world operational data. In modern platforms (social media, enterprise HR, e-commerce), static policies quickly become outdated or vaguely applied, leading to inconsistent enforcement, false-positive moderation, and unrecognized fraud.
15
+
16
+ This environment simulates this challenge by presenting the agent with a corpus of operational data alongside an existing policy framework. The agent's goal is to analyze the outcomes, identify systemic flaws or ambiguities, and act directly on the policies to optimize governance outcomes. This directly tackles live production problems faced by platforms like Meta.
17
+
18
+ ## Observation Space
19
+ The `Observation` received by the agent at every step describes the current operational context:
20
+ - `task_id` (str): Identifier for the active scenario.
21
+ - `episode_id` (str): Unique session tracker.
22
+ - `step_count` (int): Active step number (Max 5 per episode).
23
+ - `data_corpus` (List[Dict]): Represents operational examples like social media posts, HR incidents, or seller accounts along with the action taken or outcome.
24
+ - `current_policies` (List[Dict]): The list of current active policies the system follows.
25
+ - `system_metrics` & `policy_outcomes`: Operational statistics reflecting precision/recall or false-positive rates.
26
+ - `identified_issues`: Current known flaws in the governance pipeline.
27
+
28
+ ## Action Space
29
+ The Action space utilizes a highly structured Discriminated Union model to represent multi-faceted policy adjustments:
30
+
31
+ **1. ProposeClarificationAction (`propose_clarification`)**
32
+ - Targets an `ambiguous_term` in an existing policy.
33
+ - Requires a specific, measurable `suggested_definition` and `justification`.
34
+ **2. ProposeNewRuleAction (`propose_new_rule`)**
35
+ - Addresses an unhandled domain (`rule_domain`).
36
+ - Requires `new_rule` text, application `scope`, and `integration_points` connecting to older policies.
37
+ **3. EvolveProcessAction (`evolve_policy`)**
38
+ - The hardest action; holistically modifies existing rules.
39
+ - Requires a list of `policy_modifications`, realistic `expected_outcomes` deltas, and multi-metric `rollback_conditions`.
40
+
41
+ *(Each action also supports an optional `think` property allowing Chain-of-Thought meta-reasoning for a reward score bonus).*
42
+
43
+ ## Tasks
44
+ The environment provides three procedural tasks designed to ramp up in cognitive reasoning difficulty:
45
+
46
+ | Task ID | Difficulty | Expected Score | Description |
47
+ |---|---|---|---|
48
+ | `task_easy` | **Easy** | `~0.80` | **Ambiguity Clarification**: Identify and clarify vague policy terms (e.g., "harassment") in a social media community guideline to improve moderation consistency. |
49
+ | `task_medium` | **Medium** | `~0.70` | **Gap Detection**: Detect uncovered HR policy scenarios involving emerging tech (AI use, gig-worker boundaries) and propose entirely new mandatory rules. |
50
+ | `task_hard` | **Hard** | `~0.55` | **Holistic Evolution**: Analyze complex e-commerce Trust & Safety trade-offs (e.g., false-positive suspensions vs. fraud recall) to rewrite existing volume/return rate policies simultaneously. |
51
+
52
+ ## Setup & Usage
53
+
54
+ ### 1. Local Installation
55
+ ```bash
56
+ git clone <repository_url>
57
+ cd policy_evolver_env
58
+ python3 -m venv .venv
59
+ source .venv/bin/activate
60
+ pip install -r server/requirements.txt
61
+ ```
62
+
63
+ ### 2. Run the Environment API
64
+ Start the FastAPI environment server locally:
65
+ ```bash
66
+ uvicorn server.app:app --port 8000
67
+ ```
68
+ This boots all core endpoint paths (`/reset`, `/step`, `/state`, `/tasks`, `/grader`, `/health`).
69
+
70
+ ### 3. Run the Inference Baseline
71
+ The environment includes a built-in testing script named `inference.py` ready for deployment on Hugging Face Spaces.
72
+
73
+ Export your environment variables:
74
+ ```bash
75
+ export API_BASE_URL="https://api.openai.com/v1"
76
+ export MODEL_NAME="gpt-4o-mini"
77
+ export HF_TOKEN="your_huggingface_or_openai_api_key_here"
78
+ export OPENENV_BASE_URL="http://localhost:8000"
79
+ ```
80
+
81
+ Execute the agent simulation against the running environment:
82
+ ```bash
83
+ python inference.py --mode llm --output json
84
+ ```
85
+ *(If no API key is specified, `--mode rule` will execute the deterministic rule-based fallback).*
86
+
87
+ ## Baseline Scores
88
+ The bundled deterministic fallback strategy (`inference.py --mode rule`) yields the following baseline validation scores across the active grader:
89
+
90
+ - **Easy (Ambiguity Clarification):** 1.000
91
+ - **Medium (New Rule Proposal):** 1.000
92
+ - **Hard (Policy Evolution):** 0.950
93
+ - **Overall Average:** 0.983
94
+
95
+ *(Note: Live LLM runs generally average expected heuristic bounds around ~0.80, ~0.70, and ~0.55 respectively).*
__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # __init__.py
2
+ from .models import (
3
+ ProposeClarificationAction,
4
+ ProposeNewRuleAction,
5
+ EvolveProcessAction,
6
+ Action,
7
+ Observation,
8
+ State,
9
+ )
10
+ from .client import PolicyEvolverEnv
11
+
12
+ __all__ = [
13
+ "PolicyEvolverEnv",
14
+ "ProposeClarificationAction",
15
+ "ProposeNewRuleAction",
16
+ "EvolveProcessAction",
17
+ "Action",
18
+ "Observation",
19
+ "State",
20
+ ]
client.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # client.py
2
+ from openenv.core.env_client import EnvClient
3
+ from .models import Action, Observation, State
4
+
5
+
6
+ class PolicyEvolverEnv(EnvClient):
7
+ """
8
+ Client for PolicyEvolverEnv.
9
+ Usage:
10
+ async with PolicyEvolverEnv(base_url="https://your-space.hf.space") as env:
11
+ obs = await env.reset(task_id="task_easy")
12
+ result = await env.step(action)
13
+ """
14
+ observation_class = Observation
15
+ state_class = State
inference.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # baseline/run_baseline.py
2
+ """
3
+ LLM-powered baseline for PolicyEvolverEnv.
4
+
5
+ Primary path: Uses AsyncOpenAI client with OPENAI_API_KEY (or HF_TOKEN) to
6
+ run a language model against all 3 environment tasks.
7
+ Fallback path: Rule-based hardcoded actions used when no API key is available.
8
+
9
+ Run:
10
+ python -m policy_evolver_env.baseline.run_baseline # LLM baseline (needs OPENAI_API_KEY)
11
+ python -m policy_evolver_env.baseline.run_baseline --mode rule # Rule-based fallback
12
+ python -m policy_evolver_env.baseline.run_baseline --output json # JSON output
13
+
14
+ Expected scores (LLM): easy ~0.80, medium ~0.70, hard ~0.55
15
+ Expected scores (rule): easy ~0.65, medium ~0.50, hard ~0.35
16
+
17
+ Required env vars:
18
+ OPENAI_API_KEY — OpenAI key or HF Inference API token (primary)
19
+ HF_TOKEN — Hugging Face token (fallback if no OPENAI_API_KEY)
20
+ API_BASE_URL — API endpoint (default: https://api.openai.com/v1)
21
+ MODEL_NAME — Model to use (default: gpt-4o-mini)
22
+ OPENENV_BASE_URL — Environment server (default: http://localhost:8000)
23
+ """
24
+ from __future__ import annotations
25
+ import asyncio
26
+ import json
27
+ import logging
28
+ import os
29
+ import sys
30
+ import time
31
+ from typing import Dict, List, Optional
32
+
33
+ import httpx
34
+ from openai import OpenAI
35
+
36
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
37
+ logger = logging.getLogger(__name__)
38
+
39
+ # ─────────────────────────────────────────────
40
+ # Configuration (all from env vars)
41
+ # ─────────────────────────────────────────────
42
+
43
+ BASE_URL = os.getenv("OPENENV_BASE_URL", "http://127.0.0.1:8000")
44
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
45
+ API_KEY = os.getenv("HF_TOKEN", "") or os.getenv("OPENAI_API_KEY", "")
46
+ MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4o-mini")
47
+
48
+
49
+ def verify_environment() -> bool:
50
+ """Verify required env vars. Returns True if LLM mode is possible."""
51
+ if not API_KEY:
52
+ logger.warning(
53
+ "No API_KEY (HF_TOKEN) found. "
54
+ "LLM baseline will be skipped. Set one of these env vars to enable it."
55
+ )
56
+ return False
57
+ logger.info(f"API key found. Model: {MODEL_NAME} Base URL: {API_BASE_URL}")
58
+ return True
59
+
60
+
61
+ # ─────────────────────────────────────────────
62
+ # LLM Agent
63
+ # ─────────────────────────────────────────────
64
+
65
+ class PolicyEvolverAgent:
66
+ """LLM-powered agent that calls the OpenAI-compatible API."""
67
+
68
+ def __init__(self):
69
+ self.client = OpenAI(
70
+ api_key=API_KEY,
71
+ base_url=API_BASE_URL,
72
+ )
73
+ self.model = MODEL_NAME
74
+
75
+ def _call(self, prompt: str, max_tokens: int = 700, temperature: float = 0.3) -> Optional[Dict]:
76
+ """Call the LLM and parse JSON response. Returns None on failure."""
77
+ try:
78
+ resp = self.client.chat.completions.create(
79
+ model=self.model,
80
+ messages=[
81
+ {
82
+ "role": "system",
83
+ "content": (
84
+ "You are a senior policy analyst. "
85
+ "Always respond with a single valid JSON object and nothing else. "
86
+ "No markdown fences, no preamble."
87
+ ),
88
+ },
89
+ {"role": "user", "content": prompt},
90
+ ],
91
+ temperature=temperature,
92
+ max_tokens=max_tokens,
93
+ )
94
+ raw = resp.choices[0].message.content.strip()
95
+ # Strip accidental markdown fences
96
+ if raw.startswith("```"):
97
+ raw = raw.split("```")[1]
98
+ if raw.startswith("json"):
99
+ raw = raw[4:]
100
+ return json.loads(raw)
101
+ except Exception as e:
102
+ logger.warning(f"LLM call failed: {e}")
103
+ return None
104
+
105
+ def handle_easy(self, obs: Dict) -> Dict:
106
+ """Easy task: propose clarification for an ambiguous policy term."""
107
+ prompt = f"""
108
+ Analyze the following social media platform policies and user-generated data.
109
+ Identify ONE genuinely ambiguous term that causes inconsistent moderation decisions.
110
+ Propose a specific, measurable definition.
111
+
112
+ POLICIES:
113
+ {json.dumps(obs.get("current_policies", []), indent=2)}
114
+
115
+ DATA EXAMPLES (how posts were actually handled):
116
+ {json.dumps(obs.get("data_corpus", [])[:6], indent=2)}
117
+
118
+ Respond ONLY with this JSON schema:
119
+ {{
120
+ "action_type": "propose_clarification",
121
+ "ambiguous_term": "<the exact term from policies>",
122
+ "suggested_definition": "<specific, ≥15 word definition with clear criteria>",
123
+ "affected_policy_ids": ["<policy id>"],
124
+ "justification": "<why inconsistent moderation results; ≥15 words>",
125
+ "think": "<step-by-step reasoning: which posts were handled inconsistently and why>"
126
+ }}
127
+ """
128
+ result = self._call(prompt, max_tokens=600)
129
+ if result:
130
+ result["action_type"] = "propose_clarification"
131
+ return result
132
+ # Fallback
133
+ return RULE_BASED_ACTIONS["task_easy"]
134
+
135
+ def handle_medium(self, obs: Dict) -> Dict:
136
+ """Medium task: detect policy gap and propose new rule."""
137
+ prompt = f"""
138
+ You are reviewing corporate HR policies. The data shows real incidents that occurred.
139
+ Find ONE scenario category NOT adequately covered by existing policies.
140
+ Propose a specific, mandatory new rule to fill the gap.
141
+
142
+ EXISTING POLICIES:
143
+ {json.dumps(obs.get("current_policies", []), indent=2)}
144
+
145
+ INCIDENT DATA:
146
+ {json.dumps(obs.get("data_corpus", []), indent=2)}
147
+
148
+ Respond ONLY with this JSON schema:
149
+ {{
150
+ "action_type": "propose_new_rule",
151
+ "rule_domain": "<e.g. AI_use | gig_worker_post_engagement | cross_border_remote>",
152
+ "new_rule": "<mandatory rule using 'must'/'shall'/'required'; ≥20 words; no vague language>",
153
+ "scope": ["<scenario 1>", "<scenario 2>", "<scenario 3>", "<scenario 4>"],
154
+ "integration_points": ["<existing policy id 1>", "<existing policy id 2>"],
155
+ "justification": "<cite specific incident IDs and why gap exists; ≥20 words>",
156
+ "think": "<which incident type appears most frequently uncovered and why a rule is needed>"
157
+ }}
158
+ """
159
+ result = self._call(prompt, max_tokens=800)
160
+ if result:
161
+ result["action_type"] = "propose_new_rule"
162
+ return result
163
+ return RULE_BASED_ACTIONS["task_medium"]
164
+
165
+ def handle_hard(self, obs: Dict) -> Dict:
166
+ """Hard task: holistic policy evolution with trade-off reasoning."""
167
+ prompt = f"""
168
+ You are a senior Trust & Safety policy architect. The current policy framework is
169
+ underperforming. Propose specific modifications to ≥2 existing policies to improve
170
+ both precision (reduce false positives) and recall (catch more fraud) simultaneously.
171
+ Acknowledge the trade-offs explicitly.
172
+
173
+ CURRENT POLICIES:
174
+ {json.dumps(obs.get("current_policies", []), indent=2)}
175
+
176
+ PERFORMANCE METRICS (current vs target):
177
+ {json.dumps(obs.get("policy_outcomes", []), indent=2)}
178
+
179
+ SYSTEM METRICS:
180
+ {json.dumps(obs.get("system_metrics", {}), indent=2)}
181
+
182
+ KNOWN ISSUES:
183
+ {json.dumps(obs.get("identified_issues", []), indent=2)}
184
+
185
+ Respond ONLY with this JSON schema:
186
+ {{
187
+ "action_type": "evolve_policy",
188
+ "policy_modifications": [
189
+ {{
190
+ "policy_id": "<exact policy id from above>",
191
+ "change_type": "enhance",
192
+ "new_text": "<specific replacement text; must be context-aware, not blanket>",
193
+ "reason": "<cite the specific metric that proves current policy fails>"
194
+ }},
195
+ {{
196
+ "policy_id": "<second policy id>",
197
+ "change_type": "enhance",
198
+ "new_text": "<replacement text>",
199
+ "reason": "<metric-backed reason>"
200
+ }}
201
+ ],
202
+ "expected_outcomes": {{
203
+ "false_positive_rate": <realistic delta 0.01-0.40>,
204
+ "fraud_detection_rate": <realistic delta 0.01-0.40>,
205
+ "seller_trust_score": <realistic delta 0.01-0.30>,
206
+ "review_queue_overload": <realistic delta 0.01-0.40>
207
+ }},
208
+ "rollback_conditions": [
209
+ "<specific numeric threshold that triggers revert>",
210
+ "<second specific condition with metric name and number>"
211
+ ],
212
+ "justification": "<explain trade-offs: what improves, what worsens, and why net positive>",
213
+ "think": "<identify the two worst-performing metrics and trace root cause to specific policy>"
214
+ }}
215
+ """
216
+ result = self._call(prompt, max_tokens=1200, temperature=0.2)
217
+ if result:
218
+ result["action_type"] = "evolve_policy"
219
+ return result
220
+ return RULE_BASED_ACTIONS["task_hard"]
221
+
222
+
223
+ # ─────────────────────────────────────────────
224
+ # Environment interaction helpers (HTTP-based)
225
+ # ─────────────────────────────────────────────
226
+
227
+ async def env_reset(client: httpx.AsyncClient, task_id: str) -> Dict:
228
+ resp = await client.post(f"{BASE_URL}/reset", json={"task_id": task_id})
229
+ resp.raise_for_status()
230
+ return resp.json()
231
+
232
+
233
+ async def env_step(client: httpx.AsyncClient, action: Dict) -> Dict:
234
+ resp = await client.post(f"{BASE_URL}/step", json={"action": action})
235
+ resp.raise_for_status()
236
+ return resp.json()
237
+
238
+
239
+ async def run_single_task(
240
+ http: httpx.AsyncClient,
241
+ agent: Optional[PolicyEvolverAgent],
242
+ task_id: str,
243
+ ) -> Dict:
244
+ """Run one task with LLM agent (or rule fallback) and return result."""
245
+ obs = await env_reset(http, task_id)
246
+
247
+ if agent is not None:
248
+ if task_id == "task_easy":
249
+ action = agent.handle_easy(obs)
250
+ elif task_id == "task_medium":
251
+ action = agent.handle_medium(obs)
252
+ else:
253
+ action = agent.handle_hard(obs)
254
+ mode = "llm"
255
+ else:
256
+ action = RULE_BASED_ACTIONS[task_id]
257
+ mode = "rule"
258
+
259
+ result = await env_step(http, action)
260
+ reward = result.get("reward", 0.0)
261
+ logger.info(f"[{task_id}] mode={mode} score={reward:.4f} done={result.get('done')}")
262
+ return {"task_id": task_id, "reward": reward, "mode": mode, "done": result.get("done", False)}
263
+
264
+
265
+ # ─────────────────────────────────────────────
266
+ # Direct baseline (no HTTP — used by /baseline endpoint)
267
+ # ─────────────────────────────────────────────
268
+
269
+ async def run_direct_baseline() -> Dict:
270
+ """
271
+ Run baseline directly using environment and grader imports.
272
+ Used by the /baseline endpoint to avoid self-HTTP calls on HF Spaces.
273
+ """
274
+ from ..server.environment import PolicyEvolverEnvironment
275
+ from ..server.grader import grade
276
+
277
+ env = PolicyEvolverEnvironment()
278
+ use_llm = verify_environment()
279
+ agent = PolicyEvolverAgent() if use_llm else None
280
+
281
+ start = time.time()
282
+ results: List[Dict] = []
283
+
284
+ for task_id in ["task_easy", "task_medium", "task_hard"]:
285
+ try:
286
+ obs = env.reset(task_id=task_id)
287
+ obs_dict = obs.model_dump()
288
+
289
+ if agent is not None:
290
+ if task_id == "task_easy":
291
+ action = agent.handle_easy(obs_dict)
292
+ elif task_id == "task_medium":
293
+ action = agent.handle_medium(obs_dict)
294
+ else:
295
+ action = agent.handle_hard(obs_dict)
296
+ mode = "llm"
297
+ else:
298
+ action = RULE_BASED_ACTIONS[task_id]
299
+ mode = "rule"
300
+
301
+ result_obs = env.step(action)
302
+ reward = result_obs.reward
303
+ logger.info(f"[{task_id}] mode={mode} score={reward:.4f} done={result_obs.done}")
304
+ results.append({"task_id": task_id, "reward": reward, "mode": mode, "done": result_obs.done})
305
+ except Exception as e:
306
+ logger.error(f"[{task_id}] failed: {e}")
307
+ results.append({"task_id": task_id, "reward": 0.0, "mode": "error", "error": str(e)})
308
+
309
+ scores = {r["task_id"]: max(0.0, min(1.0, r["reward"])) for r in results}
310
+ overall = sum(scores.values()) / len(scores) if scores else 0.0
311
+
312
+ return {
313
+ "baseline_scores": {
314
+ "task_easy": scores.get("task_easy", 0.0),
315
+ "task_medium": scores.get("task_medium", 0.0),
316
+ "task_hard": scores.get("task_hard", 0.0),
317
+ "overall_avg": round(overall, 4),
318
+ },
319
+ "mode": "llm" if use_llm else "rule_fallback",
320
+ "model": MODEL_NAME if use_llm else "rule-based",
321
+ "runtime_seconds": round(time.time() - start, 2),
322
+ "detail": results,
323
+ }
324
+
325
+
326
+ # ─────────────────────────────────────────────
327
+ # Main HTTP-based baseline runner
328
+ # ─────────────────────────────────────────────
329
+
330
+ async def run_llm_baseline() -> Dict:
331
+ """Primary baseline: LLM agent against all 3 tasks via HTTP."""
332
+ use_llm = verify_environment()
333
+ agent = PolicyEvolverAgent() if use_llm else None
334
+
335
+ start = time.time()
336
+ results: List[Dict] = []
337
+
338
+ async with httpx.AsyncClient(timeout=120.0) as http:
339
+ for task_id in ["task_easy", "task_medium", "task_hard"]:
340
+ if time.time() - start > 1140:
341
+ logger.warning("Approaching 20min time limit — stopping early")
342
+ break
343
+ try:
344
+ r = await run_single_task(http, agent, task_id)
345
+ results.append(r)
346
+ except Exception as e:
347
+ logger.error(f"[{task_id}] failed: {e}")
348
+ results.append({"task_id": task_id, "reward": 0.0, "mode": "error", "error": str(e)})
349
+
350
+ scores = {r["task_id"]: max(0.0, min(1.0, r["reward"])) for r in results}
351
+ overall = sum(scores.values()) / len(scores) if scores else 0.0
352
+
353
+ summary = {
354
+ "baseline_scores": {
355
+ "task_easy": scores.get("task_easy", 0.0),
356
+ "task_medium": scores.get("task_medium", 0.0),
357
+ "task_hard": scores.get("task_hard", 0.0),
358
+ "overall_avg": round(overall, 4),
359
+ },
360
+ "mode": "llm" if use_llm else "rule_fallback",
361
+ "model": MODEL_NAME if use_llm else "rule-based",
362
+ "runtime_seconds": round(time.time() - start, 2),
363
+ "detail": results,
364
+ }
365
+
366
+ # Persist for analysis
367
+ try:
368
+ with open("baseline_results.json", "w") as f:
369
+ json.dump(summary, f, indent=2)
370
+ except Exception:
371
+ pass
372
+
373
+ return summary
374
+
375
+
376
+ # Keep rule-based runner available for /baseline endpoint fallback
377
+ async def run_rule_based_baseline() -> Dict:
378
+ """Fallback: hardcoded rule-based actions, no LLM required."""
379
+ results: List[Dict] = []
380
+ async with httpx.AsyncClient(timeout=60.0) as http:
381
+ for task_id, action in RULE_BASED_ACTIONS.items():
382
+ try:
383
+ await env_reset(http, task_id)
384
+ result = await env_step(http, action)
385
+ reward = max(0.0, min(1.0, result.get("reward", 0.0)))
386
+ results.append({"task_id": task_id, "reward": reward})
387
+ logger.info(f"[{task_id}] rule score={reward:.4f}")
388
+ except Exception as e:
389
+ logger.error(f"[{task_id}] rule baseline error: {e}")
390
+ results.append({"task_id": task_id, "reward": 0.0})
391
+ scores = {r["task_id"]: r["reward"] for r in results}
392
+ overall = sum(scores.values()) / len(scores) if scores else 0.0
393
+ return {**scores, "overall_avg": round(overall, 4)}
394
+
395
+
396
+ # ─────────────────────────────────────────────
397
+ # Rule-based fallback actions (used when OPENAI_API_KEY not set)
398
+ # ─────────────────────────────────────────────
399
+
400
+ RULE_BASED_ACTIONS = {
401
+ "task_easy": {
402
+ "action_type": "propose_clarification",
403
+ "ambiguous_term": "harassment",
404
+ "suggested_definition": (
405
+ "Harassment is defined as any repeated, unwanted communication or behaviour "
406
+ "directed at a specific individual that a reasonable person would find threatening, "
407
+ "intimidating, or distressing. This includes but is not limited to targeted insults, "
408
+ "threats, and sustained negative attention. Single interactions may qualify if "
409
+ "sufficiently severe."
410
+ ),
411
+ "affected_policy_ids": ["pol_002"],
412
+ "justification": (
413
+ "The term 'harassment' is subjective and moderators apply it inconsistently. "
414
+ "Different reviewers may interpret the same post differently without a measurable definition."
415
+ ),
416
+ "think": (
417
+ "Looking at the data, posts 001 and 006 were treated differently despite similar tone. "
418
+ "The key ambiguous term causing inconsistency is 'harassment' in pol_002."
419
+ ),
420
+ },
421
+ "task_medium": {
422
+ "action_type": "propose_new_rule",
423
+ "rule_domain": "AI_use",
424
+ "new_rule": (
425
+ "Employees must disclose when AI tools are used to generate, substantially edit, or "
426
+ "evaluate work products that are submitted under their name, including client proposals, "
427
+ "code submissions, and performance evaluations. AI-assisted content must be reviewed "
428
+ "and validated by the submitting employee before delivery."
429
+ ),
430
+ "scope": [
431
+ "AI-generated client proposals",
432
+ "AI-written code in performance reviews",
433
+ "AI-assisted HR decisions",
434
+ "Automated content in employee-attributed work",
435
+ ],
436
+ "integration_points": ["pol_hr_001", "pol_hr_005"],
437
+ "justification": (
438
+ "Incidents 001, 004, and 007 all involve AI use that current policies do not address. "
439
+ "There is no rule requiring disclosure or validation of AI-generated work, creating "
440
+ "a gap in accountability and intellectual honesty."
441
+ ),
442
+ "think": (
443
+ "The uncovered domain is AI use in professional work. Three of 10 incidents involve this. "
444
+ "The new rule must be mandatory (not advisory) and must specify disclosure + validation."
445
+ ),
446
+ },
447
+ "task_hard": {
448
+ "action_type": "evolve_policy",
449
+ "policy_modifications": [
450
+ {
451
+ "policy_id": "ts_pol_001",
452
+ "change_type": "enhance",
453
+ "new_text": (
454
+ "New seller accounts with more than 50 transactions in the first week will be "
455
+ "reviewed only if additional risk signals are present (e.g., chargeback rate > 5%, "
456
+ "price variance > 30%, or fraud reports). Seasonal categories (gifts, fashion) "
457
+ "have an elevated threshold of 150 transactions during peak periods."
458
+ ),
459
+ "reason": "Blanket volume threshold causes 42% false positive rate among legitimate high-volume sellers.",
460
+ },
461
+ {
462
+ "policy_id": "ts_pol_002",
463
+ "change_type": "enhance",
464
+ "new_text": (
465
+ "Return rate thresholds are applied per category: electronics > 10%, fashion > 25%, "
466
+ "general goods > 15%. Accounts exceeding category thresholds are flagged for review, "
467
+ "not automatic suspension."
468
+ ),
469
+ "reason": "Return rate varies dramatically by category; a single threshold discriminates against fashion sellers.",
470
+ },
471
+ ],
472
+ "expected_outcomes": {
473
+ "false_positive_rate": 0.20,
474
+ "fraud_detection_rate": 0.35,
475
+ "seller_trust_score": 0.15,
476
+ "review_queue_overload": 0.30,
477
+ },
478
+ "rollback_conditions": [
479
+ "false_positive_rate increases above 0.50 after policy change",
480
+ "fraud_detection_rate drops below 0.25 within 30 days",
481
+ "seller trust score decreases by more than 0.10 in 14-day survey",
482
+ ],
483
+ "justification": (
484
+ "The current framework has a 42% false positive rate because blanket thresholds don't "
485
+ "account for legitimate high-volume or high-return categories. Modifying ts_pol_001 and "
486
+ "ts_pol_002 to be context-aware reduces wrongful suspensions while maintaining fraud "
487
+ "detection via multi-signal scoring. Trade-off: fraud_detection_rate may improve more "
488
+ "slowly since we're relaxing volume triggers, but seller trust and queue overload improve "
489
+ "immediately."
490
+ ),
491
+ "think": (
492
+ "The system_metrics show false_positive_rate=0.42 and fraud_detection_rate=0.31. "
493
+ "The identified issues all point to overly broad thresholds. I should modify the two "
494
+ "most impactful policies and provide category-specific thresholds. "
495
+ "The rollback conditions should be metric-specific with concrete numbers."
496
+ ),
497
+ },
498
+ }
499
+
500
+
501
+ if __name__ == "__main__":
502
+ import argparse
503
+ parser = argparse.ArgumentParser(description="PolicyEvolverEnv baseline runner")
504
+ parser.add_argument("--mode", choices=["llm", "rule"], default="llm",
505
+ help="llm = LLM agent (needs OPENAI_API_KEY); rule = hardcoded fallback")
506
+ parser.add_argument("--output", choices=["text", "json"], default="text")
507
+ args = parser.parse_args()
508
+
509
+ if args.mode == "rule":
510
+ summary = asyncio.run(run_rule_based_baseline())
511
+ scores = summary
512
+ else:
513
+ summary = asyncio.run(run_llm_baseline())
514
+ scores = summary.get("baseline_scores", summary)
515
+
516
+ if args.output == "json":
517
+ print(json.dumps(summary, indent=2))
518
+ else:
519
+ print("\n" + "=" * 50)
520
+ print("POLICEVOLVERENV BASELINE SCORES")
521
+ print("=" * 50)
522
+ print(f"Easy (Ambiguity Clarification): {scores.get('task_easy', 0.0):.3f}")
523
+ print(f"Medium (New Rule Proposal): {scores.get('task_medium', 0.0):.3f}")
524
+ print(f"Hard (Policy Evolution): {scores.get('task_hard', 0.0):.3f}")
525
+ print(f"Overall Average: {scores.get('overall_avg', 0.0):.3f}")
526
+ print("=" * 50)
527
+
528
+ for k, v in scores.items():
529
+ if isinstance(v, float) and not (0.0 <= v <= 1.0):
530
+ raise ValueError(f"Score {k}={v} outside [0.0, 1.0] — submission invalid")
models.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # models.py
2
+ from __future__ import annotations
3
+ from pydantic import BaseModel, Field
4
+ from typing import Optional, List, Dict, Literal, Union
5
+ from enum import Enum
6
+ import uuid
7
+
8
+
9
+ class PolicyActionType(str, Enum):
10
+ PROPOSE_CLARIFICATION = "propose_clarification"
11
+ PROPOSE_NEW_RULE = "propose_new_rule"
12
+ EVOLVE_POLICY = "evolve_policy"
13
+
14
+
15
+ class ProposeClarificationAction(BaseModel):
16
+ """Easy task: identify an ambiguous policy term and clarify it."""
17
+ action_type: Literal[PolicyActionType.PROPOSE_CLARIFICATION] = PolicyActionType.PROPOSE_CLARIFICATION
18
+ ambiguous_term: str = Field(description="The exact ambiguous term found in policies")
19
+ suggested_definition: str = Field(description="A specific, actionable definition")
20
+ affected_policy_ids: List[str] = Field(default_factory=list, description="Policy IDs this affects")
21
+ justification: str = Field(description="Why this term is ambiguous")
22
+ think: Optional[str] = Field(default=None, description="Chain-of-thought reasoning (earns +0.1 bonus)")
23
+
24
+
25
+ class ProposeNewRuleAction(BaseModel):
26
+ """Medium task: detect a policy gap and propose a new rule."""
27
+ action_type: Literal[PolicyActionType.PROPOSE_NEW_RULE] = PolicyActionType.PROPOSE_NEW_RULE
28
+ rule_domain: str = Field(description="Domain the rule covers, e.g. 'content_moderation'")
29
+ new_rule: str = Field(description="The new rule text — must be clear and actionable")
30
+ scope: List[str] = Field(description="List of scenario types this rule covers")
31
+ integration_points: List[str] = Field(default_factory=list, description="How it connects to existing policies")
32
+ justification: str = Field(description="Why a gap exists and why this rule fills it")
33
+ think: Optional[str] = Field(default=None, description="Chain-of-thought reasoning (earns +0.1 bonus)")
34
+
35
+
36
+ class PolicyModification(BaseModel):
37
+ policy_id: str
38
+ change_type: Literal["enhance", "restrict", "add", "remove"]
39
+ new_text: str
40
+ reason: str
41
+
42
+
43
+ class EvolveProcessAction(BaseModel):
44
+ """Hard task: holistically evolve the policy framework."""
45
+ action_type: Literal[PolicyActionType.EVOLVE_POLICY] = PolicyActionType.EVOLVE_POLICY
46
+ policy_modifications: List[PolicyModification] = Field(description="Specific changes to make")
47
+ expected_outcomes: Dict[str, float] = Field(description="Metric name → expected delta (0.0–1.0)")
48
+ rollback_conditions: List[str] = Field(default_factory=list, description="When to revert")
49
+ justification: str = Field(description="Comprehensive reasoning")
50
+ think: Optional[str] = Field(default=None, description="Chain-of-thought reasoning (earns +0.1 bonus)")
51
+
52
+
53
+ from pydantic import RootModel
54
+
55
+ class Action(RootModel):
56
+ root: Union[ProposeClarificationAction, ProposeNewRuleAction, EvolveProcessAction] = Field(..., discriminator="action_type")
57
+
58
+
59
+ class Observation(BaseModel):
60
+ """What the agent sees after reset() or step()."""
61
+ task_id: str
62
+ episode_id: str
63
+ step_count: int
64
+ data_corpus: List[Dict] = Field(description="Scenarios/posts/actions for the agent to analyze")
65
+ current_policies: List[Dict] = Field(description="The existing policy set")
66
+ policy_outcomes: Optional[List[Dict]] = Field(default=None, description="Historical outcome data (hard task)")
67
+ system_metrics: Dict[str, float] = Field(default_factory=dict)
68
+ identified_issues: List[Dict] = Field(default_factory=list)
69
+ reward: float = 0.0
70
+ done: bool = False
71
+ info: Dict = Field(default_factory=dict)
72
+
73
+
74
+ class State(BaseModel):
75
+ """Episode metadata — returned by state() endpoint."""
76
+ episode_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
77
+ task_id: str = ""
78
+ step_count: int = 0
79
+ max_steps: int = 5
80
+ current_score: float = 0.0
81
+ best_score: float = 0.0
82
+ actions_taken: List[str] = Field(default_factory=list)
openenv.yaml ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "PolicyEvolverEnv"
2
+ description: "Policy Design and Evolution Sandbox — agents learn to evolve real-world governance frameworks through meta-reasoning"
3
+ version: "1.0.0"
4
+ author: "PolicyEvolution Team"
5
+ tags:
6
+ - "policy"
7
+ - "governance"
8
+ - "meta-reasoning"
9
+ - "content-moderation"
10
+ - "AI-safety"
11
+
12
+ environment:
13
+ module: "policy_evolver_env.server.environment"
14
+ class: "PolicyEvolverEnvironment"
15
+
16
+ observation_schema:
17
+ type: "object"
18
+ description: "Policy context, data corpus, and system state"
19
+
20
+ action_schema:
21
+ type: "object"
22
+ description: "Discriminated union on action_type field"
23
+ discriminator: "action_type"
24
+ variants:
25
+ - action_type: "propose_clarification"
26
+ schema: "ProposeClarificationAction"
27
+ - action_type: "propose_new_rule"
28
+ schema: "ProposeNewRuleAction"
29
+ - action_type: "evolve_policy"
30
+ schema: "EvolveProcessAction"
31
+
32
+ reward_range: [0.0, 1.0]
33
+
34
+ runtime:
35
+ max_steps: 5
36
+ timeout_seconds: 1200
37
+ vcpu: 2
38
+ memory_gb: 8
39
+
40
+ tasks:
41
+ - id: "task_easy"
42
+ difficulty: "easy"
43
+ description: "Identify and clarify ambiguous policy terms in a social media community guidelines"
44
+ expected_min_score: 0.70
45
+
46
+ - id: "task_medium"
47
+ difficulty: "medium"
48
+ description: "Detect policy gaps in corporate HR policies and propose new rules for emerging scenarios"
49
+ expected_min_score: 0.55
50
+
51
+ - id: "task_hard"
52
+ difficulty: "hard"
53
+ description: "Holistically evolve an e-commerce Trust & Safety framework with trade-off reasoning"
54
+ expected_min_score: 0.40
55
+
56
+ grading:
57
+ module: "policy_evolver_env.server.grader"
58
+ function: "grade"
59
+ return_range: [0.0, 1.0]
60
+
61
+ endpoints:
62
+ required:
63
+ - path: "/reset"
64
+ method: "POST"
65
+ - path: "/step"
66
+ method: "POST"
67
+ - path: "/state"
68
+ method: "GET"
69
+ - path: "/tasks"
70
+ method: "GET"
71
+ - path: "/grader"
72
+ method: "GET"
73
+ - path: "/baseline"
74
+ method: "GET"
75
+ optional:
76
+ - path: "/health"
77
+ method: "GET"
pyproject.toml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "policy-evolver-env"
7
+ version = "1.0.0"
8
+ description = "PolicyEvolverEnv — OpenEnv RL environment for policy evolution through meta-reasoning"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "openenv-core>=0.1.0",
13
+ "pydantic>=2.9.2",
14
+ "pydantic-settings>=2.6.1",
15
+ "fastapi>=0.115.4",
16
+ "uvicorn>=0.32.0",
17
+ "scikit-learn>=1.5.2",
18
+ "numpy>=1.26.4",
19
+ "openai>=1.54.4",
20
+ "httpx>=0.27.0",
21
+ "python-dotenv>=1.0.1",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest", "pytest-asyncio", "httpx"]
26
+
27
+ [tool.setuptools.packages.find]
28
+ include = ["policy_evolver_env*"]
rule_output3.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "task_easy": 0.0,
3
+ "task_medium": 0.0,
4
+ "task_hard": 0.0,
5
+ "overall_avg": 0.0
6
+ }
server/Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM openenv-base:latest
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy dependency file first for layer caching
6
+ COPY server/requirements.txt ./requirements.txt
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ # Copy full package
10
+ COPY . .
11
+
12
+ # Expose port
13
+ EXPOSE 8000
14
+
15
+ # Liveness probe — validator checks this
16
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
17
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
18
+
19
+ # Start server
20
+ CMD ["uvicorn", "policy_evolver_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
server/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # server/__init__.py
server/app.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/app.py
2
+ from __future__ import annotations
3
+ from fastapi import FastAPI, HTTPException, Query
4
+ from fastapi.responses import JSONResponse
5
+ from openenv.core.env_server import create_fastapi_app
6
+ from ..models import (
7
+ ProposeClarificationAction, ProposeNewRuleAction, EvolveProcessAction,
8
+ Observation, Action
9
+ )
10
+ from .environment import PolicyEvolverEnvironment
11
+ from .grader import grade
12
+ from .tasks import TASK_REGISTRY
13
+ import json
14
+
15
+ # Create app via OpenEnv helper — pass factory callable, action/obs classes
16
+ app = create_fastapi_app(
17
+ env=PolicyEvolverEnvironment,
18
+ action_cls=Action, # Pydantic union
19
+ observation_cls=Observation,
20
+ )
21
+
22
+
23
+ @app.get("/health")
24
+ async def health():
25
+ return {"status": "healthy", "environment": "PolicyEvolverEnv", "version": "1.0.0"}
26
+
27
+
28
+ @app.get("/tasks")
29
+ async def list_tasks():
30
+ return [
31
+ {
32
+ "task_id": tid,
33
+ "difficulty": t["difficulty"],
34
+ "description": t["description"],
35
+ "num_policies": t["num_policies"],
36
+ "num_data_points": t["num_data_points"],
37
+ }
38
+ for tid, t in TASK_REGISTRY.items()
39
+ ]
40
+
41
+
42
+ @app.get("/grader")
43
+ async def grader_endpoint(
44
+ task_id: str = Query(..., description="task_easy | task_medium | task_hard"),
45
+ action_json: str = Query(..., description="JSON-encoded action dict"),
46
+ ):
47
+ try:
48
+ action_dict = json.loads(action_json)
49
+ except json.JSONDecodeError:
50
+ raise HTTPException(status_code=400, detail="action_json must be valid JSON")
51
+ score = grade(action_dict, task_id)
52
+ return {"task_id": task_id, "score": score}
53
+
54
+
55
+ @app.get("/baseline")
56
+ async def run_baseline_endpoint():
57
+ """
58
+ Runs the LLM baseline (or rule-based fallback) and returns scores for all tasks.
59
+ Uses the grader directly instead of HTTP calls to self.
60
+ """
61
+ from ..inference import run_direct_baseline
62
+ results = await run_direct_baseline()
63
+ return results
server/environment.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/environment.py
2
+ from __future__ import annotations
3
+ import uuid
4
+ import random
5
+ from typing import Optional, Any, Dict
6
+ from openenv.core.env_server import Environment
7
+ from ..models import (
8
+ Action, Observation, State,
9
+ ProposeClarificationAction, ProposeNewRuleAction, EvolveProcessAction,
10
+ )
11
+ from .grader import grade
12
+ from .tasks import TASK_REGISTRY
13
+
14
+
15
+ class PolicyEvolverEnvironment(Environment[Action, Observation, State]):
16
+ """
17
+ Real-world environment: AI agent learns to evolve governance policies
18
+ through meta-reasoning over real-world data.
19
+ """
20
+ _instance = None
21
+
22
+ def __new__(cls, *args, **kwargs):
23
+ if cls._instance is None:
24
+ cls._instance = super().__new__(cls)
25
+ cls._instance._initialized = False
26
+ return cls._instance
27
+
28
+ def __init__(self):
29
+ if getattr(self, "_initialized", False):
30
+ return
31
+ super().__init__()
32
+ self._state = State()
33
+ self._current_task = None
34
+ self._initialized = True
35
+
36
+ def reset(
37
+ self,
38
+ seed: Optional[int] = None,
39
+ episode_id: Optional[str] = None,
40
+ **kwargs: Any,
41
+ ) -> Observation:
42
+ task_id = kwargs.get("task_id")
43
+ if task_id is None:
44
+ task_id = random.choice(list(TASK_REGISTRY.keys()))
45
+
46
+ task = TASK_REGISTRY[task_id]
47
+ self._current_task = task
48
+ self._state = State(
49
+ episode_id=episode_id or str(uuid.uuid4()),
50
+ task_id=task_id,
51
+ step_count=0,
52
+ max_steps=5,
53
+ current_score=0.0,
54
+ best_score=0.0,
55
+ actions_taken=[],
56
+ )
57
+
58
+ return Observation(
59
+ task_id=task_id,
60
+ episode_id=self._state.episode_id,
61
+ step_count=0,
62
+ data_corpus=task["data_corpus"],
63
+ current_policies=task["current_policies"],
64
+ policy_outcomes=task.get("policy_outcomes"),
65
+ system_metrics=task.get("system_metrics", {}),
66
+ identified_issues=task.get("identified_issues", []),
67
+ reward=0.0,
68
+ done=False,
69
+ info={"task_description": task["description"], "difficulty": task["difficulty"]},
70
+ )
71
+
72
+ def step(
73
+ self,
74
+ action: Action,
75
+ timeout_s: Optional[float] = None,
76
+ **kwargs: Any,
77
+ ) -> Observation:
78
+ if self._current_task is None:
79
+ raise RuntimeError("Call reset() before step()")
80
+
81
+ self._state.step_count += 1
82
+
83
+ # action can be a dict from the API or a Pydantic model
84
+ if isinstance(action, dict):
85
+ action_dict = action
86
+ else:
87
+ action_dict = action.model_dump() if hasattr(action, "model_dump") else dict(action)
88
+
89
+ reward = grade(action_dict, self._state.task_id)
90
+ self._state.current_score = reward
91
+ self._state.best_score = max(self._state.best_score, reward)
92
+
93
+ action_type = action_dict.get("action_type", "unknown") if isinstance(action_dict, dict) else "unknown"
94
+ self._state.actions_taken.append(action_type)
95
+
96
+ done = (
97
+ reward >= 0.90 or
98
+ self._state.step_count >= self._state.max_steps
99
+ )
100
+
101
+ return Observation(
102
+ task_id=self._state.task_id,
103
+ episode_id=self._state.episode_id,
104
+ step_count=self._state.step_count,
105
+ data_corpus=self._current_task["data_corpus"],
106
+ current_policies=self._current_task["current_policies"],
107
+ policy_outcomes=self._current_task.get("policy_outcomes"),
108
+ system_metrics=self._current_task.get("system_metrics", {}),
109
+ identified_issues=self._current_task.get("identified_issues", []),
110
+ reward=reward,
111
+ done=done,
112
+ info={
113
+ "best_score": self._state.best_score,
114
+ "steps_remaining": self._state.max_steps - self._state.step_count,
115
+ },
116
+ )
117
+
118
+ @property
119
+ def state(self) -> State:
120
+ return self._state
server/grader.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/grader.py
2
+ """
3
+ Deterministic grader for all three PolicyEvolverEnv tasks.
4
+ All functions return float in [0.0, 1.0].
5
+ """
6
+ from __future__ import annotations
7
+ import re
8
+ from typing import Dict, List, Any
9
+ from ..models import (
10
+ ProposeClarificationAction, ProposeNewRuleAction, EvolveProcessAction,
11
+ Observation
12
+ )
13
+ from .tasks import TASK_REGISTRY
14
+
15
+
16
+ # ─────────────────────────────────────────────
17
+ # Easy Task: Ambiguity Clarification
18
+ # ─────────────────────────────────────────────
19
+
20
+ def grade_clarification(action: ProposeClarificationAction, task: Dict) -> float:
21
+ """
22
+ Reward breakdown:
23
+ 0.35 — identified term is genuinely ambiguous (in known_ambiguous_terms)
24
+ 0.35 — definition is specific (≥12 words, contains measurement/criteria language)
25
+ 0.20 — justification addresses WHY term causes inconsistent moderation
26
+ 0.10 — think field provided (CoT bonus)
27
+ """
28
+ score = 0.0
29
+
30
+ # 0.35: Is the identified term actually ambiguous?
31
+ known = [t.lower() for t in task.get("known_ambiguous_terms", [])]
32
+ if action.ambiguous_term.lower() in known:
33
+ score += 0.35
34
+ else:
35
+ # Partial credit if it's a word that plausibly causes ambiguity
36
+ vague_words = ["reasonable", "substantial", "appropriate", "excessive", "significant",
37
+ "severe", "abusive", "hostile", "threatening", "offensive", "respectful"]
38
+ if any(w in action.ambiguous_term.lower() for w in vague_words):
39
+ score += 0.15
40
+
41
+ # 0.35: Definition quality
42
+ defn = action.suggested_definition
43
+ defn_score = 0.0
44
+ words = defn.split()
45
+ if len(words) >= 12:
46
+ defn_score += 0.10
47
+ criteria_words = ["includes", "means", "refers to", "defined as", "encompasses",
48
+ "specifically", "measurable", "example", "such as", "e.g."]
49
+ if any(w in defn.lower() for w in criteria_words):
50
+ defn_score += 0.15
51
+ action_words = ["will", "must", "shall", "is", "are", "requires"]
52
+ if any(w in defn.lower() for w in action_words):
53
+ defn_score += 0.10
54
+ score += min(defn_score, 0.35)
55
+
56
+ # 0.20: Justification quality
57
+ just = action.justification.lower()
58
+ just_score = 0.0
59
+ if len(action.justification.split()) >= 10:
60
+ just_score += 0.10
61
+ inconsistency_words = ["inconsistent", "vary", "subjective", "unclear", "different",
62
+ "interpret", "misapply", "dispute", "ambiguous"]
63
+ if any(w in just for w in inconsistency_words):
64
+ just_score += 0.10
65
+ score += min(just_score, 0.20)
66
+
67
+ # 0.10: CoT bonus
68
+ if action.think and len(action.think.strip()) > 20:
69
+ score += 0.10
70
+
71
+ return round(min(score, 1.0), 4)
72
+
73
+
74
+ # ─────────────────────────────────────────────
75
+ # Medium Task: Gap Detection + New Rule
76
+ # ─────────────────────────────────────────────
77
+
78
+ def grade_new_rule(action: ProposeNewRuleAction, task: Dict) -> float:
79
+ """
80
+ Reward breakdown:
81
+ 0.30 — rule_domain matches a genuinely uncovered domain
82
+ 0.30 — rule text is specific and actionable (not vague platitude)
83
+ 0.25 — scope covers multiple relevant scenarios
84
+ 0.05 — integration_points reference existing policies
85
+ 0.10 — think field provided (CoT bonus)
86
+ """
87
+ score = 0.0
88
+
89
+ # 0.30: Domain is genuinely uncovered
90
+ uncovered = [d.lower() for d in task.get("uncovered_domains", [])]
91
+ domain_lower = action.rule_domain.lower().replace(" ", "_")
92
+ if any(u in domain_lower or domain_lower in u for u in uncovered):
93
+ score += 0.30
94
+ else:
95
+ # Partial credit for related but not exact domain
96
+ related = ["ai", "artificial intelligence", "remote", "contractor", "freelance",
97
+ "gig", "machine learning", "automation", "offshore", "cross_border"]
98
+ if any(r in domain_lower for r in related):
99
+ score += 0.15
100
+
101
+ # 0.30: Rule text quality
102
+ rule = action.new_rule
103
+ rule_score = 0.0
104
+ if len(rule.split()) >= 15:
105
+ rule_score += 0.10
106
+ mandatory_words = ["must", "will", "shall", "required", "prohibited", "mandatory"]
107
+ if any(w in rule.lower() for w in mandatory_words):
108
+ rule_score += 0.10
109
+ conditional_words = ["when", "if", "unless", "in cases where", "prior to", "before"]
110
+ if any(w in rule.lower() for w in conditional_words):
111
+ rule_score += 0.10
112
+ # Penalise vague language
113
+ vague = ["may", "should consider", "might", "perhaps", "in some cases"]
114
+ if any(w in rule.lower() for w in vague):
115
+ rule_score -= 0.10
116
+ score += max(min(rule_score, 0.30), 0.0)
117
+
118
+ # 0.25: Scope covers multiple scenario types
119
+ if len(action.scope) >= 2:
120
+ score += 0.15
121
+ if len(action.scope) >= 4:
122
+ score += 0.10
123
+
124
+ # 0.05: Integration points reference existing policy IDs or domains
125
+ if action.integration_points and len(action.integration_points) >= 1:
126
+ score += 0.05
127
+
128
+ # 0.10: CoT bonus
129
+ if action.think and len(action.think.strip()) > 20:
130
+ score += 0.10
131
+
132
+ return round(min(score, 1.0), 4)
133
+
134
+
135
+ # ─────────────────────────────────────────────
136
+ # Hard Task: Holistic Policy Evolution
137
+ # ─────────────────────────────────────────────
138
+
139
+ def grade_evolution(action: EvolveProcessAction, task: Dict) -> float:
140
+ """
141
+ Reward breakdown:
142
+ 0.30 — ≥2 policy modifications; modifications address identified_issues
143
+ 0.25 — expected_outcomes are realistic and cover key metrics
144
+ 0.20 — rollback_conditions are specific (not generic)
145
+ 0.15 — justification addresses trade-offs (both sides)
146
+ 0.10 — think field provided (CoT bonus)
147
+ """
148
+ score = 0.0
149
+ identified_issues = [i["issue"].lower() for i in task.get("identified_issues", [])]
150
+ key_metrics = {o["metric"] for o in task.get("policy_outcomes", [])}
151
+
152
+ # 0.30: Modifications address real problems
153
+ mods = action.policy_modifications
154
+ mod_score = 0.0
155
+ if len(mods) >= 2:
156
+ mod_score += 0.15
157
+ # Check that at least one modification references a known policy ID or known issue
158
+ known_policy_ids = {p["id"] for p in task.get("current_policies", [])}
159
+ addressed = sum(1 for m in mods if m.policy_id in known_policy_ids or
160
+ any(kw in m.new_text.lower() for kw in
161
+ ["seasonal", "category", "foreign", "manual", "threshold", "volume"]))
162
+ if addressed >= 1:
163
+ mod_score += 0.10
164
+ if addressed >= 2:
165
+ mod_score += 0.05
166
+ score += min(mod_score, 0.30)
167
+
168
+ # 0.25: Expected outcomes realistic and cover key metrics
169
+ outcomes = action.expected_outcomes
170
+ outcome_score = 0.0
171
+ covered_metrics = {m for m in outcomes if m in key_metrics}
172
+ if len(covered_metrics) >= 2:
173
+ outcome_score += 0.15
174
+ # Values should be realistic deltas (not all 1.0)
175
+ non_trivial = sum(1 for v in outcomes.values() if 0.01 <= v <= 0.60)
176
+ if non_trivial >= 2:
177
+ outcome_score += 0.10
178
+ score += min(outcome_score, 0.25)
179
+
180
+ # 0.20: Rollback conditions are specific
181
+ rollbacks = action.rollback_conditions
182
+ rollback_score = 0.0
183
+ if len(rollbacks) >= 1:
184
+ rollback_score += 0.10
185
+ # Specific = contains a number or metric name
186
+ specific = sum(1 for r in rollbacks if
187
+ re.search(r'\d+', r) or
188
+ any(m in r.lower() for m in ["false positive", "fraud", "trust", "revenue", "queue"]))
189
+ if specific >= 1:
190
+ rollback_score += 0.10
191
+ score += min(rollback_score, 0.20)
192
+
193
+ # 0.15: Justification addresses trade-offs
194
+ just = action.justification.lower()
195
+ trade_off_pairs = [
196
+ (["precision", "accuracy", "false positive"], ["recall", "coverage", "missed"]),
197
+ (["seller trust", "legitimate"], ["fraud", "detection"]),
198
+ (["automation", "efficiency"], ["manual", "review"]),
199
+ ]
200
+ tradeoffs_found = 0
201
+ for side_a, side_b in trade_off_pairs:
202
+ if any(w in just for w in side_a) and any(w in just for w in side_b):
203
+ tradeoffs_found += 1
204
+ if tradeoffs_found >= 1:
205
+ score += 0.10
206
+ if tradeoffs_found >= 2:
207
+ score += 0.05
208
+
209
+ # 0.10: CoT bonus
210
+ if action.think and len(action.think.strip()) > 20:
211
+ score += 0.10
212
+
213
+ return round(min(score, 1.0), 4)
214
+
215
+
216
+ # ─────────────────────────────────────────────
217
+ # Dispatcher
218
+ # ─────────────────────────────────────────────
219
+
220
+ def grade(action_dict: Dict, task_id: str) -> float:
221
+ """
222
+ Main entry point called by /grader endpoint.
223
+ action_dict: the raw JSON body from the agent
224
+ task_id: "task_easy" | "task_medium" | "task_hard"
225
+ Returns float in [0.0, 1.0] — always clamped.
226
+ """
227
+ task = TASK_REGISTRY.get(task_id)
228
+ if task is None:
229
+ return 0.0
230
+
231
+ try:
232
+ action_type = action_dict.get("action_type")
233
+ if action_type == "propose_clarification":
234
+ action = ProposeClarificationAction(**action_dict)
235
+ raw = grade_clarification(action, task)
236
+ elif action_type == "propose_new_rule":
237
+ action = ProposeNewRuleAction(**action_dict)
238
+ raw = grade_new_rule(action, task)
239
+ elif action_type == "evolve_policy":
240
+ action = EvolveProcessAction(**action_dict)
241
+ raw = grade_evolution(action, task)
242
+ else:
243
+ return 0.0
244
+ except Exception:
245
+ return 0.0
246
+
247
+ return max(0.0, min(1.0, raw))
server/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core>=0.1.0
2
+ pydantic>=2.9.2
3
+ pydantic-settings>=2.6.1
4
+ fastapi>=0.115.4
5
+ uvicorn>=0.32.0
6
+ scikit-learn>=1.5.2
7
+ numpy>=1.26.4
8
+ openai>=1.54.4
9
+ httpx>=0.27.0
10
+ python-dotenv>=1.0.1
server/task_generator.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/task_generator.py
2
+ """
3
+ Procedural policy scenario generator.
4
+ Generates variants of each task by swapping domains, severity, and actor types.
5
+ Activated by calling generate_task_variants() and updating TASK_REGISTRY.
6
+ """
7
+ from __future__ import annotations
8
+ import random
9
+ from typing import Iterator, Dict
10
+
11
+ DOMAIN_VARIANTS = {
12
+ "social_media": ["gaming_platform", "professional_network", "dating_app", "news_forum"],
13
+ "corporate_hr": ["startup_culture", "remote_first_company", "government_agency", "university"],
14
+ "ecommerce": ["marketplace", "subscription_service", "auction_platform", "b2b_procurement"],
15
+ }
16
+
17
+ ACTOR_VARIANTS = ["new_user", "power_user", "verified_creator", "anonymous_account", "enterprise_client"]
18
+
19
+
20
+ def generate_easy_variant(base_task: Dict, domain: str) -> Dict:
21
+ variant = dict(base_task)
22
+ variant["task_id"] = f"task_easy_{domain.replace(' ', '_')}"
23
+ variant["description"] = base_task["description"].replace("social media platform", domain)
24
+ return variant
25
+
26
+
27
+ def generate_task_variants(base_task: Dict, n: int = 5) -> Iterator[Dict]:
28
+ domain_list = list(DOMAIN_VARIANTS.values())
29
+ all_domains = [d for sublist in domain_list for d in sublist]
30
+ for i in range(min(n, len(all_domains))):
31
+ domain = all_domains[i]
32
+ yield generate_easy_variant(base_task, domain)
server/tasks/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/tasks/__init__.py
2
+ from .task_easy import EASY_TASK
3
+ from .task_medium import MEDIUM_TASK
4
+ from .task_hard import HARD_TASK
5
+
6
+ TASK_REGISTRY = {
7
+ "task_easy": EASY_TASK,
8
+ "task_medium": MEDIUM_TASK,
9
+ "task_hard": HARD_TASK,
10
+ }
server/tasks/task_easy.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/tasks/task_easy.py
2
+ EASY_TASK = {
3
+ "task_id": "task_easy",
4
+ "difficulty": "easy",
5
+ "description": (
6
+ "A social media platform has vague community guidelines. "
7
+ "Identify ONE genuinely ambiguous term and propose a specific, "
8
+ "measurable definition that moderators can apply consistently."
9
+ ),
10
+ "data_corpus": [
11
+ {"id": "post_001", "text": "You're so ugly, go back to where you came from", "action_taken": "removed"},
12
+ {"id": "post_002", "text": "I hate Mondays so much", "action_taken": "allowed"},
13
+ {"id": "post_003", "text": "This politician is a clown and should resign", "action_taken": "allowed"},
14
+ {"id": "post_004", "text": "Women shouldn't be in leadership roles", "action_taken": "removed"},
15
+ {"id": "post_005", "text": "I can't stand my neighbor's dog barking", "action_taken": "allowed"},
16
+ {"id": "post_006", "text": "People like you ruin everything", "action_taken": "flagged"},
17
+ {"id": "post_007", "text": "Go to hell with your stupid opinions", "action_taken": "flagged"},
18
+ {"id": "post_008", "text": "This food is disgusting, whoever made it should be ashamed", "action_taken": "allowed"},
19
+ ],
20
+ "current_policies": [
21
+ {"id": "pol_001", "text": "Users must not post content that is offensive or inappropriate."},
22
+ {"id": "pol_002", "text": "Harassment of any kind is strictly prohibited."},
23
+ {"id": "pol_003", "text": "Content that promotes hate speech will be removed."},
24
+ {"id": "pol_004", "text": "Users should communicate in a respectful manner."},
25
+ ],
26
+ "known_ambiguous_terms": ["offensive", "inappropriate", "harassment", "hate speech", "respectful"],
27
+ "num_policies": 4,
28
+ "num_data_points": 8,
29
+ }
server/tasks/task_hard.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/tasks/task_hard.py
2
+ HARD_TASK = {
3
+ "task_id": "task_hard",
4
+ "difficulty": "hard",
5
+ "description": (
6
+ "An e-commerce platform's Trust & Safety policy framework is underperforming. "
7
+ "Historical simulation data shows high false positive rates (legitimate sellers "
8
+ "being flagged) and missed fraud cases. Evolve the policy framework to improve "
9
+ "both precision and recall while maintaining seller trust. You must propose "
10
+ "modifications to at least 2 existing policies and justify trade-offs."
11
+ ),
12
+ "data_corpus": [
13
+ {"id": "seller_001", "type": "legitimate", "flags": ["new_account", "high_volume"], "outcome": "wrongly_suspended"},
14
+ {"id": "seller_002", "type": "fraudulent", "flags": ["price_manipulation"], "outcome": "missed"},
15
+ {"id": "seller_003", "type": "legitimate", "flags": ["foreign_bank"], "outcome": "wrongly_suspended"},
16
+ {"id": "seller_004", "type": "fraudulent", "flags": ["fake_reviews", "new_account"], "outcome": "correctly_caught"},
17
+ {"id": "seller_005", "type": "legitimate", "flags": ["high_returns"], "outcome": "wrongly_suspended"},
18
+ {"id": "seller_006", "type": "fraudulent", "flags": ["stolen_card_payments"], "outcome": "missed"},
19
+ {"id": "seller_007", "type": "fraudulent", "flags": ["counterfeit_goods"], "outcome": "missed"},
20
+ {"id": "seller_008", "type": "legitimate", "flags": ["seasonal_spike"], "outcome": "wrongly_suspended"},
21
+ ],
22
+ "current_policies": [
23
+ {"id": "ts_pol_001", "text": "Any new seller account with more than 50 transactions in the first week will be suspended for review."},
24
+ {"id": "ts_pol_002", "text": "Sellers with a return rate above 15% will be flagged for investigation."},
25
+ {"id": "ts_pol_003", "text": "Sellers using non-domestic bank accounts will require manual approval."},
26
+ {"id": "ts_pol_004", "text": "Any account with 3 or more fraud reports in 30 days will be permanently banned."},
27
+ {"id": "ts_pol_005", "text": "Price changes of more than 20% within 24 hours will trigger an automatic hold."},
28
+ {"id": "ts_pol_006", "text": "Sellers receiving 5+ negative reviews in 7 days will be suspended pending review."},
29
+ ],
30
+ "policy_outcomes": [
31
+ {"metric": "false_positive_rate", "value": 0.42, "target": 0.10},
32
+ {"metric": "fraud_detection_rate", "value": 0.31, "target": 0.85},
33
+ {"metric": "seller_trust_score", "value": 0.54, "target": 0.80},
34
+ {"metric": "review_queue_overload", "value": 0.89, "target": 0.30},
35
+ {"metric": "legitimate_revenue_lost", "value": 0.28, "target": 0.05},
36
+ ],
37
+ "system_metrics": {
38
+ "false_positive_rate": 0.42,
39
+ "fraud_detection_rate": 0.31,
40
+ "seller_trust_score": 0.54,
41
+ "review_queue_overload": 0.89,
42
+ },
43
+ "identified_issues": [
44
+ {"issue": "Blanket new-account volume rule catches legitimate seasonal sellers"},
45
+ {"issue": "Return rate threshold doesn't distinguish category (electronics vs. fashion)"},
46
+ {"issue": "Manual approval bottleneck creates 14-day delays for legitimate foreign sellers"},
47
+ ],
48
+ "num_policies": 6,
49
+ "num_data_points": 8,
50
+ }
server/tasks/task_medium.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server/tasks/task_medium.py
2
+ MEDIUM_TASK = {
3
+ "task_id": "task_medium",
4
+ "difficulty": "medium",
5
+ "description": (
6
+ "A corporate HR policy set is missing rules for several emerging workplace "
7
+ "scenarios involving AI tools, remote work, and gig workers. "
8
+ "Identify ONE genuine policy gap and propose a specific new rule to address it."
9
+ ),
10
+ "data_corpus": [
11
+ {"id": "incident_001", "type": "AI_use", "desc": "Employee used ChatGPT to write client proposal without disclosure"},
12
+ {"id": "incident_002", "type": "remote_work", "desc": "Employee attended video call from a coffee shop, client data visible on screen"},
13
+ {"id": "incident_003", "type": "gig_worker", "desc": "Contractor accessed proprietary codebase after project ended"},
14
+ {"id": "incident_004", "type": "AI_use", "desc": "Manager used AI to generate performance review for employee"},
15
+ {"id": "incident_005", "type": "remote_work", "desc": "Employee shared screen showing salary data while on public WiFi"},
16
+ {"id": "incident_006", "type": "gig_worker", "desc": "Freelancer posted client project on portfolio without permission"},
17
+ {"id": "incident_007", "type": "AI_use", "desc": "Employee submitted AI-written code as their own in performance evaluation"},
18
+ {"id": "incident_008", "type": "remote_work", "desc": "Employee worked from another country for 3 months without HR approval"},
19
+ {"id": "incident_009", "type": "gig_worker", "desc": "Contractor attended team standup but was also working for a direct competitor"},
20
+ {"id": "incident_010", "type": "AI_use", "desc": "HR used AI tool to screen resumes — potential bias concerns raised"},
21
+ ],
22
+ "current_policies": [
23
+ {"id": "pol_hr_001", "text": "Employees must maintain confidentiality of client information at all times."},
24
+ {"id": "pol_hr_002", "text": "All employees are expected to comply with the company code of conduct."},
25
+ {"id": "pol_hr_003", "text": "Contractors must sign an NDA before beginning any project."},
26
+ {"id": "pol_hr_004", "text": "Employees working remotely must have a secure, dedicated workspace."},
27
+ {"id": "pol_hr_005", "text": "Any intellectual property created during employment belongs to the company."},
28
+ ],
29
+ "uncovered_domains": ["AI_use", "gig_worker_post_engagement", "cross_border_remote"],
30
+ "num_policies": 5,
31
+ "num_data_points": 10,
32
+ }
test_result.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "baseline_scores": {
3
+ "task_easy": 0.0,
4
+ "task_medium": 0.0,
5
+ "task_hard": 0.0,
6
+ "overall_avg": 0.0
7
+ },
8
+ "mode": "llm",
9
+ "model": "meta-llama/Llama-3.3-70B-Instruct",
10
+ "runtime_seconds": 0.01,
11
+ "detail": [
12
+ {
13
+ "task_id": "task_easy",
14
+ "reward": 0.0,
15
+ "mode": "error",
16
+ "error": "All connection attempts failed"
17
+ },
18
+ {
19
+ "task_id": "task_medium",
20
+ "reward": 0.0,
21
+ "mode": "error",
22
+ "error": "All connection attempts failed"
23
+ },
24
+ {
25
+ "task_id": "task_hard",
26
+ "reward": 0.0,
27
+ "mode": "error",
28
+ "error": "All connection attempts failed"
29
+ }
30
+ ]
31
+ }