diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 0b7214dd5..8a76500f9 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -90,6 +90,9 @@ jobs: - name: unity-env dockerfile: server/Dockerfile context: envs/unity_env + - name: openapp-env + dockerfile: server/Dockerfile + context: envs/openapp_env steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index efa9be50b..4770903ea 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,9 @@ outputs/ .uv/ *.backup*/ + +# ignore the log_output +log_outputs/* + +# .sesskey +*.sesskey diff --git a/docs/environments.md b/docs/environments.md index 081dd4f9f..efa191734 100644 --- a/docs/environments.md +++ b/docs/environments.md @@ -224,6 +224,25 @@ The OpenEnv community has built a catalog of ready-to-run environments that cove +
+
+ OpenApp +

+ A web application simulation environment for OpenEnv that wraps the OpenApps framework and BrowserGym. +

+
+ +
+
Web Search diff --git a/envs/openapp_env/.gitignore b/envs/openapp_env/.gitignore new file mode 100644 index 000000000..389f6e995 --- /dev/null +++ b/envs/openapp_env/.gitignore @@ -0,0 +1 @@ +OpenApps diff --git a/envs/openapp_env/README.md b/envs/openapp_env/README.md new file mode 100644 index 000000000..7c258c38f --- /dev/null +++ b/envs/openapp_env/README.md @@ -0,0 +1,584 @@ +--- +title: OpenApp Environment Server +emoji: 🌐 +colorFrom: blue +colorTo: green +sdk: docker +pinned: false +app_port: 8000 +base_path: /web +tags: + - openenv + - OpenApps + - BrowserGym + - UI-Agents + - Reinforcement-Learning +--- + + + +
+ +# OpenApp Environment + +OpenApps Environment + +*A web application simulation environment for OpenEnv that wraps the [OpenApps](https://github.com/facebookresearch/OpenApps) framework and BrowserGym.* + +
+ +## Overview + +The OpenApp environment provides a simulated web application ecosystem where agents can interact with various apps (calendar, todo, messenger, maps) using browser-based actions. + +
+OpenApps Demo +
+ +This environment is ideal for: + +- Training and evaluating UI agents +- Testing web automation strategies +- Researching human-computer interaction +- Developing multimodal agents + +## Features + +- **Multiple Apps**: Interact with calendar, todo list, messenger, and map applications +- **Browser-Based Actions**: Click, fill forms, navigate, scroll, and more +- **Task-Based Evaluation**: Optional task goals with automatic reward calculation +- **Configurable**: Customize app configurations and behavior +- **BrowserGym Integration**: Built on top of BrowserGym for robust browser interaction + +## Directory Structure + +``` +openapp_env/ +ā”œā”€ā”€ __init__.py # Package exports +ā”œā”€ā”€ client.py # HTTP client for connecting to OpenApp +ā”œā”€ā”€ models.py # Data models for actions and observations +ā”œā”€ā”€ pyproject.toml # Package dependencies and configuration +ā”œā”€ā”€ openenv.yaml # OpenEnv environment configuration +ā”œā”€ā”€ test_openapp_env.py # Unit tests for environment structure +ā”œā”€ā”€ README.md # This file +ā”œā”€ā”€ IMPLEMENTATION.md # Implementation details and design decisions +ā”œā”€ā”€ example_usage.py # Basic usage example (legacy) +ā”œā”€ā”€ assets/ # Images and media +│ ā”œā”€ā”€ OpenApps_OpenEnv_RL.png # Environment overview diagram +│ └── openapps-demo.gif # Demo animation +└── server/ # Server-side environment implementation + ā”œā”€ā”€ __init__.py + ā”œā”€ā”€ app.py # FastAPI server application + ā”œā”€ā”€ openapp_environment.py # Core environment logic (BrowserGym + OpenApps) + ā”œā”€ā”€ Dockerfile # Docker image definition + └── start.sh # Container startup script (runs both servers) +``` + +**Key Components:** + +- **client.py**: `OpenAppEnv` class that extends `HTTPEnvClient` for remote environment interaction +- **models.py**: `OpenAppAction` and `OpenAppObservation` dataclasses with validation +- **server/openapp_environment.py**: `OpenAppEnvironment` class that wraps BrowserGym and OpenApps +- **server/app.py**: FastAPI server that exposes the environment via HTTP endpoints +- **server/Dockerfile**: Self-contained Docker image with OpenApps server and FastAPI server +- **server/start.sh**: Startup script that launches both OpenApps (port 5001) and FastAPI (port 8000) + +## Installation + +There are two ways to use the OpenApp environment: **Docker mode** (recommended, fully self-contained) or **Local mode** (requires manual server setup). + +### Option 1: Docker Mode (Recommended) + +Docker mode is fully self-contained and handles all dependencies automatically. No local installation required! + +**Step 1: Build the Docker image** + +The Docker image can be built in standalone mode using only public base images: + +```bash +# Build from the environment directory +cd envs/openapp_env +docker build -t openapp-env:latest -f server/Dockerfile . +``` + +**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: +```bash +export NO_PROXY=localhost,127.0.0.1 +cd envs/openapp_env +docker build -t openapp-env:latest -f server/Dockerfile . +``` + +**What gets installed in Docker:** +- **OpenEnv core**: Installed as a dependency +- **OpenApps**: Cloned from GitHub and installed (runs server inside container) +- **Core packages**: FastAPI, Uvicorn, Pydantic, Requests (from pyproject.toml) +- **BrowserGym**: For browser automation +- **Playwright**: Chromium browser for UI interaction +- **Web interface support**: Enabled by default via `ENABLE_WEB_INTERFACE=true` + +**How Docker mode works:** +The Docker container runs TWO services automatically: +1. **OpenApps server** (port 5001) - Provides the web applications (calendar, todo, messenger, maps) +2. **FastAPI server** (port 8000) - Exposes the OpenEnv HTTP API + +Both servers start automatically when the container launches. You only interact with port 8000. + +**Build details:** +- Base image: `python:3.11-slim` (public) +- Installation: Uses `pip install -e .` with pyproject.toml +- System deps: Playwright/Chromium dependencies for browser automation +- Size: ~5.7GB (includes Chromium browser and all dependencies) + +**Step 2: Run the example** +```bash +# For Meta/Corporate Networks with proxy, also set NO_PROXY: +export NO_PROXY=localhost,127.0.0.1 + +python examples/openapp_example.py --mode docker +``` + +**Note:** For Docker mode, you only need Python installed locally to run the example script. All environment dependencies are inside the Docker container. + +### Option 2: Local Mode + +Local mode requires manual setup of the OpenApps server. This mode is useful for development or when you need to customize the OpenApps configuration. + +**Prerequisites:** +- Python 3.11+ installed +- UV package manager (recommended) or pip + +**Step 1: Install openapp_env** +```bash +cd envs/openapp_env +pip install -e . +``` + +This installs the environment package along with dependencies (BrowserGym, Playwright, etc.). + +**Step 2: Install Playwright browsers** +```bash +playwright install chromium +``` + +**Step 3: Clone and set up OpenApps** (for running the server) +```bash +# Clone OpenApps repository +git clone https://github.com/facebookresearch/OpenApps.git +cd OpenApps + +# Install dependencies +uv sync # or: pip install -e . +``` + +**Why do I need the OpenApps repository?** + +The OpenApps Python package (installed via pip in Step 1) provides the library code, but the repository contains: +- `launch.py` - The server startup script +- `config/` - Hydra configuration files +- Application templates and assets + +In Docker mode, all of this is included in the container, so you don't need to clone anything. + +## Quick Start + +### Running with Docker (Recommended) + +Docker mode is the easiest way - everything is automated: + +```bash +# For Meta/Corporate networks with proxy, set NO_PROXY first: +export NO_PROXY=localhost,127.0.0.1 + +# Run the example +python examples/openapp_example.py --mode docker +``` + +The Docker container automatically: +- Starts the OpenApps server (port 5001) +- Starts the FastAPI server (port 8000) +- Manages both services for you + +No manual server setup required! + +**What happens inside the container:** + +When you run `from_docker_image()`, the following happens automatically: + +1. **Container Startup** (`/app/start.sh` runs): + ```bash + # Launches OpenApps server in background + cd /app/openapps + python launch.py & + + # Waits for port 5001 to be ready + # Then starts FastAPI server + uvicorn openapp_env.server.app:app --host 0.0.0.0 --port 8000 + ``` + +2. **Your client code** interacts only with port 8000: + ```python + client = OpenAppEnv.from_docker_image("openapp-env:latest") + # Client -> FastAPI (port 8000) -> OpenApps (port 5001) + ``` + +3. **On cleanup**, both servers are automatically stopped when the container is removed. + +### Running Locally + +For local usage, you need the OpenApps repository to run the server: + +**Step 1: Clone OpenApps (if you haven't already)** +```bash +git clone https://github.com/facebookresearch/OpenApps.git +cd OpenApps +uv sync +``` + +**Step 2: Start OpenApps Server** (in terminal 1) + +To run the server in **headless mode** (no browser window): +```bash +cd OpenApps # or wherever you cloned it +uv run launch.py + +# or instead of the uv run you can use the Python command: +python OpenApps/launch.py +``` + +To run the server with **visible browser** for visualization: +```bash +cd OpenApps +python OpenApps/launch.py browsergym_env_args.headless=False +``` + +Wait for the server to start (you'll see "Port 5001 is available" or similar). + +**Step 3: Run your code** (in terminal 2) +```bash +export OPENAPPS_URL=http://localhost:5001 +python examples/openapp_example.py --mode local +``` + +**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. + +### Example Script + +```bash +# Run with Docker (recommended) +python examples/openapp_example.py --mode docker + +# Run locally (requires OpenApps server running) +export OPENAPPS_URL=http://localhost:5001 +python examples/openapp_example.py --mode local + +# Show browser window to visualize agent actions +python examples/openapp_example.py --mode local --show-browser + +# Run with custom number of steps +python examples/openapp_example.py --mode docker --num-steps 20 + +# See all options +python examples/openapp_example.py --help +``` + +### Visualizing Agent Interactions + +There are multiple ways to see what the agent is doing: + +**Option 1: Show Browser Window (Local Mode)** + +The key is to start the OpenApps server with visualization enabled: + +```bash +# Terminal 1: Start OpenApps server with visible browser +cd OpenApps +python OpenApps/launch.py browsergym_env_args.headless=False + +# Terminal 2: Run your agent code +export OPENAPPS_URL=http://localhost:5001 +python examples/openapp_example.py --mode local +``` + +**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. + +**Option 2: Access Web Interface Directly** + +While the OpenApps server is running, open your browser to: +- Main page: `http://localhost:5001` +- Calendar: `http://localhost:5001/calendar` +- Todo: `http://localhost:5001/todo` +- Messenger: `http://localhost:5001/messages` +- Maps: `http://localhost:5001/maps` + +**Option 3: Docker Web Interface** + +When running in Docker mode, you can also access a web interface for manual testing: + +```bash +# Start a container and keep it running +docker run -d -p 8000:8000 openapp-env:latest + +# Access the web interface +# - Interactive UI: http://localhost:8000/web +# - API docs: http://localhost:8000/docs +# - OpenApps (internal): http://localhost:5001 (inside container) +``` + +**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. + +### Basic Usage + +```python +from envs.openapp_env import OpenAppAction, OpenAppEnv + +# Create environment from Docker image +client = OpenAppEnv.from_docker_image("openapp-env:latest") + +# Reset to initial state +result = client.reset() +print(f"Starting URL: {result.observation.url}") + +# Navigate to calendar app +result = client.step(OpenAppAction( + action_type="goto", + url="http://localhost:5001/calendar" +)) + +# Click on a button (example bid) +result = client.step(OpenAppAction( + action_type="click", + bid="add-event-btn" +)) + +# Fill in a form field +result = client.step(OpenAppAction( + action_type="fill", + bid="event-title-input", + text="Team Meeting" +)) + +print(f"Reward: {result.reward}") +print(f"Done: {result.done}") + +# Cleanup +client.close() +``` + +### Action Types + +The environment supports the following action types: + +- **click**: Click on an element + - Required: `bid` (BrowserGym element ID) + +- **fill**: Fill a text input field + - Required: `bid`, `text` + +- **select_option**: Select from dropdown + - Required: `bid`, `value` + +- **goto**: Navigate to a URL + - Required: `url` + +- **scroll**: Scroll the page + - Required: `direction` ("up" or "down") + +- **send_keys**: Send keyboard input + - Required: `text` + +- **noop**: No operation + +### Observations + +Each observation includes: + +- **html**: Current page HTML content +- **url**: Current page URL +- **open_pages_urls**: List of all open page URLs +- **active_page_index**: Index of currently active page +- **screenshot**: Base64-encoded screenshot (optional) +- **axtree_txt**: Accessibility tree for element interaction +- **app_state**: Current state of all apps (calendar events, todos, messages, etc.) +- **task_info**: Information about current task (if using tasks) +- **last_action_error**: Error message if last action failed + +## Configuration + +### Environment Parameters + +```python +from envs.openapp_env.server.openapp_environment import OpenAppEnvironment + +env = OpenAppEnvironment( + web_app_port=5001, # Port for OpenApps server + headless=True, # Run browser in headless mode + task_name="add_meeting", # Optional task name + apps_config={}, # App-specific configuration + max_steps=50, # Maximum steps per episode +) +``` + +**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. + +## Tasks and Rewards + +The environment can be configured with specific tasks from OpenApps. Tasks define: +- Goal state (e.g., "Add a meeting with Dennis to the calendar") +- Reward function based on app state changes +- Success criteria + +See [OpenApps documentation](https://facebookresearch.github.io/OpenApps/) for available tasks. + +## Example: Task-Based Training + +```python +from envs.openapp_env import OpenAppAction, OpenAppEnv + +# Create environment with a specific task +client = OpenAppEnv.from_docker_image("openapp-env:latest") + +# The task will guide the agent toward a specific goal +# Rewards will be based on progress toward completing the task +result = client.reset() + +# Agent interacts to complete the task +# ... agent logic here ... + +client.close() +``` + +## Development + +### Running Server Locally (without Docker) + +```bash +cd envs/openapp_env +uv run server +``` + +The server will start at `http://localhost:8000` + +### Testing + +```python +from openapp_env.server.openapp_environment import OpenAppEnvironment +from openapp_env.models import OpenAppAction + +def test_environment(): + env = OpenAppEnvironment() + + # Test reset + obs = env.reset() + assert obs.url != "" + + # Test step + action = OpenAppAction(action_type="noop") + obs = env.step(action) + assert env.state.step_count == 1 + + # Cleanup + env.close() + +test_environment() +``` + +## Attribution + +This environment integrates: +- [OpenApps](https://github.com/facebookresearch/OpenApps) - Web application simulation framework +- [BrowserGym](https://github.com/ServiceNow/BrowserGym) - Browser automation environment + +## Troubleshooting + +### Docker Build Issues + +**Error: `Container did not become ready`** + +If you're behind a corporate proxy (Meta/Facebook networks), set `NO_PROXY`: +```bash +export NO_PROXY=localhost,127.0.0.1 +docker build -t openapp-env:latest -f envs/openapp_env/server/Dockerfile . +``` + +**Error: `Environment variable 'USER' not found`** + +This is automatically handled in the Dockerfile with `ENV USER=root`. If you see this, rebuild the image. + +**Container exits immediately** + +Check the logs to see which server failed: +```bash +docker logs +``` + +Common causes: +- OpenApps server failed to start (check for port conflicts) +- Missing dependencies (rebuild with `--no-cache`) + +### Local Mode Issues + +**Error: `OPENAPPS_URL not set`** + +Set the environment variable before running: +```bash +export OPENAPPS_URL=http://localhost:5001 +python examples/openapp_example.py --mode local +``` + +**Error: `Connection refused to localhost:5001`** + +Make sure the OpenApps server is running: +```bash +cd OpenApps +uv run launch.py +``` + +**Browser visualization not working** + +The visualization is controlled by the **server**, not the client: +```bash +# Start server with visible browser +cd OpenApps +python launch.py browsergym_env_args.headless=False +``` + +### Performance Issues + +**Docker container is slow** + +The container runs both a full Chromium browser and web applications. For faster performance: +- Increase Docker memory allocation (6GB+ recommended) +- Use headless mode (default) +- Reduce `max_steps` in environment configuration + +**Large Docker image size** + +The image is ~5.7GB due to: +- Chromium browser (~1.5GB) +- OpenApps dependencies (~2GB) +- BrowserGym and ML libraries (~2GB) + +This is expected for a full browser automation environment. + +## License + +BSD 3-Clause License (see LICENSE file in OpenEnv root directory) + +## Citation + +If you use this environment in your research, please cite both OpenEnv and OpenApps: + +```bibtex +@article{ullrich2025openapps0, + title = {OpenApps: Simulating Environment Variations to Measure UI-Agent Reliability}, + 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}, + year = {2025}, + journal = {arXiv preprint arXiv: 2511.20766} +} +``` diff --git a/envs/openapp_env/__init__.py b/envs/openapp_env/__init__.py new file mode 100644 index 000000000..6bddd9367 --- /dev/null +++ b/envs/openapp_env/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""OpenApp Environment - Web application simulation environment for UI agents.""" + +from .client import OpenAppEnv +from .models import OpenAppAction, OpenAppObservation + +__all__ = ["OpenAppAction", "OpenAppObservation", "OpenAppEnv"] diff --git a/envs/openapp_env/assets/01-messages.mov b/envs/openapp_env/assets/01-messages.mov new file mode 100644 index 000000000..4efa54bf2 Binary files /dev/null and b/envs/openapp_env/assets/01-messages.mov differ diff --git a/envs/openapp_env/assets/02-editor.mov b/envs/openapp_env/assets/02-editor.mov new file mode 100644 index 000000000..ef2c21dad Binary files /dev/null and b/envs/openapp_env/assets/02-editor.mov differ diff --git a/envs/openapp_env/assets/03-calendar.mov b/envs/openapp_env/assets/03-calendar.mov new file mode 100644 index 000000000..03adce59f Binary files /dev/null and b/envs/openapp_env/assets/03-calendar.mov differ diff --git a/envs/openapp_env/assets/04-todo.mov b/envs/openapp_env/assets/04-todo.mov new file mode 100644 index 000000000..0b2cf43b1 Binary files /dev/null and b/envs/openapp_env/assets/04-todo.mov differ diff --git a/envs/openapp_env/assets/OpenApps_OpenEnv_RL.png b/envs/openapp_env/assets/OpenApps_OpenEnv_RL.png new file mode 100644 index 000000000..70514d7ea Binary files /dev/null and b/envs/openapp_env/assets/OpenApps_OpenEnv_RL.png differ diff --git a/envs/openapp_env/assets/demo-showcase.html b/envs/openapp_env/assets/demo-showcase.html new file mode 100644 index 000000000..d4583c466 --- /dev/null +++ b/envs/openapp_env/assets/demo-showcase.html @@ -0,0 +1,624 @@ + + + + + + OpenApp + OpenEnv Demo Showcase + + + + +
+ +
+
+ OpenApp Logo +
+

