wukaixingxp commited on
Commit
a7789b5
·
verified ·
1 Parent(s): de118dc

Upload folder using huggingface_hub

Browse files
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 Env Test
3
- emoji: 👁
4
- colorFrom: green
5
  colorTo: blue
6
  sdk: docker
7
  pinned: false
8
- short_description: openenv coding-env-test
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()])