open-swarm 0.1.1743070217__py3-none-any.whl → 0.1.1743364176__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 (217) hide show
  1. open_swarm-0.1.1743364176.dist-info/METADATA +286 -0
  2. open_swarm-0.1.1743364176.dist-info/RECORD +260 -0
  3. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/WHEEL +1 -2
  4. open_swarm-0.1.1743364176.dist-info/entry_points.txt +2 -0
  5. swarm/__init__.py +0 -2
  6. swarm/auth.py +53 -49
  7. swarm/blueprints/README.md +67 -0
  8. swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
  9. swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
  10. swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
  11. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
  12. swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
  13. swarm/blueprints/divine_code/__init__.py +0 -0
  14. swarm/blueprints/divine_code/apps.py +11 -0
  15. swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
  16. swarm/blueprints/django_chat/apps.py +6 -0
  17. swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
  18. swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
  19. swarm/blueprints/django_chat/urls.py +8 -0
  20. swarm/blueprints/django_chat/views.py +32 -0
  21. swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
  22. swarm/blueprints/family_ties/apps.py +11 -0
  23. swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
  24. swarm/blueprints/family_ties/models.py +19 -0
  25. swarm/blueprints/family_ties/serializers.py +7 -0
  26. swarm/blueprints/family_ties/settings.py +16 -0
  27. swarm/blueprints/family_ties/urls.py +10 -0
  28. swarm/blueprints/family_ties/views.py +26 -0
  29. swarm/blueprints/flock/__init__.py +0 -0
  30. swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
  31. swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
  32. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
  33. swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
  34. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
  35. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
  36. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
  37. swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
  38. swarm/blueprints/rue_code/__init__.py +0 -0
  39. swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
  40. swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
  41. swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
  42. swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
  43. swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
  44. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
  45. swarm/extensions/blueprint/__init__.py +30 -15
  46. swarm/extensions/blueprint/agent_utils.py +16 -40
  47. swarm/extensions/blueprint/blueprint_base.py +141 -543
  48. swarm/extensions/blueprint/blueprint_discovery.py +112 -98
  49. swarm/extensions/blueprint/cli_handler.py +185 -0
  50. swarm/extensions/blueprint/config_loader.py +122 -0
  51. swarm/extensions/blueprint/django_utils.py +181 -79
  52. swarm/extensions/blueprint/interactive_mode.py +1 -1
  53. swarm/extensions/config/config_loader.py +83 -200
  54. swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
  55. swarm/extensions/launchers/swarm_cli.py +199 -287
  56. swarm/llm/chat_completion.py +26 -55
  57. swarm/management/__init__.py +0 -0
  58. swarm/management/commands/__init__.py +0 -0
  59. swarm/management/commands/runserver.py +58 -0
  60. swarm/permissions.py +38 -0
  61. swarm/serializers.py +96 -5
  62. swarm/settings.py +95 -110
  63. swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
  64. swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
  65. swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
  66. swarm/static/contrib/markedjs/marked.min.js +6 -0
  67. swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
  68. swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
  69. swarm/static/contrib/tabler-icons/archive.svg +21 -0
  70. swarm/static/contrib/tabler-icons/artboard.svg +27 -0
  71. swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
  72. swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
  73. swarm/static/contrib/tabler-icons/carambola.svg +19 -0
  74. swarm/static/contrib/tabler-icons/copy.svg +20 -0
  75. swarm/static/contrib/tabler-icons/download.svg +21 -0
  76. swarm/static/contrib/tabler-icons/edit.svg +21 -0
  77. swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
  78. swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
  79. swarm/static/contrib/tabler-icons/headset.svg +22 -0
  80. swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
  81. swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
  82. swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
  83. swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
  84. swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
  85. swarm/static/contrib/tabler-icons/message-star.svg +22 -0
  86. swarm/static/contrib/tabler-icons/message-x.svg +23 -0
  87. swarm/static/contrib/tabler-icons/message.svg +21 -0
  88. swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
  89. swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
  90. swarm/static/contrib/tabler-icons/robot.svg +26 -0
  91. swarm/static/contrib/tabler-icons/search.svg +19 -0
  92. swarm/static/contrib/tabler-icons/settings.svg +20 -0
  93. swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
  94. swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
  95. swarm/static/css/dropdown.css +22 -0
  96. swarm/static/htmx/htmx.min.js +0 -0
  97. swarm/static/js/dropdown.js +23 -0
  98. swarm/static/rest_mode/css/base.css +470 -0
  99. swarm/static/rest_mode/css/chat-history.css +286 -0
  100. swarm/static/rest_mode/css/chat.css +251 -0
  101. swarm/static/rest_mode/css/chatbot.css +74 -0
  102. swarm/static/rest_mode/css/chatgpt.css +62 -0
  103. swarm/static/rest_mode/css/colors/corporate.css +74 -0
  104. swarm/static/rest_mode/css/colors/pastel.css +81 -0
  105. swarm/static/rest_mode/css/colors/tropical.css +82 -0
  106. swarm/static/rest_mode/css/general.css +142 -0
  107. swarm/static/rest_mode/css/layout.css +167 -0
  108. swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
  109. swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
  110. swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
  111. swarm/static/rest_mode/css/messages.css +84 -0
  112. swarm/static/rest_mode/css/messenger.css +135 -0
  113. swarm/static/rest_mode/css/settings.css +91 -0
  114. swarm/static/rest_mode/css/simple.css +44 -0
  115. swarm/static/rest_mode/css/slack.css +58 -0
  116. swarm/static/rest_mode/css/style.css +156 -0
  117. swarm/static/rest_mode/css/theme.css +30 -0
  118. swarm/static/rest_mode/css/toast.css +40 -0
  119. swarm/static/rest_mode/js/auth.js +9 -0
  120. swarm/static/rest_mode/js/blueprint.js +41 -0
  121. swarm/static/rest_mode/js/blueprintUtils.js +12 -0
  122. swarm/static/rest_mode/js/chatLogic.js +79 -0
  123. swarm/static/rest_mode/js/debug.js +63 -0
  124. swarm/static/rest_mode/js/events.js +98 -0
  125. swarm/static/rest_mode/js/main.js +19 -0
  126. swarm/static/rest_mode/js/messages.js +264 -0
  127. swarm/static/rest_mode/js/messengerLogic.js +355 -0
  128. swarm/static/rest_mode/js/modules/apiService.js +84 -0
  129. swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
  130. swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
  131. swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
  132. swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
  133. swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
  134. swarm/static/rest_mode/js/modules/state.js +7 -0
  135. swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
  136. swarm/static/rest_mode/js/modules/validation.js +23 -0
  137. swarm/static/rest_mode/js/rendering.js +119 -0
  138. swarm/static/rest_mode/js/settings.js +130 -0
  139. swarm/static/rest_mode/js/sidebar.js +94 -0
  140. swarm/static/rest_mode/js/simpleLogic.js +37 -0
  141. swarm/static/rest_mode/js/slackLogic.js +66 -0
  142. swarm/static/rest_mode/js/splash.js +76 -0
  143. swarm/static/rest_mode/js/theme.js +111 -0
  144. swarm/static/rest_mode/js/toast.js +36 -0
  145. swarm/static/rest_mode/js/ui.js +265 -0
  146. swarm/static/rest_mode/js/validation.js +57 -0
  147. swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
  148. swarm/static/rest_mode/svg/arrow_down.svg +5 -0
  149. swarm/static/rest_mode/svg/arrow_left.svg +5 -0
  150. swarm/static/rest_mode/svg/arrow_right.svg +5 -0
  151. swarm/static/rest_mode/svg/arrow_up.svg +5 -0
  152. swarm/static/rest_mode/svg/attach.svg +8 -0
  153. swarm/static/rest_mode/svg/avatar.svg +7 -0
  154. swarm/static/rest_mode/svg/canvas.svg +6 -0
  155. swarm/static/rest_mode/svg/chat_history.svg +4 -0
  156. swarm/static/rest_mode/svg/close.svg +5 -0
  157. swarm/static/rest_mode/svg/copy.svg +4 -0
  158. swarm/static/rest_mode/svg/dark_mode.svg +3 -0
  159. swarm/static/rest_mode/svg/edit.svg +5 -0
  160. swarm/static/rest_mode/svg/layout.svg +9 -0
  161. swarm/static/rest_mode/svg/logo.svg +29 -0
  162. swarm/static/rest_mode/svg/logout.svg +5 -0
  163. swarm/static/rest_mode/svg/mobile.svg +5 -0
  164. swarm/static/rest_mode/svg/new_chat.svg +4 -0
  165. swarm/static/rest_mode/svg/not_visible.svg +5 -0
  166. swarm/static/rest_mode/svg/plus.svg +7 -0
  167. swarm/static/rest_mode/svg/run_code.svg +6 -0
  168. swarm/static/rest_mode/svg/save.svg +4 -0
  169. swarm/static/rest_mode/svg/search.svg +6 -0
  170. swarm/static/rest_mode/svg/settings.svg +4 -0
  171. swarm/static/rest_mode/svg/speaker.svg +5 -0
  172. swarm/static/rest_mode/svg/stop.svg +6 -0
  173. swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
  174. swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
  175. swarm/static/rest_mode/svg/toggle_off.svg +6 -0
  176. swarm/static/rest_mode/svg/toggle_on.svg +6 -0
  177. swarm/static/rest_mode/svg/trash.svg +10 -0
  178. swarm/static/rest_mode/svg/undo.svg +3 -0
  179. swarm/static/rest_mode/svg/visible.svg +8 -0
  180. swarm/static/rest_mode/svg/voice.svg +10 -0
  181. swarm/templates/account/login.html +22 -0
  182. swarm/templates/account/signup.html +32 -0
  183. swarm/templates/base.html +30 -0
  184. swarm/templates/chat.html +43 -0
  185. swarm/templates/index.html +35 -0
  186. swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
  187. swarm/templates/rest_mode/components/header.html +45 -0
  188. swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
  189. swarm/templates/rest_mode/components/settings_dialog.html +97 -0
  190. swarm/templates/rest_mode/components/splash_screen.html +7 -0
  191. swarm/templates/rest_mode/components/top_bar.html +28 -0
  192. swarm/templates/rest_mode/message_ui.html +50 -0
  193. swarm/templates/rest_mode/slackbot.html +30 -0
  194. swarm/templates/simple_blueprint_page.html +24 -0
  195. swarm/templates/websocket_partials/final_system_message.html +3 -0
  196. swarm/templates/websocket_partials/system_message.html +4 -0
  197. swarm/templates/websocket_partials/user_message.html +5 -0
  198. swarm/urls.py +57 -74
  199. swarm/utils/log_utils.py +63 -0
  200. swarm/views/api_views.py +48 -39
  201. swarm/views/chat_views.py +156 -70
  202. swarm/views/core_views.py +85 -90
  203. swarm/views/model_views.py +64 -121
  204. swarm/views/utils.py +65 -441
  205. open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
  206. open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
  207. open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
  208. open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
  209. swarm/agent/agent.py +0 -49
  210. swarm/core.py +0 -326
  211. swarm/extensions/mcp/__init__.py +0 -1
  212. swarm/extensions/mcp/cache_utils.py +0 -36
  213. swarm/extensions/mcp/mcp_client.py +0 -341
  214. swarm/extensions/mcp/mcp_constants.py +0 -7
  215. swarm/extensions/mcp/mcp_tool_provider.py +0 -110
  216. swarm/types.py +0 -126
  217. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/licenses/LICENSE +0 -0
