agentscope-runtime 0.1.5b1__py3-none-any.whl → 0.1.6__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.
Files changed (90) hide show
  1. agentscope_runtime/engine/agents/agentscope_agent.py +447 -0
  2. agentscope_runtime/engine/agents/agno_agent.py +19 -18
  3. agentscope_runtime/engine/agents/autogen_agent.py +13 -8
  4. agentscope_runtime/engine/agents/utils.py +53 -0
  5. agentscope_runtime/engine/deployers/__init__.py +0 -13
  6. agentscope_runtime/engine/deployers/local_deployer.py +501 -356
  7. agentscope_runtime/engine/helpers/helper.py +60 -41
  8. agentscope_runtime/engine/runner.py +11 -36
  9. agentscope_runtime/engine/schemas/agent_schemas.py +2 -70
  10. agentscope_runtime/engine/services/sandbox_service.py +62 -70
  11. agentscope_runtime/engine/services/tablestore_memory_service.py +304 -0
  12. agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
  13. agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
  14. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
  15. agentscope_runtime/sandbox/__init__.py +2 -0
  16. agentscope_runtime/sandbox/box/base/__init__.py +4 -0
  17. agentscope_runtime/sandbox/box/base/base_sandbox.py +4 -3
  18. agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
  19. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +8 -13
  20. agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
  21. agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
  22. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +8 -6
  23. agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
  24. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +80 -0
  25. agentscope_runtime/sandbox/box/sandbox.py +5 -2
  26. agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
  27. agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
  28. agentscope_runtime/sandbox/box/training_box/training_box.py +10 -15
  29. agentscope_runtime/sandbox/build.py +143 -58
  30. agentscope_runtime/sandbox/client/http_client.py +43 -49
  31. agentscope_runtime/sandbox/client/training_client.py +0 -1
  32. agentscope_runtime/sandbox/constant.py +24 -1
  33. agentscope_runtime/sandbox/custom/custom_sandbox.py +5 -5
  34. agentscope_runtime/sandbox/custom/example.py +2 -2
  35. agentscope_runtime/sandbox/enums.py +1 -0
  36. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +11 -6
  37. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +25 -9
  38. agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
  39. agentscope_runtime/sandbox/manager/container_clients/agentrun_client.py +1098 -0
  40. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +33 -205
  41. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +8 -555
  42. agentscope_runtime/sandbox/manager/sandbox_manager.py +187 -88
  43. agentscope_runtime/sandbox/manager/server/app.py +82 -14
  44. agentscope_runtime/sandbox/manager/server/config.py +50 -3
  45. agentscope_runtime/sandbox/model/container.py +6 -23
  46. agentscope_runtime/sandbox/model/manager_config.py +93 -5
  47. agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
  48. agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
  49. agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
  50. agentscope_runtime/sandbox/utils.py +124 -0
  51. agentscope_runtime/version.py +1 -1
  52. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/METADATA +168 -77
  53. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/RECORD +59 -78
  54. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/entry_points.txt +0 -1
  55. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
  56. agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
  57. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
  58. agentscope_runtime/engine/agents/llm_agent.py +0 -51
  59. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +0 -2886
  60. agentscope_runtime/engine/deployers/adapter/responses/response_api_agent_adapter.py +0 -51
  61. agentscope_runtime/engine/deployers/adapter/responses/response_api_protocol_adapter.py +0 -314
  62. agentscope_runtime/engine/deployers/cli_fc_deploy.py +0 -143
  63. agentscope_runtime/engine/deployers/kubernetes_deployer.py +0 -265
  64. agentscope_runtime/engine/deployers/modelstudio_deployer.py +0 -626
  65. agentscope_runtime/engine/deployers/utils/deployment_modes.py +0 -14
  66. agentscope_runtime/engine/deployers/utils/docker_image_utils/__init__.py +0 -8
  67. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +0 -429
  68. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +0 -240
  69. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +0 -297
  70. agentscope_runtime/engine/deployers/utils/package_project_utils.py +0 -932
  71. agentscope_runtime/engine/deployers/utils/service_utils/__init__.py +0 -9
  72. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +0 -504
  73. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_templates.py +0 -157
  74. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +0 -268
  75. agentscope_runtime/engine/deployers/utils/service_utils/service_config.py +0 -75
  76. agentscope_runtime/engine/deployers/utils/service_utils/service_factory.py +0 -220
  77. agentscope_runtime/engine/deployers/utils/wheel_packager.py +0 -389
  78. agentscope_runtime/engine/helpers/agent_api_builder.py +0 -651
  79. agentscope_runtime/engine/llms/__init__.py +0 -3
  80. agentscope_runtime/engine/llms/base_llm.py +0 -60
  81. agentscope_runtime/engine/llms/qwen_llm.py +0 -47
  82. agentscope_runtime/engine/schemas/embedding.py +0 -37
  83. agentscope_runtime/engine/schemas/modelstudio_llm.py +0 -310
  84. agentscope_runtime/engine/schemas/oai_llm.py +0 -538
  85. agentscope_runtime/engine/schemas/realtime.py +0 -254
  86. /agentscope_runtime/engine/{deployers/adapter/responses → services/utils}/__init__.py +0 -0
  87. /agentscope_runtime/{engine/deployers/utils → sandbox/box/gui/box}/__init__.py +0 -0
  88. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/WHEEL +0 -0
  89. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/licenses/LICENSE +0 -0
  90. {agentscope_runtime-0.1.5b1.dist-info → agentscope_runtime-0.1.6.dist-info}/top_level.txt +0 -0
