|
| 1 | +<!-- openenv-source: pi_env --> |
| 2 | +# Pi Environment for OpenEnv |
| 3 | + |
| 4 | +`pi_env` runs the [Pi](https://github.com/badlogic/pi-mono) coding agent |
| 5 | +inside an isolated [Hugging Face sandbox](https://huggingface.co/docs/huggingface_hub/package_reference/sandbox) |
| 6 | +against any OpenAI-compatible LLM endpoint, optionally capturing per-token |
| 7 | +logprobs for GRPO training. |
| 8 | + |
| 9 | +It mirrors `opencode_env`: same two-layer design (an |
| 10 | +in-process harness primitive + a deployable HTTP env), same transparent-proxy |
| 11 | +logprob capture, same uniform `(instruction, setup, verify)` Task shape. The |
| 12 | +agent is Pi instead of OpenCode, and the default sandbox backend is Hugging |
| 13 | +Face instead of E2B. |
| 14 | + |
| 15 | +The env is **task-agnostic** — every rollout is configured at call-time |
| 16 | +with a uniform Task shape: |
| 17 | + |
| 18 | + - **`instruction`** — prompt for the agent |
| 19 | + - **`setup`** — list of bash commands run *before* the agent (pip install, |
| 20 | + git clone, file downloads — anything you need staged in the sandbox) |
| 21 | + - **`verify`** — list of bash commands run *after* the agent (asserts, |
| 22 | + pytest invocations, score-file writes) |
| 23 | + |
| 24 | +Reward = `passed_verify / total_verify` unless any `verify` command writes |
| 25 | +a float to `/root/logs/verifier/reward.txt` (override). |
| 26 | + |
| 27 | +## In-process primitive (no HTTP) |
| 28 | + |
| 29 | +For trainers that drive a sandbox directly without an HTTP boundary — this is |
| 30 | +what loop-owning GRPO training uses. The primitive reuses the sandbox backend + |
| 31 | +proxy from `opencode_env`, so install it alongside `pi_env`: |
| 32 | + |
| 33 | +```bash |
| 34 | +pip install "openenv-opencode-env @ git+https://github.com/huggingface/OpenEnv.git#subdirectory=envs/opencode_env" |
| 35 | +``` |
| 36 | + |
| 37 | +```python |
| 38 | +import os |
| 39 | +from pi_env import PiConfig, PiSessionFactory, PiTask, HFSandboxBackend |
| 40 | + |
| 41 | +factory = PiSessionFactory( |
| 42 | + config=PiConfig( |
| 43 | + base_url="https://api.openai.com/v1", |
| 44 | + api_key=os.environ["OPENAI_API_KEY"], |
| 45 | + model="gpt-4o-mini", |
| 46 | + sandbox_home="/root", # HF sandbox execs as root |
| 47 | + ), |
| 48 | + sandbox_backend=HFSandboxBackend(image="python:3.12"), |
| 49 | + mode="transparent_proxy", # captures per-token logprobs |
| 50 | +) |
| 51 | +session = factory.create(task=PiTask(instruction="...")) |
| 52 | +session.wait_for_completion() |
| 53 | +turns = session.fetch_proxy_trace() # per-turn (tokens, logprobs) |
| 54 | +session.close() |
| 55 | +``` |
| 56 | + |
| 57 | +Pi is pointed at the endpoint via a `models.json` provider block written under |
| 58 | +`PI_CODING_AGENT_DIR` (`api: openai-completions`), then launched headless with |
| 59 | +`pi --print --no-session --mode json`. In `transparent_proxy` mode the |
| 60 | +in-sandbox proxy fronts `base_url`, injects `logprobs=true`, and writes each |
| 61 | +turn's `(messages, completion_token_ids, per_token_logps)` to |
| 62 | +`proxy_trace.jsonl`. |
| 63 | + |
| 64 | +### Sandbox backend |
| 65 | + |
| 66 | +`HFSandboxBackend` (from `opencode_env.sandbox`, shared with `opencode_env`) |
| 67 | +runs the agent in a Hugging Face sandbox. `image="python:3.12"` cold-installs |
| 68 | +Node 22 (bootstrapped) + the Pi CLI (`npm install -g @mariozechner/pi-coding-agent`) |
| 69 | ++ the proxy's Python deps on every rollout. For faster rollouts use the |
| 70 | +pre-baked image (Node + Pi + proxy deps already installed), built by CI from |
| 71 | +`hf_image/Dockerfile`: |
| 72 | + |
| 73 | +```python |
| 74 | +sandbox_backend=HFSandboxBackend(image="ghcr.io/huggingface/openenv-pi-sandbox:latest") |
| 75 | +``` |
| 76 | + |
| 77 | +Any backend satisfying the `SandboxBackend` / `SandboxHandle` / `BgJob` |
| 78 | +protocols in `opencode_env.sandbox.base` can be plugged in the same way. |
| 79 | + |
| 80 | +> The sandbox backend and interception proxy live in `opencode_env` for now; |
| 81 | +> the plan is to consolidate both into `openenv.core` so `pi_env` and |
| 82 | +> `opencode_env` share them without a cross-package import. |
| 83 | +
|
| 84 | +## Deployed env (HTTP) |
| 85 | + |
| 86 | +The deployed Space exposes: |
| 87 | + |
| 88 | +- **Web UI** at `/web` — pick endpoint, write task, hit Run, watch live phase |
| 89 | + log + reward + logprobs. |
| 90 | +- **MCP tool API** at `/mcp` — programmatic `run_rollout` calls. |
| 91 | +- **OpenAPI docs** at `/docs`, **health** at `/health`. |
| 92 | + |
| 93 | +```python |
| 94 | +import os |
| 95 | +from pi_env import PiEnv |
| 96 | + |
| 97 | +with PiEnv(base_url="https://<user>-pi-env.hf.space") as env: |
| 98 | + env.reset() |
| 99 | + result = env.run_rollout( |
| 100 | + endpoint="openai", # vllm | openai | hf_router |
| 101 | + api_key=os.environ["OPENAI_API_KEY"], # or set as a Space secret |
| 102 | + instruction=( |
| 103 | + "Create binary_search.py exposing def binary_search(arr, target) -> int " |
| 104 | + "that returns the index of target in arr, or -1 if absent." |
| 105 | + ), |
| 106 | + setup=[], |
| 107 | + verify=[ |
| 108 | + "test -f /root/workdir/binary_search.py", |
| 109 | + "python -c \"import sys; sys.path.insert(0, '/root/workdir'); " |
| 110 | + "import binary_search; " |
| 111 | + "assert binary_search.binary_search([1,2,3], 2) == 1; print('OK')\"", |
| 112 | + ], |
| 113 | + task_id="binary_search_v1", |
| 114 | + ) |
| 115 | + print("reward:", result.reward) |
| 116 | + print("turns:", len(result.proxy_turns)) |
| 117 | +``` |
| 118 | + |
| 119 | +## The MCP Tool: `run_rollout` |
| 120 | + |
| 121 | +Single tool, two ways to specify the LLM endpoint: |
| 122 | + |
| 123 | +**Option A — endpoint shorthand (recommended)**: pass `endpoint="vllm"` (or |
| 124 | +`"openai"` / `"hf_router"`). The server resolves `base_url`, `api_key`, and |
| 125 | +`model` from env vars + catalog defaults. Any explicit field overrides. |
| 126 | + |
| 127 | +**Option B — fully explicit**: pass `base_url` + `api_key` + `model` directly. |
| 128 | + |
| 129 | +| Arg | Type | Default | Notes | |
| 130 | +|---|---|---|---| |
| 131 | +| `endpoint` | `str` | `""` | One of `"vllm"` / `"openai"` / `"hf_router"`. | |
| 132 | +| `base_url` / `api_key` / `model` | `str` | `""` | Override / supply explicitly. | |
| 133 | +| `instruction` | `str` | required | Prompt passed to `pi`. | |
| 134 | +| `setup` | `list[str]` | `[]` | Bash commands run **before** the agent. | |
| 135 | +| `verify` | `list[str]` | `[]` | Bash commands run **after** the agent. | |
| 136 | +| `task_id` | `str` | `""` | Echoed back in result. | |
| 137 | +| `mode` | `str` | `"transparent_proxy"` | Or `"black_box"` (no logprobs). | |
| 138 | +| `disable_thinking` | `bool \| None` | `None` (catalog default) | Inject `chat_template_kwargs.enable_thinking=false`. | |
| 139 | +| `max_tokens_cap` | `int` | `4096` | Per-turn `max_tokens` clamp. | |
| 140 | +| `top_logprobs` | `int` | `5` | HF Router cap is 5; OpenAI 0–20; vLLM unbounded. | |
| 141 | +| `agent_timeout_s` | `float` | `600.0` | Hard wall budget for one `pi` run. | |
| 142 | +| `image` | `str` | `""` | HF sandbox image; blank → `python:3.12` (cold-installs Node + Pi). | |
| 143 | + |
| 144 | +Returns `RolloutResult` JSON with: `reward`, `setup_results[]`, |
| 145 | +`verify_results[]`, `proxy_turns[]`, `files{}`, `agent_log_tail`, |
| 146 | +`proxy_log_tail`, `wall_s`, `agent_exit_code`, `sandbox_id`, `error`. |
| 147 | + |
| 148 | +## Two Operating Modes |
| 149 | + |
| 150 | +| Mode | What it does | Best for | |
| 151 | +|---|---|---| |
| 152 | +| **`transparent_proxy`** (default) | In-sandbox proxy at `localhost:7000` forwards Pi's LLM calls to `base_url`, injects `logprobs=true`, captures per-turn `(messages, completion_tokens, logprobs)` to `proxy_trace.jsonl`. | GRPO / RL training, observability, top-k distillation. | |
| 153 | +| **`black_box`** | No proxy. Pi talks straight to `base_url`. | Smoke tests, eval, SFT data collection. | |
| 154 | + |
| 155 | +## Building the Docker Image |
| 156 | + |
| 157 | +```bash |
| 158 | +cd envs/pi_env |
| 159 | + |
| 160 | +openenv validate # check pyproject.toml + openenv.yaml + server/app.py + uv.lock |
| 161 | +openenv build -t pi-env # builds the image (uses server/Dockerfile) |
| 162 | + |
| 163 | +# run locally with an HF token (Sandbox + Jobs access) |
| 164 | +docker run -p 8000:8000 -e HF_TOKEN=hf_... pi-env |
| 165 | +``` |
| 166 | + |
| 167 | +Or build directly: |
| 168 | + |
| 169 | +```bash |
| 170 | +docker build -t pi-env -f envs/pi_env/server/Dockerfile envs/pi_env |
| 171 | +``` |
| 172 | + |
| 173 | +## Environment Variables |
| 174 | + |
| 175 | +| Variable | Required | Purpose | |
| 176 | +|---|---|---| |
| 177 | +| `HF_TOKEN` | **yes** for any rollout | Hugging Face sandbox credentials. | |
| 178 | +| `MAX_CONCURRENT_ENVS` | no | Env-instance pool size. Default `4`. | |
| 179 | +| `ENABLE_WEB_INTERFACE` | no | Set `false` to disable the `/web` Gradio mount. Default `true`. | |
| 180 | +| `VLLM_URL` / `VLLM_API_KEY` / `VLLM_MODEL` | for `endpoint="vllm"` | OAI-compatible base URL (key defaults to `intercepted`). | |
| 181 | +| `OPENAI_API_KEY` / `OPENAI_BASE_URL` / `OPENAI_MODEL` | for `endpoint="openai"` | Standard OpenAI. | |
| 182 | +| `HF_ROUTER_API_KEY` / `HF_ROUTER_BASE_URL` / `HF_ROUTER_MODEL` | for `endpoint="hf_router"` | HF Router. | |
| 183 | + |
| 184 | +Pick `provider:` suffixes that actually return logprobs: |
| 185 | +**Together / Nscale / Scaleway / SambaNova / Cerebras**. Avoid Novita / |
| 186 | +Hyperbolic / Featherless (silent drop) and Groq (HTTP 400). |
| 187 | + |
| 188 | +## Project Structure |
| 189 | + |
| 190 | +``` |
| 191 | +pi_env/ |
| 192 | +├── README.md # this file |
| 193 | +├── openenv.yaml # OpenEnv space spec |
| 194 | +├── pyproject.toml # deps + ``server`` entrypoint |
| 195 | +├── __init__.py # re-exports primitive + client + models |
| 196 | +│ |
| 197 | +├── client.py # PiEnv(MCPToolClient) |
| 198 | +├── models.py # RolloutResult / RolloutTurn / PiState |
| 199 | +│ |
| 200 | +├── config.py # PiConfig (primitive) |
| 201 | +├── harness.py # PiSession / PiSessionFactory (CLI-only) |
| 202 | +├── pi_runtime.py # models.json builder + install/run cmds |
| 203 | +├── task.py # PiTask |
| 204 | +│ |
| 205 | +└── server/ |
| 206 | + ├── __init__.py |
| 207 | + ├── app.py # FastAPI factory; mounts Gradio at /web |
| 208 | + ├── pi_environment.py # MCPEnvironment with single ``run_rollout`` tool |
| 209 | + ├── gradio_ui.py # the /web Gradio Blocks UI |
| 210 | + ├── catalog.py # endpoint shorthand resolver |
| 211 | + └── Dockerfile # multi-stage uv build (used by ``openenv build``) |
| 212 | +``` |
| 213 | + |
| 214 | +The sandbox backend + interception proxy are imported from |
| 215 | +`opencode_env.sandbox`; `pi_env` ships no `sandbox/` of its own. |
0 commit comments