@@ -1,112 +1,126 @@
1
- """
2
- Blueprint Discovery Module for Open Swarm MCP.
3
-
4
- This module dynamically discovers and imports blueprints from specified directories.
5
- It identifies classes derived from BlueprintBase as valid blueprints and extracts their metadata.
6
- """
7
-
1
+ import os
2
+ import importlib
8
3
  import importlib.util
9
4
  import inspect
10
- import logging
11
- import os
5
+ import logging # Ensure logging is imported
6
+ import sys
7
+ from typing import Dict, Type, Any
12
8
  from pathlib import Path
13
- from typing import Dict, List, Any
14
- from swarm.settings import DEBUG
15
9
 
10
+ # *** Define logger EARLIER ***
16
11
  logger = logging.getLogger(__name__)
17
- logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
18
12
 
13
+ # *** Import the ACTUAL BlueprintBase from the likely correct path ***
19
14
  try:
20
- from .blueprint_base import BlueprintBase
21
- except ImportError as e:
22
- logger.critical(f"Failed to import BlueprintBase: {e}")
23
- raise
24
-
25
- def discover_blueprints(directories: List[str]) -> Dict[str, Dict[str, Any]]:
15
+ # Adjust this path if BlueprintBase lives elsewhere
16
+ from swarm.extensions.blueprint.blueprint_base import BlueprintBase
17
+ except ImportError:
18
+ # This logger call is now safe
19
+ logger.error("Failed to import BlueprintBase from swarm.extensions.blueprint.blueprint_base. Using placeholder.", exc_info=True)
20
+ class BlueprintBase: # Fallback placeholder
21
+ metadata: Dict[str, Any] = {}
22
+ def __init__(self, *args, **kwargs): pass
23
+ async def run(self, *args, **kwargs): pass
24
+
25
+
26
+ class BlueprintLoadError(Exception):
27
+ """Custom exception for errors during blueprint loading."""
28
+ pass
29
+
30
+ def _get_blueprint_name_from_dir(dir_name: str) -> str:
31
+ """Converts directory name (e.g., 'blueprint_my_agent') to blueprint name (e.g., 'my_agent')."""
32
+ prefix = "blueprint_"
33
+ if dir_name.startswith(prefix):
34
+ return dir_name[len(prefix):]
35
+ return dir_name
36
+
37
+ def discover_blueprints(blueprint_dir: str) -> Dict[str, Type[BlueprintBase]]:
26
38
  """
27
- Discover and load blueprints from specified directories.
28
- Extract metadata including title, description, and other attributes.
39
+ Discovers blueprints (subclasses of BlueprintBase) by looking for
40
+ 'blueprint_{name}.py' files within subdirectories of the given blueprint directory.
29
41
 
30
42
  Args:
31
- directories (List[str]): List of directories to search for blueprints.
43
+ blueprint_dir: The path to the directory containing blueprint subdirectories.
32
44
 
33
45
  Returns:
34
- Dict[str, Dict[str, Any]]: Dictionary containing blueprint metadata.
46
+ A dictionary mapping blueprint names to their corresponding class objects.
35
47
  """
