open-swarm 0.1.1743070217__py3-none-any.whl → 0.1.1743362777__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. open_swarm-0.1.1743362777.dist-info/METADATA +217 -0
  2. open_swarm-0.1.1743362777.dist-info/RECORD +260 -0
  3. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743362777.dist-info}/WHEEL +1 -2
  4. open_swarm-0.1.1743362777.dist-info/entry_points.txt +2 -0
  5. swarm/__init__.py +0 -2
  6. swarm/auth.py +53 -49
  7. swarm/blueprints/README.md +67 -0
  8. swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
  9. swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
  10. swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
  11. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
  12. swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
  13. swarm/blueprints/divine_code/__init__.py +0 -0
  14. swarm/blueprints/divine_code/apps.py +11 -0
  15. swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
  16. swarm/blueprints/django_chat/apps.py +6 -0
  17. swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
  18. swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
  19. swarm/blueprints/django_chat/urls.py +8 -0
  20. swarm/blueprints/django_chat/views.py +32 -0
  21. swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
  22. swarm/blueprints/family_ties/apps.py +11 -0
  23. swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
  24. swarm/blueprints/family_ties/models.py +19 -0
  25. swarm/blueprints/family_ties/serializers.py +7 -0
  26. swarm/blueprints/family_ties/settings.py +16 -0
  27. swarm/blueprints/family_ties/urls.py +10 -0
  28. swarm/blueprints/family_ties/views.py +26 -0
  29. swarm/blueprints/flock/__init__.py +0 -0
  30. swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
  31. swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
  32. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
  33. swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
  34. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
  35. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
  36. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
  37. swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
  38. swarm/blueprints/rue_code/__init__.py +0 -0
  39. swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
  40. swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
  41. swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
  42. swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
  43. swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
  44. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
  45. swarm/extensions/blueprint/__init__.py +30 -15
  46. swarm/extensions/blueprint/agent_utils.py +16 -40
  47. swarm/extensions/blueprint/blueprint_base.py +141 -543
  48. swarm/extensions/blueprint/blueprint_discovery.py +112 -98
  49. swarm/extensions/blueprint/cli_handler.py +185 -0
  50. swarm/extensions/blueprint/config_loader.py +122 -0
  51. swarm/extensions/blueprint/django_utils.py +181 -79
  52. swarm/extensions/blueprint/interactive_mode.py +1 -1
  53. swarm/extensions/config/config_loader.py +83 -200
  54. swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
  55. swarm/extensions/launchers/swarm_cli.py +199 -287
  56. swarm/llm/chat_completion.py +26 -55
  57. swarm/management/__init__.py +0 -0
  58. swarm/management/commands/__init__.py +0 -0
  59. swarm/management/commands/runserver.py +58 -0
  60. swarm/permissions.py +38 -0
  61. swarm/serializers.py +96 -5
  62. swarm/settings.py +95 -110
  63. swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
  64. swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
  65. swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
  66. swarm/static/contrib/markedjs/marked.min.js +6 -0
  67. swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
  68. swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
  69. swarm/static/contrib/tabler-icons/archive.svg +21 -0
  70. swarm/static/contrib/tabler-icons/artboard.svg +27 -0
  71. swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
  72. swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
  73. swarm/static/contrib/tabler-icons/carambola.svg +19 -0
  74. swarm/static/contrib/tabler-icons/copy.svg +20 -0
  75. swarm/static/contrib/tabler-icons/download.svg +21 -0
  76. swarm/static/contrib/tabler-icons/edit.svg +21 -0
  77. swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
  78. swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
  79. swarm/static/contrib/tabler-icons/headset.svg +22 -0
  80. swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
  81. swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
  82. swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
  83. swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
  84. swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
  85. swarm/static/contrib/tabler-icons/message-star.svg +22 -0
  86. swarm/static/contrib/tabler-icons/message-x.svg +23 -0
  87. swarm/static/contrib/tabler-icons/message.svg +21 -0
  88. swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
  89. swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
  90. swarm/static/contrib/tabler-icons/robot.svg +26 -0
  91. swarm/static/contrib/tabler-icons/search.svg +19 -0
  92. swarm/static/contrib/tabler-icons/settings.svg +20 -0
  93. swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
  94. swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
  95. swarm/static/css/dropdown.css +22 -0
  96. swarm/static/htmx/htmx.min.js +0 -0
  97. swarm/static/js/dropdown.js +23 -0
  98. swarm/static/rest_mode/css/base.css +470 -0
  99. swarm/static/rest_mode/css/chat-history.css +286 -0
  100. swarm/static/rest_mode/css/chat.css +251 -0
  101. swarm/static/rest_mode/css/chatbot.css +74 -0
  102. swarm/static/rest_mode/css/chatgpt.css +62 -0
  103. swarm/static/rest_mode/css/colors/corporate.css +74 -0
  104. swarm/static/rest_mode/css/colors/pastel.css +81 -0
  105. swarm/static/rest_mode/css/colors/tropical.css +82 -0
  106. swarm/static/rest_mode/css/general.css +142 -0
  107. swarm/static/rest_mode/css/layout.css +167 -0
  108. swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
  109. swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
  110. swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
  111. swarm/static/rest_mode/css/messages.css +84 -0
  112. swarm/static/rest_mode/css/messenger.css +135 -0
  113. swarm/static/rest_mode/css/settings.css +91 -0
  114. swarm/static/rest_mode/css/simple.css +44 -0
  115. swarm/static/rest_mode/css/slack.css +58 -0
  116. swarm/static/rest_mode/css/style.css +156 -0
  117. swarm/static/rest_mode/css/theme.css +30 -0
  118. swarm/static/rest_mode/css/toast.css +40 -0
  119. swarm/static/rest_mode/js/auth.js +9 -0
  120. swarm/static/rest_mode/js/blueprint.js +41 -0
  121. swarm/static/rest_mode/js/blueprintUtils.js +12 -0
  122. swarm/static/rest_mode/js/chatLogic.js +79 -0
  123. swarm/static/rest_mode/js/debug.js +63 -0
  124. swarm/static/rest_mode/js/events.js +98 -0
  125. swarm/static/rest_mode/js/main.js +19 -0
  126. swarm/static/rest_mode/js/messages.js +264 -0
  127. swarm/static/rest_mode/js/messengerLogic.js +355 -0
  128. swarm/static/rest_mode/js/modules/apiService.js +84 -0
  129. swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
  130. swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
  131. swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
  132. swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
  133. swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
  134. swarm/static/rest_mode/js/modules/state.js +7 -0
  135. swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
  136. swarm/static/rest_mode/js/modules/validation.js +23 -0
  137. swarm/static/rest_mode/js/rendering.js +119 -0
  138. swarm/static/rest_mode/js/settings.js +130 -0
  139. swarm/static/rest_mode/js/sidebar.js +94 -0
  140. swarm/static/rest_mode/js/simpleLogic.js +37 -0
  141. swarm/static/rest_mode/js/slackLogic.js +66 -0
  142. swarm/static/rest_mode/js/splash.js +76 -0
  143. swarm/static/rest_mode/js/theme.js +111 -0
  144. swarm/static/rest_mode/js/toast.js +36 -0
  145. swarm/static/rest_mode/js/ui.js +265 -0
  146. swarm/static/rest_mode/js/validation.js +57 -0
  147. swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
  148. swarm/static/rest_mode/svg/arrow_down.svg +5 -0
  149. swarm/static/rest_mode/svg/arrow_left.svg +5 -0
  150. swarm/static/rest_mode/svg/arrow_right.svg +5 -0
  151. swarm/static/rest_mode/svg/arrow_up.svg +5 -0
  152. swarm/static/rest_mode/svg/attach.svg +8 -0
  153. swarm/static/rest_mode/svg/avatar.svg +7 -0
  154. swarm/static/rest_mode/svg/canvas.svg +6 -0
  155. swarm/static/rest_mode/svg/chat_history.svg +4 -0
  156. swarm/static/rest_mode/svg/close.svg +5 -0
  157. swarm/static/rest_mode/svg/copy.svg +4 -0
  158. swarm/static/rest_mode/svg/dark_mode.svg +3 -0
  159. swarm/static/rest_mode/svg/edit.svg +5 -0
  160. swarm/static/rest_mode/svg/layout.svg +9 -0
  161. swarm/static/rest_mode/svg/logo.svg +29 -0
  162. swarm/static/rest_mode/svg/logout.svg +5 -0
  163. swarm/static/rest_mode/svg/mobile.svg +5 -0
  164. swarm/static/rest_mode/svg/new_chat.svg +4 -0
  165. swarm/static/rest_mode/svg/not_visible.svg +5 -0
  166. swarm/static/rest_mode/svg/plus.svg +7 -0
  167. swarm/static/rest_mode/svg/run_code.svg +6 -0
  168. swarm/static/rest_mode/svg/save.svg +4 -0
  169. swarm/static/rest_mode/svg/search.svg +6 -0
  170. swarm/static/rest_mode/svg/settings.svg +4 -0
  171. swarm/static/rest_mode/svg/speaker.svg +5 -0
  172. swarm/static/rest_mode/svg/stop.svg +6 -0
  173. swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
  174. swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
  175. swarm/static/rest_mode/svg/toggle_off.svg +6 -0
  176. swarm/static/rest_mode/svg/toggle_on.svg +6 -0
  177. swarm/static/rest_mode/svg/trash.svg +10 -0
  178. swarm/static/rest_mode/svg/undo.svg +3 -0
  179. swarm/static/rest_mode/svg/visible.svg +8 -0
  180. swarm/static/rest_mode/svg/voice.svg +10 -0
  181. swarm/templates/account/login.html +22 -0
  182. swarm/templates/account/signup.html +32 -0
  183. swarm/templates/base.html +30 -0
  184. swarm/templates/chat.html +43 -0
  185. swarm/templates/index.html +35 -0
  186. swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
  187. swarm/templates/rest_mode/components/header.html +45 -0
  188. swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
  189. swarm/templates/rest_mode/components/settings_dialog.html +97 -0
  190. swarm/templates/rest_mode/components/splash_screen.html +7 -0
  191. swarm/templates/rest_mode/components/top_bar.html +28 -0
  192. swarm/templates/rest_mode/message_ui.html +50 -0
  193. swarm/templates/rest_mode/slackbot.html +30 -0
  194. swarm/templates/simple_blueprint_page.html +24 -0
  195. swarm/templates/websocket_partials/final_system_message.html +3 -0
  196. swarm/templates/websocket_partials/system_message.html +4 -0
  197. swarm/templates/websocket_partials/user_message.html +5 -0
  198. swarm/urls.py +57 -74
  199. swarm/utils/log_utils.py +63 -0
  200. swarm/views/api_views.py +48 -39
  201. swarm/views/chat_views.py +156 -70
  202. swarm/views/core_views.py +85 -90
  203. swarm/views/model_views.py +64 -121
  204. swarm/views/utils.py +65 -441
  205. open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
  206. open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
  207. open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
  208. open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
  209. swarm/agent/agent.py +0 -49
  210. swarm/core.py +0 -326
  211. swarm/extensions/mcp/__init__.py +0 -1
  212. swarm/extensions/mcp/cache_utils.py +0 -36
  213. swarm/extensions/mcp/mcp_client.py +0 -341
  214. swarm/extensions/mcp/mcp_constants.py +0 -7
  215. swarm/extensions/mcp/mcp_tool_provider.py +0 -110
  216. swarm/types.py +0 -126
  217. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743362777.dist-info}/licenses/LICENSE +0 -0
