lucifer0077 commited on
Commit
3025836
Β·
0 Parent(s):

CodeReviewEnv v1.0 - OpenEnv Hackathon submission

Browse files
.env ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # ════════════════════════════════════════════════════
2
+ # β–Άβ–Ά CHANGE 3: Copy this file to .env and fill in
3
+ # your actual Gemini API key
4
+ # cp .env.example .env
5
+ # ════════════════════════════════════════════════════
6
+
7
+ # Get your FREE key at: https://aistudio.google.com/app/apikey
8
+ GEMINI_API_KEY= AIzaSyD4ZdqU7eAlnUXD_TNpTa0UiEvTSqghhCU
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ EXPOSE 7860
4
+
5
+ WORKDIR /app
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY . .
11
+
12
+ RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
13
+ USER appuser
14
+
15
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
16
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7860/')" || exit 1
17
+
18
+ # ════════════════════════════════════════════════════════
19
+ # β–Άβ–Ά CHANGE 2: Pass your Gemini key when running Docker:
20
+ # docker run -p 7860:7860 -e GEMINI_API_KEY=your_key .
21
+ # On Hugging Face Spaces: set it in Settings β†’ Secrets
22
+ # ════════════════════════════════════════════════════════
23
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CodeReviewEnv
2
+
3
+ > An OpenEnv-compliant environment for training and evaluating AI agents on **real-world code review tasks**.
4
+
5
+ ---
6
+
7
+ ## Environment Description & Motivation
8
+
9
+ Software code review is one of the highest-value tasks a senior engineer performs daily. It requires identifying bugs, spotting security vulnerabilities, understanding intent, and giving actionable feedback β€” all skills that current AI agents struggle to do reliably.
10
+
11
+ **CodeReviewEnv** simulates this workflow: an agent receives a code diff (pull request) and must produce structured review comments identifying issues by line number, type, and severity, then issue a final verdict (approve / request_changes).
12
+
13
+ This fills a real gap in the OpenEnv ecosystem: no existing environment trains agents on structured, multi-criteria code analysis with dense reward feedback.
14
+
15
+ ---
16
+
17
+ ## Action Space
18
+
19
+ The agent submits an `Action` object at each step:
20
+
21
+ ```json
22
+ {
23
+ "comments": [
24
+ {
25
+ "line_number": 5,
26
+ "issue_type": "bug",
27
+ "severity": "critical",
28
+ "description": "ZeroDivisionError when numbers list is empty",
29
+ "suggested_fix": "Add: if not numbers: return 0.0"
30
+ }
31
+ ],
32
+ "verdict": "request_changes",
33
+ "summary": "This PR has 3 critical bugs that must be fixed before merging."
34
+ }
35
+ ```
36
+
37
+ | Field | Type | Values |
38
+ |---|---|---|
39
+ | `comments[].line_number` | `int` | 1-indexed line number in the diff |
40
+ | `comments[].issue_type` | `string` | `bug`, `security`, `performance`, `style`, `logic` |
41
+ | `comments[].severity` | `string` | `critical`, `major`, `minor` |
42
+ | `comments[].description` | `string` | Free-text description of the issue |
43
+ | `comments[].suggested_fix` | `string?` | Optional suggested fix |
44
+ | `verdict` | `string` | `approve`, `request_changes`, `comment` |
45
+ | `summary` | `string?` | Optional overall review summary |
46
+
47
+ ---
48
+
49
+ ## Observation Space
50
+
51
+ The agent receives an `Observation` at each step:
52
+
53
+ ```json
54
+ {
55
+ "diff": "--- a/utils/statistics.py\n+++ b/utils/statistics.py\n...",
56
+ "file_name": "utils/statistics.py",
57
+ "pr_title": "Add calculate_statistics utility module",
58
+ "pr_description": "Adding utility functions for the analytics dashboard.",
59
+ "step_number": 1,
60
+ "max_steps": 3,
61
+ "task_id": "easy",
62
+ "task_description": "Review a simple Python utility module. Find edge case bugs..."
63
+ }
64
+ ```
65
+
66
+ | Field | Type | Description |
67
+ |---|---|---|
68
+ | `diff` | `string` | Unified diff format patch |
69
+ | `file_name` | `string` | File being reviewed |
70
+ | `pr_title` | `string` | Pull request title |
71
+ | `pr_description` | `string` | PR author's description |
72
+ | `step_number` | `int` | Current step (resets on `reset()`) |
73
+ | `max_steps` | `int` | Steps budget for this task |
74
+ | `task_id` | `string` | `easy`, `medium`, or `hard` |
75
+ | `task_description` | `string` | Task objective description |
76
+
77
+ ---
78
+
79
+ ## Reward Function
80
+
81
+ Reward is shaped over the **full trajectory** β€” not just binary end-of-episode signal:
82
+
83
+ | Signal | Value |
84
+ |---|---|
85
+ | Correctly identified critical issue | +0.20 |
86
+ | Correctly identified major issue | +0.12 |
87
+ | Correctly identified minor issue | +0.05 |
88
+ | False positive comment | -0.08 |
89
+ | Correct verdict (approve/request_changes) | +0.10 |
90
+ | Wrong verdict | -0.15 |
91
+ | Step penalty (efficiency) | -0.02 per step |
92
+
93
+ Range: **[-1.0, 1.0]**
94
+
95
+ ---
96
+
97
+ ## Tasks
98
+
99
+ ### Task 1 β€” Easy: Basic Bug Detection
100
+ - **File**: `utils/statistics.py`
101
+ - **Known Issues**: 3 critical bugs (ZeroDivisionError, IndexError), 1 performance issue
102
+ - **Max Steps**: 3
103
+ - **Success Threshold**: 0.60
104
+ - **Expected Difficulty**: A competent LLM should find most issues
105
+
106
+ ### Task 2 β€” Medium: Security Vulnerability Review
107
+ - **File**: `auth/user_manager.py`
108
+ - **Known Issues**: 7 security vulnerabilities (SQL injection Γ—2, hardcoded secrets Γ—2, MD5 hashing, pickle deserialization, permission logic bug)
109
+ - **Max Steps**: 5
110
+ - **Success Threshold**: 0.45
111
+ - **Expected Difficulty**: Requires security domain knowledge
112
+
113
+ ### Task 3 β€” Hard: Concurrency & Architecture Bug Hunt
114
+ - **File**: `core/rate_limiter.py`
115
+ - **Known Issues**: 7 bugs (race conditions, dictionary mutation during iteration, silent exceptions, thread join, architecture flaws)
116
+ - **Max Steps**: 8
117
+ - **Success Threshold**: 0.35
118
+ - **Expected Difficulty**: Requires deep concurrency expertise, genuinely challenges frontier models
119
+
120
+ ---
121
+
122
+ ## Setup & Usage
123
+
124
+ ### Local Development
125
+
126
+ ```bash
127
+ # Clone the repo
128
+ git clone https://github.com/YOUR_USERNAME/code-review-env
129
+ cd code-review-env
130
+
131
+ # Install dependencies
132
+ pip install -r requirements.txt
133
+
134
+ # Run the server
135
+ python app.py
136
+ # or
137
+ uvicorn app:app --host 0.0.0.0 --port 7860 --reload
138
+ ```
139
+
140
+ Server starts at `http://localhost:7860`
141
+
142
+ ### Quick API Test
143
+
144
+ ```bash
145
+ # Reset environment (easy task)
146
+ curl -X POST http://localhost:7860/reset \
147
+ -H "Content-Type: application/json" \
148
+ -d '{"task_id": "easy"}'
149
+
150
+ # Submit an action
151
+ curl -X POST http://localhost:7860/step \
152
+ -H "Content-Type: application/json" \
153
+ -d '{
154
+ "comments": [
155
+ {
156
+ "line_number": 5,
157
+ "issue_type": "bug",
158
+ "severity": "critical",
159
+ "description": "ZeroDivisionError when numbers list is empty",
160
+ "suggested_fix": "Check if not numbers before dividing"
161
+ }
162
+ ],
163
+ "verdict": "request_changes",
164
+ "summary": "Found a critical division by zero bug"
165
+ }'
166
+
167
+ # Get all tasks
168
+ curl http://localhost:7860/tasks
169
+
170
+ # Get current state
171
+ curl http://localhost:7860/state
172
+ ```
173
+
174
+ ### Docker
175
+
176
+ ```bash
177
+ # Build
178
+ docker build -t code-review-env .
179
+
180
+ # Run
181
+ docker run -p 7860:7860 -e OPENAI_API_KEY=your_key_here code-review-env
182
+ ```
183
+
184
+ ### Run Baseline Script
185
+
186
+ ```bash
187
+ export OPENAI_API_KEY=your_key_here
188
+
189
+ # Run all 3 tasks
190
+ python baseline.py
191
+
192
+ # Run single task
193
+ python baseline.py --task easy
194
+
195
+ # Use a different model
196
+ python baseline.py --model gpt-4o
197
+ ```
198
+
199
+ ### Deploy to Hugging Face Spaces
200
+
201
+ 1. Create a new Space on huggingface.co with **Docker** SDK
202
+ 2. Tag your Space with `openenv`
203
+ 3. Push your code:
204
+ ```bash
205
+ git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/code-review-env
206
+ git push hf main
207
+ ```
208
+ 4. Set `OPENAI_API_KEY` in Space Settings β†’ Repository Secrets
209
+
210
+ ---
211
+
212
+ ## Baseline Scores
213
+
214
+ Measured with `gpt-4o-mini`, `temperature=0`, `seed=42`:
215
+
216
+ | Task | Difficulty | Score |
217
+ |---|---|---|
218
+ | Basic Bug Detection | Easy | ~0.65 |
219
+ | Security Vulnerability Review | Medium | ~0.45 |
220
+ | Concurrency & Architecture Bug Hunt | Hard | ~0.30 |
221
+
222
+ *Run `python baseline.py` with your own API key to reproduce.*
223
+
224
+ ---
225
+
226
+ ## Project Structure
227
+
228
+ ```
229
+ code-review-env/
230
+ β”œβ”€β”€ app.py # FastAPI app β€” all HTTP endpoints
231
+ β”œβ”€β”€ environment.py # Core env logic: step() / reset() / state()
232
+ β”œβ”€β”€ models.py # Pydantic models: Observation, Action, Reward
233
+ β”œβ”€β”€ tasks.py # Task definitions with code diffs & known issues
234
+ β”œβ”€β”€ graders.py # Deterministic graders returning 0.0–1.0
235
+ β”œβ”€β”€ reward.py # Reward shaping with partial progress signals
236
+ β”œβ”€β”€ baseline.py # OpenAI-based baseline inference script
237
+ β”œβ”€β”€ openenv.yaml # OpenEnv spec metadata
238
+ β”œβ”€β”€ Dockerfile # Container build
239
+ β”œβ”€β”€ requirements.txt # Pinned dependencies
240
+ └── README.md # This file
241
+ ```
__pycache__/app.cpython-310.pyc ADDED
Binary file (6.38 kB). View file
 
