File size: 11,335 Bytes
e5e36db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
---
title: InformationIntegrityEnv
emoji: πŸ”
colorFrom: blue
colorTo: green
sdk: gradio
sdk_version: 6.11.0
app_file: app.py
pinned: false
license: mit
tags:
  - openenv
  - content-moderation
  - rl-environment
  - agent-benchmark
---

# InformationIntegrityEnv

A **deterministic, benchmarkable OpenEnv environment** for evaluating LLM agents on reasoning-based content moderation.

Agents must verify claims, weigh conflicting evidence, and make explainable moderation decisions (`allow` / `flag` / `remove`). Three difficulty tiers β€” easy, medium, hard β€” provide 15 fully static scenarios that run completely offline.

---

## Architecture

```
tasks/
  easy/scenarios.json     ← 5 static scenarios (clear-cut misinformation)
  medium/scenarios.json   ← 5 static scenarios (misleading / mixed evidence)
  hard/scenarios.json     ← 5 static scenarios (conflicting evidence, nuanced)
  graders.py              ← EasyGrader / MediumGrader / HardGrader
  registry.py             ← TASKS["easy"] / TASKS["medium"] / TASKS["hard"]
        β”‚
        β–Ό
InformationIntegrityEnv         (environment.py)
  reset(difficulty, scenario_id) β†’ Observation
  step(action)                   β†’ (Observation, Reward, done, info)
  state()                        β†’ full debug state
        β”‚
  β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
  β”‚            β”‚
inference.py   server.py
(benchmark     (FastAPI
 agent loop)    REST API)
```

No external API calls occur during evaluation. `DataCollector` is only used to build/refresh datasets.

---

## Quick Start

### 1. Install

```bash
pip install -r requirements.txt
```

### 2. Set API keys (for the LLM agent β€” not needed for just the server)

```bash
export HF_TOKEN=sk-...          # or OPENAI_API_KEY
export MODEL_NAME=gpt-4o
export API_BASE_URL=https://api.openai.com/v1
```

### 3. Run the full benchmark

```bash
python inference.py --benchmark --out results.json
```

This loops over all 15 scenarios (5 per difficulty) and prints:

```
[START] episode=E01 difficulty=easy claims=1 sources=5
[STEP]  step=1 action=check_source reward=+0.200 total=+0.200 done=False ...
[STEP]  step=2 action=verify_claim  reward=+0.200 total=+0.400 done=False ...
[STEP]  step=3 action=decide        reward=+1.000 total=+1.400 done=True  ...
[END]   episode=E01 steps=3 decision=remove total_reward=+1.4000 grader_score=0.8750

...

difficulty=easy   avg_score=0.7800  avg_reward=1.2000  n=5
difficulty=medium avg_score=0.6300  avg_reward=0.9000  n=5
difficulty=hard   avg_score=0.5200  avg_reward=0.6500  n=5
Difficulty tiers well separated: easy > medium > hard βœ“
```

### 4. Single episode (local, no server)

```bash
# Random scenario from medium dataset
python inference.py --local --difficulty medium

# Specific scenario by index
python inference.py --local --difficulty hard --scenario-id 2

# Specific scenario by id
python inference.py --local --difficulty easy --scenario-id easy_001

# From a custom JSON file (DataCollector output)
python inference.py --local --scenario my_scenario.json
```

### 5. Start the API server

```bash
python server.py
# or
uvicorn server:app --reload
```

---

## Action Space

| Action | Required fields | Description |
|--------|----------------|-------------|
| `verify_claim` | `claim_id` | Investigate a claim; returns true / false / partially_true / unknown |
| `check_source` | `source_id` | Read a source; returns title, text, stance, reliability |
| `decide` | `decision` | Final call: `allow` / `flag` / `remove` β€” ends episode |

All actions accept an optional `reasoning` string (logged, not graded).

**Minimum evidence required before deciding** (or premature penalty applies):
- β‰₯ 1 claim verified
- β‰₯ 2 sources checked

---

## Observation Space

