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.
- agentscope_runtime/adapters/agentscope/message.py +32 -7
- agentscope_runtime/adapters/agentscope/stream.py +121 -91
- agentscope_runtime/adapters/agno/__init__.py +0 -0
- agentscope_runtime/adapters/agno/message.py +30 -0
- agentscope_runtime/adapters/agno/stream.py +122 -0
- agentscope_runtime/adapters/langgraph/__init__.py +12 -0
- agentscope_runtime/adapters/langgraph/message.py +257 -0
- agentscope_runtime/adapters/langgraph/stream.py +205 -0
- agentscope_runtime/cli/__init__.py +7 -0
- agentscope_runtime/cli/cli.py +63 -0
- agentscope_runtime/cli/commands/__init__.py +2 -0
- agentscope_runtime/cli/commands/chat.py +815 -0
- agentscope_runtime/cli/commands/deploy.py +1062 -0
- agentscope_runtime/cli/commands/invoke.py +58 -0
- agentscope_runtime/cli/commands/list_cmd.py +103 -0
- agentscope_runtime/cli/commands/run.py +176 -0
- agentscope_runtime/cli/commands/sandbox.py +128 -0
- agentscope_runtime/cli/commands/status.py +60 -0
- agentscope_runtime/cli/commands/stop.py +185 -0
- agentscope_runtime/cli/commands/web.py +166 -0
- agentscope_runtime/cli/loaders/__init__.py +6 -0
- agentscope_runtime/cli/loaders/agent_loader.py +295 -0
- agentscope_runtime/cli/state/__init__.py +10 -0
- agentscope_runtime/cli/utils/__init__.py +18 -0
- agentscope_runtime/cli/utils/console.py +378 -0
- agentscope_runtime/cli/utils/validators.py +118 -0
- agentscope_runtime/engine/app/agent_app.py +7 -4
- agentscope_runtime/engine/deployers/__init__.py +1 -0
- agentscope_runtime/engine/deployers/agentrun_deployer.py +152 -22
- agentscope_runtime/engine/deployers/base.py +27 -2
- agentscope_runtime/engine/deployers/kubernetes_deployer.py +158 -31
- agentscope_runtime/engine/deployers/local_deployer.py +188 -25
- agentscope_runtime/engine/deployers/modelstudio_deployer.py +109 -18
- agentscope_runtime/engine/deployers/state/__init__.py +9 -0
- agentscope_runtime/engine/deployers/state/manager.py +388 -0
- agentscope_runtime/engine/deployers/state/schema.py +96 -0
- agentscope_runtime/engine/deployers/utils/build_cache.py +736 -0
- agentscope_runtime/engine/deployers/utils/detached_app.py +105 -30
- agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +31 -10
- agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +15 -8
- agentscope_runtime/engine/deployers/utils/docker_image_utils/image_factory.py +30 -2
- agentscope_runtime/engine/deployers/utils/k8s_utils.py +241 -0
- agentscope_runtime/engine/deployers/utils/package.py +56 -6
- agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +16 -2
- agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +155 -5
- agentscope_runtime/engine/deployers/utils/wheel_packager.py +107 -123
- agentscope_runtime/engine/runner.py +25 -6
- agentscope_runtime/engine/schemas/exception.py +580 -0
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +113 -39
- agentscope_runtime/sandbox/box/shared/routers/mcp_utils.py +20 -4
- agentscope_runtime/sandbox/utils.py +2 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/METADATA +24 -7
- {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/RECORD +58 -28
- {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/entry_points.txt +1 -0
- {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/WHEEL +0 -0
- {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.1.dist-info → agentscope_runtime-1.0.2.dist-info}/top_level.txt +0 -0
|
@@ -7,6 +7,7 @@ import logging
|
|
|
7
7
|
import os
|
|
8
8
|
import time
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Dict, Optional, List, Any, Union, Tuple
|
|
12
13
|
|
|
@@ -34,7 +35,9 @@ from pydantic import BaseModel, Field
|
|
|
34
35
|
from .adapter.protocol_adapter import ProtocolAdapter
|
|
35
36
|
from .base import DeployManager
|
|
36
37
|
from .local_deployer import LocalDeployManager
|
|
38
|
+
from .state import Deployment
|
|
37
39
|
from .utils.detached_app import get_bundle_entry_script
|
|
40
|
+
from .utils.package import generate_build_directory
|
|
38
41
|
from .utils.wheel_packager import (
|
|
39
42
|
default_deploy_name,
|
|
40
43
|
generate_wrapper_project,
|
|
@@ -95,7 +98,7 @@ class AgentRunConfig(BaseModel):
|
|
|
95
98
|
|
|
96
99
|
execution_role_arn: Optional[str] = None
|
|
97
100
|
|
|
98
|
-
session_concurrency_limit: Optional[int] =
|
|
101
|
+
session_concurrency_limit: Optional[int] = 200
|
|
99
102
|
session_idle_timeout_seconds: Optional[int] = 3600
|
|
100
103
|
|
|
101
104
|
@classmethod
|
|
@@ -147,7 +150,7 @@ class AgentRunConfig(BaseModel):
|
|
|
147
150
|
|
|
148
151
|
session_concurrency_limit_str = os.environ.get(
|
|
149
152
|
"AGENT_RUN_SESSION_CONCURRENCY_LIMIT",
|
|
150
|
-
"
|
|
153
|
+
"200",
|
|
151
154
|
)
|
|
152
155
|
session_idle_timeout_seconds_str = os.environ.get(
|
|
153
156
|
"AGENT_RUN_SESSION_IDLE_TIMEOUT_SECONDS",
|
|
@@ -218,6 +221,7 @@ class OSSConfig(BaseModel):
|
|
|
218
221
|
region: str = Field("cn-hangzhou", description="OSS region")
|
|
219
222
|
access_key_id: Optional[str] = None
|
|
220
223
|
access_key_secret: Optional[str] = None
|
|
224
|
+
bucket_name: str
|
|
221
225
|
|
|
222
226
|
@classmethod
|
|
223
227
|
def from_env(cls) -> "OSSConfig":
|
|
@@ -236,6 +240,7 @@ class OSSConfig(BaseModel):
|
|
|
236
240
|
"OSS_ACCESS_KEY_SECRET",
|
|
237
241
|
os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET"),
|
|
238
242
|
),
|
|
243
|
+
bucket_name=os.environ.get("OSS_BUCKET_NAME"),
|
|
239
244
|
)
|
|
240
245
|
|
|
241
246
|
def ensure_valid(self) -> None:
|
|
@@ -245,10 +250,14 @@ class OSSConfig(BaseModel):
|
|
|
245
250
|
RuntimeError: If required AccessKey credentials are missing.
|
|
246
251
|
"""
|
|
247
252
|
# Allow fallback to Alibaba Cloud AK/SK via from_env()
|
|
248
|
-
if
|
|
253
|
+
if (
|
|
254
|
+
not self.access_key_id
|
|
255
|
+
or not self.access_key_secret
|
|
256
|
+
or not self.bucket_name
|
|
257
|
+
):
|
|
249
258
|
raise RuntimeError(
|
|
250
259
|
"Missing AccessKey for OSS. Set either OSS_ACCESS_KEY_ID/OSS_ACCESS_KEY_SECRET "
|
|
251
|
-
"or ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
|
|
260
|
+
"or ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET or OSS_BUCKET_NAME.",
|
|
252
261
|
)
|
|
253
262
|
|
|
254
263
|
|
|
@@ -278,6 +287,7 @@ class AgentRunDeployManager(DeployManager):
|
|
|
278
287
|
oss_config: Optional[OSSConfig] = None,
|
|
279
288
|
agentrun_config: Optional[AgentRunConfig] = None,
|
|
280
289
|
build_root: Optional[Union[str, Path]] = None,
|
|
290
|
+
state_manager=None,
|
|
281
291
|
):
|
|
282
292
|
"""Initialize AgentRun deployment manager.
|
|
283
293
|
|
|
@@ -285,8 +295,9 @@ class AgentRunDeployManager(DeployManager):
|
|
|
285
295
|
oss_config: OSS configuration for artifact storage. If None, loads from environment.
|
|
286
296
|
agentrun_config: AgentRun service configuration. If None, loads from environment.
|
|
287
297
|
build_root: Root directory for build artifacts. If None, uses parent directory of current working directory.
|
|
298
|
+
state_manager: Deployment state manager. If None, creates a new instance.
|
|
288
299
|
"""
|
|
289
|
-
super().__init__()
|
|
300
|
+
super().__init__(state_manager=state_manager)
|
|
290
301
|
self.oss_config = oss_config or OSSConfig.from_env()
|
|
291
302
|
self.agentrun_config = agentrun_config or AgentRunConfig.from_env()
|
|
292
303
|
self.build_root = (
|
|
@@ -416,18 +427,19 @@ class AgentRunDeployManager(DeployManager):
|
|
|
416
427
|
)
|
|
417
428
|
|
|
418
429
|
name = deploy_name or default_deploy_name()
|
|
419
|
-
|
|
430
|
+
|
|
431
|
+
# Generate build directory with platform-aware naming
|
|
432
|
+
# proj_root = project_dir.resolve()
|
|
420
433
|
if isinstance(self.build_root, Path):
|
|
421
434
|
effective_build_root = self.build_root.resolve()
|
|
422
435
|
else:
|
|
423
436
|
if self.build_root:
|
|
424
437
|
effective_build_root = Path(self.build_root).resolve()
|
|
425
438
|
else:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
).resolve()
|
|
439
|
+
# Use centralized directory generation function
|
|
440
|
+
effective_build_root = generate_build_directory("agentrun")
|
|
429
441
|
|
|
430
|
-
build_dir = effective_build_root
|
|
442
|
+
build_dir = effective_build_root
|
|
431
443
|
build_dir.mkdir(parents=True, exist_ok=True)
|
|
432
444
|
|
|
433
445
|
logger.info("Generating wrapper project: %s", name)
|
|
@@ -442,6 +454,7 @@ class AgentRunDeployManager(DeployManager):
|
|
|
442
454
|
logger.info("Building wheel package from: %s", wrapper_project_dir)
|
|
443
455
|
wheel_path = build_wheel(wrapper_project_dir)
|
|
444
456
|
logger.info("Wheel package created: %s", wheel_path)
|
|
457
|
+
|
|
445
458
|
return wheel_path, name
|
|
446
459
|
|
|
447
460
|
def _generate_env_file(
|
|
@@ -560,9 +573,14 @@ class AgentRunDeployManager(DeployManager):
|
|
|
560
573
|
FileNotFoundError: If specified files/directories don't exist.
|
|
561
574
|
"""
|
|
562
575
|
if not agentrun_id:
|
|
563
|
-
if
|
|
576
|
+
if (
|
|
577
|
+
not app
|
|
578
|
+
and not runner
|
|
579
|
+
and not project_dir
|
|
580
|
+
and not external_whl_path
|
|
581
|
+
):
|
|
564
582
|
raise ValueError(
|
|
565
|
-
"Must provide either runner, project_dir, or external_whl_path",
|
|
583
|
+
"Must provide either app, runner, project_dir, or external_whl_path",
|
|
566
584
|
)
|
|
567
585
|
try:
|
|
568
586
|
if runner or app:
|
|
@@ -579,6 +597,7 @@ class AgentRunDeployManager(DeployManager):
|
|
|
579
597
|
protocol_adapters=protocol_adapters,
|
|
580
598
|
requirements=requirements,
|
|
581
599
|
extra_packages=extra_packages,
|
|
600
|
+
platform="agentrun",
|
|
582
601
|
**kwargs,
|
|
583
602
|
)
|
|
584
603
|
if project_dir:
|
|
@@ -647,7 +666,7 @@ class AgentRunDeployManager(DeployManager):
|
|
|
647
666
|
logger.info("Uploading zip package to OSS")
|
|
648
667
|
oss_result = await self._upload_to_fixed_oss_bucket(
|
|
649
668
|
zip_file_path=zip_file_path,
|
|
650
|
-
bucket_name=
|
|
669
|
+
bucket_name=self.oss_config.bucket_name,
|
|
651
670
|
)
|
|
652
671
|
logger.info("Zip package uploaded to OSS successfully")
|
|
653
672
|
|
|
@@ -661,21 +680,50 @@ class AgentRunDeployManager(DeployManager):
|
|
|
661
680
|
environment=environment,
|
|
662
681
|
)
|
|
663
682
|
|
|
683
|
+
# Use base class UUID deploy_id (already set in __init__)
|
|
684
|
+
deploy_id = self.deploy_id
|
|
685
|
+
agent_runtime_id = agentrun_deploy_result["agent_runtime_id"]
|
|
686
|
+
endpoint_url = agentrun_deploy_result.get(
|
|
687
|
+
"agent_runtime_public_endpoint_url",
|
|
688
|
+
"",
|
|
689
|
+
)
|
|
690
|
+
console_url = (
|
|
691
|
+
f"https://functionai.console.aliyun.com/{self.agentrun_config.region_id}/"
|
|
692
|
+
f"agent/infra/agent-runtime/agent-detail?id={agent_runtime_id}"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
# Save deployment to state manager
|
|
696
|
+
deployment = Deployment(
|
|
697
|
+
id=deploy_id,
|
|
698
|
+
platform="agentrun",
|
|
699
|
+
url=console_url,
|
|
700
|
+
status="running",
|
|
701
|
+
created_at=datetime.now().isoformat(),
|
|
702
|
+
agent_source=kwargs.get("agent_source"),
|
|
703
|
+
config={
|
|
704
|
+
"agent_runtime_id": agent_runtime_id,
|
|
705
|
+
"agent_runtime_endpoint_url": endpoint_url,
|
|
706
|
+
"resource_name": name,
|
|
707
|
+
"wheel_path": str(wheel_path),
|
|
708
|
+
"artifact_url": oss_result.get("presigned_url", ""),
|
|
709
|
+
"region_id": self.agentrun_config.region_id,
|
|
710
|
+
},
|
|
711
|
+
)
|
|
712
|
+
self.state_manager.save(deployment)
|
|
713
|
+
|
|
664
714
|
# Return deployment results
|
|
665
715
|
logger.info(
|
|
666
716
|
"Deployment completed successfully. Agent runtime ID: %s",
|
|
667
|
-
|
|
717
|
+
agent_runtime_id,
|
|
668
718
|
)
|
|
669
719
|
return {
|
|
670
720
|
"message": "Agent deployed successfully to AgentRun",
|
|
671
|
-
"agentrun_id":
|
|
672
|
-
"agentrun_endpoint_url":
|
|
673
|
-
"agent_runtime_public_endpoint_url"
|
|
674
|
-
],
|
|
721
|
+
"agentrun_id": agent_runtime_id,
|
|
722
|
+
"agentrun_endpoint_url": endpoint_url,
|
|
675
723
|
"wheel_path": str(wheel_path),
|
|
676
|
-
"artifact_url": oss_result
|
|
677
|
-
"url":
|
|
678
|
-
"deploy_id":
|
|
724
|
+
"artifact_url": oss_result.get("presigned_url", ""),
|
|
725
|
+
"url": console_url,
|
|
726
|
+
"deploy_id": deploy_id,
|
|
679
727
|
"resource_name": name,
|
|
680
728
|
}
|
|
681
729
|
|
|
@@ -861,7 +909,6 @@ ls -lh /output/{zip_filename}
|
|
|
861
909
|
from alibabacloud_oss_v2.credentials import (
|
|
862
910
|
StaticCredentialsProvider,
|
|
863
911
|
)
|
|
864
|
-
import datetime
|
|
865
912
|
except ImportError as e:
|
|
866
913
|
logger.error(
|
|
867
914
|
"OSS SDK not available. Install with: pip install alibabacloud-oss-v2",
|
|
@@ -2539,3 +2586,86 @@ ls -lh /output/{zip_filename}
|
|
|
2539
2586
|
"error": str(e),
|
|
2540
2587
|
"message": f"Exception occurred while publishing agent runtime version: {str(e)}",
|
|
2541
2588
|
}
|
|
2589
|
+
|
|
2590
|
+
async def stop(self, deploy_id: str, **kwargs) -> Dict[str, Any]:
|
|
2591
|
+
"""Stop AgentRun deployment by deleting it.
|
|
2592
|
+
|
|
2593
|
+
Args:
|
|
2594
|
+
deploy_id: AgentRun runtime ID (agent_runtime_id)
|
|
2595
|
+
**kwargs: Additional parameters
|
|
2596
|
+
|
|
2597
|
+
Returns:
|
|
2598
|
+
Dict with success status, message, and details
|
|
2599
|
+
"""
|
|
2600
|
+
try:
|
|
2601
|
+
# Try to get deployment info from state for context
|
|
2602
|
+
deployment_info = None
|
|
2603
|
+
deployment = None
|
|
2604
|
+
try:
|
|
2605
|
+
deployment = self.state_manager.get(deploy_id)
|
|
2606
|
+
if deployment:
|
|
2607
|
+
deployment_info = {
|
|
2608
|
+
"url": deployment.url
|
|
2609
|
+
if hasattr(deployment, "url")
|
|
2610
|
+
else None,
|
|
2611
|
+
"resource_name": getattr(
|
|
2612
|
+
deployment,
|
|
2613
|
+
"resource_name",
|
|
2614
|
+
None,
|
|
2615
|
+
),
|
|
2616
|
+
}
|
|
2617
|
+
logger.debug(
|
|
2618
|
+
f"Fetched deployment info from state: {deployment_info}",
|
|
2619
|
+
)
|
|
2620
|
+
except Exception as e:
|
|
2621
|
+
logger.debug(
|
|
2622
|
+
f"Could not fetch deployment info from state: {e}",
|
|
2623
|
+
)
|
|
2624
|
+
|
|
2625
|
+
logger.info(f"Stopping AgentRun deployment: {deploy_id}")
|
|
2626
|
+
|
|
2627
|
+
# Get agent_runtime_id from deployment config
|
|
2628
|
+
agent_runtime_id = None
|
|
2629
|
+
if deployment and deployment.config:
|
|
2630
|
+
agent_runtime_id = deployment.config.get("agent_runtime_id")
|
|
2631
|
+
|
|
2632
|
+
if not agent_runtime_id:
|
|
2633
|
+
# Fallback: try using deploy_id as agent_runtime_id for backward compatibility
|
|
2634
|
+
agent_runtime_id = deploy_id
|
|
2635
|
+
logger.warning(
|
|
2636
|
+
f"Could not find agent_runtime_id in deployment config, "
|
|
2637
|
+
f"using deploy_id as fallback: {deploy_id}",
|
|
2638
|
+
)
|
|
2639
|
+
|
|
2640
|
+
# Use the existing delete method with agent_runtime_id
|
|
2641
|
+
result = await self.delete(agent_runtime_id)
|
|
2642
|
+
|
|
2643
|
+
if result.get("success"):
|
|
2644
|
+
# Remove from state manager on successful deletion
|
|
2645
|
+
try:
|
|
2646
|
+
self.state_manager.update_status(deploy_id, "stopped")
|
|
2647
|
+
except KeyError:
|
|
2648
|
+
logger.debug(
|
|
2649
|
+
f"Deployment {deploy_id} not found in state (already removed)",
|
|
2650
|
+
)
|
|
2651
|
+
|
|
2652
|
+
return {
|
|
2653
|
+
"success": True,
|
|
2654
|
+
"message": f"AgentRun deployment {deploy_id} deleted successfully",
|
|
2655
|
+
"details": result,
|
|
2656
|
+
}
|
|
2657
|
+
else:
|
|
2658
|
+
return {
|
|
2659
|
+
"success": False,
|
|
2660
|
+
"message": f"Failed to delete AgentRun deployment: {result.get('message', 'Unknown error')}",
|
|
2661
|
+
"details": result,
|
|
2662
|
+
}
|
|
2663
|
+
except Exception as e:
|
|
2664
|
+
logger.error(
|
|
2665
|
+
f"Failed to stop AgentRun deployment {deploy_id}: {e}",
|
|
2666
|
+
)
|
|
2667
|
+
return {
|
|
2668
|
+
"success": False,
|
|
2669
|
+
"message": f"Failed to stop AgentRun deployment: {e}",
|
|
2670
|
+
"details": {"deploy_id": deploy_id, "error": str(e)},
|
|
2671
|
+
}
|
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import uuid
|
|
3
3
|
from abc import abstractmethod, ABC
|
|
4
|
-
from typing import Dict
|
|
4
|
+
from typing import Dict, Any
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
# there is not many attributes in it, consider it as interface, instead of
|
|
8
8
|
# pydantic BaseModel
|
|
9
9
|
class DeployManager(ABC):
|
|
10
|
-
def __init__(self):
|
|
10
|
+
def __init__(self, state_manager=None):
|
|
11
11
|
self.deploy_id = str(uuid.uuid4())
|
|
12
12
|
self._app = None
|
|
13
13
|
|
|
14
|
+
# Initialize state manager - shared across all deployers
|
|
15
|
+
if state_manager is None:
|
|
16
|
+
from agentscope_runtime.engine.deployers.state import (
|
|
17
|
+
DeploymentStateManager,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
state_manager = DeploymentStateManager()
|
|
21
|
+
self.state_manager = state_manager
|
|
22
|
+
|
|
14
23
|
@abstractmethod
|
|
15
24
|
async def deploy(self, *args, **kwargs) -> Dict[str, str]:
|
|
16
25
|
"""Deploy the service and return a dictionary with deploy_id and
|
|
17
26
|
URL."""
|
|
18
27
|
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
async def stop(self, deploy_id: str, **kwargs) -> Dict[str, Any]:
|
|
31
|
+
"""Stop the deployed service.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
deploy_id: Deployment identifier
|
|
35
|
+
**kwargs: Platform-specific parameters (url, namespace, etc.)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Dict with keys:
|
|
39
|
+
- success (bool): Whether stop succeeded
|
|
40
|
+
- message (str): Status message
|
|
41
|
+
- details (dict, optional): Platform-specific details
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
-
import
|
|
4
|
+
from datetime import datetime
|
|
5
5
|
from typing import Optional, Dict, List, Union, Any
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
|
+
from agentscope_runtime.engine.deployers.state import Deployment
|
|
9
10
|
from .adapter.protocol_adapter import ProtocolAdapter
|
|
10
11
|
from .base import DeployManager
|
|
11
12
|
from .utils.docker_image_utils import (
|
|
12
13
|
ImageFactory,
|
|
13
14
|
RegistryConfig,
|
|
14
15
|
)
|
|
16
|
+
from .utils.k8s_utils import isLocalK8sEnvironment
|
|
15
17
|
from ...common.container_clients.kubernetes_client import (
|
|
16
18
|
KubernetesClient,
|
|
17
19
|
)
|
|
@@ -36,7 +38,7 @@ class K8sConfig(BaseModel):
|
|
|
36
38
|
class BuildConfig(BaseModel):
|
|
37
39
|
"""Build configuration"""
|
|
38
40
|
|
|
39
|
-
build_context_dir: str =
|
|
41
|
+
build_context_dir: Optional[str] = None # None allows caching
|
|
40
42
|
dockerfile_template: str = None
|
|
41
43
|
build_timeout: int = 600 # 10 minutes
|
|
42
44
|
push_timeout: int = 300 # 5 minutes
|
|
@@ -51,15 +53,15 @@ class KubernetesDeployManager(DeployManager):
|
|
|
51
53
|
kube_config: K8sConfig = None,
|
|
52
54
|
registry_config: RegistryConfig = RegistryConfig(),
|
|
53
55
|
use_deployment: bool = True,
|
|
54
|
-
build_context_dir: str =
|
|
56
|
+
build_context_dir: Optional[str] = None,
|
|
57
|
+
state_manager=None,
|
|
55
58
|
):
|
|
56
|
-
super().__init__()
|
|
59
|
+
super().__init__(state_manager=state_manager)
|
|
57
60
|
self.kubeconfig = kube_config
|
|
58
61
|
self.registry_config = registry_config
|
|
59
62
|
self.image_factory = ImageFactory()
|
|
60
63
|
self.use_deployment = use_deployment
|
|
61
64
|
self.build_context_dir = build_context_dir
|
|
62
|
-
self._deployed_resources = {}
|
|
63
65
|
self._built_images = {}
|
|
64
66
|
|
|
65
67
|
self.k8s_client = KubernetesClient(
|
|
@@ -67,10 +69,65 @@ class KubernetesDeployManager(DeployManager):
|
|
|
67
69
|
image_registry=self.registry_config.get_full_url(),
|
|
68
70
|
)
|
|
69
71
|
|
|
72
|
+
@staticmethod
|
|
73
|
+
def get_service_endpoint(
|
|
74
|
+
service_external_ip: Optional[str],
|
|
75
|
+
service_port: Optional[Union[int, list]],
|
|
76
|
+
fallback_host: str = "127.0.0.1",
|
|
77
|
+
) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Auto-select appropriate service endpoint based on detected environment.
|
|
80
|
+
|
|
81
|
+
Solves the common issue where Kubernetes LoadBalancer/ExternalIP is not
|
|
82
|
+
reachable from localhost in local clusters (e.g., Minikube/Kind).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
service_external_ip: ExternalIP or LoadBalancer IP from Service
|
|
86
|
+
service_port: Target port
|
|
87
|
+
fallback_host: Host to use in local environments (default:
|
|
88
|
+
127.0.0.1)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: Full HTTP endpoint URL: http://<host>:<port>
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> endpoint = get_service_endpoint('192.168.5.1', 8080)
|
|
95
|
+
>>> # In local env → 'http://127.0.0.1:8080'
|
|
96
|
+
>>> # In cloud env → 'http://192.168.5.1:8080'
|
|
97
|
+
"""
|
|
98
|
+
if not service_external_ip:
|
|
99
|
+
service_external_ip = "127.0.0.1"
|
|
100
|
+
|
|
101
|
+
if not service_port:
|
|
102
|
+
service_port = 8080
|
|
103
|
+
|
|
104
|
+
if isinstance(service_port, list):
|
|
105
|
+
service_port = service_port[0]
|
|
106
|
+
|
|
107
|
+
if isLocalK8sEnvironment():
|
|
108
|
+
host = fallback_host
|
|
109
|
+
logger.info(
|
|
110
|
+
f"Local K8s environment detected; using {host} instead of "
|
|
111
|
+
f"{service_external_ip}",
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
host = service_external_ip
|
|
115
|
+
logger.info(
|
|
116
|
+
f"Cloud/remote environment detected; using External IP: "
|
|
117
|
+
f"{host}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return f"http://{host}:{service_port}"
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def get_resource_name(deploy_id: str) -> str:
|
|
124
|
+
return f"agent-{deploy_id[:8]}"
|
|
125
|
+
|
|
70
126
|
async def deploy(
|
|
71
127
|
self,
|
|
72
128
|
app=None,
|
|
73
129
|
runner=None,
|
|
130
|
+
entrypoint: Optional[str] = None,
|
|
74
131
|
endpoint_path: str = "/process",
|
|
75
132
|
stream: bool = True,
|
|
76
133
|
custom_endpoints: Optional[List[Dict]] = None,
|
|
@@ -86,15 +143,19 @@ class KubernetesDeployManager(DeployManager):
|
|
|
86
143
|
image_name: str = "agent_llm",
|
|
87
144
|
image_tag: str = "latest",
|
|
88
145
|
push_to_registry: bool = False,
|
|
146
|
+
use_cache: bool = True,
|
|
89
147
|
**kwargs,
|
|
90
148
|
) -> Dict[str, Any]:
|
|
91
149
|
"""
|
|
92
150
|
Deploy runner to Kubernetes.
|
|
93
151
|
|
|
152
|
+
All temporary files are created in cwd/.agentscope_runtime/ by default.
|
|
153
|
+
|
|
94
154
|
Args:
|
|
95
155
|
app: Agent app to be deployed
|
|
96
156
|
runner: Complete Runner object with agent, environment_manager,
|
|
97
157
|
context_manager
|
|
158
|
+
entrypoint: Entrypoint spec (e.g., "app.py" or "app.py:handler")
|
|
98
159
|
endpoint_path: API endpoint path
|
|
99
160
|
stream: Enable streaming responses
|
|
100
161
|
custom_endpoints: Custom endpoints from agent app
|
|
@@ -108,6 +169,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
108
169
|
environment: Environment variables dict
|
|
109
170
|
mount_dir: Mount directory
|
|
110
171
|
runtime_config: K8s runtime configuration
|
|
172
|
+
use_cache: Enable build cache (default: True)
|
|
111
173
|
# Backward compatibility
|
|
112
174
|
image_name: Image name
|
|
113
175
|
image_tag: Image tag
|
|
@@ -132,6 +194,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
132
194
|
built_image_name = self.image_factory.build_image(
|
|
133
195
|
app=app,
|
|
134
196
|
runner=runner,
|
|
197
|
+
entrypoint=entrypoint,
|
|
135
198
|
requirements=requirements,
|
|
136
199
|
extra_packages=extra_packages or [],
|
|
137
200
|
base_image=base_image,
|
|
@@ -145,6 +208,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
145
208
|
port=port,
|
|
146
209
|
protocol_adapters=protocol_adapters,
|
|
147
210
|
custom_endpoints=custom_endpoints,
|
|
211
|
+
use_cache=use_cache,
|
|
148
212
|
**kwargs,
|
|
149
213
|
)
|
|
150
214
|
if not built_image_name:
|
|
@@ -173,7 +237,7 @@ class KubernetesDeployManager(DeployManager):
|
|
|
173
237
|
else:
|
|
174
238
|
volume_bindings = {}
|
|
175
239
|
|
|
176
|
-
resource_name =
|
|
240
|
+
resource_name = self.get_resource_name(deploy_id)
|
|
177
241
|
|
|
178
242
|
logger.info(f"Building kubernetes deployment for {deploy_id}")
|
|
179
243
|
|
|
@@ -197,33 +261,36 @@ class KubernetesDeployManager(DeployManager):
|
|
|
197
261
|
)
|
|
198
262
|
|
|
199
263
|
if ports:
|
|
200
|
-
url =
|
|
264
|
+
url = self.get_service_endpoint(ip, ports)
|
|
201
265
|
else:
|
|
202
|
-
url =
|
|
266
|
+
url = self.get_service_endpoint(ip, port)
|
|
203
267
|
|
|
204
268
|
logger.info(f"Deployment {deploy_id} successful: {url}")
|
|
205
269
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"
|
|
209
|
-
|
|
210
|
-
"
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
|
|
270
|
+
deployment = Deployment(
|
|
271
|
+
id=deploy_id,
|
|
272
|
+
platform="k8s",
|
|
273
|
+
url=url,
|
|
274
|
+
status="running",
|
|
275
|
+
created_at=datetime.now().isoformat(),
|
|
276
|
+
agent_source=kwargs.get("agent_source"),
|
|
277
|
+
config={
|
|
278
|
+
"service_name": _id,
|
|
279
|
+
"image": built_image_name,
|
|
280
|
+
"replicas": replicas if self.use_deployment else 1,
|
|
281
|
+
"runner": runner.__class__.__name__ if runner else None,
|
|
214
282
|
"extra_packages": extra_packages,
|
|
215
|
-
"requirements": requirements,
|
|
283
|
+
"requirements": requirements,
|
|
216
284
|
"base_image": base_image,
|
|
217
285
|
"port": port,
|
|
218
|
-
"replicas": replicas,
|
|
219
286
|
"environment": environment,
|
|
220
287
|
"runtime_config": runtime_config,
|
|
221
288
|
"endpoint_path": endpoint_path,
|
|
222
289
|
"stream": stream,
|
|
223
|
-
"protocol_adapters": protocol_adapters,
|
|
224
|
-
**kwargs,
|
|
225
290
|
},
|
|
226
|
-
|
|
291
|
+
)
|
|
292
|
+
self.state_manager.save(deployment)
|
|
293
|
+
|
|
227
294
|
return {
|
|
228
295
|
"deploy_id": deploy_id,
|
|
229
296
|
"url": url,
|
|
@@ -240,21 +307,81 @@ class KubernetesDeployManager(DeployManager):
|
|
|
240
307
|
f"Deployment failed: {e}, {traceback.format_exc()}",
|
|
241
308
|
) from e
|
|
242
309
|
|
|
243
|
-
async def stop(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
310
|
+
async def stop(
|
|
311
|
+
self,
|
|
312
|
+
deploy_id: str,
|
|
313
|
+
**kwargs,
|
|
314
|
+
) -> Dict[str, Any]:
|
|
315
|
+
"""Stop Kubernetes deployment.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
deploy_id: Deployment identifier
|
|
319
|
+
**kwargs: Additional parameters
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Dict with success status, message, and details
|
|
323
|
+
"""
|
|
324
|
+
# Derive resource name from deploy_id
|
|
325
|
+
resource_name = self.get_resource_name(deploy_id)
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
# Try to remove the deployment
|
|
329
|
+
success = self.k8s_client.remove_deployment(resource_name)
|
|
330
|
+
|
|
331
|
+
if success:
|
|
332
|
+
# Remove from state manager
|
|
333
|
+
try:
|
|
334
|
+
self.state_manager.update_status(deploy_id, "stopped")
|
|
335
|
+
except KeyError:
|
|
336
|
+
logger.debug(
|
|
337
|
+
f"Deployment {deploy_id} not found "
|
|
338
|
+
f"in state (already removed)",
|
|
339
|
+
)
|
|
247
340
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
341
|
+
return {
|
|
342
|
+
"success": True,
|
|
343
|
+
"message": f"Kubernetes deployment {resource_name} "
|
|
344
|
+
f"removed",
|
|
345
|
+
"details": {
|
|
346
|
+
"deploy_id": deploy_id,
|
|
347
|
+
"resource_name": resource_name,
|
|
348
|
+
},
|
|
349
|
+
}
|
|
350
|
+
else:
|
|
351
|
+
return {
|
|
352
|
+
"success": False,
|
|
353
|
+
"message": f"Kubernetes deployment {resource_name} not "
|
|
354
|
+
f"found (may already be deleted), Please check the "
|
|
355
|
+
f"detail in cluster",
|
|
356
|
+
"details": {
|
|
357
|
+
"deploy_id": deploy_id,
|
|
358
|
+
"resource_name": resource_name,
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.error(
|
|
363
|
+
f"Failed to remove K8s deployment {resource_name}: {e}",
|
|
364
|
+
)
|
|
365
|
+
return {
|
|
366
|
+
"success": False,
|
|
367
|
+
"message": f"Failed to remove K8s deployment: {e}",
|
|
368
|
+
"details": {
|
|
369
|
+
"deploy_id": deploy_id,
|
|
370
|
+
"resource_name": resource_name,
|
|
371
|
+
"error": str(e),
|
|
372
|
+
},
|
|
373
|
+
}
|
|
251
374
|
|
|
252
375
|
def get_status(self) -> str:
|
|
253
376
|
"""Get deployment status"""
|
|
254
|
-
|
|
377
|
+
deployment = self.state_manager.get(self.deploy_id)
|
|
378
|
+
if not deployment:
|
|
255
379
|
return "not_found"
|
|
256
380
|
|
|
257
|
-
|
|
258
|
-
|
|
381
|
+
# Get service_name from config
|
|
382
|
+
config = getattr(deployment, "config", {})
|
|
383
|
+
service_name = config.get("service_name")
|
|
384
|
+
if not service_name:
|
|
385
|
+
return "unknown"
|
|
259
386
|
|
|
260
387
|
return self.k8s_client.get_deployment_status(service_name)
|