Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- Dockerfile +26 -0
- README.md +127 -5
- __init__.py +12 -0
- client.py +55 -0
- models.py +39 -0
- openenv.yaml +5 -0
- pyproject.toml +35 -0
- server/Dockerfile.backup +25 -0
- server/README.md +51 -0
- server/__init__.py +11 -0
- server/app.py +50 -0
- server/python_codeact_env.py +115 -0
- server/python_executor.py +149 -0
- server/transforms.py +94 -0
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Base image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app/env
|
| 6 |
+
|
| 7 |
+
# Install system dependencies
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
git \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy environment files
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# Install Python dependencies
|
| 16 |
+
RUN pip install --no-cache-dir -e .
|
| 17 |
+
|
| 18 |
+
# Expose port
|
| 19 |
+
EXPOSE 8000
|
| 20 |
+
|
| 21 |
+
# Set environment variables
|
| 22 |
+
ENV PYTHONUNBUFFERED=1
|
| 23 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 24 |
+
|
| 25 |
+
# Run the server
|
| 26 |
+
CMD ["python", "-m", "uvicorn", "coding_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,11 +1,133 @@
|
|
| 1 |
---
|
| 2 |
-
title: Coding
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Coding Environment Server
|
| 3 |
+
emoji: 💻
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Coding Environment
|
| 15 |
+
|
| 16 |
+
A Python code execution environment that runs arbitrary Python code and returns results. Perfect for testing code execution infrastructure and demonstrating environment usage patterns.
|
| 17 |
+
|
| 18 |
+
## Quick Start
|
| 19 |
+
|
| 20 |
+
The simplest way to use the Coding environment is through the `CodingEnv` class:
|
| 21 |
+
|
| 22 |
+
```python
|
| 23 |
+
from envs.coding_env import CodeAction, CodingEnv
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
# Create environment from Docker image
|
| 27 |
+
coding_env = CodingEnv.from_docker_image("coding-env:latest")
|
| 28 |
+
|
| 29 |
+
# Reset
|
| 30 |
+
result = coding_env.reset()
|
| 31 |
+
print(f"Reset complete: exit_code={result.observation.exit_code}")
|
| 32 |
+
|
| 33 |
+
# Execute Python code
|
| 34 |
+
code_samples = [
|
| 35 |
+
"print('Hello, World!')",
|
| 36 |
+
"x = 5 + 3\nprint(f'Result: {x}')",
|
| 37 |
+
"import math\nprint(math.pi)"
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
for code in code_samples:
|
| 41 |
+
result = coding_env.step(CodeAction(code=code))
|
| 42 |
+
print(f"Code: {code}")
|
| 43 |
+
print(f" → stdout: {result.observation.stdout.strip()}")
|
| 44 |
+
print(f" → exit_code: {result.observation.exit_code}")
|
| 45 |
+
|
| 46 |
+
finally:
|
| 47 |
+
# Always clean up
|
| 48 |
+
coding_env.close()
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
That's it! The `CodingEnv.from_docker_image()` method handles:
|
| 52 |
+
- Starting the Docker container
|
| 53 |
+
- Waiting for the server to be ready
|
| 54 |
+
- Connecting to the environment
|
| 55 |
+
- Container cleanup when you call `close()`
|
| 56 |
+
|
| 57 |
+
## Building the Docker Image
|
| 58 |
+
|
| 59 |
+
Before using the environment, you need to build the Docker image:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
# From project root
|
| 63 |
+
docker build -t coding-env:latest -f src/envs/coding_env/server/Dockerfile .
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Environment Details
|
| 67 |
+
|
| 68 |
+
### Action
|
| 69 |
+
**CodeAction**: Contains a single field
|
| 70 |
+
- `code` (str) - The Python code to execute
|
| 71 |
+
|
| 72 |
+
### Observation
|
| 73 |
+
**CodeObservation**: Contains the execution results
|
| 74 |
+
- `stdout` (str) - Standard output from code execution
|
| 75 |
+
- `stderr` (str) - Standard error from code execution
|
| 76 |
+
- `exit_code` (int) - Exit code (0 for success, non-zero for errors)
|
| 77 |
+
|
| 78 |
+
### State
|
| 79 |
+
**CodeState**: Tracks execution state
|
| 80 |
+
- `episode_id` (str) - Unique identifier for the episode
|
| 81 |
+
- `step_count` (int) - Number of steps taken
|
| 82 |
+
- `last_exit_code` (int) - Exit code from the last execution
|
| 83 |
+
|
| 84 |
+
## Advanced Usage
|
| 85 |
+
|
| 86 |
+
### Connecting to an Existing Server
|
| 87 |
+
|
| 88 |
+
If you already have a Coding environment server running, you can connect directly:
|
| 89 |
+
|
| 90 |
+
```python
|
| 91 |
+
from envs.coding_env import CodingEnv
|
| 92 |
+
|
| 93 |
+
# Connect to existing server
|
| 94 |
+
coding_env = CodingEnv(base_url="<ENV_HTTP_URL_HERE>")
|
| 95 |
+
|
| 96 |
+
# Use as normal
|
| 97 |
+
result = coding_env.reset()
|
| 98 |
+
result = coding_env.step(CodeAction(code="print('Hello!')"))
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
Note: When connecting to an existing server, `coding_env.close()` will NOT stop the server.
|
| 102 |
+
|
| 103 |
+
## Development & Testing
|
| 104 |
+
|
| 105 |
+
### Running the Full Example
|
| 106 |
+
|
| 107 |
+
Run the complete example that demonstrates the full workflow:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
python3 src/envs/coding_env/client/example_usage.py
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
This example shows:
|
| 114 |
+
- Creating an environment from a Docker image
|
| 115 |
+
- Resetting and executing code through the environment
|
| 116 |
+
- Automatic cleanup with `close()`
|
| 117 |
+
|
| 118 |
+
## Project Structure
|
| 119 |
+
|
| 120 |
+
```
|
| 121 |
+
coding_env/
|
| 122 |
+
├── README.md # This file
|
| 123 |
+
├── models.py # Action, Observation, and State models
|
| 124 |
+
├── client/
|
| 125 |
+
│ ├── coding_env_client.py # CodingEnv client implementation
|
| 126 |
+
│ └── example_usage.py # Usage examples
|
| 127 |
+
└── server/
|
| 128 |
+
├── python_codeact_env.py # Core environment logic
|
| 129 |
+
├── app.py # FastAPI application
|
| 130 |
+
├── transforms.py # Observation transforms
|
| 131 |
+
├── Dockerfile # Container image definition
|
| 132 |
+
└── README.md # Server-specific documentation
|
| 133 |
+
```
|
__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Coding Environment - A Python code execution environment."""
|
| 8 |
+
|
| 9 |
+
from .client import CodingEnv
|
| 10 |
+
from .models import CodeAction, CodeObservation, CodeState
|
| 11 |
+
|
| 12 |
+
__all__ = ["CodingEnv", "CodeAction", "CodeObservation", "CodeState"]
|
client.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodingEnv
|
| 3 |
+
---------
|
| 4 |
+
Client-side wrapper for the Coding environment server.
|
| 5 |
+
Talks HTTP to a single base_url exposing: /reset and /step.
|
| 6 |
+
|
| 7 |
+
- users instantiate CodingEnv with a base_url provided by the higher-level
|
| 8 |
+
vector/orchestration layer.
|
| 9 |
+
- Environment authors ship the Docker image that serves the HTTP API.
|
| 10 |
+
|
| 11 |
+
(Seeds, episode IDs, request IDs, capabilities can be added later in the payloads.)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
from openenv_core.client_types import StepResult
|
| 17 |
+
|
| 18 |
+
from openenv_core.http_env_client import HTTPEnvClient
|
| 19 |
+
|
| 20 |
+
from coding_env.models import CodeAction, CodeObservation, CodeState
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class CodingEnv(HTTPEnvClient[CodeAction, CodeObservation]):
|
| 24 |
+
# --- HTTPEnvClient abstract hooks ---
|
| 25 |
+
|
| 26 |
+
def _step_payload(self, action: CodeAction) -> dict:
|
| 27 |
+
# Shape expected by the server's /step endpoint under "action"
|
| 28 |
+
return {
|
| 29 |
+
"code": action.code,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def _parse_result(self, payload: dict) -> StepResult[CodeObservation]:
|
| 33 |
+
# Expecting: { "observation": {...}, "reward": <float|null>, "done": <bool>, "info": {...} }
|
| 34 |
+
obs = CodeObservation(**payload["observation"])
|
| 35 |
+
return StepResult(
|
| 36 |
+
observation=obs,
|
| 37 |
+
reward=payload.get("reward"),
|
| 38 |
+
done=bool(payload.get("done", False)),
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
def _parse_state(self, payload: dict) -> CodeState:
|
| 42 |
+
"""
|
| 43 |
+
Parse server response into CodeState object.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
payload: JSON response from /state endpoint
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
CodeState object with episode_id, step_count, and last_exit_code
|
| 50 |
+
"""
|
| 51 |
+
return CodeState(
|
| 52 |
+
episode_id=payload.get("episode_id"),
|
| 53 |
+
step_count=payload.get("step_count", 0),
|
| 54 |
+
last_exit_code=payload.get("last_exit_code", 0),
|
| 55 |
+
)
|
models.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
envs/coding_env/models.py
|
| 3 |
+
--------------------------------
|
| 4 |
+
Action/Observation types for the Coding environment.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
|
| 11 |
+
from openenv_core.env_server.interfaces import Action, Observation, State
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class CodeAction(Action):
|
| 16 |
+
"""
|
| 17 |
+
Represents a single code execution request.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
code: str
|
| 21 |
+
# Optional: future fields like 'lint': bool, 'timeout_s': float, etc.
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class CodeObservation(Observation):
|
| 26 |
+
"""
|
| 27 |
+
Result of executing code in the environment.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
stdout: str = ""
|
| 31 |
+
stderr: str = ""
|
| 32 |
+
exit_code: int = 0
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class CodeState(State):
|
| 37 |
+
"""State for CodeAct environment with persistent execution context."""
|
| 38 |
+
|
| 39 |
+
last_exit_code: int = 0
|
openenv.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: coding_env
|
| 2 |
+
version: "0.1.0"
|
| 3 |
+
description: "Coding environment for OpenEnv"
|
| 4 |
+
action: CodingAction
|
| 5 |
+
observation: CodingObservation
|
pyproject.toml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-coding_env"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Coding Environment for OpenEnv"
|
| 9 |
+
requires-python = ">=3.10"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"openenv-core>=0.1.0",
|
| 12 |
+
"fastapi>=0.115.0",
|
| 13 |
+
"pydantic>=2.0.0",
|
| 14 |
+
"uvicorn>=0.24.0",
|
| 15 |
+
"requests>=2.31.0",
|
| 16 |
+
"smolagents>=1.22.0,<2",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[project.optional-dependencies]
|
| 20 |
+
dev = [
|
| 21 |
+
"pytest>=8.0.0",
|
| 22 |
+
"pytest-cov>=4.0.0",
|
| 23 |
+
"ipykernel>=6.29.5",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
[project.scripts]
|
| 27 |
+
server = "coding_env.server.app:main"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
[tool.setuptools]
|
| 31 |
+
packages = ["coding_env", "coding_env.server"]
|
| 32 |
+
package-dir = { "coding_env" = ".", "coding_env.server" = "server" }
|
| 33 |
+
|
| 34 |
+
[tool.setuptools.package-data]
|
| 35 |
+
coding_env = ["**/*.yaml", "**/*.yml"]
|
server/Dockerfile.backup
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
# Use the standard openenv base image
|
| 8 |
+
# Built from: docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile .
|
| 9 |
+
# In GitHub Actions, this is overridden to use the GHCR base image
|
| 10 |
+
ARG BASE_IMAGE=openenv-base:latest
|
| 11 |
+
FROM ${BASE_IMAGE}
|
| 12 |
+
|
| 13 |
+
# Copy only what's needed for this environment
|
| 14 |
+
COPY src/core/ /app/src/core/
|
| 15 |
+
COPY src/envs/coding_env/ /app/src/envs/coding_env/
|
| 16 |
+
|
| 17 |
+
# Copy README for web interface documentation
|
| 18 |
+
COPY src/envs/coding_env/README.md /app/README.md
|
| 19 |
+
|
| 20 |
+
# Health check
|
| 21 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 22 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 23 |
+
|
| 24 |
+
# Run the FastAPI server
|
| 25 |
+
CMD ["uvicorn", "envs.coding_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
server/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CodingEnv HTTP Server
|
| 2 |
+
|
| 3 |
+
This directory contains the HTTP server implementation for the CodingEnvironment.
|
| 4 |
+
|
| 5 |
+
## Running Locally
|
| 6 |
+
|
| 7 |
+
### Prerequisites
|
| 8 |
+
```bash
|
| 9 |
+
pip install fastapi uvicorn
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
### Start the server
|
| 13 |
+
```bash
|
| 14 |
+
# From the project root (/Users/pankit/git/envtorch)
|
| 15 |
+
cd src
|
| 16 |
+
uvicorn envs.coding_env.server.app:app --reload --host 0.0.0.0 --port 8000
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
The server will be available at `http://localhost:8000`
|
| 20 |
+
|
| 21 |
+
### API Endpoints
|
| 22 |
+
|
| 23 |
+
- `POST /reset` - Reset the environment
|
| 24 |
+
- `POST /step` - Execute a code action
|
| 25 |
+
- `GET /state` - Get current environment state
|
| 26 |
+
- `GET /health` - Health check
|
| 27 |
+
|
| 28 |
+
### Test with curl
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
# Health check
|
| 32 |
+
curl http://localhost:8000/health
|
| 33 |
+
|
| 34 |
+
# Reset
|
| 35 |
+
curl -X POST http://localhost:8000/reset \
|
| 36 |
+
-H "Content-Type: application/json" \
|
| 37 |
+
-d '{}'
|
| 38 |
+
|
| 39 |
+
# Execute code
|
| 40 |
+
curl -X POST http://localhost:8000/step \
|
| 41 |
+
-H "Content-Type: application/json" \
|
| 42 |
+
-d '{
|
| 43 |
+
"action": {
|
| 44 |
+
"code": "print(\"Hello from HTTP!\")"
|
| 45 |
+
},
|
| 46 |
+
"timeout_s": 15
|
| 47 |
+
}'
|
| 48 |
+
|
| 49 |
+
# Get state
|
| 50 |
+
curl http://localhost:8000/state
|
| 51 |
+
```
|
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 |
+
"""Coding environment server components."""
|
| 8 |
+
|
| 9 |
+
from .python_codeact_env import PythonCodeActEnv
|
| 10 |
+
|
| 11 |
+
__all__ = ["PythonCodeActEnv"]
|
server/app.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 Coding Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the PythonCodeActEnv
|
| 11 |
+
over HTTP endpoints, making it compatible with HTTPEnvClient.
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
# Development (with auto-reload):
|
| 15 |
+
uvicorn envs.coding_env.server.app:app --reload --host 0.0.0.0 --port 8000
|
| 16 |
+
|
| 17 |
+
# Production:
|
| 18 |
+
uvicorn envs.coding_env.server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 19 |
+
|
| 20 |
+
# Or run directly:
|
| 21 |
+
python -m envs.coding_env.server.app
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from openenv_core.env_server import create_app
|
| 25 |
+
|
| 26 |
+
from coding_env.models import CodeAction, CodeObservation
|
| 27 |
+
from coding_env.server.python_codeact_env import PythonCodeActEnv
|
| 28 |
+
|
| 29 |
+
# Create the environment instance
|
| 30 |
+
env = PythonCodeActEnv()
|
| 31 |
+
|
| 32 |
+
# Create the app with web interface and README integration
|
| 33 |
+
app = create_app(env, CodeAction, CodeObservation, env_name="coding_env")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
import uvicorn
|
| 38 |
+
|
| 39 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
"""Main entry point for running the server."""
|
| 44 |
+
import uvicorn
|
| 45 |
+
|
| 46 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
server/python_codeact_env.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
Python Code Action Environment.
|
| 9 |
+
|
| 10 |
+
This module provides a server-side environment implementation for executing
|
| 11 |
+
Python code actions using PyExecutor.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import uuid
|
| 15 |
+
|
| 16 |
+
from openenv_core.env_server.interfaces import Action, Environment, Observation
|
| 17 |
+
from coding_env.server.python_executor import PyExecutor
|
| 18 |
+
|
| 19 |
+
from coding_env.models import CodeAction, CodeObservation, CodeState
|
| 20 |
+
from .transforms import create_safe_coding_transform
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class PythonCodeActEnv(Environment):
|
| 24 |
+
"""
|
| 25 |
+
Python Code Action Environment for executing code and tracking state.
|
| 26 |
+
|
| 27 |
+
This environment executes Python code submitted as CodeAction during step,
|
| 28 |
+
maintains the last exit code in its state, and returns results wrapped
|
| 29 |
+
in CodeObservation.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
transform: Optional transform to apply to observations
|
| 33 |
+
additional_imports: List of additional module imports to authorize
|
| 34 |
+
(e.g., ["numpy", "pandas", "matplotlib"])
|
| 35 |
+
|
| 36 |
+
Example:
|
| 37 |
+
>>> env = PythonCodeActEnv()
|
| 38 |
+
>>> obs = env.reset()
|
| 39 |
+
>>> action = CodeAction(code="print('Hello, World!')")
|
| 40 |
+
>>> obs = env.step(action)
|
| 41 |
+
>>> print(obs.stdout) # "Hello, World!\n"
|
| 42 |
+
>>> print(obs.exit_code) # 0
|
| 43 |
+
>>> print(env.state.last_exit_code) # 0
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
):
|
| 49 |
+
self.transform = create_safe_coding_transform()
|
| 50 |
+
self._executor = PyExecutor()
|
| 51 |
+
self._state = CodeState()
|
| 52 |
+
|
| 53 |
+
def reset(self) -> Observation:
|
| 54 |
+
"""
|
| 55 |
+
Reset environment and start fresh execution session.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
Initial observation with empty stdout/stderr and exit_code=0
|
| 59 |
+
"""
|
| 60 |
+
# Initialize fresh state
|
| 61 |
+
self._state = CodeState(episode_id=str(uuid.uuid4()), step_count=0)
|
| 62 |
+
# Add last_exit_code to state
|
| 63 |
+
self._state.last_exit_code = 0
|
| 64 |
+
|
| 65 |
+
# Reset executor to clear any previously defined variables/functions
|
| 66 |
+
self._executor = PyExecutor()
|
| 67 |
+
|
| 68 |
+
# Reset transform to clear any accumulated state
|
| 69 |
+
self.transform = create_safe_coding_transform()
|
| 70 |
+
|
| 71 |
+
# Return initial observation
|
| 72 |
+
observation = CodeObservation(
|
| 73 |
+
stdout="",
|
| 74 |
+
stderr="",
|
| 75 |
+
exit_code=0,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
return self._apply_transform(observation)
|
| 79 |
+
|
| 80 |
+
def step(self, action: Action) -> Observation:
|
| 81 |
+
"""
|
| 82 |
+
Execute code action and return observation.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
action: CodeAction containing the code to execute
|
| 86 |
+
|
| 87 |
+
Returns:
|
| 88 |
+
CodeObservation with execution results (stdout, stderr, exit_code)
|
| 89 |
+
|
| 90 |
+
Raises:
|
| 91 |
+
ValueError: If action is not a CodeAction instance
|
| 92 |
+
"""
|
| 93 |
+
if not isinstance(action, CodeAction):
|
| 94 |
+
raise ValueError(f"Expected CodeAction, got {type(action)}")
|
| 95 |
+
|
| 96 |
+
# Execute the code using PyExecutor
|
| 97 |
+
result = self._executor.run(action.code)
|
| 98 |
+
|
| 99 |
+
# Update state
|
| 100 |
+
self._state.step_count += 1
|
| 101 |
+
self._state.last_exit_code = result.exit_code
|
| 102 |
+
|
| 103 |
+
# Create observation from execution result
|
| 104 |
+
observation = CodeObservation(
|
| 105 |
+
stdout=result.stdout,
|
| 106 |
+
stderr=result.stderr,
|
| 107 |
+
exit_code=result.exit_code,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
return self._apply_transform(observation)
|
| 111 |
+
|
| 112 |
+
@property
|
| 113 |
+
def state(self) -> CodeState:
|
| 114 |
+
"""Get current environment state including last exit code."""
|
| 115 |
+
return self._state
|
server/python_executor.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Local Python Executor (enhanced).
|
| 8 |
+
|
| 9 |
+
This module provides a safer wrapper around smolagents.LocalPythonExecutor
|
| 10 |
+
with improved exception handling and a few helpful tools registered with
|
| 11 |
+
the executor to make debugging executed code easier.
|
| 12 |
+
|
| 13 |
+
Key improvements:
|
| 14 |
+
- Register a few helper utilities via send_tools so user code can use
|
| 15 |
+
them for reporting (e.g. `format_exc`).
|
| 16 |
+
- More robust extraction of stdout/stderr/exit codes from the executor
|
| 17 |
+
result object, tolerant to different versions of smolagents.
|
| 18 |
+
- Detailed stderr on unexpected exceptions including full traceback.
|
| 19 |
+
- Structured logging for operational visibility.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import json
|
| 25 |
+
import logging
|
| 26 |
+
import traceback
|
| 27 |
+
|
| 28 |
+
from smolagents import LocalPythonExecutor
|
| 29 |
+
|
| 30 |
+
from openenv_core.env_server.types import CodeExecResult
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
logger.addHandler(logging.NullHandler())
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class PyExecutor:
|
| 37 |
+
"""Wrapper around smolagents LocalPythonExecutor.
|
| 38 |
+
|
| 39 |
+
The wrapper registers a few non-privileged helper tools to the
|
| 40 |
+
LocalPythonExecutor that can be used by the executed code to
|
| 41 |
+
format exceptions and to safely stringify results for improved
|
| 42 |
+
error reporting.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, additional_imports: list[str] | None = None):
|
| 46 |
+
if additional_imports is None:
|
| 47 |
+
additional_imports = []
|
| 48 |
+
|
| 49 |
+
self._executor = LocalPythonExecutor(additional_authorized_imports=additional_imports)
|
| 50 |
+
|
| 51 |
+
# Register helpful utilities exposed to the execution environment.
|
| 52 |
+
# These are intentionally small, read-only helpers.
|
| 53 |
+
tools = {
|
| 54 |
+
# Provide a small helper to format the current exception in the
|
| 55 |
+
# executed context. This is a *string formatting* helper only.
|
| 56 |
+
"format_exc": traceback.format_exc,
|
| 57 |
+
# Safe JSON dumps with a fallback for non-serializable objects.
|
| 58 |
+
"safe_json_dumps": lambda obj: json.dumps(obj, default=lambda o: repr(o)),
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# `send_tools` is the public API on LocalPythonExecutor to make
|
| 62 |
+
# helper callables available to the sandboxed runtime. We don't
|
| 63 |
+
# provide any builtins that could change the environment.
|
| 64 |
+
try:
|
| 65 |
+
self._executor.send_tools(tools)
|
| 66 |
+
except Exception:
|
| 67 |
+
# If the LocalPythonExecutor implementation doesn't support
|
| 68 |
+
# send_tools or fails, log and continue — the executor is still usable.
|
| 69 |
+
logger.debug("LocalPythonExecutor.send_tools failed; continuing without extra tools", exc_info=True)
|
| 70 |
+
|
| 71 |
+
def run(self, code: str) -> CodeExecResult:
|
| 72 |
+
"""Execute Python code and return a CodeExecResult.
|
| 73 |
+
|
| 74 |
+
This method is intentionally defensive: it attempts to extract
|
| 75 |
+
meaningful stdout/stderr/exit_code information from a variety of
|
| 76 |
+
possible return shapes that different versions of smolagents
|
| 77 |
+
may provide.
|
| 78 |
+
"""
|
| 79 |
+
try:
|
| 80 |
+
exec_result = self._executor(code)
|
| 81 |
+
|
| 82 |
+
# Default values
|
| 83 |
+
stdout_parts: list[str] = []
|
| 84 |
+
stderr_parts: list[str] = []
|
| 85 |
+
exit_code = 0
|
| 86 |
+
|
| 87 |
+
# Extract logs/prints
|
| 88 |
+
try:
|
| 89 |
+
logs = getattr(exec_result, "logs", None)
|
| 90 |
+
if logs:
|
| 91 |
+
stdout_parts.append(str(logs))
|
| 92 |
+
except Exception:
|
| 93 |
+
logger.debug("Failed to read exec_result.logs", exc_info=True)
|
| 94 |
+
|
| 95 |
+
# Extract the result / output value
|
| 96 |
+
try:
|
| 97 |
+
if hasattr(exec_result, "output"):
|
| 98 |
+
out_val = exec_result.output
|
| 99 |
+
# If the output is not None, stringify it in a safe way
|
| 100 |
+
if out_val is not None:
|
| 101 |
+
# Prefer JSON if possible, otherwise repr
|
| 102 |
+
try:
|
| 103 |
+
stdout_parts.append(json.dumps(out_val))
|
| 104 |
+
except Exception:
|
| 105 |
+
stdout_parts.append(repr(out_val))
|
| 106 |
+
except Exception:
|
| 107 |
+
logger.debug("Failed to read exec_result.output", exc_info=True)
|
| 108 |
+
|
| 109 |
+
# Some runtime implementations may put errors on `error` or `exception`
|
| 110 |
+
try:
|
| 111 |
+
err = getattr(exec_result, "error", None)
|
| 112 |
+
if err:
|
| 113 |
+
stderr_parts.append(str(err))
|
| 114 |
+
except Exception:
|
| 115 |
+
logger.debug("Failed to read exec_result.error", exc_info=True)
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
ex = getattr(exec_result, "exception", None)
|
| 119 |
+
if ex:
|
| 120 |
+
stderr_parts.append(str(ex))
|
| 121 |
+
except Exception:
|
| 122 |
+
logger.debug("Failed to read exec_result.exception", exc_info=True)
|
| 123 |
+
|
| 124 |
+
# Determine exit code if provided
|
| 125 |
+
try:
|
| 126 |
+
if hasattr(exec_result, "exit_code"):
|
| 127 |
+
exit_code = int(exec_result.exit_code) if exec_result.exit_code is not None else 0
|
| 128 |
+
elif hasattr(exec_result, "success"):
|
| 129 |
+
# Some versions use `success` boolean
|
| 130 |
+
exit_code = 0 if exec_result.success else 1
|
| 131 |
+
else:
|
| 132 |
+
# Fallback: if there were any stderr parts, treat as non-zero
|
| 133 |
+
exit_code = 1 if stderr_parts else 0
|
| 134 |
+
except Exception:
|
| 135 |
+
logger.debug("Failed to determine exec_result exit code", exc_info=True)
|
| 136 |
+
exit_code = 1 if stderr_parts else 0
|
| 137 |
+
|
| 138 |
+
# Compose the final stdout/stderr strings
|
| 139 |
+
stdout = "\n".join(part for part in stdout_parts if part is not None)
|
| 140 |
+
stderr = "\n".join(part for part in stderr_parts if part is not None)
|
| 141 |
+
|
| 142 |
+
return CodeExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
| 143 |
+
|
| 144 |
+
except Exception as e:
|
| 145 |
+
# Any unexpected exception from the LocalPythonExecutor is
|
| 146 |
+
# returned with a full traceback to make debugging easier.
|
| 147 |
+
tb = traceback.format_exc()
|
| 148 |
+
logger.exception("LocalPythonExecutor raised an exception during run")
|
| 149 |
+
return CodeExecResult(stdout="", stderr=tb, exit_code=1)
|
server/transforms.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"""Transforms specific to coding environments."""
|
| 8 |
+
|
| 9 |
+
import ast
|
| 10 |
+
import re
|
| 11 |
+
|
| 12 |
+
from openenv_core.env_server.base_transforms import CompositeTransform
|
| 13 |
+
from openenv_core.env_server.interfaces import Transform
|
| 14 |
+
from openenv_core.env_server.types import Observation
|
| 15 |
+
|
| 16 |
+
from coding_env.models import CodeObservation
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CodeSafetyTransform(Transform):
|
| 20 |
+
"""Evaluates code safety and assigns penalties for dangerous patterns."""
|
| 21 |
+
|
| 22 |
+
def __init__(self, penalty: float = -1.0):
|
| 23 |
+
self.penalty = penalty
|
| 24 |
+
self.dangerous_patterns = [
|
| 25 |
+
r"import\s+os",
|
| 26 |
+
r"import\s+subprocess",
|
| 27 |
+
r"eval\(",
|
| 28 |
+
r"exec\(",
|
| 29 |
+
r"__import__",
|
| 30 |
+
r"open\(",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
def __call__(self, observation: Observation) -> Observation:
|
| 34 |
+
if not isinstance(observation, CodeObservation):
|
| 35 |
+
return observation
|
| 36 |
+
|
| 37 |
+
if "last_code" in observation.metadata:
|
| 38 |
+
code = observation.metadata["last_code"]
|
| 39 |
+
for pattern in self.dangerous_patterns:
|
| 40 |
+
if re.search(pattern, code):
|
| 41 |
+
observation.reward = self.penalty
|
| 42 |
+
observation.metadata["safety_violation"] = pattern
|
| 43 |
+
break
|
| 44 |
+
else:
|
| 45 |
+
if observation.reward is None:
|
| 46 |
+
observation.reward = 0.0
|
| 47 |
+
|
| 48 |
+
return observation
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class CodeQualityTransform(Transform):
|
| 52 |
+
"""Evaluates and rewards code quality metrics."""
|
| 53 |
+
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
concise_bonus: float = 0.1,
|
| 57 |
+
max_length_threshold: int = 100,
|
| 58 |
+
syntax_penalty: float = -0.2,
|
| 59 |
+
):
|
| 60 |
+
self.concise_bonus = concise_bonus
|
| 61 |
+
self.max_length_threshold = max_length_threshold
|
| 62 |
+
self.syntax_penalty = syntax_penalty
|
| 63 |
+
|
| 64 |
+
def __call__(self, observation: Observation) -> Observation:
|
| 65 |
+
if not isinstance(observation, CodeObservation):
|
| 66 |
+
return observation
|
| 67 |
+
|
| 68 |
+
quality_score = 0.0
|
| 69 |
+
|
| 70 |
+
if "last_code" in observation.metadata:
|
| 71 |
+
code = observation.metadata["last_code"]
|
| 72 |
+
|
| 73 |
+
# Reward concise code
|
| 74 |
+
if len(code.strip()) <= self.max_length_threshold:
|
| 75 |
+
quality_score += self.concise_bonus
|
| 76 |
+
|
| 77 |
+
# Check syntax (redundant but useful for quality assessment)
|
| 78 |
+
try:
|
| 79 |
+
ast.parse(code)
|
| 80 |
+
except SyntaxError:
|
| 81 |
+
quality_score += self.syntax_penalty
|
| 82 |
+
|
| 83 |
+
# Add to existing reward
|
| 84 |
+
if observation.reward is None:
|
| 85 |
+
observation.reward = quality_score
|
| 86 |
+
else:
|
| 87 |
+
observation.reward += quality_score
|
| 88 |
+
|
| 89 |
+
return observation
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def create_safe_coding_transform() -> CompositeTransform:
|
| 93 |
+
"""Create a transform focused on safe coding practices and quality."""
|
| 94 |
+
return CompositeTransform([CodeSafetyTransform(), CodeQualityTransform()])
|