```json
{
  "content": "The post text under review",
  "claims": [
    {"id": "claim_001", "text": "...", "verification_status": "unverified", "confidence": 0.0}
  ],
  "available_sources": [
    {"id": "src_001", "title": "...", "text": "...", "url": "...",
     "stance": "contradicting", "reliability": 0.97, "source_type": "official"}
  ],
  "interaction_history": [],
  "status_flags": {
    "decision_made": false,
    "sufficient_evidence": false,
    "premature_decision": false,
    "redundant_actions": 0,
    "max_steps_reached": false
  },
  "step_count": 0,
  "task_difficulty": "medium",
  "episode_id": "uuid"
}
```

---

## Reward Function

| Component | Value | Trigger |
|-----------|-------|---------|
| Verify claim | +0.20 | Per unique claim verified |
| Check source | +0.20 | Per unique source checked |
| Multi-source bonus | +0.20 | When β‰₯ 3 unique sources checked |
| Correct decision | +1.00 | Exact ground truth match |
| Partial decision | +0.50 | One tier off (flag↔remove, flag↔allow) |
| Premature decision | βˆ’0.50 | Deciding before sufficient evidence |
| Redundant action | βˆ’0.10 | Re-checking same claim/source |
| Inefficiency | βˆ’0.05 | Per step beyond efficient threshold |
| Over-aggression | βˆ’0.30 | `remove` when ground truth is `allow`/`flag` |

Total clipped to `[-2.0, +2.0]`. All values configurable in `reward_config.yaml`.

---

## Grader Scoring

Each episode is scored `[0.0, 1.0]` across four dimensions:

| Dimension | Easy | Medium | Hard |
|-----------|------|--------|------|
| Decision correctness | 50% | 35% | 30% |
| Evidence breadth | 25% | 35% | 30% |
| Reasoning depth | 15% | 20% | 30% |
| Efficiency | 10% | 10% | 10% |

---

## Task Datasets

### Easy (5 scenarios)
Clear-cut false claims contradicted by multiple authoritative sources (WHO, CDC, NASA, FDA).
Ground truth: 3Γ— remove, 1Γ— flag, 1Γ— allow.

| ID | Topic | Ground Truth |
|----|-------|-------------|
| easy_001 | Bleach cures COVID | remove |
| easy_002 | NASA fabricated climate data | remove |
| easy_003 | mRNA vaccines alter DNA | remove |
| easy_004 | Sydney is Australia's capital | flag |
| easy_005 | Webb telescope early universe | allow |

### Medium (5 scenarios)
Partially true or misleadingly framed content. Mixed evidence requires weighing.
Ground truth: 4Γ— flag, 1Γ— allow.

| ID | Topic | Ground Truth |
|----|-------|-------------|
| medium_001 | Sugar more addictive than cocaine | flag |
| medium_002 | Real unemployment is 20%+ | flag |
| medium_003 | AI will eliminate 85M jobs | flag |
| medium_004 | Pharma knew OxyContin was addictive | allow |
| medium_005 | Facebook hid teen mental health data | flag |

### Hard (5 scenarios)
Conflicting credible evidence, genuinely contested claims, contested policy debates.
Ground truth: 5Γ— flag.

| ID | Topic | Ground Truth |
|----|-------|-------------|
| hard_001 | NATO provoked Ukraine invasion | flag |
| hard_002 | Lockdowns killed more than COVID | flag |
| hard_003 | SEC crackdown is banking conspiracy | flag |
| hard_004 | AI poses extinction-level risk | flag |
| hard_005 | British colonialism net beneficial | flag |

---

## API Reference

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/health` | Liveness probe |
| GET | `/tasks` | List all available scenario datasets |
| POST | `/reset` | Start a new episode |
| POST | `/step` | Submit one action |
| GET | `/state/{session_id}` | Full internal state (debug/audit) |
| GET | `/scenario/{session_id}` | Scenario metadata (no ground truth) |
| DELETE | `/session/{session_id}` | End session and free memory |
| POST | `/collect` | Build a live scenario via DataCollector (dataset creation only) |

### Reset β€” evaluation path (no DataCollector needed)

```bash
# Random scenario from easy dataset
curl -X POST http://localhost:8000/reset \
  -H "Content-Type: application/json" \
  -d '{"difficulty": "easy"}'

