agentscope-runtime 0.2.0b1__py3-none-any.whl → 1.0.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 (185) hide show
  1. agentscope_runtime/adapters/__init__.py +0 -0
  2. agentscope_runtime/adapters/agentscope/__init__.py +0 -0
  3. agentscope_runtime/adapters/agentscope/long_term_memory/__init__.py +6 -0
  4. agentscope_runtime/adapters/agentscope/long_term_memory/_long_term_memory_adapter.py +258 -0
  5. agentscope_runtime/adapters/agentscope/memory/__init__.py +6 -0
  6. agentscope_runtime/adapters/agentscope/memory/_memory_adapter.py +152 -0
  7. agentscope_runtime/adapters/agentscope/message.py +535 -0
  8. agentscope_runtime/adapters/agentscope/stream.py +506 -0
  9. agentscope_runtime/adapters/agentscope/tool/__init__.py +9 -0
  10. agentscope_runtime/adapters/agentscope/tool/sandbox_tool.py +69 -0
  11. agentscope_runtime/adapters/agentscope/tool/tool.py +233 -0
  12. agentscope_runtime/adapters/autogen/__init__.py +0 -0
  13. agentscope_runtime/adapters/autogen/tool/__init__.py +7 -0
  14. agentscope_runtime/adapters/autogen/tool/tool.py +211 -0
  15. agentscope_runtime/adapters/text/__init__.py +0 -0
  16. agentscope_runtime/adapters/text/stream.py +29 -0
  17. agentscope_runtime/common/collections/redis_mapping.py +4 -1
  18. agentscope_runtime/common/container_clients/fc_client.py +855 -0
  19. agentscope_runtime/common/container_clients/kubernetes_client.py +6 -13
  20. agentscope_runtime/common/utils/__init__.py +0 -0
  21. agentscope_runtime/common/utils/lazy_loader.py +57 -0
  22. agentscope_runtime/engine/__init__.py +25 -18
  23. agentscope_runtime/engine/app/agent_app.py +161 -91
  24. agentscope_runtime/engine/app/base_app.py +4 -118
  25. agentscope_runtime/engine/constant.py +8 -0
  26. agentscope_runtime/engine/deployers/__init__.py +8 -0
  27. agentscope_runtime/engine/deployers/adapter/__init__.py +2 -0
  28. agentscope_runtime/engine/deployers/adapter/a2a/a2a_adapter_utils.py +0 -21
  29. agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +28 -9
  30. agentscope_runtime/engine/deployers/adapter/responses/__init__.py +2 -0
  31. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +5 -2
  32. agentscope_runtime/engine/deployers/adapter/responses/response_api_protocol_adapter.py +1 -1
  33. agentscope_runtime/engine/deployers/agentrun_deployer.py +2541 -0
  34. agentscope_runtime/engine/deployers/cli_fc_deploy.py +1 -1
  35. agentscope_runtime/engine/deployers/kubernetes_deployer.py +9 -21
  36. agentscope_runtime/engine/deployers/local_deployer.py +47 -74
  37. agentscope_runtime/engine/deployers/modelstudio_deployer.py +216 -50
  38. agentscope_runtime/engine/deployers/utils/app_runner_utils.py +29 -0
  39. agentscope_runtime/engine/deployers/utils/detached_app.py +510 -0
  40. agentscope_runtime/engine/deployers/utils/docker_image_utils/__init__.py +1 -1
  41. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +1 -1
  42. agentscope_runtime/engine/deployers/utils/docker_image_utils/{runner_image_factory.py → image_factory.py} +121 -61
  43. agentscope_runtime/engine/deployers/utils/package.py +693 -0
  44. agentscope_runtime/engine/deployers/utils/service_utils/__init__.py +0 -5
  45. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +301 -282
  46. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_templates.py +2 -4
  47. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +23 -1
  48. agentscope_runtime/engine/deployers/utils/templates/app_main.py.j2 +84 -0
  49. agentscope_runtime/engine/deployers/utils/templates/runner_main.py.j2 +95 -0
  50. agentscope_runtime/engine/deployers/utils/{service_utils → templates}/standalone_main.py.j2 +0 -45
  51. agentscope_runtime/engine/deployers/utils/wheel_packager.py +119 -18
  52. agentscope_runtime/engine/helpers/runner.py +40 -0
  53. agentscope_runtime/engine/runner.py +171 -130
  54. agentscope_runtime/engine/schemas/agent_schemas.py +114 -3
  55. agentscope_runtime/engine/schemas/modelstudio_llm.py +4 -2
  56. agentscope_runtime/engine/schemas/oai_llm.py +23 -23
  57. agentscope_runtime/engine/schemas/response_api.py +65 -0
  58. agentscope_runtime/engine/schemas/session.py +24 -0
  59. agentscope_runtime/engine/services/__init__.py +0 -9
  60. agentscope_runtime/engine/services/agent_state/__init__.py +16 -0
  61. agentscope_runtime/engine/services/agent_state/redis_state_service.py +113 -0
  62. agentscope_runtime/engine/services/agent_state/state_service.py +179 -0
  63. agentscope_runtime/engine/services/memory/__init__.py +24 -0
  64. agentscope_runtime/engine/services/{mem0_memory_service.py → memory/mem0_memory_service.py} +17 -13
  65. agentscope_runtime/engine/services/{memory_service.py → memory/memory_service.py} +28 -7
  66. agentscope_runtime/engine/services/{redis_memory_service.py → memory/redis_memory_service.py} +1 -1
  67. agentscope_runtime/engine/services/{reme_personal_memory_service.py → memory/reme_personal_memory_service.py} +9 -6
  68. agentscope_runtime/engine/services/{reme_task_memory_service.py → memory/reme_task_memory_service.py} +2 -2
  69. agentscope_runtime/engine/services/{tablestore_memory_service.py → memory/tablestore_memory_service.py} +16 -19
  70. agentscope_runtime/engine/services/sandbox/__init__.py +13 -0
  71. agentscope_runtime/engine/services/{sandbox_service.py → sandbox/sandbox_service.py} +86 -71
  72. agentscope_runtime/engine/services/session_history/__init__.py +23 -0
  73. agentscope_runtime/engine/services/{redis_session_history_service.py → session_history/redis_session_history_service.py} +3 -2
  74. agentscope_runtime/engine/services/{session_history_service.py → session_history/session_history_service.py} +44 -34
  75. agentscope_runtime/engine/services/{tablestore_session_history_service.py → session_history/tablestore_session_history_service.py} +14 -19
  76. agentscope_runtime/engine/services/utils/tablestore_service_utils.py +2 -2
  77. agentscope_runtime/engine/tracing/base.py +10 -9
  78. agentscope_runtime/engine/tracing/message_util.py +1 -1
  79. agentscope_runtime/engine/tracing/tracing_util.py +7 -2
  80. agentscope_runtime/engine/tracing/wrapper.py +49 -31
  81. agentscope_runtime/sandbox/__init__.py +10 -2
  82. agentscope_runtime/sandbox/box/agentbay/__init__.py +4 -0
  83. agentscope_runtime/sandbox/box/agentbay/agentbay_sandbox.py +559 -0
  84. agentscope_runtime/sandbox/box/base/base_sandbox.py +12 -0
  85. agentscope_runtime/sandbox/box/browser/browser_sandbox.py +115 -11
  86. agentscope_runtime/sandbox/box/cloud/__init__.py +4 -0
  87. agentscope_runtime/sandbox/box/cloud/cloud_sandbox.py +254 -0
  88. agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +66 -0
  89. agentscope_runtime/sandbox/box/gui/gui_sandbox.py +42 -0
  90. agentscope_runtime/sandbox/box/mobile/__init__.py +4 -0
  91. agentscope_runtime/sandbox/box/mobile/box/__init__.py +0 -0
  92. agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +216 -0
  93. agentscope_runtime/sandbox/box/training_box/training_box.py +2 -44
  94. agentscope_runtime/sandbox/client/http_client.py +1 -0
  95. agentscope_runtime/sandbox/enums.py +2 -1
  96. agentscope_runtime/sandbox/manager/sandbox_manager.py +15 -2
  97. agentscope_runtime/sandbox/manager/server/app.py +12 -0
  98. agentscope_runtime/sandbox/manager/server/config.py +19 -0
  99. agentscope_runtime/sandbox/model/manager_config.py +79 -2
  100. agentscope_runtime/sandbox/utils.py +0 -18
  101. agentscope_runtime/tools/RAGs/__init__.py +0 -0
  102. agentscope_runtime/tools/RAGs/modelstudio_rag.py +377 -0
  103. agentscope_runtime/tools/RAGs/modelstudio_rag_lite.py +219 -0
  104. agentscope_runtime/tools/__init__.py +119 -0
  105. agentscope_runtime/tools/_constants.py +18 -0
  106. agentscope_runtime/tools/alipay/__init__.py +4 -0
  107. agentscope_runtime/tools/alipay/base.py +334 -0
  108. agentscope_runtime/tools/alipay/payment.py +835 -0
  109. agentscope_runtime/tools/alipay/subscribe.py +551 -0
  110. agentscope_runtime/tools/base.py +264 -0
  111. agentscope_runtime/tools/cli/__init__.py +0 -0
  112. agentscope_runtime/tools/cli/modelstudio_mcp_server.py +78 -0
  113. agentscope_runtime/tools/generations/__init__.py +75 -0
  114. agentscope_runtime/tools/generations/async_image_to_video.py +350 -0
  115. agentscope_runtime/tools/generations/async_image_to_video_wan25.py +366 -0
  116. agentscope_runtime/tools/generations/async_speech_to_video.py +422 -0
  117. agentscope_runtime/tools/generations/async_text_to_video.py +320 -0
  118. agentscope_runtime/tools/generations/async_text_to_video_wan25.py +334 -0
  119. agentscope_runtime/tools/generations/image_edit.py +208 -0
  120. agentscope_runtime/tools/generations/image_edit_wan25.py +193 -0
  121. agentscope_runtime/tools/generations/image_generation.py +202 -0
  122. agentscope_runtime/tools/generations/image_generation_wan25.py +201 -0
  123. agentscope_runtime/tools/generations/image_style_repaint.py +208 -0
  124. agentscope_runtime/tools/generations/image_to_video.py +233 -0
  125. agentscope_runtime/tools/generations/qwen_image_edit.py +205 -0
  126. agentscope_runtime/tools/generations/qwen_image_generation.py +214 -0
  127. agentscope_runtime/tools/generations/qwen_text_to_speech.py +154 -0
  128. agentscope_runtime/tools/generations/speech_to_text.py +260 -0
  129. agentscope_runtime/tools/generations/speech_to_video.py +314 -0
  130. agentscope_runtime/tools/generations/text_to_video.py +221 -0
  131. agentscope_runtime/tools/mcp_wrapper.py +215 -0
  132. agentscope_runtime/tools/realtime_clients/__init__.py +13 -0
  133. agentscope_runtime/tools/realtime_clients/asr_client.py +27 -0
  134. agentscope_runtime/tools/realtime_clients/azure_asr_client.py +195 -0
  135. agentscope_runtime/tools/realtime_clients/azure_tts_client.py +383 -0
  136. agentscope_runtime/tools/realtime_clients/modelstudio_asr_client.py +151 -0
  137. agentscope_runtime/tools/realtime_clients/modelstudio_tts_client.py +199 -0
  138. agentscope_runtime/tools/realtime_clients/realtime_tool.py +55 -0
  139. agentscope_runtime/tools/realtime_clients/tts_client.py +33 -0
  140. agentscope_runtime/tools/searches/__init__.py +3 -0
  141. agentscope_runtime/tools/searches/modelstudio_search.py +877 -0
  142. agentscope_runtime/tools/searches/modelstudio_search_lite.py +310 -0
  143. agentscope_runtime/tools/utils/__init__.py +0 -0
  144. agentscope_runtime/tools/utils/api_key_util.py +45 -0
  145. agentscope_runtime/tools/utils/crypto_utils.py +99 -0
  146. agentscope_runtime/tools/utils/mcp_util.py +35 -0
  147. agentscope_runtime/version.py +1 -1
  148. {agentscope_runtime-0.2.0b1.dist-info → agentscope_runtime-1.0.0.dist-info}/METADATA +244 -168
  149. agentscope_runtime-1.0.0.dist-info/RECORD +240 -0
  150. {agentscope_runtime-0.2.0b1.dist-info → agentscope_runtime-1.0.0.dist-info}/entry_points.txt +1 -0
  151. agentscope_runtime/engine/agents/__init__.py +0 -2
  152. agentscope_runtime/engine/agents/agentscope_agent.py +0 -488
  153. agentscope_runtime/engine/agents/agno_agent.py +0 -222
  154. agentscope_runtime/engine/agents/autogen_agent.py +0 -250
  155. agentscope_runtime/engine/agents/base_agent.py +0 -29
  156. agentscope_runtime/engine/agents/langgraph_agent.py +0 -59
  157. agentscope_runtime/engine/agents/utils.py +0 -53
  158. agentscope_runtime/engine/deployers/utils/package_project_utils.py +0 -1163
  159. agentscope_runtime/engine/deployers/utils/service_utils/service_config.py +0 -75
  160. agentscope_runtime/engine/deployers/utils/service_utils/service_factory.py +0 -220
  161. agentscope_runtime/engine/helpers/helper.py +0 -179
  162. agentscope_runtime/engine/schemas/context.py +0 -54
  163. agentscope_runtime/engine/services/context_manager.py +0 -164
  164. agentscope_runtime/engine/services/environment_manager.py +0 -50
  165. agentscope_runtime/engine/services/manager.py +0 -174
  166. agentscope_runtime/engine/services/rag_service.py +0 -195
  167. agentscope_runtime/engine/services/tablestore_rag_service.py +0 -143
  168. agentscope_runtime/sandbox/tools/__init__.py +0 -12
  169. agentscope_runtime/sandbox/tools/base/__init__.py +0 -8
  170. agentscope_runtime/sandbox/tools/base/tool.py +0 -52
  171. agentscope_runtime/sandbox/tools/browser/__init__.py +0 -57
  172. agentscope_runtime/sandbox/tools/browser/tool.py +0 -597
  173. agentscope_runtime/sandbox/tools/filesystem/__init__.py +0 -32
  174. agentscope_runtime/sandbox/tools/filesystem/tool.py +0 -319
  175. agentscope_runtime/sandbox/tools/function_tool.py +0 -321
  176. agentscope_runtime/sandbox/tools/gui/__init__.py +0 -7
  177. agentscope_runtime/sandbox/tools/gui/tool.py +0 -77
  178. agentscope_runtime/sandbox/tools/mcp_tool.py +0 -195
  179. agentscope_runtime/sandbox/tools/sandbox_tool.py +0 -104
  180. agentscope_runtime/sandbox/tools/tool.py +0 -238
  181. agentscope_runtime/sandbox/tools/utils.py +0 -68
  182. agentscope_runtime-0.2.0b1.dist-info/RECORD +0 -183
  183. {agentscope_runtime-0.2.0b1.dist-info → agentscope_runtime-1.0.0.dist-info}/WHEEL +0 -0
  184. {agentscope_runtime-0.2.0b1.dist-info → agentscope_runtime-1.0.0.dist-info}/licenses/LICENSE +0 -0
  185. {agentscope_runtime-0.2.0b1.dist-info → agentscope_runtime-1.0.0.dist-info}/top_level.txt +0 -0
