diff --git a/src/core/base.py b/src/core/base.py index b980802862..c9f684ebdb 100644 --- a/src/core/base.py +++ b/src/core/base.py @@ -43,3 +43,7 @@ def step(self, action: ActT) -> StepResult[ObsT]: StepResult[ObsT]: The resulting observation, reward, done flag, and info. """ raise NotImplementedError + + def close(self) -> None: + """Release resources (containers, sessions, etc.).""" + pass diff --git a/src/core/base_env_client.py b/src/core/base_env_client.py new file mode 100644 index 0000000000..f2cd6b0b25 --- /dev/null +++ b/src/core/base_env_client.py @@ -0,0 +1,82 @@ +""" +core/runner_env.py +Minimal HTTP-based environment client. +- Talks to a single env worker exposing: POST /reset, POST /step + +Future hooks (commented below) for: +- episode_id, seed on reset +- request_id on step +- custom headers (auth/trace) +""" + +from __future__ import annotations + +from abc import abstractmethod +from typing import Any, Dict, Generic, Optional, TypeVar + +import requests + +from .base import BaseEnv +from .types import StepResult + +ActT = TypeVar("ActT") +ObsT = TypeVar("ObsT") + + +class HTTPEnvClient(BaseEnv[ActT, ObsT], Generic[ActT, ObsT]): + def __init__( + self, + base_url: str, + request_timeout_s: float = 15.0, + default_headers: Optional[Dict[str, str]] = None, + ): + self._base = base_url.rstrip("/") + self._timeout = float(request_timeout_s) + self._http = requests.Session() + self._headers = default_headers or {} + + @abstractmethod + def _step_payload(self, action: ActT) -> dict: + """Convert an Action object to the JSON body expected by the env server.""" + raise NotImplementedError + + @abstractmethod + def _parse_result(self, payload: dict) -> StepResult[ObsT]: + """Convert a JSON response from the env server to StepResult[ObsT].""" + raise NotImplementedError + + # ---------- BaseEnv ---------- + def reset(self) -> ObsT: + body: Dict[str, Any] = {} + # TODO: later: + # body["seed"] = seed + # body["episode_id"] = episode_id + r = self._http.post( + f"{self._base}/reset", + json=body, + headers=self._headers, + timeout=self._timeout, + ) + r.raise_for_status() + return self._parse_result(r.json()).observation + + def step(self, action: ActT) -> StepResult[ObsT]: + body: Dict[str, Any] = { + "action": self._step_payload(action), + "timeout_s": int(self._timeout), + } + # TODO: later: + # body["request_id"] = str(uuid.uuid4()) + # body["episode_id"] = current_episode_id + r = self._http.post( + f"{self._base}/step", + json=body, + headers=self._headers, + timeout=self._timeout, + ) + r.raise_for_status() + return self._parse_result(r.json()) + + def close(self) -> None: + # nothing to close; higher-level libraries own lifecycles of the endpoints + pass diff --git a/src/envs/coding_env/env.py b/src/envs/coding_env/env.py index c8feb31367..a312e5de12 100644 --- a/src/envs/coding_env/env.py +++ b/src/envs/coding_env/env.py @@ -1,72 +1,50 @@ """ -envs/coding_env/env.py --------------------------------- -Concrete environment implementation using the core BaseEnv. -POC implementation runs code locally via subprocess that can be changed later. +CodingEnv +--------- +Client-side wrapper for the Coding environment server. +Talks HTTP to a single base_url exposing: /reset and /step. + +- users instantiate CodingEnv with a base_url provided by the higher-level + vector/orchestration layer. +- Environment authors ship the Docker image that serves the HTTP API. + +(Seeds, episode IDs, request IDs, capabilities can be added later in the payloads.) """ from __future__ import annotations -import subprocess from typing import Optional -from core.base import BaseEnv +from core.base_env_client import HTTPEnvClient from core.types import StepResult from .models import CodeAction, CodeObservation -class CodingEnv(BaseEnv[CodeAction, CodeObservation]): - """ - Minimal Coding Environment. - - POC behavior: - - reset(): returns a fresh, empty observation (no persistent state). - - step(action): runs Python code with `python -c` and returns stdout/stderr/exit_code. - - Future swap: - Replace _run_code_locally() with a call to your Docker/gateway backend without - changing the public API. - """ - +class CodingEnv(HTTPEnvClient[CodeAction, CodeObservation]): def __init__( self, - default_timeout_s: float = 10.0, - python_executable: str = "python", + base_url: str, + request_timeout_s: float = 15.0, ): - """ - Args: - default_timeout_s: Max seconds to allow code execution before timing out. - python_executable: Interpreter to run (e.g., "python3", a venv path, etc.). - """ - self._default_timeout_s = float(default_timeout_s) - self._python = python_executable - - # --- BaseEnv interface --- - - def reset(self) -> CodeObservation: - # No state to clear in this POC; return an initial observation. - return CodeObservation(stdout="", stderr="", exit_code=0) - - def step(self, action: CodeAction) -> StepResult[CodeObservation]: - if not isinstance(action, CodeAction): - raise TypeError(f"Expected CodeAction, got {type(action)!r}") - - # TODO: replace dummy response with the call to the code executor inside the container - obs, timed_out = CodeObservation(stderr="", stdout="", exit_code=0), False - - # Simple reward heuristic: success and no stderr -> 1.0 else 0.0 - reward: Optional[float] = ( - 1.0 if (obs.exit_code == 0 and not obs.stderr) else 0.0 + super().__init__( + base_url=base_url, + request_timeout_s=request_timeout_s, ) - info = { - "timed_out": timed_out, - "interpreter": self._python, + # --- HTTPEnvClient abstract hooks --- + + def _step_payload(self, action: CodeAction) -> dict: + # Shape expected by the server's /step endpoint under "action" + return { + "code": action.code, } + def _parse_result(self, payload: dict) -> StepResult[CodeObservation]: + # Expecting: { "observation": {...}, "reward": , "done": , "info": {...} } + obs = CodeObservation(**payload["observation"]) return StepResult( observation=obs, - reward=reward, - done=False, # Coding env is not episodic by default + reward=payload.get("reward"), + done=bool(payload.get("done", False)), ) diff --git a/src/envs/coding_env/example_usage.py b/src/envs/coding_env/example_usage.py new file mode 100644 index 0000000000..664682b7ff --- /dev/null +++ b/src/envs/coding_env/example_usage.py @@ -0,0 +1,7 @@ +from envs.coding_env.env import CodingEnv +from envs.coding_env.models import CodeAction + +env = CodingEnv(base_url="http://localhost:8080") +obs0 = env.reset() +result = env.step(CodeAction(code="print('hi')")) +print(result.observation.stdout.strip(), result.reward)