agentscope-runtime 0.1.0__py3-none-any.whl
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.
- agentscope_runtime/__init__.py +4 -0
- agentscope_runtime/engine/__init__.py +9 -0
- agentscope_runtime/engine/agents/__init__.py +2 -0
- agentscope_runtime/engine/agents/agentscope_agent/__init__.py +6 -0
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +342 -0
- agentscope_runtime/engine/agents/agentscope_agent/hooks.py +156 -0
- agentscope_runtime/engine/agents/agno_agent.py +220 -0
- agentscope_runtime/engine/agents/base_agent.py +29 -0
- agentscope_runtime/engine/agents/langgraph_agent.py +59 -0
- agentscope_runtime/engine/agents/llm_agent.py +51 -0
- agentscope_runtime/engine/deployers/__init__.py +3 -0
- agentscope_runtime/engine/deployers/adapter/__init__.py +0 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +2 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +425 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_agent_adapter.py +69 -0
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +60 -0
- agentscope_runtime/engine/deployers/adapter/protocol_adapter.py +24 -0
- agentscope_runtime/engine/deployers/base.py +17 -0
- agentscope_runtime/engine/deployers/local_deployer.py +586 -0
- agentscope_runtime/engine/helpers/helper.py +127 -0
- agentscope_runtime/engine/llms/__init__.py +3 -0
- agentscope_runtime/engine/llms/base_llm.py +60 -0
- agentscope_runtime/engine/llms/qwen_llm.py +47 -0
- agentscope_runtime/engine/misc/__init__.py +0 -0
- agentscope_runtime/engine/runner.py +186 -0
- agentscope_runtime/engine/schemas/__init__.py +0 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +551 -0
- agentscope_runtime/engine/schemas/context.py +54 -0
- agentscope_runtime/engine/services/__init__.py +9 -0
- agentscope_runtime/engine/services/base.py +77 -0
- agentscope_runtime/engine/services/context_manager.py +129 -0
- agentscope_runtime/engine/services/environment_manager.py +50 -0
- agentscope_runtime/engine/services/manager.py +174 -0
- agentscope_runtime/engine/services/memory_service.py +270 -0
- agentscope_runtime/engine/services/sandbox_service.py +198 -0
- agentscope_runtime/engine/services/session_history_service.py +256 -0
- agentscope_runtime/engine/tracing/__init__.py +40 -0
- agentscope_runtime/engine/tracing/base.py +309 -0
- agentscope_runtime/engine/tracing/local_logging_handler.py +356 -0
- agentscope_runtime/engine/tracing/tracing_metric.py +69 -0
- agentscope_runtime/engine/tracing/wrapper.py +321 -0
- agentscope_runtime/sandbox/__init__.py +14 -0
- agentscope_runtime/sandbox/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/base/__init__.py +0 -0
- agentscope_runtime/sandbox/box/base/base_sandbox.py +37 -0
- agentscope_runtime/sandbox/box/base/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +176 -0
- agentscope_runtime/sandbox/box/dummy/__init__.py +0 -0
- agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +26 -0
- agentscope_runtime/sandbox/box/filesystem/__init__.py +0 -0
- agentscope_runtime/sandbox/box/filesystem/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +87 -0
- agentscope_runtime/sandbox/box/sandbox.py +115 -0
- agentscope_runtime/sandbox/box/shared/__init__.py +0 -0
- agentscope_runtime/sandbox/box/shared/app.py +44 -0
- agentscope_runtime/sandbox/box/shared/dependencies/__init__.py +5 -0
- agentscope_runtime/sandbox/box/shared/dependencies/deps.py +22 -0
- agentscope_runtime/sandbox/box/shared/routers/__init__.py +12 -0
- agentscope_runtime/sandbox/box/shared/routers/generic.py +173 -0
- agentscope_runtime/sandbox/box/shared/routers/mcp.py +207 -0
- agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +153 -0
- agentscope_runtime/sandbox/box/shared/routers/runtime_watcher.py +187 -0
- agentscope_runtime/sandbox/box/shared/routers/workspace.py +325 -0
- agentscope_runtime/sandbox/box/training_box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/training_box/base.py +120 -0
- agentscope_runtime/sandbox/box/training_box/env_service.py +752 -0
- agentscope_runtime/sandbox/box/training_box/environments/__init__.py +0 -0
- agentscope_runtime/sandbox/box/training_box/environments/appworld/appworld_env.py +987 -0
- agentscope_runtime/sandbox/box/training_box/registry.py +54 -0
- agentscope_runtime/sandbox/box/training_box/src/trajectory.py +278 -0
- agentscope_runtime/sandbox/box/training_box/training_box.py +219 -0
- agentscope_runtime/sandbox/build.py +213 -0
- agentscope_runtime/sandbox/client/__init__.py +5 -0
- agentscope_runtime/sandbox/client/http_client.py +527 -0
- agentscope_runtime/sandbox/client/training_client.py +265 -0
- agentscope_runtime/sandbox/constant.py +5 -0
- agentscope_runtime/sandbox/custom/__init__.py +16 -0
- agentscope_runtime/sandbox/custom/custom_sandbox.py +40 -0
- agentscope_runtime/sandbox/custom/example.py +37 -0
- agentscope_runtime/sandbox/enums.py +68 -0
- agentscope_runtime/sandbox/manager/__init__.py +4 -0
- agentscope_runtime/sandbox/manager/collections/__init__.py +22 -0
- agentscope_runtime/sandbox/manager/collections/base_mapping.py +20 -0
- agentscope_runtime/sandbox/manager/collections/base_queue.py +25 -0
- agentscope_runtime/sandbox/manager/collections/base_set.py +25 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +22 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_queue.py +28 -0
- agentscope_runtime/sandbox/manager/collections/in_memory_set.py +27 -0
- agentscope_runtime/sandbox/manager/collections/redis_mapping.py +26 -0
- agentscope_runtime/sandbox/manager/collections/redis_queue.py +27 -0
- agentscope_runtime/sandbox/manager/collections/redis_set.py +23 -0
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +8 -0
- agentscope_runtime/sandbox/manager/container_clients/base_client.py +39 -0
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +170 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +694 -0
- agentscope_runtime/sandbox/manager/server/__init__.py +0 -0
- agentscope_runtime/sandbox/manager/server/app.py +194 -0
- agentscope_runtime/sandbox/manager/server/config.py +68 -0
- agentscope_runtime/sandbox/manager/server/models.py +17 -0
- agentscope_runtime/sandbox/manager/storage/__init__.py +10 -0
- agentscope_runtime/sandbox/manager/storage/data_storage.py +16 -0
- agentscope_runtime/sandbox/manager/storage/local_storage.py +44 -0
- agentscope_runtime/sandbox/manager/storage/oss_storage.py +89 -0
- agentscope_runtime/sandbox/manager/utils.py +78 -0
- agentscope_runtime/sandbox/mcp_server.py +192 -0
- agentscope_runtime/sandbox/model/__init__.py +12 -0
- agentscope_runtime/sandbox/model/api.py +16 -0
- agentscope_runtime/sandbox/model/container.py +72 -0
- agentscope_runtime/sandbox/model/manager_config.py +158 -0
- agentscope_runtime/sandbox/registry.py +129 -0
- agentscope_runtime/sandbox/tools/__init__.py +12 -0
- agentscope_runtime/sandbox/tools/base/__init__.py +8 -0
- agentscope_runtime/sandbox/tools/base/tool.py +52 -0
- agentscope_runtime/sandbox/tools/browser/__init__.py +57 -0
- agentscope_runtime/sandbox/tools/browser/tool.py +597 -0
- agentscope_runtime/sandbox/tools/filesystem/__init__.py +32 -0
- agentscope_runtime/sandbox/tools/filesystem/tool.py +319 -0
- agentscope_runtime/sandbox/tools/function_tool.py +321 -0
- agentscope_runtime/sandbox/tools/mcp_tool.py +191 -0
- agentscope_runtime/sandbox/tools/sandbox_tool.py +104 -0
- agentscope_runtime/sandbox/tools/tool.py +123 -0
- agentscope_runtime/sandbox/tools/utils.py +68 -0
- agentscope_runtime/version.py +2 -0
- agentscope_runtime-0.1.0.dist-info/METADATA +327 -0
- agentscope_runtime-0.1.0.dist-info/RECORD +131 -0
- agentscope_runtime-0.1.0.dist-info/WHEEL +5 -0
- agentscope_runtime-0.1.0.dist-info/entry_points.txt +4 -0
- agentscope_runtime-0.1.0.dist-info/licenses/LICENSE +202 -0
- agentscope_runtime-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# pylint: disable=redefined-outer-name, protected-access, too-many-branches
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import inspect
|
|
7
|
+
import socket
|
|
8
|
+
import traceback
|
|
9
|
+
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import Optional, Dict
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from ..model import (
|
|
17
|
+
ContainerModel,
|
|
18
|
+
SandboxManagerEnvConfig,
|
|
19
|
+
DEFAULT_LOCAL_MANAGER_CONFIG,
|
|
20
|
+
)
|
|
21
|
+
from ..enums import SandboxType
|
|
22
|
+
from ..registry import SandboxRegistry
|
|
23
|
+
from ..client import SandboxHttpClient, TrainingSandboxClient
|
|
24
|
+
|
|
25
|
+
from ..manager.utils import is_port_available, sweep_port
|
|
26
|
+
from ..manager.collections import (
|
|
27
|
+
RedisMapping,
|
|
28
|
+
RedisSetCollection,
|
|
29
|
+
RedisQueue,
|
|
30
|
+
InMemoryMapping,
|
|
31
|
+
InMemoryQueue,
|
|
32
|
+
InMemorySetCollection,
|
|
33
|
+
)
|
|
34
|
+
from ..manager.storage import (
|
|
35
|
+
LocalStorage,
|
|
36
|
+
OSSStorage,
|
|
37
|
+
)
|
|
38
|
+
from ..manager.container_clients import DockerClient
|
|
39
|
+
from ..constant import BROWSER_SESSION_ID
|
|
40
|
+
|
|
41
|
+
logging.basicConfig(level=logging.INFO)
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def remote_wrapper(
|
|
46
|
+
method: str = "POST",
|
|
47
|
+
success_key: str = "data",
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Decorator to handle both remote and local method execution.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def decorator(func):
|
|
54
|
+
@wraps(func)
|
|
55
|
+
def wrapper(self, *args, **kwargs):
|
|
56
|
+
if not self.http_session:
|
|
57
|
+
# Execute the original function locally
|
|
58
|
+
return func(self, *args, **kwargs)
|
|
59
|
+
|
|
60
|
+
endpoint = "/" + func.__name__
|
|
61
|
+
|
|
62
|
+
# Prepare data for remote call
|
|
63
|
+
sig = inspect.signature(func)
|
|
64
|
+
param_names = list(sig.parameters.keys())[1:] # Skip 'self'
|
|
65
|
+
data = dict(zip(param_names, args))
|
|
66
|
+
data.update(kwargs)
|
|
67
|
+
|
|
68
|
+
# Make the remote HTTP request
|
|
69
|
+
response = self._make_request(method, endpoint, data)
|
|
70
|
+
|
|
71
|
+
# Process response
|
|
72
|
+
if success_key:
|
|
73
|
+
return response.get(success_key)
|
|
74
|
+
return response
|
|
75
|
+
|
|
76
|
+
wrapper._is_remote_wrapper = True
|
|
77
|
+
wrapper._http_method = method
|
|
78
|
+
wrapper._path = "/" + func.__name__
|
|
79
|
+
|
|
80
|
+
return wrapper
|
|
81
|
+
|
|
82
|
+
return decorator
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class SandboxManager:
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
config: SandboxManagerEnvConfig = DEFAULT_LOCAL_MANAGER_CONFIG,
|
|
89
|
+
base_url=None,
|
|
90
|
+
bearer_token=None,
|
|
91
|
+
default_type: SandboxType | str = SandboxType.BASE,
|
|
92
|
+
):
|
|
93
|
+
if base_url:
|
|
94
|
+
# Initialize HTTP session for remote mode with bearer token
|
|
95
|
+
# authentication
|
|
96
|
+
self.http_session = requests.Session()
|
|
97
|
+
self.http_session.timeout = 30
|
|
98
|
+
self.base_url = base_url.rstrip("/")
|
|
99
|
+
if bearer_token:
|
|
100
|
+
self.http_session.headers.update(
|
|
101
|
+
{"Authorization": f"Bearer {bearer_token}"},
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
self.http_session = None
|
|
105
|
+
self.base_url = None
|
|
106
|
+
|
|
107
|
+
self.default_type = SandboxType(default_type)
|
|
108
|
+
self.workdir = "/workspace"
|
|
109
|
+
|
|
110
|
+
self.config = config
|
|
111
|
+
self.pool_size = self.config.pool_size
|
|
112
|
+
self.prefix = self.config.container_prefix_key
|
|
113
|
+
self.default_mount_dir = (
|
|
114
|
+
self.config.default_mount_dir or "sessions_mount_dir"
|
|
115
|
+
)
|
|
116
|
+
self.storage_folder = (
|
|
117
|
+
self.config.storage_folder or self.default_mount_dir
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if self.config.redis_enabled:
|
|
121
|
+
import redis
|
|
122
|
+
|
|
123
|
+
redis_client = redis.Redis(
|
|
124
|
+
host=self.config.redis_server,
|
|
125
|
+
port=self.config.redis_port,
|
|
126
|
+
db=self.config.redis_db,
|
|
127
|
+
username=self.config.redis_user,
|
|
128
|
+
password=self.config.redis_password,
|
|
129
|
+
decode_responses=True,
|
|
130
|
+
)
|
|
131
|
+
try:
|
|
132
|
+
redis_client.ping()
|
|
133
|
+
except ConnectionError as e:
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
"Unable to connect to the Redis server.",
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
self.container_mapping = RedisMapping(redis_client)
|
|
139
|
+
self.port_set = RedisSetCollection(
|
|
140
|
+
redis_client,
|
|
141
|
+
set_name=self.config.redis_port_key,
|
|
142
|
+
)
|
|
143
|
+
self.pool_queue = RedisQueue(
|
|
144
|
+
redis_client,
|
|
145
|
+
self.config.redis_container_pool_key,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
self.container_mapping = InMemoryMapping()
|
|
149
|
+
self.port_set = InMemorySetCollection()
|
|
150
|
+
self.pool_queue = InMemoryQueue()
|
|
151
|
+
|
|
152
|
+
self.container_deployment = self.config.container_deployment
|
|
153
|
+
|
|
154
|
+
if base_url is None:
|
|
155
|
+
if self.container_deployment == "docker":
|
|
156
|
+
self.client = DockerClient()
|
|
157
|
+
else:
|
|
158
|
+
# TODO: support k8s deployment
|
|
159
|
+
raise NotImplementedError("Not implemented")
|
|
160
|
+
else:
|
|
161
|
+
self.client = None
|
|
162
|
+
|
|
163
|
+
self.port_range = range(*self.config.port_range)
|
|
164
|
+
|
|
165
|
+
self.file_system = self.config.file_system
|
|
166
|
+
if self.file_system == "oss":
|
|
167
|
+
self.storage = OSSStorage(
|
|
168
|
+
self.config.oss_access_key_id,
|
|
169
|
+
self.config.oss_access_key_secret,
|
|
170
|
+
self.config.oss_endpoint,
|
|
171
|
+
self.config.oss_bucket_name,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
self.storage = LocalStorage()
|
|
175
|
+
|
|
176
|
+
if self.pool_size > 0:
|
|
177
|
+
self._init_container_pool()
|
|
178
|
+
|
|
179
|
+
logger.debug(str(config))
|
|
180
|
+
|
|
181
|
+
def __enter__(self):
|
|
182
|
+
logger.debug(
|
|
183
|
+
"Entering SandboxManager context (sync). "
|
|
184
|
+
"Cleanup will be performed automatically on exit.",
|
|
185
|
+
)
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
189
|
+
logger.debug(
|
|
190
|
+
"Exiting SandboxManager context (sync). Cleaning up resources.",
|
|
191
|
+
)
|
|
192
|
+
self.cleanup()
|
|
193
|
+
|
|
194
|
+
def _generate_container_key(self, session_id):
|
|
195
|
+
return f"{self.prefix}{session_id}"
|
|
196
|
+
|
|
197
|
+
def _make_request(self, method: str, endpoint: str, data: dict):
|
|
198
|
+
"""
|
|
199
|
+
Make an HTTP request to the specified endpoint.
|
|
200
|
+
"""
|
|
201
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
202
|
+
if method.upper() == "GET":
|
|
203
|
+
response = self.http_session.get(url, params=data)
|
|
204
|
+
else:
|
|
205
|
+
response = self.http_session.request(method, url, json=data)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
response.raise_for_status()
|
|
209
|
+
except requests.exceptions.HTTPError as e:
|
|
210
|
+
logger.error(f"Error making request: {e}")
|
|
211
|
+
return {"data": f"Error: {e}"}
|
|
212
|
+
|
|
213
|
+
return response.json()
|
|
214
|
+
|
|
215
|
+
def _init_container_pool(self):
|
|
216
|
+
"""
|
|
217
|
+
Init runtime pool
|
|
218
|
+
"""
|
|
219
|
+
while self.pool_queue.size() < self.pool_size:
|
|
220
|
+
try:
|
|
221
|
+
container_name = self.create()
|
|
222
|
+
container_model = self.container_mapping.get(container_name)
|
|
223
|
+
if container_model:
|
|
224
|
+
# Check the pool size again to avoid race condition
|
|
225
|
+
if self.pool_queue.size() < self.pool_size:
|
|
226
|
+
self.pool_queue.enqueue(container_model)
|
|
227
|
+
else:
|
|
228
|
+
# The pool size has reached the limit
|
|
229
|
+
self.release(container_name)
|
|
230
|
+
break
|
|
231
|
+
else:
|
|
232
|
+
logger.error("Failed to create container for pool")
|
|
233
|
+
break
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(f"Error initializing runtime pool: {e}")
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
def _find_free_ports(self, n):
|
|
239
|
+
free_ports = []
|
|
240
|
+
|
|
241
|
+
for port in self.port_range:
|
|
242
|
+
if len(free_ports) >= n:
|
|
243
|
+
break # We have found enough ports
|
|
244
|
+
|
|
245
|
+
if not self.port_set.add(port):
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
249
|
+
try:
|
|
250
|
+
s.bind(("", port))
|
|
251
|
+
free_ports.append(port) # Port is available
|
|
252
|
+
|
|
253
|
+
except OSError:
|
|
254
|
+
# Bind failed, port is in use
|
|
255
|
+
self.port_set.remove(port)
|
|
256
|
+
# Try the next one
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
if len(free_ports) < n:
|
|
260
|
+
raise RuntimeError(
|
|
261
|
+
"Not enough free ports available in the specified range.",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return free_ports
|
|
265
|
+
|
|
266
|
+
@remote_wrapper()
|
|
267
|
+
def cleanup(self):
|
|
268
|
+
logger.debug(
|
|
269
|
+
"Cleaning up resources.",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Clean up pool first
|
|
273
|
+
try:
|
|
274
|
+
while self.pool_queue.size() > 0:
|
|
275
|
+
container_json = self.pool_queue.dequeue()
|
|
276
|
+
if container_json:
|
|
277
|
+
container_model = ContainerModel(**container_json)
|
|
278
|
+
logger.debug(
|
|
279
|
+
f"Destroy container {container_model.container_id}",
|
|
280
|
+
)
|
|
281
|
+
self.release(container_model.session_id)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.error(f"Error cleaning up runtime pool: {e}")
|
|
284
|
+
|
|
285
|
+
# Clean up rest container
|
|
286
|
+
for key in self.container_mapping.scan(self.prefix):
|
|
287
|
+
try:
|
|
288
|
+
container_json = self.container_mapping.get(key)
|
|
289
|
+
if container_json:
|
|
290
|
+
container_model = ContainerModel(**container_json)
|
|
291
|
+
logger.debug(
|
|
292
|
+
f"Destroy container {container_model.container_id}",
|
|
293
|
+
)
|
|
294
|
+
self.release(container_model.session_id)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(
|
|
297
|
+
f"Error cleaning up container {key}: {e}",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@remote_wrapper()
|
|
301
|
+
def create_from_pool(self, sandbox_type=None):
|
|
302
|
+
"""Try to get a container from runtime pool"""
|
|
303
|
+
sandbox_type = SandboxType(sandbox_type)
|
|
304
|
+
if sandbox_type != self.default_type:
|
|
305
|
+
return self.create(sandbox_type=sandbox_type.value)
|
|
306
|
+
|
|
307
|
+
cnt = 0
|
|
308
|
+
try:
|
|
309
|
+
while True:
|
|
310
|
+
if cnt > self.pool_size:
|
|
311
|
+
raise RuntimeError(
|
|
312
|
+
"No container available in pool after check the pool.",
|
|
313
|
+
)
|
|
314
|
+
cnt += 1
|
|
315
|
+
|
|
316
|
+
# Add a new one to container
|
|
317
|
+
container_name = self.create()
|
|
318
|
+
new_container_model = self.container_mapping.get(
|
|
319
|
+
container_name,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if new_container_model:
|
|
323
|
+
self.pool_queue.enqueue(
|
|
324
|
+
new_container_model,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
container_json = self.pool_queue.dequeue()
|
|
328
|
+
|
|
329
|
+
if not container_json:
|
|
330
|
+
raise RuntimeError(
|
|
331
|
+
"No container available in pool.",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
container_model = ContainerModel(**container_json)
|
|
335
|
+
logger.debug(
|
|
336
|
+
f"Retrieved container from pool:"
|
|
337
|
+
f" {container_model.session_id}",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
container_model.version
|
|
342
|
+
!= SandboxRegistry.get_image_by_type(
|
|
343
|
+
self.default_type,
|
|
344
|
+
)
|
|
345
|
+
):
|
|
346
|
+
logger.warning(
|
|
347
|
+
f"Container {container_model.session_id} outdated, "
|
|
348
|
+
f"trying next one in pool",
|
|
349
|
+
)
|
|
350
|
+
self.release(container_model.session_id)
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
if self.client.inspect(container_model.container_id) is None:
|
|
354
|
+
logger.warning(
|
|
355
|
+
f"Container {container_model.container_id} not found "
|
|
356
|
+
f"or unexpected error happens.",
|
|
357
|
+
)
|
|
358
|
+
continue
|
|
359
|
+
|
|
360
|
+
if (
|
|
361
|
+
self.client.get_status(container_model.container_id)
|
|
362
|
+
== "running"
|
|
363
|
+
):
|
|
364
|
+
return container_model.container_name
|
|
365
|
+
else:
|
|
366
|
+
logger.error(
|
|
367
|
+
f"Container {container_model.container_id} is not "
|
|
368
|
+
f"running. Trying next one in pool.",
|
|
369
|
+
)
|
|
370
|
+
# Destroy the stopped container
|
|
371
|
+
self.release(container_model.session_id)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(
|
|
375
|
+
f"Error getting container from pool, create a "
|
|
376
|
+
f"new one. {e}: {traceback.format_exc()}",
|
|
377
|
+
)
|
|
378
|
+
return self.create()
|
|
379
|
+
|
|
380
|
+
@remote_wrapper()
|
|
381
|
+
def create(
|
|
382
|
+
self,
|
|
383
|
+
sandbox_type=None,
|
|
384
|
+
mount_dir=None,
|
|
385
|
+
storage_path=None,
|
|
386
|
+
environment: Optional[Dict] = None,
|
|
387
|
+
):
|
|
388
|
+
if sandbox_type is not None:
|
|
389
|
+
target_sandbox_type = SandboxType(sandbox_type)
|
|
390
|
+
else:
|
|
391
|
+
target_sandbox_type = self.default_type
|
|
392
|
+
|
|
393
|
+
image = SandboxRegistry.get_image_by_type(target_sandbox_type)
|
|
394
|
+
if not image:
|
|
395
|
+
logger.warning(
|
|
396
|
+
f"No image found for sandbox {target_sandbox_type}, "
|
|
397
|
+
f"using default",
|
|
398
|
+
)
|
|
399
|
+
image = SandboxRegistry.get_image_by_type(
|
|
400
|
+
self.default_type,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# TODO: enable for timeout for the sandbox (auto cleanup)
|
|
404
|
+
config = SandboxRegistry.get_config_by_type(target_sandbox_type)
|
|
405
|
+
environment = {
|
|
406
|
+
**(config.environment if config.environment else {}),
|
|
407
|
+
**(environment if environment else {}),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for key, value in environment.items():
|
|
411
|
+
if value is None:
|
|
412
|
+
logger.error(
|
|
413
|
+
f"Env variable {key} is None.",
|
|
414
|
+
)
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
session_id = str(uuid4())
|
|
418
|
+
|
|
419
|
+
if mount_dir is None:
|
|
420
|
+
mount_dir = os.path.join(self.default_mount_dir, session_id)
|
|
421
|
+
os.makedirs(mount_dir, exist_ok=True)
|
|
422
|
+
|
|
423
|
+
if not os.path.isabs(mount_dir):
|
|
424
|
+
mount_dir = os.path.abspath(mount_dir)
|
|
425
|
+
|
|
426
|
+
if storage_path is None:
|
|
427
|
+
storage_path = self.storage.path_join(
|
|
428
|
+
self.storage_folder,
|
|
429
|
+
session_id,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
self.storage.download_folder(storage_path, mount_dir)
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
# Check for an existing container with the same name
|
|
436
|
+
container_name = self._generate_container_key(session_id)
|
|
437
|
+
if self.client.inspect(container_name):
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"Container with name {container_name} already exists.",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
free_ports = self._find_free_ports(1)
|
|
443
|
+
|
|
444
|
+
ports = {
|
|
445
|
+
"80/tcp": free_ports[0], # nginx
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
# Generate a random secret token
|
|
449
|
+
runtime_token = secrets.token_hex(16)
|
|
450
|
+
|
|
451
|
+
# Prepare volume bindings if a mount directory is provided
|
|
452
|
+
volume_bindings = {
|
|
453
|
+
mount_dir: {
|
|
454
|
+
"bind": self.workdir,
|
|
455
|
+
"mode": "rw",
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if not self.client.create(
|
|
460
|
+
image,
|
|
461
|
+
name=container_name,
|
|
462
|
+
ports=ports,
|
|
463
|
+
volumes=volume_bindings,
|
|
464
|
+
environment={
|
|
465
|
+
"SECRET_TOKEN": runtime_token,
|
|
466
|
+
**environment,
|
|
467
|
+
},
|
|
468
|
+
runtime_config=config.runtime_config,
|
|
469
|
+
):
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
# Check the container status
|
|
473
|
+
status = self.client.get_status(container_name)
|
|
474
|
+
if self.client.get_status(container_name) != "running":
|
|
475
|
+
logger.warning(
|
|
476
|
+
f"Container {container_name} is not running. Current "
|
|
477
|
+
f"status: {status}",
|
|
478
|
+
)
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
# Build the ContainerModel
|
|
482
|
+
container_attrs = self.client.inspect(container_name)
|
|
483
|
+
|
|
484
|
+
container_model = ContainerModel(
|
|
485
|
+
session_id=session_id,
|
|
486
|
+
container_id=container_attrs["Id"], # Docker id pattern
|
|
487
|
+
container_name=container_name,
|
|
488
|
+
base_url=f"http://localhost:{ports['80/tcp']}/fastapi",
|
|
489
|
+
browser_url=f"http://localhost:{ports['80/tcp']}/steel-api"
|
|
490
|
+
f"/{runtime_token}",
|
|
491
|
+
front_browser_ws=f"ws://localhost:"
|
|
492
|
+
f"{ports['80/tcp']}/steel-api/"
|
|
493
|
+
f"{runtime_token}/v1/sessions/cast",
|
|
494
|
+
client_browser_ws=f"ws://localhost:"
|
|
495
|
+
f"{ports['80/tcp']}/steel-api/{runtime_token}/&sessionId"
|
|
496
|
+
f"={BROWSER_SESSION_ID}",
|
|
497
|
+
artifacts_sio=f"http://localhost" f":{ports['80/tcp']}/v1",
|
|
498
|
+
ports=free_ports,
|
|
499
|
+
mount_dir=str(mount_dir),
|
|
500
|
+
storage_path=storage_path,
|
|
501
|
+
runtime_token=runtime_token,
|
|
502
|
+
version=image,
|
|
503
|
+
)
|
|
504
|
+
# Register in mapping
|
|
505
|
+
self.container_mapping.set(
|
|
506
|
+
container_model.container_name,
|
|
507
|
+
container_model.model_dump(),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
logger.debug(
|
|
511
|
+
f"Created container {container_name}"
|
|
512
|
+
f":{container_model.model_dump()}",
|
|
513
|
+
)
|
|
514
|
+
return container_name
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.error(
|
|
517
|
+
f"Failed to create container: {e}: {traceback.format_exc()}",
|
|
518
|
+
)
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
@remote_wrapper()
|
|
522
|
+
def release(self, identity):
|
|
523
|
+
try:
|
|
524
|
+
container_json = self.get_info(identity)
|
|
525
|
+
|
|
526
|
+
if not container_json:
|
|
527
|
+
logger.warning(
|
|
528
|
+
f"No container found for {identity}.",
|
|
529
|
+
)
|
|
530
|
+
return True
|
|
531
|
+
|
|
532
|
+
container_info = ContainerModel(**container_json)
|
|
533
|
+
|
|
534
|
+
# remove key in mapping before we remove container
|
|
535
|
+
self.container_mapping.delete(container_json.get("container_name"))
|
|
536
|
+
|
|
537
|
+
self.client.stop(container_info.container_id, timeout=1)
|
|
538
|
+
self.client.remove(container_info.container_id, force=True)
|
|
539
|
+
|
|
540
|
+
# Release ports after the container is removed
|
|
541
|
+
for port in container_info.ports:
|
|
542
|
+
self.port_set.remove(port)
|
|
543
|
+
|
|
544
|
+
logger.debug(f"Container for {identity} destroyed.")
|
|
545
|
+
|
|
546
|
+
# Upload to storage
|
|
547
|
+
self.storage.upload_folder(
|
|
548
|
+
container_info.mount_dir,
|
|
549
|
+
container_info.storage_path,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return True
|
|
553
|
+
except Exception as e:
|
|
554
|
+
logger.error(
|
|
555
|
+
f"Failed to destroy container: {e}: "
|
|
556
|
+
f"{traceback.format_exc()}",
|
|
557
|
+
)
|
|
558
|
+
return False
|
|
559
|
+
|
|
560
|
+
@remote_wrapper()
|
|
561
|
+
def start(self, identity):
|
|
562
|
+
try:
|
|
563
|
+
container_json = self.get_info(identity)
|
|
564
|
+
|
|
565
|
+
if not container_json:
|
|
566
|
+
logger.warning(
|
|
567
|
+
f"No container found for {identity}.",
|
|
568
|
+
)
|
|
569
|
+
return False
|
|
570
|
+
|
|
571
|
+
container_info = ContainerModel(**container_json)
|
|
572
|
+
|
|
573
|
+
# Check whether the ports are occupied by other processes
|
|
574
|
+
for port in container_info.ports:
|
|
575
|
+
if is_port_available(port):
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
# If the port is occupied, sweep it
|
|
579
|
+
sweep_port(port)
|
|
580
|
+
|
|
581
|
+
self.client.start(container_info.container_id)
|
|
582
|
+
status = self.client.get_status(container_info.container_id)
|
|
583
|
+
if status != "running":
|
|
584
|
+
logger.error(
|
|
585
|
+
f"Failed to start container {identity}. "
|
|
586
|
+
f"Current status: {status}",
|
|
587
|
+
)
|
|
588
|
+
return False
|
|
589
|
+
|
|
590
|
+
logger.debug(f"Container {identity} started.")
|
|
591
|
+
return True
|
|
592
|
+
|
|
593
|
+
except Exception as e:
|
|
594
|
+
logger.error(
|
|
595
|
+
f"Failed to start container: {e}:"
|
|
596
|
+
f" {traceback.format_exc()}",
|
|
597
|
+
)
|
|
598
|
+
return False
|
|
599
|
+
|
|
600
|
+
@remote_wrapper()
|
|
601
|
+
def stop(self, identity):
|
|
602
|
+
try:
|
|
603
|
+
container_json = self.get_info(identity)
|
|
604
|
+
|
|
605
|
+
if not container_json:
|
|
606
|
+
logger.warning(f"No container found for {identity}.")
|
|
607
|
+
return True
|
|
608
|
+
|
|
609
|
+
container_info = ContainerModel(**container_json)
|
|
610
|
+
|
|
611
|
+
self.client.stop(container_info.container_id, timeout=1)
|
|
612
|
+
|
|
613
|
+
status = self.client.get_status(container_info.container_id)
|
|
614
|
+
if status != "exited":
|
|
615
|
+
logger.error(
|
|
616
|
+
f"Failed to stop container {identity}. "
|
|
617
|
+
f"Current status: {status}",
|
|
618
|
+
)
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
logger.debug(f"Container {identity} stopped.")
|
|
622
|
+
return True
|
|
623
|
+
|
|
624
|
+
except Exception as e:
|
|
625
|
+
logger.error(
|
|
626
|
+
f"Failed to stop container: {e}: {traceback.format_exc()}",
|
|
627
|
+
)
|
|
628
|
+
return False
|
|
629
|
+
|
|
630
|
+
@remote_wrapper()
|
|
631
|
+
def get_status(self, identity):
|
|
632
|
+
"""Get container status by container_name or container_id."""
|
|
633
|
+
return self.client.get_status(identity)
|
|
634
|
+
|
|
635
|
+
@remote_wrapper()
|
|
636
|
+
def get_info(self, identity):
|
|
637
|
+
"""Get container information by container_name or container_id."""
|
|
638
|
+
container_model = self.container_mapping.get(identity)
|
|
639
|
+
if container_model is None:
|
|
640
|
+
container_model = self.container_mapping.get(
|
|
641
|
+
self._generate_container_key(identity),
|
|
642
|
+
)
|
|
643
|
+
if container_model is None:
|
|
644
|
+
return None
|
|
645
|
+
if hasattr(container_model, "model_dump_json"):
|
|
646
|
+
container_model = container_model.model_dump_json()
|
|
647
|
+
|
|
648
|
+
return container_model
|
|
649
|
+
|
|
650
|
+
def _establish_connection(self, identity):
|
|
651
|
+
container_model = ContainerModel(**self.get_info(identity))
|
|
652
|
+
# TODO: make this more robust
|
|
653
|
+
enable_browser = "browser" in container_model.version
|
|
654
|
+
|
|
655
|
+
# TODO: remake docker name
|
|
656
|
+
if "appworld" in container_model.version:
|
|
657
|
+
return TrainingSandboxClient(
|
|
658
|
+
base_url=f"http://localhost:{container_model.ports[0]}",
|
|
659
|
+
).__enter__()
|
|
660
|
+
|
|
661
|
+
return SandboxHttpClient(
|
|
662
|
+
container_model,
|
|
663
|
+
enable_browser=enable_browser,
|
|
664
|
+
).__enter__()
|
|
665
|
+
|
|
666
|
+
@remote_wrapper()
|
|
667
|
+
def list_tools(self, identity, tool_type=None, **kwargs):
|
|
668
|
+
"""List tool"""
|
|
669
|
+
client = self._establish_connection(identity)
|
|
670
|
+
return client.list_tools(tool_type=tool_type, **kwargs)
|
|
671
|
+
|
|
672
|
+
@remote_wrapper()
|
|
673
|
+
def call_tool(self, identity, tool_name=None, arguments=None):
|
|
674
|
+
"""Call tool"""
|
|
675
|
+
client = self._establish_connection(identity)
|
|
676
|
+
return client.call_tool(tool_name, arguments)
|
|
677
|
+
|
|
678
|
+
@remote_wrapper()
|
|
679
|
+
def add_mcp_servers(self, identity, server_configs, overwrite=False):
|
|
680
|
+
"""
|
|
681
|
+
Add MCP servers to runtime.
|
|
682
|
+
"""
|
|
683
|
+
client = self._establish_connection(identity)
|
|
684
|
+
return client.add_mcp_servers(
|
|
685
|
+
server_configs=server_configs,
|
|
686
|
+
overwrite=overwrite,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
if __name__ == "__main__":
|
|
691
|
+
with SandboxManager() as manager:
|
|
692
|
+
name = manager.create("12345")
|
|
693
|
+
if name:
|
|
694
|
+
print(f"Created container: {name}")
|
|
File without changes
|