swarm/views/api_views.py CHANGED
@@ -1,46 +1,55 @@
1
- """
2
- API-specific views and viewsets for Open Swarm MCP Core.
3
- """
4
- from rest_framework.viewsets import ModelViewSet
1
+ import time
2
+ import logging
3
+ from rest_framework.views import APIView
4
+ from rest_framework.response import Response
5
+ from rest_framework import status
5
6
  from rest_framework.permissions import AllowAny
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
7
+ # *** Import async_to_sync ***
8
+ from asgiref.sync import async_to_sync
11
9
 
12
- logger = setup_logger(__name__)
10
+ from swarm.views.utils import get_available_blueprints
13
11
 
14
- class HiddenSpectacularAPIView(BaseSpectacularAPIView):
15
- exclude_from_schema = True
12
+ logger = logging.getLogger(__name__)
16
13
 
17
- class ChatMessageViewSet(ModelViewSet):
18
- """API viewset for managing chat messages."""
19
- authentication_classes = []
14
+ class ModelsListView(APIView):
15
+ """
16
+ API view to list available models (blueprints) compatible with OpenAI's /v1/models format.
17
+ """
20
18
  permission_classes = [AllowAny]
21
- queryset = ChatMessage.objects.all()
22
- serializer_class = ChatMessageSerializer
23
19
 
