Spaces:
Sleeping
Sleeping
Commit ·
8cd3fa7
0
Parent(s):
Initial commit of PolicyEvolverEnv - Meta Hackathon
Browse files- .dockerignore +12 -0
- .gitignore +15 -0
- README.md +95 -0
- __init__.py +20 -0
- client.py +15 -0
- inference.py +530 -0
- models.py +82 -0
- openenv.yaml +77 -0
- pyproject.toml +28 -0
- rule_output3.json +6 -0
- server/Dockerfile +20 -0
- server/__init__.py +1 -0
- server/app.py +63 -0
- server/environment.py +120 -0
- server/grader.py +247 -0
- server/requirements.txt +10 -0
- server/task_generator.py +32 -0
- server/tasks/__init__.py +10 -0
- server/tasks/task_easy.py +29 -0
- server/tasks/task_hard.py +50 -0
- server/tasks/task_medium.py +32 -0
- test_result.json +31 -0
.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 |
+
}
|