36
- blueprints = {}
37
- logger.info("Starting blueprint discovery.")
38
- swarm_blueprints = os.getenv("SWARM_BLUEPRINTS", "").split(",")
39
- if swarm_blueprints and swarm_blueprints[0]:
40
- logger.debug(f"Filtering blueprints to: {swarm_blueprints}")
41
-
42
- for directory in directories:
43
- logger.debug(f"Searching for blueprints in: {directory}")
44
- dir_path = Path(directory)
45
-
46
- if not dir_path.exists() or not dir_path.is_dir():
47
- logger.warning(f"Invalid directory: {directory}. Skipping...")
48
- continue
49
-
50
- for blueprint_file in dir_path.rglob("blueprint_*.py"):
51
- module_name = blueprint_file.stem
52
- blueprint_name = module_name.replace("blueprint_", "")
53
- if swarm_blueprints and swarm_blueprints[0] and blueprint_name not in swarm_blueprints:
54
- logger.debug(f"Skipping blueprint '{blueprint_name}' not in SWARM_BLUEPRINTS")
55
- continue
56
- module_path = str(blueprint_file.parent)
57
-
58
- logger.debug(f"Found blueprint file: {blueprint_file}")
59
- logger.debug(f"Module name: {module_name}, Blueprint name: {blueprint_name}, Module path: {module_path}")
60
-
61
- try:
62
- spec = importlib.util.spec_from_file_location(module_name, str(blueprint_file))
63
- if spec is None or spec.loader is None:
64
- logger.error(f"Cannot load module spec for blueprint file: {blueprint_file}. Skipping.")
65
- continue
66
- module = importlib.util.module_from_spec(spec)
67
- spec.loader.exec_module(module)
68
- logger.debug(f"Successfully imported module: {module_name}")
69
-
70
- for name, obj in inspect.getmembers(module, inspect.isclass):
71
- if not issubclass(obj, BlueprintBase) or obj is BlueprintBase:
72
- continue
73
-
74
- logger.debug(f"Discovered blueprint class: {name}")
75
-
76
- try:
77
- metadata = obj.metadata
78
- if callable(metadata):
79
- metadata = metadata()
80
- elif isinstance(metadata, property):
81
- if metadata.fget is not None:
82
- metadata = metadata.fget(obj)
83
- else:
84
- logger.error(f"Blueprint '{blueprint_name}' property 'metadata' has no getter.")
85
- raise ValueError(f"Blueprint '{blueprint_name}' metadata is inaccessible.")
86
-
87
- if not isinstance(metadata, dict):
88
- logger.error(f"Metadata for blueprint '{blueprint_name}' is not a dictionary.")
89
- raise ValueError(f"Metadata for blueprint '{blueprint_name}' is invalid or inaccessible.")
90
-
91
- if "title" not in metadata or "description" not in metadata:
92
- logger.error(f"Required metadata fields (title, description) are missing for blueprint '{blueprint_name}'.")
93
- raise ValueError(f"Metadata for blueprint '{blueprint_name}' is invalid or inaccessible.")
94
-
95
- except Exception as e:
96
- logger.error(f"Error retrieving metadata for blueprint '{blueprint_name}': {e}")
97
- continue
98
-
99
- blueprints[blueprint_name] = {
100
- "blueprint_class": obj,
101
- "title": metadata["title"],
102
- "description": metadata["description"],
103
- }
104
- logger.debug(f"Added blueprint '{blueprint_name}' with metadata: {metadata}")
105
- except ImportError as e:
106
- logger.error(f"Failed to import module '{module_name}': {e}")
107
- except Exception as e:
108
- logger.error(f"Unexpected error importing '{module_name}': {e}", exc_info=True)
109
-
110
- logger.info("Blueprint discovery complete.")
111
- logger.debug(f"Discovered blueprints: {list(blueprints.keys())}")
48
+ logger.info(f"Starting blueprint discovery in directory: {blueprint_dir}")
49
+ blueprints: Dict[str, Type[BlueprintBase]] = {}
50
+ base_dir = Path(blueprint_dir).resolve()
51
+
52
+ if not base_dir.is_dir():
53
+ logger.error(f"Blueprint directory not found or is not a directory: {base_dir}")
54
+ return blueprints
55
+
56
+ # Iterate over items inside the base blueprint directory
57
+ for item_name in os.listdir(base_dir):
58
+ item_path = base_dir / item_name
59
+
60
+ if not item_path.is_dir():
61
+ continue # Skip files directly under blueprints/
62
+
63
+ # Use directory name as blueprint name (e.g., 'echocraft')
64
+ blueprint_name = item_name
65
+ logger.debug(f"Processing potential blueprint '{blueprint_name}' in directory: {item_name}")
66
+
67
+ # Look for the specific .py file, e.g., blueprint_echocraft.py
68
+ py_file_name = f"blueprint_{blueprint_name}.py"
69
+ py_file_path = item_path / py_file_name
70
+
71
+ if not py_file_path.is_file():
72
+ # Also check for just {blueprint_name}.py if that's a convention
73
+ alt_py_file_name = f"{blueprint_name}.py"
74
+ alt_py_file_path = item_path / alt_py_file_name
75
+ if alt_py_file_path.is_file():
76
+ py_file_path = alt_py_file_path # Use the alternative path
77
+ py_file_name = alt_py_file_name
78
+ logger.debug(f"Found alternative blueprint file: {py_file_name}")
79
+ else:
80
+ logger.warning(f"Skipping directory '{item_name}': Neither '{py_file_name}' nor '{alt_py_file_name}' found.")
81
+ continue
82
+
83
+
84
+ # Construct module import path, e.g., blueprints.echocraft.blueprint_echocraft
85
+ module_import_path = f"{base_dir.name}.{item_name}.{py_file_path.stem}"
86
+
87
+ try:
88
+ # Ensure parent directory is in path
89
+ parent_dir = str(base_dir.parent)
90
+ if parent_dir not in sys.path:
91
+ logger.debug(f"Adding '{parent_dir}' to sys.path for blueprint discovery.")
92
+ sys.path.insert(0, parent_dir)
93
+
94
+ # Create module spec from file path
95
+ module_spec = importlib.util.spec_from_file_location(module_import_path, py_file_path)
96
+
97
+ if module_spec and module_spec.loader:
98
+ module = importlib.util.module_from_spec(module_spec)
99
+ sys.modules[module_import_path] = module
100
+ module_spec.loader.exec_module(module)
101
+ logger.debug(f"Successfully loaded module: {module_import_path}")
102
+
103
+ found_bp_class = None
104
+ for name, obj in inspect.getmembers(module):
105
+ if inspect.isclass(obj) and obj.__module__ == module_import_path and issubclass(obj, BlueprintBase) and obj is not BlueprintBase:
106
+ if found_bp_class:
107
+ logger.warning(f"Multiple BlueprintBase subclasses found in {py_file_name}. Using the first: {found_bp_class.__name__}.")
108
+ else:
109
+ logger.debug(f"Found Blueprint class '{name}' in module '{module_import_path}'")
110
+ found_bp_class = obj
111
+ blueprints[blueprint_name] = found_bp_class
112
+ # break
113
+
114
+ if not found_bp_class:
115
+ logger.warning(f"No BlueprintBase subclass found directly defined in module: {module_import_path}")
116
+ else:
117
+ logger.warning(f"Could not create module spec for {py_file_path}")
118
+
119
+ except Exception as e:
120
+ logger.error(f"Error processing blueprint file '{py_file_path}': {e}", exc_info=True)
121
+ if module_import_path in sys.modules:
122
+ del sys.modules[module_import_path]
123
+
124
+ logger.info(f"Blueprint discovery complete. Found: {list(blueprints.keys())}")
112
125
  return blueprints
