s-shah4 commited on
Commit ·
4433dc8
1
Parent(s): da8df85
Add V1 env
Browse files- .dockerignore +8 -0
- .gitignore +2 -1
- Dockerfile +11 -0
- README.md +111 -72
- app.py +3 -0
- client.py +34 -0
- env/__init__.py +3 -0
- env/adapt_env.py +265 -58
- env/executor.py +14 -3
- env/test_cases.py +129 -25
- models.py +39 -0
- openenv.yaml +35 -0
- pyproject.toml +30 -0
- server/__init__.py +3 -0
- server/app.py +30 -0
- server/requirements.txt +5 -0
- test.py +53 -0
- uv.lock +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.idea
|
| 3 |
+
venv
|
| 4 |
+
__pycache__
|
| 5 |
+
*.pyc
|
| 6 |
+
outputs
|
| 7 |
+
.adapt_tmp
|
| 8 |
+
.pip-tmp
|
.gitignore
CHANGED
|
@@ -43,6 +43,8 @@ htmlcov/
|
|
| 43 |
.coverage
|
| 44 |
.coverage.*
|
| 45 |
.cache
|
|
|
|
|
|
|
| 46 |
nosetests.xml
|
| 47 |
coverage.xml
|
| 48 |
*.cover
|
|
@@ -151,7 +153,6 @@ activemq-data/
|
|
| 151 |
.envrc
|
| 152 |
.venv
|
| 153 |
venv/
|
| 154 |
-
ENV/
|
| 155 |
env.bak/
|
| 156 |
venv.bak/
|
| 157 |
|
|
|
|
| 43 |
.coverage
|
| 44 |
.coverage.*
|
| 45 |
.cache
|
| 46 |
+
.adapt_tmp/
|
| 47 |
+
.pip-tmp/
|
| 48 |
nosetests.xml
|
| 49 |
coverage.xml
|
| 50 |
*.cover
|
|
|
|
| 153 |
.envrc
|
| 154 |
.venv
|
| 155 |
venv/
|
|
|
|
| 156 |
env.bak/
|
| 157 |
venv.bak/
|
| 158 |
|
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . .
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -e .
|
| 8 |
+
|
| 9 |
+
EXPOSE 7860
|
| 10 |
+
|
| 11 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,127 +1,166 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
ADAPT
|
| 4 |
|
| 5 |
-
|
| 6 |
|
| 7 |
-
|
| 8 |
|
| 9 |
-
|
| 10 |
-
from env.adapt_env import AdaptEnv
|
| 11 |
-
|
| 12 |
-
env = AdaptEnv()
|
| 13 |
-
|
| 14 |
-
obs = env.reset()
|
| 15 |
-
result = env.step("n=int(input())\nprint(n*2)")
|
| 16 |
-
|
| 17 |
-
reward = result["reward"]
|
| 18 |
-
```
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
```text
|
| 23 |
-
|
| 24 |
```
|
| 25 |
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
- `env/executor.py`: subprocess execution with a 2 second timeout
|
| 30 |
-
- `env/test_cases.py`: problem definition plus visible and hidden test cases
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
`reset()` returns:
|
| 35 |
|
| 36 |
-
``
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
"constraints": str,
|
| 41 |
-
"examples": list,
|
| 42 |
-
"visible_tests": list,
|
| 43 |
-
}
|
| 44 |
-
```
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
|
| 52 |
```python
|
| 53 |
{
|
| 54 |
-
"
|
| 55 |
-
"done": bool,
|
| 56 |
-
"feedback": str,
|
| 57 |
-
"pass_rate": float,
|
| 58 |
}
|
| 59 |
```
|
| 60 |
|
| 61 |
-
##
|
| 62 |
|
| 63 |
-
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
-
|
| 69 |
-
```
|
| 70 |
|
| 71 |
-
|
| 72 |
|
| 73 |
-
``
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
```
|
| 82 |
|
| 83 |
-
If `
|
| 84 |
|
| 85 |
-
##
|
| 86 |
|
| 87 |
-
|
| 88 |
|
| 89 |
```powershell
|
| 90 |
cd C:\Users\kaust\PycharmProjects\meta-rl-dsa-solver
|
|
|
|
|
|
|
| 91 |
```
|
| 92 |
|
| 93 |
-
|
| 94 |
|
| 95 |
```powershell
|
| 96 |
-
|
| 97 |
```
|
| 98 |
|
| 99 |
-
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
| 103 |
```
|
| 104 |
|
| 105 |
-
Check
|
| 106 |
|
| 107 |
```powershell
|
| 108 |
-
python -
|
| 109 |
```
|
| 110 |
|
| 111 |
-
|
| 112 |
|
| 113 |
-
```
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
```
|
| 116 |
|
| 117 |
-
|
| 118 |
|
| 119 |
```powershell
|
| 120 |
-
|
| 121 |
```
|
| 122 |
|
| 123 |
-
|
|
|
|
|
|
|
| 124 |
|
| 125 |
```powershell
|
| 126 |
-
|
| 127 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ADAPT DSA Tutor OpenEnv
|
| 3 |
+
sdk: docker
|
| 4 |
+
pinned: false
|
| 5 |
+
app_port: 7860
|
| 6 |
+
base_path: /web
|
| 7 |
+
tags:
|
| 8 |
+
- openenv
|
| 9 |
+
- reinforcement-learning
|
| 10 |
+
- code-generation
|
| 11 |
+
---
|
| 12 |
|
| 13 |
+
# ADAPT DSA Tutor OpenEnv
|
| 14 |
|
| 15 |
+
ADAPT, the Adversarial DSA Tutor, is an OpenEnv-compliant RLVR environment for training code-generation agents on small DSA tasks. The agent receives a problem prompt, examples, and visible tests, then submits Python code. The environment runs the code against visible and hidden tests and returns reward, pass-rate metrics, execution status, and feedback.
|
| 16 |
|
| 17 |
+
This repo now focuses on the environment layer only. Verifier work and training scripts are owned separately.
|
| 18 |
|
| 19 |
+
## Why This Environment
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
The hackathon asks for OpenEnv environments that can improve LLM behavior through verifiable interaction. ADAPT targets a simple but useful skill loop:
|
| 22 |
|
| 23 |
```text
|
| 24 |
+
agent writes code -> environment executes it -> hidden tests and reward signals score it -> trainer improves the agent
|
| 25 |
```
|
| 26 |
|
| 27 |
+
The differentiator is curriculum-ready DSA practice: each episode carries a problem id and difficulty tier so training can track per-tier success instead of only aggregate reward.
|
| 28 |
|
| 29 |
+
## OpenEnv Interface
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
The environment uses the latest OpenEnv API shape:
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
- `AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState])`
|
| 34 |
+
- `reset()` returns a typed observation.
|
| 35 |
+
- `step(action)` accepts an `AdaptAction` with a Python `code` string.
|
| 36 |
+
- `state` exposes episode id, step count, current problem id, difficulty, and recent metrics.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
`openenv.yaml` points to:
|
| 39 |
|
| 40 |
+
```yaml
|
| 41 |
+
app: server.app:app
|
| 42 |
+
port: 7860
|
| 43 |
+
```
|
| 44 |
|
| 45 |
+
## Action
|
| 46 |
|
| 47 |
```python
|
| 48 |
{
|
| 49 |
+
"code": "n = int(input())\nprint(n * 2)"
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
```
|
| 52 |
|
| 53 |
+
## Observation
|
| 54 |
|
| 55 |
+
Reset and step observations include:
|
| 56 |
|
| 57 |
+
- problem statement
|
| 58 |
+
- input format
|
| 59 |
+
- constraints
|
| 60 |
+
- examples
|
| 61 |
+
- visible tests
|
| 62 |
+
- problem id
|
| 63 |
+
- difficulty tier
|
| 64 |
+
- feedback
|
| 65 |
+
- pass rate, visible pass rate, and hidden pass rate
|
| 66 |
+
- syntax/runtime/timeout status
|
| 67 |
+
- reward components
|
| 68 |
|
| 69 |
+
Hidden test inputs and expected outputs are never returned in observations.
|
|
|
|
| 70 |
|
| 71 |
+
## Reward
|
| 72 |
|
| 73 |
+
Reward is clipped to `[0.0, 1.0]` and combines multiple environment-level signals:
|
| 74 |
+
|
| 75 |
+
- correctness from visible and hidden pass rate
|
| 76 |
+
- syntax validity
|
| 77 |
+
- clean execution
|
| 78 |
+
- output format compliance
|
| 79 |
+
- timeout penalty
|
| 80 |
+
- runtime error penalty
|
| 81 |
+
- static safety rejection for dangerous imports such as `os`, `subprocess`, `socket`, `pathlib`, and `shutil`
|
| 82 |
|
| 83 |
+
If `verifier.verifier.verify(code, test_cases)` exists, the environment can use it as an optional reward augmentation. If the verifier is absent, the environment still works using executor-derived reward.
|
| 84 |
|
| 85 |
+
## Local Setup
|
| 86 |
|
| 87 |
+
Use Python `3.10+`.
|
| 88 |
|
| 89 |
```powershell
|
| 90 |
cd C:\Users\kaust\PycharmProjects\meta-rl-dsa-solver
|
| 91 |
+
python -m venv .venv
|
| 92 |
+
.\.venv\Scripts\pip install -e .
|
| 93 |
```
|
| 94 |
|
| 95 |
+
For this local machine, the existing checked-out OpenEnv repo can also be used during development:
|
| 96 |
|
| 97 |
```powershell
|
| 98 |
+
$env:PYTHONPATH="C:\Users\kaust\PycharmProjects\OpenEnv\src;$PWD"
|
| 99 |
```
|
| 100 |
|
| 101 |
+
## Smoke Tests
|
| 102 |
|
| 103 |
+
Run the local smoke test:
|
| 104 |
+
|
| 105 |
+
```powershell
|
| 106 |
+
python test.py
|
| 107 |
```
|
| 108 |
|
| 109 |
+
Check syntax:
|
| 110 |
|
| 111 |
```powershell
|
| 112 |
+
python -m py_compile models.py env\adapt_env.py env\executor.py env\test_cases.py server\app.py
|
| 113 |
```
|
| 114 |
|
| 115 |
+
Start the OpenEnv server:
|
| 116 |
|
| 117 |
+
```powershell
|
| 118 |
+
uvicorn server.app:app --host 0.0.0.0 --port 7860
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
Useful endpoints:
|
| 122 |
+
|
| 123 |
+
- `GET /health`
|
| 124 |
+
- `GET /schema`
|
| 125 |
+
- `POST /reset`
|
| 126 |
+
- `POST /step`
|
| 127 |
+
- `GET /state`
|
| 128 |
+
|
| 129 |
+
Example step request:
|
| 130 |
+
|
| 131 |
+
```powershell
|
| 132 |
+
curl -X POST http://localhost:7860/step -H "Content-Type: application/json" -d "{\"action\":{\"code\":\"n=int(input())\nprint(n*2)\"}}"
|
| 133 |
```
|
| 134 |
|
| 135 |
+
Validate with OpenEnv once dependencies are installed:
|
| 136 |
|
| 137 |
```powershell
|
| 138 |
+
openenv validate .
|
| 139 |
```
|
| 140 |
|
| 141 |
+
## Hugging Face Spaces
|
| 142 |
+
|
| 143 |
+
This repo is Docker Space ready:
|
| 144 |
|
| 145 |
```powershell
|
| 146 |
+
openenv push --repo-id <your-hf-username>/adapt-dsa-tutor
|
| 147 |
```
|
| 148 |
+
|
| 149 |
+
Before final submission, add:
|
| 150 |
+
|
| 151 |
+
- live Hugging Face Space link
|
| 152 |
+
- training reward/loss plots from Disha's run
|
| 153 |
+
- before/after code example showing a problem the model failed before training and solved after training
|
| 154 |
+
- mini-blog or short video link
|
| 155 |
+
|
| 156 |
+
## Current Problem Bank
|
| 157 |
+
|
| 158 |
+
The environment includes a lightweight curated bank:
|
| 159 |
+
|
| 160 |
+
- `easy_double`
|
| 161 |
+
- `easy_sum_two`
|
| 162 |
+
- `medium_maximum`
|
| 163 |
+
- `medium_count_even`
|
| 164 |
+
- `hard_reverse_words`
|
| 165 |
+
|
| 166 |
+
This is intentionally small for submission-minimum stability. Later work can expand it to 30-50 tiered problems without changing the OpenEnv API.
|
app.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.app import app, main
|
| 2 |
+
|
| 3 |
+
__all__ = ["app", "main"]
|
client.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
import httpx
|
| 6 |
+
|
| 7 |
+
from models import AdaptAction
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AdaptEnvClient:
|
| 11 |
+
def __init__(self, base_url: str = "http://localhost:7860") -> None:
|
| 12 |
+
self.base_url = base_url.rstrip("/")
|
| 13 |
+
self._client = httpx.Client(base_url=self.base_url, timeout=30.0)
|
| 14 |
+
|
| 15 |
+
def close(self) -> None:
|
| 16 |
+
self._client.close()
|
| 17 |
+
|
| 18 |
+
def reset(self, **params: Any) -> dict[str, Any]:
|
| 19 |
+
response = self._client.post("/reset", json=params)
|
| 20 |
+
response.raise_for_status()
|
| 21 |
+
return response.json()
|
| 22 |
+
|
| 23 |
+
def step(self, code: str) -> dict[str, Any]:
|
| 24 |
+
response = self._client.post("/step", json={"action": AdaptAction(code=code).model_dump()})
|
| 25 |
+
response.raise_for_status()
|
| 26 |
+
return response.json()
|
| 27 |
+
|
| 28 |
+
def state(self) -> dict[str, Any]:
|
| 29 |
+
response = self._client.get("/state")
|
| 30 |
+
response.raise_for_status()
|
| 31 |
+
return response.json()
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
__all__ = ["AdaptEnvClient"]
|
env/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from env.adapt_env import AdaptEnvironment
|
| 2 |
+
|
| 3 |
+
__all__ = ["AdaptEnvironment"]
|
env/adapt_env.py
CHANGED
|
@@ -1,104 +1,311 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
from typing import Any
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from env.executor import run_code
|
| 6 |
-
from env.test_cases import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
-
class
|
|
|
|
|
|
|
| 10 |
def __init__(self) -> None:
|
|
|
|
|
|
|
| 11 |
self.problem: dict[str, Any] = {}
|
| 12 |
self.test_cases: list[dict[str, str]] = []
|
| 13 |
self.visible_tests: list[dict[str, str]] = []
|
| 14 |
self.hidden_tests: list[dict[str, str]] = []
|
| 15 |
-
self.
|
| 16 |
|
| 17 |
-
def reset(
|
| 18 |
-
self
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
def step(
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
self.reset()
|
| 27 |
|
| 28 |
-
self.step_count += 1
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
| 50 |
-
visible_tests = test_cases[:VISIBLE_TEST_COUNT]
|
| 51 |
-
hidden_tests = test_cases[VISIBLE_TEST_COUNT:]
|
| 52 |
-
return visible_tests, hidden_tests
|
| 53 |
|
| 54 |
-
def _build_observation(
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
def _run_all_tests(self, code: str) -> list[dict[str, Any]]:
|
| 64 |
results = []
|
| 65 |
-
|
|
|
|
| 66 |
execution = run_code(code, test_case["input"])
|
| 67 |
-
actual = execution["stdout"].strip()
|
| 68 |
expected = test_case["output"].strip()
|
| 69 |
results.append(
|
| 70 |
{
|
| 71 |
-
"
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
|
|
|
|
|
|
|
|
|
| 76 |
"passed": execution["exit_code"] == 0 and actual == expected,
|
|
|
|
| 77 |
}
|
| 78 |
)
|
| 79 |
return results
|
| 80 |
|
| 81 |
-
def
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
def _build_feedback(self, run_results: list[dict[str, Any]], pass_rate: float) -> str:
|
| 93 |
for result in run_results:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
if result["exit_code"] != 0:
|
|
|
|
| 95 |
error = result["stderr"] or "runtime error"
|
| 96 |
-
return f"Runtime error on
|
| 97 |
|
| 98 |
-
if not result["passed"]:
|
| 99 |
return (
|
| 100 |
-
f"Failed on input {result['input'].strip()}: "
|
| 101 |
f"expected {result['expected']}, got {result['actual']}"
|
| 102 |
)
|
| 103 |
|
|
|
|
|
|
|
|
|
|
| 104 |
return f"All tests passed. Pass rate: {pass_rate:.2f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import ast
|
| 4 |
from typing import Any
|
| 5 |
+
from uuid import uuid4
|
| 6 |
+
|
| 7 |
+
from openenv.core.env_server.interfaces import Environment
|
| 8 |
|
| 9 |
from env.executor import run_code
|
| 10 |
+
from env.test_cases import load_problem, split_test_cases
|
| 11 |
+
from models import AdaptAction, AdaptObservation, AdaptState
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
FORBIDDEN_IMPORTS = {"os", "pathlib", "shutil", "socket", "subprocess"}
|
| 15 |
|
| 16 |
|
| 17 |
+
class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
|
| 18 |
+
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 19 |
+
|
| 20 |
def __init__(self) -> None:
|
| 21 |
+
super().__init__()
|
| 22 |
+
self._state = AdaptState(episode_id=str(uuid4()), step_count=0)
|
| 23 |
self.problem: dict[str, Any] = {}
|
| 24 |
self.test_cases: list[dict[str, str]] = []
|
| 25 |
self.visible_tests: list[dict[str, str]] = []
|
| 26 |
self.hidden_tests: list[dict[str, str]] = []
|
| 27 |
+
self.last_results: list[dict[str, Any]] = []
|
| 28 |
|
| 29 |
+
def reset(
|
| 30 |
+
self,
|
| 31 |
+
seed: int | None = None,
|
| 32 |
+
episode_id: str | None = None,
|
| 33 |
+
problem_id: str | None = None,
|
| 34 |
+
difficulty: str | None = None,
|
| 35 |
+
**_: Any,
|
| 36 |
+
) -> AdaptObservation:
|
| 37 |
+
del seed
|
| 38 |
+
self.problem = load_problem(problem_id=problem_id, difficulty=difficulty)
|
| 39 |
+
self.test_cases = [dict(test_case) for test_case in self.problem["test_cases"]]
|
| 40 |
+
self.visible_tests, self.hidden_tests = split_test_cases(self.test_cases)
|
| 41 |
+
self.last_results = []
|
| 42 |
+
self._state = AdaptState(
|
| 43 |
+
episode_id=episode_id or str(uuid4()),
|
| 44 |
+
step_count=0,
|
| 45 |
+
problem_id=self.problem["problem_id"],
|
| 46 |
+
difficulty=self.problem["difficulty"],
|
| 47 |
+
)
|
| 48 |
+
return self._build_observation(
|
| 49 |
+
reward=0.0,
|
| 50 |
+
done=False,
|
| 51 |
+
feedback="Submit Python code that reads stdin and prints the required answer.",
|
| 52 |
+
)
|
| 53 |
|
| 54 |
+
def step(
|
| 55 |
+
self,
|
| 56 |
+
action: AdaptAction,
|
| 57 |
+
timeout_s: float | None = None,
|
| 58 |
+
**_: Any,
|
| 59 |
+
) -> AdaptObservation:
|
| 60 |
+
del timeout_s
|
| 61 |
+
if not self.problem:
|
| 62 |
self.reset()
|
| 63 |
|
| 64 |
+
self._state.step_count += 1
|
| 65 |
+
syntax_ok, syntax_error = self._check_syntax(action.code)
|
| 66 |
+
if not syntax_ok:
|
| 67 |
+
observation = self._build_observation(
|
| 68 |
+
reward=0.0,
|
| 69 |
+
done=True,
|
| 70 |
+
feedback=f"Syntax error: {syntax_error}",
|
| 71 |
+
syntax_valid=False,
|
| 72 |
+
execution_status="syntax_error",
|
| 73 |
+
)
|
| 74 |
+
self._record_metrics(observation)
|
| 75 |
+
return observation
|
| 76 |
|
| 77 |
+
safety_ok, safety_error = self._check_safety(action.code)
|
| 78 |
+
if not safety_ok:
|
| 79 |
+
observation = self._build_observation(
|
| 80 |
+
reward=0.0,
|
| 81 |
+
done=True,
|
| 82 |
+
feedback=safety_error,
|
| 83 |
+
syntax_valid=True,
|
| 84 |
+
execution_status="safety_violation",
|
| 85 |
+
)
|
| 86 |
+
self._record_metrics(observation)
|
| 87 |
+
return observation
|
| 88 |
|
| 89 |
+
run_results = self._run_all_tests(action.code)
|
| 90 |
+
self.last_results = run_results
|
| 91 |
+
metrics = self._score_results(run_results)
|
| 92 |
+
verifier_reward, verifier_metadata = self._try_verify(action.code)
|
| 93 |
+
if verifier_reward is not None:
|
| 94 |
+
metrics["reward"] = max(metrics["reward"], verifier_reward)
|
| 95 |
+
if verifier_metadata.get("feedback"):
|
| 96 |
+
metrics["feedback"] = str(verifier_metadata["feedback"])
|
| 97 |
|
| 98 |
+
observation = self._build_observation(
|
| 99 |
+
reward=metrics["reward"],
|
| 100 |
+
done=True,
|
| 101 |
+
feedback=metrics["feedback"],
|
| 102 |
+
pass_rate=metrics["pass_rate"],
|
| 103 |
+
visible_pass_rate=metrics["visible_pass_rate"],
|
| 104 |
+
hidden_pass_rate=metrics["hidden_pass_rate"],
|
| 105 |
+
syntax_valid=True,
|
| 106 |
+
execution_status=metrics["execution_status"],
|
| 107 |
+
timeout_count=metrics["timeout_count"],
|
| 108 |
+
runtime_error_count=metrics["runtime_error_count"],
|
| 109 |
+
format_compliance=metrics["format_compliance"],
|
| 110 |
+
reward_components=metrics["reward_components"],
|
| 111 |
+
)
|
| 112 |
+
self._record_metrics(observation)
|
| 113 |
+
return observation
|
| 114 |
|
| 115 |
+
@property
|
| 116 |
+
def state(self) -> AdaptState:
|
| 117 |
+
return self._state
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
+
def _build_observation(
|
| 120 |
+
self,
|
| 121 |
+
reward: float,
|
| 122 |
+
done: bool,
|
| 123 |
+
feedback: str,
|
| 124 |
+
pass_rate: float = 0.0,
|
| 125 |
+
visible_pass_rate: float = 0.0,
|
| 126 |
+
hidden_pass_rate: float = 0.0,
|
| 127 |
+
syntax_valid: bool = True,
|
| 128 |
+
execution_status: str = "not_run",
|
| 129 |
+
timeout_count: int = 0,
|
| 130 |
+
runtime_error_count: int = 0,
|
| 131 |
+
format_compliance: float = 0.0,
|
| 132 |
+
reward_components: dict[str, float] | None = None,
|
| 133 |
+
) -> AdaptObservation:
|
| 134 |
+
return AdaptObservation(
|
| 135 |
+
problem_id=self.problem["problem_id"],
|
| 136 |
+
difficulty=self.problem["difficulty"],
|
| 137 |
+
problem=self.problem["problem"],
|
| 138 |
+
input_format=self.problem["input_format"],
|
| 139 |
+
constraints=self.problem["constraints"],
|
| 140 |
+
examples=self.problem["examples"],
|
| 141 |
+
visible_tests=self.visible_tests,
|
| 142 |
+
feedback=feedback,
|
| 143 |
+
pass_rate=pass_rate,
|
| 144 |
+
visible_pass_rate=visible_pass_rate,
|
| 145 |
+
hidden_pass_rate=hidden_pass_rate,
|
| 146 |
+
syntax_valid=syntax_valid,
|
| 147 |
+
execution_status=execution_status,
|
| 148 |
+
timeout_count=timeout_count,
|
| 149 |
+
runtime_error_count=runtime_error_count,
|
| 150 |
+
format_compliance=format_compliance,
|
| 151 |
+
reward_components=reward_components or {},
|
| 152 |
+
reward=round(max(0.0, min(1.0, reward)), 4),
|
| 153 |
+
done=done,
|
| 154 |
+
)
|
| 155 |
|
| 156 |
def _run_all_tests(self, code: str) -> list[dict[str, Any]]:
|
| 157 |
results = []
|
| 158 |
+
visible_count = len(self.visible_tests)
|
| 159 |
+
for index, test_case in enumerate(self.test_cases):
|
| 160 |
execution = run_code(code, test_case["input"])
|
| 161 |
+
actual = str(execution["stdout"]).strip()
|
| 162 |
expected = test_case["output"].strip()
|
| 163 |
results.append(
|
| 164 |
{
|
| 165 |
+
"index": index,
|
| 166 |
+
"split": "visible" if index < visible_count else "hidden",
|
| 167 |
+
"input": test_case["input"] if index < visible_count else None,
|
| 168 |
+
"expected": expected if index < visible_count else None,
|
| 169 |
+
"actual": actual if index < visible_count else None,
|
| 170 |
+
"stderr": str(execution["stderr"]).strip(),
|
| 171 |
+
"exit_code": int(execution["exit_code"]),
|
| 172 |
+
"timed_out": bool(execution.get("timed_out", False)),
|
| 173 |
"passed": execution["exit_code"] == 0 and actual == expected,
|
| 174 |
+
"format_ok": execution["exit_code"] == 0 and actual != "",
|
| 175 |
}
|
| 176 |
)
|
| 177 |
return results
|
| 178 |
|
| 179 |
+
def _score_results(self, run_results: list[dict[str, Any]]) -> dict[str, Any]:
|
| 180 |
+
total = len(run_results)
|
| 181 |
+
visible = [result for result in run_results if result["split"] == "visible"]
|
| 182 |
+
hidden = [result for result in run_results if result["split"] == "hidden"]
|
| 183 |
+
pass_rate = self._pass_rate(run_results)
|
| 184 |
+
visible_pass_rate = self._pass_rate(visible)
|
| 185 |
+
hidden_pass_rate = self._pass_rate(hidden)
|
| 186 |
+
timeout_count = sum(1 for result in run_results if result["timed_out"])
|
| 187 |
+
runtime_error_count = sum(
|
| 188 |
+
1
|
| 189 |
+
for result in run_results
|
| 190 |
+
if result["exit_code"] != 0 and not result["timed_out"]
|
| 191 |
+
)
|
| 192 |
+
format_compliance = (
|
| 193 |
+
sum(1 for result in run_results if result["format_ok"]) / total
|
| 194 |
+
if total
|
| 195 |
+
else 0.0
|
| 196 |
+
)
|
| 197 |
+
timeout_rate = timeout_count / total if total else 0.0
|
| 198 |
+
runtime_error_rate = runtime_error_count / total if total else 0.0
|
| 199 |
+
reward_components = {
|
| 200 |
+
"correctness": 0.8 * pass_rate,
|
| 201 |
+
"syntax": 0.05,
|
| 202 |
+
"execution": 0.05 if runtime_error_count == 0 and timeout_count == 0 else 0.0,
|
| 203 |
+
"format": 0.1 * format_compliance,
|
| 204 |
+
"timeout_penalty": -0.2 * timeout_rate,
|
| 205 |
+
"runtime_penalty": -0.1 * runtime_error_rate,
|
| 206 |
+
}
|
| 207 |
+
reward = max(0.0, min(1.0, sum(reward_components.values())))
|
| 208 |
|
| 209 |
+
if timeout_count:
|
| 210 |
+
status = "timeout"
|
| 211 |
+
elif runtime_error_count:
|
| 212 |
+
status = "runtime_error"
|
| 213 |
+
else:
|
| 214 |
+
status = "completed"
|
| 215 |
|
| 216 |
+
return {
|
| 217 |
+
"reward": round(reward, 4),
|
| 218 |
+
"feedback": self._build_feedback(run_results, pass_rate),
|
| 219 |
+
"pass_rate": round(pass_rate, 4),
|
| 220 |
+
"visible_pass_rate": round(visible_pass_rate, 4),
|
| 221 |
+
"hidden_pass_rate": round(hidden_pass_rate, 4),
|
| 222 |
+
"timeout_count": timeout_count,
|
| 223 |
+
"runtime_error_count": runtime_error_count,
|
| 224 |
+
"format_compliance": round(format_compliance, 4),
|
| 225 |
+
"execution_status": status,
|
| 226 |
+
"reward_components": {
|
| 227 |
+
key: round(value, 4) for key, value in reward_components.items()
|
| 228 |
+
},
|
| 229 |
+
}
|
| 230 |
|
| 231 |
def _build_feedback(self, run_results: list[dict[str, Any]], pass_rate: float) -> str:
|
| 232 |
for result in run_results:
|
| 233 |
+
if result["timed_out"]:
|
| 234 |
+
label = self._safe_test_label(result)
|
| 235 |
+
return f"Timed out on {label}."
|
| 236 |
+
|
| 237 |
if result["exit_code"] != 0:
|
| 238 |
+
label = self._safe_test_label(result)
|
| 239 |
error = result["stderr"] or "runtime error"
|
| 240 |
+
return f"Runtime error on {label}: {error}"
|
| 241 |
|
| 242 |
+
if not result["passed"] and result["split"] == "visible":
|
| 243 |
return (
|
| 244 |
+
f"Failed on visible input {str(result['input']).strip()}: "
|
| 245 |
f"expected {result['expected']}, got {result['actual']}"
|
| 246 |
)
|
| 247 |
|
| 248 |
+
if not result["passed"]:
|
| 249 |
+
return f"Failed on hidden test {result['index'] + 1}."
|
| 250 |
+
|
| 251 |
return f"All tests passed. Pass rate: {pass_rate:.2f}"
|
| 252 |
+
|
| 253 |
+
def _record_metrics(self, observation: AdaptObservation) -> None:
|
| 254 |
+
self._state.last_reward = float(observation.reward or 0.0)
|
| 255 |
+
self._state.last_pass_rate = observation.pass_rate
|
| 256 |
+
self._state.last_feedback = observation.feedback
|
| 257 |
+
self._state.recent_metrics = {
|
| 258 |
+
"visible_pass_rate": observation.visible_pass_rate,
|
| 259 |
+
"hidden_pass_rate": observation.hidden_pass_rate,
|
| 260 |
+
"execution_status": observation.execution_status,
|
| 261 |
+
"timeout_count": observation.timeout_count,
|
| 262 |
+
"runtime_error_count": observation.runtime_error_count,
|
| 263 |
+
"format_compliance": observation.format_compliance,
|
| 264 |
+
"reward_components": dict(observation.reward_components),
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
def _try_verify(self, code: str) -> tuple[float | None, dict[str, Any]]:
|
| 268 |
+
try:
|
| 269 |
+
from verifier.verifier import verify
|
| 270 |
+
except ImportError:
|
| 271 |
+
return None, {}
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
reward, metadata = verify(code, self.test_cases)
|
| 275 |
+
except Exception as exc:
|
| 276 |
+
return None, {"feedback": f"Verifier unavailable: {exc}"}
|
| 277 |
+
|
| 278 |
+
return float(reward), metadata or {}
|
| 279 |
+
|
| 280 |
+
def _check_syntax(self, code: str) -> tuple[bool, str]:
|
| 281 |
+
try:
|
| 282 |
+
ast.parse(code)
|
| 283 |
+
except SyntaxError as exc:
|
| 284 |
+
return False, str(exc)
|
| 285 |
+
return True, ""
|
| 286 |
+
|
| 287 |
+
def _check_safety(self, code: str) -> tuple[bool, str]:
|
| 288 |
+
tree = ast.parse(code)
|
| 289 |
+
for node in ast.walk(tree):
|
| 290 |
+
if isinstance(node, ast.Import):
|
| 291 |
+
for alias in node.names:
|
| 292 |
+
root_name = alias.name.split(".", 1)[0]
|
| 293 |
+
if root_name in FORBIDDEN_IMPORTS:
|
| 294 |
+
return False, f"Forbidden import: {root_name}"
|
| 295 |
+
|
| 296 |
+
if isinstance(node, ast.ImportFrom):
|
| 297 |
+
root_name = (node.module or "").split(".", 1)[0]
|
| 298 |
+
if root_name in FORBIDDEN_IMPORTS:
|
| 299 |
+
return False, f"Forbidden import: {root_name}"
|
| 300 |
+
|
| 301 |
+
return True, ""
|
| 302 |
+
|
| 303 |
+
def _pass_rate(self, results: list[dict[str, Any]]) -> float:
|
| 304 |
+
if not results:
|
| 305 |
+
return 0.0
|
| 306 |
+
return sum(1 for result in results if result["passed"]) / len(results)
|
| 307 |
+
|
| 308 |
+
def _safe_test_label(self, result: dict[str, Any]) -> str:
|
| 309 |
+
if result["split"] == "visible":
|
| 310 |
+
return f"visible input {str(result['input']).strip()}"
|
| 311 |
+
return f"hidden test {result['index'] + 1}"
|
env/executor.py
CHANGED
|
@@ -1,22 +1,29 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
| 3 |
import subprocess
|
| 4 |
import sys
|
| 5 |
-
import tempfile
|
| 6 |
from pathlib import Path
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
TIMEOUT_SECONDS = 2
|
| 10 |
|
| 11 |
|
| 12 |
def run_code(code: str, input_data: str) -> dict:
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
file_path = Path(tmpdir) / "submission.py"
|
| 15 |
file_path.write_text(code, encoding="utf-8")
|
| 16 |
|
| 17 |
try:
|
| 18 |
result = subprocess.run(
|
| 19 |
-
[
|
| 20 |
input=input_data,
|
| 21 |
text=True,
|
| 22 |
capture_output=True,
|
|
@@ -27,10 +34,14 @@ def run_code(code: str, input_data: str) -> dict:
|
|
| 27 |
"stdout": exc.stdout or "",
|
| 28 |
"stderr": "Execution timed out",
|
| 29 |
"exit_code": -1,
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
return {
|
| 33 |
"stdout": result.stdout,
|
| 34 |
"stderr": result.stderr,
|
| 35 |
"exit_code": result.returncode,
|
|
|
|
| 36 |
}
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import os
|
| 4 |
+
import shutil
|
| 5 |
import subprocess
|
| 6 |
import sys
|
|
|
|
| 7 |
from pathlib import Path
|
| 8 |
+
from uuid import uuid4
|
| 9 |
|
| 10 |
|
| 11 |
TIMEOUT_SECONDS = 2
|
| 12 |
|
| 13 |
|
| 14 |
def run_code(code: str, input_data: str) -> dict:
|
| 15 |
+
temp_parent = Path(os.getenv("ADAPT_TMP_DIR", ".adapt_tmp")).resolve()
|
| 16 |
+
temp_parent.mkdir(parents=True, exist_ok=True)
|
| 17 |
+
tmpdir = temp_parent / f"run_{uuid4().hex}"
|
| 18 |
+
tmpdir.mkdir()
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
file_path = Path(tmpdir) / "submission.py"
|
| 22 |
file_path.write_text(code, encoding="utf-8")
|
| 23 |
|
| 24 |
try:
|
| 25 |
result = subprocess.run(
|
| 26 |
+
["python3", str(file_path)],
|
| 27 |
input=input_data,
|
| 28 |
text=True,
|
| 29 |
capture_output=True,
|
|
|
|
| 34 |
"stdout": exc.stdout or "",
|
| 35 |
"stderr": "Execution timed out",
|
| 36 |
"exit_code": -1,
|
| 37 |
+
"timed_out": True,
|
| 38 |
}
|
| 39 |
|
| 40 |
return {
|
| 41 |
"stdout": result.stdout,
|
| 42 |
"stderr": result.stderr,
|
| 43 |
"exit_code": result.returncode,
|
| 44 |
+
"timed_out": False,
|
| 45 |
}
|
| 46 |
+
finally:
|
| 47 |
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
env/test_cases.py
CHANGED
|
@@ -1,35 +1,139 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
]
|
| 25 |
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
|
| 30 |
-
def
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
from typing import Any
|
| 4 |
|
| 5 |
+
|
| 6 |
+
VISIBLE_TEST_COUNT = 3
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
PROBLEM_BANK: list[dict[str, Any]] = [
|
| 10 |
+
{
|
| 11 |
+
"problem_id": "easy_double",
|
| 12 |
+
"difficulty": "easy",
|
| 13 |
+
"problem": "Given an integer n, print n * 2.",
|
| 14 |
+
"input_format": "A single integer n.",
|
| 15 |
+
"constraints": "-10^9 <= n <= 10^9",
|
| 16 |
+
"examples": [
|
| 17 |
+
{"input": "2\n", "output": "4"},
|
| 18 |
+
{"input": "5\n", "output": "10"},
|
| 19 |
+
],
|
| 20 |
+
"test_cases": [
|
| 21 |
+
{"input": "2\n", "output": "4"},
|
| 22 |
+
{"input": "5\n", "output": "10"},
|
| 23 |
+
{"input": "0\n", "output": "0"},
|
| 24 |
+
{"input": "1\n", "output": "2"},
|
| 25 |
+
{"input": "-3\n", "output": "-6"},
|
| 26 |
+
{"input": "10\n", "output": "20"},
|
| 27 |
+
{"input": "999999\n", "output": "1999998"},
|
| 28 |
+
{"input": "-1000000000\n", "output": "-2000000000"},
|
| 29 |
+
],
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"problem_id": "easy_sum_two",
|
| 33 |
+
"difficulty": "easy",
|
| 34 |
+
"problem": "Given two integers a and b, print their sum.",
|
| 35 |
+
"input_format": "Two space-separated integers a and b.",
|
| 36 |
+
"constraints": "-10^9 <= a, b <= 10^9",
|
| 37 |
+
"examples": [
|
| 38 |
+
{"input": "2 3\n", "output": "5"},
|
| 39 |
+
{"input": "-4 7\n", "output": "3"},
|
| 40 |
+
],
|
| 41 |
+
"test_cases": [
|
| 42 |
+
{"input": "2 3\n", "output": "5"},
|
| 43 |
+
{"input": "-4 7\n", "output": "3"},
|
| 44 |
+
{"input": "0 0\n", "output": "0"},
|
| 45 |
+
{"input": "1000000000 1\n", "output": "1000000001"},
|
| 46 |
+
{"input": "-8 -9\n", "output": "-17"},
|
| 47 |
+
{"input": "42 -42\n", "output": "0"},
|
| 48 |
+
],
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"problem_id": "medium_maximum",
|
| 52 |
+
"difficulty": "medium",
|
| 53 |
+
"problem": "Given n integers, print the maximum value.",
|
| 54 |
+
"input_format": "First line contains n. Second line contains n space-separated integers.",
|
| 55 |
+
"constraints": "1 <= n <= 200000; -10^9 <= values <= 10^9",
|
| 56 |
+
"examples": [
|
| 57 |
+
{"input": "5\n1 7 3 2 5\n", "output": "7"},
|
| 58 |
+
{"input": "3\n-5 -2 -9\n", "output": "-2"},
|
| 59 |
+
],
|
| 60 |
+
"test_cases": [
|
| 61 |
+
{"input": "5\n1 7 3 2 5\n", "output": "7"},
|
| 62 |
+
{"input": "3\n-5 -2 -9\n", "output": "-2"},
|
| 63 |
+
{"input": "1\n42\n", "output": "42"},
|
| 64 |
+
{"input": "6\n0 0 0 0 0 0\n", "output": "0"},
|
| 65 |
+
{"input": "4\n1000000000 -1 5 999999999\n", "output": "1000000000"},
|
| 66 |
+
{"input": "7\n-10 -20 -30 -1 -40 -50 -60\n", "output": "-1"},
|
| 67 |
+
],
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"problem_id": "medium_count_even",
|
| 71 |
+
"difficulty": "medium",
|
| 72 |
+
"problem": "Given n integers, print how many of them are even.",
|
| 73 |
+
"input_format": "First line contains n. Second line contains n space-separated integers.",
|
| 74 |
+
"constraints": "1 <= n <= 200000; -10^9 <= values <= 10^9",
|
| 75 |
+
"examples": [
|
| 76 |
+
{"input": "5\n1 2 3 4 5\n", "output": "2"},
|
| 77 |
+
{"input": "4\n2 4 6 8\n", "output": "4"},
|
| 78 |
+
],
|
| 79 |
+
"test_cases": [
|
| 80 |
+
{"input": "5\n1 2 3 4 5\n", "output": "2"},
|
| 81 |
+
{"input": "4\n2 4 6 8\n", "output": "4"},
|
| 82 |
+
{"input": "3\n1 3 5\n", "output": "0"},
|
| 83 |
+
{"input": "1\n0\n", "output": "1"},
|
| 84 |
+
{"input": "6\n-2 -3 -4 -5 -6 -7\n", "output": "3"},
|
| 85 |
+
{"input": "8\n10 11 12 13 14 15 16 17\n", "output": "4"},
|
| 86 |
+
],
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"problem_id": "hard_reverse_words",
|
| 90 |
+
"difficulty": "hard",
|
| 91 |
+
"problem": "Given a sentence, print its words in reverse order.",
|
| 92 |
+
"input_format": "A single line containing words separated by one or more spaces.",
|
| 93 |
+
"constraints": "1 <= sentence length <= 10000",
|
| 94 |
+
"examples": [
|
| 95 |
+
{"input": "hello world\n", "output": "world hello"},
|
| 96 |
+
{"input": "openenv rewards matter\n", "output": "matter rewards openenv"},
|
| 97 |
+
],
|
| 98 |
+
"test_cases": [
|
| 99 |
+
{"input": "hello world\n", "output": "world hello"},
|
| 100 |
+
{"input": "openenv rewards matter\n", "output": "matter rewards openenv"},
|
| 101 |
+
{"input": "single\n", "output": "single"},
|
| 102 |
+
{"input": " trim extra spaces \n", "output": "spaces extra trim"},
|
| 103 |
+
{"input": "a b c d e\n", "output": "e d c b a"},
|
| 104 |
+
{"input": "adaptive dsa tutor\n", "output": "tutor dsa adaptive"},
|
| 105 |
+
],
|
| 106 |
+
},
|
| 107 |
]
|
| 108 |
|
| 109 |
|
| 110 |
+
def load_problem_bank() -> list[dict[str, Any]]:
|
| 111 |
+
return [_copy_problem(problem) for problem in PROBLEM_BANK]
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def load_problem(problem_id: str | None = None, difficulty: str | None = None) -> dict[str, Any]:
|
| 115 |
+
problems = load_problem_bank()
|
| 116 |
+
if problem_id is not None:
|
| 117 |
+
for problem in problems:
|
| 118 |
+
if problem["problem_id"] == problem_id:
|
| 119 |
+
return problem
|
| 120 |
+
|
| 121 |
+
if difficulty is not None:
|
| 122 |
+
for problem in problems:
|
| 123 |
+
if problem["difficulty"] == difficulty:
|
| 124 |
+
return problem
|
| 125 |
+
|
| 126 |
+
return problems[0]
|
| 127 |
|
| 128 |
|
| 129 |
+
def split_test_cases(
|
| 130 |
+
test_cases: list[dict[str, str]],
|
| 131 |
+
) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
| 132 |
+
return test_cases[:VISIBLE_TEST_COUNT], test_cases[VISIBLE_TEST_COUNT:]
|
| 133 |
|
| 134 |
|
| 135 |
+
def _copy_problem(problem: dict[str, Any]) -> dict[str, Any]:
|
| 136 |
+
copied = dict(problem)
|
| 137 |
+
copied["examples"] = [dict(example) for example in problem["examples"]]
|
| 138 |
+
copied["test_cases"] = [dict(test_case) for test_case in problem["test_cases"]]
|
| 139 |
+
return copied
|
models.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from openenv.core.env_server.types import Action, Observation, State
|
| 6 |
+
from pydantic import Field
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class AdaptAction(Action):
|
| 10 |
+
code: str = Field(..., min_length=1, description="Python code to execute.")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AdaptObservation(Observation):
|
| 14 |
+
problem_id: str = Field(default="", description="Current problem identifier.")
|
| 15 |
+
difficulty: str = Field(default="", description="Current curriculum difficulty tier.")
|
| 16 |
+
problem: str = Field(default="", description="Problem statement shown to the agent.")
|
| 17 |
+
input_format: str = Field(default="", description="Expected stdin format.")
|
| 18 |
+
constraints: str = Field(default="", description="Problem constraints.")
|
| 19 |
+
examples: list[dict[str, str]] = Field(default_factory=list)
|
| 20 |
+
visible_tests: list[dict[str, str]] = Field(default_factory=list)
|
| 21 |
+
feedback: str = Field(default="", description="Human-readable execution feedback.")
|
| 22 |
+
pass_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 23 |
+
visible_pass_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 24 |
+
hidden_pass_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 25 |
+
syntax_valid: bool = Field(default=True)
|
| 26 |
+
execution_status: str = Field(default="not_run")
|
| 27 |
+
timeout_count: int = Field(default=0, ge=0)
|
| 28 |
+
runtime_error_count: int = Field(default=0, ge=0)
|
| 29 |
+
format_compliance: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 30 |
+
reward_components: dict[str, float] = Field(default_factory=dict)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class AdaptState(State):
|
| 34 |
+
problem_id: str = Field(default="")
|
| 35 |
+
difficulty: str = Field(default="")
|
| 36 |
+
last_reward: float = Field(default=0.0)
|
| 37 |
+
last_pass_rate: float = Field(default=0.0, ge=0.0, le=1.0)
|
| 38 |
+
last_feedback: str = Field(default="")
|
| 39 |
+
recent_metrics: dict[str, Any] = Field(default_factory=dict)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: adapt_dsa_tutor
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 7860
|
| 7 |
+
description: "ADAPT: an adversarial DSA tutor environment for RLVR code generation with hidden tests, tiered problems, and anti-hacking reward signals."
|
| 8 |
+
version: "0.2.0"
|
| 9 |
+
|
| 10 |
+
observation_space:
|
| 11 |
+
type: dict
|
| 12 |
+
description: "Problem prompt, examples, visible tests, difficulty metadata, reward, pass rates, execution status, and feedback."
|
| 13 |
+
|
| 14 |
+
action_space:
|
| 15 |
+
type: dict
|
| 16 |
+
description: "AdaptAction with a Python code string submitted for stdin/stdout evaluation."
|
| 17 |
+
|
| 18 |
+
reward_range: [0.0, 1.0]
|
| 19 |
+
|
| 20 |
+
tasks:
|
| 21 |
+
- name: easy_double
|
| 22 |
+
description: "Easy arithmetic stdin/stdout problem."
|
| 23 |
+
difficulty: easy
|
| 24 |
+
- name: easy_sum_two
|
| 25 |
+
description: "Easy two-integer arithmetic problem."
|
| 26 |
+
difficulty: easy
|
| 27 |
+
- name: medium_maximum
|
| 28 |
+
description: "Medium array scanning problem."
|
| 29 |
+
difficulty: medium
|
| 30 |
+
- name: medium_count_even
|
| 31 |
+
description: "Medium counting problem over a list."
|
| 32 |
+
difficulty: medium
|
| 33 |
+
- name: hard_reverse_words
|
| 34 |
+
description: "Harder string normalization and ordering problem."
|
| 35 |
+
difficulty: hard
|
pyproject.toml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=68", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "adapt-dsa-tutor"
|
| 7 |
+
version = "0.2.0"
|
| 8 |
+
description = "OpenEnv-compliant adversarial DSA tutor environment for RLVR code generation."
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.10"
|
| 11 |
+
dependencies = [
|
| 12 |
+
"openenv-core>=0.2.3",
|
| 13 |
+
"fastapi>=0.104.0",
|
| 14 |
+
"pydantic>=2.0.0",
|
| 15 |
+
"uvicorn>=0.24.0",
|
| 16 |
+
"httpx>=0.28.0",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[project.optional-dependencies]
|
| 20 |
+
dev = [
|
| 21 |
+
"pytest>=8.0.0",
|
| 22 |
+
]
|
| 23 |
+
|
| 24 |
+
[project.scripts]
|
| 25 |
+
server = "server.app:main"
|
| 26 |
+
|
| 27 |
+
[tool.setuptools]
|
| 28 |
+
include-package-data = true
|
| 29 |
+
packages = ["env", "server"]
|
| 30 |
+
py-modules = ["app", "client", "models"]
|
server/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from server.app import app, main
|
| 2 |
+
|
| 3 |
+
__all__ = ["app", "main"]
|
server/app.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
try:
|
| 4 |
+
from openenv.core.env_server.http_server import create_app
|
| 5 |
+
except Exception as exc: # pragma: no cover
|
| 6 |
+
raise ImportError(
|
| 7 |
+
"openenv-core>=0.2.3 is required. Install with: pip install -e ."
|
| 8 |
+
) from exc
|
| 9 |
+
|
| 10 |
+
from env.adapt_env import AdaptEnvironment
|
| 11 |
+
from models import AdaptAction, AdaptObservation
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
app = create_app(
|
| 15 |
+
AdaptEnvironment,
|
| 16 |
+
AdaptAction,
|
| 17 |
+
AdaptObservation,
|
| 18 |
+
env_name="adapt_dsa_tutor",
|
| 19 |
+
max_concurrent_envs=4,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main(host: str = "0.0.0.0", port: int = 7860) -> None:
|
| 24 |
+
import uvicorn
|
| 25 |
+
|
| 26 |
+
uvicorn.run(app, host=host, port=port)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
main()
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core>=0.2.3
|
| 2 |
+
fastapi>=0.104.0
|
| 3 |
+
pydantic>=2.0.0
|
| 4 |
+
uvicorn>=0.24.0
|
| 5 |
+
httpx>=0.28.0
|
test.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from env.adapt_env import AdaptEnvironment
|
| 4 |
+
from models import AdaptAction, AdaptObservation
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def assert_hidden_tests_are_not_exposed(payload: dict) -> None:
|
| 8 |
+
text = str(payload)
|
| 9 |
+
assert "hidden_tests" not in text
|
| 10 |
+
assert "-1000000000" not in text
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def main() -> None:
|
| 14 |
+
env = AdaptEnvironment()
|
| 15 |
+
observation = env.reset()
|
| 16 |
+
assert isinstance(observation, AdaptObservation)
|
| 17 |
+
assert observation.visible_tests
|
| 18 |
+
assert observation.problem_id == "easy_double"
|
| 19 |
+
assert_hidden_tests_are_not_exposed(observation.model_dump())
|
| 20 |
+
|
| 21 |
+
correct = env.step(AdaptAction(code="n=int(input())\nprint(n*2)"))
|
| 22 |
+
print(correct)
|
| 23 |
+
assert correct.reward == 1.0, correct.model_dump()
|
| 24 |
+
assert correct.pass_rate == 1.0
|
| 25 |
+
|
| 26 |
+
wrong = env.step(AdaptAction(code="n=int(input())\nprint(n+2)"))
|
| 27 |
+
print(wrong)
|
| 28 |
+
assert 0.0 <= float(wrong.reward) < 1.0
|
| 29 |
+
assert wrong.pass_rate < 1.0
|
| 30 |
+
assert "Failed" in wrong.feedback
|
| 31 |
+
|
| 32 |
+
syntax = env.step(AdaptAction(code="def broken(:\n pass"))
|
| 33 |
+
print(syntax)
|
| 34 |
+
assert syntax.reward == 0.0
|
| 35 |
+
assert syntax.execution_status == "syntax_error"
|
| 36 |
+
|
| 37 |
+
timeout = env.step(AdaptAction(code="while True:\n pass"))
|
| 38 |
+
print(timeout)
|
| 39 |
+
assert timeout.timeout_count > 0
|
| 40 |
+
assert timeout.execution_status == "timeout"
|
| 41 |
+
|
| 42 |
+
unsafe = env.step(AdaptAction(code="import os\nprint(os.listdir('.'))"))
|
| 43 |
+
print(unsafe)
|
| 44 |
+
assert unsafe.reward == 0.0
|
| 45 |
+
assert unsafe.execution_status == "safety_violation"
|
| 46 |
+
|
| 47 |
+
assert env.state.step_count == 5
|
| 48 |
+
assert_hidden_tests_are_not_exposed(timeout.model_dump())
|
| 49 |
+
print("ADAPT OpenEnv smoke tests passed")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
main()
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|