Jibrann commited on
Commit
283975a
Β·
verified Β·
1 Parent(s): 395aabe

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. Dockerfile +69 -69
  2. README.md +250 -7
  3. inference.py +41 -41
Dockerfile CHANGED
@@ -1,69 +1,69 @@
1
- ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
2
- FROM ${BASE_IMAGE} AS builder
3
-
4
- WORKDIR /app
5
-
6
- # Ensure git is available (required for installing dependencies from VCS)
7
- RUN apt-get update && \
8
- apt-get install -y --no-install-recommends git && \
9
- rm -rf /var/lib/apt/lists/*
10
-
11
- # Build argument to control whether we're building standalone or in-repo
12
- ARG BUILD_MODE=in-repo
13
- ARG ENV_NAME=app
14
-
15
- # Copy environment code (always at root of build context)
16
- COPY . /app/env
17
-
18
- # For in-repo builds, openenv is already vendored in the build context
19
- # For standalone builds, openenv will be installed via pyproject.toml
20
- WORKDIR /app/env
21
-
22
- # Ensure uv is available (for local builds where base image lacks it)
23
- RUN if ! command -v uv >/dev/null 2>&1; then \
24
- curl -LsSf https://astral.sh/uv/install.sh | sh && \
25
- mv /root/.local/bin/uv /usr/local/bin/uv && \
26
- mv /root/.local/bin/uvx /usr/local/bin/uvx; \
27
- fi
28
-
29
- # Install dependencies using uv sync
30
- # If uv.lock exists, use it; otherwise resolve on the fly
31
- RUN --mount=type=cache,target=/root/.cache/uv \
32
- if [ -f uv.lock ]; then \
33
- uv sync --frozen --no-install-project --no-editable; \
34
- else \
35
- uv sync --no-install-project --no-editable; \
36
- fi
37
-
38
- RUN --mount=type=cache,target=/root/.cache/uv \
39
- if [ -f uv.lock ]; then \
40
- uv sync --frozen --no-editable; \
41
- else \
42
- uv sync --no-editable; \
43
- fi
44
-
45
- # Final runtime stage
46
- FROM ${BASE_IMAGE}
47
-
48
- WORKDIR /app
49
-
50
- # Copy the virtual environment from builder
51
- COPY --from=builder /app/env/.venv /app/.venv
52
-
53
- # Copy the environment code
54
- COPY --from=builder /app/env /app/env
55
-
56
- # Set PATH to use the virtual environment
57
- ENV PATH="/app/.venv/bin:$PATH"
58
-
59
- # Set PYTHONPATH so imports work correctly
60
- ENV PYTHONPATH="/app/env:$PYTHONPATH"
61
-
62
- # Health check
63
- HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
64
- CMD curl -f http://localhost:8000/health || exit 1
65
-
66
- # Run the FastAPI server
67
- # The module path is constructed to work with the /app/env structure
68
- ENV ENABLE_WEB_INTERFACE=true
69
- CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
 
1
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
2
+ FROM ${BASE_IMAGE} AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Ensure git is available (required for installing dependencies from VCS)
7
+ RUN apt-get update && \
8
+ apt-get install -y --no-install-recommends git && \
9
+ rm -rf /var/lib/apt/lists/*
10
+
11
+ # Build argument to control whether we're building standalone or in-repo
12
+ ARG BUILD_MODE=in-repo
13
+ ARG ENV_NAME=app
14
+
15
+ # Copy environment code (always at root of build context)
16
+ COPY . /app/env
17
+
18
+ # For in-repo builds, openenv is already vendored in the build context
19
+ # For standalone builds, openenv will be installed via pyproject.toml
20
+ WORKDIR /app/env
21
+
22
+ # Ensure uv is available (for local builds where base image lacks it)
23
+ RUN if ! command -v uv >/dev/null 2>&1; then \
24
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
25
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
26
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
27
+ fi
28
+
29
+ # Install dependencies using uv sync
30
+ # If uv.lock exists, use it; otherwise resolve on the fly
31
+ RUN --mount=type=cache,target=/root/.cache/uv \
32
+ if [ -f uv.lock ]; then \
33
+ uv sync --frozen --no-install-project --no-editable; \
34
+ else \
35
+ uv sync --no-install-project --no-editable; \
36
+ fi
37
+
38
+ RUN --mount=type=cache,target=/root/.cache/uv \
39
+ if [ -f uv.lock ]; then \
40
+ uv sync --frozen --no-editable; \
41
+ else \
42
+ uv sync --no-editable; \
43
+ fi
44
+
45
+ # Final runtime stage
46
+ FROM ${BASE_IMAGE}
47
+
48
+ WORKDIR /app
49
+
50
+ # Copy the virtual environment from builder
51
+ COPY --from=builder /app/env/.venv /app/.venv
52
+
53
+ # Copy the environment code
54
+ COPY --from=builder /app/env /app/env
55
+
56
+ # Set PATH to use the virtual environment
57
+ ENV PATH="/app/.venv/bin:$PATH"
58
+
59
+ # Set PYTHONPATH so imports work correctly
60
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
61
+
62
+ # Health check
63
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
64
+ CMD curl -f http://localhost:8000/health || exit 1
65
+
66
+ # Run the FastAPI server
67
+ # The module path is constructed to work with the /app/env structure
68
+ ENV ENABLE_WEB_INTERFACE=true
69
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,7 +1,250 @@
1
- ---
2
- title: Object Placement
3
- emoji: πŸ”Š
4
- colorFrom: purple
5
- colorTo: yellow
6
- base_path: /web
7
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Object Placer
3
+ emoji: πŸ”Š
4
+ colorFrom: purple
5
+ colorTo: yellow
6
+ base_path: /web
7
+ ---
8
+
9
+ # App Environment
10
+
11
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
12
+
13
+ ## Quick Start
14
+
15
+ The simplest way to use the App environment is through the `AppEnv` class:
16
+
17
+ ```python
18
+ from app import AppAction, AppEnv
19
+
20
+ try:
21
+ # Create environment from Docker image
22
+ appenv = AppEnv.from_docker_image("app-env:latest")
23
+
24
+ # Reset
25
+ result = appenv.reset()
26
+ print(f"Reset: {result.observation.echoed_message}")
27
+
28
+ # Send multiple messages
29
+ messages = ["Hello, World!", "Testing echo", "Final message"]
30
+
31
+ for msg in messages:
32
+ result = appenv.step(AppAction(message=msg))
33
+ print(f"Sent: '{msg}'")
34
+ print(f" β†’ Echoed: '{result.observation.echoed_message}'")
35
+ print(f" β†’ Length: {result.observation.message_length}")
36
+ print(f" β†’ Reward: {result.reward}")
37
+
38
+ finally:
39
+ # Always clean up
40
+ appenv.close()
41
+ ```
42
+
43
+ That's it! The `AppEnv.from_docker_image()` method handles:
44
+ - Starting the Docker container
45
+ - Waiting for the server to be ready
46
+ - Connecting to the environment
47
+ - Container cleanup when you call `close()`
48
+
49
+ ## Building the Docker Image
50
+
51
+ Before using the environment, you need to build the Docker image:
52
+
53
+ ```bash
54
+ # From project root
55
+ docker build -t app-env:latest -f server/Dockerfile .
56
+ ```
57
+
58
+ ## Deploying to Hugging Face Spaces
59
+
60
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
61
+
62
+ ```bash
63
+ # From the environment directory (where openenv.yaml is located)
64
+ openenv push
65
+
66
+ # Or specify options
67
+ openenv push --namespace my-org --private
68
+ ```
69
+
70
+ The `openenv push` command will:
71
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
72
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
73
+ 3. Upload to Hugging Face (ensuring you're logged in)
74
+
75
+ ### Prerequisites
76
+
77
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
78
+
79
+ ### Options
80
+
81
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
82
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
83
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
84
+ - `--private`: Deploy the space as private (default: public)
85
+
86
+ ### Examples
87
+
88
+ ```bash
89
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
90
+ openenv push
91
+
92
+ # Push to a specific repository
93
+ openenv push --repo-id my-org/my-env
94
+
95
+ # Push with a custom base image
96
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
97
+
98
+ # Push as a private space
99
+ openenv push --private
100
+
101
+ # Combine options
102
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
103
+ ```
104
+
105
+ After deployment, your space will be available at:
106
+ `https://huggingface.co/spaces/<repo-id>`
107
+
108
+ The deployed space includes:
109
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
110
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
111
+ - **Health Check** at `/health` - Container health monitoring
112
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
113
+
114
+ ## Environment Details
115
+
116
+ ### Action
117
+ **AppAction**: Contains a single field
118
+ - `message` (str) - The message to echo back
119
+
120
+ ### Observation
121
+ **AppObservation**: Contains the echo response and metadata
122
+ - `echoed_message` (str) - The message echoed back
123
+ - `message_length` (int) - Length of the message
124
+ - `reward` (float) - Reward based on message length (length Γ— 0.1)
125
+ - `done` (bool) - Always False for echo environment
126
+ - `metadata` (dict) - Additional info like step count
127
+
128
+ ### Reward
129
+ The reward is calculated as: `message_length Γ— 0.1`
130
+ - "Hi" β†’ reward: 0.2
131
+ - "Hello, World!" β†’ reward: 1.3
132
+ - Empty message β†’ reward: 0.0
133
+
134
+ ## Advanced Usage
135
+
136
+ ### Connecting to an Existing Server
137
+
138
+ If you already have a App environment server running, you can connect directly:
139
+
140
+ ```python
141
+ from app import AppEnv
142
+
143
+ # Connect to existing server
144
+ appenv = AppEnv(base_url="<ENV_HTTP_URL_HERE>")
145
+
146
+ # Use as normal
147
+ result = appenv.reset()
148
+ result = appenv.step(AppAction(message="Hello!"))
149
+ ```
150
+
151
+ Note: When connecting to an existing server, `appenv.close()` will NOT stop the server.
152
+
153
+ ### Using the Context Manager
154
+
155
+ The client supports context manager usage for automatic connection management:
156
+
157
+ ```python
158
+ from app import AppAction, AppEnv
159
+
160
+ # Connect with context manager (auto-connects and closes)
161
+ with AppEnv(base_url="http://localhost:8000") as env:
162
+ result = env.reset()
163
+ print(f"Reset: {result.observation.echoed_message}")
164
+ # Multiple steps with low latency
165
+ for msg in ["Hello", "World", "!"]:
166
+ result = env.step(AppAction(message=msg))
167
+ print(f"Echoed: {result.observation.echoed_message}")
168
+ ```
169
+
170
+ The client uses WebSocket connections for:
171
+ - **Lower latency**: No HTTP connection overhead per request
172
+ - **Persistent session**: Server maintains your environment state
173
+ - **Efficient for episodes**: Better for many sequential steps
174
+
175
+ ### Concurrent WebSocket Sessions
176
+
177
+ The server supports multiple concurrent WebSocket connections. To enable this,
178
+ modify `server/app.py` to use factory mode:
179
+
180
+ ```python
181
+ # In server/app.py - use factory mode for concurrent sessions
182
+ app = create_app(
183
+ AppEnvironment, # Pass class, not instance
184
+ AppAction,
185
+ AppObservation,
186
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
187
+ )
188
+ ```
189
+
190
+ Then multiple clients can connect simultaneously:
191
+
192
+ ```python
193
+ from app import AppAction, AppEnv
194
+ from concurrent.futures import ThreadPoolExecutor
195
+
196
+ def run_episode(client_id: int):
197
+ with AppEnv(base_url="http://localhost:8000") as env:
198
+ result = env.reset()
199
+ for i in range(10):
200
+ result = env.step(AppAction(message=f"Client {client_id}, step {i}"))
201
+ return client_id, result.observation.message_length
202
+
203
+ # Run 4 episodes concurrently
204
+ with ThreadPoolExecutor(max_workers=4) as executor:
205
+ results = list(executor.map(run_episode, range(4)))
206
+ ```
207
+
208
+ ## Development & Testing
209
+
210
+ ### Direct Environment Testing
211
+
212
+ Test the environment logic directly without starting the HTTP server:
213
+
214
+ ```bash
215
+ # From the server directory
216
+ python3 server/app_environment.py
217
+ ```
218
+
219
+ This verifies that:
220
+ - Environment resets correctly
221
+ - Step executes actions properly
222
+ - State tracking works
223
+ - Rewards are calculated correctly
224
+
225
+ ### Running Locally
226
+
227
+ Run the server locally for development:
228
+
229
+ ```bash
230
+ uvicorn server.app:app --reload
231
+ ```
232
+
233
+ ## Project Structure
234
+
235
+ ```
236
+ app/
237
+ β”œβ”€β”€ .dockerignore # Docker build exclusions
238
+ β”œβ”€β”€ __init__.py # Module exports
239
+ β”œβ”€β”€ README.md # This file
240
+ β”œβ”€β”€ openenv.yaml # OpenEnv manifest
241
+ β”œβ”€β”€ pyproject.toml # Project metadata and dependencies
242
+ β”œβ”€β”€ uv.lock # Locked dependencies (generated)
243
+ β”œβ”€β”€ client.py # AppEnv client
244
+ β”œβ”€β”€ models.py # Action and Observation models
245
+ └── server/
246
+ β”œβ”€β”€ __init__.py # Server module exports
247
+ β”œβ”€β”€ app_environment.py # Core environment logic
248
+ β”œβ”€β”€ app.py # FastAPI application (HTTP + WebSocket endpoints)
249
+ └── Dockerfile # Container image definition
250
+ ```
inference.py CHANGED
@@ -1,41 +1,41 @@
1
- import os
2
- import re
3
- import base64
4
- import textwrap
5
- from io import BytesIO
6
- from typing import List, Optional, Dict
7
-
8
- from openai import OpenAI
9
- import numpy as np
10
- from PIL import Image
11
-
12
-
13
- API_BASE_URL = os.getenv("API_BASE_URL")
14
- API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
15
- MODEL_NAME = os.getenv("MODEL_NAME")
16
-
17
- SYSTEM_PROMPT = textwrap.dedent(
18
- """
19
- You control a web browser through BrowserGym.
20
- Reply with exactly one action string.
21
- The action must be a valid BrowserGym command such as:
22
- - noop()
23
- - click('<BID>')
24
- - type('selector', 'text to enter')
25
- - fill('selector', 'text to enter')
26
- - send_keys('Enter')
27
- - scroll('down')
28
- Use single quotes around string arguments.
29
- When clicking, use the BrowserGym element IDs (BIDs) listed in the user message.
30
- If you are unsure, respond with noop().
31
- Do not include explanations or additional text.
32
- """
33
- ).strip()
34
-
35
-
36
- def main() -> None:
37
- client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
38
-
39
-
40
- if __name__ == "__main__":
41
- main()
 
1
+ import os
2
+ import re
3
+ import base64
4
+ import textwrap
5
+ from io import BytesIO
6
+ from typing import List, Optional, Dict
7
+
8
+ from openai import OpenAI
9
+ import numpy as np
10
+ from PIL import Image
11
+
12
+
13
+ API_BASE_URL = os.getenv("API_BASE_URL")
14
+ API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
15
+ MODEL_NAME = os.getenv("MODEL_NAME")
16
+
17
+ SYSTEM_PROMPT = textwrap.dedent(
18
+ """
19
+ You control a web browser through BrowserGym.
20
+ Reply with exactly one action string.
21
+ The action must be a valid BrowserGym command such as:
22
+ - noop()
23
+ - click('<BID>')
24
+ - type('selector', 'text to enter')
25
+ - fill('selector', 'text to enter')
26
+ - send_keys('Enter')
27
+ - scroll('down')
28
+ Use single quotes around string arguments.
29
+ When clicking, use the BrowserGym element IDs (BIDs) listed in the user message.
30
+ If you are unsure, respond with noop().
31
+ Do not include explanations or additional text.
32
+ """
33
+ ).strip()
34
+
35
+
36
+ def main() -> None:
37
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()