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.
- open_swarm-0.1.1743070217.dist-info/METADATA +258 -0
- open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
- open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
- open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
- swarm/__init__.py +3 -0
- swarm/agent/__init__.py +7 -0
- swarm/agent/agent.py +49 -0
- swarm/apps.py +53 -0
- swarm/auth.py +56 -0
- swarm/consumers.py +141 -0
- swarm/core.py +326 -0
- swarm/extensions/__init__.py +1 -0
- swarm/extensions/blueprint/__init__.py +36 -0
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/blueprint_utils.py +17 -0
- swarm/extensions/blueprint/common_utils.py +12 -0
- swarm/extensions/blueprint/django_utils.py +203 -0
- swarm/extensions/blueprint/interactive_mode.py +102 -0
- swarm/extensions/blueprint/modes/rest_mode.py +37 -0
- swarm/extensions/blueprint/output_utils.py +95 -0
- swarm/extensions/blueprint/spinner.py +91 -0
- swarm/extensions/cli/__init__.py +0 -0
- swarm/extensions/cli/blueprint_runner.py +251 -0
- swarm/extensions/cli/cli_args.py +88 -0
- swarm/extensions/cli/commands/__init__.py +0 -0
- swarm/extensions/cli/commands/blueprint_management.py +31 -0
- swarm/extensions/cli/commands/config_management.py +15 -0
- swarm/extensions/cli/commands/edit_config.py +77 -0
- swarm/extensions/cli/commands/list_blueprints.py +22 -0
- swarm/extensions/cli/commands/validate_env.py +57 -0
- swarm/extensions/cli/commands/validate_envvars.py +39 -0
- swarm/extensions/cli/interactive_shell.py +41 -0
- swarm/extensions/cli/main.py +36 -0
- swarm/extensions/cli/selection.py +43 -0
- swarm/extensions/cli/utils/discover_commands.py +32 -0
- swarm/extensions/cli/utils/env_setup.py +15 -0
- swarm/extensions/cli/utils.py +105 -0
- swarm/extensions/config/__init__.py +6 -0
- swarm/extensions/config/config_loader.py +208 -0
- swarm/extensions/config/config_manager.py +258 -0
- swarm/extensions/config/server_config.py +49 -0
- swarm/extensions/config/setup_wizard.py +103 -0
- swarm/extensions/config/utils/__init__.py +0 -0
- swarm/extensions/config/utils/logger.py +36 -0
- swarm/extensions/launchers/__init__.py +1 -0
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
- swarm/extensions/launchers/swarm_api.py +68 -0
- swarm/extensions/launchers/swarm_cli.py +304 -0
- swarm/extensions/launchers/swarm_wrapper.py +29 -0
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +36 -0
- swarm/extensions/mcp/mcp_client.py +341 -0
- swarm/extensions/mcp/mcp_constants.py +7 -0
- swarm/extensions/mcp/mcp_tool_provider.py +110 -0
- swarm/llm/chat_completion.py +195 -0
- swarm/messages.py +132 -0
- swarm/migrations/0010_initial_chat_models.py +51 -0
- swarm/migrations/__init__.py +0 -0
- swarm/models.py +45 -0
- swarm/repl/__init__.py +1 -0
- swarm/repl/repl.py +87 -0
- swarm/serializers.py +12 -0
- swarm/settings.py +189 -0
- swarm/tool_executor.py +239 -0
- swarm/types.py +126 -0
- swarm/urls.py +89 -0
- swarm/util.py +124 -0
- swarm/utils/color_utils.py +40 -0
- swarm/utils/context_utils.py +272 -0
- swarm/utils/general_utils.py +162 -0
- swarm/utils/logger.py +61 -0
- swarm/utils/logger_setup.py +25 -0
- swarm/utils/message_sequence.py +173 -0
- swarm/utils/message_utils.py +95 -0
- swarm/utils/redact.py +68 -0
- swarm/views/__init__.py +41 -0
- swarm/views/api_views.py +46 -0
- swarm/views/chat_views.py +76 -0
- swarm/views/core_views.py +118 -0
- swarm/views/message_views.py +40 -0
- swarm/views/model_views.py +135 -0
- swarm/views/utils.py +457 -0
- swarm/views/web_views.py +149 -0
- 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
|
swarm/views/__init__.py
ADDED
@@ -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
|
+
|
swarm/views/api_views.py
ADDED
@@ -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)
|