Spaces:
Sleeping
Sleeping
Yero commited on
Commit ·
c193fbb
1
Parent(s): 1680cfc
cleanup comments and readme
Browse files- Dockerfile +1 -0
- README.md +63 -70
- __pycache__/client.cpython-312.pyc +0 -0
- __pycache__/models.cpython-312.pyc +0 -0
- client.py +2 -0
- code_review_env.egg-info/PKG-INFO +106 -46
- code_review_env.egg-info/requires.txt +6 -0
- demo.py +1 -1
- inference.py +0 -1
- models.py +1 -0
- openenv.yaml +6 -1
- pyproject.toml +8 -1
- server/__pycache__/app.cpython-312.pyc +0 -0
- server/__pycache__/environment.cpython-312.pyc +0 -0
- server/app.py +1 -1
- server/environment.py +4 -7
- train_pytorch_agent.py +2 -2
Dockerfile
CHANGED
|
@@ -5,5 +5,6 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|
| 5 |
COPY . .
|
| 6 |
ENV PYTHONPATH=/
|
| 7 |
ENV PYTHONIOENCODING=utf-8
|
|
|
|
| 8 |
EXPOSE 7860
|
| 9 |
CMD ["python", "-m", "uvicorn", "code_review_env.server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 5 |
COPY . .
|
| 6 |
ENV PYTHONPATH=/
|
| 7 |
ENV PYTHONIOENCODING=utf-8
|
| 8 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 9 |
EXPOSE 7860
|
| 10 |
CMD ["python", "-m", "uvicorn", "code_review_env.server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -9,125 +9,118 @@ tags:
|
|
| 9 |
- openenv
|
| 10 |
---
|
| 11 |
|
| 12 |
-
# CodeReviewEnv
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
* 🚀 **Hugging Face Space (Live Environment)**: [https://huggingface.co/spaces/lucid987654/code-review-env](https://huggingface.co/spaces/lucid987654/code-review-env)
|
| 23 |
-
* 📁 **GitHub Repository**: [https://github.com/subwaycookiecrunch/Meta-project](https://github.com/subwaycookiecrunch/Meta-project)
|
| 24 |
|
| 25 |
---
|
| 26 |
|
| 27 |
-
###
|
| 28 |
|
| 29 |
-
|
| 30 |
-
We hit the agent with **Asymmetric Rewards**. In the real world, missing a critical bug (False Negative) is infinitely worse than accidentally flagging a safe file for review (False Positive).
|
| 31 |
|
| 32 |
-
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|---------|--------|-----------|
|
| 36 |
-
| True Positive (found a real bug) | **+1.0** | Highest reward — catching vulnerabilities is the goal |
|
| 37 |
-
| True Negative (correctly skipped safe file) | **+0.8** | Good judgment that saves review budget |
|
| 38 |
-
| False Positive (flagged a safe file) | **-0.4** | Wastes review budget, penalized |
|
| 39 |
-
| False Negative (missed a real bug) | **-0.2** | Worst failure — penalty provides learning signal |
|
| 40 |
-
| Over-budget flag attempt | **-0.5** | Hard constraint — budget is non-negotiable |
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
The action space is a straightforward, string-based categorical action:
|
| 50 |
-
* `decision`: `"flag"` or `"skip"`
|
| 51 |
|
| 52 |
-
###
|
| 53 |
-
At each step, the environment provides a rich state vector. The key metrics include:
|
| 54 |
-
* `file_path` & tracking metrics (`file_index`, `files_remaining`, `total_files`)
|
| 55 |
-
* **Difficulty & Context**: `difficulty_level`, `cve_id`, `repo_name`
|
| 56 |
-
* **Static Analysis Features**:
|
| 57 |
-
* `churn_score` — lines changed in the file (higher = more volatile)
|
| 58 |
-
* `complexity_score` — cyclomatic complexity proxy (higher = harder to review)
|
| 59 |
-
* `todo_score` — count of TODOs/FIXMEs (higher = more tech debt)
|
| 60 |
-
* `recency_score` — how recently the file was modified (higher = more recent)
|
| 61 |
-
* **Limits**: `review_budget` and `files_flagged`
|
| 62 |
-
* **Terminal Metrics**: `precision`, `recall`, `f1_score`, `true_positives`, `false_positives`, `false_negatives`, `true_negatives`
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
|
| 72 |
---
|
| 73 |
|
| 74 |
-
##
|
| 75 |
|
| 76 |
-
**
|
| 77 |
```bash
|
| 78 |
pip install openenv-core openai
|
| 79 |
```
|
| 80 |
|
| 81 |
-
**
|
| 82 |
```bash
|
| 83 |
docker build -t codereviewenv .
|
| 84 |
docker run -p 7860:7860 codereviewenv
|
| 85 |
```
|
| 86 |
-
*(If you are viewing this on Hugging Face Spaces, the server is automatically running!)*
|
| 87 |
|
| 88 |
-
**
|
| 89 |
```bash
|
| 90 |
-
export HF_TOKEN="
|
| 91 |
python inference.py
|
| 92 |
```
|
| 93 |
|
| 94 |
---
|
| 95 |
|
| 96 |
-
##
|
|
|
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
This is the standard OpenEnv submission script required by the Hackathon. We wrote a wrapper that passes the environment state into an OpenAI-compatible LLM to see if a huge model can reason through the file stats to allocate its budget. It evaluates the environment sequentially over the **Easy**, **Medium**, and **Hard** tasks.
|
| 100 |
|
| 101 |
-
|
| 102 |
|
| 103 |
-
| Difficulty |
|
| 104 |
-
|---
|
| 105 |
-
| Easy |
|
| 106 |
-
| Medium |
|
| 107 |
-
| Hard |
|
| 108 |
|
| 109 |
-
|
| 110 |
|
| 111 |
```bash
|
| 112 |
-
export HF_TOKEN="
|
| 113 |
python inference.py
|
| 114 |
```
|
| 115 |
|
| 116 |
-
###
|
| 117 |
-
|
|
|
|
| 118 |
|
| 119 |
-
We built a custom Deep Reinforcement Learning Agent using native PyTorch Policy Gradients (REINFORCE) to interface perfectly with the OpenEnv API. It iteratively converges to find the perfect risk/reward strategy.
|
| 120 |
```bash
|
| 121 |
pip install torch
|
| 122 |
python train_pytorch_agent.py
|
| 123 |
```
|
| 124 |
|
| 125 |
-
##
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
| 132 |
|
| 133 |
-
|
|
|
|
| 9 |
- openenv
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# CodeReviewEnv
|
| 13 |
|
| 14 |
+
An RL environment for vulnerability triage, built on real CVE data from the NVD.
|
| 15 |
|
| 16 |
+
The idea: most RL envs are toy problems (gridworld, cartpole, etc). We wanted something closer to what devs actually deal with — triaging security patches across a codebase with limited time and attention.
|
| 17 |
|
| 18 |
+
The agent gets a stream of files from a real CVE patch and has to decide: **flag** this file for human review, or **skip** it. There's a fixed review budget so you can't just flag everything.
|
| 19 |
|
| 20 |
+
* **HF Space**: https://huggingface.co/spaces/lucid987654/code-review-env
|
| 21 |
+
* **GitHub**: https://github.com/subwaycookiecrunch/Meta-project
|
|
|
|
|
|
|
| 22 |
|
| 23 |
---
|
| 24 |
|
| 25 |
+
### Data
|
| 26 |
|
| 27 |
+
1715 files across 65 CVEs scraped from actual GitHub vulnerability patches. Each file has four features extracted from the commit history: churn, complexity, TODO count, and recency.
|
|
|
|
| 28 |
|
| 29 |
+
### Rewards
|
| 30 |
|
| 31 |
+
Asymmetric on purpose — missing a real bug is worse than wasting a review slot on a clean file.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
| Outcome | Reward | Why |
|
| 34 |
+
|---------|--------|-----|
|
| 35 |
+
| True Positive | +1.0 | found a real bug |
|
| 36 |
+
| True Negative | +0.8 | correctly skipped clean file |
|
| 37 |
+
| False Positive | -0.4 | wasted budget on safe file |
|
| 38 |
+
| False Negative | -0.2 | missed a bug |
|
| 39 |
+
| Over-budget flag | -0.5 | budget is a hard limit |
|
| 40 |
|
| 41 |
+
### Tasks
|
| 42 |
+
|
| 43 |
+
Three difficulty levels based on repo size:
|
| 44 |
+
|
| 45 |
+
- **easy**: ≤15 files, generous budget
|
| 46 |
+
- **medium**: 16-29 files
|
| 47 |
+
- **hard**: 30+ files, tight budget — agent really has to pick its spots
|
| 48 |
|
| 49 |
+
Grading is F1 score (precision × recall), always in [0, 1].
|
| 50 |
|
| 51 |
+
---
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
### Observation fields
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
+
Each step gives you:
|
| 56 |
+
- `file_path`, `file_index`, `total_files`, `files_remaining`
|
| 57 |
+
- `churn_score`, `complexity_score`, `todo_score`, `recency_score`
|
| 58 |
+
- `review_budget`, `files_flagged`
|
| 59 |
+
- `difficulty_level`, `cve_id`, `repo_name`
|
| 60 |
+
- terminal: `precision`, `recall`, `f1_score`, `true_positives`, `false_positives`, etc.
|
| 61 |
|
| 62 |
+
Action is just `{"decision": "flag"}` or `{"decision": "skip"}`.
|
| 63 |
|
| 64 |
---
|
| 65 |
|
| 66 |
+
## Running it
|
| 67 |
|
| 68 |
+
**Install:**
|
| 69 |
```bash
|
| 70 |
pip install openenv-core openai
|
| 71 |
```
|
| 72 |
|
| 73 |
+
**Docker:**
|
| 74 |
```bash
|
| 75 |
docker build -t codereviewenv .
|
| 76 |
docker run -p 7860:7860 codereviewenv
|
| 77 |
```
|
|
|
|
| 78 |
|
| 79 |
+
**Inference:**
|
| 80 |
```bash
|
| 81 |
+
export HF_TOKEN="your_token"
|
| 82 |
python inference.py
|
| 83 |
```
|
| 84 |
|
| 85 |
---
|
| 86 |
|
| 87 |
+
## Agents
|
| 88 |
+
|
| 89 |
+
### LLM baseline (`inference.py`)
|
| 90 |
|
| 91 |
+
Sends the file stats to Qwen2.5-Coder-32B via the HF inference API and asks it to flag or skip. Runs all three difficulty levels.
|
|
|
|
| 92 |
|
| 93 |
+
Rough zero-shot numbers:
|
| 94 |
|
| 95 |
+
| Difficulty | F1 | Precision | Recall |
|
| 96 |
+
|---|---|---|---|
|
| 97 |
+
| Easy | ~0.15 | ~0.12 | ~0.25 |
|
| 98 |
+
| Medium | ~0.10 | ~0.08 | ~0.18 |
|
| 99 |
+
| Hard | ~0.08 | ~0.06 | ~0.15 |
|
| 100 |
|
| 101 |
+
Lots of room to improve — the LLM has no training signal, it's just guessing from feature names.
|
| 102 |
|
| 103 |
```bash
|
| 104 |
+
export HF_TOKEN="your_token"
|
| 105 |
python inference.py
|
| 106 |
```
|
| 107 |
|
| 108 |
+
### PyTorch agent (`train_pytorch_agent.py`)
|
| 109 |
+
|
| 110 |
+
REINFORCE with a 3-layer MLP. Takes the 6 observation features as input, outputs flag/skip probabilities. Trains directly against the env reward signal.
|
| 111 |
|
|
|
|
| 112 |
```bash
|
| 113 |
pip install torch
|
| 114 |
python train_pytorch_agent.py
|
| 115 |
```
|
| 116 |
|
| 117 |
+
## File layout
|
| 118 |
+
|
| 119 |
+
- `Dockerfile` + `openenv.yaml` — deployment config
|
| 120 |
+
- `inference.py` — LLM baseline (hackathon submission script)
|
| 121 |
+
- `train_pytorch_agent.py` — pytorch RL agent
|
| 122 |
+
- `models.py` — pydantic action/observation/state types
|
| 123 |
+
- `server/environment.py` — core env logic + reward math
|
| 124 |
+
- `data/` — the CVE dataset
|
| 125 |
|
| 126 |
+
MIT License
|
__pycache__/client.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/client.cpython-312.pyc and b/__pycache__/client.cpython-312.pyc differ
|
|
|
__pycache__/models.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/models.cpython-312.pyc and b/__pycache__/models.cpython-312.pyc differ
|
|
|
client.py
CHANGED
|
@@ -51,7 +51,9 @@ class CodeReviewEnv(EnvClient[CodeReviewAction, CodeReviewObservation, CodeRevie
|
|
| 51 |
total_files=payload.get("total_files", 0),
|
| 52 |
total_bugs=payload.get("total_bugs", 0),
|
| 53 |
current_file_index=payload.get("current_file_index", 0),
|
|
|
|
| 54 |
files_flagged=payload.get("files_flagged", 0),
|
| 55 |
correct_flags=payload.get("correct_flags", 0),
|
| 56 |
review_budget=payload.get("review_budget", 0),
|
|
|
|
| 57 |
)
|
|
|
|
| 51 |
total_files=payload.get("total_files", 0),
|
| 52 |
total_bugs=payload.get("total_bugs", 0),
|
| 53 |
current_file_index=payload.get("current_file_index", 0),
|
| 54 |
+
difficulty_level=payload.get("difficulty_level", "medium"),
|
| 55 |
files_flagged=payload.get("files_flagged", 0),
|
| 56 |
correct_flags=payload.get("correct_flags", 0),
|
| 57 |
review_budget=payload.get("review_budget", 0),
|
| 58 |
+
cumulative_reward=payload.get("cumulative_reward", 0.0),
|
| 59 |
)
|
code_review_env.egg-info/PKG-INFO
CHANGED
|
@@ -10,81 +10,141 @@ Requires-Dist: openenv-core>=0.2.0
|
|
| 10 |
Requires-Dist: fastapi>=0.100.0
|
| 11 |
Requires-Dist: uvicorn>=0.20.0
|
| 12 |
Requires-Dist: pydantic>=2.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
| correctly flag a bug | 1.0 |
|
| 29 |
-
| correctly skip safe file | 0.8 |
|
| 30 |
-
| flag a safe file | 0.4 |
|
| 31 |
-
| miss a real bug | 0.0 |
|
| 32 |
|
| 33 |
-
|
| 34 |
|
| 35 |
-
##
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
```bash
|
| 38 |
pip install openenv-core openai
|
| 39 |
```
|
| 40 |
|
| 41 |
-
|
| 42 |
```bash
|
| 43 |
docker build -t codereviewenv .
|
| 44 |
-
docker run -p
|
| 45 |
```
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
To run the evaluation script:
|
| 50 |
```bash
|
| 51 |
-
export HF_TOKEN="
|
| 52 |
python inference.py
|
| 53 |
```
|
| 54 |
|
| 55 |
-
|
| 56 |
|
| 57 |
-
|
| 58 |
-
- `churn_score` / `complexity_score` / `todo_score` / `recency_score` - features (0-100)
|
| 59 |
-
- `review_budget` - how many flags left
|
| 60 |
-
- `files_remaining` - files left in episode
|
| 61 |
-
- `cve_id` / `cvss_score` - which vulnerability this episode is about
|
| 62 |
|
| 63 |
-
##
|
|
|
|
| 64 |
|
| 65 |
-
|
| 66 |
-
1. Pull CVEs from NVD API with GitHub refs
|
| 67 |
-
2. Fetch file metadata and commit history from those repos
|
| 68 |
-
3. Compute feature scores for each file
|
| 69 |
-
4. Label files based on which ones were actually patched in the CVE fix
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
|
|
|
|
|
|
|
|
|
|
| 75 |
```
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
├── environment.py - core env logic
|
| 85 |
-
└── app.py - fastapi server
|
| 86 |
```
|
| 87 |
|
| 88 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
MIT
|
|
|
|
| 10 |
Requires-Dist: fastapi>=0.100.0
|
| 11 |
Requires-Dist: uvicorn>=0.20.0
|
| 12 |
Requires-Dist: pydantic>=2.0.0
|
| 13 |
+
Provides-Extra: train
|
| 14 |
+
Requires-Dist: torch>=2.0.0; extra == "train"
|
| 15 |
+
Provides-Extra: inference
|
| 16 |
+
Requires-Dist: openai>=1.0.0; extra == "inference"
|
| 17 |
|
| 18 |
+
---
|
| 19 |
+
title: CodeReviewEnv
|
| 20 |
+
emoji: 🛡️
|
| 21 |
+
colorFrom: yellow
|
| 22 |
+
colorTo: red
|
| 23 |
+
sdk: docker
|
| 24 |
+
pinned: false
|
| 25 |
+
tags:
|
| 26 |
+
- openenv
|
| 27 |
+
---
|
| 28 |
|
| 29 |
+
# CodeReviewEnv: Triage CVEs Like a Pro
|
| 30 |
|
| 31 |
+
*Built for the Meta/PyTorch OpenEnv Hackathon*
|
| 32 |
|
| 33 |
+
Hey! 👋 Welcome to **CodeReviewEnv**.
|
| 34 |
|
| 35 |
+
Most RL environments are toy setups like GridWorld or simple mazes. We wanted to tackle a real problem that we actually face as developers: **Vulnerability Triage**.
|
| 36 |
|
| 37 |
+
We built `CodeReviewEnv` using real-world data scraped directly from patches in the National Vulnerability Database (NVD). The agent connects to a repository, scans the files, and uses heuristics (like codebase churn, complexity, and recency) to decide whether to `flag` a file for manual security review or `skip` it and move on.
|
| 38 |
|
| 39 |
+
* 🚀 **Hugging Face Space (Live Environment)**: [https://huggingface.co/spaces/lucid987654/code-review-env](https://huggingface.co/spaces/lucid987654/code-review-env)
|
| 40 |
+
* 📁 **GitHub Repository**: [https://github.com/subwaycookiecrunch/Meta-project](https://github.com/subwaycookiecrunch/Meta-project)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
---
|
| 43 |
|
| 44 |
+
### The Problem
|
| 45 |
|
| 46 |
+
We have 1715 files across 65 authentic CVEs pulled directly from actual GitHub vulnerabilities.
|
| 47 |
+
We hit the agent with **Asymmetric Rewards**. In the real world, missing a critical bug (False Negative) is infinitely worse than accidentally flagging a safe file for review (False Positive).
|
| 48 |
+
|
| 49 |
+
Our reward table forces the agent to balance its paranoia:
|
| 50 |
+
|
| 51 |
+
| Outcome | Reward | Rationale |
|
| 52 |
+
|---------|--------|-----------|
|
| 53 |
+
| True Positive (found a real bug) | **+1.0** | Highest reward — catching vulnerabilities is the goal |
|
| 54 |
+
| True Negative (correctly skipped safe file) | **+0.8** | Good judgment that saves review budget |
|
| 55 |
+
| False Positive (flagged a safe file) | **-0.4** | Wastes review budget, penalized |
|
| 56 |
+
| False Negative (missed a real bug) | **-0.2** | Worst failure — penalty provides learning signal |
|
| 57 |
+
| Over-budget flag attempt | **-0.5** | Hard constraint — budget is non-negotiable |
|
| 58 |
+
|
| 59 |
+
Oh, and there's a strict **Review Budget**. You can't just flag everything, or you run out of budget and get heavily penalized!
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
### 🧩 Environment Specifications
|
| 64 |
+
|
| 65 |
+
#### Action Space
|
| 66 |
+
The action space is a straightforward, string-based categorical action:
|
| 67 |
+
* `decision`: `"flag"` or `"skip"`
|
| 68 |
+
|
| 69 |
+
#### Observation Space
|
| 70 |
+
At each step, the environment provides a rich state vector. The key metrics include:
|
| 71 |
+
* `file_path` & tracking metrics (`file_index`, `files_remaining`, `total_files`)
|
| 72 |
+
* **Difficulty & Context**: `difficulty_level`, `cve_id`, `repo_name`
|
| 73 |
+
* **Static Analysis Features**:
|
| 74 |
+
* `churn_score` — lines changed in the file (higher = more volatile)
|
| 75 |
+
* `complexity_score` — cyclomatic complexity proxy (higher = harder to review)
|
| 76 |
+
* `todo_score` — count of TODOs/FIXMEs (higher = more tech debt)
|
| 77 |
+
* `recency_score` — how recently the file was modified (higher = more recent)
|
| 78 |
+
* **Limits**: `review_budget` and `files_flagged`
|
| 79 |
+
* **Terminal Metrics**: `precision`, `recall`, `f1_score`, `true_positives`, `false_positives`, `false_negatives`, `true_negatives`
|
| 80 |
+
|
| 81 |
+
#### 🎯 The Three Tasks (Difficulty Tiers)
|
| 82 |
+
We've partitioned the environment into three distinct difficulty tasks, scaling gracefully by the size of the repository logic the agent needs to parse over its fixed review budget:
|
| 83 |
+
1. **Easy (`difficulty="easy"`)**: Small repositories and pull requests (≤ 15 files). The budget is relatively forgiving.
|
| 84 |
+
2. **Medium (`difficulty="medium"`)**: Average-sized PRs (16-29 files) requiring more scrutiny.
|
| 85 |
+
3. **Hard (`difficulty="hard"`)**: Large-scale patches (30+ files). The agent is strapped for budget and must be extremely selective about utilizing its flags.
|
| 86 |
+
|
| 87 |
+
Each task includes a **programmatic grader** that returns a score between 0.0 and 1.0 (the F1-score), combining precision and recall into a single metric. The grader is deterministic and reproducible.
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## Setup & Running
|
| 92 |
+
|
| 93 |
+
**1. Install deps:**
|
| 94 |
```bash
|
| 95 |
pip install openenv-core openai
|
| 96 |
```
|
| 97 |
|
| 98 |
+
**2. Spin up the FastAPI Server via Docker:**
|
| 99 |
```bash
|
| 100 |
docker build -t codereviewenv .
|
| 101 |
+
docker run -p 7860:7860 codereviewenv
|
| 102 |
```
|
| 103 |
+
*(If you are viewing this on Hugging Face Spaces, the server is automatically running!)*
|
| 104 |
|
| 105 |
+
**3. Run the inference script:**
|
|
|
|
|
|
|
| 106 |
```bash
|
| 107 |
+
export OPENAI_API_KEY="your_api_key" # or export HF_TOKEN="your_hf_token"
|
| 108 |
python inference.py
|
| 109 |
```
|
| 110 |
|
| 111 |
+
---
|
| 112 |
|
| 113 |
+
## The Agents (We built two!)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
### 1. The Zero-Shot LLM Baseline (`inference.py`)
|
| 116 |
+
This is the standard OpenEnv submission script required by the Hackathon. We wrote a wrapper that passes the environment state into an OpenAI-compatible LLM to see if a huge model can reason through the file stats to allocate its budget. It evaluates the environment sequentially over the **Easy**, **Medium**, and **Hard** tasks.
|
| 117 |
|
| 118 |
+
**Baseline Scores:**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
| Difficulty | Model | F1-Score | Precision | Recall |
|
| 121 |
+
|-----------|-------|----------|-----------|--------|
|
| 122 |
+
| Easy | Qwen2.5-Coder-32B | ~0.15 | ~0.12 | ~0.25 |
|
| 123 |
+
| Medium | Qwen2.5-Coder-32B | ~0.10 | ~0.08 | ~0.18 |
|
| 124 |
+
| Hard | Qwen2.5-Coder-32B | ~0.08 | ~0.06 | ~0.15 |
|
| 125 |
|
| 126 |
+
These are zero-shot scores — the LLM has no training on this specific task, demonstrating there is real room for improvement via RL training.
|
| 127 |
|
| 128 |
+
```bash
|
| 129 |
+
export OPENAI_API_KEY="your_api_key"
|
| 130 |
+
python inference.py
|
| 131 |
```
|
| 132 |
+
|
| 133 |
+
### 2. The Native PyTorch Agent (`train_pytorch_agent.py`)
|
| 134 |
+
**Flex Warning.** LLMs are cool, but they struggle to implicitly understand strict mathematical bounds (like rationing a flag budget perfectly over exactly 29 files with precise asymmetric scoring). So, we went bare-metal.
|
| 135 |
+
|
| 136 |
+
We built a custom Deep Reinforcement Learning Agent using native PyTorch Policy Gradients (REINFORCE) to interface perfectly with the OpenEnv API. It iteratively converges to find the perfect risk/reward strategy.
|
| 137 |
+
```bash
|
| 138 |
+
pip install torch
|
| 139 |
+
python train_pytorch_agent.py
|
|
|
|
|
|
|
| 140 |
```
|
| 141 |
|
| 142 |
+
## Hackathon Repo Tour
|
| 143 |
+
* `Dockerfile` & `openenv.yaml`: The OpenEnv backend deployment wrappers
|
| 144 |
+
* `inference.py`: The mandatory LLM endpoint validation script
|
| 145 |
+
* `train_pytorch_agent.py`: Our custom PyTorch REINFORCE brain
|
| 146 |
+
* `/models.py`: Pydantic Models for Actions & Observations
|
| 147 |
+
* `/server/environment.py`: Where the magic reward mathematics happen
|
| 148 |
+
* `/data/`: The actual scraped CVE GitHub dataset
|
| 149 |
|
| 150 |
+
*MIT License. Thanks for checking it out!*
|
code_review_env.egg-info/requires.txt
CHANGED
|
@@ -2,3 +2,9 @@ openenv-core>=0.2.0
|
|
| 2 |
fastapi>=0.100.0
|
| 3 |
uvicorn>=0.20.0
|
| 4 |
pydantic>=2.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
fastapi>=0.100.0
|
| 3 |
uvicorn>=0.20.0
|
| 4 |
pydantic>=2.0.0
|
| 5 |
+
|
| 6 |
+
[inference]
|
| 7 |
+
openai>=1.0.0
|
| 8 |
+
|
| 9 |
+
[train]
|
| 10 |
+
torch>=2.0.0
|
demo.py
CHANGED
|
@@ -12,7 +12,7 @@ def run_episode(env, agent_fn, name, episode=None):
|
|
| 12 |
if episode:
|
| 13 |
orig = random.choice
|
| 14 |
random.choice = lambda x: episode
|
| 15 |
-
obs = env.reset()
|
| 16 |
random.choice = orig
|
| 17 |
else:
|
| 18 |
obs = env.reset()
|
|
|
|
| 12 |
if episode:
|
| 13 |
orig = random.choice
|
| 14 |
random.choice = lambda x: episode
|
| 15 |
+
obs = env.reset(difficulty="medium")
|
| 16 |
random.choice = orig
|
| 17 |
else:
|
| 18 |
obs = env.reset()
|
inference.py
CHANGED
|
@@ -11,7 +11,6 @@ API_BASE_URL = os.getenv("API_BASE_URL", "https://api-inference.huggingface.co/v
|
|
| 11 |
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-Coder-32B-Instruct")
|
| 12 |
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME", "codereviewenv")
|
| 13 |
|
| 14 |
-
# HF_TOKEN is primary per sample inference.py, API_KEY as fallback
|
| 15 |
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 16 |
|
| 17 |
if API_KEY is None:
|
|
|
|
| 11 |
MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-Coder-32B-Instruct")
|
| 12 |
LOCAL_IMAGE_NAME = os.getenv("LOCAL_IMAGE_NAME", "codereviewenv")
|
| 13 |
|
|
|
|
| 14 |
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 15 |
|
| 16 |
if API_KEY is None:
|
models.py
CHANGED
|
@@ -40,3 +40,4 @@ class CodeReviewState(State):
|
|
| 40 |
files_flagged: int = 0
|
| 41 |
correct_flags: int = 0
|
| 42 |
review_budget: int = 0
|
|
|
|
|
|
| 40 |
files_flagged: int = 0
|
| 41 |
correct_flags: int = 0
|
| 42 |
review_budget: int = 0
|
| 43 |
+
cumulative_reward: float = 0.0
|
openenv.yaml
CHANGED
|
@@ -1,3 +1,8 @@
|
|
| 1 |
name: code_review_env
|
| 2 |
version: "1.0.0"
|
| 3 |
-
description: RL env for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
name: code_review_env
|
| 2 |
version: "1.0.0"
|
| 3 |
+
description: RL env for triaging CVE vulnerabilities in code repos
|
| 4 |
+
entry_point: code_review_env.server.app:app
|
| 5 |
+
tags:
|
| 6 |
+
- openenv
|
| 7 |
+
- security
|
| 8 |
+
- code-review
|
pyproject.toml
CHANGED
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
| 5 |
[project]
|
| 6 |
name = "code-review-env"
|
| 7 |
version = "1.0.0"
|
| 8 |
-
description = "RL
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|
| 11 |
license = {text = "MIT"}
|
|
@@ -16,8 +16,15 @@ dependencies = [
|
|
| 16 |
"fastapi>=0.100.0",
|
| 17 |
"uvicorn>=0.20.0",
|
| 18 |
"pydantic>=2.0.0",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"torch>=2.0.0",
|
| 20 |
]
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
[tool.setuptools.packages.find]
|
| 23 |
include = ["code_review_env*"]
|
|
|
|
| 5 |
[project]
|
| 6 |
name = "code-review-env"
|
| 7 |
version = "1.0.0"
|
| 8 |
+
description = "RL env for CVE triage on real vulnerability data"
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|
| 11 |
license = {text = "MIT"}
|
|
|
|
| 16 |
"fastapi>=0.100.0",
|
| 17 |
"uvicorn>=0.20.0",
|
| 18 |
"pydantic>=2.0.0",
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
[project.optional-dependencies]
|
| 22 |
+
train = [
|
| 23 |
"torch>=2.0.0",
|
| 24 |
]
|
| 25 |
+
inference = [
|
| 26 |
+
"openai>=1.0.0",
|
| 27 |
+
]
|
| 28 |
|
| 29 |
[tool.setuptools.packages.find]
|
| 30 |
include = ["code_review_env*"]
|
server/__pycache__/app.cpython-312.pyc
CHANGED
|
Binary files a/server/__pycache__/app.cpython-312.pyc and b/server/__pycache__/app.cpython-312.pyc differ
|
|
|
server/__pycache__/environment.cpython-312.pyc
CHANGED
|
Binary files a/server/__pycache__/environment.cpython-312.pyc and b/server/__pycache__/environment.cpython-312.pyc differ
|
|
|
server/app.py
CHANGED
|
@@ -6,7 +6,7 @@ app = create_fastapi_app(CodeReviewEnvironment, CodeReviewAction, CodeReviewObse
|
|
| 6 |
|
| 7 |
@app.get("/")
|
| 8 |
def root():
|
| 9 |
-
return {"status": "
|
| 10 |
|
| 11 |
|
| 12 |
def main():
|
|
|
|
| 6 |
|
| 7 |
@app.get("/")
|
| 8 |
def root():
|
| 9 |
+
return {"status": "ok", "env": "code_review"}
|
| 10 |
|
| 11 |
|
| 12 |
def main():
|
server/environment.py
CHANGED
|
@@ -73,7 +73,7 @@ class CodeReviewEnvironment(Environment):
|
|
| 73 |
if difficulty not in ["easy", "medium", "hard"]:
|
| 74 |
difficulty = random.choice(["easy", "medium", "hard"])
|
| 75 |
|
| 76 |
-
|
| 77 |
if difficulty == "easy":
|
| 78 |
size_filter = lambda e: len(e["files"]) <= 15
|
| 79 |
elif difficulty == "medium":
|
|
@@ -81,22 +81,19 @@ class CodeReviewEnvironment(Environment):
|
|
| 81 |
else:
|
| 82 |
size_filter = lambda e: len(e["files"]) >= 30
|
| 83 |
|
| 84 |
-
#
|
| 85 |
-
# without bugs F1 is always 0.0 regardless of agent behavior,
|
| 86 |
-
# which would trip the "graders always return same score" disqualification.
|
| 87 |
buggy_candidates = [e for e in BUGGY_EPISODES if size_filter(e)]
|
| 88 |
if buggy_candidates:
|
| 89 |
ep = random.choice(buggy_candidates)
|
| 90 |
else:
|
| 91 |
-
#
|
| 92 |
all_candidates = [e for e in EPISODES if size_filter(e)]
|
| 93 |
if not all_candidates:
|
| 94 |
all_candidates = BUGGY_EPISODES if BUGGY_EPISODES else EPISODES
|
| 95 |
ep = random.choice(all_candidates)
|
| 96 |
|
| 97 |
-
# If the chosen episode has no bugs, inject some
|
| 98 |
if ep["total_bugs"] == 0:
|
| 99 |
-
ep = dict(ep)
|
| 100 |
files = [dict(f) for f in ep["files"]]
|
| 101 |
n_inject = max(1, len(files) // 8)
|
| 102 |
targets = random.sample(range(len(files)), min(n_inject, len(files)))
|
|
|
|
| 73 |
if difficulty not in ["easy", "medium", "hard"]:
|
| 74 |
difficulty = random.choice(["easy", "medium", "hard"])
|
| 75 |
|
| 76 |
+
|
| 77 |
if difficulty == "easy":
|
| 78 |
size_filter = lambda e: len(e["files"]) <= 15
|
| 79 |
elif difficulty == "medium":
|
|
|
|
| 81 |
else:
|
| 82 |
size_filter = lambda e: len(e["files"]) >= 30
|
| 83 |
|
| 84 |
+
# need episodes w/ bugs or f1 is stuck at 0
|
|
|
|
|
|
|
| 85 |
buggy_candidates = [e for e in BUGGY_EPISODES if size_filter(e)]
|
| 86 |
if buggy_candidates:
|
| 87 |
ep = random.choice(buggy_candidates)
|
| 88 |
else:
|
| 89 |
+
# fallback: any matching ep, add synthetic bugs if clean
|
| 90 |
all_candidates = [e for e in EPISODES if size_filter(e)]
|
| 91 |
if not all_candidates:
|
| 92 |
all_candidates = BUGGY_EPISODES if BUGGY_EPISODES else EPISODES
|
| 93 |
ep = random.choice(all_candidates)
|
| 94 |
|
|
|
|
| 95 |
if ep["total_bugs"] == 0:
|
| 96 |
+
ep = dict(ep)
|
| 97 |
files = [dict(f) for f in ep["files"]]
|
| 98 |
n_inject = max(1, len(files) // 8)
|
| 99 |
targets = random.sample(range(len(files)), min(n_inject, len(files)))
|
train_pytorch_agent.py
CHANGED
|
@@ -45,7 +45,7 @@ def main():
|
|
| 45 |
optimizer = optim.Adam(policy_net.parameters(), lr=learning_rate)
|
| 46 |
|
| 47 |
num_episodes = 50
|
| 48 |
-
print(f"
|
| 49 |
|
| 50 |
env = CodeReviewEnvironment()
|
| 51 |
|
|
@@ -104,7 +104,7 @@ def main():
|
|
| 104 |
f"Reward: {sum(rewards):.1f} | "
|
| 105 |
f"F1: {obs.f1_score:.2f} (P: {obs.precision:.2f}, R: {obs.recall:.2f})")
|
| 106 |
|
| 107 |
-
print("\
|
| 108 |
|
| 109 |
if __name__ == "__main__":
|
| 110 |
main()
|
|
|
|
| 45 |
optimizer = optim.Adam(policy_net.parameters(), lr=learning_rate)
|
| 46 |
|
| 47 |
num_episodes = 50
|
| 48 |
+
print(f"training for {num_episodes} episodes")
|
| 49 |
|
| 50 |
env = CodeReviewEnvironment()
|
| 51 |
|
|
|
|
| 104 |
f"Reward: {sum(rewards):.1f} | "
|
| 105 |
f"F1: {obs.f1_score:.2f} (P: {obs.precision:.2f}, R: {obs.recall:.2f})")
|
| 106 |
|
| 107 |
+
print("\ndone.")
|
| 108 |
|
| 109 |
if __name__ == "__main__":
|
| 110 |
main()
|