Buckets:
| # Packaging & Deploying | |
| This guide walks you through creating a custom environment using the `OpenEnv` framework and the `openenv` CLI. | |
| The CLI handles scaffolding, builds, validation, and deployment so you can stay focused on environment logic. | |
| > [!NOTE] | |
| > **New to OpenEnv?** If you're just getting started, we recommend completing the [Getting Started tutorials](../tutorials/index) first. They provide a conceptual introduction to OpenEnv and reinforcement learning fundamentals. This guide is for developers ready to build production-quality environments. | |
| ## Quick Reference Card | |
| Already familiar with OpenEnv? Here's the 8-step process at a glance: | |
| | Step | Command / Action | Description | | |
| |------|------------------|-------------| | |
| | 1 | `openenv init my_env` | Scaffold new environment | | |
| | 2 | Edit `models.py` | Define Action & Observation dataclasses | | |
| | 3 | Edit `server/my_env_environment.py` | Implement `reset()` and `step()` methods | | |
| | 4 | Edit `client.py` | Implement `_step_payload()`, `_parse_result()`, `_parse_state()` | | |
| | 5 | `uv run --project . server` | Start local dev server for testing | | |
| | 6 | `openenv validate --level static --skip-build` | Validate the declared manifest contract | | |
| | 7 | `openenv push` | Deploy to Hugging Face Hub | | |
| | 8 | Share it! | Others use via `MyEnv.from_env("you/my-env").sync()` | | |
| ### CLI Quick Reference | |
| | Command | Description | | |
| |---------|-------------| | |
| | `openenv init NAME` | Scaffold new environment | | |
| | `openenv import SOURCE --name NAME --output-dir DIR` | Wrap a supported third-party environment source tree | | |
| | `uv run --project . server` | Start local dev server | | |
| | `openenv build` | Build Docker image | | |
| | `openenv validate --level static --skip-build` | Validate the declared manifest contract | | |
| | `openenv push` | Deploy to Hugging Face Hub | | |
| | `openenv push --repo-id NAME` | Deploy to specific repo | | |
| | `openenv push --private` | Deploy as private environment | | |
| | `openenv push --registry ghcr.io/ORG` | Push to GitHub Container Registry | | |
| > [!TIP] | |
| > For a hands-on tutorial that builds a complete environment step-by-step, see [Building Environments](../tutorials/index) in the Getting Started series. | |
| --- | |
| ## Overview | |
| A typical workflow looks like: | |
| 1. Scaffold a new environment with `openenv init`. | |
| 2. Customize your models, environment logic, and FastAPI server. | |
| 3. Implement a typed `EnvClient` (WebSocket-based for persistent sessions). | |
| 4. Configure dependencies and the Dockerfile once. | |
| 5. Use the CLI (`openenv build`, `openenv validate`, `openenv push`) to package and share your work. | |
| > [!NOTE] | |
| > These integrations are handled automatically by the `openenv` CLI when you run `openenv init`. | |
| ### Prerequisites | |
| - Python 3.11+ and [`uv`](https://github.com/astral-sh/uv) for dependency locking | |
| - Docker Desktop / Docker Engine | |
| - The OpenEnv library installed: `pip install openenv` | |
| ## Step-by-Step Guide | |
| Let's walk through the process of building a custom environment with OpenEnv. | |
| ### 1. Scaffold with `openenv init` | |
| ```bash | |
| # Run from anywhere – defaults to current directory | |
| openenv init my_env | |
| # Optionally choose an output directory | |
| openenv init my_env --output-dir /Users/you/envs | |
| ``` | |
| The command creates a fully-typed template with `openenv.yaml`, `pyproject.toml`, `uv.lock`, Docker assets, and stub implementations. If you're working inside this repo, move the generated folder under `envs/`. | |
| Typical layout: | |
| ``` | |
| my_env/ | |
| ├── __init__.py | |
| ├── README.md | |
| ├── client.py | |
| ├── models.py | |
| ├── openenv.yaml | |
| ├── pyproject.toml | |
| ├── uv.lock | |
| └── server/ | |
| ├── __init__.py | |
| ├── app.py | |
| ├── my_env_environment.py | |
| ├── requirements.txt | |
| └── Dockerfile | |
| ``` | |
| Python classes are generated for the action, observation, environment, and client. For example, you will find `MyEnvironment`, `MyAction`, `MyObservation`, and `MyEnv` (client) in the `my_env` directory based on the name you provided. The environment uses the core `State` class from `openenv.core.env_server.types`. | |
| If you already have an ORS/OpenReward or Prime Intellect Verifiers environment | |
| source tree, use `openenv import SOURCE --name my_env --output-dir /Users/you/envs` | |
| instead. The importer detects the source type from the code, vendors the source | |
| under the generated package, and emits an OpenEnv wrapper with task/split and | |
| MCP-style tool actions. Non-secret data files in the source tree are included as | |
| package data, and portable dependencies declared in source `pyproject.toml` or | |
| `requirements.txt` files are added to the generated environment. | |
| ### 2. Define Models | |
| Edit `models.py` to describe your action and observation using Pydantic: | |
| ```python | |
| # models.py | |
| from pydantic import Field | |
| from openenv.core.env_server.types import Action, Observation | |
| class MyAction(Action): | |
| """Your custom action.""" | |
| command: str = Field(..., description="Command to execute") | |
| parameters: dict = Field(default_factory=dict, description="Command parameters") | |
| class MyObservation(Observation): | |
| """Your custom observation.""" | |
| result: str = Field(..., description="Result of the action") | |
| success: bool = Field(..., description="Whether the action succeeded") | |
| ``` | |
| ### 3. Implement Environment Logic | |
| Customize `server/my_env_environment.py` by extending `Environment`: | |
| ```python | |
| # server/my_env_environment.py | |
| from uuid import uuid4 | |
| from openenv.core.env_server.interfaces import Environment | |
| from openenv.core.env_server.types import State | |
| from models import MyAction, MyObservation | |
| class MyEnvironment(Environment): | |
| def __init__(self): | |
| self._state = State(episode_id=str(uuid4()), step_count=0) | |
| def reset(self) -> MyObservation: | |
| self._state = State(episode_id=str(uuid4()), step_count=0) | |
| return MyObservation(result="Ready", success=True, done=False, reward=0.0) | |
| def step(self, action: MyAction) -> MyObservation: | |
| # Implement your logic here | |
| self._state.step_count += 1 | |
| result = self._execute_command(action.command) | |
| return MyObservation(result=result, success=True, done=False, reward=1.0) | |
| @property | |
| def state(self) -> State: | |
| return self._state | |
| ``` | |
| ### 4. Create the FastAPI Server | |
| `server/app.py` should expose the environment through `create_app`. | |
| **Important:** You must pass a class or factory function (not an instance) to enable WebSocket-based concurrent sessions: | |
| ```python | |
| # server/app.py | |
| from openenv.core.env_server import create_app | |
| from ..models import MyAction, MyObservation | |
| from .my_env_environment import MyEnvironment | |
| # Pass the class (factory) - each WebSocket session gets its own instance | |
| app = create_app(MyEnvironment, MyAction, MyObservation, env_name="my_env") | |
| ``` | |
| For environments with constructor arguments, create a factory function: | |
| ```python | |
| # server/app.py | |
| import os | |
| from openenv.core.env_server import create_app | |
| from ..models import MyAction, MyObservation | |
| from .my_env_environment import MyEnvironment | |
| # Read config from environment variables | |
| api_key = os.getenv("MY_API_KEY") | |
| timeout = int(os.getenv("MY_TIMEOUT", "30")) | |
| def create_my_environment(): | |
| """Factory function that creates MyEnvironment with config.""" | |
| return MyEnvironment(api_key=api_key, timeout=timeout) | |
| # Pass the factory function | |
| app = create_app(create_my_environment, MyAction, MyObservation, env_name="my_env") | |
| ``` | |
| ### 5. Implement the Client | |
| `client.py` extends `EnvClient` so users can interact with your server via WebSocket for persistent sessions: | |
| ```python | |
| # client.py | |
| from openenv.core.env_client import EnvClient | |
| from openenv.core.client_types import StepResult | |
| from openenv.core.env_server.types import State | |
| from .models import MyAction, MyObservation | |
| class MyEnv(EnvClient[MyAction, MyObservation, State]): | |
| def _step_payload(self, action: MyAction) -> dict: | |
| return {"command": action.command, "parameters": action.parameters} | |
| def _parse_result(self, payload: dict) -> StepResult[MyObservation]: | |
| obs_data = payload.get("observation", {}) | |
| obs = MyObservation( | |
| result=obs_data.get("result", ""), | |
| success=obs_data.get("success", False), | |
| done=payload.get("done", False), | |
| reward=payload.get("reward"), | |
| ) | |
| return StepResult( | |
| observation=obs, | |
| reward=payload.get("reward"), | |
| done=payload.get("done", False), | |
| ) | |
| def _parse_state(self, payload: dict) -> State: | |
| return State( | |
| episode_id=payload.get("episode_id"), | |
| step_count=payload.get("step_count", 0), | |
| ) | |
| ``` | |
| The `EnvClient` maintains a persistent WebSocket connection to the server, enabling efficient multi-step interactions with lower latency compared to HTTP. Each client instance gets its own dedicated environment session on the server. | |
| ### 6. Configure Dependencies & Dockerfile | |
| The CLI template ships with `pyproject.toml` and `server/Dockerfile`. You should manage your python dependencies with `uv` or `pip` in the `pyproject.toml` file. Other dependencies should be installed in the Dockerfile. | |
| Keep building from the `openenv-base` image so shared tooling stays available: | |
| The template's [`server/Dockerfile`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/cli/templates/openenv_env/server/Dockerfile) is a multi-stage build on `ghcr.io/huggingface/openenv-base` that installs your dependencies with `uv sync` and serves `server.app:app` on port 8000. `openenv build` works out whether you're building in the repo or standalone and sets its build arguments. | |
| If you introduced extra dependencies in the Dockerfile, you should install them in the Dockerfile before removing temp files. | |
| ### 7. Build & Validate with the CLI | |
| From the environment directory: | |
| ```bash | |
| cd envs/my_env | |
| openenv build # Builds Docker image (auto-detects context) | |
| openenv validate --level static --skip-build | |
| ``` | |
| `openenv build` understands both standalone environments and in-repo ones. Useful flags: | |
| - `--tag/-t`: override the default `openenv-<env_name>` tag | |
| - `--build-arg KEY=VALUE`: pass multiple Docker build arguments | |
| - `--dockerfile` / `--context`: custom locations when experimenting | |
| - `--no-cache`: force fresh dependency installs | |
| `openenv validate` reads the `validation:` contract in `openenv.yaml`, checks | |
| the normalized manifest against the selected severity policy, and exits | |
| non-zero when a required check fails. The current walking skeleton runs the | |
| static manifest check; the report's `levels_run` field records exactly which | |
| levels executed. Use `openenv validate --url http://localhost:8000` separately | |
| to validate a running endpoint. | |
| ### 8. Push & Share with `openenv push` | |
| Once validation passes, the CLI can deploy directly to Hugging Face Spaces or any registry: | |
| ```bash | |
| # Push to HF Spaces (auto enables web UI and prompts for login if needed) | |
| openenv push | |
| # Push to a specific repo or namespace | |
| openenv push --repo-id my-org/my-env | |
| # Push to Docker/ghcr (interface disabled by default) | |
| openenv push --registry ghcr.io/my-org | |
| # Customize image base or visibility | |
| openenv push --base-image ghcr.io/huggingface/openenv-base:latest --private | |
| # Configure Space variables and secrets at push time | |
| openenv push -e OPENSPIEL_GAME=tic_tac_toe --secret OPENAI_API_KEY=sk-... | |
| ``` | |
| Key options: | |
| - `DIRECTORY` (positional): path to the environment (defaults to the current directory) | |
| - `--repo-id`: explicit Hugging Face space name | |
| - `--registry`: push to Docker Hub, GHCR, etc. | |
| - `--interface/--no-interface`: toggle the optional web UI | |
| - `--base-image`: override the Dockerfile `FROM` | |
| - `--private`: mark the space as private | |
| - `--env-var/-e KEY=VALUE`: set a public Space variable (repeatable); overrides matching keys from `variables:` in `openenv.yaml` | |
| - `--secret KEY=VALUE`: set a private Space secret (repeatable); value is never logged | |
| - `--hardware/-H`, `--count/-n`, `--create-pr`, `--exclude`: Space hardware, number of Space instances, open a PR instead of pushing, and an ignore file with globs to leave out of the upload | |
| The command validates your `openenv.yaml`, injects Hugging Face frontmatter when needed, and uploads the prepared bundle. | |
| Space variables and secrets are only applied on direct Hugging Face Space pushes; | |
| they are not available for `--registry`, and they cannot be staged through | |
| `--create-pr`. | |
| #### Declaring public variables in `openenv.yaml` | |
| For defaults that belong with the environment (game name, benchmark, max | |
| steps…), add a `variables:` block to `openenv.yaml`; `openenv push` will apply | |
| them to the Space automatically: | |
| ```yaml | |
| variables: | |
| OPENSPIEL_GAME: catch | |
| ``` | |
| CLI `-e` overrides matching keys. Put secrets (API keys, tokens) **only** on | |
| the CLI via `--secret KEY=VALUE` — never in the yaml. | |
| ### 9. Automate Builds (optional) | |
| To trigger Docker builds on every push to `main`, add your environment to the matrix in `.github/workflows/docker-build.yml`: | |
| ```yaml | |
| strategy: | |
| matrix: | |
| image: | |
| - name: echo-env | |
| dockerfile: envs/echo_env/server/Dockerfile | |
| - name: chat-env | |
| dockerfile: envs/chat_env/server/Dockerfile | |
| - name: coding-env | |
| dockerfile: envs/coding_env/server/Dockerfile | |
| - name: my-env # Add your environment here | |
| dockerfile: envs/my_env/server/Dockerfile | |
| ``` | |
| ### Use Your Environment | |
| Here is a simple example of using your environment: | |
| ```python | |
| from my_env import MyAction, MyEnv | |
| # Create environment from Docker image (starts a container) | |
| client = MyEnv.from_docker_image("my-env:latest").sync() | |
| # Or, run the image of a Hugging Face Space locally | |
| client = MyEnv.from_env("my-org/my-env").sync() | |
| # Or, connect to an already running server | |
| client = MyEnv(base_url="http://localhost:8000").sync() | |
| # Use context manager for automatic cleanup (recommended) | |
| with client: | |
| # Reset | |
| result = client.reset() | |
| print(result.observation.result) # "Ready" | |
| # Execute actions | |
| result = client.step(MyAction(command="test", parameters={})) | |
| print(result.observation.result) | |
| print(result.observation.success) | |
| # Get state | |
| state = client.state() | |
| print(state.episode_id) | |
| print(state.step_count) | |
| # Or manually manage the connection | |
| try: | |
| client = MyEnv(base_url="http://localhost:8000").sync() | |
| result = client.reset() | |
| result = client.step(MyAction(command="test", parameters={})) | |
| finally: | |
| client.close() | |
| ``` | |
| `from_docker_image()` and `from_env()` do not return a connected client. They | |
| return a lazy bootstrap handle, and nothing starts until you resolve it: chain | |
| `.sync()` for a synchronous client (as above), or `await` the handle from async | |
| code. Using the handle directly in a `with` block raises | |
| `TypeError: '_BootstrapResult' object does not support the context manager protocol`. | |
| The equivalent async usage is: | |
| ```python | |
| import asyncio | |
| from my_env import MyAction, MyEnv | |
| async def main(): | |
| client = await MyEnv.from_docker_image("my-env:latest") | |
| async with client: | |
| result = await client.reset() | |
| result = await client.step(MyAction(command="test", parameters={})) | |
| state = await client.state() | |
| asyncio.run(main()) | |
| ``` | |
| See [Async vs Sync Usage](../guides/async-sync) for when to prefer each style. | |
| ## Nice work! You've now built and used your own OpenEnv environment. | |
| Your next steps are to: | |
| - [Try out the end-to-end tutorial](https://colab.research.google.com/github/huggingface/OpenEnv/blob/main/examples/OpenEnv_Tutorial.ipynb) | |
Xet Storage Details
- Size:
- 15.5 kB
- Xet hash:
- 68908b4f864110cab1f845acd559890cfc41de255d4d1470e68fa1caeabc4299
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.