open-swarm 0.1.1743070217__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 (89) hide show
  1. open_swarm-0.1.1743070217.dist-info/METADATA +258 -0
  2. open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
  3. open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
  4. open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
  5. open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
  6. open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
  7. swarm/__init__.py +3 -0
  8. swarm/agent/__init__.py +7 -0
  9. swarm/agent/agent.py +49 -0
  10. swarm/apps.py +53 -0
  11. swarm/auth.py +56 -0
  12. swarm/consumers.py +141 -0
  13. swarm/core.py +326 -0
  14. swarm/extensions/__init__.py +1 -0
  15. swarm/extensions/blueprint/__init__.py +36 -0
  16. swarm/extensions/blueprint/agent_utils.py +45 -0
  17. swarm/extensions/blueprint/blueprint_base.py +562 -0
  18. swarm/extensions/blueprint/blueprint_discovery.py +112 -0
  19. swarm/extensions/blueprint/blueprint_utils.py +17 -0
  20. swarm/extensions/blueprint/common_utils.py +12 -0
  21. swarm/extensions/blueprint/django_utils.py +203 -0
  22. swarm/extensions/blueprint/interactive_mode.py +102 -0
  23. swarm/extensions/blueprint/modes/rest_mode.py +37 -0
  24. swarm/extensions/blueprint/output_utils.py +95 -0
  25. swarm/extensions/blueprint/spinner.py +91 -0
  26. swarm/extensions/cli/__init__.py +0 -0
  27. swarm/extensions/cli/blueprint_runner.py +251 -0
  28. swarm/extensions/cli/cli_args.py +88 -0
  29. swarm/extensions/cli/commands/__init__.py +0 -0
  30. swarm/extensions/cli/commands/blueprint_management.py +31 -0
  31. swarm/extensions/cli/commands/config_management.py +15 -0
  32. swarm/extensions/cli/commands/edit_config.py +77 -0
  33. swarm/extensions/cli/commands/list_blueprints.py +22 -0
  34. swarm/extensions/cli/commands/validate_env.py +57 -0
  35. swarm/extensions/cli/commands/validate_envvars.py +39 -0
  36. swarm/extensions/cli/interactive_shell.py +41 -0
  37. swarm/extensions/cli/main.py +36 -0
  38. swarm/extensions/cli/selection.py +43 -0
  39. swarm/extensions/cli/utils/discover_commands.py +32 -0
  40. swarm/extensions/cli/utils/env_setup.py +15 -0
  41. swarm/extensions/cli/utils.py +105 -0
  42. swarm/extensions/config/__init__.py +6 -0
  43. swarm/extensions/config/config_loader.py +208 -0
  44. swarm/extensions/config/config_manager.py +258 -0
  45. swarm/extensions/config/server_config.py +49 -0
  46. swarm/extensions/config/setup_wizard.py +103 -0
  47. swarm/extensions/config/utils/__init__.py +0 -0
  48. swarm/extensions/config/utils/logger.py +36 -0
  49. swarm/extensions/launchers/__init__.py +1 -0
  50. swarm/extensions/launchers/build_launchers.py +14 -0
  51. swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
  52. swarm/extensions/launchers/swarm_api.py +68 -0
  53. swarm/extensions/launchers/swarm_cli.py +304 -0
  54. swarm/extensions/launchers/swarm_wrapper.py +29 -0
  55. swarm/extensions/mcp/__init__.py +1 -0
  56. swarm/extensions/mcp/cache_utils.py +36 -0
  57. swarm/extensions/mcp/mcp_client.py +341 -0
  58. swarm/extensions/mcp/mcp_constants.py +7 -0
  59. swarm/extensions/mcp/mcp_tool_provider.py +110 -0
  60. swarm/llm/chat_completion.py +195 -0
  61. swarm/messages.py +132 -0
  62. swarm/migrations/0010_initial_chat_models.py +51 -0
  63. swarm/migrations/__init__.py +0 -0
  64. swarm/models.py +45 -0
  65. swarm/repl/__init__.py +1 -0
  66. swarm/repl/repl.py +87 -0
  67. swarm/serializers.py +12 -0
  68. swarm/settings.py +189 -0
  69. swarm/tool_executor.py +239 -0
  70. swarm/types.py +126 -0
  71. swarm/urls.py +89 -0
  72. swarm/util.py +124 -0
  73. swarm/utils/color_utils.py +40 -0
  74. swarm/utils/context_utils.py +272 -0
  75. swarm/utils/general_utils.py +162 -0
  76. swarm/utils/logger.py +61 -0
  77. swarm/utils/logger_setup.py +25 -0
  78. swarm/utils/message_sequence.py +173 -0
  79. swarm/utils/message_utils.py +95 -0
  80. swarm/utils/redact.py +68 -0
  81. swarm/views/__init__.py +41 -0
  82. swarm/views/api_views.py +46 -0
  83. swarm/views/chat_views.py +76 -0
  84. swarm/views/core_views.py +118 -0
  85. swarm/views/message_views.py +40 -0
  86. swarm/views/model_views.py +135 -0
  87. swarm/views/utils.py +457 -0
  88. swarm/views/web_views.py +149 -0
  89. swarm/wsgi.py +16 -0