@@ -6,11 +6,13 @@ import logging
6
6
 
7
7
  from typing import Optional
8
8
 
9
+ import httpx
9
10
  import websockets
11
+
10
12
  from fastapi import FastAPI, HTTPException, Request, Depends
11
13
  from fastapi import WebSocket, WebSocketDisconnect
12
14
  from fastapi.middleware.cors import CORSMiddleware
13
- from fastapi.responses import JSONResponse
15
+ from fastapi.responses import JSONResponse, Response
14
16
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
17
 
16
18
  from ...manager.server.config import get_settings
@@ -20,6 +22,7 @@ from ...manager.server.models import (
20
22
  )
21
23
  from ...manager.sandbox_manager import SandboxManager
22
24
  from ...model.manager_config import SandboxManagerEnvConfig
25
+ from ...utils import dynamic_import, http_to_ws
23
26
  from ....version import __version__
24
27
 
25
28
  # Configure logging
@@ -61,6 +64,7 @@ def get_config() -> SandboxManagerEnvConfig:
61
64
  redis_enabled=settings.REDIS_ENABLED,
62
65
  container_deployment=settings.CONTAINER_DEPLOYMENT,
63
66
  default_mount_dir=settings.DEFAULT_MOUNT_DIR,
67
+ readonly_mounts=settings.READONLY_MOUNTS,
64
68
  storage_folder=settings.STORAGE_FOLDER,
65
69
  port_range=settings.PORT_RANGE,
66
70
  pool_size=settings.POOL_SIZE,
@@ -77,6 +81,18 @@ def get_config() -> SandboxManagerEnvConfig:
77
81
  redis_container_pool_key=settings.REDIS_CONTAINER_POOL_KEY,
78
82
  k8s_namespace=settings.K8S_NAMESPACE,
79
83
  kubeconfig_path=settings.KUBECONFIG_PATH,
84
+ agent_run_access_key_id=settings.AGENT_RUN_ACCESS_KEY_ID,
85
+ agent_run_access_key_secret=settings.AGENT_RUN_ACCESS_KEY_SECRET,
86
+ agent_run_account_id=settings.AGENT_RUN_ACCOUNT_ID,
87
+ agent_run_region_id=settings.AGENT_RUN_REGION_ID,
88
+ agent_run_cpu=settings.AGENT_RUN_CPU,
89
+ agent_run_memory=settings.AGENT_RUN_MEMORY,
90
+ agent_run_vpc_id=settings.AGENT_RUN_VPC_ID,
91
+ agent_run_vswitch_ids=settings.AGENT_RUN_VSWITCH_IDS,
92
+ agent_run_security_group_id=settings.AGENT_RUN_SECURITY_GROUP_ID,
93
+ agent_run_prefix=settings.AGENT_RUN_PREFIX,
94
+ agentrun_log_project=settings.AGENT_RUN_LOG_PROJECT,
95
+ agentrun_log_store=settings.AGENT_RUN_LOG_STORE,
80
96
  )
81
97
  return _config
82
98
 
