open-swarm 0.1.1743070217__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. open_swarm-0.1.1743070217.dist-info/METADATA +258 -0
  2. open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
  3. open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
  4. open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
  5. open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
  6. open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
  7. swarm/__init__.py +3 -0
  8. swarm/agent/__init__.py +7 -0
  9. swarm/agent/agent.py +49 -0
  10. swarm/apps.py +53 -0
  11. swarm/auth.py +56 -0
  12. swarm/consumers.py +141 -0
  13. swarm/core.py +326 -0
  14. swarm/extensions/__init__.py +1 -0
  15. swarm/extensions/blueprint/__init__.py +36 -0
  16. swarm/extensions/blueprint/agent_utils.py +45 -0
  17. swarm/extensions/blueprint/blueprint_base.py +562 -0
  18. swarm/extensions/blueprint/blueprint_discovery.py +112 -0
  19. swarm/extensions/blueprint/blueprint_utils.py +17 -0
  20. swarm/extensions/blueprint/common_utils.py +12 -0
  21. swarm/extensions/blueprint/django_utils.py +203 -0
  22. swarm/extensions/blueprint/interactive_mode.py +102 -0
  23. swarm/extensions/blueprint/modes/rest_mode.py +37 -0
  24. swarm/extensions/blueprint/output_utils.py +95 -0
  25. swarm/extensions/blueprint/spinner.py +91 -0
  26. swarm/extensions/cli/__init__.py +0 -0
  27. swarm/extensions/cli/blueprint_runner.py +251 -0
  28. swarm/extensions/cli/cli_args.py +88 -0
  29. swarm/extensions/cli/commands/__init__.py +0 -0
  30. swarm/extensions/cli/commands/blueprint_management.py +31 -0
  31. swarm/extensions/cli/commands/config_management.py +15 -0
  32. swarm/extensions/cli/commands/edit_config.py +77 -0
  33. swarm/extensions/cli/commands/list_blueprints.py +22 -0
  34. swarm/extensions/cli/commands/validate_env.py +57 -0
  35. swarm/extensions/cli/commands/validate_envvars.py +39 -0
  36. swarm/extensions/cli/interactive_shell.py +41 -0
  37. swarm/extensions/cli/main.py +36 -0
  38. swarm/extensions/cli/selection.py +43 -0
  39. swarm/extensions/cli/utils/discover_commands.py +32 -0
  40. swarm/extensions/cli/utils/env_setup.py +15 -0
  41. swarm/extensions/cli/utils.py +105 -0
  42. swarm/extensions/config/__init__.py +6 -0
  43. swarm/extensions/config/config_loader.py +208 -0
  44. swarm/extensions/config/config_manager.py +258 -0
  45. swarm/extensions/config/server_config.py +49 -0
  46. swarm/extensions/config/setup_wizard.py +103 -0
  47. swarm/extensions/config/utils/__init__.py +0 -0
  48. swarm/extensions/config/utils/logger.py +36 -0
  49. swarm/extensions/launchers/__init__.py +1 -0
  50. swarm/extensions/launchers/build_launchers.py +14 -0
  51. swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
  52. swarm/extensions/launchers/swarm_api.py +68 -0
  53. swarm/extensions/launchers/swarm_cli.py +304 -0
  54. swarm/extensions/launchers/swarm_wrapper.py +29 -0
  55. swarm/extensions/mcp/__init__.py +1 -0
  56. swarm/extensions/mcp/cache_utils.py +36 -0
  57. swarm/extensions/mcp/mcp_client.py +341 -0
  58. swarm/extensions/mcp/mcp_constants.py +7 -0
  59. swarm/extensions/mcp/mcp_tool_provider.py +110 -0
  60. swarm/llm/chat_completion.py +195 -0
  61. swarm/messages.py +132 -0
  62. swarm/migrations/0010_initial_chat_models.py +51 -0
  63. swarm/migrations/__init__.py +0 -0
  64. swarm/models.py +45 -0
  65. swarm/repl/__init__.py +1 -0
  66. swarm/repl/repl.py +87 -0
  67. swarm/serializers.py +12 -0
  68. swarm/settings.py +189 -0
  69. swarm/tool_executor.py +239 -0
  70. swarm/types.py +126 -0
  71. swarm/urls.py +89 -0
  72. swarm/util.py +124 -0
  73. swarm/utils/color_utils.py +40 -0
  74. swarm/utils/context_utils.py +272 -0
  75. swarm/utils/general_utils.py +162 -0
  76. swarm/utils/logger.py +61 -0
  77. swarm/utils/logger_setup.py +25 -0
  78. swarm/utils/message_sequence.py +173 -0
  79. swarm/utils/message_utils.py +95 -0
  80. swarm/utils/redact.py +68 -0
  81. swarm/views/__init__.py +41 -0
  82. swarm/views/api_views.py +46 -0
  83. swarm/views/chat_views.py +76 -0
  84. swarm/views/core_views.py +118 -0
  85. swarm/views/message_views.py +40 -0
  86. swarm/views/model_views.py +135 -0
  87. swarm/views/utils.py +457 -0
  88. swarm/views/web_views.py +149 -0
  89. swarm/wsgi.py +16 -0
