muditjai commited on
Commit
add4140
·
verified ·
1 Parent(s): 19a35ce

Upload folder using huggingface_hub

Browse files
Files changed (13) hide show
  1. Dockerfile +81 -0
  2. README.md +250 -5
  3. __init__.py +16 -0
  4. client.py +99 -0
  5. models.py +100 -0
  6. openenv.yaml +7 -0
  7. pyproject.toml +45 -0
  8. server/__init__.py +11 -0
  9. server/app.py +81 -0
  10. server/compressionenv_environment.py +315 -0
  11. server/requirements.txt +6 -0
  12. spec.md +1 -0
  13. uv.lock +0 -0
Dockerfile ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local OpenEnv sources)
10
+ # - Standalone environments (with openenv from PyPI/Git)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=compressionenv
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv is already vendored in the build context
31
+ # For standalone builds, openenv will be installed via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ ENV ENABLE_WEB_INTERFACE=true
81
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,10 +1,255 @@
1
  ---
2
- title: Compressionenv
3
- emoji: 😻
4
- colorFrom: blue
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Compressionenv Environment Server
3
+ emoji: 🎾
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # Compressionenv Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the Compressionenv environment is through the `CompressionenvEnv` class:
21
+
22
+ ```python
23
+ from compressionenv import CompressionenvAction, CompressionenvEnv
24
+
25
+ try:
26
+ # Create environment from Docker image
27
+ compressionenvenv = CompressionenvEnv.from_docker_image("compressionenv-env:latest")
28
+
29
+ # Reset
30
+ result = compressionenvenv.reset()
31
+ print(f"Reset: {result.observation.echoed_message}")
32
+
33
+ # Send multiple messages
34
+ messages = ["Hello, World!", "Testing echo", "Final message"]
35
+
36
+ for msg in messages:
37
+ result = compressionenvenv.step(CompressionenvAction(message=msg))
38
+ print(f"Sent: '{msg}'")
39
+ print(f" → Echoed: '{result.observation.echoed_message}'")
40
+ print(f" → Length: {result.observation.message_length}")
41
+ print(f" → Reward: {result.reward}")
42
+
43
+ finally:
44
+ # Always clean up
45
+ compressionenvenv.close()
46
+ ```
47
+
48
+ That's it! The `CompressionenvEnv.from_docker_image()` method handles:
49
+ - Starting the Docker container
50
+ - Waiting for the server to be ready
51
+ - Connecting to the environment
52
+ - Container cleanup when you call `close()`
53
+
54
+ ## Building the Docker Image
55
+
56
+ Before using the environment, you need to build the Docker image:
57
+
58
+ ```bash
59
+ # From project root
60
+ docker build -t compressionenv-env:latest -f server/Dockerfile .
61
+ ```
62
+
63
+ ## Deploying to Hugging Face Spaces
64
+
65
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
+
67
+ ```bash
68
+ # From the environment directory (where openenv.yaml is located)
69
+ openenv push
70
+
71
+ # Or specify options
72
+ openenv push --namespace my-org --private
73
+ ```
74
+
75
+ The `openenv push` command will:
76
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
+ 3. Upload to Hugging Face (ensuring you're logged in)
79
+
80
+ ### Prerequisites
81
+
82
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
83
+
84
+ ### Options
85
+
86
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
+ - `--private`: Deploy the space as private (default: public)
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
+ openenv push
96
+
97
+ # Push to a specific repository
98
+ openenv push --repo-id my-org/my-env
99
+
100
+ # Push with a custom base image
101
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
+
103
+ # Push as a private space
104
+ openenv push --private
105
+
106
+ # Combine options
107
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
108
+ ```
109
+
110
+ After deployment, your space will be available at:
111
+ `https://huggingface.co/spaces/<repo-id>`
112
+
113
+ The deployed space includes:
114
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
+ - **Health Check** at `/health` - Container health monitoring
117
+ - **WebSocket** at `/ws` - Persistent session endpoint for low-latency interactions
118
+
119
+ ## Environment Details
120
+
121
+ ### Action
122
+ **CompressionenvAction**: Contains a single field
123
+ - `message` (str) - The message to echo back
124
+
125
+ ### Observation
126
+ **CompressionenvObservation**: Contains the echo response and metadata
127
+ - `echoed_message` (str) - The message echoed back
128
+ - `message_length` (int) - Length of the message
129
+ - `reward` (float) - Reward based on message length (length × 0.1)
130
+ - `done` (bool) - Always False for echo environment
131
+ - `metadata` (dict) - Additional info like step count
132
+
133
+ ### Reward
134
+ The reward is calculated as: `message_length × 0.1`
135
+ - "Hi" → reward: 0.2
136
+ - "Hello, World!" → reward: 1.3
137
+ - Empty message → reward: 0.0
138
+
139
+ ## Advanced Usage
140
+
141
+ ### Connecting to an Existing Server
142
+
143
+ If you already have a Compressionenv environment server running, you can connect directly:
144
+
145
+ ```python
146
+ from compressionenv import CompressionenvEnv
147
+
148
+ # Connect to existing server
149
+ compressionenvenv = CompressionenvEnv(base_url="<ENV_HTTP_URL_HERE>")
150
+
151
+ # Use as normal
152
+ result = compressionenvenv.reset()
153
+ result = compressionenvenv.step(CompressionenvAction(message="Hello!"))
154
+ ```
155
+
156
+ Note: When connecting to an existing server, `compressionenvenv.close()` will NOT stop the server.
157
+
158
+ ### Using the Context Manager
159
+
160
+ The client supports context manager usage for automatic connection management:
161
+
162
+ ```python
163
+ from compressionenv import CompressionenvAction, CompressionenvEnv
164
+
165
+ # Connect with context manager (auto-connects and closes)
166
+ with CompressionenvEnv(base_url="http://localhost:8000") as env:
167
+ result = env.reset()
168
+ print(f"Reset: {result.observation.echoed_message}")
169
+ # Multiple steps with low latency
170
+ for msg in ["Hello", "World", "!"]:
171
+ result = env.step(CompressionenvAction(message=msg))
172
+ print(f"Echoed: {result.observation.echoed_message}")
173
+ ```
174
+
175
+ The client uses WebSocket connections for:
176
+ - **Lower latency**: No HTTP connection overhead per request
177
+ - **Persistent session**: Server maintains your environment state
178
+ - **Efficient for episodes**: Better for many sequential steps
179
+
180
+ ### Concurrent WebSocket Sessions
181
+
182
+ The server supports multiple concurrent WebSocket connections. To enable this,
183
+ modify `server/app.py` to use factory mode:
184
+
185
+ ```python
186
+ # In server/app.py - use factory mode for concurrent sessions
187
+ app = create_app(
188
+ CompressionenvEnvironment, # Pass class, not instance
189
+ CompressionenvAction,
190
+ CompressionenvObservation,
191
+ max_concurrent_envs=4, # Allow 4 concurrent sessions
192
+ )
193
+ ```
194
+
195
+ Then multiple clients can connect simultaneously:
196
+
197
+ ```python
198
+ from compressionenv import CompressionenvAction, CompressionenvEnv
199
+ from concurrent.futures import ThreadPoolExecutor
200
+
201
+ def run_episode(client_id: int):
202
+ with CompressionenvEnv(base_url="http://localhost:8000") as env:
203
+ result = env.reset()
204
+ for i in range(10):
205
+ result = env.step(CompressionenvAction(message=f"Client {client_id}, step {i}"))
206
+ return client_id, result.observation.message_length
207
+
208
+ # Run 4 episodes concurrently
209
+ with ThreadPoolExecutor(max_workers=4) as executor:
210
+ results = list(executor.map(run_episode, range(4)))
211
+ ```
212
+
213
+ ## Development & Testing
214
+
215
+ ### Direct Environment Testing
216
+
217
+ Test the environment logic directly without starting the HTTP server:
218
+
219
+ ```bash
220
+ # From the server directory
221
+ python3 server/compressionenv_environment.py
222
+ ```
223
+
224
+ This verifies that:
225
+ - Environment resets correctly
226
+ - Step executes actions properly
227
+ - State tracking works
228
+ - Rewards are calculated correctly
229
+
230
+ ### Running Locally
231
+
232
+ Run the server locally for development:
233
+
234
+ ```bash
235
+ uvicorn server.app:app --reload
236
+ ```
237
+
238
+ ## Project Structure
239
+
240
+ ```
241
+ compressionenv/
242
+ ├── .dockerignore # Docker build exclusions
243
+ ├── __init__.py # Module exports
244
+ ├── README.md # This file
245
+ ├── openenv.yaml # OpenEnv manifest
246
+ ├── pyproject.toml # Project metadata and dependencies
247
+ ├── uv.lock # Locked dependencies (generated)
248
+ ├── client.py # CompressionenvEnv client
249
+ ├── models.py # Action and Observation models
250
+ └── server/
251
+ ├── __init__.py # Server module exports
252
+ ├── compressionenv_environment.py # Core environment logic
253
+ ├── app.py # FastAPI application (HTTP + WebSocket endpoints)
254
+ └── Dockerfile # Container image definition
255
+ ```
__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Compressionenv Environment."""
8
+
9
+ from .client import CompressionenvEnv
10
+ from .models import CompressionenvAction, CompressionenvObservation
11
+
12
+ __all__ = [
13
+ "CompressionenvAction",
14
+ "CompressionenvObservation",
15
+ "CompressionenvEnv",
16
+ ]
client.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Compressionenv Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core.client_types import StepResult
12
+ from openenv.core.env_server.types import State
13
+ from openenv.core import EnvClient
14
+
15
+ from .models import CompressionenvAction, CompressionenvObservation
16
+
17
+
18
+ class CompressionenvEnv(
19
+ EnvClient[CompressionenvAction, CompressionenvObservation]
20
+ ):
21
+ """
22
+ Client for the Compressionenv Environment.
23
+
24
+ This client maintains a persistent WebSocket connection to the environment server,
25
+ enabling efficient multi-step interactions with lower latency.
26
+ Each client instance has its own dedicated environment session on the server.
27
+
28
+ Example:
29
+ >>> # Connect to a running server
30
+ >>> with CompressionenvEnv(base_url="http://localhost:8000") as client:
31
+ ... result = client.reset()
32
+ ... print(result.observation.echoed_message)
33
+ ...
34
+ ... result = client.step(CompressionenvAction(message="Hello!"))
35
+ ... print(result.observation.echoed_message)
36
+
37
+ Example with Docker:
38
+ >>> # Automatically start container and connect
39
+ >>> client = CompressionenvEnv.from_docker_image("compressionenv-env:latest")
40
+ >>> try:
41
+ ... result = client.reset()
42
+ ... result = client.step(CompressionenvAction(message="Test"))
43
+ ... finally:
44
+ ... client.close()
45
+ """
46
+
47
+ def _step_payload(self, action: CompressionenvAction) -> Dict:
48
+ """
49
+ Convert CompressionenvAction to JSON payload for step message.
50
+
51
+ Args:
52
+ action: CompressionenvAction instance
53
+
54
+ Returns:
55
+ Dictionary representation suitable for JSON encoding
56
+ """
57
+ return {
58
+ "message": action.message,
59
+ }
60
+
61
+ def _parse_result(self, payload: Dict) -> StepResult[CompressionenvObservation]:
62
+ """
63
+ Parse server response into StepResult[CompressionenvObservation].
64
+
65
+ Args:
66
+ payload: JSON response data from server
67
+
68
+ Returns:
69
+ StepResult with CompressionenvObservation
70
+ """
71
+ obs_data = payload.get("observation", {})
72
+ observation = CompressionenvObservation(
73
+ echoed_message=obs_data.get("echoed_message", ""),
74
+ message_length=obs_data.get("message_length", 0),
75
+ done=payload.get("done", False),
76
+ reward=payload.get("reward"),
77
+ metadata=obs_data.get("metadata", {}),
78
+ )
79
+
80
+ return StepResult(
81
+ observation=observation,
82
+ reward=payload.get("reward"),
83
+ done=payload.get("done", False),
84
+ )
85
+
86
+ def _parse_state(self, payload: Dict) -> State:
87
+ """
88
+ Parse server response into State object.
89
+
90
+ Args:
91
+ payload: JSON response from state request
92
+
93
+ Returns:
94
+ State object with episode_id and step_count
95
+ """
96
+ return State(
97
+ episode_id=payload.get("episode_id"),
98
+ step_count=payload.get("step_count", 0),
99
+ )
models.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Data models for the Compressionenv Environment.
9
+
10
+ The compressionenv environment gives the agent a Paul Graham essay and asks it to
11
+ propose compression + decompression algorithms (as Python code).
12
+ """
13
+
14
+ from typing import Any, Dict, Optional
15
+
16
+ from pydantic import Field
17
+
18
+ from openenv.core.env_server.types import Action, Observation
19
+
20
+
21
+ class CompressionenvAction(Action):
22
+ """
23
+ Agent-provided compression/decompression algorithms.
24
+
25
+ The environment expects `compression_code` and `decompression_code` to define:
26
+
27
+ - compress(text: str) -> bytes
28
+ - decompress(data: bytes) -> str
29
+ """
30
+
31
+ compression_code: str = Field(
32
+ ...,
33
+ description="Python code defining compress(text: str) -> bytes",
34
+ min_length=1,
35
+ )
36
+ decompression_code: str = Field(
37
+ ...,
38
+ description="Python code defining decompress(data: bytes) -> str",
39
+ min_length=1,
40
+ )
41
+ algo_name: str = Field(
42
+ default="agent_algo",
43
+ description="Optional name/label for this algorithm variant",
44
+ )
45
+
46
+
47
+ class CompressionenvObservation(Observation):
48
+ """Observation from the Compressionenv environment."""
49
+
50
+ essay_id: str = Field(..., description="Selected essay slug/id for this episode")
51
+ essay_text: str = Field(
52
+ ...,
53
+ description="Full essay text for the agent to compress",
54
+ )
55
+
56
+ valid: bool = Field(
57
+ default=False,
58
+ description="Whether the submitted algorithms successfully round-tripped",
59
+ )
60
+ error: Optional[str] = Field(
61
+ default=None,
62
+ description="Error message if the algorithms failed validation/execution",
63
+ )
64
+
65
+ compressed_size_bytes: Optional[int] = Field(
66
+ default=None,
67
+ description="Size of compressed bytes produced by the agent algorithm",
68
+ ge=0,
69
+ )
70
+ avg_prev_compressed_size_bytes: Optional[float] = Field(
71
+ default=None,
72
+ description="Average compressed size over previous successful steps for this essay",
73
+ ge=0,
74
+ )
75
+ improved_over_avg: Optional[bool] = Field(
76
+ default=None,
77
+ description="True if current compressed size < avg of previous sizes",
78
+ )
79
+
80
+ baselines_size_bytes: Dict[str, int] = Field(
81
+ default_factory=dict,
82
+ description="Baseline compressor sizes for this essay (zlib/bz2/lzma)",
83
+ )
84
+ best_baseline_size_bytes: Optional[int] = Field(
85
+ default=None,
86
+ description="Best (smallest) baseline size in bytes",
87
+ ge=0,
88
+ )
89
+ beat_any_baseline: Optional[bool] = Field(
90
+ default=None,
91
+ description="True if current compressed size is smaller than at least one baseline",
92
+ )
93
+ beat_best_baseline: Optional[bool] = Field(
94
+ default=None,
95
+ description="True if current compressed size is smaller than the best baseline",
96
+ )
97
+
98
+ reward: float = Field(default=0.0, description="Reward for this step")
99
+ done: bool = Field(default=False, description="Whether episode is done")
100
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="Extra info")
openenv.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: compressionenv
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
pyproject.toml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-compressionenv"
13
+ version = "0.1.0"
14
+ description = "Compressionenv environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv runtime (provides FastAPI server + HTTP client types)
18
+ # install from github
19
+ # "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
20
+ "openenv-core[core]>=0.2.0",
21
+ # Environment-specific dependencies
22
+ # Add all dependencies needed for your environment here
23
+ # Examples:
24
+ # "numpy>=1.19.0",
25
+ # "torch>=2.0.0",
26
+ # "gymnasium>=0.29.0",
27
+ # "openspiel>=1.0.0",
28
+ # "smolagents>=1.22.0,<2",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0.0",
34
+ "pytest-cov>=4.0.0",
35
+ ]
36
+
37
+ [project.scripts]
38
+ # Server entry point - enables running via: uv run --project . server
39
+ # or: python -m compressionenv.server.app
40
+ server = "compressionenv.server.app:main"
41
+
42
+ [tool.setuptools]
43
+ include-package-data = true
44
+ packages = ["compressionenv", "compressionenv.server"]
45
+ package-dir = { "compressionenv" = ".", "compressionenv.server" = "server" }
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Compressionenv environment server components."""
8
+
9
+ from .compressionenv_environment import CompressionenvEnvironment
10
+
11
+ __all__ = ["CompressionenvEnvironment"]
server/app.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the Compressionenv Environment.
9
+
10
+ This module creates an HTTP server that exposes the CompressionenvEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Endpoints:
14
+ - POST /reset: Reset the environment
15
+ - POST /step: Execute an action
16
+ - GET /state: Get current environment state
17
+ - GET /schema: Get action/observation schemas
18
+ - WS /ws: WebSocket endpoint for persistent sessions
19
+
20
+ Usage:
21
+ # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
+
24
+ # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
+
27
+ # Or run directly:
28
+ python -m server.app
29
+ """
30
+
31
+ try:
32
+ from openenv.core.env_server.http_server import create_app
33
+ except Exception as e: # pragma: no cover
34
+ raise ImportError(
35
+ "openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
36
+ ) from e
37
+
38
+ # Import from local models.py (PYTHONPATH includes /app/env in Docker)
39
+ from models import CompressionenvAction, CompressionenvObservation
40
+ from .compressionenv_environment import CompressionenvEnvironment
41
+
42
+
43
+ # Create the app with web interface and README integration
44
+ app = create_app(
45
+ CompressionenvEnvironment,
46
+ CompressionenvAction,
47
+ CompressionenvObservation,
48
+ env_name="compressionenv",
49
+ max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
50
+ )
51
+
52
+
53
+ def main(host: str = "0.0.0.0", port: int = 8000):
54
+ """
55
+ Entry point for direct execution via uv run or python -m.
56
+
57
+ This function enables running the server without Docker:
58
+ uv run --project . server
59
+ uv run --project . server --port 8001
60
+ python -m compressionenv.server.app
61
+
62
+ Args:
63
+ host: Host address to bind to (default: "0.0.0.0")
64
+ port: Port number to listen on (default: 8000)
65
+
66
+ For production deployments, consider using uvicorn directly with
67
+ multiple workers:
68
+ uvicorn compressionenv.server.app:app --workers 4
69
+ """
70
+ import uvicorn
71
+
72
+ uvicorn.run(app, host=host, port=port)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ import argparse
77
+
78
+ parser = argparse.ArgumentParser()
79
+ parser.add_argument("--port", type=int, default=8000)
80
+ args = parser.parse_args()
81
+ main(port=args.port)
server/compressionenv_environment.py ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Compressionenv Environment Implementation.
9
+
10
+ Environment where the agent proposes compression/decompression algorithms for a
11
+ Paul Graham essay. The environment validates round-trip correctness and scores
12
+ compressed size relative to the agent's prior attempts and baseline compressors.
13
+ """
14
+
15
+ import base64
16
+ import json
17
+ import os
18
+ import random
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
24
+ from uuid import uuid4
25
+
26
+ import bz2
27
+ import lzma
28
+ import zlib
29
+
30
+ from openenv.core.env_server.interfaces import Environment
31
+ from openenv.core.env_server.types import State
32
+
33
+ from models import CompressionenvAction, CompressionenvObservation
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class _Essay:
38
+ essay_id: str
39
+ text: str
40
+
41
+
42
+ class CompressionenvEnvironment(Environment):
43
+ """
44
+ Compression algorithm search environment.
45
+
46
+ - On `reset()`, selects a PG essay (from `../essays/*.txt`) and returns it.
47
+ - On `step()`, executes agent-provided Python code defining:
48
+ compress(text: str) -> bytes
49
+ decompress(data: bytes) -> str
50
+ Validates that decompress(compress(essay)) == essay.
51
+
52
+ Rewards (per spec):
53
+ - If algorithms fail or don't round-trip: -1 reward.
54
+ - If compressed size is lower than average of previous successful sizes for
55
+ this essay in the episode: +1 reward.
56
+ - Compare against baselines (zlib, bz2, lzma):
57
+ - If agent achieves smaller size than at least one baseline: +10 reward.
58
+ - If agent achieves smaller size than the best baseline: +20 reward.
59
+ """
60
+
61
+ # Enable concurrent WebSocket sessions.
62
+ # Set to True if your environment isolates state between instances.
63
+ # When True, multiple WebSocket clients can connect simultaneously, each
64
+ # getting their own environment instance (when using factory mode in app.py).
65
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
66
+
67
+ def __init__(self):
68
+ """Initialize the compressionenv environment."""
69
+ self._state = State(episode_id=str(uuid4()), step_count=0)
70
+ self._essay: _Essay | None = None
71
+ self._successful_sizes: list[int] = []
72
+ self._baselines: dict[str, int] = {}
73
+
74
+ def reset(self) -> CompressionenvObservation:
75
+ """
76
+ Reset the environment.
77
+
78
+ Returns:
79
+ CompressionenvObservation containing a selected essay
80
+ """
81
+ self._state = State(episode_id=str(uuid4()), step_count=0)
82
+ self._essay = self._pick_essay()
83
+ self._successful_sizes = []
84
+ self._baselines = self._compute_baselines(self._essay.text)
85
+
86
+ return CompressionenvObservation(
87
+ essay_id=self._essay.essay_id,
88
+ essay_text=self._essay.text,
89
+ valid=True,
90
+ error=None,
91
+ compressed_size_bytes=None,
92
+ avg_prev_compressed_size_bytes=None,
93
+ improved_over_avg=None,
94
+ baselines_size_bytes=self._baselines,
95
+ best_baseline_size_bytes=min(self._baselines.values()) if self._baselines else None,
96
+ beat_any_baseline=None,
97
+ beat_best_baseline=None,
98
+ done=False,
99
+ reward=0.0,
100
+ metadata={
101
+ "episode_id": self._state.episode_id,
102
+ "step_count": self._state.step_count,
103
+ "num_baselines": len(self._baselines),
104
+ },
105
+ )
106
+
107
+ def step(self, action: CompressionenvAction) -> CompressionenvObservation: # type: ignore[override]
108
+ """
109
+ Execute a step: run agent algorithms, validate, score compression size.
110
+ """
111
+ if self._essay is None:
112
+ # Defensive: ensure reset called.
113
+ self._essay = self._pick_essay()
114
+ self._baselines = self._compute_baselines(self._essay.text)
115
+ self._successful_sizes = []
116
+
117
+ self._state.step_count += 1
118
+
119
+ essay_text = self._essay.text
120
+ baselines = self._baselines
121
+ best_baseline = min(baselines.values()) if baselines else None
122
+
123
+ reward = 0.0
124
+ error: str | None = None
125
+ valid = False
126
+ compressed_size: int | None = None
127
+ improved_over_avg: bool | None = None
128
+ beat_any_baseline: bool | None = None
129
+ beat_best_baseline: bool | None = None
130
+ avg_prev: float | None = None
131
+
132
+ try:
133
+ compressed_bytes = self._run_agent_codec(
134
+ essay_text=essay_text,
135
+ compression_code=action.compression_code,
136
+ decompression_code=action.decompression_code,
137
+ )
138
+ compressed_size = len(compressed_bytes)
139
+ valid = True
140
+ except Exception as e:
141
+ error = str(e)
142
+ reward = -1.0
143
+
144
+ if valid and compressed_size is not None:
145
+ if self._successful_sizes:
146
+ avg_prev = sum(self._successful_sizes) / len(self._successful_sizes)
147
+ improved_over_avg = compressed_size < avg_prev
148
+ if improved_over_avg:
149
+ reward += 1.0
150
+ else:
151
+ avg_prev = None
152
+ improved_over_avg = None
153
+
154
+ self._successful_sizes.append(compressed_size)
155
+
156
+ if baselines:
157
+ beat_any_baseline = any(compressed_size < s for s in baselines.values())
158
+ beat_best_baseline = best_baseline is not None and compressed_size < best_baseline
159
+ if beat_best_baseline:
160
+ reward += 20.0
161
+ elif beat_any_baseline:
162
+ reward += 10.0
163
+
164
+ return CompressionenvObservation(
165
+ essay_id=self._essay.essay_id,
166
+ essay_text=essay_text,
167
+ valid=valid,
168
+ error=error,
169
+ compressed_size_bytes=compressed_size,
170
+ avg_prev_compressed_size_bytes=avg_prev,
171
+ improved_over_avg=improved_over_avg,
172
+ baselines_size_bytes=baselines,
173
+ best_baseline_size_bytes=best_baseline,
174
+ beat_any_baseline=beat_any_baseline,
175
+ beat_best_baseline=beat_best_baseline,
176
+ done=False,
177
+ reward=reward,
178
+ metadata={
179
+ "episode_id": self._state.episode_id,
180
+ "step_count": self._state.step_count,
181
+ "algo_name": action.algo_name,
182
+ "num_successful_attempts": len(self._successful_sizes),
183
+ },
184
+ )
185
+
186
+ @property
187
+ def state(self) -> State:
188
+ """
189
+ Get the current environment **state**.
190
+
191
+ In RL terms, the State is a (Markov) description of the underlying
192
+ environment that is at least as informative as any single Observation.
193
+ Here we include all information needed to reconstruct what any call to
194
+ `reset()` or `step()` would expose in an observation for this episode.
195
+
196
+ Returns:
197
+ Current State with core fields plus extra environment details.
198
+ """
199
+ # State allows extra fields, so we enrich it to be a superset of any
200
+ # single observation: from this State, an agent could derive the latest
201
+ # observation for the current episode.
202
+ if self._essay is not None:
203
+ self._state.essay_id = self._essay.essay_id # type: ignore[attr-defined]
204
+ self._state.essay_text = self._essay.text # type: ignore[attr-defined]
205
+ self._state.baselines_size_bytes = self._baselines # type: ignore[attr-defined]
206
+ self._state.num_successful_attempts = len(self._successful_sizes) # type: ignore[attr-defined]
207
+ if self._successful_sizes:
208
+ self._state.best_compressed_size_bytes = min(self._successful_sizes) # type: ignore[attr-defined]
209
+ self._state.last_compressed_size_bytes = self._successful_sizes[-1] # type: ignore[attr-defined]
210
+ if self._baselines:
211
+ self._state.best_baseline_size_bytes = min(self._baselines.values()) # type: ignore[attr-defined]
212
+ return self._state
213
+
214
+ def _pick_essay(self) -> _Essay:
215
+ # Expected layout:
216
+ # compression-openenv/
217
+ # essays/
218
+ # compressionenv/
219
+ # server/
220
+ # compressionenv_environment.py (this file)
221
+ essays_dir = Path(__file__).resolve().parents[2] / "essays"
222
+ if not essays_dir.exists():
223
+ # Try repo-level essays directory (if running from different cwd/layout).
224
+ essays_dir = Path(os.getcwd()).resolve() / "essays"
225
+ paths = sorted(essays_dir.glob("*.txt"))
226
+ if not paths:
227
+ raise FileNotFoundError(
228
+ f"No essays found in {essays_dir}. Expected PG essay .txt files."
229
+ )
230
+ path = random.choice(paths)
231
+ essay_id = path.stem
232
+ text = path.read_text(encoding="utf-8")
233
+ return _Essay(essay_id=essay_id, text=text)
234
+
235
+ def _compute_baselines(self, text: str) -> dict[str, int]:
236
+ data = text.encode("utf-8")
237
+ # Deterministic settings.
238
+ baselines: dict[str, bytes] = {
239
+ "zlib": zlib.compress(data, level=9),
240
+ "bz2": bz2.compress(data, compresslevel=9),
241
+ "lzma": lzma.compress(data, preset=9),
242
+ }
243
+ return {k: len(v) for k, v in baselines.items()}
244
+
245
+ def _run_agent_codec(
246
+ self,
247
+ essay_text: str,
248
+ compression_code: str,
249
+ decompression_code: str,
250
+ ) -> bytes:
251
+ """
252
+ Execute agent code in a subprocess and return compressed bytes.
253
+
254
+ Security note: this is not a hardened sandbox. It's a best-effort isolation
255
+ to avoid contaminating the server process, with a timeout.
256
+ """
257
+ runner = r"""
258
+ import base64
259
+ import json
260
+ import sys
261
+
262
+ payload = json.loads(sys.stdin.read())
263
+ essay_text = payload["essay_text"]
264
+ compression_code = payload["compression_code"]
265
+ decompression_code = payload["decompression_code"]
266
+
267
+ ns = {}
268
+ exec(compression_code, ns, ns)
269
+ exec(decompression_code, ns, ns)
270
+
271
+ compress = ns.get("compress")
272
+ decompress = ns.get("decompress")
273
+ if compress is None or decompress is None:
274
+ raise RuntimeError("Expected functions compress(text: str)->bytes and decompress(data: bytes)->str")
275
+
276
+ compressed = compress(essay_text)
277
+ if not isinstance(compressed, (bytes, bytearray)):
278
+ raise RuntimeError(f"compress() must return bytes, got {type(compressed)}")
279
+ compressed = bytes(compressed)
280
+
281
+ round_trip = decompress(compressed)
282
+ if not isinstance(round_trip, str):
283
+ raise RuntimeError(f"decompress() must return str, got {type(round_trip)}")
284
+ if round_trip != essay_text:
285
+ raise RuntimeError("Round-trip failed: decompress(compress(essay)) != essay")
286
+
287
+ sys.stdout.write(base64.b64encode(compressed).decode("ascii"))
288
+ """
289
+ payload = {
290
+ "essay_text": essay_text,
291
+ "compression_code": compression_code,
292
+ "decompression_code": decompression_code,
293
+ }
294
+ with tempfile.TemporaryDirectory() as td:
295
+ proc = subprocess.run(
296
+ [sys.executable, "-c", runner],
297
+ input=json.dumps(payload).encode("utf-8"),
298
+ stdout=subprocess.PIPE,
299
+ stderr=subprocess.PIPE,
300
+ cwd=td,
301
+ timeout=3.0,
302
+ env={
303
+ "PYTHONIOENCODING": "utf-8",
304
+ "PYTHONUTF8": "1",
305
+ "PYTHONDONTWRITEBYTECODE": "1",
306
+ },
307
+ )
308
+ if proc.returncode != 0:
309
+ stderr = proc.stderr.decode("utf-8", errors="replace").strip()
310
+ raise RuntimeError(stderr or f"Agent codec subprocess failed with code {proc.returncode}")
311
+ out = proc.stdout.decode("utf-8", errors="replace").strip()
312
+ try:
313
+ return base64.b64decode(out.encode("ascii"), validate=True)
314
+ except Exception as e:
315
+ raise RuntimeError(f"Failed to decode compressed output: {e}") from e
server/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ openenv[core]>=0.2.0
2
+ fastapi>=0.115.0
3
+ uvicorn>=0.24.0
4
+
5
+
6
+
spec.md ADDED
@@ -0,0 +1 @@
 
 
1
+ create an environment where agent is given a pg essay text and it comes up with a compression and decompression algorithms for it. the environment runs the algorithm on essay and gives +1 reward to agent if curent step's compressed text size is lower than avg of all compression sizes achieved so far in previous steps for that essay. it also runs compressions and decompression on the essay to verify that compression and decompression algorithms work correctly, if they don't then it's -1 reward. it also runs state of the art eg zip, bzip etc top text compressions on the essay and checks the size. if agent achieves smaller size than any of them it's +10 and if it achieves smallest size than it's +20 reward.
uv.lock ADDED
The diff for this file is too large to render. See raw diff