24
- @extend_schema(summary="List all chat messages")
25
- def list(self, request, *args, **kwargs):
26
- return super().list(request, *args, **kwargs)
20
+ def get(self, request, *args, **kwargs):
21
+ try:
22
+ # *** Use async_to_sync to call the async function ***
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
+ )
27
55
 
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,76 +1,162 @@
1
- """
2
- Chat-related views for Open Swarm MCP Core.
3
- """
4
- import asyncio
5
1
  import logging
6
2
  import json
3
+ import uuid
4
+ import time
5
+ import asyncio
6
+ from typing import Dict, Any, AsyncGenerator, List, Optional
7
+
8
+ from django.shortcuts import render
9
+ from django.http import StreamingHttpResponse, JsonResponse, Http404, HttpRequest, HttpResponse, HttpResponseBase
10
+ from django.views import View
11
+ from django.utils.decorators import method_decorator
7
12
  from django.views.decorators.csrf import csrf_exempt
13
+ from django.contrib.auth.decorators import login_required
14
+ from django.conf import settings
15
+
16
+ from rest_framework import status
17
+ from rest_framework.views import APIView
8
18
  from rest_framework.response import Response
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.
19
+ from rest_framework.permissions import IsAuthenticated, AllowAny
20
+ from rest_framework.exceptions import ValidationError, PermissionDenied, NotFound, APIException, ParseError, NotAuthenticated
21
+
22
+ from asgiref.sync import sync_to_async, async_to_sync # Import async_to_sync
23
+
24
+ # Assuming serializers are in the same app
25
+ from swarm.serializers import ChatCompletionRequestSerializer
26
+ # Assuming utils are in the same app/directory level
27
+ from .utils import get_blueprint_instance, validate_model_access, get_available_blueprints
28
+
29
+ logger = logging.getLogger(__name__)
30
+ print_logger = logging.getLogger('print_debug')
31
+
32
+ # ==============================================================================
33
+ # API Views (DRF based)
34
+ # ==============================================================================
35
+
36
+ class HealthCheckView(APIView):
37
+ permission_classes = [AllowAny]
38
+ def get(self, request, *args, **kwargs): return Response({"status": "ok"})
39
+
40
+ class ChatCompletionsView(APIView):
41
+ """
42
+ Handles chat completion requests, compatible with OpenAI API.
43
+ Permissions are now handled by DEFAULT_PERMISSION_CLASSES in settings.py.
44
+ """
45
+ serializer_class = ChatCompletionRequestSerializer
46
+
47
+ async def _handle_non_streaming(self, blueprint_instance, messages: List[Dict[str, str]], request_id: str, model_name: str) -> Response:
48
+ logger.info(f"[ReqID: {request_id}] Processing non-streaming request for model '{model_name}'.")
49
+ final_response_data = None; start_time = time.time()
47
50
  try:
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)
51
+ # *** FIX: Remove await here. run() returns the async generator directly ***
52
+ async_generator = blueprint_instance.run(messages)
53
+ async for chunk in async_generator:
54
+ if isinstance(chunk, dict) and "messages" in chunk: final_response_data = chunk["messages"]; logger.debug(f"[ReqID: {request_id}] Received final data chunk: {final_response_data}"); break
55
+ else: logger.warning(f"[ReqID: {request_id}] Unexpected chunk format: {chunk}")
56
+
57
+ if not final_response_data or not isinstance(final_response_data, list) or not final_response_data:
58
+ logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' did not return valid final data structure. Got: {final_response_data}")
59
+ raise APIException("Blueprint did not return valid data.", code=status.HTTP_500_INTERNAL_SERVER_ERROR)
60
+
61
+ if not isinstance(final_response_data[0], dict) or 'role' not in final_response_data[0]:
62
+ logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' returned invalid message structure. Got: {final_response_data[0]}")
63
+ raise APIException("Blueprint returned invalid message structure.", code=status.HTTP_500_INTERNAL_SERVER_ERROR)
64
+
65
+ 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 }
66
+ end_time = time.time(); logger.info(f"[ReqID: {request_id}] Non-streaming request completed in {end_time - start_time:.2f}s.")
67
+ return Response(response_payload, status=status.HTTP_200_OK)
68
+ except APIException: raise
69
+ 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
70
+
71
+ async def _handle_streaming(self, blueprint_instance, messages: List[Dict[str, str]], request_id: str, model_name: str) -> StreamingHttpResponse:
72
+ logger.info(f"[ReqID: {request_id}] Processing streaming request for model '{model_name}'.")
73
+ async def event_stream():
74
+ start_time = time.time(); chunk_index = 0
75
+ try:
76
+ logger.debug(f"[ReqID: {request_id}] Getting async generator from blueprint run...");
77
+ # *** FIX: Remove await here. run() returns the async generator directly ***
78
+ async_generator = blueprint_instance.run(messages)
79
+ logger.debug(f"[ReqID: {request_id}] Got async generator. Starting iteration...")
80
+ async for chunk in async_generator:
81
+ logger.debug(f"[ReqID: {request_id}] Received stream chunk {chunk_index}: {chunk}")
82
+ 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):
83
+ logger.warning(f"[ReqID: {request_id}] Skipping invalid chunk format: {chunk}"); continue
84
+ delta_content = chunk["messages"][0].get("content", "");
85
+ delta = {"role": "assistant"}
86
+ if delta_content is not None: delta["content"] = delta_content
87
+
88
+ 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}] }
89
+ 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)
90
+ 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.")
91
+ except APIException as e:
92
+ logger.error(f"[ReqID: {request_id}] API error during streaming blueprint execution: {e}", exc_info=True); error_msg = f"API error during stream: {e.detail}"; error_chunk = {"error": {"message": error_msg, "type": "api_error", "code": e.status_code}}
93
+ try: yield f"data: {json.dumps(error_chunk)}\n\n"; yield "data: [DONE]\n\n"
94
+ except Exception as send_err: logger.error(f"[ReqID: {request_id}] Failed to send error chunk: {send_err}")
95
+ except Exception as e:
96
+ logger.error(f"[ReqID: {request_id}] Unexpected error during streaming blueprint execution: {e}", exc_info=True); error_msg = f"Internal server error during stream: {str(e)}"; error_chunk = {"error": {"message": error_msg, "type": "internal_error"}}
97
+ try: yield f"data: {json.dumps(error_chunk)}\n\n"; yield "data: [DONE]\n\n"
98
+ except Exception as send_err: logger.error(f"[ReqID: {request_id}] Failed to send error chunk: {send_err}")
99
+ return StreamingHttpResponse(event_stream(), content_type="text/event-stream")
100
+
101
+ @method_decorator(csrf_exempt)
102
+ async def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
103
+ self.args = args; self.kwargs = kwargs
104
+ request = self.initialize_request(request, *args, **kwargs)
105
+ self.request = request; self.headers = self.default_response_headers
106
+ try:
107
+ print_logger.debug(f"User before initial(): {getattr(request, 'user', 'N/A')}, Auth before initial(): {getattr(request, 'auth', 'N/A')}")
108
+ # Wrap sync initial() call
109
+ await sync_to_async(self.initial)(request, *args, **kwargs)
110
+ print_logger.debug(f"User after initial(): {getattr(request, 'user', 'N/A')}, Auth after initial(): {getattr(request, 'auth', 'N/A')}")
111
+
112
+ if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
113
+ else: handler = self.http_method_not_allowed
114
+
115
+ if asyncio.iscoroutinefunction(handler):
116
+ response = await handler(request, *args, **kwargs)
117
+ else:
118
+ response = await sync_to_async(handler)(request, *args, **kwargs)
119
+ except Exception as exc: response = self.handle_exception(exc)
120
+ self.response = self.finalize_response(request, response, *args, **kwargs)
121
+ return self.response
122
+
123
+ async def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
124
+ request_id = str(uuid.uuid4()); logger.info(f"[ReqID: {request_id}] Received chat completion request.")
125
+ print_logger.debug(f"[ReqID: {request_id}] User at start of post: {getattr(request, 'user', 'N/A')}, Auth: {getattr(request, 'auth', 'N/A')}")
126
+ try:
127
+ request_data = request.data
128
+ except ParseError as e: logger.error(f"[ReqID: {request_id}] Invalid JSON body: {e.detail}"); raise e
129
+ except json.JSONDecodeError as e: logger.error(f"[ReqID: {request_id}] JSON Decode Error: {e}"); raise ParseError(f"Invalid JSON body: {e}")
130
+
131
+ serializer = self.serializer_class(data=request_data)
132
+ try:
133
+ print_logger.debug(f"[ReqID: {request_id}] Attempting serializer.is_valid(). Data: {request_data}")
134
+ # Wrap sync is_valid call
135
+ await sync_to_async(serializer.is_valid)(raise_exception=True)
136
+ print_logger.debug(f"[ReqID: {request_id}] Serializer is_valid() PASSED.")
137
+ except ValidationError as e: print_logger.error(f"[ReqID: {request_id}] Serializer validation FAILED: {e.detail}"); raise e
138
+ 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
139
+
140
+ validated_data = serializer.validated_data
141
+ model_name = validated_data['model']
142
+ messages = validated_data['messages']
143
+ stream = validated_data.get('stream', False)
144
+ blueprint_params = validated_data.get('params', None)
145
+
146
+ print_logger.debug(f"[ReqID: {request_id}] Validation passed. Checking model access for user {request.user} and model {model_name}")
147
+ # Wrap sync validate_model_access
148
+ access_granted = await sync_to_async(validate_model_access)(request.user, model_name)
149
+ if not access_granted:
150
+ logger.warning(f"[ReqID: {request_id}] User {request.user} denied access to model '{model_name}'.")
151
+ raise PermissionDenied(f"You do not have permission to access the model '{model_name}'.")
152
+
153
+ print_logger.debug(f"[ReqID: {request_id}] Access granted. Getting blueprint instance for {model_name}")
154
+ blueprint_instance = await get_blueprint_instance(model_name, params=blueprint_params)
155
+
156
+ if blueprint_instance is None:
157
+ logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' not found or failed to initialize after access checks.")
158
+ raise NotFound(f"The requested model (blueprint) '{model_name}' was not found or could not be initialized.")
159
+
160
+ if stream: return await self._handle_streaming(blueprint_instance, messages, request_id, model_name)
161
+ else: return await self._handle_non_streaming(blueprint_instance, messages, request_id, model_name)
76
162
 