@@ -1,1163 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # pylint:disable=too-many-boolean-expressions, too-many-nested-blocks
3
- # pylint:disable=too-many-return-statements, unused-variable
4
- # pylint:disable=cell-var-from-loop, too-many-branches, too-many-statements
5
-
6
- import ast
7
- import hashlib
8
- import inspect
9
- import os
10
- import re
11
- import shutil
12
- import tarfile
13
- import tempfile
14
- from pathlib import Path
15
- from typing import List, Optional, Any, Tuple, Dict
16
-
17
- from pydantic import BaseModel
18
-
19
- try:
20
- import tomllib # Python 3.11+
21
- except ImportError:
22
- try:
23
- import tomli as tomllib # type: ignore[no-redef]
24
- except ImportError:
25
- tomllib = None
26
-
27
- from .service_utils.fastapi_templates import FastAPITemplateManager
28
- from .service_utils.service_config import ServicesConfig
29
-
30
- # Default template will be loaded from template file
31
-
32
-
33
- def _get_package_version() -> str:
34
- """
35
- Get the package version from pyproject.toml file.
36
-
37
- Returns:
38
- str: The version string, or empty string if not found
39
- """
40
- # Try to find pyproject.toml in the current directory and parent
41
- # directories
42
- current_dir = Path(__file__).parent
43
- for _ in range(6): # Look up to 6 levels up
44
- pyproject_path = current_dir / "pyproject.toml"
45
- if pyproject_path.exists():
46
- break
47
- current_dir = current_dir.parent
48
- else:
49
- # Also try the current working directory
50
- pyproject_path = Path(os.getcwd()) / "pyproject.toml"
51
- if not pyproject_path.exists():
52
- return ""
53
-
54
- try:
55
- # Use tomllib to parse
56
- with open(pyproject_path, "rb") as f:
57
- data = tomllib.load(f)
58
- project = data.get("project", {})
59
- return project.get("version", "")
60
- except Exception:
61
- return ""
62
-
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
-
231
- class PackageConfig(BaseModel):
232
- """Configuration for project packaging"""
233
-
234
- requirements: Optional[List[str]] = None
235
- extra_packages: Optional[List[str]] = None
236
- output_dir: Optional[str] = None
237
- endpoint_path: Optional[str] = "/process"
238
- deployment_mode: Optional[str] = "standalone" # New: deployment mode
239
- services_config: Optional[
240
- ServicesConfig
241
- ] = None # New: services configuration
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
250
-
251
-
252
- def _find_agent_source_file(
253
- agent_obj: Any,
254
- agent_name: str,
255
- caller_frame,
256
- ) -> str:
257
- """
258
- Find the file that contains the agent instance definition (where the
259
- agent variable is assigned).
260
- This prioritizes finding where the agent instance was created rather
261
- than where the class is defined.
262
- """
263
-
264
- # Method 1: Search through the call stack to find where the agent
265
- # instance was defined
266
- frame = caller_frame
267
- found_files = [] # Store potential files for analysis
268
- agent_names_in_frames = [] # Store agent names found in each frame
269
-
270
- while frame:
271
- try:
272
- frame_filename = frame.f_code.co_filename
273
-
274
- # Skip internal/system files and focus on user code
275
- if (
276
- not frame_filename.endswith(".py")
277
- or "site-packages" in frame_filename
278
- ):
279
- frame = frame.f_back
280
- continue
281
-
282
- # Check if this frame contains our agent variable
283
- frame_locals = frame.f_locals
284
- frame_globals = frame.f_globals
285
-
286
- # Look for the agent object (by identity, not name) in both
287
- # locals and globals
288
- found_agent_name = None
289
- for var_name, var_value in frame_locals.items():
290
- if var_value is agent_obj:
291
- found_agent_name = var_name
292
- break
293
-
294
- if not found_agent_name:
295
- for var_name, var_value in frame_globals.items():
296
- if var_value is agent_obj:
297
- found_agent_name = var_name
298
- break
299
-
300
- if found_agent_name:
301
- # Found the frame where this agent instance exists
302
- found_files.append(frame_filename)
303
- agent_names_in_frames.append(found_agent_name)
304
-
305
- except (AttributeError, TypeError):
306
- # Handle any errors in frame inspection
307
- pass
308
-
309
- frame = frame.f_back
310
-
311
- # Method 2: Analyze found files to determine which one contains the
312
- # actual instance definition
313
- # Reverse the order to prioritize files found later in the stack (
314
- # typically user code)
315
- for i, file_path in enumerate(reversed(found_files)):
316
- # Get the corresponding agent name for this file
317
- agent_name_in_file = agent_names_in_frames[len(found_files) - 1 - i]
318
-
319
- try:
320
- with open(file_path, "r", encoding="utf-8") as f:
321
- content = f.read()
322
-
323
- # Check if this file contains an import statement for the agent
324
- # If so, we should look for the original source file
325
- import_patterns = [
326
- rf"^[^\#]*from\s+(\w+)\s+import\s+.*"
327
- rf"{re.escape(agent_name_in_file)}",
328
- rf"^[^\#]*from\s+([\w.]+)\s+import\s+.*"
329
- rf"{re.escape(agent_name_in_file)}",
330
- ]
331
-
332
- # Check if this file imports the agent from another module
333
- lines = content.split("\n")
334
- for line in lines:
335
- for import_pattern in import_patterns:
336
- match = re.search(import_pattern, line)
337
- if match:
338
- module_name = match.group(1)
339
- # Try to find the source module file
340
- current_dir = os.path.dirname(file_path)
341
-
342
- # Convert dotted module name to filesystem path
343
- module_path = module_name.replace(".", os.sep)
344
-
345
- # Try different possible paths for the source module
346
- possible_paths = [
347
- # Same directory - simple module name
348
- os.path.join(
349
- current_dir,
350
- f"{module_name}.py",
351
- ),
352
- # Same directory - dotted path
353
- os.path.join(
354
- current_dir,
355
- f"{module_path}.py",
356
- ),
357
- # Package in same directory
358
- os.path.join(
359
- current_dir,
360
- module_name,
361
- "__init__.py",
362
- ),
363
- # Package with dotted path
364
- os.path.join(
365
- current_dir,
366
- module_path,
367
- "__init__.py",
368
- ),
369
- # Parent directory - simple module name
370
- os.path.join(
371
- os.path.dirname(current_dir),
372
- f"{module_name}.py",
373
- ),
374
- # Parent directory - dotted path
375
- os.path.join(
376
- os.path.dirname(current_dir),
377
- f"{module_path}.py",
378
- ),
379
- # Current working directory - simple module name
380
- os.path.join(
381
- os.getcwd(),
382
- f"{module_name}.py",
383
- ),
384
- # Current working directory - dotted path
385
- os.path.join(
386
- os.getcwd(),
387
- f"{module_path}.py",
388
- ),
389
- # Current working directory - package
390
- os.path.join(
391
- os.getcwd(),
392
- module_path,
393
- "__init__.py",
394
- ),
395
- ]
396
-
397
- for source_path in possible_paths:
398
- if os.path.exists(source_path):
399
- # Check if this source file contains the
400
- # actual assignment
401
- try:
402
- with open(
403
- source_path,
404
- "r",
405
- encoding="utf-8",
406
- ) as src_f:
407
- src_content = src_f.read()
408
-
409
- # Look for the assignment in the source
410
- # file
411
- assignment_patterns = [
412
- rf"^[^\#]*{re.escape(agent_name_in_file)}" # noqa E501
413
- rf"\s*=\s*\w+\(",
414
- rf"^[^\#]*{re.escape(agent_name_in_file)}" # noqa E501
415
- rf"\s*=\s*[\w.]+\(",
416
- ]
417
-
418
- src_lines = src_content.split("\n")
419
- for src_line in src_lines:
420
- if (
421
- not src_line.strip()
422
- or src_line.strip().startswith("#")
423
- or src_line.strip().startswith(
424
- "def ",
425
- )
426
- or src_line.strip().startswith(
427
- "from ",
428
- )
429
- or src_line.strip().startswith(
430
- "import ",
431
- )
432
- or src_line.strip().startswith(
433
- "class ",
434
- )
435
- ):
436
- continue
437
-
438
- for (
439
- assign_pattern
440
- ) in assignment_patterns:
441
- if re.search(
442
- assign_pattern,
443
- src_line,
444
- ):
445
- if "=" in src_line:
446
- left_side = src_line.split(
447
- "=",
448
- )[0]
449
- if (
450
- agent_name_in_file
451
- in left_side
452
- and "("
453
- not in left_side
454
- ):
455
- indent_level = len(
456
- src_line,
457
- ) - len(
458
- src_line.lstrip(),
459
- )
460
- if indent_level <= 4:
461
- return source_path
462
-
463
- except (OSError, UnicodeDecodeError):
464
- continue
465
- break # Found import, no need to check other patterns
466
-
467
- # If no import found, check if this file itself contains the
468
- # assignment
469
- assignment_patterns = [
470
- # direct assignment: agent_name = ClassName(
471
- rf"^[^\#]*{re.escape(agent_name_in_file)}\s*=\s*\w+\(",
472
- # module assignment: agent_name = module.ClassName(
473
- rf"^[^\#]*{re.escape(agent_name_in_file)}\s*=\s*[\w.]+\(",
474
- ]
475
-
476
- # Look for actual variable assignment (not function parameters
477
- # or imports)
478
- for line_num, line in enumerate(lines):
479
- stripped_line = line.strip()
480
- # Skip comments, empty lines, function definitions, and imports
481
- if (
482
- not stripped_line
483
- or stripped_line.startswith("#")
484
- or stripped_line.startswith("def ")
485
- or stripped_line.startswith("from ")
486
- or stripped_line.startswith("import ")
487
- or stripped_line.startswith("class ")
488
- ):
489
- continue
490
-
491
- # Check if this line contains the agent assignment
492
- for pattern in assignment_patterns:
493
- if re.search(pattern, line):
494
- # Double check that this is a real assignment,
495
- # not inside function parameters by checking if the
496
- # line has '=' and the agent_name is on the left side
497
- if "=" in line:
498
- left_side = line.split("=")[0]
499
- if (
500
- agent_name_in_file in left_side
501
- and "(" not in left_side
502
- ):
503
- # Additional context check: make sure this
504
- # is not indented too much (likely inside a
505
- # function if heavily indented)
506
- indent_level = len(line) - len(line.lstrip())
507
- if (
508
- indent_level <= 4
509
- ): # Top level or minimal indentation
510
- return file_path
511
-
512
- except (OSError, UnicodeDecodeError):
513
- # If we can't read the file, continue to next file
514
- continue
515
-
516
- # Method 3: If no assignment pattern found, return the first found file
517
- if found_files:
518
- return found_files[0]
519
-
520
- # Method 4: Fall back to original caller-based approach if stack search
521
- # fails
522
- caller_filename = caller_frame.f_code.co_filename
523
- caller_dir = os.path.dirname(caller_filename)
524
-
525
- # Check if we have the import information in the caller's globals
526
- # Look for module objects that might contain the agent
527
- for var_name, var_obj in caller_frame.f_globals.items():
528
- if hasattr(var_obj, "__file__") and hasattr(var_obj, agent_name):
529
- # This looks like a module that contains our agent
530
- if getattr(var_obj, agent_name, None) is caller_frame.f_locals.get(
531
- agent_name,
532
- ):
533
- return var_obj.__file__
534
-
535
- # If direct lookup failed, try to parse the import statements
536
- try:
537
- with open(caller_filename, "r", encoding="utf-8") as f:
538
- content = f.read()
539
-
540
- tree = ast.parse(content)
541
-
542
- for node in ast.walk(tree):
543
- if isinstance(node, ast.ImportFrom):
544
- # Look for "from module_name import agent_name"
545
- if node.names and node.module:
546
- for alias in node.names:
547
- imported_name = (
548
- alias.asname if alias.asname else alias.name
549
- )
550
- if imported_name == agent_name:
551
- # Found the import statement
552
- module_path = os.path.join(
553
- caller_dir,
554
- f"{node.module}.py",
555
- )
556
- if os.path.exists(module_path):
557
- return module_path
558
- # Try relative import
559
- if node.level > 0: # relative import
560
- parent_path = caller_dir
561
- for _ in range(node.level - 1):
562
- parent_path = os.path.dirname(parent_path)
563
- module_path = os.path.join(
564
- parent_path,
565
- f"{node.module}.py",
566
- )
567
- if os.path.exists(module_path):
568
- return module_path
569
-
570
- elif isinstance(node, ast.Import):
571
- # Look for "import module_name" where agent might be
572
- # module_name.agent_name
573
- for alias in node.names:
574
- module_name = alias.asname if alias.asname else alias.name
575
- if module_name in caller_frame.f_globals:
576
- module_obj = caller_frame.f_globals[module_name]
577
- if hasattr(module_obj, "__file__") and hasattr(
578
- module_obj,
579
- agent_name,
580
- ):
581
- return module_obj.__file__
582
-
583
- except Exception as e:
584
- # If parsing fails, we'll fall back to the caller file
585
- print(e)
586
-
587
- return caller_filename
588
-
589
-
590
- def _extract_agent_name_from_source(
591
- agent_file_path: str,
592
- agent_obj: Any,
593
- ) -> str:
594
- """
595
- Extract the actual variable name of the agent from the source file by
596
- looking for variable assignments and trying to match the object type.
597
-
598
- Args:
599
- agent_file_path: Path to the source file containing agent definition
600
- agent_obj: The agent object to match
601
-
602
- Returns:
603
- str: The variable name used in the source file, or "agent" as fallback
604
- """
605
- try:
606
- with open(agent_file_path, "r", encoding="utf-8") as f:
607
- content = f.read()
608
-
609
- # Get the class name of the agent object
610
- agent_class_name = agent_obj.__class__.__name__
611
-
612
- lines = content.split("\n")
613
- potential_names = []
614
-
615
- for line in lines:
616
- stripped_line = line.strip()
617
- # Skip comments, empty lines, function definitions, and imports
618
- if (
619
- not stripped_line
620
- or stripped_line.startswith("#")
621
- or stripped_line.startswith("def ")
622
- or stripped_line.startswith("from ")
623
- or stripped_line.startswith("import ")
624
- or stripped_line.startswith("class ")
625
- ):
626
- continue
627
-
628
- # Look for variable assignment patterns: var_name = ...
629
- if "=" in line:
630
- left_side = line.split("=")[0].strip()
631
- right_side = line.split("=", 1)[1].strip()
632
-
633
- # Make sure it's a simple variable assignment (not inside
634
- # parentheses or functions)
635
- if (
636
- left_side
637
- and "(" not in left_side
638
- and left_side.isidentifier()
639
- and not left_side.startswith("_")
640
- ): # Skip private variables
641
- # Check indentation level - should be top level or
642
- # minimal indentation
643
- indent_level = len(line) - len(line.lstrip())
644
- if indent_level <= 4: # Top level or minimal indentation
645
- # Check if the right side contains the agent class name
646
- if agent_class_name in right_side:
647
- # This is likely our agent assignment
648
- potential_names.insert(0, left_side)
649
- # # Also check for assignments that might create the
650
- # agent through constructor calls
651
- # elif "(" in right_side:
652
- # potential_names.append(left_side)
653
-
654
- # Return the first potential name found (prioritizing class name
655
- # matches)
656
- if potential_names:
657
- return potential_names[0]
658
-
659
- except (OSError, UnicodeDecodeError):
660
- pass
661
-
662
- return "agent" # fallback
663
-
664
-
665
- def _calculate_directory_hash(directory_path: str) -> str:
666
- """
667
- Calculate a hash representing the entire contents of a directory.
668
-
669
- Args:
670
- directory_path: Path to the directory to hash
671
-
672
- Returns:
673
- str: SHA256 hash of the directory contents
674
- """
675
- hasher = hashlib.sha256()
676
-
677
- if not os.path.exists(directory_path):
678
- return ""
679
-
680
- # Walk through directory and hash all file contents and paths
681
- for root, dirs, files in sorted(os.walk(directory_path)):
682
- # Sort to ensure consistent ordering
683
- dirs.sort()
684
- files.sort()
685
-
686
- for filename in files:
687
- file_path = os.path.join(root, filename)
688
-
689
- # Hash the relative path
690
- rel_path = os.path.relpath(file_path, directory_path)
691
- hasher.update(rel_path.encode("utf-8"))
692
-
693
- # Hash the file contents
694
- try:
695
- with open(file_path, "rb") as f:
696
- for chunk in iter(lambda: f.read(4096), b""):
697
- hasher.update(chunk)
698
- except (OSError, IOError):
699
- # Skip files that can't be read
700
- continue
701
-
702
- return hasher.hexdigest()
703
-
704
-
705
- def _compare_directories(old_dir: str, new_dir: str) -> bool:
706
- """
707
- Compare two directories to see if their contents are identical.
708
-
709
- Args:
710
- old_dir: Path to the old directory
711
- new_dir: Path to the new directory
712
-
713
- Returns:
714
- bool: True if directories have identical contents, False otherwise
715
- """
716
- old_hash = _calculate_directory_hash(old_dir)
717
- new_hash = _calculate_directory_hash(new_dir)
718
-
719
- return old_hash == new_hash and old_hash != ""
720
-
721
-
722
- def package_project(
723
- agent: Any,
724
- config: PackageConfig,
725
- dockerfile_path: Optional[str] = None,
726
- template: Optional[str] = None, # Use template file by default
727
- ) -> Tuple[str, bool]:
728
- """
729
- Package a project with agent and dependencies into a temporary directory.
730
-
731
- Args:
732
- agent: The agent object to be packaged
733
- config: The configuration of the package
734
- dockerfile_path: Path to the Docker file
735
- template: User override template string
736
- (if None, uses standalone template file)
737
-
738
- Returns:
739
- Tuple[str, bool]: A tuple containing:
740
- - str: Path to the directory containing the packaged project
741
- - bool: True if the directory was updated,
742
- False if no update was needed
743
- """
744
- # Create temporary directory
745
- original_temp_dir = temp_dir = None
746
- if config.output_dir is None:
747
- temp_dir = tempfile.mkdtemp(prefix="agentscope_package_")
748
- needs_update = True # New directory always needs update
749
- else:
750
- temp_dir = config.output_dir
751
- # Check if directory exists and has content
752
- if os.path.exists(temp_dir) and os.listdir(temp_dir):
753
- # Directory exists and has content, create a temporary directory
754
- # first to generate new content for comparison
755
- original_temp_dir = temp_dir
756
- temp_dir = tempfile.mkdtemp(prefix="agentscope_package_new_")
757
- # copy docker file to this place
758
- if dockerfile_path:
759
- shutil.copy(
760
- dockerfile_path,
761
- os.path.join(
762
- temp_dir,
763
- "Dockerfile",
764
- ),
765
- )
766
- needs_update = None # Will be determined after comparison
767
- else:
768
- # Directory doesn't exist or is empty, needs update
769
- needs_update = True
770
- # Create directory if it doesn't exist
771
- if not os.path.exists(temp_dir):
772
- os.makedirs(temp_dir)
773
-
774
- try:
775
- # Extract agent variable name from the caller's frame
776
- frame = inspect.currentframe()
777
- caller_frame = frame.f_back
778
- agent_name = None
779
-
780
- # Look for the agent variable name in caller's locals and globals
781
- for var_name, var_value in caller_frame.f_locals.items():
782
- if var_value is agent:
783
- agent_name = var_name
784
- break
785
-
786
- if not agent_name:
787
- for var_name, var_value in caller_frame.f_globals.items():
788
- if var_value is agent:
789
- agent_name = var_name
790
- break
791
-
792
- if not agent_name:
793
- agent_name = "agent" # fallback name
794
-
795
- # Find the source file for the agent
796
- agent_file_path = _find_agent_source_file(
797
- agent,
798
- agent_name,
799
- caller_frame,
800
- )
801
-
802
- if not os.path.exists(agent_file_path):
803
- raise ValueError(
804
- f"Unable to locate agent source file: {agent_file_path}",
805
- )
806
-
807
- # Extract the actual agent variable name from the source file
808
- actual_agent_name = _extract_agent_name_from_source(
809
- agent_file_path,
810
- agent,
811
- )
812
-
813
- # Use the actual name from source file for the template
814
- agent_name = actual_agent_name
815
-
816
- # Copy agent file to temp directory as agent_file.py
817
- agent_dest_path = os.path.join(temp_dir, "agent_file.py")
818
- shutil.copy2(agent_file_path, agent_dest_path)
819
-
820
- # Copy extra package files
821
- if config.extra_packages:
822
- # Get the base directory from the agent_file_path for relative path
823
- # calculation
824
- caller_dir = os.path.dirname(agent_file_path)
825
-
826
- for extra_path in config.extra_packages:
827
- if os.path.isfile(extra_path):
828
- # Calculate relative path from caller directory
829
- if os.path.isabs(extra_path):
830
- try:
831
- # Try to get relative path from caller directory
832
- rel_path = os.path.relpath(extra_path, caller_dir)
833
- # If the relative path goes up beyond the caller
834
- # directory, just use filename
835
- if rel_path.startswith(".."):
836
- dest_path = os.path.join(
837
- temp_dir,
838
- os.path.basename(extra_path),
839
- )
840
- else:
841
- dest_path = os.path.join(temp_dir, rel_path)
842
- except ValueError:
843
- # If relative path calculation fails (e.g.,
844
- # different drives on Windows)
845
- dest_path = os.path.join(
846
- temp_dir,
847
- os.path.basename(extra_path),
848
- )
849
- else:
850
- # If it's already a relative path, use it as is
851
- dest_path = os.path.join(temp_dir, extra_path)
852
-
853
- # Create destination directory if it doesn't exist
854
- dest_dir = os.path.dirname(dest_path)
855
- if dest_dir and not os.path.exists(dest_dir):
856
- os.makedirs(dest_dir)
857
-
858
- # Copy file to destination
859
- shutil.copy2(extra_path, dest_path)
860
-
861
- elif os.path.isdir(extra_path):
862
- # Calculate relative path for directory
863
- if os.path.isabs(extra_path):
864
- try:
865
- rel_path = os.path.relpath(extra_path, caller_dir)
866
- if rel_path.startswith(".."):
867
- dest_path = os.path.join(
868
- temp_dir,
869
- os.path.basename(extra_path),
870
- )
871
- else:
872
- dest_path = os.path.join(temp_dir, rel_path)
873
- except ValueError:
874
- dest_path = os.path.join(
875
- temp_dir,
876
- os.path.basename(extra_path),
877
- )
878
- else:
879
- dest_path = os.path.join(temp_dir, extra_path)
880
-
881
- # Copy directory to destination
882
- shutil.copytree(extra_path, dest_path, dirs_exist_ok=True)
883
-
884
- # Use template manager for better template handling
885
- template_manager = FastAPITemplateManager()
886
-
887
- # Convert protocol_adapters to string representation for template
888
- protocol_adapters_str = None
889
- if config.protocol_adapters:
890
- # For standalone deployment, we need to generate code that
891
- # creates the adapters
892
- # This is a simplified approach - in practice, you might want
893
- # more sophisticated serialization
894
- adapter_imports = []
895
- adapter_instances = []
896
- for i, adapter in enumerate(config.protocol_adapters):
897
- adapter_class = adapter.__class__
898
- adapter_module = adapter_class.__module__
899
- adapter_name = adapter_class.__name__
900
-
901
- # Add import
902
- adapter_imports.append(
903
- f"from {adapter_module} import {adapter_name}",
904
- )
905
-
906
- # Add instance creation (simplified - doesn't handle
907
- # complex constructor args)
908
- adapter_instances.append(f"{adapter_name}(agent=agent)")
909
-
910
- # Create the protocol_adapters array string
911
- if adapter_instances:
912
- imports_str = "\n".join(adapter_imports)
913
- instances_str = "[" + ", ".join(adapter_instances) + "]"
914
- protocol_adapters_str = (
915
- f"# Protocol adapter imports\n{imports_str}\n\n"
916
- f"# Protocol adapters\nprotocol_adapters = {instances_str}"
917
- )
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
-
954
- # Render template - use template file by default,
955
- # or user-provided string
956
- if template is None:
957
- # Use standalone template file
958
- main_content = template_manager.render_standalone_template(
959
- agent_name=agent_name,
960
- endpoint_path=config.endpoint_path or "/process",
961
- deployment_mode=config.deployment_mode or "standalone",
962
- protocol_adapters=protocol_adapters_str,
963
- celery_config=celery_config_str,
964
- custom_endpoints=custom_endpoints_data,
965
- handler_dirs=handler_dirs,
966
- )
967
- else:
968
- # Use user-provided template string
969
- main_content = template_manager.render_template_from_string(
970
- template,
971
- agent_name=agent_name,
972
- endpoint_path=config.endpoint_path,
973
- deployment_mode=config.deployment_mode or "standalone",
974
- protocol_adapters=protocol_adapters_str,
975
- celery_config=celery_config_str,
976
- custom_endpoints=custom_endpoints_data,
977
- handler_dirs=handler_dirs,
978
- )
979
-
980
- # Write main.py
981
- main_file_path = os.path.join(temp_dir, "main.py")
982
- with open(main_file_path, "w", encoding="utf-8") as f:
983
- f.write(main_content)
984
-
985
- # Generate requirements.txt with unified dependencies
986
- requirements_path = os.path.join(temp_dir, "requirements.txt")
987
- with open(requirements_path, "w", encoding="utf-8") as f:
988
- # Get the current package version
989
- package_version = _get_package_version()
990
-
991
- # Add base requirements for the unified runtime
992
- if package_version:
993
- base_requirements = [
994
- "fastapi",
995
- "uvicorn",
996
- f"agentscope-runtime=={package_version}",
997
- f"agentscope-runtime[sandbox]=={package_version}",
998
- f"agentscope-runtime[deployment]=={package_version}",
999
- "pydantic",
1000
- "jinja2", # For template rendering
1001
- "psutil",
1002
- "redis", # For process management
1003
- ]
1004
- else:
1005
- # Fallback to unversioned if version cannot be determined
1006
- base_requirements = [
1007
- "fastapi",
1008
- "uvicorn",
1009
- "agentscope-runtime",
1010
- "agentscope-runtime[sandbox]",
1011
- "agentscope-runtime[deployment]",
1012
- "pydantic",
1013
- "jinja2", # For template rendering
1014
- "psutil", # For process management
1015
- "redis", # For process management
1016
- ]
1017
- if not config.requirements:
1018
- config.requirements = []
1019
-
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
1031
- all_requirements = sorted(
1032
- list(
1033
- set(
1034
- base_requirements
1035
- + config.requirements
1036
- + celery_requirements,
1037
- ),
1038
- ),
1039
- )
1040
- for req in all_requirements:
1041
- f.write(f"{req}\n")
1042
-
1043
- # Generate services configuration file if specified
1044
- if config.services_config:
1045
- config_path = os.path.join(temp_dir, "services_config.json")
1046
- import json
1047
-
1048
- with open(config_path, "w", encoding="utf-8") as f:
1049
- json.dump(config.services_config.model_dump(), f, indent=2)
1050
-
1051
- # If we need to determine if update is needed (existing directory case)
1052
- if needs_update is None and original_temp_dir is not None:
1053
- # Compare the original directory with the new content
1054
- if _compare_directories(original_temp_dir, temp_dir):
1055
- # Content is identical, no update needed
1056
- needs_update = False
1057
- # Clean up the temporary new directory and return
1058
- # original directory
1059
- if os.path.exists(temp_dir):
1060
- shutil.rmtree(temp_dir)
1061
- return original_temp_dir, needs_update
1062
- else:
1063
- # Content is different, update needed
1064
- needs_update = True
1065
- # Replace the content in the original directory
1066
- # First, clear the original directory
1067
- for item in os.listdir(original_temp_dir):
1068
- item_path = os.path.join(original_temp_dir, item)
1069
- if os.path.isdir(item_path):
1070
- shutil.rmtree(item_path)
1071
- else:
1072
- os.remove(item_path)
1073
-
1074
- # Copy new content to original directory
1075
- for item in os.listdir(temp_dir):
1076
- src_path = os.path.join(temp_dir, item)
1077
- dst_path = os.path.join(original_temp_dir, item)
1078
- if os.path.isdir(src_path):
1079
- shutil.copytree(src_path, dst_path)
1080
- else:
1081
- shutil.copy2(src_path, dst_path)
1082
-
1083
- # Clean up temporary directory
1084
- if os.path.exists(temp_dir):
1085
- shutil.rmtree(temp_dir)
1086
- return original_temp_dir, needs_update
1087
-
1088
- return temp_dir, needs_update or True
1089
-
1090
- except Exception as e:
1091
- # Clean up on error
1092
- if os.path.exists(temp_dir) and temp_dir != config.output_dir:
1093
- shutil.rmtree(temp_dir)
1094
- # If we're using a temporary directory for comparison, clean it up
1095
- if (
1096
- original_temp_dir
1097
- and temp_dir != original_temp_dir
1098
- and os.path.exists(temp_dir)
1099
- ):
1100
- shutil.rmtree(temp_dir)
1101
- raise e
1102
-
1103
-
1104
- def create_tar_gz(
1105
- directory_path: str,
1106
- output_path: Optional[str] = None,
1107
- ) -> str:
1108
- """
1109
- Package a directory into a tar.gz file.
1110
-
1111
- Args:
1112
- directory_path: Path to the directory to package
1113
- output_path: Optional output path for the tar.gz file. If not provided,
1114
- will create the tar.gz in the same parent directory as
1115
- the source directory
1116
-
1117
- Returns:
1118
- str: Path to the created tar.gz file
1119
-
1120
- Raises:
1121
- ValueError: If the directory doesn't exist
1122
- OSError: If there's an error creating the tar.gz file
1123
- """
1124
- if not os.path.exists(directory_path):
1125
- raise ValueError(f"Directory does not exist: {directory_path}")
1126
-
1127
- if not os.path.isdir(directory_path):
1128
- raise ValueError(f"Path is not a directory: {directory_path}")
1129
-
1130
- # Generate output path if not provided
1131
- if output_path is None:
1132
- dir_name = os.path.basename(os.path.normpath(directory_path))
1133
- parent_dir = os.path.dirname(directory_path)
1134
- output_path = os.path.join(parent_dir, f"{dir_name}.tar.gz")
1135
-
1136
- try:
1137
- with tarfile.open(output_path, "w:gz") as tar:
1138
- # Add all contents of the directory to the tar file
1139
- for root, dirs, files in os.walk(directory_path):
1140
- for file in files:
1141
- file_path = os.path.join(root, file)
1142
- # Calculate the archive name (relative to the source
1143
- # directory)
1144
- arcname = os.path.relpath(file_path, directory_path)
1145
- tar.add(file_path, arcname=arcname)
1146
-
1147
- # Also add empty directories
1148
- for dir_name in dirs:
1149
- dir_path = os.path.join(root, dir_name)
1150
- if not os.listdir(dir_path): # Empty directory
1151
- arcname = os.path.relpath(dir_path, directory_path)
1152
- tar.add(dir_path, arcname=arcname)
1153
-
1154
- return output_path
1155
-
1156
- except Exception as e:
1157
- # Clean up partial file if it exists
1158
- if os.path.exists(output_path):
1159
- try:
1160
- os.remove(output_path)
1161
- except OSError:
1162
- pass
1163
- raise OSError(f"Failed to create tar.gz file: {str(e)}") from e