agentscope-runtime 0.1.0__py3-none-any.whl → 0.1.1__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 (27) 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/memory_service.py +2 -2
  6. agentscope_runtime/engine/services/redis_memory_service.py +187 -0
  7. agentscope_runtime/engine/services/redis_session_history_service.py +155 -0
  8. agentscope_runtime/sandbox/build.py +1 -1
  9. agentscope_runtime/sandbox/custom/custom_sandbox.py +0 -1
  10. agentscope_runtime/sandbox/custom/example.py +0 -1
  11. agentscope_runtime/sandbox/manager/container_clients/__init__.py +2 -0
  12. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +246 -4
  13. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +550 -0
  14. agentscope_runtime/sandbox/manager/sandbox_manager.py +21 -82
  15. agentscope_runtime/sandbox/manager/server/app.py +55 -24
  16. agentscope_runtime/sandbox/manager/server/config.py +28 -16
  17. agentscope_runtime/sandbox/model/container.py +3 -1
  18. agentscope_runtime/sandbox/model/manager_config.py +19 -2
  19. agentscope_runtime/sandbox/tools/tool.py +111 -0
  20. agentscope_runtime/version.py +1 -1
  21. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/METADATA +74 -13
  22. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/RECORD +26 -23
  23. agentscope_runtime/sandbox/manager/utils.py +0 -78
  24. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/WHEEL +0 -0
  25. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/entry_points.txt +0 -0
  26. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/licenses/LICENSE +0 -0
  27. {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/top_level.txt +0 -0
@@ -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:
@@ -65,6 +250,13 @@ class DockerClient(BaseClient):
65
250
  if runtime_config is None:
66
251
  runtime_config = {}
67
252
 
253
+ port_mapping = {}
254
+
255
+ if ports:
256
+ free_port = self._find_free_ports(len(ports))
257
+ for container_port, host_port in zip(ports, free_port):
258
+ port_mapping[container_port] = host_port
259
+
68
260
  try:
69
261
  try:
70
262
  # Check if the image exists locally
@@ -104,17 +296,18 @@ class DockerClient(BaseClient):
104
296
  container = self.client.containers.run(
105
297
  image,
106
298
  detach=True,
107
- ports=ports,
299
+ ports=port_mapping,
108
300
  name=name,
109
301
  volumes=volumes,
110
302
  environment=environment,
111
303
  **runtime_config,
112
304
  )
113
305
  container.reload()
114
- return True
306
+ _id = container.id
307
+ return _id, list(port_mapping.values())
115
308
  except Exception as e:
116
309
  logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
117
- return False
310
+ return None, None
118
311
 
119
312
  def start(self, container_id):
120
313
  """Start a Docker container."""
@@ -122,6 +315,17 @@ class DockerClient(BaseClient):
122
315
  container = self.client.containers.get(
123
316
  container_id,
124
317
  )
318
+
319
+ # Check whether the ports are occupied by other processes
320
+ port_mapping = container.attrs["NetworkSettings"]["Ports"]
321
+ for _, mappings in port_mapping.items():
322
+ if mappings is not None:
323
+ for mapping in mappings:
324
+ host_port = int(mapping["HostPort"])
325
+ if is_port_available(host_port):
326
+ continue
327
+ sweep_port(host_port["HostPort"])
328
+
125
329
  container.start()
126
330
  return True
127
331
  except Exception as e:
@@ -146,7 +350,17 @@ class DockerClient(BaseClient):
146
350
  container = self.client.containers.get(
147
351
  container_id,
148
352
  )
353
+ # Remove ports
354
+ port_mapping = container.attrs["NetworkSettings"]["Ports"]
149
355
  container.remove(force=force)
356
+
357
+ # Iterate over each port and its mappings
358
+ for _, mappings in port_mapping.items():
359
+ if mappings is not None:
360
+ for mapping in mappings:
361
+ host_port = int(mapping["HostPort"])
362
+ self.port_set.remove(host_port)
363
+
150
364
  return True
151
365
  except Exception as e:
152
366
  logger.error(f"An error occurred: {e}, {traceback.format_exc()}")
@@ -168,3 +382,31 @@ class DockerClient(BaseClient):
168
382
  if container_attrs:
169
383
  return container_attrs["State"]["Status"]
170
384
  return None
385
+
386
+ def _find_free_ports(self, n):
387
+ free_ports = []
388
+
389
+ for port in self.port_range:
390
+ if len(free_ports) >= n:
391
+ break # We have found enough ports
392
+
393
+ if not self.port_set.add(port):
394
+ continue
395
+
396
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
397
+ try:
398
+ s.bind(("", port))
399
+ free_ports.append(port) # Port is available
400
+
401
+ except OSError:
402
+ # Bind failed, port is in use
403
+ self.port_set.remove(port)
404
+ # Try the next one
405
+ continue
406
+
407
+ if len(free_ports) < n:
408
+ raise RuntimeError(
409
+ "Not enough free ports available in the specified range.",
410
+ )
411
+
412
+ return free_ports