Buckets:

|
download
raw
9.41 kB
# Pi Environment for OpenEnv
`pi_env` runs the [Pi](https://github.com/badlogic/pi-mono) coding agent
inside an isolated [Hugging Face sandbox](https://huggingface.co/docs/huggingface_hub/package_reference/sandbox)
against any OpenAI-compatible LLM endpoint, optionally capturing per-token
logprobs for GRPO training.
It mirrors `opencode_env`: same two-layer design (an
in-process harness primitive + a deployable HTTP env), same transparent-proxy
logprob capture, same uniform `(instruction, setup, verify)` Task shape. The
agent is Pi instead of OpenCode, and the default sandbox backend is Hugging
Face instead of E2B.
The env is **task-agnostic** — every rollout is configured at call-time
with a uniform Task shape:
- **`instruction`** — prompt for the agent
- **`setup`** — list of bash commands run *before* the agent (pip install,
git clone, file downloads — anything you need staged in the sandbox)
- **`verify`** — list of bash commands run *after* the agent (asserts,
pytest invocations, score-file writes)
Reward = `passed_verify / total_verify` unless any `verify` command writes
a float to `/root/logs/verifier/reward.txt` (override).
## In-process primitive (no HTTP)
For trainers that drive a sandbox directly without an HTTP boundary — this is
what loop-owning GRPO training uses. The primitive reuses the sandbox backend +
proxy from `opencode_env`, so install it alongside `pi_env`:
```bash
pip install "openenv-opencode-env @ git+https://github.com/huggingface/OpenEnv.git#subdirectory=envs/opencode_env"
```
```python
import os
from pi_env import PiConfig, PiSessionFactory, PiTask, HFSandboxBackend
factory = PiSessionFactory(
config=PiConfig(
base_url="https://api.openai.com/v1",
api_key=os.environ["OPENAI_API_KEY"],
model="gpt-4o-mini",
sandbox_home="/root", # HF sandbox execs as root
),
sandbox_backend=HFSandboxBackend(image="python:3.12"),
mode="transparent_proxy", # captures per-token logprobs
)
session = factory.create(task=PiTask(instruction="..."))
session.wait_for_completion()
turns = session.fetch_proxy_trace() # per-turn (tokens, logprobs)
session.close()
```
Pi is pointed at the endpoint via a `models.json` provider block written under
`PI_CODING_AGENT_DIR` (`api: openai-completions`), then launched headless with
`pi --print --no-session --mode json`. In `transparent_proxy` mode the
in-sandbox proxy fronts `base_url`, injects `logprobs=true`, and writes each
turn's `(messages, completion_token_ids, per_token_logps)` to
`proxy_trace.jsonl`.
### Sandbox backend
`HFSandboxBackend` (from `opencode_env.sandbox`, shared with `opencode_env`)
runs the agent in a Hugging Face sandbox. `image="python:3.12"` cold-installs
Node 22 (bootstrapped) + the Pi CLI (`npm install -g @mariozechner/pi-coding-agent`)
+ the proxy's Python deps on every rollout. For faster rollouts use the
pre-baked image (Node + Pi + proxy deps already installed), built by CI from
`hf_image/Dockerfile`:
```python
sandbox_backend=HFSandboxBackend(image="ghcr.io/huggingface/openenv-pi-sandbox:latest")
```
Any backend satisfying the `SandboxBackend` / `SandboxHandle` / `BgJob`
protocols in `opencode_env.sandbox.base` can be plugged in the same way.
> The sandbox backend and interception proxy live in `opencode_env` for now;
> the plan is to consolidate both into `openenv.core` so `pi_env` and
> `opencode_env` share them without a cross-package import.
## Deployed env (HTTP)
The deployed Space exposes:
- **Web UI** at `/web` — pick endpoint, write task, hit Run, watch live phase
log + reward + logprobs.
- **MCP tool API** at `/mcp` — programmatic `run_rollout` calls.
- **OpenAPI docs** at `/docs`, **health** at `/health`.
```python
import os
from pi_env import PiEnv
with PiEnv(base_url="https://<user>-pi-env.hf.space") as env:
env.reset()
result = env.run_rollout(
endpoint="openai", # vllm | openai | hf_router
api_key=os.environ["OPENAI_API_KEY"], # or set as a Space secret
instruction=(
"Create binary_search.py exposing def binary_search(arr, target) -> int "
"that returns the index of target in arr, or -1 if absent."
),
setup=[],
verify=[
"test -f /root/workdir/binary_search.py",
"python -c \"import sys; sys.path.insert(0, '/root/workdir'); "
"import binary_search; "
"assert binary_search.binary_search([1,2,3], 2) == 1; print('OK')\"",
],
task_id="binary_search_v1",
)
print("reward:", result.reward)
print("turns:", len(result.proxy_turns))
```
## The MCP Tool: `run_rollout`
Single tool, two ways to specify the LLM endpoint:
**Option A — endpoint shorthand (recommended)**: pass `endpoint="vllm"` (or
`"openai"` / `"hf_router"`). The server resolves `base_url`, `api_key`, and
`model` from env vars + catalog defaults. Any explicit field overrides.
**Option B — fully explicit**: pass `base_url` + `api_key` + `model` directly.
| Arg | Type | Default | Notes |
|---|---|---|---|
| `endpoint` | `str` | `""` | One of `"vllm"` / `"openai"` / `"hf_router"`. |
| `base_url` / `api_key` / `model` | `str` | `""` | Override / supply explicitly. |
| `instruction` | `str` | required | Prompt passed to `pi`. |
| `setup` | `list[str]` | `[]` | Bash commands run **before** the agent. |
| `verify` | `list[str]` | `[]` | Bash commands run **after** the agent. |
| `task_id` | `str` | `""` | Echoed back in result. |
| `mode` | `str` | `"transparent_proxy"` | Or `"black_box"` (no logprobs). |
| `disable_thinking` | `bool \| None` | `None` (catalog default) | Inject `chat_template_kwargs.enable_thinking=false`. |
| `max_tokens_cap` | `int` | `4096` | Per-turn `max_tokens` clamp. |
| `top_logprobs` | `int` | `5` | HF Router cap is 5; OpenAI 0–20; vLLM unbounded. |
| `agent_timeout_s` | `float` | `600.0` | Hard wall budget for one `pi` run. |
| `image` | `str` | `""` | HF sandbox image; blank → `python:3.12` (cold-installs Node + Pi). |
Returns `RolloutResult` JSON with: `reward`, `setup_results[]`,
`verify_results[]`, `proxy_turns[]`, `files{}`, `agent_log_tail`,
`proxy_log_tail`, `wall_s`, `agent_exit_code`, `sandbox_id`, `error`.
## Two Operating Modes
| Mode | What it does | Best for |
|---|---|---|
| **`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. |
| **`black_box`** | No proxy. Pi talks straight to `base_url`. | Smoke tests, eval, SFT data collection. |
## Building the Docker Image
```bash
cd envs/pi_env
openenv validate # check pyproject.toml + openenv.yaml + server/app.py + uv.lock
openenv build -t pi-env # builds the image (uses server/Dockerfile)
# run locally with an HF token (Sandbox + Jobs access)
docker run -p 8000:8000 -e HF_TOKEN=hf_... pi-env
```
Or build directly:
```bash
docker build -t pi-env -f envs/pi_env/server/Dockerfile envs/pi_env
```
## Environment Variables
| Variable | Required | Purpose |
|---|---|---|
| `HF_TOKEN` | **yes** for any rollout | Hugging Face sandbox credentials. |
| `MAX_CONCURRENT_ENVS` | no | Env-instance pool size. Default `4`. |
| `ENABLE_WEB_INTERFACE` | no | Set `false` to disable the `/web` Gradio mount. Default `true`. |
| `VLLM_URL` / `VLLM_API_KEY` / `VLLM_MODEL` | for `endpoint="vllm"` | OAI-compatible base URL (key defaults to `intercepted`). |
| `OPENAI_API_KEY` / `OPENAI_BASE_URL` / `OPENAI_MODEL` | for `endpoint="openai"` | Standard OpenAI. |
| `HF_ROUTER_API_KEY` / `HF_ROUTER_BASE_URL` / `HF_ROUTER_MODEL` | for `endpoint="hf_router"` | HF Router. |
Pick `provider:` suffixes that actually return logprobs:
**Together / Nscale / Scaleway / SambaNova / Cerebras**. Avoid Novita /
Hyperbolic / Featherless (silent drop) and Groq (HTTP 400).
## Project Structure
```
pi_env/
├── README.md # this file
├── openenv.yaml # OpenEnv space spec
├── pyproject.toml # deps + ``server`` entrypoint
├── __init__.py # re-exports primitive + client + models
├── client.py # PiEnv(MCPToolClient)
├── models.py # RolloutResult / RolloutTurn / PiState
├── config.py # PiConfig (primitive)
├── harness.py # PiSession / PiSessionFactory (CLI-only)
├── pi_runtime.py # models.json builder + install/run cmds
├── task.py # PiTask
└── server/
├── __init__.py
├── app.py # FastAPI factory; mounts Gradio at /web
├── pi_environment.py # MCPEnvironment with single ``run_rollout`` tool
├── gradio_ui.py # the /web Gradio Blocks UI
├── catalog.py # endpoint shorthand resolver
└── Dockerfile # multi-stage uv build (used by ``openenv build``)
```
The sandbox backend + interception proxy are imported from
`opencode_env.sandbox`; `pi_env` ships no `sandbox/` of its own.

Xet Storage Details

Size:
9.41 kB
·
Xet hash:
663da63fdb6d3f9698335eb2a3cac011f9c132feb296d3acd646ee9d3cf5dbae

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.