@@ -134,15 +150,21 @@ def create_endpoint(method):
134
150
  logger.info(
135
151
  f"Calling {method.__name__} with data: {data}",
136
152
  )
137
- result = method(**data)
153
+
154
+ # Check if the method is asynchronous
155
+ if inspect.iscoroutinefunction(method):
156
+ # If async, just await it
157
+ result = await method(**data)
158
+ else:
159
+ # If sync, run it in a thread to avoid blocking the event loop
160
+ result = await asyncio.to_thread(method, **data)
161
+
138
162
  if hasattr(result, "model_dump_json"):
139
163
  return JSONResponse(content={"data": result.model_dump_json()})
140
164
  return JSONResponse(content={"data": result})
165
+
141
166
  except Exception as e:
142
- error = (
143
- f"Error in {method.__name__}: {str(e)},"
144
- # f" {traceback.format_exc()}"
145
- )
167
+ error = f"Error in {method.__name__}: {str(e)}"
146
168
  logger.error(error)
147
169
  raise HTTPException(status_code=500, detail=error) from e
148
170
 
@@ -194,11 +216,38 @@ async def health_check():
194
216
  """Health check endpoint"""
195
217
  return HealthResponse(
196
218
  status="healthy",
197
- version=_sandbox_manager.default_type.value,
219
+ version=str(_sandbox_manager.default_type),
198
220
  )
199
221
 
200
222
 
