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
|
@@ -4,13 +4,12 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import secrets
|
|
6
6
|
import inspect
|
|
7
|
-
import socket
|
|
8
7
|
import traceback
|
|
9
8
|
|
|
10
9
|
from functools import wraps
|
|
11
10
|
from typing import Optional, Dict
|
|
12
|
-
from uuid import uuid4
|
|
13
11
|
|
|
12
|
+
import shortuuid
|
|
14
13
|
import requests
|
|
15
14
|
|
|
16
15
|
from ..model import (
|
|
@@ -22,20 +21,17 @@ from ..enums import SandboxType
|
|
|
22
21
|
from ..registry import SandboxRegistry
|
|
23
22
|
from ..client import SandboxHttpClient, TrainingSandboxClient
|
|
24
23
|
|
|
25
|
-
from ..manager.utils import is_port_available, sweep_port
|
|
26
24
|
from ..manager.collections import (
|
|
27
25
|
RedisMapping,
|
|
28
|
-
RedisSetCollection,
|
|
29
26
|
RedisQueue,
|
|
30
27
|
InMemoryMapping,
|
|
31
28
|
InMemoryQueue,
|
|
32
|
-
InMemorySetCollection,
|
|
33
29
|
)
|
|
34
30
|
from ..manager.storage import (
|
|
35
31
|
LocalStorage,
|
|
36
32
|
OSSStorage,
|
|
37
33
|
)
|
|
38
|
-
from ..manager.container_clients import DockerClient
|
|
34
|
+
from ..manager.container_clients import DockerClient, KubernetesClient
|
|
39
35
|
from ..constant import BROWSER_SESSION_ID
|
|
40
36
|
|
|
41
37
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -136,32 +132,26 @@ class SandboxManager:
|
|
|
136
132
|
) from e
|
|
137
133
|
|
|
138
134
|
self.container_mapping = RedisMapping(redis_client)
|
|
139
|
-
self.port_set = RedisSetCollection(
|
|
140
|
-
redis_client,
|
|
141
|
-
set_name=self.config.redis_port_key,
|
|
142
|
-
)
|
|
143
135
|
self.pool_queue = RedisQueue(
|
|
144
136
|
redis_client,
|
|
145
137
|
self.config.redis_container_pool_key,
|
|
146
138
|
)
|
|
147
139
|
else:
|
|
148
140
|
self.container_mapping = InMemoryMapping()
|
|
149
|
-
self.port_set = InMemorySetCollection()
|
|
150
141
|
self.pool_queue = InMemoryQueue()
|
|
151
142
|
|
|
152
143
|
self.container_deployment = self.config.container_deployment
|
|
153
144
|
|
|
154
145
|
if base_url is None:
|
|
155
146
|
if self.container_deployment == "docker":
|
|
156
|
-
self.client = DockerClient()
|
|
147
|
+
self.client = DockerClient(config=self.config)
|
|
148
|
+
elif self.container_deployment == "k8s":
|
|
149
|
+
self.client = KubernetesClient(config=self.config)
|
|
157
150
|
else:
|
|
158
|
-
# TODO: support k8s deployment
|
|
159
151
|
raise NotImplementedError("Not implemented")
|
|
160
152
|
else:
|
|
161
153
|
self.client = None
|
|
162
154
|
|
|
163
|
-
self.port_range = range(*self.config.port_range)
|
|
164
|
-
|
|
165
155
|
self.file_system = self.config.file_system
|
|
166
156
|
if self.file_system == "oss":
|
|
167
157
|
self.storage = OSSStorage(
|
|
@@ -235,34 +225,6 @@ class SandboxManager:
|
|
|
235
225
|
logger.error(f"Error initializing runtime pool: {e}")
|
|
236
226
|
break
|
|
237
227
|
|
|
238
|
-
def _find_free_ports(self, n):
|
|
239
|
-
free_ports = []
|
|
240
|
-
|
|
241
|
-
for port in self.port_range:
|
|
242
|
-
if len(free_ports) >= n:
|
|
243
|
-
break # We have found enough ports
|
|
244
|
-
|
|
245
|
-
if not self.port_set.add(port):
|
|
246
|
-
continue
|
|
247
|
-
|
|
248
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
249
|
-
try:
|
|
250
|
-
s.bind(("", port))
|
|
251
|
-
free_ports.append(port) # Port is available
|
|
252
|
-
|
|
253
|
-
except OSError:
|
|
254
|
-
# Bind failed, port is in use
|
|
255
|
-
self.port_set.remove(port)
|
|
256
|
-
# Try the next one
|
|
257
|
-
continue
|
|
258
|
-
|
|
259
|
-
if len(free_ports) < n:
|
|
260
|
-
raise RuntimeError(
|
|
261
|
-
"Not enough free ports available in the specified range.",
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
return free_ports
|
|
265
|
-
|
|
266
228
|
@remote_wrapper()
|
|
267
229
|
def cleanup(self):
|
|
268
230
|
logger.debug(
|
|
@@ -414,7 +376,9 @@ class SandboxManager:
|
|
|
414
376
|
)
|
|
415
377
|
return None
|
|
416
378
|
|
|
417
|
-
|
|
379
|
+
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
380
|
+
short_uuid = shortuuid.ShortUUID(alphabet=alphabet).uuid()
|
|
381
|
+
session_id = str(short_uuid)
|
|
418
382
|
|
|
419
383
|
if mount_dir is None:
|
|
420
384
|
mount_dir = os.path.join(self.default_mount_dir, session_id)
|
|
@@ -439,12 +403,6 @@ class SandboxManager:
|
|
|
439
403
|
f"Container with name {container_name} already exists.",
|
|
440
404
|
)
|
|
441
405
|
|
|
442
|
-
free_ports = self._find_free_ports(1)
|
|
443
|
-
|
|
444
|
-
ports = {
|
|
445
|
-
"80/tcp": free_ports[0], # nginx
|
|
446
|
-
}
|
|
447
|
-
|
|
448
406
|
# Generate a random secret token
|
|
449
407
|
runtime_token = secrets.token_hex(16)
|
|
450
408
|
|
|
@@ -456,17 +414,19 @@ class SandboxManager:
|
|
|
456
414
|
},
|
|
457
415
|
}
|
|
458
416
|
|
|
459
|
-
|
|
417
|
+
_id, ports = self.client.create(
|
|
460
418
|
image,
|
|
461
419
|
name=container_name,
|
|
462
|
-
ports=
|
|
420
|
+
ports=["80/tcp"], # Nginx
|
|
463
421
|
volumes=volume_bindings,
|
|
464
422
|
environment={
|
|
465
423
|
"SECRET_TOKEN": runtime_token,
|
|
466
424
|
**environment,
|
|
467
425
|
},
|
|
468
426
|
runtime_config=config.runtime_config,
|
|
469
|
-
)
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
if _id is None:
|
|
470
430
|
return None
|
|
471
431
|
|
|
472
432
|
# Check the container status
|
|
@@ -478,24 +438,22 @@ class SandboxManager:
|
|
|
478
438
|
)
|
|
479
439
|
return None
|
|
480
440
|
|
|
481
|
-
#
|
|
482
|
-
container_attrs = self.client.inspect(container_name)
|
|
483
|
-
|
|
441
|
+
# TODO: update ContainerModel according to images & backend
|
|
484
442
|
container_model = ContainerModel(
|
|
485
443
|
session_id=session_id,
|
|
486
|
-
container_id=
|
|
444
|
+
container_id=_id,
|
|
487
445
|
container_name=container_name,
|
|
488
|
-
base_url=f"http://localhost:{ports[
|
|
489
|
-
browser_url=f"http://localhost:{ports[
|
|
446
|
+
base_url=f"http://localhost:{ports[0]}/fastapi",
|
|
447
|
+
browser_url=f"http://localhost:{ports[0]}/steel-api"
|
|
490
448
|
f"/{runtime_token}",
|
|
491
449
|
front_browser_ws=f"ws://localhost:"
|
|
492
|
-
f"{ports[
|
|
450
|
+
f"{ports[0]}/steel-api/"
|
|
493
451
|
f"{runtime_token}/v1/sessions/cast",
|
|
494
452
|
client_browser_ws=f"ws://localhost:"
|
|
495
|
-
f"{ports[
|
|
453
|
+
f"{ports[0]}/steel-api/{runtime_token}/&sessionId"
|
|
496
454
|
f"={BROWSER_SESSION_ID}",
|
|
497
|
-
artifacts_sio=f"http://localhost
|
|
498
|
-
ports=
|
|
455
|
+
artifacts_sio=f"http://localhost:{ports[0]}/v1",
|
|
456
|
+
ports=[ports[0]],
|
|
499
457
|
mount_dir=str(mount_dir),
|
|
500
458
|
storage_path=storage_path,
|
|
501
459
|
runtime_token=runtime_token,
|
|
@@ -537,10 +495,6 @@ class SandboxManager:
|
|
|
537
495
|
self.client.stop(container_info.container_id, timeout=1)
|
|
538
496
|
self.client.remove(container_info.container_id, force=True)
|
|
539
497
|
|
|
540
|
-
# Release ports after the container is removed
|
|
541
|
-
for port in container_info.ports:
|
|
542
|
-
self.port_set.remove(port)
|
|
543
|
-
|
|
544
498
|
logger.debug(f"Container for {identity} destroyed.")
|
|
545
499
|
|
|
546
500
|
# Upload to storage
|
|
@@ -570,14 +524,6 @@ class SandboxManager:
|
|
|
570
524
|
|
|
571
525
|
container_info = ContainerModel(**container_json)
|
|
572
526
|
|
|
573
|
-
# Check whether the ports are occupied by other processes
|
|
574
|
-
for port in container_info.ports:
|
|
575
|
-
if is_port_available(port):
|
|
576
|
-
continue
|
|
577
|
-
|
|
578
|
-
# If the port is occupied, sweep it
|
|
579
|
-
sweep_port(port)
|
|
580
|
-
|
|
581
527
|
self.client.start(container_info.container_id)
|
|
582
528
|
status = self.client.get_status(container_info.container_id)
|
|
583
529
|
if status != "running":
|
|
@@ -685,10 +631,3 @@ class SandboxManager:
|
|
|
685
631
|
server_configs=server_configs,
|
|
686
632
|
overwrite=overwrite,
|
|
687
633
|
)
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if __name__ == "__main__":
|
|
691
|
-
with SandboxManager() as manager:
|
|
692
|
-
name = manager.create("12345")
|
|
693
|
-
if name:
|
|
694
|
-
print(f"Created container: {name}")
|
|
@@ -4,12 +4,14 @@ import inspect
|
|
|
4
4
|
import logging
|
|
5
5
|
import traceback
|
|
6
6
|
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
7
9
|
from fastapi import FastAPI, HTTPException, Request, Depends
|
|
8
10
|
from fastapi.middleware.cors import CORSMiddleware
|
|
9
11
|
from fastapi.responses import JSONResponse
|
|
10
12
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
11
13
|
|
|
12
|
-
from ...manager.server.config import
|
|
14
|
+
from ...manager.server.config import get_settings
|
|
13
15
|
from ...manager.server.models import (
|
|
14
16
|
ErrorResponse,
|
|
15
17
|
HealthResponse,
|
|
@@ -42,34 +44,47 @@ app.add_middleware(
|
|
|
42
44
|
security = HTTPBearer(auto_error=False)
|
|
43
45
|
|
|
44
46
|
# Global SandboxManager instance
|
|
45
|
-
_runtime_manager = None
|
|
46
|
-
_config =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
_runtime_manager: Optional[SandboxManager] = None
|
|
48
|
+
_config: Optional[SandboxManagerEnvConfig] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_config() -> SandboxManagerEnvConfig:
|
|
52
|
+
"""Return config"""
|
|
53
|
+
global _config
|
|
54
|
+
if _config is None:
|
|
55
|
+
settings = get_settings()
|
|
56
|
+
_config = SandboxManagerEnvConfig(
|
|
57
|
+
container_prefix_key=settings.CONTAINER_PREFIX_KEY,
|
|
58
|
+
file_system=settings.FILE_SYSTEM,
|
|
59
|
+
redis_enabled=settings.REDIS_ENABLED,
|
|
60
|
+
container_deployment=settings.CONTAINER_DEPLOYMENT,
|
|
61
|
+
default_mount_dir=settings.DEFAULT_MOUNT_DIR,
|
|
62
|
+
storage_folder=settings.STORAGE_FOLDER,
|
|
63
|
+
port_range=settings.PORT_RANGE,
|
|
64
|
+
pool_size=settings.POOL_SIZE,
|
|
65
|
+
oss_endpoint=settings.OSS_ENDPOINT,
|
|
66
|
+
oss_access_key_id=settings.OSS_ACCESS_KEY_ID,
|
|
67
|
+
oss_access_key_secret=settings.OSS_ACCESS_KEY_SECRET,
|
|
68
|
+
oss_bucket_name=settings.OSS_BUCKET_NAME,
|
|
69
|
+
redis_server=settings.REDIS_SERVER,
|
|
70
|
+
redis_port=settings.REDIS_PORT,
|
|
71
|
+
redis_db=settings.REDIS_DB,
|
|
72
|
+
redis_user=settings.REDIS_USER,
|
|
73
|
+
redis_password=settings.REDIS_PASSWORD,
|
|
74
|
+
redis_port_key=settings.REDIS_PORT_KEY,
|
|
75
|
+
redis_container_pool_key=settings.REDIS_CONTAINER_POOL_KEY,
|
|
76
|
+
k8s_namespace=settings.K8S_NAMESPACE,
|
|
77
|
+
kubeconfig_path=settings.KUBECONFIG_PATH,
|
|
78
|
+
)
|
|
79
|
+
return _config
|
|
67
80
|
|
|
68
81
|
|
|
69
82
|
def verify_token(
|
|
70
83
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
71
84
|
):
|
|
72
85
|
"""Verify Bearer token"""
|
|
86
|
+
settings = get_settings()
|
|
87
|
+
|
|
73
88
|
if not hasattr(settings, "BEARER_TOKEN") or not settings.BEARER_TOKEN:
|
|
74
89
|
logger.warning("BEARER_TOKEN not configured, skipping authentication")
|
|
75
90
|
return credentials
|
|
@@ -98,8 +113,10 @@ def get_runtime_manager():
|
|
|
98
113
|
"""Get or create the global SandboxManager instance"""
|
|
99
114
|
global _runtime_manager
|
|
100
115
|
if _runtime_manager is None:
|
|
116
|
+
settings = get_settings()
|
|
117
|
+
config = get_config()
|
|
101
118
|
_runtime_manager = SandboxManager(
|
|
102
|
-
config=
|
|
119
|
+
config=config,
|
|
103
120
|
default_type=settings.DEFAULT_SANDBOX_TYPE,
|
|
104
121
|
)
|
|
105
122
|
return _runtime_manager
|
|
@@ -159,6 +176,7 @@ async def startup_event():
|
|
|
159
176
|
async def shutdown_event():
|
|
160
177
|
"""Cleanup resources on shutdown"""
|
|
161
178
|
global _runtime_manager
|
|
179
|
+
settings = get_settings()
|
|
162
180
|
if _runtime_manager and settings.AUTO_CLEANUP:
|
|
163
181
|
_runtime_manager.cleanup()
|
|
164
182
|
_runtime_manager = None
|
|
@@ -179,8 +197,21 @@ async def health_check():
|
|
|
179
197
|
|
|
180
198
|
def main():
|
|
181
199
|
"""Main entry point for the Runtime Manager Service"""
|
|
200
|
+
import argparse
|
|
201
|
+
import os
|
|
182
202
|
import uvicorn
|
|
183
203
|
|
|
204
|
+
parser = argparse.ArgumentParser(description="Runtime Manager Service")
|
|
205
|
+
parser.add_argument("--config", type=str, help="Path to config file")
|
|
206
|
+
args = parser.parse_args()
|
|
207
|
+
|
|
208
|
+
if args.config and not os.path.exists(args.config):
|
|
209
|
+
raise FileNotFoundError(
|
|
210
|
+
f"Error: Config file {args.config} does not exist",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
settings = get_settings(args.config)
|
|
214
|
+
|
|
184
215
|
uvicorn.run(
|
|
185
216
|
"agentscope_runtime.sandbox.manager.server.app:app",
|
|
186
217
|
host=settings.HOST,
|
|
@@ -2,18 +2,9 @@
|
|
|
2
2
|
import os
|
|
3
3
|
from typing import Optional, Tuple, Literal
|
|
4
4
|
from pydantic_settings import BaseSettings
|
|
5
|
-
from pydantic import field_validator
|
|
5
|
+
from pydantic import field_validator, ConfigDict
|
|
6
6
|
from dotenv import load_dotenv
|
|
7
7
|
|
|
8
|
-
env_file = ".env"
|
|
9
|
-
env_example_file = ".env.example"
|
|
10
|
-
|
|
11
|
-
# Load the appropriate .env file
|
|
12
|
-
if os.path.exists(env_file):
|
|
13
|
-
load_dotenv(env_file)
|
|
14
|
-
elif os.path.exists(env_example_file):
|
|
15
|
-
load_dotenv(env_example_file)
|
|
16
|
-
|
|
17
8
|
|
|
18
9
|
class Settings(BaseSettings):
|
|
19
10
|
"""Runtime Manager Service Settings"""
|
|
@@ -27,11 +18,10 @@ class Settings(BaseSettings):
|
|
|
27
18
|
|
|
28
19
|
# Runtime Manager settings
|
|
29
20
|
DEFAULT_SANDBOX_TYPE: str = "base"
|
|
30
|
-
WORKDIR: str = "/workspace"
|
|
31
21
|
POOL_SIZE: int = 1
|
|
32
22
|
AUTO_CLEANUP: bool = True
|
|
33
23
|
CONTAINER_PREFIX_KEY: str = "runtime_sandbox_container_"
|
|
34
|
-
CONTAINER_DEPLOYMENT: Literal["docker", "cloud"] = "docker"
|
|
24
|
+
CONTAINER_DEPLOYMENT: Literal["docker", "cloud", "k8s"] = "docker"
|
|
35
25
|
DEFAULT_MOUNT_DIR: str = "sessions_mount_dir"
|
|
36
26
|
STORAGE_FOLDER: str = "runtime_sandbox_storage"
|
|
37
27
|
PORT_RANGE: Tuple[int, int] = (49152, 59152)
|
|
@@ -53,9 +43,14 @@ class Settings(BaseSettings):
|
|
|
53
43
|
OSS_ACCESS_KEY_SECRET: str = "your-access-key-secret"
|
|
54
44
|
OSS_BUCKET_NAME: str = "your-bucket-name"
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
# K8S settings
|
|
47
|
+
K8S_NAMESPACE: str = "default"
|
|
48
|
+
KUBECONFIG_PATH: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(
|
|
51
|
+
case_sensitive=True,
|
|
52
|
+
extra="allow",
|
|
53
|
+
)
|
|
59
54
|
|
|
60
55
|
@field_validator("WORKERS", mode="before")
|
|
61
56
|
@classmethod
|
|
@@ -65,4 +60,21 @@ class Settings(BaseSettings):
|
|
|
65
60
|
return value
|
|
66
61
|
|
|
67
62
|
|
|
68
|
-
|
|
63
|
+
_settings: Optional[Settings] = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_settings(config_file: Optional[str] = None) -> Settings:
|
|
67
|
+
global _settings
|
|
68
|
+
|
|
69
|
+
env_file = ".env"
|
|
70
|
+
env_example_file = ".env.example"
|
|
71
|
+
|
|
72
|
+
if _settings is None:
|
|
73
|
+
if config_file and os.path.exists(config_file):
|
|
74
|
+
load_dotenv(config_file, override=True)
|
|
75
|
+
elif os.path.exists(env_file):
|
|
76
|
+
load_dotenv(env_file)
|
|
77
|
+
elif os.path.exists(env_example_file):
|
|
78
|
+
load_dotenv(env_example_file)
|
|
79
|
+
_settings = Settings()
|
|
80
|
+
return _settings
|
|
@@ -4,7 +4,6 @@ from typing import List
|
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
# TODO: support k8s version
|
|
8
7
|
class ContainerModel(BaseModel):
|
|
9
8
|
session_id: str = Field(
|
|
10
9
|
...,
|
|
@@ -70,3 +69,6 @@ class ContainerModel(BaseModel):
|
|
|
70
69
|
None,
|
|
71
70
|
description="Image version of the container",
|
|
72
71
|
)
|
|
72
|
+
|
|
73
|
+
class Config:
|
|
74
|
+
extra = "allow"
|
|
@@ -5,10 +5,14 @@ from typing import Optional, Literal, Tuple
|
|
|
5
5
|
from pydantic import BaseModel, Field, model_validator
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
UUID_LENGTH = 25
|
|
9
|
+
|
|
10
|
+
|
|
8
11
|
class SandboxManagerEnvConfig(BaseModel):
|
|
9
12
|
container_prefix_key: str = Field(
|
|
10
13
|
"runtime_sandbox_container_",
|
|
11
14
|
description="Prefix for keys related to Container models.",
|
|
15
|
+
max_length=63 - UUID_LENGTH, # Max length for k8s pod name
|
|
12
16
|
)
|
|
13
17
|
|
|
14
18
|
file_system: Literal["local", "oss"] = Field(
|
|
@@ -23,9 +27,10 @@ class SandboxManagerEnvConfig(BaseModel):
|
|
|
23
27
|
...,
|
|
24
28
|
description="Indicates if Redis is enabled.",
|
|
25
29
|
)
|
|
26
|
-
container_deployment: Literal["docker", "cloud"] = Field(
|
|
30
|
+
container_deployment: Literal["docker", "cloud", "k8s"] = Field(
|
|
27
31
|
...,
|
|
28
|
-
description="
|
|
32
|
+
description="Container deployment backend: 'docker', 'cloud', "
|
|
33
|
+
"or 'k8s'.",
|
|
29
34
|
)
|
|
30
35
|
|
|
31
36
|
default_mount_dir: Optional[str] = Field(
|
|
@@ -95,6 +100,18 @@ class SandboxManagerEnvConfig(BaseModel):
|
|
|
95
100
|
description="Prefix for Redis keys related to container pool.",
|
|
96
101
|
)
|
|
97
102
|
|
|
103
|
+
# Kubernetes settings
|
|
104
|
+
k8s_namespace: Optional[str] = Field(
|
|
105
|
+
"default",
|
|
106
|
+
description="Kubernetes namespace to deploy pods. Required if "
|
|
107
|
+
"container_deployment is 'k8s'.",
|
|
108
|
+
)
|
|
109
|
+
kubeconfig_path: Optional[str] = Field(
|
|
110
|
+
None,
|
|
111
|
+
description="Path to kubeconfig file. If not set, will try "
|
|
112
|
+
"in-cluster config or default kubeconfig.",
|
|
113
|
+
)
|
|
114
|
+
|
|
98
115
|
@model_validator(mode="after")
|
|
99
116
|
def check_settings(cls, self):
|
|
100
117
|
if not self.default_mount_dir:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
# pylint: disable=unused-argument
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
3
5
|
from abc import ABC, abstractmethod
|
|
4
6
|
from typing import Optional, Any, Dict
|
|
5
7
|
from ..enums import SandboxType
|
|
@@ -121,3 +123,112 @@ class Tool(ABC):
|
|
|
121
123
|
f"sandbox_type='{self.sandbox_type}'"
|
|
122
124
|
f")"
|
|
123
125
|
)
|
|
126
|
+
|
|
127
|
+
def make_function(self):
|
|
128
|
+
"""Create a function with proper type signatures from schema."""
|
|
129
|
+
tool_call = self.__call__
|
|
130
|
+
parameters = self.schema["function"]["parameters"]
|
|
131
|
+
|
|
132
|
+
# Extract properties and required parameters from the schema
|
|
133
|
+
properties = parameters.get("properties", {})
|
|
134
|
+
required = parameters.get("required", [])
|
|
135
|
+
|
|
136
|
+
# Type mapping from JSON schema types to Python types
|
|
137
|
+
type_mapping = {
|
|
138
|
+
"string": str,
|
|
139
|
+
"integer": int,
|
|
140
|
+
"number": float,
|
|
141
|
+
"boolean": bool,
|
|
142
|
+
"array": list,
|
|
143
|
+
"object": dict,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Build parameter signature
|
|
147
|
+
sig_params = []
|
|
148
|
+
for param_name, param_info in properties.items():
|
|
149
|
+
param_type = type_mapping.get(
|
|
150
|
+
param_info.get("type", "string"),
|
|
151
|
+
str,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if param_name in required:
|
|
155
|
+
# Required parameter
|
|
156
|
+
param = inspect.Parameter(
|
|
157
|
+
param_name,
|
|
158
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
159
|
+
annotation=param_type,
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
# Optional parameter with default None
|
|
163
|
+
param = inspect.Parameter(
|
|
164
|
+
param_name,
|
|
165
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
166
|
+
default=None,
|
|
167
|
+
annotation=Optional[param_type],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
sig_params.append(param)
|
|
171
|
+
|
|
172
|
+
# Create the function signature
|
|
173
|
+
new_signature = inspect.Signature(sig_params, return_annotation=Any)
|
|
174
|
+
|
|
175
|
+
def generated_function(*args, **kwargs):
|
|
176
|
+
"""
|
|
177
|
+
Dynamically generated function wrapper for the tool schema.
|
|
178
|
+
|
|
179
|
+
This function is created at runtime to match the tool's parameter
|
|
180
|
+
signature as defined in the schema. It validates arguments and
|
|
181
|
+
forwards them to the tool's call interface.
|
|
182
|
+
"""
|
|
183
|
+
# Bind arguments to signature
|
|
184
|
+
bound = new_signature.bind(*args, **kwargs)
|
|
185
|
+
bound.apply_defaults()
|
|
186
|
+
|
|
187
|
+
# Validate required parameters
|
|
188
|
+
missing_required = [
|
|
189
|
+
param_name
|
|
190
|
+
for param_name in required
|
|
191
|
+
if param_name not in bound.arguments
|
|
192
|
+
or bound.arguments[param_name] is None
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
if missing_required:
|
|
196
|
+
raise TypeError(
|
|
197
|
+
f"Missing required arguments: {set(missing_required)}",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Filter kwargs based on defined properties and remove None
|
|
201
|
+
# values for optional params
|
|
202
|
+
filtered_kwargs = {
|
|
203
|
+
k: v
|
|
204
|
+
for k, v in bound.arguments.items()
|
|
205
|
+
if k in properties and (k in required or v is not None)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return tool_call(**filtered_kwargs)
|
|
209
|
+
|
|
210
|
+
# Set the correct signature and metadata
|
|
211
|
+
generated_function.__signature__ = new_signature
|
|
212
|
+
generated_function.__name__ = self.name
|
|
213
|
+
|
|
214
|
+
# Build docstring with parameter information
|
|
215
|
+
doc_parts = []
|
|
216
|
+
for name, info in properties.items():
|
|
217
|
+
required_str = " (required)" if name in required else " (optional)"
|
|
218
|
+
doc_parts.append(
|
|
219
|
+
f" {name}: {info.get('type', 'string')}{required_str} -"
|
|
220
|
+
f" {info.get('description', '')}",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
generated_function.__doc__ = (
|
|
224
|
+
self.schema["function"]["description"]
|
|
225
|
+
+ "\n\nParameters:\n"
|
|
226
|
+
+ "\n".join(doc_parts)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Set type annotations for compatibility with typing inspection
|
|
230
|
+
annotations = {param.name: param.annotation for param in sig_params}
|
|
231
|
+
annotations["return"] = Any
|
|
232
|
+
generated_function.__annotations__ = annotations
|
|
233
|
+
|
|
234
|
+
return generated_function
|
agentscope_runtime/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
__version__ = "v0.
|
|
2
|
+
__version__ = "v0.1.1"
|