agentscope-runtime 0.1.0__py3-none-any.whl → 0.1.2__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 (36) hide show
  1. agentscope_runtime/engine/agents/agentscope_agent/agent.py +1 -0
  2. agentscope_runtime/engine/agents/agno_agent.py +1 -0
  3. agentscope_runtime/engine/agents/autogen_agent.py +245 -0
  4. agentscope_runtime/engine/schemas/agent_schemas.py +1 -1
  5. agentscope_runtime/engine/services/context_manager.py +28 -1
  6. agentscope_runtime/engine/services/memory_service.py +2 -2
  7. agentscope_runtime/engine/services/rag_service.py +101 -0
  8. agentscope_runtime/engine/services/redis_memory_service.py +187 -0
  9. agentscope_runtime/engine/services/redis_session_history_service.py +155 -0
  10. agentscope_runtime/sandbox/box/training_box/env_service.py +1 -1
  11. agentscope_runtime/sandbox/box/training_box/environments/bfcl/bfcl_dataprocess.py +216 -0
  12. agentscope_runtime/sandbox/box/training_box/environments/bfcl/bfcl_env.py +380 -0
  13. agentscope_runtime/sandbox/box/training_box/environments/bfcl/env_handler.py +934 -0
  14. agentscope_runtime/sandbox/box/training_box/training_box.py +139 -9
  15. agentscope_runtime/sandbox/build.py +1 -1
  16. agentscope_runtime/sandbox/custom/custom_sandbox.py +0 -1
  17. agentscope_runtime/sandbox/custom/example.py +0 -1
  18. agentscope_runtime/sandbox/enums.py +2 -0
  19. agentscope_runtime/sandbox/manager/container_clients/__init__.py +2 -0
  20. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +263 -11
  21. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +605 -0
  22. agentscope_runtime/sandbox/manager/sandbox_manager.py +112 -113
  23. agentscope_runtime/sandbox/manager/server/app.py +96 -28
  24. agentscope_runtime/sandbox/manager/server/config.py +28 -16
  25. agentscope_runtime/sandbox/model/__init__.py +1 -5
  26. agentscope_runtime/sandbox/model/container.py +3 -1
  27. agentscope_runtime/sandbox/model/manager_config.py +21 -15
  28. agentscope_runtime/sandbox/tools/tool.py +111 -0
  29. agentscope_runtime/version.py +1 -1
  30. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/METADATA +79 -13
  31. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/RECORD +35 -28
  32. agentscope_runtime/sandbox/manager/utils.py +0 -78
  33. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/WHEEL +0 -0
  34. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/entry_points.txt +0 -0
  35. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/licenses/LICENSE +0 -0
  36. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.2.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ with specific configuration and tool calling methods.
7
7
  """
8
8
  import platform
9
9
  from typing import Dict, Optional
10
+ import os
10
11
 
11
12
  from ...registry import SandboxRegistry
12
13
  from ...enums import SandboxType
@@ -21,14 +22,6 @@ def get_image_tag() -> str:
21
22
  return IMAGE_TAG
22
23
 
23
24
 
24
- @SandboxRegistry.register(
25
- f"agentscope/runtime-sandbox-appworld:{get_image_tag()}",
26
- sandbox_type=SandboxType.APPWORLD,
27
- runtime_config={"shm_size": "5.06gb"},
28
- security_level="medium",
29
- timeout=30,
30
- description="appworld Sandbox",
31
- )
32
25
  class TrainingSandbox(Sandbox):
33
26
  """
34
27
  Training Sandbox class for managing and executing training-related tasks.
@@ -43,6 +36,7 @@ class TrainingSandbox(Sandbox):
43
36
  timeout: int = 3000,
44
37
  base_url: Optional[str] = None,
45
38
  bearer_token: Optional[str] = None,
39
+ box_type: SandboxType = SandboxType.APPWORLD,
46
40
  ):
