agentscope-runtime 1.0.4a1__py3-none-any.whl → 1.0.5.post1__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/stream.py +2 -8
- agentscope_runtime/adapters/langgraph/stream.py +120 -70
- agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
- agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
- agentscope_runtime/adapters/utils.py +6 -0
- agentscope_runtime/cli/commands/deploy.py +836 -1
- agentscope_runtime/cli/commands/stop.py +16 -0
- agentscope_runtime/common/container_clients/__init__.py +52 -0
- agentscope_runtime/common/container_clients/agentrun_client.py +6 -4
- agentscope_runtime/common/container_clients/boxlite_client.py +442 -0
- agentscope_runtime/common/container_clients/docker_client.py +0 -20
- agentscope_runtime/common/container_clients/fc_client.py +6 -4
- agentscope_runtime/common/container_clients/gvisor_client.py +38 -0
- agentscope_runtime/common/container_clients/knative_client.py +467 -0
- agentscope_runtime/common/utils/deprecation.py +164 -0
- agentscope_runtime/engine/__init__.py +4 -0
- agentscope_runtime/engine/app/agent_app.py +16 -4
- agentscope_runtime/engine/constant.py +1 -0
- agentscope_runtime/engine/deployers/__init__.py +34 -11
- agentscope_runtime/engine/deployers/adapter/__init__.py +8 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +26 -51
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +23 -13
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +4 -201
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +152 -25
- agentscope_runtime/engine/deployers/adapter/agui/__init__.py +8 -0
- agentscope_runtime/engine/deployers/adapter/agui/agui_adapter_utils.py +652 -0
- agentscope_runtime/engine/deployers/adapter/agui/agui_protocol_adapter.py +225 -0
- agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
- agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
- agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
- agentscope_runtime/engine/deployers/pai_deployer.py +2335 -0
- agentscope_runtime/engine/deployers/utils/net_utils.py +37 -0
- agentscope_runtime/engine/deployers/utils/oss_utils.py +38 -0
- agentscope_runtime/engine/deployers/utils/package.py +46 -42
- agentscope_runtime/engine/helpers/agent_api_client.py +372 -0
- agentscope_runtime/engine/runner.py +13 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +9 -3
- agentscope_runtime/engine/services/agent_state/__init__.py +7 -0
- agentscope_runtime/engine/services/memory/__init__.py +7 -0
- agentscope_runtime/engine/services/memory/redis_memory_service.py +15 -16
- agentscope_runtime/engine/services/session_history/__init__.py +7 -0
- agentscope_runtime/engine/tracing/local_logging_handler.py +2 -3
- agentscope_runtime/engine/tracing/wrapper.py +18 -4
- agentscope_runtime/sandbox/__init__.py +14 -6
- agentscope_runtime/sandbox/box/base/__init__.py +2 -2
- agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
- agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
- agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
- agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
- agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
- agentscope_runtime/sandbox/box/sandbox.py +102 -65
- agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
- agentscope_runtime/sandbox/client/__init__.py +6 -1
- agentscope_runtime/sandbox/client/async_http_client.py +339 -0
- agentscope_runtime/sandbox/client/base.py +74 -0
- agentscope_runtime/sandbox/client/http_client.py +108 -329
- agentscope_runtime/sandbox/enums.py +7 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +275 -29
- agentscope_runtime/sandbox/manager/server/app.py +7 -1
- agentscope_runtime/sandbox/manager/server/config.py +3 -1
- agentscope_runtime/sandbox/model/manager_config.py +11 -9
- agentscope_runtime/tools/modelstudio_memory/__init__.py +106 -0
- agentscope_runtime/tools/modelstudio_memory/base.py +220 -0
- agentscope_runtime/tools/modelstudio_memory/config.py +86 -0
- agentscope_runtime/tools/modelstudio_memory/core.py +594 -0
- agentscope_runtime/tools/modelstudio_memory/exceptions.py +60 -0
- agentscope_runtime/tools/modelstudio_memory/schemas.py +253 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/METADATA +187 -74
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/RECORD +79 -55
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/WHEEL +1 -1
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,2335 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# pylint:disable=too-many-nested-blocks, too-many-return-statements,
|
|
3
|
+
# pylint:disable=too-many-branches, too-many-statements, try-except-raise
|
|
4
|
+
# pylint:disable=ungrouped-imports, arguments-renamed, protected-access
|
|
5
|
+
#
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
import asyncio
|
|
8
|
+
import fnmatch
|
|
9
|
+
import glob
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import posixpath
|
|
14
|
+
import time
|
|
15
|
+
import zipfile
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, Optional, List, Union, Any, Literal, cast
|
|
19
|
+
|
|
20
|
+
import yaml
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
from ...version import __version__
|
|
25
|
+
|
|
26
|
+
from .utils.oss_utils import parse_oss_uri
|
|
27
|
+
from .utils.net_utils import is_tcp_reachable
|
|
28
|
+
from .adapter.protocol_adapter import ProtocolAdapter
|
|
29
|
+
from .base import DeployManager
|
|
30
|
+
from .state import Deployment
|
|
31
|
+
from .utils.package import generate_build_directory
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
import alibabacloud_oss_v2 as oss
|
|
36
|
+
from alibabacloud_aiworkspace20210204.client import (
|
|
37
|
+
Client as WorkspaceClient,
|
|
38
|
+
)
|
|
39
|
+
from alibabacloud_eas20210701.client import Client as EASClient
|
|
40
|
+
from alibabacloud_tea_openapi import models as open_api_models
|
|
41
|
+
from alibabacloud_tea_openapi.client import Client as OpenApiClient
|
|
42
|
+
from alibabacloud_tea_openapi import utils_models as open_api_util_models
|
|
43
|
+
from alibabacloud_tea_openapi.utils import Utils as OpenApiUtils
|
|
44
|
+
|
|
45
|
+
PAI_AVAILABLE = True
|
|
46
|
+
except ImportError:
|
|
47
|
+
oss = None
|
|
48
|
+
WorkspaceClient = None
|
|
49
|
+
EASClient = None
|
|
50
|
+
open_api_models = None
|
|
51
|
+
OpenApiClient = None
|
|
52
|
+
open_api_util_models = None
|
|
53
|
+
OpenApiUtils = None
|
|
54
|
+
PAI_AVAILABLE = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LangStudioClient:
|
|
61
|
+
"""
|
|
62
|
+
A lightweight PAI LangStudio API client .
|
|
63
|
+
|
|
64
|
+
This client provides direct access to the PAI LangStudio API endpoints
|
|
65
|
+
using the alibabacloud_tea_openapi.client.Client for request handling.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
API_VERSION = "2024-07-10"
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
config: "open_api_models.Config",
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Initialize the LangStudio client.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
config: OpenAPI configuration with credentials and endpoint
|
|
79
|
+
"""
|
|
80
|
+
if OpenApiClient is None:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"alibabacloud_tea_openapi is required. "
|
|
83
|
+
"Install with: pip install alibabacloud_tea_openapi",
|
|
84
|
+
)
|
|
85
|
+
self._client = OpenApiClient(config)
|
|
86
|
+
self._client._endpoint_rule = ""
|
|
87
|
+
self._client.check_config(config)
|
|
88
|
+
if config.endpoint:
|
|
89
|
+
self._client._endpoint = config.endpoint
|
|
90
|
+
else:
|
|
91
|
+
self._client._endpoint = (
|
|
92
|
+
f"pailangstudio.{config.region_id}.aliyuncs.com"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _build_params(
|
|
96
|
+
self,
|
|
97
|
+
action: str,
|
|
98
|
+
pathname: str,
|
|
99
|
+
method: str,
|
|
100
|
+
) -> "open_api_util_models.Params":
|
|
101
|
+
"""Build request parameters."""
|
|
102
|
+
return open_api_util_models.Params(
|
|
103
|
+
action=action,
|
|
104
|
+
version=self.API_VERSION,
|
|
105
|
+
protocol="HTTPS",
|
|
106
|
+
pathname=pathname,
|
|
107
|
+
method=method,
|
|
108
|
+
auth_type="AK",
|
|
109
|
+
style="ROA",
|
|
110
|
+
req_body_type="json",
|
|
111
|
+
body_type="json",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _percent_encode(value: str) -> str:
|
|
116
|
+
"""URL percent-encode a value."""
|
|
117
|
+
from urllib.parse import quote
|
|
118
|
+
|
|
119
|
+
return quote(str(value), safe="")
|
|
120
|
+
|
|
121
|
+
# =========================================================================
|
|
122
|
+
# Flow APIs
|
|
123
|
+
# =========================================================================
|
|
124
|
+
|
|
125
|
+
async def list_flows_async(
|
|
126
|
+
self,
|
|
127
|
+
workspace_id: str,
|
|
128
|
+
flow_name: Optional[str] = None,
|
|
129
|
+
flow_id: Optional[str] = None,
|
|
130
|
+
flow_type: Optional[str] = None,
|
|
131
|
+
creator: Optional[str] = None,
|
|
132
|
+
user_id: Optional[str] = None,
|
|
133
|
+
sort_by: Optional[str] = None,
|
|
134
|
+
order: Optional[str] = None,
|
|
135
|
+
page_number: Optional[int] = None,
|
|
136
|
+
page_size: Optional[int] = None,
|
|
137
|
+
max_results: Optional[int] = None,
|
|
138
|
+
next_token: Optional[str] = None,
|
|
139
|
+
) -> Dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
List flows in a workspace.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
workspace_id: The workspace ID
|
|
145
|
+
flow_name: Filter by flow name
|
|
146
|
+
flow_id: Filter by flow ID
|
|
147
|
+
flow_type: Filter by flow type
|
|
148
|
+
creator: Filter by creator
|
|
149
|
+
user_id: Filter by user ID
|
|
150
|
+
sort_by: Sort field
|
|
151
|
+
order: Sort order (ASC/DESC)
|
|
152
|
+
page_number: Page number
|
|
153
|
+
page_size: Page size
|
|
154
|
+
max_results: Maximum results
|
|
155
|
+
next_token: Pagination token
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Dict containing flows list and pagination info
|
|
159
|
+
"""
|
|
160
|
+
from darabonba.runtime import RuntimeOptions
|
|
161
|
+
|
|
162
|
+
query: Dict[str, Any] = {"WorkspaceId": workspace_id}
|
|
163
|
+
if flow_name is not None:
|
|
164
|
+
query["FlowName"] = flow_name
|
|
165
|
+
if flow_id is not None:
|
|
166
|
+
query["FlowId"] = flow_id
|
|
167
|
+
if flow_type is not None:
|
|
168
|
+
query["FlowType"] = flow_type
|
|
169
|
+
if creator is not None:
|
|
170
|
+
query["Creator"] = creator
|
|
171
|
+
if user_id is not None:
|
|
172
|
+
query["UserId"] = user_id
|
|
173
|
+
if sort_by is not None:
|
|
174
|
+
query["SortBy"] = sort_by
|
|
175
|
+
if order is not None:
|
|
176
|
+
query["Order"] = order
|
|
177
|
+
if page_number is not None:
|
|
178
|
+
query["PageNumber"] = page_number
|
|
179
|
+
if page_size is not None:
|
|
180
|
+
query["PageSize"] = page_size
|
|
181
|
+
if max_results is not None:
|
|
182
|
+
query["MaxResults"] = max_results
|
|
183
|
+
if next_token is not None:
|
|
184
|
+
query["NextToken"] = next_token
|
|
185
|
+
|
|
186
|
+
req = open_api_util_models.OpenApiRequest(
|
|
187
|
+
headers={},
|
|
188
|
+
query=OpenApiUtils.query(query),
|
|
189
|
+
)
|
|
190
|
+
params = self._build_params(
|
|
191
|
+
action="ListFlows",
|
|
192
|
+
pathname="/api/v1/langstudio/flows",
|
|
193
|
+
method="GET",
|
|
194
|
+
)
|
|
195
|
+
runtime = RuntimeOptions()
|
|
196
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
197
|
+
return result.get("body", {})
|
|
198
|
+
|
|
199
|
+
async def get_flow_async(
|
|
200
|
+
self,
|
|
201
|
+
flow_id: str,
|
|
202
|
+
workspace_id: str,
|
|
203
|
+
include_code_mode_run_info: Optional[bool] = None,
|
|
204
|
+
) -> Dict[str, Any]:
|
|
205
|
+
"""
|
|
206
|
+
Get a flow by ID.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
flow_id: The flow ID
|
|
210
|
+
workspace_id: The workspace ID
|
|
211
|
+
include_code_mode_run_info: Include code mode run info
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict containing flow details
|
|
215
|
+
"""
|
|
216
|
+
from darabonba.runtime import RuntimeOptions
|
|
217
|
+
|
|
218
|
+
query: Dict[str, Any] = {"WorkspaceId": workspace_id}
|
|
219
|
+
if include_code_mode_run_info is not None:
|
|
220
|
+
query["IncludeCodeModeRunInfo"] = include_code_mode_run_info
|
|
221
|
+
|
|
222
|
+
req = open_api_util_models.OpenApiRequest(
|
|
223
|
+
headers={},
|
|
224
|
+
query=OpenApiUtils.query(query),
|
|
225
|
+
)
|
|
226
|
+
params = self._build_params(
|
|
227
|
+
action="GetFlow",
|
|
228
|
+
pathname=f"/api/v1/langstudio/flows/{self._percent_encode(flow_id)}",
|
|
229
|
+
method="GET",
|
|
230
|
+
)
|
|
231
|
+
runtime = RuntimeOptions()
|
|
232
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
233
|
+
return result.get("body", {})
|
|
234
|
+
|
|
235
|
+
async def create_flow_async(
|
|
236
|
+
self,
|
|
237
|
+
workspace_id: str,
|
|
238
|
+
flow_name: str,
|
|
239
|
+
flow_type: str,
|
|
240
|
+
description: Optional[str] = None,
|
|
241
|
+
source_uri: Optional[str] = None,
|
|
242
|
+
work_dir: Optional[str] = None,
|
|
243
|
+
create_from: Optional[str] = None,
|
|
244
|
+
accessibility: Optional[str] = None,
|
|
245
|
+
flow_template_id: Optional[str] = None,
|
|
246
|
+
runtime_id: Optional[str] = None,
|
|
247
|
+
source_flow_id: Optional[str] = None,
|
|
248
|
+
) -> Dict[str, Any]:
|
|
249
|
+
"""
|
|
250
|
+
Create a new flow.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
workspace_id: The workspace ID
|
|
254
|
+
flow_name: The flow name
|
|
255
|
+
flow_type: The flow type (e.g., "Code")
|
|
256
|
+
description: Flow description
|
|
257
|
+
source_uri: Source URI (for OSS imports)
|
|
258
|
+
work_dir: Working directory
|
|
259
|
+
create_from: Creation source (e.g., "OSS")
|
|
260
|
+
accessibility: Accessibility setting
|
|
261
|
+
flow_template_id: Template ID to create from
|
|
262
|
+
runtime_id: Runtime ID
|
|
263
|
+
source_flow_id: Source flow ID to copy from
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dict containing the created flow info with flow_id
|
|
267
|
+
"""
|
|
268
|
+
from darabonba.runtime import RuntimeOptions
|
|
269
|
+
|
|
270
|
+
body: Dict[str, Any] = {
|
|
271
|
+
"WorkspaceId": workspace_id,
|
|
272
|
+
"FlowName": flow_name,
|
|
273
|
+
"FlowType": flow_type,
|
|
274
|
+
}
|
|
275
|
+
if description is not None:
|
|
276
|
+
body["Description"] = description
|
|
277
|
+
if source_uri is not None:
|
|
278
|
+
body["SourceUri"] = source_uri
|
|
279
|
+
if work_dir is not None:
|
|
280
|
+
body["WorkDir"] = work_dir
|
|
281
|
+
if create_from is not None:
|
|
282
|
+
body["CreateFrom"] = create_from
|
|
283
|
+
if accessibility is not None:
|
|
284
|
+
body["Accessibility"] = accessibility
|
|
285
|
+
if flow_template_id is not None:
|
|
286
|
+
body["FlowTemplateId"] = flow_template_id
|
|
287
|
+
if runtime_id is not None:
|
|
288
|
+
body["RuntimeId"] = runtime_id
|
|
289
|
+
if source_flow_id is not None:
|
|
290
|
+
body["SourceFlowId"] = source_flow_id
|
|
291
|
+
|
|
292
|
+
req = open_api_util_models.OpenApiRequest(
|
|
293
|
+
headers={},
|
|
294
|
+
body=OpenApiUtils.parse_to_map(body),
|
|
295
|
+
)
|
|
296
|
+
params = self._build_params(
|
|
297
|
+
action="CreateFlow",
|
|
298
|
+
pathname="/api/v1/langstudio/flows",
|
|
299
|
+
method="POST",
|
|
300
|
+
)
|
|
301
|
+
runtime = RuntimeOptions()
|
|
302
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
303
|
+
return result.get("body", {})
|
|
304
|
+
|
|
305
|
+
async def delete_flow_async(
|
|
306
|
+
self,
|
|
307
|
+
flow_id: str,
|
|
308
|
+
workspace_id: str,
|
|
309
|
+
) -> Dict[str, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Delete a flow.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
flow_id: The flow ID to delete
|
|
315
|
+
workspace_id: The workspace ID
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dict containing deletion result
|
|
319
|
+
"""
|
|
320
|
+
from darabonba.runtime import RuntimeOptions
|
|
321
|
+
|
|
322
|
+
query: Dict[str, Any] = {"WorkspaceId": workspace_id}
|
|
323
|
+
|
|
324
|
+
req = open_api_util_models.OpenApiRequest(
|
|
325
|
+
headers={},
|
|
326
|
+
query=OpenApiUtils.query(query),
|
|
327
|
+
)
|
|
328
|
+
params = self._build_params(
|
|
329
|
+
action="DeleteFlow",
|
|
330
|
+
pathname=f"/api/v1/langstudio/flows/{self._percent_encode(flow_id)}",
|
|
331
|
+
method="DELETE",
|
|
332
|
+
)
|
|
333
|
+
runtime = RuntimeOptions()
|
|
334
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
335
|
+
return result.get("body", {})
|
|
336
|
+
|
|
337
|
+
# =========================================================================
|
|
338
|
+
# Snapshot APIs
|
|
339
|
+
# =========================================================================
|
|
340
|
+
|
|
341
|
+
async def create_snapshot_async(
|
|
342
|
+
self,
|
|
343
|
+
workspace_id: str,
|
|
344
|
+
snapshot_resource_type: str,
|
|
345
|
+
snapshot_resource_id: str,
|
|
346
|
+
snapshot_name: str,
|
|
347
|
+
source_storage_path: Optional[str] = None,
|
|
348
|
+
work_dir: Optional[str] = None,
|
|
349
|
+
description: Optional[str] = None,
|
|
350
|
+
accessibility: Optional[str] = None,
|
|
351
|
+
creation_type: Optional[str] = None,
|
|
352
|
+
) -> Dict[str, Any]:
|
|
353
|
+
"""
|
|
354
|
+
Create a snapshot.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
workspace_id: The workspace ID
|
|
358
|
+
snapshot_resource_type: Resource type (e.g., "Flow")
|
|
359
|
+
snapshot_resource_id: Resource ID (e.g., flow_id)
|
|
360
|
+
snapshot_name: Name for the snapshot
|
|
361
|
+
source_storage_path: Source storage path (OSS URI)
|
|
362
|
+
work_dir: Working directory
|
|
363
|
+
description: Snapshot description
|
|
364
|
+
accessibility: Accessibility setting
|
|
365
|
+
creation_type: Creation type
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Dict containing snapshot_id
|
|
369
|
+
"""
|
|
370
|
+
from darabonba.runtime import RuntimeOptions
|
|
371
|
+
|
|
372
|
+
body: Dict[str, Any] = {
|
|
373
|
+
"WorkspaceId": workspace_id,
|
|
374
|
+
"SnapshotResourceType": snapshot_resource_type,
|
|
375
|
+
"SnapshotResourceId": snapshot_resource_id,
|
|
376
|
+
"SnapshotName": snapshot_name,
|
|
377
|
+
}
|
|
378
|
+
if source_storage_path is not None:
|
|
379
|
+
body["SourceStoragePath"] = source_storage_path
|
|
380
|
+
if work_dir is not None:
|
|
381
|
+
body["WorkDir"] = work_dir
|
|
382
|
+
if description is not None:
|
|
383
|
+
body["Description"] = description
|
|
384
|
+
if accessibility is not None:
|
|
385
|
+
body["Accessibility"] = accessibility
|
|
386
|
+
if creation_type is not None:
|
|
387
|
+
body["CreationType"] = creation_type
|
|
388
|
+
|
|
389
|
+
req = open_api_util_models.OpenApiRequest(
|
|
390
|
+
headers={},
|
|
391
|
+
body=OpenApiUtils.parse_to_map(body),
|
|
392
|
+
)
|
|
393
|
+
params = self._build_params(
|
|
394
|
+
action="CreateSnapshot",
|
|
395
|
+
pathname="/api/v1/langstudio/snapshots",
|
|
396
|
+
method="POST",
|
|
397
|
+
)
|
|
398
|
+
runtime = RuntimeOptions()
|
|
399
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
400
|
+
return result.get("body", {})
|
|
401
|
+
|
|
402
|
+
# =========================================================================
|
|
403
|
+
# Deployment APIs
|
|
404
|
+
# =========================================================================
|
|
405
|
+
|
|
406
|
+
async def create_deployment_async(
|
|
407
|
+
self,
|
|
408
|
+
workspace_id: str,
|
|
409
|
+
resource_type: str,
|
|
410
|
+
resource_id: str,
|
|
411
|
+
resource_snapshot_id: str,
|
|
412
|
+
service_name: str,
|
|
413
|
+
work_dir: Optional[str] = None,
|
|
414
|
+
deployment_config: Optional[str] = None,
|
|
415
|
+
credential_config: Optional[Dict[str, Any]] = None,
|
|
416
|
+
enable_trace: Optional[bool] = None,
|
|
417
|
+
auto_approval: Optional[bool] = None,
|
|
418
|
+
service_group: Optional[str] = None,
|
|
419
|
+
description: Optional[str] = None,
|
|
420
|
+
accessibility: Optional[str] = None,
|
|
421
|
+
envs: Optional[Dict[str, str]] = None,
|
|
422
|
+
labels: Optional[Dict[str, str]] = None,
|
|
423
|
+
user_vpc: Optional[Dict[str, Any]] = None,
|
|
424
|
+
ecs_spec: Optional[str] = None,
|
|
425
|
+
data_sources: Optional[List[Dict[str, Any]]] = None,
|
|
426
|
+
chat_history_config: Optional[Dict[str, Any]] = None,
|
|
427
|
+
content_moderation_config: Optional[Dict[str, Any]] = None,
|
|
428
|
+
) -> Dict[str, Any]:
|
|
429
|
+
"""
|
|
430
|
+
Create a deployment.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
workspace_id: The workspace ID
|
|
434
|
+
resource_type: Resource type (e.g., "Flow")
|
|
435
|
+
resource_id: Resource ID (e.g., flow_id)
|
|
436
|
+
resource_snapshot_id: Snapshot ID
|
|
437
|
+
service_name: Service name
|
|
438
|
+
work_dir: Working directory (OSS path)
|
|
439
|
+
deployment_config: Deployment configuration JSON string
|
|
440
|
+
credential_config: Credential configuration dict
|
|
441
|
+
enable_trace: Enable tracing
|
|
442
|
+
auto_approval: Auto approve deployment
|
|
443
|
+
service_group: Service group name
|
|
444
|
+
description: Deployment description
|
|
445
|
+
accessibility: Accessibility setting
|
|
446
|
+
envs: Environment variables
|
|
447
|
+
labels: Labels/tags
|
|
448
|
+
user_vpc: VPC configuration
|
|
449
|
+
ecs_spec: ECS specification
|
|
450
|
+
data_sources: Data sources configuration
|
|
451
|
+
chat_history_config: Chat history configuration
|
|
452
|
+
content_moderation_config: Content moderation configuration
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Dict containing deployment_id
|
|
456
|
+
"""
|
|
457
|
+
from darabonba.runtime import RuntimeOptions
|
|
458
|
+
|
|
459
|
+
body: Dict[str, Any] = {
|
|
460
|
+
"WorkspaceId": workspace_id,
|
|
461
|
+
"ResourceType": resource_type,
|
|
462
|
+
"ResourceId": resource_id,
|
|
463
|
+
"ResourceSnapshotId": resource_snapshot_id,
|
|
464
|
+
"ServiceName": service_name,
|
|
465
|
+
}
|
|
466
|
+
if work_dir is not None:
|
|
467
|
+
body["WorkDir"] = work_dir
|
|
468
|
+
if deployment_config is not None:
|
|
469
|
+
body["DeploymentConfig"] = deployment_config
|
|
470
|
+
if credential_config is not None:
|
|
471
|
+
body["CredentialConfig"] = credential_config
|
|
472
|
+
if enable_trace is not None:
|
|
473
|
+
body["EnableTrace"] = enable_trace
|
|
474
|
+
if auto_approval is not None:
|
|
475
|
+
body["AutoApproval"] = auto_approval
|
|
476
|
+
if service_group is not None:
|
|
477
|
+
body["ServiceGroup"] = service_group
|
|
478
|
+
if description is not None:
|
|
479
|
+
body["Description"] = description
|
|
480
|
+
if accessibility is not None:
|
|
481
|
+
body["Accessibility"] = accessibility
|
|
482
|
+
if envs is not None:
|
|
483
|
+
body["Envs"] = envs
|
|
484
|
+
if labels is not None:
|
|
485
|
+
body["Labels"] = labels
|
|
486
|
+
if user_vpc is not None:
|
|
487
|
+
body["UserVpc"] = user_vpc
|
|
488
|
+
if ecs_spec is not None:
|
|
489
|
+
body["EcsSpec"] = ecs_spec
|
|
490
|
+
if data_sources is not None:
|
|
491
|
+
body["DataSources"] = data_sources
|
|
492
|
+
if chat_history_config is not None:
|
|
493
|
+
body["ChatHistoryConfig"] = chat_history_config
|
|
494
|
+
if content_moderation_config is not None:
|
|
495
|
+
body["ContentModerationConfig"] = content_moderation_config
|
|
496
|
+
|
|
497
|
+
req = open_api_util_models.OpenApiRequest(
|
|
498
|
+
headers={},
|
|
499
|
+
body=OpenApiUtils.parse_to_map(body),
|
|
500
|
+
)
|
|
501
|
+
params = self._build_params(
|
|
502
|
+
action="CreateDeployment",
|
|
503
|
+
pathname="/api/v1/langstudio/deployments",
|
|
504
|
+
method="POST",
|
|
505
|
+
)
|
|
506
|
+
runtime = RuntimeOptions()
|
|
507
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
508
|
+
return result.get("body", {})
|
|
509
|
+
|
|
510
|
+
async def get_deployment_async(
|
|
511
|
+
self,
|
|
512
|
+
deployment_id: str,
|
|
513
|
+
workspace_id: str,
|
|
514
|
+
) -> Dict[str, Any]:
|
|
515
|
+
"""
|
|
516
|
+
Get deployment details.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
deployment_id: The deployment ID
|
|
520
|
+
workspace_id: The workspace ID
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Dict containing deployment details including status
|
|
524
|
+
"""
|
|
525
|
+
from darabonba.runtime import RuntimeOptions
|
|
526
|
+
|
|
527
|
+
query: Dict[str, Any] = {"WorkspaceId": workspace_id}
|
|
528
|
+
|
|
529
|
+
req = open_api_util_models.OpenApiRequest(
|
|
530
|
+
headers={},
|
|
531
|
+
query=OpenApiUtils.query(query),
|
|
532
|
+
)
|
|
533
|
+
params = self._build_params(
|
|
534
|
+
action="GetDeployment",
|
|
535
|
+
pathname=(
|
|
536
|
+
f"/api/v1/langstudio/deployments/"
|
|
537
|
+
f"{self._percent_encode(deployment_id)}"
|
|
538
|
+
),
|
|
539
|
+
method="GET",
|
|
540
|
+
)
|
|
541
|
+
runtime = RuntimeOptions()
|
|
542
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
543
|
+
return result.get("body", {})
|
|
544
|
+
|
|
545
|
+
async def update_deployment_async(
|
|
546
|
+
self,
|
|
547
|
+
deployment_id: str,
|
|
548
|
+
workspace_id: str,
|
|
549
|
+
stage_action: Optional[str] = None,
|
|
550
|
+
auto_approval: Optional[bool] = None,
|
|
551
|
+
deployment_config: Optional[str] = None,
|
|
552
|
+
description: Optional[str] = None,
|
|
553
|
+
) -> Dict[str, Any]:
|
|
554
|
+
"""
|
|
555
|
+
Update a deployment.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
deployment_id: The deployment ID
|
|
559
|
+
workspace_id: The workspace ID
|
|
560
|
+
stage_action: Stage action JSON (e.g., {"Stage": 3, "Action": "Confirm"})
|
|
561
|
+
auto_approval: Auto approval setting
|
|
562
|
+
deployment_config: Deployment configuration
|
|
563
|
+
description: Deployment description
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Dict containing update result
|
|
567
|
+
"""
|
|
568
|
+
from darabonba.runtime import RuntimeOptions
|
|
569
|
+
|
|
570
|
+
body: Dict[str, Any] = {"WorkspaceId": workspace_id}
|
|
571
|
+
if stage_action is not None:
|
|
572
|
+
body["StageAction"] = stage_action
|
|
573
|
+
if auto_approval is not None:
|
|
574
|
+
body["AutoApproval"] = auto_approval
|
|
575
|
+
if deployment_config is not None:
|
|
576
|
+
body["DeploymentConfig"] = deployment_config
|
|
577
|
+
if description is not None:
|
|
578
|
+
body["Description"] = description
|
|
579
|
+
|
|
580
|
+
req = open_api_util_models.OpenApiRequest(
|
|
581
|
+
headers={},
|
|
582
|
+
body=OpenApiUtils.parse_to_map(body),
|
|
583
|
+
)
|
|
584
|
+
params = self._build_params(
|
|
585
|
+
action="UpdateDeployment",
|
|
586
|
+
pathname=(
|
|
587
|
+
f"/api/v1/langstudio/deployments/"
|
|
588
|
+
f"{self._percent_encode(deployment_id)}"
|
|
589
|
+
),
|
|
590
|
+
method="PUT",
|
|
591
|
+
)
|
|
592
|
+
runtime = RuntimeOptions()
|
|
593
|
+
result = await self._client.call_api_async(params, req, runtime)
|
|
594
|
+
return result.get("body", {})
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class ConfigBaseModel(BaseModel):
|
|
598
|
+
model_config = ConfigDict(extra="allow")
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class PAICodeConfig(ConfigBaseModel):
|
|
602
|
+
"""Code configuration for PAI deployment."""
|
|
603
|
+
|
|
604
|
+
source_dir: Optional[str] = Field(
|
|
605
|
+
None,
|
|
606
|
+
description="Path to project root directory",
|
|
607
|
+
)
|
|
608
|
+
entrypoint: Optional[str] = Field(
|
|
609
|
+
None,
|
|
610
|
+
description="Entrypoint file within source_dir",
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class PAIResourcesConfig(ConfigBaseModel):
|
|
615
|
+
"""Resource configuration for PAI deployment."""
|
|
616
|
+
|
|
617
|
+
instance_count: int = Field(1, description="Number of service instances")
|
|
618
|
+
type: Optional[Literal["public", "resource", "quota"]] = Field(
|
|
619
|
+
None,
|
|
620
|
+
description="Resource type: public, resource (EAS group), or quota",
|
|
621
|
+
)
|
|
622
|
+
instance_type: Optional[str] = Field(
|
|
623
|
+
None,
|
|
624
|
+
description="ECS instance type for public mode",
|
|
625
|
+
)
|
|
626
|
+
resource_id: Optional[str] = Field(
|
|
627
|
+
None,
|
|
628
|
+
description="EAS resource group ID for resource mode",
|
|
629
|
+
)
|
|
630
|
+
quota_id: Optional[str] = Field(
|
|
631
|
+
None,
|
|
632
|
+
description="PAI quota ID for quota mode",
|
|
633
|
+
)
|
|
634
|
+
cpu: Optional[int] = Field(
|
|
635
|
+
None,
|
|
636
|
+
description="CPU cores for resource/quota mode",
|
|
637
|
+
)
|
|
638
|
+
memory: Optional[int] = Field(
|
|
639
|
+
None,
|
|
640
|
+
description="Memory in MB for resource/quota mode",
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
class PAIVpcConfig(ConfigBaseModel):
|
|
645
|
+
"""VPC configuration for PAI deployment."""
|
|
646
|
+
|
|
647
|
+
vpc_id: Optional[str] = None
|
|
648
|
+
vswitch_id: Optional[str] = None
|
|
649
|
+
security_group_id: Optional[str] = None
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
class PAIIdentityConfig(ConfigBaseModel):
|
|
653
|
+
"""Identity/Permission configuration for PAI deployment."""
|
|
654
|
+
|
|
655
|
+
ram_role_arn: Optional[str] = Field(
|
|
656
|
+
None,
|
|
657
|
+
description="RAM role ARN for service runtime",
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class PAIObservabilityConfig(ConfigBaseModel):
|
|
662
|
+
"""Observability configuration for PAI deployment."""
|
|
663
|
+
|
|
664
|
+
enable_trace: bool = Field(True, description="Enable tracing/telemetry")
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
class PAIStorageConfig(ConfigBaseModel):
|
|
668
|
+
"""Storage configuration for PAI deployment."""
|
|
669
|
+
|
|
670
|
+
work_dir: Optional[str] = Field(
|
|
671
|
+
None,
|
|
672
|
+
description="OSS working directory for artifacts",
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class PAIContextConfig(ConfigBaseModel):
|
|
677
|
+
"""Context configuration (where to deploy)."""
|
|
678
|
+
|
|
679
|
+
workspace_id: Optional[str] = Field(
|
|
680
|
+
None,
|
|
681
|
+
description="PAI workspace ID",
|
|
682
|
+
)
|
|
683
|
+
region: Optional[str] = Field(
|
|
684
|
+
None,
|
|
685
|
+
description="Region code (e.g., cn-hangzhou)",
|
|
686
|
+
)
|
|
687
|
+
storage: PAIStorageConfig = Field(
|
|
688
|
+
default_factory=PAIStorageConfig,
|
|
689
|
+
description="Default storage configuration (fallback for spec.storage)",
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class PAISpecConfig(ConfigBaseModel):
|
|
694
|
+
"""Spec configuration (what to deploy)."""
|
|
695
|
+
|
|
696
|
+
name: Optional[str] = Field(None, description="Service name")
|
|
697
|
+
code: PAICodeConfig = Field(default_factory=PAICodeConfig)
|
|
698
|
+
service_group_name: Optional[str] = Field(
|
|
699
|
+
None,
|
|
700
|
+
description="Service group name",
|
|
701
|
+
)
|
|
702
|
+
resources: PAIResourcesConfig = Field(default_factory=PAIResourcesConfig)
|
|
703
|
+
vpc_config: PAIVpcConfig = Field(default_factory=PAIVpcConfig)
|
|
704
|
+
identity: PAIIdentityConfig = Field(
|
|
705
|
+
default_factory=PAIIdentityConfig,
|
|
706
|
+
)
|
|
707
|
+
observability: PAIObservabilityConfig = Field(
|
|
708
|
+
default_factory=PAIObservabilityConfig,
|
|
709
|
+
)
|
|
710
|
+
storage: PAIStorageConfig = Field(default_factory=PAIStorageConfig)
|
|
711
|
+
env: Dict[str, str] = Field(
|
|
712
|
+
default_factory=dict,
|
|
713
|
+
description="Environment variables",
|
|
714
|
+
)
|
|
715
|
+
tags: Dict[str, str] = Field(
|
|
716
|
+
default_factory=dict,
|
|
717
|
+
description="Tags for the deployment",
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class PAIDeployConfig(ConfigBaseModel):
|
|
722
|
+
"""
|
|
723
|
+
Complete PAI deployment configuration.
|
|
724
|
+
|
|
725
|
+
Supports both nested YAML structure and flat CLI parameters.
|
|
726
|
+
"""
|
|
727
|
+
|
|
728
|
+
# Nested structure (from config file)
|
|
729
|
+
context: PAIContextConfig = Field(default_factory=PAIContextConfig)
|
|
730
|
+
spec: PAISpecConfig = Field(default_factory=PAISpecConfig)
|
|
731
|
+
|
|
732
|
+
# Deployment behavior
|
|
733
|
+
wait: bool = Field(True, description="Wait for deployment to complete")
|
|
734
|
+
timeout: int = Field(1800, description="Deployment timeout in seconds")
|
|
735
|
+
auto_approve: bool = Field(True, description="Auto approve deployment")
|
|
736
|
+
|
|
737
|
+
@classmethod
|
|
738
|
+
def from_yaml(cls, path: Union[str, Path]) -> "PAIDeployConfig":
|
|
739
|
+
"""Load configuration from YAML file."""
|
|
740
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
741
|
+
data = yaml.safe_load(f) or {}
|
|
742
|
+
return cls.model_validate(data)
|
|
743
|
+
|
|
744
|
+
@classmethod
|
|
745
|
+
def from_dict(cls, data: Dict[str, Any]) -> "PAIDeployConfig":
|
|
746
|
+
"""Create configuration from dictionary."""
|
|
747
|
+
return cls.model_validate(data)
|
|
748
|
+
|
|
749
|
+
def merge_cli(
|
|
750
|
+
self,
|
|
751
|
+
source: Optional[str] = None,
|
|
752
|
+
name: Optional[str] = None,
|
|
753
|
+
entrypoint: Optional[str] = None,
|
|
754
|
+
workspace_id: Optional[str] = None,
|
|
755
|
+
region: Optional[str] = None,
|
|
756
|
+
oss_path: Optional[str] = None,
|
|
757
|
+
instance_type: Optional[str] = None,
|
|
758
|
+
instance_count: Optional[int] = None,
|
|
759
|
+
resource_id: Optional[str] = None,
|
|
760
|
+
quota_id: Optional[str] = None,
|
|
761
|
+
cpu: Optional[int] = None,
|
|
762
|
+
memory: Optional[int] = None,
|
|
763
|
+
service_group: Optional[str] = None,
|
|
764
|
+
resource_type: Optional[str] = None,
|
|
765
|
+
vpc_id: Optional[str] = None,
|
|
766
|
+
vswitch_id: Optional[str] = None,
|
|
767
|
+
security_group_id: Optional[str] = None,
|
|
768
|
+
ram_role_arn: Optional[str] = None,
|
|
769
|
+
enable_trace: Optional[bool] = None,
|
|
770
|
+
wait: Optional[bool] = None,
|
|
771
|
+
timeout: Optional[int] = None,
|
|
772
|
+
auto_approve: Optional[bool] = None,
|
|
773
|
+
environment: Optional[Dict[str, str]] = None,
|
|
774
|
+
tags: Optional[Dict[str, str]] = None,
|
|
775
|
+
) -> "PAIDeployConfig":
|
|
776
|
+
"""
|
|
777
|
+
Merge CLI parameters into config. CLI values override config values.
|
|
778
|
+
|
|
779
|
+
Returns a new PAIDeployConfig with merged values.
|
|
780
|
+
"""
|
|
781
|
+
data = self.model_dump()
|
|
782
|
+
|
|
783
|
+
# Context overrides
|
|
784
|
+
if workspace_id is not None:
|
|
785
|
+
data["context"]["workspace_id"] = workspace_id
|
|
786
|
+
if region is not None:
|
|
787
|
+
data["context"]["region"] = region
|
|
788
|
+
|
|
789
|
+
# Spec overrides
|
|
790
|
+
if name is not None:
|
|
791
|
+
data["spec"]["name"] = name
|
|
792
|
+
if source is not None:
|
|
793
|
+
data["spec"]["code"]["source_dir"] = source
|
|
794
|
+
if entrypoint is not None:
|
|
795
|
+
data["spec"]["code"]["entrypoint"] = entrypoint
|
|
796
|
+
if service_group is not None:
|
|
797
|
+
data["spec"]["service_group_name"] = service_group
|
|
798
|
+
|
|
799
|
+
# Resources overrides
|
|
800
|
+
if resource_type is not None:
|
|
801
|
+
data["spec"]["resources"]["type"] = resource_type
|
|
802
|
+
if instance_type is not None:
|
|
803
|
+
data["spec"]["resources"]["instance_type"] = instance_type
|
|
804
|
+
if instance_count is not None:
|
|
805
|
+
data["spec"]["resources"]["instance_count"] = instance_count
|
|
806
|
+
if resource_id is not None:
|
|
807
|
+
data["spec"]["resources"]["resource_id"] = resource_id
|
|
808
|
+
if quota_id is not None:
|
|
809
|
+
data["spec"]["resources"]["quota_id"] = quota_id
|
|
810
|
+
if cpu is not None:
|
|
811
|
+
data["spec"]["resources"]["cpu"] = cpu
|
|
812
|
+
if memory is not None:
|
|
813
|
+
data["spec"]["resources"]["memory"] = memory
|
|
814
|
+
|
|
815
|
+
# VPC overrides
|
|
816
|
+
if vpc_id is not None:
|
|
817
|
+
data["spec"]["vpc_config"]["vpc_id"] = vpc_id
|
|
818
|
+
if vswitch_id is not None:
|
|
819
|
+
data["spec"]["vpc_config"]["vswitch_id"] = vswitch_id
|
|
820
|
+
if security_group_id is not None:
|
|
821
|
+
data["spec"]["vpc_config"]["security_group_id"] = security_group_id
|
|
822
|
+
|
|
823
|
+
# IAM overrides
|
|
824
|
+
if ram_role_arn is not None:
|
|
825
|
+
data["spec"]["identity"]["ram_role_arn"] = ram_role_arn
|
|
826
|
+
|
|
827
|
+
# Observability overrides
|
|
828
|
+
if enable_trace is not None:
|
|
829
|
+
data["spec"]["observability"]["enable_trace"] = enable_trace
|
|
830
|
+
|
|
831
|
+
# Storage overrides
|
|
832
|
+
if oss_path is not None:
|
|
833
|
+
data["spec"]["storage"]["work_dir"] = oss_path
|
|
834
|
+
|
|
835
|
+
# Environment overrides (merge, CLI takes precedence)
|
|
836
|
+
if environment:
|
|
837
|
+
data["spec"]["env"].update(environment)
|
|
838
|
+
|
|
839
|
+
# Tags overrides (merge, CLI takes precedence)
|
|
840
|
+
if tags:
|
|
841
|
+
data["spec"]["tags"].update(tags)
|
|
842
|
+
|
|
843
|
+
# Deployment behavior overrides
|
|
844
|
+
if wait is not None:
|
|
845
|
+
data["wait"] = wait
|
|
846
|
+
if timeout is not None:
|
|
847
|
+
data["timeout"] = timeout
|
|
848
|
+
if auto_approve is not None:
|
|
849
|
+
data["auto_approve"] = auto_approve
|
|
850
|
+
|
|
851
|
+
return PAIDeployConfig.model_validate(data)
|
|
852
|
+
|
|
853
|
+
def resolve_resource_type(self) -> str:
|
|
854
|
+
"""
|
|
855
|
+
Resolve resource type with implicit inference.
|
|
856
|
+
|
|
857
|
+
Priority:
|
|
858
|
+
1. Explicit type if set
|
|
859
|
+
2. 'quota' if quota_id is provided
|
|
860
|
+
3. 'resource' if resource_id is provided
|
|
861
|
+
4. 'public' (default)
|
|
862
|
+
"""
|
|
863
|
+
resources = self.spec.resources
|
|
864
|
+
if resources.type:
|
|
865
|
+
return resources.type
|
|
866
|
+
if resources.quota_id:
|
|
867
|
+
return "quota"
|
|
868
|
+
if resources.resource_id:
|
|
869
|
+
return "resource"
|
|
870
|
+
return "public"
|
|
871
|
+
|
|
872
|
+
def resolve_oss_work_dir(self) -> Optional[str]:
|
|
873
|
+
"""
|
|
874
|
+
Resolve OSS work directory with fallback.
|
|
875
|
+
|
|
876
|
+
Priority:
|
|
877
|
+
1. spec.storage.work_dir if set
|
|
878
|
+
2. context.storage.work_dir as fallback
|
|
879
|
+
3. None (deployer will use workspace default)
|
|
880
|
+
"""
|
|
881
|
+
if self.spec.storage.work_dir:
|
|
882
|
+
return self.spec.storage.work_dir
|
|
883
|
+
if self.context.storage.work_dir:
|
|
884
|
+
return self.context.storage.work_dir
|
|
885
|
+
return None
|
|
886
|
+
|
|
887
|
+
def to_deployer_kwargs(self) -> Dict[str, Any]:
|
|
888
|
+
"""
|
|
889
|
+
Convert config to kwargs for PAIDeployManager.deploy().
|
|
890
|
+
"""
|
|
891
|
+
resource_type = self.resolve_resource_type()
|
|
892
|
+
resources = self.spec.resources
|
|
893
|
+
|
|
894
|
+
# Determine RAM role mode
|
|
895
|
+
ram_role_arn = self.spec.identity.ram_role_arn
|
|
896
|
+
ram_role_mode = "custom" if ram_role_arn else "default"
|
|
897
|
+
|
|
898
|
+
# Apply default values based on resource_type
|
|
899
|
+
instance_type = resources.instance_type
|
|
900
|
+
cpu = resources.cpu
|
|
901
|
+
memory = resources.memory
|
|
902
|
+
|
|
903
|
+
if resource_type == "public":
|
|
904
|
+
# Default instance_type for public mode
|
|
905
|
+
if not instance_type:
|
|
906
|
+
instance_type = "ecs.c6.large"
|
|
907
|
+
elif resource_type in ("resource", "quota"):
|
|
908
|
+
# Default cpu and memory for resource/quota mode
|
|
909
|
+
if cpu is None:
|
|
910
|
+
cpu = 2
|
|
911
|
+
if memory is None:
|
|
912
|
+
memory = 2048
|
|
913
|
+
|
|
914
|
+
kwargs = {
|
|
915
|
+
"project_dir": self.spec.code.source_dir,
|
|
916
|
+
"entrypoint": self.spec.code.entrypoint,
|
|
917
|
+
"service_name": self.spec.name,
|
|
918
|
+
"service_group_name": self.spec.service_group_name,
|
|
919
|
+
"resource_type": resource_type,
|
|
920
|
+
"instance_count": resources.instance_count,
|
|
921
|
+
"instance_type": instance_type,
|
|
922
|
+
"resource_id": resources.resource_id,
|
|
923
|
+
"quota_id": resources.quota_id,
|
|
924
|
+
"cpu": cpu,
|
|
925
|
+
"memory": memory,
|
|
926
|
+
"vpc_id": self.spec.vpc_config.vpc_id,
|
|
927
|
+
"vswitch_id": self.spec.vpc_config.vswitch_id,
|
|
928
|
+
"security_group_id": self.spec.vpc_config.security_group_id,
|
|
929
|
+
"ram_role_mode": ram_role_mode,
|
|
930
|
+
"ram_role_arn": ram_role_arn,
|
|
931
|
+
"enable_trace": self.spec.observability.enable_trace,
|
|
932
|
+
"environment": self.spec.env if self.spec.env else None,
|
|
933
|
+
"tags": self.spec.tags if self.spec.tags else None,
|
|
934
|
+
"wait": self.wait,
|
|
935
|
+
"timeout": self.timeout,
|
|
936
|
+
"auto_approve": self.auto_approve,
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
# Remove None values to use deployer defaults
|
|
940
|
+
return {k: v for k, v in kwargs.items() if v is not None}
|
|
941
|
+
|
|
942
|
+
def validate_for_deploy(self) -> None:
|
|
943
|
+
"""
|
|
944
|
+
Validate configuration is complete for deployment.
|
|
945
|
+
|
|
946
|
+
Raises:
|
|
947
|
+
ValueError: If required fields are missing
|
|
948
|
+
"""
|
|
949
|
+
errors = []
|
|
950
|
+
|
|
951
|
+
if not self.spec.name:
|
|
952
|
+
errors.append("Service name is required (spec.name or --name)")
|
|
953
|
+
|
|
954
|
+
if not self.spec.code.source_dir:
|
|
955
|
+
errors.append(
|
|
956
|
+
"Source directory is required "
|
|
957
|
+
"(spec.code.source_dir or SOURCE argument)",
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
# Validate source_dir exists
|
|
961
|
+
if self.spec.code.source_dir:
|
|
962
|
+
source_path = Path(self.spec.code.source_dir)
|
|
963
|
+
if not source_path.exists():
|
|
964
|
+
errors.append(
|
|
965
|
+
f"Source directory not found: {self.spec.code.source_dir}",
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
# Resource type specific validation
|
|
969
|
+
resource_type = self.resolve_resource_type()
|
|
970
|
+
resources = self.spec.resources
|
|
971
|
+
|
|
972
|
+
if resource_type == "resource" and not resources.resource_id:
|
|
973
|
+
errors.append("resource_id is required for resource mode")
|
|
974
|
+
if resource_type == "quota" and not resources.quota_id:
|
|
975
|
+
errors.append("quota_id is required for quota mode")
|
|
976
|
+
|
|
977
|
+
if errors:
|
|
978
|
+
raise ValueError(
|
|
979
|
+
"Configuration validation failed:\n - "
|
|
980
|
+
+ "\n - ".join(errors),
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _read_ignore_file(ignore_file_path: Path) -> List[str]:
|
|
985
|
+
"""
|
|
986
|
+
Read patterns from .gitignore or .dockerignore file.
|
|
987
|
+
|
|
988
|
+
Args:
|
|
989
|
+
ignore_file_path: Path to the ignore file
|
|
990
|
+
|
|
991
|
+
Returns:
|
|
992
|
+
List of ignore patterns
|
|
993
|
+
"""
|
|
994
|
+
patterns = []
|
|
995
|
+
if ignore_file_path.exists():
|
|
996
|
+
with open(ignore_file_path, "r", encoding="utf-8") as f:
|
|
997
|
+
for line in f:
|
|
998
|
+
line = line.strip()
|
|
999
|
+
# Skip empty lines and comments
|
|
1000
|
+
if line and not line.startswith("#"):
|
|
1001
|
+
patterns.append(line)
|
|
1002
|
+
return patterns
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _should_ignore(path: str, patterns: List[str]) -> bool:
|
|
1006
|
+
"""
|
|
1007
|
+
Check if path should be ignored based on patterns.
|
|
1008
|
+
|
|
1009
|
+
Args:
|
|
1010
|
+
path: Path to check (relative)
|
|
1011
|
+
patterns: List of ignore patterns
|
|
1012
|
+
|
|
1013
|
+
Returns:
|
|
1014
|
+
True if path should be ignored
|
|
1015
|
+
"""
|
|
1016
|
+
path_parts = Path(path).parts
|
|
1017
|
+
|
|
1018
|
+
for pattern in patterns:
|
|
1019
|
+
pattern = pattern.lstrip("/")
|
|
1020
|
+
pattern_normalized = pattern.rstrip("/")
|
|
1021
|
+
if pattern_normalized in path_parts:
|
|
1022
|
+
return True
|
|
1023
|
+
|
|
1024
|
+
if "*" in pattern or "?" in pattern:
|
|
1025
|
+
if fnmatch.fnmatch(path, pattern):
|
|
1026
|
+
return True
|
|
1027
|
+
for part in path_parts:
|
|
1028
|
+
if fnmatch.fnmatch(part, pattern):
|
|
1029
|
+
return True
|
|
1030
|
+
if (
|
|
1031
|
+
path.startswith(pattern_normalized + "/")
|
|
1032
|
+
or path == pattern_normalized
|
|
1033
|
+
):
|
|
1034
|
+
return True
|
|
1035
|
+
|
|
1036
|
+
return False
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _get_default_ignore_patterns() -> List[str]:
|
|
1040
|
+
"""
|
|
1041
|
+
Get default ignore patterns for OSS upload.
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
List of default ignore patterns (similar to .dockerignore/.gitignore)
|
|
1045
|
+
"""
|
|
1046
|
+
return [
|
|
1047
|
+
"__pycache__",
|
|
1048
|
+
"*.pyc",
|
|
1049
|
+
"*.pyo",
|
|
1050
|
+
"*.pyd",
|
|
1051
|
+
".git",
|
|
1052
|
+
".gitignore",
|
|
1053
|
+
".dockerignore",
|
|
1054
|
+
".pytest_cache",
|
|
1055
|
+
".mypy_cache",
|
|
1056
|
+
".tox",
|
|
1057
|
+
"venv",
|
|
1058
|
+
"env",
|
|
1059
|
+
".venv",
|
|
1060
|
+
"virtualenv",
|
|
1061
|
+
"node_modules",
|
|
1062
|
+
".DS_Store",
|
|
1063
|
+
"*.egg-info",
|
|
1064
|
+
"build",
|
|
1065
|
+
"dist",
|
|
1066
|
+
".cache",
|
|
1067
|
+
"*.swp",
|
|
1068
|
+
"*.swo",
|
|
1069
|
+
"*~",
|
|
1070
|
+
".idea",
|
|
1071
|
+
".vscode",
|
|
1072
|
+
"*.log",
|
|
1073
|
+
"logs",
|
|
1074
|
+
".agentscope_runtime",
|
|
1075
|
+
"*.tmp",
|
|
1076
|
+
"*.temp",
|
|
1077
|
+
".coverage",
|
|
1078
|
+
"htmlcov",
|
|
1079
|
+
".pytest_cache",
|
|
1080
|
+
]
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def _generate_deployment_tool_tags(
|
|
1084
|
+
deploy_method: str = "cli",
|
|
1085
|
+
) -> Dict[str, str]:
|
|
1086
|
+
"""
|
|
1087
|
+
Generate automatic tags for deployment tool information.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
deploy_method: Deployment method, either "cli" or "sdk"
|
|
1091
|
+
|
|
1092
|
+
Returns:
|
|
1093
|
+
Dictionary of auto-generated tags with agentscope.io/ prefix
|
|
1094
|
+
"""
|
|
1095
|
+
return {
|
|
1096
|
+
"deployed-by": "agentscope-runtime",
|
|
1097
|
+
"client-version": __version__,
|
|
1098
|
+
"deploy-method": deploy_method,
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
class PAIDeployManager(DeployManager):
|
|
1103
|
+
"""
|
|
1104
|
+
Deployer for Alibaba Cloud PAI (Platform for AI) platform.
|
|
1105
|
+
|
|
1106
|
+
This deployer:
|
|
1107
|
+
1. Packages the application and uploads to OSS
|
|
1108
|
+
2. Creates/updates a Flow snapshot
|
|
1109
|
+
3. Deploys the snapshot as a service with configurable resource types
|
|
1110
|
+
"""
|
|
1111
|
+
|
|
1112
|
+
def __init__(
|
|
1113
|
+
self,
|
|
1114
|
+
workspace_id: Optional[str] = None,
|
|
1115
|
+
region_id: Optional[str] = None,
|
|
1116
|
+
access_key_id: Optional[str] = None,
|
|
1117
|
+
access_key_secret: Optional[str] = None,
|
|
1118
|
+
security_token: Optional[str] = None,
|
|
1119
|
+
oss_path: Optional[str] = None,
|
|
1120
|
+
build_root: Optional[Union[str, Path]] = None,
|
|
1121
|
+
state_manager=None,
|
|
1122
|
+
) -> None:
|
|
1123
|
+
"""
|
|
1124
|
+
Initialize PAI deployer.
|
|
1125
|
+
"""
|
|
1126
|
+
super().__init__(state_manager=state_manager)
|
|
1127
|
+
self.workspace_id: str = workspace_id or os.getenv(
|
|
1128
|
+
"PAI_WORKSPACE_ID",
|
|
1129
|
+
"",
|
|
1130
|
+
)
|
|
1131
|
+
self.region_id = (
|
|
1132
|
+
region_id
|
|
1133
|
+
or os.getenv("REGION")
|
|
1134
|
+
or os.getenv("ALIBABA_CLOUD_REGION_ID")
|
|
1135
|
+
or os.getenv("REGION_ID")
|
|
1136
|
+
or "cn-hangzhou"
|
|
1137
|
+
)
|
|
1138
|
+
self.access_key_id = access_key_id or os.getenv(
|
|
1139
|
+
"ALIBABA_CLOUD_ACCESS_KEY_ID",
|
|
1140
|
+
)
|
|
1141
|
+
self.access_key_secret = access_key_secret or os.getenv(
|
|
1142
|
+
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
|
|
1143
|
+
)
|
|
1144
|
+
self.security_token = security_token or os.getenv(
|
|
1145
|
+
"ALIBABA_CLOUD_SECURITY_TOKEN",
|
|
1146
|
+
)
|
|
1147
|
+
self.oss_path = oss_path
|
|
1148
|
+
self.build_root = Path(build_root) if build_root else None
|
|
1149
|
+
|
|
1150
|
+
if not self.workspace_id:
|
|
1151
|
+
raise ValueError("Workspace ID is required")
|
|
1152
|
+
|
|
1153
|
+
if not self.oss_path:
|
|
1154
|
+
self.oss_path = self.get_workspace_default_oss_storage_path()
|
|
1155
|
+
|
|
1156
|
+
@classmethod
|
|
1157
|
+
def is_available(cls) -> bool:
|
|
1158
|
+
"""Check if PAI is available."""
|
|
1159
|
+
|
|
1160
|
+
return all(
|
|
1161
|
+
[
|
|
1162
|
+
oss is not None,
|
|
1163
|
+
open_api_models is not None,
|
|
1164
|
+
LangStudioClient is not None,
|
|
1165
|
+
WorkspaceClient is not None,
|
|
1166
|
+
EASClient is not None,
|
|
1167
|
+
],
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
def _assert_cloud_sdks_available(self):
|
|
1171
|
+
"""Ensure required cloud SDKs are installed."""
|
|
1172
|
+
credential_client = self._credential_client()
|
|
1173
|
+
|
|
1174
|
+
try:
|
|
1175
|
+
_ = credential_client.get_credential()
|
|
1176
|
+
except Exception as e:
|
|
1177
|
+
raise RuntimeError(
|
|
1178
|
+
f"Failed to get credential: {e}. Please check your credential "
|
|
1179
|
+
"configuration.",
|
|
1180
|
+
) from e
|
|
1181
|
+
|
|
1182
|
+
async def _create_snapshot(
|
|
1183
|
+
self,
|
|
1184
|
+
archive_oss_uri: str,
|
|
1185
|
+
proj_id: str,
|
|
1186
|
+
service_name: str,
|
|
1187
|
+
) -> str:
|
|
1188
|
+
"""
|
|
1189
|
+
Create a snapshot for given archive_oss_uri
|
|
1190
|
+
"""
|
|
1191
|
+
client = self.get_langstudio_client()
|
|
1192
|
+
|
|
1193
|
+
resp = await client.create_snapshot_async(
|
|
1194
|
+
workspace_id=self.workspace_id,
|
|
1195
|
+
snapshot_resource_type="Flow",
|
|
1196
|
+
snapshot_resource_id=proj_id,
|
|
1197
|
+
snapshot_name=(
|
|
1198
|
+
f"{service_name}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
1199
|
+
),
|
|
1200
|
+
source_storage_path=archive_oss_uri,
|
|
1201
|
+
)
|
|
1202
|
+
return resp.get("SnapshotId", "")
|
|
1203
|
+
|
|
1204
|
+
def _build_deployment_config(
|
|
1205
|
+
self,
|
|
1206
|
+
resource_type: str,
|
|
1207
|
+
instance_count: int = 1,
|
|
1208
|
+
resource_id: Optional[str] = None,
|
|
1209
|
+
quota_id: Optional[str] = None,
|
|
1210
|
+
instance_type: Optional[str] = None,
|
|
1211
|
+
cpu: Optional[int] = None,
|
|
1212
|
+
memory: Optional[int] = None,
|
|
1213
|
+
vpc_id: Optional[str] = None,
|
|
1214
|
+
vswitch_id: Optional[str] = None,
|
|
1215
|
+
security_group_id: Optional[str] = None,
|
|
1216
|
+
service_group_name: Optional[str] = None,
|
|
1217
|
+
environment: Optional[Dict[str, str]] = None,
|
|
1218
|
+
tags: Optional[Dict[str, str]] = None,
|
|
1219
|
+
) -> str:
|
|
1220
|
+
"""
|
|
1221
|
+
Build deployment configuration JSON string.
|
|
1222
|
+
"""
|
|
1223
|
+
config: Dict[str, Any] = {
|
|
1224
|
+
"metadata": {
|
|
1225
|
+
"instance": instance_count,
|
|
1226
|
+
"workspace_id": self.workspace_id,
|
|
1227
|
+
},
|
|
1228
|
+
"cloud": {
|
|
1229
|
+
"networking": {},
|
|
1230
|
+
},
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if tags:
|
|
1234
|
+
config["labels"] = tags
|
|
1235
|
+
|
|
1236
|
+
if environment:
|
|
1237
|
+
config["containers"] = [
|
|
1238
|
+
{
|
|
1239
|
+
"env": [
|
|
1240
|
+
{
|
|
1241
|
+
"name": key,
|
|
1242
|
+
"value": value,
|
|
1243
|
+
}
|
|
1244
|
+
for key, value in environment.items()
|
|
1245
|
+
],
|
|
1246
|
+
},
|
|
1247
|
+
]
|
|
1248
|
+
|
|
1249
|
+
if service_group_name:
|
|
1250
|
+
config["metadata"]["group"] = service_group_name
|
|
1251
|
+
|
|
1252
|
+
# Add resource-specific configuration
|
|
1253
|
+
if resource_type == "public":
|
|
1254
|
+
# Public resource pool
|
|
1255
|
+
if instance_type:
|
|
1256
|
+
config["cloud"]["computing"] = {
|
|
1257
|
+
"instances": [{"type": instance_type}],
|
|
1258
|
+
}
|
|
1259
|
+
elif resource_type == "resource":
|
|
1260
|
+
# EAS resource group
|
|
1261
|
+
if not resource_id:
|
|
1262
|
+
raise ValueError(
|
|
1263
|
+
"resource_id required for resource type",
|
|
1264
|
+
)
|
|
1265
|
+
config["metadata"]["resource"] = resource_id
|
|
1266
|
+
if cpu:
|
|
1267
|
+
config["metadata"]["cpu"] = cpu
|
|
1268
|
+
if memory:
|
|
1269
|
+
config["metadata"]["memory"] = memory
|
|
1270
|
+
elif resource_type == "quota":
|
|
1271
|
+
# Quota-based
|
|
1272
|
+
if not quota_id:
|
|
1273
|
+
raise ValueError("quota_id required for quota resource type")
|
|
1274
|
+
config["metadata"]["quota_id"] = quota_id
|
|
1275
|
+
if cpu:
|
|
1276
|
+
config["metadata"]["cpu"] = cpu
|
|
1277
|
+
if memory:
|
|
1278
|
+
config["metadata"]["memory"] = memory
|
|
1279
|
+
config["options"] = {"priority": 9}
|
|
1280
|
+
else:
|
|
1281
|
+
raise ValueError(f"Unsupported resource_type: {resource_type}")
|
|
1282
|
+
|
|
1283
|
+
# Add VPC configuration if provided
|
|
1284
|
+
if vpc_id:
|
|
1285
|
+
config["cloud"]["networking"]["vpc_id"] = vpc_id
|
|
1286
|
+
if vswitch_id:
|
|
1287
|
+
config["cloud"]["networking"]["vswitch_id"] = vswitch_id
|
|
1288
|
+
if security_group_id:
|
|
1289
|
+
config["cloud"]["networking"][
|
|
1290
|
+
"security_group_id"
|
|
1291
|
+
] = security_group_id
|
|
1292
|
+
|
|
1293
|
+
return json.dumps(config)
|
|
1294
|
+
|
|
1295
|
+
def _build_credential_config(
|
|
1296
|
+
self,
|
|
1297
|
+
ram_role_mode: str = "default",
|
|
1298
|
+
ram_role_arn: Optional[str] = None,
|
|
1299
|
+
) -> Dict[str, Any]:
|
|
1300
|
+
"""
|
|
1301
|
+
Build credential configuration.
|
|
1302
|
+
|
|
1303
|
+
Args:
|
|
1304
|
+
ram_role_mode: "default", "custom", or "none"
|
|
1305
|
+
ram_role_arn: RAM role ARN (required for custom mode)
|
|
1306
|
+
|
|
1307
|
+
Returns:
|
|
1308
|
+
Credential configuration dict
|
|
1309
|
+
"""
|
|
1310
|
+
if ram_role_mode == "none":
|
|
1311
|
+
return {
|
|
1312
|
+
"EnableCredentialInject": False,
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
cred_config: Dict[str, Any] = {
|
|
1316
|
+
"EnableCredentialInject": True,
|
|
1317
|
+
"AliyunEnvRoleKey": "0",
|
|
1318
|
+
"CredentialConfigItems": [
|
|
1319
|
+
{
|
|
1320
|
+
"Type": "Role",
|
|
1321
|
+
"Key": "0",
|
|
1322
|
+
"Roles": [],
|
|
1323
|
+
},
|
|
1324
|
+
],
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if ram_role_mode == "custom":
|
|
1328
|
+
if not ram_role_arn:
|
|
1329
|
+
raise ValueError(
|
|
1330
|
+
"ram_role_arn required for custom ram_role_mode",
|
|
1331
|
+
)
|
|
1332
|
+
cred_config["CredentialConfigItems"][0]["Roles"] = [ram_role_arn]
|
|
1333
|
+
|
|
1334
|
+
return cred_config
|
|
1335
|
+
|
|
1336
|
+
async def _deploy_snapshot(
|
|
1337
|
+
self,
|
|
1338
|
+
snapshot_id: str,
|
|
1339
|
+
proj_id: str,
|
|
1340
|
+
service_name: str,
|
|
1341
|
+
oss_work_dir: str,
|
|
1342
|
+
enable_trace: bool = True,
|
|
1343
|
+
resource_type: str = "public",
|
|
1344
|
+
service_group_name: Optional[str] = None,
|
|
1345
|
+
ram_role_mode: str = "default",
|
|
1346
|
+
ram_role_arn: Optional[str] = None,
|
|
1347
|
+
auto_approve: bool = True,
|
|
1348
|
+
**deployment_kwargs,
|
|
1349
|
+
) -> str:
|
|
1350
|
+
"""
|
|
1351
|
+
Deploy a snapshot as a service.
|
|
1352
|
+
"""
|
|
1353
|
+
logger.info(
|
|
1354
|
+
"Deploying snapshot %s as service %s",
|
|
1355
|
+
snapshot_id,
|
|
1356
|
+
service_name,
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
client = self.get_langstudio_client()
|
|
1360
|
+
|
|
1361
|
+
# Build deployment configuration
|
|
1362
|
+
deployment_config = self._build_deployment_config(
|
|
1363
|
+
resource_type=resource_type,
|
|
1364
|
+
service_group_name=service_group_name,
|
|
1365
|
+
**deployment_kwargs,
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
# Build credential configuration
|
|
1369
|
+
credential_config = self._build_credential_config(
|
|
1370
|
+
ram_role_mode=ram_role_mode,
|
|
1371
|
+
ram_role_arn=ram_role_arn,
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
response = await client.create_deployment_async(
|
|
1375
|
+
workspace_id=self.workspace_id,
|
|
1376
|
+
resource_type="Flow",
|
|
1377
|
+
resource_id=proj_id,
|
|
1378
|
+
resource_snapshot_id=snapshot_id,
|
|
1379
|
+
service_name=service_name,
|
|
1380
|
+
enable_trace=enable_trace,
|
|
1381
|
+
work_dir=self._oss_uri_patch_endpoint(oss_work_dir),
|
|
1382
|
+
deployment_config=deployment_config,
|
|
1383
|
+
credential_config=credential_config,
|
|
1384
|
+
auto_approval=auto_approve,
|
|
1385
|
+
service_group=service_group_name,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
deployment_id = response.get("DeploymentId", "")
|
|
1389
|
+
logger.info("Deployment created: %s", deployment_id)
|
|
1390
|
+
return deployment_id
|
|
1391
|
+
|
|
1392
|
+
def _oss_uri_patch_endpoint(self, oss_uri: str) -> str:
|
|
1393
|
+
"""
|
|
1394
|
+
Patch OSS URI endpoint to the correct endpoint.
|
|
1395
|
+
"""
|
|
1396
|
+
bucket_name, endpoint, object_key = parse_oss_uri(oss_uri)
|
|
1397
|
+
if not endpoint:
|
|
1398
|
+
endpoint = self._get_oss_endpoint(self.region_id)
|
|
1399
|
+
return f"oss://{bucket_name}.{endpoint}/{object_key}"
|
|
1400
|
+
|
|
1401
|
+
async def _wait_for_deployment(
|
|
1402
|
+
self,
|
|
1403
|
+
deployment_id: str,
|
|
1404
|
+
timeout: int = 1800,
|
|
1405
|
+
poll_interval: int = 10,
|
|
1406
|
+
) -> Dict[str, Any]:
|
|
1407
|
+
"""
|
|
1408
|
+
Wait for deployment to reach running state.
|
|
1409
|
+
|
|
1410
|
+
Args:
|
|
1411
|
+
deployment_id: Deployment ID to monitor
|
|
1412
|
+
timeout: Maximum wait time in seconds
|
|
1413
|
+
poll_interval: Polling interval in seconds
|
|
1414
|
+
|
|
1415
|
+
Returns:
|
|
1416
|
+
Final deployment status dict
|
|
1417
|
+
|
|
1418
|
+
Raises:
|
|
1419
|
+
TimeoutError: If deployment doesn't complete within timeout
|
|
1420
|
+
RuntimeError: If deployment fails
|
|
1421
|
+
"""
|
|
1422
|
+
logger.info("Waiting for deployment %s to complete...", deployment_id)
|
|
1423
|
+
client = self.get_langstudio_client()
|
|
1424
|
+
|
|
1425
|
+
start_time = time.time()
|
|
1426
|
+
|
|
1427
|
+
while time.time() - start_time < timeout:
|
|
1428
|
+
# Get deployment status
|
|
1429
|
+
response = await client.get_deployment_async(
|
|
1430
|
+
deployment_id=deployment_id,
|
|
1431
|
+
workspace_id=self.workspace_id,
|
|
1432
|
+
)
|
|
1433
|
+
status = response.get("DeploymentStatus", "")
|
|
1434
|
+
logger.info("Deployment status: %s", status)
|
|
1435
|
+
|
|
1436
|
+
if status == "Succeed":
|
|
1437
|
+
return {}
|
|
1438
|
+
elif status == "Failed":
|
|
1439
|
+
error_msg = response.get("ErrorMessage", "Unknown error")
|
|
1440
|
+
raise RuntimeError(
|
|
1441
|
+
f"Deployment {deployment_id} failed: {error_msg}",
|
|
1442
|
+
)
|
|
1443
|
+
elif status == "Canceled":
|
|
1444
|
+
raise RuntimeError(f"Deployment {deployment_id} cancled.")
|
|
1445
|
+
elif status in (
|
|
1446
|
+
"Running",
|
|
1447
|
+
"Creating",
|
|
1448
|
+
"WaitForConfirm",
|
|
1449
|
+
"Waiting",
|
|
1450
|
+
):
|
|
1451
|
+
await asyncio.sleep(poll_interval)
|
|
1452
|
+
else:
|
|
1453
|
+
logger.warning(
|
|
1454
|
+
"Deployment %s status unknown: %s",
|
|
1455
|
+
deployment_id,
|
|
1456
|
+
status,
|
|
1457
|
+
)
|
|
1458
|
+
await asyncio.sleep(poll_interval)
|
|
1459
|
+
|
|
1460
|
+
raise TimeoutError(
|
|
1461
|
+
f"Deployment {deployment_id} did not complete within {timeout} seconds",
|
|
1462
|
+
)
|
|
1463
|
+
|
|
1464
|
+
async def deploy( # pylint: disable=unused-argument
|
|
1465
|
+
self,
|
|
1466
|
+
project_dir: Optional[Union[str, Path]] = None,
|
|
1467
|
+
entrypoint: Optional[str] = None,
|
|
1468
|
+
protocol_adapters: Optional[list[ProtocolAdapter]] = None,
|
|
1469
|
+
environment: Optional[Dict[str, str]] = None,
|
|
1470
|
+
service_name: Optional[str] = None,
|
|
1471
|
+
app_name: Optional[str] = None,
|
|
1472
|
+
service_group_name: Optional[str] = None,
|
|
1473
|
+
tags: Optional[Dict[str, str]] = None,
|
|
1474
|
+
resource_type: str = "public",
|
|
1475
|
+
resource_id: Optional[str] = None,
|
|
1476
|
+
quota_id: Optional[str] = None,
|
|
1477
|
+
instance_count: int = 1,
|
|
1478
|
+
instance_type: Optional[str] = None,
|
|
1479
|
+
cpu: Optional[int] = None,
|
|
1480
|
+
memory: Optional[int] = None,
|
|
1481
|
+
vpc_id: Optional[str] = None,
|
|
1482
|
+
vswitch_id: Optional[str] = None,
|
|
1483
|
+
security_group_id: Optional[str] = None,
|
|
1484
|
+
ram_role_mode: str = "default",
|
|
1485
|
+
ram_role_arn: Optional[str] = None,
|
|
1486
|
+
enable_trace: bool = True,
|
|
1487
|
+
wait: bool = True,
|
|
1488
|
+
timeout: int = 1800,
|
|
1489
|
+
auto_approve: bool = True,
|
|
1490
|
+
**kwargs,
|
|
1491
|
+
) -> Dict[str, str]:
|
|
1492
|
+
"""
|
|
1493
|
+
Deploy application to PAI platform.
|
|
1494
|
+
|
|
1495
|
+
Args:
|
|
1496
|
+
app: AgentScope application instance
|
|
1497
|
+
runner: Runner instance
|
|
1498
|
+
endpoint_path: API endpoint path
|
|
1499
|
+
protocol_adapters: Protocol adapters
|
|
1500
|
+
environment: Environment variables
|
|
1501
|
+
project_dir: Local project directory
|
|
1502
|
+
service_name: Service name (required)
|
|
1503
|
+
app_name: Application name
|
|
1504
|
+
workspace_id: PAI workspace ID
|
|
1505
|
+
service_group_name: Service group name
|
|
1506
|
+
tags: Tags for the deployment
|
|
1507
|
+
resource_type: "public", "resource", or "quota"
|
|
1508
|
+
resource_id: EAS resource group ID
|
|
1509
|
+
quota_id: Quota ID
|
|
1510
|
+
instance_count: Number of instances
|
|
1511
|
+
instance_type: Instance type for public resource (e.g. ecs.c6.large)
|
|
1512
|
+
cpu: CPU cores (for resource/quota mode, default 2)
|
|
1513
|
+
memory: Memory in MB (for resource/quota mode, default 2048)
|
|
1514
|
+
vpc_id: VPC ID
|
|
1515
|
+
vswitch_id: VSwitch ID
|
|
1516
|
+
security_group_id: Security group ID
|
|
1517
|
+
ram_role_mode: "default", "custom", or "none"
|
|
1518
|
+
ram_role_arn: RAM role ARN
|
|
1519
|
+
enable_trace: Enable tracing
|
|
1520
|
+
wait: Wait for deployment to complete
|
|
1521
|
+
timeout: Deployment timeout in seconds
|
|
1522
|
+
custom_endpoints: Custom endpoints configuration
|
|
1523
|
+
auto_approve: Auto approve the deployment
|
|
1524
|
+
deploy_method: Deployment method ("cli" or "sdk")
|
|
1525
|
+
|
|
1526
|
+
Returns:
|
|
1527
|
+
Dict containing deployment information
|
|
1528
|
+
|
|
1529
|
+
Raises:
|
|
1530
|
+
ValueError: If required parameters are missing
|
|
1531
|
+
RuntimeError: If deployment fails
|
|
1532
|
+
"""
|
|
1533
|
+
from agentscope_runtime.engine.deployers.local_deployer import (
|
|
1534
|
+
LocalDeployManager,
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
if not service_name:
|
|
1538
|
+
raise ValueError("service_name is required for PAI deployment")
|
|
1539
|
+
|
|
1540
|
+
# Merge auto-generated tags with user tags
|
|
1541
|
+
# Priority: auto tags < user tags (user can override auto tags)
|
|
1542
|
+
|
|
1543
|
+
deploy_method = kwargs.get("deploy_method", "sdk")
|
|
1544
|
+
final_tags = _generate_deployment_tool_tags(deploy_method)
|
|
1545
|
+
if tags:
|
|
1546
|
+
final_tags.update(tags)
|
|
1547
|
+
|
|
1548
|
+
try:
|
|
1549
|
+
# Ensure SDKs are available
|
|
1550
|
+
self._assert_cloud_sdks_available()
|
|
1551
|
+
|
|
1552
|
+
app = kwargs.get("app")
|
|
1553
|
+
|
|
1554
|
+
if not project_dir and app:
|
|
1555
|
+
logger.info("Creating detached project from app/runner")
|
|
1556
|
+
project_dir = await LocalDeployManager.create_detached_project(
|
|
1557
|
+
app=app,
|
|
1558
|
+
protocol_adapters=protocol_adapters,
|
|
1559
|
+
**kwargs,
|
|
1560
|
+
)
|
|
1561
|
+
|
|
1562
|
+
if not project_dir:
|
|
1563
|
+
raise ValueError(
|
|
1564
|
+
"Either project_dir or app/runner must be provided",
|
|
1565
|
+
)
|
|
1566
|
+
|
|
1567
|
+
project_dir = Path(project_dir).resolve()
|
|
1568
|
+
if not project_dir.is_dir():
|
|
1569
|
+
raise FileNotFoundError(
|
|
1570
|
+
f"Project directory not found: {project_dir}",
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
# Create a zip archive of the project
|
|
1574
|
+
logger.info("Creating project archive")
|
|
1575
|
+
archive_path = self._create_project_archive(
|
|
1576
|
+
service_name,
|
|
1577
|
+
project_dir,
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
if not self.oss_path:
|
|
1581
|
+
raise ValueError("oss_path is required for PAI deployment")
|
|
1582
|
+
|
|
1583
|
+
oss_archive_uri = self._upload_archive(
|
|
1584
|
+
service_name=service_name,
|
|
1585
|
+
archive_path=archive_path,
|
|
1586
|
+
oss_path=self.oss_path,
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
proj_id = await self.get_or_create_langstudio_proj(
|
|
1590
|
+
service_name,
|
|
1591
|
+
oss_archive_uri,
|
|
1592
|
+
self.oss_path,
|
|
1593
|
+
)
|
|
1594
|
+
|
|
1595
|
+
# Step 2: Upload to OSS
|
|
1596
|
+
# Step 3: Create or update snapshot
|
|
1597
|
+
snapshot_id = await self._create_snapshot(
|
|
1598
|
+
archive_oss_uri=oss_archive_uri,
|
|
1599
|
+
proj_id=proj_id,
|
|
1600
|
+
service_name=service_name,
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
# Step 4: Deploy snapshot
|
|
1604
|
+
deployment_id = await self._deploy_snapshot(
|
|
1605
|
+
snapshot_id=snapshot_id,
|
|
1606
|
+
proj_id=proj_id,
|
|
1607
|
+
service_name=service_name,
|
|
1608
|
+
oss_work_dir=self.oss_path,
|
|
1609
|
+
enable_trace=enable_trace,
|
|
1610
|
+
resource_type=resource_type,
|
|
1611
|
+
service_group_name=service_group_name,
|
|
1612
|
+
ram_role_mode=ram_role_mode,
|
|
1613
|
+
ram_role_arn=ram_role_arn,
|
|
1614
|
+
instance_count=instance_count,
|
|
1615
|
+
resource_id=resource_id,
|
|
1616
|
+
quota_id=quota_id,
|
|
1617
|
+
instance_type=instance_type,
|
|
1618
|
+
cpu=cpu,
|
|
1619
|
+
memory=memory,
|
|
1620
|
+
vpc_id=vpc_id,
|
|
1621
|
+
vswitch_id=vswitch_id,
|
|
1622
|
+
security_group_id=security_group_id,
|
|
1623
|
+
auto_approve=auto_approve,
|
|
1624
|
+
environment=environment,
|
|
1625
|
+
tags=final_tags, # Use merged tags
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
# Step 5: Wait for deployment if requested
|
|
1629
|
+
if auto_approve and wait:
|
|
1630
|
+
await self._wait_for_deployment(
|
|
1631
|
+
deployment_id,
|
|
1632
|
+
timeout=timeout,
|
|
1633
|
+
)
|
|
1634
|
+
service_status = "running"
|
|
1635
|
+
|
|
1636
|
+
service = await self.get_service(service_name)
|
|
1637
|
+
endpoint = service.internet_endpoint
|
|
1638
|
+
token = service.access_token
|
|
1639
|
+
else:
|
|
1640
|
+
endpoint = None
|
|
1641
|
+
token = None
|
|
1642
|
+
service_status = "pending"
|
|
1643
|
+
|
|
1644
|
+
console_uri = self.get_deployment_console_uri(
|
|
1645
|
+
proj_id,
|
|
1646
|
+
deployment_id,
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
deployment = Deployment(
|
|
1650
|
+
id=deployment_id,
|
|
1651
|
+
platform="pai",
|
|
1652
|
+
url=endpoint,
|
|
1653
|
+
token=token,
|
|
1654
|
+
status=service_status,
|
|
1655
|
+
created_at=datetime.now().isoformat(),
|
|
1656
|
+
agent_source=str(project_dir),
|
|
1657
|
+
config={
|
|
1658
|
+
"deployment_id": deployment_id,
|
|
1659
|
+
"flow_id": proj_id,
|
|
1660
|
+
"snapshot_id": snapshot_id,
|
|
1661
|
+
"service_name": service_name,
|
|
1662
|
+
"workspace_id": self.workspace_id,
|
|
1663
|
+
"region": self.region_id,
|
|
1664
|
+
"oss_path": self.oss_path,
|
|
1665
|
+
},
|
|
1666
|
+
)
|
|
1667
|
+
self.state_manager.save(deployment)
|
|
1668
|
+
|
|
1669
|
+
# Return deployment information
|
|
1670
|
+
result = {
|
|
1671
|
+
"deploy_id": deployment_id,
|
|
1672
|
+
"flow_id": proj_id,
|
|
1673
|
+
"snapshot_id": snapshot_id,
|
|
1674
|
+
"service_name": service_name,
|
|
1675
|
+
"workspace_id": self.workspace_id,
|
|
1676
|
+
"url": console_uri,
|
|
1677
|
+
"status": service_status,
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
logger.info("PAI deployment completed successfully")
|
|
1681
|
+
logger.info("Console URL: %s", console_uri)
|
|
1682
|
+
|
|
1683
|
+
return result
|
|
1684
|
+
|
|
1685
|
+
except Exception as e:
|
|
1686
|
+
logger.error("Failed to deploy to PAI: %s", e, exc_info=True)
|
|
1687
|
+
raise
|
|
1688
|
+
|
|
1689
|
+
def get_deployment_console_uri(
|
|
1690
|
+
self,
|
|
1691
|
+
proj_id: str,
|
|
1692
|
+
deployment_id: str,
|
|
1693
|
+
) -> str:
|
|
1694
|
+
"""
|
|
1695
|
+
Return the console URI for a deployment.
|
|
1696
|
+
|
|
1697
|
+
"""
|
|
1698
|
+
return (
|
|
1699
|
+
f"https://pai.console.aliyun.com/?regionId="
|
|
1700
|
+
f"{self.region_id}&workspaceId="
|
|
1701
|
+
f"{self.workspace_id}#/lang-studio/flows/"
|
|
1702
|
+
f"flow-{proj_id}/deployments/{deployment_id}"
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
def get_service_console_uri(self, service_name: str) -> str:
|
|
1706
|
+
"""
|
|
1707
|
+
Return the console URI for a service.
|
|
1708
|
+
|
|
1709
|
+
"""
|
|
1710
|
+
return (
|
|
1711
|
+
f"https://pai.console.aliyun.com/?regionId="
|
|
1712
|
+
f"{self.region_id}&workspaceId="
|
|
1713
|
+
f"{self.workspace_id}#/eas/serviceDetail/"
|
|
1714
|
+
f"{service_name}/detail"
|
|
1715
|
+
)
|
|
1716
|
+
|
|
1717
|
+
def get_workspace_default_oss_storage_path(self) -> Optional[str]:
|
|
1718
|
+
from alibabacloud_aiworkspace20210204.models import ListConfigsRequest
|
|
1719
|
+
|
|
1720
|
+
client = self.get_workspace_client()
|
|
1721
|
+
config_key = "modelExportPath"
|
|
1722
|
+
|
|
1723
|
+
logger.warning("WorkspaceID: %s", self.workspace_id)
|
|
1724
|
+
|
|
1725
|
+
resp = client.list_configs(
|
|
1726
|
+
workspace_id=self.workspace_id,
|
|
1727
|
+
request=ListConfigsRequest(
|
|
1728
|
+
config_keys=config_key,
|
|
1729
|
+
),
|
|
1730
|
+
)
|
|
1731
|
+
default_oss_storage_uri = next(
|
|
1732
|
+
(c for c in resp.body.configs if c.config_key == config_key),
|
|
1733
|
+
None,
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
if default_oss_storage_uri:
|
|
1737
|
+
bucket, _, key = parse_oss_uri(
|
|
1738
|
+
default_oss_storage_uri.config_value,
|
|
1739
|
+
)
|
|
1740
|
+
return f"oss://{bucket}/{key}"
|
|
1741
|
+
else:
|
|
1742
|
+
return None
|
|
1743
|
+
|
|
1744
|
+
def _create_project_archive(self, service_name, project_dir: Path):
|
|
1745
|
+
build_dir = generate_build_directory("pai")
|
|
1746
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
1747
|
+
|
|
1748
|
+
ignore_patterns = _get_default_ignore_patterns()
|
|
1749
|
+
|
|
1750
|
+
project_path = Path(project_dir).resolve()
|
|
1751
|
+
|
|
1752
|
+
gitignore_path = project_path / ".gitignore"
|
|
1753
|
+
if gitignore_path.exists():
|
|
1754
|
+
ignore_patterns.extend(_read_ignore_file(gitignore_path))
|
|
1755
|
+
|
|
1756
|
+
dockerignore_path = project_path / ".dockerignore"
|
|
1757
|
+
if dockerignore_path.exists():
|
|
1758
|
+
ignore_patterns.extend(_read_ignore_file(dockerignore_path))
|
|
1759
|
+
|
|
1760
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1761
|
+
zip_filename = f"{service_name}_{timestamp}.zip"
|
|
1762
|
+
archive_path = build_dir / zip_filename
|
|
1763
|
+
|
|
1764
|
+
with zipfile.ZipFile(
|
|
1765
|
+
archive_path,
|
|
1766
|
+
"w",
|
|
1767
|
+
zipfile.ZIP_DEFLATED,
|
|
1768
|
+
) as archive:
|
|
1769
|
+
source_files = glob.glob(
|
|
1770
|
+
pathname=str(project_path / "**"),
|
|
1771
|
+
recursive=True,
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
for file_path in source_files:
|
|
1775
|
+
file_path_obj = Path(file_path)
|
|
1776
|
+
|
|
1777
|
+
# Skip if not a file (e.g., directories)
|
|
1778
|
+
if not file_path_obj.is_file():
|
|
1779
|
+
continue
|
|
1780
|
+
|
|
1781
|
+
file_relative_path = file_path_obj.relative_to(
|
|
1782
|
+
project_path,
|
|
1783
|
+
).as_posix()
|
|
1784
|
+
|
|
1785
|
+
# Skip . and .. directory references
|
|
1786
|
+
if file_relative_path in (".", ".."):
|
|
1787
|
+
continue
|
|
1788
|
+
|
|
1789
|
+
if _should_ignore(file_relative_path, ignore_patterns):
|
|
1790
|
+
logger.debug(
|
|
1791
|
+
"Skipping ignored file: %s",
|
|
1792
|
+
file_relative_path,
|
|
1793
|
+
)
|
|
1794
|
+
continue
|
|
1795
|
+
archive.write(file_path, file_relative_path)
|
|
1796
|
+
|
|
1797
|
+
logger.info("Project archived to: %s", archive_path)
|
|
1798
|
+
|
|
1799
|
+
return archive_path
|
|
1800
|
+
|
|
1801
|
+
def _upload_archive(
|
|
1802
|
+
self,
|
|
1803
|
+
archive_path: Path,
|
|
1804
|
+
oss_path: str,
|
|
1805
|
+
service_name: str,
|
|
1806
|
+
oss_endpoint: Optional[str] = None,
|
|
1807
|
+
region: Optional[str] = None,
|
|
1808
|
+
):
|
|
1809
|
+
"""
|
|
1810
|
+
Upload archive to OSS.
|
|
1811
|
+
|
|
1812
|
+
Args:
|
|
1813
|
+
archive_path: Path to the archive file
|
|
1814
|
+
oss_path: OSS path to upload the archive to
|
|
1815
|
+
|
|
1816
|
+
Returns:
|
|
1817
|
+
OSS path of the uploaded archive
|
|
1818
|
+
"""
|
|
1819
|
+
from alibabacloud_oss_v2.models import PutObjectRequest
|
|
1820
|
+
|
|
1821
|
+
bucket_name, endpoint, object_key = parse_oss_uri(oss_path)
|
|
1822
|
+
archive_obj_key = posixpath.join(
|
|
1823
|
+
object_key,
|
|
1824
|
+
"temp",
|
|
1825
|
+
f"{service_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip",
|
|
1826
|
+
)
|
|
1827
|
+
if endpoint and not oss_endpoint:
|
|
1828
|
+
oss_endpoint = endpoint
|
|
1829
|
+
|
|
1830
|
+
if not oss_endpoint:
|
|
1831
|
+
oss_endpoint = self._get_oss_endpoint(self.region_id)
|
|
1832
|
+
|
|
1833
|
+
client = self._get_oss_client(
|
|
1834
|
+
oss_endpoint=oss_endpoint,
|
|
1835
|
+
region=region,
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
client.put_object_from_file(
|
|
1839
|
+
request=PutObjectRequest(
|
|
1840
|
+
bucket=bucket_name,
|
|
1841
|
+
key=archive_obj_key,
|
|
1842
|
+
),
|
|
1843
|
+
filepath=archive_path,
|
|
1844
|
+
)
|
|
1845
|
+
return f"oss://{bucket_name}.{oss_endpoint}/{archive_obj_key}"
|
|
1846
|
+
|
|
1847
|
+
def _get_oss_client(
|
|
1848
|
+
self,
|
|
1849
|
+
oss_endpoint: Optional[str] = None,
|
|
1850
|
+
region: Optional[str] = None,
|
|
1851
|
+
):
|
|
1852
|
+
from alibabacloud_credentials.client import (
|
|
1853
|
+
Client as CredClient,
|
|
1854
|
+
)
|
|
1855
|
+
|
|
1856
|
+
class _CustomOssCredentialsProvider(
|
|
1857
|
+
oss.credentials.CredentialsProvider,
|
|
1858
|
+
):
|
|
1859
|
+
def __init__(self, credential_client: "CredClient"):
|
|
1860
|
+
self.credential_client = credential_client
|
|
1861
|
+
|
|
1862
|
+
def get_credentials(self) -> oss.credentials.Credentials:
|
|
1863
|
+
cred = self.credential_client.get_credential()
|
|
1864
|
+
|
|
1865
|
+
return oss.credentials.Credentials(
|
|
1866
|
+
access_key_id=cred.access_key_id,
|
|
1867
|
+
access_key_secret=cred.access_key_secret,
|
|
1868
|
+
security_token=cred.security_token,
|
|
1869
|
+
)
|
|
1870
|
+
|
|
1871
|
+
return oss.Client(
|
|
1872
|
+
config=oss.Config(
|
|
1873
|
+
region=region or self.region_id,
|
|
1874
|
+
endpoint=oss_endpoint,
|
|
1875
|
+
credentials_provider=_CustomOssCredentialsProvider(
|
|
1876
|
+
self._credential_client(),
|
|
1877
|
+
),
|
|
1878
|
+
),
|
|
1879
|
+
)
|
|
1880
|
+
|
|
1881
|
+
def _credential_client(self):
|
|
1882
|
+
from alibabacloud_credentials.client import (
|
|
1883
|
+
Client as CredClient,
|
|
1884
|
+
)
|
|
1885
|
+
from alibabacloud_credentials.models import Config
|
|
1886
|
+
from alibabacloud_credentials.utils import auth_constant as ac
|
|
1887
|
+
|
|
1888
|
+
if self.access_key_id and self.access_key_secret:
|
|
1889
|
+
if not self.security_token:
|
|
1890
|
+
config = Config(
|
|
1891
|
+
type=ac.ACCESS_KEY,
|
|
1892
|
+
access_key_id=self.access_key_id,
|
|
1893
|
+
access_key_secret=self.access_key_secret,
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
else:
|
|
1897
|
+
config = Config(
|
|
1898
|
+
type=ac.STS,
|
|
1899
|
+
access_key_id=self.access_key_id,
|
|
1900
|
+
access_key_secret=self.access_key_secret,
|
|
1901
|
+
security_token=self.security_token,
|
|
1902
|
+
)
|
|
1903
|
+
else:
|
|
1904
|
+
config = None
|
|
1905
|
+
|
|
1906
|
+
return CredClient(config=config)
|
|
1907
|
+
|
|
1908
|
+
def _eas_service_client(self) -> EASClient:
|
|
1909
|
+
return EASClient(
|
|
1910
|
+
config=open_api_models.Config(
|
|
1911
|
+
credential=self._credential_client(),
|
|
1912
|
+
region_id=self.region_id,
|
|
1913
|
+
endpoint=self._get_eas_endpoint(self.region_id),
|
|
1914
|
+
),
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
async def get_service(self, service_name: str) -> Optional[Any]:
|
|
1918
|
+
"""Get service information.
|
|
1919
|
+
|
|
1920
|
+
Args:
|
|
1921
|
+
service_name: Name of the service to retrieve
|
|
1922
|
+
|
|
1923
|
+
Returns:
|
|
1924
|
+
Service object if found, None otherwise
|
|
1925
|
+
"""
|
|
1926
|
+
from alibabacloud_tea_openapi.exceptions import AlibabaCloudException
|
|
1927
|
+
|
|
1928
|
+
eas_client = self._eas_service_client()
|
|
1929
|
+
|
|
1930
|
+
try:
|
|
1931
|
+
resp = await eas_client.describe_service_async(
|
|
1932
|
+
cluster_id=self.region_id,
|
|
1933
|
+
service_name=service_name,
|
|
1934
|
+
)
|
|
1935
|
+
return resp.body
|
|
1936
|
+
except AlibabaCloudException as e:
|
|
1937
|
+
if e.code == "Forbidden.PrivilegeCheckFailed":
|
|
1938
|
+
logger.warning(
|
|
1939
|
+
f"Given service name is owned by another user: {e}",
|
|
1940
|
+
)
|
|
1941
|
+
|
|
1942
|
+
raise ValueError(
|
|
1943
|
+
f"Given service name is owned by another user: "
|
|
1944
|
+
f"{service_name}. Please use a different service name.",
|
|
1945
|
+
) from e
|
|
1946
|
+
if e.code == "InvalidService.NotFound":
|
|
1947
|
+
return None
|
|
1948
|
+
raise
|
|
1949
|
+
|
|
1950
|
+
async def get_or_create_langstudio_proj(
|
|
1951
|
+
self,
|
|
1952
|
+
service_name,
|
|
1953
|
+
proj_archive_oss_uri: str,
|
|
1954
|
+
oss_path: str,
|
|
1955
|
+
):
|
|
1956
|
+
from alibabacloud_eas20210701.models import Service
|
|
1957
|
+
from alibabacloud_tea_openapi.exceptions import AlibabaCloudException
|
|
1958
|
+
|
|
1959
|
+
langstudio_client = self.get_langstudio_client()
|
|
1960
|
+
|
|
1961
|
+
service = await self.get_service(service_name)
|
|
1962
|
+
service = cast(Optional[Service], service)
|
|
1963
|
+
|
|
1964
|
+
# try to reuse existing project from service label
|
|
1965
|
+
if service and service.labels:
|
|
1966
|
+
proj_id_from_svc_label: Optional[str] = next(
|
|
1967
|
+
(
|
|
1968
|
+
label.label_value
|
|
1969
|
+
for label in service.labels
|
|
1970
|
+
if label.label_key == "FlowId"
|
|
1971
|
+
),
|
|
1972
|
+
None,
|
|
1973
|
+
)
|
|
1974
|
+
if not proj_id_from_svc_label:
|
|
1975
|
+
proj_id = None
|
|
1976
|
+
else:
|
|
1977
|
+
try:
|
|
1978
|
+
resp = await langstudio_client.get_flow_async(
|
|
1979
|
+
flow_id=proj_id_from_svc_label,
|
|
1980
|
+
workspace_id=self.workspace_id,
|
|
1981
|
+
)
|
|
1982
|
+
proj_id = resp.get("FlowId")
|
|
1983
|
+
except AlibabaCloudException as e:
|
|
1984
|
+
if e.status_code == 400:
|
|
1985
|
+
logger.info(
|
|
1986
|
+
"No flow found with id: %s, %s",
|
|
1987
|
+
proj_id_from_svc_label,
|
|
1988
|
+
e,
|
|
1989
|
+
)
|
|
1990
|
+
proj_id = None
|
|
1991
|
+
else:
|
|
1992
|
+
raise e
|
|
1993
|
+
else:
|
|
1994
|
+
proj_id = None
|
|
1995
|
+
|
|
1996
|
+
if not proj_id:
|
|
1997
|
+
flow_proj = await self._get_langstudio_proj_by_name(service_name)
|
|
1998
|
+
if flow_proj:
|
|
1999
|
+
proj_id = flow_proj.get("FlowId")
|
|
2000
|
+
|
|
2001
|
+
if not proj_id:
|
|
2002
|
+
resp = await langstudio_client.create_flow_async(
|
|
2003
|
+
workspace_id=self.workspace_id,
|
|
2004
|
+
flow_name=service_name,
|
|
2005
|
+
description=f"Project {service_name} created by Agentscope Runtime.",
|
|
2006
|
+
flow_type="Code",
|
|
2007
|
+
source_uri=proj_archive_oss_uri,
|
|
2008
|
+
work_dir=self._oss_uri_patch_endpoint(oss_path),
|
|
2009
|
+
create_from="OSS",
|
|
2010
|
+
)
|
|
2011
|
+
proj_id = resp.get("FlowId")
|
|
2012
|
+
|
|
2013
|
+
return proj_id
|
|
2014
|
+
|
|
2015
|
+
async def _get_langstudio_proj(
|
|
2016
|
+
self,
|
|
2017
|
+
flow_id: str,
|
|
2018
|
+
) -> Optional[Dict[str, Any]]:
|
|
2019
|
+
from alibabacloud_tea_openapi.exceptions import AlibabaCloudException
|
|
2020
|
+
|
|
2021
|
+
client = self.get_langstudio_client()
|
|
2022
|
+
|
|
2023
|
+
try:
|
|
2024
|
+
resp = await client.get_flow_async(
|
|
2025
|
+
flow_id=flow_id,
|
|
2026
|
+
workspace_id=self.workspace_id,
|
|
2027
|
+
)
|
|
2028
|
+
return resp
|
|
2029
|
+
except AlibabaCloudException as e:
|
|
2030
|
+
if e.status_code == 400:
|
|
2031
|
+
logger.info("No flow found with id: %s, %s", flow_id, e)
|
|
2032
|
+
return None
|
|
2033
|
+
else:
|
|
2034
|
+
raise e
|
|
2035
|
+
|
|
2036
|
+
def get_langstudio_client(self) -> LangStudioClient:
|
|
2037
|
+
client = LangStudioClient(
|
|
2038
|
+
config=open_api_models.Config(
|
|
2039
|
+
credential=self._credential_client(),
|
|
2040
|
+
region_id=self.region_id,
|
|
2041
|
+
endpoint=self._get_langstudio_endpoint(self.region_id),
|
|
2042
|
+
),
|
|
2043
|
+
)
|
|
2044
|
+
return client
|
|
2045
|
+
|
|
2046
|
+
def get_workspace_client(self) -> WorkspaceClient:
|
|
2047
|
+
from alibabacloud_tea_openapi import models as openapi_models
|
|
2048
|
+
|
|
2049
|
+
client = WorkspaceClient(
|
|
2050
|
+
config=openapi_models.Config(
|
|
2051
|
+
credential=self._credential_client(),
|
|
2052
|
+
region_id=self.region_id,
|
|
2053
|
+
endpoint=self._get_workspace_endpoint(self.region_id),
|
|
2054
|
+
),
|
|
2055
|
+
)
|
|
2056
|
+
return client
|
|
2057
|
+
|
|
2058
|
+
def _get_workspace_endpoint(self, region_id: str) -> str:
|
|
2059
|
+
internal_endpoint = f"aiworkspace-vpc.{region_id}.aliyuncs.com"
|
|
2060
|
+
public_endpoint = f"aiworkspace.{region_id}.aliyuncs.com"
|
|
2061
|
+
|
|
2062
|
+
return (
|
|
2063
|
+
internal_endpoint
|
|
2064
|
+
if is_tcp_reachable(internal_endpoint)
|
|
2065
|
+
else public_endpoint
|
|
2066
|
+
)
|
|
2067
|
+
|
|
2068
|
+
@staticmethod
|
|
2069
|
+
def _get_langstudio_endpoint(region_id: str) -> str:
|
|
2070
|
+
internal_endpoint = f"pailangstudio-vpc.{region_id}.aliyuncs.com"
|
|
2071
|
+
public_endpoint = f"pailangstudio.{region_id}.aliyuncs.com"
|
|
2072
|
+
|
|
2073
|
+
return (
|
|
2074
|
+
internal_endpoint
|
|
2075
|
+
if is_tcp_reachable(internal_endpoint)
|
|
2076
|
+
else public_endpoint
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
@staticmethod
|
|
2080
|
+
def _get_eas_endpoint(region_id: str) -> str:
|
|
2081
|
+
internal_endpoint = f"pai-eas-manage-vpc.{region_id}.aliyuncs.com"
|
|
2082
|
+
public_endpoint = f"pai-eas.{region_id}.aliyuncs.com"
|
|
2083
|
+
|
|
2084
|
+
return (
|
|
2085
|
+
internal_endpoint
|
|
2086
|
+
if is_tcp_reachable(internal_endpoint)
|
|
2087
|
+
else public_endpoint
|
|
2088
|
+
)
|
|
2089
|
+
|
|
2090
|
+
@staticmethod
|
|
2091
|
+
def _get_oss_endpoint(region_id: str) -> str:
|
|
2092
|
+
internal_endpoint = f"oss-{region_id}-internal.aliyuncs.com"
|
|
2093
|
+
public_endpoint = f"oss-{region_id}.aliyuncs.com"
|
|
2094
|
+
|
|
2095
|
+
return (
|
|
2096
|
+
internal_endpoint
|
|
2097
|
+
if is_tcp_reachable(internal_endpoint)
|
|
2098
|
+
else public_endpoint
|
|
2099
|
+
)
|
|
2100
|
+
|
|
2101
|
+
async def stop(self, deploy_id: str, **kwargs) -> Dict[str, Any]:
|
|
2102
|
+
"""
|
|
2103
|
+
Stop PAI deployment by stopping the deployed EAS service.
|
|
2104
|
+
|
|
2105
|
+
Args:
|
|
2106
|
+
deploy_id: Deployment identifier
|
|
2107
|
+
**kwargs: Additional parameters
|
|
2108
|
+
|
|
2109
|
+
Returns:
|
|
2110
|
+
Dict with success status and message
|
|
2111
|
+
"""
|
|
2112
|
+
# Get deployment from state
|
|
2113
|
+
deployment = self.state_manager.get(deploy_id)
|
|
2114
|
+
if not deployment:
|
|
2115
|
+
return {
|
|
2116
|
+
"success": False,
|
|
2117
|
+
"message": f"Deployment {deploy_id} not found",
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
service_name = deployment.config.get("service_name")
|
|
2121
|
+
if not service_name:
|
|
2122
|
+
return {
|
|
2123
|
+
"success": False,
|
|
2124
|
+
"message": "Service name not found in deployment state",
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
# Ensure SDKs available
|
|
2128
|
+
self._assert_cloud_sdks_available()
|
|
2129
|
+
|
|
2130
|
+
# Get EAS client and stop the service
|
|
2131
|
+
eas_client = self._eas_service_client()
|
|
2132
|
+
|
|
2133
|
+
logger.info("Stopping EAS service: %s", service_name)
|
|
2134
|
+
|
|
2135
|
+
await eas_client.stop_service_async(
|
|
2136
|
+
cluster_id=self.region_id,
|
|
2137
|
+
service_name=service_name,
|
|
2138
|
+
)
|
|
2139
|
+
|
|
2140
|
+
# Update deployment status in state
|
|
2141
|
+
self.state_manager.update_status(deploy_id, "stopped")
|
|
2142
|
+
|
|
2143
|
+
logger.info("EAS service stopped successfully: %s", service_name)
|
|
2144
|
+
|
|
2145
|
+
return {
|
|
2146
|
+
"success": True,
|
|
2147
|
+
"message": f"Service {service_name} stopped",
|
|
2148
|
+
"details": {
|
|
2149
|
+
"deploy_id": deploy_id,
|
|
2150
|
+
"service_name": service_name,
|
|
2151
|
+
},
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
def get_status(self) -> str:
|
|
2155
|
+
"""Get deployment status (not fully implemented)."""
|
|
2156
|
+
return "unknown"
|
|
2157
|
+
|
|
2158
|
+
async def _get_langstudio_proj_by_name(
|
|
2159
|
+
self,
|
|
2160
|
+
name: str,
|
|
2161
|
+
) -> Optional[Dict[str, Any]]:
|
|
2162
|
+
next_token = None
|
|
2163
|
+
|
|
2164
|
+
client = self.get_langstudio_client()
|
|
2165
|
+
|
|
2166
|
+
while True:
|
|
2167
|
+
resp = await client.list_flows_async(
|
|
2168
|
+
workspace_id=self.workspace_id,
|
|
2169
|
+
flow_name=name,
|
|
2170
|
+
sort_by="GmtCreateTime",
|
|
2171
|
+
order="DESC",
|
|
2172
|
+
next_token=next_token,
|
|
2173
|
+
page_size=100,
|
|
2174
|
+
)
|
|
2175
|
+
flows = resp.get("Flows", [])
|
|
2176
|
+
for flow in flows:
|
|
2177
|
+
if flow.get("FlowName") == name:
|
|
2178
|
+
return flow
|
|
2179
|
+
|
|
2180
|
+
next_token = resp.get("NextToken")
|
|
2181
|
+
if not next_token:
|
|
2182
|
+
break
|
|
2183
|
+
return None
|
|
2184
|
+
|
|
2185
|
+
async def _update_deployment(
|
|
2186
|
+
self,
|
|
2187
|
+
deployment_id: str,
|
|
2188
|
+
auto_approve: bool, # pylint: disable=unused-argument
|
|
2189
|
+
) -> Dict[str, Any]:
|
|
2190
|
+
client = self.get_langstudio_client()
|
|
2191
|
+
resp = await client.update_deployment_async(
|
|
2192
|
+
deployment_id=deployment_id,
|
|
2193
|
+
workspace_id=self.workspace_id,
|
|
2194
|
+
stage_action=json.dumps({"Stage": 3, "Action": "Confirm"}),
|
|
2195
|
+
)
|
|
2196
|
+
|
|
2197
|
+
return resp
|
|
2198
|
+
|
|
2199
|
+
async def delete_service(self, service_name: str) -> None:
|
|
2200
|
+
service_client = self._eas_service_client()
|
|
2201
|
+
|
|
2202
|
+
await service_client.delete_service_async(
|
|
2203
|
+
cluster_id=self.region_id,
|
|
2204
|
+
service_name=service_name,
|
|
2205
|
+
)
|
|
2206
|
+
|
|
2207
|
+
async def delete_project(self, project_name: str) -> None:
|
|
2208
|
+
proj = await self._get_langstudio_proj_by_name(project_name)
|
|
2209
|
+
if not proj:
|
|
2210
|
+
return
|
|
2211
|
+
client = self.get_langstudio_client()
|
|
2212
|
+
|
|
2213
|
+
await client.delete_flow_async(
|
|
2214
|
+
flow_id=proj.get("FlowId", ""),
|
|
2215
|
+
workspace_id=self.workspace_id,
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
async def wait_for_approval_stage(
|
|
2219
|
+
self,
|
|
2220
|
+
deployment_id: str,
|
|
2221
|
+
timeout: int = 300,
|
|
2222
|
+
poll_interval: int = 5,
|
|
2223
|
+
) -> bool:
|
|
2224
|
+
"""
|
|
2225
|
+
Wait for deployment to reach approval stage (WaitingForApproval).
|
|
2226
|
+
|
|
2227
|
+
Args:
|
|
2228
|
+
deployment_id: Deployment ID to monitor
|
|
2229
|
+
timeout: Maximum wait time in seconds
|
|
2230
|
+
poll_interval: Polling interval in seconds
|
|
2231
|
+
|
|
2232
|
+
Returns:
|
|
2233
|
+
True if deployment reached approval stage, False otherwise
|
|
2234
|
+
|
|
2235
|
+
Raises:
|
|
2236
|
+
TimeoutError: If deployment doesn't reach approval stage
|
|
2237
|
+
RuntimeError: If deployment fails before approval stage
|
|
2238
|
+
"""
|
|
2239
|
+
logger.info(
|
|
2240
|
+
"Waiting for deployment %s to reach approval stage...",
|
|
2241
|
+
deployment_id,
|
|
2242
|
+
)
|
|
2243
|
+
client = self.get_langstudio_client()
|
|
2244
|
+
|
|
2245
|
+
start_time = time.time()
|
|
2246
|
+
|
|
2247
|
+
while time.time() - start_time < timeout:
|
|
2248
|
+
response = await client.get_deployment_async(
|
|
2249
|
+
deployment_id=deployment_id,
|
|
2250
|
+
workspace_id=self.workspace_id,
|
|
2251
|
+
)
|
|
2252
|
+
status = response.get("DeploymentStatus", "")
|
|
2253
|
+
|
|
2254
|
+
if status == "WaitForConfirm":
|
|
2255
|
+
logger.info("Deployment is ready for approval")
|
|
2256
|
+
return True
|
|
2257
|
+
if status in ("Failed", "Canceled"):
|
|
2258
|
+
error_msg = response.get("ErrorMessage", "Unknown error")
|
|
2259
|
+
raise RuntimeError(
|
|
2260
|
+
f"Deployment {deployment_id} failed: {error_msg}",
|
|
2261
|
+
)
|
|
2262
|
+
if status in ("Running", "Creating"):
|
|
2263
|
+
await asyncio.sleep(poll_interval)
|
|
2264
|
+
continue
|
|
2265
|
+
if status == "Succeed":
|
|
2266
|
+
# Already approved and succeeded
|
|
2267
|
+
return True
|
|
2268
|
+
|
|
2269
|
+
await asyncio.sleep(poll_interval)
|
|
2270
|
+
|
|
2271
|
+
raise TimeoutError(
|
|
2272
|
+
f"Deployment {deployment_id} did not reach approval stage "
|
|
2273
|
+
f"within {timeout} seconds",
|
|
2274
|
+
)
|
|
2275
|
+
|
|
2276
|
+
async def approve_deployment(
|
|
2277
|
+
self,
|
|
2278
|
+
deployment_id: str,
|
|
2279
|
+
wait: bool = True,
|
|
2280
|
+
timeout: int = 1800,
|
|
2281
|
+
poll_interval: int = 10,
|
|
2282
|
+
) -> Dict[str, Any]:
|
|
2283
|
+
"""
|
|
2284
|
+
Approve a deployment.
|
|
2285
|
+
|
|
2286
|
+
Args:
|
|
2287
|
+
deployment_id: Deployment ID to approve
|
|
2288
|
+
wait: Wait for deployment to complete after approval
|
|
2289
|
+
timeout: Deployment timeout in seconds
|
|
2290
|
+
poll_interval: Polling interval in seconds
|
|
2291
|
+
|
|
2292
|
+
Returns:
|
|
2293
|
+
Dict with approval result
|
|
2294
|
+
"""
|
|
2295
|
+
logger.info("Approving deployment %s", deployment_id)
|
|
2296
|
+
client = self.get_langstudio_client()
|
|
2297
|
+
|
|
2298
|
+
await client.update_deployment_async(
|
|
2299
|
+
deployment_id=deployment_id,
|
|
2300
|
+
workspace_id=self.workspace_id,
|
|
2301
|
+
stage_action=json.dumps({"Stage": 3, "Action": "Confirm"}),
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
if wait:
|
|
2305
|
+
await self._wait_for_deployment(
|
|
2306
|
+
deployment_id,
|
|
2307
|
+
timeout=timeout,
|
|
2308
|
+
poll_interval=poll_interval,
|
|
2309
|
+
)
|
|
2310
|
+
|
|
2311
|
+
return {"success": True, "deployment_id": deployment_id}
|
|
2312
|
+
|
|
2313
|
+
async def cancel_deployment(
|
|
2314
|
+
self,
|
|
2315
|
+
deployment_id: str,
|
|
2316
|
+
) -> Dict[str, Any]:
|
|
2317
|
+
"""
|
|
2318
|
+
Cancel a deployment.
|
|
2319
|
+
|
|
2320
|
+
Args:
|
|
2321
|
+
deployment_id: Deployment ID to reject
|
|
2322
|
+
|
|
2323
|
+
Returns:
|
|
2324
|
+
Dict with rejection result
|
|
2325
|
+
"""
|
|
2326
|
+
logger.info("Cancelling deployment %s", deployment_id)
|
|
2327
|
+
client = self.get_langstudio_client()
|
|
2328
|
+
|
|
2329
|
+
await client.update_deployment_async(
|
|
2330
|
+
deployment_id=deployment_id,
|
|
2331
|
+
workspace_id=self.workspace_id,
|
|
2332
|
+
stage_action=json.dumps({"Stage": 3, "Action": "Cancel"}),
|
|
2333
|
+
)
|
|
2334
|
+
|
|
2335
|
+
return {"success": True, "deployment_id": deployment_id}
|