open-swarm 0.1.1743070217__py3-none-any.whl → 0.1.1743364176__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.1743364176.dist-info/METADATA +286 -0
- open_swarm-0.1.1743364176.dist-info/RECORD +260 -0
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/WHEEL +1 -2
- open_swarm-0.1.1743364176.dist-info/entry_points.txt +2 -0
- swarm/__init__.py +0 -2
- swarm/auth.py +53 -49
- swarm/blueprints/README.md +67 -0
- swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
- swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
- swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
- swarm/blueprints/divine_code/__init__.py +0 -0
- swarm/blueprints/divine_code/apps.py +11 -0
- swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
- swarm/blueprints/django_chat/apps.py +6 -0
- swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
- swarm/blueprints/django_chat/urls.py +8 -0
- swarm/blueprints/django_chat/views.py +32 -0
- swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
- swarm/blueprints/family_ties/apps.py +11 -0
- swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
- swarm/blueprints/family_ties/models.py +19 -0
- swarm/blueprints/family_ties/serializers.py +7 -0
- swarm/blueprints/family_ties/settings.py +16 -0
- swarm/blueprints/family_ties/urls.py +10 -0
- swarm/blueprints/family_ties/views.py +26 -0
- swarm/blueprints/flock/__init__.py +0 -0
- swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
- swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
- swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
- swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
- swarm/blueprints/rue_code/__init__.py +0 -0
- swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
- swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
- swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
- swarm/extensions/blueprint/__init__.py +30 -15
- swarm/extensions/blueprint/agent_utils.py +16 -40
- swarm/extensions/blueprint/blueprint_base.py +141 -543
- swarm/extensions/blueprint/blueprint_discovery.py +112 -98
- swarm/extensions/blueprint/cli_handler.py +185 -0
- swarm/extensions/blueprint/config_loader.py +122 -0
- swarm/extensions/blueprint/django_utils.py +181 -79
- swarm/extensions/blueprint/interactive_mode.py +1 -1
- swarm/extensions/config/config_loader.py +83 -200
- swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_cli.py +199 -287
- swarm/llm/chat_completion.py +26 -55
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +58 -0
- swarm/permissions.py +38 -0
- swarm/serializers.py +96 -5
- swarm/settings.py +95 -110
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
- swarm/static/contrib/markedjs/marked.min.js +6 -0
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
- swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
- swarm/static/contrib/tabler-icons/archive.svg +21 -0
- swarm/static/contrib/tabler-icons/artboard.svg +27 -0
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
- swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
- swarm/static/contrib/tabler-icons/carambola.svg +19 -0
- swarm/static/contrib/tabler-icons/copy.svg +20 -0
- swarm/static/contrib/tabler-icons/download.svg +21 -0
- swarm/static/contrib/tabler-icons/edit.svg +21 -0
- swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
- swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
- swarm/static/contrib/tabler-icons/headset.svg +22 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
- swarm/static/contrib/tabler-icons/message-star.svg +22 -0
- swarm/static/contrib/tabler-icons/message-x.svg +23 -0
- swarm/static/contrib/tabler-icons/message.svg +21 -0
- swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
- swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
- swarm/static/contrib/tabler-icons/robot.svg +26 -0
- swarm/static/contrib/tabler-icons/search.svg +19 -0
- swarm/static/contrib/tabler-icons/settings.svg +20 -0
- swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
- swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
- swarm/static/css/dropdown.css +22 -0
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +23 -0
- swarm/static/rest_mode/css/base.css +470 -0
- swarm/static/rest_mode/css/chat-history.css +286 -0
- swarm/static/rest_mode/css/chat.css +251 -0
- swarm/static/rest_mode/css/chatbot.css +74 -0
- swarm/static/rest_mode/css/chatgpt.css +62 -0
- swarm/static/rest_mode/css/colors/corporate.css +74 -0
- swarm/static/rest_mode/css/colors/pastel.css +81 -0
- swarm/static/rest_mode/css/colors/tropical.css +82 -0
- swarm/static/rest_mode/css/general.css +142 -0
- swarm/static/rest_mode/css/layout.css +167 -0
- swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
- swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
- swarm/static/rest_mode/css/messages.css +84 -0
- swarm/static/rest_mode/css/messenger.css +135 -0
- swarm/static/rest_mode/css/settings.css +91 -0
- swarm/static/rest_mode/css/simple.css +44 -0
- swarm/static/rest_mode/css/slack.css +58 -0
- swarm/static/rest_mode/css/style.css +156 -0
- swarm/static/rest_mode/css/theme.css +30 -0
- swarm/static/rest_mode/css/toast.css +40 -0
- swarm/static/rest_mode/js/auth.js +9 -0
- swarm/static/rest_mode/js/blueprint.js +41 -0
- swarm/static/rest_mode/js/blueprintUtils.js +12 -0
- swarm/static/rest_mode/js/chatLogic.js +79 -0
- swarm/static/rest_mode/js/debug.js +63 -0
- swarm/static/rest_mode/js/events.js +98 -0
- swarm/static/rest_mode/js/main.js +19 -0
- swarm/static/rest_mode/js/messages.js +264 -0
- swarm/static/rest_mode/js/messengerLogic.js +355 -0
- swarm/static/rest_mode/js/modules/apiService.js +84 -0
- swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
- swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
- swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
- swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
- swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
- swarm/static/rest_mode/js/modules/state.js +7 -0
- swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
- swarm/static/rest_mode/js/modules/validation.js +23 -0
- swarm/static/rest_mode/js/rendering.js +119 -0
- swarm/static/rest_mode/js/settings.js +130 -0
- swarm/static/rest_mode/js/sidebar.js +94 -0
- swarm/static/rest_mode/js/simpleLogic.js +37 -0
- swarm/static/rest_mode/js/slackLogic.js +66 -0
- swarm/static/rest_mode/js/splash.js +76 -0
- swarm/static/rest_mode/js/theme.js +111 -0
- swarm/static/rest_mode/js/toast.js +36 -0
- swarm/static/rest_mode/js/ui.js +265 -0
- swarm/static/rest_mode/js/validation.js +57 -0
- swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
- swarm/static/rest_mode/svg/arrow_down.svg +5 -0
- swarm/static/rest_mode/svg/arrow_left.svg +5 -0
- swarm/static/rest_mode/svg/arrow_right.svg +5 -0
- swarm/static/rest_mode/svg/arrow_up.svg +5 -0
- swarm/static/rest_mode/svg/attach.svg +8 -0
- swarm/static/rest_mode/svg/avatar.svg +7 -0
- swarm/static/rest_mode/svg/canvas.svg +6 -0
- swarm/static/rest_mode/svg/chat_history.svg +4 -0
- swarm/static/rest_mode/svg/close.svg +5 -0
- swarm/static/rest_mode/svg/copy.svg +4 -0
- swarm/static/rest_mode/svg/dark_mode.svg +3 -0
- swarm/static/rest_mode/svg/edit.svg +5 -0
- swarm/static/rest_mode/svg/layout.svg +9 -0
- swarm/static/rest_mode/svg/logo.svg +29 -0
- swarm/static/rest_mode/svg/logout.svg +5 -0
- swarm/static/rest_mode/svg/mobile.svg +5 -0
- swarm/static/rest_mode/svg/new_chat.svg +4 -0
- swarm/static/rest_mode/svg/not_visible.svg +5 -0
- swarm/static/rest_mode/svg/plus.svg +7 -0
- swarm/static/rest_mode/svg/run_code.svg +6 -0
- swarm/static/rest_mode/svg/save.svg +4 -0
- swarm/static/rest_mode/svg/search.svg +6 -0
- swarm/static/rest_mode/svg/settings.svg +4 -0
- swarm/static/rest_mode/svg/speaker.svg +5 -0
- swarm/static/rest_mode/svg/stop.svg +6 -0
- swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
- swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
- swarm/static/rest_mode/svg/toggle_off.svg +6 -0
- swarm/static/rest_mode/svg/toggle_on.svg +6 -0
- swarm/static/rest_mode/svg/trash.svg +10 -0
- swarm/static/rest_mode/svg/undo.svg +3 -0
- swarm/static/rest_mode/svg/visible.svg +8 -0
- swarm/static/rest_mode/svg/voice.svg +10 -0
- swarm/templates/account/login.html +22 -0
- swarm/templates/account/signup.html +32 -0
- swarm/templates/base.html +30 -0
- swarm/templates/chat.html +43 -0
- swarm/templates/index.html +35 -0
- swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
- swarm/templates/rest_mode/components/header.html +45 -0
- swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
- swarm/templates/rest_mode/components/settings_dialog.html +97 -0
- swarm/templates/rest_mode/components/splash_screen.html +7 -0
- swarm/templates/rest_mode/components/top_bar.html +28 -0
- swarm/templates/rest_mode/message_ui.html +50 -0
- swarm/templates/rest_mode/slackbot.html +30 -0
- swarm/templates/simple_blueprint_page.html +24 -0
- swarm/templates/websocket_partials/final_system_message.html +3 -0
- swarm/templates/websocket_partials/system_message.html +4 -0
- swarm/templates/websocket_partials/user_message.html +5 -0
- swarm/urls.py +57 -74
- swarm/utils/log_utils.py +63 -0
- swarm/views/api_views.py +48 -39
- swarm/views/chat_views.py +156 -70
- swarm/views/core_views.py +85 -90
- swarm/views/model_views.py +64 -121
- swarm/views/utils.py +65 -441
- open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
- open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
- open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
- swarm/agent/agent.py +0 -49
- swarm/core.py +0 -326
- swarm/extensions/mcp/__init__.py +0 -1
- swarm/extensions/mcp/cache_utils.py +0 -36
- swarm/extensions/mcp/mcp_client.py +0 -341
- swarm/extensions/mcp/mcp_constants.py +0 -7
- swarm/extensions/mcp/mcp_tool_provider.py +0 -110
- swarm/types.py +0 -126
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/licenses/LICENSE +0 -0
swarm/views/api_views.py
CHANGED
@@ -1,46 +1,55 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
from rest_framework.
|
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
|
-
|
7
|
-
from
|
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
|
-
|
10
|
+
from swarm.views.utils import get_available_blueprints
|
13
11
|
|
14
|
-
|
15
|
-
exclude_from_schema = True
|
12
|
+
logger = logging.getLogger(__name__)
|
16
13
|
|
17
|
-
class
|
18
|
-
"""
|
19
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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.
|
10
|
-
from rest_framework.
|
11
|
-
|
12
|
-
from
|
13
|
-
|
14
|
-
|
15
|
-
from swarm.
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
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
|
-
|
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
|
42
|
-
|
43
|
-
#
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
"""
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|