open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636295__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.1748636295.dist-info/METADATA +257 -0
- open_swarm-0.1.1748636295.dist-info/RECORD +89 -0
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +2 -1
- open_swarm-0.1.1748636295.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1748636295.dist-info/top_level.txt +1 -0
- swarm/__init__.py +2 -0
- swarm/agent/agent.py +49 -0
- swarm/auth.py +48 -113
- swarm/consumers.py +0 -19
- swarm/core.py +411 -0
- swarm/extensions/blueprint/__init__.py +16 -30
- 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/django_utils.py +79 -181
- swarm/extensions/blueprint/interactive_mode.py +72 -67
- swarm/extensions/blueprint/output_utils.py +82 -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 +345 -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/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +32 -0
- swarm/extensions/mcp/mcp_client.py +233 -0
- swarm/extensions/mcp/mcp_tool_provider.py +135 -0
- swarm/extensions/mcp/mcp_utils.py +260 -0
- swarm/llm/chat_completion.py +166 -0
- swarm/serializers.py +5 -96
- swarm/settings.py +133 -85
- swarm/types.py +91 -0
- 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 +76 -236
- swarm/views/core_views.py +87 -80
- swarm/views/model_views.py +121 -64
- swarm/views/utils.py +439 -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/agent_utils.py +0 -21
- 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.1748636295.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/views/chat_views.py
CHANGED
@@ -1,243 +1,83 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
import
|
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
|
1
|
+
"""
|
2
|
+
Chat-related views for Open Swarm MCP Core.
|
3
|
+
"""
|
4
|
+
import asyncio # Import asyncio
|
14
5
|
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
6
|
from rest_framework.response import Response
|
22
|
-
from rest_framework.
|
23
|
-
from rest_framework.
|
24
|
-
from
|
25
|
-
|
26
|
-
#
|
27
|
-
from
|
28
|
-
|
29
|
-
#
|
30
|
-
from swarm.
|
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
|
-
|
61
|
-
#
|
62
|
-
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
7
|
+
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
8
|
+
from rest_framework.permissions import IsAuthenticated
|
9
|
+
from swarm.auth import EnvOrTokenAuthentication
|
10
|
+
from swarm.utils.logger_setup import setup_logger
|
11
|
+
# Import utils but rename to avoid conflict if this module grows
|
12
|
+
from swarm.views import utils as view_utils
|
13
|
+
# from swarm.views.utils import parse_chat_request, serialize_swarm_response, get_blueprint_instance
|
14
|
+
# from swarm.views.utils import load_conversation_history, store_conversation_history, run_conversation
|
15
|
+
# from swarm.views.utils import config, llm_config # Import from utils
|
16
|
+
|
17
|
+
logger = setup_logger(__name__)
|
18
|
+
|
19
|
+
# Revert to standard sync def
|
20
|
+
@api_view(['POST'])
|
21
|
+
@csrf_exempt
|
22
|
+
@authentication_classes([EnvOrTokenAuthentication])
|
23
|
+
@permission_classes([IsAuthenticated])
|
24
|
+
def chat_completions(request): # Mark as sync again
|
25
|
+
"""Handle chat completion requests via POST."""
|
26
|
+
if request.method != "POST":
|
27
|
+
return Response({"error": "Method not allowed. Use POST."}, status=405)
|
28
|
+
logger.info(f"Authenticated User: {request.user}")
|
29
|
+
|
30
|
+
# Use functions from view_utils
|
31
|
+
parse_result = view_utils.parse_chat_request(request)
|
32
|
+
if isinstance(parse_result, Response):
|
33
|
+
return parse_result
|
34
|
+
|
35
|
+
body, model, messages, context_vars, conversation_id, tool_call_id = parse_result
|
36
|
+
# Use llm_config loaded in utils
|
37
|
+
model_type = "llm" if model in view_utils.llm_config and view_utils.llm_config[model].get("passthrough") else "blueprint"
|
38
|
+
logger.info(f"Identified model type: {model_type} for model: {model}")
|
39
|
+
|
40
|
+
# --- Handle LLM Passthrough directly ---
|
41
|
+
if model_type == "llm":
|
42
|
+
logger.warning(f"LLM Passthrough requested for model '{model}'. This is not yet fully implemented in this view.")
|
43
|
+
# TODO: Implement direct call to Swarm core or LLM client for passthrough
|
44
|
+
return Response({"error": f"LLM passthrough for model '{model}' not implemented."}, status=501)
|
45
|
+
|
46
|
+
# --- Handle Blueprint ---
|
47
|
+
blueprint_instance = view_utils.get_blueprint_instance(model, context_vars)
|
48
|
+
if isinstance(blueprint_instance, Response): # Handle error response from get_blueprint_instance
|
49
|
+
return blueprint_instance
|
50
|
+
if blueprint_instance is None: # Handle case where get_blueprint_instance signaled non-blueprint
|
51
|
+
return Response({"error": f"Model '{model}' is not a loadable blueprint."}, status=404)
|
52
|
+
|
53
|
+
|
54
|
+
messages_extended = view_utils.load_conversation_history(conversation_id, messages, tool_call_id)
|
55
|
+
|
56
|
+
try:
|
57
|
+
# Use asyncio.run() to call the async run_conversation function
|
58
|
+
# This blocks the sync view until the async operation completes.
|
127
59
|
try:
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
print_logger.debug("Permissions check passed.")
|
139
|
-
self.check_throttles(drf_request)
|
140
|
-
print_logger.debug("Throttles check passed.")
|
141
|
-
|
142
|
-
# Find and execute the handler (e.g., post).
|
143
|
-
if drf_request.method.lower() in self.http_method_names:
|
144
|
-
handler = getattr(self, drf_request.method.lower(), self.http_method_not_allowed)
|
60
|
+
response_obj, updated_context = asyncio.run(
|
61
|
+
view_utils.run_conversation(blueprint_instance, messages_extended, context_vars)
|
62
|
+
)
|
63
|
+
except RuntimeError as e:
|
64
|
+
if "cannot be called from a running event loop" in str(e):
|
65
|
+
logger.error("Detected nested asyncio.run call. This can happen in certain test/server setups.")
|
66
|
+
# If already in a loop (e.g., certain test runners or ASGI servers),
|
67
|
+
# you might need a different way to run the async code, like ensure_future
|
68
|
+
# or adapting the server setup. For now, return an error.
|
69
|
+
return Response({"error": "Server configuration error: Nested event loop detected."}, status=500)
|
145
70
|
else:
|
146
|
-
|
147
|
-
|
148
|
-
# IMPORTANT: Await the handler if it's async (like self.post)
|
149
|
-
if asyncio.iscoroutinefunction(handler):
|
150
|
-
response = await handler(drf_request, *args, **kwargs)
|
151
|
-
else:
|
152
|
-
# Wrap sync handlers if any exist (like GET, OPTIONS).
|
153
|
-
response = await sync_to_async(handler)(drf_request, *args, **kwargs)
|
154
|
-
|
155
|
-
except Exception as exc:
|
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)
|
71
|
+
raise e # Reraise other RuntimeErrors
|
225
72
|
|
73
|
+
serialized = view_utils.serialize_swarm_response(response_obj, model, updated_context)
|
226
74
|
|
227
|
-
|
228
|
-
|
229
|
-
#
|
75
|
+
if conversation_id:
|
76
|
+
serialized["conversation_id"] = conversation_id
|
77
|
+
# Storing history can remain synchronous for now unless it becomes a bottleneck
|
78
|
+
view_utils.store_conversation_history(conversation_id, messages_extended, response_obj)
|
230
79
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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)
|
80
|
+
return Response(serialized, status=200)
|
81
|
+
except Exception as e:
|
82
|
+
logger.error(f"Error during execution: {e}", exc_info=True)
|
83
|
+
return Response({"error": f"Error during execution: {str(e)}"}, status=500)
|
swarm/views/core_views.py
CHANGED
@@ -11,101 +11,108 @@ from django.http import JsonResponse, HttpResponse
|
|
11
11
|
from django.conf import settings
|
12
12
|
from django.views.decorators.csrf import csrf_exempt
|
13
13
|
from django.contrib.auth import authenticate, login
|
14
|
-
from django.contrib.auth.forms import AuthenticationForm # Use standard auth form
|
15
|
-
|
16
14
|
|
17
15
|
# Assuming blueprint discovery happens elsewhere and results are available if needed
|
18
16
|
# from .utils import blueprints_metadata # Or however metadata is accessed
|
19
|
-
|
20
|
-
# Use the current config loader
|
21
|
-
from swarm.core import config_loader, server_config
|
17
|
+
from swarm.extensions.config.config_loader import load_server_config # Import if needed
|
22
18
|
|
23
19
|
logger = logging.getLogger(__name__)
|
24
20
|
|
25
|
-
#
|
21
|
+
# Placeholder for blueprint metadata if needed by index
|
22
|
+
# In a real app, this might be loaded dynamically or passed via context
|
23
|
+
try:
|
24
|
+
# Attempt to import the discovery function if views need dynamic data
|
25
|
+
from swarm.extensions.blueprint.blueprint_discovery import discover_blueprints
|
26
|
+
# Note: Calling discover_blueprints here might be too early or cause issues.
|
27
|
+
# It's often better handled in specific views that need it (like list_models)
|
28
|
+
# or passed via Django context processors.
|
29
|
+
# For now, provide an empty dict as fallback.
|
30
|
+
try:
|
31
|
+
# Use settings.BLUEPRINTS_DIR which should be configured
|
32
|
+
blueprints_metadata = discover_blueprints(directories=[str(settings.BLUEPRINTS_DIR)])
|
33
|
+
except Exception:
|
34
|
+
blueprints_metadata = {}
|
35
|
+
except ImportError:
|
36
|
+
blueprints_metadata = {}
|
37
|
+
|
26
38
|
|
39
|
+
@csrf_exempt
|
27
40
|
def index(request):
|
28
|
-
"""Render the main index page
|
29
|
-
|
30
|
-
#
|
31
|
-
|
41
|
+
"""Render the main index page with blueprint options."""
|
42
|
+
logger.debug("Rendering index page")
|
43
|
+
# Get blueprint names from the potentially loaded metadata
|
44
|
+
blueprint_names_list = list(blueprints_metadata.keys())
|
32
45
|
context = {
|
33
|
-
|
34
|
-
|
35
|
-
|
46
|
+
"dark_mode": request.session.get('dark_mode', True),
|
47
|
+
"enable_admin": os.getenv("ENABLE_ADMIN", "false").lower() in ("true", "1", "t"),
|
48
|
+
"blueprints": blueprint_names_list # Pass the list of names
|
36
49
|
}
|
37
|
-
|
38
|
-
template_name = "swarm/index.html"
|
39
|
-
# Check if template exists? Django handles TemplateDoesNotExist.
|
40
|
-
return render(request, template_name, context)
|
41
|
-
|
42
|
-
def custom_login(request):
|
43
|
-
"""Handles user login."""
|
44
|
-
if request.method == 'POST':
|
45
|
-
form = AuthenticationForm(request, data=request.POST)
|
46
|
-
if form.is_valid():
|
47
|
-
username = form.cleaned_data.get('username')
|
48
|
-
password = form.cleaned_data.get('password')
|
49
|
-
user = authenticate(username=username, password=password)
|
50
|
-
if user is not None:
|
51
|
-
login(request, user)
|
52
|
-
logger.info(f"User '{username}' logged in successfully.")
|
53
|
-
return redirect('/') # Redirect to index after login
|
54
|
-
else:
|
55
|
-
logger.warning(f"Login failed for user '{username}': Invalid credentials.")
|
56
|
-
# Return form with error (AuthenticationForm handles this)
|
57
|
-
else:
|
58
|
-
logger.warning(f"Login form invalid: {form.errors.as_json()}")
|
59
|
-
else:
|
60
|
-
form = AuthenticationForm()
|
61
|
-
|
62
|
-
# Only render if ENABLE_WEBUI is true (checked in urls.py)
|
63
|
-
return render(request, 'swarm/login.html', {'form': form})
|
50
|
+
return render(request, "index.html", context)
|
64
51
|
|
52
|
+
DEFAULT_CONFIG = {
|
53
|
+
"llm": {
|
54
|
+
"default": {
|
55
|
+
"provider": "openai",
|
56
|
+
"model": "gpt-4o", # Example fallback model
|
57
|
+
"base_url": "https://api.openai.com/v1",
|
58
|
+
"api_key": "",
|
59
|
+
"temperature": 0.3
|
60
|
+
}
|
61
|
+
},
|
62
|
+
"blueprints": {},
|
63
|
+
"mcpServers": {}
|
64
|
+
}
|
65
65
|
|
66
66
|
def serve_swarm_config(request):
|
67
|
-
"""
|
68
|
-
# Find the config file used by the blueprint base or config loader
|
69
|
-
# This logic might need refinement depending on where config is reliably found
|
70
|
-
config_path = None
|
67
|
+
"""Serve the swarm configuration file as JSON."""
|
71
68
|
try:
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
69
|
+
# Use load_server_config which handles finding the file
|
70
|
+
config_data = load_server_config()
|
71
|
+
return JsonResponse(config_data)
|
72
|
+
except (FileNotFoundError, ValueError, Exception) as e:
|
73
|
+
logger.error(f"Error serving swarm_config.json: {e}. Serving default.")
|
74
|
+
# Return a default config on error
|
75
|
+
return JsonResponse(DEFAULT_CONFIG, status=500)
|
76
|
+
|
78
77
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
78
|
+
@csrf_exempt
|
79
|
+
def custom_login(request):
|
80
|
+
"""Handle custom login at /accounts/login/, redirecting to 'next' URL on success."""
|
81
|
+
from django.contrib.auth.models import User # Import here to avoid potential early init issues
|
82
|
+
if request.method == "POST":
|
83
|
+
username = request.POST.get("username")
|
84
|
+
password = request.POST.get("password")
|
85
|
+
user = authenticate(request, username=username, password=password)
|
86
|
+
if user is not None:
|
87
|
+
login(request, user)
|
88
|
+
next_url = request.GET.get("next", getattr(settings, 'LOGIN_REDIRECT_URL', '/')) # Use setting or fallback
|
89
|
+
logger.info(f"User '{username}' logged in successfully. Redirecting to {next_url}")
|
90
|
+
return redirect(next_url)
|
91
|
+
else:
|
92
|
+
# If ENABLE_API_AUTH is false, auto-login as testuser (for dev/test convenience)
|
93
|
+
enable_auth = os.getenv("ENABLE_API_AUTH", "true").lower() in ("true", "1", "t") # Default to TRUE
|
94
|
+
if not enable_auth:
|
95
|
+
try:
|
96
|
+
# Ensure test user exists and has a known password
|
97
|
+
user, created = User.objects.get_or_create(username="testuser")
|
98
|
+
if created or not user.has_usable_password():
|
99
|
+
user.set_password("testpass") # Set a default password
|
100
|
+
user.save()
|
90
101
|
|
91
|
-
|
92
|
-
|
93
|
-
|
102
|
+
if user.check_password("testpass"): # Check against the known password
|
103
|
+
login(request, user)
|
104
|
+
next_url = request.GET.get("next", getattr(settings, 'LOGIN_REDIRECT_URL', '/'))
|
105
|
+
logger.info(f"Auto-logged in as 'testuser' since ENABLE_API_AUTH is false")
|
106
|
+
return redirect(next_url)
|
107
|
+
else:
|
108
|
+
logger.warning("Auto-login failed: 'testuser' exists but password incorrect.")
|
94
109
|
|
95
|
-
|
96
|
-
|
110
|
+
except Exception as auto_login_err:
|
111
|
+
logger.error(f"Error during testuser auto-login attempt: {auto_login_err}")
|
112
|
+
# If authentication failed (and auto-login didn't happen or failed)
|
113
|
+
logger.warning(f"Login failed for user '{username}'.")
|
114
|
+
return render(request, "account/login.html", {"error": "Invalid credentials"})
|
115
|
+
# If GET request
|
116
|
+
return render(request, "account/login.html")
|
97
117
|
|
98
|
-
|
99
|
-
def list_available_blueprints_api(request):
|
100
|
-
"""API endpoint to list discoverable blueprints."""
|
101
|
-
# Re-use discovery logic if possible, or adapt from CLI
|
102
|
-
from swarm.extensions.blueprint.discovery import discover_blueprints # Assuming this exists
|
103
|
-
try:
|
104
|
-
bp_dir = Path(settings.BLUEPRINTS_DIR) # Assuming settings has BLUEPRINTS_DIR
|
105
|
-
discovered = discover_blueprints(directories=[str(bp_dir)])
|
106
|
-
# Format the response
|
107
|
-
blueprint_list = [{"name": name, "description": meta.get("description", "N/A")} for name, meta in discovered.items()]
|
108
|
-
return JsonResponse({"blueprints": blueprint_list})
|
109
|
-
except Exception as e:
|
110
|
-
logger.error(f"Error listing blueprints via API: {e}", exc_info=True)
|
111
|
-
return JsonResponse({"error": "Failed to list blueprints."}, status=500)
|
118
|
+
# Add any other views that were originally in the main views.py if needed
|