@@ -0,0 +1,112 @@
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
+
8
+ import importlib.util
9
+ import inspect
10
+ import logging
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Dict, List, Any
14
+ from swarm.settings import DEBUG
15
+
16
+ logger = logging.getLogger(__name__)
17
+ logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
18
+
19
+ 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]]:
26
+ """
27
+ Discover and load blueprints from specified directories.
28
+ Extract metadata including title, description, and other attributes.
29
+
30
+ Args:
31
+ directories (List[str]): List of directories to search for blueprints.
32
+
33
+ Returns:
34
+ Dict[str, Dict[str, Any]]: Dictionary containing blueprint metadata.
35
+ """
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())}")
112
+ return blueprints
@@ -0,0 +1,17 @@
1
+ """
2
+ Utility functions for blueprint management.
3
+ """
4
+
5
+ def filter_blueprints(all_blueprints: dict, allowed_blueprints_str: str) -> dict:
6
+ """
7
+ Filters the given blueprints dictionary using a comma-separated string of allowed blueprint keys.
8
+
9
+ Args:
10
+ all_blueprints (dict): A dictionary containing all discovered blueprints.
11
+ allowed_blueprints_str (str): A comma-separated string of allowed blueprint keys.
12
+
13
+ Returns:
14
+ dict: A dictionary containing only the blueprints whose keys are present in the allowed list.
15
+ """
16
+ allowed_list = [bp.strip() for bp in allowed_blueprints_str.split(",")]
17
+ return {k: v for k, v in all_blueprints.items() if k in allowed_list}
@@ -0,0 +1,12 @@
1
+ """
2
+ Common utilities potentially shared across blueprint extensions.
3
+ """
4
+
5
+ from typing import Any # Removed Dict, List as they weren't used
6
+
7
+ def get_agent_name(agent: Any) -> str:
8
+ """Return the name of an agent from its attributes ('name' or '__name__')."""
9
+ return getattr(agent, "name", getattr(agent, "__name__", "<unknown>"))
10
+
11
+ # get_token_count has been moved to swarm.utils.context_utils
12
+ # Ensure imports in other files point to the correct location.
@@ -0,0 +1,203 @@
1
+ """
2
+ Django integration utilities for blueprint extensions.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import importlib.util
8
+ from typing import Any, TYPE_CHECKING
9
+ from django.conf import settings # Import settings directly
10
+ # Import necessary URL handling functions
11
+ from django.urls import clear_url_caches, get_resolver, get_urlconf, set_urlconf, URLPattern, URLResolver
12
+ from collections import OrderedDict
13
+ from django.apps import apps as django_apps
14
+
15
+ # Use TYPE_CHECKING to avoid circular import issues if BlueprintBase imports this indirectly
16
+ if TYPE_CHECKING:
17
+ from .blueprint_base import BlueprintBase
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ def register_django_components(blueprint: 'BlueprintBase') -> None:
22
+ """Register Django settings and URLs if applicable for the given blueprint."""
23
+ if blueprint.skip_django_registration or getattr(blueprint, "_urls_registered", False):
24
+ logger.debug(f"Skipping Django registration for {blueprint.__class__.__name__}: Skipped by flag or already registered.")
25
+ return
26
+ if not os.environ.get("DJANGO_SETTINGS_MODULE"):
27
+ logger.debug("Skipping Django registration: DJANGO_SETTINGS_MODULE not set.")
28
+ return
29
+
30
+ try:
31
+ # App readiness check less critical now if called within test fixtures after setup
32
+ if not django_apps.ready and not getattr(settings, 'TESTING', False):
33
+ logger.debug("Django apps not ready; registration likely handled by AppConfig.ready().")
34
+ return
35
+
36
+ _load_local_settings(blueprint)
37
+ _merge_installed_apps(blueprint) # Still attempt, might need restart/reload
38
+
39
+ if hasattr(blueprint, 'register_blueprint_urls') and callable(blueprint.register_blueprint_urls):
40
+ logger.debug(f"Calling blueprint-specific register_blueprint_urls for {blueprint.__class__.__name__}")
41
+ blueprint.register_blueprint_urls()
42
+ blueprint._urls_registered = True
43
+ else:
44
+ logger.debug(f"Using generic URL registration for {blueprint.__class__.__name__}")
45
+ _register_blueprint_urls_generic(blueprint)
46
+
47
+ except ImportError:
48
+ logger.warning("Django not available; skipping Django component registration.")
49
+ except Exception as e:
50
+ logger.error(f"Failed to register Django components for {blueprint.__class__.__name__}: {e}", exc_info=True)
51
+
52
+ def _load_local_settings(blueprint: 'BlueprintBase') -> None:
53
+ """Load local settings.py from the blueprint's directory if it exists."""
54
+ try:
55
+ module_spec = importlib.util.find_spec(blueprint.__class__.__module__)
56
+ if module_spec and module_spec.origin:
57
+ blueprint_dir = os.path.dirname(module_spec.origin)
58
+ local_settings_path = os.path.join(blueprint_dir, "settings.py")
59
+ if os.path.isfile(local_settings_path):
60
+ spec = importlib.util.spec_from_file_location(f"{blueprint.__class__.__module__}.local_settings", local_settings_path)
61
+ if spec and spec.loader:
62
+ local_settings = importlib.util.module_from_spec(spec)
63
+ blueprint.local_settings = local_settings
64
+ spec.loader.exec_module(local_settings)
65
+ logger.debug(f"Loaded local settings from {local_settings_path} for {blueprint.__class__.__name__}")
66
+ else:
67
+ logger.warning(f"Could not create module spec for local settings at {local_settings_path}")
68
+ blueprint.local_settings = None
69
+ else: blueprint.local_settings = None
70
+ else: blueprint.local_settings = None
71
+ except Exception as e:
72
+ logger.error(f"Error loading local settings for {blueprint.__class__.__name__}: {e}", exc_info=True)
73
+ blueprint.local_settings = None
74
+
75
+
76
+ def _merge_installed_apps(blueprint: 'BlueprintBase') -> None:
77
+ """Merge INSTALLED_APPS from blueprint's local settings into main Django settings."""
78
+ if hasattr(blueprint, "local_settings") and blueprint.local_settings and hasattr(blueprint.local_settings, "INSTALLED_APPS"):
79
+ try:
80
+ blueprint_apps = getattr(blueprint.local_settings, "INSTALLED_APPS", [])
81
+ if not isinstance(blueprint_apps, (list, tuple)):
82
+ logger.warning(f"Blueprint {blueprint.__class__.__name__}'s local INSTALLED_APPS is not a list or tuple.")
83
+ return
84
+
85
+ apps_added = False
86
+ if isinstance(settings.INSTALLED_APPS, tuple):
87
+ settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
88
+
89
+ for app in blueprint_apps:
90
+ if app not in settings.INSTALLED_APPS:
91
+ settings.INSTALLED_APPS.append(app)
92
+ apps_added = True
93
+ logger.debug(f"Added app '{app}' from blueprint {blueprint.__class__.__name__} to INSTALLED_APPS.")
94
+
95
+ if apps_added:
96
+ logger.info(f"Merged INSTALLED_APPS from blueprint {blueprint.__class__.__name__}. App registry reload might be needed.")
97
+ # Attempt app registry reload - Use with caution!
98
+ try:
99
+ logger.debug("Attempting to reload Django app registry...")
100
+ django_apps.app_configs = OrderedDict()
101
+ django_apps.ready = False
102
+ django_apps.clear_cache()
103
+ django_apps.populate(settings.INSTALLED_APPS)
104
+ logger.info("Successfully reloaded Django app registry.")
105
+ except RuntimeError as e:
106
+ logger.error(f"Could not reload app registry (likely reentrant call): {e}")
107
+ except Exception as e:
108
+ logger.error(f"Error reloading Django app registry: {e}", exc_info=True)
109
+
110
+
111
+ except ImportError:
112
+ logger.error("Could not import django.conf.settings to merge INSTALLED_APPS.")
113
+ except Exception as e:
114
+ logger.error(f"Error merging INSTALLED_APPS for {blueprint.__class__.__name__}: {e}", exc_info=True)
115
+
116
+ def _register_blueprint_urls_generic(blueprint: 'BlueprintBase') -> None:
117
+ """Generic function to register blueprint URLs based on metadata."""
118
+ if getattr(blueprint, "_urls_registered", False):
119
+ logger.debug(f"URLs for {blueprint.__class__.__name__} already registered.")
120
+ return
121
+
122
+ module_path = blueprint.metadata.get("django_modules", {}).get("urls")
123
+ url_prefix = blueprint.metadata.get("url_prefix", "")
124
+
125
+ if not module_path:
126
+ logger.debug(f"No 'urls' module specified in metadata for {blueprint.__class__.__name__}; skipping generic URL registration.")
127
+ return
128
+
129
+ try:
130
+ from django.urls import include, path
131
+ from importlib import import_module
132
+
133
+ root_urlconf_name = settings.ROOT_URLCONF
134
+ if not root_urlconf_name:
135
+ logger.error("settings.ROOT_URLCONF is not set.")
136
+ return
137
+
138
+ # --- Get the root urlpatterns list directly ---
139
+ # This is potentially fragile if ROOT_URLCONF itself changes, but necessary for tests
140
+ try:
141
+ root_urlconf_module = import_module(root_urlconf_name)
142
+ if not hasattr(root_urlconf_module, 'urlpatterns') or not isinstance(root_urlconf_module.urlpatterns, list):
143
+ logger.error(f"Cannot modify urlpatterns in '{root_urlconf_name}'. It's missing or not a list.")
144
+ return
145
+ root_urlpatterns = root_urlconf_module.urlpatterns
146
+ except ImportError:
147
+ logger.error(f"Could not import main URLconf '{root_urlconf_name}' to modify urlpatterns.")
148
+ return
149
+
150
+ # Import the blueprint's URL module
151
+ try:
152
+ urls_module = import_module(module_path)
153
+ if not hasattr(urls_module, "urlpatterns"):
154
+ logger.debug(f"Blueprint URL module '{module_path}' has no 'urlpatterns'.")
155
+ blueprint._urls_registered = True
156
+ return
157
+ except ImportError:
158
+ logger.error(f"Could not import blueprint URL module: '{module_path}'")
159
+ return
160
+
161
+ if url_prefix and not url_prefix.endswith('/'): url_prefix += '/'
162
+ app_name = blueprint.metadata.get("cli_name", blueprint.__class__.__name__.lower())
163
+ new_pattern = path(url_prefix, include((urls_module, app_name)))
164
+
165
+ # Check if an identical pattern already exists
166
+ already_exists = False
167
+ for existing_pattern in root_urlpatterns:
168
+ # Compare based on pattern regex and included module/app_name if possible
169
+ if (isinstance(existing_pattern, (URLPattern, URLResolver)) and
170
+ str(existing_pattern.pattern) == str(new_pattern.pattern) and
171
+ getattr(existing_pattern, 'app_name', None) == app_name and
172
+ getattr(existing_pattern, 'namespace', None) == getattr(new_pattern, 'namespace', None)): # Check namespace too
173
+ # A bit more robust check, might need refinement
174
+ logger.warning(f"URL pattern for prefix '{url_prefix}' and app '{app_name}' seems already registered. Skipping.")
175
+ already_exists = True
176
+ break
177
+
178
+ if not already_exists:
179
+ root_urlpatterns.append(new_pattern)
180
+ logger.info(f"Dynamically registered URLs from '{module_path}' at prefix '{url_prefix}' (app_name: '{app_name}')")
181
+
182
+ # --- Force update of URL resolver ---
183
+ clear_url_caches()
184
+ # Reload the root URLconf module itself
185
+ try:
186
+ reload(root_urlconf_module)
187
+ logger.debug(f"Reloaded root URLconf module: {root_urlconf_name}")
188
+ except Exception as e:
189
+ logger.error(f"Failed to reload root URLconf module: {e}")
190
+ # Try setting urlconf to None to force re-reading from settings
191
+ set_urlconf(None)
192
+ # Explicitly getting the resolver again might help
193
+ resolver = get_resolver(get_urlconf())
194
+ resolver._populate() # Re-populate cache
195
+ logger.info(f"Cleared URL caches and attempted to refresh resolver for {root_urlconf_name}.")
196
+
197
+ blueprint._urls_registered = True
198
+
199
+ except ImportError as e:
200
+ logger.error(f"Import error during URL registration for {blueprint.__class__.__name__}: {e}")
201
+ except Exception as e:
202
+ logger.error(f"Unexpected error registering URLs for {blueprint.__class__.__name__}: {e}", exc_info=True)
203
+
@@ -0,0 +1,102 @@
1
+ """
2
+ Interactive mode logic for blueprint extensions.
3
+ """
4
+
5
+ import logging
6
+ from typing import List, Dict, Any # Added Any
7
+
8
+ # Import the standalone output function
9
+ from .output_utils import pretty_print_response
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def run_interactive_mode(blueprint, stream: bool = False) -> None:
14
+ """
15
+ Run the interactive mode for a blueprint instance.
16
+
17
+ This function implements the interactive loop where the user is
18
+ prompted for input,
19
+ and responses are generated and printed using the blueprint inst
20
+ ance's methods.
21
+ """
22
+ logger.debug("Starting interactive mode.")
23
+ if not blueprint.starting_agent or not blueprint.swarm:
24
+ logger.error("Starting agent or Swarm not initialized.")
25
+ # --- FIX: Terminate string literal correctly ---
26
+ raise ValueError("Starting agent and Swarm must be initialized.")
27
+ # --- End FIX ---
28
+
29
+ print("Blueprint Interactive Mode 🐝")
30
+ messages: List[Dict[str, Any]] = []
31
+ first_input = True
32
+ message_count = 0
33
+ while True:
34
+ spinner = getattr(blueprint, 'spinner', None)
35
+ if spinner: spinner.stop()
36
+
37
+ try:
38
+ user_input = input(blueprint.prompt).strip()
39
+ except (EOFError, KeyboardInterrupt):
40
+ print("\nExiting interactive mode.")
41
+ break
42
+
43
+ if user_input.lower() in {"exit", "quit", "/quit"}:
44
+ print("Exiting interactive mode.")
45
+ break
46
+ if first_input:
47
+ blueprint.context_variables["user_goal"] = user_input
48
+ first_input = False
49
+ messages.append({"role": "user", "content": user_input})
50
+ message_count += 1
51
+
52
+ try:
53
+ result = blueprint.run_with_context(messages, blueprint.context_variables)
54
+ swarm_response = result.get("response")
55
+ blueprint.context_variables = result.get("context_variables", blueprint.context_variables)
56
+
57
+ response_messages_objects = []
58
+ if hasattr(swarm_response, 'messages'):
59
+ response_messages_objects = swarm_response.messages
60
+ elif isinstance(swarm_response, dict) and 'messages' in swarm_response:
61
+ raw_msgs = swarm_response.get('messages', [])
62
+ if raw_msgs and not isinstance(raw_msgs[0], dict):
63
+ try: response_messages_objects = raw_msgs
64
+ except Exception: logger.error("Failed to process messages from dict response."); response_messages_objects = []
65
+ else: response_messages_objects = raw_msgs
66
+
67
+ response_messages_dicts = []
68
+ if response_messages_objects:
69
+ try:
70
+ response_messages_dicts = [
71
+ msg.model_dump(exclude_none=True) if hasattr(msg, 'model_dump') else msg
72
+ for msg in response_messages_objects
73
+ ]
74
+ except Exception as e:
75
+ logger.error(f"Failed to dump response messages to dict: {e}")
76
+ response_messages_dicts = [{"role": "system", "content": "[Error displaying response]"}]
77
+
78
+ if stream:
79
+ logger.warning("Streaming not fully supported in this interactive mode version.")
80
+ pretty_print_response(messages=response_messages_dicts, use_markdown=getattr(blueprint, 'use_markdown', False), spinner=spinner)
81
+ else:
82
+ pretty_print_response(messages=response_messages_dicts, use_markdown=getattr(blueprint, 'use_markdown', False), spinner=spinner)
83
+
84
+ messages.extend(response_messages_dicts)
85
+
86
+ if getattr(blueprint, 'update_user_goal', False) and \
87
+ (message_count - getattr(blueprint, 'last_goal_update_count', 0)) >= \
88
+ getattr(blueprint, 'update_user_goal_frequency', 5):
89
+ try:
90
+ import asyncio
91
+ asyncio.run(blueprint._update_user_goal_async(messages))
92
+ blueprint.last_goal_update_count = message_count
93
+ except AttributeError: logger.warning("Blueprint missing '_update_user_goal_async'.")
94
+ except Exception as e: logger.error(f"Error updating goal: {e}", exc_info=True)
95
+
96
+ if getattr(blueprint, 'auto_complete_task', False):
97
+ logger.warning("Auto-complete task not implemented in this interactive loop version.")
98
+
99
+ except Exception as e:
100
+ logger.error(f"Error during interactive loop turn: {e}", exc_info=True)
101
+ print(f"\n[An error occurred: {e}]")
102
+
@@ -0,0 +1,37 @@
1
+ import logging
2
+ import subprocess
3
+ import sys
4
+ import os
5
+ from swarm.utils.color_utils import color_text
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ def run_rest_mode(agent):
10
+ """
11
+ Launches the Django development server to serve REST endpoints.
12
+
13
+ Args:
14
+ agent: The agent object passed in by main.py, not actually used here.
15
+ """
16
+ try:
17
+ logger.info("Launching Django server for REST mode...")
18
+
19
+ # Retrieve host and port from environment variables, defaulting to 0.0.0.0:8000
20
+ host = os.getenv("HOST", "0.0.0.0")
21
+ port = os.getenv("PORT", "8000")
22
+
23
+ logger.info(f"Using host '{host}' and port '{port}' for the Django server.")
24
+ print(color_text(f"Starting Django REST server on http://{host}:{port}", "cyan"))
25
+
26
+ # Use subprocess to run the Django server with the specified host and port
27
+ subprocess.run(
28
+ [sys.executable, "manage.py", "runserver", f"{host}:{port}"],
29
+ check=True
30
+ )
31
+
32
+ except subprocess.CalledProcessError as e:
33
+ logger.error(f"Failed to launch Django server: {e}")
34
+ print(color_text(f"Failed to launch Django server: {e}", "red"))
35
+ except Exception as e:
36
+ logger.error(f"Unexpected error in run_rest_mode: {e}", exc_info=True)
37
+ print(color_text(f"Unexpected error in run_rest_mode: {e}", "red"))
@@ -0,0 +1,95 @@
1
+ """
2
+ Output utilities for Swarm blueprints.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+ from typing import List, Dict, Any
9
+
10
+ # Optional import for markdown rendering
11
+ try:
12
+ from rich.markdown import Markdown
13
+ from rich.console import Console
14
+ RICH_AVAILABLE = True
15
+ except ImportError:
16
+ RICH_AVAILABLE = False
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ def render_markdown(content: str) -> None:
21
+ """Render markdown content using rich, if available."""
22
+ # --- DEBUG PRINT ---
23
+ print(f"\n[DEBUG render_markdown called with rich={RICH_AVAILABLE}]", flush=True)
24
+ if not RICH_AVAILABLE:
25
+ print(content, flush=True) # Fallback print with flush
26
+ return
27
+ console = Console()
28
+ md = Markdown(content)
29
+ console.print(md) # Rich handles flushing
30
+
31
+ def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = False, spinner=None) -> None:
32
+ """Format and print messages, optionally rendering assistant content as markdown."""
33
+ # --- DEBUG PRINT ---
34
+ print(f"\n[DEBUG pretty_print_response called with {len(messages)} messages, use_markdown={use_markdown}]", flush=True)
35
+
36
+ if spinner:
37
+ spinner.stop()
38
+ sys.stdout.write("\r\033[K") # Clear spinner line
39
+ sys.stdout.flush()
40
+
41
+ if not messages:
42
+ logger.debug("No messages to print in pretty_print_response.")
43
+ return
44
+
45
+ for i, msg in enumerate(messages):
46
+ # --- DEBUG PRINT ---
47
+ print(f"\n[DEBUG Processing message {i}: type={type(msg)}]", flush=True)
48
+ if not isinstance(msg, dict):
49
+ print(f"[DEBUG Skipping non-dict message {i}]", flush=True)
50
+ continue
51
+
52
+ role = msg.get("role")
53
+ sender = msg.get("sender", role if role else "Unknown")
54
+ msg_content = msg.get("content")
55
+ tool_calls = msg.get("tool_calls")
56
+ # --- DEBUG PRINT ---
57
+ print(f"[DEBUG Message {i}: role={role}, sender={sender}, has_content={bool(msg_content)}, has_tools={bool(tool_calls)}]", flush=True)
58
+
59
+
60
+ if role == "assistant":
61
+ print(f"\033[94m{sender}\033[0m: ", end="", flush=True)
62
+ if msg_content:
63
+ # --- DEBUG PRINT ---
64
+ print(f"\n[DEBUG Assistant content found, printing/rendering... Rich={RICH_AVAILABLE}, Markdown={use_markdown}]", flush=True)
65
+ if use_markdown and RICH_AVAILABLE:
66
+ render_markdown(msg_content)
67
+ else:
68
+ # --- DEBUG PRINT ---
69
+ print(f"\n[DEBUG Using standard print for content:]", flush=True)
70
+ print(msg_content, flush=True) # Added flush
71
+ elif not tool_calls:
72
+ print(flush=True) # Flush newline if no content/tools
73
+
74
+ if tool_calls and isinstance(tool_calls, list):
75
+ print(" \033[92mTool Calls:\033[0m", flush=True)
76
+ for tc in tool_calls:
77
+ if not isinstance(tc, dict): continue
78
+ func = tc.get("function", {})
79
+ tool_name = func.get("name", "Unnamed Tool")
80
+ args_str = func.get("arguments", "{}")
81
+ try: args_obj = json.loads(args_str); args_pretty = ", ".join(f"{k}={v!r}" for k, v in args_obj.items())
82
+ except json.JSONDecodeError: args_pretty = args_str
83
+ print(f" \033[95m{tool_name}\033[0m({args_pretty})", flush=True)
84
+
85
+ elif role == "tool":
86
+ tool_name = msg.get("tool_name", msg.get("name", "tool"))
87
+ tool_id = msg.get("tool_call_id", "N/A")
88
+ try: content_obj = json.loads(msg_content); pretty_content = json.dumps(content_obj, indent=2)
89
+ except (json.JSONDecodeError, TypeError): pretty_content = msg_content
90
+ print(f" \033[93m[{tool_name} Result ID: {tool_id}]\033[0m:\n {pretty_content.replace(chr(10), chr(10) + ' ')}", flush=True)
91
+ else:
92
+ # --- DEBUG PRINT ---
93
+ print(f"[DEBUG Skipping message {i} with role '{role}']", flush=True)
94
+
95
+
@@ -0,0 +1,91 @@
1
+ """
2
+ Simple terminal spinner for interactive feedback during long operations.
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ import threading
8
+ import time
9
+
10
+ class Spinner:
11
+ """Simple terminal spinner for interactive feedback."""
12
+ # Define spinner characters (can be customized)
13
+ SPINNER_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
14
+ # SPINNER_CHARS = ['|', '/', '-', '\\'] # Simpler alternative
15
+
16
+ def __init__(self, interactive: bool):
17
+ """
18
+ Initialize the spinner.
19
+
20
+ Args:
21
+ interactive (bool): Hint whether the environment is interactive.
22
+ Spinner is disabled if False or if output is not a TTY.
23
+ """
24
+ self.interactive = interactive
25
+ # Check if output is a TTY (terminal) and interactive flag is True
26
+ self.is_tty = sys.stdout.isatty()
27
+ self.enabled = self.interactive and self.is_tty
28
+ self.running = False
29
+ self.thread: Optional[threading.Thread] = None
30
+ self.status = ""
31
+ self.index = 0
32
+
33
+ def start(self, status: str = "Processing..."):
34
+ """Start the spinner with an optional status message."""
35
+ if not self.enabled or self.running:
36
+ return # Do nothing if disabled or already running
37
+ self.status = status
38
+ self.running = True
39
+ # Run the spinner animation in a separate daemon thread
40
+ self.thread = threading.Thread(target=self._spin, daemon=True)
41
+ self.thread.start()
42
+
43
+ def stop(self):
44
+ """Stop the spinner and clear the line."""
45
+ if not self.enabled or not self.running:
46
+ return # Do nothing if disabled or not running
47
+ self.running = False
48
+ if self.thread is not None:
49
+ self.thread.join() # Wait for the thread to finish
50
+ # Clear the spinner line using ANSI escape codes
51
+ # \r: Carriage return (move cursor to beginning of line)
52
+ # \033[K: Clear line from cursor to end
53
+ sys.stdout.write("\r\033[K")
54
+ sys.stdout.flush()
55
+ self.thread = None # Reset thread
56
+
57
+ def _spin(self):
58
+ """Internal method running in the spinner thread to animate."""
59
+ while self.running:
60
+ # Get the next spinner character
61
+ char = self.SPINNER_CHARS[self.index % len(self.SPINNER_CHARS)]
62
+ # Write spinner char and status, overwrite previous line content
63
+ try:
64
+ # \r moves cursor to beginning, \033[K clears the rest of the line
65
+ sys.stdout.write(f"\r{char} {self.status}\033[K")
66
+ sys.stdout.flush()
67
+ except BlockingIOError:
68
+ # Handle potential issues if stdout is blocked (less likely for TTY)
69
+ time.sleep(0.1)
70
+ continue
71
+ self.index += 1
72
+ # Pause for animation effect
73
+ time.sleep(0.1)
74
+
75
+ # Example usage (if run directly)
76
+ if __name__ == "__main__":
77
+ print("Starting spinner test...")
78
+ s = Spinner(interactive=True) # Assume interactive for testing
79
+ s.start("Doing something cool")
80
+ try:
81
+ time.sleep(5) # Simulate work
82
+ s.stop()
83
+ print("Spinner stopped.")
84
+ s.start("Doing another thing")
85
+ time.sleep(3)
86
+ except KeyboardInterrupt:
87
+ print("\nInterrupted.")
88
+ finally:
89
+ s.stop() # Ensure spinner stops on exit/error
90
+ print("Test finished.")
91
+