agentscope-runtime 1.0.1__py3-none-any.whl → 1.0.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 (58) hide show
  1. agentscope_runtime/adapters/agentscope/message.py +32 -7
  2. agentscope_runtime/adapters/agentscope/stream.py +121 -91
  3. agentscope_runtime/adapters/agno/__init__.py +0 -0
  4. agentscope_runtime/adapters/agno/message.py +30 -0
  5. agentscope_runtime/adapters/agno/stream.py +122 -0
  6. agentscope_runtime/adapters/langgraph/__init__.py +12 -0
  7. agentscope_runtime/adapters/langgraph/message.py +257 -0
  8. agentscope_runtime/adapters/langgraph/stream.py +205 -0
  9. agentscope_runtime/cli/__init__.py +7 -0
  10. agentscope_runtime/cli/cli.py +63 -0
  11. agentscope_runtime/cli/commands/__init__.py +2 -0
  12. agentscope_runtime/cli/commands/chat.py +815 -0
  13. agentscope_runtime/cli/commands/deploy.py +1062 -0
  14. agentscope_runtime/cli/commands/invoke.py +58 -0
  15. agentscope_runtime/cli/commands/list_cmd.py +103 -0
  16. agentscope_runtime/cli/commands/run.py +176 -0
  17. agentscope_runtime/cli/commands/sandbox.py +128 -0
  18. agentscope_runtime/cli/commands/status.py +60 -0
  19. agentscope_runtime/cli/commands/stop.py +185 -0
  20. agentscope_runtime/cli/commands/web.py +166 -0
  21. agentscope_runtime/cli/loaders/__init__.py +6 -0
  22. agentscope_runtime/cli/loaders/agent_loader.py +295 -0
  23. agentscope_runtime/cli/state/__init__.py +10 -0
  24. agentscope_runtime/cli/utils/__init__.py +18 -0
  25. agentscope_runtime/cli/utils/console.py +378 -0
  26. agentscope_runtime/cli/utils/validators.py +118 -0
  27. agentscope_runtime/engine/app/agent_app.py +7 -4
  28. agentscope_runtime/engine/deployers/__init__.py +1 -0
  29. agentscope_runtime/engine/deployers/agentrun_deployer.py +152 -22
  30. agentscope_runtime/engine/deployers/base.py +27 -2
  31. agentscope_runtime/engine/deployers/kubernetes_deployer.py +158 -31
  32. agentscope_runtime/engine/deployers/local_deployer.py +188 -25
  33. agentscope_runtime/engine/deployers/modelstudio_deployer.py +109 -18
  34. agentscope_runtime/engine/deployers/state/__init__.py +9 -0
  35. agentscope_runtime/engine/deployers/state/manager.py +388 -0
  36. agentscope_runtime/engine/deployers/state/schema.py +96 -0
  37. agentscope_runtime/engine/deployers/utils/build_cache.py +736 -0
  38. agentscope_runtime/engine/deployers/utils/detached_app.py +105 -30
  39. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +31 -10
  40. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +15 -8
  41. agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +30 -2
  42. agentscope_runtime/engine/deployers/utils/k8s_utils.py +241 -0
  43. agentscope_runtime/engine/deployers/utils/package.py +56 -6
  44. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +16 -2
  45. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +155 -5
  46. agentscope_runtime/engine/deployers/utils/wheel_packager.py +107 -123
  47. agentscope_runtime/engine/runner.py +25 -6
  48. agentscope_runtime/engine/schemas/exception.py +580 -0
  49. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +113 -39
  50. agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +20 -4
  51. agentscope_runtime/sandbox/utils.py +2 -0
  52. agentscope_runtime/version.py +1 -1
  53. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/METADATA +24 -7
  54. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/RECORD +58 -28
  55. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/entry_points.txt +1 -0
  56. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/WHEEL +0 -0
  57. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/licenses/LICENSE +0 -0
  58. {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1062 @@
1
+ # -*- coding: utf-8 -*-
2
+ """agentscope deploy command - Deploy agents to various platforms."""
3
+ # pylint: disable=too-many-statements, too-many-branches
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+ import click
11
+ import yaml
12
+
13
+ from agentscope_runtime.cli.utils.console import (
14
+ echo_error,
15
+ echo_info,
16
+ echo_success,
17
+ echo_warning,
18
+ )
19
+
20
+ # Only import LocalDeployManager directly (needs app object, will use loader
21
+ # internally)
22
+ from agentscope_runtime.engine.deployers.local_deployer import (
23
+ LocalDeployManager,
24
+ )
25
+ from agentscope_runtime.engine.deployers.utils.deployment_modes import (
26
+ DeploymentMode,
27
+ )
28
+
29
+ # Optional imports for cloud deployers
30
+ try:
31
+ from agentscope_runtime.engine.deployers.modelstudio_deployer import (
32
+ ModelstudioDeployManager,
33
+ )
34
+
35
+ MODELSTUDIO_AVAILABLE = True
36
+ except ImportError:
37
+ MODELSTUDIO_AVAILABLE = False
38
+
39
+ try:
40
+ from agentscope_runtime.engine.deployers.agentrun_deployer import (
41
+ AgentRunDeployManager,
42
+ )
43
+
44
+ AGENTRUN_AVAILABLE = True
45
+ except ImportError:
46
+ AGENTRUN_AVAILABLE = False
47
+
48
+ try:
49
+ from agentscope_runtime.engine.deployers.kubernetes_deployer import (
50
+ KubernetesDeployManager,
51
+ K8sConfig,
52
+ RegistryConfig,
53
+ )
54
+
55
+ K8S_AVAILABLE = True
56
+ except ImportError:
57
+ K8S_AVAILABLE = False
58
+
59
+
60
+ def _validate_source(source: str) -> tuple[str, str]:
61
+ """
62
+ Validate source path and determine its type.
63
+
64
+ Returns:
65
+ Tuple of (absolute_path, source_type) where source_type
66
+ is 'file' or 'directory'
67
+
68
+ Raises:
69
+ ValueError: If source doesn't exist
70
+ """
71
+ abs_source = os.path.abspath(source)
72
+
73
+ if not os.path.exists(abs_source):
74
+ raise ValueError(f"Source not found: {abs_source}")
75
+
76
+ if os.path.isdir(abs_source):
77
+ return abs_source, "directory"
78
+ elif os.path.isfile(abs_source):
79
+ return abs_source, "file"
80
+ else:
81
+ raise ValueError(f"Source must be a file or directory: {abs_source}")
82
+
83
+
84
+ def _find_entrypoint(project_dir: str, entrypoint: str = None) -> str:
85
+ """
86
+ Find or validate entrypoint file in project directory.
87
+
88
+ Args:
89
+ project_dir: Project directory path
90
+ entrypoint: Optional user-specified entrypoint file name
91
+
92
+ Returns:
93
+ Entrypoint file name (relative to project_dir)
94
+
95
+ Raises:
96
+ ValueError: If entrypoint not found
97
+ """
98
+ if entrypoint:
99
+ entry_path = os.path.join(project_dir, entrypoint)
100
+ if not os.path.isfile(entry_path):
101
+ raise ValueError(f"Entrypoint file not found: {entry_path}")
102
+ return entrypoint
103
+
104
+ # Try default entry files
105
+ for candidate in ["app.py", "agent.py", "main.py"]:
106
+ candidate_path = os.path.join(project_dir, candidate)
107
+ if os.path.isfile(candidate_path):
108
+ return candidate
109
+
110
+ raise ValueError(
111
+ f"No entry point found in {project_dir}. "
112
+ f"Use --entrypoint to specify one.",
113
+ )
114
+
115
+
116
+ def _load_config_file(config_path: str) -> dict:
117
+ """
118
+ Load deployment configuration from JSON or YAML file.
119
+
120
+ Args:
121
+ config_path: Path to config file (.json, .yaml, or .yml)
122
+
123
+ Returns:
124
+ Dictionary of configuration parameters
125
+
126
+ Raises:
127
+ ValueError: If file format is unsupported or parsing fails
128
+ """
129
+ if not os.path.isfile(config_path):
130
+ raise ValueError(f"Config file not found: {config_path}")
131
+
132
+ file_ext = os.path.splitext(config_path)[1].lower()
133
+
134
+ try:
135
+ with open(config_path, "r", encoding="utf-8") as f:
136
+ if file_ext == ".json":
137
+ return json.load(f)
138
+ elif file_ext in [".yaml", ".yml"]:
139
+ return yaml.safe_load(f)
140
+ else:
141
+ raise ValueError(
142
+ f"Unsupported config file format: {file_ext}. "
143
+ f"Use .json, .yaml, or .yml",
144
+ )
145
+ except (json.JSONDecodeError, yaml.YAMLError) as e:
146
+ raise ValueError(
147
+ f"Failed to parse config file {config_path}: {e}",
148
+ ) from e
149
+
150
+
151
+ def _merge_config(config_dict: dict, cli_params: dict) -> dict:
152
+ """
153
+ Merge config file with CLI parameters. CLI parameters take precedence.
154
+
155
+ Args:
156
+ config_dict: Configuration from file
157
+ cli_params: Parameters from CLI options
158
+
159
+ Returns:
160
+ Merged configuration dictionary
161
+ """
162
+ merged = config_dict.copy()
163
+
164
+ # Override with non-None CLI parameters
165
+ for key, value in cli_params.items():
166
+ if value is not None:
167
+ # Special handling for tuples (like env)
168
+ if key == "env" and value:
169
+ # Merge environment variables
170
+ if "environment" not in merged:
171
+ merged["environment"] = {}
172
+ # CLI env overrides config environment
173
+ continue # Will be handled by _parse_environment
174
+ merged[key] = value
175
+
176
+ return merged
177
+
178
+
179
+ def _parse_environment(env_tuples: tuple, env_file: str = None) -> dict:
180
+ """
181
+ Parse environment variables from --env options and --env-file.
182
+
183
+ Args:
184
+ env_tuples: Tuple of KEY=VALUE strings from --env options
185
+ env_file: Optional path to .env file
186
+
187
+ Returns:
188
+ Dictionary of environment variables
189
+
190
+ Raises:
191
+ ValueError: If env format is invalid
192
+ """
193
+ environment = {}
194
+
195
+ # 1. Load from env file first (if provided)
196
+ if env_file:
197
+ if not os.path.isfile(env_file):
198
+ raise ValueError(f"Environment file not found: {env_file}")
199
+
200
+ with open(env_file, "r", encoding="utf-8") as f:
201
+ for line_num, line in enumerate(f, 1):
202
+ line = line.strip()
203
+ # Skip empty lines and comments
204
+ if not line or line.startswith("#"):
205
+ continue
206
+
207
+ if "=" not in line:
208
+ echo_warning(
209
+ f"Skipping invalid line {line_num} in {env_file}: "
210
+ f"{line}",
211
+ )
212
+ continue
213
+
214
+ key, value = line.split("=", 1)
215
+ key = key.strip()
216
+ value = value.strip()
217
+
218
+ # Remove quotes if present
219
+ if value.startswith('"') and value.endswith('"'):
220
+ value = value[1:-1]
221
+ elif value.startswith("'") and value.endswith("'"):
222
+ value = value[1:-1]
223
+
224
+ environment[key] = value
225
+
226
+ # 2. Override with --env options (command line takes precedence)
227
+ for env_pair in env_tuples:
228
+ if "=" not in env_pair:
229
+ raise ValueError(
230
+ f"Invalid env format: '{env_pair}'. Use KEY=VALUE format",
231
+ )
232
+
233
+ key, value = env_pair.split("=", 1)
234
+ environment[key.strip()] = value.strip()
235
+
236
+ return environment
237
+
238
+
239
+ @click.group()
240
+ def deploy():
241
+ """
242
+ Deploy agents to various platforms.
243
+
244
+ Supported platforms:
245
+ \b
246
+ - modelstudio: Alibaba Cloud ModelStudio
247
+ - agentrun: Alibaba Cloud AgentRun
248
+ - k8s: Kubernetes/ACK
249
+ - local: Local deployment (detached mode)
250
+
251
+ Use 'agentscope deploy <platform> --help' for platform-specific options.
252
+ """
253
+
254
+
255
+ @deploy.command()
256
+ @click.argument("source", required=True)
257
+ @click.option("--name", help="Deployment name", default=None)
258
+ @click.option("--host", help="Host to bind to", default=None)
259
+ @click.option(
260
+ "--port",
261
+ help="Port to expose",
262
+ default=None,
263
+ type=int,
264
+ )
265
+ @click.option(
266
+ "--entrypoint",
267
+ "-e",
268
+ help="Entrypoint file name for directory sources (e.g., 'app.py', "
269
+ "'main.py')",
270
+ default=None,
271
+ )
272
+ @click.option(
273
+ "--env",
274
+ "-E",
275
+ multiple=True,
276
+ help="Environment variable in KEY=VALUE format (can be repeated)",
277
+ )
278
+ @click.option(
279
+ "--env-file",
280
+ type=click.Path(exists=True),
281
+ help="Path to .env file with environment variables",
282
+ )
283
+ @click.option(
284
+ "--config",
285
+ "-c",
286
+ type=click.Path(exists=True),
287
+ help="Path to deployment config file (.json, .yaml, or .yml)",
288
+ )
289
+ def local(
290
+ source: str,
291
+ name: str,
292
+ host: str,
293
+ port: int,
294
+ entrypoint: str,
295
+ env: tuple,
296
+ env_file: str,
297
+ config: str,
298
+ ):
299
+ """
300
+ Deploy locally in detached mode.
301
+
302
+ SOURCE can be a Python file or project directory containing an agent.
303
+ """
304
+ try:
305
+ echo_info(f"Preparing deployment from {source}...")
306
+
307
+ # Load config file if provided
308
+ config_dict = {}
309
+ if config:
310
+ echo_info(f"Loading configuration from {config}...")
311
+ config_dict = _load_config_file(config)
312
+
313
+ # Merge CLI parameters with config (CLI takes precedence)
314
+ cli_params = {
315
+ "name": name,
316
+ "host": host,
317
+ "port": port,
318
+ "entrypoint": entrypoint,
319
+ }
320
+ merged_config = _merge_config(config_dict, cli_params)
321
+
322
+ # Extract parameters with defaults
323
+ host = merged_config.get("host", "127.0.0.1")
324
+ port = merged_config.get("port", 8090)
325
+ entrypoint = merged_config.get("entrypoint")
326
+
327
+ # Validate source
328
+ abs_source, source_type = _validate_source(source)
329
+
330
+ # Parse environment variables (from config, env_file, and CLI)
331
+ environment = merged_config.get("environment", {}).copy()
332
+ cli_env = _parse_environment(env, env_file)
333
+ environment.update(cli_env) # CLI env overrides config env
334
+
335
+ if environment:
336
+ echo_info(f"Using {len(environment)} environment variable(s)")
337
+
338
+ # Create deployer
339
+ deployer = LocalDeployManager(host=host, port=port)
340
+
341
+ # Prepare entrypoint specification
342
+ if source_type == "directory":
343
+ # For directory: find entrypoint and create path
344
+ project_dir = abs_source
345
+ entry_script = _find_entrypoint(project_dir, entrypoint)
346
+ entrypoint_spec = os.path.join(project_dir, entry_script)
347
+
348
+ echo_info(f"Using project directory: {project_dir}")
349
+ echo_info(f"Entry script: {entry_script}")
350
+ else:
351
+ # For single file: use file path directly
352
+ entrypoint_spec = abs_source
353
+
354
+ echo_info(f"Using file: {abs_source}")
355
+
356
+ # Deploy locally using entrypoint
357
+ echo_info(f"Deploying agent to {host}:{port} in detached mode...")
358
+ result = asyncio.run(
359
+ deployer.deploy(
360
+ entrypoint=entrypoint_spec,
361
+ mode=DeploymentMode.DETACHED_PROCESS,
362
+ environment=environment if environment else None,
363
+ agent_source=abs_source, # Pass source for state saving
364
+ ),
365
+ )
366
+
367
+ deploy_id = result.get("deploy_id")
368
+ url = result.get("url")
369
+
370
+ echo_success("Deployment successful!")
371
+ echo_info(f"Deployment ID: {deploy_id}")
372
+ echo_info(f"URL: {url}")
373
+ echo_info(f"Use 'agentscope stop {deploy_id}' to stop the deployment")
374
+
375
+ except Exception as e:
376
+ # Error details (including process logs) are already logged by the
377
+ # deployer
378
+ # Just show a simple error message here without the full traceback
379
+ echo_error(f"Deployment failed: {e}")
380
+ sys.exit(1)
381
+
382
+
383
+ @deploy.command()
384
+ @click.argument("source", required=True)
385
+ @click.option("--name", help="Deployment name", default=None)
386
+ @click.option(
387
+ "--entrypoint",
388
+ "-e",
389
+ help="Entrypoint file name for directory sources (e.g., 'app.py', "
390
+ "'main.py')",
391
+ default=None,
392
+ )
393
+ @click.option(
394
+ "--skip-upload",
395
+ is_flag=True,
396
+ help="Build package without uploading",
397
+ )
398
+ @click.option(
399
+ "--env",
400
+ "-E",
401
+ multiple=True,
402
+ help="Environment variable in KEY=VALUE format (can be repeated)",
403
+ )
404
+ @click.option(
405
+ "--env-file",
406
+ type=click.Path(exists=True),
407
+ help="Path to .env file with environment variables",
408
+ )
409
+ @click.option(
410
+ "--config",
411
+ "-c",
412
+ type=click.Path(exists=True),
413
+ help="Path to deployment config file (.json, .yaml, or .yml)",
414
+ )
415
+ def modelstudio(
416
+ source: str,
417
+ name: str,
418
+ entrypoint: str,
419
+ skip_upload: bool,
420
+ env: tuple,
421
+ env_file: str,
422
+ config: str,
423
+ ):
424
+ """
425
+ Deploy to Alibaba Cloud ModelStudio.
426
+
427
+ SOURCE can be a Python file or project directory containing an agent.
428
+
429
+ Required environment variables:
430
+ - ALIBABA_CLOUD_ACCESS_KEY_ID
431
+ - ALIBABA_CLOUD_ACCESS_KEY_SECRET
432
+ - MODELSTUDIO_WORKSPACE_ID
433
+ """
434
+ if not MODELSTUDIO_AVAILABLE:
435
+ echo_error("ModelStudio deployer is not available")
436
+ echo_info(
437
+ "Please install required dependencies: alibabacloud-oss-v2 "
438
+ "alibabacloud-bailian20231229",
439
+ )
440
+ sys.exit(1)
441
+
442
+ try:
443
+ echo_info(f"Preparing deployment from {source}...")
444
+
445
+ # Load config file if provided
446
+ config_dict = {}
447
+ if config:
448
+ echo_info(f"Loading configuration from {config}...")
449
+ config_dict = _load_config_file(config)
450
+
451
+ # Merge CLI parameters with config (CLI takes precedence)
452
+ cli_params = {
453
+ "name": name,
454
+ "entrypoint": entrypoint,
455
+ "skip_upload": skip_upload if skip_upload else None,
456
+ }
457
+ merged_config = _merge_config(config_dict, cli_params)
458
+
459
+ # Extract parameters
460
+ name = merged_config.get("name") or merged_config.get("deploy_name")
461
+ entrypoint = merged_config.get("entrypoint")
462
+ skip_upload = merged_config.get("skip_upload", False)
463
+
464
+ # Validate source
465
+ abs_source, source_type = _validate_source(source)
466
+
467
+ # Parse environment variables (from config, env_file, and CLI)
468
+ environment = merged_config.get("environment", {}).copy()
469
+ cli_env = _parse_environment(env, env_file)
470
+ environment.update(cli_env) # CLI env overrides config env
471
+
472
+ if environment:
473
+ echo_info(f"Using {len(environment)} environment variable(s)")
474
+
475
+ # Create deployer
476
+ deployer = ModelstudioDeployManager()
477
+
478
+ # Prepare deployment parameters - ModelStudio always needs
479
+ # project_dir + cmd
480
+ if source_type == "directory":
481
+ # For directory: use directory as project_dir
482
+ project_dir = abs_source
483
+ entry_script = _find_entrypoint(project_dir, entrypoint)
484
+ cmd = f"python {entry_script}"
485
+
486
+ echo_info(f"Using project directory: {project_dir}")
487
+ echo_info(f"Entry script: {entry_script}")
488
+ else:
489
+ # For single file: use parent directory as project_dir
490
+ file_path = abs_source
491
+ project_dir = os.path.dirname(file_path)
492
+ entry_filename = os.path.basename(file_path)
493
+ cmd = f"python {entry_filename}"
494
+
495
+ echo_info(f"Using file: {file_path}")
496
+ echo_info(f"Project directory: {project_dir}")
497
+
498
+ # Deploy to ModelStudio using project_dir + cmd
499
+ echo_info("Deploying to ModelStudio...")
500
+ result = asyncio.run(
501
+ deployer.deploy(
502
+ project_dir=project_dir,
503
+ cmd=cmd,
504
+ deploy_name=name,
505
+ skip_upload=skip_upload,
506
+ environment=environment if environment else None,
507
+ agent_source=abs_source, # Pass source for state saving
508
+ ),
509
+ )
510
+
511
+ if skip_upload:
512
+ echo_success("Package built successfully")
513
+ echo_info(f"Wheel path: {result.get('wheel_path')}")
514
+ else:
515
+ deploy_id = result.get("deploy_id")
516
+ url = result.get("url")
517
+ workspace_id = result.get("workspace_id")
518
+
519
+ echo_success("Deployment successful!")
520
+ echo_info(f"Deployment ID: {deploy_id}")
521
+ echo_info(f"Console URL: {url}")
522
+ echo_info(f"Workspace ID: {workspace_id}")
523
+
524
+ except Exception as e:
525
+ echo_error(f"Deployment failed: {e}")
526
+ import traceback
527
+
528
+ echo_error(traceback.format_exc())
529
+ sys.exit(1)
530
+
531
+
532
+ @deploy.command()
533
+ @click.argument("source", required=True)
534
+ @click.option("--name", help="Deployment name", default=None)
535
+ @click.option(
536
+ "--entrypoint",
537
+ "-e",
538
+ help="Entrypoint file name for directory sources (e.g., 'app.py', "
539
+ "'main.py')",
540
+ default=None,
541
+ )
542
+ @click.option(
543
+ "--skip-upload",
544
+ is_flag=True,
545
+ help="Build package without uploading",
546
+ )
547
+ @click.option("--region", help="Alibaba Cloud region", default=None)
548
+ @click.option("--cpu", help="CPU allocation (cores)", type=float, default=None)
549
+ @click.option(
550
+ "--memory",
551
+ help="Memory allocation (MB)",
552
+ type=int,
553
+ default=None,
554
+ )
555
+ @click.option(
556
+ "--env",
557
+ "-E",
558
+ multiple=True,
559
+ help="Environment variable in KEY=VALUE format (can be repeated)",
560
+ )
561
+ @click.option(
562
+ "--env-file",
563
+ type=click.Path(exists=True),
564
+ help="Path to .env file with environment variables",
565
+ )
566
+ @click.option(
567
+ "--config",
568
+ "-c",
569
+ type=click.Path(exists=True),
570
+ help="Path to deployment config file (.json, .yaml, or .yml)",
571
+ )
572
+ def agentrun(
573
+ source: str,
574
+ name: str,
575
+ entrypoint: str,
576
+ skip_upload: bool,
577
+ region: str,
578
+ cpu: float,
579
+ memory: int,
580
+ env: tuple,
581
+ env_file: str,
582
+ config: str,
583
+ ):
584
+ """
585
+ Deploy to Alibaba Cloud AgentRun.
586
+
587
+ SOURCE can be a Python file or project directory containing an agent.
588
+
589
+ Required environment variables:
590
+ - ALIBABA_CLOUD_ACCESS_KEY_ID
591
+ - ALIBABA_CLOUD_ACCESS_KEY_SECRET
592
+ """
593
+ if not AGENTRUN_AVAILABLE:
594
+ echo_error("AgentRun deployer is not available")
595
+ echo_info(
596
+ "Please install required dependencies: "
597
+ "alibabacloud-agentrun20250910",
598
+ )
599
+ sys.exit(1)
600
+
601
+ try:
602
+ echo_info(f"Preparing deployment from {source}...")
603
+
604
+ # Load config file if provided
605
+ config_dict = {}
606
+ if config:
607
+ echo_info(f"Loading configuration from {config}...")
608
+ config_dict = _load_config_file(config)
609
+
610
+ # Merge CLI parameters with config (CLI takes precedence)
611
+ cli_params = {
612
+ "name": name,
613
+ "entrypoint": entrypoint,
614
+ "skip_upload": skip_upload if skip_upload else None,
615
+ "region": region,
616
+ "cpu": cpu,
617
+ "memory": memory,
618
+ }
619
+ merged_config = _merge_config(config_dict, cli_params)
620
+
621
+ # Extract parameters with defaults
622
+ name = merged_config.get("name") or merged_config.get("deploy_name")
623
+ entrypoint = merged_config.get("entrypoint")
624
+ skip_upload = merged_config.get("skip_upload", False)
625
+ region = merged_config.get("region", "cn-hangzhou")
626
+ cpu = merged_config.get("cpu", 2.0)
627
+ memory = merged_config.get("memory", 2048)
628
+
629
+ # Validate source
630
+ abs_source, source_type = _validate_source(source)
631
+
632
+ # Parse environment variables (from config, env_file, and CLI)
633
+ environment = merged_config.get("environment", {}).copy()
634
+ cli_env = _parse_environment(env, env_file)
635
+ environment.update(cli_env) # CLI env overrides config env
636
+
637
+ if environment:
638
+ echo_info(f"Using {len(environment)} environment variable(s)")
639
+
640
+ # Set region and resource config
641
+ if region:
642
+ os.environ["AGENT_RUN_REGION_ID"] = region
643
+ if cpu:
644
+ os.environ["AGENT_RUN_CPU"] = str(cpu)
645
+ if memory:
646
+ os.environ["AGENT_RUN_MEMORY"] = str(memory)
647
+
648
+ # Create deployer
649
+ deployer = AgentRunDeployManager()
650
+
651
+ # Prepare deployment - AgentRun always needs project_dir + cmd
652
+ if source_type == "directory":
653
+ # For directory: use directory as project_dir
654
+ project_dir = abs_source
655
+ entry_script = _find_entrypoint(project_dir, entrypoint)
656
+ cmd = f"python {entry_script}"
657
+
658
+ echo_info(f"Using project directory: {project_dir}")
659
+ echo_info(f"Entry script: {entry_script}")
660
+ else:
661
+ # For single file: use parent directory as project_dir
662
+ file_path = abs_source
663
+ project_dir = os.path.dirname(file_path)
664
+ entry_filename = os.path.basename(file_path)
665
+ cmd = f"python {entry_filename}"
666
+
667
+ echo_info(f"Using file: {file_path}")
668
+ echo_info(f"Project directory: {project_dir}")
669
+
670
+ # Deploy to AgentRun using project_dir + cmd
671
+ echo_info("Deploying to AgentRun...")
672
+ result = asyncio.run(
673
+ deployer.deploy(
674
+ project_dir=project_dir,
675
+ cmd=cmd,
676
+ deploy_name=name,
677
+ skip_upload=skip_upload,
678
+ environment=environment if environment else None,
679
+ agent_source=abs_source, # Pass source for state saving
680
+ ),
681
+ )
682
+
683
+ if skip_upload:
684
+ echo_success("Package built successfully")
685
+ echo_info(f"Wheel path: {result.get('wheel_path')}")
686
+ else:
687
+ deploy_id = result.get("agentrun_id") or result.get("deploy_id")
688
+ url = result.get("url")
689
+ endpoint_url = result.get("agentrun_endpoint_url")
690
+
691
+ echo_success("Deployment successful!")
692
+ echo_info(f"Deployment ID: {deploy_id}")
693
+ echo_info(f"Endpoint URL: {endpoint_url}")
694
+ echo_info(f"Console URL: {url}")
695
+
696
+ except Exception as e:
697
+ echo_error(f"Deployment failed: {e}")
698
+ import traceback
699
+
700
+ echo_error(traceback.format_exc())
701
+ sys.exit(1)
702
+
703
+
704
+ @deploy.command()
705
+ @click.argument("source", required=True)
706
+ @click.option("--name", help="Deployment name", default=None)
707
+ @click.option(
708
+ "--namespace",
709
+ help="Kubernetes namespace",
710
+ default="agentscope-runtime",
711
+ )
712
+ @click.option(
713
+ "--kube-config-path",
714
+ "-c",
715
+ type=click.Path(exists=True),
716
+ help="Path to deployment config file (.json, .yaml, or .yml)",
717
+ )
718
+ @click.option(
719
+ "--replicas",
720
+ help="Number of replicas",
721
+ type=int,
722
+ default=1,
723
+ )
724
+ @click.option(
725
+ "--port",
726
+ help="Container port",
727
+ type=int,
728
+ default=8080,
729
+ )
730
+ @click.option(
731
+ "--image-name",
732
+ help="Docker image name",
733
+ default="agent_app",
734
+ )
735
+ @click.option(
736
+ "--image-tag",
737
+ help="Docker image tag",
738
+ default="linux-amd64",
739
+ )
740
+ @click.option(
741
+ "--registry-url",
742
+ help="Remote registry url",
743
+ default="localhost",
744
+ )
745
+ @click.option(
746
+ "--registry-namespace",
747
+ help="Remote registry namespace",
748
+ default="agentscope-runtime",
749
+ )
750
+ @click.option(
751
+ "--push",
752
+ is_flag=True,
753
+ help="Push image to registry",
754
+ )
755
+ @click.option(
756
+ "--entrypoint",
757
+ "-e",
758
+ help="Entrypoint file name for directory sources (e.g., 'app.py', "
759
+ "'main.py')",
760
+ default=None,
761
+ )
762
+ @click.option(
763
+ "--env",
764
+ "-E",
765
+ multiple=True,
766
+ help="Environment variable in KEY=VALUE format (can be repeated)",
767
+ )
768
+ @click.option(
769
+ "--env-file",
770
+ type=click.Path(exists=True),
771
+ help="Path to .env file with environment variables",
772
+ )
773
+ @click.option(
774
+ "--config",
775
+ "-c",
776
+ type=click.Path(exists=True),
777
+ help="Path to deployment config file (.json, .yaml, or .yml)",
778
+ )
779
+ @click.option(
780
+ "--base-image",
781
+ help="Base Docker image",
782
+ default="python:3.10-slim-bookworm",
783
+ )
784
+ @click.option(
785
+ "--requirements",
786
+ help="Python requirements (comma-separated or file path)",
787
+ default=None,
788
+ )
789
+ @click.option(
790
+ "--cpu-request",
791
+ help="CPU resource request (e.g., '200m', '1')",
792
+ default="200m",
793
+ )
794
+ @click.option(
795
+ "--cpu-limit",
796
+ help="CPU resource limit (e.g., '1000m', '2')",
797
+ default="1000m",
798
+ )
799
+ @click.option(
800
+ "--memory-request",
801
+ help="Memory resource request (e.g., '512Mi', '1Gi')",
802
+ default="512Mi",
803
+ )
804
+ @click.option(
805
+ "--memory-limit",
806
+ help="Memory resource limit (e.g., '2Gi', '4Gi')",
807
+ default="2Gi",
808
+ )
809
+ @click.option(
810
+ "--image-pull-policy",
811
+ help="Image pull policy",
812
+ type=click.Choice(["Always", "IfNotPresent", "Never"]),
813
+ default="IfNotPresent",
814
+ )
815
+ @click.option(
816
+ "--deploy-timeout",
817
+ help="Deployment timeout in seconds",
818
+ type=int,
819
+ default=300,
820
+ )
821
+ @click.option(
822
+ "--health-check",
823
+ is_flag=True,
824
+ help="Enable/disable health check",
825
+ )
826
+ @click.option(
827
+ "--platform",
828
+ help="Target platform (e.g., 'linux/amd64', 'linux/arm64')",
829
+ default="linux/amd64",
830
+ )
831
+ def k8s(
832
+ source: str,
833
+ name: str,
834
+ namespace: str,
835
+ kube_config_path: str,
836
+ replicas: int,
837
+ port: int,
838
+ image_name: str,
839
+ image_tag: str,
840
+ registry_url: str,
841
+ registry_namespace: str,
842
+ push: bool,
843
+ entrypoint: str,
844
+ env: tuple,
845
+ env_file: str,
846
+ config: str,
847
+ base_image: str,
848
+ requirements: str,
849
+ cpu_request: str,
850
+ cpu_limit: str,
851
+ memory_request: str,
852
+ memory_limit: str,
853
+ image_pull_policy: str,
854
+ deploy_timeout: int,
855
+ health_check: bool,
856
+ platform: str,
857
+ ):
858
+ """
859
+ Deploy to Kubernetes/ACK.
860
+
861
+ SOURCE can be a Python file or project directory containing an agent.
862
+
863
+ This will build a Docker image and deploy it to your Kubernetes cluster.
864
+ """
865
+ if not K8S_AVAILABLE:
866
+ echo_error("Kubernetes deployer is not available")
867
+ echo_info("Please ensure Docker and Kubernetes client are available")
868
+ sys.exit(1)
869
+
870
+ try:
871
+ echo_info(f"Preparing deployment from {source}...")
872
+
873
+ # Load config file if provided
874
+ config_dict = {}
875
+ if config:
876
+ echo_info(f"Loading configuration from {config}...")
877
+ config_dict = _load_config_file(config)
878
+
879
+ # make sure not to push if use local registry_url
880
+ if registry_url == "localhost":
881
+ push = False
882
+
883
+ # Merge CLI parameters with config (CLI takes precedence)
884
+ cli_params = {
885
+ "name": name,
886
+ "namespace": namespace,
887
+ "replicas": replicas,
888
+ "port": port,
889
+ "image_name": image_name,
890
+ "image_tag": image_tag,
891
+ "registry_url": registry_url,
892
+ "registry_namespace": registry_namespace,
893
+ "push_to_registry": push if push else None,
894
+ "entrypoint": entrypoint,
895
+ "base_image": base_image,
896
+ "requirements": requirements,
897
+ "image_pull_policy": image_pull_policy,
898
+ "deploy_timeout": deploy_timeout,
899
+ "health_check": health_check,
900
+ "platform": platform,
901
+ }
902
+ merged_config = _merge_config(config_dict, cli_params)
903
+
904
+ # Extract parameters with defaults
905
+ namespace = merged_config.get("namespace", "agentscope-runtime")
906
+ replicas = merged_config.get("replicas", 1)
907
+ port = merged_config.get("port", 8090)
908
+ image_name = merged_config.get("image_name", "agent_llm")
909
+ image_tag = merged_config.get("image_tag", "latest")
910
+ registry_url = merged_config.get("registry_url", "localhost")
911
+ registry_namespace = merged_config.get(
912
+ "registry_namespace",
913
+ "agentscope-runtime",
914
+ )
915
+ push_to_registry = merged_config.get("push_to_registry", False)
916
+ entrypoint = merged_config.get("entrypoint")
917
+ base_image = merged_config.get("base_image")
918
+ deploy_timeout = merged_config.get("deploy_timeout", 300)
919
+ health_check = merged_config.get("health_check", True)
920
+ platform = merged_config.get("platform")
921
+
922
+ # Handle requirements (can be comma-separated string, list, or file
923
+ # path)
924
+ requirements = merged_config.get("requirements")
925
+ if requirements:
926
+ if isinstance(requirements, str):
927
+ # Check if it's a file path
928
+ if os.path.isfile(requirements):
929
+ with open(requirements, "r", encoding="utf-8") as f:
930
+ requirements = [
931
+ line.strip()
932
+ for line in f
933
+ if line.strip() and not line.startswith("#")
934
+ ]
935
+ else:
936
+ # Treat as comma-separated string
937
+ requirements = [r.strip() for r in requirements.split(",")]
938
+
939
+ # Handle extra_packages
940
+ extra_packages = merged_config.get("extra_packages", [])
941
+
942
+ # Handle image_pull_policy
943
+ image_pull_policy = merged_config.get("image_pull_policy")
944
+
945
+ # Build runtime_config from resource parameters
946
+ runtime_config = merged_config.get("runtime_config", {})
947
+ if not runtime_config.get("resources"):
948
+ resources = {}
949
+ if cpu_request or memory_request:
950
+ resources["requests"] = {}
951
+ if cpu_request:
952
+ resources["requests"]["cpu"] = cpu_request
953
+ if memory_request:
954
+ resources["requests"]["memory"] = memory_request
955
+ if cpu_limit or memory_limit:
956
+ resources["limits"] = {}
957
+ if cpu_limit:
958
+ resources["limits"]["cpu"] = cpu_limit
959
+ if memory_limit:
960
+ resources["limits"]["memory"] = memory_limit
961
+ if resources:
962
+ runtime_config["resources"] = resources
963
+
964
+ if image_pull_policy and "image_pull_policy" not in runtime_config:
965
+ runtime_config["image_pull_policy"] = image_pull_policy
966
+
967
+ # Validate source
968
+ abs_source, source_type = _validate_source(source)
969
+
970
+ # Parse environment variables (from config, env_file, and CLI)
971
+ environment = merged_config.get("environment", {}).copy()
972
+ cli_env = _parse_environment(env, env_file)
973
+ environment.update(cli_env) # CLI env overrides config env
974
+
975
+ if environment:
976
+ echo_info(f"Using {len(environment)} environment variable(s)")
977
+
978
+ # Create deployer
979
+ k8s_config = K8sConfig(
980
+ k8s_namespace=namespace,
981
+ kubeconfig_path=kube_config_path,
982
+ )
983
+ registry_config = RegistryConfig(
984
+ registry_url=registry_url,
985
+ namespace=registry_namespace,
986
+ )
987
+ deployer = KubernetesDeployManager(
988
+ kube_config=k8s_config,
989
+ registry_config=registry_config,
990
+ )
991
+
992
+ # Prepare entrypoint specification
993
+ if source_type == "directory":
994
+ # For directory: find entrypoint and create path
995
+ project_dir = abs_source
996
+ entry_script = _find_entrypoint(project_dir, entrypoint)
997
+ entrypoint_spec = os.path.join(project_dir, entry_script)
998
+
999
+ echo_info(f"Using project directory: {project_dir}")
1000
+ echo_info(f"Entry script: {entry_script}")
1001
+ else:
1002
+ # For single file: use file path directly
1003
+ entrypoint_spec = abs_source
1004
+
1005
+ echo_info(f"Using file: {abs_source}")
1006
+
1007
+ # Deploy to Kubernetes using entrypoint
1008
+ echo_info("Deploying to Kubernetes...")
1009
+
1010
+ # Build deploy parameters
1011
+ deploy_params = {
1012
+ "entrypoint": entrypoint_spec,
1013
+ "port": port,
1014
+ "replicas": replicas,
1015
+ "image_name": image_name,
1016
+ "image_tag": image_tag,
1017
+ "push_to_registry": push_to_registry,
1018
+ "environment": environment if environment else None,
1019
+ }
1020
+
1021
+ # Add optional parameters if provided
1022
+ if base_image:
1023
+ deploy_params["base_image"] = base_image
1024
+ if requirements:
1025
+ deploy_params["requirements"] = requirements
1026
+ if extra_packages:
1027
+ deploy_params["extra_packages"] = extra_packages
1028
+ if runtime_config:
1029
+ deploy_params["runtime_config"] = runtime_config
1030
+ if deploy_timeout:
1031
+ deploy_params["deploy_timeout"] = deploy_timeout
1032
+ if health_check is not None:
1033
+ deploy_params["health_check"] = health_check
1034
+ if platform:
1035
+ deploy_params["platform"] = platform
1036
+
1037
+ # Add agent_source for state saving
1038
+ deploy_params["agent_source"] = abs_source
1039
+
1040
+ result = asyncio.run(deployer.deploy(**deploy_params))
1041
+
1042
+ deploy_id = result.get("deploy_id")
1043
+ url = result.get("url")
1044
+ resource_name = result.get("resource_name")
1045
+
1046
+ echo_success("Deployment successful!")
1047
+ echo_info(f"Deployment ID: {deploy_id}")
1048
+ echo_info(f"Resource Name: {resource_name}")
1049
+ echo_info(f"URL: {url}")
1050
+ echo_info(f"Namespace: {namespace}")
1051
+ echo_info(f"Replicas: {replicas}")
1052
+
1053
+ except Exception as e:
1054
+ echo_error(f"Deployment failed: {e}")
1055
+ import traceback
1056
+
1057
+ echo_error(traceback.format_exc())
1058
+ sys.exit(1)
1059
+
1060
+
1061
+ if __name__ == "__main__":
1062
+ deploy()