47
41
  """
48
42
  Initialize the Training Sandbox.
@@ -58,7 +52,7 @@ class TrainingSandbox(Sandbox):
58
52
  timeout,
59
53
  base_url,
60
54
  bearer_token,
61
- SandboxType.APPWORLD,
55
+ box_type,
62
56
  )
63
57
 
64
58
  def create_instance(
@@ -217,3 +211,139 @@ class TrainingSandbox(Sandbox):
217
211
  "instance_id": instance_id,
218
212
  },
219
213
  )
214
+
215
+
216
+ @SandboxRegistry.register(
217
+ f"agentscope/runtime-sandbox-appworld:{get_image_tag()}",
218
+ sandbox_type=SandboxType.APPWORLD,
219
+ runtime_config={"shm_size": "5.06gb"},
220
+ security_level="medium",
221
+ timeout=30,
222
+ description="appworld Sandbox",
223
+ )
224
+ class APPWorldSandbox(TrainingSandbox):
225
+ """
226
+ Training Sandbox class for managing and executing training-related tasks.
227
+
228
+ This class provides methods to create, manage, and interact with
229
+ training environment instances using specialized tool calls.
230
+ """
231
+
232
+ def __init__(
233
+ self,
234
+ sandbox_id: Optional[str] = None,
235
+ timeout: int = 3000,
236
+ base_url: Optional[str] = None,
237
+ bearer_token: Optional[str] = None,
238
+ ):
239
+ """
240
+ Initialize the Training Sandbox.
241
+
242
+ Args:
243
+ sandbox_id (Optional[str]): Unique identifier for the sandbox.
244
+ timeout (int): Maximum time allowed for sandbox operations.
245
+ base_url (Optional[str]): Base URL for sandbox API.
246
+ bearer_token (Optional[str]): Authentication token for API access.
247
+ """
248
+ super().__init__(
249
+ sandbox_id,
250
+ timeout,
251
+ base_url,
252
+ bearer_token,
253
+ SandboxType.APPWORLD,
254
+ )
255
+
256
+
257
+ DATASET_SUB_TYPE = os.environ.get("DATASET_SUB_TYPE", "multi_turn")
258
+
259
+
260
+ @SandboxRegistry.register(
261
+ f"agentscope/runtime-sandbox-bfcl:{get_image_tag()}",
262
+ sandbox_type=SandboxType.BFCL,
263
+ runtime_config={"shm_size": "8.06gb"},
264
+ security_level="medium",
265
+ environment={
266
+ "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY", ""),
267
+ "BFCL_DATA_PATH": f"/agentscope_runtime/training_box/bfcl/multi_turn/"
268
+ f"{DATASET_SUB_TYPE}_processed.jsonl",
269
+ "BFCL_SPLID_ID_PATH": f"/agentscope_runtime/training_box/"
270
+ f"bfcl/multi_turn/"
271
+ f"{DATASET_SUB_TYPE}_split_ids.json",
272
+ },
273
+ # ["all","all_scoring","multi_turn","single_turn",
274
+ # "live","non_live","non_python","python"]
275
+ timeout=30,
276
+ description="bfcl Sandbox",
277
+ )
278
+ class BFCLSandbox(TrainingSandbox):
279
+ """
280
+ Training Sandbox class for managing and executing training-related tasks.
281
+
282
+ This class provides methods to create, manage, and interact with
283
+ training environment instances using specialized tool calls.
284
+ """
285
+
286
+ def __init__(
287
+ self,
288
+ sandbox_id: Optional[str] = None,
289
+ timeout: int = 3000,
290
+ base_url: Optional[str] = None,
291
+ bearer_token: Optional[str] = None,
292
+ ):
293
+ """
294
+ Initialize the Training Sandbox.
295
+
296
+ Args:
297
+ sandbox_id (Optional[str]): Unique identifier for the sandbox.
298
+ timeout (int): Maximum time allowed for sandbox operations.
299
+ base_url (Optional[str]): Base URL for sandbox API.
300
+ bearer_token (Optional[str]): Authentication token for API access.
301
+ """
302
+ super().__init__(
303
+ sandbox_id,
304
+ timeout,
305
+ base_url,
306
+ bearer_token,
307
+ SandboxType.BFCL,
308
+ )
309
+
310
+
311
+ @SandboxRegistry.register(
312
+ f"agentscope/runtime-sandbox-webshop:{get_image_tag()}",
313
+ sandbox_type=SandboxType.WEBSHOP,
314
+ runtime_config={"shm_size": "5.06gb"},
315
+ security_level="medium",
316
+ timeout=30,
317
+ description="webshop Sandbox",
318
+ )
319
+ class WebShopSandbox(TrainingSandbox):
320
+ """
321
+ Training Sandbox class for managing and executing training-related tasks.
322
+
323
+ This class provides methods to create, manage, and interact with
324
+ training environment instances using specialized tool calls.
325
+ """
326
+
327
+ def __init__(
328
+ self,
329
+ sandbox_id: Optional[str] = None,
330
+ timeout: int = 3000,
331
+ base_url: Optional[str] = None,
332
+ bearer_token: Optional[str] = None,
333
+ ):
334
+ """
335
+ Initialize the Training Sandbox.
336
+
337
+ Args:
338
+ sandbox_id (Optional[str]): Unique identifier for the sandbox.
339
+ timeout (int): Maximum time allowed for sandbox operations.
340
+ base_url (Optional[str]): Base URL for sandbox API.
341
+ bearer_token (Optional[str]): Authentication token for API access.
342
+ """
343
+ super().__init__(
344
+ sandbox_id,
345
+ timeout,
346
+ base_url,
347
+ bearer_token,
348
+ SandboxType.BFCL,
349
+ )
@@ -124,7 +124,7 @@ def build_image(build_type, dockerfile_path=None):
124
124
  f"{free_port}:80",
125
125
  "-e",
126
126
  f"SECRET_TOKEN={secret_token}",
127
- f"{image_name}",
127
+ f"{image_name}dev",
128
128
  ],
129
129
  capture_output=True,
130
130
  text=True,
@@ -14,7 +14,6 @@ SANDBOXTYPE = "custom_sandbox"
14
14
  @SandboxRegistry.register(
15
15
  f"agentscope/runtime-sandbox-{SANDBOXTYPE}:{IMAGE_TAG}",
16
16
  sandbox_type=SANDBOXTYPE,
17
- resource_limits={"memory": "16Gi", "cpu": "4"},
18
17
  security_level="medium",
19
18
  timeout=60,
20
19
  description="my sandbox",
@@ -12,7 +12,6 @@ SANDBOX_TYPE = "example"
12
12
  @SandboxRegistry.register(
13
13
  f"agentscope/runtime-sandbox-{SANDBOX_TYPE}:{IMAGE_TAG}",
14
14
  sandbox_type=SANDBOX_TYPE,
15
- resource_limits={"memory": "16Gi", "cpu": "4"},
16
15
  security_level="medium",
17
16
  timeout=60,
18
17
  description="Example sandbox",
@@ -66,3 +66,5 @@ class SandboxType(DynamicEnum):
66
66
  BROWSER = "browser"
67
67
  FILESYSTEM = "filesystem"
68
68
  APPWORLD = "appworld"
69
+ BFCL = "bfcl"
70
+ WEBSHOP = "webshop"
@@ -1,8 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from .base_client import BaseClient
3
3
  from .docker_client import DockerClient
4
+ from .kubernetes_client import KubernetesClient
4
5
 
5
6
  __all__ = [
6
7
  "BaseClient",
7
8
  "DockerClient",
9
+ "KubernetesClient",
8
10
  ]
@@ -1,16 +1,201 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import traceback
3
3
  import logging
4
+ import platform
5
+ import socket
6
+ import subprocess
4
7
 
5
8
  import docker
9
+
6
10
  from .base_client import BaseClient
11
+ from ..collections import RedisSetCollection, InMemorySetCollection
7
12
 
8
13
 
9
14
  logger = logging.getLogger(__name__)
10
15
 
11
16
 
17
+ def is_port_available(port):
18
+ """
19
+ Check if a given port is available (not in use) on the local system.
20
+
21
+ Args:
22
+ port (int): The port number to check.
23
+
24
+ Returns:
25
+ bool: True if the port is available, False if it is in use.
26
+ """
27
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
28
+ try:
29
+ s.bind(("", port))
30
+ # Port is available
31
+ return True
32
+ except OSError:
33
+ # Port is in use
34
+ return False
35
+
36
+
37
+ def sweep_port(port):
38
+ """Sweep all processes found listening on a given port.
39
+
40
+ Args:
41
+ port (int): The port number.
42
+
43
+ Returns:
44
+ bool: True if successful, False if failed.
45
+ """
46
+ try:
47
+ system = platform.system().lower()
48
+ if system == "windows":
49
+ return _sweep_port_windows(port)
50
+ else:
51
+ return _sweep_port_unix(port)
52
+ except Exception as e:
53
+ logger.error(
54
+ f"An error occurred while killing processes on port {port}: {e}",
55
+ )
56
+ return False
57
+
58
+
59
+ def _sweep_port_unix(port):
60
+ """
61
+ Sweep all processes found listening on a given port.
62
+
63
+ Args:
64
+ port (int): The port number.
65
+
66
+ Returns:
67
+ int: Number of processes swept (terminated).
68
+ """
69
+ try:
70
+ # Use lsof to find the processes using the port
71
+ # TODO: support windows
72
+ result = subprocess.run(
73
+ ["lsof", "-i", f":{port}"],
74
+ capture_output=True,
75
+ text=True,
76
+ check=True,
77
+ )
78
+
79
+ # Parse the output
80
+ lines = result.stdout.strip().split("\n")
81
+ if len(lines) <= 1:
82
+ # No process is using the port
83
+ return True
84
+
85
+ # Iterate over each line (excluding the header) and kill each process
86
+ killed_count = 0
87
+ for line in lines[1:]:
88
+ parts = line.split()
89
+ if len(parts) > 1:
90
+ pid = parts[1]
91
+
92
+ # Kill the process using the PID
93
+ subprocess.run(["kill", "-9", pid], check=False)
94
+ killed_count += 1
95
+
96
+ if not is_port_available(port):
97
+ logger.warning(
98
+ f"Port {port} is still in use after killing processes.",
99
+ )
100
+
101
+ return True
102
+
103
+ except Exception as e:
104
+ logger.error(
105
+ f"An error occurred while killing processes on port {port}: {e}",
106
+ )
107
+ return False
108
+
109
+
110
+ def _sweep_port_windows(port):
111
+ """
112
+ Windows implementation using netstat and taskkill
113
+ """
114
+ try:
115
+ # Use netstat to find the processes using the port
116
+ result = subprocess.run(
117
+ ["netstat", "-ano"],
118
+ capture_output=True,
119
+ text=True,
120
+ check=True,
121
+ )
122
+
123
+ # Parse the output to find processes using the specific port
124
+ lines = result.stdout.strip().split("\n")
125
+ pids_to_kill = set()
126
+
127
+ for line in lines:
128
+ if f":{port}" in line and "LISTENING" in line:
129
+ parts = line.split()
130
+ if len(parts) >= 5:
131
+ pid = parts[-1] # PID is usually the last column
132
+ if pid.isdigit(): # Ensure it's a valid PID
133
+ pids_to_kill.add(pid)
134
+
135
+ if not pids_to_kill:
136
+ return True
137
+
138
+ # Kill the processes
139
+ killed_count = 0
140
+ for pid in pids_to_kill:
141
+ try:
142
+ result = subprocess.run(
143
+ ["taskkill", "/PID", pid, "/F"],
144
+ capture_output=True,
145
+ text=True,
146
+ check=False,
147
+ )
148
+ if result.returncode == 0:
149
+ killed_count += 1
150
+ except Exception as e:
151
+ logger.debug(f"Failed to kill process {pid}: {e}")
152
+ continue
153
+
154
+ if not is_port_available(port):
155
+ logger.warning(
156
+ f"Port {port} is still in use after killing processes.",
157
+ )
158
+
159
+ return True
160
+
161
+ except subprocess.CalledProcessError as e:
162
+ logger.error(f"netstat command failed: {e}")
163
+ return False
164
+ except Exception as e:
165
+ logger.error(f"Error in Windows port sweep: {e}")
166
+ return False
167
+
168
+
12
169
  class DockerClient(BaseClient):
13
- def __init__(self):
170
+ def __init__(self, config=None):
171
+ self.config = config
172
+ self.port_range = range(*self.config.port_range)
173
+
174
+ if self.config.redis_enabled:
175
+ import redis
176
+
177
+ redis_client = redis.Redis(
178
+ host=self.config.redis_server,
179
+ port=self.config.redis_port,
180
+ db=self.config.redis_db,
181
+ username=self.config.redis_user,
182
+ password=self.config.redis_password,
183
+ decode_responses=True,
184
+ )
185
+ try:
186
+ redis_client.ping()
187
+ except ConnectionError as e:
188
+ raise RuntimeError(
189
+ "Unable to connect to the Redis server.",
190
+ ) from e
191
+
192
+ self.port_set = RedisSetCollection(
193
+ redis_client,
194
+ set_name=self.config.redis_port_key,
195
+ )
196
+ else:
197
+ self.port_set = InMemorySetCollection()
198
+
14
199
  try:
15
200
  self.client = docker.from_env()
16
201
  except Exception as e:
@@ -32,9 +217,12 @@ class DockerClient(BaseClient):
32
217
  acr_registry = "agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
33
218
  acr_image = f"{acr_registry}/{image}"
34
219
 
35
- logger.debug(f"Attempting to pull from ACR: {acr_image}")
220
+ logger.info(
221
+ f"Attempting to pull from ACR: {acr_image}, it might take "
222
+ f"several minutes.",
223
+ )
36
224
  self.client.images.pull(acr_image)
37
- logger.debug(f"Successfully pulled image from ACR: {acr_image}")
225
+ logger.info(f"Successfully pulled image from ACR: {acr_image}")
38
226
 
39
227
  # Retag the image
40
228
  acr_img_obj = self.client.images.get(acr_image)
@@ -49,7 +237,9 @@ class DockerClient(BaseClient):
49
237
  logger.debug(f"Failed to remove original tag: {e}")
50
238
  return True
51
239
  except Exception as e:
52
- logger.error(f"Failed to pull from ACR: {e}")
240
+ logger.error(
241
+ f"Failed to pull from ACR: {e}, {traceback.format_exc()}",
242
+ )
53
243
  return False
54
244
 
55
245
  def create(
@@ -65,17 +255,28 @@ class DockerClient(BaseClient):
65
255
  if runtime_config is None:
66
256
  runtime_config = {}
67
257
 
258
+ port_mapping = {}
259
+
260
+ if ports:
261
+ free_port = self._find_free_ports(len(ports))
262
+ for container_port, host_port in zip(ports, free_port):
263
+ port_mapping[container_port] = host_port
264
+
68
265
  try:
69
266
  try:
70
267
  # Check if the image exists locally
71
268
  self.client.images.get(image)
72
269
  logger.debug(f"Image '{image}' found locally.")
73
270
  except docker.errors.ImageNotFound:
74
- logger.debug(
271
+ logger.info(
75
272
  f"Image '{image}' not found locally. "
76
273
  f"Attempting to pull it...",
77
274
  )
78
275
  try:
276
+ logger.info(
277
+ f"Attempting to pull: {image}, "
278
+ f"it might take several minutes.",
279
+ )
79
280
  self.client.images.pull(image)
80
281
  logger.debug(
81
282
  f"Image '{image}' successfully pulled from default "
@@ -86,7 +287,8 @@ class DockerClient(BaseClient):
86
287
  logger.warning(
87
288
  f"Failed to pull from default registry: {e}",
88
289
  )
89
- logger.debug("Trying to pull from ACR fallback...")
290
+ logger.warning("Trying to pull from ACR fallback...")
291
+
90
292
  pull_success = self._try_pull_from_acr(image)
91
293
 
92
294
  if not pull_success:
@@ -94,27 +296,28 @@ class DockerClient(BaseClient):
94
296
  f"Failed to pull image '{image}' from both "
95
297
  f"default and ACR",
96
298
  )
97
- return False
299
+ return None, None, None
98
300
 
99
301
  except docker.errors.APIError as e:
100
302
  logger.error(f"Error occurred while checking the image: {e}")
101
- return False
303
+ return None, None, None
102
304
 
103
305
  # Create and run the container
104
306
  container = self.client.containers.run(
105
307
  image,
106
308
  detach=True,
107
- ports=ports,
309
+ ports=port_mapping,
108
310
  name=name,
109
311
  volumes=volumes,
110
312
  environment=environment,
111
313
  **runtime_config,
112
314
  )
113
315
  container.reload()
114
- return True
316
+ _id = container.id
317
+ return _id, list(port_mapping.values()), "localhost"
115
318
  except Exception as e:
116
319
  logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
117
- return False
320
+ return None, None, None
118
321
 
119
322
  def start(self, container_id):
120
323
  """Start a Docker container."""
@@ -122,6 +325,17 @@ class DockerClient(BaseClient):
122
325
  container = self.client.containers.get(
123
326
  container_id,
124
327
  )
328
+
329
+ # Check whether the ports are occupied by other processes
330
+ port_mapping = container.attrs["NetworkSettings"]["Ports"]
331
+ for _, mappings in port_mapping.items():
332
+ if mappings is not None:
333
+ for mapping in mappings:
334
+ host_port = int(mapping["HostPort"])
335
+ if is_port_available(host_port):
336
+ continue
337
+ sweep_port(host_port["HostPort"])
338
+
125
339
  container.start()
126
340
  return True
127
341
  except Exception as e:
@@ -146,7 +360,17 @@ class DockerClient(BaseClient):
146
360
  container = self.client.containers.get(
147
361
  container_id,
148
362
  )
363
+ # Remove ports
364
+ port_mapping = container.attrs["NetworkSettings"]["Ports"]
149
365
  container.remove(force=force)
366
+
367
+ # Iterate over each port and its mappings
368
+ for _, mappings in port_mapping.items():
369
+ if mappings is not None:
370
+ for mapping in mappings:
371
+ host_port = int(mapping["HostPort"])
372
+ self.port_set.remove(host_port)
373
+
150
374
  return True
151
375
  except Exception as e:
152
376
  logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
@@ -168,3 +392,31 @@ class DockerClient(BaseClient):
168
392
  if container_attrs:
169
393
  return container_attrs["State"]["Status"]
170
394
  return None
395
+
396
+ def _find_free_ports(self, n):
397
+ free_ports = []
398
+
399
+ for port in self.port_range:
400
+ if len(free_ports) >= n:
401
+ break # We have found enough ports
402
+
403
+ if not self.port_set.add(port):
404
+ continue
405
+
406
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
407
+ try:
408
+ s.bind(("", port))
409
+ free_ports.append(port) # Port is available
410
+
411
+ except OSError:
412
+ # Bind failed, port is in use
413
+ self.port_set.remove(port)
414
+ # Try the next one
415
+ continue
416
+
417
+ if len(free_ports) < n:
418
+ raise RuntimeError(
419
+ "Not enough free ports available in the specified range.",
420
+ )
421
+
422
+ return free_ports