llm-sandbox 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_sandbox-0.1.0/LICENSE +21 -0
- llm_sandbox-0.1.0/PKG-INFO +181 -0
- llm_sandbox-0.1.0/README.md +162 -0
- llm_sandbox-0.1.0/llm_sandbox/__init__.py +1 -0
- llm_sandbox-0.1.0/llm_sandbox/base.py +61 -0
- llm_sandbox-0.1.0/llm_sandbox/const.py +27 -0
- llm_sandbox-0.1.0/llm_sandbox/docker.py +260 -0
- llm_sandbox-0.1.0/llm_sandbox/kubernetes.py +284 -0
- llm_sandbox-0.1.0/llm_sandbox/session.py +10 -0
- llm_sandbox-0.1.0/llm_sandbox/utils.py +90 -0
- llm_sandbox-0.1.0/pyproject.toml +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Duy Huynh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: llm-sandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight and portable LLM sandbox runtime (code interpreter) Python library
|
|
5
|
+
Home-page: https://github.com/vndee/llm-sandbox
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Duy Huynh
|
|
8
|
+
Author-email: vndee.huynh@gmail.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
|
15
|
+
Requires-Dist: kubernetes (>=30.1.0,<31.0.0)
|
|
16
|
+
Project-URL: Repository, https://github.com/vndee/llm-sandbox
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
## LLM Sandbox
|
|
20
|
+
|
|
21
|
+
*The easiest way to run large language model (LLM) generated code (code interpreter) in a safe and isolated environment.*
|
|
22
|
+
|
|
23
|
+
LLM Sandbox is a lightweight and portable sandbox environment designed to run large language model (LLM) generated code in a safe and isolated manner using Docker containers. This project aims to provide an easy-to-use interface for setting up, managing, and executing code in a controlled Docker environment, simplifying the process of running code generated by LLMs.
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- **Easy Setup:** Quickly create sandbox environments with minimal configuration.
|
|
28
|
+
- **Isolation:** Run your code in isolated Docker containers to prevent interference with your host system.
|
|
29
|
+
- **Flexibility:** Support for multiple programming languages.
|
|
30
|
+
- **Portability:** Use predefined Docker images or custom Dockerfiles.
|
|
31
|
+
- **Scalability:** Support Kubernetes and remote Docker host.
|
|
32
|
+
|
|
33
|
+
### Installation
|
|
34
|
+
|
|
35
|
+
#### Using Poetry
|
|
36
|
+
|
|
37
|
+
1. Ensure you have [Poetry](https://python-poetry.org/docs/#installation) installed.
|
|
38
|
+
2. Add the package to your project:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
poetry add llm-sandbox
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### Using pip
|
|
45
|
+
|
|
46
|
+
1. Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed.
|
|
47
|
+
2. Install the package:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pip install llm-sandbox
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Usage
|
|
54
|
+
|
|
55
|
+
#### Session Lifecycle
|
|
56
|
+
|
|
57
|
+
The `SandboxSession` class manages the lifecycle of the sandbox environment, including the creation and destruction of Docker containers. Here’s a typical lifecycle:
|
|
58
|
+
|
|
59
|
+
1. **Initialization:** Create a `SandboxSession` object with the desired configuration.
|
|
60
|
+
2. **Open Session:** Call the `open()` method to build/pull the Docker image and start the Docker container.
|
|
61
|
+
3. **Run Code:** Use the `run()` method to execute code inside the sandbox. Currently, it supports Python, Java, JavaScript, C++, Go, and Ruby. See [examples](examples) for more details.
|
|
62
|
+
4. **Close Session:** Call the `close()` method to stop and remove the Docker container. If the `keep_template` flag is set to `True`, the Docker image will not be removed, and the last container state will be committed to the image.
|
|
63
|
+
|
|
64
|
+
### Example
|
|
65
|
+
|
|
66
|
+
Here's a simple example to demonstrate how to use LLM Sandbox:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from llm_sandbox import SandboxSession
|
|
70
|
+
|
|
71
|
+
# Create a new sandbox session
|
|
72
|
+
with SandboxSession(image="python:3.9.19-bullseye", keep_template=True, lang="python") as session:
|
|
73
|
+
result = session.run("print('Hello, World!')")
|
|
74
|
+
print(result)
|
|
75
|
+
|
|
76
|
+
# With custom Dockerfile
|
|
77
|
+
with SandboxSession(dockerfile="Dockerfile", keep_template=True, lang="python") as session:
|
|
78
|
+
result = session.run("print('Hello, World!')")
|
|
79
|
+
print(result)
|
|
80
|
+
|
|
81
|
+
# Or default image
|
|
82
|
+
with SandboxSession(lang="python", keep_template=True) as session:
|
|
83
|
+
result = session.run("print('Hello, World!')")
|
|
84
|
+
print(result)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
LLM Sandbox also supports copying files between the host and the sandbox:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from llm_sandbox import SandboxSession
|
|
92
|
+
|
|
93
|
+
with SandboxSession(lang="python", keep_template=True) as session:
|
|
94
|
+
# Copy a file from the host to the sandbox
|
|
95
|
+
session.copy_to_runtime("test.py", "/sandbox/test.py")
|
|
96
|
+
|
|
97
|
+
# Run the copied Python code in the sandbox
|
|
98
|
+
result = session.run("python /sandbox/test.py")
|
|
99
|
+
print(result)
|
|
100
|
+
|
|
101
|
+
# Copy a file from the sandbox to the host
|
|
102
|
+
session.copy_from_runtime("/sandbox/output.txt", "output.txt")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For other languages usage, please refer to the [examples](examples/code_runner_docker.py).
|
|
106
|
+
|
|
107
|
+
You can also use [remote Docker host](https://docs.docker.com/config/daemon/remote-access/) as below:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import docker
|
|
111
|
+
from llm_sandbox import SandboxSession
|
|
112
|
+
|
|
113
|
+
tls_config = docker.tls.TLSConfig(
|
|
114
|
+
client_cert=("path/to/cert.pem", "path/to/key.pem"),
|
|
115
|
+
ca_cert="path/to/ca.pem",
|
|
116
|
+
verify=True
|
|
117
|
+
)
|
|
118
|
+
docker_client = docker.DockerClient(base_url="tcp://<your_host>:<port>", tls=tls_config)
|
|
119
|
+
|
|
120
|
+
with SandboxSession(
|
|
121
|
+
client=docker_client,
|
|
122
|
+
mage="python:3.9.19-bullseye",
|
|
123
|
+
keep_template=True,
|
|
124
|
+
lang="python",
|
|
125
|
+
) as session:
|
|
126
|
+
result = session.run("print('Hello, World!')")
|
|
127
|
+
print(result)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
For Kubernetes usage, please refer to the examples. Essentially, you just need to set the use_kubernetes flag to True and provide the Kubernetes client, or leave it as the default for the local context.
|
|
131
|
+
|
|
132
|
+
### API Reference
|
|
133
|
+
|
|
134
|
+
#### `SandboxSession`
|
|
135
|
+
|
|
136
|
+
##### Initialization
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
SandboxSession(
|
|
140
|
+
image: Optional[str] = None,
|
|
141
|
+
dockerfile: Optional[str] = None,
|
|
142
|
+
lang: str = SupportedLanguage.PYTHON,
|
|
143
|
+
keep_template: bool = False,
|
|
144
|
+
verbose: bool = True
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- **`image`**: Docker image to use.
|
|
149
|
+
- **`dockerfile`**: Path to the Dockerfile, if an image is not provided.
|
|
150
|
+
- **`lang`**: Language of the code (default: `SupportedLanguage.PYTHON`).
|
|
151
|
+
- **`keep_template`**: If `True`, the image and container will not be removed after the session ends.
|
|
152
|
+
- **`verbose`**: If `True`, print messages.
|
|
153
|
+
|
|
154
|
+
##### Methods
|
|
155
|
+
|
|
156
|
+
- **`open()`**: Start the Docker container.
|
|
157
|
+
- **`close()`**: Stop and remove the Docker container.
|
|
158
|
+
- **`run(code: str, libraries: Optional[List] = None)`**: Execute code inside the sandbox.
|
|
159
|
+
- **`copy_from_runtime(src: str, dest: str)`**: Copy a file from the sandbox to the host.
|
|
160
|
+
- **`copy_to_runtime(src: str, dest: str)`**: Copy a file from the host to the sandbox.
|
|
161
|
+
- **`execute_command(command: str)`**: Execute a command inside the sandbox.
|
|
162
|
+
|
|
163
|
+
### Contributing
|
|
164
|
+
|
|
165
|
+
We welcome contributions to improve LLM Sandbox! Since I am a Python developer, I am not familiar with other languages. If you are interested in adding better support for other languages, please feel free to submit a pull request.
|
|
166
|
+
|
|
167
|
+
Here is a list of things you can do to contribute:
|
|
168
|
+
- [ ] Add Java maven support.
|
|
169
|
+
- [x] Add support for JavaScript.
|
|
170
|
+
- [x] Add support for C++.
|
|
171
|
+
- [x] Add support for Go.
|
|
172
|
+
- [ ] Add support for Ruby.
|
|
173
|
+
- [x] Add remote Docker host support.
|
|
174
|
+
- [x] Add remote Kubernetes cluster support.
|
|
175
|
+
- [ ] Commit the last container state to the image before closing kubernetes session.
|
|
176
|
+
- [ ] Release version 1.0.0.
|
|
177
|
+
|
|
178
|
+
### License
|
|
179
|
+
|
|
180
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
181
|
+
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
## LLM Sandbox
|
|
2
|
+
|
|
3
|
+
*The easiest way to run large language model (LLM) generated code (code interpreter) in a safe and isolated environment.*
|
|
4
|
+
|
|
5
|
+
LLM Sandbox is a lightweight and portable sandbox environment designed to run large language model (LLM) generated code in a safe and isolated manner using Docker containers. This project aims to provide an easy-to-use interface for setting up, managing, and executing code in a controlled Docker environment, simplifying the process of running code generated by LLMs.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **Easy Setup:** Quickly create sandbox environments with minimal configuration.
|
|
10
|
+
- **Isolation:** Run your code in isolated Docker containers to prevent interference with your host system.
|
|
11
|
+
- **Flexibility:** Support for multiple programming languages.
|
|
12
|
+
- **Portability:** Use predefined Docker images or custom Dockerfiles.
|
|
13
|
+
- **Scalability:** Support Kubernetes and remote Docker host.
|
|
14
|
+
|
|
15
|
+
### Installation
|
|
16
|
+
|
|
17
|
+
#### Using Poetry
|
|
18
|
+
|
|
19
|
+
1. Ensure you have [Poetry](https://python-poetry.org/docs/#installation) installed.
|
|
20
|
+
2. Add the package to your project:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
poetry add llm-sandbox
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
#### Using pip
|
|
27
|
+
|
|
28
|
+
1. Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed.
|
|
29
|
+
2. Install the package:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install llm-sandbox
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Usage
|
|
36
|
+
|
|
37
|
+
#### Session Lifecycle
|
|
38
|
+
|
|
39
|
+
The `SandboxSession` class manages the lifecycle of the sandbox environment, including the creation and destruction of Docker containers. Here’s a typical lifecycle:
|
|
40
|
+
|
|
41
|
+
1. **Initialization:** Create a `SandboxSession` object with the desired configuration.
|
|
42
|
+
2. **Open Session:** Call the `open()` method to build/pull the Docker image and start the Docker container.
|
|
43
|
+
3. **Run Code:** Use the `run()` method to execute code inside the sandbox. Currently, it supports Python, Java, JavaScript, C++, Go, and Ruby. See [examples](examples) for more details.
|
|
44
|
+
4. **Close Session:** Call the `close()` method to stop and remove the Docker container. If the `keep_template` flag is set to `True`, the Docker image will not be removed, and the last container state will be committed to the image.
|
|
45
|
+
|
|
46
|
+
### Example
|
|
47
|
+
|
|
48
|
+
Here's a simple example to demonstrate how to use LLM Sandbox:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from llm_sandbox import SandboxSession
|
|
52
|
+
|
|
53
|
+
# Create a new sandbox session
|
|
54
|
+
with SandboxSession(image="python:3.9.19-bullseye", keep_template=True, lang="python") as session:
|
|
55
|
+
result = session.run("print('Hello, World!')")
|
|
56
|
+
print(result)
|
|
57
|
+
|
|
58
|
+
# With custom Dockerfile
|
|
59
|
+
with SandboxSession(dockerfile="Dockerfile", keep_template=True, lang="python") as session:
|
|
60
|
+
result = session.run("print('Hello, World!')")
|
|
61
|
+
print(result)
|
|
62
|
+
|
|
63
|
+
# Or default image
|
|
64
|
+
with SandboxSession(lang="python", keep_template=True) as session:
|
|
65
|
+
result = session.run("print('Hello, World!')")
|
|
66
|
+
print(result)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
LLM Sandbox also supports copying files between the host and the sandbox:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from llm_sandbox import SandboxSession
|
|
74
|
+
|
|
75
|
+
with SandboxSession(lang="python", keep_template=True) as session:
|
|
76
|
+
# Copy a file from the host to the sandbox
|
|
77
|
+
session.copy_to_runtime("test.py", "/sandbox/test.py")
|
|
78
|
+
|
|
79
|
+
# Run the copied Python code in the sandbox
|
|
80
|
+
result = session.run("python /sandbox/test.py")
|
|
81
|
+
print(result)
|
|
82
|
+
|
|
83
|
+
# Copy a file from the sandbox to the host
|
|
84
|
+
session.copy_from_runtime("/sandbox/output.txt", "output.txt")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For other languages usage, please refer to the [examples](examples/code_runner_docker.py).
|
|
88
|
+
|
|
89
|
+
You can also use [remote Docker host](https://docs.docker.com/config/daemon/remote-access/) as below:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import docker
|
|
93
|
+
from llm_sandbox import SandboxSession
|
|
94
|
+
|
|
95
|
+
tls_config = docker.tls.TLSConfig(
|
|
96
|
+
client_cert=("path/to/cert.pem", "path/to/key.pem"),
|
|
97
|
+
ca_cert="path/to/ca.pem",
|
|
98
|
+
verify=True
|
|
99
|
+
)
|
|
100
|
+
docker_client = docker.DockerClient(base_url="tcp://<your_host>:<port>", tls=tls_config)
|
|
101
|
+
|
|
102
|
+
with SandboxSession(
|
|
103
|
+
client=docker_client,
|
|
104
|
+
mage="python:3.9.19-bullseye",
|
|
105
|
+
keep_template=True,
|
|
106
|
+
lang="python",
|
|
107
|
+
) as session:
|
|
108
|
+
result = session.run("print('Hello, World!')")
|
|
109
|
+
print(result)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For Kubernetes usage, please refer to the examples. Essentially, you just need to set the use_kubernetes flag to True and provide the Kubernetes client, or leave it as the default for the local context.
|
|
113
|
+
|
|
114
|
+
### API Reference
|
|
115
|
+
|
|
116
|
+
#### `SandboxSession`
|
|
117
|
+
|
|
118
|
+
##### Initialization
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
SandboxSession(
|
|
122
|
+
image: Optional[str] = None,
|
|
123
|
+
dockerfile: Optional[str] = None,
|
|
124
|
+
lang: str = SupportedLanguage.PYTHON,
|
|
125
|
+
keep_template: bool = False,
|
|
126
|
+
verbose: bool = True
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **`image`**: Docker image to use.
|
|
131
|
+
- **`dockerfile`**: Path to the Dockerfile, if an image is not provided.
|
|
132
|
+
- **`lang`**: Language of the code (default: `SupportedLanguage.PYTHON`).
|
|
133
|
+
- **`keep_template`**: If `True`, the image and container will not be removed after the session ends.
|
|
134
|
+
- **`verbose`**: If `True`, print messages.
|
|
135
|
+
|
|
136
|
+
##### Methods
|
|
137
|
+
|
|
138
|
+
- **`open()`**: Start the Docker container.
|
|
139
|
+
- **`close()`**: Stop and remove the Docker container.
|
|
140
|
+
- **`run(code: str, libraries: Optional[List] = None)`**: Execute code inside the sandbox.
|
|
141
|
+
- **`copy_from_runtime(src: str, dest: str)`**: Copy a file from the sandbox to the host.
|
|
142
|
+
- **`copy_to_runtime(src: str, dest: str)`**: Copy a file from the host to the sandbox.
|
|
143
|
+
- **`execute_command(command: str)`**: Execute a command inside the sandbox.
|
|
144
|
+
|
|
145
|
+
### Contributing
|
|
146
|
+
|
|
147
|
+
We welcome contributions to improve LLM Sandbox! Since I am a Python developer, I am not familiar with other languages. If you are interested in adding better support for other languages, please feel free to submit a pull request.
|
|
148
|
+
|
|
149
|
+
Here is a list of things you can do to contribute:
|
|
150
|
+
- [ ] Add Java maven support.
|
|
151
|
+
- [x] Add support for JavaScript.
|
|
152
|
+
- [x] Add support for C++.
|
|
153
|
+
- [x] Add support for Go.
|
|
154
|
+
- [ ] Add support for Ruby.
|
|
155
|
+
- [x] Add remote Docker host support.
|
|
156
|
+
- [x] Add remote Kubernetes cluster support.
|
|
157
|
+
- [ ] Commit the last container state to the image before closing kubernetes session.
|
|
158
|
+
- [ ] Release version 1.0.0.
|
|
159
|
+
|
|
160
|
+
### License
|
|
161
|
+
|
|
162
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .session import SandboxSession # noqa: F401
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConsoleOutput:
|
|
6
|
+
def __init__(self, text: str):
|
|
7
|
+
self._text = text
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def text(self):
|
|
11
|
+
return self._text
|
|
12
|
+
|
|
13
|
+
def __str__(self):
|
|
14
|
+
return f"ConsoleOutput(text={self.text})"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KubernetesConsoleOutput(ConsoleOutput):
|
|
18
|
+
def __init__(self, exit_code: int, text: str):
|
|
19
|
+
super().__init__(text)
|
|
20
|
+
self.exit_code = exit_code
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"KubernetesConsoleOutput(text={self.text}, exit_code={self.exit_code})"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Session(ABC):
|
|
27
|
+
def __init__(self, lang: str, verbose: bool = True, *args, **kwargs):
|
|
28
|
+
self.lang = lang
|
|
29
|
+
self.verbose = verbose
|
|
30
|
+
super().__init__(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def open(self):
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def close(self):
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def run(self, code: str, libraries: Optional[List] = None) -> ConsoleOutput:
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def copy_to_runtime(self, src: str, dest: str):
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def copy_from_runtime(self, src: str, dest: str):
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def execute_command(self, command: str):
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
def __enter__(self):
|
|
57
|
+
self.open()
|
|
58
|
+
return self
|
|
59
|
+
|
|
60
|
+
def __exit__(self, *args, **kwargs):
|
|
61
|
+
self.close()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class SupportedLanguage:
|
|
6
|
+
PYTHON = "python"
|
|
7
|
+
JAVA = "java"
|
|
8
|
+
JAVASCRIPT = "javascript"
|
|
9
|
+
CPP = "cpp"
|
|
10
|
+
GO = "go"
|
|
11
|
+
RUBY = "ruby"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DefaultImage:
|
|
16
|
+
PYTHON = "python:3.9.19-bullseye"
|
|
17
|
+
JAVA = "openjdk:11.0.12-jdk-bullseye"
|
|
18
|
+
JAVASCRIPT = "node:22-bullseye"
|
|
19
|
+
CPP = "gcc:11.2.0-bullseye"
|
|
20
|
+
GO = "golang:1.17.0-bullseye"
|
|
21
|
+
RUBY = "ruby:3.0.2-bullseye"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
NotSupportedLibraryInstallation = ["JAVA"]
|
|
25
|
+
SupportedLanguageValues = [
|
|
26
|
+
v for k, v in SupportedLanguage.__dict__.items() if not k.startswith("__")
|
|
27
|
+
]
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import docker
|
|
4
|
+
import tarfile
|
|
5
|
+
from typing import List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from docker.models.images import Image
|
|
8
|
+
from docker.models.containers import Container
|
|
9
|
+
from llm_sandbox.utils import (
|
|
10
|
+
image_exists,
|
|
11
|
+
get_libraries_installation_command,
|
|
12
|
+
get_code_file_extension,
|
|
13
|
+
get_code_execution_command,
|
|
14
|
+
)
|
|
15
|
+
from llm_sandbox.base import Session, ConsoleOutput
|
|
16
|
+
from llm_sandbox.const import (
|
|
17
|
+
SupportedLanguage,
|
|
18
|
+
SupportedLanguageValues,
|
|
19
|
+
DefaultImage,
|
|
20
|
+
NotSupportedLibraryInstallation,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SandboxDockerSession(Session):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
client: Optional[docker.DockerClient] = None,
|
|
28
|
+
image: Optional[str] = None,
|
|
29
|
+
dockerfile: Optional[str] = None,
|
|
30
|
+
lang: str = SupportedLanguage.PYTHON,
|
|
31
|
+
keep_template: bool = False,
|
|
32
|
+
verbose: bool = True,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Create a new sandbox session
|
|
36
|
+
:param client: Docker client, if not provided, a new client will be created based on local Docker context
|
|
37
|
+
:param image: Docker image to use
|
|
38
|
+
:param dockerfile: Path to the Dockerfile, if image is not provided
|
|
39
|
+
:param lang: Language of the code
|
|
40
|
+
:param keep_template: if True, the image and container will not be removed after the session ends
|
|
41
|
+
:param verbose: if True, print messages
|
|
42
|
+
"""
|
|
43
|
+
super().__init__(lang, verbose)
|
|
44
|
+
if image and dockerfile:
|
|
45
|
+
raise ValueError("Only one of image or dockerfile should be provided")
|
|
46
|
+
|
|
47
|
+
if lang not in SupportedLanguageValues:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Language {lang} is not supported. Must be one of {SupportedLanguageValues}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not image and not dockerfile:
|
|
53
|
+
image = DefaultImage.__dict__[lang.upper()]
|
|
54
|
+
|
|
55
|
+
self.lang: str = lang
|
|
56
|
+
self.client: Optional[docker.DockerClient] = None
|
|
57
|
+
|
|
58
|
+
if not client:
|
|
59
|
+
print("Using local Docker context since client is not provided..")
|
|
60
|
+
self.client = docker.from_env()
|
|
61
|
+
else:
|
|
62
|
+
self.client = client
|
|
63
|
+
|
|
64
|
+
self.image: Union[Image, str] = image
|
|
65
|
+
self.dockerfile: Optional[str] = dockerfile
|
|
66
|
+
self.container: Optional[Container] = None
|
|
67
|
+
self.path = None
|
|
68
|
+
self.keep_template = keep_template
|
|
69
|
+
self.is_create_template: bool = False
|
|
70
|
+
self.verbose = verbose
|
|
71
|
+
|
|
72
|
+
def open(self):
|
|
73
|
+
warning_str = (
|
|
74
|
+
"Since the `keep_template` flag is set to True the docker image will not be removed after the session ends "
|
|
75
|
+
"and remains for future use."
|
|
76
|
+
)
|
|
77
|
+
if self.dockerfile:
|
|
78
|
+
self.path = os.path.dirname(self.dockerfile)
|
|
79
|
+
if self.verbose:
|
|
80
|
+
f_str = f"Building docker image from {self.dockerfile}"
|
|
81
|
+
f_str = f"{f_str}\n{warning_str}" if self.keep_template else f_str
|
|
82
|
+
print(f_str)
|
|
83
|
+
|
|
84
|
+
self.image, _ = self.client.images.build(
|
|
85
|
+
path=self.path,
|
|
86
|
+
dockerfile=os.path.basename(self.dockerfile),
|
|
87
|
+
tag=f"sandbox-{self.lang.lower()}-{os.path.basename(self.path)}",
|
|
88
|
+
)
|
|
89
|
+
self.is_create_template = True
|
|
90
|
+
|
|
91
|
+
if isinstance(self.image, str):
|
|
92
|
+
if not image_exists(self.client, self.image):
|
|
93
|
+
if self.verbose:
|
|
94
|
+
f_str = f"Pulling image {self.image}.."
|
|
95
|
+
f_str = f"{f_str}\n{warning_str}" if self.keep_template else f_str
|
|
96
|
+
print(f_str)
|
|
97
|
+
|
|
98
|
+
self.image = self.client.images.pull(self.image)
|
|
99
|
+
self.is_create_template = True
|
|
100
|
+
else:
|
|
101
|
+
self.image = self.client.images.get(self.image)
|
|
102
|
+
if self.verbose:
|
|
103
|
+
print(f"Using image {self.image.tags[-1]}")
|
|
104
|
+
|
|
105
|
+
self.container = self.client.containers.run(self.image, detach=True, tty=True)
|
|
106
|
+
|
|
107
|
+
def close(self):
|
|
108
|
+
if self.container:
|
|
109
|
+
if isinstance(self.image, Image):
|
|
110
|
+
self.container.commit(self.image.tags[-1])
|
|
111
|
+
|
|
112
|
+
self.container.remove(force=True)
|
|
113
|
+
self.container = None
|
|
114
|
+
|
|
115
|
+
if self.is_create_template and not self.keep_template:
|
|
116
|
+
# check if the image is used by any other container
|
|
117
|
+
containers = self.client.containers.list(all=True)
|
|
118
|
+
image_id = (
|
|
119
|
+
self.image.id
|
|
120
|
+
if isinstance(self.image, Image)
|
|
121
|
+
else self.client.images.get(self.image).id
|
|
122
|
+
)
|
|
123
|
+
image_in_use = any(
|
|
124
|
+
container.image.id == image_id for container in containers
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if not image_in_use:
|
|
128
|
+
if isinstance(self.image, str):
|
|
129
|
+
self.client.images.remove(self.image)
|
|
130
|
+
elif isinstance(self.image, Image):
|
|
131
|
+
self.image.remove(force=True)
|
|
132
|
+
else:
|
|
133
|
+
raise ValueError("Invalid image type")
|
|
134
|
+
else:
|
|
135
|
+
if self.verbose:
|
|
136
|
+
print(
|
|
137
|
+
f"Image {self.image.tags[-1]} is in use by other containers. Skipping removal.."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def run(self, code: str, libraries: Optional[List] = None) -> ConsoleOutput:
|
|
141
|
+
if not self.container:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
"Session is not open. Please call open() method before running code."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if libraries:
|
|
147
|
+
if self.lang.upper() in NotSupportedLibraryInstallation:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Library installation has not been supported for {self.lang} yet!"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if self.lang == SupportedLanguage.GO:
|
|
153
|
+
self.execute_command("mkdir -p /example")
|
|
154
|
+
self.execute_command("go mod init example", workdir="/example")
|
|
155
|
+
self.execute_command("go mod tidy", workdir="/example")
|
|
156
|
+
|
|
157
|
+
for library in libraries:
|
|
158
|
+
command = get_libraries_installation_command(self.lang, library)
|
|
159
|
+
_ = self.execute_command(command, workdir="/example")
|
|
160
|
+
else:
|
|
161
|
+
for library in libraries:
|
|
162
|
+
command = get_libraries_installation_command(self.lang, library)
|
|
163
|
+
_ = self.execute_command(command)
|
|
164
|
+
|
|
165
|
+
code_file = f"/tmp/code.{get_code_file_extension(self.lang)}"
|
|
166
|
+
if self.lang == SupportedLanguage.GO:
|
|
167
|
+
code_dest_file = "/example/code.go"
|
|
168
|
+
else:
|
|
169
|
+
code_dest_file = code_file
|
|
170
|
+
|
|
171
|
+
with open(code_file, "w") as f:
|
|
172
|
+
f.write(code)
|
|
173
|
+
|
|
174
|
+
self.copy_to_runtime(code_file, code_dest_file)
|
|
175
|
+
|
|
176
|
+
output = ConsoleOutput("")
|
|
177
|
+
commands = get_code_execution_command(self.lang, code_dest_file)
|
|
178
|
+
for command in commands:
|
|
179
|
+
if self.lang == SupportedLanguage.GO:
|
|
180
|
+
output = self.execute_command(command, workdir="/example")
|
|
181
|
+
else:
|
|
182
|
+
output = self.execute_command(command)
|
|
183
|
+
|
|
184
|
+
return output
|
|
185
|
+
|
|
186
|
+
def copy_from_runtime(self, src: str, dest: str):
|
|
187
|
+
if not self.container:
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"Session is not open. Please call open() method before copying files."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if self.verbose:
|
|
193
|
+
print(f"Copying {self.container.short_id}:{src} to {dest}..")
|
|
194
|
+
|
|
195
|
+
bits, stat = self.container.get_archive(src)
|
|
196
|
+
if stat["size"] == 0:
|
|
197
|
+
raise FileNotFoundError(f"File {src} not found in the container")
|
|
198
|
+
|
|
199
|
+
tarstream = io.BytesIO(b"".join(bits))
|
|
200
|
+
with tarfile.open(fileobj=tarstream, mode="r") as tar:
|
|
201
|
+
tar.extractall(os.path.dirname(dest))
|
|
202
|
+
|
|
203
|
+
def copy_to_runtime(self, src: str, dest: str):
|
|
204
|
+
if not self.container:
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
"Session is not open. Please call open() method before copying files."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
is_created_dir = False
|
|
210
|
+
directory = os.path.dirname(dest)
|
|
211
|
+
if directory and not self.container.exec_run(f"test -d {directory}")[0] == 0:
|
|
212
|
+
self.container.exec_run(f"mkdir -p {directory}")
|
|
213
|
+
is_created_dir = True
|
|
214
|
+
|
|
215
|
+
if self.verbose:
|
|
216
|
+
if is_created_dir:
|
|
217
|
+
print(f"Creating directory {self.container.short_id}:{directory}")
|
|
218
|
+
print(f"Copying {src} to {self.container.short_id}:{dest}..")
|
|
219
|
+
|
|
220
|
+
tarstream = io.BytesIO()
|
|
221
|
+
with tarfile.open(fileobj=tarstream, mode="w") as tar:
|
|
222
|
+
tar.add(src, arcname=os.path.basename(src))
|
|
223
|
+
|
|
224
|
+
tarstream.seek(0)
|
|
225
|
+
self.container.put_archive(os.path.dirname(dest), tarstream)
|
|
226
|
+
|
|
227
|
+
def execute_command(
|
|
228
|
+
self, command: Optional[str], workdir: Optional[str] = None
|
|
229
|
+
) -> ConsoleOutput:
|
|
230
|
+
if not command:
|
|
231
|
+
raise ValueError("Command cannot be empty")
|
|
232
|
+
|
|
233
|
+
if not self.container:
|
|
234
|
+
raise RuntimeError(
|
|
235
|
+
"Session is not open. Please call open() method before executing commands."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if self.verbose:
|
|
239
|
+
print(f"Executing command: {command}")
|
|
240
|
+
|
|
241
|
+
if workdir:
|
|
242
|
+
exit_code, exec_log = self.container.exec_run(
|
|
243
|
+
command, stream=True, tty=True, workdir=workdir
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
exit_code, exec_log = self.container.exec_run(
|
|
247
|
+
command, stream=True, tty=True
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
output = ""
|
|
251
|
+
if self.verbose:
|
|
252
|
+
print("Output:", end=" ")
|
|
253
|
+
|
|
254
|
+
for chunk in exec_log:
|
|
255
|
+
chunk_str = chunk.decode("utf-8")
|
|
256
|
+
output += chunk_str
|
|
257
|
+
if self.verbose:
|
|
258
|
+
print(chunk_str, end="")
|
|
259
|
+
|
|
260
|
+
return ConsoleOutput(output)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
import tarfile
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from kubernetes import client as k8s_client, config
|
|
9
|
+
from kubernetes.stream import stream
|
|
10
|
+
from llm_sandbox.base import Session, ConsoleOutput, KubernetesConsoleOutput
|
|
11
|
+
from llm_sandbox.utils import (
|
|
12
|
+
get_libraries_installation_command,
|
|
13
|
+
get_code_file_extension,
|
|
14
|
+
get_code_execution_command,
|
|
15
|
+
)
|
|
16
|
+
from llm_sandbox.const import SupportedLanguage, SupportedLanguageValues, DefaultImage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SandboxKubernetesSession(Session):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
client: Optional[k8s_client.CoreV1Api] = None,
|
|
23
|
+
image: Optional[str] = None,
|
|
24
|
+
lang: str = SupportedLanguage.PYTHON,
|
|
25
|
+
keep_template: bool = False,
|
|
26
|
+
verbose: bool = True,
|
|
27
|
+
kube_namespace: str = "default",
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Create a new sandbox session
|
|
31
|
+
:param client: Kubernetes client, if not provided, a new client will be created based on local Kubernetes context
|
|
32
|
+
:param image: Docker image to use
|
|
33
|
+
:param lang: Language of the code
|
|
34
|
+
:param keep_template: if True, the image and container will not be removed after the session ends
|
|
35
|
+
:param verbose: if True, print messages
|
|
36
|
+
:param kube_namespace: Kubernetes namespace to use, default is 'default'
|
|
37
|
+
"""
|
|
38
|
+
super().__init__(lang, verbose)
|
|
39
|
+
if lang not in SupportedLanguageValues:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Language {lang} is not supported. Must be one of {SupportedLanguageValues}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if not image:
|
|
45
|
+
image = DefaultImage.__dict__[lang.upper()]
|
|
46
|
+
|
|
47
|
+
if not client:
|
|
48
|
+
print("Using local Kubernetes context since client is not provided..")
|
|
49
|
+
config.load_kube_config()
|
|
50
|
+
self.client = k8s_client.CoreV1Api()
|
|
51
|
+
else:
|
|
52
|
+
self.client = client
|
|
53
|
+
|
|
54
|
+
self.image = image
|
|
55
|
+
self.kube_namespace = kube_namespace
|
|
56
|
+
self.pod_name = f"sandbox-{lang.lower()}-{uuid.uuid4().hex}"
|
|
57
|
+
self.keep_template = keep_template
|
|
58
|
+
self.container = None
|
|
59
|
+
|
|
60
|
+
def open(self):
|
|
61
|
+
self._create_kubernetes_pod()
|
|
62
|
+
|
|
63
|
+
def _create_kubernetes_pod(self):
|
|
64
|
+
pod_manifest = {
|
|
65
|
+
"apiVersion": "v1",
|
|
66
|
+
"kind": "Pod",
|
|
67
|
+
"metadata": {
|
|
68
|
+
"name": self.pod_name,
|
|
69
|
+
"namespace": self.kube_namespace,
|
|
70
|
+
"labels": {"app": "sandbox"},
|
|
71
|
+
},
|
|
72
|
+
"spec": {
|
|
73
|
+
"containers": [
|
|
74
|
+
{"name": "sandbox-container", "image": self.image, "tty": True}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
self.client.create_namespaced_pod(
|
|
79
|
+
namespace=self.kube_namespace, body=pod_manifest
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
while True:
|
|
83
|
+
pod = self.client.read_namespaced_pod(
|
|
84
|
+
name=self.pod_name, namespace=self.kube_namespace
|
|
85
|
+
)
|
|
86
|
+
if pod.status.phase == "Running":
|
|
87
|
+
break
|
|
88
|
+
time.sleep(1)
|
|
89
|
+
|
|
90
|
+
self.container = self.pod_name
|
|
91
|
+
|
|
92
|
+
def close(self):
|
|
93
|
+
self._delete_kubernetes_pod()
|
|
94
|
+
|
|
95
|
+
def _delete_kubernetes_pod(self):
|
|
96
|
+
self.client.delete_namespaced_pod(
|
|
97
|
+
name=self.pod_name,
|
|
98
|
+
namespace=self.kube_namespace,
|
|
99
|
+
body=k8s_client.V1DeleteOptions(),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def run(self, code: str, libraries: Optional[List] = None) -> ConsoleOutput:
|
|
103
|
+
if not self.container:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
"Session is not open. Please call open() method before running code."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if libraries:
|
|
109
|
+
if self.lang == SupportedLanguage.GO:
|
|
110
|
+
self.execute_command("mkdir -p /example")
|
|
111
|
+
self.execute_command("go mod init example", workdir="/example")
|
|
112
|
+
self.execute_command("go mod tidy", workdir="/example")
|
|
113
|
+
|
|
114
|
+
for library in libraries:
|
|
115
|
+
install_command = get_libraries_installation_command(
|
|
116
|
+
self.lang, library
|
|
117
|
+
)
|
|
118
|
+
output = self.execute_command(install_command, workdir="/example")
|
|
119
|
+
if output.exit_code != 0:
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
f"Failed to install library {library}: {output}"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
for library in libraries:
|
|
125
|
+
install_command = get_libraries_installation_command(
|
|
126
|
+
self.lang, library
|
|
127
|
+
)
|
|
128
|
+
output = self.execute_command(install_command)
|
|
129
|
+
if output.exit_code != 0:
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
f"Failed to install library {library}: {output}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
code_file = f"/tmp/code.{get_code_file_extension(self.lang)}"
|
|
135
|
+
if self.lang == SupportedLanguage.GO:
|
|
136
|
+
code_dest_file = "/example/code.go"
|
|
137
|
+
else:
|
|
138
|
+
code_dest_file = code_file
|
|
139
|
+
|
|
140
|
+
with open(code_file, "w") as f:
|
|
141
|
+
f.write(code)
|
|
142
|
+
|
|
143
|
+
self.copy_to_runtime(code_file, code_dest_file)
|
|
144
|
+
commands = get_code_execution_command(self.lang, code_dest_file)
|
|
145
|
+
|
|
146
|
+
output = KubernetesConsoleOutput(0, "")
|
|
147
|
+
for command in commands:
|
|
148
|
+
if self.lang == SupportedLanguage.GO:
|
|
149
|
+
output = self.execute_command(command, workdir="/example")
|
|
150
|
+
else:
|
|
151
|
+
output = self.execute_command(command)
|
|
152
|
+
|
|
153
|
+
if output.exit_code != 0:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
return ConsoleOutput(output.text)
|
|
157
|
+
|
|
158
|
+
def copy_to_runtime(self, src: str, dest: str):
|
|
159
|
+
if not self.container:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
"Session is not open. Please call open() method before copying files."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
start_time = time.time()
|
|
165
|
+
if self.verbose:
|
|
166
|
+
print(f"Copying {src} to {self.container}:{dest}..")
|
|
167
|
+
|
|
168
|
+
dest_dir = os.path.dirname(dest)
|
|
169
|
+
dest_file = os.path.basename(dest)
|
|
170
|
+
|
|
171
|
+
if dest_dir:
|
|
172
|
+
self.execute_command(f"mkdir -p {dest_dir}")
|
|
173
|
+
|
|
174
|
+
with open(src, "rb") as f:
|
|
175
|
+
tarstream = io.BytesIO()
|
|
176
|
+
with tarfile.open(fileobj=tarstream, mode="w") as tar:
|
|
177
|
+
tarinfo = tarfile.TarInfo(name=dest_file)
|
|
178
|
+
tarinfo.size = os.path.getsize(src)
|
|
179
|
+
tar.addfile(tarinfo, f)
|
|
180
|
+
tarstream.seek(0)
|
|
181
|
+
|
|
182
|
+
exec_command = ["tar", "xvf", "-", "-C", dest_dir]
|
|
183
|
+
resp = stream(
|
|
184
|
+
self.client.connect_get_namespaced_pod_exec,
|
|
185
|
+
self.container,
|
|
186
|
+
self.kube_namespace,
|
|
187
|
+
command=exec_command,
|
|
188
|
+
stderr=True,
|
|
189
|
+
stdin=True,
|
|
190
|
+
stdout=True,
|
|
191
|
+
tty=False,
|
|
192
|
+
_preload_content=False,
|
|
193
|
+
)
|
|
194
|
+
while resp.is_open():
|
|
195
|
+
resp.update(timeout=1)
|
|
196
|
+
if resp.peek_stdout():
|
|
197
|
+
print(resp.read_stdout())
|
|
198
|
+
if resp.peek_stderr():
|
|
199
|
+
print(resp.read_stderr())
|
|
200
|
+
resp.write_stdin(tarstream.read(4096))
|
|
201
|
+
resp.close()
|
|
202
|
+
|
|
203
|
+
end_time = time.time()
|
|
204
|
+
if self.verbose:
|
|
205
|
+
print(
|
|
206
|
+
f"Copied {src} to {self.container}:{dest} in {end_time - start_time:.2f} seconds"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def copy_from_runtime(self, src: str, dest: str):
|
|
210
|
+
if not self.container:
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"Session is not open. Please call open() method before copying files."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if self.verbose:
|
|
216
|
+
print(f"Copying {self.container}:{src} to {dest}..")
|
|
217
|
+
|
|
218
|
+
exec_command = ["tar", "cf", "-", src]
|
|
219
|
+
resp = stream(
|
|
220
|
+
self.client.connect_get_namespaced_pod_exec,
|
|
221
|
+
self.container,
|
|
222
|
+
self.kube_namespace,
|
|
223
|
+
command=exec_command,
|
|
224
|
+
stderr=True,
|
|
225
|
+
stdin=False,
|
|
226
|
+
stdout=True,
|
|
227
|
+
tty=False,
|
|
228
|
+
_preload_content=False,
|
|
229
|
+
)
|
|
230
|
+
with open(dest, "wb") as f:
|
|
231
|
+
while resp.is_open():
|
|
232
|
+
resp.update(timeout=1)
|
|
233
|
+
if resp.peek_stdout():
|
|
234
|
+
f.write(resp.read_stdout())
|
|
235
|
+
if resp.peek_stderr():
|
|
236
|
+
print(resp.read_stderr())
|
|
237
|
+
|
|
238
|
+
def execute_command(
|
|
239
|
+
self, command: str, workdir: Optional[str] = None
|
|
240
|
+
) -> KubernetesConsoleOutput:
|
|
241
|
+
if not self.container:
|
|
242
|
+
raise RuntimeError(
|
|
243
|
+
"Session is not open. Please call open() method before executing commands."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if self.verbose:
|
|
247
|
+
print(f"Executing command: {command}")
|
|
248
|
+
|
|
249
|
+
if workdir:
|
|
250
|
+
exec_command = ["sh", "-c", f"cd {workdir} && {command}"]
|
|
251
|
+
else:
|
|
252
|
+
exec_command = ["/bin/sh", "-c", command]
|
|
253
|
+
|
|
254
|
+
resp = stream(
|
|
255
|
+
self.client.connect_get_namespaced_pod_exec,
|
|
256
|
+
self.container,
|
|
257
|
+
self.kube_namespace,
|
|
258
|
+
command=exec_command,
|
|
259
|
+
stderr=True,
|
|
260
|
+
stdin=False,
|
|
261
|
+
stdout=True,
|
|
262
|
+
tty=False,
|
|
263
|
+
_preload_content=False,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
output = ""
|
|
267
|
+
if self.verbose:
|
|
268
|
+
print("Output:", end=" ")
|
|
269
|
+
|
|
270
|
+
while resp.is_open():
|
|
271
|
+
resp.update(timeout=1)
|
|
272
|
+
if resp.peek_stdout():
|
|
273
|
+
chunk = resp.read_stdout()
|
|
274
|
+
output += chunk
|
|
275
|
+
if self.verbose:
|
|
276
|
+
print(chunk, end="")
|
|
277
|
+
if resp.peek_stderr():
|
|
278
|
+
chunk = resp.read_stderr()
|
|
279
|
+
output += chunk
|
|
280
|
+
if self.verbose:
|
|
281
|
+
print(chunk, end="")
|
|
282
|
+
|
|
283
|
+
exit_code = resp.returncode
|
|
284
|
+
return KubernetesConsoleOutput(exit_code, output)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from llm_sandbox.docker import SandboxDockerSession
|
|
2
|
+
from llm_sandbox.kubernetes import SandboxKubernetesSession
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SandboxSession:
|
|
6
|
+
def __new__(cls, use_kubernetes: bool = False, *args, **kwargs):
|
|
7
|
+
if use_kubernetes:
|
|
8
|
+
return SandboxKubernetesSession(*args, **kwargs)
|
|
9
|
+
|
|
10
|
+
return SandboxDockerSession(*args, **kwargs)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import docker
|
|
2
|
+
import docker.errors
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from docker import DockerClient
|
|
6
|
+
from llm_sandbox.const import SupportedLanguage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def image_exists(client: DockerClient, image: str) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if a Docker image exists
|
|
12
|
+
:param client: Docker client
|
|
13
|
+
:param image: Docker image
|
|
14
|
+
:return: True if the image exists, False otherwise
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
client.images.get(image)
|
|
18
|
+
return True
|
|
19
|
+
except docker.errors.ImageNotFound:
|
|
20
|
+
return False
|
|
21
|
+
except Exception as e:
|
|
22
|
+
raise e
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_libraries_installation_command(lang: str, library: str) -> Optional[str]:
|
|
26
|
+
"""
|
|
27
|
+
Get the command to install libraries for the given language
|
|
28
|
+
:param lang: Programming language
|
|
29
|
+
:param library: List of libraries
|
|
30
|
+
:return: Installation command
|
|
31
|
+
"""
|
|
32
|
+
if lang == SupportedLanguage.PYTHON:
|
|
33
|
+
return f"pip install {library}"
|
|
34
|
+
elif lang == SupportedLanguage.JAVA:
|
|
35
|
+
return f"mvn install:install-file -Dfile={library}"
|
|
36
|
+
elif lang == SupportedLanguage.JAVASCRIPT:
|
|
37
|
+
return f"yarn add {library}"
|
|
38
|
+
elif lang == SupportedLanguage.CPP:
|
|
39
|
+
return f"apt-get install {library}"
|
|
40
|
+
elif lang == SupportedLanguage.GO:
|
|
41
|
+
return f"go get -u {library}"
|
|
42
|
+
elif lang == SupportedLanguage.RUBY:
|
|
43
|
+
return f"gem install {library}"
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Language {lang} is not supported")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_code_file_extension(lang: str) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Get the file extension for the given language
|
|
51
|
+
:param lang: Programming language
|
|
52
|
+
:return: File extension
|
|
53
|
+
"""
|
|
54
|
+
if lang == SupportedLanguage.PYTHON:
|
|
55
|
+
return "py"
|
|
56
|
+
elif lang == SupportedLanguage.JAVA:
|
|
57
|
+
return "java"
|
|
58
|
+
elif lang == SupportedLanguage.JAVASCRIPT:
|
|
59
|
+
return "js"
|
|
60
|
+
elif lang == SupportedLanguage.CPP:
|
|
61
|
+
return "cpp"
|
|
62
|
+
elif lang == SupportedLanguage.GO:
|
|
63
|
+
return "go"
|
|
64
|
+
elif lang == SupportedLanguage.RUBY:
|
|
65
|
+
return "rb"
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError(f"Language {lang} is not supported")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_code_execution_command(lang: str, code_file: str) -> list:
|
|
71
|
+
"""
|
|
72
|
+
Return the execution command for the given language and code file.
|
|
73
|
+
:param lang: Language of the code
|
|
74
|
+
:param code_file: Path to the code file
|
|
75
|
+
:return: List of execution commands
|
|
76
|
+
"""
|
|
77
|
+
if lang == SupportedLanguage.PYTHON:
|
|
78
|
+
return [f"python {code_file}"]
|
|
79
|
+
elif lang == SupportedLanguage.JAVA:
|
|
80
|
+
return [f"java {code_file}"]
|
|
81
|
+
elif lang == SupportedLanguage.JAVASCRIPT:
|
|
82
|
+
return [f"node {code_file}"]
|
|
83
|
+
elif lang == SupportedLanguage.CPP:
|
|
84
|
+
return [f"g++ -o a.out {code_file}", "./a.out"]
|
|
85
|
+
elif lang == SupportedLanguage.GO:
|
|
86
|
+
return [f"go run {code_file}"]
|
|
87
|
+
elif lang == SupportedLanguage.RUBY:
|
|
88
|
+
return [f"ruby {code_file}"]
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError(f"Language {lang} is not supported")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "llm-sandbox"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Lightweight and portable LLM sandbox runtime (code interpreter) Python library"
|
|
5
|
+
authors = ["Duy Huynh <vndee.huynh@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
homepage = "https://github.com/vndee/llm-sandbox"
|
|
9
|
+
repository = "https://github.com/vndee/llm-sandbox"
|
|
10
|
+
packages = [
|
|
11
|
+
{ include = "llm_sandbox" }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[tool.poetry.dependencies]
|
|
15
|
+
python = "^3.11"
|
|
16
|
+
docker = "^7.1.0"
|
|
17
|
+
kubernetes = "^30.1.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
[tool.poetry.group.dev.dependencies]
|
|
21
|
+
pytest = "^8.2.2"
|
|
22
|
+
pre-commit = "^3.7.1"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|