agentscope-runtime 1.0.2__py3-none-any.whl → 1.0.4__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/adapters/agentscope/stream.py +2 -9
- agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
- agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
- agentscope_runtime/adapters/utils.py +6 -0
- agentscope_runtime/cli/commands/deploy.py +383 -0
- agentscope_runtime/common/collections/redis_mapping.py +4 -1
- agentscope_runtime/common/container_clients/knative_client.py +466 -0
- agentscope_runtime/engine/__init__.py +4 -0
- agentscope_runtime/engine/app/agent_app.py +48 -5
- agentscope_runtime/engine/constant.py +1 -0
- agentscope_runtime/engine/deployers/__init__.py +12 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +31 -1
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +458 -41
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +76 -0
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +749 -0
- agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
- agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
- agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
- agentscope_runtime/engine/deployers/kubernetes_deployer.py +3 -0
- agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +8 -2
- agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +5 -0
- agentscope_runtime/engine/deployers/utils/net_utils.py +65 -0
- agentscope_runtime/engine/runner.py +17 -3
- agentscope_runtime/engine/schemas/exception.py +24 -0
- agentscope_runtime/engine/services/agent_state/redis_state_service.py +61 -8
- agentscope_runtime/engine/services/agent_state/state_service_factory.py +2 -5
- agentscope_runtime/engine/services/memory/redis_memory_service.py +129 -25
- agentscope_runtime/engine/services/session_history/redis_session_history_service.py +160 -34
- agentscope_runtime/engine/tracing/wrapper.py +18 -4
- agentscope_runtime/sandbox/__init__.py +14 -6
- agentscope_runtime/sandbox/box/base/__init__.py +2 -2
- agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
- agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
- agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
- agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
- agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
- agentscope_runtime/sandbox/box/sandbox.py +98 -65
- agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
- agentscope_runtime/sandbox/build.py +50 -57
- agentscope_runtime/sandbox/client/__init__.py +6 -1
- agentscope_runtime/sandbox/client/async_http_client.py +339 -0
- agentscope_runtime/sandbox/client/base.py +74 -0
- agentscope_runtime/sandbox/client/http_client.py +108 -329
- agentscope_runtime/sandbox/enums.py +7 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +264 -4
- agentscope_runtime/sandbox/manager/server/app.py +7 -1
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/METADATA +109 -29
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/RECORD +58 -46
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/WHEEL +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.2.dist-info → agentscope_runtime-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ import platform
|
|
|
5
5
|
from typing import Optional, List, Union
|
|
6
6
|
from urllib.parse import urlencode, urljoin
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from ..sandbox import Sandbox, SandboxAsync
|
|
9
9
|
|
|
10
10
|
from ...utils import build_image_uri
|
|
11
11
|
from ...registry import SandboxRegistry
|
|
@@ -15,6 +15,107 @@ from ...constant import TIMEOUT
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class HostPrerequisiteError(Exception):
|
|
19
|
+
"""Exception raised when host prerequisites
|
|
20
|
+
for MobileSandbox are not met."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check_host_readiness() -> None:
|
|
24
|
+
logger.info(
|
|
25
|
+
"Performing one-time host environment check for MobileSandbox...",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
architecture = platform.machine().lower()
|
|
29
|
+
if architecture in ("aarch64", "arm64"):
|
|
30
|
+
logger.warning(
|
|
31
|
+
"\n======================== WARNING ========================\n"
|
|
32
|
+
"ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
|
|
33
|
+
"Running this mobile sandbox on a non-x86_64 host may lead \n"
|
|
34
|
+
" to unexpected compatibility or performance issues.\n"
|
|
35
|
+
"=========================================================",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
os_type = platform.system()
|
|
39
|
+
if os_type == "Linux":
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["lsmod"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
check=True,
|
|
46
|
+
)
|
|
47
|
+
loaded_modules = result.stdout
|
|
48
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
49
|
+
loaded_modules = ""
|
|
50
|
+
logger.warning(
|
|
51
|
+
"Could not execute 'lsmod' to verify kernel modules.",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if "binder_linux" not in loaded_modules:
|
|
55
|
+
error_message = (
|
|
56
|
+
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
57
|
+
"MobileSandbox requires specific kernel modules"
|
|
58
|
+
" that appear to be missing or not loaded.\n\n"
|
|
59
|
+
"To fix this, please run the following commands"
|
|
60
|
+
" on your Linux host:\n\n"
|
|
61
|
+
"## Install required kernel modules\n"
|
|
62
|
+
"sudo apt update"
|
|
63
|
+
" && sudo apt install -y linux-modules-extra-`uname -r`\n"
|
|
64
|
+
"sudo modprobe binder_linux"
|
|
65
|
+
' devices="binder,hwbinder,vndbinder"\n'
|
|
66
|
+
"## (Optional) Load the ashmem driver for older kernels\n"
|
|
67
|
+
"sudo modprobe ashmem_linux\n"
|
|
68
|
+
"=================================================="
|
|
69
|
+
)
|
|
70
|
+
raise HostPrerequisiteError(error_message)
|
|
71
|
+
|
|
72
|
+
if os_type == "Windows":
|
|
73
|
+
try:
|
|
74
|
+
result = subprocess.run(
|
|
75
|
+
["wsl", "lsmod"],
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
check=True,
|
|
79
|
+
encoding="utf-8",
|
|
80
|
+
)
|
|
81
|
+
loaded_modules = result.stdout
|
|
82
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
83
|
+
loaded_modules = ""
|
|
84
|
+
logger.warning(
|
|
85
|
+
"Could not execute 'wsl lsmod' to verify kernel modules.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if "binder_linux" not in loaded_modules:
|
|
89
|
+
error_message = (
|
|
90
|
+
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
91
|
+
"MobileSandbox on Windows requires Docker Desktop "
|
|
92
|
+
"with the WSL 2 backend.\n"
|
|
93
|
+
"The required kernel modules seem to be missing "
|
|
94
|
+
"in your WSL 2 environment.\n\n"
|
|
95
|
+
"To fix this, please follow these steps:\n\n"
|
|
96
|
+
"1. **Ensure Docker Desktop is using WSL 2**:\n"
|
|
97
|
+
" - Open Docker Desktop -> Settings -> General.\n"
|
|
98
|
+
" - Make sure 'Use the WSL 2 based engine' "
|
|
99
|
+
"is checked.\n\n"
|
|
100
|
+
"2. **Ensure WSL is installed and updated**:\n"
|
|
101
|
+
" - Open PowerShell or Command Prompt "
|
|
102
|
+
"as Administrator.\n"
|
|
103
|
+
" - Run: wsl --install\n"
|
|
104
|
+
" - Run: wsl --update\n"
|
|
105
|
+
" (An update usually installs a recent Linux kernel "
|
|
106
|
+
"with the required modules.)\n\n"
|
|
107
|
+
"3. **Verify manually (Optional)**:\n"
|
|
108
|
+
" - After updating, run 'wsl lsmod | findstr binder' "
|
|
109
|
+
"in your terminal.\n"
|
|
110
|
+
" - If it shows 'binder_linux', "
|
|
111
|
+
"the issue should be resolved.\n"
|
|
112
|
+
"=================================================="
|
|
113
|
+
)
|
|
114
|
+
raise HostPrerequisiteError(error_message)
|
|
115
|
+
|
|
116
|
+
logger.info("Host environment check passed.")
|
|
117
|
+
|
|
118
|
+
|
|
18
119
|
class MobileMixin:
|
|
19
120
|
@property
|
|
20
121
|
def mobile_url(self):
|
|
@@ -41,9 +142,41 @@ class MobileMixin:
|
|
|
41
142
|
)
|
|
42
143
|
|
|
43
144
|
|
|
44
|
-
class
|
|
45
|
-
|
|
46
|
-
|
|
145
|
+
class AsyncMobileMixin:
|
|
146
|
+
async def get_mobile_url_async(self):
|
|
147
|
+
"""
|
|
148
|
+
Asynchronously retrieve the mobile VNC/websockify connection URL.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
str: Fully qualified URL to access the mobile sandbox UI remotely.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
RuntimeError: If the sandbox is not healthy.
|
|
155
|
+
"""
|
|
156
|
+
# Check health asynchronously
|
|
157
|
+
is_healthy = await self.manager_api.check_health_async(
|
|
158
|
+
identity=self.sandbox_id,
|
|
159
|
+
)
|
|
160
|
+
if not is_healthy:
|
|
161
|
+
raise RuntimeError(f"Sandbox {self.sandbox_id} is not healthy")
|
|
162
|
+
|
|
163
|
+
# Get container info asynchronously
|
|
164
|
+
info = await self.get_info_async()
|
|
165
|
+
|
|
166
|
+
# Local path and remote path (currently the same)
|
|
167
|
+
path = "/websockify/"
|
|
168
|
+
remote_path = "/websockify/"
|
|
169
|
+
params = {"password": info["runtime_token"]}
|
|
170
|
+
|
|
171
|
+
# Local URL if base_url is not set
|
|
172
|
+
if self.base_url is None:
|
|
173
|
+
return urljoin(info["url"], path) + "?" + urlencode(params)
|
|
174
|
+
|
|
175
|
+
# Remote URL
|
|
176
|
+
return (
|
|
177
|
+
f"{self.base_url}/desktop/{self.sandbox_id}{remote_path}"
|
|
178
|
+
f"?{urlencode(params)}"
|
|
179
|
+
)
|
|
47
180
|
|
|
48
181
|
|
|
49
182
|
@SandboxRegistry.register(
|
|
@@ -66,7 +199,7 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
66
199
|
sandbox_type: SandboxType = SandboxType.MOBILE,
|
|
67
200
|
):
|
|
68
201
|
if base_url is None and not self.__class__._host_check_done:
|
|
69
|
-
|
|
202
|
+
_check_host_readiness()
|
|
70
203
|
self.__class__._host_check_done = True
|
|
71
204
|
|
|
72
205
|
super().__init__(
|
|
@@ -77,101 +210,6 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
77
210
|
sandbox_type,
|
|
78
211
|
)
|
|
79
212
|
|
|
80
|
-
def _check_host_readiness(self) -> None:
|
|
81
|
-
logger.info(
|
|
82
|
-
"Performing one-time host environment check for MobileSandbox...",
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
architecture = platform.machine().lower()
|
|
86
|
-
if architecture in ("aarch64", "arm64"):
|
|
87
|
-
logger.warning(
|
|
88
|
-
"\n======================== WARNING ========================\n"
|
|
89
|
-
"ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
|
|
90
|
-
"Running this mobile sandbox on a non-x86_64 host may lead \n"
|
|
91
|
-
" to unexpected compatibility or performance issues.\n"
|
|
92
|
-
"=========================================================",
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
os_type = platform.system()
|
|
96
|
-
if os_type == "Linux":
|
|
97
|
-
try:
|
|
98
|
-
result = subprocess.run(
|
|
99
|
-
["lsmod"],
|
|
100
|
-
capture_output=True,
|
|
101
|
-
text=True,
|
|
102
|
-
check=True,
|
|
103
|
-
)
|
|
104
|
-
loaded_modules = result.stdout
|
|
105
|
-
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
106
|
-
loaded_modules = ""
|
|
107
|
-
logger.warning(
|
|
108
|
-
"Could not execute 'lsmod' to verify kernel modules.",
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
if "binder_linux" not in loaded_modules:
|
|
112
|
-
error_message = (
|
|
113
|
-
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
114
|
-
"MobileSandbox requires specific kernel modules"
|
|
115
|
-
" that appear to be missing or not loaded.\n\n"
|
|
116
|
-
"To fix this, please run the following commands"
|
|
117
|
-
" on your Linux host:\n\n"
|
|
118
|
-
"## Install required kernel modules\n"
|
|
119
|
-
"sudo apt update"
|
|
120
|
-
" && sudo apt install -y linux-modules-extra-`uname -r`\n"
|
|
121
|
-
"sudo modprobe binder_linux"
|
|
122
|
-
' devices="binder,hwbinder,vndbinder"\n'
|
|
123
|
-
"## (Optional) Load the ashmem driver for older kernels\n"
|
|
124
|
-
"sudo modprobe ashmem_linux\n"
|
|
125
|
-
"=================================================="
|
|
126
|
-
)
|
|
127
|
-
raise HostPrerequisiteError(error_message)
|
|
128
|
-
|
|
129
|
-
if os_type == "Windows":
|
|
130
|
-
try:
|
|
131
|
-
result = subprocess.run(
|
|
132
|
-
["wsl", "lsmod"],
|
|
133
|
-
capture_output=True,
|
|
134
|
-
text=True,
|
|
135
|
-
check=True,
|
|
136
|
-
encoding="utf-8",
|
|
137
|
-
)
|
|
138
|
-
loaded_modules = result.stdout
|
|
139
|
-
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
140
|
-
loaded_modules = ""
|
|
141
|
-
logger.warning(
|
|
142
|
-
"Could not execute 'wsl lsmod' to verify kernel modules.",
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
if "binder_linux" not in loaded_modules:
|
|
146
|
-
error_message = (
|
|
147
|
-
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
148
|
-
"MobileSandbox on Windows requires Docker Desktop "
|
|
149
|
-
"with the WSL 2 backend.\n"
|
|
150
|
-
"The required kernel modules seem to be missing "
|
|
151
|
-
"in your WSL 2 environment.\n\n"
|
|
152
|
-
"To fix this, please follow these steps:\n\n"
|
|
153
|
-
"1. **Ensure Docker Desktop is using WSL 2**:\n"
|
|
154
|
-
" - Open Docker Desktop -> Settings -> General.\n"
|
|
155
|
-
" - Make sure 'Use the WSL 2 based engine' "
|
|
156
|
-
"is checked.\n\n"
|
|
157
|
-
"2. **Ensure WSL is installed and updated**:\n"
|
|
158
|
-
" - Open PowerShell or Command Prompt "
|
|
159
|
-
"as Administrator.\n"
|
|
160
|
-
" - Run: wsl --install\n"
|
|
161
|
-
" - Run: wsl --update\n"
|
|
162
|
-
" (An update usually installs a recent Linux kernel "
|
|
163
|
-
"with the required modules.)\n\n"
|
|
164
|
-
"3. **Verify manually (Optional)**:\n"
|
|
165
|
-
" - After updating, run 'wsl lsmod | findstr binder' "
|
|
166
|
-
"in your terminal.\n"
|
|
167
|
-
" - If it shows 'binder_linux', "
|
|
168
|
-
"the issue should be resolved.\n"
|
|
169
|
-
"=================================================="
|
|
170
|
-
)
|
|
171
|
-
raise HostPrerequisiteError(error_message)
|
|
172
|
-
|
|
173
|
-
logger.info("Host environment check passed.")
|
|
174
|
-
|
|
175
213
|
def adb_use(
|
|
176
214
|
self,
|
|
177
215
|
action: str,
|
|
@@ -288,3 +326,112 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
288
326
|
def mobile_get_screenshot(self):
|
|
289
327
|
"""Take a screenshot of the current device screen."""
|
|
290
328
|
return self.call_tool("adb", {"action": "get_screenshot"})
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@SandboxRegistry.register(
|
|
332
|
+
build_image_uri("runtime-sandbox-mobile"),
|
|
333
|
+
sandbox_type=SandboxType.MOBILE_ASYNC,
|
|
334
|
+
security_level="high",
|
|
335
|
+
timeout=TIMEOUT,
|
|
336
|
+
description="Mobile Sandbox (Async)",
|
|
337
|
+
runtime_config={"privileged": True},
|
|
338
|
+
)
|
|
339
|
+
class MobileSandboxAsync(MobileMixin, AsyncMobileMixin, SandboxAsync):
|
|
340
|
+
_host_check_done = False
|
|
341
|
+
|
|
342
|
+
def __init__(
|
|
343
|
+
self,
|
|
344
|
+
sandbox_id: Optional[str] = None,
|
|
345
|
+
timeout: int = 3000,
|
|
346
|
+
base_url: Optional[str] = None,
|
|
347
|
+
bearer_token: Optional[str] = None,
|
|
348
|
+
sandbox_type: SandboxType = SandboxType.MOBILE_ASYNC,
|
|
349
|
+
):
|
|
350
|
+
if base_url is None and not self.__class__._host_check_done:
|
|
351
|
+
_check_host_readiness()
|
|
352
|
+
self.__class__._host_check_done = True
|
|
353
|
+
|
|
354
|
+
super().__init__(
|
|
355
|
+
sandbox_id,
|
|
356
|
+
timeout,
|
|
357
|
+
base_url,
|
|
358
|
+
bearer_token,
|
|
359
|
+
sandbox_type,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
async def adb_use(
|
|
363
|
+
self,
|
|
364
|
+
action: str,
|
|
365
|
+
coordinate: Optional[List[int]] = None,
|
|
366
|
+
start: Optional[List[int]] = None,
|
|
367
|
+
end: Optional[List[int]] = None,
|
|
368
|
+
duration: Optional[int] = None,
|
|
369
|
+
code: Optional[Union[int, str]] = None,
|
|
370
|
+
text: Optional[str] = None,
|
|
371
|
+
):
|
|
372
|
+
"""
|
|
373
|
+
Asynchronously execute a general-purpose ADB action.
|
|
374
|
+
"""
|
|
375
|
+
payload = {"action": action}
|
|
376
|
+
if coordinate is not None:
|
|
377
|
+
payload["coordinate"] = coordinate
|
|
378
|
+
if start is not None:
|
|
379
|
+
payload["start"] = start
|
|
380
|
+
if end is not None:
|
|
381
|
+
payload["end"] = end
|
|
382
|
+
if duration is not None:
|
|
383
|
+
payload["duration"] = duration
|
|
384
|
+
if code is not None:
|
|
385
|
+
payload["code"] = code
|
|
386
|
+
if text is not None:
|
|
387
|
+
payload["text"] = text
|
|
388
|
+
|
|
389
|
+
return await self.call_tool_async("adb", payload)
|
|
390
|
+
|
|
391
|
+
async def mobile_get_screen_resolution(self):
|
|
392
|
+
"""Asynchronously get the screen resolution."""
|
|
393
|
+
return await self.call_tool_async(
|
|
394
|
+
"adb",
|
|
395
|
+
{"action": "get_screen_resolution"},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
async def mobile_tap(self, coordinate: List[int]):
|
|
399
|
+
"""Asynchronously tap specific screen coordinates."""
|
|
400
|
+
return await self.call_tool_async(
|
|
401
|
+
"adb",
|
|
402
|
+
{"action": "tap", "coordinate": coordinate},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def mobile_swipe(
|
|
406
|
+
self,
|
|
407
|
+
start: List[int],
|
|
408
|
+
end: List[int],
|
|
409
|
+
duration: Optional[int] = None,
|
|
410
|
+
):
|
|
411
|
+
"""Asynchronously perform a swipe gesture."""
|
|
412
|
+
payload = {
|
|
413
|
+
"action": "swipe",
|
|
414
|
+
"start": start,
|
|
415
|
+
"end": end,
|
|
416
|
+
}
|
|
417
|
+
if duration is not None:
|
|
418
|
+
payload["duration"] = duration
|
|
419
|
+
return await self.call_tool_async("adb", payload)
|
|
420
|
+
|
|
421
|
+
async def mobile_input_text(self, text: str):
|
|
422
|
+
"""Asynchronously input text into the focused UI element."""
|
|
423
|
+
return await self.call_tool_async(
|
|
424
|
+
"adb",
|
|
425
|
+
{"action": "input_text", "text": text},
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
async def mobile_key_event(self, code: Union[int, str]):
|
|
429
|
+
"""Asynchronously send a key event to the device."""
|
|
430
|
+
return await self.call_tool_async(
|
|
431
|
+
"adb",
|
|
432
|
+
{"action": "key_event", "code": code},
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
async def mobile_get_screenshot(self):
|
|
436
|
+
"""Asynchronously take a screenshot."""
|
|
437
|
+
return await self.call_tool_async("adb", {"action": "get_screenshot"})
|
|
@@ -12,71 +12,46 @@ logging.basicConfig(level=logging.INFO)
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class
|
|
15
|
+
class SandboxBase:
|
|
16
16
|
"""
|
|
17
|
-
Sandbox
|
|
17
|
+
Common base class for both sync and async Sandbox interfaces.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
def __init__(
|
|
21
21
|
self,
|
|
22
22
|
sandbox_id: Optional[str] = None,
|
|
23
|
-
timeout: int = 3000,
|
|
23
|
+
timeout: int = 3000,
|
|
24
24
|
base_url: Optional[str] = None,
|
|
25
|
-
bearer_token: Optional[str] = None,
|
|
25
|
+
bearer_token: Optional[str] = None,
|
|
26
26
|
sandbox_type: SandboxType = SandboxType.BASE,
|
|
27
27
|
) -> None:
|
|
28
|
-
"""
|
|
29
|
-
Initialize the sandbox interface.
|
|
30
|
-
"""
|
|
31
28
|
self.base_url = base_url
|
|
29
|
+
self.embed_mode = not bool(base_url)
|
|
30
|
+
self.sandbox_type = sandbox_type
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
self._sandbox_id = sandbox_id
|
|
33
|
+
|
|
32
34
|
if base_url:
|
|
33
|
-
self.embed_mode = False
|
|
34
35
|
self.manager_api = SandboxManager(
|
|
35
36
|
base_url=base_url,
|
|
36
37
|
bearer_token=bearer_token,
|
|
37
|
-
)
|
|
38
|
+
)
|
|
38
39
|
else:
|
|
39
|
-
# Launch a local manager
|
|
40
|
-
self.embed_mode = True
|
|
41
40
|
self.manager_api = SandboxManager(
|
|
42
41
|
default_type=sandbox_type,
|
|
43
42
|
)
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"init the runtime, please wait.",
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
sandbox_id = self.manager_api.create_from_pool(
|
|
52
|
-
sandbox_type=SandboxType(sandbox_type).value,
|
|
53
|
-
)
|
|
54
|
-
if sandbox_id is None:
|
|
55
|
-
raise RuntimeError(
|
|
56
|
-
"No sandbox available. "
|
|
57
|
-
"Please check if sandbox images exist, build or pull "
|
|
58
|
-
"missing images in sandbox server.",
|
|
59
|
-
)
|
|
60
|
-
self._sandbox_id = sandbox_id
|
|
61
|
-
|
|
62
|
-
self._sandbox_id = sandbox_id
|
|
63
|
-
self.sandbox_type = sandbox_type
|
|
64
|
-
self.timeout = timeout
|
|
65
|
-
|
|
66
|
-
# Clean up function enabled in embed mode
|
|
67
|
-
if self.embed_mode:
|
|
68
|
-
atexit.register(self._cleanup)
|
|
69
|
-
self._register_signal_handlers()
|
|
44
|
+
@property
|
|
45
|
+
def sandbox_id(self) -> Optional[str]:
|
|
46
|
+
return self._sandbox_id
|
|
70
47
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
signals. On platforms where SIGTERM is not available (e.g.,
|
|
77
|
-
Windows), only SIGINT is handled.
|
|
78
|
-
"""
|
|
48
|
+
@sandbox_id.setter
|
|
49
|
+
def sandbox_id(self, value: str) -> None:
|
|
50
|
+
if not value:
|
|
51
|
+
raise ValueError("Sandbox ID cannot be empty.")
|
|
52
|
+
self._sandbox_id = value
|
|
79
53
|
|
|
54
|
+
def _register_signal_handlers(self):
|
|
80
55
|
def _handler(signum, frame): # pylint: disable=unused-argument
|
|
81
56
|
logger.debug(
|
|
82
57
|
f"Received signal {signum}, stopping Sandbox"
|
|
@@ -85,7 +60,6 @@ class Sandbox:
|
|
|
85
60
|
self._cleanup()
|
|
86
61
|
raise SystemExit(0)
|
|
87
62
|
|
|
88
|
-
# Windows does not support SIGTERM
|
|
89
63
|
if hasattr(signal, "SIGTERM"):
|
|
90
64
|
signals = [signal.SIGINT, signal.SIGTERM]
|
|
91
65
|
else:
|
|
@@ -107,39 +81,36 @@ class Sandbox:
|
|
|
107
81
|
specific sandbox instance.
|
|
108
82
|
"""
|
|
109
83
|
try:
|
|
110
|
-
# Remote not need to close the embed_manager
|
|
111
84
|
if self.embed_mode:
|
|
112
|
-
# Clean all
|
|
113
85
|
self.manager_api.__exit__(None, None, None)
|
|
114
86
|
else:
|
|
115
|
-
# Clean the specific sandbox
|
|
116
87
|
self.manager_api.release(self.sandbox_id)
|
|
117
88
|
except Exception as e:
|
|
118
89
|
import traceback
|
|
119
90
|
|
|
120
91
|
logger.error(
|
|
121
|
-
f"Cleanup {self.sandbox_id} error: {e}
|
|
122
|
-
f"{traceback.format_exc()}",
|
|
92
|
+
f"Cleanup {self.sandbox_id} error: {e}"
|
|
93
|
+
f"\n{traceback.format_exc()}",
|
|
123
94
|
)
|
|
124
95
|
|
|
96
|
+
|
|
97
|
+
class Sandbox(SandboxBase):
|
|
125
98
|
def __enter__(self):
|
|
99
|
+
# Create sandbox if sandbox_id not provided
|
|
100
|
+
if self.sandbox_id is None:
|
|
101
|
+
self.sandbox_id = self.manager_api.create_from_pool(
|
|
102
|
+
sandbox_type=SandboxType(self.sandbox_type).value,
|
|
103
|
+
)
|
|
104
|
+
if self.sandbox_id is None:
|
|
105
|
+
raise RuntimeError("No sandbox available.")
|
|
106
|
+
if self.embed_mode:
|
|
107
|
+
atexit.register(self._cleanup)
|
|
108
|
+
self._register_signal_handlers()
|
|
126
109
|
return self
|
|
127
110
|
|
|
128
111
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
129
112
|
self._cleanup()
|
|
130
113
|
|
|
131
|
-
@property
|
|
132
|
-
def sandbox_id(self) -> str:
|
|
133
|
-
"""Get the sandbox ID."""
|
|
134
|
-
return self._sandbox_id
|
|
135
|
-
|
|
136
|
-
@sandbox_id.setter
|
|
137
|
-
def sandbox_id(self, value: str) -> None:
|
|
138
|
-
"""Set the sandbox ID."""
|
|
139
|
-
if not value:
|
|
140
|
-
raise ValueError("Sandbox ID cannot be empty.")
|
|
141
|
-
self._sandbox_id = value
|
|
142
|
-
|
|
143
114
|
def get_info(self) -> dict:
|
|
144
115
|
return self.manager_api.get_info(self.sandbox_id)
|
|
145
116
|
|
|
@@ -156,15 +127,77 @@ class Sandbox:
|
|
|
156
127
|
) -> Any:
|
|
157
128
|
if arguments is None:
|
|
158
129
|
arguments = {}
|
|
159
|
-
|
|
160
130
|
return self.manager_api.call_tool(self.sandbox_id, name, arguments)
|
|
161
131
|
|
|
162
|
-
def add_mcp_servers(
|
|
132
|
+
def add_mcp_servers(self, server_configs: dict, overwrite=False):
|
|
133
|
+
return self.manager_api.add_mcp_servers(
|
|
134
|
+
self.sandbox_id,
|
|
135
|
+
server_configs,
|
|
136
|
+
overwrite,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class SandboxAsync(SandboxBase):
|
|
141
|
+
async def __aenter__(self):
|
|
142
|
+
if self.sandbox_id is None:
|
|
143
|
+
self.sandbox_id = await self.manager_api.create_from_pool_async(
|
|
144
|
+
sandbox_type=SandboxType(self.sandbox_type).value,
|
|
145
|
+
)
|
|
146
|
+
if self.sandbox_id is None:
|
|
147
|
+
raise RuntimeError("No sandbox available.")
|
|
148
|
+
if self.embed_mode:
|
|
149
|
+
atexit.register(self._cleanup)
|
|
150
|
+
self._register_signal_handlers()
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
154
|
+
await self._cleanup_async()
|
|
155
|
+
|
|
156
|
+
async def _cleanup_async(self):
|
|
157
|
+
try:
|
|
158
|
+
if self.embed_mode:
|
|
159
|
+
await self.manager_api.__aexit__(None, None, None)
|
|
160
|
+
else:
|
|
161
|
+
await self.manager_api.release_async(self.sandbox_id)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
import traceback
|
|
164
|
+
|
|
165
|
+
logger.error(
|
|
166
|
+
f"Async Cleanup {self.sandbox_id} error: {e}"
|
|
167
|
+
f"\n{traceback.format_exc()}",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def get_info_async(self) -> dict:
|
|
171
|
+
return await self.manager_api.get_info_async(self.sandbox_id)
|
|
172
|
+
|
|
173
|
+
async def list_tools_async(
|
|
174
|
+
self,
|
|
175
|
+
tool_type: Optional[str] = None,
|
|
176
|
+
) -> dict:
|
|
177
|
+
return await self.manager_api.list_tools_async(
|
|
178
|
+
self.sandbox_id,
|
|
179
|
+
tool_type=tool_type,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def call_tool_async(
|
|
183
|
+
self,
|
|
184
|
+
name: str,
|
|
185
|
+
arguments: Optional[dict[str, Any]] = None,
|
|
186
|
+
) -> Any:
|
|
187
|
+
if arguments is None:
|
|
188
|
+
arguments = {}
|
|
189
|
+
return await self.manager_api.call_tool_async(
|
|
190
|
+
self.sandbox_id,
|
|
191
|
+
name,
|
|
192
|
+
arguments,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
async def add_mcp_servers_async(
|
|
163
196
|
self,
|
|
164
197
|
server_configs: dict,
|
|
165
198
|
overwrite=False,
|
|
166
199
|
):
|
|
167
|
-
return self.manager_api.
|
|
200
|
+
return await self.manager_api.add_mcp_servers_async(
|
|
168
201
|
self.sandbox_id,
|
|
169
202
|
server_configs,
|
|
170
203
|
overwrite,
|