agentscope-runtime 0.1.5b2__py3-none-any.whl → 0.2.0__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/common/__init__.py +0 -0
- agentscope_runtime/common/collections/in_memory_mapping.py +27 -0
- agentscope_runtime/common/collections/redis_mapping.py +42 -0
- agentscope_runtime/common/container_clients/__init__.py +0 -0
- agentscope_runtime/common/container_clients/agentrun_client.py +1098 -0
- agentscope_runtime/common/container_clients/docker_client.py +250 -0
- agentscope_runtime/{sandbox/manager → common}/container_clients/kubernetes_client.py +6 -13
- agentscope_runtime/engine/__init__.py +12 -0
- agentscope_runtime/engine/agents/agentscope_agent.py +567 -0
- agentscope_runtime/engine/agents/agno_agent.py +26 -27
- agentscope_runtime/engine/agents/autogen_agent.py +13 -8
- agentscope_runtime/engine/agents/langgraph_agent.py +52 -9
- agentscope_runtime/engine/agents/utils.py +53 -0
- agentscope_runtime/engine/app/__init__.py +6 -0
- agentscope_runtime/engine/app/agent_app.py +239 -0
- agentscope_runtime/engine/app/base_app.py +181 -0
- agentscope_runtime/engine/app/celery_mixin.py +92 -0
- agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +5 -1
- agentscope_runtime/engine/deployers/base.py +1 -0
- agentscope_runtime/engine/deployers/cli_fc_deploy.py +39 -20
- agentscope_runtime/engine/deployers/kubernetes_deployer.py +12 -5
- agentscope_runtime/engine/deployers/local_deployer.py +61 -3
- agentscope_runtime/engine/deployers/modelstudio_deployer.py +201 -40
- agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +9 -0
- agentscope_runtime/engine/deployers/utils/package_project_utils.py +234 -3
- agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +567 -7
- agentscope_runtime/engine/deployers/utils/service_utils/standalone_main.py.j2 +211 -0
- agentscope_runtime/engine/deployers/utils/wheel_packager.py +1 -1
- agentscope_runtime/engine/helpers/helper.py +60 -41
- agentscope_runtime/engine/runner.py +40 -24
- agentscope_runtime/engine/schemas/agent_schemas.py +42 -0
- agentscope_runtime/engine/schemas/modelstudio_llm.py +14 -14
- agentscope_runtime/engine/services/sandbox_service.py +62 -70
- agentscope_runtime/engine/services/tablestore_memory_service.py +307 -0
- agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
- agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
- agentscope_runtime/engine/services/utils/__init__.py +0 -0
- agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
- agentscope_runtime/engine/tracing/__init__.py +9 -3
- agentscope_runtime/engine/tracing/asyncio_util.py +24 -0
- agentscope_runtime/engine/tracing/base.py +66 -34
- agentscope_runtime/engine/tracing/local_logging_handler.py +45 -31
- agentscope_runtime/engine/tracing/message_util.py +528 -0
- agentscope_runtime/engine/tracing/tracing_metric.py +20 -8
- agentscope_runtime/engine/tracing/tracing_util.py +130 -0
- agentscope_runtime/engine/tracing/wrapper.py +794 -169
- agentscope_runtime/sandbox/__init__.py +2 -0
- agentscope_runtime/sandbox/box/base/__init__.py +4 -0
- agentscope_runtime/sandbox/box/base/base_sandbox.py +6 -4
- agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +10 -14
- agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
- agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +2 -1
- agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +10 -7
- agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
- agentscope_runtime/sandbox/box/gui/box/__init__.py +0 -0
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +81 -0
- agentscope_runtime/sandbox/box/sandbox.py +5 -2
- agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
- agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
- agentscope_runtime/sandbox/box/training_box/training_box.py +7 -54
- agentscope_runtime/sandbox/build.py +143 -58
- agentscope_runtime/sandbox/client/http_client.py +87 -59
- agentscope_runtime/sandbox/client/training_client.py +0 -1
- agentscope_runtime/sandbox/constant.py +27 -1
- agentscope_runtime/sandbox/custom/custom_sandbox.py +7 -6
- agentscope_runtime/sandbox/custom/example.py +4 -3
- agentscope_runtime/sandbox/enums.py +1 -1
- agentscope_runtime/sandbox/manager/sandbox_manager.py +212 -106
- agentscope_runtime/sandbox/manager/server/app.py +82 -14
- agentscope_runtime/sandbox/manager/server/config.py +50 -3
- agentscope_runtime/sandbox/model/container.py +12 -23
- agentscope_runtime/sandbox/model/manager_config.py +93 -5
- agentscope_runtime/sandbox/registry.py +1 -1
- agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
- agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
- agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
- agentscope_runtime/sandbox/tools/tool.py +4 -0
- agentscope_runtime/sandbox/utils.py +124 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/METADATA +246 -111
- {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/RECORD +96 -80
- agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
- agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
- agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
- agentscope_runtime/engine/agents/llm_agent.py +0 -51
- agentscope_runtime/engine/llms/__init__.py +0 -3
- agentscope_runtime/engine/llms/base_llm.py +0 -60
- agentscope_runtime/engine/llms/qwen_llm.py +0 -47
- agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +0 -22
- agentscope_runtime/sandbox/manager/collections/redis_mapping.py +0 -26
- agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
- agentscope_runtime/sandbox/manager/container_clients/docker_client.py +0 -422
- /agentscope_runtime/{sandbox/manager → common}/collections/__init__.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/base_mapping.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/base_queue.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/base_set.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_queue.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_set.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/redis_queue.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/collections/redis_set.py +0 -0
- /agentscope_runtime/{sandbox/manager → common}/container_clients/base_client.py +0 -0
- {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/WHEEL +0 -0
- {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
9
|
import time
|
|
10
|
-
import
|
|
10
|
+
import json
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Dict, Optional, List, Union, Tuple
|
|
13
13
|
|
|
14
|
+
import requests
|
|
14
15
|
from pydantic import BaseModel, Field
|
|
15
16
|
|
|
16
17
|
from .adapter.protocol_adapter import ProtocolAdapter
|
|
@@ -90,12 +91,15 @@ class ModelstudioConfig(BaseModel):
|
|
|
90
91
|
|
|
91
92
|
@classmethod
|
|
92
93
|
def from_env(cls) -> "ModelstudioConfig":
|
|
94
|
+
raw_ws = os.environ.get("MODELSTUDIO_WORKSPACE_ID")
|
|
95
|
+
ws = raw_ws.strip() if isinstance(raw_ws, str) else ""
|
|
96
|
+
resolved_ws = ws if ws else "default"
|
|
93
97
|
return cls(
|
|
94
98
|
endpoint=os.environ.get(
|
|
95
99
|
"MODELSTUDIO_ENDPOINT",
|
|
96
100
|
"bailian.cn-beijing.aliyuncs.com",
|
|
97
101
|
),
|
|
98
|
-
workspace_id=
|
|
102
|
+
workspace_id=resolved_ws,
|
|
99
103
|
access_key_id=os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID"),
|
|
100
104
|
access_key_secret=os.environ.get(
|
|
101
105
|
"ALIBABA_CLOUD_ACCESS_KEY_SECRET",
|
|
@@ -107,8 +111,6 @@ class ModelstudioConfig(BaseModel):
|
|
|
107
111
|
|
|
108
112
|
def ensure_valid(self) -> None:
|
|
109
113
|
missing = []
|
|
110
|
-
if not self.workspace_id:
|
|
111
|
-
missing.append("MODELSTUDIO_WORKSPACE_ID")
|
|
112
114
|
if not self.access_key_id:
|
|
113
115
|
missing.append("ALIBABA_CLOUD_ACCESS_KEY_ID")
|
|
114
116
|
if not self.access_key_secret:
|
|
@@ -201,16 +203,6 @@ async def _oss_create_bucket_if_not_exists(client, bucket_name: str) -> None:
|
|
|
201
203
|
)
|
|
202
204
|
|
|
203
205
|
|
|
204
|
-
def _create_bucket_name(prefix: str, base_name: str) -> str:
|
|
205
|
-
import re as _re
|
|
206
|
-
|
|
207
|
-
ts = time.strftime("%Y%m%d-%H%M%S", time.gmtime())
|
|
208
|
-
base = _re.sub(r"\s+", "-", base_name)
|
|
209
|
-
base = _re.sub(r"[^a-zA-Z0-9-]", "", base).lower().strip("-")
|
|
210
|
-
name = f"{prefix}-{base}-{ts}"
|
|
211
|
-
return name[:63]
|
|
212
|
-
|
|
213
|
-
|
|
214
206
|
async def _oss_put_and_presign(
|
|
215
207
|
client,
|
|
216
208
|
bucket_name: str,
|
|
@@ -232,6 +224,180 @@ async def _oss_put_and_presign(
|
|
|
232
224
|
return pre.url
|
|
233
225
|
|
|
234
226
|
|
|
227
|
+
def _upload_to_oss_with_credentials(
|
|
228
|
+
api_response,
|
|
229
|
+
file_path,
|
|
230
|
+
) -> str:
|
|
231
|
+
response_data = (
|
|
232
|
+
json.loads(api_response)
|
|
233
|
+
if isinstance(api_response, str)
|
|
234
|
+
else api_response
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
body = response_data["body"]
|
|
239
|
+
data = body.get("Data")
|
|
240
|
+
if data is None:
|
|
241
|
+
messages = [
|
|
242
|
+
"\n❌ Configuration Error: "
|
|
243
|
+
"The current RAM user is not assigned to target workspace.",
|
|
244
|
+
"Bailian requires RAM users to be associated with "
|
|
245
|
+
"at least one workspace to use temporary storage.",
|
|
246
|
+
"\n🔧 How to resolve:",
|
|
247
|
+
"1. Ask the primary account to log in to the "
|
|
248
|
+
"Bailian Console: https://bailian.console.aliyun.com",
|
|
249
|
+
"2. Go to [Permission Management]",
|
|
250
|
+
"3. Go to [Add User]",
|
|
251
|
+
"4. Assign the user to a workspace",
|
|
252
|
+
"\n💡 Note: If you are not the primary account holder,"
|
|
253
|
+
" please contact your administrator to complete this step.",
|
|
254
|
+
"=" * 80,
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
for msg in messages:
|
|
258
|
+
logger.error(msg)
|
|
259
|
+
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"RAM user is not assigned to any workspace in Bailian",
|
|
262
|
+
)
|
|
263
|
+
param = data["Param"]
|
|
264
|
+
signed_url = param["Url"]
|
|
265
|
+
headers = param["Headers"]
|
|
266
|
+
except KeyError as e:
|
|
267
|
+
raise ValueError(f"Missing expected field in API response: {e}") from e
|
|
268
|
+
try:
|
|
269
|
+
with open(file_path, "rb") as file:
|
|
270
|
+
response = requests.put(signed_url, data=file, headers=headers)
|
|
271
|
+
logger.info("OSS upload status code: %d", response.status_code)
|
|
272
|
+
response.raise_for_status() # Raises for 4xx/5xx
|
|
273
|
+
logger.info("File uploaded successfully using requests")
|
|
274
|
+
return data["TempStorageLeaseId"]
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error("Failed to upload file to OSS: %s", e)
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _get_presign_url_and_upload_to_oss(
|
|
281
|
+
cfg: ModelstudioConfig,
|
|
282
|
+
wheel_path: Path,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Request a temporary storage lease, obtain a pre-signed OSS URL, and upload the file.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
cfg: ModelStudio configuration with credentials and endpoint.
|
|
289
|
+
wheel_path: Path to the wheel file to upload.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The TempStorageLeaseId returned by the service.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
Exception: Any error from the SDK or upload process (not swallowed).
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
config = open_api_models.Config(
|
|
299
|
+
access_key_id=cfg.access_key_id,
|
|
300
|
+
access_key_secret=cfg.access_key_secret,
|
|
301
|
+
)
|
|
302
|
+
config.endpoint = cfg.endpoint
|
|
303
|
+
client_modelstudio = ModelstudioClient(config)
|
|
304
|
+
|
|
305
|
+
filename = wheel_path.name
|
|
306
|
+
size = wheel_path.stat().st_size
|
|
307
|
+
|
|
308
|
+
apply_temp_storage_lease_request = (
|
|
309
|
+
ModelstudioTypes.ApplyTempStorageLeaseRequest(
|
|
310
|
+
file_name=filename,
|
|
311
|
+
size_in_bytes=size,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
runtime = util_models.RuntimeOptions()
|
|
315
|
+
headers = {}
|
|
316
|
+
workspace_id = getattr(cfg, "workspace_id", "default")
|
|
317
|
+
try:
|
|
318
|
+
response = (
|
|
319
|
+
client_modelstudio.apply_temp_storage_lease_with_options(
|
|
320
|
+
workspace_id,
|
|
321
|
+
apply_temp_storage_lease_request,
|
|
322
|
+
headers,
|
|
323
|
+
runtime,
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
except Exception as error:
|
|
327
|
+
logger.error(
|
|
328
|
+
"Error during temporary storage lease or upload: %s",
|
|
329
|
+
error,
|
|
330
|
+
)
|
|
331
|
+
error_code = None
|
|
332
|
+
recommend_url = None
|
|
333
|
+
if hasattr(error, "code"):
|
|
334
|
+
error_code = error.code
|
|
335
|
+
if hasattr(error, "data") and isinstance(error.data, dict):
|
|
336
|
+
recommend_url = error.data.get("Recommend")
|
|
337
|
+
|
|
338
|
+
if error_code == "NoPermission":
|
|
339
|
+
messages = [
|
|
340
|
+
"\n❌ Permission Denied (NoPermission)",
|
|
341
|
+
"The current account does not have permission to apply "
|
|
342
|
+
"for temporary storage (ApplyTempStorageLease).",
|
|
343
|
+
"\n🔧 How to resolve:",
|
|
344
|
+
"1. Ask the primary account holder (or an administrator)"
|
|
345
|
+
" to grant your RAM user the following permission:",
|
|
346
|
+
" - Action: `AliyunBailianDataFullAccess`",
|
|
347
|
+
"\n2. Steps to grant permission:",
|
|
348
|
+
" - Go to Alibaba Cloud RAM Console: https://ram.console.aliyun.com/users",
|
|
349
|
+
" - Locate your RAM user",
|
|
350
|
+
" - Click 'Add Permissions' and attach a policy that includes "
|
|
351
|
+
"`AliyunBailianDataFullAccess`",
|
|
352
|
+
"\n3. For further diagnostics:",
|
|
353
|
+
]
|
|
354
|
+
official_doc_link = "https://help.aliyun.com/zh/ram/"
|
|
355
|
+
if recommend_url:
|
|
356
|
+
messages.append(
|
|
357
|
+
f" - Official troubleshooting link: {recommend_url}",
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
messages.append(
|
|
361
|
+
" - Visit the Alibaba Cloud API troubleshooting page",
|
|
362
|
+
)
|
|
363
|
+
messages.append(
|
|
364
|
+
f" - Official document link: {official_doc_link or 'N/A'}",
|
|
365
|
+
)
|
|
366
|
+
messages.append(
|
|
367
|
+
"\n💡 Note: If you are not an administrator, please "
|
|
368
|
+
"contact your cloud account administrator for assistance.",
|
|
369
|
+
)
|
|
370
|
+
messages.append("=" * 80)
|
|
371
|
+
|
|
372
|
+
# 一次性记录多行日志(每行单独一条日志,便于解析)
|
|
373
|
+
for msg in messages:
|
|
374
|
+
logger.error(msg)
|
|
375
|
+
|
|
376
|
+
logger.error("Original error details: %s", error)
|
|
377
|
+
raise
|
|
378
|
+
|
|
379
|
+
temp_storage_lease_id = _upload_to_oss_with_credentials(
|
|
380
|
+
response.to_map(),
|
|
381
|
+
wheel_path,
|
|
382
|
+
)
|
|
383
|
+
return temp_storage_lease_id
|
|
384
|
+
|
|
385
|
+
except Exception as error:
|
|
386
|
+
# Log detailed error information
|
|
387
|
+
logger.error(
|
|
388
|
+
"Error during temporary storage upload: %s",
|
|
389
|
+
error,
|
|
390
|
+
)
|
|
391
|
+
if hasattr(error, "message"):
|
|
392
|
+
logger.error("Error message: %s", error.message)
|
|
393
|
+
if hasattr(error, "data") and isinstance(error.data, dict):
|
|
394
|
+
recommend = error.data.get("Recommend")
|
|
395
|
+
if recommend:
|
|
396
|
+
logger.error("Diagnostic recommendation: %s", recommend)
|
|
397
|
+
# Re-raise the exception to avoid silent failures
|
|
398
|
+
raise
|
|
399
|
+
|
|
400
|
+
|
|
235
401
|
async def _modelstudio_deploy(
|
|
236
402
|
cfg: ModelstudioConfig,
|
|
237
403
|
file_url: str,
|
|
@@ -501,32 +667,19 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
501
667
|
agent_id: Optional[str] = None,
|
|
502
668
|
agent_desc: Optional[str] = None,
|
|
503
669
|
telemetry_enabled: bool = True,
|
|
504
|
-
) -> Tuple[str, str
|
|
505
|
-
logger.info("Uploading wheel to OSS
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
os.getenv("MODELSTUDIO_WORKSPACE_ID", str(uuid.uuid4()))
|
|
510
|
-
).lower()
|
|
511
|
-
bucket_name = (f"tmp-code-deploy-" f"{bucket_suffix}")[:63]
|
|
512
|
-
await _oss_create_bucket_if_not_exists(client, bucket_name)
|
|
513
|
-
filename = wheel_path.name
|
|
514
|
-
with wheel_path.open("rb") as f:
|
|
515
|
-
file_bytes = f.read()
|
|
516
|
-
artifact_url = await _oss_put_and_presign(
|
|
517
|
-
client,
|
|
518
|
-
bucket_name,
|
|
519
|
-
filename,
|
|
520
|
-
file_bytes,
|
|
670
|
+
) -> Tuple[str, str]:
|
|
671
|
+
logger.info("Uploading wheel to OSS")
|
|
672
|
+
temp_storage_lease_id = _get_presign_url_and_upload_to_oss(
|
|
673
|
+
self.modelstudio_config,
|
|
674
|
+
wheel_path,
|
|
521
675
|
)
|
|
522
|
-
|
|
523
676
|
logger.info("Triggering Modelstudio Full-Code deploy for %s", name)
|
|
524
677
|
deploy_identifier = await _modelstudio_deploy(
|
|
525
678
|
agent_desc=agent_desc,
|
|
526
679
|
agent_id=agent_id,
|
|
527
680
|
cfg=self.modelstudio_config,
|
|
528
|
-
file_url=
|
|
529
|
-
filename=
|
|
681
|
+
file_url=temp_storage_lease_id,
|
|
682
|
+
filename=wheel_path.name,
|
|
530
683
|
deploy_name=name,
|
|
531
684
|
telemetry_enabled=telemetry_enabled,
|
|
532
685
|
)
|
|
@@ -548,7 +701,7 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
548
701
|
if deploy_identifier
|
|
549
702
|
else ""
|
|
550
703
|
)
|
|
551
|
-
return
|
|
704
|
+
return console_url, deploy_identifier
|
|
552
705
|
|
|
553
706
|
async def deploy(
|
|
554
707
|
self,
|
|
@@ -569,6 +722,9 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
569
722
|
external_whl_path: Optional[str] = None,
|
|
570
723
|
agent_id: Optional[str] = None,
|
|
571
724
|
agent_desc: Optional[str] = None,
|
|
725
|
+
custom_endpoints: Optional[
|
|
726
|
+
List[Dict]
|
|
727
|
+
] = None, # New parameter for custom endpoints
|
|
572
728
|
**kwargs,
|
|
573
729
|
) -> Dict[str, str]:
|
|
574
730
|
"""
|
|
@@ -579,7 +735,10 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
579
735
|
"""
|
|
580
736
|
if not agent_id:
|
|
581
737
|
if not runner and not project_dir and not external_whl_path:
|
|
582
|
-
raise ValueError(
|
|
738
|
+
raise ValueError(
|
|
739
|
+
"Either runner, project_dir, "
|
|
740
|
+
"or external_whl_path must be provided.",
|
|
741
|
+
)
|
|
583
742
|
|
|
584
743
|
# convert services_config to Model body
|
|
585
744
|
if services_config and isinstance(services_config, dict):
|
|
@@ -588,6 +747,8 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
588
747
|
try:
|
|
589
748
|
if runner:
|
|
590
749
|
agent = runner._agent
|
|
750
|
+
if "agent" in kwargs:
|
|
751
|
+
kwargs.pop("agent")
|
|
591
752
|
|
|
592
753
|
# Create package project for detached deployment
|
|
593
754
|
project_dir = await LocalDeployManager.create_detached_project(
|
|
@@ -595,6 +756,7 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
595
756
|
endpoint_path=endpoint_path,
|
|
596
757
|
services_config=services_config, # type: ignore[arg-type]
|
|
597
758
|
protocol_adapters=protocol_adapters,
|
|
759
|
+
custom_endpoints=custom_endpoints, # Pass custom endpoints
|
|
598
760
|
requirements=requirements,
|
|
599
761
|
extra_packages=extra_packages,
|
|
600
762
|
**kwargs,
|
|
@@ -633,7 +795,6 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
633
795
|
telemetry_enabled=telemetry_enabled,
|
|
634
796
|
)
|
|
635
797
|
|
|
636
|
-
artifact_url = ""
|
|
637
798
|
console_url = ""
|
|
638
799
|
deploy_identifier = ""
|
|
639
800
|
if not skip_upload:
|
|
@@ -642,7 +803,6 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
642
803
|
self.oss_config.ensure_valid()
|
|
643
804
|
self.modelstudio_config.ensure_valid()
|
|
644
805
|
(
|
|
645
|
-
artifact_url,
|
|
646
806
|
console_url,
|
|
647
807
|
deploy_identifier,
|
|
648
808
|
) = await self._upload_and_deploy(
|
|
@@ -655,11 +815,12 @@ class ModelstudioDeployManager(DeployManager):
|
|
|
655
815
|
|
|
656
816
|
result: Dict[str, str] = {
|
|
657
817
|
"wheel_path": str(wheel_path),
|
|
658
|
-
"artifact_url": artifact_url,
|
|
659
818
|
"resource_name": name,
|
|
660
|
-
"workspace_id": self.modelstudio_config.workspace_id or "",
|
|
661
819
|
"url": console_url,
|
|
662
820
|
}
|
|
821
|
+
env_ws = os.environ.get("MODELSTUDIO_WORKSPACE_ID")
|
|
822
|
+
if env_ws and env_ws.strip():
|
|
823
|
+
result["workspace_id"] = env_ws.strip()
|
|
663
824
|
if deploy_identifier:
|
|
664
825
|
result["deploy_id"] = deploy_identifier
|
|
665
826
|
|
|
@@ -39,6 +39,9 @@ class RunnerImageConfig(BaseModel):
|
|
|
39
39
|
endpoint_path: str = "/process"
|
|
40
40
|
protocol_adapters: Optional[List] = None # New: protocol adapters
|
|
41
41
|
services_config: Optional[ServicesConfig] = None
|
|
42
|
+
custom_endpoints: Optional[
|
|
43
|
+
List[Dict]
|
|
44
|
+
] = None # New: custom endpoints configuration
|
|
42
45
|
|
|
43
46
|
# Docker configuration
|
|
44
47
|
base_image: str = "python:3.10-slim-bookworm"
|
|
@@ -184,6 +187,7 @@ class RunnerImageFactory:
|
|
|
184
187
|
endpoint_path=config.endpoint_path,
|
|
185
188
|
protocol_adapters=config.protocol_adapters,
|
|
186
189
|
services_config=config.services_config,
|
|
190
|
+
custom_endpoints=config.custom_endpoints,
|
|
187
191
|
),
|
|
188
192
|
dockerfile_path=dockerfile_path,
|
|
189
193
|
# caller_depth is no longer needed due to automatic
|
|
@@ -245,6 +249,9 @@ class RunnerImageFactory:
|
|
|
245
249
|
push_to_registry: bool = False,
|
|
246
250
|
services_config: Optional[ServicesConfig] = None,
|
|
247
251
|
protocol_adapters: Optional[List] = None, # New: protocol adapters
|
|
252
|
+
custom_endpoints: Optional[
|
|
253
|
+
List[Dict]
|
|
254
|
+
] = None, # New parameter for custom endpoints
|
|
248
255
|
**kwargs,
|
|
249
256
|
) -> str:
|
|
250
257
|
"""
|
|
@@ -261,6 +268,7 @@ class RunnerImageFactory:
|
|
|
261
268
|
push_to_registry: Whether to push to registry
|
|
262
269
|
services_config: Optional services config
|
|
263
270
|
protocol_adapters: Protocol adapters
|
|
271
|
+
custom_endpoints: Custom endpoints from agent app
|
|
264
272
|
**kwargs: Additional configuration options
|
|
265
273
|
|
|
266
274
|
Returns:
|
|
@@ -276,6 +284,7 @@ class RunnerImageFactory:
|
|
|
276
284
|
push_to_registry=push_to_registry,
|
|
277
285
|
protocol_adapters=protocol_adapters,
|
|
278
286
|
services_config=services_config,
|
|
287
|
+
custom_endpoints=custom_endpoints,
|
|
279
288
|
**kwargs,
|
|
280
289
|
)
|
|
281
290
|
|
|
@@ -12,7 +12,7 @@ import shutil
|
|
|
12
12
|
import tarfile
|
|
13
13
|
import tempfile
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import List, Optional, Any, Tuple
|
|
15
|
+
from typing import List, Optional, Any, Tuple, Dict
|
|
16
16
|
|
|
17
17
|
from pydantic import BaseModel
|
|
18
18
|
|
|
@@ -61,6 +61,173 @@ def _get_package_version() -> str:
|
|
|
61
61
|
return ""
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
def _prepare_custom_endpoints_for_template(
|
|
65
|
+
custom_endpoints: Optional[List[Dict]],
|
|
66
|
+
temp_dir: str,
|
|
67
|
+
) -> Tuple[Optional[List[Dict]], List[str]]:
|
|
68
|
+
"""
|
|
69
|
+
Prepare custom endpoints for template rendering.
|
|
70
|
+
Copy handler source directories to ensure all dependencies are available.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
custom_endpoints: List of custom endpoint configurations
|
|
74
|
+
temp_dir: Temporary directory where files will be copied
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Tuple of:
|
|
78
|
+
- Prepared endpoint configurations with file information
|
|
79
|
+
- List of copied directory names (for sys.path setup)
|
|
80
|
+
"""
|
|
81
|
+
if not custom_endpoints:
|
|
82
|
+
return None, []
|
|
83
|
+
|
|
84
|
+
prepared_endpoints = []
|
|
85
|
+
handler_dirs_copied = set() # Track copied directories to avoid duplicates
|
|
86
|
+
copied_dir_names = [] # Track directory names for sys.path
|
|
87
|
+
|
|
88
|
+
for endpoint in custom_endpoints:
|
|
89
|
+
prepared_endpoint = {
|
|
90
|
+
"path": endpoint.get("path", "/unknown"),
|
|
91
|
+
"methods": endpoint.get("methods", ["POST"]),
|
|
92
|
+
"module": endpoint.get("module"),
|
|
93
|
+
"function_name": endpoint.get("function_name"),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Try to get handler source file if handler is provided
|
|
97
|
+
handler = endpoint.get("handler")
|
|
98
|
+
if handler and callable(handler):
|
|
99
|
+
try:
|
|
100
|
+
# Get the source file of the handler
|
|
101
|
+
handler_file = inspect.getfile(handler)
|
|
102
|
+
handler_name = handler.__name__
|
|
103
|
+
|
|
104
|
+
# Skip if it's a built-in or from site-packages
|
|
105
|
+
if (
|
|
106
|
+
not handler_file.endswith(".py")
|
|
107
|
+
or "site-packages" in handler_file
|
|
108
|
+
):
|
|
109
|
+
raise ValueError("Handler from non-user code")
|
|
110
|
+
|
|
111
|
+
# Get the directory containing the handler file
|
|
112
|
+
handler_dir = os.path.dirname(os.path.abspath(handler_file))
|
|
113
|
+
|
|
114
|
+
# Copy the entire working directory if not already copied
|
|
115
|
+
if handler_dir not in handler_dirs_copied:
|
|
116
|
+
# Create a subdirectory name for this handler's context
|
|
117
|
+
dir_name = os.path.basename(handler_dir)
|
|
118
|
+
if not dir_name or dir_name == ".":
|
|
119
|
+
dir_name = "handler_context"
|
|
120
|
+
|
|
121
|
+
# Sanitize directory name
|
|
122
|
+
dir_name = re.sub(r"[^a-zA-Z0-9_]", "_", dir_name)
|
|
123
|
+
|
|
124
|
+
# Ensure unique directory name
|
|
125
|
+
counter = 1
|
|
126
|
+
base_dir_name = dir_name
|
|
127
|
+
dest_context_dir = os.path.join(temp_dir, dir_name)
|
|
128
|
+
while os.path.exists(dest_context_dir):
|
|
129
|
+
dir_name = f"{base_dir_name}_{counter}"
|
|
130
|
+
dest_context_dir = os.path.join(temp_dir, dir_name)
|
|
131
|
+
counter += 1
|
|
132
|
+
|
|
133
|
+
# Copy entire directory structure
|
|
134
|
+
# Exclude common non-essential directories
|
|
135
|
+
ignore_patterns = shutil.ignore_patterns(
|
|
136
|
+
"__pycache__",
|
|
137
|
+
"*.pyc",
|
|
138
|
+
"*.pyo",
|
|
139
|
+
".git",
|
|
140
|
+
".gitignore",
|
|
141
|
+
".pytest_cache",
|
|
142
|
+
".mypy_cache",
|
|
143
|
+
".tox",
|
|
144
|
+
"venv",
|
|
145
|
+
"env",
|
|
146
|
+
".venv",
|
|
147
|
+
".env",
|
|
148
|
+
"node_modules",
|
|
149
|
+
".DS_Store",
|
|
150
|
+
"*.egg-info",
|
|
151
|
+
"build",
|
|
152
|
+
"dist",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
shutil.copytree(
|
|
156
|
+
handler_dir,
|
|
157
|
+
dest_context_dir,
|
|
158
|
+
ignore=ignore_patterns,
|
|
159
|
+
dirs_exist_ok=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
handler_dirs_copied.add(handler_dir)
|
|
163
|
+
copied_dir_names.append(dir_name)
|
|
164
|
+
else:
|
|
165
|
+
# Find the existing copied directory name
|
|
166
|
+
for existing_dir in os.listdir(temp_dir):
|
|
167
|
+
existing_path = os.path.join(temp_dir, existing_dir)
|
|
168
|
+
if os.path.isdir(existing_path):
|
|
169
|
+
# Check if this is the directory we already copied
|
|
170
|
+
original_handler_basename = os.path.basename(
|
|
171
|
+
handler_dir,
|
|
172
|
+
)
|
|
173
|
+
if existing_dir.startswith(
|
|
174
|
+
re.sub(
|
|
175
|
+
r"[^a-zA-Z0-9_]",
|
|
176
|
+
"_",
|
|
177
|
+
original_handler_basename,
|
|
178
|
+
),
|
|
179
|
+
):
|
|
180
|
+
dir_name = existing_dir
|
|
181
|
+
break
|
|
182
|
+
else:
|
|
183
|
+
# Fallback if not found
|
|
184
|
+
dir_name = re.sub(
|
|
185
|
+
r"[^a-zA-Z0-9_]",
|
|
186
|
+
"_",
|
|
187
|
+
os.path.basename(handler_dir),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Calculate the module path relative to the handler directory
|
|
191
|
+
handler_file_rel = os.path.relpath(handler_file, handler_dir)
|
|
192
|
+
# Convert file path to module path
|
|
193
|
+
module_parts = os.path.splitext(handler_file_rel)[0].split(
|
|
194
|
+
os.sep,
|
|
195
|
+
)
|
|
196
|
+
if module_parts[-1] == "__init__":
|
|
197
|
+
module_parts = module_parts[:-1]
|
|
198
|
+
|
|
199
|
+
# Construct the full import path
|
|
200
|
+
if module_parts:
|
|
201
|
+
module_path = f"{dir_name}.{'.'.join(module_parts)}"
|
|
202
|
+
else:
|
|
203
|
+
module_path = dir_name
|
|
204
|
+
|
|
205
|
+
# Set the module and function name for template
|
|
206
|
+
prepared_endpoint["handler_module"] = module_path
|
|
207
|
+
prepared_endpoint["function_name"] = handler_name
|
|
208
|
+
|
|
209
|
+
except (OSError, TypeError, ValueError) as e:
|
|
210
|
+
# If source file extraction fails, try module/function_name
|
|
211
|
+
import traceback
|
|
212
|
+
|
|
213
|
+
print(f"Warning: Failed to copy handler directory: {e}")
|
|
214
|
+
traceback.print_exc()
|
|
215
|
+
|
|
216
|
+
# Add inline code if no handler module/function available
|
|
217
|
+
if not prepared_endpoint.get("handler_module") and (
|
|
218
|
+
not prepared_endpoint["module"]
|
|
219
|
+
or not prepared_endpoint["function_name"]
|
|
220
|
+
):
|
|
221
|
+
prepared_endpoint["inline_code"] = endpoint.get(
|
|
222
|
+
"inline_code",
|
|
223
|
+
'lambda request: {"error": "Handler not available"}',
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
prepared_endpoints.append(prepared_endpoint)
|
|
227
|
+
|
|
228
|
+
return prepared_endpoints, copied_dir_names
|
|
229
|
+
|
|
230
|
+
|
|
64
231
|
class PackageConfig(BaseModel):
|
|
65
232
|
"""Configuration for project packaging"""
|
|
66
233
|
|
|
@@ -73,6 +240,13 @@ class PackageConfig(BaseModel):
|
|
|
73
240
|
ServicesConfig
|
|
74
241
|
] = None # New: services configuration
|
|
75
242
|
protocol_adapters: Optional[List[Any]] = None # New: protocol adapters
|
|
243
|
+
custom_endpoints: Optional[
|
|
244
|
+
List[Dict]
|
|
245
|
+
] = None # New: custom endpoints configuration
|
|
246
|
+
# Celery configuration parameters
|
|
247
|
+
broker_url: Optional[str] = None
|
|
248
|
+
backend_url: Optional[str] = None
|
|
249
|
+
enable_embedded_worker: bool = False
|
|
76
250
|
|
|
77
251
|
|
|
78
252
|
def _find_agent_source_file(
|
|
@@ -742,6 +916,41 @@ def package_project(
|
|
|
742
916
|
f"# Protocol adapters\nprotocol_adapters = {instances_str}"
|
|
743
917
|
)
|
|
744
918
|
|
|
919
|
+
# Convert celery_config to string representation for template
|
|
920
|
+
celery_config_str = None
|
|
921
|
+
config_lines = []
|
|
922
|
+
|
|
923
|
+
# Generate celery configuration code
|
|
924
|
+
config_lines.append("# Celery configuration")
|
|
925
|
+
|
|
926
|
+
if config.broker_url:
|
|
927
|
+
config_lines.append(
|
|
928
|
+
f'celery_config["broker_url"] = "{config.broker_url}"',
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
if config.backend_url:
|
|
932
|
+
config_lines.append(
|
|
933
|
+
f'celery_config["backend_url"] = "{config.backend_url}"',
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
if config.enable_embedded_worker:
|
|
937
|
+
config_lines.append(
|
|
938
|
+
f'celery_config["enable_embedded_worker"] = '
|
|
939
|
+
f"{config.enable_embedded_worker}",
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
if config_lines:
|
|
943
|
+
celery_config_str = "\n".join(config_lines)
|
|
944
|
+
|
|
945
|
+
# Prepare custom endpoints and get copied directory names
|
|
946
|
+
(
|
|
947
|
+
custom_endpoints_data,
|
|
948
|
+
handler_dirs,
|
|
949
|
+
) = _prepare_custom_endpoints_for_template(
|
|
950
|
+
config.custom_endpoints,
|
|
951
|
+
temp_dir,
|
|
952
|
+
)
|
|
953
|
+
|
|
745
954
|
# Render template - use template file by default,
|
|
746
955
|
# or user-provided string
|
|
747
956
|
if template is None:
|
|
@@ -751,6 +960,9 @@ def package_project(
|
|
|
751
960
|
endpoint_path=config.endpoint_path or "/process",
|
|
752
961
|
deployment_mode=config.deployment_mode or "standalone",
|
|
753
962
|
protocol_adapters=protocol_adapters_str,
|
|
963
|
+
celery_config=celery_config_str,
|
|
964
|
+
custom_endpoints=custom_endpoints_data,
|
|
965
|
+
handler_dirs=handler_dirs,
|
|
754
966
|
)
|
|
755
967
|
else:
|
|
756
968
|
# Use user-provided template string
|
|
@@ -760,6 +972,9 @@ def package_project(
|
|
|
760
972
|
endpoint_path=config.endpoint_path,
|
|
761
973
|
deployment_mode=config.deployment_mode or "standalone",
|
|
762
974
|
protocol_adapters=protocol_adapters_str,
|
|
975
|
+
celery_config=celery_config_str,
|
|
976
|
+
custom_endpoints=custom_endpoints_data,
|
|
977
|
+
handler_dirs=handler_dirs,
|
|
763
978
|
)
|
|
764
979
|
|
|
765
980
|
# Write main.py
|
|
@@ -802,9 +1017,25 @@ def package_project(
|
|
|
802
1017
|
if not config.requirements:
|
|
803
1018
|
config.requirements = []
|
|
804
1019
|
|
|
805
|
-
#
|
|
1020
|
+
# Add Celery requirements if Celery is configured
|
|
1021
|
+
celery_requirements = []
|
|
1022
|
+
if (
|
|
1023
|
+
config.broker_url
|
|
1024
|
+
or config.backend_url
|
|
1025
|
+
or config.enable_embedded_worker
|
|
1026
|
+
):
|
|
1027
|
+
celery_requirements = ["celery", "redis"]
|
|
1028
|
+
|
|
1029
|
+
# Combine base requirements with user requirements and Celery
|
|
1030
|
+
# requirements
|
|
806
1031
|
all_requirements = sorted(
|
|
807
|
-
list(
|
|
1032
|
+
list(
|
|
1033
|
+
set(
|
|
1034
|
+
base_requirements
|
|
1035
|
+
+ config.requirements
|
|
1036
|
+
+ celery_requirements,
|
|
1037
|
+
),
|
|
1038
|
+
),
|
|
808
1039
|
)
|
|
809
1040
|
for req in all_requirements:
|
|
810
1041
|
f.write(f"{req}\n")
|