s-shah4 commited on
Commit
4433dc8
·
1 Parent(s): da8df85

Add V1 env

Browse files
Files changed (18) hide show
  1. .dockerignore +8 -0
  2. .gitignore +2 -1
  3. Dockerfile +11 -0
  4. README.md +111 -72
  5. app.py +3 -0
  6. client.py +34 -0
  7. env/__init__.py +3 -0
  8. env/adapt_env.py +265 -58
  9. env/executor.py +14 -3
  10. env/test_cases.py +129 -25
  11. models.py +39 -0
  12. openenv.yaml +35 -0
  13. pyproject.toml +30 -0
  14. server/__init__.py +3 -0
  15. server/app.py +30 -0
  16. server/requirements.txt +5 -0
  17. test.py +53 -0
  18. 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
- # meta-rl-dsa-solver
 
 
 
 
 
 
 
 
 
 
2
 
3
- ADAPT (Adversarial DSA Tutor) is a minimal reinforcement learning environment for DSA code-generation tasks.
4
 
5
- The current implementation is V1: direct Python usage, no FastAPI, multiple test cases, hidden tests, subprocess execution, and verifier-based rewards.
6
 
7
- ## Usage
8
 
9
- ```python
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
- Flow:
21
 
22
  ```text
23
- model -> generates code -> env.step(code) -> executor runs code -> verifier evaluates -> env returns result
24
  ```
25
 
26
- ## Files
27
 
28
- - `env/adapt_env.py`: reset/step orchestration only
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
- ## Observation
33
-
34
- `reset()` returns:
35
 
36
- ```python
37
- {
38
- "problem": str,
39
- "input_format": str,
40
- "constraints": str,
41
- "examples": list,
42
- "visible_tests": list,
43
- }
44
- ```
45
 
46
- Hidden tests are kept inside the environment and are not shown in the observation.
47
 
48
- ## Step Result
 
 
 
49
 
50
- `step(code)` returns:
51
 
52
  ```python
53
  {
54
- "reward": float,
55
- "done": bool,
56
- "feedback": str,
57
- "pass_rate": float,
58
  }
59
  ```
60
 
61
- ## Verifier Requirement
62
 
63
- `env.step(code)` calls:
64
 
65
- ```python
66
- from verifier.verifier import verify
 
 
 
 
 
 
 
 
 
67
 
68
- reward, metadata = verify(code, test_cases)
69
- ```
70
 
71
- The verifier should return:
72
 
73
- ```python
74
- (
75
- 1.0,
76
- {
77
- "pass_rate": 1.0,
78
- "feedback": "All tests passed. Pass rate: 1.00",
79
- },
80
- )
81
- ```
82
 
83
- If `metadata` does not include `pass_rate` or `feedback`, the environment computes fallback values from executor results.
84
 
85
- ## Smoke Checks
86
 
87
- From this directory:
88
 
89
  ```powershell
90
  cd C:\Users\kaust\PycharmProjects\meta-rl-dsa-solver
 
 
91
  ```
92
 
93
- Check reset and visible/hidden split:
94
 
95
  ```powershell
96
- python -B -c "from env.adapt_env import AdaptEnv; env=AdaptEnv(); print(env.reset()); print(len(env.visible_tests), len(env.hidden_tests))"
97
  ```
98
 
99
- Expected split:
100
 
101
- ```text
102
- 3 5
 
 
103
  ```
104
 
105
- Check executor directly:
106
 
107
  ```powershell
108
- python -B -c "from env.executor import run_code; print(run_code('n=int(input())\nprint(n*2)', '5\n'))"
109
  ```
110
 
111
- Expected output:
112
 
113
- ```python
114
- {'stdout': '10\n', 'stderr': '', 'exit_code': 0}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  ```
116
 
117
- Once `verifier/verifier.py` exists, check the full environment:
118
 
119
  ```powershell
120
- python -B -c "from env.adapt_env import AdaptEnv; env=AdaptEnv(); env.reset(); print(env.step('n=int(input())\nprint(n*2)'))"
121
  ```
122
 
123
- Check a wrong answer:
 
 
124
 
125
  ```powershell
126
- python -B -c "from env.adapt_env import AdaptEnv; env=AdaptEnv(); env.reset(); print(env.step('n=int(input())\nprint(n+2)'))"
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 VISIBLE_TEST_COUNT, load_problem, load_test_cases
 
 
 
 
7
 
