VenuGopal8115 commited on
Commit
963884c
·
0 Parent(s):

Clean initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .cache/
5
+ *.egg-info/
6
+ .env
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+ ENV HF_DATASETS_CACHE=/app/.cache
5
+
6
+ WORKDIR /app
7
+
8
+ # Copy requirements first to leverage Docker layer caching
9
+ COPY requirements.txt .
10
+
11
+ # Install dependencies
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy the rest of the project files
15
+ COPY . .
16
+
17
+ # Expose HuggingFace Spaces default port
18
+ EXPOSE 7860
19
+
20
+ # Start the FastAPI server
21
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🏷️ LabelSense — AI-Assisted Data Labeling QA Environment
2
+
3
+ > An OpenEnv-compliant environment where an AI agent audits AI-generated labels on medical and legal text, identifies mislabeled examples, flags ambiguous cases, and proposes corrections.
4
+
5
+ ---
6
+
7
+ ## Why This Exists
8
+
9
+ AI models are increasingly used to auto-label training data at scale. The problem: label quality is inconsistent, and **confidently wrong labels corrupt downstream models silently**. No one catches them until the model is already in production.
10
+
11
+ LabelSense gives agents a structured environment to practice exactly this — reviewing batches of AI-generated labels, catching errors, and making calibrated decisions about ambiguity. It maps directly to a real MLOps workflow that every team doing data labeling at scale deals with daily.
12
+
13
+ ---
14
+
15
+ ## Environment Overview
16
+
17
+ The agent is shown examples from real medical and legal datasets, each pre-labeled by a simulated AI labeler (with injected noise). The agent must:
18
+
19
+ - Decide if each label is **correct**, **wrong**, or **ambiguous**
20
+ - If wrong — propose the **correct label**
21
+ - Signal **confidence** in its decision
22
+
23
+ The environment scores the agent on accuracy, calibration, and how responsibly it handles uncertainty.
24
+
25
+ ---
26
+
27
+ ## Datasets
28
+
29
+ | Task | Dataset | Source | Label Type |
30
+ |------|---------|--------|------------|
31
+ | Easy | Medical Question Pairs | `curaihealth/medical_questions_pairs` | Binary: similar / not similar |
32
+ | Medium | Stanford NLI | `snli` | 3-class: entailment / neutral / contradiction |
33
+ | Hard | SCOTUS (LexGLUE) | `coastalcph/lex_glue` (scotus) | 14-class: Supreme Court issue areas |
34
+
35
+ ---
36
+
37
+ ## Action & Observation Space
38
+
39
+ ### Observation
40
+ What the agent sees at each step:
41
+
42
+ ```json
43
+ {
44
+ "example_id": "task1_042",
45
+ "task": "easy",
46
+ "input": {
47
+ "text1": "Does ibuprofen reduce fever?",
48
+ "text2": "Can ibuprofen be used to treat high temperature?"
49
+ },
50
+ "ai_label": "not_similar",
51
+ "label_options": ["similar", "not_similar"]
52
+ }
53
+ ```
54
+
55
+ ### Action
56
+ What the agent responds with:
57
+
58
+ ```json
59
+ {
60
+ "example_id": "task1_042",
61
+ "verdict": "wrong",
62
+ "proposed_label": "similar",
63
+ "confidence": 0.91
64
+ }
65
+ ```
66
+
67
+ `verdict` must be one of: `correct`, `wrong`, `ambiguous`
68
+
69
+ ---
70
+
71
+ ## Reward Function
72
+
73
+ | Agent Action | Condition | Reward |
74
+ |---|---|---|
75
+ | Flags label as wrong | Label is actually wrong | +1.0 |
76
+ | Proposes correct fix | Fix matches gold label | +0.5 bonus |
77
+ | Flags as ambiguous | Example is genuinely ambiguous | +0.7 |
78
+ | Flags as ambiguous | Example is actually clear | -0.3 |
79
+ | Marks wrong label as correct | Misses the error | 0.0 |
80
+ | Proposes wrong fix confidently | High confidence, wrong answer | -0.5 |
81
+
82
+ Rewards are designed to encourage **calibrated uncertainty** — an agent that admits it doesn't know scores better than one that guesses confidently and gets it wrong.
83
+
84
+ ---
85
+
86
+ ## Tasks
87
+
88
+ ### Task 1 — Easy: Medical Question Pair Similarity
89
+ **Dataset:** `curaihealth/medical_questions_pairs`
90
+ **Label type:** Binary (0 = not similar, 1 = similar)
91
+ **Noise:** ~20% random label flips on clear-cut examples
92
+ **Expected agent score:** 0.75 – 0.90
93
+ **What makes it easy:** Labels are mostly unambiguous. Errors are random, not systematic.
94
+
95
+ ### Task 2 — Medium: Natural Language Inference
96
+ **Dataset:** `snli`
97
+ **Label type:** 3-class (entailment / neutral / contradiction)
98
+ **Noise:** Systematic bias — AI labeler over-predicts "neutral" when uncertain
99
+ **Expected agent score:** 0.55 – 0.75
100
+ **What makes it medium:** Requires understanding sentence-level logic. Neutral vs. contradiction is a common confusion point.
101
+
102
+ ### Task 3 — Hard: SCOTUS Legal Issue Classification
103
+ **Dataset:** `coastalcph/lex_glue` (scotus config)
104
+ **Label type:** 14-class Supreme Court issue areas
105
+ **Noise:** Confident wrong labels on edge cases, near-duplicate category confusion
106
+ **Expected agent score:** 0.30 – 0.55
107
+ **What makes it hard:** 14 overlapping legal categories. AI labeler is confidently wrong, not randomly wrong. Requires legal domain reasoning.
108
+
109
+ ---
110
+
111
+ ## API Endpoints
112
+
113
+ | Method | Endpoint | Description |
114
+ |--------|----------|-------------|
115
+ | POST | `/reset` | Start a new episode, returns first observation |
116
+ | POST | `/step` | Submit an action, returns next observation + reward |
117
+ | GET | `/state` | Returns current episode state and progress |
118
+ | GET | `/tasks` | Lists all tasks and their action schemas |
119
+ | POST | `/grader` | Returns grader score after episode completes |
120
+ | POST | `/baseline` | Runs baseline inference script, returns scores for all 3 tasks |
121
+
122
+ ---
123
+
124
+ ## Setup & Usage
125
+
126
+ ### Local (Python)
127
+
128
+ ```bash
129
+ # Clone the repo
130
+ git clone https://github.com/yourusername/labelsense-openenv
131
+ cd labelsense-openenv
132
+
133
+ # Create and activate virtual environment
134
+ python -m venv venv
135
+ source venv/bin/activate # Windows: venv\Scripts\activate
136
+
137
+ # Install dependencies
138
+ pip install -r requirements.txt
139
+
140
+ # Start the API server
141
+ uvicorn main:app --reload
142
+ ```
143
+
144
+ ### Docker
145
+
146
+ ```bash
147
+ docker build -t labelsense-env .
148
+ docker run -p 8000:8000 labelsense-env
149
+ ```
150
+
151
+ ### Run Baseline
152
+
153
+ ```bash
154
+ export OPENAI_API_KEY=your_key_here
155
+ python baseline.py
156
+ ```
157
+
158
+ ---
159
+
160
+ ## OpenEnv Spec Compliance
161
+
162
+ - Typed Pydantic models for `Observation`, `Action`, `Reward`
163
+ - `reset()` → returns clean initial observation
164
+ - `step(action)` → returns observation, reward, done, info
165
+ - `state()` → returns current episode state
166
+ - `openenv.yaml` with full metadata
167
+ - Validated with `openenv validate`
168
+
169
+ ---
170
+
171
+ ## Baseline Scores
172
+
173
+ | Task | Model | Cumulative Score (10 steps) |
174
+ |------|-------|-----------------------------|
175
+ | Easy (medical pairs) | llama-3.1-8b-instant | 4.30 |
176
+ | Medium (NLI) | llama-3.1-8b-instant | 2.00 |
177
+ | Hard (SCOTUS) | llama-3.1-8b-instant | 4.90 |
178
+ | **Overall Average** | | **3.73** |
179
+
180
+ *Scores represent cumulative reward over 10 steps per task. Maximum possible score per task is 10.0 (all correct with fixes). Scores above 0 indicate the agent performs better than random.*
181
+
182
+ ---
183
+
184
+ ## Project Structure
185
+
186
+ ```
187
+ labelsense-openenv/
188
+ ├── environment/
189
+ │ ├── env.py # Core environment — reset(), step(), state()
190
+ │ ├── models.py # Pydantic models: Observation, Action, Reward
191
+ │ ├── grader.py # Per-task scoring logic
192
+ │ └── noise.py # AI labeler noise injection
193
+ ├── tasks/
194
+ │ ├── task1_easy.py
195
+ │ ├── task2_medium.py
196
+ │ └── task3_hard.py
197
+ ├── data/
198
+ │ └── loader.py # HuggingFace dataset loading + sampling
199
+ ├── main.py # FastAPI app + all endpoints
200
+ ├── baseline.py # Baseline inference script (OpenAI API)
201
+ ├── openenv.yaml # OpenEnv spec metadata
202
+ ├── Dockerfile
203
+ └── README.md
204
+ ```
205
+
206
+ ---
207
+
208
+ ## License
209
+
210
+ MIT
baseline.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ import requests
5
+ from groq import Groq
6
+
7
+ API_URL = "http://localhost:8000"
8
+
9
+ def parse_json_response(text: str) -> dict:
10
+ try:
11
+ text = text.strip()
12
+ if text.startswith("```"):
13
+ lines = text.split("\n")
14
+ if len(lines) >= 2:
15
+ # Remove starting and ending markdown fences
16
+ if lines[-1].strip() == "```":
17
+ text = "\n".join(lines[1:-1])
18
+ else:
19
+ text = "\n".join(lines[1:])
20
+
21
+ parsed = json.loads(text)
22
+
23
+ return {
24
+ "verdict": parsed.get("verdict", "ambiguous"),
25
+ "proposed_label": parsed.get("proposed_label"),
26
+ "confidence": float(parsed.get("confidence", 0.5))
27
+ }
28
+ except Exception:
29
+ # Default back to ambiguous on parse failure
30
+ return {"verdict": "ambiguous", "proposed_label": None, "confidence": 0.5}
31
+
32
+ def run_baseline() -> dict:
33
+ api_key = os.environ.get("GROQ_API_KEY")
34
+ if not api_key:
35
+ print("Error: GROQ_API_KEY environment variable is missing.", file=sys.stderr)
36
+ sys.exit(1)
37
+
38
+ client = Groq(api_key=api_key)
39
+ model = "llama-3.1-8b-instant"
40
+
41
+ tasks = ["easy", "medium", "hard"]
42
+ results = {}
43
+
44
+ for task in tasks:
45
+ session_id = f"baseline_{task}"
46
+
47
+ # 1. Reset Env
48
+ reset_res = requests.post(f"{API_URL}/reset", json={
49
+ "task": task,
50
+ "episode_length": 10,
51
+ "session_id": session_id
52
+ })
53
+ reset_res.raise_for_status()
54
+ obs = reset_res.json()
55
+
56
+ done = False
57
+ while not done:
58
+ # 2. Extract inputs depending on task definition
59
+ input_fields = obs.get("input", {})
60
+ if task == "easy":
61
+ input_text = f"Text 1: {input_fields.get('text1')}\nText 2: {input_fields.get('text2')}"
62
+ elif task == "medium":
63
+ input_text = f"Premise: {input_fields.get('premise')}\nHypothesis: {input_fields.get('hypothesis')}"
64
+ else: # hard
65
+ raw_text = input_fields.get("text", "")
66
+ input_text = f"Text: {raw_text[:300]}" # Truncated to 300 chars
67
+
68
+ ai_label = obs.get("ai_label")
69
+ label_options = obs.get("label_options")
70
+
71
+ # 3. Create Model Prompt
72
+ prompt = f"""You are an expert AI auditor verifying labels for a dataset.
73
+ Your task is to review the provided input and decide if the assigned 'AI Label' is correct, wrong, or ambiguous.
74
+
75
+ Input Examples:
76
+ {input_text}
77
+
78
+ AI Label: {ai_label}
79
+ Valid Label Options: {label_options}
80
+
81
+ Instructions:
82
+ Evaluate the AI Label against the Input Examples.
83
+ Respond in pure JSON format only with the following keys:
84
+ - "verdict": purely one of "correct", "wrong", or "ambiguous"
85
+ - "proposed_label": if the verdict is "wrong", provide the correct label from the Valid Label Options as a string. Otherwise, use null.
86
+ - "confidence": a float between 0.0 and 1.0 representing your confidence.
87
+
88
+ Example of valid response:
89
+ {{"verdict": "wrong", "proposed_label": "1", "confidence": 0.85}}
90
+ """
91
+
92
+ # 4. Invoke LLM
93
+ chat_completion = client.chat.completions.create(
94
+ messages=[
95
+ {
96
+ "role": "system",
97
+ "content": "You output JSON strictly."
98
+ },
99
+ {
100
+ "role": "user",
101
+ "content": prompt,
102
+ }
103
+ ],
104
+ model=model,
105
+ temperature=0.0
106
+ )
107
+
108
+ # 5. Parse output
109
+ response_text = chat_completion.choices[0].message.content
110
+ action_dict = parse_json_response(response_text)
111
+
112
+ # 6. Step Env
113
+ step_payload = {
114
+ "session_id": session_id,
115
+ "example_id": obs.get("example_id"),
116
+ "verdict": action_dict["verdict"],
117
+ "proposed_label": action_dict.get("proposed_label"),
118
+ "confidence": action_dict.get("confidence")
119
+ }
120
+
121
+ step_res = requests.post(f"{API_URL}/step", json=step_payload)
122
+ step_res.raise_for_status()
123
+ step_data = step_res.json()
124
+
125
+ done = step_data.get("done", True)
126
+ if not done:
127
+ obs = step_data.get("observation", {})
128
+
129
+ # 7. Collect Final Grade
130
+ grader_res = requests.post(f"{API_URL}/grader", params={"session_id": session_id})
131
+ grader_res.raise_for_status()
132
+ final_info = grader_res.json()
133
+
134
+ results[task] = final_info.get("cumulative_score", 0.0)
135
+
136
+ return results
137
+
138
+ if __name__ == "__main__":
139
+ print("Running baseline evaluations... (This evaluates the Groq model against the live API)")
140
+ scores = run_baseline()
141
+
142
+ print("\n--- Baseline Results ---")
143
+ total_score = 0.0
144
+ for task, score in scores.items():
145
+ print(f"Task: {task.capitalize():<10} | Cumulative Score: {score:>5.2f}")
146
+ total_score += score
147
+
148
+ avg_score = total_score / len(scores) if scores else 0.0
149
+ print("-" * 35)
150
+ print(f"Overall Average Score: {avg_score:>5.2f}")
data/loader.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ data/loader.py - HuggingFace dataset loading for OpenEnv Labeling QA.
3
+
4
+ Loads three classification datasets, samples 150 examples from each,
5
+ and returns them as clean Python lists of dicts with standardized keys.
6
+
7
+ Dependencies: pip install datasets
8
+ """
9
+
10
+ import sys
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Constants
14
+ # ---------------------------------------------------------------------------
15
+ SAMPLE_SIZE = 150
16
+ SEED = 42
17
+
18
+ # NLI label mapping (int -> str) used by bigbio NLI datasets
19
+ _NLI_LABEL_MAP = {0: "entailment", 1: "neutral", 2: "contradiction"}
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Helpers
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _load_hf_dataset(path: str, split: str = "train", name: str = None):
27
+ """
28
+ Wrapper around datasets.load_dataset that handles trust_remote_code
29
+ gracefully across different versions of the `datasets` library.
30
+ """
31
+ from datasets import load_dataset
32
+ import inspect
33
+
34
+ kwargs = {"path": path, "split": split}
35
+ if name is not None:
36
+ kwargs["name"] = name
37
+
38
+ # Only pass trust_remote_code if the installed version supports it
39
+ sig = inspect.signature(load_dataset)
40
+ if "trust_remote_code" in sig.parameters:
41
+ kwargs["trust_remote_code"] = True
42
+
43
+ return load_dataset(**kwargs)
44
+
45
+
46
+ def _sample(dataset, n: int, seed: int = SEED):
47
+ """Return a random sample of *n* rows from a HuggingFace Dataset."""
48
+ if len(dataset) <= n:
49
+ return dataset
50
+ return dataset.shuffle(seed=seed).select(range(n))
51
+
52
+
53
+ def _safe_str(value) -> str:
54
+ """Convert a value to a stripped string, handling None gracefully."""
55
+ if value is None:
56
+ return ""
57
+ return str(value).strip()
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Task 1 - Medical Question Pairs (binary: 0 / 1)
62
+ # Dataset: curaihealth/medical_questions_pairs
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def load_task1() -> list[dict]:
66
+ """
67
+ Load the curaihealth/medical_questions_pairs dataset.
68
+
69
+ Returns a list of 150 dicts:
70
+ {id: str, text1: str, text2: str, gold_label: int}
71
+ where gold_label is 0 or 1.
72
+ """
73
+ try:
74
+ print("[Task 1] Loading curaihealth/medical_questions_pairs ...")
75
+ ds = _load_hf_dataset("curaihealth/medical_questions_pairs", split="train")
76
+
77
+ sampled = _sample(ds, SAMPLE_SIZE)
78
+
79
+ results: list[dict] = []
80
+ for idx, row in enumerate(sampled):
81
+ results.append({
82
+ "id": f"task1_{idx}",
83
+ "text1": _safe_str(row.get("question_1", row.get("question1", ""))),
84
+ "text2": _safe_str(row.get("question_2", row.get("question2", ""))),
85
+ "gold_label": int(row.get("label", 0)),
86
+ })
87
+
88
+ print(f"[Task 1] OK - Loaded {len(results)} examples.")
89
+ return results
90
+
91
+ except Exception as exc:
92
+ print(f"[Task 1] FAILED - {exc}", file=sys.stderr)
93
+ raise
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Task 2 - NLI (3-class: entailment / neutral / contradiction)
98
+ # Dataset: snli
99
+ # ---------------------------------------------------------------------------
100
+
101
+ def load_task2() -> list[dict]:
102
+ """
103
+ Load the Stanford NLI (snli) dataset.
104
+
105
+ Filters out unlabeled examples (label == -1), then samples 150.
106
+
107
+ Returns a list of 150 dicts:
108
+ {id: str, premise: str, hypothesis: str, gold_label: str}
109
+ where gold_label is "entailment", "neutral", or "contradiction".
110
+ """
111
+ try:
112
+ print("[Task 2] Loading snli ...")
113
+ ds = _load_hf_dataset("snli", split="train")
114
+
115
+ # SNLI contains some unlabeled rows marked with label == -1
116
+ ds = ds.filter(lambda x: x["label"] != -1)
117
+
118
+ sampled = _sample(ds, SAMPLE_SIZE)
119
+
120
+ results: list[dict] = []
121
+ for idx, row in enumerate(sampled):
122
+ label_str = _NLI_LABEL_MAP.get(row["label"], str(row["label"]))
123
+ results.append({
124
+ "id": f"task2_{idx}",
125
+ "premise": _safe_str(row.get("premise", "")),
126
+ "hypothesis": _safe_str(row.get("hypothesis", "")),
127
+ "gold_label": label_str,
128
+ })
129
+
130
+ print(f"[Task 2] OK - Loaded {len(results)} examples.")
131
+ return results
132
+
133
+ except Exception as exc:
134
+ print(f"[Task 2] FAILED - {exc}", file=sys.stderr)
135
+ raise
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Task 3 - SCOTUS Legal Classification (14-class: 0-13)
140
+ # Dataset: coastalcph/lex_glue config="scotus"
141
+ # ---------------------------------------------------------------------------
142
+
143
+ def load_task3() -> list[dict]:
144
+ """
145
+ Load the coastalcph/lex_glue (scotus) dataset.
146
+
147
+ Returns a list of 150 dicts:
148
+ {id: str, text: str, gold_label: int}
149
+ where gold_label is an int 0-13.
150
+ """
151
+ try:
152
+ print("[Task 3] Loading coastalcph/lex_glue (scotus) ...")
153
+ ds = _load_hf_dataset("coastalcph/lex_glue", split="train",
154
+ name="scotus")
155
+
156
+ sampled = _sample(ds, SAMPLE_SIZE)
157
+
158
+ results: list[dict] = []
159
+ for idx, row in enumerate(sampled):
160
+ results.append({
161
+ "id": f"task3_{idx}",
162
+ "text": _safe_str(row.get("text", "")),
163
+ "gold_label": int(row.get("label", 0)),
164
+ })
165
+
166
+ print(f"[Task 3] OK - Loaded {len(results)} examples.")
167
+ return results
168
+
169
+ except Exception as exc:
170
+ print(f"[Task 3] FAILED - {exc}", file=sys.stderr)
171
+ raise
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Main - quick smoke test
176
+ # ---------------------------------------------------------------------------
177
+
178
+ if __name__ == "__main__":
179
+ print("=" * 60)
180
+ print(" OpenEnv Labeling QA - Dataset Loader Smoke Test")
181
+ print("=" * 60)
182
+
183
+ # -- Task 1 ---------------------------------------------------------------
184
+ try:
185
+ t1 = load_task1()
186
+ print(f"\n[Sample] Task 1 ({len(t1)} total):")
187
+ print(f" {t1[0]}\n")
188
+ except Exception as e:
189
+ print(f"\n[ERROR] Task 1: {e}\n")
190
+
191
+ # -- Task 2 ---------------------------------------------------------------
192
+ try:
193
+ t2 = load_task2()
194
+ print(f"[Sample] Task 2 ({len(t2)} total):")
195
+ print(f" {t2[0]}\n")
196
+ except Exception as e:
197
+ print(f"\n[ERROR] Task 2: {e}\n")
198
+
199
+ # -- Task 3 ---------------------------------------------------------------
200
+ try:
201
+ t3 = load_task3()
202
+ print(f"[Sample] Task 3 ({len(t3)} total):")
203
+ print(f" {t3[0]}\n")
204
+ except Exception as e:
205
+ print(f"\n[ERROR] Task 3: {e}\n")
206
+
207
+ print("=" * 60)
208
+ print(" Done.")
209
+ print("=" * 60)
environment/env.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import random
4
+
5
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
6
+
7
+ from environment.models import Observation, Action, Reward, StepResult, EpisodeState
8
+ from environment.grader import grade
9
+ from data.loader import load_task1, load_task2, load_task3
10
+ from environment.noise import inject_noise_task1, inject_noise_task2, inject_noise_task3
11
+
12
+ class LabelingQAEnv:
13
+ def __init__(self, task: str = "easy", episode_length: int = 10):
14
+ if task not in ["easy", "medium", "hard"]:
15
+ raise ValueError(f"Task must be one of 'easy', 'medium', 'hard'. Got {task}")
16
+
17
+ self.task = task
18
+ self.episode_length = episode_length
19
+
20
+ if task == "easy":
21
+ data = load_task1()
22
+ self.examples = inject_noise_task1(data)
23
+ elif task == "medium":
24
+ data = load_task2()
25
+ self.examples = inject_noise_task2(data)
26
+ elif task == "hard":
27
+ data = load_task3()
28
+ self.examples = inject_noise_task3(data)
29
+
30
+ self.current_step = 0
31
+ self.cumulative_score = 0.0
32
+ self.done = False
33
+ self.current_examples = []
34
+
35
+ def _build_observation(self, example: dict) -> Observation:
36
+ if self.task == "easy":
37
+ input_dict = {
38
+ "text1": example.get("text1", ""),
39
+ "text2": example.get("text2", "")
40
+ }
41
+ label_options = ["0", "1"]
42
+ elif self.task == "medium":
43
+ input_dict = {
44
+ "premise": example.get("premise", ""),
45
+ "hypothesis": example.get("hypothesis", "")
46
+ }
47
+ label_options = ["entailment", "neutral", "contradiction"]
48
+ else: # hard
49
+ text = example.get("text", "")
50
+ input_dict = {"text": text[:500]}
51
+ label_options = [str(i) for i in range(14)]
52
+
53
+ return Observation(
54
+ example_id=str(example["id"]),
55
+ task=self.task,
56
+ input=input_dict,
57
+ ai_label=str(example["ai_label"]),
58
+ label_options=label_options,
59
+ episode_step=self.current_step,
60
+ total_steps=self.episode_length
61
+ )
62
+
63
+ def reset(self) -> Observation:
64
+ if self.episode_length > len(self.examples):
65
+ raise ValueError("Episode length exceeds available examples.")
66
+
67
+ self.current_examples = random.sample(self.examples, self.episode_length)
68
+ self.current_step = 0
69
+ self.cumulative_score = 0.0
70
+ self.done = False
71
+
72
+ return self._build_observation(self.current_examples[0])
73
+
74
+ def step(self, action: Action) -> StepResult:
75
+ if self.done:
76
+ raise RuntimeError("Episode is already done.")
77
+
78
+ current_example = self.current_examples[self.current_step]
79
+
80
+ # Pydantic v2 compatible dict dump
81
+ action_dict = action.model_dump() if hasattr(action, 'model_dump') else action.dict()
82
+ score_dict = grade(self.task, action_dict, current_example)
83
+
84
+ reward = Reward(
85
+ example_id=str(current_example["id"]),
86
+ score=score_dict["score"],
87
+ reason=score_dict["reason"],
88
+ gold_label=str(score_dict["gold_label"])
89
+ )
90
+
91
+ self.current_step += 1
92
+ self.cumulative_score += reward.score
93
+
94
+ if self.current_step >= self.episode_length:
95
+ self.done = True
96
+
97
+ obs = None if self.done else self._build_observation(self.current_examples[self.current_step])
98
+ info = {
99
+ "cumulative_score": self.cumulative_score,
100
+ "step": self.current_step
101
+ }
102
+
103
+ return StepResult(
104
+ observation=obs,
105
+ reward=reward,
106
+ done=self.done,
107
+ info=info
108
+ )
109
+
110
+ def state(self) -> EpisodeState:
111
+ return EpisodeState(
112
+ task=self.task,
113
+ current_step=self.current_step,
114
+ total_steps=self.episode_length,
115
+ cumulative_score=self.cumulative_score,
116
+ done=self.done
117
+ )
118
+
119
+
120
+ if __name__ == "__main__":
121
+ import sys
122
+ import os
123
+ # Dynamically inject root path specifically inside __main__ execution for simplicity
124
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
125
+
126
+ tasks = ["easy", "medium", "hard"]
127
+ for t in tasks:
128
+ print(f"\n{'='*50}")
129
+ print(f"Testing LabelingQAEnv(task='{t}')")
130
+ print(f"{'='*50}")
131
+
132
+ # Keep episode length short for testing
133
+ env = LabelingQAEnv(task=t, episode_length=3)
134
+ obs = env.reset()
135
+
136
+ for i in range(3):
137
+ print(f"\n--- Step {i+1} ---")
138
+
139
+ # Safely check for model_dump or standard dict wrapper
140
+ obs_dict = obs.model_dump() if hasattr(obs, 'model_dump') else obs.dict()
141
+ print(f"Observation: {obs_dict}")
142
+
143
+ first_option = obs.label_options[0]
144
+ action = Action(
145
+ example_id=obs.example_id,
146
+ verdict="wrong",
147
+ proposed_label=first_option,
148
+ confidence=0.8
149
+ )
150
+
151
+ res = env.step(action)
152
+ reward_dict = res.reward.model_dump() if hasattr(res.reward, 'model_dump') else res.reward.dict()
153
+
154
+ print(f"Reward: {reward_dict}")
155
+ print(f"Cumulative Score: {env.cumulative_score}")
156
+
157
+ obs = res.observation
environment/grader.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def grade_task1(action: dict, example: dict) -> dict:
2
+ verdict = action.get("verdict")
3
+ proposed_label = action.get("proposed_label")
4
+ gold_label_str = str(example["gold_label"])
5
+ is_noisy = example.get("is_noisy", False)
6
+
7
+ score = 0.0
8
+ reason = ""
9
+
10
+ if verdict == "ambiguous":
11
+ score = 0.3
12
+ reason = "Partial credit for ambiguous (task 1 has no truly ambiguous cases)."
13
+ elif is_noisy and verdict == "wrong":
14
+ score = 1.0
15
+ reason = "Correctly identified noisy label."
16
+ if proposed_label == gold_label_str:
17
+ score = min(1.0, score + 0.5)
18
+ reason = "Correctly identified noisy label and proposed the correct label."
19
+ elif is_noisy and verdict == "correct":
20
+ score = 0.0
21
+ reason = "Failed to identify noisy label."
22
+ elif not is_noisy and verdict == "correct":
23
+ score = 1.0
24
+ reason = "Correctly accepted a valid label."
25
+ elif not is_noisy and verdict == "wrong":
26
+ score = -0.5
27
+ reason = "Incorrectly rejected a valid label."
28
+
29
+ return {"score": float(score), "reason": reason, "gold_label": gold_label_str}
30
+
31
+ def grade_task2(action: dict, example: dict) -> dict:
32
+ verdict = action.get("verdict")
33
+ proposed_label = action.get("proposed_label")
34
+ gold_label_str = str(example["gold_label"])
35
+ is_noisy = example.get("is_noisy", False)
36
+
37
+ score = 0.0
38
+ reason = ""
39
+
40
+ if verdict == "ambiguous":
41
+ if gold_label_str == "neutral":
42
+ score = 0.5
43
+ reason = "Correctly identified neutral/ambiguous case."
44
+ else:
45
+ score = -0.3
46
+ reason = "Incorrectly labeled non-neutral case as ambiguous."
47
+ elif is_noisy and verdict == "wrong":
48
+ score = 1.0
49
+ reason = "Correctly identified noisy label."
50
+ if proposed_label == gold_label_str:
51
+ score = min(1.0, score + 0.5)
52
+ reason = "Correctly identified noisy label and proposed the correct label."
53
+ elif is_noisy and verdict == "correct":
54
+ score = 0.0
55
+ reason = "Failed to identify noisy label."
56
+ elif not is_noisy and verdict == "correct":
57
+ score = 1.0
58
+ reason = "Correctly accepted a valid label."
59
+ elif not is_noisy and verdict == "wrong":
60
+ score = -0.5
61
+ reason = "Incorrectly rejected a valid label."
62
+
63
+ return {"score": float(score), "reason": reason, "gold_label": gold_label_str}
64
+
65
+ def grade_task3(action: dict, example: dict) -> dict:
66
+ verdict = action.get("verdict")
67
+ proposed_label = action.get("proposed_label")
68
+ confidence = action.get("confidence", 1.0)
69
+ gold_label_str = str(example["gold_label"])
70
+ is_noisy = example.get("is_noisy", False)
71
+
72
+ score = 0.0
73
+ reason = ""
74
+
75
+ if verdict == "ambiguous":
76
+ score = 0.4
77
+ reason = "Partial credit for ambiguous (legal categories genuinely overlap)."
78
+ elif is_noisy and verdict == "wrong":
79
+ score = 1.0
80
+ reason = "Correctly identified noisy label."
81
+ if proposed_label == gold_label_str:
82
+ score = min(1.0, score + 0.5)
83
+ reason = "Correctly identified noisy label and proposed the correct label."
84
+ elif is_noisy and verdict == "correct":
85
+ if confidence > 0.8:
86
+ score = -0.5
87
+ reason = "Failed to identify noisy label and penalized for overconfidence."
88
+ else:
89
+ score = 0.0
90
+ reason = "Failed to identify noisy label."
91
+ elif not is_noisy and verdict == "correct":
92
+ score = 1.0
93
+ reason = "Correctly accepted a valid label."
94
+ elif not is_noisy and verdict == "wrong":
95
+ score = -0.5
96
+ reason = "Incorrectly rejected a valid label."
97
+
98
+ return {"score": float(score), "reason": reason, "gold_label": gold_label_str}
99
+
100
+ def grade(task: str, action: dict, example: dict) -> dict:
101
+ """
102
+ Unified entry point - routes to correct grader based on task ("easy", "medium", "hard").
103
+ """
104
+ if task == "easy":
105
+ return grade_task1(action, example)
106
+ elif task == "medium":
107
+ return grade_task2(action, example)
108
+ elif task == "hard":
109
+ return grade_task3(action, example)
110
+ else:
111
+ raise ValueError(f"Unknown task: {task}")
112
+
113
+ if __name__ == "__main__":
114
+ print("--- Task 1 (Easy) Sanity Check ---")
115
+ ex1 = {"gold_label": 1, "ai_label": "0", "is_noisy": True}
116
+ act1 = {"verdict": "wrong", "proposed_label": "1", "confidence": 0.9}
117
+ res1 = grade("easy", act1, ex1)
118
+ print(f"Action: {act1}")
119
+ print(f"Example: {ex1}")
120
+ print(f"Result: {res1}\n")
121
+
122
+ print("--- Task 2 (Medium) Sanity Check ---")
123
+ ex2 = {"gold_label": "neutral", "ai_label": "entailment", "is_noisy": True}
124
+ act2 = {"verdict": "ambiguous", "proposed_label": None, "confidence": 0.5}
125
+ res2 = grade("medium", act2, ex2)
126
+ print(f"Action: {act2}")
127
+ print(f"Example: {ex2}")
128
+ print(f"Result: {res2}\n")
129
+
130
+ print("--- Task 3 (Hard) Sanity Check ---")
131
+ ex3 = {"gold_label": 11, "ai_label": "10", "is_noisy": True}
132
+ act3 = {"verdict": "correct", "proposed_label": None, "confidence": 0.9}
133
+ res3 = grade("hard", act3, ex3)
134
+ print(f"Action: {act3}")
135
+ print(f"Example: {ex3}")
136
+ print(f"Result: {res3}\n")
environment/models.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Literal
2
+ from pydantic import BaseModel, Field, model_validator
3
+
4
+ class Observation(BaseModel):
5
+ """What the agent observes at each step in the environment."""
6
+ example_id: str
7
+ task: Literal["easy", "medium", "hard"]
8
+ input: dict
9
+ ai_label: str
10
+ label_options: list[str]
11
+ episode_step: int
12
+ total_steps: int
13
+
14
+ class Action(BaseModel):
15
+ """The action the agent takes for a given observation."""
16
+ example_id: str
17
+ verdict: Literal["correct", "wrong", "ambiguous"]
18
+ proposed_label: str | None = None
19
+ confidence: float = Field(ge=0.0, le=1.0)
20
+
21
+ @model_validator(mode='after')
22
+ def check_proposed_label(self) -> 'Action':
23
+ if self.verdict == "wrong" and self.proposed_label is None:
24
+ raise ValueError('proposed_label must be provided when verdict is "wrong"')
25
+ return self
26
+
27
+ class Reward(BaseModel):
28
+ """The reward given after an action is taken."""
29
+ example_id: str
30
+ score: float = Field(ge=-1.0, le=1.0)
31
+ reason: str
32
+ gold_label: str
33
+
34
+ class StepResult(BaseModel):
35
+ """The full return value of taking a step in the environment."""
36
+ observation: Observation | None
37
+ reward: Reward
38
+ done: bool
39
+ info: dict
40
+
41
+ class EpisodeState(BaseModel):
42
+ """The overall state of the current episode."""
43
+ task: str
44
+ current_step: int
45
+ total_steps: int
46
+ cumulative_score: float
47
+ done: bool
environment/noise.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ def inject_noise_task1(examples: list[dict]) -> list[dict]:
4
+ """
5
+ Task 1: gold_label is "0" or "1".
6
+ Noise type: random flips - 20% of examples get their label flipped to the opposite.
7
+ """
8
+ random.seed(42)
9
+ noised_examples = []
10
+
11
+ for ex in examples:
12
+ new_ex = ex.copy()
13
+ gold_label = str(new_ex["gold_label"])
14
+
15
+ # 20% chance to flip
16
+ if random.random() < 0.20:
17
+ ai_label = "1" if gold_label == "0" else "0"
18
+ is_noisy = True
19
+ else:
20
+ ai_label = gold_label
21
+ is_noisy = False
22
+
23
+ new_ex["ai_label"] = ai_label
24
+ new_ex["is_noisy"] = is_noisy
25
+ noised_examples.append(new_ex)
26
+
27
+ return noised_examples
28
+
29
+ def inject_noise_task2(examples: list[dict]) -> list[dict]:
30
+ """
31
+ Task 2: gold_label is "entailment", "neutral", or "contradiction".
32
+ Noise type: systematic bias - AI always predicts "neutral" when the correct label
33
+ is "contradiction". Flips 15% of "entailment" to "neutral" too.
34
+ """
35
+ random.seed(42)
36
+ noised_examples = []
37
+
38
+ for ex in examples:
39
+ new_ex = ex.copy()
40
+ gold_label = str(new_ex["gold_label"])
41
+
42
+ is_noisy = False
43
+ ai_label = gold_label
44
+
45
+ if gold_label == "contradiction":
46
+ ai_label = "neutral"
47
+ is_noisy = True
48
+ elif gold_label == "entailment":
49
+ if random.random() < 0.15:
50
+ ai_label = "neutral"
51
+ is_noisy = True
52
+
53
+ new_ex["ai_label"] = ai_label
54
+ new_ex["is_noisy"] = is_noisy
55
+ noised_examples.append(new_ex)
56
+
57
+ return noised_examples
58
+
59
+ def inject_noise_task3(examples: list[dict]) -> list[dict]:
60
+ """
61
+ Task 3: gold_label is int 0-13.
62
+ Noise type: confident wrong labels on edge cases - for examples where gold_label
63
+ is in [0, 1, 2, 3], 30% chance of being mislabeled to a nearby category (+1 or -1).
64
+ For all others, 10% random flip to any other label.
65
+ """
66
+ random.seed(42)
67
+ noised_examples = []
68
+
69
+ for ex in examples:
70
+ new_ex = ex.copy()
71
+ gold_label = int(new_ex["gold_label"])
72
+
73
+ is_noisy = False
74
+ ai_label = gold_label
75
+
76
+ if gold_label in [0, 1, 2, 3]:
77
+ if random.random() < 0.30:
78
+ is_noisy = True
79
+ offset = random.choice([-1, 1])
80
+ ai_label = max(0, min(13, gold_label + offset))
81
+ else:
82
+ if random.random() < 0.10:
83
+ is_noisy = True
84
+ possible_labels = [l for l in range(14) if l != gold_label]
85
+ ai_label = random.choice(possible_labels)
86
+
87
+ new_ex["ai_label"] = str(ai_label)
88
+ new_ex["is_noisy"] = is_noisy
89
+ noised_examples.append(new_ex)
90
+
91
+ return noised_examples
92
+
93
+ if __name__ == "__main__":
94
+ import sys
95
+ import os
96
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
97
+
98
+ from data.loader import load_task1, load_task2, load_task3
99
+
100
+ # Load
101
+ print("Loading datasets...")
102
+ data1 = load_task1()
103
+ data2 = load_task2()
104
+ data3 = load_task3()
105
+
106
+ # Task 1
107
+ print(f"\nTask 1 loaded: {len(data1)} examples")
108
+ noised1 = inject_noise_task1(data1)
109
+ num_noisy1 = sum(1 for ex in noised1 if ex["is_noisy"])
110
+ print(f"Task 1 noised: {num_noisy1} / {len(noised1)}")
111
+ noisy_example1 = next((ex for ex in noised1 if ex["is_noisy"]), None)
112
+ if noisy_example1:
113
+ print(f"Task 1 example: {noisy_example1}")
114
+
115
+ # Task 2
116
+ print(f"\nTask 2 loaded: {len(data2)} examples")
117
+ noised2 = inject_noise_task2(data2)
118
+ num_noisy2 = sum(1 for ex in noised2 if ex["is_noisy"])
119
+ print(f"Task 2 noised: {num_noisy2} / {len(noised2)}")
120
+ noisy_example2 = next((ex for ex in noised2 if ex["is_noisy"]), None)
121
+ if noisy_example2:
122
+ print(f"Task 2 example: {noisy_example2}")
123
+
124
+ # Task 3
125
+ print(f"\nTask 3 loaded: {len(data3)} examples")
126
+ noised3 = inject_noise_task3(data3)
127
+ num_noisy3 = sum(1 for ex in noised3 if ex["is_noisy"])
128
+ print(f"Task 3 noised: {num_noisy3} / {len(noised3)}")
129
+ noisy_example3 = next((ex for ex in noised3 if ex["is_noisy"]), None)
130
+ if noisy_example3:
131
+ print(f"Task 3 example: {noisy_example3}")
inference.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import time
4
+ import json
5
+ import requests
6
+ from openai import OpenAI
7
+
8
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://api.groq.com/openai/v1")
9
+ HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
10
+ MODEL_NAME = os.getenv("MODEL_NAME", "llama-3.1-8b-instant")
11
+
12
+ def build_prompt(task_name, obs):
13
+ base_instruction = (
14
+ "You are an expert data labeling quality assurance AI. "
15
+ "Your job is to audit the provided 'ai_label' against the input and the 'label_options'. "
16
+ "Respond ONLY with a valid JSON object. Do not include any other text, reasoning, or markdown formatting.\n"
17
+ "Required JSON keys:\n"
18
+ "- 'verdict': string, one of ['correct', 'wrong', 'ambiguous'].\n"
19
+ "- 'proposed_label': string, the correct label if verdict is 'wrong', or null if correct/ambiguous.\n"
20
+ "- 'confidence': float, between 0.0 and 1.0.\n\n"
21
+ )
22
+
23
+ input_data = obs.get("input", {})
24
+ ai_label = obs.get("ai_label")
25
+ label_options = obs.get("label_options", [])
26
+
27
+ if task_name == "easy":
28
+ text1 = input_data.get("sentence1", input_data.get("text1", ""))
29
+ text2 = input_data.get("sentence2", input_data.get("text2", ""))
30
+ prompt = f"Task: Easy - Binary similarity.\nText 1: {text1}\nText 2: {text2}\nAI Label: {ai_label}\nLabel Options: {label_options}"
31
+ elif task_name == "medium":
32
+ premise = input_data.get("premise", "")
33
+ hypothesis = input_data.get("hypothesis", "")
34
+ prompt = f"Task: Medium - NLI.\nPremise: {premise}\nHypothesis: {hypothesis}\nAI Label: {ai_label}\nLabel Options: {label_options}"
35
+ elif task_name == "hard":
36
+ text = input_data.get("text", "")[:300]
37
+ prompt = f"Task: Hard - SCOTUS legal issue classification.\nText (truncated to 300 chars): {text}...\nAI Label: {ai_label}\nLabel Options: {label_options}"
38
+ else:
39
+ prompt = f"Task: {task_name}\nInput: {input_data}\nAI Label: {ai_label}\nLabel Options: {label_options}"
40
+
41
+ return base_instruction + prompt
42
+
43
+ def run_baseline() -> dict:
44
+ client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
45
+ tasks = ["easy", "medium", "hard"]
46
+
47
+ global_start_time = time.time()
48
+
49
+ scores = {}
50
+
51
+ for task in tasks:
52
+ session_id = f"inference_{task}"
53
+
54
+ try:
55
+ start_res = requests.post(
56
+ "http://localhost:7860/reset",
57
+ json={"task": task, "episode_length": 10, "session_id": session_id}
58
+ )
59
+ start_res.raise_for_status()
60
+ env_state = start_res.json()
61
+ except Exception as e:
62
+ print(f"Error resetting environment for task {task}: {e}")
63
+ scores[task] = 0.0
64
+ continue
65
+
66
+ obs = env_state.get("observation", {})
67
+ done = env_state.get("done", False)
68
+
69
+ while not done:
70
+ if time.time() - global_start_time > 18 * 60:
71
+ print("Time limit exceeding 18 minutes. Breaking early.")
72
+ break
73
+
74
+ prompt = build_prompt(task, obs)
75
+
76
+ try:
77
+ response = client.chat.completions.create(
78
+ model=MODEL_NAME,
79
+ messages=[{"role": "user", "content": prompt}],
80
+ temperature=0.0
81
+ )
82
+
83
+ raw_content = response.choices[0].message.content.strip()
84
+
85
+ # Strip markdown fences if present
86
+ if raw_content.startswith("```json"):
87
+ raw_content = raw_content[7:]
88
+ elif raw_content.startswith("```"):
89
+ raw_content = raw_content[3:]
90
+ if raw_content.endswith("```"):
91
+ raw_content = raw_content[:-3]
92
+
93
+ raw_content = raw_content.strip()
94
+ parsed_action = json.loads(raw_content)
95
+
96
+ action = {
97
+ "example_id": obs.get("example_id"),
98
+ "verdict": parsed_action.get("verdict", "ambiguous"),
99
+ "proposed_label": str(parsed_action.get("proposed_label")) if parsed_action.get("proposed_label") is not None else None,
100
+ "confidence": float(parsed_action.get("confidence", 0.5))
101
+ }
102
+ except Exception as e:
103
+ action = {
104
+ "example_id": obs.get("example_id"),
105
+ "verdict": "ambiguous",
106
+ "proposed_label": None,
107
+ "confidence": 0.5
108
+ }
109
+
110
+ payload = {
111
+ "session_id": session_id,
112
+ "action": action
113
+ }
114
+
115
+ try:
116
+ step_res = requests.post("http://localhost:7860/step", json=payload)
117
+ step_res.raise_for_status()
118
+ env_state = step_res.json()
119
+
120
+ obs = env_state.get("observation", {})
121
+ done = env_state.get("done", False)
122
+ except Exception as e:
123
+ print(f"Error stepping environment: {e}")
124
+ break
125
+
126
+ # Get final score
127
+ try:
128
+ grader_res = requests.post("http://localhost:7860/grader", json={"session_id": session_id})
129
+ grader_res.raise_for_status()
130
+ score = grader_res.json().get("score", 0.0)
131
+ scores[task] = score
132
+ except Exception as e:
133
+ print(f"Error getting score for task {task}: {e}")
134
+ scores[task] = 0.0
135
+
136
+ avg_score = sum(scores.values()) / len(scores) if scores else 0.0
137
+ scores["average"] = avg_score
138
+ return scores
139
+
140
+ if __name__ == "__main__":
141
+ if not HF_TOKEN:
142
+ print("Error: HF_TOKEN or API_KEY environment variable is not set.", file=sys.stderr)
143
+ print("Please set it to run the baseline evaluation.", file=sys.stderr)
144
+ sys.exit(1)
145
+
146
+ print("Running LabelSense Baseline...")
147
+ results = run_baseline()
148
+
149
+ print("\n=== LabelSense Baseline Results ===")
150
+ print(f"Task: Easy | Score: {results.get('easy', 0.0):.2f}")
151
+ print(f"Task: Medium | Score: {results.get('medium', 0.0):.2f}")
152
+ print(f"Task: Hard | Score: {results.get('hard', 0.0):.2f}")
153
+ print(f"Average Score: {results.get('average', 0.0):.2f}")
main.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel
3
+ from typing import Dict, Optional
4
+
5
+ from environment.env import LabelingQAEnv
6
+ from environment.models import Action
7
+
8
+ app = FastAPI(title="LabelSense OpenEnv")
9
+
10
+ envs: Dict[str, LabelingQAEnv] = {}
11
+
12
+ class ResetRequest(BaseModel):
13
+ task: str = "easy"
14
+ episode_length: int = 10
15
+ session_id: str = "default"
16
+
17
+ class StepRequest(BaseModel):
18
+ session_id: str = "default"
19
+ example_id: str
20
+ verdict: str
21
+ proposed_label: str | None = None
22
+ confidence: float = 0.8
23
+
24
+ @app.post("/reset")
25
+ def reset_endpoint(req: ResetRequest):
26
+ try:
27
+ env = LabelingQAEnv(task=req.task, episode_length=req.episode_length)
28
+ except ValueError as e:
29
+ raise HTTPException(status_code=400, detail=str(e))
30
+
31
+ envs[req.session_id] = env
32
+
33
+ try:
34
+ obs = env.reset()
35
+ except Exception as e:
36
+ raise HTTPException(status_code=500, detail=str(e))
37
+
38
+ return obs.model_dump() if hasattr(obs, 'model_dump') else obs.dict()
39
+
40
+ @app.post("/step")
41
+ def step_endpoint(req: StepRequest):
42
+ if req.session_id not in envs:
43
+ raise HTTPException(status_code=404, detail="Session not found")
44
+
45
+ env = envs[req.session_id]
46
+
47
+ try:
48
+ action = Action(
49
+ example_id=req.example_id,
50
+ verdict=req.verdict,
51
+ proposed_label=req.proposed_label,
52
+ confidence=req.confidence
53
+ )
54
+ res = env.step(action)
55
+ except Exception as e:
56
+ raise HTTPException(status_code=400, detail=str(e))
57
+
58
+ return res.model_dump() if hasattr(res, 'model_dump') else res.dict()
59
+
60
+ @app.get("/state")
61
+ def state_endpoint(session_id: str = "default"):
62
+ if session_id not in envs:
63
+ raise HTTPException(status_code=404, detail="Session not found")
64
+
65
+ env = envs[session_id]
66
+ state = env.state()
67
+ return state.model_dump() if hasattr(state, 'model_dump') else state.dict()
68
+
69
+ @app.get("/tasks")
70
+ def tasks_endpoint():
71
+ schema = {
72
+ "session_id": "str",
73
+ "example_id": "str",
74
+ "verdict": "str",
75
+ "proposed_label": "str | None",
76
+ "confidence": "float"
77
+ }
78
+
79
+ return [
80
+ {
81
+ "name": "easy",
82
+ "difficulty": "Easy",
83
+ "description": "Binary classification setup on medical subsets",
84
+ "action_schema": schema
85
+ },
86
+ {
87
+ "name": "medium",
88
+ "difficulty": "Medium",
89
+ "description": "NLI textual entailment configuration",
90
+ "action_schema": schema
91
+ },
92
+ {
93
+ "name": "hard",
94
+ "difficulty": "Hard",
95
+ "description": "Complex multi-label edge case tagging",
96
+ "action_schema": schema
97
+ }
98
+ ]
99
+
100
+ @app.post("/grader")
101
+ def grader_endpoint(session_id: str = "default"):
102
+ if session_id not in envs:
103
+ raise HTTPException(status_code=404, detail="Session not found")
104
+
105
+ env = envs[session_id]
106
+ state = env.state()
107
+
108
+ return {
109
+ "session_id": session_id,
110
+ "task": state.task,
111
+ "cumulative_score": state.cumulative_score,
112
+ "total_steps": state.total_steps,
113
+ "done": state.done
114
+ }
115
+
116
+ @app.post("/baseline")
117
+ def baseline_endpoint():
118
+ try:
119
+ from baseline import run_baseline
120
+ return run_baseline()
121
+ except ImportError:
122
+ return {"status": "baseline not yet implemented"}
123
+ except Exception as e:
124
+ raise HTTPException(status_code=500, detail=str(e))
125
+
126
+ @app.get("/")
127
+ def health_endpoint():
128
+ return {"status": "ok", "service": "LabelSense OpenEnv"}
openenv.yaml ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: labelsense-labeling-qa
2
+ version: 1.0.0
3
+ description: >
4
+ An OpenEnv environment where an AI agent audits AI-generated labels on
5
+ medical and legal text datasets. The agent identifies mislabeled examples,
6
+ flags ambiguous cases, and proposes corrections. Simulates a real MLOps
7
+ data quality workflow.
8
+
9
+ tags:
10
+ - openenv
11
+ - data-labeling
12
+ - quality-assurance
13
+ - medical
14
+ - legal
15
+ - nlp
16
+
17
+ author: VenuGopal811
18
+ license: MIT
19
+
20
+ observation_space:
21
+ type: object
22
+ fields:
23
+ example_id: string
24
+ task: string
25
+ input: object
26
+ ai_label: string
27
+ label_options: array
28
+ episode_step: integer
29
+ total_steps: integer
30
+
31
+ action_space:
32
+ type: object
33
+ fields:
34
+ example_id: string
35
+ verdict:
36
+ type: string
37
+ enum: [correct, wrong, ambiguous]
38
+ proposed_label:
39
+ type: string
40
+ nullable: true
41
+ confidence:
42
+ type: float
43
+ min: 0.0
44
+ max: 1.0
45
+
46
+ tasks:
47
+ - name: easy
48
+ description: Binary medical question pair similarity labeling. ~20% noisy labels.
49
+ difficulty: easy
50
+ dataset: curaihealth/medical_questions_pairs
51
+ label_type: binary
52
+ labels: ["0", "1"]
53
+ expected_score_range: [0.5, 1.0]
54
+
55
+ - name: medium
56
+ description: 3-class NLI labeling with systematic neutral bias noise.
57
+ difficulty: medium
58
+ dataset: snli
59
+ label_type: multiclass
60
+ labels: [entailment, neutral, contradiction]
61
+ expected_score_range: [0.2, 0.7]
62
+
63
+ - name: hard
64
+ description: 14-class SCOTUS legal issue area classification with confident wrong labels.
65
+ difficulty: hard
66
+ dataset: coastalcph/lex_glue (scotus)
67
+ label_type: multiclass
68
+ labels: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"]
69
+ expected_score_range: [-0.5, 0.5]
70
+
71
+ endpoints:
72
+ reset: POST /reset
73
+ step: POST /step
74
+ state: GET /state
75
+ tasks: GET /tasks
76
+ grader: POST /grader
77
+ baseline: POST /baseline
78
+
79
+ baseline:
80
+ model: llama-3.1-8b-instant
81
+ script: inference.py
82
+ scores:
83
+ easy: 4.30
84
+ medium: 2.00
85
+ hard: 4.90
86
+ average: 3.73
87
+
88
+ runtime:
89
+ python: "3.11"
90
+ framework: fastapi
91
+ port: 7860
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic
4
+ datasets
5
+ groq
6
+ openai
7
+ requests
tasks/task1_easy.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Task 1 - Easy
tasks/task2_medium.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Task 2 - Medium
tasks/task3_hard.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Task 3 - Hard