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.
Files changed (107) hide show
  1. agentscope_runtime/common/__init__.py +0 -0
  2. agentscope_runtime/common/collections/in_memory_mapping.py +27 -0
  3. agentscope_runtime/common/collections/redis_mapping.py +42 -0
  4. agentscope_runtime/common/container_clients/__init__.py +0 -0
  5. agentscope_runtime/common/container_clients/agentrun_client.py +1098 -0
  6. agentscope_runtime/common/container_clients/docker_client.py +250 -0
  7. agentscope_runtime/{sandbox/manager → common}/container_clients/kubernetes_client.py +6 -13
  8. agentscope_runtime/engine/__init__.py +12 -0
  9. agentscope_runtime/engine/agents/agentscope_agent.py +567 -0
  10. agentscope_runtime/engine/agents/agno_agent.py +26 -27
  11. agentscope_runtime/engine/agents/autogen_agent.py +13 -8
  12. agentscope_runtime/engine/agents/langgraph_agent.py +52 -9
  13. agentscope_runtime/engine/agents/utils.py +53 -0
  14. agentscope_runtime/engine/app/__init__.py +6 -0
  15. agentscope_runtime/engine/app/agent_app.py +239 -0
  16. agentscope_runtime/engine/app/base_app.py +181 -0
  17. agentscope_runtime/engine/app/celery_mixin.py +92 -0
  18. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +5 -1
  19. agentscope_runtime/engine/deployers/base.py +1 -0
  20. agentscope_runtime/engine/deployers/cli_fc_deploy.py +39 -20
  21. agentscope_runtime/engine/deployers/kubernetes_deployer.py +12 -5
  22. agentscope_runtime/engine/deployers/local_deployer.py +61 -3
  23. agentscope_runtime/engine/deployers/modelstudio_deployer.py +201 -40
  24. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +9 -0
  25. agentscope_runtime/engine/deployers/utils/package_project_utils.py +234 -3
  26. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +567 -7
  27. agentscope_runtime/engine/deployers/utils/service_utils/standalone_main.py.j2 +211 -0
  28. agentscope_runtime/engine/deployers/utils/wheel_packager.py +1 -1
  29. agentscope_runtime/engine/helpers/helper.py +60 -41
  30. agentscope_runtime/engine/runner.py +40 -24
  31. agentscope_runtime/engine/schemas/agent_schemas.py +42 -0
  32. agentscope_runtime/engine/schemas/modelstudio_llm.py +14 -14
  33. agentscope_runtime/engine/services/sandbox_service.py +62 -70
  34. agentscope_runtime/engine/services/tablestore_memory_service.py +307 -0
  35. agentscope_runtime/engine/services/tablestore_rag_service.py +143 -0
  36. agentscope_runtime/engine/services/tablestore_session_history_service.py +293 -0
  37. agentscope_runtime/engine/services/utils/__init__.py +0 -0
  38. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +352 -0
  39. agentscope_runtime/engine/tracing/__init__.py +9 -3
  40. agentscope_runtime/engine/tracing/asyncio_util.py +24 -0
  41. agentscope_runtime/engine/tracing/base.py +66 -34
  42. agentscope_runtime/engine/tracing/local_logging_handler.py +45 -31
  43. agentscope_runtime/engine/tracing/message_util.py +528 -0
  44. agentscope_runtime/engine/tracing/tracing_metric.py +20 -8
  45. agentscope_runtime/engine/tracing/tracing_util.py +130 -0
  46. agentscope_runtime/engine/tracing/wrapper.py +794 -169
  47. agentscope_runtime/sandbox/__init__.py +2 -0
  48. agentscope_runtime/sandbox/box/base/__init__.py +4 -0
  49. agentscope_runtime/sandbox/box/base/base_sandbox.py +6 -4
  50. agentscope_runtime/sandbox/box/browser/__init__.py +4 -0
  51. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +10 -14
  52. agentscope_runtime/sandbox/box/dummy/__init__.py +4 -0
  53. agentscope_runtime/sandbox/box/dummy/dummy_sandbox.py +2 -1
  54. agentscope_runtime/sandbox/box/filesystem/__init__.py +4 -0
  55. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +10 -7
  56. agentscope_runtime/sandbox/box/gui/__init__.py +4 -0
  57. agentscope_runtime/sandbox/box/gui/box/__init__.py +0 -0
  58. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +81 -0
  59. agentscope_runtime/sandbox/box/sandbox.py +5 -2
  60. agentscope_runtime/sandbox/box/shared/routers/generic.py +20 -1
  61. agentscope_runtime/sandbox/box/training_box/__init__.py +4 -0
  62. agentscope_runtime/sandbox/box/training_box/training_box.py +7 -54
  63. agentscope_runtime/sandbox/build.py +143 -58
  64. agentscope_runtime/sandbox/client/http_client.py +87 -59
  65. agentscope_runtime/sandbox/client/training_client.py +0 -1
  66. agentscope_runtime/sandbox/constant.py +27 -1
  67. agentscope_runtime/sandbox/custom/custom_sandbox.py +7 -6
  68. agentscope_runtime/sandbox/custom/example.py +4 -3
  69. agentscope_runtime/sandbox/enums.py +1 -1
  70. agentscope_runtime/sandbox/manager/sandbox_manager.py +212 -106
  71. agentscope_runtime/sandbox/manager/server/app.py +82 -14
  72. agentscope_runtime/sandbox/manager/server/config.py +50 -3
  73. agentscope_runtime/sandbox/model/container.py +12 -23
  74. agentscope_runtime/sandbox/model/manager_config.py +93 -5
  75. agentscope_runtime/sandbox/registry.py +1 -1
  76. agentscope_runtime/sandbox/tools/gui/__init__.py +7 -0
  77. agentscope_runtime/sandbox/tools/gui/tool.py +77 -0
  78. agentscope_runtime/sandbox/tools/mcp_tool.py +6 -2
  79. agentscope_runtime/sandbox/tools/tool.py +4 -0
  80. agentscope_runtime/sandbox/utils.py +124 -0
  81. agentscope_runtime/version.py +1 -1
  82. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/METADATA +246 -111
  83. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/RECORD +96 -80
  84. agentscope_runtime/engine/agents/agentscope_agent/__init__.py +0 -6
  85. agentscope_runtime/engine/agents/agentscope_agent/agent.py +0 -401
  86. agentscope_runtime/engine/agents/agentscope_agent/hooks.py +0 -169
  87. agentscope_runtime/engine/agents/llm_agent.py +0 -51
  88. agentscope_runtime/engine/llms/__init__.py +0 -3
  89. agentscope_runtime/engine/llms/base_llm.py +0 -60
  90. agentscope_runtime/engine/llms/qwen_llm.py +0 -47
  91. agentscope_runtime/sandbox/manager/collections/in_memory_mapping.py +0 -22
  92. agentscope_runtime/sandbox/manager/collections/redis_mapping.py +0 -26
  93. agentscope_runtime/sandbox/manager/container_clients/__init__.py +0 -10
  94. agentscope_runtime/sandbox/manager/container_clients/docker_client.py +0 -422
  95. /agentscope_runtime/{sandbox/manager → common}/collections/__init__.py +0 -0
  96. /agentscope_runtime/{sandbox/manager → common}/collections/base_mapping.py +0 -0
  97. /agentscope_runtime/{sandbox/manager → common}/collections/base_queue.py +0 -0
  98. /agentscope_runtime/{sandbox/manager → common}/collections/base_set.py +0 -0
  99. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_queue.py +0 -0
  100. /agentscope_runtime/{sandbox/manager → common}/collections/in_memory_set.py +0 -0
  101. /agentscope_runtime/{sandbox/manager → common}/collections/redis_queue.py +0 -0
  102. /agentscope_runtime/{sandbox/manager → common}/collections/redis_set.py +0 -0
  103. /agentscope_runtime/{sandbox/manager → common}/container_clients/base_client.py +0 -0
  104. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/WHEEL +0 -0
  105. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/entry_points.txt +0 -0
  106. {agentscope_runtime-0.1.5b2.dist-info → agentscope_runtime-0.2.0.dist-info}/licenses/LICENSE +0 -0
  107. {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 uuid
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=os.environ.get("MODELSTUDIO_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, str]:
505
- logger.info("Uploading wheel to OSS and generating presigned URL")
506
- client = _oss_get_client(self.oss_config)
507
-
508
- bucket_suffix = (
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=artifact_url,
529
- filename=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 artifact_url, console_url, deploy_identifier
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
- # Combine base requirements with user requirements
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(set(base_requirements + config.requirements)),
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")