# Specific scenario by index
curl -X POST http://localhost:8000/reset \
  -d '{"difficulty": "medium", "scenario_id": 2}'

# Specific scenario by id
curl -X POST http://localhost:8000/reset \
  -d '{"difficulty": "hard", "scenario_id": "hard_001"}'
```

### Full episode example

```bash
SESSION=$(curl -s -X POST http://localhost:8000/reset \
  -H "Content-Type: application/json" \
  -d '{"difficulty": "easy"}' | jq -r .session_id)

# Check a source
curl -X POST http://localhost:8000/step \
  -d "{\"session_id\": \"$SESSION\", \"action\": {\"type\": \"check_source\", \"source_id\": \"src_001\"}}"

# Verify a claim
curl -X POST http://localhost:8000/step \
  -d "{\"session_id\": \"$SESSION\", \"action\": {\"type\": \"verify_claim\", \"claim_id\": \"claim_001\"}}"

# Decide
curl -X POST http://localhost:8000/step \
  -d "{\"session_id\": \"$SESSION\", \"action\": {\"type\": \"decide\", \"decision\": \"remove\"}}"
```

---

## Docker

### Server (default)

```bash
docker build -t info-integrity-env .
docker run --env-file .env -p 8000:8000 info-integrity-env
```

### Benchmark (offline β€” no external services needed)

```bash
docker run --env-file .env info-integrity-env \
  python inference.py --benchmark --out /tmp/results.json
```

### Full stack with Redis (for live data collection)

```bash
docker compose up --build
```

---

## Baseline Results

| Difficulty | Avg Grader Score | Avg Reward | Decision Accuracy |
|-----------|-----------------|------------|-----------------|
| Easy | ~0.78 | ~1.20 | ~85% |
| Medium | ~0.63 | ~0.90 | ~70% |
| Hard | ~0.52 | ~0.65 | ~55% |

*Results vary by LLM. Scores above from `gpt-4o` with temperature=0.*

---

## Extending the Environment

| File | What to change |
|------|---------------|
| `tasks/{difficulty}/scenarios.json` | Add or edit scenarios (run `openenv validate` after) |
| `reward_config.yaml` | Tune reward/penalty magnitudes |
| `tasks/graders.py` | Register custom graders: `GraderFactory.register("hard_v2", MyGrader)` |
| `data_collector.py` | Add new search backends via `SearchBackend` ABC |
| `domain_reliability.yaml` | Add trusted domains with custom reliability scores |

---

## Project Structure

```
β”œβ”€β”€ models.py                    Pydantic models (Observation, Action, Reward, Scenario, ...)
β”œβ”€β”€ environment.py               InformationIntegrityEnv (OpenEnv core)
β”œβ”€β”€ reward.py                    Configurable dense reward function
β”œβ”€β”€ explainability.py            Structured reasoning + template-based explanation
β”œβ”€β”€ data_collector.py            Multi-backend data collection (dataset creation only)
β”œβ”€β”€ inference.py                 Benchmark agent + single-episode runner
β”œβ”€β”€ server.py                    FastAPI server (HF Space / Docker)
β”œβ”€β”€ tasks/
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ graders.py               Easy / Medium / Hard deterministic graders
β”‚   β”œβ”€β”€ registry.py              TASKS registry β€” lazy-loads scenario datasets
β”‚   β”œβ”€β”€ easy/
β”‚   β”‚   └── scenarios.json       5 easy scenarios
β”‚   β”œβ”€β”€ medium/
β”‚   β”‚   └── scenarios.json       5 medium scenarios
β”‚   └── hard/
β”‚       └── scenarios.json       5 hard scenarios
β”œβ”€β”€ openenv.yaml                 OpenEnv v1.1.0 specification
β”œβ”€β”€ reward_config.yaml           Reward magnitudes (runtime-configurable)
β”œβ”€β”€ domain_reliability.yaml      Domain trust scores for DataCollector
β”œβ”€β”€ Dockerfile                   Production container
β”œβ”€β”€ docker-compose.yml           Local dev stack (server + Redis)
└── requirements.txt
```