8
 
9
- class AdaptEnv:
 
 
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.step_count = 0
16
 
17
- def reset(self) -> dict:
18
- self.problem = self._load_problem()
19
- self.test_cases = load_test_cases()
20
- self.visible_tests, self.hidden_tests = self._split_test_cases(self.test_cases)
21
- self.step_count = 0
22
- return self._build_observation()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- def step(self, code: str) -> dict:
25
- if not self.test_cases:
 
 
 
 
 
 
26
  self.reset()
27
 
28
- self.step_count += 1
29
- run_results = self._run_all_tests(code)
30
- reward, metadata = self._verify_code(code)
31
- metadata = metadata or {}
 
 
 
 
 
 
 
 
32
 
33
- pass_rate = float(metadata.get("pass_rate", self._compute_pass_rate(run_results)))
34
- feedback = str(metadata.get("feedback") or self._build_feedback(run_results, pass_rate))
 
 
 
 
 
 
 
 
 
35
 
36
- return {
37
- "reward": float(reward),
38
- "done": True,
39
- "feedback": feedback,
40
- "pass_rate": pass_rate,
41
- }
 
 
42
 
43
- def _load_problem(self) -> dict:
44
- return load_problem()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- def _split_test_cases(
47
- self,
48
- test_cases: list[dict[str, str]],
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(self) -> dict:
55
- return {
56
- "problem": self.problem["problem"],
57
- "input_format": self.problem["input_format"],
58
- "constraints": self.problem["constraints"],
59
- "examples": self.problem["examples"],
60
- "visible_tests": self.visible_tests,
61
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  def _run_all_tests(self, code: str) -> list[dict[str, Any]]:
64
  results = []
65
- for test_case in self.test_cases:
 
66
  execution = run_code(code, test_case["input"])
67
- actual = execution["stdout"].strip()
68
  expected = test_case["output"].strip()
69
  results.append(
70
  {
71
- "input": test_case["input"],
72
- "expected": expected,
73
- "actual": actual,
74
- "stderr": execution["stderr"].strip(),
75
- "exit_code": execution["exit_code"],
 
 
 
76
  "passed": execution["exit_code"] == 0 and actual == expected,
 
77
  }
78
  )
79
  return results
80
 
81
- def _verify_code(self, code: str) -> tuple[float, dict[str, Any]]:
82
- from verifier.verifier import verify
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- return verify(code, self.test_cases)
 
 
 
 
 
85
 
86
- def _compute_pass_rate(self, run_results: list[dict[str, Any]]) -> float:
87
- if not run_results:
88
- return 0.0
89
- passed = sum(1 for result in run_results if result["passed"])
90
- return passed / len(run_results)
 
 
 
 
 
 
 
 
 
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 input {result['input'].strip()}: {error}"
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
- with tempfile.TemporaryDirectory() as tmpdir:
 
 
 
 
 
14
  file_path = Path(tmpdir) / "submission.py"
15
  file_path.write_text(code, encoding="utf-8")
16
 
17
  try:
18
  result = subprocess.run(
19
- [sys.executable, str(file_path)],
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
- PROBLEM = {
5
- "problem": "Given an integer n, print n * 2.",
6
- "input_format": "A single integer n.",
7
- "constraints": "-10^9 <= n <= 10^9",
8
- "examples": [
9
- {"input": "2\n", "output": "4"},
10
- {"input": "5\n", "output": "10"},
11
- ],
12
- }
13
-
14
-
15
- TEST_CASES = [
16
- {"input": "2\n", "output": "4"},
17
- {"input": "5\n", "output": "10"},
18
- {"input": "0\n", "output": "0"},
19
- {"input": "1\n", "output": "2"},
20
- {"input": "-3\n", "output": "-6"},
21
- {"input": "10\n", "output": "20"},
22
- {"input": "999999\n", "output": "1999998"},
23
- {"input": "-1000000000\n", "output": "-2000000000"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ]
25
 
26
 
27
- VISIBLE_TEST_COUNT = 3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
 
30
- def load_problem() -> dict:
31
- return dict(PROBLEM)
 
 
32
 
33
 
34
- def load_test_cases() -> list[dict[str, str]]:
35
- return [dict(test_case) for test_case in TEST_CASES]
 
 
 
 
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