@@ -0,0 +1,173 @@
1
+ """
2
+ Utilities for validating and repairing message sequences.
3
+ """
4
+
5
+ from typing import List, Dict, Any
6
+ import json
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ try:
12
+ from .message_utils import filter_duplicate_system_messages
13
+ except ImportError:
14
+ try: from swarm.utils.message_utils import filter_duplicate_system_messages
15
+ except ImportError:
16
+ logger.warning("filter_duplicate_system_messages not found. Using dummy.")
17
+ def filter_duplicate_system_messages(messages):
18
+ output = []; system_found = False
19
+ for msg in messages:
20
+ if isinstance(msg, dict) and msg.get("role") == "system":
21
+ if not system_found: output.append(msg); system_found = True
22
+ # *** Fix in dummy: Append non-dicts too if needed, or filter here?
23
+ # Let's assume the filter should focus only on system duplicates for now.
24
+ elif not (isinstance(msg, dict) and msg.get("role") == "system"):
25
+ output.append(msg)
26
+ return output
27
+
28
+ def validate_message_sequence(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
29
+ """
30
+ Ensure tool messages correspond to valid tool calls in the sequence.
31
+ Also filters out non-dictionary items.
32
+ """
33
+ if not isinstance(messages, list):
34
+ logger.error(f"Invalid messages type for validation: {type(messages)}. Returning [].")
35
+ return []
36
+ logger.debug(f"Validating message sequence with {len(messages)} messages")
37
+
38
+ # *** FIX: Filter non-dicts FIRST ***
39
+ dict_messages = [msg for msg in messages if isinstance(msg, dict)]
40
+ if len(dict_messages) < len(messages):
41
+ logger.warning(f"Removed {len(messages) - len(dict_messages)} non-dictionary items during validation.")
42
+
43
+ try:
44
+ # *** FIX: Operate ONLY on dict_messages ***
45
+ valid_tool_call_ids = {
46
+ tc["id"]
47
+ for msg in dict_messages # Use filtered list
48
+ if msg.get("role") == "assistant" and isinstance(msg.get("tool_calls"), list)
49
+ for tc in msg.get("tool_calls", [])
50
+ if isinstance(tc, dict) and "id" in tc
51
+ }
52
+ except Exception as e:
53
+ logger.error(f"Error building valid_tool_call_ids: {e}", exc_info=True)
54
+ valid_tool_call_ids = set()
55
+
56
+ validated_messages = []
57
+ # *** FIX: Operate ONLY on dict_messages ***
58
+ for msg in dict_messages:
59
+ role = msg.get("role")
60
+ if role == "tool":
61
+ tool_call_id = msg.get("tool_call_id")
62
+ if tool_call_id in valid_tool_call_ids:
63
+ validated_messages.append(msg)
64
+ else:
65
+ logger.warning(f"Removing orphan tool message: {str(msg)[:100]}")
66
+ # *** FIX: Add basic check for other essential roles before appending ***
67
+ elif role in ["system", "user", "assistant"]:
68
+ # We could add more role-specific checks here if needed (like _is_valid_message)
69
+ # For now, just ensure it's one of the expected roles.
70
+ validated_messages.append(msg)
71
+ else:
72
+ logger.warning(f"Removing message with unknown/missing role during validation: {str(msg)[:100]}")
73
+
74
+
75
+ return validated_messages
76
+
77
+ def repair_message_payload(messages: List[Dict[str, Any]], debug: bool = False) -> List[Dict[str, Any]]:
78
+ """
79
+ Repair the message sequence by potentially inserting dummy messages for missing pairs.
80
+ Filters invalid messages and orphan tools first.
81
+ """
82
+ if not isinstance(messages, list):
83
+ logger.error(f"Invalid messages type for repair: {type(messages)}. Returning [].")
84
+ return []
85
+ logger.debug(f"Repairing message payload with {len(messages)} messages")
86
+
87
+ try:
88
+ messages_no_dup_sys = filter_duplicate_system_messages(messages)
89
+ except Exception as e:
90
+ logger.error(f"Error during filter_duplicate_system_messages: {e}. Proceeding.")
91
+ messages_no_dup_sys = messages
92
+
93
+ # Run validation which now correctly handles non-dicts first
94
+ repaired_validated = validate_message_sequence(messages_no_dup_sys)
95
+ logger.debug(f"After validation, {len(repaired_validated)} messages remain for repair loop.")
96
+
97
+
98
+ final_sequence = []
99
+ i = 0
100
+ processed_tool_ids = set()
101
+
102
+ while i < len(repaired_validated):
103
+ msg = repaired_validated[i]
104
+ role = msg.get("role") # Should always be a valid dict here
105
+
106
+ if role == "assistant" and isinstance(msg.get("tool_calls"), list) and msg["tool_calls"]:
107
+ logger.debug(f"Repair Loop: Processing assistant at index {i}")
108
+ final_sequence.append(msg)
109
+ tool_calls = msg["tool_calls"]
110
+ expected_ids = {tc.get("id") for tc in tool_calls if isinstance(tc, dict) and tc.get("id")}
111
+
112
+ j = i + 1
113
+ found_ids_for_this_call = set()
114
+ logger.debug(f" Looking ahead for tools from index {j}. Expected IDs: {expected_ids}")
115
+ while j < len(repaired_validated) and repaired_validated[j].get("role") == "tool":
116
+ tool_msg = repaired_validated[j]
117
+ tool_call_id = tool_msg.get("tool_call_id")
118
+ logger.debug(f" Checking tool at index {j} (ID: {tool_call_id})")
119
+ if tool_call_id in expected_ids:
120
+ final_sequence.append(tool_msg)
121
+ found_ids_for_this_call.add(tool_call_id)
122
+ processed_tool_ids.add(tool_call_id)
123
+ logger.debug(f" Found and appended expected tool response.")
124
+ else:
125
+ logger.debug(f" Tool ID does not match current assistant call. Stopping lookahead.")
126
+ break
127
+ j += 1
128
+
129
+ missing_ids_for_this_call = expected_ids - found_ids_for_this_call
130
+ if missing_ids_for_this_call:
131
+ logger.warning(f" Missing {len(missing_ids_for_this_call)} tool responses for assistant call {i}. IDs: {missing_ids_for_this_call}. Inserting dummies.")
132
+ for missing_id in missing_ids_for_this_call:
133
+ tool_name = "unknown_tool"
134
+ for tc in tool_calls:
135
+ if isinstance(tc, dict) and tc.get("id") == missing_id:
136
+ tool_name = tc.get("function", {}).get("name", "unknown_tool"); break
137
+ dummy_tool = {"role": "tool", "tool_call_id": missing_id, "name": tool_name, "content": f"Error: Tool response for {tool_name} missing."} # Use name field like T1_RESP
138
+ logger.debug(f" Appending dummy tool: {dummy_tool}")
139
+ final_sequence.append(dummy_tool)
140
+ processed_tool_ids.add(missing_id)
141
+ # else:
142
+ # logger.debug(f" All tool responses found for assistant call {i}.")
143
+
144
+ i = j # Move main index past assistant and its processed tools
145
+
146
+ elif role == "tool":
147
+ # This case handles tools that were *not* removed by validation (meaning their ID exists somewhere)
148
+ # but were not found immediately after their corresponding assistant call by the lookahead above.
149
+ # OR tools whose assistant call was completely missing/invalid.
150
+ tool_call_id = msg.get("tool_call_id")
151
+ if tool_call_id in processed_tool_ids:
152
+ logger.debug(f"Repair Loop: Skipping tool msg at index {i} (id: {tool_call_id}), already processed.")
153
+ i += 1
154
+ else:
155
+ # Insert a dummy assistant call before it
156
+ logger.warning(f"Repair Loop: Found tool msg {i} (id: {tool_call_id}) without preceding assistant. Inserting dummy assistant.")
157
+ tool_name = msg.get("name", "unknown_tool") # Get tool name from tool message itself
158
+ dummy_assistant = {"role": "assistant", "content": None, "tool_calls": [{"id": tool_call_id, "type": "function", "function": {"name": tool_name, "arguments": "{}"}}]}
159
+ final_sequence.append(dummy_assistant)
160
+ final_sequence.append(msg)
161
+ processed_tool_ids.add(tool_call_id)
162
+ i += 1
163
+ else:
164
+ # System or User message
165
+ logger.debug(f"Repair Loop: Processing {role} at index {i}")
166
+ final_sequence.append(msg)
167
+ i += 1
168
+
169
+ if debug: logger.debug(f"Repaired payload: {json.dumps(final_sequence, indent=2, default=str)}")
170
+ elif len(messages) != len(final_sequence): # Log if changes were made (even without full debug)
171
+ logger.info(f"Repair changed message count from {len(messages)} to {len(final_sequence)}")
172
+
173
+ return final_sequence
@@ -0,0 +1,95 @@
1
+ """
2
+ Utility functions for processing chat messages in the Swarm framework.
3
+ """
4
+
5
+ import logging
6
+ import json # Added import
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def filter_duplicate_system_messages(messages):
11
+ """Remove duplicate system messages, keeping only the first occurrence."""
12
+ filtered = []
13
+ system_found = False
14
+ for msg in messages:
15
+ # Ensure msg is a dictionary and has a 'role' key
16
+ if isinstance(msg, dict) and "role" in msg:
17
+ if msg["role"] == "system":
18
+ if not system_found:
19
+ filtered.append(msg)
20
+ system_found = True
21
+ # Else: skip subsequent system messages
22
+ else:
23
+ filtered.append(msg)
24
+ elif isinstance(msg, dict): # Handle dicts without 'role' if necessary
25
+ # Keep dicts without roles based on previous behavior? Or filter?
26
+ # Let's keep them for now, consistent with test_filter_mixed_valid_invalid expectation
27
+ logger.warning(f"Message dictionary missing 'role' key: {msg}")
28
+ filtered.append(msg)
29
+ else:
30
+ logger.warning(f"Skipping non-dictionary item in messages list: {type(msg)}")
31
+ # Skip non-dict items
32
+
33
+ return filtered
34
+
35
+ def filter_messages(messages):
36
+ """Filter out messages with empty/None content or only whitespace, unless they have tool_calls."""
37
+ result = []
38
+ if not isinstance(messages, list):
39
+ logger.error(f"filter_messages received non-list input: {type(messages)}")
40
+ return []
41
+
42
+ for msg in messages:
43
+ if not isinstance(msg, dict):
44
+ logger.warning(f"Skipping non-dictionary item in messages list: {type(msg)}")
45
+ continue
46
+
47
+ content = msg.get('content')
48
+ tool_calls = msg.get('tool_calls')
49
+
50
+ # Keep message if it has non-whitespace content OR has non-empty tool_calls
51
+ has_valid_content = content is not None and isinstance(content, str) and content.strip() != ""
52
+ has_tool_calls = tool_calls is not None and isinstance(tool_calls, list) and len(tool_calls) > 0
53
+
54
+ if has_valid_content or has_tool_calls:
55
+ result.append(msg)
56
+ else:
57
+ logger.debug(f"Filtering out message due to empty/None/whitespace content and no tool calls: {msg}")
58
+
59
+ return result
60
+
61
+
62
+ def update_null_content(input_data):
63
+ """
64
+ Replace 'content: None' with 'content: ""' in a message dictionary or list of dictionaries.
65
+ Does NOT add the 'content' key if it's missing.
66
+ """
67
+ if isinstance(input_data, dict):
68
+ # Process a single message dictionary
69
+ # Check if 'content' key exists AND its value is None
70
+ if 'content' in input_data and input_data['content'] is None:
71
+ input_data['content'] = ""
72
+ logger.debug(f"Updated 'content: None' to 'content: \"\"' in dict: {input_data.get('role', 'N/A')}")
73
+ return input_data
74
+ elif isinstance(input_data, list):
75
+ # Process a list of messages (modify in-place or create new list)
76
+ # Creating new list to avoid modifying original list unexpectedly
77
+ processed_list = []
78
+ for msg in input_data:
79
+ if isinstance(msg, dict):
80
+ # Create a copy to modify
81
+ new_msg = msg.copy()
82
+ if 'content' in new_msg and new_msg['content'] is None:
83
+ new_msg['content'] = ""
84
+ logger.debug(f"Updated 'content: None' to 'content: \"\"' in list item: {new_msg.get('role', 'N/A')}")
85
+ processed_list.append(new_msg)
86
+ else:
87
+ logger.warning(f"Skipping non-dictionary item during null content update: {type(msg)}")
88
+ processed_list.append(msg) # Append non-dict item unchanged
89
+ return processed_list
90
+ else:
91
+ # Return other types unchanged
92
+ logger.warning(f"update_null_content received unexpected type: {type(input_data)}. Returning unchanged.")
93
+ return input_data
94
+
95
+ # redact_sensitive_data is now centralized in swarm.utils.redact
swarm/utils/redact.py ADDED
@@ -0,0 +1,68 @@
1
+ """
2
+ Utilities for redacting sensitive data.
3
+ """
4
+
5
+ import re
6
+ from typing import Union, Dict, List, Optional
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ DEFAULT_SENSITIVE_KEYS = ["secret", "password", "api_key", "apikey", "token", "access_token", "client_secret"]
12
+
13
+ def redact_sensitive_data(
14
+ data: Union[str, Dict, List],
15
+ sensitive_keys: Optional[List[str]] = None,
16
+ reveal_chars: int = 4,
17
+ mask: str = "[REDACTED]"
18
+ ) -> Union[str, Dict, List]:
19
+ """
20
+ Recursively redact sensitive information from dictionaries or lists based on keys.
21
+ Applies partial redaction to string values associated with sensitive keys.
22
+ Does NOT redact standalone strings.
23
+
24
+ Args:
25
+ data: Input data to redact (dict or list). Other types returned as is.
26
+ sensitive_keys: List of dictionary keys to treat as sensitive. Defaults to common keys.
27
+ reveal_chars: Number of initial/trailing characters to reveal (0 means full redaction).
28
+ mask: String used for redaction in the middle or for full redaction of strings.
29
+
30
+ Returns:
31
+ Redacted data structure of the same type as input.
32
+ """
33
+ keys_to_redact = sensitive_keys if sensitive_keys is not None else DEFAULT_SENSITIVE_KEYS
34
+ keys_to_redact_lower = {key.lower() for key in keys_to_redact}
35
+
36
+ if isinstance(data, dict):
37
+ redacted_dict = {}
38
+ for key, value in data.items():
39
+ if isinstance(key, str) and key.lower() in keys_to_redact_lower:
40
+ if isinstance(value, str):
41
+ val_len = len(value)
42
+ if reveal_chars > 0 and val_len > reveal_chars * 2:
43
+ redacted_dict[key] = f"{value[:reveal_chars]}{mask}{value[-reveal_chars:]}"
44
+ elif val_len > 0:
45
+ # Use the provided mask string directly for full redaction
46
+ redacted_dict[key] = mask
47
+ else:
48
+ redacted_dict[key] = "" # Redact empty string as empty
49
+ else:
50
+ # Use specific placeholder for non-strings
51
+ redacted_dict[key] = "[REDACTED NON-STRING]"
52
+ else:
53
+ # Recursively redact nested structures if key is not sensitive
54
+ redacted_dict[key] = redact_sensitive_data(value, keys_to_redact, reveal_chars, mask)
55
+ return redacted_dict
56
+
57
+ elif isinstance(data, list):
58
+ # Recursively redact items in a list ONLY if they are dicts or lists themselves.
59
+ processed_list = []
60
+ for item in data:
61
+ if isinstance(item, (dict, list)):
62
+ processed_list.append(redact_sensitive_data(item, keys_to_redact, reveal_chars, mask))
63
+ else:
64
+ processed_list.append(item) # Keep non-dict/list items (like strings) unchanged
65
+ return processed_list
66
+
67
+ # Return data unchanged if it's not a dict or list (including standalone strings)
68
+ return data
@@ -0,0 +1,41 @@
1
+ """
2
+ Initializes the views package and exposes key view modules and viewsets.
3
+ """
4
+ # Import the view modules to make them accessible via swarm.views.*
5
+ # Use try-except for robustness during development/refactoring
6
+ try:
7
+ from . import core_views
8
+ except ImportError as e:
9
+ print(f"Warning: Could not import swarm.views.core_views: {e}")
10
+ core_views = None
11
+
12
+ try:
13
+ from . import chat_views
14
+ except ImportError as e:
15
+ print(f"Warning: Could not import swarm.views.chat_views: {e}")
16
+ chat_views = None
17
+
18
+ try:
19
+ from . import model_views
20
+ except ImportError as e:
21
+ print(f"Warning: Could not import swarm.views.model_views: {e}")
22
+ model_views = None
23
+
24
+ try:
25
+ from . import message_views
26
+ from .message_views import ChatMessageViewSet
27
+ except ImportError as e:
28
+ print(f"Warning: Could not import swarm.views.message_views: {e}")
29
+ message_views = None
30
+ ChatMessageViewSet = None # Ensure it's None if import fails
31
+
32
+ try:
33
+ from . import utils
34
+ except ImportError as e:
35
+ print(f"Warning: Could not import swarm.views.utils: {e}")
36
+ utils = None
37
+
38
+
39
+ # Expose only successfully imported components
40
+ __all__ = [name for name, obj in globals().items() if obj is not None and not name.startswith('_')]
41
+
@@ -0,0 +1,46 @@
1
+ """
2
+ API-specific views and viewsets for Open Swarm MCP Core.
3
+ """
4
+ from rest_framework.viewsets import ModelViewSet
5
+ 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
11
+
12
+ logger = setup_logger(__name__)
13
+
14
+ class HiddenSpectacularAPIView(BaseSpectacularAPIView):
15
+ exclude_from_schema = True
16
+
17
+ class ChatMessageViewSet(ModelViewSet):
18
+ """API viewset for managing chat messages."""
19
+ authentication_classes = []
20
+ permission_classes = [AllowAny]
21
+ queryset = ChatMessage.objects.all()
22
+ serializer_class = ChatMessageSerializer
23
+
24
+ @extend_schema(summary="List all chat messages")
25
+ def list(self, request, *args, **kwargs):
26
+ return super().list(request, *args, **kwargs)
27
+
28
+ @extend_schema(summary="Retrieve a chat message by its unique id")
29
+ def retrieve(self, request, *args, **kwargs):
30
+ return super().retrieve(request, *args, **kwargs)
31
+
32
+ @extend_schema(summary="Create a new chat message")
33
+ def create(self, request, *args, **kwargs):
34
+ return super().create(request, *args, **kwargs)
35
+
36
+ @extend_schema(summary="Update an existing chat message")
37
+ def update(self, request, *args, **kwargs):
38
+ return super().update(request, *args, **kwargs)
39
+
40
+ @extend_schema(summary="Partially update a chat message")
41
+ def partial_update(self, request, *args, **kwargs):
42
+ return super().partial_update(request, *args, **kwargs)
43
+
44
+ @extend_schema(summary="Delete a chat message by its unique id")
45
+ def destroy(self, request, *args, **kwargs):
46
+ return super().destroy(request, *args, **kwargs)
@@ -0,0 +1,76 @@
1
+ """
2
+ Chat-related views for Open Swarm MCP Core.
3
+ """
4
+ import asyncio
5
+ import logging
6
+ import json
7
+ from django.views.decorators.csrf import csrf_exempt
8
+ 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.
47
+ 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)
76
+
@@ -0,0 +1,118 @@
1
+ """
2
+ Core/UI related views for the Swarm framework.
3
+ """
4
+ import os
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from django.shortcuts import render, redirect
10
+ from django.http import JsonResponse, HttpResponse
11
+ from django.conf import settings
12
+ from django.views.decorators.csrf import csrf_exempt
13
+ from django.contrib.auth import authenticate, login
14
+
15
+ # Assuming blueprint discovery happens elsewhere and results are available if needed
16
+ # 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
+ logger = logging.getLogger(__name__)
20
+
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
+
38
+
39
+ @csrf_exempt
40
+ 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())
45
+ 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
49
+ }
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
+ }
65
+
66
+ def serve_swarm_config(request):
67
+ """Serve the swarm configuration file as JSON."""
68
+ 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)
76
+
77
+
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
@@ -0,0 +1,40 @@
1
+ """
2
+ Views related to Chat Messages.
3
+ """
4
+ from rest_framework.viewsets import ModelViewSet
5
+ from rest_framework.permissions import AllowAny
6
+ from drf_spectacular.utils import extend_schema
7
+
8
+ from swarm.models import ChatMessage
9
+ from swarm.serializers import ChatMessageSerializer
10
+
11
+ class ChatMessageViewSet(ModelViewSet):
12
+ """API viewset for managing chat messages."""
13
+ authentication_classes = []
14
+ permission_classes = [AllowAny]
15
+ queryset = ChatMessage.objects.all().order_by('-timestamp') # Order by timestamp descending
16
+ serializer_class = ChatMessageSerializer
17
+
18
+ @extend_schema(summary="List all chat messages")
19
+ def list(self, request, *args, **kwargs):
20
+ return super().list(request, *args, **kwargs)
21
+
22
+ @extend_schema(summary="Retrieve a chat message by its unique id")
23
+ def retrieve(self, request, *args, **kwargs):
24
+ return super().retrieve(request, *args, **kwargs)
25
+
26
+ @extend_schema(summary="Create a new chat message")
27
+ def create(self, request, *args, **kwargs):
28
+ return super().create(request, *args, **kwargs)
29
+
30
+ @extend_schema(summary="Update an existing chat message")
31
+ def update(self, request, *args, **kwargs):
32
+ return super().update(request, *args, **kwargs)
33
+
34
+ @extend_schema(summary="Partially update a chat message")
35
+ def partial_update(self, request, *args, **kwargs):
36
+ return super().partial_update(request, *args, **kwargs)
37
+
38
+ @extend_schema(summary="Delete a chat message by its unique id")
39
+ def destroy(self, request, *args, **kwargs):
40
+ return super().destroy(request, *args, **kwargs)