swarm/views/core_views.py CHANGED
@@ -11,108 +11,103 @@ 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
+
14
16
 
15
17
  # Assuming blueprint discovery happens elsewhere and results are available if needed
16
18
  # from .utils import blueprints_metadata # Or however metadata is accessed
17
- from swarm.extensions.config.config_loader import load_server_config # Import if needed
18
19
 
19
- logger = logging.getLogger(__name__)
20
+ # Use the current config loader
21
+ from swarm.extensions.config.config_loader import load_config, find_config_file, DEFAULT_CONFIG_FILENAME
20
22
 
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 = {}
23
+ logger = logging.getLogger(__name__)
37
24
 
25
+ # --- Web UI Views (if ENABLE_WEBUI is True) ---
38
26
 
39
- @csrf_exempt
40
27
  def index(request):
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())
28
+ """Render the main index page (likely the chat UI)."""
29
+ # This view might need context data like available models/blueprints
30
+ # It should only be active if ENABLE_WEBUI is true (checked in urls.py)
31
+ logger.debug(f"Index view called for user: {request.user}")
45
32
  context = {
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
33
+ 'title': settings.SWARM_TITLE or "Open Swarm",
34
+ 'description': settings.SWARM_DESCRIPTION or "A Swarm Framework Interface",
35
+ # Add other context needed by the template
49
36
  }
