Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- Dockerfile +10 -3
- README.md +122 -26
- src/core/README.md +180 -0
- src/core/__init__.py +1 -1
- src/core/__pycache__/__init__.cpython-311.pyc +0 -0
- src/core/__pycache__/__init__.cpython-313.pyc +0 -0
- src/core/__pycache__/http_env_client.cpython-311.pyc +0 -0
- src/core/__pycache__/http_env_client.cpython-313.pyc +0 -0
- src/core/__pycache__/types.cpython-311.pyc +0 -0
- src/core/__pycache__/types.cpython-313.pyc +0 -0
- src/core/client_types.py +22 -0
- src/core/containers/__pycache__/__init__.cpython-311.pyc +0 -0
- src/core/containers/__pycache__/__init__.cpython-313.pyc +0 -0
- src/core/containers/runtime/__pycache__/__init__.cpython-311.pyc +0 -0
- src/core/containers/runtime/__pycache__/__init__.cpython-313.pyc +0 -0
- src/core/containers/runtime/__pycache__/providers.cpython-311.pyc +0 -0
- src/core/containers/runtime/__pycache__/providers.cpython-313.pyc +0 -0
- src/core/containers/runtime/providers.py +6 -2
- src/core/env_server/__pycache__/__init__.cpython-311.pyc +0 -0
- src/core/env_server/__pycache__/__init__.cpython-313.pyc +0 -0
- src/core/env_server/__pycache__/base_transforms.cpython-311.pyc +0 -0
- src/core/env_server/__pycache__/base_transforms.cpython-313.pyc +0 -0
- src/core/env_server/__pycache__/http_server.cpython-311.pyc +0 -0
- src/core/env_server/__pycache__/http_server.cpython-313.pyc +0 -0
- src/core/env_server/__pycache__/interfaces.cpython-311.pyc +0 -0
- src/core/env_server/__pycache__/interfaces.cpython-313.pyc +0 -0
- src/core/env_server/__pycache__/types.cpython-311.pyc +0 -0
- src/core/env_server/__pycache__/types.cpython-313.pyc +0 -0
- src/core/env_server/__pycache__/web_interface.cpython-311.pyc +0 -0
- src/core/env_server/__pycache__/web_interface.cpython-313.pyc +0 -0
- src/core/http_env_client.py +15 -5
- src/core/pyproject.toml +46 -0
- src/core/tools/__init__.py +6 -1
- src/core/tools/git_server_client.py +362 -0
- src/envs/echo_env/README.md +13 -0
- src/envs/echo_env/__pycache__/__init__.cpython-311.pyc +0 -0
- src/envs/echo_env/__pycache__/__init__.cpython-313.pyc +0 -0
- src/envs/echo_env/__pycache__/client.cpython-311.pyc +0 -0
- src/envs/echo_env/__pycache__/client.cpython-313.pyc +0 -0
- src/envs/echo_env/__pycache__/models.cpython-311.pyc +0 -0
- src/envs/echo_env/client.py +2 -1
- src/envs/echo_env/server/__pycache__/__init__.cpython-311.pyc +0 -0
- src/envs/echo_env/server/__pycache__/app.cpython-311.pyc +0 -0
- src/envs/echo_env/server/__pycache__/echo_environment.cpython-311.pyc +0 -0
Dockerfile
CHANGED
|
@@ -4,17 +4,24 @@
|
|
| 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
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Copy only what's needed for this environment
|
| 11 |
COPY src/core/ /app/src/core/
|
| 12 |
COPY src/envs/echo_env/ /app/src/envs/echo_env/
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
# Health check
|
| 15 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 16 |
CMD curl -f http://localhost:8000/health || exit 1
|
| 17 |
|
| 18 |
# Run the FastAPI server
|
| 19 |
-
CMD ["uvicorn", "envs.echo_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 20 |
ENV ENABLE_WEB_INTERFACE=true
|
|
|
|
|
|
|
|
|
| 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=ghcr.io/meta-pytorch/openenv-base:latest
|
| 11 |
+
FROM ghcr.io/meta-pytorch/openenv-base:latest
|
| 12 |
|
| 13 |
# Copy only what's needed for this environment
|
| 14 |
COPY src/core/ /app/src/core/
|
| 15 |
COPY src/envs/echo_env/ /app/src/envs/echo_env/
|
| 16 |
|
| 17 |
+
# Copy README for web interface documentation
|
| 18 |
+
COPY src/envs/echo_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 |
ENV ENABLE_WEB_INTERFACE=true
|
| 26 |
+
|
| 27 |
+
CMD ["uvicorn", "envs.echo_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🔊
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
app_port: 8000
|
|
@@ -11,40 +11,136 @@ tags:
|
|
| 11 |
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
##
|
| 19 |
|
| 20 |
-
|
| 21 |
-
Built with FastAPI and OpenEnv framework.
|
| 22 |
|
| 23 |
-
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
- **Live Updates**: WebSocket-based real-time updates
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
{
|
| 40 |
-
"message": "Hello World"
|
| 41 |
-
}
|
| 42 |
```
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Echo Environment Server
|
| 3 |
emoji: 🔊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
app_port: 8000
|
|
|
|
| 11 |
- openenv
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# Echo 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 Echo environment is through the `EchoEnv` class:
|
|
|
|
| 21 |
|
| 22 |
+
```python
|
| 23 |
+
from envs.echo_env import EchoAction, EchoEnv
|
| 24 |
|
| 25 |
+
try:
|
| 26 |
+
# Create environment from Docker image
|
| 27 |
+
echo_env = EchoEnv.from_docker_image("echo-env:latest")
|
|
|
|
| 28 |
|
| 29 |
+
# Reset
|
| 30 |
+
result = echo_env.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 = echo_env.step(EchoAction(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 |
+
echo_env.close()
|
|
|
|
|
|
|
|
|
|
| 46 |
```
|
| 47 |
|
| 48 |
+
That's it! The `EchoEnv.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 echo-env:latest -f src/envs/echo_env/server/Dockerfile .
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
## Environment Details
|
| 64 |
+
|
| 65 |
+
### Action
|
| 66 |
+
**EchoAction**: Contains a single field
|
| 67 |
+
- `message` (str) - The message to echo back
|
| 68 |
+
|
| 69 |
+
### Observation
|
| 70 |
+
**EchoObservation**: Contains the echo response and metadata
|
| 71 |
+
- `echoed_message` (str) - The message echoed back
|
| 72 |
+
- `message_length` (int) - Length of the message
|
| 73 |
+
- `reward` (float) - Reward based on message length (length × 0.1)
|
| 74 |
+
- `done` (bool) - Always False for echo environment
|
| 75 |
+
- `metadata` (dict) - Additional info like step count
|
| 76 |
+
|
| 77 |
+
### Reward
|
| 78 |
+
The reward is calculated as: `message_length × 0.1`
|
| 79 |
+
- "Hi" → reward: 0.2
|
| 80 |
+
- "Hello, World!" → reward: 1.3
|
| 81 |
+
- Empty message → reward: 0.0
|
| 82 |
+
|
| 83 |
+
## Advanced Usage
|
| 84 |
+
|
| 85 |
+
### Connecting to an Existing Server
|
| 86 |
+
|
| 87 |
+
If you already have an Echo environment server running, you can connect directly:
|
| 88 |
+
|
| 89 |
+
```python
|
| 90 |
+
from envs.echo_env import EchoEnv
|
| 91 |
+
|
| 92 |
+
# Connect to existing server
|
| 93 |
+
echo_env = EchoEnv(base_url="<ENV_HTTP_URL_HERE>")
|
| 94 |
+
|
| 95 |
+
# Use as normal
|
| 96 |
+
result = echo_env.reset()
|
| 97 |
+
result = echo_env.step(EchoAction(message="Hello!"))
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
Note: When connecting to an existing server, `echo_env.close()` will NOT stop the server.
|
| 101 |
+
|
| 102 |
+
## Development & Testing
|
| 103 |
+
|
| 104 |
+
### Direct Environment Testing
|
| 105 |
+
|
| 106 |
+
Test the environment logic directly without starting the HTTP server:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# From the server directory
|
| 110 |
+
python3 src/envs/echo_env/server/test_echo_env.py
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
This verifies that:
|
| 114 |
+
- Environment resets correctly
|
| 115 |
+
- Step executes actions properly
|
| 116 |
+
- State tracking works
|
| 117 |
+
- Rewards are calculated correctly
|
| 118 |
+
|
| 119 |
+
### Running the Full Example
|
| 120 |
+
|
| 121 |
+
Run the complete example that demonstrates the full workflow:
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
python3 examples/local_echo_env.py
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
This example shows:
|
| 128 |
+
- Creating an environment from a Docker image
|
| 129 |
+
- Resetting and stepping through the environment
|
| 130 |
+
- Automatic cleanup with `close()`
|
| 131 |
+
|
| 132 |
+
## Project Structure
|
| 133 |
+
|
| 134 |
+
```
|
| 135 |
+
echo_env/
|
| 136 |
+
├── __init__.py # Module exports
|
| 137 |
+
├── README.md # This file
|
| 138 |
+
├── client.py # EchoEnv client implementation
|
| 139 |
+
├── models.py # Action and Observation models
|
| 140 |
+
└── server/
|
| 141 |
+
├── __init__.py # Server module exports
|
| 142 |
+
├── echo_environment.py # Core environment logic
|
| 143 |
+
├── app.py # FastAPI application
|
| 144 |
+
├── test_echo_env.py # Direct environment tests
|
| 145 |
+
└── Dockerfile # Container image definition
|
| 146 |
+
```
|
src/core/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# <img width="35" height="35" alt="image" src="https://github.com/user-attachments/assets/2700a971-e5d6-4036-b03f-2f89c9791609" /> OpenEnv: Agentic Execution Environments
|
| 2 |
+
|
| 3 |
+
An e2e framework for creating, deploying and using isolated execution environments for agentic RL training, built using Gymnasium style simple APIs. OpenEnv provides a standard for interacting with agentic execution environments via simple Gymnasium style APIs - step(), reset(), state(). Users of agentic execution environments can interact with the environment during RL training loops using these simple APIs.
|
| 4 |
+
|
| 5 |
+
In addition to making it easier for researchers and RL framework writers, we also provide tools for environment creators making it easier for them to create richer environments and make them available over familar protocols like HTTP and packaged using canonical technologies like docker. Environment creators can use the OpenEnv framework to create environments that are isolated, secure, and easy to deploy and use.
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
## Overview
|
| 9 |
+
`openenv-core` provides the foundational building blocks for creating and interacting with containerized environments over HTTP. It enables you to build agent environments that can be deployed as Docker containers and accessed via a simple HTTP API.
|
| 10 |
+
|
| 11 |
+
> ⚠️ **Early Development Warning** OpenEnv is currently in an experimental
|
| 12 |
+
> stage. You should expect bugs, incomplete features, and APIs that may change
|
| 13 |
+
> in future versions. The project welcomes bugfixes, but to make sure things are
|
| 14 |
+
> well coordinated you should discuss any significant change before starting the
|
| 15 |
+
> work. It's recommended that you signal your intention to contribute in the
|
| 16 |
+
> issue tracker, either by filing a new issue or by claiming an existing one.
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# OpenEnv Core
|
| 20 |
+
|
| 21 |
+
Core components for OpenEnv - a framework for building HTTP-based agentic environments.
|
| 22 |
+
|
| 23 |
+
## Features
|
| 24 |
+
|
| 25 |
+
- **HTTPEnvClient**: Generic HTTP client for interacting with remote environments
|
| 26 |
+
- **HTTPEnvServer**: FastAPI-based server wrapper for exposing environments over HTTP
|
| 27 |
+
- **Container Providers**: Pluggable architecture for running containers (Docker, Kubernetes, etc.)
|
| 28 |
+
- **Type System**: Strongly-typed Action/Observation/State interfaces
|
| 29 |
+
- **Web Interface**: Optional web UI for interacting with environments
|
| 30 |
+
|
| 31 |
+
## Installation
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
pip install openenv-core
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
For development:
|
| 38 |
+
```bash
|
| 39 |
+
pip install openenv-core[dev]
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## Quick Start
|
| 43 |
+
|
| 44 |
+
### Creating an Environment Client
|
| 45 |
+
|
| 46 |
+
```python
|
| 47 |
+
from openenv_core import HTTPEnvClient, StepResult
|
| 48 |
+
from dataclasses import dataclass
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class MyAction:
|
| 52 |
+
text: str
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class MyObservation:
|
| 56 |
+
response: str
|
| 57 |
+
|
| 58 |
+
class MyEnvClient(HTTPEnvClient[MyAction, MyObservation]):
|
| 59 |
+
def _step_payload(self, action: MyAction) -> dict:
|
| 60 |
+
return {"text": action.text}
|
| 61 |
+
|
| 62 |
+
def _parse_result(self, payload: dict) -> StepResult[MyObservation]:
|
| 63 |
+
obs_data = payload["observation"]
|
| 64 |
+
return StepResult(
|
| 65 |
+
observation=MyObservation(**obs_data),
|
| 66 |
+
reward=payload.get("reward"),
|
| 67 |
+
done=payload.get("done", False)
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def _parse_state(self, payload: dict) -> Any:
|
| 71 |
+
return payload
|
| 72 |
+
|
| 73 |
+
# Use with Docker
|
| 74 |
+
env = MyEnvClient.from_docker_image("my-env:latest")
|
| 75 |
+
result = env.reset()
|
| 76 |
+
step_result = env.step(MyAction(text="hello"))
|
| 77 |
+
env.close()
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### Creating an Environment Server
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
from openenv_core.env_server import Environment, HTTPEnvServer, create_app
|
| 84 |
+
from dataclasses import dataclass
|
| 85 |
+
|
| 86 |
+
@dataclass
|
| 87 |
+
class MyAction:
|
| 88 |
+
text: str
|
| 89 |
+
|
| 90 |
+
@dataclass
|
| 91 |
+
class MyObservation:
|
| 92 |
+
response: str
|
| 93 |
+
reward: float = 0.0
|
| 94 |
+
done: bool = False
|
| 95 |
+
|
| 96 |
+
class MyEnvironment(Environment):
|
| 97 |
+
def reset(self) -> MyObservation:
|
| 98 |
+
return MyObservation(response="Ready")
|
| 99 |
+
|
| 100 |
+
def step(self, action: MyAction) -> MyObservation:
|
| 101 |
+
return MyObservation(
|
| 102 |
+
response=f"Echo: {action.text}",
|
| 103 |
+
reward=1.0,
|
| 104 |
+
done=False
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
# Create FastAPI app
|
| 108 |
+
env = MyEnvironment()
|
| 109 |
+
app = create_app(env, MyAction, MyObservation)
|
| 110 |
+
|
| 111 |
+
# Run with: uvicorn module:app --host 0.0.0.0 --port 8000
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Container Providers
|
| 115 |
+
|
| 116 |
+
OpenEnv Core supports multiple container providers:
|
| 117 |
+
|
| 118 |
+
### Local Docker Provider
|
| 119 |
+
|
| 120 |
+
```python
|
| 121 |
+
from openenv_core.containers.runtime import LocalDockerProvider
|
| 122 |
+
|
| 123 |
+
provider = LocalDockerProvider()
|
| 124 |
+
base_url = provider.start_container("my-env:latest")
|
| 125 |
+
provider.wait_for_ready(base_url)
|
| 126 |
+
# Use environment...
|
| 127 |
+
provider.stop_container()
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
### Kubernetes Provider (Coming Soon)
|
| 131 |
+
|
| 132 |
+
```python
|
| 133 |
+
from openenv_core.containers.runtime import KubernetesProvider
|
| 134 |
+
|
| 135 |
+
provider = KubernetesProvider(namespace="envs")
|
| 136 |
+
base_url = provider.start_container("my-env:latest")
|
| 137 |
+
# Use environment...
|
| 138 |
+
provider.stop_container()
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
## API Reference
|
| 143 |
+
|
| 144 |
+
### HTTPEnvClient
|
| 145 |
+
|
| 146 |
+
Base class for environment clients with these abstract methods:
|
| 147 |
+
|
| 148 |
+
- `_step_payload(action)`: Convert action to JSON
|
| 149 |
+
- `_parse_result(payload)`: Parse response to StepResult
|
| 150 |
+
- `_parse_state(payload)`: Parse state response
|
| 151 |
+
|
| 152 |
+
### HTTPEnvServer
|
| 153 |
+
|
| 154 |
+
Server wrapper with these methods:
|
| 155 |
+
|
| 156 |
+
- `register_routes(app)`: Register endpoints on FastAPI app
|
| 157 |
+
- `_deserialize_action(data)`: Convert JSON to Action
|
| 158 |
+
- `_serialize_observation(obs)`: Convert Observation to JSON
|
| 159 |
+
|
| 160 |
+
### Environment Interface
|
| 161 |
+
|
| 162 |
+
Base interface for environment implementations:
|
| 163 |
+
|
| 164 |
+
- `reset()`: Reset environment and return initial observation
|
| 165 |
+
- `step(action)`: Execute action and return observation
|
| 166 |
+
- `state`: Property returning current environment state
|
| 167 |
+
|
| 168 |
+
## License
|
| 169 |
+
|
| 170 |
+
This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.
|
| 171 |
+
|
| 172 |
+
## Contributing
|
| 173 |
+
|
| 174 |
+
Contributions are welcome! Please see the main OpenEnv repository for contribution guidelines.
|
| 175 |
+
|
| 176 |
+
## Links
|
| 177 |
+
|
| 178 |
+
- **Homepage**: https://github.com/facebookresearch/OpenEnv
|
| 179 |
+
- **Documentation**: https://github.com/facebookresearch/OpenEnv/blob/main/README.md
|
| 180 |
+
- **Bug Tracker**: https://github.com/facebookresearch/OpenEnv/issues
|
src/core/__init__.py
CHANGED
|
@@ -8,8 +8,8 @@
|
|
| 8 |
|
| 9 |
# Re-export main components from submodules for convenience
|
| 10 |
from .env_server import *
|
|
|
|
| 11 |
from .http_env_client import HTTPEnvClient
|
| 12 |
-
from .types import StepResult
|
| 13 |
|
| 14 |
# Note: MCP module doesn't export anything yet
|
| 15 |
|
|
|
|
| 8 |
|
| 9 |
# Re-export main components from submodules for convenience
|
| 10 |
from .env_server import *
|
| 11 |
+
from .client_types import StepResult
|
| 12 |
from .http_env_client import HTTPEnvClient
|
|
|
|
| 13 |
|
| 14 |
# Note: MCP module doesn't export anything yet
|
| 15 |
|
src/core/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (400 Bytes). View file
|
|
|
src/core/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (383 Bytes). View file
|
|
|
src/core/__pycache__/http_env_client.cpython-311.pyc
ADDED
|
Binary file (7.68 kB). View file
|
|
|
src/core/__pycache__/http_env_client.cpython-313.pyc
ADDED
|
Binary file (6.93 kB). View file
|
|
|
src/core/__pycache__/types.cpython-311.pyc
ADDED
|
Binary file (1.09 kB). View file
|
|
|
src/core/__pycache__/types.cpython-313.pyc
ADDED
|
Binary file (993 Bytes). View file
|
|
|
src/core/client_types.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Type definitions for EnvTorch
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from typing import Any, Generic, Optional, TypeVar
|
| 4 |
+
|
| 5 |
+
# Generic type for observations
|
| 6 |
+
ObsT = TypeVar("ObsT") # TypeVar for typehinting in IDEs
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class StepResult(Generic[ObsT]):
|
| 11 |
+
"""
|
| 12 |
+
Represents the result of one environment step.
|
| 13 |
+
|
| 14 |
+
Attributes:
|
| 15 |
+
observation: The environment's observation after the action.
|
| 16 |
+
reward: Scalar reward for this step (optional).
|
| 17 |
+
done: Whether the episode is finished.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
observation: ObsT
|
| 21 |
+
reward: Optional[float] = None
|
| 22 |
+
done: bool = False
|
src/core/containers/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (206 Bytes). View file
|
|
|
src/core/containers/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (224 Bytes). View file
|
|
|
src/core/containers/runtime/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (389 Bytes). View file
|
|
|
src/core/containers/runtime/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (375 Bytes). View file
|
|
|
src/core/containers/runtime/__pycache__/providers.cpython-311.pyc
ADDED
|
Binary file (10.9 kB). View file
|
|
|
src/core/containers/runtime/__pycache__/providers.cpython-313.pyc
ADDED
|
Binary file (9.64 kB). View file
|
|
|
src/core/containers/runtime/providers.py
CHANGED
|
@@ -169,8 +169,12 @@ class LocalDockerProvider(ContainerProvider):
|
|
| 169 |
cmd.append(image)
|
| 170 |
|
| 171 |
# Run container
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
# Wait a moment for container to start
|
| 176 |
time.sleep(1)
|
|
|
|
| 169 |
cmd.append(image)
|
| 170 |
|
| 171 |
# Run container
|
| 172 |
+
try:
|
| 173 |
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
| 174 |
+
self._container_id = result.stdout.strip()
|
| 175 |
+
except subprocess.CalledProcessError as e:
|
| 176 |
+
error_msg = f"Failed to start Docker container.\nCommand: {' '.join(cmd)}\nExit code: {e.returncode}\nStderr: {e.stderr}\nStdout: {e.stdout}"
|
| 177 |
+
raise RuntimeError(error_msg) from e
|
| 178 |
|
| 179 |
# Wait a moment for container to start
|
| 180 |
time.sleep(1)
|
src/core/env_server/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (898 Bytes). View file
|
|
|
src/core/env_server/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (788 Bytes). View file
|
|
|
src/core/env_server/__pycache__/base_transforms.cpython-311.pyc
ADDED
|
Binary file (1.67 kB). View file
|
|
|
src/core/env_server/__pycache__/base_transforms.cpython-313.pyc
ADDED
|
Binary file (1.57 kB). View file
|
|
|
src/core/env_server/__pycache__/http_server.cpython-311.pyc
ADDED
|
Binary file (9.2 kB). View file
|
|
|
src/core/env_server/__pycache__/http_server.cpython-313.pyc
ADDED
|
Binary file (8.33 kB). View file
|
|
|
src/core/env_server/__pycache__/interfaces.cpython-311.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|
src/core/env_server/__pycache__/interfaces.cpython-313.pyc
ADDED
|
Binary file (4.68 kB). View file
|
|
|
src/core/env_server/__pycache__/types.cpython-311.pyc
ADDED
|
Binary file (2.39 kB). View file
|
|
|
src/core/env_server/__pycache__/types.cpython-313.pyc
ADDED
|
Binary file (2.66 kB). View file
|
|
|
src/core/env_server/__pycache__/web_interface.cpython-311.pyc
ADDED
|
Binary file (29.9 kB). View file
|
|
|
src/core/env_server/__pycache__/web_interface.cpython-313.pyc
ADDED
|
Binary file (59.3 kB). View file
|
|
|
src/core/http_env_client.py
CHANGED
|
@@ -12,11 +12,12 @@ Future hooks (commented below) for:
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
from abc import ABC, abstractmethod
|
| 15 |
-
from typing import
|
| 16 |
-
|
| 17 |
import requests
|
| 18 |
|
| 19 |
-
from .
|
|
|
|
| 20 |
|
| 21 |
if TYPE_CHECKING:
|
| 22 |
from .containers.runtime import ContainerProvider
|
|
@@ -45,6 +46,7 @@ class HTTPEnvClient(ABC, Generic[ActT, ObsT]):
|
|
| 45 |
cls: Type[EnvClientT],
|
| 46 |
image: str,
|
| 47 |
provider: Optional["ContainerProvider"] = None,
|
|
|
|
| 48 |
) -> EnvClientT:
|
| 49 |
"""
|
| 50 |
Create an environment client by spinning up a Docker container locally.
|
|
@@ -60,6 +62,8 @@ class HTTPEnvClient(ABC, Generic[ActT, ObsT]):
|
|
| 60 |
Args:
|
| 61 |
image: Docker image name to run (e.g., "echo-env:latest")
|
| 62 |
provider: Container provider to use (defaults to LocalDockerProvider)
|
|
|
|
|
|
|
| 63 |
|
| 64 |
Returns:
|
| 65 |
An instance of the client class connected to the running container
|
|
@@ -71,6 +75,12 @@ class HTTPEnvClient(ABC, Generic[ActT, ObsT]):
|
|
| 71 |
>>> # Create environment from image
|
| 72 |
>>> env = CodingEnv.from_docker_image("coding-env:latest")
|
| 73 |
>>>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
>>> # Use the environment
|
| 75 |
>>> result = env.reset()
|
| 76 |
>>> print(result.observation)
|
|
@@ -86,8 +96,8 @@ class HTTPEnvClient(ABC, Generic[ActT, ObsT]):
|
|
| 86 |
if provider is None:
|
| 87 |
provider = LocalDockerProvider()
|
| 88 |
|
| 89 |
-
# 1. Start container
|
| 90 |
-
base_url = provider.start_container(image)
|
| 91 |
|
| 92 |
# 2. Wait for server to be ready
|
| 93 |
provider.wait_for_ready(base_url)
|
|
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
from abc import ABC, abstractmethod
|
| 15 |
+
from typing import Any, Dict, Generic, Optional, Type, TYPE_CHECKING, TypeVar
|
| 16 |
+
|
| 17 |
import requests
|
| 18 |
|
| 19 |
+
from .client_types import StepResult
|
| 20 |
+
from .containers.runtime import LocalDockerProvider
|
| 21 |
|
| 22 |
if TYPE_CHECKING:
|
| 23 |
from .containers.runtime import ContainerProvider
|
|
|
|
| 46 |
cls: Type[EnvClientT],
|
| 47 |
image: str,
|
| 48 |
provider: Optional["ContainerProvider"] = None,
|
| 49 |
+
**kwargs: Any,
|
| 50 |
) -> EnvClientT:
|
| 51 |
"""
|
| 52 |
Create an environment client by spinning up a Docker container locally.
|
|
|
|
| 62 |
Args:
|
| 63 |
image: Docker image name to run (e.g., "echo-env:latest")
|
| 64 |
provider: Container provider to use (defaults to LocalDockerProvider)
|
| 65 |
+
**kwargs: Additional arguments to pass to provider.start_container()
|
| 66 |
+
(e.g., env_vars, port)
|
| 67 |
|
| 68 |
Returns:
|
| 69 |
An instance of the client class connected to the running container
|
|
|
|
| 75 |
>>> # Create environment from image
|
| 76 |
>>> env = CodingEnv.from_docker_image("coding-env:latest")
|
| 77 |
>>>
|
| 78 |
+
>>> # Create environment with custom env vars
|
| 79 |
+
>>> env = CodingEnv.from_docker_image(
|
| 80 |
+
... "coding-env:latest",
|
| 81 |
+
... env_vars={"MY_VAR": "value"}
|
| 82 |
+
... )
|
| 83 |
+
>>>
|
| 84 |
>>> # Use the environment
|
| 85 |
>>> result = env.reset()
|
| 86 |
>>> print(result.observation)
|
|
|
|
| 96 |
if provider is None:
|
| 97 |
provider = LocalDockerProvider()
|
| 98 |
|
| 99 |
+
# 1. Start container with optional kwargs (e.g., env_vars, port)
|
| 100 |
+
base_url = provider.start_container(image, **kwargs)
|
| 101 |
|
| 102 |
# 2. Wait for server to be ready
|
| 103 |
provider.wait_for_ready(base_url)
|
src/core/pyproject.toml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=45", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "openenv-core"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "Core components for OpenEnv - HTTP-based agentic environments"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.8"
|
| 11 |
+
license = {text = "BSD-3-Clause"}
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "Meta Platforms, Inc.", email = "opensource@meta.com"}
|
| 14 |
+
]
|
| 15 |
+
keywords = ["environment", "agent", "http", "docker", "fastapi"]
|
| 16 |
+
|
| 17 |
+
dependencies = [
|
| 18 |
+
"requests>=2.25.0",
|
| 19 |
+
"fastapi>=0.104.0",
|
| 20 |
+
"uvicorn>=0.24.0",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
[project.optional-dependencies]
|
| 24 |
+
dev = [
|
| 25 |
+
"pytest>=7.0.0",
|
| 26 |
+
"black>=23.0.0",
|
| 27 |
+
"ruff>=0.1.0",
|
| 28 |
+
"mypy>=1.0.0",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
[project.urls]
|
| 32 |
+
Homepage = "https://github.com/facebookresearch/OpenEnv"
|
| 33 |
+
Repository = "https://github.com/facebookresearch/OpenEnv"
|
| 34 |
+
Documentation = "https://github.com/facebookresearch/OpenEnv/blob/main/README.md"
|
| 35 |
+
"Bug Tracker" = "https://github.com/facebookresearch/OpenEnv/issues"
|
| 36 |
+
|
| 37 |
+
[tool.setuptools]
|
| 38 |
+
py-modules = ["openenv_core.__init__", "openenv_core.http_env_client", "openenv_core.client_types"]
|
| 39 |
+
packages = [
|
| 40 |
+
"openenv_core",
|
| 41 |
+
"openenv_core.containers",
|
| 42 |
+
"openenv_core.containers.runtime",
|
| 43 |
+
"openenv_core.env_server",
|
| 44 |
+
"openenv_core.tools"
|
| 45 |
+
]
|
| 46 |
+
package-dir = {"openenv_core" = "."}
|
src/core/tools/__init__.py
CHANGED
|
@@ -6,6 +6,11 @@
|
|
| 6 |
|
| 7 |
"""Core tools for code execution and other utilities."""
|
| 8 |
|
|
|
|
| 9 |
from .local_python_executor import PyExecutor
|
| 10 |
|
| 11 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
"""Core tools for code execution and other utilities."""
|
| 8 |
|
| 9 |
+
from .git_server_client import GitServerClient, RepoInfo
|
| 10 |
from .local_python_executor import PyExecutor
|
| 11 |
|
| 12 |
+
__all__ = [
|
| 13 |
+
"PyExecutor",
|
| 14 |
+
"GitServerClient",
|
| 15 |
+
"RepoInfo",
|
| 16 |
+
]
|
src/core/tools/git_server_client.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Git Server Client for connecting to external Gitea instance.
|
| 4 |
+
|
| 5 |
+
This module provides a lightweight client for interacting with a shared
|
| 6 |
+
Gitea service, optimized for task-based isolation where multiple environment
|
| 7 |
+
instances share the same Gitea server but have isolated workspaces.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import shutil
|
| 13 |
+
import subprocess
|
| 14 |
+
import time
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from urllib.parse import urlparse
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class RepoInfo:
|
| 22 |
+
"""Information about a repository."""
|
| 23 |
+
|
| 24 |
+
name: str
|
| 25 |
+
url: str
|
| 26 |
+
commit: str
|
| 27 |
+
clone_url: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class GitServerClient:
|
| 31 |
+
"""
|
| 32 |
+
Client for connecting to an external Gitea server.
|
| 33 |
+
|
| 34 |
+
This client is optimized for task-based isolation where:
|
| 35 |
+
- Multiple tasks share the same Gitea instance
|
| 36 |
+
- Each task has its own isolated workspace
|
| 37 |
+
- Fast reset() via git operations (no server restart)
|
| 38 |
+
- Repos are pre-migrated to Gitea once
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
gitea_url: URL of the Gitea server (e.g., "http://gitea:3000")
|
| 42 |
+
username: Gitea username for authentication
|
| 43 |
+
password: Gitea password for authentication
|
| 44 |
+
workspace_dir: Local workspace directory for cloning repos
|
| 45 |
+
|
| 46 |
+
Example:
|
| 47 |
+
>>> # Connect to shared Gitea (credentials from environment)
|
| 48 |
+
>>> import os
|
| 49 |
+
>>> client = GitServerClient(
|
| 50 |
+
... gitea_url=os.getenv("GITEA_URL"),
|
| 51 |
+
... username=os.getenv("GITEA_USERNAME"),
|
| 52 |
+
... password=os.getenv("GITEA_PASSWORD")
|
| 53 |
+
... )
|
| 54 |
+
>>> client.wait_for_ready()
|
| 55 |
+
>>> # Clone repo to workspace
|
| 56 |
+
>>> path = client.clone_to_workspace("my-repo", commit="abc123")
|
| 57 |
+
>>> # Fast reset to base state
|
| 58 |
+
>>> client.reset_workspace("my-repo", commit="abc123")
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
gitea_url: str,
|
| 64 |
+
username: str,
|
| 65 |
+
password: str,
|
| 66 |
+
workspace_dir: str = "/workspace",
|
| 67 |
+
):
|
| 68 |
+
"""Initialize Git Server Client."""
|
| 69 |
+
self.gitea_url = gitea_url.rstrip("/")
|
| 70 |
+
self.username = username
|
| 71 |
+
self.password = password
|
| 72 |
+
self.workspace_dir = Path(workspace_dir)
|
| 73 |
+
self.is_ready = False
|
| 74 |
+
|
| 75 |
+
# Parse Gitea URL
|
| 76 |
+
parsed = urlparse(self.gitea_url)
|
| 77 |
+
self.domain = parsed.hostname or "localhost"
|
| 78 |
+
self.port = parsed.port or 3000
|
| 79 |
+
|
| 80 |
+
# Ensure workspace exists
|
| 81 |
+
os.makedirs(self.workspace_dir, exist_ok=True)
|
| 82 |
+
|
| 83 |
+
# Configure git credentials
|
| 84 |
+
self._configure_git()
|
| 85 |
+
|
| 86 |
+
def _configure_git(self):
|
| 87 |
+
"""Configure git credentials for automatic authentication."""
|
| 88 |
+
home_dir = Path.home()
|
| 89 |
+
|
| 90 |
+
# Git config
|
| 91 |
+
git_config = f"""[user]
|
| 92 |
+
name = {self.username}
|
| 93 |
+
email = {self.username}@local.env
|
| 94 |
+
[init]
|
| 95 |
+
defaultBranch = main
|
| 96 |
+
[credential]
|
| 97 |
+
helper = store
|
| 98 |
+
"""
|
| 99 |
+
gitconfig_path = home_dir / ".gitconfig"
|
| 100 |
+
gitconfig_path.write_text(git_config)
|
| 101 |
+
|
| 102 |
+
# Git credentials
|
| 103 |
+
git_credentials = f"http://{self.username}:{self.password}@{self.domain}:{self.port}\n"
|
| 104 |
+
gitcreds_path = home_dir / ".git-credentials"
|
| 105 |
+
gitcreds_path.write_text(git_credentials)
|
| 106 |
+
gitcreds_path.chmod(0o600)
|
| 107 |
+
|
| 108 |
+
def wait_for_ready(self, timeout: int = 30) -> bool:
|
| 109 |
+
"""
|
| 110 |
+
Wait for Gitea server to be ready.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
timeout: Maximum seconds to wait
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
True if server is ready, False otherwise
|
| 117 |
+
"""
|
| 118 |
+
start_time = time.time()
|
| 119 |
+
while time.time() - start_time < timeout:
|
| 120 |
+
try:
|
| 121 |
+
result = subprocess.run(
|
| 122 |
+
["curl", "-sf", f"{self.gitea_url}/"],
|
| 123 |
+
capture_output=True,
|
| 124 |
+
timeout=5,
|
| 125 |
+
)
|
| 126 |
+
if result.returncode == 0:
|
| 127 |
+
self.is_ready = True
|
| 128 |
+
return True
|
| 129 |
+
except subprocess.TimeoutExpired:
|
| 130 |
+
pass
|
| 131 |
+
except Exception:
|
| 132 |
+
pass
|
| 133 |
+
|
| 134 |
+
time.sleep(1)
|
| 135 |
+
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
def list_repositories(self) -> list[dict[str, str]]:
|
| 139 |
+
"""
|
| 140 |
+
List all repositories in Gitea.
|
| 141 |
+
|
| 142 |
+
Returns:
|
| 143 |
+
List of repository information dictionaries
|
| 144 |
+
"""
|
| 145 |
+
if not self.is_ready:
|
| 146 |
+
raise RuntimeError("Gitea server is not ready")
|
| 147 |
+
|
| 148 |
+
result = subprocess.run(
|
| 149 |
+
[
|
| 150 |
+
"curl",
|
| 151 |
+
"-s",
|
| 152 |
+
f"{self.gitea_url}/api/v1/user/repos",
|
| 153 |
+
"-u",
|
| 154 |
+
f"{self.username}:{self.password}",
|
| 155 |
+
],
|
| 156 |
+
capture_output=True,
|
| 157 |
+
text=True,
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
if result.returncode != 0:
|
| 161 |
+
return []
|
| 162 |
+
|
| 163 |
+
try:
|
| 164 |
+
repos = json.loads(result.stdout)
|
| 165 |
+
return [
|
| 166 |
+
{
|
| 167 |
+
"name": repo["name"],
|
| 168 |
+
"full_name": repo["full_name"],
|
| 169 |
+
"clone_url": repo["clone_url"],
|
| 170 |
+
"description": repo.get("description", ""),
|
| 171 |
+
}
|
| 172 |
+
for repo in repos
|
| 173 |
+
]
|
| 174 |
+
except (json.JSONDecodeError, KeyError):
|
| 175 |
+
return []
|
| 176 |
+
|
| 177 |
+
def clone_to_workspace(
|
| 178 |
+
self, repo_name: str, target_dir: str | None = None, commit: str = "main"
|
| 179 |
+
) -> str:
|
| 180 |
+
"""
|
| 181 |
+
Clone a repository to the workspace at a specific commit.
|
| 182 |
+
|
| 183 |
+
This creates a fresh clone optimized for task isolation.
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
repo_name: Name of repository to clone
|
| 187 |
+
target_dir: Target directory name (defaults to repo_name)
|
| 188 |
+
commit: Commit hash or branch to checkout
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
Path to cloned repository
|
| 192 |
+
|
| 193 |
+
Raises:
|
| 194 |
+
RuntimeError: If clone fails
|
| 195 |
+
"""
|
| 196 |
+
if not self.is_ready:
|
| 197 |
+
raise RuntimeError("Gitea server is not ready")
|
| 198 |
+
|
| 199 |
+
target_dir = target_dir or repo_name
|
| 200 |
+
target_path = self.workspace_dir / target_dir
|
| 201 |
+
|
| 202 |
+
# Remove existing directory if present
|
| 203 |
+
if target_path.exists():
|
| 204 |
+
shutil.rmtree(target_path)
|
| 205 |
+
|
| 206 |
+
clone_url = f"{self.gitea_url}/{self.username}/{repo_name}.git"
|
| 207 |
+
|
| 208 |
+
# Clone repository
|
| 209 |
+
result = subprocess.run(
|
| 210 |
+
["git", "clone", clone_url, str(target_path)],
|
| 211 |
+
capture_output=True,
|
| 212 |
+
text=True,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
if result.returncode != 0:
|
| 216 |
+
raise RuntimeError(f"Clone failed: {result.stderr}")
|
| 217 |
+
|
| 218 |
+
# Checkout specific commit
|
| 219 |
+
if commit != "main":
|
| 220 |
+
result = subprocess.run(
|
| 221 |
+
["git", "checkout", commit],
|
| 222 |
+
cwd=str(target_path),
|
| 223 |
+
capture_output=True,
|
| 224 |
+
text=True,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
if result.returncode != 0:
|
| 228 |
+
raise RuntimeError(f"Checkout failed: {result.stderr}")
|
| 229 |
+
|
| 230 |
+
return str(target_path)
|
| 231 |
+
|
| 232 |
+
def reset_workspace(self, repo_name: str, commit: str = "main") -> bool:
|
| 233 |
+
"""
|
| 234 |
+
Fast reset of workspace to base state (optimized for task resets).
|
| 235 |
+
|
| 236 |
+
This is much faster than re-cloning. It:
|
| 237 |
+
1. Checks out the target commit
|
| 238 |
+
2. Resets to that commit (hard)
|
| 239 |
+
3. Cleans untracked files
|
| 240 |
+
|
| 241 |
+
Args:
|
| 242 |
+
repo_name: Name of repository (directory in workspace)
|
| 243 |
+
commit: Commit hash or branch to reset to
|
| 244 |
+
|
| 245 |
+
Returns:
|
| 246 |
+
True if reset successful
|
| 247 |
+
|
| 248 |
+
Raises:
|
| 249 |
+
RuntimeError: If reset fails
|
| 250 |
+
"""
|
| 251 |
+
repo_path = self.workspace_dir / repo_name
|
| 252 |
+
|
| 253 |
+
if not repo_path.exists():
|
| 254 |
+
raise RuntimeError(f"Repository not found in workspace: {repo_name}")
|
| 255 |
+
|
| 256 |
+
# Fetch latest (in case commit is new)
|
| 257 |
+
subprocess.run(
|
| 258 |
+
["git", "fetch", "--all"],
|
| 259 |
+
cwd=str(repo_path),
|
| 260 |
+
capture_output=True,
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# Checkout and hard reset to commit
|
| 264 |
+
result = subprocess.run(
|
| 265 |
+
["git", "checkout", commit],
|
| 266 |
+
cwd=str(repo_path),
|
| 267 |
+
capture_output=True,
|
| 268 |
+
text=True,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
if result.returncode != 0:
|
| 272 |
+
raise RuntimeError(f"Checkout failed: {result.stderr}")
|
| 273 |
+
|
| 274 |
+
result = subprocess.run(
|
| 275 |
+
["git", "reset", "--hard", f"origin/{commit}" if commit != "main" else commit],
|
| 276 |
+
cwd=str(repo_path),
|
| 277 |
+
capture_output=True,
|
| 278 |
+
text=True,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
if result.returncode != 0:
|
| 282 |
+
# Try without origin/ prefix
|
| 283 |
+
result = subprocess.run(
|
| 284 |
+
["git", "reset", "--hard", commit],
|
| 285 |
+
cwd=str(repo_path),
|
| 286 |
+
capture_output=True,
|
| 287 |
+
text=True,
|
| 288 |
+
)
|
| 289 |
+
if result.returncode != 0:
|
| 290 |
+
raise RuntimeError(f"Reset failed: {result.stderr}")
|
| 291 |
+
|
| 292 |
+
# Clean untracked files and directories
|
| 293 |
+
subprocess.run(
|
| 294 |
+
["git", "clean", "-fdx"],
|
| 295 |
+
cwd=str(repo_path),
|
| 296 |
+
capture_output=True,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
return True
|
| 300 |
+
|
| 301 |
+
def execute_git_command(
|
| 302 |
+
self, command: str, working_dir: str = ""
|
| 303 |
+
) -> tuple[int, str, str]:
|
| 304 |
+
"""
|
| 305 |
+
Execute a git command in the workspace.
|
| 306 |
+
|
| 307 |
+
Args:
|
| 308 |
+
command: Git command to execute (without 'git' prefix)
|
| 309 |
+
working_dir: Working directory relative to workspace
|
| 310 |
+
|
| 311 |
+
Returns:
|
| 312 |
+
Tuple of (exit_code, stdout, stderr)
|
| 313 |
+
"""
|
| 314 |
+
work_path = (
|
| 315 |
+
self.workspace_dir / working_dir if working_dir else self.workspace_dir
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
if not work_path.exists():
|
| 319 |
+
return (1, "", f"Working directory does not exist: {work_path}")
|
| 320 |
+
|
| 321 |
+
# Split command safely
|
| 322 |
+
cmd_parts = ["git"] + command.split()
|
| 323 |
+
|
| 324 |
+
result = subprocess.run(
|
| 325 |
+
cmd_parts,
|
| 326 |
+
cwd=str(work_path),
|
| 327 |
+
capture_output=True,
|
| 328 |
+
text=True,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
return (result.returncode, result.stdout, result.stderr)
|
| 332 |
+
|
| 333 |
+
def get_current_commit(self, repo_name: str) -> str:
|
| 334 |
+
"""
|
| 335 |
+
Get current commit hash of a workspace repository.
|
| 336 |
+
|
| 337 |
+
Args:
|
| 338 |
+
repo_name: Name of repository in workspace
|
| 339 |
+
|
| 340 |
+
Returns:
|
| 341 |
+
Commit hash
|
| 342 |
+
"""
|
| 343 |
+
repo_path = self.workspace_dir / repo_name
|
| 344 |
+
|
| 345 |
+
if not repo_path.exists():
|
| 346 |
+
raise RuntimeError(f"Repository not found: {repo_name}")
|
| 347 |
+
|
| 348 |
+
result = subprocess.run(
|
| 349 |
+
["git", "rev-parse", "HEAD"],
|
| 350 |
+
cwd=str(repo_path),
|
| 351 |
+
capture_output=True,
|
| 352 |
+
text=True,
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
if result.returncode != 0:
|
| 356 |
+
raise RuntimeError(f"Failed to get commit: {result.stderr}")
|
| 357 |
+
|
| 358 |
+
return result.stdout.strip()
|
| 359 |
+
|
| 360 |
+
def workspace_exists(self, repo_name: str) -> bool:
|
| 361 |
+
"""Check if a repository exists in workspace."""
|
| 362 |
+
return (self.workspace_dir / repo_name).exists()
|
src/envs/echo_env/README.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Echo Environment
|
| 2 |
|
| 3 |
A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Echo Environment Server
|
| 3 |
+
emoji: 🔊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
app_port: 8000
|
| 9 |
+
base_path: /web
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
# Echo Environment
|
| 15 |
|
| 16 |
A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
|
src/envs/echo_env/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (412 Bytes). View file
|
|
|
src/envs/echo_env/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (390 Bytes). View file
|
|
|
src/envs/echo_env/__pycache__/client.cpython-311.pyc
ADDED
|
Binary file (3.96 kB). View file
|
|
|
src/envs/echo_env/__pycache__/client.cpython-313.pyc
ADDED
|
Binary file (3.43 kB). View file
|
|
|
src/envs/echo_env/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (1.34 kB). View file
|
|
|
src/envs/echo_env/client.py
CHANGED
|
@@ -13,9 +13,10 @@ over HTTP.
|
|
| 13 |
|
| 14 |
from typing import Any, Dict
|
| 15 |
|
|
|
|
|
|
|
| 16 |
from core.env_server.types import State
|
| 17 |
from core.http_env_client import HTTPEnvClient
|
| 18 |
-
from core.types import StepResult
|
| 19 |
|
| 20 |
from .models import EchoAction, EchoObservation
|
| 21 |
|
|
|
|
| 13 |
|
| 14 |
from typing import Any, Dict
|
| 15 |
|
| 16 |
+
from core.client_types import StepResult
|
| 17 |
+
|
| 18 |
from core.env_server.types import State
|
| 19 |
from core.http_env_client import HTTPEnvClient
|
|
|
|
| 20 |
|
| 21 |
from .models import EchoAction, EchoObservation
|
| 22 |
|
src/envs/echo_env/server/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (304 Bytes). View file
|
|
|
src/envs/echo_env/server/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (1.16 kB). View file
|
|
|
src/envs/echo_env/server/__pycache__/echo_environment.cpython-311.pyc
ADDED
|
Binary file (3.59 kB). View file
|
|
|