__pycache__/environment.cpython-310.pyc ADDED
Binary file (4.58 kB). View file
 
__pycache__/graders.cpython-310.pyc ADDED
Binary file (3.52 kB). View file
 
__pycache__/models.cpython-310.pyc ADDED
Binary file (4.2 kB). View file
 
__pycache__/reward.cpython-310.pyc ADDED
Binary file (3.76 kB). View file
 
__pycache__/tasks.cpython-310.pyc ADDED
Binary file (10.9 kB). View file
 
app.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI application exposing the CodeReviewEnv via HTTP.
3
+
4
+ Endpoints:
5
+ POST /reset β€” reset environment, get initial observation
6
+ POST /step β€” submit an action, get observation + reward
7
+ GET /state β€” get current environment state
8
+ GET /tasks β€” list all tasks with action schema
9
+ POST /grader β€” score a completed episode
10
+ POST /baseline β€” run baseline inference and return scores
11
+ """
12
+
13
+ from fastapi import FastAPI, HTTPException
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from pydantic import BaseModel
16
+ from typing import Any, Dict, Optional
17
+
18
+ from models import (
19
+ Action,
20
+ Observation,
21
+ EnvironmentState,
22
+ TaskInfo,
23
+ GraderInput,
24
+ GraderOutput,
25
+ )
26
+ from environment import CodeReviewEnv
27
+ from graders import grade_episode
28
+ from tasks import get_all_tasks
29
+
30
+ # ── App setup ─────────────────────────────────────────────────────────────
31
+ app = FastAPI(
32
+ title="CodeReviewEnv",
33
+ description=(
34
+ "An OpenEnv-compliant environment for training and evaluating AI agents "
35
+ "on real-world code review tasks. Agents receive code diffs and must "
36
+ "identify bugs, security issues, and quality problems."
37
+ ),
38
+ version="1.0.0",
39
+ )
40
+
41
+ app.add_middleware(
42
+ CORSMiddleware,
43
+ allow_origins=["*"],
44
+ allow_methods=["*"],
45
+ allow_headers=["*"],
46
+ )
47
+
48
+ # Single shared environment instance (stateful per session)
49
+ env = CodeReviewEnv()
50
+
51
+
52
+ # ── Request / Response schemas ────────────────────────────────────────────
53
+
54
+ class ResetRequest(BaseModel):
55
+ task_id: Optional[str] = "easy"
56
+
57
+
58
+ class StepResponse(BaseModel):
59
+ observation: Observation
60
+ reward: float
61
+ done: bool
62
+ info: Dict[str, Any]
63
+
64
+
65
+ class BaselineScore(BaseModel):
66
+ task_id: str
67
+ task_name: str
68
+ difficulty: str
69
+ score: float
70
+ feedback: str
71
+
72
+
73
+ class BaselineResponse(BaseModel):
74
+ scores: list[BaselineScore]
75
+ model_used: str
76
+ note: str
77
+
78
+
79
+ # ── Endpoints ─────────────────────────────────────────────────────────────
80
+
81
+ @app.get("/", tags=["Health"])
82
+ def root():
83
+ return {
84
+ "status": "ok",
85
+ "environment": "CodeReviewEnv",
86
+ "version": "1.0.0",
87
+ "endpoints": ["/reset", "/step", "/state", "/tasks", "/grader", "/baseline"],
88
+ }
89
+
90
+
91
+ @app.post("/reset", response_model=Observation, tags=["OpenEnv"])
92
+ def reset(request: ResetRequest):
93
+ """Reset the environment to a clean state. Returns the initial observation."""
94
+ try:
95
+ obs = env.reset(task_id=request.task_id)
96
+ return obs
97
+ except ValueError as e:
98
+ raise HTTPException(status_code=400, detail=str(e))
99
+
100
+
101
+ @app.post("/step", response_model=StepResponse, tags=["OpenEnv"])
102
+ def step(action: Action):
103
+ """
104
+ Submit an action to the environment.
105
+ Returns the next observation, reward, done flag, and info dict.
106
+ """
107
+ try:
108
+ obs, reward, done, info = env.step(action)
109
+ return StepResponse(observation=obs, reward=reward, done=done, info=info)
110
+ except RuntimeError as e:
111
+ raise HTTPException(status_code=400, detail=str(e))
112
+
113
+
114
+ @app.get("/state", response_model=EnvironmentState, tags=["OpenEnv"])
115
+ def state():
116
+ """Return the full current internal state of the environment."""
117
+ return env.state()
118
+
119
+
120
+ @app.get("/tasks", tags=["OpenEnv"])
121
+ def tasks():
122
+ """
123
+ Return all available tasks with their action schema.
124
+ Used by agents to discover what tasks exist and what actions are valid.
125
+ """
126
+ action_schema = {
127
+ "type": "object",
128
+ "required": ["verdict"],
129
+ "properties": {
130
+ "comments": {
131
+ "type": "array",
132
+ "description": "List of code review comments",
133
+ "items": {
134
+ "type": "object",
135
+ "required": ["line_number", "issue_type", "severity", "description"],
136
+ "properties": {
137
+ "line_number": {"type": "integer", "description": "Line number (1-indexed)"},
138
+ "issue_type": {
139
+ "type": "string",
140
+ "enum": ["bug", "security", "performance", "style", "logic"],
141
+ },
142
+ "severity": {
143
+ "type": "string",
144
+ "enum": ["critical", "major", "minor"],
145
+ },
146
+ "description": {"type": "string", "description": "Issue description"},
147
+ "suggested_fix": {"type": "string", "description": "Optional fix suggestion"},
148
+ },
149
+ },
150
+ },
151
+ "verdict": {
152
+ "type": "string",
153
+ "enum": ["approve", "request_changes", "comment"],
154
+ "description": "Final review verdict",
155
+ },
156
+ "summary": {
157
+ "type": "string",
158
+ "description": "Optional overall review summary",
159
+ },
160
+ },
161
+ }
162
+
163
+ result = []
164
+ for t in get_all_tasks():
165
+ result.append(
166
+ {
167
+ "id": t["id"],
168
+ "name": t["name"],
169
+ "description": t["description"],
170
+ "difficulty": t["difficulty"],
171
+ "max_steps": t["max_steps"],
172
+ "pr_title": t["pr_title"],
173
+ "file_name": t["file_name"],
174
+ "action_schema": action_schema,
175
+ }
176
+ )
177
+ return {"tasks": result, "action_schema": action_schema}
178
+
179
+
180
+ @app.post("/grader", response_model=GraderOutput, tags=["OpenEnv"])
181
+ def grader(grader_input: GraderInput):
182
+ """
183
+ Score a completed episode. Returns deterministic score between 0.0–1.0.
184
+ Accepts episode history produced by /step calls.
185
+ """
186
+ try:
187
+ result = grade_episode(grader_input)
188
+ return result
189
+ except Exception as e:
190
+ raise HTTPException(status_code=400, detail=str(e))
191
+
192
+
193
+ @app.post("/baseline", response_model=BaselineResponse, tags=["OpenEnv"])
194
+ def baseline():
195
+ """
196
+ Trigger the baseline inference script.
197
+ Runs a GPT model against all 3 tasks and returns reproducible scores.
198
+ Note: requires OPENAI_API_KEY environment variable.
199
+ """
200
+ try:
201
+ import subprocess
202
+ import json
203
+ import sys
204
+
205
+ result = subprocess.run(
206
+ [sys.executable, "baseline.py", "--output-json"],
207
+ capture_output=True,
208
+ text=True,
209
+ timeout=300,
210
+ )
211
+
212
+ if result.returncode != 0:
213
+ raise HTTPException(
214
+ status_code=500,
215
+ detail=f"Baseline script failed: {result.stderr}",
216
+ )
217
+
218
+ scores_data = json.loads(result.stdout)
219
+ return BaselineResponse(
220
+ scores=scores_data["scores"],
221
+ model_used=scores_data.get("model_used", "gpt-4o-mini"),
222
+ note=scores_data.get("note", ""),
223
+ )
224
+ except subprocess.TimeoutExpired:
225
+ raise HTTPException(status_code=504, detail="Baseline script timed out")
226
+ except json.JSONDecodeError as e:
227
+ raise HTTPException(status_code=500, detail=f"Failed to parse baseline output: {e}")
228
+
229
+
230
+ # ── Entry point ───────────────────────────────────────────────────────────
231
+
232
+ if __name__ == "__main__":
233
+ import uvicorn
234
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)
baseline.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Baseline inference script for CodeReviewEnv.
3
+
4
+ Uses Google Gemini API (FREE tier) via the OpenAI-compatible client.
5
+ Gemini free tier: 1500 requests/day on gemini-1.5-flash β€” no credit card needed.
6
+
7
+ Get your free API key at: https://aistudio.google.com/app/apikey
8
+
9
+ Usage:
10
+ python baseline.py
11
+ python baseline.py --output-json # used by /baseline endpoint
12
+ python baseline.py --task easy # single task only
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import json
18
+ import argparse
19
+ from typing import Dict, Any
20
+
21
+ from openai import OpenAI
22
+ from environment import CodeReviewEnv
23
+ from graders import grade_episode
24
+ from models import Action, CodeComment, GraderInput
25
+
26
+
27
+ # ════════════════════════════════════════════════════════════
28
+ # β–Άβ–Ά CHANGE 1: Put your free Gemini API key here
29
+ # OR set environment variable: export GEMINI_API_KEY=
30
+ # Get free key at: https://aistudio.google.com/app/apikey
31
+ # ════════════════════════════════════════════════════════════
32
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyD4ZdqU7eAlnUXD_TNpTa0UiEvTSqghhCU")
33
+
34
+ GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
35
+ DEFAULT_MODEL = "gemini-3.1-flash-lite-preview"
36
+
37
+
38
+ SYSTEM_PROMPT = """You are an expert code reviewer. You will be given a code diff from a pull request.
39
+ Your job is to identify ALL bugs, security vulnerabilities, performance issues, and logic errors.
40
+
41
+ For each issue you find, specify:
42
+ - line_number: integer line number in the diff
43
+ - issue_type: one of "bug", "security", "performance", "style", "logic"
44
+ - severity: one of "critical", "major", "minor"
45
+ - description: clear explanation
46
+ - suggested_fix: optional fix
47
+
48
+ Respond with ONLY valid JSON, no markdown, no extra text:
49
+ {
50
+ "comments": [
51
+ {
52
+ "line_number": <int>,
53
+ "issue_type": "<type>",
54
+ "severity": "<severity>",
55
+ "description": "<description>",
56
+ "suggested_fix": "<optional>"
57
+ }
58
+ ],
59
+ "verdict": "<approve|request_changes|comment>",
60
+ "summary": "<brief summary>"
61
+ }
62
+
63
+ Look for: empty list crashes, SQL injection, hardcoded secrets, weak crypto (MD5),
64
+ race conditions, silent exceptions, dict mutation during iteration, logic errors."""
65
+
66
+
67
+ def build_user_prompt(obs: Dict[str, Any]) -> str:
68
+ return f"""PR Title: {obs['pr_title']}
69
+ File: {obs['file_name']}
70
+ Task: {obs['task_description']}
71
+
72
+ Code Diff:
73
+ {obs['diff']}
74
+
75
+ Return ONLY a JSON object with your findings."""
76
+
77
+
78
+ def parse_llm_response(content: str) -> Action:
79
+ clean = content.strip()
80
+ if clean.startswith("```"):
81
+ lines = clean.split("\n")
82
+ clean = "\n".join(lines[1:])
83
+ if clean.strip().endswith("```"):
84
+ clean = clean.strip()[:-3].strip()
85
+
86
+ data = json.loads(clean)
87
+ comments = []
88
+ for c in data.get("comments", []):
89
+ try:
90
+ comments.append(CodeComment(
91
+ line_number=int(c.get("line_number", 1)),
92
+ issue_type=c.get("issue_type", "bug"),
93
+ severity=c.get("severity", "minor"),
94
+ description=str(c.get("description", "")),
95
+ suggested_fix=c.get("suggested_fix"),
96
+ ))
97
+ except Exception:
98
+ continue
99
+ return Action(
100
+ comments=comments,
101
+ verdict=data.get("verdict", "comment"),
102
+ summary=data.get("summary"),
103
+ )
104
+
105
+
106
+ def run_task(client: OpenAI, task_id: str, model: str, verbose: bool = True) -> Dict[str, Any]:
107
+ env = CodeReviewEnv(task_id=task_id)
108
+ obs = env.reset(task_id=task_id)
109
+
110
+ if verbose:
111
+ print(f"\n{'='*60}\n Task: {task_id.upper()} β€” {obs.file_name}\n{'='*60}")
112
+
113
+ try:
114
+ response = client.chat.completions.create(
115
+ model=model,
116
+ messages=[
117
+ {"role": "system", "content": SYSTEM_PROMPT},
118
+ {"role": "user", "content": build_user_prompt(obs.model_dump())},
119
+ ],
120
+ temperature=0.0,
121
+ max_tokens=2000,
122
+ )
123
+ action = parse_llm_response(response.choices[0].message.content)
124
+ except Exception as e:
125
+ if verbose:
126
+ print(f" [ERROR] {e}")
127
+ action = Action(comments=[], verdict="comment", summary=f"Error: {e}")
128
+
129
+ _, reward, _, info = env.step(action)
130
+ episode_history = [{
131
+ "step": 1,
132
+ "action": action.model_dump(),
133
+ "reward": reward,
134
+ "reward_breakdown": info.get("reward_breakdown", {}),
135
+ "reward_message": info.get("reward_message", ""),
136
+ "issues_found_this_step": info.get("issues_found", 0),
137
+ "false_positives_this_step": info.get("false_positives", 0),
138
+ }]
139
+
140
+ result = grade_episode(GraderInput(task_id=task_id, episode_history=episode_history))
141
+
142
+ if verbose:
143
+ print(f" Comments : {len(action.comments)}")
144
+ print(f" Verdict : {action.verdict}")
145
+ print(f" Score : {result.score:.4f}")
146
+ print(f" Feedback : {result.feedback[:100]}")
147
+
148
+ return {
149
+ "task_id": task_id,
150
+ "task_name": env._task.get("name", task_id),
151
+ "difficulty": env._task.get("difficulty", task_id),
152
+ "score": result.score,
153
+ "feedback": result.feedback,
154
+ }
155
+
156
+
157
+ def main():
158
+ parser = argparse.ArgumentParser()
159
+ parser.add_argument("--model", default=DEFAULT_MODEL)
160
+ parser.add_argument("--task", default=None)
161
+ parser.add_argument("--output-json", action="store_true")
162
+ args = parser.parse_args()
163
+
164
+ if GEMINI_API_KEY == "YOUR_GEMINI_API_KEY_HERE":
165
+ print("ERROR: Set GEMINI_API_KEY env variable or edit baseline.py", file=sys.stderr)
166
+ print("Get free key: https://aistudio.google.com/app/apikey", file=sys.stderr)
167
+ sys.exit(1)
168
+
169
+ client = OpenAI(api_key=GEMINI_API_KEY, base_url=GEMINI_BASE_URL)
170
+ task_ids = [args.task] if args.task else ["easy", "medium", "hard"]
171
+ results = [run_task(client, t, args.model, not args.output_json) for t in task_ids]
172
+
173
+ if args.output_json:
174
+ print(json.dumps({
175
+ "scores": [{"task_id": r["task_id"], "task_name": r["task_name"],
176
+ "difficulty": r["difficulty"], "score": r["score"],
177
+ "feedback": r["feedback"]} for r in results],
178
+ "model_used": args.model,
179
+ "note": "Temperature=0. Provider: Google Gemini free tier.",
180
+ }))
181
+ else:
182
+ print(f"\n{'='*60}\n BASELINE SCORES\n{'='*60}")
183
+ for r in results:
184
+ bar = "β–ˆ" * int(r["score"]*20) + "β–‘" * (20 - int(r["score"]*20))
185
+ print(f" {r['task_id']:8s} [{bar}] {r['score']:.4f}")
186
+ print(f" Average: {sum(r['score'] for r in results)/len(results):.4f}")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
environment.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeReviewEnv β€” OpenEnv-compliant environment for AI agent code review training.
3
+
4
+ Implements: step() / reset() / state()
5
+ """
6
+
7
+ from typing import Any, Dict, Optional, Tuple
8
+ from models import Action, Observation, Reward, EnvironmentState
9
+ from tasks import get_task, get_all_tasks
10
+ from reward import calculate_reward
11
+
12
+
13
+ class CodeReviewEnv:
14
+ """
15
+ An OpenEnv environment that simulates a human code reviewer's workflow.
16
+
17
+ The agent receives a code diff and must identify bugs, security issues,
18
+ and style problems by submitting CodeComment actions with severity ratings.
19
+ At the end of each episode the agent issues a final verdict.
20
+ """
21
+
22
+ def __init__(self, task_id: str = "easy"):
23
+ self.task_id = task_id
24
+ self._task: Dict[str, Any] = {}
25
+ self._step_number: int = 0
26
+ self._done: bool = False
27
+ self._total_reward: float = 0.0
28
+ self._episode_history: list = []
29
+ self._current_observation: Optional[Observation] = None
30
+
31
+ # ──────────────────────────────────────────────────────────────────────
32
+ # OpenEnv Core API
33
+ # ──────────────────────────────────────────────────────────────────────
34
+
35
+ def reset(self, task_id: Optional[str] = None) -> Observation:
36
+ """Reset the environment to a clean initial state. Returns first observation."""
37
+ if task_id:
38
+ self.task_id = task_id
39
+
40
+ self._task = get_task(self.task_id)
41
+ if not self._task:
42
+ raise ValueError(f"Unknown task_id: '{self.task_id}'. "
43
+ f"Valid options: {[t['id'] for t in get_all_tasks()]}")
44
+
45
+ self._step_number = 0
46
+ self._done = False
47
+ self._total_reward = 0.0
48
+ self._episode_history = []
49
+
50
+ self._current_observation = Observation(
51
+ diff=self._task["diff"],
52
+ file_name=self._task["file_name"],
53
+ pr_title=self._task["pr_title"],
54
+ pr_description=self._task["pr_description"],
55
+ step_number=self._step_number,
56
+ max_steps=self._task["max_steps"],
57
+ task_id=self.task_id,
58
+ task_description=self._task["description"],
59
+ )
60
+ return self._current_observation
61
+
62
+ def step(self, action: Action) -> Tuple[Observation, float, bool, Dict[str, Any]]:
63
+ """
64
+ Process one agent action and return (observation, reward, done, info).
65
+
66
+ Args:
67
+ action: An Action object with comments and a verdict.
68
+
69
+ Returns:
70
+ observation: Updated observation (same diff, updated step counter).
71
+ reward: Float reward signal for this step.
72
+ done: True when episode is complete.
73
+ info: Dict with reward breakdown and debug info.
74
+ """
75
+ if self._done:
76
+ raise RuntimeError(
77
+ "Episode is finished. Call reset() to start a new episode."
78
+ )
79
+ if not self._task:
80
+ raise RuntimeError("Environment not initialised. Call reset() first.")
81
+
82
+ self._step_number += 1
83
+
84
+ # Calculate reward for this action
85
+ reward_obj: Reward = calculate_reward(
86
+ action=action,
87
+ known_issues=self._task["known_issues"],
88
+ required_verdict=self._task["required_verdict"],
89
+ step_number=self._step_number,
90
+ )
91
+
92
+ self._total_reward += reward_obj.value
93
+
94
+ # Record step in history
95
+ step_record = {
96
+ "step": self._step_number,
97
+ "action": action.model_dump(),
98
+ "reward": reward_obj.value,
99
+ "reward_breakdown": reward_obj.breakdown,
100
+ "reward_message": reward_obj.message,
101
+ "issues_found_this_step": reward_obj.issues_found,
102
+ "false_positives_this_step": reward_obj.false_positives,
103
+ }
104
+ self._episode_history.append(step_record)
105
+
106
+ # Determine if episode is done
107
+ max_steps = self._task["max_steps"]
108
+ verdict_issued = action.verdict in ("approve", "request_changes")
109
+ self._done = verdict_issued or (self._step_number >= max_steps)
110
+
111
+ # Build next observation
112
+ self._current_observation = Observation(
113
+ diff=self._task["diff"],
114
+ file_name=self._task["file_name"],
115
+ pr_title=self._task["pr_title"],
116
+ pr_description=self._task["pr_description"],
117
+ step_number=self._step_number,
118
+ max_steps=max_steps,
119
+ task_id=self.task_id,
120
+ task_description=self._task["description"],
121
+ )
122
+
123
+ info = {
124
+ "reward_breakdown": reward_obj.breakdown,
125
+ "reward_message": reward_obj.message,
126
+ "issues_found": reward_obj.issues_found,
127
+ "issues_missed": reward_obj.issues_missed,
128
+ "false_positives": reward_obj.false_positives,
129
+ "total_reward_so_far": round(self._total_reward, 4),
130
+ "steps_remaining": max(0, max_steps - self._step_number),
131
+ }
132
+
133
+ return self._current_observation, reward_obj.value, self._done, info
134
+
135
+ def state(self) -> EnvironmentState:
136
+ """Return the full current internal state of the environment."""
137
+ return EnvironmentState(
138
+ task_id=self.task_id,
139
+ step_number=self._step_number,
140
+ max_steps=self._task.get("max_steps", 0) if self._task else 0,
141
+ done=self._done,
142
+ total_reward=round(self._total_reward, 4),
143
+ current_diff=self._task.get("diff", "") if self._task else "",
144
+ known_issue_count=len(self._task.get("known_issues", [])) if self._task else 0,
145
+ agent_comment_count=sum(
146
+ len(s.get("action", {}).get("comments", []))
147
+ for s in self._episode_history
148
+ ),
149
+ episode_history=self._episode_history,
150
+ )
graders.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Graders for each task. Each grader accepts episode history and returns
3
+ a deterministic float score between 0.0 and 1.0.
4
+ """
5
+
6
+ from typing import List, Dict, Any
7
+ from reward import match_comments_to_issues
8
+ from tasks import get_task
9
+ from models import GraderInput, GraderOutput
10
+
11
+
12
+ def _extract_all_comments(episode_history: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
13
+ """Collect all comments the agent made across all steps in the episode."""
14
+ all_comments = []
15
+ for step in episode_history:
16
+ action = step.get("action", {})
17
+ comments = action.get("comments", [])
18
+ all_comments.extend(comments)
19
+ return all_comments
20
+
21
+
22
+ def _get_final_verdict(episode_history: List[Dict[str, Any]]) -> str:
23
+ """Get the last verdict the agent issued."""
24
+ for step in reversed(episode_history):
25
+ verdict = step.get("action", {}).get("verdict")
26
+ if verdict:
27
+ return verdict
28
+ return "comment"
29
+
30
+
31
+ def grade_episode(grader_input: GraderInput) -> GraderOutput:
32
+ """
33
+ Master grader β€” routes to the correct task grader and returns
34
+ a deterministic, reproducible score between 0.0 and 1.0.
35
+ """
36
+ task = get_task(grader_input.task_id)
37
+ if not task:
38
+ return GraderOutput(
39
+ score=0.0,
40
+ task_id=grader_input.task_id,
41
+ breakdown={"error": 0.0},
42
+ feedback="Unknown task ID",
43
+ issues_found=0,
44
+ issues_missed=0,
45
+ false_positives=0,
46
+ )
47
+
48
+ known_issues: List[Dict[str, Any]] = task["known_issues"]
49
+ required_verdict: str = task["required_verdict"]
50
+ history = grader_input.episode_history
51
+
52
+ all_comments = _extract_all_comments(history)
53
+ final_verdict = _get_final_verdict(history)
54
+
55
+ issues_found, issues_missed, false_positives = match_comments_to_issues(
56
+ all_comments, known_issues
57
+ )
58
+
59
+ total_issues = len(known_issues)
60
+ breakdown: Dict[str, float] = {}
61
+ feedback_parts: List[str] = []
62
+
63
+ # ── 1. Detection rate (60% of score) ──────────────────────────────────
64
+ detection_rate = issues_found / total_issues if total_issues > 0 else 0.0
65
+ detection_score = round(detection_rate * 0.60, 4)
66
+ breakdown["detection_score"] = detection_score
67
+ feedback_parts.append(
68
+ f"Detected {issues_found}/{total_issues} issues (detection_score={detection_score:.3f})"
69
+ )
70
+
71
+ # ── 2. Precision penalty β€” false positives (max -0.20) ────────────────
72
+ precision_penalty = min(0.20, false_positives * 0.05)
73
+ breakdown["precision_penalty"] = round(-precision_penalty, 4)
74
+ if false_positives > 0:
75
+ feedback_parts.append(
76
+ f"{false_positives} false positive(s) (penalty={-precision_penalty:.3f})"
77
+ )
78
+
79
+ # ── 3. Verdict correctness (15% of score) ─────────────────────────────
80
+ if final_verdict == required_verdict:
81
+ verdict_score = 0.15
82
+ feedback_parts.append(f"Correct verdict '{final_verdict}' (+0.15)")
83
+ else:
84
+ verdict_score = 0.0
85
+ feedback_parts.append(
86
+ f"Wrong verdict '{final_verdict}', expected '{required_verdict}' (+0.00)"
87
+ )
88
+ breakdown["verdict_score"] = verdict_score
89
+
90
+ # ── 4. Severity weighting bonus (up to 0.15) ──────────────────────────
91
+ severity_map = {"critical": 3, "major": 2, "minor": 1}
92
+ total_severity_weight = sum(
93
+ severity_map.get(i["severity"], 1) for i in known_issues
94
+ )
95
+ found_severity_weight = 0.0
96
+ matched = set()
97
+ for comment in all_comments:
98
+ for idx, issue in enumerate(known_issues):
99
+ if idx in matched:
100
+ continue
101
+ from reward import _line_proximity, _keywords_match
102
+ if _line_proximity(comment.get("line_number", 0), issue["line_number"]) and \
103
+ _keywords_match(comment.get("description", ""), issue["keywords"]):
104
+ found_severity_weight += severity_map.get(issue["severity"], 1)
105
+ matched.add(idx)
106
+ break
107
+
108
+ severity_score = 0.0
109
+ if total_severity_weight > 0:
110
+ severity_score = round(
111
+ (found_severity_weight / total_severity_weight) * 0.15, 4
112
+ )
113
+ breakdown["severity_weighted_score"] = severity_score
114
+ if severity_score > 0:
115
+ feedback_parts.append(f"Severity-weighted coverage: +{severity_score:.3f}")
116
+
117
+ # ── 5. Efficiency bonus β€” fewer steps = small bonus (up to 0.10) ──────
118
+ steps_used = len(history)
119
+ max_steps = task.get("max_steps", 5)
120
+ if steps_used <= max(1, max_steps // 2):
121
+ efficiency_bonus = 0.10
122
+ elif steps_used <= max_steps:
123
+ efficiency_bonus = 0.05
124
+ else:
125
+ efficiency_bonus = 0.0
126
+ breakdown["efficiency_bonus"] = efficiency_bonus
127
+
128
+ # ── Final score ────────────────────────────────────────────────────────
129
+ raw_score = (
130
+ detection_score
131
+ - precision_penalty
132
+ + verdict_score
133
+ + severity_score
134
+ + efficiency_bonus
135
+ )
136
+ final_score = round(max(0.0, min(1.0, raw_score)), 4)
137
+
138
+ return GraderOutput(
139
+ score=final_score,
140
+ task_id=grader_input.task_id,
141
+ breakdown=breakdown,
142
+ feedback=" | ".join(feedback_parts),
143
+ issues_found=issues_found,
144
+ issues_missed=issues_missed,
145
+ false_positives=false_positives,
146
+ )
models.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List, Literal, Dict, Any
3
+
4
+
5
+ class CodeComment(BaseModel):
6
+ line_number: int = Field(..., description="Line number being commented on (1-indexed)")
7
+ issue_type: Literal["bug", "security", "performance", "style", "logic"] = Field(
8
+ ..., description="Type of issue found"
9
+ )
10
+ severity: Literal["critical", "major", "minor"] = Field(
11
+ ..., description="Severity level of the issue"
12
+ )
13
+ description: str = Field(..., description="Detailed description of the issue found")
14
+ suggested_fix: Optional[str] = Field(None, description="Suggested fix for the issue")
15
+
16
+
17
+ class Action(BaseModel):
18
+ comments: List[CodeComment] = Field(
19
+ default_factory=list, description="List of code review comments on specific lines"
20
+ )
21
+ verdict: Literal["approve", "request_changes", "comment"] = Field(
22
+ ..., description="Final verdict on the pull request"
23
+ )
24
+ summary: Optional[str] = Field(None, description="Overall summary of the review")
25
+
26
+
27
+ class Observation(BaseModel):
28
+ diff: str = Field(..., description="The code diff/patch to review")
29
+ file_name: str = Field(..., description="Name of the file being reviewed")
30
+ pr_title: str = Field(..., description="Title of the pull request")
31
+ pr_description: str = Field(..., description="Description of the pull request")
32
+ step_number: int = Field(..., description="Current step number in the episode")
33
+ max_steps: int = Field(..., description="Maximum steps allowed in this episode")
34
+ task_id: str = Field(..., description="Current task identifier (easy/medium/hard)")
35
+ task_description: str = Field(..., description="Description of the task objective")
36
+
37
+
38
+ class Reward(BaseModel):
39
+ value: float = Field(..., description="Reward value between -1.0 and 1.0")
40
+ breakdown: Dict[str, float] = Field(
41
+ default_factory=dict, description="Breakdown of reward components"
42
+ )
43
+ message: str = Field(..., description="Human-readable explanation of the reward")
44
+ issues_found: int = Field(0, description="Number of issues correctly identified")
45
+ issues_missed: int = Field(0, description="Number of known issues missed")
46
+ false_positives: int = Field(0, description="Number of false positive comments")
47
+
48
+
49
+ class EnvironmentState(BaseModel):
50
+ task_id: str
51
+ step_number: int
52
+ max_steps: int
53
+ done: bool
54
+ total_reward: float
55
+ current_diff: str
56
+ known_issue_count: int
57
+ agent_comment_count: int
58
+ episode_history: List[Dict[str, Any]]
59
+
60
+
61
+ class TaskInfo(BaseModel):
62
+ id: str
63
+ name: str
64
+ description: str
65
+ difficulty: Literal["easy", "medium", "hard"]
66
+ max_steps: int
67
+ action_schema: Dict[str, Any]
68
+
69
+
70
+ class GraderInput(BaseModel):
71
+ task_id: str
72
+ episode_history: List[Dict[str, Any]]
73
+ final_action: Optional[Dict[str, Any]] = None
74
+
75
+
76
+ class GraderOutput(BaseModel):
77
+ score: float = Field(..., ge=0.0, le=1.0, description="Score between 0.0 and 1.0")
78
+ task_id: str
79
+ breakdown: Dict[str, float]
80
+ feedback: str
81
+ issues_found: int
82
+ issues_missed: int
83
+ false_positives: int
pyrightconfig.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "include": ["."],
3
+ "extraPaths": ["."],
4
+ "pythonVersion": "3.10",
5
+ "reportMissingImports": false
6
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.30.6
3
+ pydantic==2.8.2
4
+ openai==1.51.0
5
+ pyyaml==6.0.2
6
+ python-dotenv==1.0.1
7
+ httpx==0.27.2
reward.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any, Tuple
2
+ from models import Action, Reward
3
+
4
+
5
+ SEVERITY_SCORES = {
6
+ "critical": 0.20,
7
+ "major": 0.12,
8
+ "minor": 0.05,
9
+ }
10
+
11
+ FALSE_POSITIVE_PENALTY = -0.08
12
+ WRONG_VERDICT_PENALTY = -0.15
13
+ CORRECT_VERDICT_BONUS = 0.10
14
+ STEP_PENALTY = -0.02 # small penalty per step to encourage efficiency
15
+
16
+
17
+ def _keywords_match(comment_text: str, known_keywords: List[str]) -> bool:
18
+ """Check if a comment description contains enough keywords to match a known issue."""
19
+ text_lower = comment_text.lower()
20
+ matches = sum(1 for kw in known_keywords if kw.lower() in text_lower)
21
+ return matches >= 1
22
+
23
+
24
+ def _line_proximity(comment_line: int, known_line: int, tolerance: int = 4) -> bool:
25
+ """Allow line number matching within a tolerance window."""
26
+ return abs(comment_line - known_line) <= tolerance
27
+
28
+
29
+ def match_comments_to_issues(
30
+ comments: List[Dict[str, Any]],
31
+ known_issues: List[Dict[str, Any]],
32
+ ) -> Tuple[int, int, int]:
33
+ """
34
+ Match agent comments against known issues.
35
+ Returns: (issues_found, issues_missed, false_positives)
36
+ """
37
+ matched_issues = set()
38
+ false_positives = 0
39
+
40
+ for comment in comments:
41
+ comment_matched = False
42
+ for idx, issue in enumerate(known_issues):
43
+ if idx in matched_issues:
44
+ continue
45
+ line_ok = _line_proximity(
46
+ comment.get("line_number", 0), issue["line_number"]
47
+ )
48
+ keyword_ok = _keywords_match(
49
+ comment.get("description", ""), issue["keywords"]
50
+ )
51
+ if line_ok and keyword_ok:
52
+ matched_issues.add(idx)
53
+ comment_matched = True
54
+ break
55
+
56
+ if not comment_matched:
57
+ false_positives += 1
58
+
59
+ issues_found = len(matched_issues)
60
+ issues_missed = len(known_issues) - issues_found
61
+ return issues_found, issues_missed, false_positives
62
+
63
+
64
+ def calculate_reward(
65
+ action: Action,
66
+ known_issues: List[Dict[str, Any]],
67
+ required_verdict: str,
68
+ step_number: int,
69
+ ) -> Reward:
70
+ """
71
+ Calculate reward with full trajectory signal (not just binary end-of-episode).
72
+ Rewards partial progress and penalizes undesirable behaviour.
73
+ """
74
+ comments_data = [c.model_dump() for c in action.comments]
75
+ issues_found, issues_missed, false_positives = match_comments_to_issues(
76
+ comments_data, known_issues
77
+ )
78
+
79
+ breakdown: Dict[str, float] = {}
80
+
81
+ # --- Positive: reward for each correctly identified issue ---
82
+ # Weight by severity of the issues found
83
+ issue_reward = 0.0
84
+ matched_issues = set()
85
+ for comment in comments_data:
86
+ for idx, issue in enumerate(known_issues):
87
+ if idx in matched_issues:
88
+ continue
89
+ if _line_proximity(comment.get("line_number", 0), issue["line_number"]) and \
90
+ _keywords_match(comment.get("description", ""), issue["keywords"]):
91
+ severity_bonus = SEVERITY_SCORES.get(issue["severity"], 0.05)
92
+ issue_reward += severity_bonus
93
+ matched_issues.add(idx)
94
+ break
95
+
96
+ breakdown["issue_detection"] = round(issue_reward, 4)
97
+
98
+ # --- Negative: penalty for false positives ---
99
+ fp_penalty = false_positives * FALSE_POSITIVE_PENALTY
100
+ breakdown["false_positive_penalty"] = round(fp_penalty, 4)
101
+
102
+ # --- Verdict correctness ---
103
+ if action.verdict == required_verdict:
104
+ breakdown["correct_verdict"] = CORRECT_VERDICT_BONUS
105
+ else:
106
+ breakdown["wrong_verdict"] = WRONG_VERDICT_PENALTY
107
+
108
+ # --- Small step efficiency penalty ---
109
+ step_pen = step_number * STEP_PENALTY
110
+ breakdown["step_penalty"] = round(step_pen, 4)
111
+
112
+ total = sum(breakdown.values())
113
+ total = round(max(-1.0, min(1.0, total)), 4)
114
+
115
+ message_parts = []
116
+ if issues_found > 0:
117
+ message_parts.append(f"Found {issues_found}/{len(known_issues)} known issues (+{issue_reward:.2f})")
118
+ if issues_missed > 0:
119
+ message_parts.append(f"Missed {issues_missed} issue(s)")
120
+ if false_positives > 0:
121
+ message_parts.append(f"{false_positives} false positive(s) ({fp_penalty:.2f})")
122
+ if action.verdict == required_verdict:
123
+ message_parts.append(f"Correct verdict '{action.verdict}' (+{CORRECT_VERDICT_BONUS})")
124
+ else:
125
+ message_parts.append(
126
+ f"Wrong verdict '{action.verdict}' (expected '{required_verdict}') ({WRONG_VERDICT_PENALTY})"
127
+ )
128
+
129
+ return Reward(
130
+ value=total,
131
+ breakdown=breakdown,
132
+ message=" | ".join(message_parts) if message_parts else "No reward signal",
133
+ issues_found=issues_found,
134
+ issues_missed=issues_missed,
135
+ false_positives=false_positives,
136
+ )
run_test.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quick diagnostic: test all imports and run baseline for 'easy' task.
3
+ """
4
+ import sys
5
+ import traceback
6
+
7
+ print("Python:", sys.version)
8
+ print("Testing imports...")
9
+
10
+ try:
11
+ from openai import OpenAI
12
+ print("[OK] openai")
13
+ except ImportError as e:
14
+ print(f"[FAIL] openai: {e}")
15
+
16
+ try:
17
+ from environment import CodeReviewEnv
18
+ print("[OK] environment")
19
+ except Exception as e:
20
+ print(f"[FAIL] environment: {e}")
21
+ traceback.print_exc()
22
+
23
+ try:
24
+ from graders import grade_episode
25
+ print("[OK] graders")
26
+ except Exception as e:
27
+ print(f"[FAIL] graders: {e}")
28
+ traceback.print_exc()
29
+
30
+ try:
31
+ from models import Action, CodeComment, GraderInput
32
+ print("[OK] models")
33
+ except Exception as e:
34
+ print(f"[FAIL] models: {e}")
35
+ traceback.print_exc()
36
+
37
+ print("\nAll import checks done.")
38
+
39
+ # Now try running the actual baseline
40
+ try:
41
+ import baseline
42
+ # Override sys.argv to simulate --task easy
43
+ sys.argv = ["baseline.py", "--task", "easy"]
44
+ baseline.main()
45
+ except Exception as e:
46
+ print(f"\n[ERROR in main]: {e}")
47
+ traceback.print_exc()
tasks.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, List
2
+
3
+ TASKS: Dict[str, Dict[str, Any]] = {
4
+ "easy": {
5
+ "id": "easy",
6
+ "name": "Basic Bug Detection",
7
+ "description": (
8
+ "Review a simple Python utility module. Find edge case bugs and "
9
+ "performance issues. The code looks functional at first glance but "
10
+ "has several critical and minor issues."
11
+ ),
12
+ "difficulty": "easy",
13
+ "max_steps": 3,
14
+ "pr_title": "Add calculate_statistics utility module",
15
+ "pr_description": (
16
+ "Adding utility functions for calculating statistics on lists of numbers. "
17
+ "Used by the analytics dashboard."
18
+ ),
19
+ "file_name": "utils/statistics.py",
20
+ "diff": """\
21
+ --- a/utils/statistics.py
22
+ +++ b/utils/statistics.py
23
+ @@ -0,0 +1,30 @@
24
+ +def calculate_average(numbers):
25
+ + total = 0
26
+ + for num in numbers:
27
+ + total += num
28
+ + return total / len(numbers)
29
+ +
30
+ +def calculate_max(numbers):
31
+ + max_val = numbers[0]
32
+ + for num in numbers:
33
+ + if num > max_val:
34
+ + max_val = num
35
+ + return max_val
36
+ +
37
+ +def get_percentage(value, total):
38
+ + return (value / total) * 100
39
+ +
40
+ +def find_duplicates(items):
41
+ + seen = []
42
+ + duplicates = []
43
+ + for item in items:
44
+ + if item in seen:
45
+ + duplicates.append(item)
46
+ + seen.append(item)
47
+ + return duplicates
48
+ +
49
+ +def safe_divide(a, b):
50
+ + if b != 0:
51
+ + return a / b
52
+ + else:
53
+ + return 0
54
+ """,
55
+ "known_issues": [
56
+ {
57
+ "line_number": 5,
58
+ "issue_type": "bug",
59
+ "severity": "critical",
60
+ "description": "ZeroDivisionError when numbers list is empty β€” len(numbers) returns 0",
61
+ "keywords": ["zero", "division", "empty", "len", "zerodivision", "divide by zero"],
62
+ },
63
+ {
64
+ "line_number": 8,
65
+ "issue_type": "bug",
66
+ "severity": "critical",
67
+ "description": "IndexError when numbers list is empty β€” numbers[0] raises IndexError",
68
+ "keywords": ["index", "indexerror", "empty", "list", "numbers[0]", "first element"],
69
+ },
70
+ {
71
+ "line_number": 14,
72
+ "issue_type": "bug",
73
+ "severity": "critical",
74
+ "description": "ZeroDivisionError in get_percentage when total is 0",
75
+ "keywords": ["zero", "division", "total", "percentage", "zerodivision"],
76
+ },
77
+ {
78
+ "line_number": 17,
79
+ "issue_type": "performance",
80
+ "severity": "minor",
81
+ "description": "Using list for 'seen' causes O(n^2) complexity β€” use a set for O(1) lookups",
82
+ "keywords": ["set", "performance", "o(n)", "o(n^2)", "lookup", "efficiency", "complexity"],
83
+ },
84
+ ],
85
+ "required_verdict": "request_changes",
86
+ "success_threshold": 0.6,
87
+ },
88
+
89
+ "medium": {
90
+ "id": "medium",
91
+ "name": "Security Vulnerability Review",
92
+ "description": (
93
+ "Review a user authentication module. Identify security vulnerabilities "
94
+ "including SQL injection, hardcoded credentials, weak cryptography, and "
95
+ "logic errors in permission checking."
96
+ ),
97
+ "difficulty": "medium",
98
+ "max_steps": 5,
99
+ "pr_title": "Add user authentication and database access layer",
100
+ "pr_description": (
101
+ "Implements user login, password reset, and role-based permission checks. "
102
+ "Connects to PostgreSQL for user data."
103
+ ),
104
+ "file_name": "auth/user_manager.py",
105
+ "diff": """\
106
+ --- a/auth/user_manager.py
107
+ +++ b/auth/user_manager.py
108
+ @@ -0,0 +1,52 @@
109
+ +import hashlib
110
+ +
111
+ +DB_PASSWORD = "admin123"
112
+ +API_SECRET = "supersecret_key_do_not_share"
113
+ +
114
+ +def get_user(user_id):
115
+ + query = f"SELECT * FROM users WHERE id = {user_id}"
116
+ + return execute_query(query)
117
+ +
118
+ +def login(username, password):
119
+ + hashed = hashlib.md5(password.encode()).hexdigest()
120
+ + query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{hashed}'"
121
+ + user = execute_query(query)
122
+ + if user:
123
+ + return {"status": "success", "user": user}
124
+ + return {"status": "failed"}
125
+ +
126
+ +def reset_password(email, new_password):
127
+ + if len(new_password) > 6:
128
+ + query = f"UPDATE users SET password = '{new_password}' WHERE email = '{email}'"
129
+ + execute_query(query)
130
+ + return True
131
+ + return False
132
+ +
133
+ +def delete_user(user_id):
134
+ + execute_query(f"DELETE FROM users WHERE id = {user_id}")
135
+ + return True
136
+ +
137
+ +def serialize_user(user_data):
138
+ + import pickle
139
+ + return pickle.dumps(user_data)
140
+ +
141
+ +def deserialize_user(data):
142
+ + import pickle
143
+ + return pickle.loads(data)
144
+ +
145
+ +def log_action(user, action):
146
+ + log_entry = f"[{action}] User: {user}"
147
+ + print(log_entry)
148
+ +
149
+ +def check_permission(user_role, required_role):
150
+ + roles = ["user", "moderator", "admin"]
151
+ + return roles.index(user_role) == roles.index(required_role)
152
+ """,
153
+ "known_issues": [
154
+ {
155
+ "line_number": 3,
156
+ "issue_type": "security",
157
+ "severity": "critical",
158
+ "description": "Hardcoded database password in source code β€” use environment variables",
159
+ "keywords": ["hardcoded", "password", "credential", "environment variable", "env", "secret"],
160
+ },
161
+ {
162
+ "line_number": 4,
163
+ "issue_type": "security",
164
+ "severity": "critical",
165
+ "description": "Hardcoded API secret key in source code β€” use environment variables",
166
+ "keywords": ["hardcoded", "secret", "api key", "credential", "environment variable", "env"],
167
+ },
168
+ {
169
+ "line_number": 7,
170
+ "issue_type": "security",
171
+ "severity": "critical",
172
+ "description": "SQL injection β€” user_id interpolated directly into query, use parameterized queries",
173
+ "keywords": ["sql injection", "parameterized", "sanitize", "f-string", "interpolat", "injection"],
174
+ },
175
+ {
176
+ "line_number": 11,
177
+ "issue_type": "security",
178
+ "severity": "critical",
179
+ "description": "MD5 is cryptographically broken for password hashing β€” use bcrypt or argon2",
180
+ "keywords": ["md5", "bcrypt", "argon2", "hash", "cryptograph", "broken", "weak"],
181
+ },
182
+ {
183
+ "line_number": 12,
184
+ "issue_type": "security",
185
+ "severity": "critical",
186
+ "description": "SQL injection in login β€” username interpolated into query string",
187
+ "keywords": ["sql injection", "parameterized", "username", "injection", "interpolat"],
188
+ },
189
+ {
190
+ "line_number": 20,
191
+ "issue_type": "security",
192
+ "severity": "critical",
193
+ "description": "Password stored in plaintext β€” must be hashed before storage",
194
+ "keywords": ["plaintext", "hash", "password", "bcrypt", "plain text", "unhashed"],
195
+ },
196
+ {
197
+ "line_number": 33,
198
+ "issue_type": "security",
199
+ "severity": "critical",
200
+ "description": "pickle.loads() on untrusted data allows arbitrary code execution β€” use JSON instead",
201
+ "keywords": ["pickle", "arbitrary code", "unsafe", "deserialization", "json", "remote code"],
202
+ },
203
+ {
204
+ "line_number": 41,
205
+ "issue_type": "bug",
206
+ "severity": "major",
207
+ "description": "Permission check uses == instead of >= β€” admin cannot access user-level resources",
208
+ "keywords": ["permission", ">=", "role", "hierarchy", "comparison", "greater", "equal"],
209
+ },
210
+ ],
211
+ "required_verdict": "request_changes",
212
+ "success_threshold": 0.45,
213
+ },
214
+
215
+ "hard": {
216
+ "id": "hard",
217
+ "name": "Concurrency & Architecture Bug Hunt",
218
+ "description": (
219
+ "Review a distributed rate limiter and async task queue. Find subtle "
220
+ "concurrency bugs, race conditions, silent exception swallowing, "
221
+ "mutation bugs, and architectural issues that only manifest under load."
222
+ ),
223
+ "difficulty": "hard",
224
+ "max_steps": 8,
225
+ "pr_title": "Implement distributed rate limiter and async task queue",
226
+ "pr_description": (
227
+ "Adds a rate limiter and background task queue for handling "
228
+ "high-throughput job processing with retry logic."
229
+ ),
230
+ "file_name": "core/rate_limiter.py",
231
+ "diff": """\
232
+ --- a/core/rate_limiter.py
233
+ +++ b/core/rate_limiter.py
234
+ @@ -0,0 +1,82 @@
235
+ +import time
236
+ +import threading
237
+ +from collections import defaultdict
238
+ +
239
+ +class RateLimiter:
240
+ + def __init__(self, max_requests, window_seconds):
241
+ + self.max_requests = max_requests
242
+ + self.window_seconds = window_seconds
243
+ + self.requests = defaultdict(list)
244
+ + self.lock = threading.Lock()
245
+ +
246
+ + def is_allowed(self, user_id):
247
+ + now = time.time()
248
+ + window_start = now - self.window_seconds
249
+ + with self.lock:
250
+ + self.requests[user_id] = [
251
+ + t for t in self.requests[user_id] if t > window_start
252
+ + ]
253
+ + if len(self.requests[user_id]) < self.max_requests:
254
+ + self.requests[user_id].append(now)
255
+ + return True
256
+ + return False
257
+ +
258
+ +class TaskQueue:
259
+ + def __init__(self, max_workers=4):
260
+ + self.queue = []
261
+ + self.max_workers = max_workers
262
+ + self.workers = []
263
+ + self.running = False
264
+ +
265
+ + def add_task(self, task_fn, *args):
266
+ + self.queue.append((task_fn, args))
267
+ +
268
+ + def _worker(self):
269
+ + while self.running:
270
+ + if self.queue:
271
+ + task_fn, args = self.queue.pop(0)
272
+ + try:
273
+ + task_fn(*args)
274
+ + except Exception:
275
+ + pass
276
+ + time.sleep(0.01)
277
+ +
278
+ + def start(self):
279
+ + self.running = True
280
+ + for _ in range(self.max_workers):
281
+ + t = threading.Thread(target=self._worker)
282
+ + t.start()
283
+ + self.workers.append(t)
284
+ +
285
+ + def stop(self):
286
+ + self.running = False
287
+ +
288
+ +class RetryQueue:
289
+ + def __init__(self, max_retries=3):
290
+ + self.max_retries = max_retries
291
+ + self.failed_tasks = {}
292
+ + self.retry_counts = defaultdict(int)
293
+ +
294
+ + def add_failed(self, task_id, task_fn):
295
+ + self.failed_tasks[task_id] = task_fn
296
+ +
297
+ + def retry_all(self):
298
+ + for task_id, task_fn in self.failed_tasks.items():
299
+ + if self.retry_counts[task_id] < self.max_retries:
300
+ + try:
301
+ + task_fn()
302
+ + del self.failed_tasks[task_id]
303
+ + except Exception:
304
+ + self.retry_counts[task_id] += 1
305
+ +
306
+ + def get_stats(self):
307
+ + return {
308
+ + "pending": len(self.failed_tasks),
309
+ + "total_retries": sum(self.retry_counts.values())
310
+ + }
311
+ +
312
+ +def process_batch(items, batch_size=100):
313
+ + results = []
314
+ + for i in range(0, len(items), batch_size):
315
+ + batch = items[i:i+batch_size]
316
+ + result = process_items(batch)
317
+ + results.append(result)
318
+ + return results
319
+ +
320
+ +def merge_configs(base_config, override_config):
321
+ + merged = base_config
322
+ + for key, value in override_config.items():
323
+ + merged[key] = value
324
+ + return merged
325
+ """,
326
+ "known_issues": [
327
+ {
328
+ "line_number": 9,
329
+ "issue_type": "bug",
330
+ "severity": "critical",
331
+ "description": "RateLimiter uses in-memory storage β€” not actually distributed, state not shared across processes or instances",
332
+ "keywords": ["distributed", "in-memory", "shared", "redis", "instance", "process", "not distributed"],
333
+ },
334
+ {
335
+ "line_number": 25,
336
+ "issue_type": "bug",
337
+ "severity": "critical",
338
+ "description": "TaskQueue.queue is a plain list with no lock β€” race condition when multiple workers call queue.pop(0) simultaneously",
339
+ "keywords": ["race condition", "thread", "lock", "synchronization", "queue", "concurrent", "list", "pop"],
340
+ },
341
+ {
342
+ "line_number": 39,
343
+ "issue_type": "bug",
344
+ "severity": "major",
345
+ "description": "Exceptions silently swallowed with bare except pass β€” failed tasks are lost with no logging or retry",
346
+ "keywords": ["exception", "silent", "swallow", "pass", "log", "lost", "bare except", "suppress"],
347
+ },
348
+ {
349
+ "line_number": 49,
350
+ "issue_type": "bug",
351
+ "severity": "major",
352
+ "description": "stop() sets running=False but never calls thread.join() β€” threads may still execute after stop() returns",
353
+ "keywords": ["join", "thread", "stop", "shutdown", "daemon", "join()", "running"],
354
+ },
355
+ {
356
+ "line_number": 63,
357
+ "issue_type": "bug",
358
+ "severity": "critical",
359
+ "description": "retry_all() deletes from failed_tasks while iterating over it β€” RuntimeError: dictionary changed size during iteration",
360
+ "keywords": ["dictionary", "iteration", "modify", "runtimeerror", "list()", "copy", "iterate", "delete"],
361
+ },
362
+ {
363
+ "line_number": 73,
364
+ "issue_type": "bug",
365
+ "severity": "major",
366
+ "description": "process_batch uses append instead of extend β€” returns list of batch-results not individual results, wrong data shape",
367
+ "keywords": ["extend", "append", "batch", "result", "granularity", "list", "shape"],
368
+ },
369
+ {
370
+ "line_number": 79,
371
+ "issue_type": "bug",
372
+ "severity": "major",
373
+ "description": "merge_configs mutates base_config in-place β€” merged = base_config is a reference, not a copy; use base_config.copy()",
374
+ "keywords": ["copy", "reference", "mutate", "shallow copy", "dict", "base_config", "in-place"],
375
+ },
376
+ {
377
+ "line_number": 25,
378
+ "issue_type": "performance",
379
+ "severity": "minor",
380
+ "description": "Use collections.deque instead of list for O(1) popleft() vs O(n) pop(0)",
381
+ "keywords": ["deque", "o(1)", "performance", "popleft", "list", "collections"],
382
+ },
383
+ ],
384
+ "required_verdict": "request_changes",
385
+ "success_threshold": 0.35,
386
+ },
387
+ }
388
+
389
+
390
+ def get_task(task_id: str) -> Dict[str, Any]:
391
+ return TASKS.get(task_id, {})
392
+
393
+
394
+ def get_all_tasks() -> List[Dict[str, Any]]:
395
+ return list(TASKS.values())