codefix-env 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codefix_env/__init__.py +80 -0
- codefix_env/cli.py +83 -0
- codefix_env/client.py +194 -0
- codefix_env/env.py +503 -0
- codefix_env/models.py +234 -0
- codefix_env/rewards.py +157 -0
- codefix_env/tasks/__init__.py +75 -0
- codefix_env/tasks/easy.py +244 -0
- codefix_env/tasks/hard.py +430 -0
- codefix_env/tasks/medium.py +342 -0
- codefix_env/utils/__init__.py +33 -0
- codefix_env/utils/metrics.py +165 -0
- codefix_env/utils/reward_model.py +79 -0
- codefix_env/utils/sandbox.py +392 -0
- codefix_env-0.2.1.dist-info/METADATA +663 -0
- codefix_env-0.2.1.dist-info/RECORD +20 -0
- codefix_env-0.2.1.dist-info/WHEEL +5 -0
- codefix_env-0.2.1.dist-info/entry_points.txt +2 -0
- codefix_env-0.2.1.dist-info/licenses/LICENSE +201 -0
- codefix_env-0.2.1.dist-info/top_level.txt +1 -0
codefix_env/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeFix-Env — RL Environment for Automated Code Debugging
|
|
3
|
+
==========================================================
|
|
4
|
+
Quick start::
|
|
5
|
+
from codefix_env import CodeFixEnvironment, CodeFixAction, CodeFixClient
|
|
6
|
+
# Server-side (direct, no HTTP)
|
|
7
|
+
env = CodeFixEnvironment()
|
|
8
|
+
obs = env.reset(task_id="easy-001-missing-return")
|
|
9
|
+
result = env.step(CodeFixAction(action_type="run_tests"))
|
|
10
|
+
# Client-side (HTTP)
|
|
11
|
+
async with CodeFixClient("http://localhost:8000") as env:
|
|
12
|
+
obs = await env.reset()
|
|
13
|
+
result = await env.step(CodeFixAction(action_type="submit_fix"))
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.1"
|
|
17
|
+
__author__ = "Shailendra Dhakad"
|
|
18
|
+
|
|
19
|
+
from codefix_env.client import CodeFixClient, SyncCodeFixClient
|
|
20
|
+
from codefix_env.env import CodeFixEnvironment
|
|
21
|
+
from codefix_env.models import (
|
|
22
|
+
ActionType,
|
|
23
|
+
BugCategory,
|
|
24
|
+
CodeFixAction,
|
|
25
|
+
CodeFixObservation,
|
|
26
|
+
CodeFixState,
|
|
27
|
+
Difficulty,
|
|
28
|
+
StepResult,
|
|
29
|
+
Task,
|
|
30
|
+
TestCase,
|
|
31
|
+
TestResult,
|
|
32
|
+
)
|
|
33
|
+
from codefix_env.rewards import RewardPipeline
|
|
34
|
+
from codefix_env.tasks import list_tasks, load_task, random_task, task_count
|
|
35
|
+
from codefix_env.utils.metrics import EpisodeMetrics, ScoringConfig
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Core
|
|
39
|
+
"CodeFixEnvironment",
|
|
40
|
+
"CodeFixClient",
|
|
41
|
+
"SyncCodeFixClient",
|
|
42
|
+
# Models
|
|
43
|
+
"CodeFixAction",
|
|
44
|
+
"CodeFixObservation",
|
|
45
|
+
"CodeFixState",
|
|
46
|
+
"StepResult",
|
|
47
|
+
"Task",
|
|
48
|
+
"TestCase",
|
|
49
|
+
"TestResult",
|
|
50
|
+
"ActionType",
|
|
51
|
+
"Difficulty",
|
|
52
|
+
"BugCategory",
|
|
53
|
+
# Rewards
|
|
54
|
+
"RewardPipeline",
|
|
55
|
+
"ScoringConfig",
|
|
56
|
+
"RewardMLP",
|
|
57
|
+
"EpisodeMetrics",
|
|
58
|
+
# Tasks
|
|
59
|
+
"load_task",
|
|
60
|
+
"random_task",
|
|
61
|
+
"list_tasks",
|
|
62
|
+
"task_count",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name):
|
|
67
|
+
"""
|
|
68
|
+
Lazy attribute access for RewardMLP only. RewardMLP lives in
|
|
69
|
+
utils/reward_model.py (a torch-only module kept separate from the
|
|
70
|
+
rest of the package) so that `import codefix_env` never pays torch's
|
|
71
|
+
import cost unless someone actually touches RewardMLP. This matters
|
|
72
|
+
most for sandboxed worker processes (utils/sandbox.py) that re-import
|
|
73
|
+
the whole package on every spawn, especially on Windows where
|
|
74
|
+
multiprocessing uses "spawn" instead of "fork".
|
|
75
|
+
"""
|
|
76
|
+
if name == "RewardMLP":
|
|
77
|
+
from codefix_env.utils.reward_model import RewardMLP
|
|
78
|
+
|
|
79
|
+
return RewardMLP
|
|
80
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
codefix_env/cli.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeFix-Env CLI
|
|
3
|
+
================
|
|
4
|
+
Entry point registered in pyproject.toml as `codefix-server`.
|
|
5
|
+
|
|
6
|
+
Previously pyproject.toml declared:
|
|
7
|
+
[project.scripts]
|
|
8
|
+
codefix-server = "codefix_env.cli:app"
|
|
9
|
+
|
|
10
|
+
...but this module did not exist, so `pip install -e .` produced a broken
|
|
11
|
+
console script. This file makes that entry point real.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
codefix-server serve # start the API server
|
|
15
|
+
codefix-server serve --port 9000
|
|
16
|
+
codefix-server tasks # list all tasks
|
|
17
|
+
codefix-server info # print version + task counts
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
from codefix_env import __version__
|
|
27
|
+
from codefix_env.tasks import list_tasks, task_count
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(help="CodeFix-Env command line interface.")
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
def serve(
|
|
35
|
+
host: str = typer.Option("0.0.0.0", help="Bind host."),
|
|
36
|
+
port: int = typer.Option(8000, help="Bind port."),
|
|
37
|
+
reload: bool = typer.Option(False, help="Auto-reload on code changes (dev only)."),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Start the CodeFix-Env FastAPI server."""
|
|
40
|
+
import sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
import uvicorn
|
|
44
|
+
|
|
45
|
+
# `server/` lives at the repo root, not under src/, so it isn't on
|
|
46
|
+
# sys.path just because codefix_env is pip-installed. Add the repo
|
|
47
|
+
# root explicitly so `server.app:app` is importable regardless of
|
|
48
|
+
# cwd or PYTHONPATH — this was silently broken before (CI only
|
|
49
|
+
# tested `codefix-server info`, never `serve`, so it went unnoticed).
|
|
50
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
51
|
+
if str(repo_root) not in sys.path:
|
|
52
|
+
sys.path.insert(0, str(repo_root))
|
|
53
|
+
|
|
54
|
+
uvicorn.run("server.app:app", host=host, port=port, reload=reload)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def tasks(
|
|
59
|
+
difficulty: str = typer.Option(None, help="Filter: easy | medium | hard"),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""List all available debugging tasks."""
|
|
62
|
+
from codefix_env.models import Difficulty
|
|
63
|
+
|
|
64
|
+
diff_filter = Difficulty(difficulty) if difficulty else None
|
|
65
|
+
table = Table(title="CodeFix-Env Tasks")
|
|
66
|
+
table.add_column("ID")
|
|
67
|
+
table.add_column("Title")
|
|
68
|
+
table.add_column("Difficulty")
|
|
69
|
+
table.add_column("Category")
|
|
70
|
+
for t in list_tasks(difficulty=diff_filter):
|
|
71
|
+
table.add_row(t.id, t.title, str(t.difficulty), str(t.bug_category))
|
|
72
|
+
console.print(table)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command()
|
|
76
|
+
def info() -> None:
|
|
77
|
+
"""Print version and task counts."""
|
|
78
|
+
console.print(f"[bold]codefix-env[/bold] v{__version__}")
|
|
79
|
+
console.print(task_count())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
app()
|
codefix_env/client.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CodeFixClient — Async & Sync HTTP Client
|
|
3
|
+
=========================================
|
|
4
|
+
Communicates with the CodeFix-Env FastAPI server via HTTP + WebSocket.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
# Async (recommended for RL training loops)
|
|
9
|
+
async with CodeFixClient("http://localhost:8000") as env:
|
|
10
|
+
obs = await env.reset()
|
|
11
|
+
result = await env.step(CodeFixAction(action_type="run_tests"))
|
|
12
|
+
|
|
13
|
+
# Sync (via .sync() wrapper — for notebooks and scripts)
|
|
14
|
+
with CodeFixClient("http://localhost:8000").sync() as env:
|
|
15
|
+
obs = env.reset()
|
|
16
|
+
result = env.step(CodeFixAction(action_type="run_tests"))
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
from codefix_env.models import (
|
|
27
|
+
CodeFixAction,
|
|
28
|
+
CodeFixObservation,
|
|
29
|
+
CodeFixState,
|
|
30
|
+
Difficulty,
|
|
31
|
+
StepResult,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CodeFixClient:
|
|
38
|
+
"""
|
|
39
|
+
Async HTTP client for the CodeFix-Env server.
|
|
40
|
+
Provides the same reset() / step() / state() interface as the server-side env.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
base_url: str = "http://localhost:8000",
|
|
46
|
+
timeout: float = 30.0,
|
|
47
|
+
max_retries: int = 3,
|
|
48
|
+
):
|
|
49
|
+
self.base_url = base_url.rstrip("/")
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.max_retries = max_retries
|
|
52
|
+
self._client: httpx.AsyncClient | None = None
|
|
53
|
+
|
|
54
|
+
# ── Context manager ───────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
async def __aenter__(self) -> CodeFixClient:
|
|
57
|
+
self._client = httpx.AsyncClient(
|
|
58
|
+
base_url=self.base_url,
|
|
59
|
+
timeout=self.timeout,
|
|
60
|
+
)
|
|
61
|
+
# Health check
|
|
62
|
+
try:
|
|
63
|
+
r = await self._client.get("/health")
|
|
64
|
+
r.raise_for_status()
|
|
65
|
+
logger.info("Connected to CodeFix-Env at %s", self.base_url)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.warning("Health check failed: %s. Proceeding anyway.", e)
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __aexit__(self, *args) -> None:
|
|
71
|
+
if self._client:
|
|
72
|
+
await self._client.aclose()
|
|
73
|
+
self._client = None
|
|
74
|
+
|
|
75
|
+
# ── Core API ──────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async def reset(
|
|
78
|
+
self,
|
|
79
|
+
task_id: str | None = None,
|
|
80
|
+
difficulty: Difficulty | None = None,
|
|
81
|
+
seed: int | None = None,
|
|
82
|
+
) -> CodeFixObservation:
|
|
83
|
+
"""Reset the environment and return the initial observation."""
|
|
84
|
+
payload: dict = {}
|
|
85
|
+
if task_id:
|
|
86
|
+
payload["task_id"] = task_id
|
|
87
|
+
if difficulty:
|
|
88
|
+
payload["difficulty"] = difficulty
|
|
89
|
+
if seed is not None:
|
|
90
|
+
payload["seed"] = seed
|
|
91
|
+
data = await self._post("/reset", payload)
|
|
92
|
+
return CodeFixObservation.model_validate(data)
|
|
93
|
+
|
|
94
|
+
async def step(self, action: CodeFixAction) -> StepResult:
|
|
95
|
+
"""Execute an action and return the step result."""
|
|
96
|
+
data = await self._post("/step", action.model_dump())
|
|
97
|
+
return StepResult.model_validate(data)
|
|
98
|
+
|
|
99
|
+
async def state(self) -> CodeFixState:
|
|
100
|
+
"""Get internal episode state."""
|
|
101
|
+
data = await self._get("/state")
|
|
102
|
+
return CodeFixState.model_validate(data)
|
|
103
|
+
|
|
104
|
+
async def list_tasks(self, difficulty: str | None = None) -> list[dict]:
|
|
105
|
+
"""List all available tasks."""
|
|
106
|
+
params = {}
|
|
107
|
+
if difficulty:
|
|
108
|
+
params["difficulty"] = difficulty
|
|
109
|
+
data = await self._get("/tasks", params=params)
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
async def health(self) -> dict:
|
|
113
|
+
"""Ping the server."""
|
|
114
|
+
return await self._get("/health")
|
|
115
|
+
|
|
116
|
+
# ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
async def _post(self, path: str, body: dict) -> dict:
|
|
119
|
+
assert self._client, "Use `async with CodeFixClient(...) as env:`"
|
|
120
|
+
for attempt in range(self.max_retries):
|
|
121
|
+
try:
|
|
122
|
+
r = await self._client.post(path, json=body)
|
|
123
|
+
r.raise_for_status()
|
|
124
|
+
return r.json()
|
|
125
|
+
except httpx.HTTPStatusError as e:
|
|
126
|
+
logger.error(
|
|
127
|
+
"HTTP %d on POST %s: %s", e.response.status_code, path, e.response.text
|
|
128
|
+
)
|
|
129
|
+
raise
|
|
130
|
+
except httpx.TransportError:
|
|
131
|
+
logger.error(
|
|
132
|
+
"Transport error on POST %s (attempt %d/%d)",
|
|
133
|
+
path,
|
|
134
|
+
attempt + 1,
|
|
135
|
+
self.max_retries,
|
|
136
|
+
)
|
|
137
|
+
if attempt == self.max_retries - 1:
|
|
138
|
+
raise
|
|
139
|
+
await asyncio.sleep(0.5 * (attempt + 1))
|
|
140
|
+
raise RuntimeError(f"Failed after {self.max_retries} retries")
|
|
141
|
+
|
|
142
|
+
async def _get(self, path: str, params: dict | None = None) -> dict:
|
|
143
|
+
assert self._client, "Use `async with CodeFixClient(...) as env:`"
|
|
144
|
+
r = await self._client.get(path, params=params)
|
|
145
|
+
r.raise_for_status()
|
|
146
|
+
return r.json()
|
|
147
|
+
|
|
148
|
+
# ── Sync wrapper ──────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
def sync(self) -> SyncCodeFixClient:
|
|
151
|
+
"""Return a synchronous wrapper around this async client."""
|
|
152
|
+
return SyncCodeFixClient(self)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SyncCodeFixClient:
|
|
156
|
+
"""
|
|
157
|
+
Synchronous wrapper around CodeFixClient.
|
|
158
|
+
Runs the async client in a dedicated event loop.
|
|
159
|
+
|
|
160
|
+
Usage::
|
|
161
|
+
|
|
162
|
+
with CodeFixClient("http://localhost:8000").sync() as env:
|
|
163
|
+
obs = env.reset()
|
|
164
|
+
result = env.step(action)
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, async_client: CodeFixClient):
|
|
168
|
+
self._async = async_client
|
|
169
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
170
|
+
|
|
171
|
+
def __enter__(self) -> SyncCodeFixClient:
|
|
172
|
+
self._loop = asyncio.new_event_loop()
|
|
173
|
+
self._loop.run_until_complete(self._async.__aenter__())
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def __exit__(self, *args) -> None:
|
|
177
|
+
if self._loop:
|
|
178
|
+
self._loop.run_until_complete(self._async.__aexit__(*args))
|
|
179
|
+
self._loop.close()
|
|
180
|
+
|
|
181
|
+
def _run(self, coro):
|
|
182
|
+
return self._loop.run_until_complete(coro)
|
|
183
|
+
|
|
184
|
+
def reset(self, **kwargs) -> CodeFixObservation:
|
|
185
|
+
return self._run(self._async.reset(**kwargs))
|
|
186
|
+
|
|
187
|
+
def step(self, action: CodeFixAction) -> StepResult:
|
|
188
|
+
return self._run(self._async.step(action))
|
|
189
|
+
|
|
190
|
+
def state(self) -> CodeFixState:
|
|
191
|
+
return self._run(self._async.state())
|
|
192
|
+
|
|
193
|
+
def list_tasks(self, **kwargs) -> list[dict]:
|
|
194
|
+
return self._run(self._async.list_tasks(**kwargs))
|