Paulito Palmes, PhD commited on
Commit
dae63d1
·
1 Parent(s): 0ee5d04
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +3 -1
  2. openenv/CODE_OF_CONDUCT.md +0 -80
  3. openenv/CONTRIBUTING.md +0 -39
  4. openenv/LICENSE +0 -28
  5. openenv/README.md +0 -299
  6. openenv/pyproject.toml +0 -57
  7. openenv/scripts/CONVERT.md +0 -491
  8. openenv/scripts/convert_env.sh +0 -502
  9. openenv/scripts/deploy_to_hf.sh +0 -615
  10. openenv/scripts/manage_hf_collection.py +0 -296
  11. openenv/scripts/prepare_hf_deployment.sh +0 -170
  12. openenv/scripts/setup_shared_gitea.sh +0 -83
  13. openenv/src/__init__.py +0 -7
  14. openenv/src/core/README.md +0 -180
  15. openenv/src/core/__init__.py +0 -19
  16. openenv/src/core/client_types.py +0 -22
  17. openenv/src/core/containers/__init__.py +0 -7
  18. openenv/src/core/containers/images/Dockerfile +0 -61
  19. openenv/src/core/containers/images/README.md +0 -92
  20. openenv/src/core/containers/runtime/__init__.py +0 -15
  21. openenv/src/core/containers/runtime/providers.py +0 -293
  22. openenv/src/core/containers/test_local_docker_provider.py +0 -258
  23. openenv/src/core/env_server/__init__.py +0 -35
  24. openenv/src/core/env_server/base_transforms.py +0 -29
  25. openenv/src/core/env_server/http_server.py +0 -257
  26. openenv/src/core/env_server/interfaces.py +0 -118
  27. openenv/src/core/env_server/types.py +0 -57
  28. openenv/src/core/env_server/web_interface.py +0 -1613
  29. openenv/src/core/http_env_client.py +0 -203
  30. openenv/src/core/tools/__init__.py +0 -16
  31. openenv/src/core/tools/git_server_client.py +0 -362
  32. openenv/src/core/tools/local_python_executor.py +0 -152
  33. openenv/src/core/uv.lock +0 -0
  34. openenv/src/envs/README.md +0 -382
  35. openenv/src/envs/atari_env/README.md +0 -396
  36. openenv/src/envs/atari_env/__init__.py +0 -31
  37. openenv/src/envs/atari_env/client.py +0 -119
  38. openenv/src/envs/atari_env/models.py +0 -86
  39. openenv/src/envs/atari_env/server/Dockerfile +0 -43
  40. openenv/src/envs/atari_env/server/__init__.py +0 -15
  41. openenv/src/envs/atari_env/server/app.py +0 -73
  42. openenv/src/envs/atari_env/server/atari_environment.py +0 -245
  43. openenv/src/envs/atari_env/server/requirements.txt +0 -3
  44. openenv/src/envs/atari_env/test_atari_docker.sh +0 -333
  45. openenv/src/envs/browsergym_env/README.md +0 -554
  46. openenv/src/envs/browsergym_env/__init__.py +0 -72
  47. openenv/src/envs/browsergym_env/client.py +0 -123
  48. openenv/src/envs/browsergym_env/models.py +0 -92
  49. openenv/src/envs/browsergym_env/openenv.yaml +0 -5
  50. openenv/src/envs/browsergym_env/pyproject.toml +0 -39
Dockerfile CHANGED
@@ -14,8 +14,10 @@ COPY . .
14
 
15
  # Install Python dependencies
16
  #RUN pip install --no-cache-dir -e .
 
 
 
17
  RUN pip install --no-cache-dir -r ./requirements.txt
18
- RUN pip install --no-cache-dir -e ./openenv
19
 
20
  # Expose port
21
  EXPOSE 8000
 
14
 
15
  # Install Python dependencies
16
  #RUN pip install --no-cache-dir -e .
17
+
18
+ pip install git+https://github.com/meta-pytorch/OpenEnv.git
19
+
20
  RUN pip install --no-cache-dir -r ./requirements.txt
 
21
 
22
  # Expose port
23
  EXPOSE 8000
openenv/CODE_OF_CONDUCT.md DELETED
@@ -1,80 +0,0 @@
1
- # Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to make participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, sex characteristics, gender identity and expression,
9
- level of experience, education, socio-economic status, nationality, personal
10
- appearance, race, religion, or sexual identity and orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies within all project spaces, and it also applies when
49
- an individual is representing the project or its community in public spaces.
50
- Examples of representing a project or community include using an official
51
- project e-mail address, posting via an official social media account, or acting
52
- as an appointed representative at an online or offline event. Representation of
53
- a project may be further defined and clarified by project maintainers.
54
-
55
- This Code of Conduct also applies outside the project spaces when there is a
56
- reasonable belief that an individual's behavior may have a negative impact on
57
- the project or its community.
58
-
59
- ## Enforcement
60
-
61
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
- reported by contacting the project team at <opensource-conduct@meta.com>. All
63
- complaints will be reviewed and investigated and will result in a response that
64
- is deemed necessary and appropriate to the circumstances. The project team is
65
- obligated to maintain confidentiality with regard to the reporter of an incident.
66
- Further details of specific enforcement policies may be posted separately.
67
-
68
- Project maintainers who do not follow or enforce the Code of Conduct in good
69
- faith may face temporary or permanent repercussions as determined by other
70
- members of the project's leadership.
71
-
72
- ## Attribution
73
-
74
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
75
- available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
76
-
77
- [homepage]: https://www.contributor-covenant.org
78
-
79
- For answers to common questions about this code of conduct, see
80
- https://www.contributor-covenant.org/faq
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/CONTRIBUTING.md DELETED
@@ -1,39 +0,0 @@
1
- # Contributing to __________
2
- We want to make contributing to this project as easy and transparent as
3
- possible.
4
-
5
- ## Our Development Process
6
- ... (in particular how this is synced with internal changes to the project)
7
-
8
- ## Pull Requests
9
- We actively welcome your pull requests.
10
-
11
- 1. Fork the repo and create your branch from `main`.
12
- 2. If you've added code that should be tested, add tests.
13
- 3. If you've changed APIs, update the documentation.
14
- 4. Ensure the test suite passes.
15
- 5. Make sure your code lints.
16
- 6. If you haven't already, complete the Contributor License Agreement ("CLA").
17
-
18
- ## Contributor License Agreement ("CLA")
19
- In order to accept your pull request, we need you to submit a CLA. You only need
20
- to do this once to work on any of Meta's open source projects.
21
-
22
- Complete your CLA here: <https://code.facebook.com/cla>
23
-
24
- ## Issues
25
- We use GitHub issues to track public bugs. Please ensure your description is
26
- clear and has sufficient instructions to be able to reproduce the issue.
27
-
28
- Meta has a [bounty program](https://bugbounty.meta.com/) for the safe
29
- disclosure of security bugs. In those cases, please go through the process
30
- outlined on that page and do not file a public issue.
31
-
32
- ## Coding Style
33
- * 2 spaces for indentation rather than tabs
34
- * 80 character line length
35
- * ...
36
-
37
- ## License
38
- By contributing to __________, you agree that your contributions will be licensed
39
- under the LICENSE file in the root directory of this source tree.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/LICENSE DELETED
@@ -1,28 +0,0 @@
1
- BSD 3-Clause License
2
-
3
- (c) Meta Platforms, Inc. and affiliates.
4
-
5
- Redistribution and use in source and binary forms, with or without modification,
6
- are permitted provided that the following conditions are met:
7
-
8
- 1. Redistributions of source code must retain the above copyright notice,this list
9
- of conditions and the following disclaimer.
10
-
11
- 2. Redistributions in binary form must reproduce the above copyright notice, this
12
- list of conditions and the following disclaimer in the documentation
13
- and/or other materials provided with the distribution.
14
-
15
- 3. Neither the name of the copyright holder nor the names of its contributors may
16
- be used to endorse or promote products derived from this software without specific
17
- prior written permission.
18
-
19
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY
20
- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21
- OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
22
- SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23
- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24
- TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
25
- BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
- ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
28
- DAMAGE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/README.md DELETED
@@ -1,299 +0,0 @@
1
- # <img width="35" height="35" alt="image" src="https://github.com/user-attachments/assets/2700a971-e5d6-4036-b03f-2f89c9791609" /> OpenEnv: Agentic Execution Environments
2
-
3
- An e2e framework for creating, deploying and using isolated execution environments for agentic RL training, built using Gymnasium style simple APIs.
4
-
5
- [![PyPI](https://img.shields.io/pypi/v/openenv-core?color=blue)](https://pypi.org/project/openenv-core/)
6
- [![Discord](https://img.shields.io/badge/Discord-OpenEnv-7289da?style=flat&logo=discord&logoColor=white)](https://discord.gg/YsTYBh6PD9)
7
- [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/meta-pytorch/OpenEnv/blob/main/examples/OpenEnv_Tutorial.ipynb)
8
- [![Docs](https://img.shields.io/badge/Docs-Explore-blue?logo=readthedocs&logoColor=white)](https://meta-pytorch.org/OpenEnv/)
9
-
10
- ---
11
-
12
- **🚀 Featured Example:** Train LLMs to play BlackJack using [torchforge](https://github.com/meta-pytorch/torchforge) (PyTorch's agentic RL framework): [`examples/grpo_blackjack/`](examples/grpo_blackjack/)
13
-
14
- ## OpenEnv on partner platforms:
15
-
16
- - [Lightning AI Studio](https://lightning.ai/environments?section=featured)
17
- - [TRL example](https://huggingface.co/docs/trl/main/en/openenv)
18
- - [Unsloth Google Colab](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/OpenEnv_gpt_oss_(20B)_Reinforcement_Learning_2048_Game.ipynb)
19
- - [ART example](https://art.openpipe.ai/integrations/openenv-integration)
20
- - [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%20-%20OpenEnv%20GRPO%20with%20trl.ipynb)
21
-
22
- ## Overview
23
-
24
- OpenEnv provides a standard for interacting with agentic execution environments via simple Gymnasium style APIs - `step()`, `reset()`, `state()`. Users of agentic execution environments can interact with the environment during RL training loops using these simple APIs.
25
-
26
- In addition to making it easier for researchers and RL framework writers, we also provide tools for environment creators making it easier for them to create richer environments and make them available over familiar protocols like HTTP and packaged using canonical technologies like docker. Environment creators can use the OpenEnv framework to create environments that are isolated, secure, and easy to deploy and use.
27
-
28
- The OpenEnv CLI (`openenv`) provides commands to initialize new environments and deploy them to Hugging Face Spaces.
29
-
30
- > ⚠️ **Early Development Warning** OpenEnv is currently in an experimental
31
- > stage. You should expect bugs, incomplete features, and APIs that may change
32
- > in future versions. The project welcomes bugfixes, but to make sure things are
33
- > well coordinated you should discuss any significant change before starting the
34
- > work. It's recommended that you signal your intention to contribute in the
35
- > issue tracker, either by filing a new issue or by claiming an existing one.
36
-
37
- ### RFCs
38
-
39
- Below is a list of active and historical RFCs for OpenEnv. RFCs are proposals for major changes or features. Please review and contribute!
40
-
41
- - [RFC 001: Baseline API and Interface Specifications](https://github.com/meta-pytorch/OpenEnv/pull/26)
42
-
43
- ## Architecture
44
-
45
- ### Component Overview
46
-
47
- ```
48
- ┌─────────────────────────────────────────────────────────┐
49
- │ Client Application │
50
- │ ┌────────────────┐ ┌──────────────────┐ │
51
- │ │ EchoEnv │ │ CodingEnv │ │
52
- │ │ (HTTPEnvClient)│ │ (HTTPEnvClient) │ │
53
- │ └────────┬───────┘ └────────┬─────────┘ │
54
- └───────────┼───────────────────────────────┼─────────────┘
55
- │ HTTP │ HTTP
56
- │ (reset, step, state) │
57
- ┌───────────▼───────────────────────────────▼─────────────┐
58
- │ Docker Containers (Isolated) │
59
- │ ┌──────────────────────┐ ┌──────────────────────┐ │
60
- │ │ FastAPI Server │ │ FastAPI Server │ │
61
- │ │ EchoEnvironment │ │ PythonCodeActEnv │ │
62
- │ │ (Environment base) │ │ (Environment base) │ │
63
- │ └──────────────────────┘ └──────────────────────┘ │
64
- └────────────────────────────────���────────────────────────┘
65
- ```
66
-
67
- ### Core Components
68
-
69
- #### 1. Web Interface
70
-
71
- OpenEnv includes a built-in web interface for interactive environment exploration and debugging. The web interface provides:
72
-
73
- - **Two-Pane Layout**: HumanAgent interaction on the left, state observation on the right
74
- - **Real-time Updates**: WebSocket-based live updates without page refresh
75
- - **Dynamic Forms**: Automatically generated action forms based on environment Action types
76
- - **Action History**: Complete log of all actions taken and their results
77
-
78
- The web interface is **conditionally enabled** based on environment variables:
79
-
80
- - **Local Development**: Disabled by default for lightweight development
81
- - **Manual Override**: Enable with `ENABLE_WEB_INTERFACE=true`
82
-
83
- To use the web interface:
84
-
85
- ```python
86
- from core.env_server import create_web_interface_app
87
- from your_env.models import YourAction, YourObservation
88
- from your_env.server.your_environment import YourEnvironment
89
-
90
- env = YourEnvironment()
91
- app = create_web_interface_app(env, YourAction, YourObservation)
92
- ```
93
-
94
- When enabled, open `http://localhost:8000/web` in your browser to interact with the environment.
95
-
96
- #### 2. Environment (Server-Side)
97
- Base class for implementing environment logic:
98
- - **`reset()`**: Initialize a new episode, returns initial `Observation`
99
- - **`step(action)`**: Execute an `Action`, returns resulting `Observation`
100
- - **`state()`**: Access episode metadata (`State` with episode_id, step_count, etc.)
101
-
102
- #### 3. HTTPEnvClient (Client-Side)
103
- Base class for HTTP communication:
104
- - Handles HTTP requests to environment server
105
- - Contains a utility to spin up a docker container locally for the corresponding environment
106
- - Type-safe action/observation parsing
107
-
108
- #### 4. Container Providers
109
- Manage container deployment:
110
- - `LocalDockerProvider`: Run containers on local Docker daemon
111
- - `KubernetesProvider`: Deploy to K8s clusters (future)
112
-
113
- #### 5. Models
114
- Type-safe data structures:
115
- - `Action`: Base class for environment actions
116
- - `Observation`: Base class for environment observations
117
- - `State`: Episode state tracking
118
- - `StepResult`: Combines observation, reward, done flag
119
-
120
- ## Project Structure
121
-
122
- ### For Environment Creators
123
-
124
- Use the CLI to quickly scaffold a new environment:
125
-
126
- ```bash
127
- openenv init my_env
128
- ```
129
-
130
- This creates the following structure:
131
-
132
- ```
133
- my_env/
134
- ├── .dockerignore # Docker build exclusions
135
- ├── __init__.py # Export YourAction, YourObservation, YourEnv
136
- ├── models.py # Define Action, Observation, State dataclasses
137
- ├── client.py # Implement YourEnv(HTTPEnvClient)
138
- ├── README.md # Document your environment
139
- ├── openenv.yaml # Environment manifest
140
- ├── pyproject.toml # Dependencies and package configuration
141
- ├── outputs/ # Runtime outputs (logs, evals) - gitignored
142
- │ ├── logs/
143
- │ └── evals/
144
- └── server/
145
- ├── your_environment.py # Implement YourEnvironment(Environment)
146
- ├── app.py # Create FastAPI app
147
- ├── requirements.txt # Dependencies for Docker (can be generated)
148
- └── Dockerfile # Define container image
149
- ```
150
-
151
- #### Dependency Management
152
-
153
- OpenEnv uses `pyproject.toml` as the primary dependency specification:
154
-
155
- - **Environment-level `pyproject.toml`**: Each environment defines its own dependencies
156
- - **Root-level `pyproject.toml`**: Contains shared core dependencies (fastapi, pydantic, uvicorn)
157
- - **Server `requirements.txt`**: Can be auto-generated from `pyproject.toml` for Docker builds
158
-
159
- **Development Workflow:**
160
-
161
- ```bash
162
- # Install environment in editable mode
163
- cd my_env
164
- pip install -e .
165
-
166
- # Or using uv (faster)
167
- uv pip install -e .
168
-
169
- # Run server locally without Docker
170
- uv run server --host 0.0.0.0 --port 8000
171
- ```
172
-
173
- **Benefits:**
174
- - ✅ **Client-side extensions**: Modify client classes locally without repo changes
175
- - ✅ **Better dependency management**: Clear separation between environments
176
- - ✅ **Flexible workflows**: Use pip, uv, or Docker for different scenarios
177
- - ✅ **CI/CD ready**: Automated dependency generation and validation
178
-
179
- See [`src/envs/README.md`](src/envs/README.md) for a complete guide on building environments.
180
-
181
- ### For Environment Users
182
-
183
- To use an environment:
184
- 1. Import from `envs.your_env`: `from envs.echo_env import EchoAction, EchoEnv`
185
- 2. Create client: `client = EchoEnv.from_docker_image("echo-env:latest")`
186
- 3. Interact: `client.reset()`, `client.step(action)`, `client.state()`
187
- 4. Cleanup: `client.close()`
188
-
189
- See example scripts in `examples/` directory.
190
-
191
- ## CLI Commands
192
-
193
- The OpenEnv CLI provides commands to manage environments:
194
-
195
- - **`openenv init <env_name>`** - Initialize a new environment from template
196
- - **`openenv push [--repo-id <repo>] [--private]`** - Deploy environment to Hugging Face Spaces
197
-
198
- ### Quick Start
199
-
200
- ```bash
201
- # Create a new environment
202
- openenv init my_game_env
203
-
204
- # Deploy to Hugging Face (will prompt for login if needed)
205
- cd my_game_env
206
- openenv push
207
- ```
208
-
209
- For detailed options: `openenv init --help` and `openenv push --help`.
210
-
211
- ## Design Principles
212
-
213
- 1. **Separation of Concerns**: Clear client-server boundaries
214
- 2. **Type Safety**: Strongly-typed actions, observations, and state
215
- 3. **Container Isolation**: Each environment runs in its own container
216
- 4. **Simple APIs**: Minimal, intuitive interfaces
217
-
218
- ## Quick Start
219
-
220
- ### Using the Echo Environment(Example)
221
-
222
- ```python
223
- from envs.echo_env import EchoAction, EchoEnv
224
-
225
- # Automatically start container and connect
226
- client = EchoEnv.from_docker_image("echo-env:latest")
227
-
228
- # Reset the environment
229
- result = client.reset()
230
- print(result.observation.echoed_message) # "Echo environment ready!"
231
-
232
- # Send messages
233
- result = client.step(EchoAction(message="Hello, World!"))
234
- print(result.observation.echoed_message) # "Hello, World!"
235
- print(result.reward) # 1.3 (based on message length)
236
-
237
- # Cleanup
238
- client.close() # Stops and removes container
239
- ```
240
-
241
- ## Requirements
242
-
243
- - Python 3.11+
244
- - Docker Desktop or Docker Engine
245
- - FastAPI >= 0.104.0
246
- - Uvicorn >= 0.24.0
247
- - Requests >= 2.25.0
248
- - smolagents (for coding environment)
249
-
250
- ## Supported RL Tools
251
- The goal of this project is to support a broad set of open and closed tools to help standardize the agentic RL community. If you have a project that supports OpenEnv environments, please put up a PR to add your tool name along with a link to your documentation.
252
-
253
- ### torchforge
254
- See GRPO BlackJack training example: [`examples/grpo_blackjack/`](examples/grpo_blackjack/)
255
-
256
- ### TRL
257
- See the [TRL example](https://huggingface.co/docs/trl/main/en/openenv) on how to integrate OpenEnv environments with GRPO training.
258
-
259
- ### Unsloth
260
- See the 2048 game example based on gpt-oss: [Colab notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/OpenEnv_gpt_oss_(20B)_Reinforcement_Learning_2048_Game.ipynb)
261
-
262
- ### SkyRL
263
- See the [SkyRL example](https://skyrl.readthedocs.io/en/latest/examples/openenv.html) on how to train on OpenEnv environments with SkyRL.
264
-
265
- ### ART
266
- See the [ART example](https://art.openpipe.ai/integrations/openenv-integration) on how OpenEnv environments can be used to train models with ART.
267
-
268
- ### Oumi
269
- See the [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%20-%20OpenEnv%20GRPO%20with%20trl.ipynb) on how OpenEnv environments can be used to train models with Oumi.
270
-
271
- ## Example Environments
272
-
273
- ### Echo Environment
274
- A simple environment that echoes back messages with metadata. Perfect for:
275
- - Testing the HTTP server infrastructure
276
- - Learning the framework basics
277
- - Verifying container deployment
278
-
279
- See: [`src/envs/echo_env/README.md`](src/envs/echo_env/README.md)
280
-
281
- ### Coding Environment
282
- Executes arbitrary Python code in a sandboxed environment. Features:
283
- - Safe code execution using smolagents
284
- - Capture stdout, stderr, and exit codes
285
- - Persistent execution context within episodes
286
- - Error handling with detailed messages
287
-
288
- See: [`src/envs/coding_env/README.md`](src/envs/coding_env/README.md)
289
-
290
- ## Community Support & Acknowledgments
291
- This is an open and community-centric project. If you would like to add your name here, please put up a pull request and tag @jspisak for review. Ty!!
292
-
293
- Supporters include: Meta-PyTorch, Hugging Face, [Patronus AI](https://patronus.ai), [Surge AI](https://surgehq.ai), [LastMile AI](https://www.lastmileai.dev), Unsloth AI, Reflection AI, vLLM, SkyRL (UC-Berkeley), LightningAI, Axolotl AI, Stanford Scaling Intelligence Lab, Mithril, [OpenMined](https://openmined.org/), [Fleet AI](https://fleetai.com), [Halluminate](https://halluminate.ai/) ..
294
-
295
- And we'd also like to acknowledge the team at Farama Foundation as the OpenEnv API was heavily inspired by the work you all have done on Gymnasium. Cheers!
296
-
297
- ## License
298
-
299
- BSD 3-Clause License (see [LICENSE](./LICENSE) file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/pyproject.toml DELETED
@@ -1,57 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=45", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "openenv"
7
- version = "0.1.1"
8
- description = "A unified framework for reinforcement learning environments"
9
- readme = "README.md"
10
- requires-python = ">=3.10"
11
- dependencies = [
12
- # Core shared dependencies - minimal set required for all environments
13
- # Heavy dependencies (torch, numpy, smolagents, etc.) should be in
14
- # individual environment pyproject.toml files
15
- "fastapi>=0.104.0",
16
- "pydantic>=2.0.0",
17
- "uvicorn>=0.24.0",
18
- "requests>=2.25.0",
19
- # CLI dependencies
20
- "typer>=0.9.0",
21
- "rich>=13.0.0",
22
- "pyyaml>=6.0",
23
- "huggingface_hub>=0.20.0",
24
- "openai>=2.7.2",
25
- "tomli>=2.3.0",
26
- "tomli-w>=1.2.0"
27
- ]
28
-
29
- [project.scripts]
30
- openenv = "openenv_cli.__main__:main"
31
-
32
- [tool.setuptools]
33
- package-dir = {"" = "src"}
34
- include-package-data = true
35
-
36
- [tool.setuptools.package-data]
37
- "openenv_cli" = ["templates/**/*"]
38
-
39
- [tool.setuptools.packages.find]
40
- where = ["src"]
41
-
42
- [tool.coverage.run]
43
- omit = [
44
- "openenv_cli/templates/**",
45
- "**/templates/**",
46
- "openenv_cli/__main__.py",
47
- ]
48
-
49
- [tool.coverage.report]
50
- exclude_lines = [
51
- "pragma: no cover",
52
- "def __repr__",
53
- "raise AssertionError",
54
- "raise NotImplementedError",
55
- "if __name__ == .__main__.:",
56
- "if TYPE_CHECKING:",
57
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/scripts/CONVERT.md DELETED
@@ -1,491 +0,0 @@
1
- # Converting Your Environment to OpenEnv Standard
2
-
3
- This guide helps you convert an existing `src/envs/<env_name>` environment to a standalone, OpenEnv CLI-compatible environment that can be independently developed, versioned, and deployed.
4
-
5
- ## Overview
6
-
7
- The new OpenEnv standard enables:
8
- - **Independent repositories**: Each environment can have its own git repo
9
- - **Standalone deployment**: Use `openenv push` to deploy directly to HuggingFace or Docker registries
10
- - **Better dependency management**: Use `pyproject.toml` with `uv` for fast, reliable builds
11
- - **Development flexibility**: Work on environments without the full OpenEnv monorepo
12
-
13
- ## Prerequisites
14
-
15
- - Python 3.10+
16
- - `uv` package manager ([installation guide](https://github.com/astral-sh/uv))
17
- - Docker (for building and testing)
18
- - Git (for version control)
19
-
20
- ## Quick Start: Automated Conversion
21
-
22
- We provide a script to automate most of the conversion process:
23
-
24
- ```bash
25
- # From the OpenEnv repository root
26
- ./scripts/convert_env.sh src/envs/my_env /path/to/new/my_env_standalone
27
- ```
28
-
29
- > **Note:** The converter requires `python3` on your PATH and works with the default Bash shipped on macOS. When prompted, answer `y` to proceed and leave the optional naming prompts blank to accept the defaults.
30
-
31
- This script will:
32
- 1. Copy your environment to a new directory
33
- 2. Convert `requirements.txt` to `pyproject.toml` (if needed)
34
- 3. Add HuggingFace frontmatter to README
35
- 4. Update Dockerfile for standalone builds
36
- 5. Initialize a new git repository
37
- 6. Create necessary configuration files
38
- 7. Rewrite imports so the environment depends on `openenv-core` and installs as a proper Python package
39
-
40
- After running the script, jump to [Step 4: Testing Your Conversion](#step-4-testing-your-conversion).
41
-
42
- ## Manual Conversion Steps
43
-
44
- If you prefer to convert manually or need to understand what's happening:
45
-
46
- ### Step 1: Create New Environment Directory
47
-
48
- ```bash
49
- # Create a new directory for your standalone environment
50
- mkdir -p ~/my_projects/my_env_standalone
51
- cd ~/my_projects/my_env_standalone
52
-
53
- # Copy your existing environment
54
- cp -r /path/to/OpenEnv/src/envs/my_env/* .
55
-
56
- # Initialize git repository
57
- git init
58
- git add .
59
- git commit -m "Initial commit: Convert from OpenEnv monorepo"
60
- ```
61
-
62
- ### Step 2: Convert Dependencies to pyproject.toml
63
-
64
- If your environment uses `server/requirements.txt`, convert it to `pyproject.toml`:
65
-
66
- #### Option A: Automated Conversion
67
-
68
- ```python
69
- # Run this Python script in your environment directory
70
- import re
71
- from pathlib import Path
72
-
73
- env_name = Path.cwd().name
74
- requirements_file = Path("server/requirements.txt")
75
-
76
- if requirements_file.exists():
77
- # Parse requirements.txt
78
- deps = []
79
- with open(requirements_file) as f:
80
- for line in f:
81
- line = line.strip()
82
- if line and not line.startswith("#"):
83
- deps.append(f' "{line}",')
84
-
85
- deps_str = "\n".join(deps)
86
-
87
- # Create pyproject.toml
88
- pyproject_content = f'''[build-system]
89
- requires = ["setuptools>=45", "wheel"]
90
- build-backend = "setuptools.build_meta"
91
-
92
- [project]
93
- name = "openenv-{env_name}"
94
- version = "0.1.0"
95
- description = "{env_name.replace('_', ' ').title()} Environment for OpenEnv"
96
- requires-python = ">=3.10"
97
- dependencies = [
98
- {deps_str}
99
- "openenv-core>=0.1.0",
100
- ]
101
-
102
- [project.optional-dependencies]
103
- dev = [
104
- "pytest>=8.0.0",
105
- "pytest-cov>=4.0.0",
106
- "ipykernel>=6.29.5",
107
- ]
108
-
109
- [project.scripts]
110
- server = "{env_name}.server.app:main"
111
-
112
- [tool.setuptools]
113
- packages = ["{env_name}", "{env_name}.server"]
114
- package-dir = { "{env_name}" = ".", "{env_name}.server" = "server" }
115
-
116
- [tool.setuptools.package-data]
117
- {env_name} = ["**/*.yaml", "**/*.yml"]
118
- '''
119
-
120
- Path("pyproject.toml").write_text(pyproject_content)
121
- print(f"✓ Created pyproject.toml from {requirements_file}")
122
- else:
123
- print("No requirements.txt found - pyproject.toml may already exist")
124
- ```
125
-
126
- #### Option B: Manual Creation
127
-
128
- Create `pyproject.toml` at the environment root:
129
-
130
- ```toml
131
- [build-system]
132
- requires = ["setuptools>=45", "wheel"]
133
- build-backend = "setuptools.build_meta"
134
-
135
- [project]
136
- name = "openenv-my_env"
137
- version = "0.1.0"
138
- description = "My Environment for OpenEnv"
139
- requires-python = ">=3.10"
140
- dependencies = [
141
- "openenv-core>=0.1.0",
142
- "fastapi>=0.115.0",
143
- "pydantic>=2.0.0",
144
- "uvicorn>=0.24.0",
145
- "requests>=2.25.0",
146
- # Add your environment-specific dependencies here
147
- ]
148
-
149
- [project.optional-dependencies]
150
- dev = [
151
- "pytest>=8.0.0",
152
- "pytest-cov>=4.0.0",
153
- "ipykernel>=6.29.5",
154
- ]
155
-
156
- [project.scripts]
157
- server = "my_env.server.app:main"
158
-
159
- [tool.setuptools]
160
- packages = ["my_env"]
161
-
162
- [tool.setuptools]
163
- packages = ["{env_name}", "{env_name}.server"]
164
- package-dir = { "{env_name}" = ".", "{env_name}.server" = "server" }
165
- ```
166
-
167
- **Important**: Replace `my_env` with your actual environment name throughout the file.
168
-
169
- ### Step 3: Update Files for Standalone Usage
170
-
171
- #### 3.1: Add HuggingFace Frontmatter to README.md
172
-
173
- Add this YAML frontmatter to the top of your `README.md`:
174
-
175
- ```markdown
176
- ---
177
- title: My Environment Server
178
- emoji: 🎮
179
- colorFrom: '#00C9FF'
180
- colorTo: '#1B2845'
181
- sdk: docker
182
- pinned: false
183
- app_port: 8000
184
- base_path: /web
185
- tags:
186
- - openenv
187
- ---
188
-
189
- # My Environment
190
-
191
- [Rest of your README content...]
192
- ```
193
-
194
- **Customize**:
195
- - `title`: Your environment's display name
196
- - `emoji`: Pick an emoji that represents your environment ([emoji list](https://emojipedia.org/))
197
- - `colorFrom` / `colorTo`: Gradient colors for HuggingFace card (hex codes)
198
-
199
- #### 3.2: Update Dockerfile
200
-
201
- Your `server/Dockerfile` needs to support standalone builds. Update it to:
202
-
203
- ```dockerfile
204
- # Base image
205
- FROM python:3.11-slim
206
-
207
- # Set working directory
208
- WORKDIR /app/env
209
-
210
- # Install system dependencies (if needed)
211
- RUN apt-get update && apt-get install -y \
212
- git \
213
- && rm -rf /var/lib/apt/lists/*
214
-
215
- # Copy environment files
216
- COPY . .
217
-
218
- # Install Python dependencies
219
- RUN pip install --no-cache-dir -e .
220
-
221
- # Expose port
222
- EXPOSE 8000
223
-
224
- # Set environment variables
225
- ENV PYTHONUNBUFFERED=1
226
- ENV ENABLE_WEB_INTERFACE=true
227
-
228
- # Run the server
229
- CMD ["python", "-m", "uvicorn", "my_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
230
- ```
231
-
232
- **Key changes**:
233
- - Uses `pip install -e .` to install from `pyproject.toml`
234
- - Copies entire environment directory
235
- - Sets `ENABLE_WEB_INTERFACE=true` for HuggingFace deployments
236
- - Replace `my_env` with your environment name
237
-
238
- #### 3.3: Update app.py
239
-
240
- Ensure your `server/app.py` has a proper `main()` function:
241
-
242
- ```python
243
- # At the end of server/app.py
244
- def main():
245
- """Main entry point for running the server."""
246
- import uvicorn
247
- uvicorn.run(app, host="0.0.0.0", port=8000)
248
-
249
- if __name__ == "__main__":
250
- main()
251
- ```
252
-
253
- This enables running with `uv run server`.
254
-
255
- #### 3.4: Create uv.lock
256
-
257
- Generate a lockfile for reproducible builds:
258
-
259
- ```bash
260
- # Install uv if you haven't already
261
- curl -LsSf https://astral.sh/uv/install.sh | sh
262
-
263
- # Generate lockfile
264
- uv lock
265
- ```
266
-
267
- This creates `uv.lock` which pins all dependencies.
268
-
269
- #### 3.5: Add .gitignore
270
-
271
- Create or update `.gitignore`:
272
-
273
- ```gitignore
274
- # Python
275
- __pycache__/
276
- *.py[cod]
277
- *$py.class
278
- *.so
279
- .Python
280
- build/
281
- develop-eggs/
282
- dist/
283
- downloads/
284
- eggs/
285
- .eggs/
286
- lib/
287
- lib64/
288
- parts/
289
- sdist/
290
- var/
291
- wheels/
292
- *.egg-info/
293
- .installed.cfg
294
- *.egg
295
-
296
- # Virtual environments
297
- venv/
298
- env/
299
- ENV/
300
-
301
- # IDE
302
- .vscode/
303
- .idea/
304
- *.swp
305
- *.swo
306
- *~
307
-
308
- # OS
309
- .DS_Store
310
- Thumbs.db
311
-
312
- # Environment outputs
313
- outputs/
314
- logs/
315
- *.log
316
-
317
- # Testing
318
- .pytest_cache/
319
- .coverage
320
- htmlcov/
321
- ```
322
-
323
- ### Step 4: Testing Your Conversion
324
-
325
- #### 4.1: Install and Test Locally
326
-
327
- ```bash
328
- # Install in editable mode
329
- uv pip install -e .
330
-
331
- # Test imports
332
- python -c "from my_env import MyAction, MyObservation, MyEnv"
333
-
334
- # Run server locally
335
- uv run server
336
- ```
337
-
338
- Visit `http://localhost:8000/docs` to see the API documentation.
339
-
340
- #### 4.2: Build Docker Image
341
-
342
- ```bash
343
- # Build the image
344
- openenv build
345
-
346
- # Or manually:
347
- docker build -t my_env:latest -f server/Dockerfile .
348
-
349
- # Test the container
350
- docker run -p 8000:8000 my_env:latest
351
- ```
352
-
353
- #### 4.3: Validate Structure
354
-
355
- Use the OpenEnv CLI to validate your environment:
356
-
357
- ```bash
358
- openenv validate
359
- ```
360
-
361
- This checks for:
362
- - Required files (`openenv.yaml`, `pyproject.toml`, etc.)
363
- - Correct dependency structure
364
- - Valid server entry point
365
- - Docker build capability
366
-
367
- ### Step 5: Deploy Your Environment
368
-
369
- #### Deploy to HuggingFace Spaces
370
-
371
- ```bash
372
- # Deploy to HuggingFace (with web interface)
373
- openenv push
374
-
375
- # Deploy to specific repo
376
- openenv push --repo-id myusername/my-env
377
-
378
- # Deploy privately
379
- openenv push --private
380
- ```
381
-
382
- #### Deploy to Docker Registry
383
-
384
- ```bash
385
- # Build and push to Docker Hub
386
- openenv push --registry docker.io/myusername
387
-
388
- # Push to GitHub Container Registry
389
- openenv push --registry ghcr.io/myorg
390
-
391
- # Push to custom registry
392
- openenv push --registry myregistry.io/path/to/repo
393
- ```
394
-
395
- ## Directory Structure After Conversion
396
-
397
- Your standalone environment should look like this:
398
-
399
- ```
400
- my_env_standalone/
401
- ├── .git/ # Git repository
402
- ├── .gitignore # Ignore patterns
403
- ├── README.md # With HF frontmatter
404
- ├── openenv.yaml # Environment manifest
405
- ├── pyproject.toml # Dependencies and metadata
406
- ├── uv.lock # Locked dependencies
407
- ├── __init__.py # Module exports
408
- ├── client.py # Environment client
409
- ├── models.py # Action/Observation models
410
- └── server/
411
- ├── __init__.py
412
- ├── app.py # FastAPI app (with main())
413
- ├── Dockerfile # Standalone build
414
- └── my_env_environment.py # Core environment logic
415
- ```
416
-
417
- ## Common Issues and Solutions
418
-
419
- ### Issue: Import Errors After Installation
420
-
421
- **Solution**: Reinstall in editable mode:
422
- ```bash
423
- uv pip install -e . --force-reinstall
424
- ```
425
-
426
- ### Issue: Docker Build Fails
427
-
428
- **Solutions**:
429
- 1. Ensure `pyproject.toml` has all dependencies
430
- 2. Check Dockerfile COPY commands are correct
431
- 3. Verify base image is accessible
432
-
433
- ### Issue: `openenv` Commands Not Found
434
-
435
- **Solution**: Install openenv-cli:
436
- ```bash
437
- pip install openenv-cli
438
- # or
439
- uv pip install openenv-cli
440
- ```
441
-
442
- ### Issue: Server Entry Point Not Found
443
-
444
- **Solution**: Ensure `pyproject.toml` has correct entry point:
445
- ```toml
446
- [project.scripts]
447
- server = "my_env.server.app:main" # Replace my_env with your name
448
- ```
449
-
450
- ### Issue: Missing openenv-core Dependency
451
-
452
- **Solution**: Add to `pyproject.toml`:
453
- ```toml
454
- dependencies = [
455
- "openenv-core>=0.1.0",
456
- # ... other dependencies
457
- ]
458
- ```
459
-
460
- For local development, install core from the OpenEnv repo:
461
- ```bash
462
- pip install -e /path/to/OpenEnv/src/core
463
- ```
464
-
465
- ## Development Workflow
466
-
467
- Once converted, you can work independently:
468
-
469
- ```bash
470
- # Clone your standalone repo
471
- git clone https://github.com/myusername/my_env_standalone
472
- cd my_env_standalone
473
-
474
- # Install dependencies
475
- uv pip install -e .
476
-
477
- # Run locally
478
- uv run server
479
-
480
- # Make changes to client.py, models.py, etc.
481
-
482
- # Test changes
483
- openenv validate
484
- openenv build
485
-
486
- # Deploy updates
487
- openenv push
488
- ```
489
- ## Automated Script
490
-
491
- For full automation, see the `scripts/convert_env.sh` script included in this repository.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/scripts/convert_env.sh DELETED
@@ -1,502 +0,0 @@
1
- #!/bin/bash
2
- # Copyright (c) Meta Platforms, Inc. and affiliates.
3
- # All rights reserved.
4
- #
5
- # This source code is licensed under the BSD-style license found in the
6
- # LICENSE file in the root directory of this source tree.
7
-
8
- set -e # Exit on error
9
-
10
- # Colors for output
11
- RED='\033[0;31m'
12
- GREEN='\033[0;32m'
13
- YELLOW='\033[1;33m'
14
- BLUE='\033[0;34m'
15
- NC='\033[0m' # No Color
16
-
17
- # Helper functions
18
- print_info() {
19
- echo -e "${BLUE}ℹ${NC} $1"
20
- }
21
-
22
- print_success() {
23
- echo -e "${GREEN}✓${NC} $1"
24
- }
25
-
26
- print_warning() {
27
- echo -e "${YELLOW}⚠${NC} $1"
28
- }
29
-
30
- print_error() {
31
- echo -e "${RED}✗${NC} $1"
32
- }
33
-
34
- print_header() {
35
- echo ""
36
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
37
- echo -e "${BLUE}$1${NC}"
38
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
39
- }
40
-
41
- # Usage information
42
- usage() {
43
- cat << EOF
44
- Usage: $0 <source_env_dir> <target_dir>
45
-
46
- Convert an OpenEnv environment from the monorepo to a standalone repository.
47
-
48
- Arguments:
49
- source_env_dir Path to existing environment (e.g., src/envs/echo_env)
50
- target_dir Path for new standalone environment (e.g., ~/my_envs/echo_env_standalone)
51
-
52
- Example:
53
- $0 src/envs/echo_env ~/my_envs/echo_env_standalone
54
-
55
- The script will:
56
- 1. Copy environment files to target directory
57
- 2. Convert requirements.txt to pyproject.toml
58
- 3. Add HuggingFace frontmatter to README
59
- 4. Update Dockerfile for standalone builds
60
- 5. Initialize git repository
61
- 6. Generate uv.lock for dependencies
62
-
63
- EOF
64
- exit 1
65
- }
66
-
67
- # Check arguments
68
- if [ $# -ne 2 ]; then
69
- usage
70
- fi
71
-
72
- SOURCE_DIR="$1"
73
- TARGET_DIR="$2"
74
-
75
- # Validate source directory
76
- if [ ! -d "$SOURCE_DIR" ]; then
77
- print_error "Source directory does not exist: $SOURCE_DIR"
78
- exit 1
79
- fi
80
-
81
- # Check if it looks like an OpenEnv environment (either has openenv.yaml or server/ directory)
82
- if [ ! -f "$SOURCE_DIR/openenv.yaml" ] && [ ! -d "$SOURCE_DIR/server" ]; then
83
- print_error "Not an OpenEnv environment (missing openenv.yaml and server/): $SOURCE_DIR"
84
- exit 1
85
- fi
86
-
87
- # Warn if it's a legacy environment
88
- if [ ! -f "$SOURCE_DIR/openenv.yaml" ]; then
89
- print_warning "Legacy environment detected (no openenv.yaml) - will create one"
90
- fi
91
-
92
- # Extract environment name from source directory
93
- ENV_NAME=$(basename "$SOURCE_DIR")
94
- if [[ "$ENV_NAME" == *"_env" ]]; then
95
- BASE_NAME="${ENV_NAME%_env}"
96
- else
97
- BASE_NAME="$ENV_NAME"
98
- fi
99
-
100
- # Convert to class name (capitalize first letter of each word)
101
- CLASS_NAME=$(echo "$BASE_NAME" | sed -r 's/(^|_)([a-z])/\U\2/g')
102
-
103
- print_header "OpenEnv Environment Conversion"
104
- print_info "Source: $SOURCE_DIR"
105
- print_info "Target: $TARGET_DIR"
106
- print_info "Environment: $ENV_NAME"
107
- print_info "Class Name: $CLASS_NAME"
108
-
109
- # Confirm with user
110
- echo ""
111
- read -p "Continue with conversion? (y/N): " -n 1 -r
112
- echo
113
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
114
- print_info "Conversion cancelled"
115
- exit 0
116
- fi
117
-
118
- # Step 1: Copy environment files
119
- print_header "Step 1: Copying Environment Files"
120
-
121
- if [ -d "$TARGET_DIR" ]; then
122
- print_warning "Target directory already exists: $TARGET_DIR"
123
- read -p "Remove existing directory? (y/N): " -n 1 -r
124
- echo
125
- if [[ $REPLY =~ ^[Yy]$ ]]; then
126
- rm -rf "$TARGET_DIR"
127
- print_success "Removed existing directory"
128
- else
129
- print_error "Cannot proceed with existing directory"
130
- exit 1
131
- fi
132
- fi
133
-
134
- mkdir -p "$TARGET_DIR"
135
- cp -r "$SOURCE_DIR"/* "$TARGET_DIR/"
136
- print_success "Copied environment files to $TARGET_DIR"
137
-
138
- cd "$TARGET_DIR"
139
-
140
- # Step 1.5: Create openenv.yaml if missing (legacy environments)
141
- if [ ! -f "openenv.yaml" ]; then
142
- print_header "Step 1.5: Creating openenv.yaml"
143
- print_info "Legacy environment detected - creating openenv.yaml"
144
-
145
- cat > openenv.yaml << EOF
146
- name: ${ENV_NAME}
147
- version: "0.1.0"
148
- description: "${BASE_NAME^} environment for OpenEnv"
149
- action: ${CLASS_NAME}Action
150
- observation: ${CLASS_NAME}Observation
151
- EOF
152
-
153
- print_success "Created openenv.yaml"
154
- fi
155
-
156
- # Step 2: Convert requirements.txt to pyproject.toml
157
- print_header "Step 2: Setting Up pyproject.toml"
158
-
159
- if [ -f "pyproject.toml" ]; then
160
- print_success "pyproject.toml already exists"
161
- else
162
- print_info "Creating pyproject.toml"
163
-
164
- # Collect dependencies from requirements.txt if it exists
165
- DEPS=""
166
- if [ -f "server/requirements.txt" ]; then
167
- print_info "Converting server/requirements.txt to dependencies"
168
- while IFS= read -r line; do
169
- # Skip empty lines and comments
170
- if [[ -n "$line" && ! "$line" =~ ^[[:space:]]*# ]]; then
171
- DEPS="${DEPS} \"${line}\",\n"
172
- fi
173
- done < "server/requirements.txt"
174
- fi
175
-
176
- # Always add openenv-core
177
- DEPS="${DEPS} \"openenv-core>=0.1.0\","
178
-
179
- # Create pyproject.toml
180
- cat > pyproject.toml << EOF
181
- [build-system]
182
- requires = ["setuptools>=45", "wheel"]
183
- build-backend = "setuptools.build_meta"
184
-
185
- [project]
186
- name = "openenv-${ENV_NAME}"
187
- version = "0.1.0"
188
- description = "${BASE_NAME^} Environment for OpenEnv"
189
- requires-python = ">=3.10"
190
- dependencies = [
191
- $(echo -e "$DEPS")
192
- ]
193
-
194
- [project.optional-dependencies]
195
- dev = [
196
- "pytest>=8.0.0",
197
- "pytest-cov>=4.0.0",
198
- "ipykernel>=6.29.5",
199
- ]
200
-
201
- [project.scripts]
202
- server = "${ENV_NAME}.server.app:main"
203
-
204
- [tool.setuptools]
205
- packages = ["${ENV_NAME}"]
206
-
207
- [tool.setuptools.package-data]
208
- ${ENV_NAME} = ["**/*.yaml", "**/*.yml"]
209
- EOF
210
-
211
- print_success "Created pyproject.toml"
212
- fi
213
-
214
- # Step 3: Add HuggingFace frontmatter to README
215
- print_header "Step 3: Updating README with HuggingFace Frontmatter"
216
-
217
- if [ -f "README.md" ]; then
218
- # Check if frontmatter already exists
219
- if head -n 1 "README.md" | grep -q "^---$"; then
220
- print_success "README.md already has frontmatter"
221
- else
222
- print_info "Adding HuggingFace frontmatter to README.md"
223
-
224
- # Create temporary file with frontmatter
225
- cat > README.tmp << 'EOF'
226
- ---
227
- title: __TITLE__ Environment Server
228
- emoji: 🎮
229
- colorFrom: '#00C9FF'
230
- colorTo: '#1B2845'
231
- sdk: docker
232
- pinned: false
233
- app_port: 8000
234
- base_path: /web
235
- tags:
236
- - openenv
237
- ---
238
-
239
- EOF
240
-
241
- # Replace placeholder with actual title
242
- TITLE="${BASE_NAME^}"
243
- sed -i "s/__TITLE__/${TITLE}/g" README.tmp
244
-
245
- # Append original README content
246
- cat "README.md" >> README.tmp
247
- mv README.tmp "README.md"
248
-
249
- print_success "Added HuggingFace frontmatter to README.md"
250
- fi
251
- else
252
- print_warning "README.md not found - creating basic one"
253
- cat > README.md << EOF
254
- ---
255
- title: ${BASE_NAME^} Environment Server
256
- emoji: 🎮
257
- colorFrom: '#00C9FF'
258
- colorTo: '#1B2845'
259
- sdk: docker
260
- pinned: false
261
- app_port: 8000
262
- base_path: /web
263
- tags:
264
- - openenv
265
- ---
266
-
267
- # ${BASE_NAME^} Environment
268
-
269
- An OpenEnv environment.
270
-
271
- ## Quick Start
272
-
273
- \`\`\`python
274
- from ${ENV_NAME} import ${CLASS_NAME}Action, ${CLASS_NAME}Env
275
-
276
- # Create environment from Docker image
277
- env = ${CLASS_NAME}Env.from_docker_image("${ENV_NAME}:latest")
278
-
279
- # Reset
280
- result = env.reset()
281
-
282
- # Step
283
- result = env.step(${CLASS_NAME}Action(...))
284
-
285
- # Clean up
286
- env.close()
287
- \`\`\`
288
-
289
- ## Building
290
-
291
- \`\`\`bash
292
- openenv build
293
- \`\`\`
294
-
295
- ## Deploying
296
-
297
- \`\`\`bash
298
- openenv push
299
- \`\`\`
300
- EOF
301
- print_success "Created README.md"
302
- fi
303
-
304
- # Step 4: Update Dockerfile
305
- print_header "Step 4: Updating Dockerfile for Standalone Builds"
306
-
307
- if [ -f "server/Dockerfile" ]; then
308
- # Check if Dockerfile already has standalone pattern
309
- if grep -q "pip install.*-e \." "server/Dockerfile"; then
310
- print_success "Dockerfile already configured for standalone builds"
311
- else
312
- print_info "Updating Dockerfile for standalone builds"
313
-
314
- # Create updated Dockerfile
315
- cat > server/Dockerfile.new << 'EOF'
316
- # Base image
317
- FROM python:3.11-slim
318
-
319
- # Set working directory
320
- WORKDIR /app/env
321
-
322
- # Install system dependencies
323
- RUN apt-get update && apt-get install -y \
324
- git \
325
- && rm -rf /var/lib/apt/lists/*
326
-
327
- # Copy environment files
328
- COPY . .
329
-
330
- # Install Python dependencies
331
- RUN pip install --no-cache-dir -e .
332
-
333
- # Expose port
334
- EXPOSE 8000
335
-
336
- # Set environment variables
337
- ENV PYTHONUNBUFFERED=1
338
- ENV ENABLE_WEB_INTERFACE=true
339
-
340
- # Run the server
341
- CMD ["python", "-m", "uvicorn", "__ENV_NAME__.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
342
- EOF
343
-
344
- # Replace placeholder
345
- sed -i "s/__ENV_NAME__/${ENV_NAME}/g" server/Dockerfile.new
346
-
347
- # Backup original and replace
348
- mv server/Dockerfile server/Dockerfile.backup
349
- mv server/Dockerfile.new server/Dockerfile
350
-
351
- print_success "Updated Dockerfile (backup saved as server/Dockerfile.backup)"
352
- fi
353
- else
354
- print_warning "server/Dockerfile not found"
355
- fi
356
-
357
- # Step 5: Ensure app.py has main() function
358
- print_header "Step 5: Checking server/app.py"
359
-
360
- if [ -f "server/app.py" ]; then
361
- if grep -q "def main()" "server/app.py"; then
362
- print_success "server/app.py has main() function"
363
- else
364
- print_warning "server/app.py missing main() function - adding it"
365
- cat >> server/app.py << 'EOF'
366
-
367
-
368
- def main():
369
- """Main entry point for running the server."""
370
- import uvicorn
371
- uvicorn.run(app, host="0.0.0.0", port=8000)
372
-
373
-
374
- if __name__ == "__main__":
375
- main()
376
- EOF
377
- print_success "Added main() function to server/app.py"
378
- fi
379
- else
380
- print_warning "server/app.py not found"
381
- fi
382
-
383
- # Step 6: Create .gitignore
384
- print_header "Step 6: Creating .gitignore"
385
-
386
- cat > .gitignore << 'EOF'
387
- # Python
388
- __pycache__/
389
- *.py[cod]
390
- *$py.class
391
- *.so
392
- .Python
393
- build/
394
- develop-eggs/
395
- dist/
396
- downloads/
397
- eggs/
398
- .eggs/
399
- lib/
400
- lib64/
401
- parts/
402
- sdist/
403
- var/
404
- wheels/
405
- *.egg-info/
406
- .installed.cfg
407
- *.egg
408
- MANIFEST
409
-
410
- # Virtual environments
411
- venv/
412
- env/
413
- ENV/
414
- .venv/
415
-
416
- # IDE
417
- .vscode/
418
- .idea/
419
- *.swp
420
- *.swo
421
- *~
422
- .DS_Store
423
-
424
- # Testing
425
- .pytest_cache/
426
- .coverage
427
- htmlcov/
428
- .tox/
429
-
430
- # Environment outputs
431
- outputs/
432
- logs/
433
- *.log
434
-
435
- # Build artifacts
436
- *.backup
437
- EOF
438
-
439
- print_success "Created .gitignore"
440
-
441
- # Step 7: Initialize git repository
442
- print_header "Step 7: Initializing Git Repository"
443
-
444
- if [ -d ".git" ]; then
445
- print_warning "Git repository already initialized"
446
- else
447
- git init
448
- git add .
449
- git commit -m "Initial commit: Converted from OpenEnv monorepo
450
-
451
- Environment: ${ENV_NAME}
452
- Converted using convert_env.sh script"
453
-
454
- print_success "Initialized git repository"
455
- fi
456
-
457
- # Step 8: Generate uv.lock (if uv is available)
458
- print_header "Step 8: Generating Dependency Lock File"
459
-
460
- if command -v uv &> /dev/null; then
461
- print_info "Generating uv.lock..."
462
- if uv lock; then
463
- print_success "Generated uv.lock"
464
- else
465
- print_warning "Failed to generate uv.lock - you may need to fix pyproject.toml dependencies"
466
- fi
467
- else
468
- print_warning "uv not found - skipping lock file generation"
469
- print_info "Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh"
470
- fi
471
-
472
- # Final summary
473
- print_header "Conversion Complete!"
474
-
475
- echo ""
476
- print_success "Environment converted successfully!"
477
- echo ""
478
- print_info "Next steps:"
479
- echo ""
480
- echo " 1. Review the converted files:"
481
- echo " cd $TARGET_DIR"
482
- echo ""
483
- echo " 2. Install dependencies:"
484
- echo " uv pip install -e ."
485
- echo ""
486
- echo " 3. Test locally:"
487
- echo " uv run server"
488
- echo ""
489
- echo " 4. Validate structure:"
490
- echo " openenv validate"
491
- echo ""
492
- echo " 5. Build Docker image:"
493
- echo " openenv build"
494
- echo ""
495
- echo " 6. Deploy to HuggingFace:"
496
- echo " openenv push"
497
- echo ""
498
- echo " 7. Or push to Docker registry:"
499
- echo " openenv push --registry docker.io/myusername"
500
- echo ""
501
- print_info "For detailed documentation, see CONVERT.md"
502
- echo ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/scripts/deploy_to_hf.sh DELETED
@@ -1,615 +0,0 @@
1
- #!/bin/bash
2
-
3
- # OpenEnv Hugging Face Deployment Preparation Script
4
- # This script prepares files for deployment to Hugging Face Spaces
5
-
6
- set -euo pipefail
7
-
8
- usage() {
9
- cat <<'EOF'
10
- Usage: scripts/deploy_to_hf.sh --env <environment_name> [options]
11
-
12
- Required arguments:
13
- --env <name> Environment name under src/envs (e.g. textarena_env)
14
-
15
- Optional arguments:
16
- --base-sha <sha|tag> Override openenv-base image reference (defaults to :latest)
17
- --hf-namespace <user> Hugging Face username/organization (defaults to HF_USERNAME or meta-openenv)
18
- --staging-dir <path> Output directory for staging (defaults to hf-staging)
19
- --space-suffix <suffix> Suffix to add to space name (e.g., "-test" for test spaces)
20
- --private Deploy the space as private (default: public)
21
- --dry-run Prepare files without pushing to Hugging Face Spaces
22
- -h, --help Show this help message
23
-
24
- Positional compatibility:
25
- You can also call the script as:
26
- scripts/deploy_to_hf.sh <env_name> [base_image_sha]
27
-
28
- Examples:
29
- scripts/deploy_to_hf.sh --env textarena_env --hf-namespace my-team
30
- scripts/deploy_to_hf.sh echo_env --private --hf-namespace my-org
31
- EOF
32
- }
33
-
34
- sed_in_place() {
35
- local expression="$1"
36
- local target_file="$2"
37
- if sed --version >/dev/null 2>&1; then
38
- sed -i "$expression" "$target_file"
39
- else
40
- sed -i '' "$expression" "$target_file"
41
- fi
42
- }
43
-
44
- SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
45
- REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
46
- cd "$REPO_ROOT"
47
-
48
- # Detect if we're running in GitHub Actions
49
- IS_GITHUB_ACTIONS=false
50
- if [ -n "${GITHUB_ACTIONS:-}" ]; then
51
- IS_GITHUB_ACTIONS=true
52
- fi
53
-
54
- # Check for hf CLI - but allow dry-run mode to work without it
55
- if ! command -v hf >/dev/null 2>&1; then
56
- echo "Warning: huggingface-hub CLI 'hf' not found in PATH." >&2
57
- echo "Install the HF CLI: curl -LsSf https://hf.co/cli/install.sh | sh" >&2
58
- if [ "${1:-}" != "--dry-run" ] && [ "${2:-}" != "--dry-run" ]; then
59
- echo "Error: hf is required for deployment (use --dry-run to skip deployment)" >&2
60
- exit 1
61
- fi
62
- fi
63
-
64
- ENV_NAME=""
65
- BASE_IMAGE_SHA=""
66
- HF_NAMESPACE="${HF_NAMESPACE:-}" # Initialize from env var if set, otherwise empty
67
- STAGING_DIR="hf-staging"
68
- SPACE_SUFFIX=""
69
- PRIVATE=false
70
- DRY_RUN=false
71
- HUB_TAG="openenv"
72
-
73
- while [[ $# -gt 0 ]]; do
74
- case "$1" in
75
- --env)
76
- ENV_NAME="$2"
77
- shift 2
78
- ;;
79
- --hub-tag)
80
- HUB_TAG="$2"
81
- shift 2
82
- ;;
83
- --base-sha|--base-image-sha)
84
- BASE_IMAGE_SHA="$2"
85
- shift 2
86
- ;;
87
- --namespace|--hf-namespace|--hf-user|--hf-username)
88
- HF_NAMESPACE="$2"
89
- shift 2
90
- ;;
91
- --staging-dir)
92
- STAGING_DIR="$2"
93
- shift 2
94
- ;;
95
- --suffix|--space-suffix)
96
- SPACE_SUFFIX="$2"
97
- shift 2
98
- ;;
99
- --private)
100
- PRIVATE=true
101
- shift
102
- ;;
103
- --dry-run)
104
- DRY_RUN=true
105
- shift
106
- ;;
107
- -h|--help)
108
- usage
109
- exit 0
110
- ;;
111
- --)
112
- shift
113
- break
114
- ;;
115
- -* )
116
- echo "Unknown option: $1" >&2
117
- usage
118
- exit 1
119
- ;;
120
- *)
121
- if [ -z "$ENV_NAME" ]; then
122
- ENV_NAME="$1"
123
- elif [ -z "$BASE_IMAGE_SHA" ]; then
124
- BASE_IMAGE_SHA="$1"
125
- else
126
- echo "Unexpected positional argument: $1" >&2
127
- usage
128
- exit 1
129
- fi
130
- shift
131
- ;;
132
- esac
133
- done
134
-
135
- if [ -z "$HUB_TAG" ]; then
136
- HUB_TAG="openenv"
137
- fi
138
-
139
- if [ -z "$ENV_NAME" ]; then
140
- echo "Error: Environment name is required" >&2
141
- usage
142
- exit 1
143
- fi
144
-
145
- if [[ "$ENV_NAME" == *","* || "$ENV_NAME" == *" "* ]]; then
146
- echo "Error: only one environment can be deployed per invocation (received '$ENV_NAME')." >&2
147
- exit 1
148
- fi
149
-
150
- if [ ! -d "src/envs/$ENV_NAME" ]; then
151
- echo "Error: Environment '$ENV_NAME' not found under src/envs" >&2
152
- exit 1
153
- fi
154
-
155
- # Try to get HF_USERNAME, but handle failures gracefully (especially in CI before auth)
156
- if command -v hf >/dev/null 2>&1; then
157
- HF_USERNAME=$(hf auth whoami 2>/dev/null | head -n1 | tr -d '\n' || echo "")
158
- fi
159
-
160
- if [ -z "$HF_NAMESPACE" ]; then
161
- # Check HF_USERNAME (env var or detected from CLI)
162
- if [ -n "${HF_USERNAME:-}" ]; then
163
- HF_NAMESPACE="${HF_USERNAME}"
164
- else
165
- HF_NAMESPACE="meta-openenv"
166
- fi
167
- fi
168
-
169
- echo "🙋 Using namespace: $HF_NAMESPACE. You can override with --hf-namespace"
170
-
171
- # Set base image reference (using GHCR)
172
- if [ -n "$BASE_IMAGE_SHA" ]; then
173
- BASE_IMAGE_REF="ghcr.io/meta-pytorch/openenv-base:$BASE_IMAGE_SHA"
174
- echo "Using specific SHA for openenv-base: $BASE_IMAGE_SHA"
175
- else
176
- BASE_IMAGE_REF="ghcr.io/meta-pytorch/openenv-base:latest"
177
- fi
178
-
179
- # Create staging directory
180
- CURRENT_STAGING_DIR="${STAGING_DIR}/${HF_NAMESPACE}/${ENV_NAME}"
181
- # Ensure clean staging directory
182
- rm -rf "$CURRENT_STAGING_DIR"
183
- mkdir -p "$CURRENT_STAGING_DIR/src/core"
184
- mkdir -p "$CURRENT_STAGING_DIR/src/envs/$ENV_NAME"
185
-
186
- # Copy core files
187
- cp -R src/core/* "$CURRENT_STAGING_DIR/src/core/"
188
-
189
- # Copy environment files
190
- cp -R src/envs/$ENV_NAME/* "$CURRENT_STAGING_DIR/src/envs/$ENV_NAME/"
191
-
192
- echo "📁 Copied core and $ENV_NAME environment files to $CURRENT_STAGING_DIR"
193
-
194
- # Create environment-specific multi-stage Dockerfile
195
- create_environment_dockerfile() {
196
- local env_name=$1
197
-
198
- # Create base Dockerfile
199
- cat > "$CURRENT_STAGING_DIR/Dockerfile" << DOCKERFILE_EOF
200
- # Copyright (c) Meta Platforms, Inc. and affiliates.
201
- # All rights reserved.
202
- #
203
- # This source code is licensed under the BSD-style license found in the
204
- # LICENSE file in the root directory of this source tree.
205
-
206
- # Use the specified openenv-base image
207
- FROM $BASE_IMAGE_REF
208
- DOCKERFILE_EOF
209
-
210
- # Add environment-specific dependencies
211
- case $env_name in
212
- "echo_env")
213
- # Echo environment needs no additional dependencies
214
- ;;
215
- "coding_env")
216
- cat >> "$CURRENT_STAGING_DIR/Dockerfile" << 'DOCKERFILE_EOF'
217
- # Install smolagents for code execution
218
- RUN pip install --no-cache-dir smolagents
219
- DOCKERFILE_EOF
220
- ;;
221
- "chat_env")
222
- cat >> "$CURRENT_STAGING_DIR/Dockerfile" << 'DOCKERFILE_EOF'
223
- # Install additional dependencies for ChatEnvironment
224
- RUN pip install --no-cache-dir torch transformers
225
-
226
- # Set up cache directory for Hugging Face models
227
- RUN mkdir -p /.cache && chmod 777 /.cache
228
- ENV HF_HOME=/.cache
229
- ENV TRANSFORMERS_CACHE=/.cache
230
-
231
- # Pre-download the GPT-2 model to avoid permission issues during runtime
232
- RUN python -c "from transformers import GPT2Tokenizer; GPT2Tokenizer.from_pretrained('gpt2')"
233
- DOCKERFILE_EOF
234
- ;;
235
- "atari_env")
236
- cat >> "$CURRENT_STAGING_DIR/Dockerfile" << 'DOCKERFILE_EOF'
237
- # Install ALE-specific dependencies
238
- RUN pip install --no-cache-dir \
239
- gymnasium>=0.29.0 \
240
- ale-py>=0.8.0 \
241
- numpy>=1.24.0
242
- DOCKERFILE_EOF
243
- ;;
244
- "textarena_env")
245
- cat >> "$CURRENT_STAGING_DIR/Dockerfile" << 'DOCKERFILE_EOF'
246
- # Install system libraries required by TextArena
247
- RUN apt-get update && apt-get install -y --no-install-recommends \
248
- libgl1 \
249
- libglib2.0-0 \
250
- && rm -rf /var/lib/apt/lists/*
251
-
252
- # Install TextArena and supporting Python packages
253
- RUN pip install --no-cache-dir \
254
- textarena==0.6.1 \
255
- nltk==3.9.2
256
- DOCKERFILE_EOF
257
- ;;
258
- "openspiel_env")
259
- # OpenSpiel requires special C++ build process - replace entire Dockerfile
260
- cat > "$CURRENT_STAGING_DIR/Dockerfile" << DOCKERFILE_EOF
261
- # OpenSpiel environment using pre-built OpenSpiel base image
262
- ARG OPENSPIEL_BASE_IMAGE=ghcr.io/meta-pytorch/openenv-openspiel-base:sha-e622c7e
263
- FROM \${OPENSPIEL_BASE_IMAGE}
264
-
265
- # Copy OpenEnv core (base image already set WORKDIR=/app)
266
- WORKDIR /app
267
- COPY src/core/ /app/src/core/
268
-
269
- # Copy OpenSpiel environment
270
- COPY src/envs/openspiel_env/ /app/src/envs/openspiel_env/
271
-
272
- # Extend Python path for OpenEnv (base image set PYTHONPATH=/app/src)
273
- # We prepend OpenSpiel paths
274
- ENV PYTHONPATH=/repo:/repo/build/python:/app/src
275
-
276
- # OpenSpiel-specific environment variables (can be overridden at runtime)
277
- ENV OPENSPIEL_GAME=catch
278
- ENV OPENSPIEL_AGENT_PLAYER=0
279
- ENV OPENSPIEL_OPPONENT_POLICY=random
280
-
281
- # Health check (curl is provided by openenv-base)
282
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
283
- CMD curl -f http://localhost:8000/health || exit 1
284
-
285
- # Note: EXPOSE 8000 already set by openenv-base
286
-
287
- # Run the FastAPI server (uvicorn installed by openenv-base)
288
- CMD ["uvicorn", "envs.openspiel_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
289
- DOCKERFILE_EOF
290
- echo "Created special OpenSpiel Dockerfile with C++ build process"
291
- echo "OpenSpiel builds can take 10-15 minutes due to C++ compilation"
292
- return # Skip the common parts since OpenSpiel has its own complete Dockerfile
293
- ;;
294
- esac
295
-
296
- # Add common parts
297
- cat >> "$CURRENT_STAGING_DIR/Dockerfile" << 'DOCKERFILE_EOF'
298
-
299
- # Copy only what's needed for this environment
300
- COPY src/core/ /app/src/core/
301
- COPY src/envs/ENV_NAME_PLACEHOLDER/ /app/src/envs/ENV_NAME_PLACEHOLDER/
302
-
303
- # Health check
304
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
305
- CMD curl -f http://localhost:8000/health || exit 1
306
-
307
- # Run the FastAPI server
308
- CMD ["uvicorn", "envs.ENV_NAME_PLACEHOLDER.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
309
- DOCKERFILE_EOF
310
-
311
- # Replace placeholder with actual environment name
312
- sed_in_place "s/ENV_NAME_PLACEHOLDER/$env_name/g" "$CURRENT_STAGING_DIR/Dockerfile"
313
- }
314
-
315
- create_environment_dockerfile "$ENV_NAME"
316
-
317
- # Add web interface support
318
- echo "ENV ENABLE_WEB_INTERFACE=true" >> $CURRENT_STAGING_DIR/Dockerfile
319
-
320
- # Create environment-specific README
321
- create_readme() {
322
- local env_name=$1
323
-
324
- # Capitalize first letter of environment name
325
- env_title=$(echo "$env_name" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')
326
-
327
- # Set environment-specific colors and emoji
328
- case $env_name in
329
- "atari_env")
330
- EMOJI="🕹️"
331
- COLOR_FROM="red"
332
- COLOR_TO="yellow"
333
- ;;
334
- "coding_env")
335
- EMOJI="💻"
336
- COLOR_FROM="blue"
337
- COLOR_TO="gray"
338
- ;;
339
- "openspiel_env")
340
- EMOJI="🎮"
341
- COLOR_FROM="purple"
342
- COLOR_TO="indigo"
343
- ;;
344
- "echo_env")
345
- EMOJI="🔊"
346
- COLOR_FROM="blue"
347
- COLOR_TO="gray"
348
- ;;
349
- "chat_env")
350
- EMOJI="💬"
351
- COLOR_FROM="blue"
352
- COLOR_TO="green"
353
- ;;
354
- "textarena_env")
355
- EMOJI="📜"
356
- COLOR_FROM="green"
357
- COLOR_TO="blue"
358
- ;;
359
- *)
360
- EMOJI="🐳"
361
- COLOR_FROM="blue"
362
- COLOR_TO="green"
363
- ;;
364
- esac
365
-
366
- cat > "$CURRENT_STAGING_DIR/README.md" << README_EOF
367
- ---
368
- title: ${env_title} Environment Server
369
- emoji: ${EMOJI}
370
- colorFrom: ${COLOR_FROM}
371
- colorTo: ${COLOR_TO}
372
- sdk: docker
373
- pinned: false
374
- app_port: 8000
375
- base_path: /web
376
- tags:
377
- - ${HUB_TAG}
378
- ---
379
-
380
- # ${env_title} Environment Server
381
-
382
- FastAPI server for ${env_name} environment powered by Meta's OpenEnv.
383
-
384
- ## About
385
-
386
- This Space provides a containerized environment for ${env_name} interactions.
387
- Built with FastAPI and OpenEnv framework.
388
-
389
- ## Web Interface
390
-
391
- This deployment includes an interactive web interface for exploring the environment:
392
- - **HumanAgent Interface**: Interact with the environment using a web form
393
- - **State Observer**: Real-time view of environment state and action history
394
- - **Live Updates**: WebSocket-based real-time updates
395
-
396
- Access the web interface at: \`/web\`
397
-
398
- README_EOF
399
-
400
- # Add environment-specific information
401
- case $env_name in
402
- "echo_env")
403
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
404
- ## Echo Environment
405
-
406
- Simple test environment that echoes back messages. Perfect for testing the OpenEnv APIs.
407
-
408
- ### Usage
409
- Send a POST request to `/step` with:
410
- ```json
411
- {
412
- "message": "Hello World"
413
- }
414
- ```
415
- README_EOF
416
- ;;
417
- "coding_env")
418
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
419
- ## Coding Environment
420
-
421
- Executes Python code in a sandboxed environment with safety checks.
422
-
423
- ### Usage
424
- Send a POST request to `/step` with:
425
- ```json
426
- {
427
- "code": "print('Hello World')"
428
- }
429
- ```
430
- README_EOF
431
- ;;
432
- "chat_env")
433
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
434
- ## Chat Environment
435
-
436
- Provides a chat-based interface for LLMs with tokenization support.
437
-
438
- ### Usage
439
- Send a POST request to `/step` with tokenized input:
440
- ```json
441
- {
442
- "tokens": [1, 2, 3, 4, 5]
443
- }
444
- ```
445
- README_EOF
446
- ;;
447
- "atari_env")
448
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
449
- ## Atari Environment
450
-
451
- Provides Atari 2600 games via the Arcade Learning Environment (ALE).
452
-
453
- ### Usage
454
- Send a POST request to `/step` with:
455
- ```json
456
- {
457
- "action_id": 0,
458
- "game_name": "pong"
459
- }
460
- ```
461
- README_EOF
462
- ;;
463
- "openspiel_env")
464
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
465
- ## OpenSpiel Environment
466
-
467
- Provides access to OpenSpiel games for multi-agent reinforcement learning.
468
-
469
- ### Usage
470
- Send a POST request to `/step` with:
471
- ```json
472
- {
473
- "action": {
474
- "action_id": 1
475
- }
476
- }
477
- ```
478
- README_EOF
479
- ;;
480
- "textarena_env")
481
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
482
- ## TextArena Environment
483
-
484
- Runs TextArena games such as Wordle, GuessTheNumber, or Chess through a unified HTTP API.
485
-
486
- ### Usage
487
- Send a POST request to `/step` with:
488
- ```json
489
- {
490
- "message": "raise shield"
491
- }
492
- ```
493
- README_EOF
494
- ;;
495
- *)
496
- cat >> "$CURRENT_STAGING_DIR/README.md" << 'README_EOF'
497
-
498
- ## API Documentation
499
-
500
- Visit `/docs` for interactive API documentation.
501
-
502
- ## Health Check
503
-
504
- The environment provides a health check endpoint at `/health`.
505
- README_EOF
506
- ;;
507
- esac
508
- }
509
-
510
- create_readme "$ENV_NAME"
511
- echo "📝 Created README and web interface support for HF Space"
512
-
513
- if $DRY_RUN; then
514
- echo "👀 Dry run enabled; skipping Hugging Face upload."
515
- exit 0
516
- fi
517
-
518
- echo "🔑 Ensuring Hugging Face authentication..."
519
-
520
- # Set up authentication based on environment
521
- TOKEN_ARGS=()
522
- if [ "$IS_GITHUB_ACTIONS" = true ]; then
523
- if [ -z "${HF_TOKEN:-}" ]; then
524
- echo "Error: HF_TOKEN secret is required in GitHub Actions" >&2
525
- echo "Please set the HF_TOKEN secret in repository settings" >&2
526
- exit 1
527
- fi
528
- echo "Using HF_TOKEN from GitHub Actions environment"
529
- # In CI, pass token directly to commands via --token flag
530
- TOKEN_ARGS=(--token "$HF_TOKEN")
531
- elif [ -n "${HF_TOKEN:-}" ]; then
532
- # If HF_TOKEN is set locally, use it
533
- echo "Using HF_TOKEN environment variable"
534
- TOKEN_ARGS=(--token "$HF_TOKEN")
535
- else
536
- # Interactive mode: check if user is authenticated
537
- if ! hf auth whoami >/dev/null 2>&1; then
538
- echo "Not authenticated. Please login to Hugging Face..."
539
- hf auth login
540
- if ! hf auth whoami >/dev/null 2>&1; then
541
- echo "Error: Hugging Face authentication failed" >&2
542
- exit 1
543
- fi
544
- fi
545
- fi
546
-
547
- # Verify authentication works (skip in CI if using token directly)
548
- if [ ${#TOKEN_ARGS[@]} -eq 0 ]; then
549
- if ! hf auth whoami >/dev/null 2>&1; then
550
- echo "Error: Not authenticated with Hugging Face" >&2
551
- echo "Run 'hf auth login' or set HF_TOKEN environment variable" >&2
552
- exit 1
553
- fi
554
- CURRENT_USER=$(hf auth whoami | head -n1 | tr -d '\n')
555
- echo "✅ Authenticated as: $CURRENT_USER"
556
- if [ "$CURRENT_USER" != "$HF_NAMESPACE" ]; then
557
- echo "⚠️ Deploying to namespace '$HF_NAMESPACE' (different from your user '$CURRENT_USER')"
558
- fi
559
- else
560
- echo "✅ Token configured for deployment"
561
- fi
562
-
563
- SPACE_REPO="${HF_NAMESPACE}/${ENV_NAME}${SPACE_SUFFIX}"
564
-
565
- # Get absolute path to staging directory
566
- if [ ! -d "$CURRENT_STAGING_DIR" ]; then
567
- echo "Error: Staging directory not found: $CURRENT_STAGING_DIR" >&2
568
- exit 1
569
- fi
570
- CURRENT_STAGING_DIR_ABS=$(cd "$CURRENT_STAGING_DIR" && pwd)
571
-
572
- # Determine privacy flag (only add --private if needed, default is public)
573
- PRIVATE_FLAG=""
574
- if [ "$PRIVATE" = true ]; then
575
- PRIVATE_FLAG="--private"
576
- fi
577
-
578
- echo "Creating space: $SPACE_REPO"
579
- echo "Command: hf repo create $SPACE_REPO --repo-type space --space-sdk docker --exist-ok $PRIVATE_FLAG ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"}"
580
- # create the space if it doesn't exist
581
- # Temporarily disable exit-on-error for this command
582
- set +e
583
- CREATE_OUTPUT=$(hf repo create "$SPACE_REPO" --repo-type space --space-sdk docker --exist-ok $PRIVATE_FLAG ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"} 2>&1)
584
- CREATE_EXIT_CODE=$?
585
- set -e
586
- if [ $CREATE_EXIT_CODE -ne 0 ]; then
587
- echo "❌ Space creation failed with exit code $CREATE_EXIT_CODE" >&2
588
- echo "Error output:" >&2
589
- echo "$CREATE_OUTPUT" >&2
590
- echo "" >&2
591
- fi
592
-
593
- echo "Uploading files to space: $SPACE_REPO"
594
- echo "Command: hf upload --repo-type=space $PRIVATE_FLAG ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"} $SPACE_REPO $CURRENT_STAGING_DIR_ABS"
595
- # upload the staged content (if repo doesn't exist, it will be created with the privacy setting)
596
- SPACE_UPLOAD_RESULT=$(hf upload --repo-type=space $PRIVATE_FLAG ${TOKEN_ARGS[@]+"${TOKEN_ARGS[@]}"} "$SPACE_REPO" "$CURRENT_STAGING_DIR_ABS" 2>&1)
597
- UPLOAD_EXIT_CODE=$?
598
- if [ $UPLOAD_EXIT_CODE -ne 0 ]; then
599
- echo "❌ Upload failed with exit code $UPLOAD_EXIT_CODE" >&2
600
- echo "Error output:" >&2
601
- echo "$SPACE_UPLOAD_RESULT" >&2
602
- echo "" >&2
603
- echo " Space: $SPACE_REPO" >&2
604
- echo " Staging dir: $CURRENT_STAGING_DIR_ABS" >&2
605
- echo " Files to upload:" >&2
606
- ls -la "$CURRENT_STAGING_DIR_ABS" >&2 || true
607
- exit 1
608
- fi
609
- # print the URL of the deployed space
610
- echo "✅ Upload completed for https://huggingface.co/spaces/$SPACE_REPO"
611
-
612
- # Cleanup the staging directory after successful deployment
613
- if [ -d "$CURRENT_STAGING_DIR_ABS" ]; then
614
- rm -rf "$CURRENT_STAGING_DIR_ABS"
615
- fi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/scripts/manage_hf_collection.py DELETED
@@ -1,296 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Hugging Face Collection Manager for OpenEnv
4
-
5
- This script automatically discovers Docker Spaces tagged with 'openenv' on Hugging Face
6
- and adds them to the Environment Hub collection if they're not already present.
7
-
8
- Usage:
9
- python scripts/manage_hf_collection.py [--dry-run] [--verbose]
10
-
11
- Environment Variables:
12
- HF_TOKEN: Required. Your Hugging Face API token with write access to collections.
13
- """
14
-
15
- import argparse
16
- import logging
17
- import os
18
- import sys
19
- from typing import Set, List
20
- from huggingface_hub import HfApi, list_spaces
21
- from huggingface_hub.utils import HfHubHTTPError
22
-
23
-
24
- # Constants
25
- COLLECTION_SLUG = "openenv/environment-hub-68f16377abea1ea114fa0743"
26
- TAG_FILTER = "openenv"
27
- SPACE_TYPE = "docker"
28
-
29
- # Configure logging
30
- logging.basicConfig(
31
- level=logging.INFO,
32
- format='%(asctime)s - %(levelname)s - %(message)s',
33
- datefmt='%Y-%m-%d %H:%M:%S'
34
- )
35
- logger = logging.getLogger(__name__)
36
-
37
-
38
- def setup_api() -> HfApi:
39
- """
40
- Initialize and authenticate the Hugging Face API client.
41
-
42
- Returns:
43
- HfApi: Authenticated API client
44
-
45
- Raises:
46
- SystemExit: If HF_TOKEN is not set
47
- """
48
- hf_token = os.environ.get('HF_TOKEN')
49
-
50
- if not hf_token:
51
- logger.error("HF_TOKEN environment variable is not set!")
52
- logger.error("Please set it with: export HF_TOKEN=your_token_here")
53
- sys.exit(1)
54
-
55
- logger.info("Authenticating with Hugging Face...")
56
- api = HfApi(token=hf_token)
57
-
58
- try:
59
- whoami = api.whoami()
60
- logger.info(f"✓ Authenticated as: {whoami['name']}")
61
- except Exception as e:
62
- logger.error(f"Failed to authenticate with Hugging Face: {e}")
63
- sys.exit(1)
64
-
65
- return api
66
-
67
-
68
- def get_collection_spaces(api: HfApi) -> Set[str]:
69
- """
70
- Retrieve the list of spaces currently in the Environment Hub collection.
71
-
72
- Args:
73
- api: Authenticated HfApi client
74
-
75
- Returns:
76
- Set of space IDs (in format "owner/space-name")
77
- """
78
- logger.info(f"Fetching current collection: {COLLECTION_SLUG}")
79
-
80
- try:
81
- collection = api.get_collection(COLLECTION_SLUG)
82
-
83
- # Extract space IDs from collection items
84
- space_ids = set()
85
- for item in collection.items:
86
- if item.item_type == "space":
87
- space_ids.add(item.item_id)
88
-
89
- logger.info(f"✓ Found {len(space_ids)} spaces in collection")
90
- return space_ids
91
-
92
- except HfHubHTTPError as e:
93
- if e.response.status_code == 404:
94
- logger.error(f"Collection not found: {COLLECTION_SLUG}")
95
- logger.error("Please ensure the collection exists and you have access to it")
96
- else:
97
- logger.error(f"Error fetching collection: {e}")
98
- sys.exit(1)
99
- except Exception as e:
100
- logger.error(f"Unexpected error fetching collection: {e}")
101
- sys.exit(1)
102
-
103
-
104
- def discover_openenv_spaces(api: HfApi) -> List[str]:
105
- """
106
- Search for all Docker Spaces tagged with 'openenv'.
107
-
108
- Args:
109
- api: Authenticated HfApi client
110
-
111
- Returns:
112
- List of space IDs (in format "owner/space-name")
113
- """
114
- logger.info(f"Searching for Docker Spaces with tag '{TAG_FILTER}'...")
115
-
116
- try:
117
- # List all spaces with the openenv tag using search parameter
118
- spaces = list(list_spaces(
119
- search=TAG_FILTER,
120
- full=False,
121
- sort="trending_score",
122
- direction=-1
123
- ))
124
-
125
- # Filter for Docker spaces with the openenv tag
126
- # Note: search may return spaces that mention 'openenv' in description too,
127
- # so we need to verify the tag is actually present
128
- docker_spaces_with_tag = []
129
- for space in spaces:
130
- # Get full space info to check tags
131
- try:
132
- space_info = api.space_info(space.id)
133
- # Check if it's a Docker space and has the openenv tag
134
- if (hasattr(space_info, 'sdk') and space_info.sdk == 'docker' and
135
- hasattr(space_info, 'tags') and TAG_FILTER in space_info.tags and
136
- space_info.runtime.stage != "RUNTIME_ERROR"):
137
- docker_spaces_with_tag.append(space.id)
138
- except Exception as e:
139
- logger.warning(f"Could not fetch info for space {space.id}: {e}")
140
- continue
141
-
142
- logger.info(f"✓ Discovered {len(docker_spaces_with_tag)} Docker spaces with tag '{TAG_FILTER}'")
143
-
144
- return docker_spaces_with_tag
145
-
146
- except Exception as e:
147
- logger.error(f"Error discovering spaces: {e}")
148
- sys.exit(1)
149
-
150
-
151
- def add_spaces_to_collection(
152
- api: HfApi,
153
- space_ids: List[str],
154
- dry_run: bool = False
155
- ) -> int:
156
- """
157
- Add new spaces to the Environment Hub collection.
158
-
159
- Args:
160
- api: Authenticated HfApi client
161
- space_ids: List of space IDs to add
162
- dry_run: If True, only simulate the addition without making changes
163
-
164
- Returns:
165
- Number of spaces added (or would be added in dry-run mode)
166
- """
167
- if not space_ids:
168
- logger.info("No new spaces to add")
169
- return 0
170
-
171
- added_count = 0
172
- failed_count = 0
173
-
174
- for space_id in space_ids:
175
- if dry_run:
176
- logger.info(f"[DRY RUN] Would add space: {space_id}")
177
- added_count += 1
178
- else:
179
- try:
180
- logger.info(f"Adding space to collection: {space_id}")
181
- api.add_collection_item(
182
- collection_slug=COLLECTION_SLUG,
183
- item_id=space_id,
184
- item_type="space"
185
- )
186
- logger.info(f"✓ Successfully added: {space_id}")
187
- added_count += 1
188
- except HfHubHTTPError as e:
189
- if e.response.status_code == 409:
190
- # Space already in collection (race condition)
191
- logger.warning(f"Space already in collection: {space_id}")
192
- else:
193
- logger.error(f"Failed to add {space_id}: {e}")
194
- failed_count += 1
195
- except Exception as e:
196
- logger.error(f"Unexpected error adding {space_id}: {e}")
197
- failed_count += 1
198
-
199
- if failed_count > 0:
200
- logger.warning(f"Failed to add {failed_count} spaces")
201
-
202
- return added_count
203
-
204
-
205
- def main():
206
- """Main execution function."""
207
- parser = argparse.ArgumentParser(
208
- description="Manage Hugging Face Environment Hub collection for OpenEnv spaces",
209
- formatter_class=argparse.RawDescriptionHelpFormatter,
210
- epilog="""
211
- Examples:
212
- # Run in dry-run mode to preview changes
213
- python scripts/manage_hf_collection.py --dry-run --verbose
214
-
215
- # Run for real to add spaces to collection
216
- python scripts/manage_hf_collection.py
217
-
218
- # View verbose output
219
- python scripts/manage_hf_collection.py --verbose
220
-
221
- Environment Variables:
222
- HF_TOKEN: Required. Your Hugging Face API token.
223
- """
224
- )
225
-
226
- parser.add_argument(
227
- '--dry-run',
228
- action='store_true',
229
- help='Preview changes without modifying the collection'
230
- )
231
-
232
- parser.add_argument(
233
- '--verbose',
234
- action='store_true',
235
- help='Enable verbose logging output'
236
- )
237
-
238
- args = parser.parse_args()
239
-
240
- # Set logging level
241
- if args.verbose:
242
- logger.setLevel(logging.DEBUG)
243
- logger.debug("Verbose logging enabled")
244
-
245
- if args.dry_run:
246
- logger.info("=" * 60)
247
- logger.info("DRY RUN MODE - No changes will be made")
248
- logger.info("=" * 60)
249
-
250
- # Step 1: Setup API
251
- api = setup_api()
252
-
253
- # Step 2: Get current collection spaces
254
- current_spaces = get_collection_spaces(api)
255
-
256
- if args.verbose:
257
- logger.debug(f"Current spaces in collection: {sorted(current_spaces)}")
258
-
259
- # Step 3: Discover all openenv spaces
260
- discovered_spaces = discover_openenv_spaces(api)
261
-
262
- if args.verbose:
263
- logger.debug(f"Discovered spaces: {sorted(discovered_spaces)}")
264
-
265
- # Step 4: Find new spaces not yet in collection
266
- new_spaces = [s for s in discovered_spaces if s not in current_spaces]
267
-
268
- logger.info("=" * 60)
269
- logger.info(f"Summary:")
270
- logger.info(f" Total spaces in collection: {len(current_spaces)}")
271
- logger.info(f" Total spaces discovered: {len(discovered_spaces)}")
272
- logger.info(f" New spaces to add: {len(new_spaces)}")
273
- logger.info("=" * 60)
274
-
275
- if new_spaces:
276
- logger.info(f"New spaces found:")
277
- for space in new_spaces:
278
- logger.info(f" - {space}")
279
-
280
- # Step 5: Add new spaces to collection
281
- added_count = add_spaces_to_collection(api, new_spaces, dry_run=args.dry_run)
282
-
283
- # Final summary
284
- logger.info("=" * 60)
285
- if args.dry_run:
286
- logger.info(f"[DRY RUN] Would add {added_count} new spaces to collection")
287
- else:
288
- logger.info(f"✓ Successfully added {added_count} new spaces to collection")
289
- logger.info("=" * 60)
290
-
291
- logger.info(f"Collection URL: https://huggingface.co/collections/{COLLECTION_SLUG}")
292
-
293
-
294
- if __name__ == "__main__":
295
- main()
296
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/scripts/prepare_hf_deployment.sh DELETED
@@ -1,170 +0,0 @@
1
- #!/bin/bash
2
-
3
- # OpenEnv Hugging Face Deployment Preparation Script
4
- # This script prepares files for deployment to Hugging Face Spaces
5
-
6
- set -e
7
-
8
- # Cross-platform sed in-place editing
9
- # BSD sed (macOS) requires -i '', GNU sed (Linux) requires -i
10
- sed_inplace() {
11
- if sed --version >/dev/null 2>&1; then
12
- # GNU sed
13
- sed -i "$@"
14
- else
15
- # BSD sed
16
- sed -i '' "$@"
17
- fi
18
- }
19
-
20
- ENV_NAME="$1"
21
- BASE_IMAGE_SHA="$2"
22
- STAGING_DIR="hf-staging"
23
-
24
- if [ -z "$ENV_NAME" ]; then
25
- echo "Error: Environment name is required"
26
- exit 1
27
- fi
28
-
29
- # Validate environment name
30
- ENV_NAMES="$ENV_NAME"
31
-
32
- # Set base image reference (using GHCR)
33
- if [ -n "$BASE_IMAGE_SHA" ]; then
34
- BASE_IMAGE_REF="ghcr.io/meta-pytorch/openenv-base:$BASE_IMAGE_SHA"
35
- echo "Using specific SHA for openenv-base: $BASE_IMAGE_SHA"
36
- else
37
- BASE_IMAGE_REF="ghcr.io/meta-pytorch/openenv-base:latest"
38
- echo "Using latest tag for openenv-base"
39
- fi
40
-
41
- echo "Preparing $ENV_NAME environment for deployment..."
42
-
43
- # Create staging directory
44
- CURRENT_STAGING_DIR="${STAGING_DIR}_${ENV_NAME}"
45
- mkdir -p $CURRENT_STAGING_DIR/src/core
46
- mkdir -p $CURRENT_STAGING_DIR/src/envs/$ENV_NAME
47
-
48
- # Copy core files
49
- cp -r src/core/* $CURRENT_STAGING_DIR/src/core/
50
- echo "Copied core files"
51
-
52
- # Copy environment files
53
- cp -r src/envs/$ENV_NAME/* $CURRENT_STAGING_DIR/src/envs/$ENV_NAME/
54
- echo "Copied $ENV_NAME environment files"
55
-
56
- # Copy and modify the static Dockerfile from the environment
57
- create_environment_dockerfile() {
58
- local env_name=$1
59
- local dockerfile_path="src/envs/$env_name/server/Dockerfile"
60
- local prepare_script="src/envs/$env_name/server/prepare_hf.sh"
61
-
62
- if [ ! -f "$dockerfile_path" ]; then
63
- echo "Error: Dockerfile not found at $dockerfile_path"
64
- exit 1
65
- fi
66
-
67
- # Copy the static Dockerfile
68
- cp "$dockerfile_path" "$CURRENT_STAGING_DIR/Dockerfile"
69
- echo "Copied static Dockerfile from $dockerfile_path"
70
-
71
- # Check if environment has custom HF preparation script
72
- if [ -f "$prepare_script" ]; then
73
- echo "Found custom HF preparation script, executing..."
74
- chmod +x "$prepare_script"
75
- "$prepare_script" "$CURRENT_STAGING_DIR/Dockerfile" "$BASE_IMAGE_REF"
76
- else
77
- # Standard Dockerfile modification: replace ARG BASE_IMAGE with FROM
78
- sed_inplace "s|ARG BASE_IMAGE=.*||g" "$CURRENT_STAGING_DIR/Dockerfile"
79
- sed_inplace "s|FROM \${BASE_IMAGE}|FROM $BASE_IMAGE_REF|g" "$CURRENT_STAGING_DIR/Dockerfile"
80
- echo "Modified Dockerfile with base image: $BASE_IMAGE_REF"
81
- fi
82
-
83
- # Add web interface support before the final CMD
84
- # Use awk for cross-platform compatibility
85
- awk '/^CMD \[/{print "ENV ENABLE_WEB_INTERFACE=true\n"; print; next} 1' "$CURRENT_STAGING_DIR/Dockerfile" > "$CURRENT_STAGING_DIR/Dockerfile.tmp"
86
- mv "$CURRENT_STAGING_DIR/Dockerfile.tmp" "$CURRENT_STAGING_DIR/Dockerfile"
87
- echo "Enabled web interface"
88
- }
89
-
90
- create_environment_dockerfile $ENV_NAME
91
-
92
- # Copy and prepend HF-specific intro to README
93
- create_readme() {
94
- local env_name=$1
95
- local readme_source="src/envs/$env_name/README.md"
96
-
97
- if [ ! -f "$readme_source" ]; then
98
- echo "Error: README not found at $readme_source"
99
- exit 1
100
- fi
101
-
102
- # Check if README already has HF front matter
103
- if head -n 1 "$readme_source" | grep -q "^---$"; then
104
- echo "README has HF front matter, inserting HF deployment section after it"
105
-
106
- # Find the line number of the closing --- (second occurrence)
107
- local closing_line=$(grep -n "^---$" "$readme_source" | sed -n '2p' | cut -d: -f1)
108
-
109
- if [ -z "$closing_line" ]; then
110
- echo "Error: Could not find closing --- in front matter"
111
- exit 1
112
- fi
113
-
114
- # Split the README: front matter + rest
115
- head -n "$closing_line" "$readme_source" > "$CURRENT_STAGING_DIR/README.md"
116
-
117
- # Add HF-specific deployment info right after front matter
118
- cat >> $CURRENT_STAGING_DIR/README.md << 'README_EOF'
119
-
120
- ## 🚀 Hugging Face Space Deployment
121
-
122
- This is a Hugging Face Space deployment of the OpenEnv environment. It includes:
123
-
124
- - **Web Interface** at `/web` - Interactive UI for exploring the environment
125
- - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
126
- - **Health Check** at `/health` - Container health monitoring
127
-
128
- ### Connecting from Code
129
-
130
- ```python
131
- from envs.ENV_NAME_PLACEHOLDER import ENV_CLASS_PLACEHOLDER
132
-
133
- # Connect to this HF Space
134
- env = ENV_CLASS_PLACEHOLDER(base_url="https://huggingface.co/spaces/openenv/ENV_NAME_PLACEHOLDER")
135
-
136
- # Use the environment
137
- result = env.reset()
138
- result = env.step(action)
139
- ```
140
-
141
- For full documentation, see the [OpenEnv repository](https://github.com/meta-pytorch/OpenEnv).
142
-
143
- README_EOF
144
-
145
- # Append the rest of the original README (skip front matter)
146
- tail -n "+$((closing_line + 1))" "$readme_source" >> "$CURRENT_STAGING_DIR/README.md"
147
- else
148
- echo "Error: README missing HF front matter at $readme_source"
149
- echo "Please add YAML front matter to the environment README"
150
- exit 1
151
- fi
152
-
153
- # Set environment-specific class name
154
- case $env_name in
155
- "echo_env") ENV_CLASS="EchoEnv" ;;
156
- "coding_env") ENV_CLASS="CodingEnv" ;;
157
- "chat_env") ENV_CLASS="ChatEnv" ;;
158
- "atari_env") ENV_CLASS="AtariEnv" ;;
159
- "openspiel_env") ENV_CLASS="OpenSpielEnv" ;;
160
- *) ENV_CLASS="Env" ;;
161
- esac
162
-
163
- # Replace placeholders (cross-platform)
164
- sed_inplace "s/ENV_NAME_PLACEHOLDER/$env_name/g" "$CURRENT_STAGING_DIR/README.md"
165
- sed_inplace "s/ENV_CLASS_PLACEHOLDER/$ENV_CLASS/g" "$CURRENT_STAGING_DIR/README.md"
166
- }
167
-
168
- create_readme $ENV_NAME
169
- echo "Copied and enhanced README for HF Space"
170
- echo "Completed preparation for $ENV_NAME environment"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/scripts/setup_shared_gitea.sh DELETED
@@ -1,83 +0,0 @@
1
- #!/bin/bash
2
- # Setup script for shared Gitea instance
3
- # This script starts Gitea, waits for it to be ready, and creates the admin user
4
- # Requires: .env file with GITEA_USERNAME and GITEA_PASSWORD
5
-
6
- set -e
7
-
8
- # Load credentials from .env file
9
- if [ -f .env ]; then
10
- export $(cat .env | grep -E '^(GITEA_USERNAME|GITEA_PASSWORD)=' | xargs)
11
- else
12
- echo "❌ Error: .env file not found"
13
- echo " Please copy .env.example to .env and configure credentials"
14
- exit 1
15
- fi
16
-
17
- echo "====================================="
18
- echo "Setting up shared Gitea instance"
19
- echo "====================================="
20
- echo
21
-
22
- # Start Gitea with docker-compose
23
- echo "1. Starting Gitea container..."
24
- docker-compose -f src/envs/git_env/docker-compose.gitea.yml up -d
25
-
26
- # Wait for Gitea to be healthy
27
- echo "2. Waiting for Gitea to be ready..."
28
- timeout=60
29
- elapsed=0
30
- while [ $elapsed -lt $timeout ]; do
31
- if docker exec openenv-gitea curl -sf http://localhost:3000/ > /dev/null 2>&1; then
32
- echo " ✓ Gitea is ready!"
33
- break
34
- fi
35
- echo " Waiting... (${elapsed}s/${timeout}s)"
36
- sleep 2
37
- elapsed=$((elapsed + 2))
38
- done
39
-
40
- if [ $elapsed -ge $timeout ]; then
41
- echo " ✗ Timeout waiting for Gitea"
42
- exit 1
43
- fi
44
-
45
- # Initialize Gitea (POST to root URL)
46
- echo "3. Initializing Gitea configuration..."
47
- docker exec openenv-gitea curl -s -X POST \
48
- -H "Content-Type: application/x-www-form-urlencoded" \
49
- -d "db_type=sqlite3" \
50
- -d "db_path=%2Fdata%2Fgitea%2Fgitea.db" \
51
- -d "app_name=Gitea" \
52
- -d "repo_root_path=%2Fdata%2Fgit%2Frepositories" \
53
- -d "run_user=git" \
54
- -d "domain=gitea" \
55
- -d "http_port=3000" \
56
- -d "app_url=http%3A%2F%2Fgitea%3A3000%2F" \
57
- -d "log_root_path=%2Fdata%2Fgitea%2Flog" \
58
- -d "offline_mode=on" \
59
- http://localhost:3000/ > /dev/null || echo " (Config may already exist)"
60
-
61
- # Create admin user
62
- echo "4. Creating admin user ($GITEA_USERNAME)..."
63
- docker exec openenv-gitea su git -c \
64
- "gitea admin user create --username $GITEA_USERNAME --password $GITEA_PASSWORD --email ${GITEA_USERNAME}@local.env --admin" \
65
- 2>&1 | grep -q "already exists" && echo " ✓ User already exists" || echo " ✓ User created"
66
-
67
- echo
68
- echo "====================================="
69
- echo "✓ Gitea setup complete!"
70
- echo "====================================="
71
- echo
72
- echo "Gitea is now available at:"
73
- echo " - Web UI: http://localhost:3000"
74
- echo " - From containers: http://gitea:3000"
75
- echo
76
- echo "Admin credentials are configured from .env file"
77
- echo
78
- echo "To stop Gitea:"
79
- echo " docker-compose -f src/envs/git_env/docker-compose.gitea.yml down"
80
- echo
81
- echo "To remove all data:"
82
- echo " docker-compose -f src/envs/git_env/docker-compose.gitea.yml down -v"
83
- echo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """EnvTorch: Standardized agentic execution environments."""
 
 
 
 
 
 
 
 
openenv/src/core/README.md DELETED
@@ -1,180 +0,0 @@
1
- # <img width="35" height="35" alt="image" src="https://github.com/user-attachments/assets/2700a971-e5d6-4036-b03f-2f89c9791609" /> OpenEnv: Agentic Execution Environments
2
-
3
- An e2e framework for creating, deploying and using isolated execution environments for agentic RL training, built using Gymnasium style simple APIs. OpenEnv provides a standard for interacting with agentic execution environments via simple Gymnasium style APIs - step(), reset(), state(). Users of agentic execution environments can interact with the environment during RL training loops using these simple APIs.
4
-
5
- In addition to making it easier for researchers and RL framework writers, we also provide tools for environment creators making it easier for them to create richer environments and make them available over familiar protocols like HTTP and packaged using canonical technologies like docker. Environment creators can use the OpenEnv framework to create environments that are isolated, secure, and easy to deploy and use.
6
-
7
-
8
- ## Overview
9
- `openenv-core` provides the foundational building blocks for creating and interacting with containerized environments over HTTP. It enables you to build agent environments that can be deployed as Docker containers and accessed via a simple HTTP API.
10
-
11
- > ⚠️ **Early Development Warning** OpenEnv is currently in an experimental
12
- > stage. You should expect bugs, incomplete features, and APIs that may change
13
- > in future versions. The project welcomes bugfixes, but to make sure things are
14
- > well coordinated you should discuss any significant change before starting the
15
- > work. It's recommended that you signal your intention to contribute in the
16
- > issue tracker, either by filing a new issue or by claiming an existing one.
17
-
18
-
19
- # OpenEnv Core
20
-
21
- Core components for OpenEnv - a framework for building HTTP-based agentic environments.
22
-
23
- ## Features
24
-
25
- - **HTTPEnvClient**: Generic HTTP client for interacting with remote environments
26
- - **HTTPEnvServer**: FastAPI-based server wrapper for exposing environments over HTTP
27
- - **Container Providers**: Pluggable architecture for running containers (Docker, Kubernetes, etc.)
28
- - **Type System**: Strongly-typed Action/Observation/State interfaces
29
- - **Web Interface**: Optional web UI for interacting with environments
30
-
31
- ## Installation
32
-
33
- ```bash
34
- pip install openenv-core
35
- ```
36
-
37
- For development:
38
- ```bash
39
- pip install openenv-core[dev]
40
- ```
41
-
42
- ## Quick Start
43
-
44
- ### Creating an Environment Client
45
-
46
- ```python
47
- from openenv_core import HTTPEnvClient, StepResult
48
- from dataclasses import dataclass
49
-
50
- @dataclass
51
- class MyAction:
52
- text: str
53
-
54
- @dataclass
55
- class MyObservation:
56
- response: str
57
-
58
- class MyEnvClient(HTTPEnvClient[MyAction, MyObservation]):
59
- def _step_payload(self, action: MyAction) -> dict:
60
- return {"text": action.text}
61
-
62
- def _parse_result(self, payload: dict) -> StepResult[MyObservation]:
63
- obs_data = payload["observation"]
64
- return StepResult(
65
- observation=MyObservation(**obs_data),
66
- reward=payload.get("reward"),
67
- done=payload.get("done", False)
68
- )
69
-
70
- def _parse_state(self, payload: dict) -> Any:
71
- return payload
72
-
73
- # Use with Docker
74
- env = MyEnvClient.from_docker_image("my-env:latest")
75
- result = env.reset()
76
- step_result = env.step(MyAction(text="hello"))
77
- env.close()
78
- ```
79
-
80
- ### Creating an Environment Server
81
-
82
- ```python
83
- from openenv_core.env_server import Environment, HTTPEnvServer, create_app
84
- from dataclasses import dataclass
85
-
86
- @dataclass
87
- class MyAction:
88
- text: str
89
-
90
- @dataclass
91
- class MyObservation:
92
- response: str
93
- reward: float = 0.0
94
- done: bool = False
95
-
96
- class MyEnvironment(Environment):
97
- def reset(self) -> MyObservation:
98
- return MyObservation(response="Ready")
99
-
100
- def step(self, action: MyAction) -> MyObservation:
101
- return MyObservation(
102
- response=f"Echo: {action.text}",
103
- reward=1.0,
104
- done=False
105
- )
106
-
107
- # Create FastAPI app
108
- env = MyEnvironment()
109
- app = create_app(env, MyAction, MyObservation)
110
-
111
- # Run with: uvicorn module:app --host 0.0.0.0 --port 8000
112
- ```
113
-
114
- ## Container Providers
115
-
116
- OpenEnv Core supports multiple container providers:
117
-
118
- ### Local Docker Provider
119
-
120
- ```python
121
- from openenv_core.containers.runtime import LocalDockerProvider
122
-
123
- provider = LocalDockerProvider()
124
- base_url = provider.start_container("my-env:latest")
125
- provider.wait_for_ready(base_url)
126
- # Use environment...
127
- provider.stop_container()
128
- ```
129
-
130
- ### Kubernetes Provider (Coming Soon)
131
-
132
- ```python
133
- from openenv_core.containers.runtime import KubernetesProvider
134
-
135
- provider = KubernetesProvider(namespace="envs")
136
- base_url = provider.start_container("my-env:latest")
137
- # Use environment...
138
- provider.stop_container()
139
- ```
140
-
141
-
142
- ## API Reference
143
-
144
- ### HTTPEnvClient
145
-
146
- Base class for environment clients with these abstract methods:
147
-
148
- - `_step_payload(action)`: Convert action to JSON
149
- - `_parse_result(payload)`: Parse response to StepResult
150
- - `_parse_state(payload)`: Parse state response
151
-
152
- ### HTTPEnvServer
153
-
154
- Server wrapper with these methods:
155
-
156
- - `register_routes(app)`: Register endpoints on FastAPI app
157
- - `_deserialize_action(data)`: Convert JSON to Action
158
- - `_serialize_observation(obs)`: Convert Observation to JSON
159
-
160
- ### Environment Interface
161
-
162
- Base interface for environment implementations:
163
-
164
- - `reset()`: Reset environment and return initial observation
165
- - `step(action)`: Execute action and return observation
166
- - `state`: Property returning current environment state
167
-
168
- ## License
169
-
170
- This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.
171
-
172
- ## Contributing
173
-
174
- Contributions are welcome! Please see the main OpenEnv repository for contribution guidelines.
175
-
176
- ## Links
177
-
178
- - **Homepage**: https://github.com/meta-pytorch/OpenEnv
179
- - **Documentation**: https://github.com/meta-pytorch/OpenEnv/blob/main/README.md
180
- - **Bug Tracker**: https://github.com/meta-pytorch/OpenEnv/issues
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/__init__.py DELETED
@@ -1,19 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Core components for agentic environments."""
8
-
9
- # Re-export main components from submodules for convenience
10
- from .env_server import *
11
- from .client_types import StepResult
12
- from .http_env_client import HTTPEnvClient
13
-
14
- # Note: MCP module doesn't export anything yet
15
-
16
- __all__ = [
17
- "HTTPEnvClient",
18
- "StepResult",
19
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/client_types.py DELETED
@@ -1,22 +0,0 @@
1
- # Type definitions for EnvTorch
2
- from dataclasses import dataclass
3
- from typing import Any, Generic, Optional, TypeVar
4
-
5
- # Generic type for observations
6
- ObsT = TypeVar("ObsT") # TypeVar for typehinting in IDEs
7
-
8
-
9
- @dataclass
10
- class StepResult(Generic[ObsT]):
11
- """
12
- Represents the result of one environment step.
13
-
14
- Attributes:
15
- observation: The environment's observation after the action.
16
- reward: Scalar reward for this step (optional).
17
- done: Whether the episode is finished.
18
- """
19
-
20
- observation: ObsT
21
- reward: Optional[float] = None
22
- done: bool = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/containers/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Container management for environment servers."""
 
 
 
 
 
 
 
 
openenv/src/core/containers/images/Dockerfile DELETED
@@ -1,61 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- #
8
- # OpenEnv Base Image
9
- #
10
- # This is the standard base image for all OpenEnv environment servers.
11
- # It includes the minimal dependencies needed to run HTTP environment servers
12
- # and uv for fast dependency management.
13
- #
14
- # Build from repo root: docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
15
- # Tag: docker tag openenv-base:latest openenv-base:0.2.0
16
- #
17
-
18
- FROM ghcr.io/astral-sh/uv:0.5.27-python3.11-bookworm-slim AS builder
19
-
20
- # Set working directory
21
- WORKDIR /app
22
-
23
- # Copy core pyproject.toml and lockfile for dependency installation
24
- COPY src/core/pyproject.toml src/core/uv.lock* ./
25
-
26
- # Install core dependencies using uv with cache mount
27
- RUN --mount=type=cache,target=/root/.cache/uv \
28
- uv pip install --system -r pyproject.toml
29
-
30
- # Final runtime stage
31
- FROM python:3.11-slim
32
-
33
- # Set metadata
34
- LABEL maintainer="OpenEnv Team"
35
- LABEL description="Base image for OpenEnv based environment servers with uv"
36
- LABEL version="0.2.0"
37
-
38
- # Install system dependencies
39
- RUN apt-get update && apt-get install -y --no-install-recommends \
40
- curl \
41
- ca-certificates \
42
- && rm -rf /var/lib/apt/lists/*
43
-
44
- # Copy uv from builder
45
- COPY --from=builder /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
46
-
47
- # Copy installed Python packages from builder
48
- COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
49
-
50
- # Set working directory
51
- WORKDIR /app
52
-
53
- # Default environment variables
54
- ENV PYTHONPATH=/app/src
55
- ENV PYTHONUNBUFFERED=1
56
- ENV UV_SYSTEM_PYTHON=1
57
-
58
- # Default expose port (can be overridden)
59
- EXPOSE 8000
60
-
61
- # Note: CMD should be specified in child Dockerfiles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/containers/images/README.md DELETED
@@ -1,92 +0,0 @@
1
- # OpenEnv Base Image
2
-
3
- Standard base image for all OpenEnv environment servers.
4
-
5
- ## What's Included
6
-
7
- | Layer | Size | Contents |
8
- |-------|------|----------|
9
- | python:3.11-slim | 200 MB | Base Python runtime |
10
- | + Dependencies | 100 MB | FastAPI, uvicorn, requests |
11
- | **Total** | **~300 MB** | Ready for environment servers |
12
-
13
- ## Image Sizes
14
-
15
- ```
16
- openenv-base:latest 300 MB (python + fastapi + uvicorn)
17
- ```
18
- echo-env:latest 500 MB (python + fastapi + uvicorn + app)
19
- coding-env:latest 520 MB (python + fastapi + uvicorn + app + tools)
20
- another-env:latest 510 MB (python + fastapi + uvicorn + app)
21
- ---
22
- Total: 1.5 GB (with lots of duplication)
23
- ```
24
-
25
- ### With Base Images (✅ Solution)
26
- ```
27
- openenv-base:latest 300 MB (python + fastapi + uvicorn)
28
- echo-env:latest 50 MB (app only, uses base)
29
- coding-env:latest 70 MB (app + tools, uses base)
30
- another-env:latest 45 MB (app only, uses base)
31
- ---
32
- Total: 465 MB (base shared, minimal duplication)
33
- ```
34
-
35
- ## Building the Base Image
36
-
37
- ```bash
38
- # From project root
39
- docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
40
- ```
41
-
42
- ## Usage in Environment Dockerfiles
43
-
44
- Each environment Dockerfile should start with:
45
-
46
- ```dockerfile
47
- FROM openenv-base:latest
48
-
49
- # Copy only environment-specific files
50
- COPY src/core/ /app/src/core/
51
- COPY src/envs/my_env/ /app/src/envs/my_env/
52
-
53
- # Run the server
54
- CMD ["uvicorn", "envs.my_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
55
- ```
56
-
57
- ## Base Image Contents
58
-
59
- - Python 3.11-slim
60
- - FastAPI >= 0.104.0
61
- - Uvicorn >= 0.24.0
62
- - Requests >= 2.25.0
63
- - curl (for health checks)
64
-
65
- ## Example: Building Echo Environment
66
-
67
- ```bash
68
- # Step 1: Build base image (do this once)
69
- docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
70
-
71
- # Step 2: Build echo environment (uses base)
72
- docker build -t echo-env:latest -f src/envs/echo_env/server/Dockerfile .
73
-
74
- # Step 3: Run echo environment
75
- docker run -p 8000:8000 echo-env:latest
76
- ```
77
-
78
- ## Updating the Base
79
-
80
- When dependencies need updating:
81
-
82
- 1. Update `src/core/containers/images/Dockerfile`
83
- 2. Rebuild base image
84
- 3. Rebuild all environment images (they'll use new base)
85
-
86
- ```bash
87
- # Update base
88
- docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
89
-
90
- # Rebuild environments (they automatically use new base)
91
- docker build -t echo-env:latest -f src/envs/echo_env/server/Dockerfile .
92
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/containers/runtime/__init__.py DELETED
@@ -1,15 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Container runtime providers."""
8
-
9
- from .providers import ContainerProvider, KubernetesProvider, LocalDockerProvider
10
-
11
- __all__ = [
12
- "ContainerProvider",
13
- "LocalDockerProvider",
14
- "KubernetesProvider",
15
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/containers/runtime/providers.py DELETED
@@ -1,293 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Container provider abstractions for running environment servers.
9
-
10
- This module provides a pluggable architecture for different container providers
11
- (local Docker, Kubernetes, cloud providers, etc.) to be used with HTTPEnvClient.
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- from abc import ABC, abstractmethod
17
- from typing import Any, Dict, Optional
18
-
19
-
20
- class ContainerProvider(ABC):
21
- """
22
- Abstract base class for container providers.
23
-
24
- Providers implement this interface to support different container platforms:
25
- - LocalDockerProvider: Runs containers on local Docker daemon
26
- - KubernetesProvider: Runs containers in Kubernetes cluster
27
- - FargateProvider: Runs containers on AWS Fargate
28
- - CloudRunProvider: Runs containers on Google Cloud Run
29
-
30
- The provider manages a single container lifecycle and provides the base URL
31
- for connecting to it.
32
-
33
- Example:
34
- >>> provider = LocalDockerProvider()
35
- >>> base_url = provider.start_container("echo-env:latest")
36
- >>> print(base_url) # http://localhost:8000
37
- >>> # Use the environment via base_url
38
- >>> provider.stop_container()
39
- """
40
-
41
- @abstractmethod
42
- def start_container(
43
- self,
44
- image: str,
45
- port: Optional[int] = None,
46
- env_vars: Optional[Dict[str, str]] = None,
47
- **kwargs: Any,
48
- ) -> str:
49
- """
50
- Start a container from the specified image.
51
-
52
- Args:
53
- image: Container image name (e.g., "echo-env:latest")
54
- port: Port to expose (if None, provider chooses)
55
- env_vars: Environment variables to pass to container
56
- **kwargs: Provider-specific options
57
-
58
- Returns:
59
- Base URL to connect to the container (e.g., "http://localhost:8000")
60
-
61
- Raises:
62
- RuntimeError: If container fails to start
63
- """
64
- pass
65
-
66
- @abstractmethod
67
- def stop_container(self) -> None:
68
- """
69
- Stop and remove the running container.
70
-
71
- This cleans up the container that was started by start_container().
72
- """
73
- pass
74
-
75
- @abstractmethod
76
- def wait_for_ready(self, base_url: str, timeout_s: float = 30.0) -> None:
77
- """
78
- Wait for the container to be ready to accept requests.
79
-
80
- This typically polls the /health endpoint until it returns 200.
81
-
82
- Args:
83
- base_url: Base URL of the container
84
- timeout_s: Maximum time to wait
85
-
86
- Raises:
87
- TimeoutError: If container doesn't become ready in time
88
- """
89
- pass
90
-
91
-
92
- class LocalDockerProvider(ContainerProvider):
93
- """
94
- Container provider for local Docker daemon.
95
-
96
- This provider runs containers on the local machine using Docker.
97
- Useful for development and testing.
98
-
99
- Example:
100
- >>> provider = LocalDockerProvider()
101
- >>> base_url = provider.start_container("echo-env:latest")
102
- >>> # Container running on http://localhost:<random-port>
103
- >>> provider.stop_container()
104
- """
105
-
106
- def __init__(self):
107
- """Initialize the local Docker provider."""
108
- self._container_id: Optional[str] = None
109
- self._container_name: Optional[str] = None
110
-
111
- # Check if Docker is available
112
- import subprocess
113
-
114
- try:
115
- subprocess.run(
116
- ["docker", "version"],
117
- check=True,
118
- capture_output=True,
119
- timeout=5,
120
- )
121
- except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
122
- raise RuntimeError(
123
- "Docker is not available. Please install Docker Desktop or Docker Engine."
124
- )
125
-
126
- def start_container(
127
- self,
128
- image: str,
129
- port: Optional[int] = None,
130
- env_vars: Optional[Dict[str, str]] = None,
131
- **kwargs: Any,
132
- ) -> str:
133
- """
134
- Start a Docker container locally.
135
-
136
- Args:
137
- image: Docker image name
138
- port: Port to expose (if None, finds available port)
139
- env_vars: Environment variables for the container
140
- **kwargs: Additional Docker run options
141
-
142
- Returns:
143
- Base URL to connect to the container
144
- """
145
- import subprocess
146
- import time
147
-
148
- # Find available port if not specified
149
- if port is None:
150
- port = self._find_available_port()
151
-
152
- # Generate container name
153
- self._container_name = self._generate_container_name(image)
154
-
155
- # Build docker run command
156
- cmd = [
157
- "docker", "run",
158
- "-d", # Detached
159
- "--name", self._container_name,
160
- "-p", f"{port}:8000", # Map port
161
- ]
162
-
163
- # Add environment variables
164
- if env_vars:
165
- for key, value in env_vars.items():
166
- cmd.extend(["-e", f"{key}={value}"])
167
-
168
- # Add image
169
- cmd.append(image)
170
-
171
- # Run container
172
- try:
173
- result = subprocess.run(cmd, capture_output=True, text=True, check=True)
174
- self._container_id = result.stdout.strip()
175
- except subprocess.CalledProcessError as e:
176
- error_msg = f"Failed to start Docker container.\nCommand: {' '.join(cmd)}\nExit code: {e.returncode}\nStderr: {e.stderr}\nStdout: {e.stdout}"
177
- raise RuntimeError(error_msg) from e
178
-
179
- # Wait a moment for container to start
180
- time.sleep(1)
181
-
182
- base_url = f"http://localhost:{port}"
183
- return base_url
184
-
185
- def stop_container(self) -> None:
186
- """
187
- Stop and remove the Docker container.
188
- """
189
- if self._container_id is None:
190
- return
191
-
192
- import subprocess
193
-
194
- try:
195
- # Stop container
196
- subprocess.run(
197
- ["docker", "stop", self._container_id],
198
- capture_output=True,
199
- check=True,
200
- timeout=10,
201
- )
202
-
203
- # Remove container
204
- subprocess.run(
205
- ["docker", "rm", self._container_id],
206
- capture_output=True,
207
- check=True,
208
- timeout=10,
209
- )
210
- except subprocess.CalledProcessError:
211
- # Container might already be stopped/removed
212
- pass
213
- finally:
214
- self._container_id = None
215
- self._container_name = None
216
-
217
- def wait_for_ready(self, base_url: str, timeout_s: float = 30.0) -> None:
218
- """
219
- Wait for container to be ready by polling /health endpoint.
220
-
221
- Args:
222
- base_url: Base URL of the container
223
- timeout_s: Maximum time to wait
224
-
225
- Raises:
226
- TimeoutError: If container doesn't become ready
227
- """
228
- import time
229
- import requests
230
-
231
- start_time = time.time()
232
- health_url = f"{base_url}/health"
233
-
234
- while time.time() - start_time < timeout_s:
235
- try:
236
- response = requests.get(health_url, timeout=2.0)
237
- if response.status_code == 200:
238
- return
239
- except requests.RequestException:
240
- pass
241
-
242
- time.sleep(0.5)
243
-
244
- raise TimeoutError(
245
- f"Container at {base_url} did not become ready within {timeout_s}s"
246
- )
247
-
248
- def _find_available_port(self) -> int:
249
- """
250
- Find an available port on localhost.
251
-
252
- Returns:
253
- An available port number
254
- """
255
- import socket
256
-
257
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
258
- s.bind(("", 0))
259
- s.listen(1)
260
- port = s.getsockname()[1]
261
- return port
262
-
263
- def _generate_container_name(self, image: str) -> str:
264
- """
265
- Generate a unique container name based on image name and timestamp.
266
-
267
- Args:
268
- image: Docker image name
269
-
270
- Returns:
271
- A unique container name
272
- """
273
- import time
274
-
275
- clean_image = image.split("/")[-1].split(":")[0]
276
- timestamp = int(time.time() * 1000)
277
- return f"{clean_image}-{timestamp}"
278
-
279
-
280
- class KubernetesProvider(ContainerProvider):
281
- """
282
- Container provider for Kubernetes clusters.
283
-
284
- This provider creates pods in a Kubernetes cluster and exposes them
285
- via services or port-forwarding.
286
-
287
- Example:
288
- >>> provider = KubernetesProvider(namespace="envtorch-dev")
289
- >>> base_url = provider.start_container("echo-env:latest")
290
- >>> # Pod running in k8s, accessible via service or port-forward
291
- >>> provider.stop_container()
292
- """
293
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/containers/test_local_docker_provider.py DELETED
@@ -1,258 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- End-to-end test for LocalDockerProvider.
4
-
5
- This script tests the complete flow:
6
- 1. Start a container using LocalDockerProvider
7
- 2. Wait for it to be ready
8
- 3. Make HTTP requests to test the environment
9
- 4. Clean up the container
10
- """
11
-
12
- import sys
13
- from pathlib import Path
14
-
15
- # Add src to path
16
- sys.path.insert(0, str(Path(__file__).parent.parent.parent))
17
-
18
- import requests
19
-
20
- from core.containers.runtime import LocalDockerProvider
21
-
22
- # TODO: Remove this test or make it a functional test sicne this will be tested in e2e test for echo env
23
- def test_local_docker_provider():
24
- """Test LocalDockerProvider end-to-end."""
25
- print("=" * 60)
26
- print("LocalDockerProvider End-to-End Test")
27
- print("=" * 60)
28
- print()
29
-
30
- provider = None
31
-
32
- try:
33
- # Step 1: Create provider
34
- print("Step 1: Creating LocalDockerProvider...")
35
- provider = LocalDockerProvider()
36
- print("✓ Provider created\n")
37
-
38
- # Step 2: Start container
39
- print("Step 2: Starting echo-env container...")
40
- base_url = provider.start_container("echo-env:latest")
41
- print(f"✓ Container started at: {base_url}")
42
- if provider._container_id:
43
- print(f" Container ID: {provider._container_id[:12]}...")
44
- if provider._container_name:
45
- print(f" Container name: {provider._container_name}\n")
46
-
47
- # Step 3: Wait for ready
48
- print("Step 3: Waiting for container to be ready...")
49
- provider.wait_for_ready(base_url, timeout_s=30.0)
50
- print("✓ Container is ready!\n")
51
-
52
- # Step 4: Test health endpoint
53
- print("Step 4: Testing /health endpoint...")
54
- response = requests.get(f"{base_url}/health")
55
- print(f" Status: {response.status_code}")
56
- print(f" Response: {response.json()}")
57
- assert response.status_code == 200
58
- assert response.json()["status"] == "healthy"
59
- print("✓ Health check passed\n")
60
-
61
- # Step 5: Test reset endpoint
62
- print("Step 5: Testing /reset endpoint...")
63
- response = requests.post(
64
- f"{base_url}/reset",
65
- json={},
66
- headers={"Content-Type": "application/json"},
67
- )
68
- print(f" Status: {response.status_code}")
69
- data = response.json()
70
- print(f" Message: {data['observation']['echoed_message']}")
71
- print(f" Reward: {data['reward']}")
72
- print(f" Done: {data['done']}")
73
- assert response.status_code == 200
74
- assert data["observation"]["echoed_message"] == "Echo environment ready!"
75
- print("✓ Reset test passed\n")
76
-
77
- # Step 6: Test step endpoint
78
- print("Step 6: Testing /step endpoint...")
79
- response = requests.post(
80
- f"{base_url}/step",
81
- json={"action": {"message": "Hello from LocalDockerProvider!"}},
82
- headers={"Content-Type": "application/json"},
83
- )
84
- print(f" Status: {response.status_code}")
85
- data = response.json()
86
- print(f" Echoed: {data['observation']['echoed_message']}")
87
- print(f" Length: {data['observation']['message_length']}")
88
- print(f" Reward: {data['reward']}")
89
- assert response.status_code == 200
90
- assert data["observation"]["echoed_message"] == "Hello from LocalDockerProvider!"
91
- assert data["observation"]["message_length"] == 31
92
- print("✓ Step test passed\n")
93
-
94
- # Step 7: Test state endpoint
95
- print("Step 7: Testing /state endpoint...")
96
- response = requests.get(f"{base_url}/state")
97
- print(f" Status: {response.status_code}")
98
- data = response.json()
99
- print(f" Episode ID: {data['episode_id']}")
100
- print(f" Step count: {data['step_count']}")
101
- assert response.status_code == 200
102
- assert data["step_count"] == 1 # One step from above
103
- print("✓ State test passed\n")
104
-
105
- # Step 8: Multiple steps
106
- print("Step 8: Testing multiple steps...")
107
- for i in range(3):
108
- response = requests.post(
109
- f"{base_url}/step",
110
- json={"action": {"message": f"Message {i+1}"}},
111
- headers={"Content-Type": "application/json"},
112
- )
113
- assert response.status_code == 200
114
- print(f" Step {i+1}: ✓")
115
-
116
- # Check state updated
117
- response = requests.get(f"{base_url}/state")
118
- data = response.json()
119
- assert data["step_count"] == 4 # 1 + 3 more steps
120
- print(f" Final step count: {data['step_count']}")
121
- print("✓ Multiple steps test passed\n")
122
-
123
- print("=" * 60)
124
- print("✓ All tests passed!")
125
- print("=" * 60)
126
- print()
127
-
128
- return True
129
-
130
- except Exception as e:
131
- print(f"\n❌ Test failed: {e}")
132
- import traceback
133
- traceback.print_exc()
134
- return False
135
-
136
- finally:
137
- # Step 9: Cleanup
138
- if provider is not None:
139
- print("\nStep 9: Cleaning up container...")
140
- try:
141
- provider.stop_container()
142
- print("✓ Container stopped and removed\n")
143
- except Exception as e:
144
- print(f"⚠️ Cleanup warning: {e}\n")
145
-
146
-
147
- def test_provider_with_custom_port():
148
- """Test provider with custom port."""
149
- print("=" * 60)
150
- print("LocalDockerProvider with Custom Port Test")
151
- print("=" * 60)
152
- print()
153
-
154
- provider = None
155
-
156
- try:
157
- provider = LocalDockerProvider()
158
-
159
- print("Starting container on custom port 8123...")
160
- base_url = provider.start_container("echo-env:latest", port=8123)
161
- print(f"✓ Started at: {base_url}")
162
- assert ":8123" in base_url
163
-
164
- print("Waiting for ready...")
165
- provider.wait_for_ready(base_url)
166
- print("✓ Ready!")
167
-
168
- print("Testing health...")
169
- response = requests.get(f"{base_url}/health")
170
- assert response.status_code == 200
171
- print("✓ Health check passed")
172
-
173
- print("\n✓ Custom port test passed!\n")
174
- return True
175
-
176
- except Exception as e:
177
- print(f"\n❌ Test failed: {e}")
178
- return False
179
-
180
- finally:
181
- if provider is not None:
182
- provider.stop_container()
183
- print("✓ Cleaned up\n")
184
-
185
-
186
- def test_provider_with_env_vars():
187
- """Test provider with environment variables."""
188
- print("=" * 60)
189
- print("LocalDockerProvider with Environment Variables Test")
190
- print("=" * 60)
191
- print()
192
-
193
- provider = None
194
-
195
- try:
196
- provider = LocalDockerProvider()
197
-
198
- print("Starting container with environment variables...")
199
- base_url = provider.start_container(
200
- "echo-env:latest",
201
- env_vars={"DEBUG": "true", "LOG_LEVEL": "info"}
202
- )
203
- print(f"✓ Started at: {base_url}")
204
-
205
- print("Waiting for ready...")
206
- provider.wait_for_ready(base_url)
207
- print("✓ Ready!")
208
-
209
- print("Testing health...")
210
- response = requests.get(f"{base_url}/health")
211
- assert response.status_code == 200
212
- print("✓ Health check passed")
213
-
214
- print("\n✓ Environment variables test passed!\n")
215
- return True
216
-
217
- except Exception as e:
218
- print(f"\n❌ Test failed: {e}")
219
- return False
220
-
221
- finally:
222
- if provider is not None:
223
- provider.stop_container()
224
- print("✓ Cleaned up\n")
225
-
226
-
227
- if __name__ == "__main__":
228
- print()
229
- print("🐳 LocalDockerProvider Test Suite")
230
- print()
231
-
232
- results = []
233
-
234
- # Run basic test
235
- results.append(("Basic End-to-End", test_local_docker_provider()))
236
-
237
- # Run custom port test
238
- results.append(("Custom Port", test_provider_with_custom_port()))
239
-
240
- # Run environment variables test
241
- results.append(("Environment Variables", test_provider_with_env_vars()))
242
-
243
- # Summary
244
- print("=" * 60)
245
- print("Test Summary")
246
- print("=" * 60)
247
- for name, passed in results:
248
- status = "✓ PASSED" if passed else "✗ FAILED"
249
- print(f"{name:25} {status}")
250
- print("=" * 60)
251
-
252
- all_passed = all(result for _, result in results)
253
- if all_passed:
254
- print("\n🎉 All tests passed!")
255
- exit(0)
256
- else:
257
- print("\n❌ Some tests failed")
258
- exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/env_server/__init__.py DELETED
@@ -1,35 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Core environment interfaces and types."""
8
-
9
- from .base_transforms import CompositeTransform, NullTransform
10
- from .http_server import HTTPEnvServer, create_app, create_fastapi_app
11
- from .interfaces import Environment, Message, ModelTokenizer, Transform
12
- from .types import Action, Observation, State
13
- from .web_interface import create_web_interface_app, WebInterfaceManager
14
-
15
- __all__ = [
16
- # Core interfaces
17
- "Environment",
18
- "Transform",
19
- "Message",
20
- "ModelTokenizer",
21
- # Types
22
- "Action",
23
- "Observation",
24
- "State",
25
- # Base transforms
26
- "CompositeTransform",
27
- "NullTransform",
28
- # HTTP Server
29
- "HTTPEnvServer",
30
- "create_app",
31
- "create_fastapi_app",
32
- # Web Interface
33
- "create_web_interface_app",
34
- "WebInterfaceManager",
35
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/env_server/base_transforms.py DELETED
@@ -1,29 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Base transform implementations for composing environment-specific transforms."""
8
-
9
- from .interfaces import Transform
10
- from .types import Observation
11
-
12
-
13
- class CompositeTransform(Transform):
14
- """Combines multiple transforms into a single transform."""
15
-
16
- def __init__(self, transforms: list[Transform]):
17
- self.transforms = transforms
18
-
19
- def __call__(self, observation: Observation) -> Observation:
20
- for transform in self.transforms:
21
- observation = transform(observation)
22
- return observation
23
-
24
-
25
- class NullTransform(Transform):
26
- """Default transform that passes through unchanged."""
27
-
28
- def __call__(self, observation: Observation) -> Observation:
29
- return observation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/env_server/http_server.py DELETED
@@ -1,257 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- HTTP server wrapper for Environment instances.
9
-
10
- This module provides utilities to wrap any Environment subclass and expose it
11
- over HTTP endpoints that HTTPEnvClient can consume.
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import asyncio
17
- import os
18
- from concurrent.futures import ThreadPoolExecutor
19
- from dataclasses import asdict
20
- from typing import Any, Dict, Type
21
-
22
- from .interfaces import Environment
23
- from .types import Action, Observation
24
- from fastapi import Body, FastAPI
25
-
26
- class HTTPEnvServer:
27
- """
28
- HTTP server wrapper for Environment instances.
29
-
30
- This class wraps an Environment and exposes its reset(), step(), and state
31
- methods as HTTP endpoints compatible with HTTPEnvClient.
32
-
33
- The server expects:
34
- - Action deserialization: Converts JSON dict to Action subclass
35
- - Observation serialization: Converts Observation subclass to JSON dict
36
-
37
- Example:
38
- >>> from core.env_server import HTTPEnvServer
39
- >>> from envs.coding_env.server import CodeExecutionEnvironment
40
- >>>
41
- >>> env = CodeExecutionEnvironment()
42
- >>> server = HTTPEnvServer(env)
43
- >>>
44
- >>> # Register routes with FastAPI
45
- >>> from fastapi import FastAPI
46
- >>> app = FastAPI()
47
- >>> server.register_routes(app)
48
- """
49
-
50
- def __init__(
51
- self,
52
- env: Environment,
53
- action_cls: Type[Action],
54
- observation_cls: Type[Observation],
55
- ):
56
- """
57
- Initialize HTTP server wrapper.
58
-
59
- Args:
60
- env: The Environment instance to wrap
61
- action_cls: The Action subclass this environment expects
62
- observation_cls: The Observation subclass this environment returns
63
- """
64
- self.env = env
65
- self.action_cls = action_cls
66
- self.observation_cls = observation_cls
67
- # Create thread pool for running sync code in async context
68
- # This is needed for environments using sync libraries (e.g., Playwright sync API)
69
- self._executor = ThreadPoolExecutor(max_workers=1)
70
-
71
- def register_routes(self, app: Any) -> None:
72
- """
73
- Register HTTP routes on a FastAPI application.
74
-
75
- Args:
76
- app: FastAPI application instance
77
- """
78
-
79
- if not isinstance(app, FastAPI):
80
- raise TypeError("app must be a FastAPI instance")
81
-
82
- @app.post("/reset")
83
- async def reset(request: Dict[str, Any] = Body(default={})) -> Dict[str, Any]:
84
- """Reset endpoint - returns initial observation."""
85
- # TODO: Handle seed, episode_id from request if provided
86
- # Run sync environment code in thread pool to avoid blocking asyncio loop
87
- loop = asyncio.get_event_loop()
88
- observation = await loop.run_in_executor(self._executor, self.env.reset)
89
- return self._serialize_observation(observation)
90
-
91
- @app.post("/step")
92
- async def step(request: Dict[str, Any]) -> Dict[str, Any]:
93
- """Step endpoint - executes action and returns observation."""
94
- # Support both {"action": {...}} and direct action fields
95
- action_data = request.get("action", request)
96
- # TODO: Handle timeout_s, request_id, episode_id from request if provided
97
-
98
- # Deserialize action
99
- action = self._deserialize_action(action_data)
100
-
101
- # Execute step in thread pool to avoid blocking asyncio loop
102
- loop = asyncio.get_event_loop()
103
- observation = await loop.run_in_executor(
104
- self._executor, self.env.step, action
105
- )
106
-
107
- # Return serialized observation
108
- return self._serialize_observation(observation)
109
-
110
- @app.get("/state")
111
- async def get_state() -> Dict[str, Any]:
112
- """State endpoint - returns current environment state."""
113
- state = self.env.state
114
- return asdict(state)
115
-
116
- @app.get("/health")
117
- async def health() -> Dict[str, str]:
118
- """Health check endpoint."""
119
- return {"status": "healthy"}
120
-
121
-
122
- def _deserialize_action(self, action_data: Dict[str, Any]) -> Action:
123
- """
124
- Convert JSON dict to Action instance.
125
-
126
- Args:
127
- action_data: Dictionary containing action data
128
-
129
- Returns:
130
- Action instance
131
-
132
- Note:
133
- This is a simple implementation. Subclasses may need to override
134
- for more complex deserialization logic.
135
- """
136
- # Remove metadata if present (it will be set via kw_only field)
137
- metadata = action_data.pop("metadata", {})
138
- action = self.action_cls(**action_data)
139
- action.metadata = metadata
140
- return action
141
-
142
- def _serialize_observation(self, observation: Observation) -> Dict[str, Any]:
143
- """
144
- Convert Observation instance to JSON-compatible dict.
145
-
146
- Args:
147
- observation: Observation instance
148
-
149
- Returns:
150
- Dictionary compatible with HTTPEnvClient._parse_result()
151
-
152
- The format matches what HTTPEnvClient expects:
153
- {
154
- "observation": {...}, # Observation fields
155
- "reward": float | None,
156
- "done": bool,
157
- }
158
- """
159
- obs_dict = asdict(observation)
160
-
161
- # Convert numpy arrays to lists for JSON serialization
162
- def _convert_numpy(obj):
163
- """Recursively convert numpy arrays to lists."""
164
- if hasattr(obj, '__array__'): # numpy array
165
- return obj.tolist()
166
- elif isinstance(obj, dict):
167
- return {k: _convert_numpy(v) for k, v in obj.items()}
168
- elif isinstance(obj, (list, tuple)):
169
- return type(obj)(_convert_numpy(item) for item in obj)
170
- return obj
171
-
172
- obs_dict = _convert_numpy(obs_dict)
173
-
174
- # Extract reward and done (these are part of StepResult on client side)
175
- reward = obs_dict.pop("reward", None)
176
- done = obs_dict.pop("done", False)
177
- obs_dict.pop("metadata", None) # Remove metadata from observation
178
-
179
- # Return in HTTPEnvClient expected format
180
- return {
181
- "observation": obs_dict,
182
- "reward": reward,
183
- "done": done,
184
- }
185
-
186
- def create_app(
187
- env: Environment,
188
- action_cls: Type[Action],
189
- observation_cls: Type[Observation],
190
- env_name: Optional[str] = None,
191
- ) -> Any:
192
- """
193
- Create a FastAPI application with or without web interface.
194
-
195
- This function creates a FastAPI app with the web interface enabled by default,
196
- including README integration for better user experience.
197
-
198
- Args:
199
- env: The Environment instance to serve
200
- action_cls: The Action subclass this environment expects
201
- observation_cls: The Observation subclass this environment returns
202
- env_name: Optional environment name for README loading
203
-
204
- Returns:
205
- FastAPI application instance with or without web interface and README integration
206
- """
207
- # Check if web interface should be enabled
208
- # This can be controlled via environment variable or build argument
209
- enable_web = (
210
- os.getenv("ENABLE_WEB_INTERFACE", "false").lower() in ("true", "1", "yes")
211
- )
212
-
213
- if enable_web:
214
- # Import web interface only when needed
215
- from .web_interface import create_web_interface_app
216
- return create_web_interface_app(env, action_cls, observation_cls, env_name)
217
- else:
218
- # Use standard FastAPI app without web interface
219
- return create_fastapi_app(env, action_cls, observation_cls)
220
-
221
-
222
- def create_fastapi_app(
223
- env: Environment,
224
- action_cls: Type[Action],
225
- observation_cls: Type[Observation],
226
- ) -> Any:
227
- """
228
- Create a FastAPI application with routes for the given environment.
229
-
230
- Args:
231
- env: The Environment instance to serve
232
- action_cls: The Action subclass this environment expects
233
- observation_cls: The Observation subclass this environment returns
234
-
235
- Returns:
236
- FastAPI application instance with routes registered
237
-
238
- Example:
239
- >>> from envs.coding_env.server import CodeExecutionEnvironment
240
- >>> from envs.coding_env.models import CodeAction, CodeObservation
241
- >>>
242
- >>> env = CodeExecutionEnvironment()
243
- >>> app = create_fastapi_app(env, CodeAction, CodeObservation)
244
- >>>
245
- >>> # Run with: uvicorn module:app --host 0.0.0.0 --port 8000
246
- """
247
- try:
248
- from fastapi import FastAPI
249
- except ImportError:
250
- raise ImportError(
251
- "FastAPI is required. Install with: pip install fastapi uvicorn"
252
- )
253
-
254
- app = FastAPI(title="Environment HTTP Server")
255
- server = HTTPEnvServer(env, action_cls, observation_cls)
256
- server.register_routes(app)
257
- return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/env_server/interfaces.py DELETED
@@ -1,118 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- from abc import ABC, abstractmethod
8
- from typing import Any, Protocol, TypedDict
9
-
10
- from .types import Action, Observation, State
11
-
12
-
13
- class Message(TypedDict):
14
- """A message in a conversation.
15
-
16
- Compatible with Huggingface chat template format.
17
- """
18
-
19
- role: str
20
- content: str
21
-
22
-
23
- class ModelTokenizer(Protocol):
24
- """Protocol for tokenizers that support chat templates.
25
-
26
- This protocol defines the interface that tokenizers must implement
27
- to work with chat-based environments. It's compatible with
28
- Huggingface transformers tokenizers.
29
- """
30
-
31
- def apply_chat_template(
32
- self,
33
- conversation: list[Message],
34
- tokenize: bool = True,
35
- return_tensors: str | None = None,
36
- **kwargs: Any,
37
- ) -> Any:
38
- """Apply a chat template to format and optionally tokenize a conversation.
39
-
40
- Args:
41
- conversation: List of message dictionaries with 'role' and 'content'
42
- tokenize: Whether to tokenize the output
43
- return_tensors: Format for returned tensors ('pt' for PyTorch)
44
- **kwargs: Additional arguments
45
-
46
- Returns:
47
- Formatted and optionally tokenized conversation
48
- """
49
- ...
50
-
51
- def decode(
52
- self, token_ids: Any, skip_special_tokens: bool = False, **kwargs: Any
53
- ) -> str:
54
- """Decode token IDs back to text.
55
-
56
- Args:
57
- token_ids: Token IDs to decode
58
- skip_special_tokens: Whether to skip special tokens in output
59
- **kwargs: Additional arguments
60
-
61
- Returns:
62
- Decoded text string
63
- """
64
- ...
65
-
66
-
67
- class Transform(ABC):
68
- """Transform observations to add rewards, metrics, or other modifications.
69
-
70
- Transforms follow the TorchRL pattern where they take an observation
71
- and return a (potentially modified) observation. This allows for
72
- flexible reward computation and observation augmentation.
73
- """
74
-
75
- @abstractmethod
76
- def __call__(self, observation: Observation) -> Observation:
77
- """Transform an observation.
78
-
79
- Args:
80
- observation: The input observation
81
-
82
- Returns:
83
- The transformed observation
84
- """
85
- pass
86
-
87
-
88
- class Environment(ABC):
89
- """Base class for all environment servers following Gym/Gymnasium API.
90
-
91
- Args:
92
- transform: Optional transform to apply to observations
93
- """
94
-
95
- def __init__(self, transform: Transform | None = None):
96
- self.transform = transform
97
-
98
- @abstractmethod
99
- def reset(self) -> Observation:
100
- """Reset the environment and return initial observation."""
101
- pass
102
-
103
- @abstractmethod
104
- def step(self, action: Action) -> Observation:
105
- """Take a step in the environment."""
106
- pass
107
-
108
- @property
109
- @abstractmethod
110
- def state(self) -> State:
111
- """Get the current environment state."""
112
- pass
113
-
114
- def _apply_transform(self, observation: Observation) -> Observation:
115
- """Apply transform if one is provided."""
116
- if self.transform is not None:
117
- return self.transform(observation)
118
- return observation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/env_server/types.py DELETED
@@ -1,57 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- from dataclasses import dataclass, field
8
- from typing import Any, Dict, List, Optional, Union
9
-
10
-
11
- # Type aliases
12
- Scalar = Union[int, float, bool]
13
-
14
-
15
- @dataclass(kw_only=True)
16
- class Action:
17
- """Base class for all environment actions."""
18
-
19
- metadata: Dict[str, Any] = field(default_factory=dict)
20
-
21
-
22
- @dataclass(kw_only=True)
23
- class Observation:
24
- """Base class for all environment observations."""
25
-
26
- done: bool = False
27
- reward: Union[bool, int, float, None] = None
28
- metadata: Dict[str, Any] = field(default_factory=dict)
29
-
30
-
31
- @dataclass
32
- class State:
33
- """Base class for environment state."""
34
-
35
- episode_id: Optional[str] = None
36
- step_count: int = 0
37
-
38
-
39
- @dataclass
40
- class CodeExecResult:
41
- """Result of code execution containing stdout, stderr, and exit code."""
42
-
43
- stdout: str
44
- stderr: str
45
- exit_code: int
46
-
47
-
48
- @dataclass
49
- class EnvironmentMetadata:
50
- """Metadata about an environment for documentation and UI purposes."""
51
-
52
- name: str
53
- description: str
54
- readme_content: Optional[str] = None
55
- version: Optional[str] = None
56
- author: Optional[str] = None
57
- documentation_url: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/env_server/web_interface.py DELETED
@@ -1,1613 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Web interface for OpenEnv environments.
9
-
10
- This module provides a web-based interface for interacting with OpenEnv environments,
11
- including a two-pane layout for HumanAgent interaction and state observation.
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import json
17
- import time
18
- from dataclasses import asdict, dataclass
19
- from typing import Any, Dict, List, Optional, Type
20
- from datetime import datetime
21
-
22
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
23
- from fastapi.responses import HTMLResponse, FileResponse
24
- from fastapi.staticfiles import StaticFiles
25
- from pydantic import BaseModel
26
-
27
- from .interfaces import Environment
28
- from .types import Action, Observation, State, EnvironmentMetadata
29
-
30
-
31
- def load_environment_metadata(env: Environment, env_name: Optional[str] = None) -> EnvironmentMetadata:
32
- """
33
- Load environment metadata including README content.
34
-
35
- Args:
36
- env: The environment instance
37
- env_name: Optional environment name for README file lookup
38
-
39
- Returns:
40
- EnvironmentMetadata with loaded information
41
- """
42
- # Try to get metadata from environment if it has a method for it
43
- if hasattr(env, 'get_metadata'):
44
- return env.get_metadata()
45
-
46
- # Default metadata
47
- metadata = EnvironmentMetadata(
48
- name=env_name or env.__class__.__name__,
49
- description=f"{env.__class__.__name__} environment",
50
- version="1.0.0"
51
- )
52
-
53
- # Try to load README from file system
54
- readme_content = _load_readme_from_filesystem(env_name)
55
- if readme_content:
56
- metadata.readme_content = readme_content
57
-
58
- return metadata
59
-
60
-
61
- def _load_readme_from_filesystem(env_name: Optional[str]) -> Optional[str]:
62
- """
63
- Load README content from the filesystem.
64
-
65
- Tries multiple locations:
66
- 1. Container filesystem: /app/README.md
67
- 2. Local development: src/envs/{env_name}/README.md
68
- 3. Environment variable: ENV_README_PATH
69
- """
70
- import os
71
- from pathlib import Path
72
-
73
- # Try container filesystem first
74
- container_readme = Path("/app/README.md")
75
- if container_readme.exists():
76
- try:
77
- return container_readme.read_text(encoding='utf-8')
78
- except Exception:
79
- pass
80
-
81
- # Try environment variable path
82
- custom_path = os.environ.get("ENV_README_PATH")
83
- if custom_path and Path(custom_path).exists():
84
- try:
85
- return Path(custom_path).read_text(encoding='utf-8')
86
- except Exception:
87
- pass
88
-
89
- # Try local development path
90
- if env_name:
91
- local_readme = Path(f"src/envs/{env_name}/README.md")
92
- if local_readme.exists():
93
- try:
94
- return local_readme.read_text(encoding='utf-8')
95
- except Exception:
96
- pass
97
-
98
- return None
99
-
100
-
101
- @dataclass
102
- class ActionLog:
103
- """Log entry for an action taken."""
104
- timestamp: str
105
- action: Dict[str, Any]
106
- observation: Dict[str, Any]
107
- reward: Optional[float]
108
- done: bool
109
- step_count: int
110
-
111
-
112
- @dataclass
113
- class EpisodeState:
114
- """Current episode state for the web interface."""
115
- episode_id: Optional[str]
116
- step_count: int
117
- current_observation: Optional[Dict[str, Any]]
118
- action_logs: List[ActionLog]
119
- is_reset: bool = True
120
-
121
-
122
- class WebInterfaceManager:
123
- """Manages the web interface for an environment."""
124
-
125
- def __init__(
126
- self,
127
- env: Environment,
128
- action_cls: Type[Action],
129
- observation_cls: Type[Observation],
130
- metadata: Optional[EnvironmentMetadata] = None,
131
- ):
132
- self.env = env
133
- self.action_cls = action_cls
134
- self.observation_cls = observation_cls
135
- self.metadata = metadata or EnvironmentMetadata(
136
- name=env.__class__.__name__,
137
- description=f"{env.__class__.__name__} environment"
138
- )
139
- self.episode_state = EpisodeState(
140
- episode_id=None,
141
- step_count=0,
142
- current_observation=None,
143
- action_logs=[]
144
- )
145
- self.connected_clients: List[WebSocket] = []
146
-
147
- async def connect_websocket(self, websocket: WebSocket):
148
- """Connect a new WebSocket client."""
149
- await websocket.accept()
150
- self.connected_clients.append(websocket)
151
-
152
- # Send current state to the new client
153
- await self._send_state_update()
154
-
155
- async def disconnect_websocket(self, websocket: WebSocket):
156
- """Disconnect a WebSocket client."""
157
- if websocket in self.connected_clients:
158
- self.connected_clients.remove(websocket)
159
-
160
- async def _send_state_update(self):
161
- """Send current state to all connected clients."""
162
- if not self.connected_clients:
163
- return
164
-
165
- state_data = {
166
- "type": "state_update",
167
- "episode_state": asdict(self.episode_state)
168
- }
169
-
170
- # Send to all connected clients
171
- disconnected_clients = []
172
- for client in self.connected_clients:
173
- try:
174
- await client.send_text(json.dumps(state_data))
175
- except:
176
- disconnected_clients.append(client)
177
-
178
- # Remove disconnected clients
179
- for client in disconnected_clients:
180
- self.connected_clients.remove(client)
181
-
182
- async def reset_environment(self) -> Dict[str, Any]:
183
- """Reset the environment and update state."""
184
- observation = self.env.reset()
185
- state = self.env.state
186
-
187
- # Update episode state
188
- self.episode_state.episode_id = state.episode_id
189
- self.episode_state.step_count = 0
190
- self.episode_state.current_observation = asdict(observation)
191
- self.episode_state.action_logs = []
192
- self.episode_state.is_reset = True
193
-
194
- # Send state update
195
- await self._send_state_update()
196
-
197
- return {
198
- "observation": asdict(observation),
199
- "reward": observation.reward,
200
- "done": observation.done,
201
- }
202
-
203
- async def step_environment(self, action_data: Dict[str, Any]) -> Dict[str, Any]:
204
- """Execute a step in the environment and update state."""
205
- # Deserialize action
206
- action = self._deserialize_action(action_data)
207
-
208
- # Execute step
209
- observation = self.env.step(action)
210
- state = self.env.state
211
-
212
- # Create action log
213
- action_log = ActionLog(
214
- timestamp=datetime.now().isoformat(),
215
- action=asdict(action),
216
- observation=asdict(observation),
217
- reward=observation.reward,
218
- done=observation.done,
219
- step_count=state.step_count
220
- )
221
-
222
- # Update episode state
223
- self.episode_state.episode_id = state.episode_id
224
- self.episode_state.step_count = state.step_count
225
- self.episode_state.current_observation = asdict(observation)
226
- self.episode_state.action_logs.append(action_log)
227
- self.episode_state.is_reset = False
228
-
229
- # Send state update
230
- await self._send_state_update()
231
-
232
- return {
233
- "observation": asdict(observation),
234
- "reward": observation.reward,
235
- "done": observation.done,
236
- }
237
-
238
- def get_state(self) -> Dict[str, Any]:
239
- """Get current environment state."""
240
- state = self.env.state
241
- return asdict(state)
242
-
243
- def _deserialize_action(self, action_data: Dict[str, Any]) -> Action:
244
- """Convert JSON dict to Action instance."""
245
- metadata = action_data.pop("metadata", {})
246
-
247
- # Handle tensor fields that come from JSON as lists
248
- processed_data = {}
249
- for key, value in action_data.items():
250
- if key == "tokens" and isinstance(value, (list, str)):
251
- # Convert list or string to tensor
252
- if isinstance(value, str):
253
- # If it's a string, try to parse it as a list of numbers
254
- try:
255
- import json
256
- value = json.loads(value)
257
- except:
258
- # If parsing fails, treat as empty list
259
- value = []
260
- if isinstance(value, list):
261
- import torch
262
- processed_data[key] = torch.tensor(value, dtype=torch.long)
263
- else:
264
- processed_data[key] = value
265
- elif key == "action_id" and isinstance(value, str):
266
- # Convert action_id from string to int
267
- try:
268
- processed_data[key] = int(value)
269
- except ValueError:
270
- # If conversion fails, keep original value
271
- processed_data[key] = value
272
- else:
273
- processed_data[key] = value
274
-
275
- action = self.action_cls(**processed_data)
276
- action.metadata = metadata
277
- return action
278
-
279
-
280
- def create_web_interface_app(
281
- env: Environment,
282
- action_cls: Type[Action],
283
- observation_cls: Type[Observation],
284
- env_name: Optional[str] = None,
285
- ) -> FastAPI:
286
- """
287
- Create a FastAPI application with web interface for the given environment.
288
-
289
- Args:
290
- env: The Environment instance to serve
291
- action_cls: The Action subclass this environment expects
292
- observation_cls: The Observation subclass this environment returns
293
- env_name: Optional environment name for README loading
294
-
295
- Returns:
296
- FastAPI application instance with web interface
297
- """
298
- from .http_server import create_fastapi_app
299
-
300
- # Create the base environment app
301
- app = create_fastapi_app(env, action_cls, observation_cls)
302
-
303
- # Load environment metadata
304
- metadata = load_environment_metadata(env, env_name)
305
-
306
- # Create web interface manager
307
- web_manager = WebInterfaceManager(env, action_cls, observation_cls, metadata)
308
-
309
- # Add web interface routes
310
- @app.get("/web", response_class=HTMLResponse)
311
- async def web_interface():
312
- """Serve the web interface."""
313
- return get_web_interface_html(action_cls, web_manager.metadata)
314
-
315
- @app.get("/web/metadata")
316
- async def web_metadata():
317
- """Get environment metadata."""
318
- return asdict(web_manager.metadata)
319
-
320
- @app.websocket("/ws")
321
- async def websocket_endpoint(websocket: WebSocket):
322
- """WebSocket endpoint for real-time updates."""
323
- await web_manager.connect_websocket(websocket)
324
- try:
325
- while True:
326
- # Keep connection alive
327
- await websocket.receive_text()
328
- except WebSocketDisconnect:
329
- await web_manager.disconnect_websocket(websocket)
330
-
331
- @app.post("/web/reset")
332
- async def web_reset():
333
- """Reset endpoint for web interface."""
334
- return await web_manager.reset_environment()
335
-
336
- @app.post("/web/step")
337
- async def web_step(request: Dict[str, Any]):
338
- """Step endpoint for web interface."""
339
- # Check if this is a message-based request (chat environment)
340
- if "message" in request:
341
- message = request["message"]
342
- # Convert message to action using the environment's message_to_action method
343
- action = web_manager.env.message_to_action(message)
344
- action_data = {"tokens": action.tokens.tolist()}
345
- else:
346
- action_data = request.get("action", {})
347
-
348
- return await web_manager.step_environment(action_data)
349
-
350
- @app.get("/web/state")
351
- async def web_state():
352
- """State endpoint for web interface."""
353
- return web_manager.get_state()
354
-
355
- return app
356
-
357
-
358
- def get_web_interface_html(action_cls: Type[Action], metadata: Optional[EnvironmentMetadata] = None) -> str:
359
- """Generate the HTML for the web interface."""
360
-
361
- # Check if this is a chat environment by looking for tokens field
362
- is_chat_env = False
363
- if hasattr(action_cls, '__dataclass_fields__'):
364
- for field_name, field_info in action_cls.__dataclass_fields__.items():
365
- if field_name == 'tokens' and hasattr(field_info.type, '__name__') and 'Tensor' in field_info.type.__name__:
366
- is_chat_env = True
367
- break
368
-
369
- # Get action fields for dynamic form generation with enhanced metadata
370
- action_fields = _extract_action_fields(action_cls)
371
-
372
- return f"""
373
- <!DOCTYPE html>
374
- <html lang="en">
375
- <head>
376
- <meta charset="UTF-8">
377
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
378
- <title>OpenEnv Web Interface</title>
379
- <style>
380
- * {{
381
- margin: 0;
382
- padding: 0;
383
- box-sizing: border-box;
384
- }}
385
-
386
- body {{
387
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
388
- background-color: #f5f5f5;
389
- height: 100vh;
390
- overflow: hidden;
391
- }}
392
-
393
- .container {{
394
- display: flex;
395
- height: 100vh;
396
- }}
397
-
398
- .left-pane {{
399
- width: 50%;
400
- background: white;
401
- border-right: 1px solid #e0e0e0;
402
- display: flex;
403
- flex-direction: column;
404
- }}
405
-
406
- .right-pane {{
407
- width: 50%;
408
- background: #fafafa;
409
- display: flex;
410
- flex-direction: column;
411
- }}
412
-
413
- .pane-header {{
414
- padding: 20px;
415
- border-bottom: 1px solid #e0e0e0;
416
- background: #f8f9fa;
417
- font-weight: 600;
418
- font-size: 16px;
419
- }}
420
-
421
- .pane-content {{
422
- flex: 1;
423
- padding: 20px;
424
- overflow-y: auto;
425
- }}
426
-
427
- .action-form {{
428
- background: white;
429
- border: 1px solid #e0e0e0;
430
- border-radius: 8px;
431
- padding: 20px;
432
- margin-bottom: 20px;
433
- }}
434
-
435
- .form-group {{
436
- margin-bottom: 15px;
437
- }}
438
-
439
- .form-group label {{
440
- display: block;
441
- margin-bottom: 5px;
442
- font-weight: 500;
443
- color: #333;
444
- }}
445
-
446
- .form-group input, .form-group textarea {{
447
- width: 100%;
448
- padding: 8px 12px;
449
- border: 1px solid #ddd;
450
- border-radius: 4px;
451
- font-size: 14px;
452
- }}
453
-
454
- .form-group input:focus, .form-group textarea:focus {{
455
- outline: none;
456
- border-color: #007bff;
457
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
458
- }}
459
-
460
- .btn {{
461
- background: #007bff;
462
- color: white;
463
- border: none;
464
- padding: 10px 20px;
465
- border-radius: 4px;
466
- cursor: pointer;
467
- font-size: 14px;
468
- margin-right: 10px;
469
- margin-bottom: 10px;
470
- }}
471
-
472
- .btn:hover {{
473
- background: #0056b3;
474
- }}
475
-
476
- .btn:disabled {{
477
- background: #6c757d;
478
- cursor: not-allowed;
479
- }}
480
-
481
- .btn-secondary {{
482
- background: #6c757d;
483
- }}
484
-
485
- .btn-secondary:hover {{
486
- background: #545b62;
487
- }}
488
-
489
- .state-display {{
490
- background: white;
491
- border: 1px solid #e0e0e0;
492
- border-radius: 8px;
493
- padding: 15px;
494
- margin-bottom: 20px;
495
- }}
496
-
497
- .state-item {{
498
- margin-bottom: 8px;
499
- }}
500
-
501
- .state-label {{
502
- font-weight: 500;
503
- color: #666;
504
- }}
505
-
506
- .state-value {{
507
- color: #333;
508
- font-family: monospace;
509
- }}
510
-
511
- .logs-container {{
512
- background: white;
513
- border: 1px solid #e0e0e0;
514
- border-radius: 8px;
515
- padding: 15px;
516
- max-height: 400px;
517
- overflow-y: auto;
518
- }}
519
-
520
- .log-entry {{
521
- border-bottom: 1px solid #f0f0f0;
522
- padding: 10px 0;
523
- }}
524
-
525
- .log-entry:last-child {{
526
- border-bottom: none;
527
- }}
528
-
529
- .log-timestamp {{
530
- font-size: 12px;
531
- color: #666;
532
- margin-bottom: 5px;
533
- }}
534
-
535
- .log-action {{
536
- background: #e3f2fd;
537
- padding: 8px;
538
- border-radius: 4px;
539
- margin-bottom: 5px;
540
- font-family: monospace;
541
- font-size: 12px;
542
- }}
543
-
544
- .log-observation {{
545
- background: #f3e5f5;
546
- padding: 8px;
547
- border-radius: 4px;
548
- font-family: monospace;
549
- font-size: 12px;
550
- }}
551
-
552
- .log-reward {{
553
- font-weight: 600;
554
- color: #28a745;
555
- }}
556
-
557
- .log-done {{
558
- font-weight: 600;
559
- color: #dc3545;
560
- }}
561
-
562
- .status-indicator {{
563
- display: inline-block;
564
- width: 8px;
565
- height: 8px;
566
- border-radius: 50%;
567
- margin-right: 8px;
568
- }}
569
-
570
- .status-connected {{
571
- background: #28a745;
572
- }}
573
-
574
- .status-disconnected {{
575
- background: #dc3545;
576
- }}
577
-
578
- .json-display {{
579
- background: #f8f9fa;
580
- border: 1px solid #e9ecef;
581
- border-radius: 4px;
582
- padding: 10px;
583
- font-family: monospace;
584
- font-size: 12px;
585
- white-space: pre-wrap;
586
- max-height: 200px;
587
- overflow-y: auto;
588
- }}
589
-
590
- /* Chat Interface Styles */
591
- .chat-interface {{
592
- background: white;
593
- border: 1px solid #e0e0e0;
594
- border-radius: 8px;
595
- padding: 20px;
596
- margin-bottom: 20px;
597
- }}
598
-
599
- .chat-messages {{
600
- background: #f8f9fa;
601
- border: 1px solid #e0e0e0;
602
- border-radius: 8px;
603
- padding: 15px;
604
- margin-bottom: 15px;
605
- max-height: 400px;
606
- overflow-y: auto;
607
- }}
608
-
609
- .chat-message {{
610
- margin-bottom: 15px;
611
- padding: 10px;
612
- border-radius: 8px;
613
- }}
614
-
615
- .chat-message:last-child {{
616
- margin-bottom: 0;
617
- }}
618
-
619
- .chat-message.user {{
620
- background: #e3f2fd;
621
- margin-left: 20px;
622
- }}
623
-
624
- .chat-message.assistant {{
625
- background: #f3e5f5;
626
- margin-right: 20px;
627
- }}
628
-
629
- .chat-message.system {{
630
- background: #e8f5e8;
631
- font-style: italic;
632
- }}
633
-
634
- .message-role {{
635
- font-weight: 600;
636
- font-size: 12px;
637
- color: #666;
638
- margin-bottom: 5px;
639
- }}
640
-
641
- .message-content {{
642
- font-size: 14px;
643
- line-height: 1.4;
644
- }}
645
-
646
- .chat-input-container {{
647
- border-top: 1px solid #e0e0e0;
648
- padding-top: 15px;
649
- }}
650
-
651
- .role-selector {{
652
- margin-bottom: 10px;
653
- }}
654
-
655
- .role-selector label {{
656
- font-weight: 500;
657
- margin-right: 10px;
658
- }}
659
-
660
- .role-selector select {{
661
- padding: 5px 10px;
662
- border: 1px solid #ddd;
663
- border-radius: 4px;
664
- }}
665
-
666
- .message-input {{
667
- display: flex;
668
- gap: 10px;
669
- align-items: flex-end;
670
- }}
671
-
672
- .message-input textarea {{
673
- flex: 1;
674
- padding: 10px;
675
- border: 1px solid #ddd;
676
- border-radius: 4px;
677
- resize: vertical;
678
- font-family: inherit;
679
- }}
680
-
681
- .message-input textarea:focus {{
682
- outline: none;
683
- border-color: #007bff;
684
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
685
- }}
686
-
687
- /* Instructions Section Styles */
688
- .instructions-section {{
689
- background: white;
690
- border: 1px solid #e0e0e0;
691
- border-radius: 8px;
692
- padding: 20px;
693
- margin-bottom: 20px;
694
- }}
695
-
696
- .instructions-header {{
697
- display: flex;
698
- justify-content: space-between;
699
- align-items: center;
700
- margin-bottom: 15px;
701
- }}
702
-
703
- .instructions-title {{
704
- font-size: 18px;
705
- font-weight: 600;
706
- color: #333;
707
- margin: 0;
708
- }}
709
-
710
- .instructions-toggle {{
711
- background: #f8f9fa;
712
- border: 1px solid #dee2e6;
713
- border-radius: 4px;
714
- padding: 5px 10px;
715
- cursor: pointer;
716
- font-size: 12px;
717
- color: #6c757d;
718
- }}
719
-
720
- .instructions-toggle:hover {{
721
- background: #e9ecef;
722
- }}
723
-
724
- .instructions-content {{
725
- display: none;
726
- max-height: 400px;
727
- overflow-y: auto;
728
- border-top: 1px solid #e0e0e0;
729
- padding-top: 15px;
730
- }}
731
-
732
- .instructions-content.expanded {{
733
- display: block;
734
- }}
735
-
736
- .instructions-content h1,
737
- .instructions-content h2,
738
- .instructions-content h3 {{
739
- color: #333;
740
- margin-top: 20px;
741
- margin-bottom: 10px;
742
- }}
743
-
744
- .instructions-content h1 {{
745
- font-size: 24px;
746
- border-bottom: 2px solid #007bff;
747
- padding-bottom: 10px;
748
- }}
749
-
750
- .instructions-content h2 {{
751
- font-size: 20px;
752
- }}
753
-
754
- .instructions-content h3 {{
755
- font-size: 16px;
756
- }}
757
-
758
- .instructions-content p {{
759
- margin-bottom: 10px;
760
- line-height: 1.6;
761
- }}
762
-
763
- .instructions-content code {{
764
- background: #f8f9fa;
765
- padding: 2px 4px;
766
- border-radius: 3px;
767
- font-family: monospace;
768
- font-size: 14px;
769
- }}
770
-
771
- .instructions-content pre {{
772
- background: #f8f9fa;
773
- border: 1px solid #e9ecef;
774
- border-radius: 4px;
775
- padding: 15px;
776
- overflow-x: auto;
777
- margin: 10px 0;
778
- }}
779
-
780
- .instructions-content pre code {{
781
- background: none;
782
- padding: 0;
783
- }}
784
-
785
- .instructions-content ul,
786
- .instructions-content ol {{
787
- margin: 10px 0;
788
- padding-left: 20px;
789
- }}
790
-
791
- .instructions-content li {{
792
- margin-bottom: 5px;
793
- }}
794
-
795
- .instructions-content table {{
796
- border-collapse: collapse;
797
- width: 100%;
798
- margin: 15px 0;
799
- }}
800
-
801
- .instructions-content th,
802
- .instructions-content td {{
803
- border: 1px solid #dee2e6;
804
- padding: 8px 12px;
805
- text-align: left;
806
- }}
807
-
808
- .instructions-content th {{
809
- background: #f8f9fa;
810
- font-weight: 600;
811
- }}
812
-
813
- /* Enhanced Form Styles */
814
- .help-text {{
815
- display: block;
816
- margin-top: 5px;
817
- font-size: 12px;
818
- color: #6c757d;
819
- font-style: italic;
820
- }}
821
-
822
- .form-group label {{
823
- font-weight: 500;
824
- color: #333;
825
- margin-bottom: 5px;
826
- }}
827
-
828
- .form-group select {{
829
- width: 100%;
830
- padding: 8px 12px;
831
- border: 1px solid #ddd;
832
- border-radius: 4px;
833
- font-size: 14px;
834
- background-color: white;
835
- }}
836
-
837
- .form-group select:focus {{
838
- outline: none;
839
- border-color: #007bff;
840
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
841
- }}
842
-
843
- .form-group textarea {{
844
- width: 100%;
845
- padding: 8px 12px;
846
- border: 1px solid #ddd;
847
- border-radius: 4px;
848
- font-size: 14px;
849
- font-family: inherit;
850
- resize: vertical;
851
- }}
852
-
853
- .form-group textarea:focus {{
854
- outline: none;
855
- border-color: #007bff;
856
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
857
- }}
858
-
859
- .form-group input[type="number"] {{
860
- width: 100%;
861
- padding: 8px 12px;
862
- border: 1px solid #ddd;
863
- border-radius: 4px;
864
- font-size: 14px;
865
- }}
866
-
867
- .form-group input[type="number"]:focus {{
868
- outline: none;
869
- border-color: #007bff;
870
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
871
- }}
872
-
873
- .form-group input[type="text"]:focus {{
874
- outline: none;
875
- border-color: #007bff;
876
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
877
- }}
878
-
879
- .required-indicator {{
880
- color: #dc3545;
881
- font-weight: bold;
882
- }}
883
-
884
- .form-group .field-description {{
885
- font-size: 11px;
886
- color: #666;
887
- margin-top: 2px;
888
- font-style: italic;
889
- }}
890
- </style>
891
- </head>
892
- <body>
893
- <div class="container">
894
- <!-- Left Pane: HumanAgent Interface -->
895
- <div class="left-pane">
896
- <div class="pane-header">
897
- <span class="status-indicator status-disconnected" id="connection-status"></span>
898
- HumanAgent Interface
899
- </div>
900
- <div class="pane-content">
901
- <!-- Instructions Section -->
902
- {_generate_instructions_section(metadata)}
903
-
904
- <!-- Action Form or Chat Interface -->
905
- {_generate_action_interface(action_fields, is_chat_env)}
906
-
907
- <!-- Control Buttons -->
908
- <div style="margin-bottom: 20px;">
909
- <button class="btn btn-secondary" id="reset-btn">Reset Environment</button>
910
- <button class="btn btn-secondary" id="state-btn">Get State</button>
911
- </div>
912
-
913
- <!-- Current State Display -->
914
- <div class="state-display">
915
- <h3>Current State</h3>
916
- <div id="current-state">
917
- <div class="state-item">
918
- <span class="state-label">Status:</span>
919
- <span class="state-value" id="env-status">Not initialized</span>
920
- </div>
921
- <div class="state-item">
922
- <span class="state-label">Episode ID:</span>
923
- <span class="state-value" id="episode-id">-</span>
924
- </div>
925
- <div class="state-item">
926
- <span class="state-label">Step Count:</span>
927
- <span class="state-value" id="step-count">0</span>
928
- </div>
929
- </div>
930
- </div>
931
- </div>
932
- </div>
933
-
934
- <!-- Right Pane: State Observer -->
935
- <div class="right-pane">
936
- <div class="pane-header">
937
- State Observer
938
- </div>
939
- <div class="pane-content">
940
- <!-- Current Observation -->
941
- <div class="state-display">
942
- <h3>Current Observation</h3>
943
- <div id="current-observation" class="json-display">
944
- No observation yet
945
- </div>
946
- </div>
947
-
948
- <!-- Action Logs -->
949
- <div class="logs-container">
950
- <h3>Action History</h3>
951
- <div id="action-logs">
952
- No actions taken yet
953
- </div>
954
- </div>
955
- </div>
956
- </div>
957
- </div>
958
-
959
- <script>
960
- class OpenEnvWebInterface {{
961
- constructor() {{
962
- this.ws = null;
963
- this.isConnected = false;
964
- this.init();
965
- }}
966
-
967
- init() {{
968
- this.connectWebSocket();
969
- this.setupEventListeners();
970
- }}
971
-
972
- connectWebSocket() {{
973
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
974
- const wsUrl = `${{protocol}}//${{window.location.host}}/ws`;
975
-
976
- this.ws = new WebSocket(wsUrl);
977
-
978
- this.ws.onopen = () => {{
979
- this.isConnected = true;
980
- this.updateConnectionStatus(true);
981
- console.log('WebSocket connected');
982
- }};
983
-
984
- this.ws.onmessage = (event) => {{
985
- const data = JSON.parse(event.data);
986
- if (data.type === 'state_update') {{
987
- this.updateUI(data.episode_state);
988
- }}
989
- }};
990
-
991
- this.ws.onclose = () => {{
992
- this.isConnected = false;
993
- this.updateConnectionStatus(false);
994
- console.log('WebSocket disconnected');
995
- // Attempt to reconnect after 3 seconds
996
- setTimeout(() => this.connectWebSocket(), 3000);
997
- }};
998
-
999
- this.ws.onerror = (error) => {{
1000
- console.error('WebSocket error:', error);
1001
- }};
1002
- }}
1003
-
1004
- setupEventListeners() {{
1005
- // Instructions toggle
1006
- const instructionsToggle = document.getElementById('instructions-toggle');
1007
- const instructionsContent = document.getElementById('instructions-content');
1008
- if (instructionsToggle && instructionsContent) {{
1009
- instructionsToggle.addEventListener('click', () => {{
1010
- instructionsContent.classList.toggle('expanded');
1011
- instructionsToggle.textContent = instructionsContent.classList.contains('expanded')
1012
- ? 'Hide Instructions' : 'Show Instructions';
1013
- }});
1014
- }}
1015
-
1016
- // Check if this is a chat environment
1017
- const isChatEnv = document.getElementById('chat-messages') !== null;
1018
-
1019
- if (isChatEnv) {{
1020
- // Chat environment event listeners
1021
- document.getElementById('send-message-btn').addEventListener('click', () => {{
1022
- this.sendMessage();
1023
- }});
1024
-
1025
- // Send message on Enter (but allow Shift+Enter for new lines)
1026
- document.getElementById('message-input').addEventListener('keydown', (e) => {{
1027
- if (e.key === 'Enter' && !e.shiftKey) {{
1028
- e.preventDefault();
1029
- this.sendMessage();
1030
- }}
1031
- }});
1032
- }} else {{
1033
- // Traditional action form submission
1034
- const actionForm = document.getElementById('action-form');
1035
- if (actionForm) {{
1036
- actionForm.addEventListener('submit', (e) => {{
1037
- e.preventDefault();
1038
- this.submitAction();
1039
- }});
1040
- }}
1041
- }}
1042
-
1043
- // Reset button
1044
- document.getElementById('reset-btn').addEventListener('click', () => {{
1045
- this.resetEnvironment();
1046
- }});
1047
-
1048
- // State button
1049
- document.getElementById('state-btn').addEventListener('click', () => {{
1050
- this.getState();
1051
- }});
1052
- }}
1053
-
1054
- async sendMessage() {{
1055
- const messageInput = document.getElementById('message-input');
1056
- const roleSelect = document.getElementById('message-role');
1057
- const message = messageInput.value.trim();
1058
- const role = roleSelect.value;
1059
-
1060
- if (!message) {{
1061
- return;
1062
- }}
1063
-
1064
- // Add message to chat display immediately
1065
- this.addMessageToChat(role, message);
1066
-
1067
- // Clear input
1068
- messageInput.value = '';
1069
-
1070
- try {{
1071
- // Send message to server to convert to action and step
1072
- const response = await fetch('/web/step', {{
1073
- method: 'POST',
1074
- headers: {{ 'Content-Type': 'application/json' }},
1075
- body: JSON.stringify({{
1076
- message: {{
1077
- role: role,
1078
- content: message
1079
- }}
1080
- }})
1081
- }});
1082
-
1083
- if (!response.ok) {{
1084
- throw new Error(`HTTP error! status: ${{response.status}}`);
1085
- }}
1086
-
1087
- const result = await response.json();
1088
- console.log('Message sent:', result);
1089
- }} catch (error) {{
1090
- console.error('Error sending message:', error);
1091
- alert('Error sending message: ' + error.message);
1092
- }}
1093
- }}
1094
-
1095
- addMessageToChat(role, content) {{
1096
- const chatMessages = document.getElementById('chat-messages');
1097
- const messageDiv = document.createElement('div');
1098
- messageDiv.className = `chat-message ${{role}}`;
1099
-
1100
- messageDiv.innerHTML = `
1101
- <div class="message-role">${{role.charAt(0).toUpperCase() + role.slice(1)}}</div>
1102
- <div class="message-content">${{content}}</div>
1103
- `;
1104
-
1105
- chatMessages.appendChild(messageDiv);
1106
- chatMessages.scrollTop = chatMessages.scrollHeight;
1107
- }}
1108
-
1109
- async submitAction() {{
1110
- const formData = new FormData(document.getElementById('action-form'));
1111
- const action = {{}};
1112
-
1113
- // Collect form data
1114
- for (const [key, value] of formData.entries()) {{
1115
- if (value !== '') {{
1116
- // Handle tensor fields (tokens) - convert comma-separated string to array
1117
- if (key === 'tokens') {{
1118
- try {{
1119
- action[key] = value.split(',').map(x => parseInt(x.trim())).filter(x => !isNaN(x));
1120
- }} catch (e) {{
1121
- console.error('Error parsing tokens:', e);
1122
- action[key] = [];
1123
- }}
1124
- }} else {{
1125
- action[key] = value;
1126
- }}
1127
- }}
1128
- }}
1129
-
1130
- try {{
1131
- const response = await fetch('/web/step', {{
1132
- method: 'POST',
1133
- headers: {{ 'Content-Type': 'application/json' }},
1134
- body: JSON.stringify({{ action }})
1135
- }});
1136
-
1137
- if (!response.ok) {{
1138
- throw new Error(`HTTP error! status: ${{response.status}}`);
1139
- }}
1140
-
1141
- const result = await response.json();
1142
- console.log('Step result:', result);
1143
- }} catch (error) {{
1144
- console.error('Error submitting action:', error);
1145
- alert('Error submitting action: ' + error.message);
1146
- }}
1147
- }}
1148
-
1149
- async resetEnvironment() {{
1150
- try {{
1151
- const response = await fetch('/web/reset', {{
1152
- method: 'POST',
1153
- headers: {{ 'Content-Type': 'application/json' }}
1154
- }});
1155
-
1156
- if (!response.ok) {{
1157
- throw new Error(`HTTP error! status: ${{response.status}}`);
1158
- }}
1159
-
1160
- const result = await response.json();
1161
- console.log('Reset result:', result);
1162
- }} catch (error) {{
1163
- console.error('Error resetting environment:', error);
1164
- alert('Error resetting environment: ' + error.message);
1165
- }}
1166
- }}
1167
-
1168
- async getState() {{
1169
- try {{
1170
- const response = await fetch('/web/state');
1171
- const state = await response.json();
1172
- console.log('Current state:', state);
1173
- alert('Current state: ' + JSON.stringify(state, null, 2));
1174
- }} catch (error) {{
1175
- console.error('Error getting state:', error);
1176
- alert('Error getting state: ' + error.message);
1177
- }}
1178
- }}
1179
-
1180
- updateConnectionStatus(connected) {{
1181
- const indicator = document.getElementById('connection-status');
1182
- if (connected) {{
1183
- indicator.className = 'status-indicator status-connected';
1184
- }} else {{
1185
- indicator.className = 'status-indicator status-disconnected';
1186
- }}
1187
- }}
1188
-
1189
- updateUI(episodeState) {{
1190
- // Check if this is a chat environment
1191
- const isChatEnv = document.getElementById('chat-messages') !== null;
1192
-
1193
- // Update current state
1194
- document.getElementById('env-status').textContent =
1195
- episodeState.is_reset ? 'Reset' : 'Running';
1196
- document.getElementById('episode-id').textContent =
1197
- episodeState.episode_id || '-';
1198
- document.getElementById('step-count').textContent =
1199
- episodeState.step_count.toString();
1200
-
1201
- if (isChatEnv) {{
1202
- // Update chat interface
1203
- this.updateChatInterface(episodeState);
1204
- }} else {{
1205
- // Update traditional observation display
1206
- const observationDiv = document.getElementById('current-observation');
1207
- if (episodeState.current_observation) {{
1208
- observationDiv.textContent = JSON.stringify(
1209
- episodeState.current_observation, null, 2
1210
- );
1211
- }} else {{
1212
- observationDiv.textContent = 'No observation yet';
1213
- }}
1214
- }}
1215
-
1216
- // Update action logs
1217
- const logsDiv = document.getElementById('action-logs');
1218
- if (episodeState.action_logs.length === 0) {{
1219
- logsDiv.innerHTML = 'No actions taken yet';
1220
- }} else {{
1221
- logsDiv.innerHTML = episodeState.action_logs.map(log => `
1222
- <div class="log-entry">
1223
- <div class="log-timestamp">${{log.timestamp}} (Step ${{log.step_count}})</div>
1224
- <div class="log-action">Action: ${{JSON.stringify(log.action, null, 2)}}</div>
1225
- <div class="log-observation">Observation: ${{JSON.stringify(log.observation, null, 2)}}</div>
1226
- <div>
1227
- <span class="log-reward">Reward: ${{log.reward !== null ? log.reward : 'None'}}</span>
1228
- ${{log.done ? '<span class="log-done">DONE</span>' : ''}}
1229
- </div>
1230
- </div>
1231
- `).join('');
1232
- }}
1233
- }}
1234
-
1235
- updateChatInterface(episodeState) {{
1236
- const chatMessages = document.getElementById('chat-messages');
1237
- if (!chatMessages) return;
1238
-
1239
- // Clear existing messages (except system message)
1240
- const systemMessage = chatMessages.querySelector('.chat-message.system');
1241
- chatMessages.innerHTML = '';
1242
- if (systemMessage) {{
1243
- chatMessages.appendChild(systemMessage);
1244
- }}
1245
-
1246
- // Add messages from current observation
1247
- if (episodeState.current_observation && episodeState.current_observation.messages) {{
1248
- episodeState.current_observation.messages.forEach(msg => {{
1249
- this.addMessageToChat(msg.role, msg.content);
1250
- }});
1251
- }}
1252
- }}
1253
- }}
1254
-
1255
- // Initialize the web interface when the page loads
1256
- document.addEventListener('DOMContentLoaded', () => {{
1257
- new OpenEnvWebInterface();
1258
- }});
1259
- </script>
1260
- </body>
1261
- </html>
1262
- """.replace('{_generate_action_form_fields(action_fields)}', _generate_action_form_fields(action_fields))
1263
-
1264
-
1265
- def _generate_instructions_section(metadata: Optional[EnvironmentMetadata]) -> str:
1266
- """Generate the instructions section with environment documentation."""
1267
- if not metadata or not metadata.readme_content:
1268
- return ''
1269
-
1270
- # Convert markdown to HTML (basic conversion)
1271
- import re
1272
- html_content = _markdown_to_html(metadata.readme_content)
1273
-
1274
- return f'''
1275
- <!-- Instructions Section -->
1276
- <div class="instructions-section">
1277
- <div class="instructions-header">
1278
- <h3 class="instructions-title">{metadata.name}</h3>
1279
- <button class="instructions-toggle" id="instructions-toggle">Show Instructions</button>
1280
- </div>
1281
- <div class="instructions-content" id="instructions-content">
1282
- <div class="instructions-readme">
1283
- {html_content}
1284
- </div>
1285
- </div>
1286
- </div>
1287
- '''
1288
-
1289
-
1290
- def _extract_action_fields(action_cls: Type[Action]) -> List[Dict[str, Any]]:
1291
- """Extract enhanced field metadata from Action class for form generation."""
1292
- import typing
1293
- from typing import get_origin, get_args
1294
-
1295
- action_fields = []
1296
- if not hasattr(action_cls, '__dataclass_fields__'):
1297
- return action_fields
1298
-
1299
- for field_name, field_info in action_cls.__dataclass_fields__.items():
1300
- if field_name == 'metadata':
1301
- continue
1302
-
1303
- field_type = field_info.type
1304
- field_metadata = _extract_field_metadata(field_name, field_info)
1305
-
1306
- # Determine input type based on field type
1307
- input_type = _determine_input_type(field_type)
1308
-
1309
- # Check if field is required
1310
- is_required = field_info.default is field_info.default_factory
1311
-
1312
- action_fields.append({
1313
- 'name': field_name,
1314
- 'type': input_type,
1315
- 'required': is_required,
1316
- 'description': field_metadata.get('description', ''),
1317
- 'default_value': field_metadata.get('default_value'),
1318
- 'choices': field_metadata.get('choices', []),
1319
- 'min_value': field_metadata.get('min_value'),
1320
- 'max_value': field_metadata.get('max_value'),
1321
- 'placeholder': field_metadata.get('placeholder', ''),
1322
- 'help_text': field_metadata.get('help_text', ''),
1323
- })
1324
-
1325
- return action_fields
1326
-
1327
-
1328
- def _extract_field_metadata(field_name: str, field_info) -> Dict[str, Any]:
1329
- """Extract metadata from dataclass field including docstring and type hints."""
1330
- import typing
1331
- from typing import get_origin, get_args, Literal, Union, Optional
1332
-
1333
- metadata = {}
1334
-
1335
- # Extract description from field docstring or annotation
1336
- if hasattr(field_info, 'metadata') and field_info.metadata:
1337
- # Check for custom metadata
1338
- for meta in field_info.metadata:
1339
- if isinstance(meta, dict):
1340
- metadata.update(meta)
1341
-
1342
- # Extract type information
1343
- field_type = field_info.type
1344
- origin = get_origin(field_type)
1345
-
1346
- # Handle Literal types for dropdown choices
1347
- if origin is Literal:
1348
- args = get_args(field_type)
1349
- metadata['choices'] = list(args)
1350
-
1351
- # Handle Optional types
1352
- if origin is Union:
1353
- args = get_args(field_type)
1354
- if len(args) == 2 and type(None) in args:
1355
- # This is Optional[SomeType]
1356
- non_none_type = args[0] if args[1] is type(None) else args[1]
1357
- metadata['optional'] = True
1358
- # Recursively check the non-None type for choices
1359
- if get_origin(non_none_type) is Literal:
1360
- metadata['choices'] = list(get_args(non_none_type))
1361
- else:
1362
- # Regular Union type
1363
- metadata['choices'] = [str(arg) for arg in args if arg is not type(None)]
1364
-
1365
- # Handle numeric constraints
1366
- if field_type in (int, float):
1367
- # Check for common constraint patterns in field name
1368
- if 'count' in field_name.lower() or 'num' in field_name.lower():
1369
- metadata['min_value'] = 0
1370
- if 'id' in field_name.lower():
1371
- metadata['min_value'] = 0
1372
-
1373
- # Generate placeholder text
1374
- if 'message' in field_name.lower():
1375
- metadata['placeholder'] = f'Enter {field_name.replace("_", " ")}...'
1376
- elif 'code' in field_name.lower():
1377
- metadata['placeholder'] = 'Enter Python code here...'
1378
- elif 'tokens' in field_name.lower():
1379
- metadata['placeholder'] = 'Enter comma-separated token IDs (e.g., 1,2,3,4,5)'
1380
- else:
1381
- metadata['placeholder'] = f'Enter {field_name.replace("_", " ")}...'
1382
-
1383
- # Generate help text based on field name and type
1384
- if 'action_id' in field_name.lower():
1385
- metadata['help_text'] = 'The action ID to execute in the environment'
1386
- elif 'game_name' in field_name.lower():
1387
- metadata['help_text'] = 'Name of the game or environment'
1388
- elif 'tokens' in field_name.lower():
1389
- metadata['help_text'] = 'Token IDs as a comma-separated list of integers'
1390
- elif 'code' in field_name.lower():
1391
- metadata['help_text'] = 'Python code to execute in the environment'
1392
- elif 'message' in field_name.lower():
1393
- metadata['help_text'] = 'Text message to send'
1394
-
1395
- return metadata
1396
-
1397
-
1398
- def _determine_input_type(field_type) -> str:
1399
- """Determine the appropriate HTML input type for a field type."""
1400
- import typing
1401
- from typing import get_origin, get_args, Literal, Union
1402
-
1403
- # Handle direct types
1404
- if field_type == str:
1405
- return "text"
1406
- elif field_type == int:
1407
- return "number"
1408
- elif field_type == float:
1409
- return "number"
1410
- elif field_type == bool:
1411
- return "checkbox"
1412
-
1413
- # Handle complex types
1414
- origin = get_origin(field_type)
1415
-
1416
- if origin is Literal:
1417
- return "select"
1418
- elif origin is Union:
1419
- args = get_args(field_type)
1420
- if len(args) == 2 and type(None) in args:
1421
- # Optional type - use the non-None type
1422
- non_none_type = args[0] if args[1] is type(None) else args[1]
1423
- return _determine_input_type(non_none_type)
1424
- elif all(isinstance(arg, str) for arg in args if arg is not type(None)):
1425
- return "select"
1426
- else:
1427
- return "text"
1428
- elif hasattr(field_type, '__name__') and 'Tensor' in field_type.__name__:
1429
- return "tensor"
1430
- else:
1431
- return "text"
1432
-
1433
-
1434
- def _markdown_to_html(markdown: str) -> str:
1435
- """Convert basic markdown to HTML for README display."""
1436
- import html
1437
- import re
1438
-
1439
- # Escape HTML first
1440
- html_content = html.escape(markdown)
1441
-
1442
- # Convert headers
1443
- html_content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html_content, flags=re.MULTILINE)
1444
- html_content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html_content, flags=re.MULTILINE)
1445
- html_content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html_content, flags=re.MULTILINE)
1446
-
1447
- # Convert code blocks
1448
- html_content = re.sub(r'```(.*?)\n(.*?)\n```', r'<pre><code>\2</code></pre>', html_content, flags=re.DOTALL)
1449
- html_content = re.sub(r'`([^`]+)`', r'<code>\1</code>', html_content)
1450
-
1451
- # Convert bold and italic
1452
- html_content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html_content)
1453
- html_content = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html_content)
1454
-
1455
- # Convert lists
1456
- html_content = re.sub(r'^- (.*?)$', r'<li>\1</li>', html_content, flags=re.MULTILINE)
1457
- html_content = re.sub(r'(<li>.*</li>)', r'<ul>\1</ul>', html_content, flags=re.DOTALL)
1458
-
1459
- # Convert line breaks
1460
- html_content = html_content.replace('\n', '<br>')
1461
-
1462
- return html_content
1463
-
1464
-
1465
- def _generate_action_interface(action_fields: List[Dict[str, Any]], is_chat_env: bool) -> str:
1466
- """Generate either a chat interface or action form based on environment type."""
1467
- if is_chat_env:
1468
- return _generate_chat_interface()
1469
- else:
1470
- return _generate_action_form(action_fields)
1471
-
1472
- def _generate_chat_interface() -> str:
1473
- """Generate a chat-style interface for chat environments."""
1474
- return '''
1475
- <!-- Chat Interface -->
1476
- <div class="chat-interface">
1477
- <h3>Chat Interface</h3>
1478
- <div class="chat-messages" id="chat-messages">
1479
- <div class="chat-message system">
1480
- <div class="message-role">System</div>
1481
- <div class="message-content">Chat environment ready. Send a message to start the conversation.</div>
1482
- </div>
1483
- </div>
1484
- <div class="chat-input-container">
1485
- <div class="role-selector">
1486
- <label for="message-role">Role:</label>
1487
- <select id="message-role">
1488
- <option value="user">User</option>
1489
- <option value="assistant">Assistant</option>
1490
- </select>
1491
- </div>
1492
- <div class="message-input">
1493
- <textarea id="message-input" placeholder="Type your message here..." rows="3"></textarea>
1494
- <button class="btn" id="send-message-btn">Send Message</button>
1495
- </div>
1496
- </div>
1497
- </div>
1498
- '''
1499
-
1500
- def _generate_action_form(action_fields: List[Dict[str, Any]]) -> str:
1501
- """Generate a traditional action form for non-chat environments."""
1502
- return f'''
1503
- <!-- Action Form -->
1504
- <div class="action-form">
1505
- <h3>Take Action</h3>
1506
- <form id="action-form">
1507
- {_generate_action_form_fields(action_fields)}
1508
- <button type="submit" class="btn" id="step-btn">Step</button>
1509
- </form>
1510
- </div>
1511
- '''
1512
-
1513
- def _generate_action_form_fields(action_fields: List[Dict[str, Any]]) -> str:
1514
- """Generate HTML form fields for action input with enhanced metadata."""
1515
- if not action_fields:
1516
- return '<p>No action fields available</p>'
1517
-
1518
- fields_html = []
1519
- for field in action_fields:
1520
- field_html = _generate_single_field(field)
1521
- fields_html.append(field_html)
1522
-
1523
- return '\n'.join(fields_html)
1524
-
1525
-
1526
- def _generate_single_field(field: Dict[str, Any]) -> str:
1527
- """Generate HTML for a single form field with enhanced metadata."""
1528
- field_name = field['name']
1529
- field_type = field['type']
1530
- required = field['required']
1531
- placeholder = field.get('placeholder', '')
1532
- help_text = field.get('help_text', '')
1533
- choices = field.get('choices', [])
1534
- min_value = field.get('min_value')
1535
- max_value = field.get('max_value')
1536
- default_value = field.get('default_value')
1537
-
1538
- # Build label with required indicator
1539
- label_text = field_name.replace('_', ' ').title()
1540
- if required:
1541
- label_text += ' <span style="color: red;">*</span>'
1542
-
1543
- # Build input attributes
1544
- input_attrs = []
1545
- if required:
1546
- input_attrs.append('required')
1547
- if placeholder:
1548
- input_attrs.append(f'placeholder="{placeholder}"')
1549
- if min_value is not None:
1550
- input_attrs.append(f'min="{min_value}"')
1551
- if max_value is not None:
1552
- input_attrs.append(f'max="{max_value}"')
1553
- if default_value is not None:
1554
- input_attrs.append(f'value="{default_value}"')
1555
-
1556
- attrs_str = ' '.join(input_attrs)
1557
-
1558
- if field_type == 'checkbox':
1559
- return f'''
1560
- <div class="form-group">
1561
- <label>
1562
- <input type="checkbox" name="{field_name}" value="true" {attrs_str}>
1563
- {label_text}
1564
- </label>
1565
- {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1566
- </div>
1567
- '''
1568
-
1569
- elif field_type == 'select':
1570
- options_html = []
1571
- if not required:
1572
- options_html.append(f'<option value="">-- Select {label_text} --</option>')
1573
-
1574
- for choice in choices:
1575
- selected = 'selected' if str(choice) == str(default_value) else ''
1576
- options_html.append(f'<option value="{choice}" {selected}>{choice}</option>')
1577
-
1578
- return f'''
1579
- <div class="form-group">
1580
- <label for="{field_name}">{label_text}:</label>
1581
- <select name="{field_name}" id="{field_name}" {attrs_str}>
1582
- {''.join(options_html)}
1583
- </select>
1584
- {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1585
- </div>
1586
- '''
1587
-
1588
- elif field_type == 'tensor':
1589
- return f'''
1590
- <div class="form-group">
1591
- <label for="{field_name}">{label_text} (comma-separated integers):</label>
1592
- <input type="text" name="{field_name}" id="{field_name}" {attrs_str}>
1593
- <small class="help-text">{help_text or 'Enter token IDs as comma-separated integers (e.g., 1,2,3,4,5)'}</small>
1594
- </div>
1595
- '''
1596
-
1597
- elif field_type == 'text' and ('message' in field_name.lower() or 'code' in field_name.lower()):
1598
- return f'''
1599
- <div class="form-group">
1600
- <label for="{field_name}">{label_text}:</label>
1601
- <textarea name="{field_name}" id="{field_name}" rows="3" {attrs_str}></textarea>
1602
- {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1603
- </div>
1604
- '''
1605
-
1606
- else:
1607
- return f'''
1608
- <div class="form-group">
1609
- <label for="{field_name}">{label_text}:</label>
1610
- <input type="{field_type}" name="{field_name}" id="{field_name}" {attrs_str}>
1611
- {f'<small class="help-text">{help_text}</small>' if help_text else ''}
1612
- </div>
1613
- '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/http_env_client.py DELETED
@@ -1,203 +0,0 @@
1
- """
2
- core/runner_env.py
3
- Minimal HTTP-based environment client.
4
- - Talks to a single env worker exposing: POST /reset, POST /step
5
-
6
- Future hooks (commented below) for:
7
- - episode_id, seed on reset
8
- - request_id on step
9
- - custom headers (auth/trace)
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- from abc import ABC, abstractmethod
15
- from typing import Any, Dict, Generic, Optional, Type, TYPE_CHECKING, TypeVar
16
-
17
- import requests
18
-
19
- from .client_types import StepResult
20
- from .containers.runtime import LocalDockerProvider
21
-
22
- if TYPE_CHECKING:
23
- from .containers.runtime import ContainerProvider
24
-
25
- ActT = TypeVar("ActT")
26
- ObsT = TypeVar("ObsT")
27
- EnvClientT = TypeVar("EnvClientT", bound="HTTPEnvClient")
28
-
29
-
30
- class HTTPEnvClient(ABC, Generic[ActT, ObsT]):
31
- def __init__(
32
- self,
33
- base_url: str,
34
- request_timeout_s: float = 15.0,
35
- default_headers: Optional[Dict[str, str]] = None,
36
- provider: Optional["ContainerProvider"] = None,
37
- ):
38
- self._base = base_url.rstrip("/")
39
- self._timeout = float(request_timeout_s)
40
- self._http = requests.Session()
41
- self._headers = default_headers or {}
42
- self._provider = provider
43
-
44
- @classmethod
45
- def from_docker_image(
46
- cls: Type[EnvClientT],
47
- image: str,
48
- provider: Optional["ContainerProvider"] = None,
49
- **kwargs: Any,
50
- ) -> EnvClientT:
51
- """
52
- Create an environment client by spinning up a Docker container locally.
53
-
54
- This is a development utility that:
55
- 1. Starts a Docker container from the specified image
56
- 2. Waits for the server to be ready
57
- 3. Creates and returns a client instance connected to the container
58
-
59
- Note: The container lifecycle management is left to the user or higher-level
60
- orchestration. The container will keep running until manually stopped.
61
-
62
- Args:
63
- image: Docker image name to run (e.g., "echo-env:latest")
64
- provider: Container provider to use (defaults to LocalDockerProvider)
65
- **kwargs: Additional arguments to pass to provider.start_container()
66
- (e.g., env_vars, port)
67
-
68
- Returns:
69
- An instance of the client class connected to the running container
70
-
71
- Example:
72
- >>> from envs.coding_env.client import CodingEnv
73
- >>> from envs.coding_env.models import CodeAction
74
- >>>
75
- >>> # Create environment from image
76
- >>> env = CodingEnv.from_docker_image("coding-env:latest")
77
- >>>
78
- >>> # Create environment with custom env vars
79
- >>> env = CodingEnv.from_docker_image(
80
- ... "coding-env:latest",
81
- ... env_vars={"MY_VAR": "value"}
82
- ... )
83
- >>>
84
- >>> # Use the environment
85
- >>> result = env.reset()
86
- >>> print(result.observation)
87
- >>>
88
- >>> step_result = env.step(CodeAction(code="print('hello')"))
89
- >>> print(step_result.observation.stdout)
90
- >>>
91
- >>> # Cleanup (optional)
92
- >>> env.close()
93
- """
94
-
95
- # Use default provider if none provided
96
- if provider is None:
97
- provider = LocalDockerProvider()
98
-
99
- # 1. Start container with optional kwargs (e.g., env_vars, port)
100
- base_url = provider.start_container(image, **kwargs)
101
-
102
- # 2. Wait for server to be ready
103
- provider.wait_for_ready(base_url)
104
-
105
- # 3. Create and return client instance with provider reference
106
- return cls(base_url=base_url, provider=provider)
107
-
108
- @classmethod
109
- def from_hub(cls: Type[EnvClientT], repo_id: str, provider: Optional["ContainerProvider"] = None, **kwargs: Any) -> EnvClientT:
110
- """
111
- Create an environment client by pulling from a Hugging Face model hub.
112
- """
113
-
114
- if provider is None:
115
- provider = LocalDockerProvider()
116
-
117
- if "tag" in kwargs:
118
- tag = kwargs["tag"]
119
- else:
120
- tag = "latest"
121
-
122
- base_url = f"registry.hf.space/{repo_id.replace('/', '-')}:{tag}"
123
-
124
- return cls.from_docker_image(image=base_url, provider=provider)
125
-
126
- @abstractmethod
127
- def _step_payload(self, action: ActT) -> dict:
128
- """Convert an Action object to the JSON body expected by the env server."""
129
- raise NotImplementedError
130
-
131
- @abstractmethod
132
- def _parse_result(self, payload: dict) -> StepResult[ObsT]:
133
- """Convert a JSON response from the env server to StepResult[ObsT]."""
134
- raise NotImplementedError
135
-
136
- @abstractmethod
137
- def _parse_state(self, payload: dict) -> Any:
138
- """Convert a JSON response from the state endpoint to a State object."""
139
- raise NotImplementedError
140
-
141
- # ---------- Environment Server Interface Methods ----------
142
- def reset(self) -> StepResult[ObsT]:
143
- body: Dict[str, Any] = {}
144
- # TODO: later:
145
- # body["seed"] = seed
146
- # body["episode_id"] = episode_id
147
- r = self._http.post(
148
- f"{self._base}/reset",
149
- json=body,
150
- headers=self._headers,
151
- timeout=self._timeout,
152
- )
153
- r.raise_for_status()
154
- return self._parse_result(r.json())
155
-
156
- def step(self, action: ActT) -> StepResult[ObsT]:
157
- body: Dict[str, Any] = {
158
- "action": self._step_payload(action),
159
- "timeout_s": int(self._timeout),
160
- }
161
- # TODO: later:
162
- # body["request_id"] = str(uuid.uuid4())
163
- # body["episode_id"] = current_episode_id
164
- r = self._http.post(
165
- f"{self._base}/step",
166
- json=body,
167
- headers=self._headers,
168
- timeout=self._timeout,
169
- )
170
- r.raise_for_status()
171
- return self._parse_result(r.json())
172
-
173
- def state(self) -> Any:
174
- """
175
- Get the current environment state from the server.
176
-
177
- Returns:
178
- State object with environment state information (e.g., episode_id, step_count)
179
-
180
- Example:
181
- >>> client = EchoEnv.from_docker_image("echo-env:latest")
182
- >>> result = client.reset()
183
- >>> state = client.state()
184
- >>> print(state.episode_id)
185
- >>> print(state.step_count)
186
- """
187
- r = self._http.get(
188
- f"{self._base}/state",
189
- headers=self._headers,
190
- timeout=self._timeout,
191
- )
192
- r.raise_for_status()
193
- return self._parse_state(r.json())
194
-
195
- def close(self) -> None:
196
- """
197
- Close the environment and clean up resources.
198
-
199
- If this client was created via from_docker_image(), this will stop
200
- and remove the associated container.
201
- """
202
- if self._provider is not None:
203
- self._provider.stop_container()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/tools/__init__.py DELETED
@@ -1,16 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Core tools for code execution and other utilities."""
8
-
9
- from .git_server_client import GitServerClient, RepoInfo
10
- from .local_python_executor import PyExecutor
11
-
12
- __all__ = [
13
- "PyExecutor",
14
- "GitServerClient",
15
- "RepoInfo",
16
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/tools/git_server_client.py DELETED
@@ -1,362 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Git Server Client for connecting to external Gitea instance.
4
-
5
- This module provides a lightweight client for interacting with a shared
6
- Gitea service, optimized for task-based isolation where multiple environment
7
- instances share the same Gitea server but have isolated workspaces.
8
- """
9
-
10
- import json
11
- import os
12
- import shutil
13
- import subprocess
14
- import time
15
- from dataclasses import dataclass
16
- from pathlib import Path
17
- from urllib.parse import urlparse
18
-
19
-
20
- @dataclass
21
- class RepoInfo:
22
- """Information about a repository."""
23
-
24
- name: str
25
- url: str
26
- commit: str
27
- clone_url: str
28
-
29
-
30
- class GitServerClient:
31
- """
32
- Client for connecting to an external Gitea server.
33
-
34
- This client is optimized for task-based isolation where:
35
- - Multiple tasks share the same Gitea instance
36
- - Each task has its own isolated workspace
37
- - Fast reset() via git operations (no server restart)
38
- - Repos are pre-migrated to Gitea once
39
-
40
- Args:
41
- gitea_url: URL of the Gitea server (e.g., "http://gitea:3000")
42
- username: Gitea username for authentication
43
- password: Gitea password for authentication
44
- workspace_dir: Local workspace directory for cloning repos
45
-
46
- Example:
47
- >>> # Connect to shared Gitea (credentials from environment)
48
- >>> import os
49
- >>> client = GitServerClient(
50
- ... gitea_url=os.getenv("GITEA_URL"),
51
- ... username=os.getenv("GITEA_USERNAME"),
52
- ... password=os.getenv("GITEA_PASSWORD")
53
- ... )
54
- >>> client.wait_for_ready()
55
- >>> # Clone repo to workspace
56
- >>> path = client.clone_to_workspace("my-repo", commit="abc123")
57
- >>> # Fast reset to base state
58
- >>> client.reset_workspace("my-repo", commit="abc123")
59
- """
60
-
61
- def __init__(
62
- self,
63
- gitea_url: str,
64
- username: str,
65
- password: str,
66
- workspace_dir: str = "/workspace",
67
- ):
68
- """Initialize Git Server Client."""
69
- self.gitea_url = gitea_url.rstrip("/")
70
- self.username = username
71
- self.password = password
72
- self.workspace_dir = Path(workspace_dir)
73
- self.is_ready = False
74
-
75
- # Parse Gitea URL
76
- parsed = urlparse(self.gitea_url)
77
- self.domain = parsed.hostname or "localhost"
78
- self.port = parsed.port or 3000
79
-
80
- # Ensure workspace exists
81
- os.makedirs(self.workspace_dir, exist_ok=True)
82
-
83
- # Configure git credentials
84
- self._configure_git()
85
-
86
- def _configure_git(self):
87
- """Configure git credentials for automatic authentication."""
88
- home_dir = Path.home()
89
-
90
- # Git config
91
- git_config = f"""[user]
92
- name = {self.username}
93
- email = {self.username}@local.env
94
- [init]
95
- defaultBranch = main
96
- [credential]
97
- helper = store
98
- """
99
- gitconfig_path = home_dir / ".gitconfig"
100
- gitconfig_path.write_text(git_config)
101
-
102
- # Git credentials
103
- git_credentials = f"http://{self.username}:{self.password}@{self.domain}:{self.port}\n"
104
- gitcreds_path = home_dir / ".git-credentials"
105
- gitcreds_path.write_text(git_credentials)
106
- gitcreds_path.chmod(0o600)
107
-
108
- def wait_for_ready(self, timeout: int = 30) -> bool:
109
- """
110
- Wait for Gitea server to be ready.
111
-
112
- Args:
113
- timeout: Maximum seconds to wait
114
-
115
- Returns:
116
- True if server is ready, False otherwise
117
- """
118
- start_time = time.time()
119
- while time.time() - start_time < timeout:
120
- try:
121
- result = subprocess.run(
122
- ["curl", "-sf", f"{self.gitea_url}/"],
123
- capture_output=True,
124
- timeout=5,
125
- )
126
- if result.returncode == 0:
127
- self.is_ready = True
128
- return True
129
- except subprocess.TimeoutExpired:
130
- pass
131
- except Exception:
132
- pass
133
-
134
- time.sleep(1)
135
-
136
- return False
137
-
138
- def list_repositories(self) -> list[dict[str, str]]:
139
- """
140
- List all repositories in Gitea.
141
-
142
- Returns:
143
- List of repository information dictionaries
144
- """
145
- if not self.is_ready:
146
- raise RuntimeError("Gitea server is not ready")
147
-
148
- result = subprocess.run(
149
- [
150
- "curl",
151
- "-s",
152
- f"{self.gitea_url}/api/v1/user/repos",
153
- "-u",
154
- f"{self.username}:{self.password}",
155
- ],
156
- capture_output=True,
157
- text=True,
158
- )
159
-
160
- if result.returncode != 0:
161
- return []
162
-
163
- try:
164
- repos = json.loads(result.stdout)
165
- return [
166
- {
167
- "name": repo["name"],
168
- "full_name": repo["full_name"],
169
- "clone_url": repo["clone_url"],
170
- "description": repo.get("description", ""),
171
- }
172
- for repo in repos
173
- ]
174
- except (json.JSONDecodeError, KeyError):
175
- return []
176
-
177
- def clone_to_workspace(
178
- self, repo_name: str, target_dir: str | None = None, commit: str = "main"
179
- ) -> str:
180
- """
181
- Clone a repository to the workspace at a specific commit.
182
-
183
- This creates a fresh clone optimized for task isolation.
184
-
185
- Args:
186
- repo_name: Name of repository to clone
187
- target_dir: Target directory name (defaults to repo_name)
188
- commit: Commit hash or branch to check out
189
-
190
- Returns:
191
- Path to cloned repository
192
-
193
- Raises:
194
- RuntimeError: If clone fails
195
- """
196
- if not self.is_ready:
197
- raise RuntimeError("Gitea server is not ready")
198
-
199
- target_dir = target_dir or repo_name
200
- target_path = self.workspace_dir / target_dir
201
-
202
- # Remove existing directory if present
203
- if target_path.exists():
204
- shutil.rmtree(target_path)
205
-
206
- clone_url = f"{self.gitea_url}/{self.username}/{repo_name}.git"
207
-
208
- # Clone repository
209
- result = subprocess.run(
210
- ["git", "clone", clone_url, str(target_path)],
211
- capture_output=True,
212
- text=True,
213
- )
214
-
215
- if result.returncode != 0:
216
- raise RuntimeError(f"Clone failed: {result.stderr}")
217
-
218
- # Checkout specific commit
219
- if commit != "main":
220
- result = subprocess.run(
221
- ["git", "checkout", commit],
222
- cwd=str(target_path),
223
- capture_output=True,
224
- text=True,
225
- )
226
-
227
- if result.returncode != 0:
228
- raise RuntimeError(f"Checkout failed: {result.stderr}")
229
-
230
- return str(target_path)
231
-
232
- def reset_workspace(self, repo_name: str, commit: str = "main") -> bool:
233
- """
234
- Fast reset of workspace to base state (optimized for task resets).
235
-
236
- This is much faster than re-cloning. It:
237
- 1. Checks out the target commit
238
- 2. Resets to that commit (hard)
239
- 3. Cleans untracked files
240
-
241
- Args:
242
- repo_name: Name of repository (directory in workspace)
243
- commit: Commit hash or branch to reset to
244
-
245
- Returns:
246
- True if reset successful
247
-
248
- Raises:
249
- RuntimeError: If reset fails
250
- """
251
- repo_path = self.workspace_dir / repo_name
252
-
253
- if not repo_path.exists():
254
- raise RuntimeError(f"Repository not found in workspace: {repo_name}")
255
-
256
- # Fetch latest (in case commit is new)
257
- subprocess.run(
258
- ["git", "fetch", "--all"],
259
- cwd=str(repo_path),
260
- capture_output=True,
261
- )
262
-
263
- # Checkout and hard reset to commit
264
- result = subprocess.run(
265
- ["git", "checkout", commit],
266
- cwd=str(repo_path),
267
- capture_output=True,
268
- text=True,
269
- )
270
-
271
- if result.returncode != 0:
272
- raise RuntimeError(f"Checkout failed: {result.stderr}")
273
-
274
- result = subprocess.run(
275
- ["git", "reset", "--hard", f"origin/{commit}" if commit != "main" else commit],
276
- cwd=str(repo_path),
277
- capture_output=True,
278
- text=True,
279
- )
280
-
281
- if result.returncode != 0:
282
- # Try without origin/ prefix
283
- result = subprocess.run(
284
- ["git", "reset", "--hard", commit],
285
- cwd=str(repo_path),
286
- capture_output=True,
287
- text=True,
288
- )
289
- if result.returncode != 0:
290
- raise RuntimeError(f"Reset failed: {result.stderr}")
291
-
292
- # Clean untracked files and directories
293
- subprocess.run(
294
- ["git", "clean", "-fdx"],
295
- cwd=str(repo_path),
296
- capture_output=True,
297
- )
298
-
299
- return True
300
-
301
- def execute_git_command(
302
- self, command: str, working_dir: str = ""
303
- ) -> tuple[int, str, str]:
304
- """
305
- Execute a git command in the workspace.
306
-
307
- Args:
308
- command: Git command to execute (without 'git' prefix)
309
- working_dir: Working directory relative to workspace
310
-
311
- Returns:
312
- Tuple of (exit_code, stdout, stderr)
313
- """
314
- work_path = (
315
- self.workspace_dir / working_dir if working_dir else self.workspace_dir
316
- )
317
-
318
- if not work_path.exists():
319
- return (1, "", f"Working directory does not exist: {work_path}")
320
-
321
- # Split command safely
322
- cmd_parts = ["git"] + command.split()
323
-
324
- result = subprocess.run(
325
- cmd_parts,
326
- cwd=str(work_path),
327
- capture_output=True,
328
- text=True,
329
- )
330
-
331
- return (result.returncode, result.stdout, result.stderr)
332
-
333
- def get_current_commit(self, repo_name: str) -> str:
334
- """
335
- Get current commit hash of a workspace repository.
336
-
337
- Args:
338
- repo_name: Name of repository in workspace
339
-
340
- Returns:
341
- Commit hash
342
- """
343
- repo_path = self.workspace_dir / repo_name
344
-
345
- if not repo_path.exists():
346
- raise RuntimeError(f"Repository not found: {repo_name}")
347
-
348
- result = subprocess.run(
349
- ["git", "rev-parse", "HEAD"],
350
- cwd=str(repo_path),
351
- capture_output=True,
352
- text=True,
353
- )
354
-
355
- if result.returncode != 0:
356
- raise RuntimeError(f"Failed to get commit: {result.stderr}")
357
-
358
- return result.stdout.strip()
359
-
360
- def workspace_exists(self, repo_name: str) -> bool:
361
- """Check if a repository exists in workspace."""
362
- return (self.workspace_dir / repo_name).exists()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/tools/local_python_executor.py DELETED
@@ -1,152 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """Local Python Executor (enhanced).
8
-
9
- This module provides a safer wrapper around smolagents.LocalPythonExecutor
10
- with improved exception handling and a few helpful tools registered with
11
- the executor to make debugging executed code easier.
12
-
13
- Key improvements:
14
- - Register a few helper utilities via send_tools so user code can use
15
- them for reporting (e.g. `format_exc`).
16
- - More robust extraction of stdout/stderr/exit codes from the executor
17
- result object, tolerant to different versions of smolagents.
18
- - Detailed stderr on unexpected exceptions including full traceback.
19
- - Structured logging for operational visibility.
20
- """
21
-
22
- from __future__ import annotations
23
-
24
- import json
25
- import logging
26
- import traceback
27
- from typing import Any
28
-
29
- from smolagents import LocalPythonExecutor
30
-
31
- from core.env_server.types import CodeExecResult
32
-
33
- logger = logging.getLogger(__name__)
34
- logger.addHandler(logging.NullHandler())
35
-
36
-
37
- class PyExecutor:
38
- """Wrapper around smolagents LocalPythonExecutor.
39
-
40
- The wrapper registers a few non-privileged helper tools to the
41
- LocalPythonExecutor that can be used by the executed code to
42
- format exceptions and to safely stringify results for improved
43
- error reporting.
44
- """
45
-
46
- def __init__(self, additional_imports: list[str] | None = None):
47
- if additional_imports is None:
48
- additional_imports = []
49
-
50
- self._executor = LocalPythonExecutor(
51
- additional_authorized_imports=additional_imports
52
- )
53
-
54
- # Register helpful utilities exposed to the execution environment.
55
- # These are intentionally small, read-only helpers.
56
- tools = {
57
- # Provide a small helper to format the current exception in the
58
- # executed context. This is a *string formatting* helper only.
59
- "format_exc": traceback.format_exc,
60
- # Safe JSON dumps with a fallback for non-serializable objects.
61
- "safe_json_dumps": lambda obj: json.dumps(obj, default=lambda o: repr(o)),
62
- }
63
-
64
- # `send_tools` is the public API on LocalPythonExecutor to make
65
- # helper callables available to the sandboxed runtime. We don't
66
- # provide any builtins that could change the environment.
67
- try:
68
- self._executor.send_tools(tools)
69
- except Exception:
70
- # If the LocalPythonExecutor implementation doesn't support
71
- # send_tools or fails, log and continue — the executor is still usable.
72
- logger.debug("LocalPythonExecutor.send_tools failed; continuing without extra tools", exc_info=True)
73
-
74
- def run(self, code: str) -> CodeExecResult:
75
- """Execute Python code and return a CodeExecResult.
76
-
77
- This method is intentionally defensive: it attempts to extract
78
- meaningful stdout/stderr/exit_code information from a variety of
79
- possible return shapes that different versions of smolagents
80
- may provide.
81
- """
82
- try:
83
- exec_result = self._executor(code)
84
-
85
- # Default values
86
- stdout_parts: list[str] = []
87
- stderr_parts: list[str] = []
88
- exit_code = 0
89
-
90
- # Extract logs/prints
91
- try:
92
- logs = getattr(exec_result, "logs", None)
93
- if logs:
94
- stdout_parts.append(str(logs))
95
- except Exception:
96
- logger.debug("Failed to read exec_result.logs", exc_info=True)
97
-
98
- # Extract the result / output value
99
- try:
100
- if hasattr(exec_result, "output"):
101
- out_val = exec_result.output
102
- # If the output is not None, stringify it in a safe way
103
- if out_val is not None:
104
- # Prefer JSON if possible, otherwise repr
105
- try:
106
- stdout_parts.append(json.dumps(out_val))
107
- except Exception:
108
- stdout_parts.append(repr(out_val))
109
- except Exception:
110
- logger.debug("Failed to read exec_result.output", exc_info=True)
111
-
112
- # Some runtime implementations may put errors on `error` or `exception`
113
- try:
114
- err = getattr(exec_result, "error", None)
115
- if err:
116
- stderr_parts.append(str(err))
117
- except Exception:
118
- logger.debug("Failed to read exec_result.error", exc_info=True)
119
-
120
- try:
121
- ex = getattr(exec_result, "exception", None)
122
- if ex:
123
- stderr_parts.append(str(ex))
124
- except Exception:
125
- logger.debug("Failed to read exec_result.exception", exc_info=True)
126
-
127
- # Determine exit code if provided
128
- try:
129
- if hasattr(exec_result, "exit_code"):
130
- exit_code = int(exec_result.exit_code) if exec_result.exit_code is not None else 0
131
- elif hasattr(exec_result, "success"):
132
- # Some versions use `success` boolean
133
- exit_code = 0 if exec_result.success else 1
134
- else:
135
- # Fallback: if there were any stderr parts, treat as non-zero
136
- exit_code = 1 if stderr_parts else 0
137
- except Exception:
138
- logger.debug("Failed to determine exec_result exit code", exc_info=True)
139
- exit_code = 1 if stderr_parts else 0
140
-
141
- # Compose the final stdout/stderr strings
142
- stdout = "\n".join(part for part in stdout_parts if part is not None)
143
- stderr = "\n".join(part for part in stderr_parts if part is not None)
144
-
145
- return CodeExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
146
-
147
- except Exception as e:
148
- # Any unexpected exception from the LocalPythonExecutor is
149
- # returned with a full traceback to make debugging easier.
150
- tb = traceback.format_exc()
151
- logger.exception("LocalPythonExecutor raised an exception during run")
152
- return CodeExecResult(stdout="", stderr=tb, exit_code=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/core/uv.lock DELETED
The diff for this file is too large to render. See raw diff
 
openenv/src/envs/README.md DELETED
@@ -1,382 +0,0 @@
1
- # Building Your Own Environment
2
-
3
- This guide shows you how to create a custom environment using the EnvTorch framework.
4
-
5
- ## Overview
6
-
7
- Creating an environment involves five main steps:
8
- 1. Define your models (Action, Observation, State)
9
- 2. Implement the environment APIs: step, reset, state
10
- 3. Create the FastAPI server
11
- 4. Build a Docker image and push it to a public docker repo for community to access it
12
- 5. Subclass HTTPEnvclient and implement the parsing methods for result and state.
13
-
14
- ## Step-by-Step Guide
15
-
16
- ### 1. Define Models
17
-
18
- Create your action, observation, and state models using Python dataclasses:
19
-
20
- ```python
21
- # models.py
22
- from dataclasses import dataclass
23
- from core.env_server import Action, Observation, State
24
-
25
- @dataclass
26
- class MyAction(Action):
27
- """Your custom action."""
28
- command: str
29
- parameters: dict
30
-
31
- @dataclass
32
- class MyObservation(Observation):
33
- """Your custom observation."""
34
- result: str
35
- success: bool
36
-
37
- @dataclass
38
- class MyState(State):
39
- """Custom state fields."""
40
- custom_field: int = 0
41
- ```
42
-
43
- ### 2. Implement Environment
44
-
45
- Implement the three core methods: `reset()`, `step()`, and `state`:
46
-
47
- ```python
48
- # server/my_environment.py
49
- import uuid
50
- from core.env_server import Environment
51
- from ..models import MyAction, MyObservation, MyState
52
-
53
- class MyEnvironment(Environment):
54
- def __init__(self):
55
- super().__init__()
56
- self._state = MyState()
57
-
58
- def reset(self) -> MyObservation:
59
- self._state = MyState(episode_id=str(uuid.uuid4()))
60
- return MyObservation(result="Ready", success=True)
61
-
62
- def step(self, action: MyAction) -> MyObservation:
63
- # Implement your logic here
64
- self._state.step_count += 1
65
- result = self._execute_command(action.command)
66
- return MyObservation(result=result, success=True)
67
-
68
- @property
69
- def state(self) -> MyState:
70
- return self._state
71
- ```
72
-
73
- ### 3. Create FastAPI Server
74
-
75
- Use the `create_fastapi_app` helper to create your HTTP server:
76
-
77
- ```python
78
- # server/app.py
79
- from core.env_server import create_fastapi_app
80
- from ..models import MyAction, MyObservation
81
- from .my_environment import MyEnvironment
82
-
83
- env = MyEnvironment()
84
- app = create_fastapi_app(env, MyAction, MyObservation)
85
- ```
86
-
87
- ### 4. Define Dependencies
88
-
89
- **For Python-only dependencies (most common case):**
90
-
91
- Create `src/envs/my_env/server/requirements.txt`:
92
- ```txt
93
- your-package>=1.0.0
94
- another-package
95
- ```
96
-
97
- **For complex setup (optional, only if needed):**
98
-
99
- If you need additional setup beyond pip install, create `src/envs/my_env/server/install_deps.sh`:
100
- ```bash
101
- #!/bin/bash
102
- set -e
103
-
104
- # Install Python dependencies
105
- pip install --no-cache-dir -r /tmp/requirements.txt
106
-
107
- # Additional setup commands (only if needed)
108
- mkdir -p /some/directory
109
- # ... other setup steps
110
- ```
111
-
112
- ### 5. Create Dockerfile
113
-
114
- Build your Docker image from the openenv-base. Place this at `src/envs/my_env/server/Dockerfile`:
115
-
116
- **Simple case (just requirements.txt):**
117
- ```dockerfile
118
- # Accept base image as build argument for CI/CD flexibility
119
- ARG BASE_IMAGE=openenv-base:latest
120
- FROM ${BASE_IMAGE}
121
-
122
- # Install dependencies
123
- COPY src/envs/my_env/server/requirements.txt /tmp/requirements.txt
124
- RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt
125
-
126
- # Copy environment code
127
- COPY src/core/ /app/src/core/
128
- COPY src/envs/my_env/ /app/src/envs/my_env/
129
-
130
- # Health check
131
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
132
- CMD curl -f http://localhost:8000/health || exit 1
133
-
134
- # Run server
135
- CMD ["uvicorn", "envs.my_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
136
- ```
137
-
138
- **Complex case (requirements.txt + install_deps.sh):**
139
- ```dockerfile
140
- ARG BASE_IMAGE=openenv-base:latest
141
- FROM ${BASE_IMAGE}
142
-
143
- # Install dependencies and run setup
144
- COPY src/envs/my_env/server/requirements.txt /tmp/requirements.txt
145
- COPY src/envs/my_env/server/install_deps.sh /tmp/install_deps.sh
146
- RUN chmod +x /tmp/install_deps.sh && \
147
- /tmp/install_deps.sh && \
148
- rm /tmp/install_deps.sh /tmp/requirements.txt
149
-
150
- # Copy environment code
151
- COPY src/core/ /app/src/core/
152
- COPY src/envs/my_env/ /app/src/envs/my_env/
153
-
154
- # Health check
155
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
156
- CMD curl -f http://localhost:8000/health || exit 1
157
-
158
- # Run server
159
- CMD ["uvicorn", "envs.my_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
160
- ```
161
-
162
- ### 5. Update GitHub Actions Workflow
163
-
164
- **Important**: To enable automatic Docker image builds on GitHub, add your environment to the workflow matrix.
165
-
166
- Edit `.github/workflows/docker-build.yml` and add your environment to the matrix:
167
-
168
- ```yaml
169
- strategy:
170
- matrix:
171
- image:
172
- - name: echo-env
173
- dockerfile: src/envs/echo_env/server/Dockerfile
174
- - name: chat-env
175
- dockerfile: src/envs/chat_env/server/Dockerfile
176
- - name: coding-env
177
- dockerfile: src/envs/coding_env/server/Dockerfile
178
- - name: my-env # Add your environment here
179
- dockerfile: src/envs/my_env/server/Dockerfile
180
- ```
181
-
182
- Once added, every push to `main` will automatically:
183
- - Build your Docker image
184
- - Push it to GitHub Container Registry as `ghcr.io/YOUR_USERNAME/openenv-my-env:latest`
185
-
186
- ### 6. Implement Client
187
-
188
- Create a client that extends `HTTPEnvClient`:
189
-
190
- ```python
191
- # client.py
192
- from core.http_env_client import HTTPEnvClient
193
- from core.types import StepResult
194
- from .models import MyAction, MyObservation, MyState
195
-
196
- class MyEnv(HTTPEnvClient[MyAction, MyObservation]):
197
- def _step_payload(self, action: MyAction) -> dict:
198
- return {"command": action.command, "parameters": action.parameters}
199
-
200
- def _parse_result(self, payload: dict) -> StepResult[MyObservation]:
201
- obs = MyObservation(**payload["observation"])
202
- return StepResult(
203
- observation=obs,
204
- reward=payload.get("reward"),
205
- done=payload.get("done", False),
206
- )
207
-
208
- def _parse_state(self, payload: dict) -> MyState:
209
- return MyState(**payload)
210
- ```
211
-
212
- ## Building and Using Your Environment
213
-
214
- ### Build Docker Images
215
-
216
- ```bash
217
- # First, build the base image (if not already built)
218
- docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
219
-
220
- # Then build your environment image
221
- docker build -t my-env:latest -f src/envs/my_env/server/Dockerfile .
222
- ```
223
-
224
- ### Use Your Environment
225
-
226
- ```python
227
- from envs.my_env import MyAction, MyEnv
228
-
229
- # Create environment from Docker image
230
- client = MyEnv.from_docker_image("my-env:latest")
231
-
232
- # Reset
233
- result = client.reset()
234
- print(result.observation.result) # "Ready"
235
-
236
- # Execute actions
237
- result = client.step(MyAction(command="test", parameters={}))
238
- print(result.observation.result)
239
- print(result.observation.success)
240
-
241
- # Get state
242
- state = client.state()
243
- print(state.episode_id)
244
- print(state.step_count)
245
-
246
- # Cleanup
247
- client.close()
248
- ```
249
-
250
- ## Project Structure
251
-
252
- Organize your environment following this structure:
253
-
254
- ```
255
- src/envs/my_env/
256
- ├── __init__.py # Export MyAction, MyObservation, MyState, MyEnv
257
- ├── models.py # Action, Observation, State definitions
258
- ├── client.py # MyEnv client implementation
259
- ├── README.md # Environment documentation
260
- └── server/
261
- ├── __init__.py
262
- ├── my_environment.py # Environment logic
263
- ├── app.py # FastAPI application
264
- └── Dockerfile # Docker image definition
265
- ```
266
-
267
- ## Example Environments
268
-
269
- Study these examples to see the patterns in action:
270
-
271
- ### Echo Environment
272
- Location: `src/envs/echo_env/`
273
-
274
- A minimal environment that echoes messages back. Great for:
275
- - Learning the basics
276
- - Testing infrastructure
277
- - Reference implementation
278
-
279
- See: [`echo_env/README.md`](echo_env/README.md)
280
-
281
- ### Coding Environment
282
- Location: `src/envs/coding_env/`
283
-
284
- Executes Python code in a sandboxed environment. Demonstrates:
285
- - Complex environment logic
286
- - Error handling
287
- - External tool integration (smolagents)
288
-
289
- See: [`coding_env/README.md`](coding_env/README.md)
290
-
291
- ## Best Practices
292
-
293
- ### 1. Type Safety
294
- Always use typed dataclasses for actions, observations, and state:
295
- ```python
296
- @dataclass
297
- class MyAction(Action):
298
- command: str # Use explicit types
299
- count: int = 0 # Provide defaults when appropriate
300
- ```
301
-
302
- ### 2. Error Handling
303
- Handle errors gracefully in your environment:
304
- ```python
305
- def step(self, action: MyAction) -> MyObservation:
306
- try:
307
- result = self._process(action)
308
- return MyObservation(result=result, success=True)
309
- except Exception as e:
310
- return MyObservation(result="", success=False, error=str(e))
311
- ```
312
-
313
- ### 3. State Management
314
- Track all relevant episode state:
315
- ```python
316
- @dataclass
317
- class MyState(State):
318
- # Add custom fields
319
- accumulated_reward: float = 0.0
320
- last_action: str = ""
321
- ```
322
-
323
- ### 4. Documentation
324
- Provide comprehensive README for your environment:
325
- - Overview and purpose
326
- - Quick start example
327
- - Action/Observation specifications
328
- - Build instructions
329
- - Usage examples
330
-
331
- ### 5. Testing
332
- Test your environment before containerization:
333
- ```python
334
- # test_my_environment.py
335
- from envs.my_env.server.my_environment import MyEnvironment
336
- from envs.my_env.models import MyAction
337
-
338
- def test_environment():
339
- env = MyEnvironment()
340
-
341
- # Test reset
342
- obs = env.reset()
343
- assert obs.success
344
-
345
- # Test step
346
- action = MyAction(command="test", parameters={})
347
- obs = env.step(action)
348
- assert obs.success
349
-
350
- # Test state
351
- assert env.state.step_count == 1
352
- ```
353
-
354
- ## Advanced Topics
355
-
356
- ### Custom Transforms
357
- Apply transformations to observations:
358
-
359
- ```python
360
- from core.env_server import Transform
361
-
362
- class MyTransform(Transform):
363
- def __call__(self, observation: Observation) -> Observation:
364
- # Transform observation
365
- return modified_observation
366
-
367
- # Use in environment
368
- env = MyEnvironment(transform=MyTransform())
369
- ```
370
-
371
- ### Additional Dependencies
372
- Install environment-specific packages in Dockerfile:
373
-
374
- ```dockerfile
375
- FROM openenv-base:latest
376
-
377
- # Install specific versions
378
- RUN pip install --no-cache-dir \
379
- numpy==1.24.0 \
380
- pandas==2.0.0 \
381
- your-custom-package==1.0.0
382
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/README.md DELETED
@@ -1,396 +0,0 @@
1
- ---
2
- title: Atari Environment Server
3
- emoji: 🕹️
4
- colorFrom: '#FF6200'
5
- colorTo: '#D4151B'
6
- sdk: docker
7
- pinned: false
8
- app_port: 8000
9
- base_path: /web
10
- tags:
11
- - openenv
12
- ---
13
-
14
- # Atari Environment
15
-
16
- Integration of Atari 2600 games with the OpenEnv framework via the Arcade Learning Environment (ALE). ALE provides access to 100+ classic Atari games for RL research.
17
-
18
- ## Supported Games
19
-
20
- ALE supports 100+ Atari 2600 games including:
21
-
22
- ### Popular Games
23
- - **Pong** - Classic two-player tennis
24
- - **Breakout** - Break bricks with a ball
25
- - **Space Invaders** - Shoot descending aliens
26
- - **Pac-Man / Ms. Pac-Man** - Navigate mazes and eat pellets
27
- - **Asteroids** - Destroy asteroids in space
28
- - **Defender** - Side-scrolling space shooter
29
- - **Centipede** - Shoot segmented centipede
30
- - **Donkey Kong** - Jump over barrels to save princess
31
- - **Frogger** - Cross road and river safely
32
- - **Q*bert** - Jump on pyramid cubes
33
-
34
- And many more! For a complete list, see [ALE documentation](https://ale.farama.org/environments/complete_list/).
35
-
36
- ## Architecture
37
-
38
- ```
39
- ┌────────────────────────────────────┐
40
- │ RL Training Code (Client) │
41
- │ AtariEnv.step(action) │
42
- └──────────────┬─────────────────────┘
43
- │ HTTP
44
- ┌──────────────▼─────────────────────┐
45
- │ FastAPI Server (Docker) │
46
- │ AtariEnvironment │
47
- │ ├─ Wraps ALEInterface │
48
- │ ├─ Handles observations │
49
- │ └─ Action execution │
50
- └────────────────────────────────────┘
51
- ```
52
-
53
- ## Installation & Usage
54
-
55
- ### Option 1: Local Development (without Docker)
56
-
57
- **Requirements:**
58
- - Python 3.11+
59
- - ale-py installed: `pip install ale-py`
60
-
61
- ```python
62
- from envs.atari_env import AtariEnv, AtariAction
63
-
64
- # Start local server manually
65
- # python -m envs.atari_env.server.app
66
-
67
- # Connect to local server
68
- env = AtariEnv(base_url="http://localhost:8000")
69
-
70
- # Reset environment
71
- result = env.reset()
72
- print(f"Screen shape: {result.observation.screen_shape}")
73
- print(f"Legal actions: {result.observation.legal_actions}")
74
- print(f"Lives: {result.observation.lives}")
75
-
76
- # Take actions
77
- for _ in range(10):
78
- action_id = 2 # UP action
79
- result = env.step(AtariAction(action_id=action_id, game_name="pong"))
80
- print(f"Reward: {result.reward}, Done: {result.done}")
81
- if result.done:
82
- break
83
-
84
- # Cleanup
85
- env.close()
86
- ```
87
-
88
- ### Option 2: Docker (Recommended)
89
-
90
- **Build Atari image:**
91
-
92
- ```bash
93
- cd OpenEnv
94
-
95
- # Build the image
96
- docker build \
97
- -f src/envs/atari_env/server/Dockerfile \
98
- -t atari-env:latest \
99
- .
100
- ```
101
-
102
- **Run specific games:**
103
-
104
- ```bash
105
- # Pong (default)
106
- docker run -p 8000:8000 atari-env:latest
107
-
108
- # Breakout
109
- docker run -p 8000:8000 -e ATARI_GAME=breakout atari-env:latest
110
-
111
- # Space Invaders with grayscale observation
112
- docker run -p 8000:8000 \
113
- -e ATARI_GAME=space_invaders \
114
- -e ATARI_OBS_TYPE=grayscale \
115
- atari-env:latest
116
-
117
- # Ms. Pac-Man with full action space
118
- docker run -p 8000:8000 \
119
- -e ATARI_GAME=ms_pacman \
120
- -e ATARI_FULL_ACTION_SPACE=true \
121
- atari-env:latest
122
- ```
123
-
124
- **Use with from_docker_image():**
125
-
126
- ```python
127
- from envs.atari_env import AtariEnv, AtariAction
128
- import numpy as np
129
-
130
- # Automatically starts container
131
- env = AtariEnv.from_docker_image("atari-env:latest")
132
-
133
- result = env.reset()
134
- result = env.step(AtariAction(action_id=2)) # UP
135
-
136
- # Reshape screen for visualization
137
- screen = np.array(result.observation.screen).reshape(result.observation.screen_shape)
138
- print(f"Screen shape: {screen.shape}") # (210, 160, 3) for RGB
139
-
140
- env.close() # Stops container
141
- ```
142
-
143
- ## Observation Types
144
-
145
- ### 1. RGB (Default)
146
- - **Shape**: [210, 160, 3]
147
- - **Description**: Full-color screen observation
148
- - **Usage**: Most realistic, good for vision-based learning
149
-
150
- ```python
151
- docker run -p 8000:8000 -e ATARI_OBS_TYPE=rgb atari-env:latest
152
- ```
153
-
154
- ### 2. Grayscale
155
- - **Shape**: [210, 160]
156
- - **Description**: Grayscale screen observation
157
- - **Usage**: Reduced dimensionality, faster processing
158
-
159
- ```python
160
- docker run -p 8000:8000 -e ATARI_OBS_TYPE=grayscale atari-env:latest
161
- ```
162
-
163
- ### 3. RAM
164
- - **Shape**: [128]
165
- - **Description**: Raw 128-byte Atari 2600 RAM contents
166
- - **Usage**: Compact representation, useful for specific research
167
-
168
- ```python
169
- docker run -p 8000:8000 -e ATARI_OBS_TYPE=ram atari-env:latest
170
- ```
171
-
172
- ## Action Spaces
173
-
174
- ### Minimal Action Set (Default)
175
- Game-specific minimal actions (typically 4-9 actions).
176
- - Pong: 6 actions (NOOP, FIRE, UP, DOWN, etc.)
177
- - Breakout: 4 actions (NOOP, FIRE, LEFT, RIGHT)
178
-
179
- ```python
180
- docker run -p 8000:8000 -e ATARI_FULL_ACTION_SPACE=false atari-env:latest
181
- ```
182
-
183
- ### Full Action Set
184
- All 18 possible Atari 2600 actions:
185
- 0. NOOP
186
- 1. FIRE
187
- 2. UP
188
- 3. RIGHT
189
- 4. LEFT
190
- 5. DOWN
191
- 6. UPRIGHT
192
- 7. UPLEFT
193
- 8. DOWNRIGHT
194
- 9. DOWNLEFT
195
- 10. UPFIRE
196
- 11. RIGHTFIRE
197
- 12. LEFTFIRE
198
- 13. DOWNFIRE
199
- 14. UPRIGHTFIRE
200
- 15. UPLEFTFIRE
201
- 16. DOWNRIGHTFIRE
202
- 17. DOWNLEFTFIRE
203
-
204
- ```python
205
- docker run -p 8000:8000 -e ATARI_FULL_ACTION_SPACE=true atari-env:latest
206
- ```
207
-
208
- ## Configuration
209
-
210
- ### Environment Variables
211
-
212
- - `ATARI_GAME`: Game name (default: "pong")
213
- - `ATARI_OBS_TYPE`: Observation type - "rgb", "grayscale", "ram" (default: "rgb")
214
- - `ATARI_FULL_ACTION_SPACE`: Use full action space - "true"/"false" (default: "false")
215
- - `ATARI_MODE`: Game mode (optional, game-specific)
216
- - `ATARI_DIFFICULTY`: Game difficulty (optional, game-specific)
217
- - `ATARI_REPEAT_ACTION_PROB`: Sticky action probability 0.0-1.0 (default: "0.0")
218
- - `ATARI_FRAMESKIP`: Frames to skip per action (default: "4")
219
-
220
- ### Example: Breakout with Custom Settings
221
-
222
- ```bash
223
- docker run -p 8000:8000 \
224
- -e ATARI_GAME=breakout \
225
- -e ATARI_OBS_TYPE=grayscale \
226
- -e ATARI_FULL_ACTION_SPACE=true \
227
- -e ATARI_REPEAT_ACTION_PROB=0.25 \
228
- -e ATARI_FRAMESKIP=4 \
229
- atari-env:latest
230
- ```
231
-
232
- ## API Reference
233
-
234
- ### AtariAction
235
-
236
- ```python
237
- @dataclass
238
- class AtariAction(Action):
239
- action_id: int # Action index to execute
240
- game_name: str = "pong" # Game name
241
- obs_type: str = "rgb" # Observation type
242
- full_action_space: bool = False # Full or minimal action space
243
- ```
244
-
245
- ### AtariObservation
246
-
247
- ```python
248
- @dataclass
249
- class AtariObservation(Observation):
250
- screen: List[int] # Flattened screen pixels
251
- screen_shape: List[int] # Original screen shape
252
- legal_actions: List[int] # Legal action indices
253
- lives: int # Lives remaining
254
- episode_frame_number: int # Frame # in episode
255
- frame_number: int # Total frame #
256
- done: bool # Episode finished
257
- reward: Optional[float] # Reward from last action
258
- ```
259
-
260
- ### AtariState
261
-
262
- ```python
263
- @dataclass
264
- class AtariState(State):
265
- episode_id: str # Unique episode ID
266
- step_count: int # Number of steps
267
- game_name: str # Game name
268
- obs_type: str # Observation type
269
- full_action_space: bool # Action space type
270
- mode: Optional[int] # Game mode
271
- difficulty: Optional[int] # Game difficulty
272
- repeat_action_probability: float # Sticky action prob
273
- frameskip: int # Frameskip setting
274
- ```
275
-
276
- ## Example Script
277
-
278
- ```python
279
- #!/usr/bin/env python3
280
- """Example training loop with Atari environment."""
281
-
282
- import numpy as np
283
- from envs.atari_env import AtariEnv, AtariAction
284
-
285
- # Start environment
286
- env = AtariEnv.from_docker_image("atari-env:latest")
287
-
288
- # Training loop
289
- for episode in range(10):
290
- result = env.reset()
291
- episode_reward = 0
292
- steps = 0
293
-
294
- while not result.done:
295
- # Random policy (replace with your RL agent)
296
- action_id = np.random.choice(result.observation.legal_actions)
297
-
298
- # Take action
299
- result = env.step(AtariAction(action_id=action_id))
300
-
301
- episode_reward += result.reward or 0
302
- steps += 1
303
-
304
- # Reshape screen for processing
305
- screen = np.array(result.observation.screen).reshape(
306
- result.observation.screen_shape
307
- )
308
-
309
- # Your RL training code here
310
- # ...
311
-
312
- print(f"Episode {episode}: reward={episode_reward:.2f}, steps={steps}")
313
-
314
- env.close()
315
- ```
316
-
317
- ## Testing
318
-
319
- ### Local Testing
320
-
321
- ```bash
322
- # Install dependencies
323
- pip install ale-py fastapi uvicorn requests
324
-
325
- # Start server
326
- cd /Users/sanyambhutani/OpenEnv/OpenEnv
327
- export PYTHONPATH=/Users/sanyambhutani/OpenEnv/OpenEnv/src
328
- python -m envs.atari_env.server.app
329
-
330
- # Test from another terminal
331
- python -c "
332
- from envs.atari_env import AtariEnv, AtariAction
333
- env = AtariEnv(base_url='http://localhost:8000')
334
- result = env.reset()
335
- print(f'Initial obs: {result.observation.screen_shape}')
336
- result = env.step(AtariAction(action_id=2))
337
- print(f'After step: reward={result.reward}, done={result.done}')
338
- env.close()
339
- "
340
- ```
341
-
342
- ### Docker Testing
343
-
344
- ```bash
345
- # Build and run
346
- docker build -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
347
- docker run -p 8000:8000 atari-env:latest
348
-
349
- # Test in another terminal
350
- curl http://localhost:8000/health
351
- curl -X POST http://localhost:8000/reset
352
- ```
353
-
354
- ## Popular Games and Their Characteristics
355
-
356
- | Game | Minimal Actions | Lives | Difficulty | Notes |
357
- |------|----------------|-------|-----------|-------|
358
- | Pong | 6 | 1 | Low | Good for learning basics |
359
- | Breakout | 4 | 5 | Medium | Classic RL benchmark |
360
- | Space Invaders | 6 | 3 | Medium | Shooting game |
361
- | Ms. Pac-Man | 9 | 3 | High | Complex navigation |
362
- | Asteroids | 14 | 3 | Medium | Continuous shooting |
363
- | Montezuma's Revenge | 18 | 5 | Very High | Exploration challenge |
364
- | Pitfall | 18 | 1 | High | Platformer |
365
- | Seaquest | 18 | 3 | High | Submarine rescue |
366
-
367
- ## Limitations & Notes
368
-
369
- - **Frame perfect timing**: Some games require precise timing
370
- - **Exploration**: Games like Montezuma's Revenge are notoriously difficult
371
- - **Observation delay**: HTTP adds minimal latency vs local gym
372
- - **Determinism**: Set `ATARI_REPEAT_ACTION_PROB=0.0` for deterministic behavior
373
- - **ROMs**: All ROMs are bundled with ale-py package
374
-
375
- ## References
376
-
377
- - [Arcade Learning Environment Paper (2013)](https://jair.org/index.php/jair/article/view/10819)
378
- - [ALE GitHub](https://github.com/Farama-Foundation/Arcade-Learning-Environment)
379
- - [ALE Documentation](https://ale.farama.org/)
380
- - [Gymnasium Atari Environments](https://gymnasium.farama.org/environments/atari/)
381
-
382
- ## Citation
383
-
384
- If you use ALE in your research, please cite:
385
-
386
- ```bibtex
387
- @Article{bellemare13arcade,
388
- author = {{Bellemare}, M.~G. and {Naddaf}, Y. and {Veness}, J. and {Bowling}, M.},
389
- title = {The Arcade Learning Environment: An Evaluation Platform for General Agents},
390
- journal = {Journal of Artificial Intelligence Research},
391
- year = "2013",
392
- month = "jun",
393
- volume = "47",
394
- pages = "253--279",
395
- }
396
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/__init__.py DELETED
@@ -1,31 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Atari Environment for OpenEnv.
9
-
10
- This module provides OpenEnv integration for Atari 2600 games via the
11
- Arcade Learning Environment (ALE).
12
-
13
- Example:
14
- >>> from envs.atari_env import AtariEnv, AtariAction
15
- >>>
16
- >>> # Connect to a running server or start via Docker
17
- >>> env = AtariEnv.from_docker_image("atari-env:latest")
18
- >>>
19
- >>> # Reset and interact
20
- >>> result = env.reset()
21
- >>> result = env.step(AtariAction(action_id=2)) # UP
22
- >>> print(result.reward, result.done)
23
- >>>
24
- >>> # Cleanup
25
- >>> env.close()
26
- """
27
-
28
- from .client import AtariEnv
29
- from .models import AtariAction, AtariObservation, AtariState
30
-
31
- __all__ = ["AtariEnv", "AtariAction", "AtariObservation", "AtariState"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/client.py DELETED
@@ -1,119 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Atari Environment HTTP Client.
9
-
10
- This module provides the client for connecting to an Atari Environment server
11
- over HTTP.
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- from typing import Any, Dict, TYPE_CHECKING
17
-
18
- from core.client_types import StepResult
19
-
20
- from core.http_env_client import HTTPEnvClient
21
-
22
- from .models import AtariAction, AtariObservation, AtariState
23
-
24
- if TYPE_CHECKING:
25
- from core.containers.runtime import ContainerProvider
26
-
27
-
28
- class AtariEnv(HTTPEnvClient[AtariAction, AtariObservation]):
29
- """
30
- HTTP client for Atari Environment.
31
-
32
- This client connects to an AtariEnvironment HTTP server and provides
33
- methods to interact with it: reset(), step(), and state access.
34
-
35
- Example:
36
- >>> # Connect to a running server
37
- >>> client = AtariEnv(base_url="http://localhost:8000")
38
- >>> result = client.reset()
39
- >>> print(result.observation.screen_shape)
40
- >>>
41
- >>> # Take an action
42
- >>> result = client.step(AtariAction(action_id=2)) # UP
43
- >>> print(result.reward, result.done)
44
-
45
- Example with Docker:
46
- >>> # Automatically start container and connect
47
- >>> client = AtariEnv.from_docker_image("atari-env:latest")
48
- >>> result = client.reset()
49
- >>> result = client.step(AtariAction(action_id=0)) # NOOP
50
- """
51
-
52
- def _step_payload(self, action: AtariAction) -> Dict[str, Any]:
53
- """
54
- Convert AtariAction to JSON payload for step request.
55
-
56
- Args:
57
- action: AtariAction instance.
58
-
59
- Returns:
60
- Dictionary representation suitable for JSON encoding.
61
- """
62
- return {
63
- "action_id": action.action_id,
64
- "game_name": action.game_name,
65
- "obs_type": action.obs_type,
66
- "full_action_space": action.full_action_space,
67
- }
68
-
69
- def _parse_result(self, payload: Dict[str, Any]) -> StepResult[AtariObservation]:
70
- """
71
- Parse server response into StepResult[AtariObservation].
72
-
73
- Args:
74
- payload: JSON response from server.
75
-
76
- Returns:
77
- StepResult with AtariObservation.
78
- """
79
- obs_data = payload.get("observation", {})
80
-
81
- observation = AtariObservation(
82
- screen=obs_data.get("screen", []),
83
- screen_shape=obs_data.get("screen_shape", []),
84
- legal_actions=obs_data.get("legal_actions", []),
85
- lives=obs_data.get("lives", 0),
86
- episode_frame_number=obs_data.get("episode_frame_number", 0),
87
- frame_number=obs_data.get("frame_number", 0),
88
- done=payload.get("done", False),
89
- reward=payload.get("reward"),
90
- metadata=obs_data.get("metadata", {}),
91
- )
92
-
93
- return StepResult(
94
- observation=observation,
95
- reward=payload.get("reward"),
96
- done=payload.get("done", False),
97
- )
98
-
99
- def _parse_state(self, payload: Dict[str, Any]) -> AtariState:
100
- """
101
- Parse server response into AtariState object.
102
-
103
- Args:
104
- payload: JSON response from /state endpoint.
105
-
106
- Returns:
107
- AtariState object with environment state information.
108
- """
109
- return AtariState(
110
- episode_id=payload.get("episode_id"),
111
- step_count=payload.get("step_count", 0),
112
- game_name=payload.get("game_name", "unknown"),
113
- obs_type=payload.get("obs_type", "rgb"),
114
- full_action_space=payload.get("full_action_space", False),
115
- mode=payload.get("mode"),
116
- difficulty=payload.get("difficulty"),
117
- repeat_action_probability=payload.get("repeat_action_probability", 0.0),
118
- frameskip=payload.get("frameskip", 4),
119
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/models.py DELETED
@@ -1,86 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Data models for Atari Environment.
9
-
10
- This module defines the Action, Observation, and State types for Atari games
11
- via the Arcade Learning Environment (ALE).
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- from dataclasses import dataclass, field
17
- from typing import Any, Dict, List, Literal, Optional
18
-
19
- from core.env_server import Action, Observation, State
20
-
21
-
22
- @dataclass
23
- class AtariAction(Action):
24
- """
25
- Action for Atari environments.
26
-
27
- Attributes:
28
- action_id: The integer action ID to take (from legal_actions).
29
- game_name: Name of the Atari game (e.g., "pong", "breakout", "space_invaders").
30
- obs_type: Observation type ("rgb", "grayscale", or "ram").
31
- full_action_space: Whether to use full (18 actions) or minimal action space.
32
- """
33
- action_id: int
34
- game_name: str = "pong"
35
- obs_type: Literal["rgb", "grayscale", "ram"] = "rgb"
36
- full_action_space: bool = False
37
-
38
-
39
- @dataclass
40
- class AtariObservation(Observation):
41
- """
42
- Observation from Atari environment.
43
-
44
- This represents what the agent sees after taking an action.
45
-
46
- Attributes:
47
- screen: Screen observation as a flattened list of pixels.
48
- Shape depends on obs_type:
49
- - rgb: [210, 160, 3] flattened
50
- - grayscale: [210, 160] flattened
51
- - ram: [128] (RAM contents)
52
- screen_shape: Original shape of the screen before flattening.
53
- legal_actions: List of legal action IDs the agent can take.
54
- lives: Number of lives remaining.
55
- episode_frame_number: Frame number within current episode.
56
- frame_number: Total frame number since environment creation.
57
- """
58
- screen: List[int]
59
- screen_shape: List[int]
60
- legal_actions: List[int]
61
- lives: int = 0
62
- episode_frame_number: int = 0
63
- frame_number: int = 0
64
-
65
-
66
- @dataclass
67
- class AtariState(State):
68
- """
69
- State for Atari environment.
70
-
71
- Attributes:
72
- game_name: Name of the Atari game.
73
- obs_type: Observation type ("rgb", "grayscale", or "ram").
74
- full_action_space: Whether using full or minimal action space.
75
- mode: Game mode (if applicable).
76
- difficulty: Game difficulty (if applicable).
77
- repeat_action_probability: Probability of repeating previous action (sticky actions).
78
- frameskip: Number of frames to skip per action.
79
- """
80
- game_name: str = "pong"
81
- obs_type: Literal["rgb", "grayscale", "ram"] = "rgb"
82
- full_action_space: bool = False
83
- mode: Optional[int] = None
84
- difficulty: Optional[int] = None
85
- repeat_action_probability: float = 0.0
86
- frameskip: int = 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/server/Dockerfile DELETED
@@ -1,43 +0,0 @@
1
- # Dockerfile for Atari Environment
2
- # This image provides Atari 2600 games via the Arcade Learning Environment (ALE)
3
-
4
- # Configurable base image - defaults to local build, can be overridden for CI/CD
5
- # Base image provides: fastapi, uvicorn, requests, curl, PYTHONPATH=/app/src
6
- #
7
- # Local build: docker build -t envtorch-base:latest -f src/core/containers/images/Dockerfile .
8
- # docker build -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
9
- #
10
- # CI/CD build: docker build --build-arg BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest \
11
- # -f src/envs/atari_env/server/Dockerfile -t atari-env:latest .
12
- ARG BASE_IMAGE=openenv-base:latest
13
- FROM ${BASE_IMAGE}
14
-
15
- # Install dependencies
16
- COPY src/envs/atari_env/server/requirements.txt /tmp/requirements.txt
17
- RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt
18
-
19
- # Copy OpenEnv core (base image already set WORKDIR=/app)
20
- COPY src/core/ /app/src/core/
21
-
22
- # Copy Atari environment code
23
- COPY src/envs/atari_env/ /app/src/envs/atari_env/
24
-
25
- # Copy README for web interface documentation
26
- COPY src/envs/atari_env/README.md /app/README.md
27
-
28
- # Atari-specific environment variables (can be overridden at runtime)
29
- ENV ATARI_GAME=pong
30
- ENV ATARI_OBS_TYPE=rgb
31
- ENV ATARI_FULL_ACTION_SPACE=false
32
- ENV ATARI_REPEAT_ACTION_PROB=0.0
33
- ENV ATARI_FRAMESKIP=4
34
-
35
- # Expose port
36
- EXPOSE 8000
37
-
38
- # Health check
39
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
40
- CMD curl -f http://localhost:8000/health || exit 1
41
-
42
- # Run the FastAPI server
43
- CMD ["uvicorn", "envs.atari_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/server/__init__.py DELETED
@@ -1,15 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Atari Environment Server.
9
-
10
- Server-side implementation of Atari environment for OpenEnv.
11
- """
12
-
13
- from .atari_environment import AtariEnvironment
14
-
15
- __all__ = ["AtariEnvironment"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/server/app.py DELETED
@@ -1,73 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- FastAPI application for the Atari Environment.
9
-
10
- This module creates an HTTP server that exposes Atari games
11
- over HTTP endpoints, making them compatible with HTTPEnvClient.
12
-
13
- Usage:
14
- # Development (with auto-reload):
15
- uvicorn envs.atari_env.server.app:app --reload --host 0.0.0.0 --port 8000
16
-
17
- # Production:
18
- uvicorn envs.atari_env.server.app:app --host 0.0.0.0 --port 8000 --workers 4
19
-
20
- # Or run directly:
21
- python -m envs.atari_env.server.app
22
-
23
- Environment variables:
24
- ATARI_GAME: Game name to serve (default: "pong")
25
- ATARI_OBS_TYPE: Observation type (default: "rgb")
26
- ATARI_FULL_ACTION_SPACE: Use full action space (default: "false")
27
- ATARI_MODE: Game mode (optional)
28
- ATARI_DIFFICULTY: Game difficulty (optional)
29
- ATARI_REPEAT_ACTION_PROB: Sticky action probability (default: "0.0")
30
- ATARI_FRAMESKIP: Frameskip (default: "4")
31
- """
32
-
33
- import os
34
-
35
- from core.env_server import create_app
36
-
37
- from ..models import AtariAction, AtariObservation
38
- from .atari_environment import AtariEnvironment
39
-
40
- # Get configuration from environment variables
41
- game_name = os.getenv("ATARI_GAME", "pong")
42
- obs_type = os.getenv("ATARI_OBS_TYPE", "rgb")
43
- full_action_space = os.getenv("ATARI_FULL_ACTION_SPACE", "false").lower() == "true"
44
- repeat_action_prob = float(os.getenv("ATARI_REPEAT_ACTION_PROB", "0.0"))
45
- frameskip = int(os.getenv("ATARI_FRAMESKIP", "4"))
46
-
47
- # Optional parameters
48
- mode = os.getenv("ATARI_MODE")
49
- difficulty = os.getenv("ATARI_DIFFICULTY")
50
-
51
- # Convert to int if specified
52
- mode = int(mode) if mode is not None else None
53
- difficulty = int(difficulty) if difficulty is not None else None
54
-
55
- # Create the environment instance
56
- env = AtariEnvironment(
57
- game_name=game_name,
58
- obs_type=obs_type,
59
- full_action_space=full_action_space,
60
- mode=mode,
61
- difficulty=difficulty,
62
- repeat_action_probability=repeat_action_prob,
63
- frameskip=frameskip,
64
- )
65
-
66
- # Create the FastAPI app with web interface and README integration
67
- app = create_app(env, AtariAction, AtariObservation, env_name="atari_env")
68
-
69
-
70
- if __name__ == "__main__":
71
- import uvicorn
72
-
73
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/server/atari_environment.py DELETED
@@ -1,245 +0,0 @@
1
- # Copyright (c) Meta Platforms, Inc. and affiliates.
2
- # All rights reserved.
3
- #
4
- # This source code is licensed under the BSD-style license found in the
5
- # LICENSE file in the root directory of this source tree.
6
-
7
- """
8
- Atari Environment Server Implementation.
9
-
10
- This module wraps ALE's ALEInterface and exposes it
11
- via the OpenEnv Environment interface.
12
- """
13
-
14
- import uuid
15
- from typing import Any, Dict, Literal, Optional
16
-
17
- from core.env_server import Action, Environment, Observation
18
-
19
- from ..models import AtariAction, AtariObservation, AtariState
20
-
21
- # Import ALE
22
- try:
23
- from ale_py import ALEInterface, roms
24
- import numpy as np
25
- except ImportError as e:
26
- raise ImportError(
27
- "ALE (Arcade Learning Environment) is not installed. "
28
- "Please install it with: pip install ale-py"
29
- ) from e
30
-
31
-
32
- class AtariEnvironment(Environment):
33
- """
34
- Atari Environment wrapper for OpenEnv.
35
-
36
- This environment wraps Atari 2600 games via the Arcade Learning Environment (ALE)
37
- and provides a clean interface for RL training.
38
-
39
- Supported games include: pong, breakout, space_invaders, and 100+ others.
40
-
41
- Args:
42
- game_name: Name of the Atari game (e.g., "pong", "breakout").
43
- obs_type: Observation type - "rgb", "grayscale", or "ram".
44
- full_action_space: Use full action space (18 actions) vs minimal.
45
- mode: Game mode (if applicable).
46
- difficulty: Game difficulty (if applicable).
47
- repeat_action_probability: Sticky action probability (default 0.0).
48
- frameskip: Number of frames to skip per action (default 4).
49
-
50
- Example:
51
- >>> env = AtariEnvironment("pong")
52
- >>> obs = env.reset()
53
- >>> print(obs.screen_shape) # [210, 160, 3]
54
- >>> obs = env.step(AtariAction(action_id=2)) # UP
55
- >>> print(obs.reward, obs.done)
56
- """
57
-
58
- def __init__(
59
- self,
60
- game_name: str = "pong",
61
- obs_type: Literal["rgb", "grayscale", "ram"] = "rgb",
62
- full_action_space: bool = False,
63
- mode: Optional[int] = None,
64
- difficulty: Optional[int] = None,
65
- repeat_action_probability: float = 0.0,
66
- frameskip: int = 4,
67
- ):
68
- """Initialize Atari environment."""
69
- super().__init__()
70
-
71
- self.game_name = game_name
72
- self.obs_type = obs_type
73
- self.full_action_space = full_action_space
74
- self.mode = mode
75
- self.difficulty = difficulty
76
- self.repeat_action_probability = repeat_action_probability
77
- self.frameskip = frameskip
78
-
79
- # Create ALE interface
80
- self.ale = ALEInterface()
81
-
82
- # Configure ALE
83
- from ale_py import LoggerMode
84
- self.ale.setLoggerMode(LoggerMode.Error) # Error mode only
85
- self.ale.setFloat("repeat_action_probability", repeat_action_probability)
86
-
87
- # Load ROM
88
- try:
89
- rom_path = roms.get_rom_path(game_name)
90
- self.ale.loadROM(rom_path)
91
- except Exception as e:
92
- raise ValueError(
93
- f"Failed to load Atari game '{game_name}': {e}\n"
94
- f"Available games can be found via: ale_py.roms.list_roms()"
95
- ) from e
96
-
97
- # Set mode and difficulty if specified
98
- if mode is not None:
99
- self.ale.setMode(mode)
100
- if difficulty is not None:
101
- self.ale.setDifficulty(difficulty)
102
-
103
- # Get action set
104
- if full_action_space:
105
- self._action_set = self.ale.getLegalActionSet()
106
- else:
107
- self._action_set = self.ale.getMinimalActionSet()
108
-
109
- # Get screen dimensions for observation space
110
- self.screen_height, self.screen_width = self.ale.getScreenDims()
111
- if obs_type == "rgb":
112
- self.screen_shape = [self.screen_height, self.screen_width, 3]
113
- elif obs_type == "grayscale":
114
- self.screen_shape = [self.screen_height, self.screen_width]
115
- elif obs_type == "ram":
116
- self.screen_shape = [self.ale.getRAMSize()]
117
- else:
118
- raise ValueError(f"Invalid obs_type: {obs_type}")
119
-
120
- # Initialize state
121
- self._state = AtariState(
122
- game_name=game_name,
123
- obs_type=obs_type,
124
- full_action_space=full_action_space,
125
- mode=mode,
126
- difficulty=difficulty,
127
- repeat_action_probability=repeat_action_probability,
128
- frameskip=frameskip,
129
- )
130
-
131
- def reset(self) -> Observation:
132
- """
133
- Reset the environment and return initial observation.
134
-
135
- Returns:
136
- Initial observation for the agent.
137
- """
138
- # Reset ALE
139
- self.ale.reset_game()
140
-
141
- # Reset state tracking
142
- self._state.episode_id = str(uuid.uuid4())
143
- self._state.step_count = 0
144
-
145
- # Get initial observation
146
- return self._make_observation()
147
-
148
- def step(self, action: Action) -> Observation:
149
- """
150
- Execute agent's action and return resulting observation.
151
-
152
- Args:
153
- action: AtariAction containing the action_id to execute.
154
-
155
- Returns:
156
- Observation after action execution.
157
-
158
- Raises:
159
- ValueError: If action is not an AtariAction.
160
- """
161
- if not isinstance(action, AtariAction):
162
- raise ValueError(f"Expected AtariAction, got {type(action)}")
163
-
164
- # Validate action_id
165
- if action.action_id < 0 or action.action_id >= len(self._action_set):
166
- raise ValueError(
167
- f"Invalid action_id: {action.action_id}. "
168
- f"Valid range: [0, {len(self._action_set) - 1}]"
169
- )
170
-
171
- # Get actual ALE action
172
- ale_action = self._action_set[action.action_id]
173
-
174
- # Execute action with frameskip
175
- total_reward = 0.0
176
- for _ in range(self.frameskip):
177
- total_reward += self.ale.act(ale_action)
178
- if self.ale.game_over():
179
- break
180
-
181
- self._state.step_count += 1
182
-
183
- # Get observation
184
- obs = self._make_observation()
185
- obs.reward = total_reward
186
-
187
- return obs
188
-
189
- @property
190
- def state(self) -> AtariState:
191
- """Get current environment state."""
192
- return self._state
193
-
194
- def _make_observation(self) -> AtariObservation:
195
- """
196
- Create an AtariObservation from current ALE state.
197
-
198
- Returns:
199
- AtariObservation for the agent.
200
- """
201
- # Get screen observation
202
- if self.obs_type == "rgb":
203
- screen = self.ale.getScreenRGB()
204
- elif self.obs_type == "grayscale":
205
- screen = self.ale.getScreenGrayscale()
206
- elif self.obs_type == "ram":
207
- screen = self.ale.getRAM()
208
- else:
209
- raise ValueError(f"Invalid obs_type: {self.obs_type}")
210
-
211
- # Flatten screen for JSON serialization
212
- # Handle both numpy arrays and lists
213
- if hasattr(screen, "flatten"):
214
- screen_flat = screen.flatten().tolist()
215
- elif hasattr(screen, "tolist"):
216
- screen_flat = screen.tolist()
217
- else:
218
- screen_flat = list(screen)
219
-
220
- # Get game info
221
- lives = self.ale.lives()
222
- episode_frame_number = self.ale.getEpisodeFrameNumber()
223
- frame_number = self.ale.getFrameNumber()
224
- done = self.ale.game_over()
225
-
226
- # Create legal actions list (indices into action_set)
227
- legal_actions = list(range(len(self._action_set)))
228
-
229
- # Create observation
230
- obs = AtariObservation(
231
- screen=screen_flat,
232
- screen_shape=self.screen_shape,
233
- legal_actions=legal_actions,
234
- lives=lives,
235
- episode_frame_number=episode_frame_number,
236
- frame_number=frame_number,
237
- done=done,
238
- reward=0.0, # Will be filled in by step()
239
- metadata={
240
- "game_name": self.game_name,
241
- "action_meanings": [str(a) for a in self._action_set],
242
- },
243
- )
244
-
245
- return obs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/atari_env/server/requirements.txt DELETED
@@ -1,3 +0,0 @@
1
- gymnasium>=0.29.0
2
- ale-py>=0.8.0
3
- numpy>=1.24.0
 
 
 
 
openenv/src/envs/atari_env/test_atari_docker.sh DELETED
@@ -1,333 +0,0 @@
1
- #!/bin/bash
2
- # Comprehensive Docker test for Atari environment
3
- # Tests: Build, Start, Health, Reset, Step, State, Cleanup
4
-
5
- set -e # Exit on error
6
-
7
- # Colors for output
8
- RED='\033[0;31m'
9
- GREEN='\033[0;32m'
10
- YELLOW='\033[1;33m'
11
- BLUE='\033[0;34m'
12
- NC='\033[0m' # No Color
13
-
14
- # Configuration
15
- IMAGE_NAME="atari-env"
16
- IMAGE_TAG="test"
17
- CONTAINER_NAME="atari-env-test"
18
- PORT="8765" # Use non-standard port to avoid conflicts
19
- HEALTH_RETRIES=30
20
- HEALTH_DELAY=2
21
-
22
- # Cleanup function
23
- cleanup() {
24
- echo -e "\n${BLUE}Cleaning up...${NC}"
25
- docker stop ${CONTAINER_NAME} 2>/dev/null || true
26
- docker rm ${CONTAINER_NAME} 2>/dev/null || true
27
- echo -e "${GREEN}✓${NC} Cleanup complete"
28
- }
29
-
30
- # Set trap to cleanup on exit
31
- trap cleanup EXIT
32
-
33
- # Header
34
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
35
- echo " ATARI ENVIRONMENT DOCKER TEST"
36
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
37
- echo ""
38
-
39
- # Check prerequisites
40
- echo -e "${BLUE}Checking prerequisites...${NC}"
41
- if ! command -v docker &> /dev/null; then
42
- echo -e "${RED}✗${NC} Docker is not installed"
43
- exit 1
44
- fi
45
- echo -e "${GREEN}✓${NC} Docker is installed"
46
-
47
- if ! command -v curl &> /dev/null; then
48
- echo -e "${RED}✗${NC} curl is not installed"
49
- exit 1
50
- fi
51
- echo -e "${GREEN}✓${NC} curl is installed"
52
-
53
- # Check if we're in the right directory
54
- if [ ! -f "src/envs/atari_env/server/Dockerfile" ]; then
55
- echo -e "${RED}✗${NC} Must run from OpenEnv root directory"
56
- exit 1
57
- fi
58
- echo -e "${GREEN}✓${NC} In correct directory"
59
-
60
- # Step 1: Build Docker image
61
- echo ""
62
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
63
- echo -e "${BLUE}STEP 1: Building Docker Image${NC}"
64
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
65
-
66
- echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..."
67
- if docker build -f src/envs/atari_env/server/Dockerfile -t ${IMAGE_NAME}:${IMAGE_TAG} . 2>&1 | tee /tmp/atari_build.log | tail -n 20; then
68
- echo -e "${GREEN}✓${NC} Docker image built successfully"
69
- else
70
- echo -e "${RED}✗${NC} Docker build failed"
71
- echo "See /tmp/atari_build.log for full output"
72
- exit 1
73
- fi
74
-
75
- # Check image exists
76
- if docker image inspect ${IMAGE_NAME}:${IMAGE_TAG} &> /dev/null; then
77
- IMAGE_SIZE=$(docker image inspect ${IMAGE_NAME}:${IMAGE_TAG} --format='{{.Size}}' | awk '{print $1/1024/1024}')
78
- echo -e "${GREEN}✓${NC} Image size: ${IMAGE_SIZE} MB"
79
- else
80
- echo -e "${RED}✗${NC} Image not found after build"
81
- exit 1
82
- fi
83
-
84
- # Step 2: Start container
85
- echo ""
86
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
87
- echo -e "${BLUE}STEP 2: Starting Container${NC}"
88
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
89
-
90
- # Clean up any existing container
91
- docker rm -f ${CONTAINER_NAME} 2>/dev/null || true
92
-
93
- echo "Starting container on port ${PORT}..."
94
- docker run -d \
95
- --name ${CONTAINER_NAME} \
96
- -p ${PORT}:8000 \
97
- -e ATARI_GAME=pong \
98
- -e ATARI_OBS_TYPE=ram \
99
- -e ATARI_FRAMESKIP=4 \
100
- ${IMAGE_NAME}:${IMAGE_TAG}
101
-
102
- if [ $? -eq 0 ]; then
103
- echo -e "${GREEN}✓${NC} Container started: ${CONTAINER_NAME}"
104
- else
105
- echo -e "${RED}✗${NC} Failed to start container"
106
- exit 1
107
- fi
108
-
109
- # Wait for container to be running
110
- sleep 2
111
- if docker ps | grep -q ${CONTAINER_NAME}; then
112
- echo -e "${GREEN}✓${NC} Container is running"
113
- else
114
- echo -e "${RED}✗${NC} Container is not running"
115
- docker logs ${CONTAINER_NAME}
116
- exit 1
117
- fi
118
-
119
- # Step 3: Wait for health check
120
- echo ""
121
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
122
- echo -e "${BLUE}STEP 3: Waiting for Server${NC}"
123
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
124
-
125
- echo "Waiting for server to be ready (timeout: ${HEALTH_RETRIES}s)..."
126
- for i in $(seq 1 ${HEALTH_RETRIES}); do
127
- if curl -s http://localhost:${PORT}/health > /dev/null 2>&1; then
128
- echo -e "${GREEN}✓${NC} Server is ready (${i}s)"
129
- break
130
- fi
131
-
132
- if [ $i -eq ${HEALTH_RETRIES} ]; then
133
- echo -e "${RED}✗${NC} Server did not become ready in time"
134
- echo "Container logs:"
135
- docker logs ${CONTAINER_NAME}
136
- exit 1
137
- fi
138
-
139
- echo -n "."
140
- sleep ${HEALTH_DELAY}
141
- done
142
-
143
- # Step 4: Test health endpoint
144
- echo ""
145
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
146
- echo -e "${BLUE}STEP 4: Testing Health Endpoint${NC}"
147
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
148
-
149
- HEALTH_RESPONSE=$(curl -s http://localhost:${PORT}/health)
150
- echo "Response: ${HEALTH_RESPONSE}"
151
-
152
- if echo "${HEALTH_RESPONSE}" | grep -q "healthy"; then
153
- echo -e "${GREEN}✓${NC} Health endpoint working"
154
- else
155
- echo -e "${RED}✗${NC} Health endpoint failed"
156
- exit 1
157
- fi
158
-
159
- # Step 5: Test reset endpoint
160
- echo ""
161
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
162
- echo -e "${BLUE}STEP 5: Testing Reset Endpoint${NC}"
163
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
164
-
165
- RESET_RESPONSE=$(curl -s -X POST http://localhost:${PORT}/reset -H "Content-Type: application/json" -d '{}')
166
-
167
- if [ -z "${RESET_RESPONSE}" ]; then
168
- echo -e "${RED}✗${NC} Reset endpoint returned empty response"
169
- docker logs ${CONTAINER_NAME} | tail -20
170
- exit 1
171
- fi
172
-
173
- echo "Response (first 200 chars): ${RESET_RESPONSE:0:200}..."
174
-
175
- # Check if response contains expected fields
176
- if echo "${RESET_RESPONSE}" | grep -q "observation" && \
177
- echo "${RESET_RESPONSE}" | grep -q "screen" && \
178
- echo "${RESET_RESPONSE}" | grep -q "legal_actions"; then
179
- echo -e "${GREEN}✓${NC} Reset endpoint working"
180
-
181
- # Extract some info
182
- SCREEN_LEN=$(echo "${RESET_RESPONSE}" | grep -o '"screen":\[[^]]*\]' | wc -c)
183
- echo " Screen data length: ${SCREEN_LEN} chars"
184
- else
185
- echo -e "${RED}✗${NC} Reset response missing required fields"
186
- echo "Full response: ${RESET_RESPONSE}"
187
- exit 1
188
- fi
189
-
190
- # Step 6: Test step endpoint
191
- echo ""
192
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
193
- echo -e "${BLUE}STEP 6: Testing Step Endpoint${NC}"
194
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
195
-
196
- STEP_PAYLOAD='{"action": {"action_id": 0, "game_name": "pong"}}'
197
- STEP_RESPONSE=$(curl -s -X POST http://localhost:${PORT}/step -H "Content-Type: application/json" -d "${STEP_PAYLOAD}")
198
-
199
- if [ -z "${STEP_RESPONSE}" ]; then
200
- echo -e "${RED}✗${NC} Step endpoint returned empty response"
201
- docker logs ${CONTAINER_NAME} | tail -20
202
- exit 1
203
- fi
204
-
205
- echo "Response (first 200 chars): ${STEP_RESPONSE:0:200}..."
206
-
207
- # Check if response contains expected fields
208
- if echo "${STEP_RESPONSE}" | grep -q "observation" && \
209
- echo "${STEP_RESPONSE}" | grep -q "reward" && \
210
- echo "${STEP_RESPONSE}" | grep -q "done"; then
211
- echo -e "${GREEN}✓${NC} Step endpoint working"
212
-
213
- # Extract reward and done
214
- REWARD=$(echo "${STEP_RESPONSE}" | grep -o '"reward":[^,}]*' | cut -d: -f2)
215
- DONE=$(echo "${STEP_RESPONSE}" | grep -o '"done":[^,}]*' | cut -d: -f2)
216
- echo " Reward: ${REWARD}"
217
- echo " Done: ${DONE}"
218
- else
219
- echo -e "${RED}✗${NC} Step response missing required fields"
220
- echo "Full response: ${STEP_RESPONSE}"
221
- exit 1
222
- fi
223
-
224
- # Step 7: Test state endpoint
225
- echo ""
226
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
227
- echo -e "${BLUE}STEP 7: Testing State Endpoint${NC}"
228
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
229
-
230
- STATE_RESPONSE=$(curl -s http://localhost:${PORT}/state)
231
-
232
- if [ -z "${STATE_RESPONSE}" ]; then
233
- echo -e "${RED}✗${NC} State endpoint returned empty response"
234
- docker logs ${CONTAINER_NAME} | tail -20
235
- exit 1
236
- fi
237
-
238
- echo "Response: ${STATE_RESPONSE}"
239
-
240
- # Check if response contains expected fields
241
- if echo "${STATE_RESPONSE}" | grep -q "episode_id" && \
242
- echo "${STATE_RESPONSE}" | grep -q "step_count" && \
243
- echo "${STATE_RESPONSE}" | grep -q "game_name"; then
244
- echo -e "${GREEN}✓${NC} State endpoint working"
245
-
246
- # Extract info
247
- GAME_NAME=$(echo "${STATE_RESPONSE}" | grep -o '"game_name":"[^"]*"' | cut -d'"' -f4)
248
- STEP_COUNT=$(echo "${STATE_RESPONSE}" | grep -o '"step_count":[^,}]*' | cut -d: -f2)
249
- echo " Game: ${GAME_NAME}"
250
- echo " Steps: ${STEP_COUNT}"
251
- else
252
- echo -e "${RED}✗${NC} State response missing required fields"
253
- echo "Full response: ${STATE_RESPONSE}"
254
- exit 1
255
- fi
256
-
257
- # Step 8: Test multiple steps
258
- echo ""
259
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
260
- echo -e "${BLUE}STEP 8: Testing Multiple Steps${NC}"
261
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
262
-
263
- echo "Taking 10 steps..."
264
- TOTAL_REWARD=0
265
- for i in {1..10}; do
266
- ACTION_ID=$((RANDOM % 3)) # Random action 0-2
267
- STEP_PAYLOAD="{\"action\": {\"action_id\": ${ACTION_ID}, \"game_name\": \"pong\"}}"
268
- STEP_RESPONSE=$(curl -s -X POST http://localhost:${PORT}/step -H "Content-Type: application/json" -d "${STEP_PAYLOAD}")
269
-
270
- if ! echo "${STEP_RESPONSE}" | grep -q "observation"; then
271
- echo -e "${RED}✗${NC} Step ${i} failed"
272
- exit 1
273
- fi
274
-
275
- REWARD=$(echo "${STEP_RESPONSE}" | grep -o '"reward":[^,}]*' | cut -d: -f2 | sed 's/null/0/')
276
- DONE=$(echo "${STEP_RESPONSE}" | grep -o '"done":[^,}]*' | cut -d: -f2)
277
-
278
- echo " Step ${i}: action=${ACTION_ID}, reward=${REWARD}, done=${DONE}"
279
-
280
- if [ "${DONE}" = "true" ]; then
281
- echo " Episode completed early at step ${i}"
282
- break
283
- fi
284
- done
285
-
286
- echo -e "${GREEN}✓${NC} Multiple steps completed successfully"
287
-
288
- # Step 9: Check container logs for errors
289
- echo ""
290
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
291
- echo -e "${BLUE}STEP 9: Checking Container Logs${NC}"
292
- echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
293
-
294
- LOGS=$(docker logs ${CONTAINER_NAME} 2>&1)
295
-
296
- if echo "${LOGS}" | grep -i "error" | grep -v "LoggerMode.Error"; then
297
- echo -e "${YELLOW}⚠${NC} Found errors in logs:"
298
- echo "${LOGS}" | grep -i "error" | head -5
299
- else
300
- echo -e "${GREEN}✓${NC} No errors in container logs"
301
- fi
302
-
303
- if echo "${LOGS}" | grep -i "exception"; then
304
- echo -e "${RED}✗${NC} Found exceptions in logs:"
305
- echo "${LOGS}" | grep -i "exception" | head -5
306
- exit 1
307
- else
308
- echo -e "${GREEN}✓${NC} No exceptions in container logs"
309
- fi
310
-
311
- # Final Summary
312
- echo ""
313
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
314
- echo -e "${GREEN}✅ ALL DOCKER TESTS PASSED${NC}"
315
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
316
- echo ""
317
- echo "Summary:"
318
- echo " ✓ Docker image built successfully"
319
- echo " ✓ Container started and ran"
320
- echo " ✓ Health endpoint working"
321
- echo " ✓ Reset endpoint working"
322
- echo " ✓ Step endpoint working"
323
- echo " ✓ State endpoint working"
324
- echo " ✓ Multiple steps working"
325
- echo " ✓ No errors or exceptions"
326
- echo ""
327
- echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}"
328
- echo "Container: ${CONTAINER_NAME}"
329
- echo "Port: ${PORT}"
330
- echo ""
331
- echo "To keep container running: docker start ${CONTAINER_NAME}"
332
- echo "To view logs: docker logs ${CONTAINER_NAME}"
333
- echo ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/browsergym_env/README.md DELETED
@@ -1,554 +0,0 @@
1
- ---
2
- title: BrowserGym Environment Server
3
- emoji: 🌐
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- app_port: 8000
9
- base_path: /web
10
- tags:
11
- - openenv
12
- - browsergym
13
- - web-automation
14
- - reinforcement-learning
15
- ---
16
-
17
- # BrowserGym Environment
18
-
19
- BrowserGym is a unified framework for web-based agent tasks that provides access to multiple benchmarks under a single Gymnasium-compatible API. This integration brings the complete training-to-evaluation pipeline for web agents into OpenEnv.
20
-
21
- ## Why BrowserGym?
22
-
23
- BrowserGym provides a complete pipeline for developing web agents: train on simple tasks, then evaluate on realistic websites.
24
-
25
- **What are these benchmarks?**
26
-
27
- - **MiniWoB++ (Training)**: 100+ synthetic web tasks like "click this button", "fill out this form", "select from dropdown". Each task is a simple webpage with a clear objective. Fast resets, randomized variations, dense rewards. Perfect for learning basic web navigation skills. **No external setup needed** - tasks run in isolated browser sessions.
28
-
29
- - **WebArena (Evaluation)**: 812 tasks on real websites (e-commerce, forums, GitLab, Wikipedia). Tasks like "find the cheapest laptop and add to cart" or "create a merge request for bug #123". Multistep, requires reasoning, sparse rewards. Tests if your agent can handle actual websites. **Requires running 7 backend services** (shopping site, GitLab instance, etc.).
30
-
31
- - **VisualWebArena**: Similar to WebArena but requires visual understanding - agents need to interpret images, identify UI elements visually, handle multimodal content.
32
-
33
- - **WorkArena**: Enterprise software tasks (CRM, project management, business workflows). Tests automation on corporate-style applications.
34
-
35
- **The training → evaluation pipeline:**
36
- 1. Train on MiniWoB (simple, controlled, fast iterations)
37
- 2. Evaluate on WebArena (complex, realistic, measures real-world capability)
38
-
39
- **Key advantage**: You can start training immediately with MiniWoB. No need to set up infrastructure just to test if your code works.
40
-
41
- ## Quick Start - Training (MiniWoB)
42
-
43
- ### No Setup Required! 🎉
44
-
45
- ```python
46
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
47
-
48
- # Create environment for MiniWoB training task
49
- env = BrowserGymEnv.from_docker_image(
50
- "ghcr.io/openenv/browsergym-env:latest",
51
- environment={
52
- "BROWSERGYM_BENCHMARK": "miniwob",
53
- "BROWSERGYM_TASK_NAME": "click-test", # or "click-button", "click-dialog", etc.
54
- }
55
- )
56
-
57
- # Train your agent!
58
- for episode in range(1000):
59
- result = env.reset()
60
- print(f"Goal: {result.observation.goal}")
61
-
62
- done = False
63
- while not done:
64
- # Your agent decides what to do
65
- action_str = agent.get_action(result.observation.text)
66
- action = BrowserGymAction(action_str=action_str)
67
-
68
- result = env.step(action)
69
- done = result.done
70
-
71
- print(f"Reward: {result.reward}")
72
-
73
- env.close()
74
- ```
75
-
76
- ### Available Tasks by Benchmark
77
-
78
- #### MiniWoB++ Tasks (Training - 100+ tasks)
79
-
80
- MiniWoB tasks are organized by difficulty and type. Here are the main categories:
81
-
82
- **Click Tasks** (Basic interaction)
83
- | Task Name | Description | Difficulty |
84
- |-----------|-------------|------------|
85
- | `click-test` | Click a single button | ⭐ Easy |
86
- | `click-button` | Click button with specific text | ⭐ Easy |
87
- | `click-button-sequence` | Click buttons in order | ⭐⭐ Medium |
88
- | `click-checkboxes` | Select specific checkboxes | ⭐⭐ Medium |
89
- | `click-checkboxes-soft` | Select checkboxes (multiple valid) | ⭐⭐ Medium |
90
- | `click-checkboxes-large` | Many checkboxes to select from | ⭐⭐ Medium |
91
- | `click-checkboxes-transfer` | Transfer learning variation | ⭐⭐ Medium |
92
- | `click-dialog` | Click correct button in dialog | ⭐ Easy |
93
- | `click-dialog-2` | More complex dialog | ⭐⭐ Medium |
94
- | `click-link` | Click on a link | ⭐ Easy |
95
- | `click-option` | Select from dropdown | ⭐⭐ Medium |
96
- | `click-pie` | Click on pie chart slice | ⭐⭐ Medium |
97
- | `click-scroll-list` | Click item in scrollable list | ⭐⭐⭐ Hard |
98
- | `click-shades` | Click on specific color shade | ⭐⭐ Medium |
99
- | `click-shape` | Click on specific shape | ⭐⭐ Medium |
100
- | `click-tab` | Switch between tabs | ⭐⭐ Medium |
101
- | `click-tab-2` | More complex tab switching | ⭐⭐⭐ Hard |
102
- | `click-widget` | Click on UI widget | ⭐⭐ Medium |
103
-
104
- **Text Entry Tasks** (Typing and forms)
105
- | Task Name | Description | Difficulty |
106
- |-----------|-------------|------------|
107
- | `enter-text` | Type text into input field | ⭐ Easy |
108
- | `enter-text-dynamic` | Dynamic text entry | ⭐⭐ Medium |
109
- | `enter-text-2` | Multiple text fields | ⭐⭐ Medium |
110
- | `enter-password` | Fill password field | ⭐ Easy |
111
- | `enter-date` | Enter a date | ⭐⭐ Medium |
112
- | `enter-time` | Enter a time | ⭐⭐ Medium |
113
- | `login-user` | Complete login form | ⭐⭐ Medium |
114
- | `login-user-popup` | Login via popup | ⭐⭐⭐ Hard |
115
-
116
- **Navigation Tasks** (Multi-step interaction)
117
- | Task Name | Description | Difficulty |
118
- |-----------|-------------|------------|
119
- | `navigate-tree` | Navigate through tree structure | ⭐⭐⭐ Hard |
120
- | `search-engine` | Use search interface | ⭐⭐ Medium |
121
- | `use-autocomplete` | Interact with autocomplete | ⭐⭐⭐ Hard |
122
- | `book-flight` | Book a flight (complex form) | ⭐⭐⭐⭐ Very Hard |
123
- | `choose-date` | Pick date from calendar | ⭐⭐⭐ Hard |
124
- | `choose-date-easy` | Simplified date picker | ⭐⭐ Medium |
125
- | `choose-date-medium` | Medium difficulty date picker | ⭐⭐⭐ Hard |
126
- | `choose-list` | Select from long list | ⭐⭐ Medium |
127
-
128
- **Visual/Spatial Tasks** (Requires visual understanding)
129
- | Task Name | Description | Difficulty |
130
- |-----------|-------------|------------|
131
- | `count-sides` | Count sides of shape | ⭐⭐ Medium |
132
- | `count-shape` | Count specific shapes | ⭐⭐ Medium |
133
- | `find-word` | Find word in text | ⭐⭐ Medium |
134
- | `focus-text` | Focus on text element | ⭐ Easy |
135
- | `focus-text-2` | More complex focus task | ⭐⭐ Medium |
136
- | `grid-coordinate` | Click grid coordinate | ⭐⭐ Medium |
137
- | `guess-number` | Guess a number game | ⭐⭐⭐ Hard |
138
- | `identify-shape` | Identify shape type | ⭐⭐ Medium |
139
- | `read-table` | Extract info from table | ⭐⭐⭐ Hard |
140
- | `read-table-2` | More complex table reading | ⭐⭐⭐ Hard |
141
-
142
- **Email/Social Tasks** (Realistic scenarios)
143
- | Task Name | Description | Difficulty |
144
- |-----------|-------------|------------|
145
- | `email-inbox` | Manage email inbox | ⭐⭐⭐⭐ Very Hard |
146
- | `email-inbox-forward` | Forward emails | ⭐⭐⭐⭐ Very Hard |
147
- | `email-inbox-nl` | Natural language email task | ⭐⭐⭐⭐ Very Hard |
148
- | `email-inbox-star-reply` | Star and reply to emails | ⭐⭐⭐⭐ Very Hard |
149
- | `social-media` | Social media interaction | ⭐⭐⭐⭐ Very Hard |
150
- | `social-media-some` | Partial social media task | ⭐⭐⭐ Hard |
151
-
152
- **Total:** 100+ tasks across all categories
153
-
154
- **Usage:**
155
- ```python
156
- # Easy task for quick testing
157
- env = BrowserGymEnv(environment={"BROWSERGYM_TASK_NAME": "click-test"})
158
-
159
- # Medium difficulty for training
160
- env = BrowserGymEnv(environment={"BROWSERGYM_TASK_NAME": "click-checkboxes"})
161
-
162
- # Hard task for evaluation
163
- env = BrowserGymEnv(environment={"BROWSERGYM_TASK_NAME": "email-inbox"})
164
- ```
165
-
166
- #### WebArena Tasks (Evaluation - 812 tasks)
167
-
168
- WebArena tasks are organized by website and difficulty. Tasks are numbered 0-811.
169
-
170
- **By Website:**
171
- | Website | Task Count | Description | Example Tasks |
172
- |---------|------------|-------------|---------------|
173
- | Shopping | ~200 | E-commerce site | Search products, add to cart, checkout |
174
- | Shopping Admin | ~150 | Admin panel | Manage products, orders, customers |
175
- | Reddit | ~150 | Forum/social | Post, comment, search discussions |
176
- | GitLab | ~200 | Code repository | Create issues, merge requests, review code |
177
- | Wikipedia | ~100 | Knowledge base | Search, read, extract information |
178
- | Map | ~12 | Location service | Find places, get directions |
179
-
180
- **By Difficulty:**
181
- | Difficulty | Task Count | Steps Required | Example |
182
- |------------|------------|----------------|---------|
183
- | Easy | ~200 | 1-5 steps | "Find the price of product X" |
184
- | Medium | ~400 | 5-15 steps | "Add cheapest laptop to cart" |
185
- | Hard | ~212 | 15+ steps | "Create merge request for bug fix" |
186
-
187
- **Usage:**
188
- ```python
189
- # Task 0 (usually easy)
190
- env = BrowserGymEnv(environment={
191
- "BROWSERGYM_BENCHMARK": "webarena",
192
- "BROWSERGYM_TASK_NAME": "0",
193
- "SHOPPING": "http://your-server:7770",
194
- # ... other URLs
195
- })
196
-
197
- # Task 156 (GitLab merge request)
198
- env = BrowserGymEnv(environment={
199
- "BROWSERGYM_BENCHMARK": "webarena",
200
- "BROWSERGYM_TASK_NAME": "156",
201
- # ... URLs
202
- })
203
- ```
204
-
205
- **Note:** WebArena tasks require the full backend infrastructure. See [WebArena setup guide](https://github.com/web-arena-x/webarena/tree/main/environment_docker).
206
-
207
- #### VisualWebArena Tasks (910 tasks)
208
-
209
- Similar to WebArena but requires visual understanding. Tasks involve:
210
- - Image-based reasoning
211
- - Visual element identification
212
- - Multimodal interaction (text + images)
213
-
214
- #### WorkArena Tasks
215
-
216
- Enterprise software automation tasks:
217
- - CRM operations
218
- - Project management
219
- - Business workflows
220
-
221
- **Full task lists:**
222
- - [MiniWoB++ tasks](https://github.com/Farama-Foundation/miniwob-plusplus/tree/master/miniwob/environment)
223
- - [WebArena tasks](https://github.com/web-arena-x/webarena/blob/main/config_files/)
224
- - [BrowserGym documentation](https://github.com/ServiceNow/BrowserGym)
225
-
226
- ## Evaluation (WebArena)
227
-
228
- ### Prerequisites
229
-
230
- WebArena requires setting up backend infrastructure. See the [WebArena documentation](https://github.com/web-arena-x/webarena/tree/main/environment_docker).
231
-
232
- ### Usage
233
-
234
- ```python
235
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
236
-
237
- # Create environment for WebArena evaluation
238
- env = BrowserGymEnv.from_docker_image(
239
- "ghcr.io/openenv/browsergym-env:latest",
240
- environment={
241
- "BROWSERGYM_BENCHMARK": "webarena",
242
- "BROWSERGYM_TASK_NAME": "0", # Task ID
243
- # WebArena backend URLs (required)
244
- "SHOPPING": "http://your-server:7770",
245
- "SHOPPING_ADMIN": "http://your-server:7780/admin",
246
- "REDDIT": "http://your-server:9999",
247
- "GITLAB": "http://your-server:8023",
248
- "MAP": "http://your-server:3000",
249
- "WIKIPEDIA": "http://your-server:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing",
250
- "HOMEPAGE": "http://your-server:4399",
251
- }
252
- )
253
-
254
- # Evaluate your trained agent
255
- result = env.reset()
256
- while not result.done:
257
- action_str = agent.get_action(result.observation)
258
- action = BrowserGymAction(action_str=action_str)
259
- result = env.step(action)
260
-
261
- print(f"Success: {result.reward}")
262
- env.close()
263
- ```
264
-
265
- ## Building the Docker Image
266
-
267
- ### Prerequisites
268
-
269
- 1. **Base Image**: Build the OpenEnv base image first:
270
-
271
- ```bash
272
- # From the OpenEnv repository root
273
- docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
274
- ```
275
-
276
- ### Build the BrowserGym Environment
277
-
278
- ```bash
279
- # From the OpenEnv repository root
280
- docker build -t browsergym-env:latest -f src/envs/browsergym_env/server/Dockerfile .
281
- ```
282
-
283
- ### Run the Server
284
-
285
- #### For MiniWoB (Training):
286
-
287
- ```bash
288
- docker run -p 8000:8000 \
289
- -e BROWSERGYM_BENCHMARK="miniwob" \
290
- -e BROWSERGYM_TASK_NAME="click-test" \
291
- browsergym-env:latest
292
- ```
293
-
294
- #### For WebArena (Evaluation):
295
-
296
- ```bash
297
- docker run -p 8000:8000 \
298
- -e BROWSERGYM_BENCHMARK="webarena" \
299
- -e BROWSERGYM_TASK_NAME="0" \
300
- -e SHOPPING="http://your-server:7770" \
301
- -e SHOPPING_ADMIN="http://your-server:7780/admin" \
302
- -e REDDIT="http://your-server:9999" \
303
- -e GITLAB="http://your-server:8023" \
304
- -e MAP="http://your-server:3000" \
305
- -e WIKIPEDIA="http://your-server:8888/wikipedia_en_all_maxi_2022-05/A/User:The_other_Kiwix_guy/Landing" \
306
- -e HOMEPAGE="http://your-server:4399" \
307
- browsergym-env:latest
308
- ```
309
-
310
- ## Environment Details
311
-
312
- ### Action
313
-
314
- Actions in BrowserGym are natural language strings that describe browser operations:
315
-
316
- ```python
317
- from envs.browsergym_env import BrowserGymAction
318
-
319
- # Click actions
320
- action = BrowserGymAction(action_str="click('Submit button')")
321
- action = BrowserGymAction(action_str="click('element_id_123')")
322
-
323
- # Type actions
324
- action = BrowserGymAction(action_str="fill('username', 'john@example.com')")
325
- action = BrowserGymAction(action_str="fill('password', 'secret123')")
326
-
327
- # Navigate actions
328
- action = BrowserGymAction(action_str="goto('https://example.com')")
329
-
330
- # Keyboard actions
331
- action = BrowserGymAction(action_str="press('Enter')")
332
- action = BrowserGymAction(action_str="press('Tab')")
333
-
334
- # Scroll actions
335
- action = BrowserGymAction(action_str="scroll('down')")
336
- ```
337
-
338
- ### Observation
339
-
340
- Observations contain multiple modalities:
341
-
342
- ```python
343
- result = env.step(action)
344
- obs = result.observation
345
-
346
- # Text observations
347
- print(obs.text) # Primary text representation (AXTree or DOM)
348
- print(obs.axtree_txt) # Accessibility tree
349
- print(obs.pruned_html) # Pruned HTML (interactive elements only)
350
-
351
- # Page metadata
352
- print(obs.url) # Current URL
353
- print(obs.goal) # Task goal/instruction
354
-
355
- # Visual (if enabled)
356
- if obs.screenshot is not None:
357
- print(obs.screenshot.shape) # [height, width, channels]
358
-
359
- # Error handling
360
- if obs.last_action_error:
361
- print(f"Action failed: {obs.error}")
362
-
363
- # Episode status
364
- print(obs.done) # True if episode ended
365
- print(obs.reward) # Reward for the step
366
-
367
- # Access full BrowserGym data (includes timestamps, etc.)
368
- print(obs.metadata["browsergym_obs"]) # Full observation dict from BrowserGym
369
- print(obs.metadata["browsergym_info"]) # Full info dict (timestamps, page state, etc.)
370
- ```
371
-
372
- #### Advanced: Accessing Raw BrowserGym Data
373
-
374
- For VisualWebArena or custom training, you may need additional data like timestamps or browser state. The full BrowserGym observation and info dicts are preserved in `metadata`:
375
-
376
- ```python
377
- result = env.step(action)
378
-
379
- # Access timestamps (if available)
380
- info = result.observation.metadata["browsergym_info"]
381
- if "timestamp" in info:
382
- print(f"Action timestamp: {info['timestamp']}")
383
-
384
- # Access additional observation fields
385
- obs_dict = result.observation.metadata["browsergym_obs"]
386
- if "dom_object" in obs_dict:
387
- dom = obs_dict["dom_object"]
388
- # Work with raw DOM object
389
-
390
- # Access page performance data
391
- if "performance" in info:
392
- print(f"Page load time: {info['performance']}")
393
- ```
394
-
395
- ### State
396
-
397
- The environment state tracks progress:
398
-
399
- ```python
400
- state = env.state()
401
-
402
- print(f"Benchmark: {state.benchmark}") # 'miniwob', 'webarena', etc.
403
- print(f"Task: {state.task_name}") # Task name/ID
404
- print(f"Episode: {state.episode_id}") # Unique episode ID
405
- print(f"Steps: {state.step_count}") # Number of steps taken
406
- print(f"Total Reward: {state.cum_reward}") # Cumulative reward
407
- print(f"Goal: {state.goal}") # Task instruction
408
- print(f"URL: {state.current_url}") # Current page URL
409
- ```
410
-
411
- ## Configuration
412
-
413
- Environment variables:
414
-
415
- ### Common Settings
416
- - `BROWSERGYM_BENCHMARK`: Benchmark to use (`miniwob`, `webarena`, `visualwebarena`, `workarena`)
417
- - `BROWSERGYM_TASK_NAME`: Specific task name (optional, will use first available if not set)
418
- - `BROWSERGYM_HEADLESS`: Run browser in headless mode (default: `true`)
419
- - `BROWSERGYM_VIEWPORT_WIDTH`: Browser viewport width (default: `1280`)
420
- - `BROWSERGYM_VIEWPORT_HEIGHT`: Browser viewport height (default: `720`)
421
- - `BROWSERGYM_TIMEOUT`: Action timeout in milliseconds (default: `10000`)
422
-
423
- ### WebArena-Specific (only needed for WebArena benchmark)
424
- - `SHOPPING`: Shopping website URL
425
- - `SHOPPING_ADMIN`: Shopping admin panel URL
426
- - `REDDIT`: Reddit-like forum URL
427
- - `GITLAB`: GitLab instance URL
428
- - `MAP`: Map service URL
429
- - `WIKIPEDIA`: Wikipedia instance URL
430
- - `HOMEPAGE`: Homepage URL
431
-
432
- ## Supported Benchmarks
433
-
434
- ### 1. MiniWoB++ (Training) ✅ Recommended for Training
435
-
436
- - **100+ tasks** ranging from simple (click buttons) to complex (form filling, navigation)
437
- - **Fast**: Instant resets, quick episodes
438
- - **Randomized**: Task variations for generalization
439
- - **No setup**: Works out-of-the-box
440
- - **Dense rewards**: Immediate feedback for learning
441
-
442
- **Use Case**: Train agents on fundamental web navigation skills
443
-
444
- ### 2. WebArena (Evaluation) 📊 Benchmark
445
-
446
- - **812 realistic tasks** across 6 websites
447
- - **Complex**: Multi-step reasoning, real web interfaces
448
- - **Requires setup**: Need to run 7 backend services
449
- - **Sparse rewards**: Binary success/failure
450
- - **Evaluation-focused**: Test real-world performance
451
-
452
- **Use Case**: Evaluate agents on realistic web tasks
453
-
454
- ### 3. VisualWebArena (Evaluation) 👁️ Visual Benchmark
455
-
456
- - **910 tasks** requiring visual understanding
457
- - **Multimodal**: Both text and visual observations
458
- - **Requires setup**: Similar to WebArena
459
- - **Challenging**: Requires visual reasoning
460
-
461
- **Use Case**: Test visual web navigation capabilities
462
-
463
- ### 4. WorkArena (Evaluation) 💼 Enterprise Benchmark
464
-
465
- - **Enterprise tasks**: CRM, project management, etc.
466
- - **Realistic workflows**: Real enterprise software
467
- - **Requires setup**: Enterprise software instances
468
-
469
- **Use Case**: Evaluate on business automation tasks
470
-
471
- ## Typical Training Pipeline
472
-
473
- ```python
474
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
475
-
476
- # Stage 1: Train on MiniWoB (simple tasks, fast)
477
- train_env = BrowserGymEnv.from_docker_image(
478
- "browsergym-env:latest",
479
- environment={
480
- "BROWSERGYM_BENCHMARK": "miniwob",
481
- "BROWSERGYM_TASK_NAME": "click-button",
482
- }
483
- )
484
-
485
- # Train your agent (RL, imitation learning, etc.)
486
- agent.train(train_env, num_episodes=10000)
487
- train_env.close()
488
-
489
- # Stage 2: Evaluate on WebArena (complex tasks, realistic)
490
- eval_env = BrowserGymEnv.from_docker_image(
491
- "browsergym-env:latest",
492
- environment={
493
- "BROWSERGYM_BENCHMARK": "webarena",
494
- "BROWSERGYM_TASK_NAME": "0",
495
- # ... WebArena URLs
496
- }
497
- )
498
-
499
- # Test performance
500
- success_rate = agent.evaluate(eval_env, num_tasks=812)
501
- print(f"WebArena Success Rate: {success_rate:.2%}")
502
- eval_env.close()
503
- ```
504
-
505
- ## Development & Testing
506
-
507
- ### Running Tests
508
-
509
- ```bash
510
- # From the OpenEnv repository root
511
- pytest tests/envs/test_browsergym_env.py
512
- ```
513
-
514
- ### Local Development
515
-
516
- ```bash
517
- # Install in development mode
518
- cd /path/to/OpenEnv
519
- pip install -e .
520
-
521
- # Install BrowserGym
522
- pip install browsergym browsergym-miniwob browsergym-webarena
523
-
524
- # Run the server locally
525
- cd src/envs/browsergym_env/server
526
- export BROWSERGYM_BENCHMARK=miniwob
527
- export BROWSERGYM_TASK_NAME=click-test
528
- python app.py
529
- ```
530
-
531
- ## Project Structure
532
-
533
- ```
534
- browsergym_env/
535
- ├── __init__.py # Module exports
536
- ├── models.py # Action, Observation, State dataclasses
537
- ├── client.py # HTTPEnvClient implementation
538
- ├── README.md # This file
539
- └── server/
540
- ├── __init__.py
541
- ├── app.py # FastAPI application
542
- ├── browsergym_environment.py # Environment implementation
543
- ├── Dockerfile # Container specification
544
- └── requirements.txt # Python dependencies
545
- ```
546
-
547
- ## References
548
-
549
- - [BrowserGym GitHub](https://github.com/ServiceNow/BrowserGym)
550
- - [MiniWoB++ Paper](https://arxiv.org/abs/1802.08802)
551
- - [WebArena Paper](https://arxiv.org/abs/2307.13854)
552
- - [WebArena Website](https://webarena.dev/)
553
- - [VisualWebArena Paper](https://jykoh.com/vwa)
554
- - [OpenEnv Documentation](https://github.com/meta-pytorch/OpenEnv)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/browsergym_env/__init__.py DELETED
@@ -1,72 +0,0 @@
1
- """BrowserGym Environment for OpenEnv.
2
-
3
- BrowserGym is a unified framework for web-based agent tasks that provides
4
- access to multiple benchmarks under a single Gymnasium-compatible API.
5
-
6
- Included Benchmarks:
7
- - **MiniWoB++**: 100+ simple web tasks for training (no external infrastructure!)
8
- - **WebArena**: 812 realistic evaluation tasks (requires backend setup)
9
- - **VisualWebArena**: Visual web navigation tasks
10
- - **WorkArena**: Enterprise task automation
11
-
12
- Key Features:
13
- - Unified API across all benchmarks
14
- - Gymnasium-compatible interface
15
- - Support for multiple observation types (text, visual, DOM)
16
- - Action spaces for natural language commands
17
- - Perfect for training (MiniWoB) and evaluation (WebArena)
18
-
19
- Training Example (MiniWoB - works immediately):
20
- ```python
21
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
22
-
23
- # Create training environment - no backend setup needed!
24
- env = BrowserGymEnv.from_docker_image(
25
- "browsergym-env:latest",
26
- environment={
27
- "BROWSERGYM_BENCHMARK": "miniwob",
28
- "BROWSERGYM_TASK_NAME": "click-test",
29
- }
30
- )
31
-
32
- # Train your agent
33
- for episode in range(1000):
34
- result = env.reset()
35
- while not result.done:
36
- action = agent.get_action(result.observation)
37
- result = env.step(action)
38
-
39
- env.close()
40
- ```
41
-
42
- Evaluation Example (WebArena - requires backend):
43
- ```python
44
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
45
-
46
- # Create evaluation environment
47
- env = BrowserGymEnv.from_docker_image(
48
- "browsergym-env:latest",
49
- environment={
50
- "BROWSERGYM_BENCHMARK": "webarena",
51
- "BROWSERGYM_TASK_NAME": "0",
52
- "SHOPPING": "http://your-server:7770",
53
- # ... other backend URLs
54
- }
55
- )
56
-
57
- # Evaluate your trained agent
58
- result = env.reset()
59
- # ... run evaluation
60
- env.close()
61
- ```
62
- """
63
-
64
- from .client import BrowserGymEnv
65
- from .models import BrowserGymAction, BrowserGymObservation, BrowserGymState
66
-
67
- __all__ = [
68
- "BrowserGymEnv",
69
- "BrowserGymAction",
70
- "BrowserGymObservation",
71
- "BrowserGymState",
72
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/browsergym_env/client.py DELETED
@@ -1,123 +0,0 @@
1
- """HTTP client for the BrowserGym environment."""
2
-
3
- from typing import Any, Dict
4
-
5
- from openenv_core.http_env_client import HTTPEnvClient, StepResult
6
- from browsergym_env.models import (
7
- BrowserGymAction,
8
- BrowserGymObservation,
9
- BrowserGymState,
10
- )
11
-
12
-
13
- class BrowserGymEnv(HTTPEnvClient[BrowserGymAction, BrowserGymObservation]):
14
- """Client for interacting with the BrowserGym environment over HTTP.
15
-
16
- BrowserGym provides unified access to multiple web navigation benchmarks:
17
- - MiniWoB++: 100+ training tasks (no external infrastructure needed!)
18
- - WebArena: 812 evaluation tasks (requires backend setup)
19
- - VisualWebArena: Visual navigation tasks
20
- - WorkArena: Enterprise automation tasks
21
-
22
- Example usage for TRAINING (MiniWoB - works out of the box):
23
- ```python
24
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
25
-
26
- # Create environment for MiniWoB training task
27
- env = BrowserGymEnv.from_docker_image(
28
- "browsergym-env:latest",
29
- environment={
30
- "BROWSERGYM_BENCHMARK": "miniwob",
31
- "BROWSERGYM_TASK_NAME": "click-test",
32
- }
33
- )
34
-
35
- # Reset and get initial observation
36
- result = env.reset()
37
- print(f"Task: {result.observation.goal}")
38
- print(f"Page: {result.observation.text[:200]}")
39
-
40
- # Take actions
41
- action = BrowserGymAction(action_str="click('Submit button')")
42
- result = env.step(action)
43
- print(f"Reward: {result.reward}")
44
- print(f"Done: {result.done}")
45
-
46
- env.close()
47
- ```
48
-
49
- Example usage for EVALUATION (WebArena - requires backend):
50
- ```python
51
- from envs.browsergym_env import BrowserGymEnv, BrowserGymAction
52
-
53
- # Create environment for WebArena evaluation
54
- env = BrowserGymEnv.from_docker_image(
55
- "browsergym-env:latest",
56
- environment={
57
- "BROWSERGYM_BENCHMARK": "webarena",
58
- "BROWSERGYM_TASK_NAME": "0", # Task 0
59
- # WebArena backend URLs
60
- "SHOPPING": "http://your-server:7770",
61
- "GITLAB": "http://your-server:8023",
62
- # ... other URLs
63
- }
64
- )
65
-
66
- result = env.reset()
67
- # ... interact with environment
68
- env.close()
69
- ```
70
-
71
- Available benchmarks:
72
- - miniwob: MiniWoB++ tasks (training, no setup required)
73
- - webarena: WebArena tasks (evaluation, requires backend)
74
- - visualwebarena: Visual WebArena tasks (evaluation, requires backend)
75
- - workarena: WorkArena tasks (evaluation, requires backend)
76
- """
77
-
78
- def _step_payload(self, action: BrowserGymAction) -> Dict[str, Any]:
79
- """Convert a BrowserGymAction to the JSON payload for the server."""
80
- return {
81
- "action_str": action.action_str,
82
- "metadata": action.metadata,
83
- }
84
-
85
- def _parse_result(
86
- self, payload: Dict[str, Any]
87
- ) -> StepResult[BrowserGymObservation]:
88
- """Parse the server response into a StepResult."""
89
- obs_data = payload.get("observation", {})
90
-
91
- observation = BrowserGymObservation(
92
- text=obs_data.get("text", ""),
93
- url=obs_data.get("url", ""),
94
- screenshot=obs_data.get("screenshot"),
95
- goal=obs_data.get("goal", ""),
96
- axtree_txt=obs_data.get("axtree_txt", ""),
97
- pruned_html=obs_data.get("pruned_html", ""),
98
- error=obs_data.get("error", ""),
99
- last_action_error=obs_data.get("last_action_error", False),
100
- done=payload.get("done", False),
101
- reward=payload.get("reward"),
102
- metadata=obs_data.get("metadata", {}),
103
- )
104
-
105
- return StepResult(
106
- observation=observation,
107
- reward=payload.get("reward"),
108
- done=payload.get("done", False),
109
- )
110
-
111
- def _parse_state(self, payload: Dict[str, Any]) -> BrowserGymState:
112
- """Parse the server state response into a BrowserGymState object."""
113
- return BrowserGymState(
114
- episode_id=payload.get("episode_id"),
115
- step_count=payload.get("step_count", 0),
116
- benchmark=payload.get("benchmark", ""),
117
- task_name=payload.get("task_name", ""),
118
- task_id=payload.get("task_id"),
119
- goal=payload.get("goal", ""),
120
- current_url=payload.get("current_url", ""),
121
- max_steps=payload.get("max_steps"),
122
- cum_reward=payload.get("cum_reward", 0.0),
123
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/browsergym_env/models.py DELETED
@@ -1,92 +0,0 @@
1
- """Data models for the BrowserGym environment.
2
-
3
- BrowserGym is a unified framework for web-based agent tasks, combining multiple
4
- benchmarks including MiniWoB (training), WebArena (evaluation), VisualWebArena,
5
- and more under a single Gymnasium-compatible API.
6
- """
7
-
8
- from dataclasses import dataclass
9
- from typing import List, Optional
10
-
11
- from openenv_core.env_server.types import Action, Observation, State
12
-
13
-
14
- @dataclass(kw_only=True)
15
- class BrowserGymAction(Action):
16
- """Action to be executed in the BrowserGym environment.
17
-
18
- BrowserGym supports high-level natural language actions that can be parsed
19
- into browser operations.
20
-
21
- Example actions:
22
- - "click('Submit button')"
23
- - "fill('username', 'john@example.com')"
24
- - "goto('https://example.com')"
25
- - "scroll(down)"
26
- - "send_keys('Enter')"
27
- """
28
-
29
- action_str: str
30
- """Natural language action string (e.g., "click('Submit')")"""
31
-
32
-
33
- @dataclass(kw_only=True)
34
- class BrowserGymObservation(Observation):
35
- """Observation returned from the BrowserGym environment.
36
-
37
- Contains multiple observation modalities including text (accessibility tree
38
- or DOM), visual (screenshot), and page metadata.
39
- """
40
-
41
- text: str = ""
42
- """Text representation of the page (accessibility tree or DOM)"""
43
-
44
- url: str = ""
45
- """Current URL of the page"""
46
-
47
- screenshot: Optional[List[List[List[int]]]] = None
48
- """Screenshot as numpy array [height, width, channels] (if visual observation enabled)"""
49
-
50
- goal: str = ""
51
- """Task goal/instruction for the current episode"""
52
-
53
- axtree_txt: str = ""
54
- """Full accessibility tree as text"""
55
-
56
- pruned_html: str = ""
57
- """Pruned HTML content (interactive elements only)"""
58
-
59
- error: str = ""
60
- """Error message if action execution failed"""
61
-
62
- last_action_error: bool = False
63
- """Whether the last action resulted in an error"""
64
-
65
-
66
- @dataclass
67
- class BrowserGymState(State):
68
- """State of the BrowserGym environment.
69
-
70
- Tracks the current benchmark, task, and progress through an episode.
71
- """
72
-
73
- benchmark: str = ""
74
- """Benchmark name (e.g., 'miniwob', 'webarena', 'visualwebarena')"""
75
-
76
- task_name: str = ""
77
- """Specific task within the benchmark (e.g., 'click-test', 'click-button')"""
78
-
79
- task_id: Optional[str] = None
80
- """Task ID for evaluation benchmarks (e.g., WebArena task number)"""
81
-
82
- goal: str = ""
83
- """Task goal/instruction"""
84
-
85
- current_url: str = ""
86
- """Current URL of the active page"""
87
-
88
- max_steps: Optional[int] = None
89
- """Maximum steps allowed for this task"""
90
-
91
- cum_reward: float = 0.0
92
- """Cumulative reward for the current episode"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
openenv/src/envs/browsergym_env/openenv.yaml DELETED
@@ -1,5 +0,0 @@
1
- name: browsergym_env
2
- version: "0.1.0"
3
- description: "BrowserGym environment for web automation tasks using Playwright"
4
- action: BrowserGymAction
5
- observation: BrowserGymObservation
 
 
 
 
 
 
openenv/src/envs/browsergym_env/pyproject.toml DELETED
@@ -1,39 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools>=45", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "openenv-browsergym_env"
7
- version = "0.1.0"
8
- description = "BrowserGym Environment for OpenEnv - Web automation using Playwright"
9
- requires-python = ">=3.10"
10
- dependencies = [
11
- "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git#subdirectory=src/core",
12
- "fastapi>=0.104.0",
13
- "uvicorn>=0.24.0",
14
- "pydantic>=2.0.0",
15
- "requests>=2.25.0",
16
- "browsergym-core>=0.2.0",
17
- "browsergym-miniwob>=0.2.0",
18
- "browsergym-webarena>=0.2.0",
19
- "gymnasium>=0.29.0",
20
- "playwright>=1.40.0",
21
- "Pillow>=10.0.0",
22
- ]
23
-
24
- [project.optional-dependencies]
25
- dev = [
26
- "pytest>=8.0.0",
27
- "pytest-cov>=4.0.0",
28
- "ipykernel>=6.29.5",
29
- ]
30
-
31
- [project.scripts]
32
- server = "browsergym_env.server.app:main"
33
-
34
- [tool.setuptools]
35
- packages = ["browsergym_env", "browsergym_env.server"]
36
- package-dir = { "browsergym_env" = ".", "browsergym_env.server" = "server" }
37
-
38
- [tool.setuptools.package-data]
39
- browsergym_env = ["**/*.yaml", "**/*.yml", "**/*.md"]