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.
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +1 -0
- agentscope_runtime/engine/agents/agno_agent.py +1 -0
- agentscope_runtime/engine/agents/autogen_agent.py +245 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +1 -1
- agentscope_runtime/engine/services/memory_service.py +2 -2
- agentscope_runtime/engine/services/redis_memory_service.py +187 -0
- agentscope_runtime/engine/services/redis_session_history_service.py +155 -0
- agentscope_runtime/sandbox/build.py +1 -1
- agentscope_runtime/sandbox/custom/custom_sandbox.py +0 -1
- agentscope_runtime/sandbox/custom/example.py +0 -1
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +2 -0
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +246 -4
- agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +550 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +21 -82
- agentscope_runtime/sandbox/manager/server/app.py +55 -24
- agentscope_runtime/sandbox/manager/server/config.py +28 -16
- agentscope_runtime/sandbox/model/container.py +3 -1
- agentscope_runtime/sandbox/model/manager_config.py +19 -2
- agentscope_runtime/sandbox/tools/tool.py +111 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/METADATA +74 -13
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/RECORD +26 -23
- agentscope_runtime/sandbox/manager/utils.py +0 -78
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/WHEEL +0 -0
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-0.1.0.dist-info → agentscope_runtime-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {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=
|
|
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
|
-
|
|
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
|
|
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
|