open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636259__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.1748636259.dist-info/METADATA +188 -0
- open_swarm-0.1.1748636259.dist-info/RECORD +82 -0
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636259.dist-info}/WHEEL +2 -1
- open_swarm-0.1.1748636259.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1748636259.dist-info/top_level.txt +1 -0
- swarm/agent/agent.py +49 -0
- swarm/auth.py +48 -113
- swarm/consumers.py +0 -19
- swarm/extensions/blueprint/__init__.py +16 -30
- swarm/{core → extensions/blueprint}/agent_utils.py +1 -1
- swarm/extensions/blueprint/blueprint_base.py +458 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/output_utils.py +95 -0
- swarm/{core → extensions/blueprint}/spinner.py +21 -30
- swarm/extensions/cli/cli_args.py +0 -6
- swarm/extensions/cli/commands/blueprint_management.py +9 -47
- swarm/extensions/cli/commands/config_management.py +6 -5
- swarm/extensions/cli/commands/edit_config.py +7 -16
- swarm/extensions/cli/commands/list_blueprints.py +1 -1
- swarm/extensions/cli/commands/validate_env.py +4 -11
- swarm/extensions/cli/commands/validate_envvars.py +6 -6
- swarm/extensions/cli/interactive_shell.py +2 -16
- swarm/extensions/config/config_loader.py +201 -107
- swarm/{core → extensions/config}/config_manager.py +38 -50
- swarm/{core → extensions/config}/server_config.py +0 -32
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_api.py +64 -8
- swarm/extensions/launchers/swarm_cli.py +300 -8
- swarm/llm/chat_completion.py +195 -0
- swarm/serializers.py +5 -96
- swarm/settings.py +111 -99
- swarm/urls.py +74 -57
- swarm/utils/context_utils.py +4 -10
- swarm/utils/general_utils.py +0 -21
- swarm/utils/redact.py +36 -23
- swarm/views/api_views.py +39 -48
- swarm/views/chat_views.py +70 -237
- swarm/views/core_views.py +87 -80
- swarm/views/model_views.py +121 -64
- swarm/views/utils.py +441 -65
- swarm/views/web_views.py +2 -2
- open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
- open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
- open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
- swarm/blueprints/README.md +0 -68
- swarm/blueprints/blueprint_audit_status.json +0 -27
- swarm/blueprints/chatbot/README.md +0 -40
- swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
- swarm/blueprints/chatbot/metadata.json +0 -23
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
- swarm/blueprints/chucks_angels/README.md +0 -11
- swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
- swarm/blueprints/chucks_angels/test_basic.py +0 -3
- swarm/blueprints/codey/CODEY.md +0 -15
- swarm/blueprints/codey/README.md +0 -115
- swarm/blueprints/codey/blueprint_codey.py +0 -1072
- swarm/blueprints/codey/codey_cli.py +0 -373
- swarm/blueprints/codey/instructions.md +0 -17
- swarm/blueprints/codey/metadata.json +0 -23
- swarm/blueprints/common/operation_box_utils.py +0 -83
- swarm/blueprints/digitalbutlers/README.md +0 -11
- swarm/blueprints/digitalbutlers/__init__.py +0 -1
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
- swarm/blueprints/digitalbutlers/test_basic.py +0 -3
- swarm/blueprints/divine_code/README.md +0 -3
- swarm/blueprints/divine_code/__init__.py +0 -10
- swarm/blueprints/divine_code/apps.py +0 -11
- swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
- swarm/blueprints/django_chat/apps.py +0 -6
- swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
- swarm/blueprints/django_chat/urls.py +0 -8
- swarm/blueprints/django_chat/views.py +0 -32
- swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
- swarm/blueprints/flock/README.md +0 -11
- swarm/blueprints/flock/__init__.py +0 -8
- swarm/blueprints/flock/blueprint_flock.py +0 -7
- swarm/blueprints/flock/test_basic.py +0 -3
- swarm/blueprints/geese/README.md +0 -10
- swarm/blueprints/geese/__init__.py +0 -8
- swarm/blueprints/geese/blueprint_geese.py +0 -384
- swarm/blueprints/geese/geese_cli.py +0 -102
- swarm/blueprints/jeeves/README.md +0 -41
- swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
- swarm/blueprints/jeeves/jeeves_cli.py +0 -55
- swarm/blueprints/jeeves/metadata.json +0 -24
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
- swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
- swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
- swarm/blueprints/poets/blueprint_poets.py +0 -546
- swarm/blueprints/poets/poets_cli.py +0 -23
- swarm/blueprints/rue_code/README.md +0 -8
- swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
- swarm/blueprints/rue_code/rue_code_cli.py +0 -43
- swarm/blueprints/stewie/apps.py +0 -12
- swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
- swarm/blueprints/stewie/models.py +0 -19
- swarm/blueprints/stewie/serializers.py +0 -10
- swarm/blueprints/stewie/settings.py +0 -17
- swarm/blueprints/stewie/urls.py +0 -11
- swarm/blueprints/stewie/views.py +0 -26
- swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
- swarm/blueprints/whinge_surf/README.md +0 -22
- swarm/blueprints/whinge_surf/__init__.py +0 -1
- swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
- swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
- swarm/blueprints/zeus/__init__.py +0 -2
- swarm/blueprints/zeus/apps.py +0 -4
- swarm/blueprints/zeus/blueprint_zeus.py +0 -270
- swarm/blueprints/zeus/zeus_cli.py +0 -13
- swarm/cli/async_input.py +0 -65
- swarm/cli/async_input_demo.py +0 -32
- swarm/core/blueprint_base.py +0 -769
- swarm/core/blueprint_discovery.py +0 -125
- swarm/core/blueprint_runner.py +0 -59
- swarm/core/blueprint_ux.py +0 -109
- swarm/core/build_launchers.py +0 -15
- swarm/core/cli/__init__.py +0 -1
- swarm/core/cli/commands/__init__.py +0 -1
- swarm/core/cli/commands/blueprint_management.py +0 -7
- swarm/core/cli/interactive_shell.py +0 -14
- swarm/core/cli/main.py +0 -50
- swarm/core/cli/utils/__init__.py +0 -1
- swarm/core/cli/utils/discover_commands.py +0 -18
- swarm/core/config_loader.py +0 -122
- swarm/core/output_utils.py +0 -193
- swarm/core/session_logger.py +0 -42
- swarm/core/slash_commands.py +0 -89
- swarm/core/swarm_api.py +0 -68
- swarm/core/swarm_cli.py +0 -216
- swarm/core/utils/__init__.py +0 -0
- swarm/extensions/blueprint/cli_handler.py +0 -197
- swarm/extensions/blueprint/runnable_blueprint.py +0 -42
- swarm/extensions/cli/utils/__init__.py +0 -1
- swarm/extensions/cli/utils/async_input.py +0 -46
- swarm/extensions/cli/utils/prompt_user.py +0 -3
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +0 -58
- swarm/middleware.py +0 -65
- swarm/permissions.py +0 -38
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
- swarm/static/contrib/markedjs/marked.min.js +0 -6
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
- swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
- swarm/static/contrib/tabler-icons/archive.svg +0 -21
- swarm/static/contrib/tabler-icons/artboard.svg +0 -27
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
- swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
- swarm/static/contrib/tabler-icons/carambola.svg +0 -19
- swarm/static/contrib/tabler-icons/copy.svg +0 -20
- swarm/static/contrib/tabler-icons/download.svg +0 -21
- swarm/static/contrib/tabler-icons/edit.svg +0 -21
- swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
- swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
- swarm/static/contrib/tabler-icons/headset.svg +0 -22
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
- swarm/static/contrib/tabler-icons/message-star.svg +0 -22
- swarm/static/contrib/tabler-icons/message-x.svg +0 -23
- swarm/static/contrib/tabler-icons/message.svg +0 -21
- swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
- swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
- swarm/static/contrib/tabler-icons/robot.svg +0 -26
- swarm/static/contrib/tabler-icons/search.svg +0 -19
- swarm/static/contrib/tabler-icons/settings.svg +0 -20
- swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
- swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
- swarm/static/css/dropdown.css +0 -22
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +0 -23
- swarm/static/rest_mode/css/base.css +0 -470
- swarm/static/rest_mode/css/chat-history.css +0 -286
- swarm/static/rest_mode/css/chat.css +0 -251
- swarm/static/rest_mode/css/chatbot.css +0 -74
- swarm/static/rest_mode/css/chatgpt.css +0 -62
- swarm/static/rest_mode/css/colors/corporate.css +0 -74
- swarm/static/rest_mode/css/colors/pastel.css +0 -81
- swarm/static/rest_mode/css/colors/tropical.css +0 -82
- swarm/static/rest_mode/css/general.css +0 -142
- swarm/static/rest_mode/css/layout.css +0 -167
- swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
- swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
- swarm/static/rest_mode/css/messages.css +0 -84
- swarm/static/rest_mode/css/messenger.css +0 -135
- swarm/static/rest_mode/css/settings.css +0 -91
- swarm/static/rest_mode/css/simple.css +0 -44
- swarm/static/rest_mode/css/slack.css +0 -58
- swarm/static/rest_mode/css/style.css +0 -156
- swarm/static/rest_mode/css/theme.css +0 -30
- swarm/static/rest_mode/css/toast.css +0 -40
- swarm/static/rest_mode/js/auth.js +0 -9
- swarm/static/rest_mode/js/blueprint.js +0 -41
- swarm/static/rest_mode/js/blueprintUtils.js +0 -12
- swarm/static/rest_mode/js/chatLogic.js +0 -79
- swarm/static/rest_mode/js/debug.js +0 -63
- swarm/static/rest_mode/js/events.js +0 -98
- swarm/static/rest_mode/js/main.js +0 -19
- swarm/static/rest_mode/js/messages.js +0 -264
- swarm/static/rest_mode/js/messengerLogic.js +0 -355
- swarm/static/rest_mode/js/modules/apiService.js +0 -84
- swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
- swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
- swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
- swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
- swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
- swarm/static/rest_mode/js/modules/state.js +0 -7
- swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
- swarm/static/rest_mode/js/modules/validation.js +0 -23
- swarm/static/rest_mode/js/rendering.js +0 -119
- swarm/static/rest_mode/js/settings.js +0 -130
- swarm/static/rest_mode/js/sidebar.js +0 -94
- swarm/static/rest_mode/js/simpleLogic.js +0 -37
- swarm/static/rest_mode/js/slackLogic.js +0 -66
- swarm/static/rest_mode/js/splash.js +0 -76
- swarm/static/rest_mode/js/theme.js +0 -111
- swarm/static/rest_mode/js/toast.js +0 -36
- swarm/static/rest_mode/js/ui.js +0 -265
- swarm/static/rest_mode/js/validation.js +0 -57
- swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
- swarm/static/rest_mode/svg/arrow_down.svg +0 -5
- swarm/static/rest_mode/svg/arrow_left.svg +0 -5
- swarm/static/rest_mode/svg/arrow_right.svg +0 -5
- swarm/static/rest_mode/svg/arrow_up.svg +0 -5
- swarm/static/rest_mode/svg/attach.svg +0 -8
- swarm/static/rest_mode/svg/avatar.svg +0 -7
- swarm/static/rest_mode/svg/canvas.svg +0 -6
- swarm/static/rest_mode/svg/chat_history.svg +0 -4
- swarm/static/rest_mode/svg/close.svg +0 -5
- swarm/static/rest_mode/svg/copy.svg +0 -4
- swarm/static/rest_mode/svg/dark_mode.svg +0 -3
- swarm/static/rest_mode/svg/edit.svg +0 -5
- swarm/static/rest_mode/svg/layout.svg +0 -9
- swarm/static/rest_mode/svg/logo.svg +0 -29
- swarm/static/rest_mode/svg/logout.svg +0 -5
- swarm/static/rest_mode/svg/mobile.svg +0 -5
- swarm/static/rest_mode/svg/new_chat.svg +0 -4
- swarm/static/rest_mode/svg/not_visible.svg +0 -5
- swarm/static/rest_mode/svg/plus.svg +0 -7
- swarm/static/rest_mode/svg/run_code.svg +0 -6
- swarm/static/rest_mode/svg/save.svg +0 -4
- swarm/static/rest_mode/svg/search.svg +0 -6
- swarm/static/rest_mode/svg/settings.svg +0 -4
- swarm/static/rest_mode/svg/speaker.svg +0 -5
- swarm/static/rest_mode/svg/stop.svg +0 -6
- swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
- swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
- swarm/static/rest_mode/svg/toggle_off.svg +0 -6
- swarm/static/rest_mode/svg/toggle_on.svg +0 -6
- swarm/static/rest_mode/svg/trash.svg +0 -10
- swarm/static/rest_mode/svg/undo.svg +0 -3
- swarm/static/rest_mode/svg/visible.svg +0 -8
- swarm/static/rest_mode/svg/voice.svg +0 -10
- swarm/templates/account/login.html +0 -22
- swarm/templates/account/signup.html +0 -32
- swarm/templates/base.html +0 -30
- swarm/templates/chat.html +0 -43
- swarm/templates/index.html +0 -35
- swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
- swarm/templates/rest_mode/components/header.html +0 -45
- swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
- swarm/templates/rest_mode/components/settings_dialog.html +0 -97
- swarm/templates/rest_mode/components/splash_screen.html +0 -7
- swarm/templates/rest_mode/components/top_bar.html +0 -28
- swarm/templates/rest_mode/message_ui.html +0 -50
- swarm/templates/rest_mode/slackbot.html +0 -30
- swarm/templates/simple_blueprint_page.html +0 -24
- swarm/templates/websocket_partials/final_system_message.html +0 -3
- swarm/templates/websocket_partials/system_message.html +0 -4
- swarm/templates/websocket_partials/user_message.html +0 -5
- swarm/utils/ansi_box.py +0 -34
- swarm/utils/disable_tracing.py +0 -38
- swarm/utils/log_utils.py +0 -63
- swarm/utils/openai_patch.py +0 -33
- swarm/ux/ansi_box.py +0 -43
- swarm/ux/spinner.py +0 -53
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636259.dist-info}/licenses/LICENSE +0 -0
- /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
- /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
- /swarm/{core → extensions/config}/setup_wizard.py +0 -0
- /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
- /swarm/{core → extensions/config}/utils/logger.py +0 -0
- /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
swarm/utils/redact.py
CHANGED
@@ -13,43 +13,56 @@ DEFAULT_SENSITIVE_KEYS = ["secret", "password", "api_key", "apikey", "token", "a
|
|
13
13
|
def redact_sensitive_data(
|
14
14
|
data: Union[str, Dict, List],
|
15
15
|
sensitive_keys: Optional[List[str]] = None,
|
16
|
-
reveal_chars: int =
|
16
|
+
reveal_chars: int = 4,
|
17
17
|
mask: str = "[REDACTED]"
|
18
18
|
) -> Union[str, Dict, List]:
|
19
19
|
"""
|
20
20
|
Recursively redact sensitive information from dictionaries or lists based on keys.
|
21
|
-
|
22
|
-
If reveal_chars > 0, partially masks (preserves reveal_chars at start/end).
|
23
|
-
If a custom mask is provided, always use it (for test compatibility).
|
21
|
+
Applies partial redaction to string values associated with sensitive keys.
|
24
22
|
Does NOT redact standalone strings.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
data: Input data to redact (dict or list). Other types returned as is.
|
26
|
+
sensitive_keys: List of dictionary keys to treat as sensitive. Defaults to common keys.
|
27
|
+
reveal_chars: Number of initial/trailing characters to reveal (0 means full redaction).
|
28
|
+
mask: String used for redaction in the middle or for full redaction of strings.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Redacted data structure of the same type as input.
|
25
32
|
"""
|
26
|
-
keys_to_redact =
|
27
|
-
|
28
|
-
|
29
|
-
return val
|
30
|
-
if mask != "[REDACTED]":
|
31
|
-
return mask
|
32
|
-
if reveal_chars == 0:
|
33
|
-
return mask
|
34
|
-
if len(val) >= 2 * reveal_chars + 1:
|
35
|
-
return val[:reveal_chars] + mask + val[-reveal_chars:]
|
36
|
-
return mask
|
33
|
+
keys_to_redact = sensitive_keys if sensitive_keys is not None else DEFAULT_SENSITIVE_KEYS
|
34
|
+
keys_to_redact_lower = {key.lower() for key in keys_to_redact}
|
35
|
+
|
37
36
|
if isinstance(data, dict):
|
38
37
|
redacted_dict = {}
|
39
|
-
for
|
40
|
-
if isinstance(
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
for key, value in data.items():
|
39
|
+
if isinstance(key, str) and key.lower() in keys_to_redact_lower:
|
40
|
+
if isinstance(value, str):
|
41
|
+
val_len = len(value)
|
42
|
+
if reveal_chars > 0 and val_len > reveal_chars * 2:
|
43
|
+
redacted_dict[key] = f"{value[:reveal_chars]}{mask}{value[-reveal_chars:]}"
|
44
|
+
elif val_len > 0:
|
45
|
+
# Use the provided mask string directly for full redaction
|
46
|
+
redacted_dict[key] = mask
|
47
|
+
else:
|
48
|
+
redacted_dict[key] = "" # Redact empty string as empty
|
49
|
+
else:
|
50
|
+
# Use specific placeholder for non-strings
|
51
|
+
redacted_dict[key] = "[REDACTED NON-STRING]"
|
44
52
|
else:
|
45
|
-
|
53
|
+
# Recursively redact nested structures if key is not sensitive
|
54
|
+
redacted_dict[key] = redact_sensitive_data(value, keys_to_redact, reveal_chars, mask)
|
46
55
|
return redacted_dict
|
56
|
+
|
47
57
|
elif isinstance(data, list):
|
58
|
+
# Recursively redact items in a list ONLY if they are dicts or lists themselves.
|
48
59
|
processed_list = []
|
49
60
|
for item in data:
|
50
61
|
if isinstance(item, (dict, list)):
|
51
|
-
processed_list.append(redact_sensitive_data(item,
|
62
|
+
processed_list.append(redact_sensitive_data(item, keys_to_redact, reveal_chars, mask))
|
52
63
|
else:
|
53
|
-
processed_list.append(item)
|
64
|
+
processed_list.append(item) # Keep non-dict/list items (like strings) unchanged
|
54
65
|
return processed_list
|
66
|
+
|
67
|
+
# Return data unchanged if it's not a dict or list (including standalone strings)
|
55
68
|
return data
|
swarm/views/api_views.py
CHANGED
@@ -1,55 +1,46 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
from rest_framework.
|
5
|
-
from rest_framework import status
|
1
|
+
"""
|
2
|
+
API-specific views and viewsets for Open Swarm MCP Core.
|
3
|
+
"""
|
4
|
+
from rest_framework.viewsets import ModelViewSet
|
6
5
|
from rest_framework.permissions import AllowAny
|
7
|
-
|
8
|
-
from
|
6
|
+
from drf_spectacular.views import SpectacularAPIView as BaseSpectacularAPIView
|
7
|
+
from drf_spectacular.utils import extend_schema
|
8
|
+
from swarm.utils.logger_setup import setup_logger
|
9
|
+
from swarm.models import ChatMessage
|
10
|
+
from swarm.serializers import ChatMessageSerializer
|
9
11
|
|
10
|
-
|
12
|
+
logger = setup_logger(__name__)
|
11
13
|
|
12
|
-
|
14
|
+
class HiddenSpectacularAPIView(BaseSpectacularAPIView):
|
15
|
+
exclude_from_schema = True
|
13
16
|
|
14
|
-
class
|
15
|
-
"""
|
16
|
-
|
17
|
-
"""
|
17
|
+
class ChatMessageViewSet(ModelViewSet):
|
18
|
+
"""API viewset for managing chat messages."""
|
19
|
+
authentication_classes = []
|
18
20
|
permission_classes = [AllowAny]
|
21
|
+
queryset = ChatMessage.objects.all()
|
22
|
+
serializer_class = ChatMessageSerializer
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
available_blueprints = async_to_sync(get_available_blueprints)()
|
24
|
-
|
25
|
-
models_data = []
|
26
|
-
current_time = int(time.time())
|
27
|
-
if isinstance(available_blueprints, dict):
|
28
|
-
blueprint_ids = available_blueprints.keys()
|
29
|
-
elif isinstance(available_blueprints, list):
|
30
|
-
blueprint_ids = available_blueprints
|
31
|
-
else:
|
32
|
-
logger.error(f"Unexpected type from get_available_blueprints: {type(available_blueprints)}")
|
33
|
-
blueprint_ids = []
|
34
|
-
|
35
|
-
for blueprint_id in blueprint_ids:
|
36
|
-
models_data.append({
|
37
|
-
"id": blueprint_id,
|
38
|
-
"object": "model",
|
39
|
-
"created": current_time,
|
40
|
-
"owned_by": "open-swarm",
|
41
|
-
})
|
42
|
-
|
43
|
-
response_payload = {
|
44
|
-
"object": "list",
|
45
|
-
"data": models_data,
|
46
|
-
}
|
47
|
-
return Response(response_payload, status=status.HTTP_200_OK)
|
48
|
-
|
49
|
-
except Exception as e:
|
50
|
-
logger.exception("Error retrieving available models.")
|
51
|
-
return Response(
|
52
|
-
{"error": "Failed to retrieve models list."},
|
53
|
-
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
54
|
-
)
|
24
|
+
@extend_schema(summary="List all chat messages")
|
25
|
+
def list(self, request, *args, **kwargs):
|
26
|
+
return super().list(request, *args, **kwargs)
|
55
27
|
|
28
|
+
@extend_schema(summary="Retrieve a chat message by its unique id")
|
29
|
+
def retrieve(self, request, *args, **kwargs):
|
30
|
+
return super().retrieve(request, *args, **kwargs)
|
31
|
+
|
32
|
+
@extend_schema(summary="Create a new chat message")
|
33
|
+
def create(self, request, *args, **kwargs):
|
34
|
+
return super().create(request, *args, **kwargs)
|
35
|
+
|
36
|
+
@extend_schema(summary="Update an existing chat message")
|
37
|
+
def update(self, request, *args, **kwargs):
|
38
|
+
return super().update(request, *args, **kwargs)
|
39
|
+
|
40
|
+
@extend_schema(summary="Partially update a chat message")
|
41
|
+
def partial_update(self, request, *args, **kwargs):
|
42
|
+
return super().partial_update(request, *args, **kwargs)
|
43
|
+
|
44
|
+
@extend_schema(summary="Delete a chat message by its unique id")
|
45
|
+
def destroy(self, request, *args, **kwargs):
|
46
|
+
return super().destroy(request, *args, **kwargs)
|
swarm/views/chat_views.py
CHANGED
@@ -1,243 +1,76 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
"""
|
2
|
+
Chat-related views for Open Swarm MCP Core.
|
3
|
+
"""
|
4
|
+
import asyncio
|
3
5
|
import logging
|
4
6
|
import json
|
5
|
-
import uuid
|
6
|
-
import time
|
7
|
-
import asyncio
|
8
|
-
from typing import Dict, Any, AsyncGenerator, List, Optional
|
9
|
-
|
10
|
-
from django.shortcuts import render
|
11
|
-
from django.http import StreamingHttpResponse, JsonResponse, Http404, HttpRequest, HttpResponse, HttpResponseBase
|
12
|
-
from django.views import View
|
13
|
-
from django.utils.decorators import method_decorator
|
14
7
|
from django.views.decorators.csrf import csrf_exempt
|
15
|
-
from django.contrib.auth.decorators import login_required
|
16
|
-
from django.conf import settings
|
17
|
-
from django.urls import reverse # Needed for reverse() used in tests
|
18
|
-
|
19
|
-
from rest_framework import status
|
20
|
-
from rest_framework.views import APIView
|
21
8
|
from rest_framework.response import Response
|
22
|
-
from rest_framework.
|
23
|
-
from rest_framework.
|
24
|
-
from
|
25
|
-
|
26
|
-
|
27
|
-
from
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
# Default permission classes are likely set in settings.py
|
61
|
-
# permission_classes = [IsAuthenticated] # Example default
|
62
|
-
|
63
|
-
# --- Internal Helper Methods (Unchanged) ---
|
64
|
-
|
65
|
-
async def _handle_non_streaming(self, blueprint_instance, messages: List[Dict[str, str]], request_id: str, model_name: str) -> Response:
|
66
|
-
""" Handles non-streaming requests. """
|
67
|
-
logger.info(f"[ReqID: {request_id}] Processing non-streaming request for model '{model_name}'.")
|
68
|
-
final_response_data = None; start_time = time.time()
|
69
|
-
try:
|
70
|
-
# The blueprint's run method should be an async generator.
|
71
|
-
async_generator = blueprint_instance.run(messages)
|
72
|
-
async for chunk in async_generator:
|
73
|
-
# Check if the chunk contains the expected final message list.
|
74
|
-
if isinstance(chunk, dict) and "messages" in chunk and isinstance(chunk["messages"], list):
|
75
|
-
final_response_data = chunk["messages"]
|
76
|
-
logger.debug(f"[ReqID: {request_id}] Received final data chunk.")
|
77
|
-
break # Stop after getting the final data
|
78
|
-
else:
|
79
|
-
logger.warning(f"[ReqID: {request_id}] Unexpected chunk format during non-streaming run: {chunk}")
|
80
|
-
|
81
|
-
if not final_response_data or not isinstance(final_response_data, list) or not final_response_data:
|
82
|
-
logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' did not return a valid final message list. Got: {final_response_data}")
|
83
|
-
raise APIException("Blueprint did not return valid data.", code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
84
|
-
|
85
|
-
if not isinstance(final_response_data[0], dict) or 'role' not in final_response_data[0]:
|
86
|
-
logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' returned invalid message structure. Got: {final_response_data[0]}")
|
87
|
-
raise APIException("Blueprint returned invalid message structure.", code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
88
|
-
|
89
|
-
response_payload = { "id": f"chatcmpl-{request_id}", "object": "chat.completion", "created": int(time.time()), "model": model_name, "choices": [{"index": 0, "message": final_response_data[0], "logprobs": None, "finish_reason": "stop"}], "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}, "system_fingerprint": None }
|
90
|
-
end_time = time.time(); logger.info(f"[ReqID: {request_id}] Non-streaming request completed in {end_time - start_time:.2f}s.")
|
91
|
-
return Response(response_payload, status=status.HTTP_200_OK)
|
92
|
-
except APIException: raise
|
93
|
-
except Exception as e: logger.error(f"[ReqID: {request_id}] Unexpected error during non-streaming blueprint execution: {e}", exc_info=True); raise APIException(f"Internal server error during generation: {e}", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
|
94
|
-
|
95
|
-
async def _handle_streaming(self, blueprint_instance, messages: List[Dict[str, str]], request_id: str, model_name: str) -> StreamingHttpResponse:
|
96
|
-
""" Handles streaming requests using SSE. """
|
97
|
-
logger.info(f"[ReqID: {request_id}] Processing streaming request for model '{model_name}'.")
|
98
|
-
async def event_stream():
|
99
|
-
start_time = time.time(); chunk_index = 0
|
100
|
-
try:
|
101
|
-
logger.debug(f"[ReqID: {request_id}] Getting async generator from blueprint.run()..."); async_generator = blueprint_instance.run(messages); logger.debug(f"[ReqID: {request_id}] Got async generator. Starting iteration...")
|
102
|
-
async for chunk in async_generator:
|
103
|
-
logger.debug(f"[ReqID: {request_id}] Received stream chunk {chunk_index}: {chunk}")
|
104
|
-
if not isinstance(chunk, dict) or "messages" not in chunk or not isinstance(chunk["messages"], list) or not chunk["messages"] or not isinstance(chunk["messages"][0], dict): logger.warning(f"[ReqID: {request_id}] Skipping invalid chunk format: {chunk}"); continue
|
105
|
-
delta_content = chunk["messages"][0].get("content"); delta = {"role": "assistant"}
|
106
|
-
if delta_content is not None: delta["content"] = delta_content
|
107
|
-
response_chunk = { "id": f"chatcmpl-{request_id}", "object": "chat.completion.chunk", "created": int(time.time()), "model": model_name, "choices": [{"index": 0, "delta": delta, "logprobs": None, "finish_reason": None}] }
|
108
|
-
logger.debug(f"[ReqID: {request_id}] Sending SSE chunk {chunk_index}"); yield f"data: {json.dumps(response_chunk)}\n\n"; chunk_index += 1; await asyncio.sleep(0.01)
|
109
|
-
logger.debug(f"[ReqID: {request_id}] Finished iterating stream. Sending [DONE]."); yield "data: [DONE]\n\n"; end_time = time.time(); logger.info(f"[ReqID: {request_id}] Streaming request completed in {end_time - start_time:.2f}s.")
|
110
|
-
except APIException as e: logger.error(f"[ReqID: {request_id}] API error during streaming: {e}", exc_info=True); error_msg = f"API error: {e.detail}"; error_chunk = {"error": {"message": error_msg, "type": "api_error", "code": e.status_code}}; yield f"data: {json.dumps(error_chunk)}\n\n"; yield "data: [DONE]\n\n"
|
111
|
-
except Exception as e: logger.error(f"[ReqID: {request_id}] Unexpected error during streaming: {e}", exc_info=True); error_msg = f"Internal server error: {str(e)}"; error_chunk = {"error": {"message": error_msg, "type": "internal_error"}}; yield f"data: {json.dumps(error_chunk)}\n\n"; yield "data: [DONE]\n\n"
|
112
|
-
return StreamingHttpResponse(event_stream(), content_type="text/event-stream")
|
113
|
-
|
114
|
-
# --- Restore Custom dispatch method (wrapping perform_authentication) ---
|
115
|
-
@method_decorator(csrf_exempt)
|
116
|
-
async def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
117
|
-
"""
|
118
|
-
Override DRF's dispatch method to specifically wrap the authentication step.
|
119
|
-
"""
|
120
|
-
self.args = args
|
121
|
-
self.kwargs = kwargs
|
122
|
-
drf_request: Request = self.initialize_request(request, *args, **kwargs)
|
123
|
-
self.request = drf_request
|
124
|
-
self.headers = self.default_response_headers
|
125
|
-
|
126
|
-
response = None
|
9
|
+
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
10
|
+
from rest_framework.permissions import IsAuthenticated
|
11
|
+
from swarm.auth import EnvOrTokenAuthentication
|
12
|
+
from swarm.utils.logger_setup import setup_logger
|
13
|
+
from swarm.views import utils as view_utils
|
14
|
+
from swarm.extensions.config.config_loader import config
|
15
|
+
from swarm.settings import Settings
|
16
|
+
|
17
|
+
logger = setup_logger(__name__)
|
18
|
+
|
19
|
+
# Removed _run_async_in_sync helper
|
20
|
+
|
21
|
+
@api_view(['POST'])
|
22
|
+
@csrf_exempt
|
23
|
+
@authentication_classes([EnvOrTokenAuthentication])
|
24
|
+
@permission_classes([IsAuthenticated])
|
25
|
+
def chat_completions(request): # Sync view
|
26
|
+
"""Handle chat completion requests via POST."""
|
27
|
+
if request.method != "POST":
|
28
|
+
return Response({"error": "Method not allowed. Use POST."}, status=405)
|
29
|
+
logger.info(f"Authenticated User: {request.user}")
|
30
|
+
|
31
|
+
parse_result = view_utils.parse_chat_request(request)
|
32
|
+
if isinstance(parse_result, Response): return parse_result
|
33
|
+
|
34
|
+
body, model, messages, context_vars, conversation_id, tool_call_id = parse_result
|
35
|
+
model_type = "llm" if model in config.get('llm', {}) and config.get('llm', {}).get(model, {}).get("passthrough") else "blueprint"
|
36
|
+
logger.info(f"Identified model type: {model_type} for model: {model}")
|
37
|
+
|
38
|
+
if model_type == "llm":
|
39
|
+
return Response({"error": f"LLM passthrough for model '{model}' not implemented."}, status=501)
|
40
|
+
|
41
|
+
try:
|
42
|
+
blueprint_instance = view_utils.get_blueprint_instance(model, context_vars)
|
43
|
+
messages_extended = view_utils.load_conversation_history(conversation_id, messages, tool_call_id)
|
44
|
+
|
45
|
+
# Try running the async function using asyncio.run()
|
46
|
+
# This might fail in test environments with existing loops.
|
127
47
|
try:
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
# Let DRF handle exceptions to generate appropriate responses
|
157
|
-
response = self.handle_exception(exc)
|
158
|
-
|
159
|
-
# Finalize response should now receive a valid Response/StreamingHttpResponse
|
160
|
-
self.response = self.finalize_response(drf_request, response, *args, **kwargs)
|
161
|
-
return self.response
|
162
|
-
|
163
|
-
# --- POST Handler (Keep sync_to_async wrappers here too) ---
|
164
|
-
async def post(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponseBase:
|
165
|
-
"""
|
166
|
-
Handles POST requests for chat completions. Assumes dispatch has handled auth/perms.
|
167
|
-
"""
|
168
|
-
request_id = str(uuid.uuid4())
|
169
|
-
logger.info(f"[ReqID: {request_id}] Processing POST request.")
|
170
|
-
print_logger.debug(f"[ReqID: {request_id}] User in post: {getattr(request, 'user', 'N/A')}, Auth: {getattr(request, 'auth', 'N/A')}")
|
171
|
-
|
172
|
-
# --- Request Body Parsing & Validation ---
|
173
|
-
try: request_data = request.data
|
174
|
-
except ParseError as e: logger.error(f"[ReqID: {request_id}] Invalid request body format: {e.detail}"); raise e
|
175
|
-
except json.JSONDecodeError as e: logger.error(f"[ReqID: {request_id}] JSON Decode Error: {e}"); raise ParseError(f"Invalid JSON body: {e}")
|
176
|
-
|
177
|
-
# --- Serialization and Validation ---
|
178
|
-
serializer = self.serializer_class(data=request_data)
|
179
|
-
try:
|
180
|
-
print_logger.debug(f"[ReqID: {request_id}] Validating request data: {request_data}")
|
181
|
-
# Wrap sync is_valid call as it *might* do DB lookups
|
182
|
-
await sync_to_async(serializer.is_valid)(raise_exception=True)
|
183
|
-
print_logger.debug(f"[ReqID: {request_id}] Request data validation successful.")
|
184
|
-
except ValidationError as e: print_logger.error(f"[ReqID: {request_id}] Request data validation FAILED: {e.detail}"); raise e
|
185
|
-
except Exception as e: print_logger.error(f"[ReqID: {request_id}] Unexpected error during serializer validation: {e}", exc_info=True); raise APIException(f"Internal error during request validation: {e}", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
|
186
|
-
|
187
|
-
validated_data = serializer.validated_data
|
188
|
-
model_name = validated_data['model']
|
189
|
-
messages = validated_data['messages']
|
190
|
-
stream = validated_data.get('stream', False)
|
191
|
-
blueprint_params = validated_data.get('params', None)
|
192
|
-
|
193
|
-
# --- Model Access Validation ---
|
194
|
-
# This function likely performs sync DB lookups, so wrap it.
|
195
|
-
print_logger.debug(f"[ReqID: {request_id}] Checking model access for user '{request.user}' and model '{model_name}'")
|
196
|
-
try:
|
197
|
-
access_granted = await sync_to_async(validate_model_access)(request.user, model_name)
|
198
|
-
except Exception as e:
|
199
|
-
logger.error(f"[ReqID: {request_id}] Error during model access validation for model '{model_name}': {e}", exc_info=True)
|
200
|
-
raise APIException("Error checking model permissions.", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
|
201
|
-
|
202
|
-
if not access_granted:
|
203
|
-
logger.warning(f"[ReqID: {request_id}] User '{request.user}' denied access to model '{model_name}'.")
|
204
|
-
raise PermissionDenied(f"You do not have permission to access the model '{model_name}'.")
|
205
|
-
print_logger.debug(f"[ReqID: {request_id}] Model access granted.")
|
206
|
-
|
207
|
-
# --- Get Blueprint Instance ---
|
208
|
-
# This function should ideally be async or sync-safe.
|
209
|
-
print_logger.debug(f"[ReqID: {request_id}] Getting blueprint instance for '{model_name}' with params: {blueprint_params}")
|
210
|
-
try:
|
211
|
-
blueprint_instance = await get_blueprint_instance(model_name, params=blueprint_params)
|
212
|
-
except Exception as e:
|
213
|
-
logger.error(f"[ReqID: {request_id}] Error getting blueprint instance for '{model_name}': {e}", exc_info=True)
|
214
|
-
raise APIException(f"Failed to load model '{model_name}': {e}", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
|
215
|
-
|
216
|
-
if blueprint_instance is None:
|
217
|
-
logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' not found or failed to initialize (get_blueprint_instance returned None).")
|
218
|
-
raise NotFound(f"The requested model (blueprint) '{model_name}' was not found or could not be initialized.")
|
219
|
-
|
220
|
-
# --- Handle Streaming or Non-Streaming Response ---
|
221
|
-
if stream:
|
222
|
-
return await self._handle_streaming(blueprint_instance, messages, request_id, model_name)
|
223
|
-
else:
|
224
|
-
return await self._handle_non_streaming(blueprint_instance, messages, request_id, model_name)
|
225
|
-
|
226
|
-
|
227
|
-
# ==============================================================================
|
228
|
-
# Simple Django Views (Example for Web UI - if ENABLE_WEBUI=True)
|
229
|
-
# ==============================================================================
|
48
|
+
logger.debug("Attempting asyncio.run(run_conversation)...")
|
49
|
+
response_obj, updated_context = asyncio.run(
|
50
|
+
view_utils.run_conversation(blueprint_instance, messages_extended, context_vars)
|
51
|
+
)
|
52
|
+
logger.debug("asyncio.run(run_conversation) completed.")
|
53
|
+
except RuntimeError as e:
|
54
|
+
# Catch potential nested loop errors specifically from asyncio.run()
|
55
|
+
logger.error(f"Asyncio run error: {e}", exc_info=True)
|
56
|
+
# Return a 500 error, as the async call couldn't be completed
|
57
|
+
return Response({"error": f"Server execution error: {str(e)}"}, status=500)
|
58
|
+
|
59
|
+
|
60
|
+
serialized = view_utils.serialize_swarm_response(response_obj, model, updated_context)
|
61
|
+
|
62
|
+
if conversation_id:
|
63
|
+
serialized["conversation_id"] = conversation_id
|
64
|
+
view_utils.store_conversation_history(conversation_id, messages_extended, response_obj)
|
65
|
+
|
66
|
+
return Response(serialized, status=200)
|
67
|
+
|
68
|
+
except FileNotFoundError as e:
|
69
|
+
logger.warning(f"Blueprint not found for model '{model}': {e}")
|
70
|
+
return Response({"error": f"Blueprint not found: {model}"}, status=404)
|
71
|
+
# Catch other exceptions, including the potential RuntimeError from above
|
72
|
+
except Exception as e:
|
73
|
+
logger.error(f"Error during execution for model '{model}': {e}", exc_info=True)
|
74
|
+
error_msg = str(e) if Settings().debug else "An internal error occurred."
|
75
|
+
return Response({"error": f"Error during execution: {error_msg}"}, status=500)
|
230
76
|
|
231
|
-
@method_decorator(csrf_exempt, name='dispatch') # Apply csrf_exempt if needed
|
232
|
-
@method_decorator(login_required, name='dispatch') # Require login
|
233
|
-
class IndexView(View):
|
234
|
-
""" Renders the main chat interface page. """
|
235
|
-
def get(self, request):
|
236
|
-
""" Handles GET requests to render the index page. """
|
237
|
-
# Assuming get_available_blueprints is sync safe
|
238
|
-
available_blueprints = get_available_blueprints()
|
239
|
-
context = {
|
240
|
-
'available_blueprints': available_blueprints,
|
241
|
-
'user': request.user, # User should be available here
|
242
|
-
}
|
243
|
-
return render(request, 'index.html', context)
|