50
- return render(request, "index.html", context)
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
- }
37
+ # Ensure the template exists
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})
64
+
65
65
 
66
66
  def serve_swarm_config(request):
67
- """Serve the swarm configuration file as JSON."""
67
+ """Serves the swarm_config.json content."""
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
68
71
  try:
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)
72
+ # Use the same logic as BlueprintBase if possible, or find_config_file
73
+ config_path = find_config_file(filename=DEFAULT_CONFIG_FILENAME, start_dir=Path(settings.BASE_DIR).parent) # Search from project root
74
+ if not config_path:
75
+ # Fallback to location relative to settings? Unlikely to be correct.
76
+ config_path = Path(settings.BASE_DIR) / '..' / DEFAULT_CONFIG_FILENAME # Adjust relative path if needed
77
+ config_path = config_path.resolve()
78
+
79
+ if config_path and config_path.exists():
80
+ logger.info(f"Serving config from: {config_path}")
81
+ # Load config to potentially redact sensitive info before serving
82
+ config_data = load_config(config_path)
83
+ # Redact sensitive keys (e.g., api_key)
84
+ if 'llm' in config_data:
85
+ for profile in config_data['llm']: config_data['llm'][profile].pop('api_key', None)
86
+ return JsonResponse(config_data)
87
+ else:
88
+ logger.error(f"Swarm config file not found at expected locations (tried: {config_path})")
89
+ return JsonResponse({"error": "Configuration file not found."}, status=404)
90
+
91
+ except Exception as e:
92
+ logger.error(f"Error serving swarm config: {e}", exc_info=True)
93
+ return JsonResponse({"error": "Failed to load or serve configuration."}, status=500)
94
+
95
+ # --- Potentially other core API views if needed ---
96
+ # Example: A view to list available blueprints (might duplicate CLI list command logic)
97
+
98
+ @csrf_exempt # If POST is needed and no CSRF token available from UI
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)
76
112
 
77
113
 
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()
101
-
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.")
109
-
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")
117
-
118
- # Add any other views that were originally in the main views.py if needed