agentscope-runtime 1.0.4a1__py3-none-any.whl → 1.0.5.post1__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 -8
- agentscope_runtime/adapters/langgraph/stream.py +120 -70
- 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 +836 -1
- agentscope_runtime/cli/commands/stop.py +16 -0
- agentscope_runtime/common/container_clients/__init__.py +52 -0
- agentscope_runtime/common/container_clients/agentrun_client.py +6 -4
- agentscope_runtime/common/container_clients/boxlite_client.py +442 -0
- agentscope_runtime/common/container_clients/docker_client.py +0 -20
- agentscope_runtime/common/container_clients/fc_client.py +6 -4
- agentscope_runtime/common/container_clients/gvisor_client.py +38 -0
- agentscope_runtime/common/container_clients/knative_client.py +467 -0
- agentscope_runtime/common/utils/deprecation.py +164 -0
- agentscope_runtime/engine/__init__.py +4 -0
- agentscope_runtime/engine/app/agent_app.py +16 -4
- agentscope_runtime/engine/constant.py +1 -0
- agentscope_runtime/engine/deployers/__init__.py +34 -11
- agentscope_runtime/engine/deployers/adapter/__init__.py +8 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +26 -51
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +23 -13
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +4 -201
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +152 -25
- agentscope_runtime/engine/deployers/adapter/agui/__init__.py +8 -0
- agentscope_runtime/engine/deployers/adapter/agui/agui_adapter_utils.py +652 -0
- agentscope_runtime/engine/deployers/adapter/agui/agui_protocol_adapter.py +225 -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/pai_deployer.py +2335 -0
- agentscope_runtime/engine/deployers/utils/net_utils.py +37 -0
- agentscope_runtime/engine/deployers/utils/oss_utils.py +38 -0
- agentscope_runtime/engine/deployers/utils/package.py +46 -42
- agentscope_runtime/engine/helpers/agent_api_client.py +372 -0
- agentscope_runtime/engine/runner.py +13 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +9 -3
- agentscope_runtime/engine/services/agent_state/__init__.py +7 -0
- agentscope_runtime/engine/services/memory/__init__.py +7 -0
- agentscope_runtime/engine/services/memory/redis_memory_service.py +15 -16
- agentscope_runtime/engine/services/session_history/__init__.py +7 -0
- agentscope_runtime/engine/tracing/local_logging_handler.py +2 -3
- 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 +102 -65
- agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
- 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 +275 -29
- agentscope_runtime/sandbox/manager/server/app.py +7 -1
- agentscope_runtime/sandbox/manager/server/config.py +3 -1
- agentscope_runtime/sandbox/model/manager_config.py +11 -9
- agentscope_runtime/tools/modelstudio_memory/__init__.py +106 -0
- agentscope_runtime/tools/modelstudio_memory/base.py +220 -0
- agentscope_runtime/tools/modelstudio_memory/config.py +86 -0
- agentscope_runtime/tools/modelstudio_memory/core.py +594 -0
- agentscope_runtime/tools/modelstudio_memory/exceptions.py +60 -0
- agentscope_runtime/tools/modelstudio_memory/schemas.py +253 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/METADATA +187 -74
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/RECORD +79 -55
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/WHEEL +1 -1
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/top_level.txt +0 -0
|
@@ -6,77 +6,56 @@ from typing import Any, Optional
|
|
|
6
6
|
|
|
7
7
|
from ..enums import SandboxType
|
|
8
8
|
from ..manager.sandbox_manager import SandboxManager
|
|
9
|
+
from ..manager.server.app import get_config
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
logging.basicConfig(level=logging.INFO)
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
class
|
|
16
|
+
class SandboxBase:
|
|
16
17
|
"""
|
|
17
|
-
Sandbox
|
|
18
|
+
Common base class for both sync and async Sandbox interfaces.
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
21
|
def __init__(
|
|
21
22
|
self,
|
|
22
23
|
sandbox_id: Optional[str] = None,
|
|
23
|
-
timeout: int = 3000,
|
|
24
|
+
timeout: int = 3000,
|
|
24
25
|
base_url: Optional[str] = None,
|
|
25
|
-
bearer_token: Optional[str] = None,
|
|
26
|
+
bearer_token: Optional[str] = None,
|
|
26
27
|
sandbox_type: SandboxType = SandboxType.BASE,
|
|
27
28
|
) -> None:
|
|
28
|
-
"""
|
|
29
|
-
Initialize the sandbox interface.
|
|
30
|
-
"""
|
|
31
29
|
self.base_url = base_url
|
|
30
|
+
self.embed_mode = not bool(base_url)
|
|
31
|
+
self.sandbox_type = sandbox_type
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
self._sandbox_id = sandbox_id
|
|
34
|
+
|
|
32
35
|
if base_url:
|
|
33
|
-
|
|
36
|
+
# Remote Manager
|
|
34
37
|
self.manager_api = SandboxManager(
|
|
35
38
|
base_url=base_url,
|
|
36
39
|
bearer_token=bearer_token,
|
|
37
|
-
)
|
|
40
|
+
)
|
|
38
41
|
else:
|
|
39
|
-
#
|
|
40
|
-
self.embed_mode = True
|
|
42
|
+
# Embedded Manager
|
|
41
43
|
self.manager_api = SandboxManager(
|
|
44
|
+
config=get_config(),
|
|
42
45
|
default_type=sandbox_type,
|
|
43
46
|
)
|
|
44
47
|
|
|
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()
|
|
48
|
+
@property
|
|
49
|
+
def sandbox_id(self) -> Optional[str]:
|
|
50
|
+
return self._sandbox_id
|
|
70
51
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
signals. On platforms where SIGTERM is not available (e.g.,
|
|
77
|
-
Windows), only SIGINT is handled.
|
|
78
|
-
"""
|
|
52
|
+
@sandbox_id.setter
|
|
53
|
+
def sandbox_id(self, value: str) -> None:
|
|
54
|
+
if not value:
|
|
55
|
+
raise ValueError("Sandbox ID cannot be empty.")
|
|
56
|
+
self._sandbox_id = value
|
|
79
57
|
|
|
58
|
+
def _register_signal_handlers(self):
|
|
80
59
|
def _handler(signum, frame): # pylint: disable=unused-argument
|
|
81
60
|
logger.debug(
|
|
82
61
|
f"Received signal {signum}, stopping Sandbox"
|
|
@@ -85,7 +64,6 @@ class Sandbox:
|
|
|
85
64
|
self._cleanup()
|
|
86
65
|
raise SystemExit(0)
|
|
87
66
|
|
|
88
|
-
# Windows does not support SIGTERM
|
|
89
67
|
if hasattr(signal, "SIGTERM"):
|
|
90
68
|
signals = [signal.SIGINT, signal.SIGTERM]
|
|
91
69
|
else:
|
|
@@ -107,39 +85,36 @@ class Sandbox:
|
|
|
107
85
|
specific sandbox instance.
|
|
108
86
|
"""
|
|
109
87
|
try:
|
|
110
|
-
# Remote not need to close the embed_manager
|
|
111
88
|
if self.embed_mode:
|
|
112
|
-
# Clean all
|
|
113
89
|
self.manager_api.__exit__(None, None, None)
|
|
114
90
|
else:
|
|
115
|
-
# Clean the specific sandbox
|
|
116
91
|
self.manager_api.release(self.sandbox_id)
|
|
117
92
|
except Exception as e:
|
|
118
93
|
import traceback
|
|
119
94
|
|
|
120
95
|
logger.error(
|
|
121
|
-
f"Cleanup {self.sandbox_id} error: {e}
|
|
122
|
-
f"{traceback.format_exc()}",
|
|
96
|
+
f"Cleanup {self.sandbox_id} error: {e}"
|
|
97
|
+
f"\n{traceback.format_exc()}",
|
|
123
98
|
)
|
|
124
99
|
|
|
100
|
+
|
|
101
|
+
class Sandbox(SandboxBase):
|
|
125
102
|
def __enter__(self):
|
|
103
|
+
# Create sandbox if sandbox_id not provided
|
|
104
|
+
if self.sandbox_id is None:
|
|
105
|
+
self.sandbox_id = self.manager_api.create_from_pool(
|
|
106
|
+
sandbox_type=SandboxType(self.sandbox_type).value,
|
|
107
|
+
)
|
|
108
|
+
if self.sandbox_id is None:
|
|
109
|
+
raise RuntimeError("No sandbox available.")
|
|
110
|
+
if self.embed_mode:
|
|
111
|
+
atexit.register(self._cleanup)
|
|
112
|
+
self._register_signal_handlers()
|
|
126
113
|
return self
|
|
127
114
|
|
|
128
115
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
129
116
|
self._cleanup()
|
|
130
117
|
|
|
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
118
|
def get_info(self) -> dict:
|
|
144
119
|
return self.manager_api.get_info(self.sandbox_id)
|
|
145
120
|
|
|
@@ -156,15 +131,77 @@ class Sandbox:
|
|
|
156
131
|
) -> Any:
|
|
157
132
|
if arguments is None:
|
|
158
133
|
arguments = {}
|
|
159
|
-
|
|
160
134
|
return self.manager_api.call_tool(self.sandbox_id, name, arguments)
|
|
161
135
|
|
|
162
|
-
def add_mcp_servers(
|
|
136
|
+
def add_mcp_servers(self, server_configs: dict, overwrite=False):
|
|
137
|
+
return self.manager_api.add_mcp_servers(
|
|
138
|
+
self.sandbox_id,
|
|
139
|
+
server_configs,
|
|
140
|
+
overwrite,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SandboxAsync(SandboxBase):
|
|
145
|
+
async def __aenter__(self):
|
|
146
|
+
if self.sandbox_id is None:
|
|
147
|
+
self.sandbox_id = await self.manager_api.create_from_pool_async(
|
|
148
|
+
sandbox_type=SandboxType(self.sandbox_type).value,
|
|
149
|
+
)
|
|
150
|
+
if self.sandbox_id is None:
|
|
151
|
+
raise RuntimeError("No sandbox available.")
|
|
152
|
+
if self.embed_mode:
|
|
153
|
+
atexit.register(self._cleanup)
|
|
154
|
+
self._register_signal_handlers()
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
158
|
+
await self._cleanup_async()
|
|
159
|
+
|
|
160
|
+
async def _cleanup_async(self):
|
|
161
|
+
try:
|
|
162
|
+
if self.embed_mode:
|
|
163
|
+
await self.manager_api.__aexit__(None, None, None)
|
|
164
|
+
else:
|
|
165
|
+
await self.manager_api.release_async(self.sandbox_id)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
import traceback
|
|
168
|
+
|
|
169
|
+
logger.error(
|
|
170
|
+
f"Async Cleanup {self.sandbox_id} error: {e}"
|
|
171
|
+
f"\n{traceback.format_exc()}",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def get_info_async(self) -> dict:
|
|
175
|
+
return await self.manager_api.get_info_async(self.sandbox_id)
|
|
176
|
+
|
|
177
|
+
async def list_tools_async(
|
|
178
|
+
self,
|
|
179
|
+
tool_type: Optional[str] = None,
|
|
180
|
+
) -> dict:
|
|
181
|
+
return await self.manager_api.list_tools_async(
|
|
182
|
+
self.sandbox_id,
|
|
183
|
+
tool_type=tool_type,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
async def call_tool_async(
|
|
187
|
+
self,
|
|
188
|
+
name: str,
|
|
189
|
+
arguments: Optional[dict[str, Any]] = None,
|
|
190
|
+
) -> Any:
|
|
191
|
+
if arguments is None:
|
|
192
|
+
arguments = {}
|
|
193
|
+
return await self.manager_api.call_tool_async(
|
|
194
|
+
self.sandbox_id,
|
|
195
|
+
name,
|
|
196
|
+
arguments,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
async def add_mcp_servers_async(
|
|
163
200
|
self,
|
|
164
201
|
server_configs: dict,
|
|
165
202
|
overwrite=False,
|
|
166
203
|
):
|
|
167
|
-
return self.manager_api.
|
|
204
|
+
return await self.manager_api.add_mcp_servers_async(
|
|
168
205
|
self.sandbox_id,
|
|
169
206
|
server_configs,
|
|
170
207
|
overwrite,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import io
|
|
3
3
|
import sys
|
|
4
4
|
import logging
|
|
5
|
-
import
|
|
5
|
+
import asyncio
|
|
6
6
|
import traceback
|
|
7
7
|
from contextlib import redirect_stderr, redirect_stdout
|
|
8
8
|
|
|
@@ -44,26 +44,33 @@ async def run_ipython_cell(
|
|
|
44
44
|
stdout_buf = io.StringIO()
|
|
45
45
|
stderr_buf = io.StringIO()
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
def thread_target():
|
|
48
|
+
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
|
49
|
+
preprocessing_exc_tuple = None
|
|
50
|
+
try:
|
|
51
|
+
transformed_cell = ipy.transform_cell(code)
|
|
52
|
+
except Exception:
|
|
53
|
+
transformed_cell = code
|
|
54
|
+
preprocessing_exc_tuple = sys.exc_info()
|
|
55
|
+
|
|
56
|
+
if transformed_cell is None:
|
|
57
|
+
raise HTTPException(
|
|
58
|
+
status_code=500,
|
|
59
|
+
detail=(
|
|
60
|
+
"IPython cell transformation failed: "
|
|
61
|
+
"transformed_cell is None."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
asyncio.run(
|
|
66
|
+
ipy.run_cell_async(
|
|
67
|
+
code,
|
|
68
|
+
transformed_cell=transformed_cell,
|
|
69
|
+
preprocessing_exc_tuple=preprocessing_exc_tuple,
|
|
70
|
+
),
|
|
60
71
|
)
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
code,
|
|
64
|
-
transformed_cell=transformed_cell,
|
|
65
|
-
preprocessing_exc_tuple=preprocessing_exc_tuple,
|
|
66
|
-
)
|
|
73
|
+
await asyncio.to_thread(thread_target)
|
|
67
74
|
|
|
68
75
|
stdout_content = stdout_buf.getvalue()
|
|
69
76
|
stderr_content = stderr_buf.getvalue()
|
|
@@ -128,16 +135,16 @@ async def run_shell_command(
|
|
|
128
135
|
if not command:
|
|
129
136
|
raise HTTPException(status_code=400, detail="Command is required.")
|
|
130
137
|
|
|
131
|
-
|
|
138
|
+
proc = await asyncio.create_subprocess_shell(
|
|
132
139
|
command,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
stderr=subprocess.PIPE,
|
|
136
|
-
text=True,
|
|
137
|
-
check=False,
|
|
140
|
+
stdout=asyncio.subprocess.PIPE,
|
|
141
|
+
stderr=asyncio.subprocess.PIPE,
|
|
138
142
|
)
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
|
|
144
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
145
|
+
|
|
146
|
+
stdout_content = stdout_bytes.decode()
|
|
147
|
+
stderr_content = stderr_bytes.decode()
|
|
141
148
|
|
|
142
149
|
content_list = []
|
|
143
150
|
|
|
@@ -161,7 +168,7 @@ async def run_shell_command(
|
|
|
161
168
|
content_list.append(
|
|
162
169
|
TextContent(
|
|
163
170
|
type="text",
|
|
164
|
-
text=str(
|
|
171
|
+
text=str(proc.returncode),
|
|
165
172
|
description="returncode",
|
|
166
173
|
),
|
|
167
174
|
)
|
|
@@ -173,7 +180,7 @@ async def run_shell_command(
|
|
|
173
180
|
+ "\n"
|
|
174
181
|
+ stderr_content
|
|
175
182
|
+ "\n"
|
|
176
|
-
+ str(
|
|
183
|
+
+ str(proc.returncode),
|
|
177
184
|
description="output",
|
|
178
185
|
),
|
|
179
186
|
)
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
from .http_client import SandboxHttpClient
|
|
3
3
|
from .training_client import TrainingSandboxClient
|
|
4
|
+
from .async_http_client import SandboxHttpAsyncClient
|
|
4
5
|
|
|
5
|
-
__all__ = [
|
|
6
|
+
__all__ = [
|
|
7
|
+
"SandboxHttpClient",
|
|
8
|
+
"SandboxHttpAsyncClient",
|
|
9
|
+
"TrainingSandboxClient",
|
|
10
|
+
]
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# pylint: disable=unused-argument
|
|
3
|
+
import logging
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from .base import SandboxHttpBase
|
|
11
|
+
from ..model import ContainerModel
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SandboxHttpAsyncClient(SandboxHttpBase):
|
|
17
|
+
"""
|
|
18
|
+
A Python async client for interacting with the runtime API.
|
|
19
|
+
Connect directly to the container.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
model: Optional[ContainerModel] = None,
|
|
25
|
+
timeout: int = 60,
|
|
26
|
+
domain: str = "localhost",
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize the Python async client.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
model (ContainerModel): The pydantic model representing the
|
|
33
|
+
runtime sandbox.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(model, timeout, domain)
|
|
36
|
+
self.client = httpx.AsyncClient(
|
|
37
|
+
timeout=self.timeout,
|
|
38
|
+
headers=self.headers,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def __aenter__(self):
|
|
42
|
+
# Wait for the runtime api server to be healthy
|
|
43
|
+
await self.wait_until_healthy()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
47
|
+
await self.client.aclose()
|
|
48
|
+
|
|
49
|
+
async def _request(self, method: str, url: str, **kwargs):
|
|
50
|
+
return await self.client.request(method, url, **kwargs)
|
|
51
|
+
|
|
52
|
+
async def safe_request(self, method: str, url: str, **kwargs):
|
|
53
|
+
"""
|
|
54
|
+
Unified HTTP request method with async exception handling.
|
|
55
|
+
Returns JSON if possible, otherwise plain text.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
r = await self._request(method, url, **kwargs)
|
|
59
|
+
r.raise_for_status()
|
|
60
|
+
try:
|
|
61
|
+
return r.json()
|
|
62
|
+
except ValueError:
|
|
63
|
+
return r.text
|
|
64
|
+
except httpx.RequestError as e:
|
|
65
|
+
logger.error(f"HTTP error: {e}")
|
|
66
|
+
return {
|
|
67
|
+
"isError": True,
|
|
68
|
+
"content": [{"type": "text", "text": str(e)}],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async def check_health(self) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Check if the runtime service is running by verifying the health
|
|
74
|
+
endpoint.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
bool: True if the service is reachable, False otherwise.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
r = await self._request(
|
|
81
|
+
"get",
|
|
82
|
+
f"{self.base_url}/healthz",
|
|
83
|
+
)
|
|
84
|
+
return r.status_code == 200
|
|
85
|
+
except httpx.RequestError:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
async def wait_until_healthy(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Wait until the runtime service is running for a specified timeout.
|
|
91
|
+
"""
|
|
92
|
+
start_time = asyncio.get_event_loop().time()
|
|
93
|
+
while (
|
|
94
|
+
asyncio.get_event_loop().time() - start_time < self.start_timeout
|
|
95
|
+
):
|
|
96
|
+
if await self.check_health():
|
|
97
|
+
return
|
|
98
|
+
await asyncio.sleep(1)
|
|
99
|
+
raise TimeoutError(
|
|
100
|
+
"Runtime service did not start within the specified timeout.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def add_mcp_servers(self, server_configs, overwrite=False):
|
|
104
|
+
"""
|
|
105
|
+
Add MCP servers to runtime.
|
|
106
|
+
"""
|
|
107
|
+
endpoint = f"{self.base_url}/mcp/add_servers"
|
|
108
|
+
return await self.safe_request(
|
|
109
|
+
"post",
|
|
110
|
+
endpoint,
|
|
111
|
+
json={"server_configs": server_configs, "overwrite": overwrite},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
async def list_tools(self, tool_type=None, **kwargs) -> dict:
|
|
115
|
+
"""
|
|
116
|
+
List available MCP tools plus generic built-in tools.
|
|
117
|
+
"""
|
|
118
|
+
data = await self.safe_request(
|
|
119
|
+
"get",
|
|
120
|
+
f"{self.base_url}/mcp/list_tools",
|
|
121
|
+
)
|
|
122
|
+
if isinstance(data, dict) and "isError" not in data:
|
|
123
|
+
data["generic"] = self.generic_tools
|
|
124
|
+
if tool_type:
|
|
125
|
+
return {tool_type: data.get(tool_type, {})}
|
|
126
|
+
return data
|
|
127
|
+
|
|
128
|
+
async def call_tool(
|
|
129
|
+
self,
|
|
130
|
+
name: str,
|
|
131
|
+
arguments: Optional[dict[str, Any]] = None,
|
|
132
|
+
) -> dict:
|
|
133
|
+
"""
|
|
134
|
+
Call a specific MCP tool.
|
|
135
|
+
|
|
136
|
+
If it's a generic tool, call the corresponding local method.
|
|
137
|
+
"""
|
|
138
|
+
if arguments is None:
|
|
139
|
+
arguments = {}
|
|
140
|
+
if name in self.generic_tools:
|
|
141
|
+
if name == "run_ipython_cell":
|
|
142
|
+
return await self.run_ipython_cell(**arguments)
|
|
143
|
+
elif name == "run_shell_command":
|
|
144
|
+
return await self.run_shell_command(**arguments)
|
|
145
|
+
|
|
146
|
+
return await self.safe_request(
|
|
147
|
+
"post",
|
|
148
|
+
f"{self.base_url}/mcp/call_tool",
|
|
149
|
+
json={"tool_name": name, "arguments": arguments},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
async def run_ipython_cell(
|
|
153
|
+
self,
|
|
154
|
+
code: str = Field(description="IPython code to execute"),
|
|
155
|
+
) -> dict:
|
|
156
|
+
"""
|
|
157
|
+
Run an IPython cell.
|
|
158
|
+
"""
|
|
159
|
+
return await self.safe_request(
|
|
160
|
+
"post",
|
|
161
|
+
f"{self.base_url}/tools/run_ipython_cell",
|
|
162
|
+
json={"code": code},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def run_shell_command(
|
|
166
|
+
self,
|
|
167
|
+
command: str = Field(description="Shell command to execute"),
|
|
168
|
+
) -> dict:
|
|
169
|
+
"""
|
|
170
|
+
Run a shell command.
|
|
171
|
+
"""
|
|
172
|
+
return await self.safe_request(
|
|
173
|
+
"post",
|
|
174
|
+
f"{self.base_url}/tools/run_shell_command",
|
|
175
|
+
json={"command": command},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def commit_changes(
|
|
179
|
+
self,
|
|
180
|
+
commit_message: str = "Automated commit",
|
|
181
|
+
) -> dict:
|
|
182
|
+
"""
|
|
183
|
+
Commit the uncommitted changes with a given commit message.
|
|
184
|
+
"""
|
|
185
|
+
return await self.safe_request(
|
|
186
|
+
"post",
|
|
187
|
+
f"{self.base_url}/watcher/commit_changes",
|
|
188
|
+
json={"commit_message": commit_message},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def generate_diff(
|
|
192
|
+
self,
|
|
193
|
+
commit_a: Optional[str] = None,
|
|
194
|
+
commit_b: Optional[str] = None,
|
|
195
|
+
) -> dict:
|
|
196
|
+
"""
|
|
197
|
+
Generate the diff between two commits or between uncommitted changes
|
|
198
|
+
and the latest commit.
|
|
199
|
+
"""
|
|
200
|
+
return await self.safe_request(
|
|
201
|
+
"post",
|
|
202
|
+
f"{self.base_url}/watcher/generate_diff",
|
|
203
|
+
json={"commit_a": commit_a, "commit_b": commit_b},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
async def git_logs(self) -> dict:
|
|
207
|
+
"""
|
|
208
|
+
Retrieve the git logs.
|
|
209
|
+
"""
|
|
210
|
+
return await self.safe_request(
|
|
211
|
+
"get",
|
|
212
|
+
f"{self.base_url}/watcher/git_logs",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# -------- Workspace File APIs --------
|
|
216
|
+
|
|
217
|
+
async def get_workspace_file(self, file_path: str) -> dict:
|
|
218
|
+
"""
|
|
219
|
+
Retrieve a file from the /workspace directory.
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
endpoint = f"{self.base_url}/workspace/files"
|
|
223
|
+
params = {"file_path": file_path}
|
|
224
|
+
r = await self._request("get", endpoint, params=params)
|
|
225
|
+
r.raise_for_status()
|
|
226
|
+
|
|
227
|
+
# Check for empty content
|
|
228
|
+
if r.headers.get("Content-Length") == "0":
|
|
229
|
+
logger.warning(f"The file {file_path} is empty.")
|
|
230
|
+
return {"data": b""}
|
|
231
|
+
|
|
232
|
+
# Accumulate the content in chunks
|
|
233
|
+
file_content = bytearray()
|
|
234
|
+
async for chunk in r.aiter_bytes():
|
|
235
|
+
file_content.extend(chunk)
|
|
236
|
+
|
|
237
|
+
return {"data": bytes(file_content)}
|
|
238
|
+
except httpx.RequestError as e:
|
|
239
|
+
logger.error(f"An error occurred while retrieving the file: {e}")
|
|
240
|
+
return {
|
|
241
|
+
"isError": True,
|
|
242
|
+
"content": [{"type": "text", "text": str(e)}],
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async def create_or_edit_workspace_file(
|
|
246
|
+
self,
|
|
247
|
+
file_path: str,
|
|
248
|
+
content: str,
|
|
249
|
+
) -> dict:
|
|
250
|
+
"""
|
|
251
|
+
Create or edit a file within the /workspace directory.
|
|
252
|
+
"""
|
|
253
|
+
return await self.safe_request(
|
|
254
|
+
"post",
|
|
255
|
+
f"{self.base_url}/workspace/files",
|
|
256
|
+
params={"file_path": file_path},
|
|
257
|
+
json={"content": content},
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
async def list_workspace_directories(
|
|
261
|
+
self,
|
|
262
|
+
directory: str = "/workspace",
|
|
263
|
+
) -> dict:
|
|
264
|
+
"""
|
|
265
|
+
List files in the specified directory within the /workspace.
|
|
266
|
+
"""
|
|
267
|
+
return await self.safe_request(
|
|
268
|
+
"get",
|
|
269
|
+
f"{self.base_url}/workspace/list-directories",
|
|
270
|
+
params={"directory": directory},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async def create_workspace_directory(self, directory_path: str) -> dict:
|
|
274
|
+
"""
|
|
275
|
+
Create a directory within the /workspace directory.
|
|
276
|
+
"""
|
|
277
|
+
return await self.safe_request(
|
|
278
|
+
"post",
|
|
279
|
+
f"{self.base_url}/workspace/directories",
|
|
280
|
+
params={"directory_path": directory_path},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
async def delete_workspace_file(self, file_path: str) -> dict:
|
|
284
|
+
"""
|
|
285
|
+
Delete a file within the /workspace directory.
|
|
286
|
+
"""
|
|
287
|
+
return await self.safe_request(
|
|
288
|
+
"delete",
|
|
289
|
+
f"{self.base_url}/workspace/files",
|
|
290
|
+
params={"file_path": file_path},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
async def delete_workspace_directory(
|
|
294
|
+
self,
|
|
295
|
+
directory_path: str,
|
|
296
|
+
recursive: bool = False,
|
|
297
|
+
) -> dict:
|
|
298
|
+
"""
|
|
299
|
+
Delete a directory within the /workspace directory.
|
|
300
|
+
"""
|
|
301
|
+
return await self.safe_request(
|
|
302
|
+
"delete",
|
|
303
|
+
f"{self.base_url}/workspace/directories",
|
|
304
|
+
params={"directory_path": directory_path, "recursive": recursive},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
async def move_or_rename_workspace_item(
|
|
308
|
+
self,
|
|
309
|
+
source_path: str,
|
|
310
|
+
destination_path: str,
|
|
311
|
+
) -> dict:
|
|
312
|
+
"""
|
|
313
|
+
Move or rename a file or directory within the /workspace directory.
|
|
314
|
+
"""
|
|
315
|
+
return await self.safe_request(
|
|
316
|
+
"put",
|
|
317
|
+
f"{self.base_url}/workspace/move",
|
|
318
|
+
params={
|
|
319
|
+
"source_path": source_path,
|
|
320
|
+
"destination_path": destination_path,
|
|
321
|
+
},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
async def copy_workspace_item(
|
|
325
|
+
self,
|
|
326
|
+
source_path: str,
|
|
327
|
+
destination_path: str,
|
|
328
|
+
) -> dict:
|
|
329
|
+
"""
|
|
330
|
+
Copy a file or directory within the /workspace directory.
|
|
331
|
+
"""
|
|
332
|
+
return await self.safe_request(
|
|
333
|
+
"post",
|
|
334
|
+
f"{self.base_url}/workspace/copy",
|
|
335
|
+
params={
|
|
336
|
+
"source_path": source_path,
|
|
337
|
+
"destination_path": destination_path,
|
|
338
|
+
},
|
|
339
|
+
)
|