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.
@@ -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"