Spaces:
Running
Running
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +12 -0
- .gitignore +1 -0
- Dockerfile +128 -0
- README.md +595 -5
- __init__.py +12 -0
- assets/01-messages.mov +3 -0
- assets/02-editor.mov +3 -0
- assets/03-calendar.mov +3 -0
- assets/04-todo.mov +3 -0
- assets/OpenApps_OpenEnv_RL.png +3 -0
- assets/demo-showcase.html +624 -0
- assets/openapps-demo.gif +3 -0
- client.py +139 -0
- envs/openapp_env/.gitignore +1 -0
- envs/openapp_env/README.md +584 -0
- envs/openapp_env/__init__.py +12 -0
- envs/openapp_env/assets/01-messages.mov +3 -0
- envs/openapp_env/assets/02-editor.mov +3 -0
- envs/openapp_env/assets/03-calendar.mov +3 -0
- envs/openapp_env/assets/04-todo.mov +3 -0
- envs/openapp_env/assets/OpenApps_OpenEnv_RL.png +3 -0
- envs/openapp_env/assets/demo-showcase.html +624 -0
- envs/openapp_env/assets/openapps-demo.gif +3 -0
- envs/openapp_env/client.py +139 -0
- envs/openapp_env/example_usage.py +279 -0
- envs/openapp_env/models.py +86 -0
- envs/openapp_env/openenv.yaml +6 -0
- envs/openapp_env/pyproject.toml +58 -0
- envs/openapp_env/server/Dockerfile +128 -0
- envs/openapp_env/server/__init__.py +7 -0
- envs/openapp_env/server/app.py +59 -0
- envs/openapp_env/server/openapp_environment.py +659 -0
- envs/openapp_env/server/start.sh +44 -0
- envs/openapp_env/test_openapp_env.py +144 -0
- example_usage.py +279 -0
- models.py +86 -0
- openenv.yaml +6 -0
- pyproject.toml +58 -0
- server/Dockerfile +128 -0
- server/__init__.py +7 -0
- server/app.py +59 -0
- server/openapp_environment.py +659 -0
- server/start.sh +44 -0
- src/__init__.py +7 -0
- src/openenv.egg-info/PKG-INFO +337 -0
- src/openenv.egg-info/SOURCES.txt +142 -0
- src/openenv.egg-info/dependency_links.txt +1 -0
- src/openenv.egg-info/entry_points.txt +2 -0
- src/openenv.egg-info/requires.txt +32 -0
- src/openenv.egg-info/top_level.txt +2 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,15 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/01-messages.mov filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/02-editor.mov filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/03-calendar.mov filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
assets/04-todo.mov filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
assets/OpenApps_OpenEnv_RL.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
assets/openapps-demo.gif filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
envs/openapp_env/assets/01-messages.mov filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
envs/openapp_env/assets/02-editor.mov filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
envs/openapp_env/assets/03-calendar.mov filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
envs/openapp_env/assets/04-todo.mov filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
envs/openapp_env/assets/OpenApps_OpenEnv_RL.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
envs/openapp_env/assets/openapps-demo.gif filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
OpenApps
|
Dockerfile
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Dockerfile for OpenApp Environment
|
| 8 |
+
# This image provides OpenApps web application simulation for UI agent training
|
| 9 |
+
#
|
| 10 |
+
# This Dockerfile works for both local builds and HuggingFace Spaces deployment:
|
| 11 |
+
# - Local build: cd envs/openapp_env && docker build -t openapp-env:latest -f server/Dockerfile .
|
| 12 |
+
# - HuggingFace: Automatically deployed via `openenv push`
|
| 13 |
+
#
|
| 14 |
+
# Run with web interface:
|
| 15 |
+
# docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true openapp-env:latest
|
| 16 |
+
|
| 17 |
+
FROM python:3.11-slim
|
| 18 |
+
|
| 19 |
+
# Set metadata
|
| 20 |
+
LABEL maintainer="OpenEnv Team"
|
| 21 |
+
LABEL description="OpenApp Environment with BrowserGym for UI agent training"
|
| 22 |
+
LABEL org.opencontainers.image.source="https://github.com/meta-pytorch/OpenEnv"
|
| 23 |
+
|
| 24 |
+
# Set working directory
|
| 25 |
+
WORKDIR /app/env
|
| 26 |
+
|
| 27 |
+
# Install system dependencies
|
| 28 |
+
# - git: required to clone OpenApps from GitHub
|
| 29 |
+
# - curl: for healthcheck
|
| 30 |
+
# - Playwright/BrowserGym dependencies: fonts, libraries for browser automation
|
| 31 |
+
RUN apt-get update && \
|
| 32 |
+
apt-get install -y --no-install-recommends \
|
| 33 |
+
git \
|
| 34 |
+
curl \
|
| 35 |
+
ca-certificates \
|
| 36 |
+
wget \
|
| 37 |
+
gnupg \
|
| 38 |
+
# Playwright/Chromium dependencies
|
| 39 |
+
libnss3 \
|
| 40 |
+
libnspr4 \
|
| 41 |
+
libatk1.0-0 \
|
| 42 |
+
libatk-bridge2.0-0 \
|
| 43 |
+
libcups2 \
|
| 44 |
+
libdrm2 \
|
| 45 |
+
libdbus-1-3 \
|
| 46 |
+
libxkbcommon0 \
|
| 47 |
+
libxcomposite1 \
|
| 48 |
+
libxdamage1 \
|
| 49 |
+
libxfixes3 \
|
| 50 |
+
libxrandr2 \
|
| 51 |
+
libgbm1 \
|
| 52 |
+
libasound2 \
|
| 53 |
+
libpango-1.0-0 \
|
| 54 |
+
libcairo2 \
|
| 55 |
+
libatspi2.0-0 \
|
| 56 |
+
libxshmfence1 \
|
| 57 |
+
fonts-liberation \
|
| 58 |
+
libappindicator3-1 \
|
| 59 |
+
xdg-utils && \
|
| 60 |
+
rm -rf /var/lib/apt/lists/*
|
| 61 |
+
|
| 62 |
+
# Set environment variables
|
| 63 |
+
ENV PYTHONUNBUFFERED=1
|
| 64 |
+
|
| 65 |
+
# Set working directory
|
| 66 |
+
WORKDIR /app/env
|
| 67 |
+
|
| 68 |
+
# Copy environment files
|
| 69 |
+
# Context is always the env directory (envs/openapp_env/)
|
| 70 |
+
# - GitHub Actions: uses context: envs/openapp_env
|
| 71 |
+
# - HuggingFace: openenv push uploads env dir as context
|
| 72 |
+
COPY . /app/env
|
| 73 |
+
|
| 74 |
+
# Install OpenApps FIRST to establish openai<2 (required by agentlab)
|
| 75 |
+
# This must happen before openenv-core to avoid version conflict
|
| 76 |
+
WORKDIR /app
|
| 77 |
+
RUN git clone https://github.com/facebookresearch/OpenApps.git openapps && \
|
| 78 |
+
cd openapps && \
|
| 79 |
+
pip install --no-cache-dir -e .
|
| 80 |
+
|
| 81 |
+
# Verify OpenApps installation
|
| 82 |
+
RUN python -c "import open_apps; print('✓ OpenApps installed')"
|
| 83 |
+
|
| 84 |
+
# Install openenv-core from GitHub with --no-deps to avoid openai>=2.7.2 conflict
|
| 85 |
+
# Then install only the server dependencies (no openai needed for server)
|
| 86 |
+
RUN pip install --no-cache-dir --no-deps "openenv-core[core]>=0.2.1" && \
|
| 87 |
+
pip install --no-cache-dir fastapi pydantic uvicorn requests websockets
|
| 88 |
+
|
| 89 |
+
# Install openapp_env and remaining dependencies
|
| 90 |
+
WORKDIR /app/env
|
| 91 |
+
RUN pip install --no-cache-dir -e .
|
| 92 |
+
|
| 93 |
+
# Verify installation
|
| 94 |
+
RUN python -c "import openapp_env; print('✓ openapp_env installed')" && \
|
| 95 |
+
python -c "import openapp_env.server.app; print('✓ openapp_env.server.app importable')"
|
| 96 |
+
|
| 97 |
+
# Install Playwright browsers (Chromium for BrowserGym)
|
| 98 |
+
# We already installed system dependencies above, so just install the browser
|
| 99 |
+
RUN playwright install chromium
|
| 100 |
+
|
| 101 |
+
# Copy startup script
|
| 102 |
+
WORKDIR /app/env
|
| 103 |
+
COPY server/start.sh /app/start.sh
|
| 104 |
+
RUN chmod +x /app/start.sh
|
| 105 |
+
|
| 106 |
+
# OpenApp-specific environment variables (can be overridden at runtime)
|
| 107 |
+
ENV OPENAPPS_URL=http://localhost:5001
|
| 108 |
+
ENV OPENAPPS_PORT=5001
|
| 109 |
+
ENV OPENAPP_HEADLESS=true
|
| 110 |
+
ENV OPENAPP_MAX_STEPS=50
|
| 111 |
+
|
| 112 |
+
# Hydra requires USER environment variable
|
| 113 |
+
ENV USER=root
|
| 114 |
+
|
| 115 |
+
# Enable web interface by default (set to false to disable)
|
| 116 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 117 |
+
|
| 118 |
+
# Expose ports (8000 for FastAPI, 5001 for OpenApps)
|
| 119 |
+
EXPOSE 8000 5001
|
| 120 |
+
|
| 121 |
+
# Health check
|
| 122 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
| 123 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 124 |
+
|
| 125 |
+
# Run the startup script that launches both OpenApps server and FastAPI server
|
| 126 |
+
# Web interface will be available at /web if ENABLE_WEB_INTERFACE=true
|
| 127 |
+
# API documentation available at /docs
|
| 128 |
+
CMD ["/app/start.sh"]
|
README.md
CHANGED
|
@@ -1,10 +1,600 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: OpenApp Environment Server
|
| 3 |
+
emoji: 🌐
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- OpenApps
|
| 13 |
+
- BrowserGym
|
| 14 |
+
- UI-Agents
|
| 15 |
+
- Reinforcement-Learning
|
| 16 |
---
|
| 17 |
|
| 18 |
+
## Hugging Face Space Deployment
|
| 19 |
+
|
| 20 |
+
This Space is built from OpenEnv environment `openapp_env`.
|
| 21 |
+
|
| 22 |
+
- Space URL: `https://huggingface.co/spaces/openenv/openapp_env-v2-1-0`
|
| 23 |
+
- OpenEnv pinned ref: `v2.1.0`
|
| 24 |
+
- Hub tag: `openenv`
|
| 25 |
+
|
| 26 |
+
### Connecting from Code
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
from envs.openapp_env import Env
|
| 30 |
+
|
| 31 |
+
env = Env(base_url="https://huggingface.co/spaces/openenv/openapp_env-v2-1-0")
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
<!--
|
| 35 |
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 36 |
+
All rights reserved.
|
| 37 |
+
|
| 38 |
+
This source code is licensed under the BSD-style license found in the
|
| 39 |
+
LICENSE file in the root directory of this source tree.
|
| 40 |
+
-->
|
| 41 |
+
|
| 42 |
+
<div align="center">
|
| 43 |
+
|
| 44 |
+
# OpenApp Environment
|
| 45 |
+
|
| 46 |
+
<img src="assets/OpenApps_OpenEnv_RL.png" alt="OpenApps Environment" width="800"/>
|
| 47 |
+
|
| 48 |
+
*A web application simulation environment for OpenEnv that wraps the [OpenApps](https://github.com/facebookresearch/OpenApps) framework and BrowserGym.*
|
| 49 |
+
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
## Overview
|
| 53 |
+
|
| 54 |
+
The OpenApp environment provides a simulated web application ecosystem where agents can interact with various apps (calendar, todo, messenger, maps) using browser-based actions.
|
| 55 |
+
|
| 56 |
+
<div align="center">
|
| 57 |
+
<img src="assets/openapps-demo.gif" alt="OpenApps Demo" width="800"/>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
This environment is ideal for:
|
| 61 |
+
|
| 62 |
+
- Training and evaluating UI agents
|
| 63 |
+
- Testing web automation strategies
|
| 64 |
+
- Researching human-computer interaction
|
| 65 |
+
- Developing multimodal agents
|
| 66 |
+
|
| 67 |
+
## Features
|
| 68 |
+
|
| 69 |
+
- **Multiple Apps**: Interact with calendar, todo list, messenger, and map applications
|
| 70 |
+
- **Browser-Based Actions**: Click, fill forms, navigate, scroll, and more
|
| 71 |
+
- **Task-Based Evaluation**: Optional task goals with automatic reward calculation
|
| 72 |
+
- **Configurable**: Customize app configurations and behavior
|
| 73 |
+
- **BrowserGym Integration**: Built on top of BrowserGym for robust browser interaction
|
| 74 |
+
|
| 75 |
+
## Directory Structure
|
| 76 |
+
|
| 77 |
+
```
|
| 78 |
+
openapp_env/
|
| 79 |
+
├── __init__.py # Package exports
|
| 80 |
+
├── client.py # HTTP client for connecting to OpenApp
|
| 81 |
+
├── models.py # Data models for actions and observations
|
| 82 |
+
├── pyproject.toml # Package dependencies and configuration
|
| 83 |
+
├── openenv.yaml # OpenEnv environment configuration
|
| 84 |
+
├── test_openapp_env.py # Unit tests for environment structure
|
| 85 |
+
├── README.md # This file
|
| 86 |
+
├── IMPLEMENTATION.md # Implementation details and design decisions
|
| 87 |
+
├── example_usage.py # Basic usage example (legacy)
|
| 88 |
+
├── assets/ # Images and media
|
| 89 |
+
│ ├── OpenApps_OpenEnv_RL.png # Environment overview diagram
|
| 90 |
+
│ └── openapps-demo.gif # Demo animation
|
| 91 |
+
└── server/ # Server-side environment implementation
|
| 92 |
+
├── __init__.py
|
| 93 |
+
├── app.py # FastAPI server application
|
| 94 |
+
├── openapp_environment.py # Core environment logic (BrowserGym + OpenApps)
|
| 95 |
+
├── Dockerfile # Docker image definition
|
| 96 |
+
└── start.sh # Container startup script (runs both servers)
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
**Key Components:**
|
| 100 |
+
|
| 101 |
+
- **client.py**: `OpenAppEnv` class that extends `HTTPEnvClient` for remote environment interaction
|
| 102 |
+
- **models.py**: `OpenAppAction` and `OpenAppObservation` dataclasses with validation
|
| 103 |
+
- **server/openapp_environment.py**: `OpenAppEnvironment` class that wraps BrowserGym and OpenApps
|
| 104 |
+
- **server/app.py**: FastAPI server that exposes the environment via HTTP endpoints
|
| 105 |
+
- **server/Dockerfile**: Self-contained Docker image with OpenApps server and FastAPI server
|
| 106 |
+
- **server/start.sh**: Startup script that launches both OpenApps (port 5001) and FastAPI (port 8000)
|
| 107 |
+
|
| 108 |
+
## Installation
|
| 109 |
+
|
| 110 |
+
There are two ways to use the OpenApp environment: **Docker mode** (recommended, fully self-contained) or **Local mode** (requires manual server setup).
|
| 111 |
+
|
| 112 |
+
### Option 1: Docker Mode (Recommended)
|
| 113 |
+
|
| 114 |
+
Docker mode is fully self-contained and handles all dependencies automatically. No local installation required!
|
| 115 |
+
|
| 116 |
+
**Step 1: Build the Docker image**
|
| 117 |
+
|
| 118 |
+
The Docker image can be built in standalone mode using only public base images:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
# Build from the environment directory
|
| 122 |
+
cd envs/openapp_env
|
| 123 |
+
docker build -t openapp-env:latest -f server/Dockerfile .
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
**Note for Meta/Corporate Networks:** If you're behind a proxy (HTTP_PROXY/HTTPS_PROXY set), you may need to bypass it for localhost connections:
|
| 127 |
+
```bash
|
| 128 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 129 |
+
cd envs/openapp_env
|
| 130 |
+
docker build -t openapp-env:latest -f server/Dockerfile .
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
**What gets installed in Docker:**
|
| 134 |
+
- **OpenEnv core**: Installed as a dependency
|
| 135 |
+
- **OpenApps**: Cloned from GitHub and installed (runs server inside container)
|
| 136 |
+
- **Core packages**: FastAPI, Uvicorn, Pydantic, Requests (from pyproject.toml)
|
| 137 |
+
- **BrowserGym**: For browser automation
|
| 138 |
+
- **Playwright**: Chromium browser for UI interaction
|
| 139 |
+
- **Web interface support**: Enabled by default via `ENABLE_WEB_INTERFACE=true`
|
| 140 |
+
|
| 141 |
+
**How Docker mode works:**
|
| 142 |
+
The Docker container runs TWO services automatically:
|
| 143 |
+
1. **OpenApps server** (port 5001) - Provides the web applications (calendar, todo, messenger, maps)
|
| 144 |
+
2. **FastAPI server** (port 8000) - Exposes the OpenEnv HTTP API
|
| 145 |
+
|
| 146 |
+
Both servers start automatically when the container launches. You only interact with port 8000.
|
| 147 |
+
|
| 148 |
+
**Build details:**
|
| 149 |
+
- Base image: `python:3.11-slim` (public)
|
| 150 |
+
- Installation: Uses `pip install -e .` with pyproject.toml
|
| 151 |
+
- System deps: Playwright/Chromium dependencies for browser automation
|
| 152 |
+
- Size: ~5.7GB (includes Chromium browser and all dependencies)
|
| 153 |
+
|
| 154 |
+
**Step 2: Run the example**
|
| 155 |
+
```bash
|
| 156 |
+
# For Meta/Corporate Networks with proxy, also set NO_PROXY:
|
| 157 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 158 |
+
|
| 159 |
+
python examples/openapp_example.py --mode docker
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
**Note:** For Docker mode, you only need Python installed locally to run the example script. All environment dependencies are inside the Docker container.
|
| 163 |
+
|
| 164 |
+
### Option 2: Local Mode
|
| 165 |
+
|
| 166 |
+
Local mode requires manual setup of the OpenApps server. This mode is useful for development or when you need to customize the OpenApps configuration.
|
| 167 |
+
|
| 168 |
+
**Prerequisites:**
|
| 169 |
+
- Python 3.11+ installed
|
| 170 |
+
- UV package manager (recommended) or pip
|
| 171 |
+
|
| 172 |
+
**Step 1: Install openapp_env**
|
| 173 |
+
```bash
|
| 174 |
+
cd envs/openapp_env
|
| 175 |
+
pip install -e .
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
This installs the environment package along with dependencies (BrowserGym, Playwright, etc.).
|
| 179 |
+
|
| 180 |
+
**Step 2: Install Playwright browsers**
|
| 181 |
+
```bash
|
| 182 |
+
playwright install chromium
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
**Step 3: Clone and set up OpenApps** (for running the server)
|
| 186 |
+
```bash
|
| 187 |
+
# Clone OpenApps repository
|
| 188 |
+
git clone https://github.com/facebookresearch/OpenApps.git
|
| 189 |
+
cd OpenApps
|
| 190 |
+
|
| 191 |
+
# Install dependencies
|
| 192 |
+
uv sync # or: pip install -e .
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
**Why do I need the OpenApps repository?**
|
| 196 |
+
|
| 197 |
+
The OpenApps Python package (installed via pip in Step 1) provides the library code, but the repository contains:
|
| 198 |
+
- `launch.py` - The server startup script
|
| 199 |
+
- `config/` - Hydra configuration files
|
| 200 |
+
- Application templates and assets
|
| 201 |
+
|
| 202 |
+
In Docker mode, all of this is included in the container, so you don't need to clone anything.
|
| 203 |
+
|
| 204 |
+
## Quick Start
|
| 205 |
+
|
| 206 |
+
### Running with Docker (Recommended)
|
| 207 |
+
|
| 208 |
+
Docker mode is the easiest way - everything is automated:
|
| 209 |
+
|
| 210 |
+
```bash
|
| 211 |
+
# For Meta/Corporate networks with proxy, set NO_PROXY first:
|
| 212 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 213 |
+
|
| 214 |
+
# Run the example
|
| 215 |
+
python examples/openapp_example.py --mode docker
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
The Docker container automatically:
|
| 219 |
+
- Starts the OpenApps server (port 5001)
|
| 220 |
+
- Starts the FastAPI server (port 8000)
|
| 221 |
+
- Manages both services for you
|
| 222 |
+
|
| 223 |
+
No manual server setup required!
|
| 224 |
+
|
| 225 |
+
**What happens inside the container:**
|
| 226 |
+
|
| 227 |
+
When you run `from_docker_image()`, the following happens automatically:
|
| 228 |
+
|
| 229 |
+
1. **Container Startup** (`/app/start.sh` runs):
|
| 230 |
+
```bash
|
| 231 |
+
# Launches OpenApps server in background
|
| 232 |
+
cd /app/openapps
|
| 233 |
+
python launch.py &
|
| 234 |
+
|
| 235 |
+
# Waits for port 5001 to be ready
|
| 236 |
+
# Then starts FastAPI server
|
| 237 |
+
uvicorn openapp_env.server.app:app --host 0.0.0.0 --port 8000
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
2. **Your client code** interacts only with port 8000:
|
| 241 |
+
```python
|
| 242 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 243 |
+
# Client -> FastAPI (port 8000) -> OpenApps (port 5001)
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
3. **On cleanup**, both servers are automatically stopped when the container is removed.
|
| 247 |
+
|
| 248 |
+
### Running Locally
|
| 249 |
+
|
| 250 |
+
For local usage, you need the OpenApps repository to run the server:
|
| 251 |
+
|
| 252 |
+
**Step 1: Clone OpenApps (if you haven't already)**
|
| 253 |
+
```bash
|
| 254 |
+
git clone https://github.com/facebookresearch/OpenApps.git
|
| 255 |
+
cd OpenApps
|
| 256 |
+
uv sync
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
**Step 2: Start OpenApps Server** (in terminal 1)
|
| 260 |
+
|
| 261 |
+
To run the server in **headless mode** (no browser window):
|
| 262 |
+
```bash
|
| 263 |
+
cd OpenApps # or wherever you cloned it
|
| 264 |
+
uv run launch.py
|
| 265 |
+
|
| 266 |
+
# or instead of the uv run you can use the Python command:
|
| 267 |
+
python OpenApps/launch.py
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
To run the server with **visible browser** for visualization:
|
| 271 |
+
```bash
|
| 272 |
+
cd OpenApps
|
| 273 |
+
python OpenApps/launch.py browsergym_env_args.headless=False
|
| 274 |
+
```
|
| 275 |
+
|
| 276 |
+
Wait for the server to start (you'll see "Port 5001 is available" or similar).
|
| 277 |
+
|
| 278 |
+
**Step 3: Run your code** (in terminal 2)
|
| 279 |
+
```bash
|
| 280 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 281 |
+
python examples/openapp_example.py --mode local
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
**Note:** The OpenApps Python package (installed via pip) provides the modules, but you need the full repository to run launch.py with its config files.
|
| 285 |
+
|
| 286 |
+
### Example Script
|
| 287 |
+
|
| 288 |
+
```bash
|
| 289 |
+
# Run with Docker (recommended)
|
| 290 |
+
python examples/openapp_example.py --mode docker
|
| 291 |
+
|
| 292 |
+
# Run locally (requires OpenApps server running)
|
| 293 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 294 |
+
python examples/openapp_example.py --mode local
|
| 295 |
+
|
| 296 |
+
# Show browser window to visualize agent actions
|
| 297 |
+
python examples/openapp_example.py --mode local --show-browser
|
| 298 |
+
|
| 299 |
+
# Run with custom number of steps
|
| 300 |
+
python examples/openapp_example.py --mode docker --num-steps 20
|
| 301 |
+
|
| 302 |
+
# See all options
|
| 303 |
+
python examples/openapp_example.py --help
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
### Visualizing Agent Interactions
|
| 307 |
+
|
| 308 |
+
There are multiple ways to see what the agent is doing:
|
| 309 |
+
|
| 310 |
+
**Option 1: Show Browser Window (Local Mode)**
|
| 311 |
+
|
| 312 |
+
The key is to start the OpenApps server with visualization enabled:
|
| 313 |
+
|
| 314 |
+
```bash
|
| 315 |
+
# Terminal 1: Start OpenApps server with visible browser
|
| 316 |
+
cd OpenApps
|
| 317 |
+
python OpenApps/launch.py browsergym_env_args.headless=False
|
| 318 |
+
|
| 319 |
+
# Terminal 2: Run your agent code
|
| 320 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 321 |
+
python examples/openapp_example.py --mode local
|
| 322 |
+
```
|
| 323 |
+
|
| 324 |
+
**Important:** The browser visualization is controlled by the OpenApps server, not the client. You must launch the server with `browsergym_env_args.headless=False` to see the browser window.
|
| 325 |
+
|
| 326 |
+
**Option 2: Access Web Interface Directly**
|
| 327 |
+
|
| 328 |
+
While the OpenApps server is running, open your browser to:
|
| 329 |
+
- Main page: `http://localhost:5001`
|
| 330 |
+
- Calendar: `http://localhost:5001/calendar`
|
| 331 |
+
- Todo: `http://localhost:5001/todo`
|
| 332 |
+
- Messenger: `http://localhost:5001/messages`
|
| 333 |
+
- Maps: `http://localhost:5001/maps`
|
| 334 |
+
|
| 335 |
+
**Option 3: Docker Web Interface**
|
| 336 |
+
|
| 337 |
+
When running in Docker mode, you can also access a web interface for manual testing:
|
| 338 |
+
|
| 339 |
+
```bash
|
| 340 |
+
# Start a container and keep it running
|
| 341 |
+
docker run -d -p 8000:8000 openapp-env:latest
|
| 342 |
+
|
| 343 |
+
# Access the web interface
|
| 344 |
+
# - Interactive UI: http://localhost:8000/web
|
| 345 |
+
# - API docs: http://localhost:8000/docs
|
| 346 |
+
# - OpenApps (internal): http://localhost:5001 (inside container)
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
**Note:** In Docker mode, the OpenApps server runs inside the container and is not directly accessible from your host machine. The FastAPI server at port 8000 acts as a proxy to interact with OpenApps.
|
| 350 |
+
|
| 351 |
+
### Basic Usage
|
| 352 |
+
|
| 353 |
+
```python
|
| 354 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 355 |
+
|
| 356 |
+
# Create environment from Docker image
|
| 357 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 358 |
+
|
| 359 |
+
# Reset to initial state
|
| 360 |
+
result = client.reset()
|
| 361 |
+
print(f"Starting URL: {result.observation.url}")
|
| 362 |
+
|
| 363 |
+
# Navigate to calendar app
|
| 364 |
+
result = client.step(OpenAppAction(
|
| 365 |
+
action_type="goto",
|
| 366 |
+
url="http://localhost:5001/calendar"
|
| 367 |
+
))
|
| 368 |
+
|
| 369 |
+
# Click on a button (example bid)
|
| 370 |
+
result = client.step(OpenAppAction(
|
| 371 |
+
action_type="click",
|
| 372 |
+
bid="add-event-btn"
|
| 373 |
+
))
|
| 374 |
+
|
| 375 |
+
# Fill in a form field
|
| 376 |
+
result = client.step(OpenAppAction(
|
| 377 |
+
action_type="fill",
|
| 378 |
+
bid="event-title-input",
|
| 379 |
+
text="Team Meeting"
|
| 380 |
+
))
|
| 381 |
+
|
| 382 |
+
print(f"Reward: {result.reward}")
|
| 383 |
+
print(f"Done: {result.done}")
|
| 384 |
+
|
| 385 |
+
# Cleanup
|
| 386 |
+
client.close()
|
| 387 |
+
```
|
| 388 |
+
|
| 389 |
+
### Action Types
|
| 390 |
+
|
| 391 |
+
The environment supports the following action types:
|
| 392 |
+
|
| 393 |
+
- **click**: Click on an element
|
| 394 |
+
- Required: `bid` (BrowserGym element ID)
|
| 395 |
+
|
| 396 |
+
- **fill**: Fill a text input field
|
| 397 |
+
- Required: `bid`, `text`
|
| 398 |
+
|
| 399 |
+
- **select_option**: Select from dropdown
|
| 400 |
+
- Required: `bid`, `value`
|
| 401 |
+
|
| 402 |
+
- **goto**: Navigate to a URL
|
| 403 |
+
- Required: `url`
|
| 404 |
+
|
| 405 |
+
- **scroll**: Scroll the page
|
| 406 |
+
- Required: `direction` ("up" or "down")
|
| 407 |
+
|
| 408 |
+
- **send_keys**: Send keyboard input
|
| 409 |
+
- Required: `text`
|
| 410 |
+
|
| 411 |
+
- **noop**: No operation
|
| 412 |
+
|
| 413 |
+
### Observations
|
| 414 |
+
|
| 415 |
+
Each observation includes:
|
| 416 |
+
|
| 417 |
+
- **html**: Current page HTML content
|
| 418 |
+
- **url**: Current page URL
|
| 419 |
+
- **open_pages_urls**: List of all open page URLs
|
| 420 |
+
- **active_page_index**: Index of currently active page
|
| 421 |
+
- **screenshot**: Base64-encoded screenshot (optional)
|
| 422 |
+
- **axtree_txt**: Accessibility tree for element interaction
|
| 423 |
+
- **app_state**: Current state of all apps (calendar events, todos, messages, etc.)
|
| 424 |
+
- **task_info**: Information about current task (if using tasks)
|
| 425 |
+
- **last_action_error**: Error message if last action failed
|
| 426 |
+
|
| 427 |
+
## Configuration
|
| 428 |
+
|
| 429 |
+
### Environment Parameters
|
| 430 |
+
|
| 431 |
+
```python
|
| 432 |
+
from envs.openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 433 |
+
|
| 434 |
+
env = OpenAppEnvironment(
|
| 435 |
+
web_app_port=5001, # Port for OpenApps server
|
| 436 |
+
headless=True, # Run browser in headless mode
|
| 437 |
+
task_name="add_meeting", # Optional task name
|
| 438 |
+
apps_config={}, # App-specific configuration
|
| 439 |
+
max_steps=50, # Maximum steps per episode
|
| 440 |
+
)
|
| 441 |
+
```
|
| 442 |
+
|
| 443 |
+
**Note:** OpenApps is automatically detected from the installed Python package. You can optionally override with `openapps_path` parameter or `OPENAPPS_PATH` environment variable if needed.
|
| 444 |
+
|
| 445 |
+
## Tasks and Rewards
|
| 446 |
+
|
| 447 |
+
The environment can be configured with specific tasks from OpenApps. Tasks define:
|
| 448 |
+
- Goal state (e.g., "Add a meeting with Dennis to the calendar")
|
| 449 |
+
- Reward function based on app state changes
|
| 450 |
+
- Success criteria
|
| 451 |
+
|
| 452 |
+
See [OpenApps documentation](https://facebookresearch.github.io/OpenApps/) for available tasks.
|
| 453 |
+
|
| 454 |
+
## Example: Task-Based Training
|
| 455 |
+
|
| 456 |
+
```python
|
| 457 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 458 |
+
|
| 459 |
+
# Create environment with a specific task
|
| 460 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 461 |
+
|
| 462 |
+
# The task will guide the agent toward a specific goal
|
| 463 |
+
# Rewards will be based on progress toward completing the task
|
| 464 |
+
result = client.reset()
|
| 465 |
+
|
| 466 |
+
# Agent interacts to complete the task
|
| 467 |
+
# ... agent logic here ...
|
| 468 |
+
|
| 469 |
+
client.close()
|
| 470 |
+
```
|
| 471 |
+
|
| 472 |
+
## Development
|
| 473 |
+
|
| 474 |
+
### Running Server Locally (without Docker)
|
| 475 |
+
|
| 476 |
+
```bash
|
| 477 |
+
cd envs/openapp_env
|
| 478 |
+
uv run server
|
| 479 |
+
```
|
| 480 |
+
|
| 481 |
+
The server will start at `http://localhost:8000`
|
| 482 |
+
|
| 483 |
+
### Testing
|
| 484 |
+
|
| 485 |
+
```python
|
| 486 |
+
from openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 487 |
+
from openapp_env.models import OpenAppAction
|
| 488 |
+
|
| 489 |
+
def test_environment():
|
| 490 |
+
env = OpenAppEnvironment()
|
| 491 |
+
|
| 492 |
+
# Test reset
|
| 493 |
+
obs = env.reset()
|
| 494 |
+
assert obs.url != ""
|
| 495 |
+
|
| 496 |
+
# Test step
|
| 497 |
+
action = OpenAppAction(action_type="noop")
|
| 498 |
+
obs = env.step(action)
|
| 499 |
+
assert env.state.step_count == 1
|
| 500 |
+
|
| 501 |
+
# Cleanup
|
| 502 |
+
env.close()
|
| 503 |
+
|
| 504 |
+
test_environment()
|
| 505 |
+
```
|
| 506 |
+
|
| 507 |
+
## Attribution
|
| 508 |
+
|
| 509 |
+
This environment integrates:
|
| 510 |
+
- [OpenApps](https://github.com/facebookresearch/OpenApps) - Web application simulation framework
|
| 511 |
+
- [BrowserGym](https://github.com/ServiceNow/BrowserGym) - Browser automation environment
|
| 512 |
+
|
| 513 |
+
## Troubleshooting
|
| 514 |
+
|
| 515 |
+
### Docker Build Issues
|
| 516 |
+
|
| 517 |
+
**Error: `Container did not become ready`**
|
| 518 |
+
|
| 519 |
+
If you're behind a corporate proxy (Meta/Facebook networks), set `NO_PROXY`:
|
| 520 |
+
```bash
|
| 521 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 522 |
+
docker build -t openapp-env:latest -f envs/openapp_env/server/Dockerfile .
|
| 523 |
+
```
|
| 524 |
+
|
| 525 |
+
**Error: `Environment variable 'USER' not found`**
|
| 526 |
+
|
| 527 |
+
This is automatically handled in the Dockerfile with `ENV USER=root`. If you see this, rebuild the image.
|
| 528 |
+
|
| 529 |
+
**Container exits immediately**
|
| 530 |
+
|
| 531 |
+
Check the logs to see which server failed:
|
| 532 |
+
```bash
|
| 533 |
+
docker logs <container-id>
|
| 534 |
+
```
|
| 535 |
+
|
| 536 |
+
Common causes:
|
| 537 |
+
- OpenApps server failed to start (check for port conflicts)
|
| 538 |
+
- Missing dependencies (rebuild with `--no-cache`)
|
| 539 |
+
|
| 540 |
+
### Local Mode Issues
|
| 541 |
+
|
| 542 |
+
**Error: `OPENAPPS_URL not set`**
|
| 543 |
+
|
| 544 |
+
Set the environment variable before running:
|
| 545 |
+
```bash
|
| 546 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 547 |
+
python examples/openapp_example.py --mode local
|
| 548 |
+
```
|
| 549 |
+
|
| 550 |
+
**Error: `Connection refused to localhost:5001`**
|
| 551 |
+
|
| 552 |
+
Make sure the OpenApps server is running:
|
| 553 |
+
```bash
|
| 554 |
+
cd OpenApps
|
| 555 |
+
uv run launch.py
|
| 556 |
+
```
|
| 557 |
+
|
| 558 |
+
**Browser visualization not working**
|
| 559 |
+
|
| 560 |
+
The visualization is controlled by the **server**, not the client:
|
| 561 |
+
```bash
|
| 562 |
+
# Start server with visible browser
|
| 563 |
+
cd OpenApps
|
| 564 |
+
python launch.py browsergym_env_args.headless=False
|
| 565 |
+
```
|
| 566 |
+
|
| 567 |
+
### Performance Issues
|
| 568 |
+
|
| 569 |
+
**Docker container is slow**
|
| 570 |
+
|
| 571 |
+
The container runs both a full Chromium browser and web applications. For faster performance:
|
| 572 |
+
- Increase Docker memory allocation (6GB+ recommended)
|
| 573 |
+
- Use headless mode (default)
|
| 574 |
+
- Reduce `max_steps` in environment configuration
|
| 575 |
+
|
| 576 |
+
**Large Docker image size**
|
| 577 |
+
|
| 578 |
+
The image is ~5.7GB due to:
|
| 579 |
+
- Chromium browser (~1.5GB)
|
| 580 |
+
- OpenApps dependencies (~2GB)
|
| 581 |
+
- BrowserGym and ML libraries (~2GB)
|
| 582 |
+
|
| 583 |
+
This is expected for a full browser automation environment.
|
| 584 |
+
|
| 585 |
+
## License
|
| 586 |
+
|
| 587 |
+
BSD 3-Clause License (see LICENSE file in OpenEnv root directory)
|
| 588 |
+
|
| 589 |
+
## Citation
|
| 590 |
+
|
| 591 |
+
If you use this environment in your research, please cite both OpenEnv and OpenApps:
|
| 592 |
+
|
| 593 |
+
```bibtex
|
| 594 |
+
@article{ullrich2025openapps0,
|
| 595 |
+
title = {OpenApps: Simulating Environment Variations to Measure UI-Agent Reliability},
|
| 596 |
+
author = {Karen Ullrich and Jingtong Su and Claudia Shi and Arjun Subramonian and Amir Bar and Ivan Evtimov and Nikolaos Tsilivis and Randall Balestriero and Julia Kempe and Mark Ibrahim},
|
| 597 |
+
year = {2025},
|
| 598 |
+
journal = {arXiv preprint arXiv: 2511.20766}
|
| 599 |
+
}
|
| 600 |
+
```
|
__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""OpenApp Environment - Web application simulation environment for UI agents."""
|
| 8 |
+
|
| 9 |
+
from .client import OpenAppEnv
|
| 10 |
+
from .models import OpenAppAction, OpenAppObservation
|
| 11 |
+
|
| 12 |
+
__all__ = ["OpenAppAction", "OpenAppObservation", "OpenAppEnv"]
|
assets/01-messages.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0b4333519dedb001fdc95c47430349e1e33f03daf0082b3d80d2e66f604e812e
|
| 3 |
+
size 10889481
|
assets/02-editor.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49677c425607944fdcc4a35f0cefa55115b34fa1c61b8597074e5920e6be31ad
|
| 3 |
+
size 11875181
|
assets/03-calendar.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9b4011bb80014623250bd375a3c52012f0a43469dcff966c0fe538727cd9c194
|
| 3 |
+
size 10553742
|
assets/04-todo.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6b2231f1e2587ddabbf59fb793693e645e907cf6d7dd5d6bd9c79cf781f016e6
|
| 3 |
+
size 9591496
|
assets/OpenApps_OpenEnv_RL.png
ADDED
|
Git LFS Details
|
assets/demo-showcase.html
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>OpenApp + OpenEnv Demo Showcase</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--primary-color: #6366f1;
|
| 11 |
+
--primary-dark: #4f46e5;
|
| 12 |
+
--secondary-color: #10b981;
|
| 13 |
+
--accent-color: #f59e0b;
|
| 14 |
+
--bg-dark: #0f172a;
|
| 15 |
+
--bg-card: #1e293b;
|
| 16 |
+
--bg-card-hover: #334155;
|
| 17 |
+
--text-primary: #f8fafc;
|
| 18 |
+
--text-secondary: #94a3b8;
|
| 19 |
+
--gradient-1: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
| 20 |
+
--gradient-2: linear-gradient(135deg, #10b981 0%, #14b8a6 100%);
|
| 21 |
+
--gradient-3: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
|
| 22 |
+
--gradient-4: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
* {
|
| 26 |
+
margin: 0;
|
| 27 |
+
padding: 0;
|
| 28 |
+
box-sizing: border-box;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 33 |
+
background: var(--bg-dark);
|
| 34 |
+
color: var(--text-primary);
|
| 35 |
+
min-height: 100vh;
|
| 36 |
+
overflow-x: hidden;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Animated background */
|
| 40 |
+
.bg-animation {
|
| 41 |
+
position: fixed;
|
| 42 |
+
top: 0;
|
| 43 |
+
left: 0;
|
| 44 |
+
width: 100%;
|
| 45 |
+
height: 100%;
|
| 46 |
+
z-index: -1;
|
| 47 |
+
overflow: hidden;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.bg-animation::before {
|
| 51 |
+
content: '';
|
| 52 |
+
position: absolute;
|
| 53 |
+
top: -50%;
|
| 54 |
+
left: -50%;
|
| 55 |
+
width: 200%;
|
| 56 |
+
height: 200%;
|
| 57 |
+
background: radial-gradient(circle at 20% 80%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
|
| 58 |
+
radial-gradient(circle at 80% 20%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
|
| 59 |
+
radial-gradient(circle at 40% 40%, rgba(245, 158, 11, 0.05) 0%, transparent 40%);
|
| 60 |
+
animation: bgRotate 30s linear infinite;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
@keyframes bgRotate {
|
| 64 |
+
0% { transform: rotate(0deg); }
|
| 65 |
+
100% { transform: rotate(360deg); }
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Header */
|
| 69 |
+
header {
|
| 70 |
+
text-align: center;
|
| 71 |
+
padding: 4rem 2rem 3rem;
|
| 72 |
+
position: relative;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.logo-container {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
justify-content: center;
|
| 79 |
+
gap: 1rem;
|
| 80 |
+
margin-bottom: 1.5rem;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.logo-img {
|
| 84 |
+
width: 80px;
|
| 85 |
+
height: 80px;
|
| 86 |
+
border-radius: 16px;
|
| 87 |
+
box-shadow: 0 10px 40px rgba(99, 102, 241, 0.3);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
h1 {
|
| 91 |
+
font-size: 3.5rem;
|
| 92 |
+
font-weight: 800;
|
| 93 |
+
background: linear-gradient(135deg, #fff 0%, #a5b4fc 50%, #6366f1 100%);
|
| 94 |
+
-webkit-background-clip: text;
|
| 95 |
+
-webkit-text-fill-color: transparent;
|
| 96 |
+
background-clip: text;
|
| 97 |
+
margin-bottom: 1rem;
|
| 98 |
+
letter-spacing: -0.02em;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.subtitle {
|
| 102 |
+
font-size: 1.25rem;
|
| 103 |
+
color: var(--text-secondary);
|
| 104 |
+
max-width: 700px;
|
| 105 |
+
margin: 0 auto 2rem;
|
| 106 |
+
line-height: 1.6;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.badge-container {
|
| 110 |
+
display: flex;
|
| 111 |
+
justify-content: center;
|
| 112 |
+
gap: 1rem;
|
| 113 |
+
flex-wrap: wrap;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.badge {
|
| 117 |
+
display: inline-flex;
|
| 118 |
+
align-items: center;
|
| 119 |
+
gap: 0.5rem;
|
| 120 |
+
padding: 0.5rem 1rem;
|
| 121 |
+
background: var(--bg-card);
|
| 122 |
+
border-radius: 9999px;
|
| 123 |
+
font-size: 0.875rem;
|
| 124 |
+
font-weight: 500;
|
| 125 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.badge-icon {
|
| 129 |
+
width: 18px;
|
| 130 |
+
height: 18px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* Main content */
|
| 134 |
+
main {
|
| 135 |
+
max-width: 1400px;
|
| 136 |
+
margin: 0 auto;
|
| 137 |
+
padding: 2rem;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Section title */
|
| 141 |
+
.section-title {
|
| 142 |
+
text-align: center;
|
| 143 |
+
margin-bottom: 3rem;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.section-title h2 {
|
| 147 |
+
font-size: 2rem;
|
| 148 |
+
font-weight: 700;
|
| 149 |
+
margin-bottom: 0.5rem;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.section-title p {
|
| 153 |
+
color: var(--text-secondary);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Demo grid */
|
| 157 |
+
.demo-grid {
|
| 158 |
+
display: grid;
|
| 159 |
+
grid-template-columns: repeat(2, 1fr);
|
| 160 |
+
gap: 2rem;
|
| 161 |
+
margin-bottom: 4rem;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
@media (max-width: 1024px) {
|
| 165 |
+
.demo-grid {
|
| 166 |
+
grid-template-columns: 1fr;
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Demo card */
|
| 171 |
+
.demo-card {
|
| 172 |
+
background: var(--bg-card);
|
| 173 |
+
border-radius: 20px;
|
| 174 |
+
overflow: hidden;
|
| 175 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 176 |
+
transition: all 0.3s ease;
|
| 177 |
+
position: relative;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.demo-card:hover {
|
| 181 |
+
transform: translateY(-5px);
|
| 182 |
+
border-color: rgba(99, 102, 241, 0.5);
|
| 183 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4),
|
| 184 |
+
0 0 40px rgba(99, 102, 241, 0.1);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.demo-card::before {
|
| 188 |
+
content: '';
|
| 189 |
+
position: absolute;
|
| 190 |
+
top: 0;
|
| 191 |
+
left: 0;
|
| 192 |
+
right: 0;
|
| 193 |
+
height: 4px;
|
| 194 |
+
opacity: 0;
|
| 195 |
+
transition: opacity 0.3s ease;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.demo-card:nth-child(1)::before { background: var(--gradient-1); }
|
| 199 |
+
.demo-card:nth-child(2)::before { background: var(--gradient-2); }
|
| 200 |
+
.demo-card:nth-child(3)::before { background: var(--gradient-3); }
|
| 201 |
+
.demo-card:nth-child(4)::before { background: var(--gradient-4); }
|
| 202 |
+
|
| 203 |
+
.demo-card:hover::before {
|
| 204 |
+
opacity: 1;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.video-container {
|
| 208 |
+
position: relative;
|
| 209 |
+
width: 100%;
|
| 210 |
+
background: #000;
|
| 211 |
+
aspect-ratio: 16 / 10;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.video-container video {
|
| 215 |
+
width: 100%;
|
| 216 |
+
height: 100%;
|
| 217 |
+
object-fit: contain;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.play-overlay {
|
| 221 |
+
position: absolute;
|
| 222 |
+
top: 0;
|
| 223 |
+
left: 0;
|
| 224 |
+
width: 100%;
|
| 225 |
+
height: 100%;
|
| 226 |
+
display: flex;
|
| 227 |
+
align-items: center;
|
| 228 |
+
justify-content: center;
|
| 229 |
+
background: rgba(0, 0, 0, 0.4);
|
| 230 |
+
opacity: 0;
|
| 231 |
+
transition: opacity 0.3s ease;
|
| 232 |
+
cursor: pointer;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.video-container:hover .play-overlay {
|
| 236 |
+
opacity: 1;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.play-overlay.hidden {
|
| 240 |
+
display: none;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.play-button {
|
| 244 |
+
width: 80px;
|
| 245 |
+
height: 80px;
|
| 246 |
+
background: rgba(255, 255, 255, 0.95);
|
| 247 |
+
border-radius: 50%;
|
| 248 |
+
display: flex;
|
| 249 |
+
align-items: center;
|
| 250 |
+
justify-content: center;
|
| 251 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
| 252 |
+
transition: transform 0.3s ease;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.play-button:hover {
|
| 256 |
+
transform: scale(1.1);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.play-button svg {
|
| 260 |
+
width: 32px;
|
| 261 |
+
height: 32px;
|
| 262 |
+
fill: var(--bg-dark);
|
| 263 |
+
margin-left: 4px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.demo-info {
|
| 267 |
+
padding: 1.5rem;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.demo-header {
|
| 271 |
+
display: flex;
|
| 272 |
+
align-items: center;
|
| 273 |
+
gap: 0.75rem;
|
| 274 |
+
margin-bottom: 0.75rem;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.demo-icon {
|
| 278 |
+
width: 40px;
|
| 279 |
+
height: 40px;
|
| 280 |
+
border-radius: 10px;
|
| 281 |
+
display: flex;
|
| 282 |
+
align-items: center;
|
| 283 |
+
justify-content: center;
|
| 284 |
+
font-size: 1.25rem;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.demo-card:nth-child(1) .demo-icon { background: var(--gradient-1); }
|
| 288 |
+
.demo-card:nth-child(2) .demo-icon { background: var(--gradient-2); }
|
| 289 |
+
.demo-card:nth-child(3) .demo-icon { background: var(--gradient-3); }
|
| 290 |
+
.demo-card:nth-child(4) .demo-icon { background: var(--gradient-4); }
|
| 291 |
+
|
| 292 |
+
.demo-title {
|
| 293 |
+
font-size: 1.25rem;
|
| 294 |
+
font-weight: 600;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.demo-description {
|
| 298 |
+
color: var(--text-secondary);
|
| 299 |
+
font-size: 0.95rem;
|
| 300 |
+
line-height: 1.6;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* Features section */
|
| 304 |
+
.features-section {
|
| 305 |
+
margin-top: 4rem;
|
| 306 |
+
padding: 3rem;
|
| 307 |
+
background: var(--bg-card);
|
| 308 |
+
border-radius: 24px;
|
| 309 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.features-grid {
|
| 313 |
+
display: grid;
|
| 314 |
+
grid-template-columns: repeat(3, 1fr);
|
| 315 |
+
gap: 2rem;
|
| 316 |
+
margin-top: 2rem;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
@media (max-width: 768px) {
|
| 320 |
+
.features-grid {
|
| 321 |
+
grid-template-columns: 1fr;
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.feature-card {
|
| 326 |
+
text-align: center;
|
| 327 |
+
padding: 1.5rem;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.feature-icon {
|
| 331 |
+
width: 60px;
|
| 332 |
+
height: 60px;
|
| 333 |
+
background: var(--gradient-1);
|
| 334 |
+
border-radius: 16px;
|
| 335 |
+
display: flex;
|
| 336 |
+
align-items: center;
|
| 337 |
+
justify-content: center;
|
| 338 |
+
margin: 0 auto 1rem;
|
| 339 |
+
font-size: 1.5rem;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.feature-card:nth-child(2) .feature-icon { background: var(--gradient-2); }
|
| 343 |
+
.feature-card:nth-child(3) .feature-icon { background: var(--gradient-3); }
|
| 344 |
+
|
| 345 |
+
.feature-title {
|
| 346 |
+
font-size: 1.125rem;
|
| 347 |
+
font-weight: 600;
|
| 348 |
+
margin-bottom: 0.5rem;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.feature-desc {
|
| 352 |
+
color: var(--text-secondary);
|
| 353 |
+
font-size: 0.9rem;
|
| 354 |
+
line-height: 1.5;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* Footer */
|
| 358 |
+
footer {
|
| 359 |
+
text-align: center;
|
| 360 |
+
padding: 3rem 2rem;
|
| 361 |
+
color: var(--text-secondary);
|
| 362 |
+
font-size: 0.875rem;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
footer a {
|
| 366 |
+
color: var(--primary-color);
|
| 367 |
+
text-decoration: none;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
footer a:hover {
|
| 371 |
+
text-decoration: underline;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Video controls styling */
|
| 375 |
+
video::-webkit-media-controls-panel {
|
| 376 |
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
/* Responsive adjustments */
|
| 380 |
+
@media (max-width: 640px) {
|
| 381 |
+
h1 {
|
| 382 |
+
font-size: 2.5rem;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.subtitle {
|
| 386 |
+
font-size: 1rem;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.demo-grid {
|
| 390 |
+
gap: 1.5rem;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
main {
|
| 394 |
+
padding: 1rem;
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
</style>
|
| 398 |
+
</head>
|
| 399 |
+
<body>
|
| 400 |
+
<div class="bg-animation"></div>
|
| 401 |
+
|
| 402 |
+
<header>
|
| 403 |
+
<div class="logo-container">
|
| 404 |
+
<img src="OpenApps_OpenEnv_RL.png" alt="OpenApp Logo" class="logo-img">
|
| 405 |
+
</div>
|
| 406 |
+
<h1>OpenApp + OpenEnv</h1>
|
| 407 |
+
<p class="subtitle">
|
| 408 |
+
A powerful integration bringing realistic web application environments to reinforcement learning agents.
|
| 409 |
+
Watch AI agents interact with calendar, messaging, code editor, and task management apps.
|
| 410 |
+
</p>
|
| 411 |
+
<div class="badge-container">
|
| 412 |
+
<span class="badge">
|
| 413 |
+
<svg class="badge-icon" viewBox="0 0 24 24" fill="currentColor">
|
| 414 |
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
| 415 |
+
</svg>
|
| 416 |
+
BrowserGym Compatible
|
| 417 |
+
</span>
|
| 418 |
+
<span class="badge">
|
| 419 |
+
<svg class="badge-icon" viewBox="0 0 24 24" fill="currentColor">
|
| 420 |
+
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
|
| 421 |
+
</svg>
|
| 422 |
+
5+ Web Apps
|
| 423 |
+
</span>
|
| 424 |
+
<span class="badge">
|
| 425 |
+
<svg class="badge-icon" viewBox="0 0 24 24" fill="currentColor">
|
| 426 |
+
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
| 427 |
+
</svg>
|
| 428 |
+
RL Ready
|
| 429 |
+
</span>
|
| 430 |
+
</div>
|
| 431 |
+
</header>
|
| 432 |
+
|
| 433 |
+
<main>
|
| 434 |
+
<div class="section-title">
|
| 435 |
+
<h2>🎬 Demo Showcase</h2>
|
| 436 |
+
<p>Watch our AI agents perform real tasks in realistic web environments</p>
|
| 437 |
+
</div>
|
| 438 |
+
|
| 439 |
+
<div class="demo-grid">
|
| 440 |
+
<!-- Messenger Demo -->
|
| 441 |
+
<div class="demo-card">
|
| 442 |
+
<div class="video-container">
|
| 443 |
+
<video id="video1" controls preload="metadata" poster="">
|
| 444 |
+
<source src="01-messages.mov" type="video/quicktime">
|
| 445 |
+
<source src="01-messages.mov" type="video/mp4">
|
| 446 |
+
Your browser does not support the video tag.
|
| 447 |
+
</video>
|
| 448 |
+
<div class="play-overlay" onclick="playVideo('video1', this)">
|
| 449 |
+
<div class="play-button">
|
| 450 |
+
<svg viewBox="0 0 24 24">
|
| 451 |
+
<path d="M8 5v14l11-7z"/>
|
| 452 |
+
</svg>
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="demo-info">
|
| 457 |
+
<div class="demo-header">
|
| 458 |
+
<div class="demo-icon">💬</div>
|
| 459 |
+
<h3 class="demo-title">Messenger App</h3>
|
| 460 |
+
</div>
|
| 461 |
+
<p class="demo-description">
|
| 462 |
+
AI agent navigates conversations, types messages interactively, and sends them to contacts.
|
| 463 |
+
Demonstrates natural language input and real-time chat interactions.
|
| 464 |
+
</p>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<!-- Code Editor Demo -->
|
| 469 |
+
<div class="demo-card">
|
| 470 |
+
<div class="video-container">
|
| 471 |
+
<video id="video2" controls preload="metadata">
|
| 472 |
+
<source src="02-editor.mov" type="video/quicktime">
|
| 473 |
+
<source src="02-editor.mov" type="video/mp4">
|
| 474 |
+
Your browser does not support the video tag.
|
| 475 |
+
</video>
|
| 476 |
+
<div class="play-overlay" onclick="playVideo('video2', this)">
|
| 477 |
+
<div class="play-button">
|
| 478 |
+
<svg viewBox="0 0 24 24">
|
| 479 |
+
<path d="M8 5v14l11-7z"/>
|
| 480 |
+
</svg>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
<div class="demo-info">
|
| 485 |
+
<div class="demo-header">
|
| 486 |
+
<div class="demo-icon">💻</div>
|
| 487 |
+
<h3 class="demo-title">Code Editor</h3>
|
| 488 |
+
</div>
|
| 489 |
+
<p class="demo-description">
|
| 490 |
+
Agent creates files and writes a complete PyTorch training loop with syntax highlighting.
|
| 491 |
+
Shows code generation, file management, and save operations.
|
| 492 |
+
</p>
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
|
| 496 |
+
<!-- Calendar Demo -->
|
| 497 |
+
<div class="demo-card">
|
| 498 |
+
<div class="video-container">
|
| 499 |
+
<video id="video3" controls preload="metadata">
|
| 500 |
+
<source src="03-calendar.mov" type="video/quicktime">
|
| 501 |
+
<source src="03-calendar.mov" type="video/mp4">
|
| 502 |
+
Your browser does not support the video tag.
|
| 503 |
+
</video>
|
| 504 |
+
<div class="play-overlay" onclick="playVideo('video3', this)">
|
| 505 |
+
<div class="play-button">
|
| 506 |
+
<svg viewBox="0 0 24 24">
|
| 507 |
+
<path d="M8 5v14l11-7z"/>
|
| 508 |
+
</svg>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
</div>
|
| 512 |
+
<div class="demo-info">
|
| 513 |
+
<div class="demo-header">
|
| 514 |
+
<div class="demo-icon">📅</div>
|
| 515 |
+
<h3 class="demo-title">Calendar App</h3>
|
| 516 |
+
</div>
|
| 517 |
+
<p class="demo-description">
|
| 518 |
+
Navigate between calendar and agenda views, browse events across months,
|
| 519 |
+
and view detailed event information. Perfect for scheduling tasks.
|
| 520 |
+
</p>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<!-- Todo Demo -->
|
| 525 |
+
<div class="demo-card">
|
| 526 |
+
<div class="video-container">
|
| 527 |
+
<video id="video4" controls preload="metadata">
|
| 528 |
+
<source src="04-todo.mov" type="video/quicktime">
|
| 529 |
+
<source src="04-todo.mov" type="video/mp4">
|
| 530 |
+
Your browser does not support the video tag.
|
| 531 |
+
</video>
|
| 532 |
+
<div class="play-overlay" onclick="playVideo('video4', this)">
|
| 533 |
+
<div class="play-button">
|
| 534 |
+
<svg viewBox="0 0 24 24">
|
| 535 |
+
<path d="M8 5v14l11-7z"/>
|
| 536 |
+
</svg>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
<div class="demo-info">
|
| 541 |
+
<div class="demo-header">
|
| 542 |
+
<div class="demo-icon">✅</div>
|
| 543 |
+
<h3 class="demo-title">Todo Manager</h3>
|
| 544 |
+
</div>
|
| 545 |
+
<p class="demo-description">
|
| 546 |
+
Browse and manage task lists, edit task details, mark items complete,
|
| 547 |
+
and organize priorities. Demonstrates CRUD operations on structured data.
|
| 548 |
+
</p>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
|
| 553 |
+
<!-- Features Section -->
|
| 554 |
+
<div class="features-section">
|
| 555 |
+
<div class="section-title">
|
| 556 |
+
<h2>✨ Key Features</h2>
|
| 557 |
+
<p>Why OpenApp + OpenEnv is perfect for AI agent research</p>
|
| 558 |
+
</div>
|
| 559 |
+
<div class="features-grid">
|
| 560 |
+
<div class="feature-card">
|
| 561 |
+
<div class="feature-icon">🎮</div>
|
| 562 |
+
<h4 class="feature-title">Gymnasium Compatible</h4>
|
| 563 |
+
<p class="feature-desc">
|
| 564 |
+
Standard RL interface with observations, actions, and rewards.
|
| 565 |
+
Drop-in replacement for existing training pipelines.
|
| 566 |
+
</p>
|
| 567 |
+
</div>
|
| 568 |
+
<div class="feature-card">
|
| 569 |
+
<div class="feature-icon">🌐</div>
|
| 570 |
+
<h4 class="feature-title">Real Web Apps</h4>
|
| 571 |
+
<p class="feature-desc">
|
| 572 |
+
Authentic web applications with HTML, CSS, and JavaScript.
|
| 573 |
+
No simplified simulations – real browser interactions.
|
| 574 |
+
</p>
|
| 575 |
+
</div>
|
| 576 |
+
<div class="feature-card">
|
| 577 |
+
<div class="feature-icon">🔄</div>
|
| 578 |
+
<h4 class="feature-title">Configurable Tasks</h4>
|
| 579 |
+
<p class="feature-desc">
|
| 580 |
+
YAML-based configuration for custom scenarios, data, and rewards.
|
| 581 |
+
Easily create new training environments.
|
| 582 |
+
</p>
|
| 583 |
+
</div>
|
| 584 |
+
</div>
|
| 585 |
+
</div>
|
| 586 |
+
</main>
|
| 587 |
+
|
| 588 |
+
<footer>
|
| 589 |
+
<p>
|
| 590 |
+
Built for the <strong>OpenEnv Hackathon</strong> 🏆<br>
|
| 591 |
+
<a href="https://github.com/anthropics/anthropic-cookbook/tree/main/misc/openenv" target="_blank">OpenEnv Framework</a> •
|
| 592 |
+
<a href="https://github.com/ServiceNow/BrowserGym" target="_blank">BrowserGym</a>
|
| 593 |
+
</p>
|
| 594 |
+
</footer>
|
| 595 |
+
|
| 596 |
+
<script>
|
| 597 |
+
function playVideo(videoId, overlay) {
|
| 598 |
+
const video = document.getElementById(videoId);
|
| 599 |
+
video.play();
|
| 600 |
+
overlay.classList.add('hidden');
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
// Show overlay again when video ends or is paused
|
| 604 |
+
document.querySelectorAll('video').forEach(video => {
|
| 605 |
+
video.addEventListener('ended', function() {
|
| 606 |
+
const overlay = this.parentElement.querySelector('.play-overlay');
|
| 607 |
+
overlay.classList.remove('hidden');
|
| 608 |
+
});
|
| 609 |
+
});
|
| 610 |
+
|
| 611 |
+
// Add intersection observer for lazy loading
|
| 612 |
+
const videos = document.querySelectorAll('video');
|
| 613 |
+
const observer = new IntersectionObserver((entries) => {
|
| 614 |
+
entries.forEach(entry => {
|
| 615 |
+
if (entry.isIntersecting) {
|
| 616 |
+
entry.target.load();
|
| 617 |
+
}
|
| 618 |
+
});
|
| 619 |
+
}, { threshold: 0.25 });
|
| 620 |
+
|
| 621 |
+
videos.forEach(video => observer.observe(video));
|
| 622 |
+
</script>
|
| 623 |
+
</body>
|
| 624 |
+
</html>
|
assets/openapps-demo.gif
ADDED
|
Git LFS Details
|
client.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
OpenApp Environment HTTP Client.
|
| 9 |
+
|
| 10 |
+
This module provides the client for connecting to an OpenApp Environment server
|
| 11 |
+
over HTTP.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from typing import Any, Dict
|
| 15 |
+
|
| 16 |
+
# Support both in-repo and standalone imports
|
| 17 |
+
try:
|
| 18 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 19 |
+
from openenv.core.client_types import StepResult
|
| 20 |
+
from openenv.core.env_server.types import State
|
| 21 |
+
from openenv.core.env_client import EnvClient
|
| 22 |
+
from .models import OpenAppAction, OpenAppObservation
|
| 23 |
+
except ImportError:
|
| 24 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 25 |
+
from openenv.core.client_types import StepResult
|
| 26 |
+
from openenv.core.env_server.types import State
|
| 27 |
+
from openenv.core.env_client import EnvClient
|
| 28 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class OpenAppEnv(EnvClient[OpenAppAction, OpenAppObservation, State]):
|
| 32 |
+
"""
|
| 33 |
+
HTTP client for the OpenApp Environment.
|
| 34 |
+
|
| 35 |
+
This client connects to an OpenAppEnvironment HTTP server and provides
|
| 36 |
+
methods to interact with it: reset(), step(), and state access.
|
| 37 |
+
|
| 38 |
+
The OpenApp environment simulates web applications (calendar, todo, messenger, maps)
|
| 39 |
+
and allows agents to interact with them using browser-based actions.
|
| 40 |
+
|
| 41 |
+
Example:
|
| 42 |
+
>>> # Connect to a running server
|
| 43 |
+
>>> client = OpenAppEnv(base_url="http://localhost:8000")
|
| 44 |
+
>>> result = client.reset()
|
| 45 |
+
>>> print(result.observation.url)
|
| 46 |
+
>>>
|
| 47 |
+
>>> # Click on an element
|
| 48 |
+
>>> result = client.step(OpenAppAction(action_type="click", bid="123"))
|
| 49 |
+
>>> print(result.observation.html)
|
| 50 |
+
>>> print(result.reward)
|
| 51 |
+
|
| 52 |
+
Example with Docker:
|
| 53 |
+
>>> # Automatically start container and connect
|
| 54 |
+
>>> client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 55 |
+
>>> result = client.reset()
|
| 56 |
+
>>> # Fill a text field
|
| 57 |
+
>>> result = client.step(OpenAppAction(
|
| 58 |
+
... action_type="fill",
|
| 59 |
+
... bid="456",
|
| 60 |
+
... text="Meeting with team"
|
| 61 |
+
... ))
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def _step_payload(self, action: OpenAppAction) -> Dict:
|
| 65 |
+
"""
|
| 66 |
+
Convert OpenAppAction to JSON payload for step request.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
action: OpenAppAction instance
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Dictionary representation suitable for JSON encoding
|
| 73 |
+
"""
|
| 74 |
+
payload = {
|
| 75 |
+
"action_type": action.action_type,
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Add optional fields if present
|
| 79 |
+
if action.bid is not None:
|
| 80 |
+
payload["bid"] = action.bid
|
| 81 |
+
if action.text is not None:
|
| 82 |
+
payload["text"] = action.text
|
| 83 |
+
if action.value is not None:
|
| 84 |
+
payload["value"] = action.value
|
| 85 |
+
if action.url is not None:
|
| 86 |
+
payload["url"] = action.url
|
| 87 |
+
if action.direction is not None:
|
| 88 |
+
payload["direction"] = action.direction
|
| 89 |
+
if action.metadata:
|
| 90 |
+
payload["metadata"] = action.metadata
|
| 91 |
+
|
| 92 |
+
return payload
|
| 93 |
+
|
| 94 |
+
def _parse_result(self, payload: Dict) -> StepResult[OpenAppObservation]:
|
| 95 |
+
"""
|
| 96 |
+
Parse server response into StepResult[OpenAppObservation].
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
payload: JSON response from server
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
StepResult with OpenAppObservation
|
| 103 |
+
"""
|
| 104 |
+
obs_data = payload.get("observation", {})
|
| 105 |
+
observation = OpenAppObservation(
|
| 106 |
+
html=obs_data.get("html", ""),
|
| 107 |
+
url=obs_data.get("url", ""),
|
| 108 |
+
open_pages_urls=obs_data.get("open_pages_urls", []),
|
| 109 |
+
active_page_index=obs_data.get("active_page_index", 0),
|
| 110 |
+
screenshot=obs_data.get("screenshot"),
|
| 111 |
+
axtree_txt=obs_data.get("axtree_txt", ""),
|
| 112 |
+
app_state=obs_data.get("app_state", {}),
|
| 113 |
+
task_info=obs_data.get("task_info"),
|
| 114 |
+
last_action_error=obs_data.get("last_action_error"),
|
| 115 |
+
done=payload.get("done", False),
|
| 116 |
+
reward=payload.get("reward"),
|
| 117 |
+
metadata=obs_data.get("metadata", {}),
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
return StepResult(
|
| 121 |
+
observation=observation,
|
| 122 |
+
reward=payload.get("reward"),
|
| 123 |
+
done=payload.get("done", False),
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 127 |
+
"""
|
| 128 |
+
Parse server response into State object.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
payload: JSON response from /state endpoint
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
State object with episode_id and step_count
|
| 135 |
+
"""
|
| 136 |
+
return State(
|
| 137 |
+
episode_id=payload.get("episode_id"),
|
| 138 |
+
step_count=payload.get("step_count", 0),
|
| 139 |
+
)
|
envs/openapp_env/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
OpenApps
|
envs/openapp_env/README.md
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: OpenApp Environment Server
|
| 3 |
+
emoji: 🌐
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- OpenApps
|
| 13 |
+
- BrowserGym
|
| 14 |
+
- UI-Agents
|
| 15 |
+
- Reinforcement-Learning
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
<!--
|
| 19 |
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 20 |
+
All rights reserved.
|
| 21 |
+
|
| 22 |
+
This source code is licensed under the BSD-style license found in the
|
| 23 |
+
LICENSE file in the root directory of this source tree.
|
| 24 |
+
-->
|
| 25 |
+
|
| 26 |
+
<div align="center">
|
| 27 |
+
|
| 28 |
+
# OpenApp Environment
|
| 29 |
+
|
| 30 |
+
<img src="assets/OpenApps_OpenEnv_RL.png" alt="OpenApps Environment" width="800"/>
|
| 31 |
+
|
| 32 |
+
*A web application simulation environment for OpenEnv that wraps the [OpenApps](https://github.com/facebookresearch/OpenApps) framework and BrowserGym.*
|
| 33 |
+
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
## Overview
|
| 37 |
+
|
| 38 |
+
The OpenApp environment provides a simulated web application ecosystem where agents can interact with various apps (calendar, todo, messenger, maps) using browser-based actions.
|
| 39 |
+
|
| 40 |
+
<div align="center">
|
| 41 |
+
<img src="assets/openapps-demo.gif" alt="OpenApps Demo" width="800"/>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
This environment is ideal for:
|
| 45 |
+
|
| 46 |
+
- Training and evaluating UI agents
|
| 47 |
+
- Testing web automation strategies
|
| 48 |
+
- Researching human-computer interaction
|
| 49 |
+
- Developing multimodal agents
|
| 50 |
+
|
| 51 |
+
## Features
|
| 52 |
+
|
| 53 |
+
- **Multiple Apps**: Interact with calendar, todo list, messenger, and map applications
|
| 54 |
+
- **Browser-Based Actions**: Click, fill forms, navigate, scroll, and more
|
| 55 |
+
- **Task-Based Evaluation**: Optional task goals with automatic reward calculation
|
| 56 |
+
- **Configurable**: Customize app configurations and behavior
|
| 57 |
+
- **BrowserGym Integration**: Built on top of BrowserGym for robust browser interaction
|
| 58 |
+
|
| 59 |
+
## Directory Structure
|
| 60 |
+
|
| 61 |
+
```
|
| 62 |
+
openapp_env/
|
| 63 |
+
├── __init__.py # Package exports
|
| 64 |
+
├── client.py # HTTP client for connecting to OpenApp
|
| 65 |
+
├── models.py # Data models for actions and observations
|
| 66 |
+
├── pyproject.toml # Package dependencies and configuration
|
| 67 |
+
├── openenv.yaml # OpenEnv environment configuration
|
| 68 |
+
├── test_openapp_env.py # Unit tests for environment structure
|
| 69 |
+
├── README.md # This file
|
| 70 |
+
├── IMPLEMENTATION.md # Implementation details and design decisions
|
| 71 |
+
├── example_usage.py # Basic usage example (legacy)
|
| 72 |
+
├── assets/ # Images and media
|
| 73 |
+
│ ├── OpenApps_OpenEnv_RL.png # Environment overview diagram
|
| 74 |
+
│ └── openapps-demo.gif # Demo animation
|
| 75 |
+
└── server/ # Server-side environment implementation
|
| 76 |
+
├── __init__.py
|
| 77 |
+
├── app.py # FastAPI server application
|
| 78 |
+
├── openapp_environment.py # Core environment logic (BrowserGym + OpenApps)
|
| 79 |
+
├── Dockerfile # Docker image definition
|
| 80 |
+
└── start.sh # Container startup script (runs both servers)
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
**Key Components:**
|
| 84 |
+
|
| 85 |
+
- **client.py**: `OpenAppEnv` class that extends `HTTPEnvClient` for remote environment interaction
|
| 86 |
+
- **models.py**: `OpenAppAction` and `OpenAppObservation` dataclasses with validation
|
| 87 |
+
- **server/openapp_environment.py**: `OpenAppEnvironment` class that wraps BrowserGym and OpenApps
|
| 88 |
+
- **server/app.py**: FastAPI server that exposes the environment via HTTP endpoints
|
| 89 |
+
- **server/Dockerfile**: Self-contained Docker image with OpenApps server and FastAPI server
|
| 90 |
+
- **server/start.sh**: Startup script that launches both OpenApps (port 5001) and FastAPI (port 8000)
|
| 91 |
+
|
| 92 |
+
## Installation
|
| 93 |
+
|
| 94 |
+
There are two ways to use the OpenApp environment: **Docker mode** (recommended, fully self-contained) or **Local mode** (requires manual server setup).
|
| 95 |
+
|
| 96 |
+
### Option 1: Docker Mode (Recommended)
|
| 97 |
+
|
| 98 |
+
Docker mode is fully self-contained and handles all dependencies automatically. No local installation required!
|
| 99 |
+
|
| 100 |
+
**Step 1: Build the Docker image**
|
| 101 |
+
|
| 102 |
+
The Docker image can be built in standalone mode using only public base images:
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# Build from the environment directory
|
| 106 |
+
cd envs/openapp_env
|
| 107 |
+
docker build -t openapp-env:latest -f server/Dockerfile .
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
**Note for Meta/Corporate Networks:** If you're behind a proxy (HTTP_PROXY/HTTPS_PROXY set), you may need to bypass it for localhost connections:
|
| 111 |
+
```bash
|
| 112 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 113 |
+
cd envs/openapp_env
|
| 114 |
+
docker build -t openapp-env:latest -f server/Dockerfile .
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
**What gets installed in Docker:**
|
| 118 |
+
- **OpenEnv core**: Installed as a dependency
|
| 119 |
+
- **OpenApps**: Cloned from GitHub and installed (runs server inside container)
|
| 120 |
+
- **Core packages**: FastAPI, Uvicorn, Pydantic, Requests (from pyproject.toml)
|
| 121 |
+
- **BrowserGym**: For browser automation
|
| 122 |
+
- **Playwright**: Chromium browser for UI interaction
|
| 123 |
+
- **Web interface support**: Enabled by default via `ENABLE_WEB_INTERFACE=true`
|
| 124 |
+
|
| 125 |
+
**How Docker mode works:**
|
| 126 |
+
The Docker container runs TWO services automatically:
|
| 127 |
+
1. **OpenApps server** (port 5001) - Provides the web applications (calendar, todo, messenger, maps)
|
| 128 |
+
2. **FastAPI server** (port 8000) - Exposes the OpenEnv HTTP API
|
| 129 |
+
|
| 130 |
+
Both servers start automatically when the container launches. You only interact with port 8000.
|
| 131 |
+
|
| 132 |
+
**Build details:**
|
| 133 |
+
- Base image: `python:3.11-slim` (public)
|
| 134 |
+
- Installation: Uses `pip install -e .` with pyproject.toml
|
| 135 |
+
- System deps: Playwright/Chromium dependencies for browser automation
|
| 136 |
+
- Size: ~5.7GB (includes Chromium browser and all dependencies)
|
| 137 |
+
|
| 138 |
+
**Step 2: Run the example**
|
| 139 |
+
```bash
|
| 140 |
+
# For Meta/Corporate Networks with proxy, also set NO_PROXY:
|
| 141 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 142 |
+
|
| 143 |
+
python examples/openapp_example.py --mode docker
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
**Note:** For Docker mode, you only need Python installed locally to run the example script. All environment dependencies are inside the Docker container.
|
| 147 |
+
|
| 148 |
+
### Option 2: Local Mode
|
| 149 |
+
|
| 150 |
+
Local mode requires manual setup of the OpenApps server. This mode is useful for development or when you need to customize the OpenApps configuration.
|
| 151 |
+
|
| 152 |
+
**Prerequisites:**
|
| 153 |
+
- Python 3.11+ installed
|
| 154 |
+
- UV package manager (recommended) or pip
|
| 155 |
+
|
| 156 |
+
**Step 1: Install openapp_env**
|
| 157 |
+
```bash
|
| 158 |
+
cd envs/openapp_env
|
| 159 |
+
pip install -e .
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
This installs the environment package along with dependencies (BrowserGym, Playwright, etc.).
|
| 163 |
+
|
| 164 |
+
**Step 2: Install Playwright browsers**
|
| 165 |
+
```bash
|
| 166 |
+
playwright install chromium
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
**Step 3: Clone and set up OpenApps** (for running the server)
|
| 170 |
+
```bash
|
| 171 |
+
# Clone OpenApps repository
|
| 172 |
+
git clone https://github.com/facebookresearch/OpenApps.git
|
| 173 |
+
cd OpenApps
|
| 174 |
+
|
| 175 |
+
# Install dependencies
|
| 176 |
+
uv sync # or: pip install -e .
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
**Why do I need the OpenApps repository?**
|
| 180 |
+
|
| 181 |
+
The OpenApps Python package (installed via pip in Step 1) provides the library code, but the repository contains:
|
| 182 |
+
- `launch.py` - The server startup script
|
| 183 |
+
- `config/` - Hydra configuration files
|
| 184 |
+
- Application templates and assets
|
| 185 |
+
|
| 186 |
+
In Docker mode, all of this is included in the container, so you don't need to clone anything.
|
| 187 |
+
|
| 188 |
+
## Quick Start
|
| 189 |
+
|
| 190 |
+
### Running with Docker (Recommended)
|
| 191 |
+
|
| 192 |
+
Docker mode is the easiest way - everything is automated:
|
| 193 |
+
|
| 194 |
+
```bash
|
| 195 |
+
# For Meta/Corporate networks with proxy, set NO_PROXY first:
|
| 196 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 197 |
+
|
| 198 |
+
# Run the example
|
| 199 |
+
python examples/openapp_example.py --mode docker
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
The Docker container automatically:
|
| 203 |
+
- Starts the OpenApps server (port 5001)
|
| 204 |
+
- Starts the FastAPI server (port 8000)
|
| 205 |
+
- Manages both services for you
|
| 206 |
+
|
| 207 |
+
No manual server setup required!
|
| 208 |
+
|
| 209 |
+
**What happens inside the container:**
|
| 210 |
+
|
| 211 |
+
When you run `from_docker_image()`, the following happens automatically:
|
| 212 |
+
|
| 213 |
+
1. **Container Startup** (`/app/start.sh` runs):
|
| 214 |
+
```bash
|
| 215 |
+
# Launches OpenApps server in background
|
| 216 |
+
cd /app/openapps
|
| 217 |
+
python launch.py &
|
| 218 |
+
|
| 219 |
+
# Waits for port 5001 to be ready
|
| 220 |
+
# Then starts FastAPI server
|
| 221 |
+
uvicorn openapp_env.server.app:app --host 0.0.0.0 --port 8000
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
2. **Your client code** interacts only with port 8000:
|
| 225 |
+
```python
|
| 226 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 227 |
+
# Client -> FastAPI (port 8000) -> OpenApps (port 5001)
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
3. **On cleanup**, both servers are automatically stopped when the container is removed.
|
| 231 |
+
|
| 232 |
+
### Running Locally
|
| 233 |
+
|
| 234 |
+
For local usage, you need the OpenApps repository to run the server:
|
| 235 |
+
|
| 236 |
+
**Step 1: Clone OpenApps (if you haven't already)**
|
| 237 |
+
```bash
|
| 238 |
+
git clone https://github.com/facebookresearch/OpenApps.git
|
| 239 |
+
cd OpenApps
|
| 240 |
+
uv sync
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
**Step 2: Start OpenApps Server** (in terminal 1)
|
| 244 |
+
|
| 245 |
+
To run the server in **headless mode** (no browser window):
|
| 246 |
+
```bash
|
| 247 |
+
cd OpenApps # or wherever you cloned it
|
| 248 |
+
uv run launch.py
|
| 249 |
+
|
| 250 |
+
# or instead of the uv run you can use the Python command:
|
| 251 |
+
python OpenApps/launch.py
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
To run the server with **visible browser** for visualization:
|
| 255 |
+
```bash
|
| 256 |
+
cd OpenApps
|
| 257 |
+
python OpenApps/launch.py browsergym_env_args.headless=False
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
Wait for the server to start (you'll see "Port 5001 is available" or similar).
|
| 261 |
+
|
| 262 |
+
**Step 3: Run your code** (in terminal 2)
|
| 263 |
+
```bash
|
| 264 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 265 |
+
python examples/openapp_example.py --mode local
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
**Note:** The OpenApps Python package (installed via pip) provides the modules, but you need the full repository to run launch.py with its config files.
|
| 269 |
+
|
| 270 |
+
### Example Script
|
| 271 |
+
|
| 272 |
+
```bash
|
| 273 |
+
# Run with Docker (recommended)
|
| 274 |
+
python examples/openapp_example.py --mode docker
|
| 275 |
+
|
| 276 |
+
# Run locally (requires OpenApps server running)
|
| 277 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 278 |
+
python examples/openapp_example.py --mode local
|
| 279 |
+
|
| 280 |
+
# Show browser window to visualize agent actions
|
| 281 |
+
python examples/openapp_example.py --mode local --show-browser
|
| 282 |
+
|
| 283 |
+
# Run with custom number of steps
|
| 284 |
+
python examples/openapp_example.py --mode docker --num-steps 20
|
| 285 |
+
|
| 286 |
+
# See all options
|
| 287 |
+
python examples/openapp_example.py --help
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
### Visualizing Agent Interactions
|
| 291 |
+
|
| 292 |
+
There are multiple ways to see what the agent is doing:
|
| 293 |
+
|
| 294 |
+
**Option 1: Show Browser Window (Local Mode)**
|
| 295 |
+
|
| 296 |
+
The key is to start the OpenApps server with visualization enabled:
|
| 297 |
+
|
| 298 |
+
```bash
|
| 299 |
+
# Terminal 1: Start OpenApps server with visible browser
|
| 300 |
+
cd OpenApps
|
| 301 |
+
python OpenApps/launch.py browsergym_env_args.headless=False
|
| 302 |
+
|
| 303 |
+
# Terminal 2: Run your agent code
|
| 304 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 305 |
+
python examples/openapp_example.py --mode local
|
| 306 |
+
```
|
| 307 |
+
|
| 308 |
+
**Important:** The browser visualization is controlled by the OpenApps server, not the client. You must launch the server with `browsergym_env_args.headless=False` to see the browser window.
|
| 309 |
+
|
| 310 |
+
**Option 2: Access Web Interface Directly**
|
| 311 |
+
|
| 312 |
+
While the OpenApps server is running, open your browser to:
|
| 313 |
+
- Main page: `http://localhost:5001`
|
| 314 |
+
- Calendar: `http://localhost:5001/calendar`
|
| 315 |
+
- Todo: `http://localhost:5001/todo`
|
| 316 |
+
- Messenger: `http://localhost:5001/messages`
|
| 317 |
+
- Maps: `http://localhost:5001/maps`
|
| 318 |
+
|
| 319 |
+
**Option 3: Docker Web Interface**
|
| 320 |
+
|
| 321 |
+
When running in Docker mode, you can also access a web interface for manual testing:
|
| 322 |
+
|
| 323 |
+
```bash
|
| 324 |
+
# Start a container and keep it running
|
| 325 |
+
docker run -d -p 8000:8000 openapp-env:latest
|
| 326 |
+
|
| 327 |
+
# Access the web interface
|
| 328 |
+
# - Interactive UI: http://localhost:8000/web
|
| 329 |
+
# - API docs: http://localhost:8000/docs
|
| 330 |
+
# - OpenApps (internal): http://localhost:5001 (inside container)
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
**Note:** In Docker mode, the OpenApps server runs inside the container and is not directly accessible from your host machine. The FastAPI server at port 8000 acts as a proxy to interact with OpenApps.
|
| 334 |
+
|
| 335 |
+
### Basic Usage
|
| 336 |
+
|
| 337 |
+
```python
|
| 338 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 339 |
+
|
| 340 |
+
# Create environment from Docker image
|
| 341 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 342 |
+
|
| 343 |
+
# Reset to initial state
|
| 344 |
+
result = client.reset()
|
| 345 |
+
print(f"Starting URL: {result.observation.url}")
|
| 346 |
+
|
| 347 |
+
# Navigate to calendar app
|
| 348 |
+
result = client.step(OpenAppAction(
|
| 349 |
+
action_type="goto",
|
| 350 |
+
url="http://localhost:5001/calendar"
|
| 351 |
+
))
|
| 352 |
+
|
| 353 |
+
# Click on a button (example bid)
|
| 354 |
+
result = client.step(OpenAppAction(
|
| 355 |
+
action_type="click",
|
| 356 |
+
bid="add-event-btn"
|
| 357 |
+
))
|
| 358 |
+
|
| 359 |
+
# Fill in a form field
|
| 360 |
+
result = client.step(OpenAppAction(
|
| 361 |
+
action_type="fill",
|
| 362 |
+
bid="event-title-input",
|
| 363 |
+
text="Team Meeting"
|
| 364 |
+
))
|
| 365 |
+
|
| 366 |
+
print(f"Reward: {result.reward}")
|
| 367 |
+
print(f"Done: {result.done}")
|
| 368 |
+
|
| 369 |
+
# Cleanup
|
| 370 |
+
client.close()
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
### Action Types
|
| 374 |
+
|
| 375 |
+
The environment supports the following action types:
|
| 376 |
+
|
| 377 |
+
- **click**: Click on an element
|
| 378 |
+
- Required: `bid` (BrowserGym element ID)
|
| 379 |
+
|
| 380 |
+
- **fill**: Fill a text input field
|
| 381 |
+
- Required: `bid`, `text`
|
| 382 |
+
|
| 383 |
+
- **select_option**: Select from dropdown
|
| 384 |
+
- Required: `bid`, `value`
|
| 385 |
+
|
| 386 |
+
- **goto**: Navigate to a URL
|
| 387 |
+
- Required: `url`
|
| 388 |
+
|
| 389 |
+
- **scroll**: Scroll the page
|
| 390 |
+
- Required: `direction` ("up" or "down")
|
| 391 |
+
|
| 392 |
+
- **send_keys**: Send keyboard input
|
| 393 |
+
- Required: `text`
|
| 394 |
+
|
| 395 |
+
- **noop**: No operation
|
| 396 |
+
|
| 397 |
+
### Observations
|
| 398 |
+
|
| 399 |
+
Each observation includes:
|
| 400 |
+
|
| 401 |
+
- **html**: Current page HTML content
|
| 402 |
+
- **url**: Current page URL
|
| 403 |
+
- **open_pages_urls**: List of all open page URLs
|
| 404 |
+
- **active_page_index**: Index of currently active page
|
| 405 |
+
- **screenshot**: Base64-encoded screenshot (optional)
|
| 406 |
+
- **axtree_txt**: Accessibility tree for element interaction
|
| 407 |
+
- **app_state**: Current state of all apps (calendar events, todos, messages, etc.)
|
| 408 |
+
- **task_info**: Information about current task (if using tasks)
|
| 409 |
+
- **last_action_error**: Error message if last action failed
|
| 410 |
+
|
| 411 |
+
## Configuration
|
| 412 |
+
|
| 413 |
+
### Environment Parameters
|
| 414 |
+
|
| 415 |
+
```python
|
| 416 |
+
from envs.openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 417 |
+
|
| 418 |
+
env = OpenAppEnvironment(
|
| 419 |
+
web_app_port=5001, # Port for OpenApps server
|
| 420 |
+
headless=True, # Run browser in headless mode
|
| 421 |
+
task_name="add_meeting", # Optional task name
|
| 422 |
+
apps_config={}, # App-specific configuration
|
| 423 |
+
max_steps=50, # Maximum steps per episode
|
| 424 |
+
)
|
| 425 |
+
```
|
| 426 |
+
|
| 427 |
+
**Note:** OpenApps is automatically detected from the installed Python package. You can optionally override with `openapps_path` parameter or `OPENAPPS_PATH` environment variable if needed.
|
| 428 |
+
|
| 429 |
+
## Tasks and Rewards
|
| 430 |
+
|
| 431 |
+
The environment can be configured with specific tasks from OpenApps. Tasks define:
|
| 432 |
+
- Goal state (e.g., "Add a meeting with Dennis to the calendar")
|
| 433 |
+
- Reward function based on app state changes
|
| 434 |
+
- Success criteria
|
| 435 |
+
|
| 436 |
+
See [OpenApps documentation](https://facebookresearch.github.io/OpenApps/) for available tasks.
|
| 437 |
+
|
| 438 |
+
## Example: Task-Based Training
|
| 439 |
+
|
| 440 |
+
```python
|
| 441 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 442 |
+
|
| 443 |
+
# Create environment with a specific task
|
| 444 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 445 |
+
|
| 446 |
+
# The task will guide the agent toward a specific goal
|
| 447 |
+
# Rewards will be based on progress toward completing the task
|
| 448 |
+
result = client.reset()
|
| 449 |
+
|
| 450 |
+
# Agent interacts to complete the task
|
| 451 |
+
# ... agent logic here ...
|
| 452 |
+
|
| 453 |
+
client.close()
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
## Development
|
| 457 |
+
|
| 458 |
+
### Running Server Locally (without Docker)
|
| 459 |
+
|
| 460 |
+
```bash
|
| 461 |
+
cd envs/openapp_env
|
| 462 |
+
uv run server
|
| 463 |
+
```
|
| 464 |
+
|
| 465 |
+
The server will start at `http://localhost:8000`
|
| 466 |
+
|
| 467 |
+
### Testing
|
| 468 |
+
|
| 469 |
+
```python
|
| 470 |
+
from openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 471 |
+
from openapp_env.models import OpenAppAction
|
| 472 |
+
|
| 473 |
+
def test_environment():
|
| 474 |
+
env = OpenAppEnvironment()
|
| 475 |
+
|
| 476 |
+
# Test reset
|
| 477 |
+
obs = env.reset()
|
| 478 |
+
assert obs.url != ""
|
| 479 |
+
|
| 480 |
+
# Test step
|
| 481 |
+
action = OpenAppAction(action_type="noop")
|
| 482 |
+
obs = env.step(action)
|
| 483 |
+
assert env.state.step_count == 1
|
| 484 |
+
|
| 485 |
+
# Cleanup
|
| 486 |
+
env.close()
|
| 487 |
+
|
| 488 |
+
test_environment()
|
| 489 |
+
```
|
| 490 |
+
|
| 491 |
+
## Attribution
|
| 492 |
+
|
| 493 |
+
This environment integrates:
|
| 494 |
+
- [OpenApps](https://github.com/facebookresearch/OpenApps) - Web application simulation framework
|
| 495 |
+
- [BrowserGym](https://github.com/ServiceNow/BrowserGym) - Browser automation environment
|
| 496 |
+
|
| 497 |
+
## Troubleshooting
|
| 498 |
+
|
| 499 |
+
### Docker Build Issues
|
| 500 |
+
|
| 501 |
+
**Error: `Container did not become ready`**
|
| 502 |
+
|
| 503 |
+
If you're behind a corporate proxy (Meta/Facebook networks), set `NO_PROXY`:
|
| 504 |
+
```bash
|
| 505 |
+
export NO_PROXY=localhost,127.0.0.1
|
| 506 |
+
docker build -t openapp-env:latest -f envs/openapp_env/server/Dockerfile .
|
| 507 |
+
```
|
| 508 |
+
|
| 509 |
+
**Error: `Environment variable 'USER' not found`**
|
| 510 |
+
|
| 511 |
+
This is automatically handled in the Dockerfile with `ENV USER=root`. If you see this, rebuild the image.
|
| 512 |
+
|
| 513 |
+
**Container exits immediately**
|
| 514 |
+
|
| 515 |
+
Check the logs to see which server failed:
|
| 516 |
+
```bash
|
| 517 |
+
docker logs <container-id>
|
| 518 |
+
```
|
| 519 |
+
|
| 520 |
+
Common causes:
|
| 521 |
+
- OpenApps server failed to start (check for port conflicts)
|
| 522 |
+
- Missing dependencies (rebuild with `--no-cache`)
|
| 523 |
+
|
| 524 |
+
### Local Mode Issues
|
| 525 |
+
|
| 526 |
+
**Error: `OPENAPPS_URL not set`**
|
| 527 |
+
|
| 528 |
+
Set the environment variable before running:
|
| 529 |
+
```bash
|
| 530 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 531 |
+
python examples/openapp_example.py --mode local
|
| 532 |
+
```
|
| 533 |
+
|
| 534 |
+
**Error: `Connection refused to localhost:5001`**
|
| 535 |
+
|
| 536 |
+
Make sure the OpenApps server is running:
|
| 537 |
+
```bash
|
| 538 |
+
cd OpenApps
|
| 539 |
+
uv run launch.py
|
| 540 |
+
```
|
| 541 |
+
|
| 542 |
+
**Browser visualization not working**
|
| 543 |
+
|
| 544 |
+
The visualization is controlled by the **server**, not the client:
|
| 545 |
+
```bash
|
| 546 |
+
# Start server with visible browser
|
| 547 |
+
cd OpenApps
|
| 548 |
+
python launch.py browsergym_env_args.headless=False
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
### Performance Issues
|
| 552 |
+
|
| 553 |
+
**Docker container is slow**
|
| 554 |
+
|
| 555 |
+
The container runs both a full Chromium browser and web applications. For faster performance:
|
| 556 |
+
- Increase Docker memory allocation (6GB+ recommended)
|
| 557 |
+
- Use headless mode (default)
|
| 558 |
+
- Reduce `max_steps` in environment configuration
|
| 559 |
+
|
| 560 |
+
**Large Docker image size**
|
| 561 |
+
|
| 562 |
+
The image is ~5.7GB due to:
|
| 563 |
+
- Chromium browser (~1.5GB)
|
| 564 |
+
- OpenApps dependencies (~2GB)
|
| 565 |
+
- BrowserGym and ML libraries (~2GB)
|
| 566 |
+
|
| 567 |
+
This is expected for a full browser automation environment.
|
| 568 |
+
|
| 569 |
+
## License
|
| 570 |
+
|
| 571 |
+
BSD 3-Clause License (see LICENSE file in OpenEnv root directory)
|
| 572 |
+
|
| 573 |
+
## Citation
|
| 574 |
+
|
| 575 |
+
If you use this environment in your research, please cite both OpenEnv and OpenApps:
|
| 576 |
+
|
| 577 |
+
```bibtex
|
| 578 |
+
@article{ullrich2025openapps0,
|
| 579 |
+
title = {OpenApps: Simulating Environment Variations to Measure UI-Agent Reliability},
|
| 580 |
+
author = {Karen Ullrich and Jingtong Su and Claudia Shi and Arjun Subramonian and Amir Bar and Ivan Evtimov and Nikolaos Tsilivis and Randall Balestriero and Julia Kempe and Mark Ibrahim},
|
| 581 |
+
year = {2025},
|
| 582 |
+
journal = {arXiv preprint arXiv: 2511.20766}
|
| 583 |
+
}
|
| 584 |
+
```
|
envs/openapp_env/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""OpenApp Environment - Web application simulation environment for UI agents."""
|
| 8 |
+
|
| 9 |
+
from .client import OpenAppEnv
|
| 10 |
+
from .models import OpenAppAction, OpenAppObservation
|
| 11 |
+
|
| 12 |
+
__all__ = ["OpenAppAction", "OpenAppObservation", "OpenAppEnv"]
|
envs/openapp_env/assets/01-messages.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0b4333519dedb001fdc95c47430349e1e33f03daf0082b3d80d2e66f604e812e
|
| 3 |
+
size 10889481
|
envs/openapp_env/assets/02-editor.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49677c425607944fdcc4a35f0cefa55115b34fa1c61b8597074e5920e6be31ad
|
| 3 |
+
size 11875181
|
envs/openapp_env/assets/03-calendar.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9b4011bb80014623250bd375a3c52012f0a43469dcff966c0fe538727cd9c194
|
| 3 |
+
size 10553742
|
envs/openapp_env/assets/04-todo.mov
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6b2231f1e2587ddabbf59fb793693e645e907cf6d7dd5d6bd9c79cf781f016e6
|
| 3 |
+
size 9591496
|
envs/openapp_env/assets/OpenApps_OpenEnv_RL.png
ADDED
|
Git LFS Details
|
envs/openapp_env/assets/demo-showcase.html
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>OpenApp + OpenEnv Demo Showcase</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--primary-color: #6366f1;
|
| 11 |
+
--primary-dark: #4f46e5;
|
| 12 |
+
--secondary-color: #10b981;
|
| 13 |
+
--accent-color: #f59e0b;
|
| 14 |
+
--bg-dark: #0f172a;
|
| 15 |
+
--bg-card: #1e293b;
|
| 16 |
+
--bg-card-hover: #334155;
|
| 17 |
+
--text-primary: #f8fafc;
|
| 18 |
+
--text-secondary: #94a3b8;
|
| 19 |
+
--gradient-1: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
| 20 |
+
--gradient-2: linear-gradient(135deg, #10b981 0%, #14b8a6 100%);
|
| 21 |
+
--gradient-3: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
|
| 22 |
+
--gradient-4: linear-gradient(135deg, #ec4899 0%, #f43f5e 100%);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
* {
|
| 26 |
+
margin: 0;
|
| 27 |
+
padding: 0;
|
| 28 |
+
box-sizing: border-box;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 33 |
+
background: var(--bg-dark);
|
| 34 |
+
color: var(--text-primary);
|
| 35 |
+
min-height: 100vh;
|
| 36 |
+
overflow-x: hidden;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Animated background */
|
| 40 |
+
.bg-animation {
|
| 41 |
+
position: fixed;
|
| 42 |
+
top: 0;
|
| 43 |
+
left: 0;
|
| 44 |
+
width: 100%;
|
| 45 |
+
height: 100%;
|
| 46 |
+
z-index: -1;
|
| 47 |
+
overflow: hidden;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.bg-animation::before {
|
| 51 |
+
content: '';
|
| 52 |
+
position: absolute;
|
| 53 |
+
top: -50%;
|
| 54 |
+
left: -50%;
|
| 55 |
+
width: 200%;
|
| 56 |
+
height: 200%;
|
| 57 |
+
background: radial-gradient(circle at 20% 80%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
|
| 58 |
+
radial-gradient(circle at 80% 20%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
|
| 59 |
+
radial-gradient(circle at 40% 40%, rgba(245, 158, 11, 0.05) 0%, transparent 40%);
|
| 60 |
+
animation: bgRotate 30s linear infinite;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
@keyframes bgRotate {
|
| 64 |
+
0% { transform: rotate(0deg); }
|
| 65 |
+
100% { transform: rotate(360deg); }
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Header */
|
| 69 |
+
header {
|
| 70 |
+
text-align: center;
|
| 71 |
+
padding: 4rem 2rem 3rem;
|
| 72 |
+
position: relative;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.logo-container {
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
justify-content: center;
|
| 79 |
+
gap: 1rem;
|
| 80 |
+
margin-bottom: 1.5rem;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.logo-img {
|
| 84 |
+
width: 80px;
|
| 85 |
+
height: 80px;
|
| 86 |
+
border-radius: 16px;
|
| 87 |
+
box-shadow: 0 10px 40px rgba(99, 102, 241, 0.3);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
h1 {
|
| 91 |
+
font-size: 3.5rem;
|
| 92 |
+
font-weight: 800;
|
| 93 |
+
background: linear-gradient(135deg, #fff 0%, #a5b4fc 50%, #6366f1 100%);
|
| 94 |
+
-webkit-background-clip: text;
|
| 95 |
+
-webkit-text-fill-color: transparent;
|
| 96 |
+
background-clip: text;
|
| 97 |
+
margin-bottom: 1rem;
|
| 98 |
+
letter-spacing: -0.02em;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.subtitle {
|
| 102 |
+
font-size: 1.25rem;
|
| 103 |
+
color: var(--text-secondary);
|
| 104 |
+
max-width: 700px;
|
| 105 |
+
margin: 0 auto 2rem;
|
| 106 |
+
line-height: 1.6;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.badge-container {
|
| 110 |
+
display: flex;
|
| 111 |
+
justify-content: center;
|
| 112 |
+
gap: 1rem;
|
| 113 |
+
flex-wrap: wrap;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.badge {
|
| 117 |
+
display: inline-flex;
|
| 118 |
+
align-items: center;
|
| 119 |
+
gap: 0.5rem;
|
| 120 |
+
padding: 0.5rem 1rem;
|
| 121 |
+
background: var(--bg-card);
|
| 122 |
+
border-radius: 9999px;
|
| 123 |
+
font-size: 0.875rem;
|
| 124 |
+
font-weight: 500;
|
| 125 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.badge-icon {
|
| 129 |
+
width: 18px;
|
| 130 |
+
height: 18px;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
/* Main content */
|
| 134 |
+
main {
|
| 135 |
+
max-width: 1400px;
|
| 136 |
+
margin: 0 auto;
|
| 137 |
+
padding: 2rem;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Section title */
|
| 141 |
+
.section-title {
|
| 142 |
+
text-align: center;
|
| 143 |
+
margin-bottom: 3rem;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.section-title h2 {
|
| 147 |
+
font-size: 2rem;
|
| 148 |
+
font-weight: 700;
|
| 149 |
+
margin-bottom: 0.5rem;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.section-title p {
|
| 153 |
+
color: var(--text-secondary);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Demo grid */
|
| 157 |
+
.demo-grid {
|
| 158 |
+
display: grid;
|
| 159 |
+
grid-template-columns: repeat(2, 1fr);
|
| 160 |
+
gap: 2rem;
|
| 161 |
+
margin-bottom: 4rem;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
@media (max-width: 1024px) {
|
| 165 |
+
.demo-grid {
|
| 166 |
+
grid-template-columns: 1fr;
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Demo card */
|
| 171 |
+
.demo-card {
|
| 172 |
+
background: var(--bg-card);
|
| 173 |
+
border-radius: 20px;
|
| 174 |
+
overflow: hidden;
|
| 175 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 176 |
+
transition: all 0.3s ease;
|
| 177 |
+
position: relative;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.demo-card:hover {
|
| 181 |
+
transform: translateY(-5px);
|
| 182 |
+
border-color: rgba(99, 102, 241, 0.5);
|
| 183 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4),
|
| 184 |
+
0 0 40px rgba(99, 102, 241, 0.1);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.demo-card::before {
|
| 188 |
+
content: '';
|
| 189 |
+
position: absolute;
|
| 190 |
+
top: 0;
|
| 191 |
+
left: 0;
|
| 192 |
+
right: 0;
|
| 193 |
+
height: 4px;
|
| 194 |
+
opacity: 0;
|
| 195 |
+
transition: opacity 0.3s ease;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.demo-card:nth-child(1)::before { background: var(--gradient-1); }
|
| 199 |
+
.demo-card:nth-child(2)::before { background: var(--gradient-2); }
|
| 200 |
+
.demo-card:nth-child(3)::before { background: var(--gradient-3); }
|
| 201 |
+
.demo-card:nth-child(4)::before { background: var(--gradient-4); }
|
| 202 |
+
|
| 203 |
+
.demo-card:hover::before {
|
| 204 |
+
opacity: 1;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.video-container {
|
| 208 |
+
position: relative;
|
| 209 |
+
width: 100%;
|
| 210 |
+
background: #000;
|
| 211 |
+
aspect-ratio: 16 / 10;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.video-container video {
|
| 215 |
+
width: 100%;
|
| 216 |
+
height: 100%;
|
| 217 |
+
object-fit: contain;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.play-overlay {
|
| 221 |
+
position: absolute;
|
| 222 |
+
top: 0;
|
| 223 |
+
left: 0;
|
| 224 |
+
width: 100%;
|
| 225 |
+
height: 100%;
|
| 226 |
+
display: flex;
|
| 227 |
+
align-items: center;
|
| 228 |
+
justify-content: center;
|
| 229 |
+
background: rgba(0, 0, 0, 0.4);
|
| 230 |
+
opacity: 0;
|
| 231 |
+
transition: opacity 0.3s ease;
|
| 232 |
+
cursor: pointer;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.video-container:hover .play-overlay {
|
| 236 |
+
opacity: 1;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.play-overlay.hidden {
|
| 240 |
+
display: none;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.play-button {
|
| 244 |
+
width: 80px;
|
| 245 |
+
height: 80px;
|
| 246 |
+
background: rgba(255, 255, 255, 0.95);
|
| 247 |
+
border-radius: 50%;
|
| 248 |
+
display: flex;
|
| 249 |
+
align-items: center;
|
| 250 |
+
justify-content: center;
|
| 251 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
| 252 |
+
transition: transform 0.3s ease;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.play-button:hover {
|
| 256 |
+
transform: scale(1.1);
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.play-button svg {
|
| 260 |
+
width: 32px;
|
| 261 |
+
height: 32px;
|
| 262 |
+
fill: var(--bg-dark);
|
| 263 |
+
margin-left: 4px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.demo-info {
|
| 267 |
+
padding: 1.5rem;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.demo-header {
|
| 271 |
+
display: flex;
|
| 272 |
+
align-items: center;
|
| 273 |
+
gap: 0.75rem;
|
| 274 |
+
margin-bottom: 0.75rem;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.demo-icon {
|
| 278 |
+
width: 40px;
|
| 279 |
+
height: 40px;
|
| 280 |
+
border-radius: 10px;
|
| 281 |
+
display: flex;
|
| 282 |
+
align-items: center;
|
| 283 |
+
justify-content: center;
|
| 284 |
+
font-size: 1.25rem;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.demo-card:nth-child(1) .demo-icon { background: var(--gradient-1); }
|
| 288 |
+
.demo-card:nth-child(2) .demo-icon { background: var(--gradient-2); }
|
| 289 |
+
.demo-card:nth-child(3) .demo-icon { background: var(--gradient-3); }
|
| 290 |
+
.demo-card:nth-child(4) .demo-icon { background: var(--gradient-4); }
|
| 291 |
+
|
| 292 |
+
.demo-title {
|
| 293 |
+
font-size: 1.25rem;
|
| 294 |
+
font-weight: 600;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.demo-description {
|
| 298 |
+
color: var(--text-secondary);
|
| 299 |
+
font-size: 0.95rem;
|
| 300 |
+
line-height: 1.6;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* Features section */
|
| 304 |
+
.features-section {
|
| 305 |
+
margin-top: 4rem;
|
| 306 |
+
padding: 3rem;
|
| 307 |
+
background: var(--bg-card);
|
| 308 |
+
border-radius: 24px;
|
| 309 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.features-grid {
|
| 313 |
+
display: grid;
|
| 314 |
+
grid-template-columns: repeat(3, 1fr);
|
| 315 |
+
gap: 2rem;
|
| 316 |
+
margin-top: 2rem;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
@media (max-width: 768px) {
|
| 320 |
+
.features-grid {
|
| 321 |
+
grid-template-columns: 1fr;
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.feature-card {
|
| 326 |
+
text-align: center;
|
| 327 |
+
padding: 1.5rem;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.feature-icon {
|
| 331 |
+
width: 60px;
|
| 332 |
+
height: 60px;
|
| 333 |
+
background: var(--gradient-1);
|
| 334 |
+
border-radius: 16px;
|
| 335 |
+
display: flex;
|
| 336 |
+
align-items: center;
|
| 337 |
+
justify-content: center;
|
| 338 |
+
margin: 0 auto 1rem;
|
| 339 |
+
font-size: 1.5rem;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.feature-card:nth-child(2) .feature-icon { background: var(--gradient-2); }
|
| 343 |
+
.feature-card:nth-child(3) .feature-icon { background: var(--gradient-3); }
|
| 344 |
+
|
| 345 |
+
.feature-title {
|
| 346 |
+
font-size: 1.125rem;
|
| 347 |
+
font-weight: 600;
|
| 348 |
+
margin-bottom: 0.5rem;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.feature-desc {
|
| 352 |
+
color: var(--text-secondary);
|
| 353 |
+
font-size: 0.9rem;
|
| 354 |
+
line-height: 1.5;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
/* Footer */
|
| 358 |
+
footer {
|
| 359 |
+
text-align: center;
|
| 360 |
+
padding: 3rem 2rem;
|
| 361 |
+
color: var(--text-secondary);
|
| 362 |
+
font-size: 0.875rem;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
footer a {
|
| 366 |
+
color: var(--primary-color);
|
| 367 |
+
text-decoration: none;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
footer a:hover {
|
| 371 |
+
text-decoration: underline;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Video controls styling */
|
| 375 |
+
video::-webkit-media-controls-panel {
|
| 376 |
+
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
/* Responsive adjustments */
|
| 380 |
+
@media (max-width: 640px) {
|
| 381 |
+
h1 {
|
| 382 |
+
font-size: 2.5rem;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.subtitle {
|
| 386 |
+
font-size: 1rem;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.demo-grid {
|
| 390 |
+
gap: 1.5rem;
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
main {
|
| 394 |
+
padding: 1rem;
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
</style>
|
| 398 |
+
</head>
|
| 399 |
+
<body>
|
| 400 |
+
<div class="bg-animation"></div>
|
| 401 |
+
|
| 402 |
+
<header>
|
| 403 |
+
<div class="logo-container">
|
| 404 |
+
<img src="OpenApps_OpenEnv_RL.png" alt="OpenApp Logo" class="logo-img">
|
| 405 |
+
</div>
|
| 406 |
+
<h1>OpenApp + OpenEnv</h1>
|
| 407 |
+
<p class="subtitle">
|
| 408 |
+
A powerful integration bringing realistic web application environments to reinforcement learning agents.
|
| 409 |
+
Watch AI agents interact with calendar, messaging, code editor, and task management apps.
|
| 410 |
+
</p>
|
| 411 |
+
<div class="badge-container">
|
| 412 |
+
<span class="badge">
|
| 413 |
+
<svg class="badge-icon" viewBox="0 0 24 24" fill="currentColor">
|
| 414 |
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
| 415 |
+
</svg>
|
| 416 |
+
BrowserGym Compatible
|
| 417 |
+
</span>
|
| 418 |
+
<span class="badge">
|
| 419 |
+
<svg class="badge-icon" viewBox="0 0 24 24" fill="currentColor">
|
| 420 |
+
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
|
| 421 |
+
</svg>
|
| 422 |
+
5+ Web Apps
|
| 423 |
+
</span>
|
| 424 |
+
<span class="badge">
|
| 425 |
+
<svg class="badge-icon" viewBox="0 0 24 24" fill="currentColor">
|
| 426 |
+
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
| 427 |
+
</svg>
|
| 428 |
+
RL Ready
|
| 429 |
+
</span>
|
| 430 |
+
</div>
|
| 431 |
+
</header>
|
| 432 |
+
|
| 433 |
+
<main>
|
| 434 |
+
<div class="section-title">
|
| 435 |
+
<h2>🎬 Demo Showcase</h2>
|
| 436 |
+
<p>Watch our AI agents perform real tasks in realistic web environments</p>
|
| 437 |
+
</div>
|
| 438 |
+
|
| 439 |
+
<div class="demo-grid">
|
| 440 |
+
<!-- Messenger Demo -->
|
| 441 |
+
<div class="demo-card">
|
| 442 |
+
<div class="video-container">
|
| 443 |
+
<video id="video1" controls preload="metadata" poster="">
|
| 444 |
+
<source src="01-messages.mov" type="video/quicktime">
|
| 445 |
+
<source src="01-messages.mov" type="video/mp4">
|
| 446 |
+
Your browser does not support the video tag.
|
| 447 |
+
</video>
|
| 448 |
+
<div class="play-overlay" onclick="playVideo('video1', this)">
|
| 449 |
+
<div class="play-button">
|
| 450 |
+
<svg viewBox="0 0 24 24">
|
| 451 |
+
<path d="M8 5v14l11-7z"/>
|
| 452 |
+
</svg>
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="demo-info">
|
| 457 |
+
<div class="demo-header">
|
| 458 |
+
<div class="demo-icon">💬</div>
|
| 459 |
+
<h3 class="demo-title">Messenger App</h3>
|
| 460 |
+
</div>
|
| 461 |
+
<p class="demo-description">
|
| 462 |
+
AI agent navigates conversations, types messages interactively, and sends them to contacts.
|
| 463 |
+
Demonstrates natural language input and real-time chat interactions.
|
| 464 |
+
</p>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<!-- Code Editor Demo -->
|
| 469 |
+
<div class="demo-card">
|
| 470 |
+
<div class="video-container">
|
| 471 |
+
<video id="video2" controls preload="metadata">
|
| 472 |
+
<source src="02-editor.mov" type="video/quicktime">
|
| 473 |
+
<source src="02-editor.mov" type="video/mp4">
|
| 474 |
+
Your browser does not support the video tag.
|
| 475 |
+
</video>
|
| 476 |
+
<div class="play-overlay" onclick="playVideo('video2', this)">
|
| 477 |
+
<div class="play-button">
|
| 478 |
+
<svg viewBox="0 0 24 24">
|
| 479 |
+
<path d="M8 5v14l11-7z"/>
|
| 480 |
+
</svg>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
<div class="demo-info">
|
| 485 |
+
<div class="demo-header">
|
| 486 |
+
<div class="demo-icon">💻</div>
|
| 487 |
+
<h3 class="demo-title">Code Editor</h3>
|
| 488 |
+
</div>
|
| 489 |
+
<p class="demo-description">
|
| 490 |
+
Agent creates files and writes a complete PyTorch training loop with syntax highlighting.
|
| 491 |
+
Shows code generation, file management, and save operations.
|
| 492 |
+
</p>
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
|
| 496 |
+
<!-- Calendar Demo -->
|
| 497 |
+
<div class="demo-card">
|
| 498 |
+
<div class="video-container">
|
| 499 |
+
<video id="video3" controls preload="metadata">
|
| 500 |
+
<source src="03-calendar.mov" type="video/quicktime">
|
| 501 |
+
<source src="03-calendar.mov" type="video/mp4">
|
| 502 |
+
Your browser does not support the video tag.
|
| 503 |
+
</video>
|
| 504 |
+
<div class="play-overlay" onclick="playVideo('video3', this)">
|
| 505 |
+
<div class="play-button">
|
| 506 |
+
<svg viewBox="0 0 24 24">
|
| 507 |
+
<path d="M8 5v14l11-7z"/>
|
| 508 |
+
</svg>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
</div>
|
| 512 |
+
<div class="demo-info">
|
| 513 |
+
<div class="demo-header">
|
| 514 |
+
<div class="demo-icon">📅</div>
|
| 515 |
+
<h3 class="demo-title">Calendar App</h3>
|
| 516 |
+
</div>
|
| 517 |
+
<p class="demo-description">
|
| 518 |
+
Navigate between calendar and agenda views, browse events across months,
|
| 519 |
+
and view detailed event information. Perfect for scheduling tasks.
|
| 520 |
+
</p>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<!-- Todo Demo -->
|
| 525 |
+
<div class="demo-card">
|
| 526 |
+
<div class="video-container">
|
| 527 |
+
<video id="video4" controls preload="metadata">
|
| 528 |
+
<source src="04-todo.mov" type="video/quicktime">
|
| 529 |
+
<source src="04-todo.mov" type="video/mp4">
|
| 530 |
+
Your browser does not support the video tag.
|
| 531 |
+
</video>
|
| 532 |
+
<div class="play-overlay" onclick="playVideo('video4', this)">
|
| 533 |
+
<div class="play-button">
|
| 534 |
+
<svg viewBox="0 0 24 24">
|
| 535 |
+
<path d="M8 5v14l11-7z"/>
|
| 536 |
+
</svg>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
</div>
|
| 540 |
+
<div class="demo-info">
|
| 541 |
+
<div class="demo-header">
|
| 542 |
+
<div class="demo-icon">✅</div>
|
| 543 |
+
<h3 class="demo-title">Todo Manager</h3>
|
| 544 |
+
</div>
|
| 545 |
+
<p class="demo-description">
|
| 546 |
+
Browse and manage task lists, edit task details, mark items complete,
|
| 547 |
+
and organize priorities. Demonstrates CRUD operations on structured data.
|
| 548 |
+
</p>
|
| 549 |
+
</div>
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
|
| 553 |
+
<!-- Features Section -->
|
| 554 |
+
<div class="features-section">
|
| 555 |
+
<div class="section-title">
|
| 556 |
+
<h2>✨ Key Features</h2>
|
| 557 |
+
<p>Why OpenApp + OpenEnv is perfect for AI agent research</p>
|
| 558 |
+
</div>
|
| 559 |
+
<div class="features-grid">
|
| 560 |
+
<div class="feature-card">
|
| 561 |
+
<div class="feature-icon">🎮</div>
|
| 562 |
+
<h4 class="feature-title">Gymnasium Compatible</h4>
|
| 563 |
+
<p class="feature-desc">
|
| 564 |
+
Standard RL interface with observations, actions, and rewards.
|
| 565 |
+
Drop-in replacement for existing training pipelines.
|
| 566 |
+
</p>
|
| 567 |
+
</div>
|
| 568 |
+
<div class="feature-card">
|
| 569 |
+
<div class="feature-icon">🌐</div>
|
| 570 |
+
<h4 class="feature-title">Real Web Apps</h4>
|
| 571 |
+
<p class="feature-desc">
|
| 572 |
+
Authentic web applications with HTML, CSS, and JavaScript.
|
| 573 |
+
No simplified simulations – real browser interactions.
|
| 574 |
+
</p>
|
| 575 |
+
</div>
|
| 576 |
+
<div class="feature-card">
|
| 577 |
+
<div class="feature-icon">🔄</div>
|
| 578 |
+
<h4 class="feature-title">Configurable Tasks</h4>
|
| 579 |
+
<p class="feature-desc">
|
| 580 |
+
YAML-based configuration for custom scenarios, data, and rewards.
|
| 581 |
+
Easily create new training environments.
|
| 582 |
+
</p>
|
| 583 |
+
</div>
|
| 584 |
+
</div>
|
| 585 |
+
</div>
|
| 586 |
+
</main>
|
| 587 |
+
|
| 588 |
+
<footer>
|
| 589 |
+
<p>
|
| 590 |
+
Built for the <strong>OpenEnv Hackathon</strong> 🏆<br>
|
| 591 |
+
<a href="https://github.com/anthropics/anthropic-cookbook/tree/main/misc/openenv" target="_blank">OpenEnv Framework</a> •
|
| 592 |
+
<a href="https://github.com/ServiceNow/BrowserGym" target="_blank">BrowserGym</a>
|
| 593 |
+
</p>
|
| 594 |
+
</footer>
|
| 595 |
+
|
| 596 |
+
<script>
|
| 597 |
+
function playVideo(videoId, overlay) {
|
| 598 |
+
const video = document.getElementById(videoId);
|
| 599 |
+
video.play();
|
| 600 |
+
overlay.classList.add('hidden');
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
// Show overlay again when video ends or is paused
|
| 604 |
+
document.querySelectorAll('video').forEach(video => {
|
| 605 |
+
video.addEventListener('ended', function() {
|
| 606 |
+
const overlay = this.parentElement.querySelector('.play-overlay');
|
| 607 |
+
overlay.classList.remove('hidden');
|
| 608 |
+
});
|
| 609 |
+
});
|
| 610 |
+
|
| 611 |
+
// Add intersection observer for lazy loading
|
| 612 |
+
const videos = document.querySelectorAll('video');
|
| 613 |
+
const observer = new IntersectionObserver((entries) => {
|
| 614 |
+
entries.forEach(entry => {
|
| 615 |
+
if (entry.isIntersecting) {
|
| 616 |
+
entry.target.load();
|
| 617 |
+
}
|
| 618 |
+
});
|
| 619 |
+
}, { threshold: 0.25 });
|
| 620 |
+
|
| 621 |
+
videos.forEach(video => observer.observe(video));
|
| 622 |
+
</script>
|
| 623 |
+
</body>
|
| 624 |
+
</html>
|
envs/openapp_env/assets/openapps-demo.gif
ADDED
|
Git LFS Details
|
envs/openapp_env/client.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
OpenApp Environment HTTP Client.
|
| 9 |
+
|
| 10 |
+
This module provides the client for connecting to an OpenApp Environment server
|
| 11 |
+
over HTTP.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from typing import Any, Dict
|
| 15 |
+
|
| 16 |
+
# Support both in-repo and standalone imports
|
| 17 |
+
try:
|
| 18 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 19 |
+
from openenv.core.client_types import StepResult
|
| 20 |
+
from openenv.core.env_server.types import State
|
| 21 |
+
from openenv.core.env_client import EnvClient
|
| 22 |
+
from .models import OpenAppAction, OpenAppObservation
|
| 23 |
+
except ImportError:
|
| 24 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 25 |
+
from openenv.core.client_types import StepResult
|
| 26 |
+
from openenv.core.env_server.types import State
|
| 27 |
+
from openenv.core.env_client import EnvClient
|
| 28 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class OpenAppEnv(EnvClient[OpenAppAction, OpenAppObservation, State]):
|
| 32 |
+
"""
|
| 33 |
+
HTTP client for the OpenApp Environment.
|
| 34 |
+
|
| 35 |
+
This client connects to an OpenAppEnvironment HTTP server and provides
|
| 36 |
+
methods to interact with it: reset(), step(), and state access.
|
| 37 |
+
|
| 38 |
+
The OpenApp environment simulates web applications (calendar, todo, messenger, maps)
|
| 39 |
+
and allows agents to interact with them using browser-based actions.
|
| 40 |
+
|
| 41 |
+
Example:
|
| 42 |
+
>>> # Connect to a running server
|
| 43 |
+
>>> client = OpenAppEnv(base_url="http://localhost:8000")
|
| 44 |
+
>>> result = client.reset()
|
| 45 |
+
>>> print(result.observation.url)
|
| 46 |
+
>>>
|
| 47 |
+
>>> # Click on an element
|
| 48 |
+
>>> result = client.step(OpenAppAction(action_type="click", bid="123"))
|
| 49 |
+
>>> print(result.observation.html)
|
| 50 |
+
>>> print(result.reward)
|
| 51 |
+
|
| 52 |
+
Example with Docker:
|
| 53 |
+
>>> # Automatically start container and connect
|
| 54 |
+
>>> client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 55 |
+
>>> result = client.reset()
|
| 56 |
+
>>> # Fill a text field
|
| 57 |
+
>>> result = client.step(OpenAppAction(
|
| 58 |
+
... action_type="fill",
|
| 59 |
+
... bid="456",
|
| 60 |
+
... text="Meeting with team"
|
| 61 |
+
... ))
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def _step_payload(self, action: OpenAppAction) -> Dict:
|
| 65 |
+
"""
|
| 66 |
+
Convert OpenAppAction to JSON payload for step request.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
action: OpenAppAction instance
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
Dictionary representation suitable for JSON encoding
|
| 73 |
+
"""
|
| 74 |
+
payload = {
|
| 75 |
+
"action_type": action.action_type,
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# Add optional fields if present
|
| 79 |
+
if action.bid is not None:
|
| 80 |
+
payload["bid"] = action.bid
|
| 81 |
+
if action.text is not None:
|
| 82 |
+
payload["text"] = action.text
|
| 83 |
+
if action.value is not None:
|
| 84 |
+
payload["value"] = action.value
|
| 85 |
+
if action.url is not None:
|
| 86 |
+
payload["url"] = action.url
|
| 87 |
+
if action.direction is not None:
|
| 88 |
+
payload["direction"] = action.direction
|
| 89 |
+
if action.metadata:
|
| 90 |
+
payload["metadata"] = action.metadata
|
| 91 |
+
|
| 92 |
+
return payload
|
| 93 |
+
|
| 94 |
+
def _parse_result(self, payload: Dict) -> StepResult[OpenAppObservation]:
|
| 95 |
+
"""
|
| 96 |
+
Parse server response into StepResult[OpenAppObservation].
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
payload: JSON response from server
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
StepResult with OpenAppObservation
|
| 103 |
+
"""
|
| 104 |
+
obs_data = payload.get("observation", {})
|
| 105 |
+
observation = OpenAppObservation(
|
| 106 |
+
html=obs_data.get("html", ""),
|
| 107 |
+
url=obs_data.get("url", ""),
|
| 108 |
+
open_pages_urls=obs_data.get("open_pages_urls", []),
|
| 109 |
+
active_page_index=obs_data.get("active_page_index", 0),
|
| 110 |
+
screenshot=obs_data.get("screenshot"),
|
| 111 |
+
axtree_txt=obs_data.get("axtree_txt", ""),
|
| 112 |
+
app_state=obs_data.get("app_state", {}),
|
| 113 |
+
task_info=obs_data.get("task_info"),
|
| 114 |
+
last_action_error=obs_data.get("last_action_error"),
|
| 115 |
+
done=payload.get("done", False),
|
| 116 |
+
reward=payload.get("reward"),
|
| 117 |
+
metadata=obs_data.get("metadata", {}),
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
return StepResult(
|
| 121 |
+
observation=observation,
|
| 122 |
+
reward=payload.get("reward"),
|
| 123 |
+
done=payload.get("done", False),
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
def _parse_state(self, payload: Dict) -> State:
|
| 127 |
+
"""
|
| 128 |
+
Parse server response into State object.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
payload: JSON response from /state endpoint
|
| 132 |
+
|
| 133 |
+
Returns:
|
| 134 |
+
State object with episode_id and step_count
|
| 135 |
+
"""
|
| 136 |
+
return State(
|
| 137 |
+
episode_id=payload.get("episode_id"),
|
| 138 |
+
step_count=payload.get("step_count", 0),
|
| 139 |
+
)
|
envs/openapp_env/example_usage.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 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 |
+
"""
|
| 9 |
+
Example usage of OpenApp Environment.
|
| 10 |
+
|
| 11 |
+
This script demonstrates how to use the OpenApp environment with OpenEnv.
|
| 12 |
+
|
| 13 |
+
For a complete runnable example, see: examples/openapp_example.py
|
| 14 |
+
|
| 15 |
+
Visualization Options:
|
| 16 |
+
To see the browser window and watch agent interactions:
|
| 17 |
+
|
| 18 |
+
Terminal 1: Start OpenApps server with visible browser
|
| 19 |
+
cd OpenApps
|
| 20 |
+
python OpenApps/launch.py browsergym_env_args.headless=False
|
| 21 |
+
|
| 22 |
+
Terminal 2: Run your agent code
|
| 23 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 24 |
+
python examples/openapp_example.py --mode local
|
| 25 |
+
|
| 26 |
+
Or access OpenApps web interface at http://localhost:5001
|
| 27 |
+
Docker mode web interface at http://localhost:8000/web
|
| 28 |
+
|
| 29 |
+
Important:
|
| 30 |
+
Browser visualization is controlled by the OpenApps SERVER, not the client.
|
| 31 |
+
Launch the server with 'browsergym_env_args.headless=False' to see the browser.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
import sys
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
|
| 37 |
+
# Add src to path for local testing
|
| 38 |
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
| 39 |
+
|
| 40 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def example_basic_usage():
|
| 44 |
+
"""Basic usage example."""
|
| 45 |
+
print("=" * 60)
|
| 46 |
+
print("OpenApp Environment - Basic Usage Example")
|
| 47 |
+
print("=" * 60)
|
| 48 |
+
|
| 49 |
+
# Option 1: Connect to a running server
|
| 50 |
+
print("\nOption 1: Connect to running server")
|
| 51 |
+
print("client = OpenAppEnv(base_url='http://localhost:8000')")
|
| 52 |
+
|
| 53 |
+
# Option 2: Start from Docker image (recommended)
|
| 54 |
+
print("\nOption 2: Start from Docker image")
|
| 55 |
+
print("client = OpenAppEnv.from_docker_image('openapp-env:latest')")
|
| 56 |
+
|
| 57 |
+
print("\n" + "-" * 60)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def example_actions():
|
| 61 |
+
"""Example of different action types."""
|
| 62 |
+
print("\nExample Actions")
|
| 63 |
+
print("-" * 60)
|
| 64 |
+
|
| 65 |
+
# Navigate to a page
|
| 66 |
+
print("\n1. Navigate to calendar app:")
|
| 67 |
+
print("action = OpenAppAction(")
|
| 68 |
+
print(" action_type='goto',")
|
| 69 |
+
print(" url='http://localhost:5001/calendar'")
|
| 70 |
+
print(")")
|
| 71 |
+
print("result = client.step(action)")
|
| 72 |
+
|
| 73 |
+
# Click on an element
|
| 74 |
+
print("\n2. Click on a button:")
|
| 75 |
+
print("action = OpenAppAction(")
|
| 76 |
+
print(" action_type='click',")
|
| 77 |
+
print(" bid='add-event-btn' # BrowserGym element ID")
|
| 78 |
+
print(")")
|
| 79 |
+
print("result = client.step(action)")
|
| 80 |
+
|
| 81 |
+
# Fill a form field
|
| 82 |
+
print("\n3. Fill in text input:")
|
| 83 |
+
print("action = OpenAppAction(")
|
| 84 |
+
print(" action_type='fill',")
|
| 85 |
+
print(" bid='event-title-input',")
|
| 86 |
+
print(" text='Team Meeting'")
|
| 87 |
+
print(")")
|
| 88 |
+
print("result = client.step(action)")
|
| 89 |
+
|
| 90 |
+
# Select from dropdown
|
| 91 |
+
print("\n4. Select from dropdown:")
|
| 92 |
+
print("action = OpenAppAction(")
|
| 93 |
+
print(" action_type='select_option',")
|
| 94 |
+
print(" bid='time-select',")
|
| 95 |
+
print(" value='14:00'")
|
| 96 |
+
print(")")
|
| 97 |
+
print("result = client.step(action)")
|
| 98 |
+
|
| 99 |
+
# Scroll the page
|
| 100 |
+
print("\n5. Scroll down:")
|
| 101 |
+
print("action = OpenAppAction(")
|
| 102 |
+
print(" action_type='scroll',")
|
| 103 |
+
print(" direction='down'")
|
| 104 |
+
print(")")
|
| 105 |
+
print("result = client.step(action)")
|
| 106 |
+
|
| 107 |
+
# No operation
|
| 108 |
+
print("\n6. No operation (useful for observation):")
|
| 109 |
+
print("action = OpenAppAction(action_type='noop')")
|
| 110 |
+
print("result = client.step(action)")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def example_observations():
|
| 114 |
+
"""Example of observation structure."""
|
| 115 |
+
print("\n\nObservation Structure")
|
| 116 |
+
print("-" * 60)
|
| 117 |
+
|
| 118 |
+
print("\nAfter reset() or step(), you receive:")
|
| 119 |
+
print("result.observation.html # Current page HTML")
|
| 120 |
+
print("result.observation.url # Current URL")
|
| 121 |
+
print("result.observation.open_pages_urls # All open pages")
|
| 122 |
+
print("result.observation.axtree_txt # Accessibility tree")
|
| 123 |
+
print("result.observation.app_state # App states (calendar, todo, etc.)")
|
| 124 |
+
print("result.observation.task_info # Task information (if using tasks)")
|
| 125 |
+
print("result.observation.screenshot # Page screenshot (base64)")
|
| 126 |
+
print("result.observation.last_action_error # Error from last action")
|
| 127 |
+
print("result.reward # Step reward")
|
| 128 |
+
print("result.done # Episode done flag")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def example_complete_workflow():
|
| 132 |
+
"""Complete workflow example."""
|
| 133 |
+
print("\n\nComplete Workflow Example")
|
| 134 |
+
print("=" * 60)
|
| 135 |
+
|
| 136 |
+
example_code = """
|
| 137 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 138 |
+
|
| 139 |
+
# Create client (starts Docker container)
|
| 140 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
# Reset environment
|
| 144 |
+
result = client.reset()
|
| 145 |
+
print(f"Starting at: {result.observation.url}")
|
| 146 |
+
|
| 147 |
+
# Navigate to calendar
|
| 148 |
+
result = client.step(OpenAppAction(
|
| 149 |
+
action_type="goto",
|
| 150 |
+
url="http://localhost:5001/calendar"
|
| 151 |
+
))
|
| 152 |
+
|
| 153 |
+
# Click to add new event
|
| 154 |
+
result = client.step(OpenAppAction(
|
| 155 |
+
action_type="click",
|
| 156 |
+
bid="new-event-button"
|
| 157 |
+
))
|
| 158 |
+
|
| 159 |
+
# Fill event title
|
| 160 |
+
result = client.step(OpenAppAction(
|
| 161 |
+
action_type="fill",
|
| 162 |
+
bid="title-input",
|
| 163 |
+
text="Project Review Meeting"
|
| 164 |
+
))
|
| 165 |
+
|
| 166 |
+
# Fill event date
|
| 167 |
+
result = client.step(OpenAppAction(
|
| 168 |
+
action_type="fill",
|
| 169 |
+
bid="date-input",
|
| 170 |
+
text="2025-12-15"
|
| 171 |
+
))
|
| 172 |
+
|
| 173 |
+
# Submit form
|
| 174 |
+
result = client.step(OpenAppAction(
|
| 175 |
+
action_type="click",
|
| 176 |
+
bid="submit-button"
|
| 177 |
+
))
|
| 178 |
+
|
| 179 |
+
print(f"Reward: {result.reward}")
|
| 180 |
+
print(f"Done: {result.done}")
|
| 181 |
+
print(f"App State: {result.observation.app_state}")
|
| 182 |
+
|
| 183 |
+
finally:
|
| 184 |
+
# Always cleanup
|
| 185 |
+
client.close()
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
print(example_code)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def example_with_tasks():
|
| 192 |
+
"""Example using OpenApps tasks."""
|
| 193 |
+
print("\n\nUsing Tasks (Task-Based RL)")
|
| 194 |
+
print("=" * 60)
|
| 195 |
+
|
| 196 |
+
example_code = """
|
| 197 |
+
# Environment can be configured with specific tasks
|
| 198 |
+
# Tasks define goals and automatic reward calculation
|
| 199 |
+
|
| 200 |
+
from envs.openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 201 |
+
|
| 202 |
+
env = OpenAppEnvironment(
|
| 203 |
+
openapps_url="http://localhost:5001", # OpenApps server URL
|
| 204 |
+
task_name="add_meeting_with_dennis", # Optional task name
|
| 205 |
+
headless=False, # Set to False to watch the browser
|
| 206 |
+
max_steps=50,
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
obs = env.reset()
|
| 210 |
+
# Now the environment has a goal: add a meeting with Dennis
|
| 211 |
+
# Rewards will be based on progress toward this goal
|
| 212 |
+
|
| 213 |
+
# Agent loop
|
| 214 |
+
done = False
|
| 215 |
+
while not done:
|
| 216 |
+
action = agent.get_action(obs) # Your agent
|
| 217 |
+
obs = env.step(action)
|
| 218 |
+
done = obs.done
|
| 219 |
+
|
| 220 |
+
print(f"Task completed! Reward: {obs.reward}")
|
| 221 |
+
env.close()
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
print(example_code)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def example_visualization():
|
| 228 |
+
"""Example of visualization options."""
|
| 229 |
+
print("\n\nVisualization Options")
|
| 230 |
+
print("=" * 60)
|
| 231 |
+
|
| 232 |
+
example_code = """
|
| 233 |
+
# Option 1: Show browser window (watch agent in real-time)
|
| 234 |
+
from envs.openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 235 |
+
|
| 236 |
+
env = OpenAppEnvironment(
|
| 237 |
+
openapps_url="http://localhost:5001",
|
| 238 |
+
headless=False, # Show browser window
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
obs = env.reset()
|
| 242 |
+
# You'll see a browser window open!
|
| 243 |
+
|
| 244 |
+
# Option 2: Access web interface manually
|
| 245 |
+
# While OpenApps server is running, open in your browser:
|
| 246 |
+
# - Main: http://localhost:5001
|
| 247 |
+
# - Calendar: http://localhost:5001/calendar
|
| 248 |
+
# - Todo: http://localhost:5001/todo
|
| 249 |
+
# - Messenger: http://localhost:5001/messenger
|
| 250 |
+
# - Maps: http://localhost:5001/maps
|
| 251 |
+
|
| 252 |
+
# Option 3: Use the example script with --show-browser
|
| 253 |
+
# python examples/openapp_example.py --mode local --show-browser
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
print(example_code)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def main():
|
| 260 |
+
"""Run all examples."""
|
| 261 |
+
example_basic_usage()
|
| 262 |
+
example_actions()
|
| 263 |
+
example_observations()
|
| 264 |
+
example_complete_workflow()
|
| 265 |
+
example_with_tasks()
|
| 266 |
+
example_visualization()
|
| 267 |
+
|
| 268 |
+
print("\n" + "=" * 60)
|
| 269 |
+
print("For a complete runnable example:")
|
| 270 |
+
print(" python examples/openapp_example.py --mode local --show-browser")
|
| 271 |
+
print("\nFor more information, see:")
|
| 272 |
+
print("- README.md in this directory")
|
| 273 |
+
print("- OpenApps docs: https://facebookresearch.github.io/OpenApps/")
|
| 274 |
+
print("- OpenEnv docs: https://meta-pytorch.org/OpenEnv/")
|
| 275 |
+
print("=" * 60)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
main()
|
envs/openapp_env/models.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 the OpenApp Environment.
|
| 9 |
+
|
| 10 |
+
The OpenApp environment provides a simulated web application environment
|
| 11 |
+
for training and evaluating UI agents that interact with various apps
|
| 12 |
+
(calendar, todo, messenger, maps, etc.) using browser actions.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from typing import Any, Dict, List, Optional
|
| 16 |
+
|
| 17 |
+
from pydantic import Field
|
| 18 |
+
|
| 19 |
+
# Support both in-repo and standalone imports
|
| 20 |
+
try:
|
| 21 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 22 |
+
from openenv.core.env_server.types import Action, Observation
|
| 23 |
+
except ImportError:
|
| 24 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 25 |
+
from openenv.core.env_server.types import Action, Observation
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class OpenAppAction(Action):
|
| 29 |
+
"""
|
| 30 |
+
Action for the OpenApp environment.
|
| 31 |
+
|
| 32 |
+
Supports BrowserGym-style actions for web interaction:
|
| 33 |
+
- click: Click on an element (requires bid - BrowserGym ID)
|
| 34 |
+
- fill: Fill a text field (requires bid and text)
|
| 35 |
+
- select_option: Select from dropdown (requires bid and value)
|
| 36 |
+
- goto: Navigate to URL (requires url)
|
| 37 |
+
- scroll: Scroll the page (requires direction)
|
| 38 |
+
- send_keys: Send keyboard input (requires text)
|
| 39 |
+
- noop: No operation
|
| 40 |
+
|
| 41 |
+
Attributes:
|
| 42 |
+
action_type: Type of action to perform
|
| 43 |
+
bid: BrowserGym element ID (for click, fill, select_option)
|
| 44 |
+
text: Text content (for fill, send_keys)
|
| 45 |
+
value: Value to select (for select_option)
|
| 46 |
+
url: URL to navigate to (for goto)
|
| 47 |
+
direction: Scroll direction - 'up' or 'down' (for scroll)
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
action_type: str = Field(
|
| 51 |
+
..., description="Type of action: click, fill, select_option, goto, scroll, send_keys, noop"
|
| 52 |
+
)
|
| 53 |
+
bid: Optional[str] = Field(default=None, description="BrowserGym element ID")
|
| 54 |
+
text: Optional[str] = Field(default=None, description="Text content for fill or send_keys")
|
| 55 |
+
value: Optional[str] = Field(default=None, description="Value for select_option")
|
| 56 |
+
url: Optional[str] = Field(default=None, description="URL for goto action")
|
| 57 |
+
direction: Optional[str] = Field(default=None, description="Scroll direction: 'up' or 'down'")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class OpenAppObservation(Observation):
|
| 61 |
+
"""
|
| 62 |
+
Observation from the OpenApp environment.
|
| 63 |
+
|
| 64 |
+
Provides comprehensive state information about the web apps and browser state.
|
| 65 |
+
|
| 66 |
+
Attributes:
|
| 67 |
+
html: Current page HTML content
|
| 68 |
+
url: Current page URL
|
| 69 |
+
open_pages_urls: List of all open page URLs
|
| 70 |
+
active_page_index: Index of currently active page
|
| 71 |
+
screenshot: Base64-encoded screenshot (optional)
|
| 72 |
+
axtree_txt: Accessibility tree as text (for element interaction)
|
| 73 |
+
app_state: Current state of all apps (calendar, todo, messenger, map)
|
| 74 |
+
task_info: Information about the current task (if any)
|
| 75 |
+
last_action_error: Error message from last action (if failed)
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
html: str = Field(default="", description="Current page HTML content")
|
| 79 |
+
url: str = Field(default="", description="Current page URL")
|
| 80 |
+
open_pages_urls: List[str] = Field(default_factory=list, description="List of all open page URLs")
|
| 81 |
+
active_page_index: int = Field(default=0, ge=0, description="Index of currently active page")
|
| 82 |
+
screenshot: Optional[str] = Field(default=None, description="Base64-encoded screenshot")
|
| 83 |
+
axtree_txt: str = Field(default="", description="Accessibility tree as text")
|
| 84 |
+
app_state: Dict[str, Any] = Field(default_factory=dict, description="State of all apps")
|
| 85 |
+
task_info: Optional[Dict[str, Any]] = Field(default=None, description="Current task information")
|
| 86 |
+
last_action_error: Optional[str] = Field(default=None, description="Error from last action")
|
envs/openapp_env/openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: openapp_env
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
envs/openapp_env/pyproject.toml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-openapp_env"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "OpenApp Environment for OpenEnv - web application simulation environment for UI agents"
|
| 15 |
+
requires-python = ">=3.11,<3.14"
|
| 16 |
+
dependencies = [
|
| 17 |
+
# NOTE: openenv-core is NOT listed here to avoid openai version conflict
|
| 18 |
+
# It is installed separately in the Dockerfile with --no-deps to avoid
|
| 19 |
+
# openai>=2.7.2 conflicting with OpenApps' openai<2 requirement.
|
| 20 |
+
# For local development, install manually:
|
| 21 |
+
# pip install --no-deps "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git"
|
| 22 |
+
# pip install fastapi pydantic uvicorn requests websockets
|
| 23 |
+
#
|
| 24 |
+
# NOTE: open_apps is also NOT listed here for the same reason.
|
| 25 |
+
# Install manually for local development:
|
| 26 |
+
# pip install git+https://github.com/facebookresearch/OpenApps.git
|
| 27 |
+
#
|
| 28 |
+
# Server dependencies (these are installed by Dockerfile separately for openenv-core)
|
| 29 |
+
"fastapi>=0.115.0",
|
| 30 |
+
"pydantic>=2.0.0",
|
| 31 |
+
"uvicorn[standard]>=0.24.0",
|
| 32 |
+
"requests>=2.31.0",
|
| 33 |
+
"websockets>=15.0.1",
|
| 34 |
+
# BrowserGym dependencies
|
| 35 |
+
"browsergym>=0.13.3",
|
| 36 |
+
"playwright>=1.40.0",
|
| 37 |
+
# Additional dependencies for web app interaction
|
| 38 |
+
"python-multipart>=0.0.20",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
[project.optional-dependencies]
|
| 42 |
+
dev = [
|
| 43 |
+
"pytest>=8.0.0",
|
| 44 |
+
"pytest-cov>=4.0.0",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
[project.scripts]
|
| 48 |
+
server = "openapp_env.server.app:main"
|
| 49 |
+
|
| 50 |
+
[tool.setuptools]
|
| 51 |
+
packages = ["openapp_env", "openapp_env.server"]
|
| 52 |
+
package-dir = { "openapp_env" = ".", "openapp_env.server" = "server" }
|
| 53 |
+
|
| 54 |
+
[tool.setuptools.package-data]
|
| 55 |
+
openapp_env = ["**/*.yaml", "**/*.yml", "**/*.md"]
|
| 56 |
+
|
| 57 |
+
[tool.hatch.metadata]
|
| 58 |
+
allow-direct-references = true
|
envs/openapp_env/server/Dockerfile
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Dockerfile for OpenApp Environment
|
| 8 |
+
# This image provides OpenApps web application simulation for UI agent training
|
| 9 |
+
#
|
| 10 |
+
# This Dockerfile works for both local builds and HuggingFace Spaces deployment:
|
| 11 |
+
# - Local build: cd envs/openapp_env && docker build -t openapp-env:latest -f server/Dockerfile .
|
| 12 |
+
# - HuggingFace: Automatically deployed via `openenv push`
|
| 13 |
+
#
|
| 14 |
+
# Run with web interface:
|
| 15 |
+
# docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true openapp-env:latest
|
| 16 |
+
|
| 17 |
+
FROM python:3.11-slim
|
| 18 |
+
|
| 19 |
+
# Set metadata
|
| 20 |
+
LABEL maintainer="OpenEnv Team"
|
| 21 |
+
LABEL description="OpenApp Environment with BrowserGym for UI agent training"
|
| 22 |
+
LABEL org.opencontainers.image.source="https://github.com/meta-pytorch/OpenEnv"
|
| 23 |
+
|
| 24 |
+
# Set working directory
|
| 25 |
+
WORKDIR /app/env
|
| 26 |
+
|
| 27 |
+
# Install system dependencies
|
| 28 |
+
# - git: required to clone OpenApps from GitHub
|
| 29 |
+
# - curl: for healthcheck
|
| 30 |
+
# - Playwright/BrowserGym dependencies: fonts, libraries for browser automation
|
| 31 |
+
RUN apt-get update && \
|
| 32 |
+
apt-get install -y --no-install-recommends \
|
| 33 |
+
git \
|
| 34 |
+
curl \
|
| 35 |
+
ca-certificates \
|
| 36 |
+
wget \
|
| 37 |
+
gnupg \
|
| 38 |
+
# Playwright/Chromium dependencies
|
| 39 |
+
libnss3 \
|
| 40 |
+
libnspr4 \
|
| 41 |
+
libatk1.0-0 \
|
| 42 |
+
libatk-bridge2.0-0 \
|
| 43 |
+
libcups2 \
|
| 44 |
+
libdrm2 \
|
| 45 |
+
libdbus-1-3 \
|
| 46 |
+
libxkbcommon0 \
|
| 47 |
+
libxcomposite1 \
|
| 48 |
+
libxdamage1 \
|
| 49 |
+
libxfixes3 \
|
| 50 |
+
libxrandr2 \
|
| 51 |
+
libgbm1 \
|
| 52 |
+
libasound2 \
|
| 53 |
+
libpango-1.0-0 \
|
| 54 |
+
libcairo2 \
|
| 55 |
+
libatspi2.0-0 \
|
| 56 |
+
libxshmfence1 \
|
| 57 |
+
fonts-liberation \
|
| 58 |
+
libappindicator3-1 \
|
| 59 |
+
xdg-utils && \
|
| 60 |
+
rm -rf /var/lib/apt/lists/*
|
| 61 |
+
|
| 62 |
+
# Set environment variables
|
| 63 |
+
ENV PYTHONUNBUFFERED=1
|
| 64 |
+
|
| 65 |
+
# Set working directory
|
| 66 |
+
WORKDIR /app/env
|
| 67 |
+
|
| 68 |
+
# Copy environment files
|
| 69 |
+
# Context is always the env directory (envs/openapp_env/)
|
| 70 |
+
# - GitHub Actions: uses context: envs/openapp_env
|
| 71 |
+
# - HuggingFace: openenv push uploads env dir as context
|
| 72 |
+
COPY . /app/env
|
| 73 |
+
|
| 74 |
+
# Install OpenApps FIRST to establish openai<2 (required by agentlab)
|
| 75 |
+
# This must happen before openenv-core to avoid version conflict
|
| 76 |
+
WORKDIR /app
|
| 77 |
+
RUN git clone https://github.com/facebookresearch/OpenApps.git openapps && \
|
| 78 |
+
cd openapps && \
|
| 79 |
+
pip install --no-cache-dir -e .
|
| 80 |
+
|
| 81 |
+
# Verify OpenApps installation
|
| 82 |
+
RUN python -c "import open_apps; print('✓ OpenApps installed')"
|
| 83 |
+
|
| 84 |
+
# Install openenv-core from GitHub with --no-deps to avoid openai>=2.7.2 conflict
|
| 85 |
+
# Then install only the server dependencies (no openai needed for server)
|
| 86 |
+
RUN pip install --no-cache-dir --no-deps "openenv-core[core]>=0.2.1" && \
|
| 87 |
+
pip install --no-cache-dir fastapi pydantic uvicorn requests websockets
|
| 88 |
+
|
| 89 |
+
# Install openapp_env and remaining dependencies
|
| 90 |
+
WORKDIR /app/env
|
| 91 |
+
RUN pip install --no-cache-dir -e .
|
| 92 |
+
|
| 93 |
+
# Verify installation
|
| 94 |
+
RUN python -c "import openapp_env; print('✓ openapp_env installed')" && \
|
| 95 |
+
python -c "import openapp_env.server.app; print('✓ openapp_env.server.app importable')"
|
| 96 |
+
|
| 97 |
+
# Install Playwright browsers (Chromium for BrowserGym)
|
| 98 |
+
# We already installed system dependencies above, so just install the browser
|
| 99 |
+
RUN playwright install chromium
|
| 100 |
+
|
| 101 |
+
# Copy startup script
|
| 102 |
+
WORKDIR /app/env
|
| 103 |
+
COPY server/start.sh /app/start.sh
|
| 104 |
+
RUN chmod +x /app/start.sh
|
| 105 |
+
|
| 106 |
+
# OpenApp-specific environment variables (can be overridden at runtime)
|
| 107 |
+
ENV OPENAPPS_URL=http://localhost:5001
|
| 108 |
+
ENV OPENAPPS_PORT=5001
|
| 109 |
+
ENV OPENAPP_HEADLESS=true
|
| 110 |
+
ENV OPENAPP_MAX_STEPS=50
|
| 111 |
+
|
| 112 |
+
# Hydra requires USER environment variable
|
| 113 |
+
ENV USER=root
|
| 114 |
+
|
| 115 |
+
# Enable web interface by default (set to false to disable)
|
| 116 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 117 |
+
|
| 118 |
+
# Expose ports (8000 for FastAPI, 5001 for OpenApps)
|
| 119 |
+
EXPOSE 8000 5001
|
| 120 |
+
|
| 121 |
+
# Health check
|
| 122 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
| 123 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 124 |
+
|
| 125 |
+
# Run the startup script that launches both OpenApps server and FastAPI server
|
| 126 |
+
# Web interface will be available at /web if ENABLE_WEB_INTERFACE=true
|
| 127 |
+
# API documentation available at /docs
|
| 128 |
+
CMD ["/app/start.sh"]
|
envs/openapp_env/server/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""OpenApp Environment Server."""
|
envs/openapp_env/server/app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 OpenApp Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the OpenAppEnvironment
|
| 11 |
+
over HTTP endpoints, making it compatible with HTTPEnvClient.
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
# Development (with auto-reload):
|
| 15 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 16 |
+
|
| 17 |
+
# Production:
|
| 18 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 19 |
+
|
| 20 |
+
# Or run directly:
|
| 21 |
+
uv run --project . server
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
# Support both in-repo and standalone imports
|
| 25 |
+
try:
|
| 26 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 27 |
+
from openenv.core.env_server.http_server import create_app
|
| 28 |
+
from ..models import OpenAppAction, OpenAppObservation
|
| 29 |
+
from .openapp_environment import OpenAppEnvironment
|
| 30 |
+
except ImportError:
|
| 31 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 32 |
+
from openenv.core.env_server.http_server import create_app
|
| 33 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 34 |
+
from openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 35 |
+
|
| 36 |
+
# Create the app with web interface and README integration
|
| 37 |
+
# Pass the class (factory) instead of an instance for WebSocket session support
|
| 38 |
+
# Each client gets its own environment instance. The environment reads
|
| 39 |
+
# OPENAPPS_URL from environment variables in __init__.
|
| 40 |
+
app = create_app(OpenAppEnvironment, OpenAppAction, OpenAppObservation, env_name="openapp_env")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main():
|
| 44 |
+
"""
|
| 45 |
+
Entry point for direct execution via uv run or python -m.
|
| 46 |
+
|
| 47 |
+
This function enables running the server without Docker:
|
| 48 |
+
uv run --project . server
|
| 49 |
+
python -m envs.openapp_env.server.app
|
| 50 |
+
openenv serve openapp_env
|
| 51 |
+
|
| 52 |
+
"""
|
| 53 |
+
import uvicorn
|
| 54 |
+
|
| 55 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
main()
|
envs/openapp_env/server/openapp_environment.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
OpenApp Environment Implementation.
|
| 9 |
+
|
| 10 |
+
A web application simulation environment that wraps OpenApps and BrowserGym.
|
| 11 |
+
This environment provides agent interaction with simulated web apps including
|
| 12 |
+
calendar, todo, messenger, and maps applications.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
import subprocess
|
| 18 |
+
import time
|
| 19 |
+
import urllib.request
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any, Dict, Optional, Tuple
|
| 22 |
+
from uuid import uuid4
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
# Support both in-repo and standalone imports
|
| 27 |
+
try:
|
| 28 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 29 |
+
from openenv.core.env_server.interfaces import Environment
|
| 30 |
+
from openenv.core.env_server.types import State
|
| 31 |
+
from ..models import OpenAppAction, OpenAppObservation
|
| 32 |
+
except ImportError:
|
| 33 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 34 |
+
from openenv.core.env_server.interfaces import Environment
|
| 35 |
+
from openenv.core.env_server.types import State
|
| 36 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class GenericOpenAppsTask:
|
| 40 |
+
"""
|
| 41 |
+
A generic task for OpenApps interaction without specific goals.
|
| 42 |
+
|
| 43 |
+
This is a simple wrapper that allows BrowserGym to interact with OpenApps
|
| 44 |
+
without requiring a specific task. For task-based interaction, use the
|
| 45 |
+
OpenAppsTask from open_apps.tasks.add_tasks_to_browsergym.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
base_url: str,
|
| 51 |
+
seed: int = 1,
|
| 52 |
+
**kwargs,
|
| 53 |
+
) -> None:
|
| 54 |
+
"""
|
| 55 |
+
Initialize generic OpenApps task.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
base_url: Base URL of the OpenApps server
|
| 59 |
+
seed: Random seed (required by BrowserGym)
|
| 60 |
+
**kwargs: Additional arguments (ignored)
|
| 61 |
+
"""
|
| 62 |
+
try:
|
| 63 |
+
from browsergym.core.task import AbstractBrowserTask
|
| 64 |
+
import playwright.sync_api
|
| 65 |
+
except ImportError:
|
| 66 |
+
raise ImportError(
|
| 67 |
+
"BrowserGym is required. Install with: pip install browsergym"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Store as instance attributes
|
| 71 |
+
self.base_url = base_url
|
| 72 |
+
self.seed = seed
|
| 73 |
+
|
| 74 |
+
# BrowserGym task properties
|
| 75 |
+
self.viewport = {"width": 1024, "height": 768}
|
| 76 |
+
self.slow_mo = 100
|
| 77 |
+
self.timeout = 5000
|
| 78 |
+
|
| 79 |
+
# Additional properties that BrowserGym might expect
|
| 80 |
+
self.locale = None
|
| 81 |
+
self.timezone_id = None
|
| 82 |
+
self.geolocation = None
|
| 83 |
+
|
| 84 |
+
def setup(
|
| 85 |
+
self, page: "playwright.sync_api.Page"
|
| 86 |
+
) -> Tuple[str, Dict[str, Any]]:
|
| 87 |
+
"""
|
| 88 |
+
Set up the task by navigating to the base URL.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
page: Playwright page object
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
Tuple of (goal_string, info_dict)
|
| 95 |
+
"""
|
| 96 |
+
page.goto(self.base_url)
|
| 97 |
+
return "Explore OpenApps", {}
|
| 98 |
+
|
| 99 |
+
def teardown(self) -> None:
|
| 100 |
+
"""Clean up after task completion."""
|
| 101 |
+
pass
|
| 102 |
+
|
| 103 |
+
def validate(
|
| 104 |
+
self, page: "playwright.sync_api.Page", chat_messages: list[str]
|
| 105 |
+
) -> Tuple[float, bool, str, Dict[str, Any]]:
|
| 106 |
+
"""
|
| 107 |
+
Validate task state and return reward.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
page: Playwright page object
|
| 111 |
+
chat_messages: List of chat messages
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Tuple of (reward, done, message, info)
|
| 115 |
+
"""
|
| 116 |
+
# Generic task never completes automatically
|
| 117 |
+
return 0.0, False, "", {}
|
| 118 |
+
|
| 119 |
+
def cheat(
|
| 120 |
+
self, page: "playwright.sync_api.Page", chat_messages: list[str]
|
| 121 |
+
) -> None:
|
| 122 |
+
"""Cheat method (no-op for generic task)."""
|
| 123 |
+
pass
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class OpenAppEnvironment(Environment):
|
| 127 |
+
"""
|
| 128 |
+
A web application environment that wraps OpenApps and BrowserGym.
|
| 129 |
+
|
| 130 |
+
This environment launches OpenApps web server and provides a BrowserGym-like
|
| 131 |
+
interface for agents to interact with simulated web applications.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
openapps_path: Path to OpenApps directory (default: auto-detect)
|
| 135 |
+
web_app_port: Port for OpenApps web server (default: 5001)
|
| 136 |
+
headless: Run browser in headless mode (default: True)
|
| 137 |
+
task_name: Optional task name to evaluate (e.g., "add_meeting_with_dennis")
|
| 138 |
+
apps_config: Configuration for apps (default: all enabled)
|
| 139 |
+
max_steps: Maximum steps per episode (default: 50)
|
| 140 |
+
|
| 141 |
+
Example:
|
| 142 |
+
>>> env = OpenAppEnvironment()
|
| 143 |
+
>>> obs = env.reset()
|
| 144 |
+
>>> print(obs.url) # Starting page URL
|
| 145 |
+
>>>
|
| 146 |
+
>>> # Click on an element
|
| 147 |
+
>>> action = OpenAppAction(action_type="click", bid="calendar-btn")
|
| 148 |
+
>>> obs = env.step(action)
|
| 149 |
+
>>> print(obs.html)
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
openapps_url: Optional[str] = None,
|
| 155 |
+
openapps_path: Optional[str] = None,
|
| 156 |
+
web_app_port: int = 5001,
|
| 157 |
+
headless: bool = True,
|
| 158 |
+
task_name: Optional[str] = None,
|
| 159 |
+
apps_config: Optional[Dict[str, Any]] = None,
|
| 160 |
+
max_steps: int = 50,
|
| 161 |
+
):
|
| 162 |
+
"""Initialize the OpenApp environment."""
|
| 163 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 164 |
+
self._max_steps = max_steps
|
| 165 |
+
|
| 166 |
+
# OpenApps configuration
|
| 167 |
+
# Priority: 1. openapps_url, 2. OPENAPPS_URL env var, 3. Try to find/launch
|
| 168 |
+
self.openapps_url = openapps_url or os.environ.get("OPENAPPS_URL")
|
| 169 |
+
if not self.openapps_url:
|
| 170 |
+
self.web_app_port = web_app_port
|
| 171 |
+
self.openapps_url = f"http://localhost:{web_app_port}"
|
| 172 |
+
|
| 173 |
+
self.openapps_path = openapps_path
|
| 174 |
+
self.headless = headless
|
| 175 |
+
self.task_name = task_name
|
| 176 |
+
self.apps_config = apps_config or {}
|
| 177 |
+
|
| 178 |
+
# Runtime state
|
| 179 |
+
self._apps_process: Optional[subprocess.Popen] = None
|
| 180 |
+
self._browser_env = None
|
| 181 |
+
self._current_html = ""
|
| 182 |
+
self._current_url = ""
|
| 183 |
+
self._current_axtree = ""
|
| 184 |
+
self._app_state = {}
|
| 185 |
+
self._last_action_error = None
|
| 186 |
+
self._episode_reward = 0.0
|
| 187 |
+
|
| 188 |
+
def _detect_openapps_path(self) -> str:
|
| 189 |
+
"""
|
| 190 |
+
Auto-detect OpenApps path.
|
| 191 |
+
|
| 192 |
+
Since OpenApps is installed as a Python package, we use the installed
|
| 193 |
+
package location instead of requiring a separate directory.
|
| 194 |
+
"""
|
| 195 |
+
# Check if user provided a custom path via environment variable
|
| 196 |
+
env_path = os.environ.get("OPENAPPS_PATH")
|
| 197 |
+
if env_path and Path(env_path).exists():
|
| 198 |
+
return env_path
|
| 199 |
+
|
| 200 |
+
# Try to find OpenApps as an installed package
|
| 201 |
+
try:
|
| 202 |
+
import open_apps
|
| 203 |
+
|
| 204 |
+
openapps_pkg_path = Path(open_apps.__file__).parent.parent
|
| 205 |
+
if openapps_pkg_path.exists():
|
| 206 |
+
return str(openapps_pkg_path)
|
| 207 |
+
except ImportError:
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
raise ValueError(
|
| 211 |
+
"OpenApps not found. Please install it with: "
|
| 212 |
+
"pip install git+https://github.com/facebookresearch/OpenApps.git "
|
| 213 |
+
"or set OPENAPPS_PATH environment variable."
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
def _launch_openapps_server(self) -> Optional[subprocess.Popen]:
|
| 217 |
+
"""
|
| 218 |
+
Launch OpenApps web server in background.
|
| 219 |
+
|
| 220 |
+
Returns None if server is expected to be already running (OPENAPPS_URL set).
|
| 221 |
+
"""
|
| 222 |
+
# If OPENAPPS_URL is set, assume server is already running
|
| 223 |
+
if os.environ.get("OPENAPPS_URL"):
|
| 224 |
+
logger.info(f"Using existing OpenApps server at {self.openapps_url}")
|
| 225 |
+
# Wait for server to be available
|
| 226 |
+
self._wait_for_server(max_wait=5)
|
| 227 |
+
return None
|
| 228 |
+
|
| 229 |
+
# Otherwise, provide helpful error message
|
| 230 |
+
raise NotImplementedError(
|
| 231 |
+
"Automatic OpenApps server launch is not yet implemented.\n"
|
| 232 |
+
"\n"
|
| 233 |
+
"Please start OpenApps manually in a separate terminal:\n"
|
| 234 |
+
" 1. Clone OpenApps: git clone https://github.com/facebookresearch/OpenApps.git\n"
|
| 235 |
+
" 2. Install: cd OpenApps && uv sync\n"
|
| 236 |
+
" 3. Run: uv run launch.py\n"
|
| 237 |
+
"\n"
|
| 238 |
+
"Then set the OPENAPPS_URL environment variable:\n"
|
| 239 |
+
" export OPENAPPS_URL=http://localhost:5001\n"
|
| 240 |
+
"\n"
|
| 241 |
+
"Or use Docker mode which handles this automatically:\n"
|
| 242 |
+
" python examples/openapp_example.py --mode docker\n"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
def _wait_for_server(self, max_wait: int = 30):
|
| 246 |
+
"""Wait for OpenApps server to become available."""
|
| 247 |
+
for i in range(max_wait):
|
| 248 |
+
try:
|
| 249 |
+
response = urllib.request.urlopen(self.openapps_url, timeout=2)
|
| 250 |
+
if response.status == 200:
|
| 251 |
+
return
|
| 252 |
+
except Exception:
|
| 253 |
+
pass
|
| 254 |
+
time.sleep(1)
|
| 255 |
+
|
| 256 |
+
raise TimeoutError(f"OpenApps server did not start within {max_wait} seconds")
|
| 257 |
+
|
| 258 |
+
def _initialize_browser(self):
|
| 259 |
+
"""Initialize BrowserGym environment for interaction."""
|
| 260 |
+
try:
|
| 261 |
+
from browsergym.core.env import BrowserEnv
|
| 262 |
+
except ImportError:
|
| 263 |
+
raise ImportError(
|
| 264 |
+
"BrowserGym is required for OpenApp environment. "
|
| 265 |
+
"Install it with: pip install browsergym"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Create BrowserGym environment with generic OpenApps task
|
| 269 |
+
self._browser_env = BrowserEnv(
|
| 270 |
+
task_entrypoint=GenericOpenAppsTask,
|
| 271 |
+
task_kwargs={"base_url": self.openapps_url},
|
| 272 |
+
headless=self.headless,
|
| 273 |
+
slow_mo=200, # Slow down actions so they're visible (200ms delay)
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
def _get_current_observation(self) -> Dict[str, Any]:
|
| 277 |
+
"""Extract current observation from browser state."""
|
| 278 |
+
if self._browser_env is None:
|
| 279 |
+
return {
|
| 280 |
+
"html": "",
|
| 281 |
+
"url": self.openapps_url,
|
| 282 |
+
"open_pages_urls": [self.openapps_url],
|
| 283 |
+
"active_page_index": 0,
|
| 284 |
+
"axtree_txt": "",
|
| 285 |
+
"app_state": {},
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# Get browser state (implementation depends on BrowserGym API)
|
| 289 |
+
# This is a simplified version - actual implementation would use BrowserGym's observation
|
| 290 |
+
return {
|
| 291 |
+
"html": self._current_html,
|
| 292 |
+
"url": self._current_url,
|
| 293 |
+
"open_pages_urls": [self._current_url],
|
| 294 |
+
"active_page_index": 0,
|
| 295 |
+
"axtree_txt": self._current_axtree,
|
| 296 |
+
"app_state": self._app_state,
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
def reset(self) -> OpenAppObservation:
|
| 300 |
+
"""
|
| 301 |
+
Reset the environment.
|
| 302 |
+
|
| 303 |
+
Returns:
|
| 304 |
+
OpenAppObservation with initial state
|
| 305 |
+
"""
|
| 306 |
+
# Reset state
|
| 307 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 308 |
+
self._episode_reward = 0.0
|
| 309 |
+
self._last_action_error = None
|
| 310 |
+
|
| 311 |
+
# Check if OpenApps server is running, start if needed
|
| 312 |
+
if self._apps_process is None and not os.environ.get("OPENAPPS_URL"):
|
| 313 |
+
self._apps_process = self._launch_openapps_server()
|
| 314 |
+
|
| 315 |
+
# Initialize browser
|
| 316 |
+
if self._browser_env is None:
|
| 317 |
+
self._initialize_browser()
|
| 318 |
+
|
| 319 |
+
# Reset the BrowserGym environment
|
| 320 |
+
try:
|
| 321 |
+
obs, info = self._browser_env.reset()
|
| 322 |
+
# Extract observation data from BrowserGym
|
| 323 |
+
self._current_url = obs.get("url", self.openapps_url)
|
| 324 |
+
self._current_html = obs.get("dom_txt", "")
|
| 325 |
+
self._current_axtree = obs.get("axtree_txt", "")
|
| 326 |
+
self._app_state = {}
|
| 327 |
+
except Exception as e:
|
| 328 |
+
logger.warning(f"Failed to reset browser environment: {e}")
|
| 329 |
+
# Fallback to placeholder values
|
| 330 |
+
self._current_url = self.openapps_url
|
| 331 |
+
self._current_html = "<html><body>OpenApps Ready</body></html>"
|
| 332 |
+
self._current_axtree = ""
|
| 333 |
+
self._app_state = {}
|
| 334 |
+
|
| 335 |
+
obs_data = self._get_current_observation()
|
| 336 |
+
|
| 337 |
+
return OpenAppObservation(
|
| 338 |
+
html=obs_data["html"],
|
| 339 |
+
url=obs_data["url"],
|
| 340 |
+
open_pages_urls=obs_data["open_pages_urls"],
|
| 341 |
+
active_page_index=obs_data["active_page_index"],
|
| 342 |
+
axtree_txt=obs_data["axtree_txt"],
|
| 343 |
+
app_state=obs_data["app_state"],
|
| 344 |
+
task_info={"task_name": self.task_name} if self.task_name else None,
|
| 345 |
+
last_action_error=None,
|
| 346 |
+
done=False,
|
| 347 |
+
reward=0.0,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
def step(self, action: OpenAppAction) -> OpenAppObservation: # type: ignore[override]
|
| 351 |
+
"""
|
| 352 |
+
Execute a step in the environment.
|
| 353 |
+
|
| 354 |
+
Args:
|
| 355 |
+
action: OpenAppAction to execute
|
| 356 |
+
|
| 357 |
+
Returns:
|
| 358 |
+
OpenAppObservation with resulting state and reward
|
| 359 |
+
"""
|
| 360 |
+
self._state.step_count += 1
|
| 361 |
+
self._last_action_error = None
|
| 362 |
+
reward = 0.0
|
| 363 |
+
|
| 364 |
+
try:
|
| 365 |
+
# Execute action based on type
|
| 366 |
+
if action.action_type == "click":
|
| 367 |
+
reward = self._execute_click(action.bid)
|
| 368 |
+
elif action.action_type == "fill":
|
| 369 |
+
reward = self._execute_fill(action.bid, action.text)
|
| 370 |
+
elif action.action_type == "select_option":
|
| 371 |
+
reward = self._execute_select(action.bid, action.value)
|
| 372 |
+
elif action.action_type == "goto":
|
| 373 |
+
reward = self._execute_goto(action.url)
|
| 374 |
+
elif action.action_type == "scroll":
|
| 375 |
+
reward = self._execute_scroll(action.direction)
|
| 376 |
+
elif action.action_type == "send_keys":
|
| 377 |
+
reward = self._execute_send_keys(action.text)
|
| 378 |
+
elif action.action_type == "noop":
|
| 379 |
+
reward = 0.0
|
| 380 |
+
else:
|
| 381 |
+
self._last_action_error = f"Unknown action type: {action.action_type}"
|
| 382 |
+
reward = -0.1
|
| 383 |
+
|
| 384 |
+
except Exception as e:
|
| 385 |
+
self._last_action_error = str(e)
|
| 386 |
+
reward = -0.1
|
| 387 |
+
|
| 388 |
+
# Update cumulative reward
|
| 389 |
+
self._episode_reward += reward
|
| 390 |
+
|
| 391 |
+
# Check if episode is done
|
| 392 |
+
done = self._state.step_count >= self._max_steps
|
| 393 |
+
|
| 394 |
+
# Get current observation
|
| 395 |
+
obs_data = self._get_current_observation()
|
| 396 |
+
|
| 397 |
+
return OpenAppObservation(
|
| 398 |
+
html=obs_data["html"],
|
| 399 |
+
url=obs_data["url"],
|
| 400 |
+
open_pages_urls=obs_data["open_pages_urls"],
|
| 401 |
+
active_page_index=obs_data["active_page_index"],
|
| 402 |
+
axtree_txt=obs_data["axtree_txt"],
|
| 403 |
+
app_state=obs_data["app_state"],
|
| 404 |
+
task_info={"task_name": self.task_name} if self.task_name else None,
|
| 405 |
+
last_action_error=self._last_action_error,
|
| 406 |
+
done=done,
|
| 407 |
+
reward=reward,
|
| 408 |
+
metadata={"cumulative_reward": self._episode_reward},
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
def _execute_click(self, bid: str) -> float:
|
| 412 |
+
"""Execute click action. Returns reward.
|
| 413 |
+
|
| 414 |
+
Supports two modes:
|
| 415 |
+
1. CSS selector mode: If bid starts with '#', '.', or '[', it's treated as a CSS selector
|
| 416 |
+
and uses Playwright directly (e.g., bid="#msg-input")
|
| 417 |
+
2. BrowserGym mode: Otherwise, uses BrowserGym's accessibility tree bid
|
| 418 |
+
"""
|
| 419 |
+
if self._browser_env is None:
|
| 420 |
+
return 0.0
|
| 421 |
+
|
| 422 |
+
try:
|
| 423 |
+
# Check if bid is a CSS selector (starts with # or other CSS selector chars)
|
| 424 |
+
if bid.startswith('#') or bid.startswith('.') or bid.startswith('['):
|
| 425 |
+
# Use Playwright directly for CSS selectors
|
| 426 |
+
return self._execute_click_playwright(bid)
|
| 427 |
+
|
| 428 |
+
# BrowserGym action format: click("bid")
|
| 429 |
+
action = f'click("{bid}")'
|
| 430 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 431 |
+
|
| 432 |
+
# Update current state from observation
|
| 433 |
+
self._current_url = obs.get("url", self._current_url)
|
| 434 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 435 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 436 |
+
|
| 437 |
+
return float(reward) if reward else 0.0
|
| 438 |
+
except Exception as e:
|
| 439 |
+
self._last_action_error = f"Click failed: {str(e)}"
|
| 440 |
+
return -0.1
|
| 441 |
+
|
| 442 |
+
def _execute_fill(self, bid: str, text: str) -> float:
|
| 443 |
+
"""Execute fill action. Returns reward.
|
| 444 |
+
|
| 445 |
+
Supports two modes:
|
| 446 |
+
1. CSS selector mode: If bid starts with '#', it's treated as an HTML ID selector
|
| 447 |
+
and uses Playwright directly (e.g., bid="#msg-input")
|
| 448 |
+
2. BrowserGym mode: Otherwise, uses BrowserGym's accessibility tree bid
|
| 449 |
+
"""
|
| 450 |
+
if self._browser_env is None:
|
| 451 |
+
return 0.0
|
| 452 |
+
|
| 453 |
+
try:
|
| 454 |
+
# Check if bid is a CSS selector (starts with # or other CSS selector chars)
|
| 455 |
+
if bid.startswith('#') or bid.startswith('.') or bid.startswith('['):
|
| 456 |
+
# Use Playwright directly for CSS selectors
|
| 457 |
+
return self._execute_fill_playwright(bid, text)
|
| 458 |
+
|
| 459 |
+
# BrowserGym action format: fill("bid", "text")
|
| 460 |
+
action = f'fill("{bid}", "{text}")'
|
| 461 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 462 |
+
|
| 463 |
+
# Update current state from observation
|
| 464 |
+
self._current_url = obs.get("url", self._current_url)
|
| 465 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 466 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 467 |
+
|
| 468 |
+
return float(reward) if reward else 0.0
|
| 469 |
+
except Exception as e:
|
| 470 |
+
self._last_action_error = f"Fill failed: {str(e)}"
|
| 471 |
+
return -0.1
|
| 472 |
+
|
| 473 |
+
def _execute_fill_playwright(self, selector: str, text: str) -> float:
|
| 474 |
+
"""Execute fill action using Playwright directly with CSS selector."""
|
| 475 |
+
try:
|
| 476 |
+
# Access the underlying Playwright page from BrowserGym
|
| 477 |
+
page = self._browser_env.unwrapped.page
|
| 478 |
+
|
| 479 |
+
# Wait for element and fill it
|
| 480 |
+
page.wait_for_selector(selector, timeout=5000)
|
| 481 |
+
page.fill(selector, text)
|
| 482 |
+
|
| 483 |
+
# Small delay to let the page update
|
| 484 |
+
page.wait_for_timeout(200)
|
| 485 |
+
|
| 486 |
+
# Update observation after action
|
| 487 |
+
self._update_observation_from_page(page)
|
| 488 |
+
|
| 489 |
+
return 0.0
|
| 490 |
+
except Exception as e:
|
| 491 |
+
self._last_action_error = f"Fill (Playwright) failed: {str(e)}"
|
| 492 |
+
return -0.1
|
| 493 |
+
|
| 494 |
+
def _execute_click_playwright(self, selector: str) -> float:
|
| 495 |
+
"""Execute click action using Playwright directly with CSS selector."""
|
| 496 |
+
try:
|
| 497 |
+
# Access the underlying Playwright page from BrowserGym
|
| 498 |
+
page = self._browser_env.unwrapped.page
|
| 499 |
+
|
| 500 |
+
# Wait for element and click it
|
| 501 |
+
page.wait_for_selector(selector, timeout=5000)
|
| 502 |
+
page.click(selector)
|
| 503 |
+
|
| 504 |
+
# Longer delay to let HTMX process the request
|
| 505 |
+
page.wait_for_timeout(500)
|
| 506 |
+
|
| 507 |
+
# Update observation after action
|
| 508 |
+
self._update_observation_from_page(page)
|
| 509 |
+
|
| 510 |
+
return 0.0
|
| 511 |
+
except Exception as e:
|
| 512 |
+
self._last_action_error = f"Click (Playwright) failed: {str(e)}"
|
| 513 |
+
return -0.1
|
| 514 |
+
|
| 515 |
+
def _execute_press_key_playwright(self, key: str) -> float:
|
| 516 |
+
"""Execute key press using Playwright directly."""
|
| 517 |
+
try:
|
| 518 |
+
# Access the underlying Playwright page from BrowserGym
|
| 519 |
+
page = self._browser_env.unwrapped.page
|
| 520 |
+
|
| 521 |
+
# Press the key
|
| 522 |
+
page.keyboard.press(key)
|
| 523 |
+
|
| 524 |
+
# Delay to let the page update
|
| 525 |
+
page.wait_for_timeout(500)
|
| 526 |
+
|
| 527 |
+
# Update observation after action
|
| 528 |
+
self._update_observation_from_page(page)
|
| 529 |
+
|
| 530 |
+
return 0.0
|
| 531 |
+
except Exception as e:
|
| 532 |
+
self._last_action_error = f"Press key (Playwright) failed: {str(e)}"
|
| 533 |
+
return -0.1
|
| 534 |
+
|
| 535 |
+
def _update_observation_from_page(self, page) -> None:
|
| 536 |
+
"""Update internal observation state from Playwright page."""
|
| 537 |
+
try:
|
| 538 |
+
self._current_url = page.url
|
| 539 |
+
# Note: We can't easily get axtree from Playwright directly,
|
| 540 |
+
# so we'll just update URL. The next BrowserGym action will sync the state.
|
| 541 |
+
except Exception:
|
| 542 |
+
pass
|
| 543 |
+
|
| 544 |
+
def _execute_select(self, bid: str, value: str) -> float:
|
| 545 |
+
"""Execute select option action. Returns reward."""
|
| 546 |
+
if self._browser_env is None:
|
| 547 |
+
return 0.0
|
| 548 |
+
|
| 549 |
+
try:
|
| 550 |
+
# BrowserGym action format: select_option("bid", "value")
|
| 551 |
+
action = f'select_option("{bid}", "{value}")'
|
| 552 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 553 |
+
|
| 554 |
+
# Update current state from observation
|
| 555 |
+
self._current_url = obs.get("url", self._current_url)
|
| 556 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 557 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 558 |
+
|
| 559 |
+
return float(reward) if reward else 0.0
|
| 560 |
+
except Exception as e:
|
| 561 |
+
self._last_action_error = f"Select failed: {str(e)}"
|
| 562 |
+
return -0.1
|
| 563 |
+
|
| 564 |
+
def _execute_goto(self, url: str) -> float:
|
| 565 |
+
"""Execute navigation action. Returns reward."""
|
| 566 |
+
if self._browser_env is None:
|
| 567 |
+
self._current_url = url
|
| 568 |
+
return 0.0
|
| 569 |
+
|
| 570 |
+
try:
|
| 571 |
+
# BrowserGym action format: goto("url")
|
| 572 |
+
action = f'goto("{url}")'
|
| 573 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 574 |
+
|
| 575 |
+
# Update current state from observation
|
| 576 |
+
self._current_url = obs.get("url", url)
|
| 577 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 578 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 579 |
+
|
| 580 |
+
return float(reward) if reward else 0.0
|
| 581 |
+
except Exception as e:
|
| 582 |
+
self._last_action_error = f"Goto failed: {str(e)}"
|
| 583 |
+
self._current_url = url # Update URL even if failed
|
| 584 |
+
return -0.1
|
| 585 |
+
|
| 586 |
+
def _execute_scroll(self, direction: str) -> float:
|
| 587 |
+
"""Execute scroll action. Returns reward."""
|
| 588 |
+
if self._browser_env is None:
|
| 589 |
+
return 0.0
|
| 590 |
+
|
| 591 |
+
try:
|
| 592 |
+
# BrowserGym action format: scroll("direction")
|
| 593 |
+
action = f'scroll("{direction}")'
|
| 594 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 595 |
+
|
| 596 |
+
# Update current state from observation
|
| 597 |
+
self._current_url = obs.get("url", self._current_url)
|
| 598 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 599 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 600 |
+
|
| 601 |
+
return float(reward) if reward else 0.0
|
| 602 |
+
except Exception as e:
|
| 603 |
+
self._last_action_error = f"Scroll failed: {str(e)}"
|
| 604 |
+
return -0.1
|
| 605 |
+
|
| 606 |
+
def _execute_send_keys(self, text: str) -> float:
|
| 607 |
+
"""Execute send keys action. Returns reward."""
|
| 608 |
+
if self._browser_env is None:
|
| 609 |
+
return 0.0
|
| 610 |
+
|
| 611 |
+
try:
|
| 612 |
+
# Special handling for Enter key - use Playwright directly for reliability
|
| 613 |
+
if text == "\n" or text.lower() == "enter":
|
| 614 |
+
return self._execute_press_key_playwright("Enter")
|
| 615 |
+
|
| 616 |
+
# BrowserGym action format: send_keys("text")
|
| 617 |
+
action = f'send_keys("{text}")'
|
| 618 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 619 |
+
|
| 620 |
+
# Update current state from observation
|
| 621 |
+
self._current_url = obs.get("url", self._current_url)
|
| 622 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 623 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 624 |
+
|
| 625 |
+
return float(reward) if reward else 0.0
|
| 626 |
+
except Exception as e:
|
| 627 |
+
self._last_action_error = f"Send keys failed: {str(e)}"
|
| 628 |
+
return -0.1
|
| 629 |
+
|
| 630 |
+
@property
|
| 631 |
+
def state(self) -> State:
|
| 632 |
+
"""
|
| 633 |
+
Get the current environment state.
|
| 634 |
+
|
| 635 |
+
Returns:
|
| 636 |
+
Current State with episode_id and step_count
|
| 637 |
+
"""
|
| 638 |
+
return self._state
|
| 639 |
+
|
| 640 |
+
def close(self):
|
| 641 |
+
"""Clean up resources."""
|
| 642 |
+
if hasattr(self, "_browser_env") and self._browser_env is not None:
|
| 643 |
+
try:
|
| 644 |
+
self._browser_env.close()
|
| 645 |
+
except Exception:
|
| 646 |
+
pass
|
| 647 |
+
self._browser_env = None
|
| 648 |
+
|
| 649 |
+
if hasattr(self, "_apps_process") and self._apps_process is not None:
|
| 650 |
+
try:
|
| 651 |
+
self._apps_process.terminate()
|
| 652 |
+
self._apps_process.wait(timeout=5)
|
| 653 |
+
except Exception:
|
| 654 |
+
self._apps_process.kill()
|
| 655 |
+
self._apps_process = None
|
| 656 |
+
|
| 657 |
+
def __del__(self):
|
| 658 |
+
"""Cleanup on deletion."""
|
| 659 |
+
self.close()
|
envs/openapp_env/server/start.sh
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Startup script for OpenApp Environment Docker container
|
| 9 |
+
# This script starts both the OpenApps server and the FastAPI environment server
|
| 10 |
+
|
| 11 |
+
set -e
|
| 12 |
+
|
| 13 |
+
echo "Starting OpenApp Environment..."
|
| 14 |
+
|
| 15 |
+
# Start OpenApps server in the background
|
| 16 |
+
echo "Starting OpenApps server on port ${OPENAPPS_PORT:-5001}..."
|
| 17 |
+
cd /app/openapps
|
| 18 |
+
# Run launch.py directly - it uses Hydra and needs the config directory
|
| 19 |
+
# Redirect OpenApps output to a log file so we can debug if needed
|
| 20 |
+
python launch.py > /tmp/openapps.log 2>&1 &
|
| 21 |
+
OPENAPPS_PID=$!
|
| 22 |
+
|
| 23 |
+
# Wait for OpenApps server to be ready
|
| 24 |
+
echo "Waiting for OpenApps server to be ready..."
|
| 25 |
+
for i in {1..60}; do
|
| 26 |
+
# Check if OpenApps server is responding using curl
|
| 27 |
+
if curl -sf http://localhost:${OPENAPPS_PORT:-5001} >/dev/null 2>&1; then
|
| 28 |
+
echo "OpenApps server is ready on port ${OPENAPPS_PORT:-5001}!"
|
| 29 |
+
break
|
| 30 |
+
fi
|
| 31 |
+
if [ $i -eq 60 ]; then
|
| 32 |
+
echo "ERROR: OpenApps server failed to start within 60 seconds"
|
| 33 |
+
echo "OpenApps log output:"
|
| 34 |
+
cat /tmp/openapps.log || echo "No log file found"
|
| 35 |
+
kill $OPENAPPS_PID 2>/dev/null || true
|
| 36 |
+
exit 1
|
| 37 |
+
fi
|
| 38 |
+
sleep 1
|
| 39 |
+
done
|
| 40 |
+
|
| 41 |
+
# Start the FastAPI environment server
|
| 42 |
+
echo "Starting FastAPI environment server on port 8000..."
|
| 43 |
+
cd /app/env
|
| 44 |
+
exec uvicorn openapp_env.server.app:app --host 0.0.0.0 --port 8000
|
envs/openapp_env/test_openapp_env.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 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 |
+
"""
|
| 9 |
+
Simple test script for OpenApp Environment.
|
| 10 |
+
|
| 11 |
+
This script tests the basic functionality of the OpenApp environment
|
| 12 |
+
to ensure it follows OpenEnv standards.
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
# From OpenEnv root directory
|
| 16 |
+
python3 envs/openapp_env/test_openapp_env.py
|
| 17 |
+
|
| 18 |
+
# Or from openapp_env directory
|
| 19 |
+
cd envs/openapp_env
|
| 20 |
+
python3 test_openapp_env.py
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import sys
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
# Add src to path for local testing
|
| 27 |
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 28 |
+
|
| 29 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 30 |
+
from openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_models():
|
| 34 |
+
"""Test that models are properly defined."""
|
| 35 |
+
print("Testing models...")
|
| 36 |
+
|
| 37 |
+
# Test creating an action
|
| 38 |
+
action = OpenAppAction(action_type="noop")
|
| 39 |
+
assert action.action_type == "noop"
|
| 40 |
+
|
| 41 |
+
# Test click action
|
| 42 |
+
click_action = OpenAppAction(action_type="click", bid="test-btn")
|
| 43 |
+
assert click_action.bid == "test-btn"
|
| 44 |
+
|
| 45 |
+
# Test fill action
|
| 46 |
+
fill_action = OpenAppAction(action_type="fill", bid="input", text="Hello")
|
| 47 |
+
assert fill_action.text == "Hello"
|
| 48 |
+
|
| 49 |
+
print("✓ Models test passed")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def test_environment_basic():
|
| 53 |
+
"""Test basic environment functionality."""
|
| 54 |
+
print("\nTesting environment...")
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
# Create environment (note: this will check if OpenApps is installed as a package)
|
| 58 |
+
env = OpenAppEnvironment(
|
| 59 |
+
max_steps=10,
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# Test that environment has required methods
|
| 63 |
+
assert hasattr(env, "reset")
|
| 64 |
+
assert hasattr(env, "step")
|
| 65 |
+
assert hasattr(env, "state")
|
| 66 |
+
assert hasattr(env, "close")
|
| 67 |
+
|
| 68 |
+
print("✓ Environment structure test passed")
|
| 69 |
+
|
| 70 |
+
except (ValueError, ImportError) as e:
|
| 71 |
+
# Expected if OpenApps is not installed as a package
|
| 72 |
+
if "OpenApps not found" in str(e) or "open_apps" in str(e):
|
| 73 |
+
print(
|
| 74 |
+
"✓ Environment structure test passed (OpenApps not installed, expected)"
|
| 75 |
+
)
|
| 76 |
+
else:
|
| 77 |
+
raise
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_client_server_contract():
|
| 81 |
+
"""Test that client and server follow the contract."""
|
| 82 |
+
print("\nTesting client-server contract...")
|
| 83 |
+
|
| 84 |
+
# Test that action can be serialized to dict
|
| 85 |
+
action = OpenAppAction(
|
| 86 |
+
action_type="click", bid="test-123", metadata={"test": "value"}
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# Simulate what client._step_payload would do
|
| 90 |
+
payload = {
|
| 91 |
+
"action_type": action.action_type,
|
| 92 |
+
"bid": action.bid,
|
| 93 |
+
"metadata": action.metadata,
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
assert payload["action_type"] == "click"
|
| 97 |
+
assert payload["bid"] == "test-123"
|
| 98 |
+
|
| 99 |
+
# Test observation construction
|
| 100 |
+
obs = OpenAppObservation(
|
| 101 |
+
html="<html></html>",
|
| 102 |
+
url="http://localhost:5001",
|
| 103 |
+
open_pages_urls=["http://localhost:5001"],
|
| 104 |
+
done=False,
|
| 105 |
+
reward=0.0,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
assert obs.url == "http://localhost:5001"
|
| 109 |
+
assert obs.done is False
|
| 110 |
+
|
| 111 |
+
print("✓ Client-server contract test passed")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def main():
|
| 115 |
+
"""Run all tests."""
|
| 116 |
+
print("=" * 60)
|
| 117 |
+
print("OpenApp Environment - Structure Tests")
|
| 118 |
+
print("=" * 60)
|
| 119 |
+
|
| 120 |
+
try:
|
| 121 |
+
test_models()
|
| 122 |
+
test_environment_basic()
|
| 123 |
+
test_client_server_contract()
|
| 124 |
+
|
| 125 |
+
print("\n" + "=" * 60)
|
| 126 |
+
print("All tests passed! ✓")
|
| 127 |
+
print("=" * 60)
|
| 128 |
+
print("\nNote: Full integration tests require:")
|
| 129 |
+
print(
|
| 130 |
+
"1. OpenApps installed: pip install git+https://github.com/facebookresearch/OpenApps.git"
|
| 131 |
+
)
|
| 132 |
+
print("2. Playwright browsers installed: playwright install chromium")
|
| 133 |
+
print("3. BrowserGym dependencies installed")
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f"\n✗ Test failed: {e}")
|
| 137 |
+
import traceback
|
| 138 |
+
|
| 139 |
+
traceback.print_exc()
|
| 140 |
+
sys.exit(1)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
if __name__ == "__main__":
|
| 144 |
+
main()
|
example_usage.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 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 |
+
"""
|
| 9 |
+
Example usage of OpenApp Environment.
|
| 10 |
+
|
| 11 |
+
This script demonstrates how to use the OpenApp environment with OpenEnv.
|
| 12 |
+
|
| 13 |
+
For a complete runnable example, see: examples/openapp_example.py
|
| 14 |
+
|
| 15 |
+
Visualization Options:
|
| 16 |
+
To see the browser window and watch agent interactions:
|
| 17 |
+
|
| 18 |
+
Terminal 1: Start OpenApps server with visible browser
|
| 19 |
+
cd OpenApps
|
| 20 |
+
python OpenApps/launch.py browsergym_env_args.headless=False
|
| 21 |
+
|
| 22 |
+
Terminal 2: Run your agent code
|
| 23 |
+
export OPENAPPS_URL=http://localhost:5001
|
| 24 |
+
python examples/openapp_example.py --mode local
|
| 25 |
+
|
| 26 |
+
Or access OpenApps web interface at http://localhost:5001
|
| 27 |
+
Docker mode web interface at http://localhost:8000/web
|
| 28 |
+
|
| 29 |
+
Important:
|
| 30 |
+
Browser visualization is controlled by the OpenApps SERVER, not the client.
|
| 31 |
+
Launch the server with 'browsergym_env_args.headless=False' to see the browser.
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
import sys
|
| 35 |
+
from pathlib import Path
|
| 36 |
+
|
| 37 |
+
# Add src to path for local testing
|
| 38 |
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
| 39 |
+
|
| 40 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def example_basic_usage():
|
| 44 |
+
"""Basic usage example."""
|
| 45 |
+
print("=" * 60)
|
| 46 |
+
print("OpenApp Environment - Basic Usage Example")
|
| 47 |
+
print("=" * 60)
|
| 48 |
+
|
| 49 |
+
# Option 1: Connect to a running server
|
| 50 |
+
print("\nOption 1: Connect to running server")
|
| 51 |
+
print("client = OpenAppEnv(base_url='http://localhost:8000')")
|
| 52 |
+
|
| 53 |
+
# Option 2: Start from Docker image (recommended)
|
| 54 |
+
print("\nOption 2: Start from Docker image")
|
| 55 |
+
print("client = OpenAppEnv.from_docker_image('openapp-env:latest')")
|
| 56 |
+
|
| 57 |
+
print("\n" + "-" * 60)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def example_actions():
|
| 61 |
+
"""Example of different action types."""
|
| 62 |
+
print("\nExample Actions")
|
| 63 |
+
print("-" * 60)
|
| 64 |
+
|
| 65 |
+
# Navigate to a page
|
| 66 |
+
print("\n1. Navigate to calendar app:")
|
| 67 |
+
print("action = OpenAppAction(")
|
| 68 |
+
print(" action_type='goto',")
|
| 69 |
+
print(" url='http://localhost:5001/calendar'")
|
| 70 |
+
print(")")
|
| 71 |
+
print("result = client.step(action)")
|
| 72 |
+
|
| 73 |
+
# Click on an element
|
| 74 |
+
print("\n2. Click on a button:")
|
| 75 |
+
print("action = OpenAppAction(")
|
| 76 |
+
print(" action_type='click',")
|
| 77 |
+
print(" bid='add-event-btn' # BrowserGym element ID")
|
| 78 |
+
print(")")
|
| 79 |
+
print("result = client.step(action)")
|
| 80 |
+
|
| 81 |
+
# Fill a form field
|
| 82 |
+
print("\n3. Fill in text input:")
|
| 83 |
+
print("action = OpenAppAction(")
|
| 84 |
+
print(" action_type='fill',")
|
| 85 |
+
print(" bid='event-title-input',")
|
| 86 |
+
print(" text='Team Meeting'")
|
| 87 |
+
print(")")
|
| 88 |
+
print("result = client.step(action)")
|
| 89 |
+
|
| 90 |
+
# Select from dropdown
|
| 91 |
+
print("\n4. Select from dropdown:")
|
| 92 |
+
print("action = OpenAppAction(")
|
| 93 |
+
print(" action_type='select_option',")
|
| 94 |
+
print(" bid='time-select',")
|
| 95 |
+
print(" value='14:00'")
|
| 96 |
+
print(")")
|
| 97 |
+
print("result = client.step(action)")
|
| 98 |
+
|
| 99 |
+
# Scroll the page
|
| 100 |
+
print("\n5. Scroll down:")
|
| 101 |
+
print("action = OpenAppAction(")
|
| 102 |
+
print(" action_type='scroll',")
|
| 103 |
+
print(" direction='down'")
|
| 104 |
+
print(")")
|
| 105 |
+
print("result = client.step(action)")
|
| 106 |
+
|
| 107 |
+
# No operation
|
| 108 |
+
print("\n6. No operation (useful for observation):")
|
| 109 |
+
print("action = OpenAppAction(action_type='noop')")
|
| 110 |
+
print("result = client.step(action)")
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def example_observations():
|
| 114 |
+
"""Example of observation structure."""
|
| 115 |
+
print("\n\nObservation Structure")
|
| 116 |
+
print("-" * 60)
|
| 117 |
+
|
| 118 |
+
print("\nAfter reset() or step(), you receive:")
|
| 119 |
+
print("result.observation.html # Current page HTML")
|
| 120 |
+
print("result.observation.url # Current URL")
|
| 121 |
+
print("result.observation.open_pages_urls # All open pages")
|
| 122 |
+
print("result.observation.axtree_txt # Accessibility tree")
|
| 123 |
+
print("result.observation.app_state # App states (calendar, todo, etc.)")
|
| 124 |
+
print("result.observation.task_info # Task information (if using tasks)")
|
| 125 |
+
print("result.observation.screenshot # Page screenshot (base64)")
|
| 126 |
+
print("result.observation.last_action_error # Error from last action")
|
| 127 |
+
print("result.reward # Step reward")
|
| 128 |
+
print("result.done # Episode done flag")
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def example_complete_workflow():
|
| 132 |
+
"""Complete workflow example."""
|
| 133 |
+
print("\n\nComplete Workflow Example")
|
| 134 |
+
print("=" * 60)
|
| 135 |
+
|
| 136 |
+
example_code = """
|
| 137 |
+
from envs.openapp_env import OpenAppAction, OpenAppEnv
|
| 138 |
+
|
| 139 |
+
# Create client (starts Docker container)
|
| 140 |
+
client = OpenAppEnv.from_docker_image("openapp-env:latest")
|
| 141 |
+
|
| 142 |
+
try:
|
| 143 |
+
# Reset environment
|
| 144 |
+
result = client.reset()
|
| 145 |
+
print(f"Starting at: {result.observation.url}")
|
| 146 |
+
|
| 147 |
+
# Navigate to calendar
|
| 148 |
+
result = client.step(OpenAppAction(
|
| 149 |
+
action_type="goto",
|
| 150 |
+
url="http://localhost:5001/calendar"
|
| 151 |
+
))
|
| 152 |
+
|
| 153 |
+
# Click to add new event
|
| 154 |
+
result = client.step(OpenAppAction(
|
| 155 |
+
action_type="click",
|
| 156 |
+
bid="new-event-button"
|
| 157 |
+
))
|
| 158 |
+
|
| 159 |
+
# Fill event title
|
| 160 |
+
result = client.step(OpenAppAction(
|
| 161 |
+
action_type="fill",
|
| 162 |
+
bid="title-input",
|
| 163 |
+
text="Project Review Meeting"
|
| 164 |
+
))
|
| 165 |
+
|
| 166 |
+
# Fill event date
|
| 167 |
+
result = client.step(OpenAppAction(
|
| 168 |
+
action_type="fill",
|
| 169 |
+
bid="date-input",
|
| 170 |
+
text="2025-12-15"
|
| 171 |
+
))
|
| 172 |
+
|
| 173 |
+
# Submit form
|
| 174 |
+
result = client.step(OpenAppAction(
|
| 175 |
+
action_type="click",
|
| 176 |
+
bid="submit-button"
|
| 177 |
+
))
|
| 178 |
+
|
| 179 |
+
print(f"Reward: {result.reward}")
|
| 180 |
+
print(f"Done: {result.done}")
|
| 181 |
+
print(f"App State: {result.observation.app_state}")
|
| 182 |
+
|
| 183 |
+
finally:
|
| 184 |
+
# Always cleanup
|
| 185 |
+
client.close()
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
print(example_code)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def example_with_tasks():
|
| 192 |
+
"""Example using OpenApps tasks."""
|
| 193 |
+
print("\n\nUsing Tasks (Task-Based RL)")
|
| 194 |
+
print("=" * 60)
|
| 195 |
+
|
| 196 |
+
example_code = """
|
| 197 |
+
# Environment can be configured with specific tasks
|
| 198 |
+
# Tasks define goals and automatic reward calculation
|
| 199 |
+
|
| 200 |
+
from envs.openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 201 |
+
|
| 202 |
+
env = OpenAppEnvironment(
|
| 203 |
+
openapps_url="http://localhost:5001", # OpenApps server URL
|
| 204 |
+
task_name="add_meeting_with_dennis", # Optional task name
|
| 205 |
+
headless=False, # Set to False to watch the browser
|
| 206 |
+
max_steps=50,
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
obs = env.reset()
|
| 210 |
+
# Now the environment has a goal: add a meeting with Dennis
|
| 211 |
+
# Rewards will be based on progress toward this goal
|
| 212 |
+
|
| 213 |
+
# Agent loop
|
| 214 |
+
done = False
|
| 215 |
+
while not done:
|
| 216 |
+
action = agent.get_action(obs) # Your agent
|
| 217 |
+
obs = env.step(action)
|
| 218 |
+
done = obs.done
|
| 219 |
+
|
| 220 |
+
print(f"Task completed! Reward: {obs.reward}")
|
| 221 |
+
env.close()
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
print(example_code)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def example_visualization():
|
| 228 |
+
"""Example of visualization options."""
|
| 229 |
+
print("\n\nVisualization Options")
|
| 230 |
+
print("=" * 60)
|
| 231 |
+
|
| 232 |
+
example_code = """
|
| 233 |
+
# Option 1: Show browser window (watch agent in real-time)
|
| 234 |
+
from envs.openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 235 |
+
|
| 236 |
+
env = OpenAppEnvironment(
|
| 237 |
+
openapps_url="http://localhost:5001",
|
| 238 |
+
headless=False, # Show browser window
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
obs = env.reset()
|
| 242 |
+
# You'll see a browser window open!
|
| 243 |
+
|
| 244 |
+
# Option 2: Access web interface manually
|
| 245 |
+
# While OpenApps server is running, open in your browser:
|
| 246 |
+
# - Main: http://localhost:5001
|
| 247 |
+
# - Calendar: http://localhost:5001/calendar
|
| 248 |
+
# - Todo: http://localhost:5001/todo
|
| 249 |
+
# - Messenger: http://localhost:5001/messenger
|
| 250 |
+
# - Maps: http://localhost:5001/maps
|
| 251 |
+
|
| 252 |
+
# Option 3: Use the example script with --show-browser
|
| 253 |
+
# python examples/openapp_example.py --mode local --show-browser
|
| 254 |
+
"""
|
| 255 |
+
|
| 256 |
+
print(example_code)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def main():
|
| 260 |
+
"""Run all examples."""
|
| 261 |
+
example_basic_usage()
|
| 262 |
+
example_actions()
|
| 263 |
+
example_observations()
|
| 264 |
+
example_complete_workflow()
|
| 265 |
+
example_with_tasks()
|
| 266 |
+
example_visualization()
|
| 267 |
+
|
| 268 |
+
print("\n" + "=" * 60)
|
| 269 |
+
print("For a complete runnable example:")
|
| 270 |
+
print(" python examples/openapp_example.py --mode local --show-browser")
|
| 271 |
+
print("\nFor more information, see:")
|
| 272 |
+
print("- README.md in this directory")
|
| 273 |
+
print("- OpenApps docs: https://facebookresearch.github.io/OpenApps/")
|
| 274 |
+
print("- OpenEnv docs: https://meta-pytorch.org/OpenEnv/")
|
| 275 |
+
print("=" * 60)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
if __name__ == "__main__":
|
| 279 |
+
main()
|
models.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 the OpenApp Environment.
|
| 9 |
+
|
| 10 |
+
The OpenApp environment provides a simulated web application environment
|
| 11 |
+
for training and evaluating UI agents that interact with various apps
|
| 12 |
+
(calendar, todo, messenger, maps, etc.) using browser actions.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from typing import Any, Dict, List, Optional
|
| 16 |
+
|
| 17 |
+
from pydantic import Field
|
| 18 |
+
|
| 19 |
+
# Support both in-repo and standalone imports
|
| 20 |
+
try:
|
| 21 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 22 |
+
from openenv.core.env_server.types import Action, Observation
|
| 23 |
+
except ImportError:
|
| 24 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 25 |
+
from openenv.core.env_server.types import Action, Observation
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class OpenAppAction(Action):
|
| 29 |
+
"""
|
| 30 |
+
Action for the OpenApp environment.
|
| 31 |
+
|
| 32 |
+
Supports BrowserGym-style actions for web interaction:
|
| 33 |
+
- click: Click on an element (requires bid - BrowserGym ID)
|
| 34 |
+
- fill: Fill a text field (requires bid and text)
|
| 35 |
+
- select_option: Select from dropdown (requires bid and value)
|
| 36 |
+
- goto: Navigate to URL (requires url)
|
| 37 |
+
- scroll: Scroll the page (requires direction)
|
| 38 |
+
- send_keys: Send keyboard input (requires text)
|
| 39 |
+
- noop: No operation
|
| 40 |
+
|
| 41 |
+
Attributes:
|
| 42 |
+
action_type: Type of action to perform
|
| 43 |
+
bid: BrowserGym element ID (for click, fill, select_option)
|
| 44 |
+
text: Text content (for fill, send_keys)
|
| 45 |
+
value: Value to select (for select_option)
|
| 46 |
+
url: URL to navigate to (for goto)
|
| 47 |
+
direction: Scroll direction - 'up' or 'down' (for scroll)
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
action_type: str = Field(
|
| 51 |
+
..., description="Type of action: click, fill, select_option, goto, scroll, send_keys, noop"
|
| 52 |
+
)
|
| 53 |
+
bid: Optional[str] = Field(default=None, description="BrowserGym element ID")
|
| 54 |
+
text: Optional[str] = Field(default=None, description="Text content for fill or send_keys")
|
| 55 |
+
value: Optional[str] = Field(default=None, description="Value for select_option")
|
| 56 |
+
url: Optional[str] = Field(default=None, description="URL for goto action")
|
| 57 |
+
direction: Optional[str] = Field(default=None, description="Scroll direction: 'up' or 'down'")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class OpenAppObservation(Observation):
|
| 61 |
+
"""
|
| 62 |
+
Observation from the OpenApp environment.
|
| 63 |
+
|
| 64 |
+
Provides comprehensive state information about the web apps and browser state.
|
| 65 |
+
|
| 66 |
+
Attributes:
|
| 67 |
+
html: Current page HTML content
|
| 68 |
+
url: Current page URL
|
| 69 |
+
open_pages_urls: List of all open page URLs
|
| 70 |
+
active_page_index: Index of currently active page
|
| 71 |
+
screenshot: Base64-encoded screenshot (optional)
|
| 72 |
+
axtree_txt: Accessibility tree as text (for element interaction)
|
| 73 |
+
app_state: Current state of all apps (calendar, todo, messenger, map)
|
| 74 |
+
task_info: Information about the current task (if any)
|
| 75 |
+
last_action_error: Error message from last action (if failed)
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
html: str = Field(default="", description="Current page HTML content")
|
| 79 |
+
url: str = Field(default="", description="Current page URL")
|
| 80 |
+
open_pages_urls: List[str] = Field(default_factory=list, description="List of all open page URLs")
|
| 81 |
+
active_page_index: int = Field(default=0, ge=0, description="Index of currently active page")
|
| 82 |
+
screenshot: Optional[str] = Field(default=None, description="Base64-encoded screenshot")
|
| 83 |
+
axtree_txt: str = Field(default="", description="Accessibility tree as text")
|
| 84 |
+
app_state: Dict[str, Any] = Field(default_factory=dict, description="State of all apps")
|
| 85 |
+
task_info: Optional[Dict[str, Any]] = Field(default=None, description="Current task information")
|
| 86 |
+
last_action_error: Optional[str] = Field(default=None, description="Error from last action")
|
openenv.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
spec_version: 1
|
| 2 |
+
name: openapp_env
|
| 3 |
+
type: space
|
| 4 |
+
runtime: fastapi
|
| 5 |
+
app: server.app:app
|
| 6 |
+
port: 8000
|
pyproject.toml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-openapp_env"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "OpenApp Environment for OpenEnv - web application simulation environment for UI agents"
|
| 15 |
+
requires-python = ">=3.11,<3.14"
|
| 16 |
+
dependencies = [
|
| 17 |
+
# NOTE: openenv-core is NOT listed here to avoid openai version conflict
|
| 18 |
+
# It is installed separately in the Dockerfile with --no-deps to avoid
|
| 19 |
+
# openai>=2.7.2 conflicting with OpenApps' openai<2 requirement.
|
| 20 |
+
# For local development, install manually:
|
| 21 |
+
# pip install --no-deps "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git"
|
| 22 |
+
# pip install fastapi pydantic uvicorn requests websockets
|
| 23 |
+
#
|
| 24 |
+
# NOTE: open_apps is also NOT listed here for the same reason.
|
| 25 |
+
# Install manually for local development:
|
| 26 |
+
# pip install git+https://github.com/facebookresearch/OpenApps.git
|
| 27 |
+
#
|
| 28 |
+
# Server dependencies (these are installed by Dockerfile separately for openenv-core)
|
| 29 |
+
"fastapi>=0.115.0",
|
| 30 |
+
"pydantic>=2.0.0",
|
| 31 |
+
"uvicorn[standard]>=0.24.0",
|
| 32 |
+
"requests>=2.31.0",
|
| 33 |
+
"websockets>=15.0.1",
|
| 34 |
+
# BrowserGym dependencies
|
| 35 |
+
"browsergym>=0.13.3",
|
| 36 |
+
"playwright>=1.40.0",
|
| 37 |
+
# Additional dependencies for web app interaction
|
| 38 |
+
"python-multipart>=0.0.20",
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
[project.optional-dependencies]
|
| 42 |
+
dev = [
|
| 43 |
+
"pytest>=8.0.0",
|
| 44 |
+
"pytest-cov>=4.0.0",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
[project.scripts]
|
| 48 |
+
server = "openapp_env.server.app:main"
|
| 49 |
+
|
| 50 |
+
[tool.setuptools]
|
| 51 |
+
packages = ["openapp_env", "openapp_env.server"]
|
| 52 |
+
package-dir = { "openapp_env" = ".", "openapp_env.server" = "server" }
|
| 53 |
+
|
| 54 |
+
[tool.setuptools.package-data]
|
| 55 |
+
openapp_env = ["**/*.yaml", "**/*.yml", "**/*.md"]
|
| 56 |
+
|
| 57 |
+
[tool.hatch.metadata]
|
| 58 |
+
allow-direct-references = true
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Dockerfile for OpenApp Environment
|
| 8 |
+
# This image provides OpenApps web application simulation for UI agent training
|
| 9 |
+
#
|
| 10 |
+
# This Dockerfile works for both local builds and HuggingFace Spaces deployment:
|
| 11 |
+
# - Local build: cd envs/openapp_env && docker build -t openapp-env:latest -f server/Dockerfile .
|
| 12 |
+
# - HuggingFace: Automatically deployed via `openenv push`
|
| 13 |
+
#
|
| 14 |
+
# Run with web interface:
|
| 15 |
+
# docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true openapp-env:latest
|
| 16 |
+
|
| 17 |
+
FROM python:3.11-slim
|
| 18 |
+
|
| 19 |
+
# Set metadata
|
| 20 |
+
LABEL maintainer="OpenEnv Team"
|
| 21 |
+
LABEL description="OpenApp Environment with BrowserGym for UI agent training"
|
| 22 |
+
LABEL org.opencontainers.image.source="https://github.com/meta-pytorch/OpenEnv"
|
| 23 |
+
|
| 24 |
+
# Set working directory
|
| 25 |
+
WORKDIR /app/env
|
| 26 |
+
|
| 27 |
+
# Install system dependencies
|
| 28 |
+
# - git: required to clone OpenApps from GitHub
|
| 29 |
+
# - curl: for healthcheck
|
| 30 |
+
# - Playwright/BrowserGym dependencies: fonts, libraries for browser automation
|
| 31 |
+
RUN apt-get update && \
|
| 32 |
+
apt-get install -y --no-install-recommends \
|
| 33 |
+
git \
|
| 34 |
+
curl \
|
| 35 |
+
ca-certificates \
|
| 36 |
+
wget \
|
| 37 |
+
gnupg \
|
| 38 |
+
# Playwright/Chromium dependencies
|
| 39 |
+
libnss3 \
|
| 40 |
+
libnspr4 \
|
| 41 |
+
libatk1.0-0 \
|
| 42 |
+
libatk-bridge2.0-0 \
|
| 43 |
+
libcups2 \
|
| 44 |
+
libdrm2 \
|
| 45 |
+
libdbus-1-3 \
|
| 46 |
+
libxkbcommon0 \
|
| 47 |
+
libxcomposite1 \
|
| 48 |
+
libxdamage1 \
|
| 49 |
+
libxfixes3 \
|
| 50 |
+
libxrandr2 \
|
| 51 |
+
libgbm1 \
|
| 52 |
+
libasound2 \
|
| 53 |
+
libpango-1.0-0 \
|
| 54 |
+
libcairo2 \
|
| 55 |
+
libatspi2.0-0 \
|
| 56 |
+
libxshmfence1 \
|
| 57 |
+
fonts-liberation \
|
| 58 |
+
libappindicator3-1 \
|
| 59 |
+
xdg-utils && \
|
| 60 |
+
rm -rf /var/lib/apt/lists/*
|
| 61 |
+
|
| 62 |
+
# Set environment variables
|
| 63 |
+
ENV PYTHONUNBUFFERED=1
|
| 64 |
+
|
| 65 |
+
# Set working directory
|
| 66 |
+
WORKDIR /app/env
|
| 67 |
+
|
| 68 |
+
# Copy environment files
|
| 69 |
+
# Context is always the env directory (envs/openapp_env/)
|
| 70 |
+
# - GitHub Actions: uses context: envs/openapp_env
|
| 71 |
+
# - HuggingFace: openenv push uploads env dir as context
|
| 72 |
+
COPY . /app/env
|
| 73 |
+
|
| 74 |
+
# Install OpenApps FIRST to establish openai<2 (required by agentlab)
|
| 75 |
+
# This must happen before openenv-core to avoid version conflict
|
| 76 |
+
WORKDIR /app
|
| 77 |
+
RUN git clone https://github.com/facebookresearch/OpenApps.git openapps && \
|
| 78 |
+
cd openapps && \
|
| 79 |
+
pip install --no-cache-dir -e .
|
| 80 |
+
|
| 81 |
+
# Verify OpenApps installation
|
| 82 |
+
RUN python -c "import open_apps; print('✓ OpenApps installed')"
|
| 83 |
+
|
| 84 |
+
# Install openenv-core from GitHub with --no-deps to avoid openai>=2.7.2 conflict
|
| 85 |
+
# Then install only the server dependencies (no openai needed for server)
|
| 86 |
+
RUN pip install --no-cache-dir --no-deps "openenv-core[core]>=0.2.1" && \
|
| 87 |
+
pip install --no-cache-dir fastapi pydantic uvicorn requests websockets
|
| 88 |
+
|
| 89 |
+
# Install openapp_env and remaining dependencies
|
| 90 |
+
WORKDIR /app/env
|
| 91 |
+
RUN pip install --no-cache-dir -e .
|
| 92 |
+
|
| 93 |
+
# Verify installation
|
| 94 |
+
RUN python -c "import openapp_env; print('✓ openapp_env installed')" && \
|
| 95 |
+
python -c "import openapp_env.server.app; print('✓ openapp_env.server.app importable')"
|
| 96 |
+
|
| 97 |
+
# Install Playwright browsers (Chromium for BrowserGym)
|
| 98 |
+
# We already installed system dependencies above, so just install the browser
|
| 99 |
+
RUN playwright install chromium
|
| 100 |
+
|
| 101 |
+
# Copy startup script
|
| 102 |
+
WORKDIR /app/env
|
| 103 |
+
COPY server/start.sh /app/start.sh
|
| 104 |
+
RUN chmod +x /app/start.sh
|
| 105 |
+
|
| 106 |
+
# OpenApp-specific environment variables (can be overridden at runtime)
|
| 107 |
+
ENV OPENAPPS_URL=http://localhost:5001
|
| 108 |
+
ENV OPENAPPS_PORT=5001
|
| 109 |
+
ENV OPENAPP_HEADLESS=true
|
| 110 |
+
ENV OPENAPP_MAX_STEPS=50
|
| 111 |
+
|
| 112 |
+
# Hydra requires USER environment variable
|
| 113 |
+
ENV USER=root
|
| 114 |
+
|
| 115 |
+
# Enable web interface by default (set to false to disable)
|
| 116 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 117 |
+
|
| 118 |
+
# Expose ports (8000 for FastAPI, 5001 for OpenApps)
|
| 119 |
+
EXPOSE 8000 5001
|
| 120 |
+
|
| 121 |
+
# Health check
|
| 122 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
| 123 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 124 |
+
|
| 125 |
+
# Run the startup script that launches both OpenApps server and FastAPI server
|
| 126 |
+
# Web interface will be available at /web if ENABLE_WEB_INTERFACE=true
|
| 127 |
+
# API documentation available at /docs
|
| 128 |
+
CMD ["/app/start.sh"]
|
server/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""OpenApp Environment Server."""
|
server/app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 OpenApp Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the OpenAppEnvironment
|
| 11 |
+
over HTTP endpoints, making it compatible with HTTPEnvClient.
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
# Development (with auto-reload):
|
| 15 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 16 |
+
|
| 17 |
+
# Production:
|
| 18 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 19 |
+
|
| 20 |
+
# Or run directly:
|
| 21 |
+
uv run --project . server
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
# Support both in-repo and standalone imports
|
| 25 |
+
try:
|
| 26 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 27 |
+
from openenv.core.env_server.http_server import create_app
|
| 28 |
+
from ..models import OpenAppAction, OpenAppObservation
|
| 29 |
+
from .openapp_environment import OpenAppEnvironment
|
| 30 |
+
except ImportError:
|
| 31 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 32 |
+
from openenv.core.env_server.http_server import create_app
|
| 33 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 34 |
+
from openapp_env.server.openapp_environment import OpenAppEnvironment
|
| 35 |
+
|
| 36 |
+
# Create the app with web interface and README integration
|
| 37 |
+
# Pass the class (factory) instead of an instance for WebSocket session support
|
| 38 |
+
# Each client gets its own environment instance. The environment reads
|
| 39 |
+
# OPENAPPS_URL from environment variables in __init__.
|
| 40 |
+
app = create_app(OpenAppEnvironment, OpenAppAction, OpenAppObservation, env_name="openapp_env")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main():
|
| 44 |
+
"""
|
| 45 |
+
Entry point for direct execution via uv run or python -m.
|
| 46 |
+
|
| 47 |
+
This function enables running the server without Docker:
|
| 48 |
+
uv run --project . server
|
| 49 |
+
python -m envs.openapp_env.server.app
|
| 50 |
+
openenv serve openapp_env
|
| 51 |
+
|
| 52 |
+
"""
|
| 53 |
+
import uvicorn
|
| 54 |
+
|
| 55 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
main()
|
server/openapp_environment.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
OpenApp Environment Implementation.
|
| 9 |
+
|
| 10 |
+
A web application simulation environment that wraps OpenApps and BrowserGym.
|
| 11 |
+
This environment provides agent interaction with simulated web apps including
|
| 12 |
+
calendar, todo, messenger, and maps applications.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import logging
|
| 16 |
+
import os
|
| 17 |
+
import subprocess
|
| 18 |
+
import time
|
| 19 |
+
import urllib.request
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any, Dict, Optional, Tuple
|
| 22 |
+
from uuid import uuid4
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
# Support both in-repo and standalone imports
|
| 27 |
+
try:
|
| 28 |
+
# In-repo imports (when running from OpenEnv repository)
|
| 29 |
+
from openenv.core.env_server.interfaces import Environment
|
| 30 |
+
from openenv.core.env_server.types import State
|
| 31 |
+
from ..models import OpenAppAction, OpenAppObservation
|
| 32 |
+
except ImportError:
|
| 33 |
+
# Standalone imports (when environment is standalone with openenv-core from pip)
|
| 34 |
+
from openenv.core.env_server.interfaces import Environment
|
| 35 |
+
from openenv.core.env_server.types import State
|
| 36 |
+
from openapp_env.models import OpenAppAction, OpenAppObservation
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class GenericOpenAppsTask:
|
| 40 |
+
"""
|
| 41 |
+
A generic task for OpenApps interaction without specific goals.
|
| 42 |
+
|
| 43 |
+
This is a simple wrapper that allows BrowserGym to interact with OpenApps
|
| 44 |
+
without requiring a specific task. For task-based interaction, use the
|
| 45 |
+
OpenAppsTask from open_apps.tasks.add_tasks_to_browsergym.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
base_url: str,
|
| 51 |
+
seed: int = 1,
|
| 52 |
+
**kwargs,
|
| 53 |
+
) -> None:
|
| 54 |
+
"""
|
| 55 |
+
Initialize generic OpenApps task.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
base_url: Base URL of the OpenApps server
|
| 59 |
+
seed: Random seed (required by BrowserGym)
|
| 60 |
+
**kwargs: Additional arguments (ignored)
|
| 61 |
+
"""
|
| 62 |
+
try:
|
| 63 |
+
from browsergym.core.task import AbstractBrowserTask
|
| 64 |
+
import playwright.sync_api
|
| 65 |
+
except ImportError:
|
| 66 |
+
raise ImportError(
|
| 67 |
+
"BrowserGym is required. Install with: pip install browsergym"
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# Store as instance attributes
|
| 71 |
+
self.base_url = base_url
|
| 72 |
+
self.seed = seed
|
| 73 |
+
|
| 74 |
+
# BrowserGym task properties
|
| 75 |
+
self.viewport = {"width": 1024, "height": 768}
|
| 76 |
+
self.slow_mo = 100
|
| 77 |
+
self.timeout = 5000
|
| 78 |
+
|
| 79 |
+
# Additional properties that BrowserGym might expect
|
| 80 |
+
self.locale = None
|
| 81 |
+
self.timezone_id = None
|
| 82 |
+
self.geolocation = None
|
| 83 |
+
|
| 84 |
+
def setup(
|
| 85 |
+
self, page: "playwright.sync_api.Page"
|
| 86 |
+
) -> Tuple[str, Dict[str, Any]]:
|
| 87 |
+
"""
|
| 88 |
+
Set up the task by navigating to the base URL.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
page: Playwright page object
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
Tuple of (goal_string, info_dict)
|
| 95 |
+
"""
|
| 96 |
+
page.goto(self.base_url)
|
| 97 |
+
return "Explore OpenApps", {}
|
| 98 |
+
|
| 99 |
+
def teardown(self) -> None:
|
| 100 |
+
"""Clean up after task completion."""
|
| 101 |
+
pass
|
| 102 |
+
|
| 103 |
+
def validate(
|
| 104 |
+
self, page: "playwright.sync_api.Page", chat_messages: list[str]
|
| 105 |
+
) -> Tuple[float, bool, str, Dict[str, Any]]:
|
| 106 |
+
"""
|
| 107 |
+
Validate task state and return reward.
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
page: Playwright page object
|
| 111 |
+
chat_messages: List of chat messages
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
Tuple of (reward, done, message, info)
|
| 115 |
+
"""
|
| 116 |
+
# Generic task never completes automatically
|
| 117 |
+
return 0.0, False, "", {}
|
| 118 |
+
|
| 119 |
+
def cheat(
|
| 120 |
+
self, page: "playwright.sync_api.Page", chat_messages: list[str]
|
| 121 |
+
) -> None:
|
| 122 |
+
"""Cheat method (no-op for generic task)."""
|
| 123 |
+
pass
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class OpenAppEnvironment(Environment):
|
| 127 |
+
"""
|
| 128 |
+
A web application environment that wraps OpenApps and BrowserGym.
|
| 129 |
+
|
| 130 |
+
This environment launches OpenApps web server and provides a BrowserGym-like
|
| 131 |
+
interface for agents to interact with simulated web applications.
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
openapps_path: Path to OpenApps directory (default: auto-detect)
|
| 135 |
+
web_app_port: Port for OpenApps web server (default: 5001)
|
| 136 |
+
headless: Run browser in headless mode (default: True)
|
| 137 |
+
task_name: Optional task name to evaluate (e.g., "add_meeting_with_dennis")
|
| 138 |
+
apps_config: Configuration for apps (default: all enabled)
|
| 139 |
+
max_steps: Maximum steps per episode (default: 50)
|
| 140 |
+
|
| 141 |
+
Example:
|
| 142 |
+
>>> env = OpenAppEnvironment()
|
| 143 |
+
>>> obs = env.reset()
|
| 144 |
+
>>> print(obs.url) # Starting page URL
|
| 145 |
+
>>>
|
| 146 |
+
>>> # Click on an element
|
| 147 |
+
>>> action = OpenAppAction(action_type="click", bid="calendar-btn")
|
| 148 |
+
>>> obs = env.step(action)
|
| 149 |
+
>>> print(obs.html)
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
openapps_url: Optional[str] = None,
|
| 155 |
+
openapps_path: Optional[str] = None,
|
| 156 |
+
web_app_port: int = 5001,
|
| 157 |
+
headless: bool = True,
|
| 158 |
+
task_name: Optional[str] = None,
|
| 159 |
+
apps_config: Optional[Dict[str, Any]] = None,
|
| 160 |
+
max_steps: int = 50,
|
| 161 |
+
):
|
| 162 |
+
"""Initialize the OpenApp environment."""
|
| 163 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 164 |
+
self._max_steps = max_steps
|
| 165 |
+
|
| 166 |
+
# OpenApps configuration
|
| 167 |
+
# Priority: 1. openapps_url, 2. OPENAPPS_URL env var, 3. Try to find/launch
|
| 168 |
+
self.openapps_url = openapps_url or os.environ.get("OPENAPPS_URL")
|
| 169 |
+
if not self.openapps_url:
|
| 170 |
+
self.web_app_port = web_app_port
|
| 171 |
+
self.openapps_url = f"http://localhost:{web_app_port}"
|
| 172 |
+
|
| 173 |
+
self.openapps_path = openapps_path
|
| 174 |
+
self.headless = headless
|
| 175 |
+
self.task_name = task_name
|
| 176 |
+
self.apps_config = apps_config or {}
|
| 177 |
+
|
| 178 |
+
# Runtime state
|
| 179 |
+
self._apps_process: Optional[subprocess.Popen] = None
|
| 180 |
+
self._browser_env = None
|
| 181 |
+
self._current_html = ""
|
| 182 |
+
self._current_url = ""
|
| 183 |
+
self._current_axtree = ""
|
| 184 |
+
self._app_state = {}
|
| 185 |
+
self._last_action_error = None
|
| 186 |
+
self._episode_reward = 0.0
|
| 187 |
+
|
| 188 |
+
def _detect_openapps_path(self) -> str:
|
| 189 |
+
"""
|
| 190 |
+
Auto-detect OpenApps path.
|
| 191 |
+
|
| 192 |
+
Since OpenApps is installed as a Python package, we use the installed
|
| 193 |
+
package location instead of requiring a separate directory.
|
| 194 |
+
"""
|
| 195 |
+
# Check if user provided a custom path via environment variable
|
| 196 |
+
env_path = os.environ.get("OPENAPPS_PATH")
|
| 197 |
+
if env_path and Path(env_path).exists():
|
| 198 |
+
return env_path
|
| 199 |
+
|
| 200 |
+
# Try to find OpenApps as an installed package
|
| 201 |
+
try:
|
| 202 |
+
import open_apps
|
| 203 |
+
|
| 204 |
+
openapps_pkg_path = Path(open_apps.__file__).parent.parent
|
| 205 |
+
if openapps_pkg_path.exists():
|
| 206 |
+
return str(openapps_pkg_path)
|
| 207 |
+
except ImportError:
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
raise ValueError(
|
| 211 |
+
"OpenApps not found. Please install it with: "
|
| 212 |
+
"pip install git+https://github.com/facebookresearch/OpenApps.git "
|
| 213 |
+
"or set OPENAPPS_PATH environment variable."
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
def _launch_openapps_server(self) -> Optional[subprocess.Popen]:
|
| 217 |
+
"""
|
| 218 |
+
Launch OpenApps web server in background.
|
| 219 |
+
|
| 220 |
+
Returns None if server is expected to be already running (OPENAPPS_URL set).
|
| 221 |
+
"""
|
| 222 |
+
# If OPENAPPS_URL is set, assume server is already running
|
| 223 |
+
if os.environ.get("OPENAPPS_URL"):
|
| 224 |
+
logger.info(f"Using existing OpenApps server at {self.openapps_url}")
|
| 225 |
+
# Wait for server to be available
|
| 226 |
+
self._wait_for_server(max_wait=5)
|
| 227 |
+
return None
|
| 228 |
+
|
| 229 |
+
# Otherwise, provide helpful error message
|
| 230 |
+
raise NotImplementedError(
|
| 231 |
+
"Automatic OpenApps server launch is not yet implemented.\n"
|
| 232 |
+
"\n"
|
| 233 |
+
"Please start OpenApps manually in a separate terminal:\n"
|
| 234 |
+
" 1. Clone OpenApps: git clone https://github.com/facebookresearch/OpenApps.git\n"
|
| 235 |
+
" 2. Install: cd OpenApps && uv sync\n"
|
| 236 |
+
" 3. Run: uv run launch.py\n"
|
| 237 |
+
"\n"
|
| 238 |
+
"Then set the OPENAPPS_URL environment variable:\n"
|
| 239 |
+
" export OPENAPPS_URL=http://localhost:5001\n"
|
| 240 |
+
"\n"
|
| 241 |
+
"Or use Docker mode which handles this automatically:\n"
|
| 242 |
+
" python examples/openapp_example.py --mode docker\n"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
def _wait_for_server(self, max_wait: int = 30):
|
| 246 |
+
"""Wait for OpenApps server to become available."""
|
| 247 |
+
for i in range(max_wait):
|
| 248 |
+
try:
|
| 249 |
+
response = urllib.request.urlopen(self.openapps_url, timeout=2)
|
| 250 |
+
if response.status == 200:
|
| 251 |
+
return
|
| 252 |
+
except Exception:
|
| 253 |
+
pass
|
| 254 |
+
time.sleep(1)
|
| 255 |
+
|
| 256 |
+
raise TimeoutError(f"OpenApps server did not start within {max_wait} seconds")
|
| 257 |
+
|
| 258 |
+
def _initialize_browser(self):
|
| 259 |
+
"""Initialize BrowserGym environment for interaction."""
|
| 260 |
+
try:
|
| 261 |
+
from browsergym.core.env import BrowserEnv
|
| 262 |
+
except ImportError:
|
| 263 |
+
raise ImportError(
|
| 264 |
+
"BrowserGym is required for OpenApp environment. "
|
| 265 |
+
"Install it with: pip install browsergym"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# Create BrowserGym environment with generic OpenApps task
|
| 269 |
+
self._browser_env = BrowserEnv(
|
| 270 |
+
task_entrypoint=GenericOpenAppsTask,
|
| 271 |
+
task_kwargs={"base_url": self.openapps_url},
|
| 272 |
+
headless=self.headless,
|
| 273 |
+
slow_mo=200, # Slow down actions so they're visible (200ms delay)
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
def _get_current_observation(self) -> Dict[str, Any]:
|
| 277 |
+
"""Extract current observation from browser state."""
|
| 278 |
+
if self._browser_env is None:
|
| 279 |
+
return {
|
| 280 |
+
"html": "",
|
| 281 |
+
"url": self.openapps_url,
|
| 282 |
+
"open_pages_urls": [self.openapps_url],
|
| 283 |
+
"active_page_index": 0,
|
| 284 |
+
"axtree_txt": "",
|
| 285 |
+
"app_state": {},
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
# Get browser state (implementation depends on BrowserGym API)
|
| 289 |
+
# This is a simplified version - actual implementation would use BrowserGym's observation
|
| 290 |
+
return {
|
| 291 |
+
"html": self._current_html,
|
| 292 |
+
"url": self._current_url,
|
| 293 |
+
"open_pages_urls": [self._current_url],
|
| 294 |
+
"active_page_index": 0,
|
| 295 |
+
"axtree_txt": self._current_axtree,
|
| 296 |
+
"app_state": self._app_state,
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
def reset(self) -> OpenAppObservation:
|
| 300 |
+
"""
|
| 301 |
+
Reset the environment.
|
| 302 |
+
|
| 303 |
+
Returns:
|
| 304 |
+
OpenAppObservation with initial state
|
| 305 |
+
"""
|
| 306 |
+
# Reset state
|
| 307 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 308 |
+
self._episode_reward = 0.0
|
| 309 |
+
self._last_action_error = None
|
| 310 |
+
|
| 311 |
+
# Check if OpenApps server is running, start if needed
|
| 312 |
+
if self._apps_process is None and not os.environ.get("OPENAPPS_URL"):
|
| 313 |
+
self._apps_process = self._launch_openapps_server()
|
| 314 |
+
|
| 315 |
+
# Initialize browser
|
| 316 |
+
if self._browser_env is None:
|
| 317 |
+
self._initialize_browser()
|
| 318 |
+
|
| 319 |
+
# Reset the BrowserGym environment
|
| 320 |
+
try:
|
| 321 |
+
obs, info = self._browser_env.reset()
|
| 322 |
+
# Extract observation data from BrowserGym
|
| 323 |
+
self._current_url = obs.get("url", self.openapps_url)
|
| 324 |
+
self._current_html = obs.get("dom_txt", "")
|
| 325 |
+
self._current_axtree = obs.get("axtree_txt", "")
|
| 326 |
+
self._app_state = {}
|
| 327 |
+
except Exception as e:
|
| 328 |
+
logger.warning(f"Failed to reset browser environment: {e}")
|
| 329 |
+
# Fallback to placeholder values
|
| 330 |
+
self._current_url = self.openapps_url
|
| 331 |
+
self._current_html = "<html><body>OpenApps Ready</body></html>"
|
| 332 |
+
self._current_axtree = ""
|
| 333 |
+
self._app_state = {}
|
| 334 |
+
|
| 335 |
+
obs_data = self._get_current_observation()
|
| 336 |
+
|
| 337 |
+
return OpenAppObservation(
|
| 338 |
+
html=obs_data["html"],
|
| 339 |
+
url=obs_data["url"],
|
| 340 |
+
open_pages_urls=obs_data["open_pages_urls"],
|
| 341 |
+
active_page_index=obs_data["active_page_index"],
|
| 342 |
+
axtree_txt=obs_data["axtree_txt"],
|
| 343 |
+
app_state=obs_data["app_state"],
|
| 344 |
+
task_info={"task_name": self.task_name} if self.task_name else None,
|
| 345 |
+
last_action_error=None,
|
| 346 |
+
done=False,
|
| 347 |
+
reward=0.0,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
def step(self, action: OpenAppAction) -> OpenAppObservation: # type: ignore[override]
|
| 351 |
+
"""
|
| 352 |
+
Execute a step in the environment.
|
| 353 |
+
|
| 354 |
+
Args:
|
| 355 |
+
action: OpenAppAction to execute
|
| 356 |
+
|
| 357 |
+
Returns:
|
| 358 |
+
OpenAppObservation with resulting state and reward
|
| 359 |
+
"""
|
| 360 |
+
self._state.step_count += 1
|
| 361 |
+
self._last_action_error = None
|
| 362 |
+
reward = 0.0
|
| 363 |
+
|
| 364 |
+
try:
|
| 365 |
+
# Execute action based on type
|
| 366 |
+
if action.action_type == "click":
|
| 367 |
+
reward = self._execute_click(action.bid)
|
| 368 |
+
elif action.action_type == "fill":
|
| 369 |
+
reward = self._execute_fill(action.bid, action.text)
|
| 370 |
+
elif action.action_type == "select_option":
|
| 371 |
+
reward = self._execute_select(action.bid, action.value)
|
| 372 |
+
elif action.action_type == "goto":
|
| 373 |
+
reward = self._execute_goto(action.url)
|
| 374 |
+
elif action.action_type == "scroll":
|
| 375 |
+
reward = self._execute_scroll(action.direction)
|
| 376 |
+
elif action.action_type == "send_keys":
|
| 377 |
+
reward = self._execute_send_keys(action.text)
|
| 378 |
+
elif action.action_type == "noop":
|
| 379 |
+
reward = 0.0
|
| 380 |
+
else:
|
| 381 |
+
self._last_action_error = f"Unknown action type: {action.action_type}"
|
| 382 |
+
reward = -0.1
|
| 383 |
+
|
| 384 |
+
except Exception as e:
|
| 385 |
+
self._last_action_error = str(e)
|
| 386 |
+
reward = -0.1
|
| 387 |
+
|
| 388 |
+
# Update cumulative reward
|
| 389 |
+
self._episode_reward += reward
|
| 390 |
+
|
| 391 |
+
# Check if episode is done
|
| 392 |
+
done = self._state.step_count >= self._max_steps
|
| 393 |
+
|
| 394 |
+
# Get current observation
|
| 395 |
+
obs_data = self._get_current_observation()
|
| 396 |
+
|
| 397 |
+
return OpenAppObservation(
|
| 398 |
+
html=obs_data["html"],
|
| 399 |
+
url=obs_data["url"],
|
| 400 |
+
open_pages_urls=obs_data["open_pages_urls"],
|
| 401 |
+
active_page_index=obs_data["active_page_index"],
|
| 402 |
+
axtree_txt=obs_data["axtree_txt"],
|
| 403 |
+
app_state=obs_data["app_state"],
|
| 404 |
+
task_info={"task_name": self.task_name} if self.task_name else None,
|
| 405 |
+
last_action_error=self._last_action_error,
|
| 406 |
+
done=done,
|
| 407 |
+
reward=reward,
|
| 408 |
+
metadata={"cumulative_reward": self._episode_reward},
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
def _execute_click(self, bid: str) -> float:
|
| 412 |
+
"""Execute click action. Returns reward.
|
| 413 |
+
|
| 414 |
+
Supports two modes:
|
| 415 |
+
1. CSS selector mode: If bid starts with '#', '.', or '[', it's treated as a CSS selector
|
| 416 |
+
and uses Playwright directly (e.g., bid="#msg-input")
|
| 417 |
+
2. BrowserGym mode: Otherwise, uses BrowserGym's accessibility tree bid
|
| 418 |
+
"""
|
| 419 |
+
if self._browser_env is None:
|
| 420 |
+
return 0.0
|
| 421 |
+
|
| 422 |
+
try:
|
| 423 |
+
# Check if bid is a CSS selector (starts with # or other CSS selector chars)
|
| 424 |
+
if bid.startswith('#') or bid.startswith('.') or bid.startswith('['):
|
| 425 |
+
# Use Playwright directly for CSS selectors
|
| 426 |
+
return self._execute_click_playwright(bid)
|
| 427 |
+
|
| 428 |
+
# BrowserGym action format: click("bid")
|
| 429 |
+
action = f'click("{bid}")'
|
| 430 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 431 |
+
|
| 432 |
+
# Update current state from observation
|
| 433 |
+
self._current_url = obs.get("url", self._current_url)
|
| 434 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 435 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 436 |
+
|
| 437 |
+
return float(reward) if reward else 0.0
|
| 438 |
+
except Exception as e:
|
| 439 |
+
self._last_action_error = f"Click failed: {str(e)}"
|
| 440 |
+
return -0.1
|
| 441 |
+
|
| 442 |
+
def _execute_fill(self, bid: str, text: str) -> float:
|
| 443 |
+
"""Execute fill action. Returns reward.
|
| 444 |
+
|
| 445 |
+
Supports two modes:
|
| 446 |
+
1. CSS selector mode: If bid starts with '#', it's treated as an HTML ID selector
|
| 447 |
+
and uses Playwright directly (e.g., bid="#msg-input")
|
| 448 |
+
2. BrowserGym mode: Otherwise, uses BrowserGym's accessibility tree bid
|
| 449 |
+
"""
|
| 450 |
+
if self._browser_env is None:
|
| 451 |
+
return 0.0
|
| 452 |
+
|
| 453 |
+
try:
|
| 454 |
+
# Check if bid is a CSS selector (starts with # or other CSS selector chars)
|
| 455 |
+
if bid.startswith('#') or bid.startswith('.') or bid.startswith('['):
|
| 456 |
+
# Use Playwright directly for CSS selectors
|
| 457 |
+
return self._execute_fill_playwright(bid, text)
|
| 458 |
+
|
| 459 |
+
# BrowserGym action format: fill("bid", "text")
|
| 460 |
+
action = f'fill("{bid}", "{text}")'
|
| 461 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 462 |
+
|
| 463 |
+
# Update current state from observation
|
| 464 |
+
self._current_url = obs.get("url", self._current_url)
|
| 465 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 466 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 467 |
+
|
| 468 |
+
return float(reward) if reward else 0.0
|
| 469 |
+
except Exception as e:
|
| 470 |
+
self._last_action_error = f"Fill failed: {str(e)}"
|
| 471 |
+
return -0.1
|
| 472 |
+
|
| 473 |
+
def _execute_fill_playwright(self, selector: str, text: str) -> float:
|
| 474 |
+
"""Execute fill action using Playwright directly with CSS selector."""
|
| 475 |
+
try:
|
| 476 |
+
# Access the underlying Playwright page from BrowserGym
|
| 477 |
+
page = self._browser_env.unwrapped.page
|
| 478 |
+
|
| 479 |
+
# Wait for element and fill it
|
| 480 |
+
page.wait_for_selector(selector, timeout=5000)
|
| 481 |
+
page.fill(selector, text)
|
| 482 |
+
|
| 483 |
+
# Small delay to let the page update
|
| 484 |
+
page.wait_for_timeout(200)
|
| 485 |
+
|
| 486 |
+
# Update observation after action
|
| 487 |
+
self._update_observation_from_page(page)
|
| 488 |
+
|
| 489 |
+
return 0.0
|
| 490 |
+
except Exception as e:
|
| 491 |
+
self._last_action_error = f"Fill (Playwright) failed: {str(e)}"
|
| 492 |
+
return -0.1
|
| 493 |
+
|
| 494 |
+
def _execute_click_playwright(self, selector: str) -> float:
|
| 495 |
+
"""Execute click action using Playwright directly with CSS selector."""
|
| 496 |
+
try:
|
| 497 |
+
# Access the underlying Playwright page from BrowserGym
|
| 498 |
+
page = self._browser_env.unwrapped.page
|
| 499 |
+
|
| 500 |
+
# Wait for element and click it
|
| 501 |
+
page.wait_for_selector(selector, timeout=5000)
|
| 502 |
+
page.click(selector)
|
| 503 |
+
|
| 504 |
+
# Longer delay to let HTMX process the request
|
| 505 |
+
page.wait_for_timeout(500)
|
| 506 |
+
|
| 507 |
+
# Update observation after action
|
| 508 |
+
self._update_observation_from_page(page)
|
| 509 |
+
|
| 510 |
+
return 0.0
|
| 511 |
+
except Exception as e:
|
| 512 |
+
self._last_action_error = f"Click (Playwright) failed: {str(e)}"
|
| 513 |
+
return -0.1
|
| 514 |
+
|
| 515 |
+
def _execute_press_key_playwright(self, key: str) -> float:
|
| 516 |
+
"""Execute key press using Playwright directly."""
|
| 517 |
+
try:
|
| 518 |
+
# Access the underlying Playwright page from BrowserGym
|
| 519 |
+
page = self._browser_env.unwrapped.page
|
| 520 |
+
|
| 521 |
+
# Press the key
|
| 522 |
+
page.keyboard.press(key)
|
| 523 |
+
|
| 524 |
+
# Delay to let the page update
|
| 525 |
+
page.wait_for_timeout(500)
|
| 526 |
+
|
| 527 |
+
# Update observation after action
|
| 528 |
+
self._update_observation_from_page(page)
|
| 529 |
+
|
| 530 |
+
return 0.0
|
| 531 |
+
except Exception as e:
|
| 532 |
+
self._last_action_error = f"Press key (Playwright) failed: {str(e)}"
|
| 533 |
+
return -0.1
|
| 534 |
+
|
| 535 |
+
def _update_observation_from_page(self, page) -> None:
|
| 536 |
+
"""Update internal observation state from Playwright page."""
|
| 537 |
+
try:
|
| 538 |
+
self._current_url = page.url
|
| 539 |
+
# Note: We can't easily get axtree from Playwright directly,
|
| 540 |
+
# so we'll just update URL. The next BrowserGym action will sync the state.
|
| 541 |
+
except Exception:
|
| 542 |
+
pass
|
| 543 |
+
|
| 544 |
+
def _execute_select(self, bid: str, value: str) -> float:
|
| 545 |
+
"""Execute select option action. Returns reward."""
|
| 546 |
+
if self._browser_env is None:
|
| 547 |
+
return 0.0
|
| 548 |
+
|
| 549 |
+
try:
|
| 550 |
+
# BrowserGym action format: select_option("bid", "value")
|
| 551 |
+
action = f'select_option("{bid}", "{value}")'
|
| 552 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 553 |
+
|
| 554 |
+
# Update current state from observation
|
| 555 |
+
self._current_url = obs.get("url", self._current_url)
|
| 556 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 557 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 558 |
+
|
| 559 |
+
return float(reward) if reward else 0.0
|
| 560 |
+
except Exception as e:
|
| 561 |
+
self._last_action_error = f"Select failed: {str(e)}"
|
| 562 |
+
return -0.1
|
| 563 |
+
|
| 564 |
+
def _execute_goto(self, url: str) -> float:
|
| 565 |
+
"""Execute navigation action. Returns reward."""
|
| 566 |
+
if self._browser_env is None:
|
| 567 |
+
self._current_url = url
|
| 568 |
+
return 0.0
|
| 569 |
+
|
| 570 |
+
try:
|
| 571 |
+
# BrowserGym action format: goto("url")
|
| 572 |
+
action = f'goto("{url}")'
|
| 573 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 574 |
+
|
| 575 |
+
# Update current state from observation
|
| 576 |
+
self._current_url = obs.get("url", url)
|
| 577 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 578 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 579 |
+
|
| 580 |
+
return float(reward) if reward else 0.0
|
| 581 |
+
except Exception as e:
|
| 582 |
+
self._last_action_error = f"Goto failed: {str(e)}"
|
| 583 |
+
self._current_url = url # Update URL even if failed
|
| 584 |
+
return -0.1
|
| 585 |
+
|
| 586 |
+
def _execute_scroll(self, direction: str) -> float:
|
| 587 |
+
"""Execute scroll action. Returns reward."""
|
| 588 |
+
if self._browser_env is None:
|
| 589 |
+
return 0.0
|
| 590 |
+
|
| 591 |
+
try:
|
| 592 |
+
# BrowserGym action format: scroll("direction")
|
| 593 |
+
action = f'scroll("{direction}")'
|
| 594 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 595 |
+
|
| 596 |
+
# Update current state from observation
|
| 597 |
+
self._current_url = obs.get("url", self._current_url)
|
| 598 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 599 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 600 |
+
|
| 601 |
+
return float(reward) if reward else 0.0
|
| 602 |
+
except Exception as e:
|
| 603 |
+
self._last_action_error = f"Scroll failed: {str(e)}"
|
| 604 |
+
return -0.1
|
| 605 |
+
|
| 606 |
+
def _execute_send_keys(self, text: str) -> float:
|
| 607 |
+
"""Execute send keys action. Returns reward."""
|
| 608 |
+
if self._browser_env is None:
|
| 609 |
+
return 0.0
|
| 610 |
+
|
| 611 |
+
try:
|
| 612 |
+
# Special handling for Enter key - use Playwright directly for reliability
|
| 613 |
+
if text == "\n" or text.lower() == "enter":
|
| 614 |
+
return self._execute_press_key_playwright("Enter")
|
| 615 |
+
|
| 616 |
+
# BrowserGym action format: send_keys("text")
|
| 617 |
+
action = f'send_keys("{text}")'
|
| 618 |
+
obs, reward, done, truncated, info = self._browser_env.step(action)
|
| 619 |
+
|
| 620 |
+
# Update current state from observation
|
| 621 |
+
self._current_url = obs.get("url", self._current_url)
|
| 622 |
+
self._current_html = obs.get("dom_txt", self._current_html)
|
| 623 |
+
self._current_axtree = obs.get("axtree_txt", self._current_axtree)
|
| 624 |
+
|
| 625 |
+
return float(reward) if reward else 0.0
|
| 626 |
+
except Exception as e:
|
| 627 |
+
self._last_action_error = f"Send keys failed: {str(e)}"
|
| 628 |
+
return -0.1
|
| 629 |
+
|
| 630 |
+
@property
|
| 631 |
+
def state(self) -> State:
|
| 632 |
+
"""
|
| 633 |
+
Get the current environment state.
|
| 634 |
+
|
| 635 |
+
Returns:
|
| 636 |
+
Current State with episode_id and step_count
|
| 637 |
+
"""
|
| 638 |
+
return self._state
|
| 639 |
+
|
| 640 |
+
def close(self):
|
| 641 |
+
"""Clean up resources."""
|
| 642 |
+
if hasattr(self, "_browser_env") and self._browser_env is not None:
|
| 643 |
+
try:
|
| 644 |
+
self._browser_env.close()
|
| 645 |
+
except Exception:
|
| 646 |
+
pass
|
| 647 |
+
self._browser_env = None
|
| 648 |
+
|
| 649 |
+
if hasattr(self, "_apps_process") and self._apps_process is not None:
|
| 650 |
+
try:
|
| 651 |
+
self._apps_process.terminate()
|
| 652 |
+
self._apps_process.wait(timeout=5)
|
| 653 |
+
except Exception:
|
| 654 |
+
self._apps_process.kill()
|
| 655 |
+
self._apps_process = None
|
| 656 |
+
|
| 657 |
+
def __del__(self):
|
| 658 |
+
"""Cleanup on deletion."""
|
| 659 |
+
self.close()
|
server/start.sh
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Startup script for OpenApp Environment Docker container
|
| 9 |
+
# This script starts both the OpenApps server and the FastAPI environment server
|
| 10 |
+
|
| 11 |
+
set -e
|
| 12 |
+
|
| 13 |
+
echo "Starting OpenApp Environment..."
|
| 14 |
+
|
| 15 |
+
# Start OpenApps server in the background
|
| 16 |
+
echo "Starting OpenApps server on port ${OPENAPPS_PORT:-5001}..."
|
| 17 |
+
cd /app/openapps
|
| 18 |
+
# Run launch.py directly - it uses Hydra and needs the config directory
|
| 19 |
+
# Redirect OpenApps output to a log file so we can debug if needed
|
| 20 |
+
python launch.py > /tmp/openapps.log 2>&1 &
|
| 21 |
+
OPENAPPS_PID=$!
|
| 22 |
+
|
| 23 |
+
# Wait for OpenApps server to be ready
|
| 24 |
+
echo "Waiting for OpenApps server to be ready..."
|
| 25 |
+
for i in {1..60}; do
|
| 26 |
+
# Check if OpenApps server is responding using curl
|
| 27 |
+
if curl -sf http://localhost:${OPENAPPS_PORT:-5001} >/dev/null 2>&1; then
|
| 28 |
+
echo "OpenApps server is ready on port ${OPENAPPS_PORT:-5001}!"
|
| 29 |
+
break
|
| 30 |
+
fi
|
| 31 |
+
if [ $i -eq 60 ]; then
|
| 32 |
+
echo "ERROR: OpenApps server failed to start within 60 seconds"
|
| 33 |
+
echo "OpenApps log output:"
|
| 34 |
+
cat /tmp/openapps.log || echo "No log file found"
|
| 35 |
+
kill $OPENAPPS_PID 2>/dev/null || true
|
| 36 |
+
exit 1
|
| 37 |
+
fi
|
| 38 |
+
sleep 1
|
| 39 |
+
done
|
| 40 |
+
|
| 41 |
+
# Start the FastAPI environment server
|
| 42 |
+
echo "Starting FastAPI environment server on port 8000..."
|
| 43 |
+
cd /app/env
|
| 44 |
+
exec uvicorn openapp_env.server.app:app --host 0.0.0.0 --port 8000
|
src/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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."""
|
src/openenv.egg-info/PKG-INFO
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: openenv
|
| 3 |
+
Version: 0.2.0
|
| 4 |
+
Summary: A unified framework for reinforcement learning environments
|
| 5 |
+
Requires-Python: >=3.10
|
| 6 |
+
Description-Content-Type: text/markdown
|
| 7 |
+
License-File: LICENSE
|
| 8 |
+
Requires-Dist: fastapi>=0.104.0
|
| 9 |
+
Requires-Dist: pydantic>=2.0.0
|
| 10 |
+
Requires-Dist: uvicorn>=0.24.0
|
| 11 |
+
Requires-Dist: requests>=2.25.0
|
| 12 |
+
Requires-Dist: typer>=0.9.0
|
| 13 |
+
Requires-Dist: rich>=13.0.0
|
| 14 |
+
Requires-Dist: pyyaml>=6.0
|
| 15 |
+
Requires-Dist: huggingface_hub>=0.20.0
|
| 16 |
+
Requires-Dist: openai>=2.7.2
|
| 17 |
+
Requires-Dist: tomli>=2.3.0
|
| 18 |
+
Requires-Dist: tomli-w>=1.2.0
|
| 19 |
+
Requires-Dist: websockets>=15.0.1
|
| 20 |
+
Provides-Extra: core
|
| 21 |
+
Requires-Dist: fastapi>=0.104.0; extra == "core"
|
| 22 |
+
Requires-Dist: pydantic>=2.0.0; extra == "core"
|
| 23 |
+
Requires-Dist: uvicorn>=0.24.0; extra == "core"
|
| 24 |
+
Requires-Dist: requests>=2.25.0; extra == "core"
|
| 25 |
+
Requires-Dist: websockets>=15.0.1; extra == "core"
|
| 26 |
+
Provides-Extra: cli
|
| 27 |
+
Requires-Dist: typer>=0.9.0; extra == "cli"
|
| 28 |
+
Requires-Dist: rich>=13.0.0; extra == "cli"
|
| 29 |
+
Requires-Dist: pyyaml>=6.0; extra == "cli"
|
| 30 |
+
Requires-Dist: huggingface_hub>=0.20.0; extra == "cli"
|
| 31 |
+
Requires-Dist: openai>=2.7.2; extra == "cli"
|
| 32 |
+
Requires-Dist: tomli>=2.3.0; extra == "cli"
|
| 33 |
+
Requires-Dist: tomli-w>=1.2.0; extra == "cli"
|
| 34 |
+
Provides-Extra: all
|
| 35 |
+
Requires-Dist: openenv[core]; extra == "all"
|
| 36 |
+
Requires-Dist: openenv[cli]; extra == "all"
|
| 37 |
+
Dynamic: license-file
|
| 38 |
+
|
| 39 |
+
# <img width="35" height="35" alt="image" src="https://github.com/user-attachments/assets/2700a971-e5d6-4036-b03f-2f89c9791609" /> OpenEnv: Agentic Execution Environments
|
| 40 |
+
|
| 41 |
+
An e2e framework for creating, deploying and using isolated execution environments for agentic RL training, built using Gymnasium style simple APIs.
|
| 42 |
+
|
| 43 |
+
[](https://pypi.org/project/openenv/)
|
| 44 |
+
[](https://discord.gg/YsTYBh6PD9)
|
| 45 |
+
[](https://colab.research.google.com/github/meta-pytorch/OpenEnv/blob/main/examples/OpenEnv_Tutorial.ipynb)
|
| 46 |
+
[](https://meta-pytorch.org/OpenEnv/)
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
**🚀 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/)
|
| 51 |
+
|
| 52 |
+
## OpenEnv on partner platforms:
|
| 53 |
+
|
| 54 |
+
- [Lightning AI Studio](https://lightning.ai/environments?section=featured)
|
| 55 |
+
- [TRL example](https://huggingface.co/docs/trl/main/en/openenv)
|
| 56 |
+
- [Unsloth Google Colab](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/OpenEnv_gpt_oss_(20B)_Reinforcement_Learning_2048_Game.ipynb)
|
| 57 |
+
- [ART example](https://art.openpipe.ai/integrations/openenv-integration)
|
| 58 |
+
- [Oumi example](https://github.com/oumi-ai/oumi/blob/main/notebooks/Oumi%20-%20OpenEnv%20GRPO%20with%20trl.ipynb)
|
| 59 |
+
|
| 60 |
+
## Overview
|
| 61 |
+
|
| 62 |
+
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.
|
| 63 |
+
|
| 64 |
+
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.
|
| 65 |
+
|
| 66 |
+
The OpenEnv CLI (`openenv`) provides commands to initialize new environments and deploy them to Hugging Face Spaces.
|
| 67 |
+
|
| 68 |
+
> ⚠️ **Early Development Warning** OpenEnv is currently in an experimental
|
| 69 |
+
> stage. You should expect bugs, incomplete features, and APIs that may change
|
| 70 |
+
> in future versions. The project welcomes bugfixes, but to make sure things are
|
| 71 |
+
> well coordinated you should discuss any significant change before starting the
|
| 72 |
+
> work. It's recommended that you signal your intention to contribute in the
|
| 73 |
+
> issue tracker, either by filing a new issue or by claiming an existing one.
|
| 74 |
+
|
| 75 |
+
### RFCs
|
| 76 |
+
|
| 77 |
+
Below is a list of active and historical RFCs for OpenEnv. RFCs are proposals for major changes or features. Please review and contribute!
|
| 78 |
+
|
| 79 |
+
- [RFC 001: Baseline API and Interface Specifications](https://github.com/meta-pytorch/OpenEnv/pull/26)
|
| 80 |
+
|
| 81 |
+
## Architecture
|
| 82 |
+
|
| 83 |
+
### Component Overview
|
| 84 |
+
|
| 85 |
+
```
|
| 86 |
+
┌─────────────────────────────────────────────────────────┐
|
| 87 |
+
│ Client Application │
|
| 88 |
+
│ ┌────────────────┐ ┌──────────────────┐ │
|
| 89 |
+
│ │ EchoEnv │ │ CodingEnv │ │
|
| 90 |
+
│ │ (HTTPEnvClient)│ �� (HTTPEnvClient) │ │
|
| 91 |
+
│ └────────┬───────┘ └────────┬─────────┘ │
|
| 92 |
+
└───────────┼───────────────────────────────┼─────────────┘
|
| 93 |
+
│ HTTP │ HTTP
|
| 94 |
+
│ (reset, step, state) │
|
| 95 |
+
┌───────────▼───────────────────────────────▼─────────────┐
|
| 96 |
+
│ Docker Containers (Isolated) │
|
| 97 |
+
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
| 98 |
+
│ │ FastAPI Server │ │ FastAPI Server │ │
|
| 99 |
+
│ │ EchoEnvironment │ │ PythonCodeActEnv │ │
|
| 100 |
+
│ │ (Environment base) │ │ (Environment base) │ │
|
| 101 |
+
│ └──────────────────────┘ └──────────────────────┘ │
|
| 102 |
+
└─────────────────────────────────────────────────────────┘
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### Core Components
|
| 106 |
+
|
| 107 |
+
#### 1. Web Interface
|
| 108 |
+
|
| 109 |
+
OpenEnv includes a built-in web interface for interactive environment exploration and debugging. The web interface provides:
|
| 110 |
+
|
| 111 |
+
- **Two-Pane Layout**: HumanAgent interaction on the left, state observation on the right
|
| 112 |
+
- **Real-time Updates**: WebSocket-based live updates without page refresh
|
| 113 |
+
- **Dynamic Forms**: Automatically generated action forms based on environment Action types
|
| 114 |
+
- **Action History**: Complete log of all actions taken and their results
|
| 115 |
+
|
| 116 |
+
The web interface is **conditionally enabled** based on environment variables:
|
| 117 |
+
|
| 118 |
+
- **Local Development**: Disabled by default for lightweight development
|
| 119 |
+
- **Manual Override**: Enable with `ENABLE_WEB_INTERFACE=true`
|
| 120 |
+
|
| 121 |
+
To use the web interface:
|
| 122 |
+
|
| 123 |
+
```python
|
| 124 |
+
from openenv.core.env_server import create_web_interface_app
|
| 125 |
+
from your_env.models import YourAction, YourObservation
|
| 126 |
+
from your_env.server.your_environment import YourEnvironment
|
| 127 |
+
|
| 128 |
+
env = YourEnvironment()
|
| 129 |
+
app = create_web_interface_app(env, YourAction, YourObservation)
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
When enabled, open `http://localhost:8000/web` in your browser to interact with the environment.
|
| 133 |
+
|
| 134 |
+
#### 2. Environment (Server-Side)
|
| 135 |
+
Base class for implementing environment logic:
|
| 136 |
+
- **`reset()`**: Initialize a new episode, returns initial `Observation`
|
| 137 |
+
- **`step(action)`**: Execute an `Action`, returns resulting `Observation`
|
| 138 |
+
- **`state()`**: Access episode metadata (`State` with episode_id, step_count, etc.)
|
| 139 |
+
|
| 140 |
+
#### 3. HTTPEnvClient (Client-Side)
|
| 141 |
+
Base class for HTTP communication:
|
| 142 |
+
- Handles HTTP requests to environment server
|
| 143 |
+
- Contains a utility to spin up a docker container locally for the corresponding environment
|
| 144 |
+
- Type-safe action/observation parsing
|
| 145 |
+
|
| 146 |
+
#### 4. Container Providers
|
| 147 |
+
Manage container deployment:
|
| 148 |
+
- `LocalDockerProvider`: Run containers on local Docker daemon
|
| 149 |
+
- `KubernetesProvider`: Deploy to K8s clusters (future)
|
| 150 |
+
|
| 151 |
+
#### 5. Models
|
| 152 |
+
Type-safe data structures:
|
| 153 |
+
- `Action`: Base class for environment actions
|
| 154 |
+
- `Observation`: Base class for environment observations
|
| 155 |
+
- `State`: Episode state tracking
|
| 156 |
+
- `StepResult`: Combines observation, reward, done flag
|
| 157 |
+
|
| 158 |
+
## Project Structure
|
| 159 |
+
|
| 160 |
+
### For Environment Creators
|
| 161 |
+
|
| 162 |
+
Use the CLI to quickly scaffold a new environment:
|
| 163 |
+
|
| 164 |
+
```bash
|
| 165 |
+
openenv init my_env
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
This creates the following structure:
|
| 169 |
+
|
| 170 |
+
```
|
| 171 |
+
my_env/
|
| 172 |
+
├── .dockerignore # Docker build exclusions
|
| 173 |
+
├── __init__.py # Export YourAction, YourObservation, YourEnv
|
| 174 |
+
├── models.py # Define Action, Observation, State dataclasses
|
| 175 |
+
├── client.py # Implement YourEnv(HTTPEnvClient)
|
| 176 |
+
├── README.md # Document your environment
|
| 177 |
+
├── openenv.yaml # Environment manifest
|
| 178 |
+
├── pyproject.toml # Dependencies and package configuration
|
| 179 |
+
├── outputs/ # Runtime outputs (logs, evals) - gitignored
|
| 180 |
+
│ ├── logs/
|
| 181 |
+
│ └── evals/
|
| 182 |
+
└── server/
|
| 183 |
+
├── your_environment.py # Implement YourEnvironment(Environment)
|
| 184 |
+
├── app.py # Create FastAPI app
|
| 185 |
+
├── requirements.txt # Dependencies for Docker (can be generated)
|
| 186 |
+
└── Dockerfile # Define container image
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
#### Dependency Management
|
| 190 |
+
|
| 191 |
+
OpenEnv uses `pyproject.toml` as the primary dependency specification:
|
| 192 |
+
|
| 193 |
+
- **Environment-level `pyproject.toml`**: Each environment defines its own dependencies
|
| 194 |
+
- **Root-level `pyproject.toml`**: Contains shared core dependencies (fastapi, pydantic, uvicorn)
|
| 195 |
+
- **Server `requirements.txt`**: Can be auto-generated from `pyproject.toml` for Docker builds
|
| 196 |
+
|
| 197 |
+
**Development Workflow:**
|
| 198 |
+
|
| 199 |
+
```bash
|
| 200 |
+
# Install environment in editable mode
|
| 201 |
+
cd my_env
|
| 202 |
+
pip install -e .
|
| 203 |
+
|
| 204 |
+
# Or using uv (faster)
|
| 205 |
+
uv pip install -e .
|
| 206 |
+
|
| 207 |
+
# Run server locally without Docker
|
| 208 |
+
uv run server --host 0.0.0.0 --port 8000
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
**Benefits:**
|
| 212 |
+
- ✅ **Client-side extensions**: Modify client classes locally without repo changes
|
| 213 |
+
- ✅ **Better dependency management**: Clear separation between environments
|
| 214 |
+
- ✅ **Flexible workflows**: Use pip, uv, or Docker for different scenarios
|
| 215 |
+
- ✅ **CI/CD ready**: Automated dependency generation and validation
|
| 216 |
+
|
| 217 |
+
See [`envs/README.md`](envs/README.md) for a complete guide on building environments.
|
| 218 |
+
|
| 219 |
+
### For Environment Users
|
| 220 |
+
|
| 221 |
+
To use an environment:
|
| 222 |
+
1. Import from `envs.your_env`: `from envs.echo_env import EchoAction, EchoEnv`
|
| 223 |
+
2. Create client: `client = EchoEnv.from_docker_image("echo-env:latest")`
|
| 224 |
+
3. Interact: `client.reset()`, `client.step(action)`, `client.state()`
|
| 225 |
+
4. Cleanup: `client.close()`
|
| 226 |
+
|
| 227 |
+
See example scripts in `examples/` directory.
|
| 228 |
+
|
| 229 |
+
## CLI Commands
|
| 230 |
+
|
| 231 |
+
The OpenEnv CLI provides commands to manage environments:
|
| 232 |
+
|
| 233 |
+
- **`openenv init <env_name>`** - Initialize a new environment from template
|
| 234 |
+
- **`openenv push [--repo-id <repo>] [--private]`** - Deploy environment to Hugging Face Spaces
|
| 235 |
+
|
| 236 |
+
### Quick Start
|
| 237 |
+
|
| 238 |
+
```bash
|
| 239 |
+
# Create a new environment
|
| 240 |
+
openenv init my_game_env
|
| 241 |
+
|
| 242 |
+
# Deploy to Hugging Face (will prompt for login if needed)
|
| 243 |
+
cd my_game_env
|
| 244 |
+
openenv push
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
For detailed options: `openenv init --help` and `openenv push --help`.
|
| 248 |
+
|
| 249 |
+
## Design Principles
|
| 250 |
+
|
| 251 |
+
1. **Separation of Concerns**: Clear client-server boundaries
|
| 252 |
+
2. **Type Safety**: Strongly-typed actions, observations, and state
|
| 253 |
+
3. **Container Isolation**: Each environment runs in its own container
|
| 254 |
+
4. **Simple APIs**: Minimal, intuitive interfaces
|
| 255 |
+
|
| 256 |
+
## Quick Start
|
| 257 |
+
|
| 258 |
+
### Using the Echo Environment(Example)
|
| 259 |
+
|
| 260 |
+
```python
|
| 261 |
+
from envs.echo_env import EchoAction, EchoEnv
|
| 262 |
+
|
| 263 |
+
# Automatically start container and connect
|
| 264 |
+
client = EchoEnv.from_docker_image("echo-env:latest")
|
| 265 |
+
|
| 266 |
+
# Reset the environment
|
| 267 |
+
result = client.reset()
|
| 268 |
+
print(result.observation.echoed_message) # "Echo environment ready!"
|
| 269 |
+
|
| 270 |
+
# Send messages
|
| 271 |
+
result = client.step(EchoAction(message="Hello, World!"))
|
| 272 |
+
print(result.observation.echoed_message) # "Hello, World!"
|
| 273 |
+
print(result.reward) # 1.3 (based on message length)
|
| 274 |
+
|
| 275 |
+
# Cleanup
|
| 276 |
+
client.close() # Stops and removes container
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
## Requirements
|
| 280 |
+
|
| 281 |
+
- Python 3.11+
|
| 282 |
+
- Docker Desktop or Docker Engine
|
| 283 |
+
- FastAPI >= 0.104.0
|
| 284 |
+
- Uvicorn >= 0.24.0
|
| 285 |
+
- Requests >= 2.25.0
|
| 286 |
+
- smolagents (for coding environment)
|
| 287 |
+
|
| 288 |
+
## Supported RL Tools
|
| 289 |
+
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.
|
| 290 |
+
|
| 291 |
+
### torchforge
|
| 292 |
+
See GRPO BlackJack training example: [`examples/grpo_blackjack/`](examples/grpo_blackjack/)
|
| 293 |
+
|
| 294 |
+
### TRL
|
| 295 |
+
See the [TRL example](https://huggingface.co/docs/trl/main/en/openenv) on how to integrate OpenEnv environments with GRPO training.
|
| 296 |
+
|
| 297 |
+
### Unsloth
|
| 298 |
+
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)
|
| 299 |
+
|
| 300 |
+
### SkyRL
|
| 301 |
+
See the [SkyRL example](https://skyrl.readthedocs.io/en/latest/examples/openenv.html) on how to train on OpenEnv environments with SkyRL.
|
| 302 |
+
|
| 303 |
+
### ART
|
| 304 |
+
See the [ART example](https://art.openpipe.ai/integrations/openenv-integration) on how OpenEnv environments can be used to train models with ART.
|
| 305 |
+
|
| 306 |
+
### Oumi
|
| 307 |
+
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.
|
| 308 |
+
|
| 309 |
+
## Example Environments
|
| 310 |
+
|
| 311 |
+
### Echo Environment
|
| 312 |
+
A simple environment that echoes back messages with metadata. Perfect for:
|
| 313 |
+
- Testing the HTTP server infrastructure
|
| 314 |
+
- Learning the framework basics
|
| 315 |
+
- Verifying container deployment
|
| 316 |
+
|
| 317 |
+
See: [`envs/echo_env/README.md`](envs/echo_env/README.md)
|
| 318 |
+
|
| 319 |
+
### Coding Environment
|
| 320 |
+
Executes arbitrary Python code in a sandboxed environment. Features:
|
| 321 |
+
- Safe code execution using smolagents
|
| 322 |
+
- Capture stdout, stderr, and exit codes
|
| 323 |
+
- Persistent execution context within episodes
|
| 324 |
+
- Error handling with detailed messages
|
| 325 |
+
|
| 326 |
+
See: [`envs/coding_env/README.md`](envs/coding_env/README.md)
|
| 327 |
+
|
| 328 |
+
## Community Support & Acknowledgments
|
| 329 |
+
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!!
|
| 330 |
+
|
| 331 |
+
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/), [Turing](https://www.turing.com/) ..
|
| 332 |
+
|
| 333 |
+
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!
|
| 334 |
+
|
| 335 |
+
## License
|
| 336 |
+
|
| 337 |
+
BSD 3-Clause License (see [LICENSE](./LICENSE) file)
|
src/openenv.egg-info/SOURCES.txt
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LICENSE
|
| 2 |
+
README.md
|
| 3 |
+
pyproject.toml
|
| 4 |
+
envs/atari_env/__init__.py
|
| 5 |
+
envs/atari_env/client.py
|
| 6 |
+
envs/atari_env/models.py
|
| 7 |
+
envs/atari_env/server/__init__.py
|
| 8 |
+
envs/atari_env/server/app.py
|
| 9 |
+
envs/atari_env/server/atari_environment.py
|
| 10 |
+
envs/browsergym_env/__init__.py
|
| 11 |
+
envs/browsergym_env/client.py
|
| 12 |
+
envs/browsergym_env/models.py
|
| 13 |
+
envs/browsergym_env/server/__init__.py
|
| 14 |
+
envs/browsergym_env/server/app.py
|
| 15 |
+
envs/browsergym_env/server/browsergym_environment.py
|
| 16 |
+
envs/chat_env/__init__.py
|
| 17 |
+
envs/chat_env/client.py
|
| 18 |
+
envs/chat_env/models.py
|
| 19 |
+
envs/chat_env/server/__init__.py
|
| 20 |
+
envs/chat_env/server/app.py
|
| 21 |
+
envs/chat_env/server/chat_environment.py
|
| 22 |
+
envs/chat_env/server/test_chat_env.py
|
| 23 |
+
envs/coding_env/__init__.py
|
| 24 |
+
envs/coding_env/client.py
|
| 25 |
+
envs/coding_env/models.py
|
| 26 |
+
envs/coding_env/server/__init__.py
|
| 27 |
+
envs/coding_env/server/app.py
|
| 28 |
+
envs/coding_env/server/python_codeact_env.py
|
| 29 |
+
envs/coding_env/server/python_executor.py
|
| 30 |
+
envs/coding_env/server/transforms.py
|
| 31 |
+
envs/connect4_env/__init__.py
|
| 32 |
+
envs/connect4_env/client.py
|
| 33 |
+
envs/connect4_env/models.py
|
| 34 |
+
envs/connect4_env/server/__init__.py
|
| 35 |
+
envs/connect4_env/server/app.py
|
| 36 |
+
envs/connect4_env/server/connect4_environment.py
|
| 37 |
+
envs/dipg_safety_env/__init__.py
|
| 38 |
+
envs/dipg_safety_env/client.py
|
| 39 |
+
envs/dipg_safety_env/models.py
|
| 40 |
+
envs/dipg_safety_env/server/__init__.py
|
| 41 |
+
envs/dipg_safety_env/server/app.py
|
| 42 |
+
envs/dipg_safety_env/server/dipg_environment.py
|
| 43 |
+
envs/echo_env/__init__.py
|
| 44 |
+
envs/echo_env/client.py
|
| 45 |
+
envs/echo_env/models.py
|
| 46 |
+
envs/echo_env/build/lib/server/__init__.py
|
| 47 |
+
envs/echo_env/build/lib/server/app.py
|
| 48 |
+
envs/echo_env/build/lib/server/echo_environment.py
|
| 49 |
+
envs/echo_env/server/__init__.py
|
| 50 |
+
envs/echo_env/server/app.py
|
| 51 |
+
envs/echo_env/server/echo_environment.py
|
| 52 |
+
envs/finrl_env/__init__.py
|
| 53 |
+
envs/finrl_env/client.py
|
| 54 |
+
envs/finrl_env/models.py
|
| 55 |
+
envs/finrl_env/server/__init__.py
|
| 56 |
+
envs/finrl_env/server/app.py
|
| 57 |
+
envs/finrl_env/server/finrl_environment.py
|
| 58 |
+
envs/git_env/__init__.py
|
| 59 |
+
envs/git_env/client.py
|
| 60 |
+
envs/git_env/models.py
|
| 61 |
+
envs/git_env/server/__init__.py
|
| 62 |
+
envs/git_env/server/app.py
|
| 63 |
+
envs/git_env/server/git_task_environment.py
|
| 64 |
+
envs/openspiel_env/__init__.py
|
| 65 |
+
envs/openspiel_env/client.py
|
| 66 |
+
envs/openspiel_env/models.py
|
| 67 |
+
envs/openspiel_env/server/__init__.py
|
| 68 |
+
envs/openspiel_env/server/app.py
|
| 69 |
+
envs/openspiel_env/server/openspiel_environment.py
|
| 70 |
+
envs/openspiel_env/server/opponent_policies.py
|
| 71 |
+
envs/play/build/lib/server/__init__.py
|
| 72 |
+
envs/play/build/lib/server/app.py
|
| 73 |
+
envs/play/build/lib/server/play_environment.py
|
| 74 |
+
envs/sumo_rl_env/__init__.py
|
| 75 |
+
envs/sumo_rl_env/client.py
|
| 76 |
+
envs/sumo_rl_env/models.py
|
| 77 |
+
envs/sumo_rl_env/server/__init__.py
|
| 78 |
+
envs/sumo_rl_env/server/app.py
|
| 79 |
+
envs/sumo_rl_env/server/sumo_environment.py
|
| 80 |
+
envs/textarena_env/__init__.py
|
| 81 |
+
envs/textarena_env/client.py
|
| 82 |
+
envs/textarena_env/models.py
|
| 83 |
+
envs/textarena_env/rewards.py
|
| 84 |
+
envs/textarena_env/build/lib/server/__init__.py
|
| 85 |
+
envs/textarena_env/build/lib/server/app.py
|
| 86 |
+
envs/textarena_env/build/lib/server/environment.py
|
| 87 |
+
envs/textarena_env/server/__init__.py
|
| 88 |
+
envs/textarena_env/server/app.py
|
| 89 |
+
envs/textarena_env/server/environment.py
|
| 90 |
+
src/openenv/__init__.py
|
| 91 |
+
src/openenv.egg-info/PKG-INFO
|
| 92 |
+
src/openenv.egg-info/SOURCES.txt
|
| 93 |
+
src/openenv.egg-info/dependency_links.txt
|
| 94 |
+
src/openenv.egg-info/entry_points.txt
|
| 95 |
+
src/openenv.egg-info/requires.txt
|
| 96 |
+
src/openenv.egg-info/top_level.txt
|
| 97 |
+
src/openenv/cli/__init__.py
|
| 98 |
+
src/openenv/cli/__main__.py
|
| 99 |
+
src/openenv/cli/_cli_utils.py
|
| 100 |
+
src/openenv/cli/_validation.py
|
| 101 |
+
src/openenv/cli/commands/__init__.py
|
| 102 |
+
src/openenv/cli/commands/build.py
|
| 103 |
+
src/openenv/cli/commands/init.py
|
| 104 |
+
src/openenv/cli/commands/push.py
|
| 105 |
+
src/openenv/cli/commands/serve.py
|
| 106 |
+
src/openenv/cli/commands/validate.py
|
| 107 |
+
src/openenv/cli/templates/__init__.py
|
| 108 |
+
src/openenv/cli/templates/__pycache__/__init__.cpython-311.pyc
|
| 109 |
+
src/openenv/cli/templates/__pycache__/__init__.cpython-313.pyc
|
| 110 |
+
src/openenv/cli/templates/openenv_env/README.md
|
| 111 |
+
src/openenv/cli/templates/openenv_env/__init__.py
|
| 112 |
+
src/openenv/cli/templates/openenv_env/client.py
|
| 113 |
+
src/openenv/cli/templates/openenv_env/models.py
|
| 114 |
+
src/openenv/cli/templates/openenv_env/openenv.yaml
|
| 115 |
+
src/openenv/cli/templates/openenv_env/pyproject.toml
|
| 116 |
+
src/openenv/cli/templates/openenv_env/server/Dockerfile
|
| 117 |
+
src/openenv/cli/templates/openenv_env/server/__ENV_NAME___environment.py
|
| 118 |
+
src/openenv/cli/templates/openenv_env/server/__init__.py
|
| 119 |
+
src/openenv/cli/templates/openenv_env/server/app.py
|
| 120 |
+
src/openenv/cli/templates/openenv_env/server/requirements.txt
|
| 121 |
+
src/openenv/core/__init__.py
|
| 122 |
+
src/openenv/core/client_types.py
|
| 123 |
+
src/openenv/core/env_client.py
|
| 124 |
+
src/openenv/core/utils.py
|
| 125 |
+
src/openenv/core/containers/__init__.py
|
| 126 |
+
src/openenv/core/containers/test_local_docker_provider.py
|
| 127 |
+
src/openenv/core/containers/runtime/__init__.py
|
| 128 |
+
src/openenv/core/containers/runtime/providers.py
|
| 129 |
+
src/openenv/core/containers/runtime/uv_provider.py
|
| 130 |
+
src/openenv/core/env_server/__init__.py
|
| 131 |
+
src/openenv/core/env_server/base_transforms.py
|
| 132 |
+
src/openenv/core/env_server/exceptions.py
|
| 133 |
+
src/openenv/core/env_server/http_server.py
|
| 134 |
+
src/openenv/core/env_server/interfaces.py
|
| 135 |
+
src/openenv/core/env_server/route_config.py
|
| 136 |
+
src/openenv/core/env_server/serialization.py
|
| 137 |
+
src/openenv/core/env_server/types.py
|
| 138 |
+
src/openenv/core/env_server/web_interface.py
|
| 139 |
+
src/openenv/core/tools/__init__.py
|
| 140 |
+
src/openenv/core/tools/git_server_client.py
|
| 141 |
+
src/openenv/core/tools/local_python_executor.py
|
| 142 |
+
src/openenv_core/__init__.py
|
src/openenv.egg-info/dependency_links.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
src/openenv.egg-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
openenv = openenv.cli.__main__:main
|
src/openenv.egg-info/requires.txt
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.104.0
|
| 2 |
+
pydantic>=2.0.0
|
| 3 |
+
uvicorn>=0.24.0
|
| 4 |
+
requests>=2.25.0
|
| 5 |
+
typer>=0.9.0
|
| 6 |
+
rich>=13.0.0
|
| 7 |
+
pyyaml>=6.0
|
| 8 |
+
huggingface_hub>=0.20.0
|
| 9 |
+
openai>=2.7.2
|
| 10 |
+
tomli>=2.3.0
|
| 11 |
+
tomli-w>=1.2.0
|
| 12 |
+
websockets>=15.0.1
|
| 13 |
+
|
| 14 |
+
[all]
|
| 15 |
+
openenv[core]
|
| 16 |
+
openenv[cli]
|
| 17 |
+
|
| 18 |
+
[cli]
|
| 19 |
+
typer>=0.9.0
|
| 20 |
+
rich>=13.0.0
|
| 21 |
+
pyyaml>=6.0
|
| 22 |
+
huggingface_hub>=0.20.0
|
| 23 |
+
openai>=2.7.2
|
| 24 |
+
tomli>=2.3.0
|
| 25 |
+
tomli-w>=1.2.0
|
| 26 |
+
|
| 27 |
+
[core]
|
| 28 |
+
fastapi>=0.104.0
|
| 29 |
+
pydantic>=2.0.0
|
| 30 |
+
uvicorn>=0.24.0
|
| 31 |
+
requests>=2.25.0
|
| 32 |
+
websockets>=15.0.1
|
src/openenv.egg-info/top_level.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv
|
| 2 |
+
openenv_core
|