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.
- open_swarm-0.1.1743364176.dist-info/METADATA +286 -0
- open_swarm-0.1.1743364176.dist-info/RECORD +260 -0
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/WHEEL +1 -2
- open_swarm-0.1.1743364176.dist-info/entry_points.txt +2 -0
- swarm/__init__.py +0 -2
- swarm/auth.py +53 -49
- swarm/blueprints/README.md +67 -0
- swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
- swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
- swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
- swarm/blueprints/divine_code/__init__.py +0 -0
- swarm/blueprints/divine_code/apps.py +11 -0
- swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
- swarm/blueprints/django_chat/apps.py +6 -0
- swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
- swarm/blueprints/django_chat/urls.py +8 -0
- swarm/blueprints/django_chat/views.py +32 -0
- swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
- swarm/blueprints/family_ties/apps.py +11 -0
- swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
- swarm/blueprints/family_ties/models.py +19 -0
- swarm/blueprints/family_ties/serializers.py +7 -0
- swarm/blueprints/family_ties/settings.py +16 -0
- swarm/blueprints/family_ties/urls.py +10 -0
- swarm/blueprints/family_ties/views.py +26 -0
- swarm/blueprints/flock/__init__.py +0 -0
- swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
- swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
- swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
- swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
- swarm/blueprints/rue_code/__init__.py +0 -0
- swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
- swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
- swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
- swarm/extensions/blueprint/__init__.py +30 -15
- swarm/extensions/blueprint/agent_utils.py +16 -40
- swarm/extensions/blueprint/blueprint_base.py +141 -543
- swarm/extensions/blueprint/blueprint_discovery.py +112 -98
- swarm/extensions/blueprint/cli_handler.py +185 -0
- swarm/extensions/blueprint/config_loader.py +122 -0
- swarm/extensions/blueprint/django_utils.py +181 -79
- swarm/extensions/blueprint/interactive_mode.py +1 -1
- swarm/extensions/config/config_loader.py +83 -200
- swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_cli.py +199 -287
- swarm/llm/chat_completion.py +26 -55
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +58 -0
- swarm/permissions.py +38 -0
- swarm/serializers.py +96 -5
- swarm/settings.py +95 -110
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
- swarm/static/contrib/markedjs/marked.min.js +6 -0
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
- swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
- swarm/static/contrib/tabler-icons/archive.svg +21 -0
- swarm/static/contrib/tabler-icons/artboard.svg +27 -0
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
- swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
- swarm/static/contrib/tabler-icons/carambola.svg +19 -0
- swarm/static/contrib/tabler-icons/copy.svg +20 -0
- swarm/static/contrib/tabler-icons/download.svg +21 -0
- swarm/static/contrib/tabler-icons/edit.svg +21 -0
- swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
- swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
- swarm/static/contrib/tabler-icons/headset.svg +22 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
- swarm/static/contrib/tabler-icons/message-star.svg +22 -0
- swarm/static/contrib/tabler-icons/message-x.svg +23 -0
- swarm/static/contrib/tabler-icons/message.svg +21 -0
- swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
- swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
- swarm/static/contrib/tabler-icons/robot.svg +26 -0
- swarm/static/contrib/tabler-icons/search.svg +19 -0
- swarm/static/contrib/tabler-icons/settings.svg +20 -0
- swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
- swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
- swarm/static/css/dropdown.css +22 -0
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +23 -0
- swarm/static/rest_mode/css/base.css +470 -0
- swarm/static/rest_mode/css/chat-history.css +286 -0
- swarm/static/rest_mode/css/chat.css +251 -0
- swarm/static/rest_mode/css/chatbot.css +74 -0
- swarm/static/rest_mode/css/chatgpt.css +62 -0
- swarm/static/rest_mode/css/colors/corporate.css +74 -0
- swarm/static/rest_mode/css/colors/pastel.css +81 -0
- swarm/static/rest_mode/css/colors/tropical.css +82 -0
- swarm/static/rest_mode/css/general.css +142 -0
- swarm/static/rest_mode/css/layout.css +167 -0
- swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
- swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
- swarm/static/rest_mode/css/messages.css +84 -0
- swarm/static/rest_mode/css/messenger.css +135 -0
- swarm/static/rest_mode/css/settings.css +91 -0
- swarm/static/rest_mode/css/simple.css +44 -0
- swarm/static/rest_mode/css/slack.css +58 -0
- swarm/static/rest_mode/css/style.css +156 -0
- swarm/static/rest_mode/css/theme.css +30 -0
- swarm/static/rest_mode/css/toast.css +40 -0
- swarm/static/rest_mode/js/auth.js +9 -0
- swarm/static/rest_mode/js/blueprint.js +41 -0
- swarm/static/rest_mode/js/blueprintUtils.js +12 -0
- swarm/static/rest_mode/js/chatLogic.js +79 -0
- swarm/static/rest_mode/js/debug.js +63 -0
- swarm/static/rest_mode/js/events.js +98 -0
- swarm/static/rest_mode/js/main.js +19 -0
- swarm/static/rest_mode/js/messages.js +264 -0
- swarm/static/rest_mode/js/messengerLogic.js +355 -0
- swarm/static/rest_mode/js/modules/apiService.js +84 -0
- swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
- swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
- swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
- swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
- swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
- swarm/static/rest_mode/js/modules/state.js +7 -0
- swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
- swarm/static/rest_mode/js/modules/validation.js +23 -0
- swarm/static/rest_mode/js/rendering.js +119 -0
- swarm/static/rest_mode/js/settings.js +130 -0
- swarm/static/rest_mode/js/sidebar.js +94 -0
- swarm/static/rest_mode/js/simpleLogic.js +37 -0
- swarm/static/rest_mode/js/slackLogic.js +66 -0
- swarm/static/rest_mode/js/splash.js +76 -0
- swarm/static/rest_mode/js/theme.js +111 -0
- swarm/static/rest_mode/js/toast.js +36 -0
- swarm/static/rest_mode/js/ui.js +265 -0
- swarm/static/rest_mode/js/validation.js +57 -0
- swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
- swarm/static/rest_mode/svg/arrow_down.svg +5 -0
- swarm/static/rest_mode/svg/arrow_left.svg +5 -0
- swarm/static/rest_mode/svg/arrow_right.svg +5 -0
- swarm/static/rest_mode/svg/arrow_up.svg +5 -0
- swarm/static/rest_mode/svg/attach.svg +8 -0
- swarm/static/rest_mode/svg/avatar.svg +7 -0
- swarm/static/rest_mode/svg/canvas.svg +6 -0
- swarm/static/rest_mode/svg/chat_history.svg +4 -0
- swarm/static/rest_mode/svg/close.svg +5 -0
- swarm/static/rest_mode/svg/copy.svg +4 -0
- swarm/static/rest_mode/svg/dark_mode.svg +3 -0
- swarm/static/rest_mode/svg/edit.svg +5 -0
- swarm/static/rest_mode/svg/layout.svg +9 -0
- swarm/static/rest_mode/svg/logo.svg +29 -0
- swarm/static/rest_mode/svg/logout.svg +5 -0
- swarm/static/rest_mode/svg/mobile.svg +5 -0
- swarm/static/rest_mode/svg/new_chat.svg +4 -0
- swarm/static/rest_mode/svg/not_visible.svg +5 -0
- swarm/static/rest_mode/svg/plus.svg +7 -0
- swarm/static/rest_mode/svg/run_code.svg +6 -0
- swarm/static/rest_mode/svg/save.svg +4 -0
- swarm/static/rest_mode/svg/search.svg +6 -0
- swarm/static/rest_mode/svg/settings.svg +4 -0
- swarm/static/rest_mode/svg/speaker.svg +5 -0
- swarm/static/rest_mode/svg/stop.svg +6 -0
- swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
- swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
- swarm/static/rest_mode/svg/toggle_off.svg +6 -0
- swarm/static/rest_mode/svg/toggle_on.svg +6 -0
- swarm/static/rest_mode/svg/trash.svg +10 -0
- swarm/static/rest_mode/svg/undo.svg +3 -0
- swarm/static/rest_mode/svg/visible.svg +8 -0
- swarm/static/rest_mode/svg/voice.svg +10 -0
- swarm/templates/account/login.html +22 -0
- swarm/templates/account/signup.html +32 -0
- swarm/templates/base.html +30 -0
- swarm/templates/chat.html +43 -0
- swarm/templates/index.html +35 -0
- swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
- swarm/templates/rest_mode/components/header.html +45 -0
- swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
- swarm/templates/rest_mode/components/settings_dialog.html +97 -0
- swarm/templates/rest_mode/components/splash_screen.html +7 -0
- swarm/templates/rest_mode/components/top_bar.html +28 -0
- swarm/templates/rest_mode/message_ui.html +50 -0
- swarm/templates/rest_mode/slackbot.html +30 -0
- swarm/templates/simple_blueprint_page.html +24 -0
- swarm/templates/websocket_partials/final_system_message.html +3 -0
- swarm/templates/websocket_partials/system_message.html +4 -0
- swarm/templates/websocket_partials/user_message.html +5 -0
- swarm/urls.py +57 -74
- swarm/utils/log_utils.py +63 -0
- swarm/views/api_views.py +48 -39
- swarm/views/chat_views.py +156 -70
- swarm/views/core_views.py +85 -90
- swarm/views/model_views.py +64 -121
- swarm/views/utils.py +65 -441
- open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
- open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
- open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
- swarm/agent/agent.py +0 -49
- swarm/core.py +0 -326
- swarm/extensions/mcp/__init__.py +0 -1
- swarm/extensions/mcp/cache_utils.py +0 -36
- swarm/extensions/mcp/mcp_client.py +0 -341
- swarm/extensions/mcp/mcp_constants.py +0 -7
- swarm/extensions/mcp/mcp_tool_provider.py +0 -110
- swarm/types.py +0 -126
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/licenses/LICENSE +0 -0
@@ -1,112 +1,126 @@
|
|
1
|
-
|
2
|
-
|
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
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
43
|
+
blueprint_dir: The path to the directory containing blueprint subdirectories.
|
32
44
|
|
33
45
|
Returns:
|
34
|
-
|
46
|
+
A dictionary mapping blueprint names to their corresponding class objects.
|
35
47
|
"""
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
}
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|