OpenApp + OpenEnv

+

+ A powerful integration bringing realistic web application environments to reinforcement learning agents. + Watch AI agents interact with calendar, messaging, code editor, and task management apps. +

+
+ + + + + BrowserGym Compatible + + + + + + 5+ Web Apps + + + + + + RL Ready + +
+
+ +
+
+

šŸŽ¬ Demo Showcase

+

Watch our AI agents perform real tasks in realistic web environments

+
+ +
+ +
+
+ +
+
+ + + +
+
+
+
+
+
šŸ’¬
+

Messenger App

+
+

+ AI agent navigates conversations, types messages interactively, and sends them to contacts. + Demonstrates natural language input and real-time chat interactions. +

+
+
+ + +
+
+ +
+
+ + + +
+
+
+
+
+
šŸ’»
+

Code Editor

+
+

+ Agent creates files and writes a complete PyTorch training loop with syntax highlighting. + Shows code generation, file management, and save operations. +

+
+
+ + +
+
+ +
+
+ + + +
+
+
+
+
+
šŸ“…
+

Calendar App

+
+

+ Navigate between calendar and agenda views, browse events across months, + and view detailed event information. Perfect for scheduling tasks. +

+
+
+ + +
+
+ +
+
+ + + +
+
+
+
+
+
āœ…
+

Todo Manager

+
+

+ Browse and manage task lists, edit task details, mark items complete, + and organize priorities. Demonstrates CRUD operations on structured data. +

+
+
+
+ + +
+
+

✨ Key Features

+

Why OpenApp + OpenEnv is perfect for AI agent research

+
+
+
+
šŸŽ®
+

Gymnasium Compatible

+

+ Standard RL interface with observations, actions, and rewards. + Drop-in replacement for existing training pipelines. +

+
+
+
🌐
+

Real Web Apps

+

+ Authentic web applications with HTML, CSS, and JavaScript. + No simplified simulations – real browser interactions. +

+
+
+
šŸ”„
+

Configurable Tasks

+

+ YAML-based configuration for custom scenarios, data, and rewards. + Easily create new training environments. +

