agentscope-runtime 0.1.3__py3-none-any.whl → 0.1.5b1__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 (44) hide show
  1. agentscope_runtime/engine/agents/agentscope_agent/agent.py +3 -0
  2. agentscope_runtime/engine/deployers/__init__.py +13 -0
  3. agentscope_runtime/engine/deployers/adapter/responses/__init__.py +0 -0
  4. agentscope_runtime/engine/deployers/adapter/responses/response_api_adapter_utils.py +2886 -0
  5. agentscope_runtime/engine/deployers/adapter/responses/response_api_agent_adapter.py +51 -0
  6. agentscope_runtime/engine/deployers/adapter/responses/response_api_protocol_adapter.py +314 -0
  7. agentscope_runtime/engine/deployers/cli_fc_deploy.py +143 -0
  8. agentscope_runtime/engine/deployers/kubernetes_deployer.py +265 -0
  9. agentscope_runtime/engine/deployers/local_deployer.py +356 -501
  10. agentscope_runtime/engine/deployers/modelstudio_deployer.py +626 -0
  11. agentscope_runtime/engine/deployers/utils/__init__.py +0 -0
  12. agentscope_runtime/engine/deployers/utils/deployment_modes.py +14 -0
  13. agentscope_runtime/engine/deployers/utils/docker_image_utils/__init__.py +8 -0
  14. agentscope_runtime/engine/deployers/utils/docker_image_utils/docker_image_builder.py +429 -0
  15. agentscope_runtime/engine/deployers/utils/docker_image_utils/dockerfile_generator.py +240 -0
  16. agentscope_runtime/engine/deployers/utils/docker_image_utils/runner_image_factory.py +297 -0
  17. agentscope_runtime/engine/deployers/utils/package_project_utils.py +932 -0
  18. agentscope_runtime/engine/deployers/utils/service_utils/__init__.py +9 -0
  19. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_factory.py +504 -0
  20. agentscope_runtime/engine/deployers/utils/service_utils/fastapi_templates.py +157 -0
  21. agentscope_runtime/engine/deployers/utils/service_utils/process_manager.py +268 -0
  22. agentscope_runtime/engine/deployers/utils/service_utils/service_config.py +75 -0
  23. agentscope_runtime/engine/deployers/utils/service_utils/service_factory.py +220 -0
  24. agentscope_runtime/engine/deployers/utils/wheel_packager.py +389 -0
  25. agentscope_runtime/engine/helpers/agent_api_builder.py +651 -0
  26. agentscope_runtime/engine/runner.py +36 -10
  27. agentscope_runtime/engine/schemas/agent_schemas.py +70 -2
  28. agentscope_runtime/engine/schemas/embedding.py +37 -0
  29. agentscope_runtime/engine/schemas/modelstudio_llm.py +310 -0
  30. agentscope_runtime/engine/schemas/oai_llm.py +538 -0
  31. agentscope_runtime/engine/schemas/realtime.py +254 -0
  32. agentscope_runtime/engine/services/context_manager.py +2 -0
  33. agentscope_runtime/engine/services/mem0_memory_service.py +124 -0
  34. agentscope_runtime/engine/services/memory_service.py +2 -1
  35. agentscope_runtime/engine/services/redis_session_history_service.py +4 -3
  36. agentscope_runtime/engine/services/session_history_service.py +4 -3
  37. agentscope_runtime/sandbox/manager/container_clients/kubernetes_client.py +555 -10
  38. agentscope_runtime/version.py +1 -1
  39. {agentscope_runtime-0.1.3.dist-info → agentscope_runtime-0.1.5b1.dist-info}/METADATA +25 -5
  40. {agentscope_runtime-0.1.3.dist-info → agentscope_runtime-0.1.5b1.dist-info}/RECORD +44 -17
  41. {agentscope_runtime-0.1.3.dist-info → agentscope_runtime-0.1.5b1.dist-info}/entry_points.txt +1 -0
  42. {agentscope_runtime-0.1.3.dist-info → agentscope_runtime-0.1.5b1.dist-info}/WHEEL +0 -0
  43. {agentscope_runtime-0.1.3.dist-info → agentscope_runtime-0.1.5b1.dist-info}/licenses/LICENSE +0 -0
  44. {agentscope_runtime-0.1.3.dist-info → agentscope_runtime-0.1.5b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,932 @@
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
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
+ class PackageConfig(BaseModel):
65
+ """Configuration for project packaging"""
66
+
67
+ requirements: Optional[List[str]] = None
68
+ extra_packages: Optional[List[str]] = None
69
+ output_dir: Optional[str] = None
70
+ endpoint_path: Optional[str] = "/process"
71
+ deployment_mode: Optional[str] = "standalone" # New: deployment mode
72
+ services_config: Optional[
73
+ ServicesConfig
74
+ ] = None # New: services configuration
75
+ protocol_adapters: Optional[List[Any]] = None # New: protocol adapters
76
+
77
+
78
+ def _find_agent_source_file(
79
+ agent_obj: Any,
80
+ agent_name: str,
81
+ caller_frame,
82
+ ) -> str:
83
+ """
84
+ Find the file that contains the agent instance definition (where the
85
+ agent variable is assigned).
86
+ This prioritizes finding where the agent instance was created rather
87
+ than where the class is defined.
88
+ """
89
+
90
+ # Method 1: Search through the call stack to find where the agent
91
+ # instance was defined
92
+ frame = caller_frame
93
+ found_files = [] # Store potential files for analysis
94
+ agent_names_in_frames = [] # Store agent names found in each frame
95
+
96
+ while frame:
97
+ try:
98
+ frame_filename = frame.f_code.co_filename
99
+
100
+ # Skip internal/system files and focus on user code
101
+ if (
102
+ not frame_filename.endswith(".py")
103
+ or "site-packages" in frame_filename
104
+ ):
105
+ frame = frame.f_back
106
+ continue
107
+
108
+ # Check if this frame contains our agent variable
109
+ frame_locals = frame.f_locals
110
+ frame_globals = frame.f_globals
111
+
112
+ # Look for the agent object (by identity, not name) in both
113
+ # locals and globals
114
+ found_agent_name = None
115
+ for var_name, var_value in frame_locals.items():
116
+ if var_value is agent_obj:
117
+ found_agent_name = var_name
118
+ break
119
+
120
+ if not found_agent_name:
121
+ for var_name, var_value in frame_globals.items():
122
+ if var_value is agent_obj:
123
+ found_agent_name = var_name
124
+ break
125
+
126
+ if found_agent_name:
127
+ # Found the frame where this agent instance exists
128
+ found_files.append(frame_filename)
129
+ agent_names_in_frames.append(found_agent_name)
130
+
131
+ except (AttributeError, TypeError):
132
+ # Handle any errors in frame inspection
133
+ pass
134
+
135
+ frame = frame.f_back
136
+
137
+ # Method 2: Analyze found files to determine which one contains the
138
+ # actual instance definition
139
+ # Reverse the order to prioritize files found later in the stack (
140
+ # typically user code)
141
+ for i, file_path in enumerate(reversed(found_files)):
142
+ # Get the corresponding agent name for this file
143
+ agent_name_in_file = agent_names_in_frames[len(found_files) - 1 - i]
144
+
145
+ try:
146
+ with open(file_path, "r", encoding="utf-8") as f:
147
+ content = f.read()
148
+
149
+ # Check if this file contains an import statement for the agent
150
+ # If so, we should look for the original source file
151
+ import_patterns = [
152
+ rf"^[^\#]*from\s+(\w+)\s+import\s+.*"
153
+ rf"{re.escape(agent_name_in_file)}",
154
+ rf"^[^\#]*from\s+([\w.]+)\s+import\s+.*"
155
+ rf"{re.escape(agent_name_in_file)}",
156
+ ]
157
+
158
+ # Check if this file imports the agent from another module
159
+ lines = content.split("\n")
160
+ for line in lines:
161
+ for import_pattern in import_patterns:
162
+ match = re.search(import_pattern, line)
163
+ if match:
164
+ module_name = match.group(1)
165
+ # Try to find the source module file
166
+ current_dir = os.path.dirname(file_path)
167
+
168
+ # Convert dotted module name to filesystem path
169
+ module_path = module_name.replace(".", os.sep)
170
+
171
+ # Try different possible paths for the source module
172
+ possible_paths = [
173
+ # Same directory - simple module name
174
+ os.path.join(
175
+ current_dir,
176
+ f"{module_name}.py",
177
+ ),
178
+ # Same directory - dotted path
179
+ os.path.join(
180
+ current_dir,
181
+ f"{module_path}.py",
182
+ ),
183
+ # Package in same directory
184
+ os.path.join(
185
+ current_dir,
186
+ module_name,
187
+ "__init__.py",
188
+ ),
189
+ # Package with dotted path
190
+ os.path.join(
191
+ current_dir,
192
+ module_path,
193
+ "__init__.py",
194
+ ),
195
+ # Parent directory - simple module name
196
+ os.path.join(
197
+ os.path.dirname(current_dir),
198
+ f"{module_name}.py",
199
+ ),
200
+ # Parent directory - dotted path
201
+ os.path.join(
202
+ os.path.dirname(current_dir),
203
+ f"{module_path}.py",
204
+ ),
205
+ # Current working directory - simple module name
206
+ os.path.join(
207
+ os.getcwd(),
208
+ f"{module_name}.py",
209
+ ),
210
+ # Current working directory - dotted path
211
+ os.path.join(
212
+ os.getcwd(),
213
+ f"{module_path}.py",
214
+ ),
215
+ # Current working directory - package
216
+ os.path.join(
217
+ os.getcwd(),
218
+ module_path,
219
+ "__init__.py",
220
+ ),
221
+ ]
222
+
223
+ for source_path in possible_paths:
224
+ if os.path.exists(source_path):
225
+ # Check if this source file contains the
226
+ # actual assignment
227
+ try:
228
+ with open(
229
+ source_path,
230
+ "r",
231
+ encoding="utf-8",
232
+ ) as src_f:
233
+ src_content = src_f.read()
234
+
235
+ # Look for the assignment in the source
236
+ # file
237
+ assignment_patterns = [
238
+ rf"^[^\#]*{re.escape(agent_name_in_file)}" # noqa E501
239
+ rf"\s*=\s*\w+\(",
240
+ rf"^[^\#]*{re.escape(agent_name_in_file)}" # noqa E501
241
+ rf"\s*=\s*[\w.]+\(",
242
+ ]
243
+
244
+ src_lines = src_content.split("\n")
245
+ for src_line in src_lines:
246
+ if (
247
+ not src_line.strip()
248
+ or src_line.strip().startswith("#")
249
+ or src_line.strip().startswith(
250
+ "def ",
251
+ )
252
+ or src_line.strip().startswith(
253
+ "from ",
254
+ )
255
+ or src_line.strip().startswith(
256
+ "import ",
257
+ )
258
+ or src_line.strip().startswith(
259
+ "class ",
260
+ )
261
+ ):
262
+ continue
263
+
264
+ for (
265
+ assign_pattern
266
+ ) in assignment_patterns:
267
+ if re.search(
268
+ assign_pattern,
269
+ src_line,
270
+ ):
271
+ if "=" in src_line:
272
+ left_side = src_line.split(
273
+ "=",
274
+ )[0]
275
+ if (
276
+ agent_name_in_file
277
+ in left_side
278
+ and "("
279
+ not in left_side
280
+ ):
281
+ indent_level = len(
282
+ src_line,
283
+ ) - len(
284
+ src_line.lstrip(),
285
+ )
286
+ if indent_level <= 4:
287
+ return source_path
288
+
289
+ except (OSError, UnicodeDecodeError):
290
+ continue
291
+ break # Found import, no need to check other patterns
292
+
293
+ # If no import found, check if this file itself contains the
294
+ # assignment
295
+ assignment_patterns = [
296
+ # direct assignment: agent_name = ClassName(
297
+ rf"^[^\#]*{re.escape(agent_name_in_file)}\s*=\s*\w+\(",
298
+ # module assignment: agent_name = module.ClassName(
299
+ rf"^[^\#]*{re.escape(agent_name_in_file)}\s*=\s*[\w.]+\(",
300
+ ]
301
+
302
+ # Look for actual variable assignment (not function parameters
303
+ # or imports)
304
+ for line_num, line in enumerate(lines):
305
+ stripped_line = line.strip()
306
+ # Skip comments, empty lines, function definitions, and imports
307
+ if (
308
+ not stripped_line
309
+ or stripped_line.startswith("#")
310
+ or stripped_line.startswith("def ")
311
+ or stripped_line.startswith("from ")
312
+ or stripped_line.startswith("import ")
313
+ or stripped_line.startswith("class ")
314
+ ):
315
+ continue
316
+
317
+ # Check if this line contains the agent assignment
318
+ for pattern in assignment_patterns:
319
+ if re.search(pattern, line):
320
+ # Double check that this is a real assignment,
321
+ # not inside function parameters by checking if the
322
+ # line has '=' and the agent_name is on the left side
323
+ if "=" in line:
324
+ left_side = line.split("=")[0]
325
+ if (
326
+ agent_name_in_file in left_side
327
+ and "(" not in left_side
328
+ ):
329
+ # Additional context check: make sure this
330
+ # is not indented too much (likely inside a
331
+ # function if heavily indented)
332
+ indent_level = len(line) - len(line.lstrip())
333
+ if (
334
+ indent_level <= 4
335
+ ): # Top level or minimal indentation
336
+ return file_path
337
+
338
+ except (OSError, UnicodeDecodeError):
339
+ # If we can't read the file, continue to next file
340
+ continue
341
+
342
+ # Method 3: If no assignment pattern found, return the first found file
343
+ if found_files:
344
+ return found_files[0]
345
+
346
+ # Method 4: Fall back to original caller-based approach if stack search
347
+ # fails
348
+ caller_filename = caller_frame.f_code.co_filename
349
+ caller_dir = os.path.dirname(caller_filename)
350
+
351
+ # Check if we have the import information in the caller's globals
352
+ # Look for module objects that might contain the agent
353
+ for var_name, var_obj in caller_frame.f_globals.items():
354
+ if hasattr(var_obj, "__file__") and hasattr(var_obj, agent_name):
355
+ # This looks like a module that contains our agent
356
+ if getattr(var_obj, agent_name, None) is caller_frame.f_locals.get(
357
+ agent_name,
358
+ ):
359
+ return var_obj.__file__
360
+
361
+ # If direct lookup failed, try to parse the import statements
362
+ try:
363
+ with open(caller_filename, "r", encoding="utf-8") as f:
364
+ content = f.read()
365
+
366
+ tree = ast.parse(content)
367
+
368
+ for node in ast.walk(tree):
369
+ if isinstance(node, ast.ImportFrom):
370
+ # Look for "from module_name import agent_name"
371
+ if node.names and node.module:
372
+ for alias in node.names:
373
+ imported_name = (
374
+ alias.asname if alias.asname else alias.name
375
+ )
376
+ if imported_name == agent_name:
377
+ # Found the import statement
378
+ module_path = os.path.join(
379
+ caller_dir,
380
+ f"{node.module}.py",
381
+ )
382
+ if os.path.exists(module_path):
383
+ return module_path
384
+ # Try relative import
385
+ if node.level > 0: # relative import
386
+ parent_path = caller_dir
387
+ for _ in range(node.level - 1):
388
+ parent_path = os.path.dirname(parent_path)
389
+ module_path = os.path.join(
390
+ parent_path,
391
+ f"{node.module}.py",
392
+ )
393
+ if os.path.exists(module_path):
394
+ return module_path
395
+
396
+ elif isinstance(node, ast.Import):
397
+ # Look for "import module_name" where agent might be
398
+ # module_name.agent_name
399
+ for alias in node.names:
400
+ module_name = alias.asname if alias.asname else alias.name
401
+ if module_name in caller_frame.f_globals:
402
+ module_obj = caller_frame.f_globals[module_name]
403
+ if hasattr(module_obj, "__file__") and hasattr(
404
+ module_obj,
405
+ agent_name,
406
+ ):
407
+ return module_obj.__file__
408
+
409
+ except Exception as e:
410
+ # If parsing fails, we'll fall back to the caller file
411
+ print(e)
412
+
413
+ return caller_filename
414
+
415
+
416
+ def _extract_agent_name_from_source(
417
+ agent_file_path: str,
418
+ agent_obj: Any,
419
+ ) -> str:
420
+ """
421
+ Extract the actual variable name of the agent from the source file by
422
+ looking for variable assignments and trying to match the object type.
423
+
424
+ Args:
425
+ agent_file_path: Path to the source file containing agent definition
426
+ agent_obj: The agent object to match
427
+
428
+ Returns:
429
+ str: The variable name used in the source file, or "agent" as fallback
430
+ """
431
+ try:
432
+ with open(agent_file_path, "r", encoding="utf-8") as f:
433
+ content = f.read()
434
+
435
+ # Get the class name of the agent object
436
+ agent_class_name = agent_obj.__class__.__name__
437
+
438
+ lines = content.split("\n")
439
+ potential_names = []
440
+
441
+ for line in lines:
442
+ stripped_line = line.strip()
443
+ # Skip comments, empty lines, function definitions, and imports
444
+ if (
445
+ not stripped_line
446
+ or stripped_line.startswith("#")
447
+ or stripped_line.startswith("def ")
448
+ or stripped_line.startswith("from ")
449
+ or stripped_line.startswith("import ")
450
+ or stripped_line.startswith("class ")
451
+ ):
452
+ continue
453
+
454
+ # Look for variable assignment patterns: var_name = ...
455
+ if "=" in line:
456
+ left_side = line.split("=")[0].strip()
457
+ right_side = line.split("=", 1)[1].strip()
458
+
459
+ # Make sure it's a simple variable assignment (not inside
460
+ # parentheses or functions)
461
+ if (
462
+ left_side
463
+ and "(" not in left_side
464
+ and left_side.isidentifier()
465
+ and not left_side.startswith("_")
466
+ ): # Skip private variables
467
+ # Check indentation level - should be top level or
468
+ # minimal indentation
469
+ indent_level = len(line) - len(line.lstrip())
470
+ if indent_level <= 4: # Top level or minimal indentation
471
+ # Check if the right side contains the agent class name
472
+ if agent_class_name in right_side:
473
+ # This is likely our agent assignment
474
+ potential_names.insert(0, left_side)
475
+ # # Also check for assignments that might create the
476
+ # agent through constructor calls
477
+ # elif "(" in right_side:
478
+ # potential_names.append(left_side)
479
+
480
+ # Return the first potential name found (prioritizing class name
481
+ # matches)
482
+ if potential_names:
483
+ return potential_names[0]
484
+
485
+ except (OSError, UnicodeDecodeError):
486
+ pass
487
+
488
+ return "agent" # fallback
489
+
490
+
491
+ def _calculate_directory_hash(directory_path: str) -> str:
492
+ """
493
+ Calculate a hash representing the entire contents of a directory.
494
+
495
+ Args:
496
+ directory_path: Path to the directory to hash
497
+
498
+ Returns:
499
+ str: SHA256 hash of the directory contents
500
+ """
501
+ hasher = hashlib.sha256()
502
+
503
+ if not os.path.exists(directory_path):
504
+ return ""
505
+
506
+ # Walk through directory and hash all file contents and paths
507
+ for root, dirs, files in sorted(os.walk(directory_path)):
508
+ # Sort to ensure consistent ordering
509
+ dirs.sort()
510
+ files.sort()
511
+
512
+ for filename in files:
513
+ file_path = os.path.join(root, filename)
514
+
515
+ # Hash the relative path
516
+ rel_path = os.path.relpath(file_path, directory_path)
517
+ hasher.update(rel_path.encode("utf-8"))
518
+
519
+ # Hash the file contents
520
+ try:
521
+ with open(file_path, "rb") as f:
522
+ for chunk in iter(lambda: f.read(4096), b""):
523
+ hasher.update(chunk)
524
+ except (OSError, IOError):
525
+ # Skip files that can't be read
526
+ continue
527
+
528
+ return hasher.hexdigest()
529
+
530
+
531
+ def _compare_directories(old_dir: str, new_dir: str) -> bool:
532
+ """
533
+ Compare two directories to see if their contents are identical.
534
+
535
+ Args:
536
+ old_dir: Path to the old directory
537
+ new_dir: Path to the new directory
538
+
539
+ Returns:
540
+ bool: True if directories have identical contents, False otherwise
541
+ """
542
+ old_hash = _calculate_directory_hash(old_dir)
543
+ new_hash = _calculate_directory_hash(new_dir)
544
+
545
+ return old_hash == new_hash and old_hash != ""
546
+
547
+
548
+ def package_project(
549
+ agent: Any,
550
+ config: PackageConfig,
551
+ dockerfile_path: Optional[str] = None,
552
+ template: Optional[str] = None, # Use template file by default
553
+ ) -> Tuple[str, bool]:
554
+ """
555
+ Package a project with agent and dependencies into a temporary directory.
556
+
557
+ Args:
558
+ agent: The agent object to be packaged
559
+ config: The configuration of the package
560
+ dockerfile_path: Path to the Docker file
561
+ template: User override template string
562
+ (if None, uses standalone template file)
563
+
564
+ Returns:
565
+ Tuple[str, bool]: A tuple containing:
566
+ - str: Path to the directory containing the packaged project
567
+ - bool: True if the directory was updated,
568
+ False if no update was needed
569
+ """
570
+ # Create temporary directory
571
+ original_temp_dir = temp_dir = None
572
+ if config.output_dir is None:
573
+ temp_dir = tempfile.mkdtemp(prefix="agentscope_package_")
574
+ needs_update = True # New directory always needs update
575
+ else:
576
+ temp_dir = config.output_dir
577
+ # Check if directory exists and has content
578
+ if os.path.exists(temp_dir) and os.listdir(temp_dir):
579
+ # Directory exists and has content, create a temporary directory
580
+ # first to generate new content for comparison
581
+ original_temp_dir = temp_dir
582
+ temp_dir = tempfile.mkdtemp(prefix="agentscope_package_new_")
583
+ # copy docker file to this place
584
+ if dockerfile_path:
585
+ shutil.copy(
586
+ dockerfile_path,
587
+ os.path.join(
588
+ temp_dir,
589
+ "Dockerfile",
590
+ ),
591
+ )
592
+ needs_update = None # Will be determined after comparison
593
+ else:
594
+ # Directory doesn't exist or is empty, needs update
595
+ needs_update = True
596
+ # Create directory if it doesn't exist
597
+ if not os.path.exists(temp_dir):
598
+ os.makedirs(temp_dir)
599
+
600
+ try:
601
+ # Extract agent variable name from the caller's frame
602
+ frame = inspect.currentframe()
603
+ caller_frame = frame.f_back
604
+ agent_name = None
605
+
606
+ # Look for the agent variable name in caller's locals and globals
607
+ for var_name, var_value in caller_frame.f_locals.items():
608
+ if var_value is agent:
609
+ agent_name = var_name
610
+ break
611
+
612
+ if not agent_name:
613
+ for var_name, var_value in caller_frame.f_globals.items():
614
+ if var_value is agent:
615
+ agent_name = var_name
616
+ break
617
+
618
+ if not agent_name:
619
+ agent_name = "agent" # fallback name
620
+
621
+ # Find the source file for the agent
622
+ agent_file_path = _find_agent_source_file(
623
+ agent,
624
+ agent_name,
625
+ caller_frame,
626
+ )
627
+
628
+ if not os.path.exists(agent_file_path):
629
+ raise ValueError(
630
+ f"Unable to locate agent source file: {agent_file_path}",
631
+ )
632
+
633
+ # Extract the actual agent variable name from the source file
634
+ actual_agent_name = _extract_agent_name_from_source(
635
+ agent_file_path,
636
+ agent,
637
+ )
638
+
639
+ # Use the actual name from source file for the template
640
+ agent_name = actual_agent_name
641
+
642
+ # Copy agent file to temp directory as agent_file.py
643
+ agent_dest_path = os.path.join(temp_dir, "agent_file.py")
644
+ shutil.copy2(agent_file_path, agent_dest_path)
645
+
646
+ # Copy extra package files
647
+ if config.extra_packages:
648
+ # Get the base directory from the agent_file_path for relative path
649
+ # calculation
650
+ caller_dir = os.path.dirname(agent_file_path)
651
+
652
+ for extra_path in config.extra_packages:
653
+ if os.path.isfile(extra_path):
654
+ # Calculate relative path from caller directory
655
+ if os.path.isabs(extra_path):
656
+ try:
657
+ # Try to get relative path from caller directory
658
+ rel_path = os.path.relpath(extra_path, caller_dir)
659
+ # If the relative path goes up beyond the caller
660
+ # directory, just use filename
661
+ if rel_path.startswith(".."):
662
+ dest_path = os.path.join(
663
+ temp_dir,
664
+ os.path.basename(extra_path),
665
+ )
666
+ else:
667
+ dest_path = os.path.join(temp_dir, rel_path)
668
+ except ValueError:
669
+ # If relative path calculation fails (e.g.,
670
+ # different drives on Windows)
671
+ dest_path = os.path.join(
672
+ temp_dir,
673
+ os.path.basename(extra_path),
674
+ )
675
+ else:
676
+ # If it's already a relative path, use it as is
677
+ dest_path = os.path.join(temp_dir, extra_path)
678
+
679
+ # Create destination directory if it doesn't exist
680
+ dest_dir = os.path.dirname(dest_path)
681
+ if dest_dir and not os.path.exists(dest_dir):
682
+ os.makedirs(dest_dir)
683
+
684
+ # Copy file to destination
685
+ shutil.copy2(extra_path, dest_path)
686
+
687
+ elif os.path.isdir(extra_path):
688
+ # Calculate relative path for directory
689
+ if os.path.isabs(extra_path):
690
+ try:
691
+ rel_path = os.path.relpath(extra_path, caller_dir)
692
+ if rel_path.startswith(".."):
693
+ dest_path = os.path.join(
694
+ temp_dir,
695
+ os.path.basename(extra_path),
696
+ )
697
+ else:
698
+ dest_path = os.path.join(temp_dir, rel_path)
699
+ except ValueError:
700
+ dest_path = os.path.join(
701
+ temp_dir,
702
+ os.path.basename(extra_path),
703
+ )
704
+ else:
705
+ dest_path = os.path.join(temp_dir, extra_path)
706
+
707
+ # Copy directory to destination
708
+ shutil.copytree(extra_path, dest_path, dirs_exist_ok=True)
709
+
710
+ # Use template manager for better template handling
711
+ template_manager = FastAPITemplateManager()
712
+
713
+ # Convert protocol_adapters to string representation for template
714
+ protocol_adapters_str = None
715
+ if config.protocol_adapters:
716
+ # For standalone deployment, we need to generate code that
717
+ # creates the adapters
718
+ # This is a simplified approach - in practice, you might want
719
+ # more sophisticated serialization
720
+ adapter_imports = []
721
+ adapter_instances = []
722
+ for i, adapter in enumerate(config.protocol_adapters):
723
+ adapter_class = adapter.__class__
724
+ adapter_module = adapter_class.__module__
725
+ adapter_name = adapter_class.__name__
726
+
727
+ # Add import
728
+ adapter_imports.append(
729
+ f"from {adapter_module} import {adapter_name}",
730
+ )
731
+
732
+ # Add instance creation (simplified - doesn't handle
733
+ # complex constructor args)
734
+ adapter_instances.append(f"{adapter_name}(agent=agent)")
735
+
736
+ # Create the protocol_adapters array string
737
+ if adapter_instances:
738
+ imports_str = "\n".join(adapter_imports)
739
+ instances_str = "[" + ", ".join(adapter_instances) + "]"
740
+ protocol_adapters_str = (
741
+ f"# Protocol adapter imports\n{imports_str}\n\n"
742
+ f"# Protocol adapters\nprotocol_adapters = {instances_str}"
743
+ )
744
+
745
+ # Render template - use template file by default,
746
+ # or user-provided string
747
+ if template is None:
748
+ # Use standalone template file
749
+ main_content = template_manager.render_standalone_template(
750
+ agent_name=agent_name,
751
+ endpoint_path=config.endpoint_path or "/process",
752
+ deployment_mode=config.deployment_mode or "standalone",
753
+ protocol_adapters=protocol_adapters_str,
754
+ )
755
+ else:
756
+ # Use user-provided template string
757
+ main_content = template_manager.render_template_from_string(
758
+ template,
759
+ agent_name=agent_name,
760
+ endpoint_path=config.endpoint_path,
761
+ deployment_mode=config.deployment_mode or "standalone",
762
+ protocol_adapters=protocol_adapters_str,
763
+ )
764
+
765
+ # Write main.py
766
+ main_file_path = os.path.join(temp_dir, "main.py")
767
+ with open(main_file_path, "w", encoding="utf-8") as f:
768
+ f.write(main_content)
769
+
770
+ # Generate requirements.txt with unified dependencies
771
+ requirements_path = os.path.join(temp_dir, "requirements.txt")
772
+ with open(requirements_path, "w", encoding="utf-8") as f:
773
+ # Get the current package version
774
+ package_version = _get_package_version()
775
+
776
+ # Add base requirements for the unified runtime
777
+ if package_version:
778
+ base_requirements = [
779
+ "fastapi",
780
+ "uvicorn",
781
+ f"agentscope-runtime=={package_version}",
782
+ f"agentscope-runtime[sandbox]=={package_version}",
783
+ f"agentscope-runtime[deployment]=={package_version}",
784
+ "pydantic",
785
+ "jinja2", # For template rendering
786
+ "psutil",
787
+ "redis", # For process management
788
+ ]
789
+ else:
790
+ # Fallback to unversioned if version cannot be determined
791
+ base_requirements = [
792
+ "fastapi",
793
+ "uvicorn",
794
+ "agentscope-runtime",
795
+ "agentscope-runtime[sandbox]",
796
+ "agentscope-runtime[deployment]",
797
+ "pydantic",
798
+ "jinja2", # For template rendering
799
+ "psutil", # For process management
800
+ "redis", # For process management
801
+ ]
802
+ if not config.requirements:
803
+ config.requirements = []
804
+
805
+ # Combine base requirements with user requirements
806
+ all_requirements = sorted(
807
+ list(set(base_requirements + config.requirements)),
808
+ )
809
+ for req in all_requirements:
810
+ f.write(f"{req}\n")
811
+
812
+ # Generate services configuration file if specified
813
+ if config.services_config:
814
+ config_path = os.path.join(temp_dir, "services_config.json")
815
+ import json
816
+
817
+ with open(config_path, "w", encoding="utf-8") as f:
818
+ json.dump(config.services_config.model_dump(), f, indent=2)
819
+
820
+ # If we need to determine if update is needed (existing directory case)
821
+ if needs_update is None and original_temp_dir is not None:
822
+ # Compare the original directory with the new content
823
+ if _compare_directories(original_temp_dir, temp_dir):
824
+ # Content is identical, no update needed
825
+ needs_update = False
826
+ # Clean up the temporary new directory and return
827
+ # original directory
828
+ if os.path.exists(temp_dir):
829
+ shutil.rmtree(temp_dir)
830
+ return original_temp_dir, needs_update
831
+ else:
832
+ # Content is different, update needed
833
+ needs_update = True
834
+ # Replace the content in the original directory
835
+ # First, clear the original directory
836
+ for item in os.listdir(original_temp_dir):
837
+ item_path = os.path.join(original_temp_dir, item)
838
+ if os.path.isdir(item_path):
839
+ shutil.rmtree(item_path)
840
+ else:
841
+ os.remove(item_path)
842
+
843
+ # Copy new content to original directory
844
+ for item in os.listdir(temp_dir):
845
+ src_path = os.path.join(temp_dir, item)
846
+ dst_path = os.path.join(original_temp_dir, item)
847
+ if os.path.isdir(src_path):
848
+ shutil.copytree(src_path, dst_path)
849
+ else:
850
+ shutil.copy2(src_path, dst_path)
851
+
852
+ # Clean up temporary directory
853
+ if os.path.exists(temp_dir):
854
+ shutil.rmtree(temp_dir)
855
+ return original_temp_dir, needs_update
856
+
857
+ return temp_dir, needs_update or True
858
+
859
+ except Exception as e:
860
+ # Clean up on error
861
+ if os.path.exists(temp_dir) and temp_dir != config.output_dir:
862
+ shutil.rmtree(temp_dir)
863
+ # If we're using a temporary directory for comparison, clean it up
864
+ if (
865
+ original_temp_dir
866
+ and temp_dir != original_temp_dir
867
+ and os.path.exists(temp_dir)
868
+ ):
869
+ shutil.rmtree(temp_dir)
870
+ raise e
871
+
872
+
873
+ def create_tar_gz(
874
+ directory_path: str,
875
+ output_path: Optional[str] = None,
876
+ ) -> str:
877
+ """
878
+ Package a directory into a tar.gz file.
879
+
880
+ Args:
881
+ directory_path: Path to the directory to package
882
+ output_path: Optional output path for the tar.gz file. If not provided,
883
+ will create the tar.gz in the same parent directory as
884
+ the source directory
885
+
886
+ Returns:
887
+ str: Path to the created tar.gz file
888
+
889
+ Raises:
890
+ ValueError: If the directory doesn't exist
891
+ OSError: If there's an error creating the tar.gz file
892
+ """
893
+ if not os.path.exists(directory_path):
894
+ raise ValueError(f"Directory does not exist: {directory_path}")
895
+
896
+ if not os.path.isdir(directory_path):
897
+ raise ValueError(f"Path is not a directory: {directory_path}")
898
+
899
+ # Generate output path if not provided
900
+ if output_path is None:
901
+ dir_name = os.path.basename(os.path.normpath(directory_path))
902
+ parent_dir = os.path.dirname(directory_path)
903
+ output_path = os.path.join(parent_dir, f"{dir_name}.tar.gz")
904
+
905
+ try:
906
+ with tarfile.open(output_path, "w:gz") as tar:
907
+ # Add all contents of the directory to the tar file
908
+ for root, dirs, files in os.walk(directory_path):
909
+ for file in files:
910
+ file_path = os.path.join(root, file)
911
+ # Calculate the archive name (relative to the source
912
+ # directory)
913
+ arcname = os.path.relpath(file_path, directory_path)
914
+ tar.add(file_path, arcname=arcname)
915
+
916
+ # Also add empty directories
917
+ for dir_name in dirs:
918
+ dir_path = os.path.join(root, dir_name)
919
+ if not os.listdir(dir_path): # Empty directory
920
+ arcname = os.path.relpath(dir_path, directory_path)
921
+ tar.add(dir_path, arcname=arcname)
922
+
923
+ return output_path
924
+
925
+ except Exception as e:
926
+ # Clean up partial file if it exists
927
+ if os.path.exists(output_path):
928
+ try:
929
+ os.remove(output_path)
930
+ except OSError:
931
+ pass
932
+ raise OSError(f"Failed to create tar.gz file: {str(e)}") from e