Spaces:
Runtime error
Runtime error
Paulito Palmes, PhD commited on
Commit ·
dae63d1
1
Parent(s): 0ee5d04
init
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Dockerfile +3 -1
- openenv/CODE_OF_CONDUCT.md +0 -80
- openenv/CONTRIBUTING.md +0 -39
- openenv/LICENSE +0 -28
- openenv/README.md +0 -299
- openenv/pyproject.toml +0 -57
- openenv/scripts/CONVERT.md +0 -491
- openenv/scripts/convert_env.sh +0 -502
- openenv/scripts/deploy_to_hf.sh +0 -615
- openenv/scripts/manage_hf_collection.py +0 -296
- openenv/scripts/prepare_hf_deployment.sh +0 -170
- openenv/scripts/setup_shared_gitea.sh +0 -83
- openenv/src/__init__.py +0 -7
- openenv/src/core/README.md +0 -180
- openenv/src/core/__init__.py +0 -19
- openenv/src/core/client_types.py +0 -22
- openenv/src/core/containers/__init__.py +0 -7
- openenv/src/core/containers/images/Dockerfile +0 -61
- openenv/src/core/containers/images/README.md +0 -92
- openenv/src/core/containers/runtime/__init__.py +0 -15
- openenv/src/core/containers/runtime/providers.py +0 -293
- openenv/src/core/containers/test_local_docker_provider.py +0 -258
- openenv/src/core/env_server/__init__.py +0 -35
- openenv/src/core/env_server/base_transforms.py +0 -29
- openenv/src/core/env_server/http_server.py +0 -257
- openenv/src/core/env_server/interfaces.py +0 -118
- openenv/src/core/env_server/types.py +0 -57
- openenv/src/core/env_server/web_interface.py +0 -1613
- openenv/src/core/http_env_client.py +0 -203
- openenv/src/core/tools/__init__.py +0 -16
- openenv/src/core/tools/git_server_client.py +0 -362
- openenv/src/core/tools/local_python_executor.py +0 -152
- openenv/src/core/uv.lock +0 -0
- openenv/src/envs/README.md +0 -382
- openenv/src/envs/atari_env/README.md +0 -396
- openenv/src/envs/atari_env/__init__.py +0 -31
- openenv/src/envs/atari_env/client.py +0 -119
- openenv/src/envs/atari_env/models.py +0 -86
- openenv/src/envs/atari_env/server/Dockerfile +0 -43
- openenv/src/envs/atari_env/server/__init__.py +0 -15
- openenv/src/envs/atari_env/server/app.py +0 -73
- openenv/src/envs/atari_env/server/atari_environment.py +0 -245
- openenv/src/envs/atari_env/server/requirements.txt +0 -3
- openenv/src/envs/atari_env/test_atari_docker.sh +0 -333
- openenv/src/envs/browsergym_env/README.md +0 -554
- openenv/src/envs/browsergym_env/__init__.py +0 -72
- openenv/src/envs/browsergym_env/client.py +0 -123
- openenv/src/envs/browsergym_env/models.py +0 -92
- openenv/src/envs/browsergym_env/openenv.yaml +0 -5
- 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 |
-
[](https://pypi.org/project/openenv-core/)
|
| 6 |
-
[](https://discord.gg/YsTYBh6PD9)
|
| 7 |
-
[](https://colab.research.google.com/github/meta-pytorch/OpenEnv/blob/main/examples/OpenEnv_Tutorial.ipynb)
|
| 8 |
-
[](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"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|