201
- @app.websocket("/browser/{sandbox_id}/cast")
223
+ @app.get("/desktop/{sandbox_id}/{path:path}")
224
+ async def proxy_vnc_static(sandbox_id: str, path: str):
225
+ container_json = _sandbox_manager.container_mapping.get(sandbox_id)
226
+ if not container_json:
227
+ return Response(status_code=404)
228
+
229
+ base_url = container_json.get("url")
230
+ if not base_url:
231
+ return Response(status_code=404)
232
+
233
+ target_url = f"{base_url}/{path}"
234
+
235
+ try:
236
+ async with httpx.AsyncClient() as client:
237
+ upstream = await client.get(target_url)
238
+ return Response(
239
+ content=upstream.content,
240
+ media_type=upstream.headers.get("content-type"),
241
+ )
242
+ except httpx.RequestError as exc:
243
+ logger.error(f"Upstream request to {target_url} failed: {repr(exc)}")
244
+ return JSONResponse(
245
+ status_code=502,
246
+ content={"detail": f"Upstream request failed: {str(exc)}"},
247
+ )
248
+
249
+
250
+ @app.websocket("/desktop/{sandbox_id}")
202
251
  async def websocket_endpoint(
203
252
  websocket: WebSocket,
204
253
  sandbox_id: str,
@@ -212,7 +261,7 @@ async def websocket_endpoint(
212
261
  )
213
262
  service_address = None
214
263
  if container_json:
215
- service_address = container_json.get("front_browser_ws")
264
+ service_address = http_to_ws(f"{container_json.get('url')}/websockify")
216
265
 
217
266
  logger.debug(f"service_address: {service_address}")
218
267
 
@@ -237,8 +286,17 @@ async def websocket_endpoint(
237
286
  # Forward messages from client to target server
238
287
  async def forward_to_service():
239
288
  try:
240
- async for message in websocket.iter_text():
241
- await target_ws.send(message)
289
+ while True:
290
+ message = await websocket.receive()
291
+
292
+ if message["type"] == "websocket.receive":
293
+ if "bytes" in message:
294
+ await target_ws.send(message["bytes"])
295
+ elif "text" in message:
296
+ await target_ws.send(message["text"])
297
+ elif message["type"] == "websocket.disconnect":
298
+ break
299
+
242
300
  except WebSocketDisconnect:
243
301
  logger.debug(
244
302
  f"WebSocket disconnected from client for sandbox"
@@ -266,9 +324,6 @@ async def websocket_endpoint(
266
324
  await websocket.close()
267
325
 
268
326
 
269
- # TODO: add socketio relay endpoint for filesystem
270
-
271
-
272
327
  def setup_logging(log_level: str):
273
328
  """Setup logging configuration based on log level"""
274
329
  # Convert string to logging level
@@ -311,8 +366,21 @@ def main():
311
366
  default="INFO",
312
367
  help="Set the logging level (default: INFO)",
313
368
  )
369
+
370
+ parser.add_argument(
371
+ "--extension",
372
+ action="append",
373
+ help="Path to a Python file or module name to load as an extension",
374
+ )
375
+
314
376
  args = parser.parse_args()
315
377
 
378
+ if args.extension:
379
+ for ext in args.extension:
380
+ logger.info(f"Loading extension: {ext}")
381
+ mod = dynamic_import(ext)
382
+ logger.info(f"Extension loaded: {mod.__name__}")
383
+
316
384
  # Setup logging based on command line argument
317
385
  setup_logging(args.log_level)
318
386
 
@@ -1,6 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import os
3
- from typing import Optional, Tuple, Literal
3
+ import json
4
+
5
+ from typing import Optional, Tuple, Literal, Dict, Union, List
4
6
  from pydantic_settings import BaseSettings
5
7
  from pydantic import field_validator, ConfigDict
6
8
  from dotenv import load_dotenv
@@ -17,12 +19,22 @@ class Settings(BaseSettings):
17
19
  BEARER_TOKEN: Optional[str] = None
18
20
 
19
21
  # Runtime Manager settings
20
- DEFAULT_SANDBOX_TYPE: str = "base"
22
+ DEFAULT_SANDBOX_TYPE: Union[str, List[str]] = "base"
21
23
  POOL_SIZE: int = 1
22
24
  AUTO_CLEANUP: bool = True
23
25
  CONTAINER_PREFIX_KEY: str = "runtime_sandbox_container_"
24
- CONTAINER_DEPLOYMENT: Literal["docker", "cloud", "k8s"] = "docker"
26
+ CONTAINER_DEPLOYMENT: Literal[
27
+ "docker",
28
+ "cloud",
29
+ "k8s",
30
+ "agentrun",
31
+ ] = "docker"
25
32
  DEFAULT_MOUNT_DIR: str = "sessions_mount_dir"
33
+ # Read-only mounts (host_path -> container_path)
34
+ # Example in .env:
35
+ # READONLY_MOUNTS={"\/opt\/shared": "\/mnt\/shared", "\/etc\/timezone":
36
+ # "\/etc\/timezone"}
37
+ READONLY_MOUNTS: Optional[Dict[str, str]] = None
26
38
  STORAGE_FOLDER: str = "runtime_sandbox_storage"
27
39
  PORT_RANGE: Tuple[int, int] = (49152, 59152)
28
40
 
@@ -47,6 +59,24 @@ class Settings(BaseSettings):
47
59
  K8S_NAMESPACE: str = "default"
48
60
  KUBECONFIG_PATH: Optional[str] = None
49
61
 
62
+ # AgentRun settings
63
+ AGENT_RUN_ACCOUNT_ID: Optional[str] = None
64
+ AGENT_RUN_ACCESS_KEY_ID: Optional[str] = None
65
+ AGENT_RUN_ACCESS_KEY_SECRET: Optional[str] = None
66
+ AGENT_RUN_REGION_ID: str = "cn-hangzhou"
67
+
68
+ AGENT_RUN_CPU: float = 2.0
69
+ AGENT_RUN_MEMORY: int = 2048
70
+
71
+ AGENT_RUN_VPC_ID: Optional[str] = None
72
+ AGENT_RUN_VSWITCH_IDS: Optional[list[str]] = None
73
+ AGENT_RUN_SECURITY_GROUP_ID: Optional[str] = None
74
+
75
+ AGENT_RUN_PREFIX: str = "agentscope-sandbox"
76
+
77
+ AGENT_RUN_LOG_PROJECT: Optional[str] = None
78
+ AGENT_RUN_LOG_STORE: Optional[str] = None
79
+
50
80
  model_config = ConfigDict(
51
81
  case_sensitive=True,
52
82
  extra="allow",
@@ -59,6 +89,23 @@ class Settings(BaseSettings):
59
89
  return 1
60
90
  return value
61
91
 
92
+ @field_validator("DEFAULT_SANDBOX_TYPE", mode="before")
93
+ @classmethod
94
+ def parse_default_type(cls, v):
95
+ if isinstance(v, str):
96
+ v = v.strip()
97
+ if v.startswith("[") and v.endswith("]"):
98
+ try:
99
+ return json.loads(v)
100
+ except json.JSONDecodeError:
101
+ return [
102
+ item.strip()
103
+ for item in v[1:-1].split(",")
104
+ if item.strip()
105
+ ]
106
+ return [item.strip() for item in v.split(",") if item.strip()]
107
+ return v
108
+
62
109
 
63
110
  _settings: Optional[Settings] = None
64
111
 
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
- from typing import List
2
+ from typing import List, Optional, Dict
3
3
 
4
4
  from pydantic import BaseModel, Field
5
5
 
@@ -20,31 +20,12 @@ class ContainerModel(BaseModel):
20
20
  description="Human-readable name for the container",
21
21
  )
22
22
 
23
- base_url: str = Field(
23
+ url: str = Field(
24
24
  ...,
25
- description="Base URL for accessing the container",
25
+ description="URL for accessing the container",
26
26
  )
27
27
 
28
- browser_url: str = Field(
29
- ...,
30
- description="URL for browser interface within the container",
31
- )
32
-
33
- front_browser_ws: str = Field(
34
- ...,
35
- description="WebSocket URL for the browser used by frontend",
36
- )
37
-
38
- client_browser_ws: str = Field(
39
- ...,
40
- description="WebSocket URL for the browser used by runtime client",
41
- )
42
-
43
- artifacts_sio: str = Field(
44
- ...,
45
- description="Socketio URL for the artifacts used by frontend",
46
- )
47
- ports: List[int] = Field(
28
+ ports: List[int | str] = Field(
48
29
  ...,
49
30
  description="List of occupied port numbers",
50
31
  )
@@ -70,5 +51,7 @@ class ContainerModel(BaseModel):
70
51
  description="Image version of the container",
71
52
  )
72
53
 
54
+ meta: Optional[Dict] = Field(default_factory=dict)
55
+
73
56
  class Config:
74
57
  extra = "allow"
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # pylint: disable=no-self-argument
3
3
  import os
4
- from typing import Optional, Literal, Tuple
4
+ from typing import Optional, Literal, Tuple, Dict
5
5
  from pydantic import BaseModel, Field, model_validator
6
6
 
7
7
 
@@ -27,10 +27,15 @@ class SandboxManagerEnvConfig(BaseModel):
27
27
  ...,
28
28
  description="Indicates if Redis is enabled.",
29
29
  )
30
- container_deployment: Literal["docker", "cloud", "k8s"] = Field(
30
+ container_deployment: Literal[
31
+ "docker",
32
+ "cloud",
33
+ "k8s",
34
+ "agentrun",
35
+ ] = Field(
31
36
  ...,
32
- description="Container deployment backend: 'docker', 'cloud', "
33
- "or 'k8s'.",
37
+ description="Container deployment backend: 'docker', 'cloud', 'k8s'"
38
+ "or 'agentrun'.",
34
39
  )
35
40
 
36
41
  default_mount_dir: Optional[str] = Field(
@@ -38,6 +43,13 @@ class SandboxManagerEnvConfig(BaseModel):
38
43
  description="Path for local file system storage.",
39
44
  )
40
45
 
46
+ readonly_mounts: Optional[Dict[str, str]] = Field(
47
+ default=None,
48
+ description="Read-only mount mapping: host_path -> container_path. "
49
+ "Example: { '/data/shared': '/mnt/shared', '/etc/timezone': "
50
+ "'/etc/timezone' }",
51
+ )
52
+
41
53
  port_range: Tuple[int, int] = Field(
42
54
  (49152, 59152),
43
55
  description="Range of ports to be used by the manager.",
@@ -112,8 +124,64 @@ class SandboxManagerEnvConfig(BaseModel):
112
124
  "in-cluster config or default kubeconfig.",
113
125
  )
114
126
 
127
+ # AgentRun settings
128
+ agent_run_access_key_id: Optional[str] = Field(
129
+ None,
130
+ description="Access key ID for AgentRun. Required if "
131
+ "container_deployment is 'agentrun'.",
132
+ )
133
+ agent_run_access_key_secret: Optional[str] = Field(
134
+ None,
135
+ description="Access key secret for AgentRun. "
136
+ "Required if container_deployment is 'agentrun'.",
137
+ )
138
+ agent_run_account_id: Optional[str] = Field(
139
+ None,
140
+ description="Account ID for AgentRun. Required if "
141
+ "container_deployment is 'agentrun'.",
142
+ )
143
+ agent_run_region_id: str = Field(
144
+ "cn-hangzhou",
145
+ description="Region ID for AgentRun.",
146
+ )
147
+ agent_run_cpu: float = Field(
148
+ 2,
149
+ description="CPU allocation for AgentRun containers.",
150
+ )
151
+ agent_run_memory: int = Field(
152
+ 2048,
153
+ description="Memory allocation for AgentRun containers in MB.",
154
+ )
155
+ agent_run_vpc_id: Optional[str] = Field(
156
+ None,
157
+ description="VPC ID for AgentRun. Required if container_deployment "
158
+ "is 'agentrun'.",
159
+ )
160
+ agent_run_vswitch_ids: Optional[list[str]] = Field(
161
+ None,
162
+ description="VSwitch IDs for AgentRun. Required if "
163
+ "container_deployment is 'agentrun'.",
164
+ )
165
+ agent_run_security_group_id: Optional[str] = Field(
166
+ None,
167
+ description="Security group ID for AgentRun. "
168
+ "Required if container_deployment is 'agentrun'.",
169
+ )
170
+ agent_run_prefix: str = Field(
171
+ "agentscope-sandbox_",
172
+ description="Prefix for AgentRun resources.",
173
+ )
174
+ agentrun_log_project: Optional[str] = Field(
175
+ None,
176
+ description="Log project for AgentRun.",
177
+ )
178
+ agentrun_log_store: Optional[str] = Field(
179
+ None,
180
+ description="Log store for AgentRun.",
181
+ )
182
+
115
183
  @model_validator(mode="after")
116
- def check_settings(cls, self):
184
+ def check_settings(self):
117
185
  if self.default_mount_dir:
118
186
  os.makedirs(self.default_mount_dir, exist_ok=True)
119
187
 
@@ -161,4 +229,24 @@ class SandboxManagerEnvConfig(BaseModel):
161
229
  f"{field_name} must be set when redis is enabled",
162
230
  )
163
231
 
232
+ if self.container_deployment == "agentrun":
233
+ required_agentrun_fields = [
234
+ self.agent_run_access_key_id,
235
+ self.agent_run_access_key_secret,
236
+ self.agent_run_account_id,
237
+ ]
238
+ for field_name, field_value in zip(
239
+ [
240
+ "agent_run_access_key_id",
241
+ "agent_run_access_key_secret",
242
+ "agent_run_account_id",
243
+ ],
244
+ required_agentrun_fields,
245
+ ):
246
+ if not field_value:
247
+ raise ValueError(
248
+ f"{field_name} must be set when "
249
+ f"container_deployment is 'agentrun'",
250
+ )
251
+
164
252
  return self
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ from .tool import computer_use
3
+
4
+
5
+ __all__ = [
6
+ "computer_use",
7
+ ]
@@ -0,0 +1,77 @@
1
+ # -*- coding: utf-8 -*-
2
+ # flake8: noqa: E501
3
+ # pylint: disable=line-too-long
4
+ from typing import Dict
5
+
6
+ from ..sandbox_tool import SandboxTool
7
+
8
+
9
+ class ComputerUseTool(SandboxTool):
10
+ """
11
+ Use a mouse and keyboard to interact with a computer, and take screenshots.
12
+ """
13
+
14
+ _name: str = "computer"
15
+ _sandbox_type: str = "gui"
16
+ _tool_type: str = "computer_use"
17
+ _schema: Dict = {
18
+ "name": "computer",
19
+ "description": (
20
+ "Use a mouse and keyboard to interact with a computer, and take screenshots.\n"
21
+ "* This is an interface to a desktop GUI. You do not have access to a terminal or applications menu. You must click on desktop icons to start applications.\n"
22
+ "* Always prefer using keyboard shortcuts rather than clicking, where possible.\n"
23
+ "* If you see boxes with two letters in them, typing these letters will click that element. Use this instead of other shortcuts or clicking, where possible.\n"
24
+ "* Some applications may take time to start or process actions, so you may need to wait and take successive screenshots to see the results of your actions. E.g. if you click on Firefox and a window doesn't open, try taking another screenshot.\n"
25
+ "* Whenever you intend to move the cursor to click on an element like an icon, you should consult a screenshot to determine the coordinates of the element before moving the cursor.\n"
26
+ "* If you tried clicking on a program or link but it failed to load, even after waiting, try adjusting your cursor position so that the tip of the cursor visually falls on the element that you want to click.\n"
27
+ "* Make sure to click any buttons, links, icons, etc with the cursor tip in the center of the element. Don't click boxes on their edges unless asked."
28
+ ),
29
+ "parameters": {
30
+ "type": "object",
31
+ "properties": {
32
+ "action": {
33
+ "type": "string",
34
+ "enum": [
35
+ "key",
36
+ "type",
37
+ "mouse_move",
38
+ "left_click",
39
+ "left_click_drag",
40
+ "right_click",
41
+ "middle_click",
42
+ "double_click",
43
+ "get_screenshot",
44
+ "get_cursor_position",
45
+ ],
46
+ "description": (
47
+ "The action to perform. The available actions are:\n"
48
+ "* key: Press a key or key-combination on the keyboard.\n"
49
+ "* type: Type a string of text on the keyboard.\n"
50
+ "* get_cursor_position: Get the current (x, y) pixel coordinate of the cursor on the screen.\n"
51
+ "* mouse_move: Move the cursor to a specified (x, y) pixel coordinate on the screen.\n"
52
+ "* left_click: Click the left mouse button.\n"
53
+ "* left_click_drag: Click and drag the cursor to a specified (x, y) pixel coordinate on the screen.\n"
54
+ "* right_click: Click the right mouse button.\n"
55
+ "* middle_click: Click the middle mouse button.\n"
56
+ "* double_click: Double-click the left mouse button.\n"
57
+ "* get_screenshot: Take a screenshot of the screen."
58
+ ),
59
+ },
60
+ "coordinate": {
61
+ "type": "array",
62
+ "items": {"type": "number"},
63
+ "minItems": 2,
64
+ "maxItems": 2,
65
+ "description": "(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates",
66
+ },
67
+ "text": {
68
+ "type": "string",
69
+ "description": "Text to type or key command to execute",
70
+ },
71
+ },
72
+ "required": ["action"],
73
+ },
74
+ }
75
+
76
+
77
+ computer_use = ComputerUseTool()
@@ -119,6 +119,7 @@ class MCPConfigConverter:
119
119
  self,
120
120
  *,
121
121
  sandbox: Optional[Sandbox] = None,
122
+ sandbox_type: Optional[SandboxType | str] = None,
122
123
  ) -> List[MCPTool]:
123
124
  """
124
125
  Converts to a list of MCPTool instances.
@@ -130,7 +131,10 @@ class MCPConfigConverter:
130
131
  if box is None:
131
132
  from ..registry import SandboxRegistry
132
133
 
133
- cls_ = SandboxRegistry.get_classes_by_type("base")
134
+ if sandbox_type is None:
135
+ sandbox_type = SandboxType.BASE
136
+
137
+ cls_ = SandboxRegistry.get_classes_by_type(sandbox_type)
134
138
  # Use proper context manager
135
139
  with cls_() as tmp_box:
136
140
  return self._process_tools(tmp_box)
@@ -156,7 +160,7 @@ class MCPConfigConverter:
156
160
  MCPTool(
157
161
  sandbox=box,
158
162
  name=tool_name,
159
- sandbox_type=SandboxType.BASE,
163
+ sandbox_type=box.sandbox_type,
160
164
  tool_type=server_name,
161
165
  schema=tool_info["json_schema"]["function"],
162
166
  server_configs={
@@ -0,0 +1,124 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import importlib
4
+ import platform
5
+
6
+ from urllib.parse import urlparse, urlunparse
7
+
8
+ from .constant import REGISTRY, IMAGE_NAMESPACE, IMAGE_TAG
9
+
10
+
11
+ def build_image_uri(
12
+ image_name: str,
13
+ tag: str = None,
14
+ registry: str = None,
15
+ namespace: str = None,
16
+ arm64_compatible: bool = True,
17
+ ) -> str:
18
+ """
19
+ Build a fully qualified Docker image URI.
20
+
21
+ Parameters
22
+ ----------
23
+ image_name : str
24
+ Name of the Docker image without registry/namespace.
25
+ Example: `"runtime-sandbox-base"`.
26
+ tag : str, optional
27
+ Base image tag. Defaults to the global ``IMAGE_TAG`` if not provided.
28
+ registry : str, optional
29
+ Docker registry address. Defaults to the global ``REGISTRY``.
30
+ If empty or whitespace, registry prefix will be omitted.
31
+ namespace : str, optional
32
+ Docker image namespace. Defaults to the global ``IMAGE_NAMESPACE``.
33
+ arm64_compatible : bool, optional
34
+ Whether the image is ARM64-compatible without special tagging.
35
+ If ``True`` (default), the tag is returned unchanged.
36
+ If ``False``, the function will detect the current machine
37
+ architecture and append ``-arm64`` to the tag if running on ARM64 (
38
+ e.g., ``arm64``, ``aarch64``).
39
+
40
+ Returns
41
+ -------
42
+ str
43
+ Fully qualified Docker image URI in the form:
44
+ ``<registry>/<namespace>/<image_name>:<tag>`` or
45
+ ``<namespace>/<image_name>:<tag>`` if registry is omitted.
46
+
47
+ Examples
48
+ --------
49
+ >>> build_image_uri("runtime-sandbox-base")
50
+ 'agentscope-registry.ap-southeast-1.cr.aliyuncs.com/agentscope/runtime-sandbox-base:latest'
51
+
52
+ >>> build_image_uri("runtime-sandbox-base", tag="v1.2.3")
53
+ 'agentscope-registry.ap-southeast-1.cr.aliyuncs.com/agentscope/runtime-sandbox-base:v1.2.3'
54
+
55
+ >>> build_image_uri("runtime-sandbox-base", registry="")
56
+ 'agentscope/runtime-sandbox-base:latest'
57
+
58
+ >>> build_image_uri("runtime-sandbox-base", arm64_compatible=False)
59
+ 'agentscope-registry.ap-southeast-1.cr.aliyuncs.com/agentscope/runtime
60
+ -sandbox-base:latest-arm64'
61
+ # (Above example assumes running on ARM64 machine)
62
+ """
63
+ reg = registry if registry is not None else REGISTRY
64
+ reg = "" if reg.strip() == "" else f"{reg.strip()}/"
65
+
66
+ final_namespace = namespace if namespace is not None else IMAGE_NAMESPACE
67
+ final_tag = tag or IMAGE_TAG
68
+
69
+ # TODO: make manifest compatible and remove this
70
+ # Adjust tag based on ARM64 compatibility
71
+ if not arm64_compatible:
72
+ machine = platform.machine().lower()
73
+ if "arm" in machine or "aarch64" in machine:
74
+ final_tag = f"{final_tag}-arm64"
75
+
76
+ return f"{reg}{final_namespace}/{image_name}:{final_tag}"
77
+
78
+
79
+ def dynamic_import(ext: str):
80
+ """
81
+ Dynamically import a Python file or module.
82
+
83
+ Parameters
84
+ ----------
85
+ ext : str
86
+ File path to a Python script OR a module name to import.
87
+
88
+ Returns
89
+ -------
90
+ module : object
91
+ The imported Python module/object.
92
+ """
93
+ if os.path.isfile(ext):
94
+ module_name = os.path.splitext(os.path.basename(ext))[0]
95
+ spec = importlib.util.spec_from_file_location(module_name, ext)
96
+ module = importlib.util.module_from_spec(spec)
97
+ spec.loader.exec_module(module)
98
+ return module
99
+ else:
100
+ return importlib.import_module(ext)
101
+
102
+
103
+ def http_to_ws(url, use_localhost=True):
104
+ parsed = urlparse(url)
105
+ ws_scheme = "wss" if parsed.scheme == "https" else "ws"
106
+
107
+ hostname = parsed.hostname
108
+ if use_localhost and hostname == "127.0.0.1":
109
+ hostname = "localhost"
110
+
111
+ if parsed.port:
112
+ new_netloc = f"{hostname}:{parsed.port}"
113
+ else:
114
+ new_netloc = hostname
115
+
116
+ ws_url = urlunparse(parsed._replace(scheme=ws_scheme, netloc=new_netloc))
117
+ return ws_url
118
+
119
+
120
+ def get_platform():
121
+ machine = platform.machine().lower()
122
+ if "arm" in machine or "aarch64" in machine:
123
+ return "linux/arm64"
124
+ return "linux/amd64"
@@ -1,2 +1,2 @@
1
1
  # -*- coding: utf-8 -*-
2
- __version__ = "0.1.5b1"
2
+ __version__ = "v0.1.6"