+
+
+
+
+ + + + + + diff --git a/envs/openapp_env/assets/openapps-demo.gif b/envs/openapp_env/assets/openapps-demo.gif new file mode 100644 index 000000000..97c3298a2 Binary files /dev/null and b/envs/openapp_env/assets/openapps-demo.gif differ diff --git a/envs/openapp_env/client.py b/envs/openapp_env/client.py new file mode 100644 index 000000000..6efc93710 --- /dev/null +++ b/envs/openapp_env/client.py @@ -0,0 +1,139 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +OpenApp Environment HTTP Client. + +This module provides the client for connecting to an OpenApp Environment server +over HTTP. +""" + +from typing import Any, Dict + +# Support both in-repo and standalone imports +try: + # In-repo imports (when running from OpenEnv repository) + from openenv.core.client_types import StepResult + from openenv.core.env_server.types import State + from openenv.core.env_client import EnvClient + from .models import OpenAppAction, OpenAppObservation +except ImportError: + # Standalone imports (when environment is standalone with openenv-core from pip) + from openenv.core.client_types import StepResult + from openenv.core.env_server.types import State + from openenv.core.env_client import EnvClient + from openapp_env.models import OpenAppAction, OpenAppObservation + + +class OpenAppEnv(EnvClient[OpenAppAction, OpenAppObservation, State]): + """ + HTTP client for the OpenApp Environment. + + This client connects to an OpenAppEnvironment HTTP server and provides + methods to interact with it: reset(), step(), and state access. + + The OpenApp environment simulates web applications (calendar, todo, messenger, maps) + and allows agents to interact with them using browser-based actions. + + Example: + >>> # Connect to a running server + >>> client = OpenAppEnv(base_url="http://localhost:8000") + >>> result = client.reset() + >>> print(result.observation.url) + >>> + >>> # Click on an element + >>> result = client.step(OpenAppAction(action_type="click", bid="123")) + >>> print(result.observation.html) + >>> print(result.reward) + + Example with Docker: + >>> # Automatically start container and connect + >>> client = OpenAppEnv.from_docker_image("openapp-env:latest") + >>> result = client.reset() + >>> # Fill a text field + >>> result = client.step(OpenAppAction( + ... action_type="fill", + ... bid="456", + ... text="Meeting with team" + ... )) + """ + + def _step_payload(self, action: OpenAppAction) -> Dict: + """ + Convert OpenAppAction to JSON payload for step request. + + Args: + action: OpenAppAction instance + + Returns: + Dictionary representation suitable for JSON encoding + """ + payload = { + "action_type": action.action_type, + } + + # Add optional fields if present + if action.bid is not None: + payload["bid"] = action.bid + if action.text is not None: + payload["text"] = action.text + if action.value is not None: + payload["value"] = action.value + if action.url is not None: + payload["url"] = action.url + if action.direction is not None: + payload["direction"] = action.direction + if action.metadata: + payload["metadata"] = action.metadata + + return payload + + def _parse_result(self, payload: Dict) -> StepResult[OpenAppObservation]: + """ + Parse server response into StepResult[OpenAppObservation]. + + Args: + payload: JSON response from server + + Returns: + StepResult with OpenAppObservation + """ + obs_data = payload.get("observation", {}) + observation = OpenAppObservation( + html=obs_data.get("html", ""), + url=obs_data.get("url", ""), + open_pages_urls=obs_data.get("open_pages_urls", []), + active_page_index=obs_data.get("active_page_index", 0), + screenshot=obs_data.get("screenshot"), + axtree_txt=obs_data.get("axtree_txt", ""), + app_state=obs_data.get("app_state", {}), + task_info=obs_data.get("task_info"), + last_action_error=obs_data.get("last_action_error"), + done=payload.get("done", False), + reward=payload.get("reward"), + metadata=obs_data.get("metadata", {}), + ) + + return StepResult( + observation=observation, + reward=payload.get("reward"), + done=payload.get("done", False), + ) + + def _parse_state(self, payload: Dict) -> State: + """ + Parse server response into State object. + + Args: + payload: JSON response from /state endpoint + + Returns: + State object with episode_id and step_count + """ + return State( + episode_id=payload.get("episode_id"), + step_count=payload.get("step_count", 0), + ) diff --git a/envs/openapp_env/example_usage.py b/envs/openapp_env/example_usage.py new file mode 100644 index 000000000..7010c684f --- /dev/null +++ b/envs/openapp_env/example_usage.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +Example usage of OpenApp Environment. + +This script demonstrates how to use the OpenApp environment with OpenEnv. + +For a complete runnable example, see: examples/openapp_example.py + +Visualization Options: + To see the browser window and watch agent interactions: + + Terminal 1: Start OpenApps server with visible browser + cd OpenApps + python OpenApps/launch.py browsergym_env_args.headless=False + + Terminal 2: Run your agent code + export OPENAPPS_URL=http://localhost:5001 + python examples/openapp_example.py --mode local + + Or access OpenApps web interface at http://localhost:5001 + Docker mode web interface at http://localhost:8000/web + +Important: + Browser visualization is controlled by the OpenApps SERVER, not the client. + Launch the server with 'browsergym_env_args.headless=False' to see the browser. +""" + +import sys +from pathlib import Path + +# Add src to path for local testing +sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) + +from envs.openapp_env import OpenAppAction, OpenAppEnv + + +def example_basic_usage(): + """Basic usage example.""" + print("=" * 60) + print("OpenApp Environment - Basic Usage Example") + print("=" * 60) + + # Option 1: Connect to a running server + print("\nOption 1: Connect to running server") + print("client = OpenAppEnv(base_url='http://localhost:8000')") + + # Option 2: Start from Docker image (recommended) + print("\nOption 2: Start from Docker image") + print("client = OpenAppEnv.from_docker_image('openapp-env:latest')") + + print("\n" + "-" * 60) + + +def example_actions(): + """Example of different action types.""" + print("\nExample Actions") + print("-" * 60) + + # Navigate to a page + print("\n1. Navigate to calendar app:") + print("action = OpenAppAction(") + print(" action_type='goto',") + print(" url='http://localhost:5001/calendar'") + print(")") + print("result = client.step(action)") + + # Click on an element + print("\n2. Click on a button:") + print("action = OpenAppAction(") + print(" action_type='click',") + print(" bid='add-event-btn' # BrowserGym element ID") + print(")") + print("result = client.step(action)") + + # Fill a form field + print("\n3. Fill in text input:") + print("action = OpenAppAction(") + print(" action_type='fill',") + print(" bid='event-title-input',") + print(" text='Team Meeting'") + print(")") + print("result = client.step(action)") + + # Select from dropdown + print("\n4. Select from dropdown:") + print("action = OpenAppAction(") + print(" action_type='select_option',") + print(" bid='time-select',") + print(" value='14:00'") + print(")") + print("result = client.step(action)") + + # Scroll the page + print("\n5. Scroll down:") + print("action = OpenAppAction(") + print(" action_type='scroll',") + print(" direction='down'") + print(")") + print("result = client.step(action)") + + # No operation + print("\n6. No operation (useful for observation):") + print("action = OpenAppAction(action_type='noop')") + print("result = client.step(action)") + + +def example_observations(): + """Example of observation structure.""" + print("\n\nObservation Structure") + print("-" * 60) + + print("\nAfter reset() or step(), you receive:") + print("result.observation.html # Current page HTML") + print("result.observation.url # Current URL") + print("result.observation.open_pages_urls # All open pages") + print("result.observation.axtree_txt # Accessibility tree") + print("result.observation.app_state # App states (calendar, todo, etc.)") + print("result.observation.task_info # Task information (if using tasks)") + print("result.observation.screenshot # Page screenshot (base64)") + print("result.observation.last_action_error # Error from last action") + print("result.reward # Step reward") + print("result.done # Episode done flag") + + +def example_complete_workflow(): + """Complete workflow example.""" + print("\n\nComplete Workflow Example") + print("=" * 60) + + example_code = """ +from envs.openapp_env import OpenAppAction, OpenAppEnv + +# Create client (starts Docker container) +client = OpenAppEnv.from_docker_image("openapp-env:latest") + +try: + # Reset environment + result = client.reset() + print(f"Starting at: {result.observation.url}") + + # Navigate to calendar + result = client.step(OpenAppAction( + action_type="goto", + url="http://localhost:5001/calendar" + )) + + # Click to add new event + result = client.step(OpenAppAction( + action_type="click", + bid="new-event-button" + )) + + # Fill event title + result = client.step(OpenAppAction( + action_type="fill", + bid="title-input", + text="Project Review Meeting" + )) + + # Fill event date + result = client.step(OpenAppAction( + action_type="fill", + bid="date-input", + text="2025-12-15" + )) + + # Submit form + result = client.step(OpenAppAction( + action_type="click", + bid="submit-button" + )) + + print(f"Reward: {result.reward}") + print(f"Done: {result.done}") + print(f"App State: {result.observation.app_state}") + +finally: + # Always cleanup + client.close() +""" + + print(example_code) + + +def example_with_tasks(): + """Example using OpenApps tasks.""" + print("\n\nUsing Tasks (Task-Based RL)") + print("=" * 60) + + example_code = """ +# Environment can be configured with specific tasks +# Tasks define goals and automatic reward calculation + +from envs.openapp_env.server.openapp_environment import OpenAppEnvironment + +env = OpenAppEnvironment( + openapps_url="http://localhost:5001", # OpenApps server URL + task_name="add_meeting_with_dennis", # Optional task name + headless=False, # Set to False to watch the browser + max_steps=50, +) + +obs = env.reset() +# Now the environment has a goal: add a meeting with Dennis +# Rewards will be based on progress toward this goal + +# Agent loop +done = False +while not done: + action = agent.get_action(obs) # Your agent + obs = env.step(action) + done = obs.done + +print(f"Task completed! Reward: {obs.reward}") +env.close() +""" + + print(example_code) + + +def example_visualization(): + """Example of visualization options.""" + print("\n\nVisualization Options") + print("=" * 60) + + example_code = """ +# Option 1: Show browser window (watch agent in real-time) +from envs.openapp_env.server.openapp_environment import OpenAppEnvironment + +env = OpenAppEnvironment( + openapps_url="http://localhost:5001", + headless=False, # Show browser window +) + +obs = env.reset() +# You'll see a browser window open! + +# Option 2: Access web interface manually +# While OpenApps server is running, open in your browser: +# - Main: http://localhost:5001 +# - Calendar: http://localhost:5001/calendar +# - Todo: http://localhost:5001/todo +# - Messenger: http://localhost:5001/messenger +# - Maps: http://localhost:5001/maps + +# Option 3: Use the example script with --show-browser +# python examples/openapp_example.py --mode local --show-browser +""" + + print(example_code) + + +def main(): + """Run all examples.""" + example_basic_usage() + example_actions() + example_observations() + example_complete_workflow() + example_with_tasks() + example_visualization() + + print("\n" + "=" * 60) + print("For a complete runnable example:") + print(" python examples/openapp_example.py --mode local --show-browser") + print("\nFor more information, see:") + print("- README.md in this directory") + print("- OpenApps docs: https://facebookresearch.github.io/OpenApps/") + print("- OpenEnv docs: https://meta-pytorch.org/OpenEnv/") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/envs/openapp_env/models.py b/envs/openapp_env/models.py new file mode 100644 index 000000000..d4d706286 --- /dev/null +++ b/envs/openapp_env/models.py @@ -0,0 +1,86 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +Data models for the OpenApp Environment. + +The OpenApp environment provides a simulated web application environment +for training and evaluating UI agents that interact with various apps +(calendar, todo, messenger, maps, etc.) using browser actions. +""" + +from typing import Any, Dict, List, Optional + +from pydantic import Field + +# Support both in-repo and standalone imports +try: + # In-repo imports (when running from OpenEnv repository) + from openenv.core.env_server.types import Action, Observation +except ImportError: + # Standalone imports (when environment is standalone with openenv-core from pip) + from openenv.core.env_server.types import Action, Observation + + +class OpenAppAction(Action): + """ + Action for the OpenApp environment. + + Supports BrowserGym-style actions for web interaction: + - click: Click on an element (requires bid - BrowserGym ID) + - fill: Fill a text field (requires bid and text) + - select_option: Select from dropdown (requires bid and value) + - goto: Navigate to URL (requires url) + - scroll: Scroll the page (requires direction) + - send_keys: Send keyboard input (requires text) + - noop: No operation + + Attributes: + action_type: Type of action to perform + bid: BrowserGym element ID (for click, fill, select_option) + text: Text content (for fill, send_keys) + value: Value to select (for select_option) + url: URL to navigate to (for goto) + direction: Scroll direction - 'up' or 'down' (for scroll) + """ + + action_type: str = Field( + ..., description="Type of action: click, fill, select_option, goto, scroll, send_keys, noop" + ) + bid: Optional[str] = Field(default=None, description="BrowserGym element ID") + text: Optional[str] = Field(default=None, description="Text content for fill or send_keys") + value: Optional[str] = Field(default=None, description="Value for select_option") + url: Optional[str] = Field(default=None, description="URL for goto action") + direction: Optional[str] = Field(default=None, description="Scroll direction: 'up' or 'down'") + + +class OpenAppObservation(Observation): + """ + Observation from the OpenApp environment. + + Provides comprehensive state information about the web apps and browser state. + + Attributes: + html: Current page HTML content + url: Current page URL + open_pages_urls: List of all open page URLs + active_page_index: Index of currently active page + screenshot: Base64-encoded screenshot (optional) + axtree_txt: Accessibility tree as text (for element interaction) + app_state: Current state of all apps (calendar, todo, messenger, map) + task_info: Information about the current task (if any) + last_action_error: Error message from last action (if failed) + """ + + html: str = Field(default="", description="Current page HTML content") + url: str = Field(default="", description="Current page URL") + open_pages_urls: List[str] = Field(default_factory=list, description="List of all open page URLs") + active_page_index: int = Field(default=0, ge=0, description="Index of currently active page") + screenshot: Optional[str] = Field(default=None, description="Base64-encoded screenshot") + axtree_txt: str = Field(default="", description="Accessibility tree as text") + app_state: Dict[str, Any] = Field(default_factory=dict, description="State of all apps") + task_info: Optional[Dict[str, Any]] = Field(default=None, description="Current task information") + last_action_error: Optional[str] = Field(default=None, description="Error from last action") diff --git a/envs/openapp_env/openenv.yaml b/envs/openapp_env/openenv.yaml new file mode 100644 index 000000000..91341f735 --- /dev/null +++ b/envs/openapp_env/openenv.yaml @@ -0,0 +1,6 @@ +spec_version: 1 +name: openapp_env +type: space +runtime: fastapi +app: server.app:app +port: 8000 diff --git a/envs/openapp_env/pyproject.toml b/envs/openapp_env/pyproject.toml new file mode 100644 index 000000000..b9504973e --- /dev/null +++ b/envs/openapp_env/pyproject.toml @@ -0,0 +1,58 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "openenv-openapp_env" +version = "0.1.0" +description = "OpenApp Environment for OpenEnv - web application simulation environment for UI agents" +requires-python = ">=3.11,<3.14" +dependencies = [ + # NOTE: openenv-core is NOT listed here to avoid openai version conflict + # It is installed separately in the Dockerfile with --no-deps to avoid + # openai>=2.7.2 conflicting with OpenApps' openai<2 requirement. + # For local development, install manually: + # pip install --no-deps "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git" + # pip install fastapi pydantic uvicorn requests websockets + # + # NOTE: open_apps is also NOT listed here for the same reason. + # Install manually for local development: + # pip install git+https://github.com/facebookresearch/OpenApps.git + # + # Server dependencies (these are installed by Dockerfile separately for openenv-core) + "fastapi>=0.115.0", + "pydantic>=2.0.0", + "uvicorn[standard]>=0.24.0", + "requests>=2.31.0", + "websockets>=15.0.1", + # BrowserGym dependencies + "browsergym>=0.13.3", + "playwright>=1.40.0", + # Additional dependencies for web app interaction + "python-multipart>=0.0.20", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.0.0", +] + +[project.scripts] +server = "openapp_env.server.app:main" + +[tool.setuptools] +packages = ["openapp_env", "openapp_env.server"] +package-dir = { "openapp_env" = ".", "openapp_env.server" = "server" } + +[tool.setuptools.package-data] +openapp_env = ["**/*.yaml", "**/*.yml", "**/*.md"] + +[tool.hatch.metadata] +allow-direct-references = true diff --git a/envs/openapp_env/server/Dockerfile b/envs/openapp_env/server/Dockerfile new file mode 100644 index 000000000..ebfc0fad3 --- /dev/null +++ b/envs/openapp_env/server/Dockerfile @@ -0,0 +1,128 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Dockerfile for OpenApp Environment +# This image provides OpenApps web application simulation for UI agent training +# +# This Dockerfile works for both local builds and HuggingFace Spaces deployment: +# - Local build: cd envs/openapp_env && docker build -t openapp-env:latest -f server/Dockerfile . +# - HuggingFace: Automatically deployed via `openenv push` +# +# Run with web interface: +# docker run -p 8000:8000 -e ENABLE_WEB_INTERFACE=true openapp-env:latest + +FROM python:3.11-slim + +# Set metadata +LABEL maintainer="OpenEnv Team" +LABEL description="OpenApp Environment with BrowserGym for UI agent training" +LABEL org.opencontainers.image.source="https://github.com/meta-pytorch/OpenEnv" + +# Set working directory +WORKDIR /app/env + +# Install system dependencies +# - git: required to clone OpenApps from GitHub +# - curl: for healthcheck +# - Playwright/BrowserGym dependencies: fonts, libraries for browser automation +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + curl \ + ca-certificates \ + wget \ + gnupg \ + # Playwright/Chromium dependencies + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libdbus-1-3 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ + libatspi2.0-0 \ + libxshmfence1 \ + fonts-liberation \ + libappindicator3-1 \ + xdg-utils && \ + rm -rf /var/lib/apt/lists/* + +# Set environment variables +ENV PYTHONUNBUFFERED=1 + +# Set working directory +WORKDIR /app/env + +# Copy environment files +# Context is always the env directory (envs/openapp_env/) +# - GitHub Actions: uses context: envs/openapp_env +# - HuggingFace: openenv push uploads env dir as context +COPY . /app/env + +# Install OpenApps FIRST to establish openai<2 (required by agentlab) +# This must happen before openenv-core to avoid version conflict +WORKDIR /app +RUN git clone https://github.com/facebookresearch/OpenApps.git openapps && \ + cd openapps && \ + pip install --no-cache-dir -e . + +# Verify OpenApps installation +RUN python -c "import open_apps; print('āœ“ OpenApps installed')" + +# Install openenv-core from GitHub with --no-deps to avoid openai>=2.7.2 conflict +# Then install only the server dependencies (no openai needed for server) +RUN pip install --no-cache-dir --no-deps "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git" && \ + pip install --no-cache-dir fastapi pydantic uvicorn requests websockets + +# Install openapp_env and remaining dependencies +WORKDIR /app/env +RUN pip install --no-cache-dir -e . + +# Verify installation +RUN python -c "import openapp_env; print('āœ“ openapp_env installed')" && \ + python -c "import openapp_env.server.app; print('āœ“ openapp_env.server.app importable')" + +# Install Playwright browsers (Chromium for BrowserGym) +# We already installed system dependencies above, so just install the browser +RUN playwright install chromium + +# Copy startup script +WORKDIR /app/env +COPY server/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# OpenApp-specific environment variables (can be overridden at runtime) +ENV OPENAPPS_URL=http://localhost:5001 +ENV OPENAPPS_PORT=5001 +ENV OPENAPP_HEADLESS=true +ENV OPENAPP_MAX_STEPS=50 + +# Hydra requires USER environment variable +ENV USER=root + +# Enable web interface by default (set to false to disable) +ENV ENABLE_WEB_INTERFACE=true + +# Expose ports (8000 for FastAPI, 5001 for OpenApps) +EXPOSE 8000 5001 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run the startup script that launches both OpenApps server and FastAPI server +# Web interface will be available at /web if ENABLE_WEB_INTERFACE=true +# API documentation available at /docs +CMD ["/app/start.sh"] diff --git a/envs/openapp_env/server/__init__.py b/envs/openapp_env/server/__init__.py new file mode 100644 index 000000000..44ce51c6e --- /dev/null +++ b/envs/openapp_env/server/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""OpenApp Environment Server.""" diff --git a/envs/openapp_env/server/app.py b/envs/openapp_env/server/app.py new file mode 100644 index 000000000..18f3b984d --- /dev/null +++ b/envs/openapp_env/server/app.py @@ -0,0 +1,59 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +FastAPI application for the OpenApp Environment. + +This module creates an HTTP server that exposes the OpenAppEnvironment +over HTTP endpoints, making it compatible with HTTPEnvClient. + +Usage: + # Development (with auto-reload): + uvicorn server.app:app --reload --host 0.0.0.0 --port 8000 + + # Production: + uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4 + + # Or run directly: + uv run --project . server +""" + +# Support both in-repo and standalone imports +try: + # In-repo imports (when running from OpenEnv repository) + from openenv.core.env_server.http_server import create_app + from ..models import OpenAppAction, OpenAppObservation + from .openapp_environment import OpenAppEnvironment +except ImportError: + # Standalone imports (when environment is standalone with openenv-core from pip) + from openenv.core.env_server.http_server import create_app + from openapp_env.models import OpenAppAction, OpenAppObservation + from openapp_env.server.openapp_environment import OpenAppEnvironment + +# Create the app with web interface and README integration +# Pass the class (factory) instead of an instance for WebSocket session support +# Each client gets its own environment instance. The environment reads +# OPENAPPS_URL from environment variables in __init__. +app = create_app(OpenAppEnvironment, OpenAppAction, OpenAppObservation, env_name="openapp_env") + + +def main(): + """ + Entry point for direct execution via uv run or python -m. + + This function enables running the server without Docker: + uv run --project . server + python -m envs.openapp_env.server.app + openenv serve openapp_env + + """ + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000) + + +if __name__ == "__main__": + main() diff --git a/envs/openapp_env/server/openapp_environment.py b/envs/openapp_env/server/openapp_environment.py new file mode 100644 index 000000000..e58375bb6 --- /dev/null +++ b/envs/openapp_env/server/openapp_environment.py @@ -0,0 +1,659 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +OpenApp Environment Implementation. + +A web application simulation environment that wraps OpenApps and BrowserGym. +This environment provides agent interaction with simulated web apps including +calendar, todo, messenger, and maps applications. +""" + +import logging +import os +import subprocess +import time +import urllib.request +from pathlib import Path +from typing import Any, Dict, Optional, Tuple +from uuid import uuid4 + +logger = logging.getLogger(__name__) + +# Support both in-repo and standalone imports +try: + # In-repo imports (when running from OpenEnv repository) + from openenv.core.env_server.interfaces import Environment + from openenv.core.env_server.types import State + from ..models import OpenAppAction, OpenAppObservation +except ImportError: + # Standalone imports (when environment is standalone with openenv-core from pip) + from openenv.core.env_server.interfaces import Environment + from openenv.core.env_server.types import State + from openapp_env.models import OpenAppAction, OpenAppObservation + + +class GenericOpenAppsTask: + """ + A generic task for OpenApps interaction without specific goals. + + This is a simple wrapper that allows BrowserGym to interact with OpenApps + without requiring a specific task. For task-based interaction, use the + OpenAppsTask from open_apps.tasks.add_tasks_to_browsergym. + """ + + def __init__( + self, + base_url: str, + seed: int = 1, + **kwargs, + ) -> None: + """ + Initialize generic OpenApps task. + + Args: + base_url: Base URL of the OpenApps server + seed: Random seed (required by BrowserGym) + **kwargs: Additional arguments (ignored) + """ + try: + from browsergym.core.task import AbstractBrowserTask + import playwright.sync_api + except ImportError: + raise ImportError( + "BrowserGym is required. Install with: pip install browsergym" + ) + + # Store as instance attributes + self.base_url = base_url + self.seed = seed + + # BrowserGym task properties + self.viewport = {"width": 1024, "height": 768} + self.slow_mo = 100 + self.timeout = 5000 + + # Additional properties that BrowserGym might expect + self.locale = None + self.timezone_id = None + self.geolocation = None + + def setup( + self, page: "playwright.sync_api.Page" + ) -> Tuple[str, Dict[str, Any]]: + """ + Set up the task by navigating to the base URL. + + Args: + page: Playwright page object + + Returns: + Tuple of (goal_string, info_dict) + """ + page.goto(self.base_url) + return "Explore OpenApps", {} + + def teardown(self) -> None: + """Clean up after task completion.""" + pass + + def validate( + self, page: "playwright.sync_api.Page", chat_messages: list[str] + ) -> Tuple[float, bool, str, Dict[str, Any]]: + """ + Validate task state and return reward. + + Args: + page: Playwright page object + chat_messages: List of chat messages + + Returns: + Tuple of (reward, done, message, info) + """ + # Generic task never completes automatically + return 0.0, False, "", {} + + def cheat( + self, page: "playwright.sync_api.Page", chat_messages: list[str] + ) -> None: + """Cheat method (no-op for generic task).""" + pass + + +class OpenAppEnvironment(Environment): + """ + A web application environment that wraps OpenApps and BrowserGym. + + This environment launches OpenApps web server and provides a BrowserGym-like + interface for agents to interact with simulated web applications. + + Args: + openapps_path: Path to OpenApps directory (default: auto-detect) + web_app_port: Port for OpenApps web server (default: 5001) + headless: Run browser in headless mode (default: True) + task_name: Optional task name to evaluate (e.g., "add_meeting_with_dennis") + apps_config: Configuration for apps (default: all enabled) + max_steps: Maximum steps per episode (default: 50) + + Example: + >>> env = OpenAppEnvironment() + >>> obs = env.reset() + >>> print(obs.url) # Starting page URL + >>> + >>> # Click on an element + >>> action = OpenAppAction(action_type="click", bid="calendar-btn") + >>> obs = env.step(action) + >>> print(obs.html) + """ + + def __init__( + self, + openapps_url: Optional[str] = None, + openapps_path: Optional[str] = None, + web_app_port: int = 5001, + headless: bool = True, + task_name: Optional[str] = None, + apps_config: Optional[Dict[str, Any]] = None, + max_steps: int = 50, + ): + """Initialize the OpenApp environment.""" + self._state = State(episode_id=str(uuid4()), step_count=0) + self._max_steps = max_steps + + # OpenApps configuration + # Priority: 1. openapps_url, 2. OPENAPPS_URL env var, 3. Try to find/launch + self.openapps_url = openapps_url or os.environ.get("OPENAPPS_URL") + if not self.openapps_url: + self.web_app_port = web_app_port + self.openapps_url = f"http://localhost:{web_app_port}" + + self.openapps_path = openapps_path + self.headless = headless + self.task_name = task_name + self.apps_config = apps_config or {} + + # Runtime state + self._apps_process: Optional[subprocess.Popen] = None + self._browser_env = None + self._current_html = "" + self._current_url = "" + self._current_axtree = "" + self._app_state = {} + self._last_action_error = None + self._episode_reward = 0.0 + + def _detect_openapps_path(self) -> str: + """ + Auto-detect OpenApps path. + + Since OpenApps is installed as a Python package, we use the installed + package location instead of requiring a separate directory. + """ + # Check if user provided a custom path via environment variable + env_path = os.environ.get("OPENAPPS_PATH") + if env_path and Path(env_path).exists(): + return env_path + + # Try to find OpenApps as an installed package + try: + import open_apps + + openapps_pkg_path = Path(open_apps.__file__).parent.parent + if openapps_pkg_path.exists(): + return str(openapps_pkg_path) + except ImportError: + pass + + raise ValueError( + "OpenApps not found. Please install it with: " + "pip install git+https://github.com/facebookresearch/OpenApps.git " + "or set OPENAPPS_PATH environment variable." + ) + + def _launch_openapps_server(self) -> Optional[subprocess.Popen]: + """ + Launch OpenApps web server in background. + + Returns None if server is expected to be already running (OPENAPPS_URL set). + """ + # If OPENAPPS_URL is set, assume server is already running + if os.environ.get("OPENAPPS_URL"): + logger.info(f"Using existing OpenApps server at {self.openapps_url}") + # Wait for server to be available + self._wait_for_server(max_wait=5) + return None + + # Otherwise, provide helpful error message + raise NotImplementedError( + "Automatic OpenApps server launch is not yet implemented.\n" + "\n" + "Please start OpenApps manually in a separate terminal:\n" + " 1. Clone OpenApps: git clone https://github.com/facebookresearch/OpenApps.git\n" + " 2. Install: cd OpenApps && uv sync\n" + " 3. Run: uv run launch.py\n" + "\n" + "Then set the OPENAPPS_URL environment variable:\n" + " export OPENAPPS_URL=http://localhost:5001\n" + "\n" + "Or use Docker mode which handles this automatically:\n" + " python examples/openapp_example.py --mode docker\n" + ) + + def _wait_for_server(self, max_wait: int = 30): + """Wait for OpenApps server to become available.""" + for i in range(max_wait): + try: + response = urllib.request.urlopen(self.openapps_url, timeout=2) + if response.status == 200: + return + except Exception: + pass + time.sleep(1) + + raise TimeoutError(f"OpenApps server did not start within {max_wait} seconds") + + def _initialize_browser(self): + """Initialize BrowserGym environment for interaction.""" + try: + from browsergym.core.env import BrowserEnv + except ImportError: + raise ImportError( + "BrowserGym is required for OpenApp environment. " + "Install it with: pip install browsergym" + ) + + # Create BrowserGym environment with generic OpenApps task + self._browser_env = BrowserEnv( + task_entrypoint=GenericOpenAppsTask, + task_kwargs={"base_url": self.openapps_url}, + headless=self.headless, + slow_mo=200, # Slow down actions so they're visible (200ms delay) + ) + + def _get_current_observation(self) -> Dict[str, Any]: + """Extract current observation from browser state.""" + if self._browser_env is None: + return { + "html": "", + "url": self.openapps_url, + "open_pages_urls": [self.openapps_url], + "active_page_index": 0, + "axtree_txt": "", + "app_state": {}, + } + + # Get browser state (implementation depends on BrowserGym API) + # This is a simplified version - actual implementation would use BrowserGym's observation + return { + "html": self._current_html, + "url": self._current_url, + "open_pages_urls": [self._current_url], + "active_page_index": 0, + "axtree_txt": self._current_axtree, + "app_state": self._app_state, + } + + def reset(self) -> OpenAppObservation: + """ + Reset the environment. + + Returns: + OpenAppObservation with initial state + """ + # Reset state + self._state = State(episode_id=str(uuid4()), step_count=0) + self._episode_reward = 0.0 + self._last_action_error = None + + # Check if OpenApps server is running, start if needed + if self._apps_process is None and not os.environ.get("OPENAPPS_URL"): + self._apps_process = self._launch_openapps_server() + + # Initialize browser + if self._browser_env is None: + self._initialize_browser() + + # Reset the BrowserGym environment + try: + obs, info = self._browser_env.reset() + # Extract observation data from BrowserGym + self._current_url = obs.get("url", self.openapps_url) + self._current_html = obs.get("dom_txt", "") + self._current_axtree = obs.get("axtree_txt", "") + self._app_state = {} + except Exception as e: + logger.warning(f"Failed to reset browser environment: {e}") + # Fallback to placeholder values + self._current_url = self.openapps_url + self._current_html = "OpenApps Ready" + self._current_axtree = "" + self._app_state = {} + + obs_data = self._get_current_observation() + + return OpenAppObservation( + html=obs_data["html"], + url=obs_data["url"], + open_pages_urls=obs_data["open_pages_urls"], + active_page_index=obs_data["active_page_index"], + axtree_txt=obs_data["axtree_txt"], + app_state=obs_data["app_state"], + task_info={"task_name": self.task_name} if self.task_name else None, + last_action_error=None, + done=False, + reward=0.0, + ) + + def step(self, action: OpenAppAction) -> OpenAppObservation: # type: ignore[override] + """ + Execute a step in the environment. + + Args: + action: OpenAppAction to execute + + Returns: + OpenAppObservation with resulting state and reward + """ + self._state.step_count += 1 + self._last_action_error = None + reward = 0.0 + + try: + # Execute action based on type + if action.action_type == "click": + reward = self._execute_click(action.bid) + elif action.action_type == "fill": + reward = self._execute_fill(action.bid, action.text) + elif action.action_type == "select_option": + reward = self._execute_select(action.bid, action.value) + elif action.action_type == "goto": + reward = self._execute_goto(action.url) + elif action.action_type == "scroll": + reward = self._execute_scroll(action.direction) + elif action.action_type == "send_keys": + reward = self._execute_send_keys(action.text) + elif action.action_type == "noop": + reward = 0.0 + else: + self._last_action_error = f"Unknown action type: {action.action_type}" + reward = -0.1 + + except Exception as e: + self._last_action_error = str(e) + reward = -0.1 + + # Update cumulative reward + self._episode_reward += reward + + # Check if episode is done + done = self._state.step_count >= self._max_steps + + # Get current observation + obs_data = self._get_current_observation() + + return OpenAppObservation( + html=obs_data["html"], + url=obs_data["url"], + open_pages_urls=obs_data["open_pages_urls"], + active_page_index=obs_data["active_page_index"], + axtree_txt=obs_data["axtree_txt"], + app_state=obs_data["app_state"], + task_info={"task_name": self.task_name} if self.task_name else None, + last_action_error=self._last_action_error, + done=done, + reward=reward, + metadata={"cumulative_reward": self._episode_reward}, + ) + + def _execute_click(self, bid: str) -> float: + """Execute click action. Returns reward. + + Supports two modes: + 1. CSS selector mode: If bid starts with '#', '.', or '[', it's treated as a CSS selector + and uses Playwright directly (e.g., bid="#msg-input") + 2. BrowserGym mode: Otherwise, uses BrowserGym's accessibility tree bid + """ + if self._browser_env is None: + return 0.0 + + try: + # Check if bid is a CSS selector (starts with # or other CSS selector chars) + if bid.startswith('#') or bid.startswith('.') or bid.startswith('['): + # Use Playwright directly for CSS selectors + return self._execute_click_playwright(bid) + + # BrowserGym action format: click("bid") + action = f'click("{bid}")' + obs, reward, done, truncated, info = self._browser_env.step(action) + + # Update current state from observation + self._current_url = obs.get("url", self._current_url) + self._current_html = obs.get("dom_txt", self._current_html) + self._current_axtree = obs.get("axtree_txt", self._current_axtree) + + return float(reward) if reward else 0.0 + except Exception as e: + self._last_action_error = f"Click failed: {str(e)}" + return -0.1 + + def _execute_fill(self, bid: str, text: str) -> float: + """Execute fill action. Returns reward. + + Supports two modes: + 1. CSS selector mode: If bid starts with '#', it's treated as an HTML ID selector + and uses Playwright directly (e.g., bid="#msg-input") + 2. BrowserGym mode: Otherwise, uses BrowserGym's accessibility tree bid + """ + if self._browser_env is None: + return 0.0 + + try: + # Check if bid is a CSS selector (starts with # or other CSS selector chars) + if bid.startswith('#') or bid.startswith('.') or bid.startswith('['): + # Use Playwright directly for CSS selectors + return self._execute_fill_playwright(bid, text) + + # BrowserGym action format: fill("bid", "text") + action = f'fill("{bid}", "{text}")' + obs, reward, done, truncated, info = self._browser_env.step(action) + + # Update current state from observation + self._current_url = obs.get("url", self._current_url) + self._current_html = obs.get("dom_txt", self._current_html) + self._current_axtree = obs.get("axtree_txt", self._current_axtree) + + return float(reward) if reward else 0.0 + except Exception as e: + self._last_action_error = f"Fill failed: {str(e)}" + return -0.1 + + def _execute_fill_playwright(self, selector: str, text: str) -> float: + """Execute fill action using Playwright directly with CSS selector.""" + try: + # Access the underlying Playwright page from BrowserGym + page = self._browser_env.unwrapped.page + + # Wait for element and fill it + page.wait_for_selector(selector, timeout=5000) + page.fill(selector, text) + + # Small delay to let the page update + page.wait_for_timeout(200) + + # Update observation after action + self._update_observation_from_page(page) + + return 0.0 + except Exception as e: + self._last_action_error = f"Fill (Playwright) failed: {str(e)}" + return -0.1 + + def _execute_click_playwright(self, selector: str) -> float: + """Execute click action using Playwright directly with CSS selector.""" + try: + # Access the underlying Playwright page from BrowserGym + page = self._browser_env.unwrapped.page + + # Wait for element and click it + page.wait_for_selector(selector, timeout=5000) + page.click(selector) + + # Longer delay to let HTMX process the request + page.wait_for_timeout(500) + + # Update observation after action + self._update_observation_from_page(page) + + return 0.0 + except Exception as e: + self._last_action_error = f"Click (Playwright) failed: {str(e)}" + return -0.1 + + def _execute_press_key_playwright(self, key: str) -> float: + """Execute key press using Playwright directly.""" + try: + # Access the underlying Playwright page from BrowserGym + page = self._browser_env.unwrapped.page + + # Press the key + page.keyboard.press(key) + + # Delay to let the page update + page.wait_for_timeout(500) + + # Update observation after action + self._update_observation_from_page(page) + + return 0.0 + except Exception as e: + self._last_action_error = f"Press key (Playwright) failed: {str(e)}" + return -0.1 + + def _update_observation_from_page(self, page) -> None: + """Update internal observation state from Playwright page.""" + try: + self._current_url = page.url + # Note: We can't easily get axtree from Playwright directly, + # so we'll just update URL. The next BrowserGym action will sync the state. + except Exception: + pass + + def _execute_select(self, bid: str, value: str) -> float: + """Execute select option action. Returns reward.""" + if self._browser_env is None: + return 0.0 + + try: + # BrowserGym action format: select_option("bid", "value") + action = f'select_option("{bid}", "{value}")' + obs, reward, done, truncated, info = self._browser_env.step(action) + + # Update current state from observation + self._current_url = obs.get("url", self._current_url) + self._current_html = obs.get("dom_txt", self._current_html) + self._current_axtree = obs.get("axtree_txt", self._current_axtree) + + return float(reward) if reward else 0.0 + except Exception as e: + self._last_action_error = f"Select failed: {str(e)}" + return -0.1 + + def _execute_goto(self, url: str) -> float: + """Execute navigation action. Returns reward.""" + if self._browser_env is None: + self._current_url = url + return 0.0 + + try: + # BrowserGym action format: goto("url") + action = f'goto("{url}")' + obs, reward, done, truncated, info = self._browser_env.step(action) + + # Update current state from observation + self._current_url = obs.get("url", url) + self._current_html = obs.get("dom_txt", self._current_html) + self._current_axtree = obs.get("axtree_txt", self._current_axtree) + + return float(reward) if reward else 0.0 + except Exception as e: + self._last_action_error = f"Goto failed: {str(e)}" + self._current_url = url # Update URL even if failed + return -0.1 + + def _execute_scroll(self, direction: str) -> float: + """Execute scroll action. Returns reward.""" + if self._browser_env is None: + return 0.0 + + try: + # BrowserGym action format: scroll("direction") + action = f'scroll("{direction}")' + obs, reward, done, truncated, info = self._browser_env.step(action) + + # Update current state from observation + self._current_url = obs.get("url", self._current_url) + self._current_html = obs.get("dom_txt", self._current_html) + self._current_axtree = obs.get("axtree_txt", self._current_axtree) + + return float(reward) if reward else 0.0 + except Exception as e: + self._last_action_error = f"Scroll failed: {str(e)}" + return -0.1 + + def _execute_send_keys(self, text: str) -> float: + """Execute send keys action. Returns reward.""" + if self._browser_env is None: + return 0.0 + + try: + # Special handling for Enter key - use Playwright directly for reliability + if text == "\n" or text.lower() == "enter": + return self._execute_press_key_playwright("Enter") + + # BrowserGym action format: send_keys("text") + action = f'send_keys("{text}")' + obs, reward, done, truncated, info = self._browser_env.step(action) + + # Update current state from observation + self._current_url = obs.get("url", self._current_url) + self._current_html = obs.get("dom_txt", self._current_html) + self._current_axtree = obs.get("axtree_txt", self._current_axtree) + + return float(reward) if reward else 0.0 + except Exception as e: + self._last_action_error = f"Send keys failed: {str(e)}" + return -0.1 + + @property + def state(self) -> State: + """ + Get the current environment state. + + Returns: + Current State with episode_id and step_count + """ + return self._state + + def close(self): + """Clean up resources.""" + if hasattr(self, "_browser_env") and self._browser_env is not None: + try: + self._browser_env.close() + except Exception: + pass + self._browser_env = None + + if hasattr(self, "_apps_process") and self._apps_process is not None: + try: + self._apps_process.terminate() + self._apps_process.wait(timeout=5) + except Exception: + self._apps_process.kill() + self._apps_process = None + + def __del__(self): + """Cleanup on deletion.""" + self.close() diff --git a/envs/openapp_env/server/start.sh b/envs/openapp_env/server/start.sh new file mode 100755 index 000000000..360bab3e9 --- /dev/null +++ b/envs/openapp_env/server/start.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Startup script for OpenApp Environment Docker container +# This script starts both the OpenApps server and the FastAPI environment server + +set -e + +echo "Starting OpenApp Environment..." + +# Start OpenApps server in the background +echo "Starting OpenApps server on port ${OPENAPPS_PORT:-5001}..." +cd /app/openapps +# Run launch.py directly - it uses Hydra and needs the config directory +# Redirect OpenApps output to a log file so we can debug if needed +python launch.py > /tmp/openapps.log 2>&1 & +OPENAPPS_PID=$! + +# Wait for OpenApps server to be ready +echo "Waiting for OpenApps server to be ready..." +for i in {1..60}; do + # Check if OpenApps server is responding using curl + if curl -sf http://localhost:${OPENAPPS_PORT:-5001} >/dev/null 2>&1; then + echo "OpenApps server is ready on port ${OPENAPPS_PORT:-5001}!" + break + fi + if [ $i -eq 60 ]; then + echo "ERROR: OpenApps server failed to start within 60 seconds" + echo "OpenApps log output:" + cat /tmp/openapps.log || echo "No log file found" + kill $OPENAPPS_PID 2>/dev/null || true + exit 1 + fi + sleep 1 +done + +# Start the FastAPI environment server +echo "Starting FastAPI environment server on port 8000..." +cd /app/env +exec uvicorn openapp_env.server.app:app --host 0.0.0.0 --port 8000 diff --git a/envs/openapp_env/test_openapp_env.py b/envs/openapp_env/test_openapp_env.py new file mode 100644 index 000000000..f1d7ff94f --- /dev/null +++ b/envs/openapp_env/test_openapp_env.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +Simple test script for OpenApp Environment. + +This script tests the basic functionality of the OpenApp environment +to ensure it follows OpenEnv standards. + +Usage: + # From OpenEnv root directory + python3 envs/openapp_env/test_openapp_env.py + + # Or from openapp_env directory + cd envs/openapp_env + python3 test_openapp_env.py +""" + +import sys +from pathlib import Path + +# Add src to path for local testing +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from openapp_env.models import OpenAppAction, OpenAppObservation +from openapp_env.server.openapp_environment import OpenAppEnvironment + + +def test_models(): + """Test that models are properly defined.""" + print("Testing models...") + + # Test creating an action + action = OpenAppAction(action_type="noop") + assert action.action_type == "noop" + + # Test click action + click_action = OpenAppAction(action_type="click", bid="test-btn") + assert click_action.bid == "test-btn" + + # Test fill action + fill_action = OpenAppAction(action_type="fill", bid="input", text="Hello") + assert fill_action.text == "Hello" + + print("āœ“ Models test passed") + + +def test_environment_basic(): + """Test basic environment functionality.""" + print("\nTesting environment...") + + try: + # Create environment (note: this will check if OpenApps is installed as a package) + env = OpenAppEnvironment( + max_steps=10, + ) + + # Test that environment has required methods + assert hasattr(env, "reset") + assert hasattr(env, "step") + assert hasattr(env, "state") + assert hasattr(env, "close") + + print("āœ“ Environment structure test passed") + + except (ValueError, ImportError) as e: + # Expected if OpenApps is not installed as a package + if "OpenApps not found" in str(e) or "open_apps" in str(e): + print( + "āœ“ Environment structure test passed (OpenApps not installed, expected)" + ) + else: + raise + + +def test_client_server_contract(): + """Test that client and server follow the contract.""" + print("\nTesting client-server contract...") + + # Test that action can be serialized to dict + action = OpenAppAction( + action_type="click", bid="test-123", metadata={"test": "value"} + ) + + # Simulate what client._step_payload would do + payload = { + "action_type": action.action_type, + "bid": action.bid, + "metadata": action.metadata, + } + + assert payload["action_type"] == "click" + assert payload["bid"] == "test-123" + + # Test observation construction + obs = OpenAppObservation( + html="", + url="http://localhost:5001", + open_pages_urls=["http://localhost:5001"], + done=False, + reward=0.0, + ) + + assert obs.url == "http://localhost:5001" + assert obs.done is False + + print("āœ“ Client-server contract test passed") + + +def main(): + """Run all tests.""" + print("=" * 60) + print("OpenApp Environment - Structure Tests") + print("=" * 60) + + try: + test_models() + test_environment_basic() + test_client_server_contract() + + print("\n" + "=" * 60) + print("All tests passed! āœ“") + print("=" * 60) + print("\nNote: Full integration tests require:") + print( + "1. OpenApps installed: pip install git+https://github.com/facebookresearch/OpenApps.git" + ) + print("2. Playwright browsers installed: playwright install chromium") + print("3. BrowserGym dependencies installed") + + except Exception as e: + print(f"\nāœ— Test failed: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/examples/openapp_example.py b/examples/openapp_example.py new file mode 100644 index 000000000..b7ca05fa1 --- /dev/null +++ b/examples/openapp_example.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +Example usage of the OpenApp Environment. + +This script demonstrates how to use the OpenApp environment with OpenEnv. +It can be run in two modes: +1. With Docker: Uses the Docker image to run the environment +2. Local: Directly uses the OpenAppEnvironment class (requires OpenApps installed) + +Usage: + # Run with Docker (recommended) + python examples/openapp_example.py --mode docker + + # Run locally without Docker + python examples/openapp_example.py --mode local + + # Run with custom number of steps + python examples/openapp_example.py --mode docker --num-steps 20 + +Visualization Options: + # To SEE the browser window and watch agent interactions in real-time: + # + # Terminal 1: Start OpenApps server with visible browser + cd OpenApps + python OpenApps/launch.py browsergym_env_args.headless=False + + # Terminal 2: Run your agent code + export OPENAPPS_URL=http://localhost:5001 + python examples/openapp_example.py --mode local + + # Or access the web interface directly in your browser: + # - OpenApps: http://localhost:5001 + # - Calendar: http://localhost:5001/calendar + # - Todo: http://localhost:5001/todo + # - Messenger: http://localhost:5001/messages + # - Maps: http://localhost:5001/maps + + # Docker mode web interface + # - Web UI: http://localhost:8000/web + # - API docs: http://localhost:8000/docs + +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. +""" + +import argparse +import os +import sys +import time +from pathlib import Path + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def run_with_docker(num_steps: int = 15, headless: bool = True): + """Run OpenApp environment using Docker container.""" + from openapp_env import OpenAppAction, OpenAppEnv + + print("=" * 70) + print("Starting OpenApp environment with Docker...") + print(f"Headless mode: {headless}") + print("=" * 70) + + try: + # Create environment from Docker image + env = OpenAppEnv.from_docker_image("openapp-env:latest") + + # Reset to start a new session + print("\n[1/4] Resetting environment...") + result = env.reset() + print(f"āœ“ Environment reset") + print(f" Starting URL: {result.observation.url}") + print(f" Open pages: {len(result.observation.open_pages_urls)}") + print(f" HTML length: {len(result.observation.html)} characters") + + # Example actions to demonstrate different action types + actions = [ + { + "description": "Navigate to calendar app", + "action": OpenAppAction( + action_type="goto", url="http://localhost:5001/calendar" + ), + }, + { + "description": "Scroll down to see more content", + "action": OpenAppAction(action_type="scroll", direction="down"), + }, + { + "description": "Navigate to todo app", + "action": OpenAppAction( + action_type="goto", url="http://localhost:5001/todo" + ), + }, + { + "description": "Navigate to messenger app", + "action": OpenAppAction( + action_type="goto", url="http://localhost:5001/messenger" + ), + }, + { + "description": "Navigate to maps app", + "action": OpenAppAction( + action_type="goto", url="http://localhost:5001/maps" + ), + }, + { + "description": "Navigate back to home", + "action": OpenAppAction( + action_type="goto", url="http://localhost:5001" + ), + }, + ] + + # Run demonstration steps + print(f"\n[2/4] Running {min(num_steps, len(actions))} demonstration steps...") + for i, action_info in enumerate(actions[:num_steps]): + print(f"\nStep {i+1}: {action_info['description']}") + print(f" Action type: {action_info['action'].action_type}") + + result = env.step(action_info["action"]) + + print(f" āœ“ Action executed") + print(f" Current URL: {result.observation.url}") + print(f" Reward: {result.reward}") + print(f" Done: {result.done}") + + if result.observation.last_action_error: + print(f" āš ļø Error: {result.observation.last_action_error}") + + # Check app state if available + if result.observation.app_state: + print( + f" App state keys: {list(result.observation.app_state.keys())}" + ) + + # Small delay for readability + time.sleep(0.5) + + if result.done: + print(f"\nāœ“ Episode finished at step {i+1}!") + break + + # Get final state + print(f"\n[3/4] Getting final environment state...") + state = env.state() + print(f"āœ“ Final state:") + print(f" Episode ID: {state.episode_id}") + print(f" Total steps: {state.step_count}") + + # Summary + print(f"\n[4/4] Session Summary:") + print(f" Steps taken: {state.step_count}") + print(f" Final URL: {result.observation.url}") + print(f" Episode complete: {result.done}") + + # Web interface info + print(f"\n" + "=" * 70) + print(f"šŸ’” TIP: Access the web interface at http://localhost:8000/web") + print(f" - Interactive UI for manual testing") + print(f" - API documentation at http://localhost:8000/docs") + print(f"=" * 70) + + except Exception as e: + print(f"\nāŒ Error: {e}") + import traceback + + traceback.print_exc() + return 1 + finally: + print("\n[Cleanup] Closing environment...") + env.close() + print("āœ“ Environment closed") + + return 0 + + +def run_local(num_steps: int = 15, headless: bool = True): + """Run OpenApp environment locally without Docker.""" + + # Check if OPENAPPS_URL is set + if not os.environ.get("OPENAPPS_URL"): + print("=" * 70) + print("āŒ ERROR: OPENAPPS_URL not set") + print("=" * 70) + print("\nLocal mode requires OpenApps server to be running.") + print("\nPlease follow these steps:") + print("\n1. Start OpenApps server in a separate terminal:") + print(" cd /path/to/OpenApps") + print(" uv run launch.py") + print("\n2. Set the OPENAPPS_URL environment variable:") + print(" export OPENAPPS_URL=http://localhost:5001") + print("\n3. Run this script again:") + print(" python examples/openapp_example.py --mode local") + print("\nAlternatively, use Docker mode (recommended):") + print(" python examples/openapp_example.py --mode docker") + print("=" * 70) + return 1 + + try: + # NOTE: This example imports from the server module directly for local development. + # This is intentional for local testing/debugging where no HTTP server is involved. + # In production, use the client API (OpenAppEnv) which communicates over HTTP/WebSocket. + # See run_with_docker() for the recommended production pattern. + from openapp_env.models import OpenAppAction + from openapp_env.server.openapp_environment import OpenAppEnvironment + except ImportError as e: + print(f"āŒ Error importing local modules: {e}") + print("\nMake sure you have installed the environment:") + print(" cd envs/openapp_env") + print(" pip install -e .") + return 1 + + print("=" * 70) + print("Starting OpenApp environment locally...") + print(f"Using OpenApps server at: {os.environ.get('OPENAPPS_URL')}") + print(f"Headless mode: {headless}") + print("=" * 70) + + try: + # Create environment locally + print("\n[1/4] Initializing local environment...") + env = OpenAppEnvironment( + openapps_url=os.environ.get("OPENAPPS_URL"), + headless=headless, + max_steps=50, + ) + + # Reset environment + print("\n[2/4] Resetting environment...") + result = env.reset() + print(f"āœ“ Environment reset") + print(f" Starting URL: {result.url}") + print(f" HTML length: {len(result.html)} characters") + + # Take some example steps + print(f"\n[3/4] Running {num_steps} steps...") + for i in range(num_steps): + # Simple actions for demonstration + actions = [ + OpenAppAction( + action_type="goto", url=f"{os.environ.get('OPENAPPS_URL')}/calendar" + ), + OpenAppAction(action_type="scroll", direction="down"), + OpenAppAction( + action_type="goto", url=f"{os.environ.get('OPENAPPS_URL')}/todo" + ), + OpenAppAction(action_type="noop"), + ] + + action = actions[i % len(actions)] + result = env.step(action) + + if i % 5 == 0 or result.done: + print(f"Step {i+1}:") + print(f" Action: {action.action_type}") + print(f" URL: {result.url}") + print(f" Reward: {result.reward}") + print(f" Done: {result.done}") + + if result.done: + print(f"\nāœ“ Episode finished at step {i+1}!") + break + + # Get final state + print(f"\n[4/4] Final state:") + print(f" Episode ID: {env.state.episode_id}") + print(f" Total steps: {env.state.step_count}") + + except Exception as e: + print(f"\nāŒ Error: {e}") + import traceback + + traceback.print_exc() + return 1 + finally: + print("\n[Cleanup] Closing environment...") + env.close() + print("āœ“ Environment closed") + + return 0 + + +def main(): + parser = argparse.ArgumentParser( + description="OpenApp Environment Example", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Run with Docker (recommended) + python examples/openapp_example.py --mode docker + + # Run locally without Docker + python examples/openapp_example.py --mode local + + # Show browser window to visualize agent actions + python examples/openapp_example.py --mode local --show-browser + + # Run with custom number of steps + python examples/openapp_example.py --mode docker --num-steps 20 + +Visualization: + - Use --show-browser to see the browser window and watch agent interactions + - Access OpenApps web interface at http://localhost:5001 (when server is running) + - Docker mode web interface: http://localhost:8000/web + +Note: + - Docker mode requires: docker build -t openapp-env:latest -f envs/openapp_env/server/Dockerfile envs/openapp_env + - Local mode requires: pip install -e envs/openapp_env && playwright install chromium + """, + ) + + parser.add_argument( + "--mode", + choices=["docker", "local"], + default="docker", + help="Run mode: 'docker' (recommended) or 'local'", + ) + parser.add_argument( + "--num-steps", type=int, default=15, help="Number of steps to run (default: 15)" + ) + parser.add_argument( + "--headless", + action="store_true", + default=False, + help="Run browser in headless mode (no visible window)", + ) + parser.add_argument( + "--show-browser", + action="store_true", + default=False, + help="Show browser window (opposite of --headless, easier to remember)", + ) + + args = parser.parse_args() + + # Determine headless mode: default to True unless --show-browser is used + headless = not args.show_browser if args.show_browser else args.headless + + print("\n" + "=" * 70) + print("OpenApp Environment Example") + print("=" * 70) + print(f"Mode: {args.mode}") + print(f"Steps: {args.num_steps}") + print(f"Headless: {headless}") + + if args.mode == "docker": + return run_with_docker(args.num_steps, headless) + else: + return run_local(args.num_steps, headless) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/openapp_recording_demo.py b/examples/openapp_recording_demo.py new file mode 100644 index 000000000..c5b2c40a0 --- /dev/null +++ b/examples/openapp_recording_demo.py @@ -0,0 +1,873 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +""" +Demo script optimized for recording videos of OpenApp environment interactions. + +This script provides slower, more visible agent interactions suitable for video recording. + +SETUP FOR RECORDING: + +Terminal 1 - Start OpenApps server with visible browser: + cd OpenApps + python launch.py browsergym_env_args.headless=False + +Terminal 2 - Run this script: + export OPENAPPS_URL=http://localhost:5001 + python examples/openapp_recording_demo.py + +Then use screen recording software to capture the browser window. + +Options: + --scenario: Choose demo scenario (calendar, todo, messages, shopping, all) + --delay: Seconds to wait between actions (default: 2) + --verbose: Print detailed information during recording +""" + +import argparse +import os +import sys +import time +from pathlib import Path + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from openapp_env.models import OpenAppAction +from openapp_env.server.openapp_environment import OpenAppEnvironment + + +class RecordingDemo: + """Demo scenarios optimized for video recording.""" + + def __init__(self, openapps_url: str, delay: float = 2.0, verbose: bool = False): + """ + Initialize recording demo. + + Args: + openapps_url: URL of OpenApps server + delay: Seconds to wait between actions + verbose: Print detailed information + """ + self.openapps_url = openapps_url + self.delay = delay + self.verbose = verbose + self.env = None + + def setup(self): + """Set up the environment.""" + print("=" * 70) + print("OpenApp Recording Demo") + print("=" * 70) + print(f"OpenApps server: {self.openapps_url}") + print(f"Action delay: {self.delay}s") + print(f"Verbose mode: {self.verbose}") + print() + print("āŗļø START YOUR SCREEN RECORDING NOW!") + print(" Recording the browser window that appears...") + print() + + # Give user time to start recording + for i in range(3, 0, -1): + print(f" Starting in {i}...", end='\r') + time.sleep(1) + print("\n" + "=" * 70) + print() + + # Create environment + self.env = OpenAppEnvironment( + openapps_url=self.openapps_url, + headless=False, # Visible browser + max_steps=100, + ) + + def wait(self, message: str = None): + """Wait between actions with optional message.""" + if message and self.verbose: + print(f" {message}") + time.sleep(self.delay) + + def step(self, action: OpenAppAction, description: str): + """Execute action with description.""" + print(f"šŸŽ¬ {description}") + if self.verbose: + print(f" Action: {action.action_type}") + if hasattr(action, 'url') and action.url: + print(f" URL: {action.url}") + if hasattr(action, 'bid') and action.bid: + print(f" Element: {action.bid}") + if hasattr(action, 'text') and action.text: + print(f" Text: {action.text}") + + result = self.env.step(action) + + if self.verbose: + print(f" āœ“ Current URL: {result.url}") + if result.last_action_error: + print(f" āš ļø Error: {result.last_action_error}") + + self.wait() + return result + + def calendar_scenario(self): + """Demonstrate calendar interactions with meaningful actions.""" + print("\n" + "=" * 70) + print("SCENARIO: Calendar Management") + print("=" * 70) + print("Demonstrating: View calendar, switch views, navigate months, view events\n") + + self.wait("Resetting environment...") + self.env.reset() + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "1/10 - Navigate to home page" + ) + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar"), + "2/10 - Open calendar application (Calendar view)" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "3/10 - Scroll down to view calendar grid" + ) + + # Switch to Agenda view + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar?view=agenda"), + "4/10 - Switch to Agenda view" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "5/10 - Browse agenda events list" + ) + + # Switch back to Calendar view + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar?view=calendar"), + "6/10 - Switch back to Calendar view" + ) + + # Navigate to next month + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar/calendar_content/2026/1?view=calendar"), + "7/10 - Navigate to next month (January 2026)" + ) + + # Navigate to previous month + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar/calendar_content/2025/12?view=calendar"), + "8/10 - Navigate back to current month (December 2025)" + ) + + # View a specific event (if available) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar/event/1"), + "9/10 - View event details" + ) + + # Return to calendar + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar"), + "10/10 - Return to calendar main view" + ) + + print("āœ“ Calendar scenario complete!\n") + + def todo_scenario(self): + """Demonstrate todo list interactions with meaningful actions.""" + print("\n" + "=" * 70) + print("SCENARIO: Todo List Management") + print("=" * 70) + print("Demonstrating: Browse tasks, view edit interface, navigate todo items\n") + + self.wait("Resetting environment...") + self.env.reset() + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "1/11 - Navigate to home page" + ) + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo"), + "2/11 - Open todo list application" + ) + + # Give viewers time to see the initial todo list + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "3/11 - Browse through todo items" + ) + + # View edit interface for first todo + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo/edit/0"), + "4/11 - Open edit interface for first task" + ) + + # Return to main todo view + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo"), + "5/11 - Return to todo list" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "6/11 - Scroll through more tasks" + ) + + # View edit interface for another todo + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo/edit/5"), + "7/11 - Open edit interface for another task" + ) + + # Return to main todo view + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo"), + "8/11 - Return to todo list" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "9/11 - Final browse through todo items" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="up"), + "10/11 - Scroll back to top" + ) + + # Return to home + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "11/11 - Return to home page" + ) + + print("āœ“ Todo scenario complete!\n") + + def messages_scenario(self): + """Demonstrate messenger with actual message sending.""" + print("\n" + "=" * 70) + print("SCENARIO: Messenger - Send Message") + print("=" * 70) + print("Demonstrating: Browse conversations, send interactive message to Alice\n") + + self.wait("Resetting environment...") + self.env.reset() + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "1/14 - Navigate to home page" + ) + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/messages"), + "2/14 - Open messenger application" + ) + + # View conversations list + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "3/14 - Browse conversation list" + ) + # time.sleep(3) + # Open conversation with Alice + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/messages/Alice"), + "4/14 - Open conversation with Alice" + ) + + # Scroll to view conversation history + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "5/14 - Scroll through message history with Alice" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="up"), + "6/14 - Scroll back to message input" + ) + + # Click on the message input field to focus it + print(" → Clicking message input field to focus it...") + result = self.step( + OpenAppAction( + action_type="click", + bid="msg-input" + ), + "7/14 - Click message input field" + ) + if hasattr(result, 'last_action_error') and result.last_action_error: + print(f" āš ļø Error clicking input: {result.last_action_error}") + + # Type the message using fill with CSS selector (directly targets the input element by HTML ID) + print(" → Typing message using fill with CSS selector...") + result = self.step( + OpenAppAction( + action_type="fill", + bid="#msg-input", + text="Hi, We are submitting the OpenApp for OpenEnv Hackathon and we are super excited about this!" + ), + "8/14 - Type message: 'Hi, We are submitting the OpenApp for OpenEnv Hackathon...'" + ) + if hasattr(result, 'last_action_error') and result.last_action_error: + print(f" āš ļø Error typing message: {result.last_action_error}") + + # Pause to let viewers see the typed message + self.step( + OpenAppAction(action_type="noop"), + "9/14 - View typed message before sending" + ) + + # Send the message by pressing Enter + print(" → Pressing Enter to send message...") + result = self.step( + OpenAppAction( + action_type="send_keys", + text="\n" + ), + "10/14 - Press Enter to send message" + ) + if hasattr(result, 'last_action_error') and result.last_action_error: + print(f" āš ļø Error sending message: {result.last_action_error}") + + # Pause to see Alice's response + self.step( + OpenAppAction(action_type="noop"), + "11/14 - View Alice's response" + ) + + # Go back to messages list + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/messages"), + "12/14 - Return to conversations list" + ) + + # Return to home + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "13/14 - Return to home page" + ) + + # Final pause + self.step( + OpenAppAction(action_type="noop"), + "14/14 - Demo complete" + ) + + print("āœ“ Messenger scenario complete!\n") + + def codeeditor_scenario(self): + """Demonstrate code editor by typing a PyTorch training loop.""" + print("\n" + "=" * 70) + print("SCENARIO: Code Editor - PyTorch Training Loop") + print("=" * 70) + print("Demonstrating: Create file, type code interactively, save file\n") + + self.wait("Resetting environment...") + self.env.reset() + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "1/15 - Navigate to home page" + ) + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/codeeditor"), + "2/15 - Open code editor application" + ) + + # Give viewers time to see the code editor interface + self.step( + OpenAppAction(action_type="noop"), + "3/15 - View code editor interface with file tree" + ) + + # Create a new file + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/codeeditor/train.py"), + "4/15 - Create new file: train.py" + ) + + # The PyTorch training loop code to type + pytorch_code = '''import torch +import torch.nn as nn +import torch.optim as optim + +# Define a simple neural network +class SimpleNet(nn.Module): + def __init__(self): + super(SimpleNet, self).__init__() + self.fc1 = nn.Linear(784, 128) + self.fc2 = nn.Linear(128, 10) + self.relu = nn.ReLU() + + def forward(self, x): + x = self.relu(self.fc1(x)) + return self.fc2(x) + +# Initialize model, loss, and optimizer +model = SimpleNet() +criterion = nn.CrossEntropyLoss() +optimizer = optim.Adam(model.parameters(), lr=0.001) + +# Training loop +for epoch in range(10): + optimizer.zero_grad() + outputs = model(inputs) + loss = criterion(outputs, labels) + loss.backward() + optimizer.step() + print(f"Epoch {epoch}: Loss = {loss.item():.4f}") +''' + + # Type the code line by line for dramatic effect + lines = pytorch_code.strip().split('\n') + + # Type first few lines with imports + current_text = "" + for i, line in enumerate(lines[:4]): # imports + current_text += line + "\n" + + self.step( + OpenAppAction( + action_type="fill", + bid="#editor", + text=current_text.strip() + ), + "5/15 - Type imports: torch, nn, optim" + ) + + # Add class definition + for i, line in enumerate(lines[4:10]): # class header + current_text += line + "\n" + + self.step( + OpenAppAction( + action_type="fill", + bid="#editor", + text=current_text.strip() + ), + "6/15 - Define SimpleNet class with layers" + ) + + # Add forward method + for i, line in enumerate(lines[10:15]): # forward method + current_text += line + "\n" + + self.step( + OpenAppAction( + action_type="fill", + bid="#editor", + text=current_text.strip() + ), + "7/15 - Add forward() method" + ) + + # Add model initialization + for i, line in enumerate(lines[15:20]): # model init + current_text += line + "\n" + + self.step( + OpenAppAction( + action_type="fill", + bid="#editor", + text=current_text.strip() + ), + "8/15 - Initialize model, loss function, optimizer" + ) + + # Add training loop + for i, line in enumerate(lines[20:]): # training loop + current_text += line + "\n" + + self.step( + OpenAppAction( + action_type="fill", + bid="#editor", + text=current_text.strip() + ), + "9/15 - Add training loop with forward/backward pass" + ) + + # Pause to let viewers see the complete code + self.step( + OpenAppAction(action_type="noop"), + "10/15 - View complete PyTorch training code" + ) + + # Save the file by clicking the save button + self.step( + OpenAppAction(action_type="click", bid="Save"), + "13/15 - Save the file" + ) + + # Return to code editor index + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/codeeditor"), + "14/15 - Return to code editor to see saved file" + ) + + # Return to home + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "15/15 - Return to home page" + ) + + print("āœ“ Code Editor scenario complete!\n") + + def maps_scenario(self): + """Demonstrate maps with search and landmark exploration.""" + print("\n" + "=" * 70) + print("SCENARIO: Maps Exploration with Search") + print("=" * 70) + print("Demonstrating: Search locations, view landmarks, explore map areas\n") + + self.wait("Resetting environment...") + self.env.reset() + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "1/12 - Navigate to home page" + ) + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/maps"), + "2/12 - Open maps application" + ) + + # Give viewers time to see the initial map with landmarks + self.step( + OpenAppAction(action_type="noop"), + "3/12 - View map with landmarks and search interface" + ) + + # Scroll to explore different parts of the interface + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "4/12 - Scroll to view route planning interface" + ) + + # Pause to show the route planning form + self.step( + OpenAppAction(action_type="noop"), + "5/12 - View route planning controls (From/To location fields)" + ) + + # Scroll back up to the map + self.step( + OpenAppAction(action_type="scroll", direction="up"), + "6/12 - Scroll back to main map view" + ) + + # Use the search API to find a location (this demonstrates the search feature) + # Search for "San Francisco" to pan the map + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/maps/where?q=San+Francisco"), + "7/12 - Search for location: San Francisco" + ) + + # Return to maps to see the result + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/maps"), + "8/12 - Return to map view" + ) + + # Explore the map by scrolling + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "9/12 - Pan map to explore different areas" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="up"), + "10/12 - Pan map back" + ) + + self.step( + OpenAppAction(action_type="noop"), + "11/12 - Final view of map with all features" + ) + + # Return to home + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "12/12 - Return to home page" + ) + + print("āœ“ Maps scenario complete!\n") + + def app_tour_scenario(self): + """Tour through all applications with meaningful interactions.""" + print("\n" + "=" * 70) + print("SCENARIO: Complete App Tour") + print("=" * 70) + print("Demonstrating: All OpenApps applications with diverse interactions\n") + + self.wait("Starting tour...") + self.env.reset() + + # Home page + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "1/21 - Start at home page" + ) + + # === Calendar === + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar"), + "2/21 - Calendar: View calendar application" + ) + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "3/21 - Browse calendar events" + ) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar?view=agenda"), + "4/21 - Switch to Agenda view" + ) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/calendar/event/1"), + "5/21 - View event details" + ) + + # === Todo === + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo"), + "6/21 - Todo: Open task manager" + ) + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "7/21 - Browse todo items" + ) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo/edit/0"), + "8/21 - View task edit interface" + ) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/todo"), + "9/21 - Return to todo list" + ) + + # === Messenger === + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/messages"), + "10/26 - Messenger: View conversations list" + ) + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "11/26 - Browse conversations" + ) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/messages/Alice"), + "12/26 - Open conversation with Alice" + ) + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "13/26 - View message history" + ) + # Send a message to Alice + self.step( + OpenAppAction( + action_type="fill", + bid="#msg-input", + text="Hi, We are submitting the OpenApp for OpenEnv Hackathon and we are super excited about this!" + ), + "14/26 - Type hackathon message to Alice" + ) + self.step( + OpenAppAction(action_type="send_keys", text="\n"), + "15/26 - Send message (press Enter)" + ) + self.step( + OpenAppAction(action_type="noop"), + "16/26 - View Alice's response" + ) + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/messages"), + "17/26 - Return to conversations list" + ) + + # === Maps === + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/maps"), + "18/26 - Maps: Open navigation app" + ) + self.step( + OpenAppAction(action_type="noop"), + "19/26 - View maps with landmarks" + ) + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "20/26 - View route planning interface" + ) + + # Search for a location + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/maps/where?q=Golden+Gate+Park"), + "21/26 - Search: Golden Gate Park" + ) + + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/maps"), + "22/26 - Return to map view" + ) + + self.step( + OpenAppAction(action_type="scroll", direction="down"), + "23/26 - Explore map areas" + ) + + # Return home + self.step( + OpenAppAction(action_type="goto", url=f"{self.openapps_url}/"), + "24/26 - Return to home page" + ) + + print("āœ“ App tour complete!\n") + + def cleanup(self): + """Clean up and close environment.""" + print("\n" + "=" * 70) + print("šŸŽ¬ Recording Demo Complete!") + print("=" * 70) + print() + print("ā¹ļø STOP YOUR SCREEN RECORDING NOW!") + print() + print("Waiting 1 seconds before cleanup...") + time.sleep(1) + + if self.env: + self.env.close() + print("āœ“ Environment closed") + print() + + +def main(): + parser = argparse.ArgumentParser( + description="OpenApp Recording Demo - Optimized for video recording", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Tour all apps (recommended for comprehensive demo) + python examples/openapp_recording_demo.py --scenario all + + # Record calendar interactions only + python examples/openapp_recording_demo.py --scenario calendar + + # Record todo list interactions + python examples/openapp_recording_demo.py --scenario todo + + # Record messenger interactions + python examples/openapp_recording_demo.py --scenario messages + + # Record maps interactions + python examples/openapp_recording_demo.py --scenario maps + + # Record code editor with PyTorch training loop + python examples/openapp_recording_demo.py --scenario codeeditor + + # Slower pacing for detailed recording (3 seconds between actions) + python examples/openapp_recording_demo.py --scenario calendar --delay 3 + + # Verbose output with detailed logs + python examples/openapp_recording_demo.py --scenario all --verbose + +Before running: + 1. Terminal 1: cd OpenApps && python launch.py browsergym_env_args.headless=False + 2. Terminal 2: export OPENAPPS_URL=http://localhost:5001 + 3. Start your screen recording software + 4. Run this script + """, + ) + + parser.add_argument( + "--scenario", + choices=["calendar", "todo", "messages", "maps", "codeeditor", "all"], + default="all", + help="Demo scenario to record (default: all)", + ) + parser.add_argument( + "--delay", + type=float, + default=2.0, + help="Seconds to wait between actions (default: 2.0)", + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Print detailed information during recording", + ) + + args = parser.parse_args() + + # Check if OPENAPPS_URL is set + openapps_url = os.environ.get("OPENAPPS_URL") + if not openapps_url: + print("=" * 70) + print("āŒ ERROR: OPENAPPS_URL not set") + print("=" * 70) + print() + print("Please set up OpenApps server first:") + print() + print("Terminal 1 - Start OpenApps server with visible browser:") + print(" cd OpenApps") + print(" python launch.py browsergym_env_args.headless=False") + print() + print("Terminal 2 - Set URL and run this script:") + print(" export OPENAPPS_URL=http://localhost:5001") + print(" python examples/openapp_recording_demo.py") + print() + print("=" * 70) + return 1 + + # Create demo + demo = RecordingDemo( + openapps_url=openapps_url, + delay=args.delay, + verbose=args.verbose, + ) + + try: + # Setup + demo.setup() + + # Run selected scenario + if args.scenario == "all": + demo.app_tour_scenario() + elif args.scenario == "calendar": + demo.calendar_scenario() + elif args.scenario == "todo": + demo.todo_scenario() + elif args.scenario == "messages": + demo.messages_scenario() + elif args.scenario == "maps": + demo.maps_scenario() + elif args.scenario == "codeeditor": + demo.codeeditor_scenario() + + return 0 + + except KeyboardInterrupt: + print("\n\nāš ļø Recording interrupted by user") + return 1 + except Exception as e: + print(f"\n\nāŒ Error during recording: {e}") + import traceback + traceback.print_exc() + return 1 + finally: + demo.cleanup() + + +if __name__ == "__main__": + sys.exit(main())