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.
@@ -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))