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.
- open_swarm-0.1.1743070217.dist-info/METADATA +258 -0
- open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
- open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
- open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
- swarm/__init__.py +3 -0
- swarm/agent/__init__.py +7 -0
- swarm/agent/agent.py +49 -0
- swarm/apps.py +53 -0
- swarm/auth.py +56 -0
- swarm/consumers.py +141 -0
- swarm/core.py +326 -0
- swarm/extensions/__init__.py +1 -0
- swarm/extensions/blueprint/__init__.py +36 -0
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/blueprint_utils.py +17 -0
- swarm/extensions/blueprint/common_utils.py +12 -0
- swarm/extensions/blueprint/django_utils.py +203 -0
- swarm/extensions/blueprint/interactive_mode.py +102 -0
- swarm/extensions/blueprint/modes/rest_mode.py +37 -0
- swarm/extensions/blueprint/output_utils.py +95 -0
- swarm/extensions/blueprint/spinner.py +91 -0
- swarm/extensions/cli/__init__.py +0 -0
- swarm/extensions/cli/blueprint_runner.py +251 -0
- swarm/extensions/cli/cli_args.py +88 -0
- swarm/extensions/cli/commands/__init__.py +0 -0
- swarm/extensions/cli/commands/blueprint_management.py +31 -0
- swarm/extensions/cli/commands/config_management.py +15 -0
- swarm/extensions/cli/commands/edit_config.py +77 -0
- swarm/extensions/cli/commands/list_blueprints.py +22 -0
- swarm/extensions/cli/commands/validate_env.py +57 -0
- swarm/extensions/cli/commands/validate_envvars.py +39 -0
- swarm/extensions/cli/interactive_shell.py +41 -0
- swarm/extensions/cli/main.py +36 -0
- swarm/extensions/cli/selection.py +43 -0
- swarm/extensions/cli/utils/discover_commands.py +32 -0
- swarm/extensions/cli/utils/env_setup.py +15 -0
- swarm/extensions/cli/utils.py +105 -0
- swarm/extensions/config/__init__.py +6 -0
- swarm/extensions/config/config_loader.py +208 -0
- swarm/extensions/config/config_manager.py +258 -0
- swarm/extensions/config/server_config.py +49 -0
- swarm/extensions/config/setup_wizard.py +103 -0
- swarm/extensions/config/utils/__init__.py +0 -0
- swarm/extensions/config/utils/logger.py +36 -0
- swarm/extensions/launchers/__init__.py +1 -0
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
- swarm/extensions/launchers/swarm_api.py +68 -0
- swarm/extensions/launchers/swarm_cli.py +304 -0
- swarm/extensions/launchers/swarm_wrapper.py +29 -0
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +36 -0
- swarm/extensions/mcp/mcp_client.py +341 -0
- swarm/extensions/mcp/mcp_constants.py +7 -0
- swarm/extensions/mcp/mcp_tool_provider.py +110 -0
- swarm/llm/chat_completion.py +195 -0
- swarm/messages.py +132 -0
- swarm/migrations/0010_initial_chat_models.py +51 -0
- swarm/migrations/__init__.py +0 -0
- swarm/models.py +45 -0
- swarm/repl/__init__.py +1 -0
- swarm/repl/repl.py +87 -0
- swarm/serializers.py +12 -0
- swarm/settings.py +189 -0
- swarm/tool_executor.py +239 -0
- swarm/types.py +126 -0
- swarm/urls.py +89 -0
- swarm/util.py +124 -0
- swarm/utils/color_utils.py +40 -0
- swarm/utils/context_utils.py +272 -0
- swarm/utils/general_utils.py +162 -0
- swarm/utils/logger.py +61 -0
- swarm/utils/logger_setup.py +25 -0
- swarm/utils/message_sequence.py +173 -0
- swarm/utils/message_utils.py +95 -0
- swarm/utils/redact.py +68 -0
- swarm/views/__init__.py +41 -0
- swarm/views/api_views.py +46 -0
- swarm/views/chat_views.py +76 -0
- swarm/views/core_views.py +118 -0
- swarm/views/message_views.py +40 -0
- swarm/views/model_views.py +135 -0
- swarm/views/utils.py +457 -0
- swarm/views/web_views.py +149 -0
- 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
|
+
|