126
+
@@ -0,0 +1,185 @@
1
+ import argparse
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ import signal
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Dict, Optional, Type
9
+
10
+ # Import BlueprintBase type hint carefully
11
+ from typing import TYPE_CHECKING
12
+ if TYPE_CHECKING:
13
+ from .blueprint_base import BlueprintBase
14
+
15
+ logger = logging.getLogger("swarm.cli")
16
+
17
+ async def _run_blueprint_async_with_shutdown(blueprint: 'BlueprintBase', instruction: str):
18
+ """Runs the blueprint's async method and handles graceful shutdown."""
19
+ loop = asyncio.get_running_loop()
20
+ stop_event = asyncio.Event()
21
+
22
+ def signal_handler():
23
+ print("\n[bold yellow]Shutdown signal received. Attempting graceful exit...[/bold yellow]", file=sys.stderr)
24
+ logger.warning("Shutdown signal received.")
25
+ stop_event.set()
26
+
27
+ # Add signal handlers for SIGINT (Ctrl+C) and SIGTERM
28
+ for sig in (signal.SIGINT, signal.SIGTERM):
29
+ try:
30
+ loop.add_signal_handler(sig, signal_handler)
31
+ except NotImplementedError:
32
+ # Fallback for Windows or environments where add_signal_handler is not supported
33
+ try:
34
+ # signal.signal must be called in the main thread
35
+ signal.signal(sig, lambda s, f: loop.call_soon_threadsafe(signal_handler))
36
+ logger.debug(f"Registered signal handler for {sig.name} using signal.signal fallback.")
37
+ except ValueError as e:
38
+ logger.error(f"Could not set signal handler for {sig.name} using fallback: {e}. Graceful shutdown via signal might not work.")
39
+ except Exception as e:
40
+ logger.error(f"Unexpected error setting fallback signal handler for {sig.name}: {e}", exc_info=True)
41
+
42
+
43
+ # Wrap the main execution in a task to allow cancellation
44
+ main_task = loop.create_task(blueprint._run_non_interactive(instruction), name=f"BlueprintRun_{blueprint.__class__.__name__}")
45
+
46
+ # Wait for either the main task or the stop event
47
+ done, pending = await asyncio.wait(
48
+ [main_task, loop.create_task(stop_event.wait(), name="ShutdownWatcher")],
49
+ return_when=asyncio.FIRST_COMPLETED
50
+ )
51
+
52
+ # Cleanup signal handlers after wait returns
53
+ for sig in (signal.SIGINT, signal.SIGTERM):
54
+ try:
55
+ loop.remove_signal_handler(sig)
56
+ except NotImplementedError:
57
+ try:
58
+ signal.signal(sig, signal.SIG_DFL) # Restore default handler
59
+ except Exception:
60
+ pass # Ignore errors during cleanup
61
+
62
+ # Check if the stop event was triggered
63
+ if stop_event.is_set():
64
+ logger.warning("Graceful shutdown initiated. Cancelling main task...")
65
+ if not main_task.done():
66
+ main_task.cancel()
67
+ try:
68
+ # Wait briefly for cancellation to propagate and cleanup within the task
69
+ await asyncio.wait_for(main_task, timeout=10.0) # Increased timeout slightly
70
+ except asyncio.CancelledError:
71
+ logger.info("Main task successfully cancelled.")
72
+ except asyncio.TimeoutError:
73
+ logger.error("Main task did not cancel within timeout. Potential resource leak.")
74
+ except Exception as e:
75
+ logger.error(f"Error during task cancellation waiting: {e}", exc_info=True)
76
+ else:
77
+ logger.info("Main task already completed before cancellation request.")
78
+ # The _run_non_interactive's AsyncExitStack should handle MCP cleanup
79
+ else:
80
+ # If the main task finished first, check for exceptions
81
+ if main_task in done:
82
+ try:
83
+ main_task.result() # Raise exception if one occurred in the task
84
+ logger.debug("Main task completed successfully.")
85
+ except asyncio.CancelledError:
86
+ logger.info("Main task was cancelled externally (unexpected).")
87
+ except Exception as e:
88
+ # Error should have been logged within _run_non_interactive
89
+ # We exit here because the main operation failed
90
+ logger.critical(f"Blueprint execution failed with unhandled exception: {e}", exc_info=True)
91
+ sys.exit(1) # Exit with error status if task failed
92
+
93
+
94
+ def run_blueprint_cli(
95
+ blueprint_cls: Type['BlueprintBase'],
96
+ swarm_version: str,
97
+ default_config_path: Path
98
+ ):
99
+ """
100
+ Parses CLI arguments, instantiates, and runs a blueprint.
101
+
102
+ Args:
103
+ blueprint_cls (Type[BlueprintBase]): The blueprint class to run.
104
+ swarm_version (str): The core swarm version string.
105
+ default_config_path (Path): Default path to swarm_config.json.
106
+ """
107
+ # --- Argument Parsing ---
108
+ metadata = getattr(blueprint_cls, 'metadata', {})
109
+ parser = argparse.ArgumentParser(
110
+ description=metadata.get("description", f"Run {blueprint_cls.__name__}"),
111
+ formatter_class=argparse.RawTextHelpFormatter
112
+ )
113
+ parser.add_argument("--instruction", type=str, required=True, help="Initial instruction for the blueprint.")
114
+ parser.add_argument("--config-path", type=str, default=None, help=f"Path to swarm_config.json (Default: {default_config_path})")
115
+ parser.add_argument("--config", type=str, metavar="JSON_FILE_OR_STRING", default=None, help="JSON config overrides (file path or string). Merged last.")
116
+ parser.add_argument("--profile", type=str, default=None, help="Configuration profile to use.")
117
+ parser.add_argument("--debug", action="store_true", help="Enable DEBUG logging level.")
118
+ parser.add_argument("--quiet", action="store_true", help="Suppress most logs and headers, print only final output.")
119
+ parser.add_argument('--markdown', action=argparse.BooleanOptionalAction, default=None, help="Enable/disable markdown output (--markdown / --no-markdown). Overrides config/default.")
120
+ parser.add_argument("--version", action="version", version=f"%(prog)s (BP: {metadata.get('name', 'N/A')} v{metadata.get('version', 'N/A')}, Core: {swarm_version})")
121
+ args = parser.parse_args()
122
+
123
+ # --- Load CLI Config Overrides ---
124
+ cli_config_overrides = {}
125
+ if args.config:
126
+ config_arg = args.config
127
+ config_override_path = Path(config_arg)
128
+ temp_logger = logging.getLogger("swarm.cli.config") # Temp logger for this part
129
+ if config_override_path.is_file():
130
+ temp_logger.info(f"Attempting to load CLI config overrides from file: {config_override_path}")
131
+ try:
132
+ with open(config_override_path, "r", encoding="utf-8") as f:
133
+ cli_config_overrides = json.load(f)
134
+ temp_logger.debug(f"Loaded overrides keys: {list(cli_config_overrides.keys())}")
135
+ except Exception as e:
136
+ temp_logger.error(f"Failed to load --config file: {e}", exc_info=args.debug)
137
+ sys.exit(f"Error reading config override file: {e}")
138
+ else:
139
+ temp_logger.info("Attempting to parse --config argument as JSON string.")
140
+ try:
141
+ cli_config_overrides = json.loads(config_arg)
142
+ if not isinstance(cli_config_overrides, dict):
143
+ raise TypeError("--config JSON string must resolve to a dictionary.")
144
+ temp_logger.debug(f"--config JSON string parsed successfully. Keys: {list(cli_config_overrides.keys())}")
145
+ except Exception as e:
146
+ temp_logger.error(f"Failed parsing --config JSON string: {e}")
147
+ sys.exit(f"Error: Invalid --config value: {e}")
148
+
149
+ # --- Instantiate and Run Blueprint ---
150
+ blueprint_instance: Optional['BlueprintBase'] = None
151
+ try:
152
+ # Instantiate the blueprint, passing necessary config/flags
153
+ blueprint_instance = blueprint_cls(
154
+ config_path_override=args.config_path,
155
+ profile_override=args.profile,
156
+ config_overrides=cli_config_overrides,
157
+ debug=args.debug,
158
+ quiet=args.quiet,
159
+ force_markdown=args.markdown,
160
+ # Pass necessary context if needed by __init__
161
+ # default_config_path=default_config_path,
162
+ # swarm_version=swarm_version
163
+ )
164
+
165
+ # Run the async part with shutdown handling
166
+ asyncio.run(_run_blueprint_async_with_shutdown(blueprint_instance, args.instruction))
167
+
168
+ except (ValueError, TypeError, FileNotFoundError) as config_err:
169
+ logger.critical(f"[Initialization Error] Configuration problem: {config_err}", exc_info=args.debug)
170
+ sys.exit(1)
171
+ except ImportError as ie:
172
+ # Catch potential issues if dependencies are missing
173
+ logger.critical(f"[Import Error] Failed to import required module for {blueprint_cls.__name__}: {ie}. Please check dependencies.", exc_info=args.debug)
174
+ sys.exit(1)
175
+ except KeyboardInterrupt:
176
+ logger.info("Execution interrupted by user (KeyboardInterrupt).")
177
+ # Should be handled by signal handler now, but keep as fallback
178
+ sys.exit(130) # Standard exit code for Ctrl+C
179
+ except Exception as e:
180
+ logger.critical(f"[Execution Error] An unexpected error occurred: {e}", exc_info=True)
181
+ sys.exit(1)
182
+ finally:
183
+ logger.debug("Blueprint CLI execution finished.")
184
+ # Any final cleanup outside the async loop (rarely needed here)
185
+
@@ -0,0 +1,122 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional, Union
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ logger = logging.getLogger("swarm.config")
10
+
11
+ def _substitute_env_vars(value: Any) -> Any:
12
+ """Recursively substitute environment variables in strings, lists, and dicts."""
13
+ if isinstance(value, str):
14
+ return os.path.expandvars(value)
15
+ elif isinstance(value, list):
16
+ return [_substitute_env_vars(item) for item in value]
17
+ elif isinstance(value, dict):
18
+ return {k: _substitute_env_vars(v) for k, v in value.items()}
19
+ else:
20
+ return value
21
+
22
+ def load_environment(project_root: Path):
23
+ """Loads environment variables from a `.env` file located at the project root."""
24
+ dotenv_path = project_root / ".env"
25
+ logger.debug(f"Checking for .env file at: {dotenv_path}")
26
+ try:
27
+ if dotenv_path.is_file():
28
+ loaded = load_dotenv(dotenv_path=dotenv_path, override=True)
29
+ if loaded:
30
+ logger.debug(f".env file Loaded/Overridden at: {dotenv_path}")
31
+ else:
32
+ logger.debug(f"No .env file found at {dotenv_path}.")
33
+ except Exception as e:
34
+ logger.error(f"Error loading .env file '{dotenv_path}': {e}", exc_info=logger.level <= logging.DEBUG)
35
+
36
+ def load_full_configuration(
37
+ blueprint_class_name: str,
38
+ default_config_path: Path,
39
+ config_path_override: Optional[Union[str, Path]] = None,
40
+ profile_override: Optional[str] = None,
41
+ cli_config_overrides: Optional[Dict[str, Any]] = None,
42
+ ) -> Dict[str, Any]:
43
+ """
44
+ Loads and merges configuration settings from base file, blueprint specifics, profiles, and CLI overrides.
45
+
46
+ Args:
47
+ blueprint_class_name (str): The name of the blueprint class (e.g., "MyBlueprint").
48
+ default_config_path (Path): The default path to the swarm_config.json file.
49
+ config_path_override (Optional[Union[str, Path]]): Path specified via CLI argument.
50
+ profile_override (Optional[str]): Profile specified via CLI argument.
51
+ cli_config_overrides (Optional[Dict[str, Any]]): Overrides provided via CLI argument.
52
+
53
+ Returns:
54
+ Dict[str, Any]: The final, merged configuration dictionary.
55
+
56
+ Raises:
57
+ ValueError: If the configuration file has JSON errors or cannot be read.
58
+ FileNotFoundError: If a specific config_path_override is given but the file doesn't exist.
59
+ """
60
+ config_path = Path(config_path_override) if config_path_override else default_config_path
61
+ logger.debug(f"Attempting to load base configuration from: {config_path}")
62
+ base_config = {}
63
+ if config_path.is_file():
64
+ try:
65
+ with open(config_path, "r", encoding="utf-8") as f:
66
+ base_config = json.load(f)
67
+ logger.debug(f"Successfully loaded base configuration from: {config_path}")
68
+ except json.JSONDecodeError as e:
69
+ raise ValueError(f"Config Error: Failed to parse JSON in {config_path}: {e}") from e
70
+ except Exception as e:
71
+ raise ValueError(f"Config Error: Failed to read {config_path}: {e}") from e
72
+ else:
73
+ if config_path_override:
74
+ raise FileNotFoundError(f"Configuration Error: Specified config file not found: {config_path}")
75
+ else:
76
+ logger.warning(f"Default configuration file not found at {config_path}. Proceeding without base configuration.")
77
+
78
+ # 1. Start with base defaults
79
+ final_config = base_config.get("defaults", {}).copy()
80
+ logger.debug(f"Applied base defaults. Keys: {list(final_config.keys())}")
81
+
82
+ # 2. Merge base llm and mcpServers sections
83
+ if "llm" in base_config:
84
+ final_config.setdefault("llm", {}).update(base_config["llm"])
85
+ logger.debug("Merged base 'llm'.")
86
+ if "mcpServers" in base_config:
87
+ final_config.setdefault("mcpServers", {}).update(base_config["mcpServers"])
88
+ logger.debug("Merged base 'mcpServers'.")
89
+
90
+ # 3. Merge blueprint-specific settings
91
+ blueprint_settings = base_config.get("blueprints", {}).get(blueprint_class_name, {})
92
+ if blueprint_settings:
93
+ final_config.update(blueprint_settings)
94
+ logger.debug(f"Merged BP '{blueprint_class_name}' settings. Keys: {list(blueprint_settings.keys())}")
95
+
96
+ # 4. Determine and merge profile settings
97
+ # Priority: CLI > Blueprint Specific > Base Defaults > "default"
98
+ profile_in_bp_settings = blueprint_settings.get("default_profile")
99
+ profile_in_base_defaults = base_config.get("defaults", {}).get("default_profile")
100
+ profile_to_use = profile_override or profile_in_bp_settings or profile_in_base_defaults or "default"
101
+ logger.debug(f"Using profile: '{profile_to_use}'")
102
+ profile_settings = base_config.get("profiles", {}).get(profile_to_use, {})
103
+ if profile_settings:
104
+ final_config.update(profile_settings)
105
+ logger.debug(f"Merged profile '{profile_to_use}'. Keys: {list(profile_settings.keys())}")
106
+ elif profile_to_use != "default" and (profile_override or profile_in_bp_settings or profile_in_base_defaults):
107
+ logger.warning(f"Profile '{profile_to_use}' requested but not found.")
108
+
109
+ # 5. Merge CLI overrides (highest priority)
110
+ if cli_config_overrides:
111
+ final_config.update(cli_config_overrides)
112
+ logger.debug(f"Merged CLI overrides. Keys: {list(cli_config_overrides.keys())}")
113
+
114
+ # Ensure top-level keys exist
115
+ final_config.setdefault("llm", {})
116
+ final_config.setdefault("mcpServers", {})
117
+
118
+ # 6. Substitute environment variables in the final config
119
+ final_config = _substitute_env_vars(final_config)
120
+ logger.debug("Applied final env var substitution.")
121
+
122
+ return final_config