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
swarm/messages.py
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
"""
|
2
|
+
Message handling utilities for the Swarm framework.
|
3
|
+
Defines the ChatMessage structure.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
from types import SimpleNamespace
|
9
|
+
from typing import Optional, List, Dict, Any, Union
|
10
|
+
|
11
|
+
# Import the specific Pydantic model used for tool calls
|
12
|
+
from .types import ChatCompletionMessageToolCall
|
13
|
+
|
14
|
+
# Configure module-level logging
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
# logger.setLevel(logging.DEBUG) # Uncomment for detailed message logging
|
17
|
+
if not logger.handlers:
|
18
|
+
stream_handler = logging.StreamHandler()
|
19
|
+
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s")
|
20
|
+
stream_handler.setFormatter(formatter)
|
21
|
+
logger.addHandler(stream_handler)
|
22
|
+
|
23
|
+
|
24
|
+
class ChatMessage(SimpleNamespace):
|
25
|
+
"""
|
26
|
+
Represents a chat message within the Swarm framework, aiming for compatibility
|
27
|
+
with OpenAI's chat completion message structure while allowing custom fields.
|
28
|
+
|
29
|
+
Attributes:
|
30
|
+
role (str): The role of the message sender (e.g., "system", "user", "assistant", "tool").
|
31
|
+
content (Optional[str]): The text content of the message. Can be None for assistant messages
|
32
|
+
requesting tool calls without initial text.
|
33
|
+
sender (str): Custom field identifying the specific agent or source (not sent to LLM API).
|
34
|
+
function_call (Optional[dict]): Deprecated field for legacy function calls.
|
35
|
+
tool_calls (Optional[List[ChatCompletionMessageToolCall]]): A list of tool calls requested
|
36
|
+
by the assistant.
|
37
|
+
tool_call_id (Optional[str]): Identifier for a tool response message, linking it to a specific
|
38
|
+
tool_call requested by the assistant.
|
39
|
+
name (Optional[str]): The name of the tool that was called (used in 'tool' role messages).
|
40
|
+
"""
|
41
|
+
|
42
|
+
def __init__(self, **kwargs):
|
43
|
+
"""
|
44
|
+
Initialize a ChatMessage instance.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
**kwargs: Arbitrary keyword arguments mapping to message attributes.
|
48
|
+
Defaults are provided for common fields.
|
49
|
+
"""
|
50
|
+
# Define default values for standard message attributes
|
51
|
+
defaults = {
|
52
|
+
"role": "assistant", # Default role
|
53
|
+
"content": None, # Default content to None (as per OpenAI spec for tool calls)
|
54
|
+
"sender": "assistant", # Default custom sender
|
55
|
+
"function_call": None, # Deprecated field
|
56
|
+
"tool_calls": None, # For assistant requests
|
57
|
+
"tool_call_id": None, # For tool responses
|
58
|
+
"name": None # For tool responses (function name)
|
59
|
+
}
|
60
|
+
# Merge provided kwargs with defaults
|
61
|
+
merged_attrs = defaults | kwargs
|
62
|
+
super().__init__(**merged_attrs)
|
63
|
+
|
64
|
+
# --- Validation and Type Conversion for tool_calls ---
|
65
|
+
# Ensure tool_calls, if present, is a list
|
66
|
+
if self.tool_calls is not None and not isinstance(self.tool_calls, list):
|
67
|
+
logger.warning(f"ChatMessage 'tool_calls' received non-list type ({type(self.tool_calls)}) for sender '{self.sender}'. Resetting to None.")
|
68
|
+
self.tool_calls = None
|
69
|
+
elif isinstance(self.tool_calls, list):
|
70
|
+
# Convert dictionary items in the list to ChatCompletionMessageToolCall objects
|
71
|
+
validated_tool_calls = []
|
72
|
+
for i, tc in enumerate(self.tool_calls):
|
73
|
+
if isinstance(tc, ChatCompletionMessageToolCall):
|
74
|
+
validated_tool_calls.append(tc)
|
75
|
+
elif isinstance(tc, dict):
|
76
|
+
try:
|
77
|
+
# Attempt to instantiate the Pydantic model
|
78
|
+
validated_tool_calls.append(ChatCompletionMessageToolCall(**tc))
|
79
|
+
except Exception as e: # Catch potential validation errors from Pydantic
|
80
|
+
logger.warning(f"Failed to convert dict to ChatCompletionMessageToolCall at index {i} for sender '{self.sender}': {e}. Skipping this tool call.")
|
81
|
+
logger.debug(f"Invalid tool call dict: {tc}")
|
82
|
+
else:
|
83
|
+
logger.warning(f"Invalid item type in 'tool_calls' list at index {i} ({type(tc)}) for sender '{self.sender}'. Skipping.")
|
84
|
+
self.tool_calls = validated_tool_calls if validated_tool_calls else None # Set back validated list or None if empty/all invalid
|
85
|
+
if self.tool_calls:
|
86
|
+
logger.debug(f"Validated {len(self.tool_calls)} tool calls for ChatMessage (sender: '{self.sender}')")
|
87
|
+
|
88
|
+
|
89
|
+
def model_dump(self) -> Dict[str, Any]:
|
90
|
+
"""
|
91
|
+
Serialize the message attributes relevant for the OpenAI API into a dictionary.
|
92
|
+
Excludes custom fields like 'sender'. Handles optional fields correctly.
|
93
|
+
"""
|
94
|
+
# Start with required 'role'
|
95
|
+
d: Dict[str, Any] = {"role": self.role}
|
96
|
+
|
97
|
+
# Include 'content' only if it's not None
|
98
|
+
# OpenAI API allows null content for assistant messages with tool_calls
|
99
|
+
if self.content is not None:
|
100
|
+
d["content"] = self.content
|
101
|
+
|
102
|
+
# Include 'tool_calls' if present and not empty
|
103
|
+
if self.tool_calls: # Checks for non-None and non-empty list after validation
|
104
|
+
# Ensure each item is dumped correctly using its own model_dump if available
|
105
|
+
d["tool_calls"] = [tc.model_dump() if hasattr(tc, 'model_dump') else tc for tc in self.tool_calls]
|
106
|
+
|
107
|
+
# Include 'tool_call_id' if present (for 'tool' role messages)
|
108
|
+
if self.tool_call_id is not None:
|
109
|
+
d["tool_call_id"] = self.tool_call_id
|
110
|
+
|
111
|
+
# Include 'name' if present (for 'tool' role messages, matches function name)
|
112
|
+
if self.name is not None:
|
113
|
+
d["name"] = self.name
|
114
|
+
|
115
|
+
# Include deprecated 'function_call' if present
|
116
|
+
if self.function_call is not None:
|
117
|
+
d["function_call"] = self.function_call
|
118
|
+
|
119
|
+
return d
|
120
|
+
|
121
|
+
|
122
|
+
def model_dump_json(self) -> str:
|
123
|
+
"""Serialize the message to a JSON string suitable for the OpenAI API."""
|
124
|
+
# Use the model_dump method to get the correct dictionary structure first
|
125
|
+
api_dict = self.model_dump()
|
126
|
+
# Serialize the dictionary to JSON
|
127
|
+
try:
|
128
|
+
return json.dumps(api_dict)
|
129
|
+
except TypeError as e:
|
130
|
+
logger.error(f"Failed to serialize ChatMessage to JSON for sender '{self.sender}': {e}")
|
131
|
+
# Fallback: return a basic JSON representation on error
|
132
|
+
return json.dumps({"role": self.role, "content": f"[Serialization Error: {e}]"})
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Generated manually on 2025-02-24 to establish core Swarm chat models
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
import django.db.models.deletion
|
5
|
+
from django.conf import settings
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
initial = True
|
10
|
+
|
11
|
+
dependencies = []
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.CreateModel(
|
15
|
+
name='ChatConversation',
|
16
|
+
fields=[
|
17
|
+
('conversation_id', models.CharField(max_length=255, primary_key=True, serialize=False)),
|
18
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
19
|
+
('student', models.ForeignKey(
|
20
|
+
blank=True,
|
21
|
+
null=True,
|
22
|
+
on_delete=django.db.models.deletion.CASCADE,
|
23
|
+
to=settings.AUTH_USER_MODEL,
|
24
|
+
)),
|
25
|
+
],
|
26
|
+
options={
|
27
|
+
'verbose_name': 'Chat Conversation',
|
28
|
+
'verbose_name_plural': 'Chat Conversations',
|
29
|
+
},
|
30
|
+
),
|
31
|
+
migrations.CreateModel(
|
32
|
+
name='ChatMessage',
|
33
|
+
fields=[
|
34
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
35
|
+
('sender', models.CharField(max_length=50)),
|
36
|
+
('content', models.TextField()),
|
37
|
+
('timestamp', models.DateTimeField(auto_now_add=True)),
|
38
|
+
('tool_call_id', models.CharField(blank=True, max_length=255, null=True)),
|
39
|
+
('conversation', models.ForeignKey(
|
40
|
+
on_delete=django.db.models.deletion.CASCADE,
|
41
|
+
related_name='chat_messages',
|
42
|
+
to='swarm.chatconversation',
|
43
|
+
)),
|
44
|
+
],
|
45
|
+
options={
|
46
|
+
'ordering': ['timestamp'],
|
47
|
+
'verbose_name': 'Chat Message',
|
48
|
+
'verbose_name_plural': 'Chat Messages',
|
49
|
+
},
|
50
|
+
),
|
51
|
+
]
|
File without changes
|
swarm/models.py
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
from django.db import models
|
2
|
+
|
3
|
+
class ChatConversation(models.Model):
|
4
|
+
"""Represents a single chat session."""
|
5
|
+
conversation_id = models.CharField(max_length=255, primary_key=True)
|
6
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
7
|
+
student = models.ForeignKey("auth.User", on_delete=models.CASCADE, blank=True, null=True)
|
8
|
+
|
9
|
+
class Meta:
|
10
|
+
app_label = "swarm"
|
11
|
+
verbose_name = "Chat Conversation"
|
12
|
+
verbose_name_plural = "Chat Conversations"
|
13
|
+
|
14
|
+
def __str__(self):
|
15
|
+
return f"ChatConversation({self.conversation_id})"
|
16
|
+
|
17
|
+
@property
|
18
|
+
def messages(self):
|
19
|
+
return self.chat_messages.all()
|
20
|
+
|
21
|
+
class ChatMessage(models.Model):
|
22
|
+
"""Stores individual chat messages within a conversation."""
|
23
|
+
conversation = models.ForeignKey(ChatConversation, related_name="chat_messages", on_delete=models.CASCADE)
|
24
|
+
sender = models.CharField(max_length=50)
|
25
|
+
content = models.TextField()
|
26
|
+
timestamp = models.DateTimeField(auto_now_add=True)
|
27
|
+
tool_call_id = models.CharField(max_length=255, blank=True, null=True)
|
28
|
+
|
29
|
+
class Meta:
|
30
|
+
ordering = ["timestamp"]
|
31
|
+
verbose_name = "Chat Message"
|
32
|
+
verbose_name_plural = "Chat Messages"
|
33
|
+
|
34
|
+
def __str__(self):
|
35
|
+
return self.content[:50]
|
36
|
+
|
37
|
+
__all__ = [
|
38
|
+
"ChatConversation",
|
39
|
+
"ChatMessage",
|
40
|
+
]
|
41
|
+
|
42
|
+
# Alias the module to prevent conflicting model registrations.
|
43
|
+
import sys
|
44
|
+
sys.modules["swarm.models"] = sys.modules[__name__]
|
45
|
+
sys.modules["src.swarm.models"] = sys.modules["swarm.models"]
|
swarm/repl/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from .repl import run_demo_loop
|
swarm/repl/repl.py
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
from swarm import Swarm
|
4
|
+
|
5
|
+
|
6
|
+
def process_and_print_streaming_response(response):
|
7
|
+
content = ""
|
8
|
+
last_sender = ""
|
9
|
+
|
10
|
+
for chunk in response:
|
11
|
+
if "sender" in chunk:
|
12
|
+
last_sender = chunk["sender"]
|
13
|
+
|
14
|
+
if "content" in chunk and chunk["content"] is not None:
|
15
|
+
if not content and last_sender:
|
16
|
+
print(f"\033[94m{last_sender}:\033[0m", end=" ", flush=True)
|
17
|
+
last_sender = ""
|
18
|
+
print(chunk["content"], end="", flush=True)
|
19
|
+
content += chunk["content"]
|
20
|
+
|
21
|
+
if "tool_calls" in chunk and chunk["tool_calls"] is not None:
|
22
|
+
for tool_call in chunk["tool_calls"]:
|
23
|
+
f = tool_call["function"]
|
24
|
+
name = f["name"]
|
25
|
+
if not name:
|
26
|
+
continue
|
27
|
+
print(f"\033[94m{last_sender}: \033[95m{name}\033[0m()")
|
28
|
+
|
29
|
+
if "delim" in chunk and chunk["delim"] == "end" and content:
|
30
|
+
print() # End of response message
|
31
|
+
content = ""
|
32
|
+
|
33
|
+
if "response" in chunk:
|
34
|
+
return chunk["response"]
|
35
|
+
|
36
|
+
|
37
|
+
def pretty_print_messages(messages) -> None:
|
38
|
+
for message in messages:
|
39
|
+
if message["role"] != "assistant":
|
40
|
+
continue
|
41
|
+
|
42
|
+
# print agent name in blue
|
43
|
+
print(f"\033[94m{message['sender']}\033[0m:", end=" ")
|
44
|
+
|
45
|
+
# print response, if any
|
46
|
+
if message["content"]:
|
47
|
+
print(message["content"])
|
48
|
+
|
49
|
+
# print tool calls in purple, if any
|
50
|
+
tool_calls = message.get("tool_calls") or []
|
51
|
+
if len(tool_calls) > 1:
|
52
|
+
print()
|
53
|
+
for tool_call in tool_calls:
|
54
|
+
f = tool_call["function"]
|
55
|
+
name, args = f["name"], f["arguments"]
|
56
|
+
arg_str = json.dumps(json.loads(args)).replace(":", "=")
|
57
|
+
print(f"\033[95m{name}\033[0m({arg_str[1:-1]})")
|
58
|
+
|
59
|
+
|
60
|
+
def run_demo_loop(
|
61
|
+
starting_agent, context_variables=None, stream=False, debug=False
|
62
|
+
) -> None:
|
63
|
+
client = Swarm()
|
64
|
+
print("Starting Swarm CLI 🐝")
|
65
|
+
|
66
|
+
messages = []
|
67
|
+
agent = starting_agent
|
68
|
+
|
69
|
+
while True:
|
70
|
+
user_input = input("\033[90mUser\033[0m: ")
|
71
|
+
messages.append({"role": "user", "content": user_input})
|
72
|
+
|
73
|
+
response = client.run(
|
74
|
+
agent=agent,
|
75
|
+
messages=messages,
|
76
|
+
context_variables=context_variables or {},
|
77
|
+
stream=stream,
|
78
|
+
debug=debug,
|
79
|
+
)
|
80
|
+
|
81
|
+
if stream:
|
82
|
+
response = process_and_print_streaming_response(response)
|
83
|
+
else:
|
84
|
+
pretty_print_messages(response.messages)
|
85
|
+
|
86
|
+
messages.extend(response.messages)
|
87
|
+
agent = response.agent
|
swarm/serializers.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
from rest_framework import serializers
|
2
|
+
from swarm.models import ChatMessage, ChatConversation
|
3
|
+
|
4
|
+
class ChatMessageSerializer(serializers.ModelSerializer):
|
5
|
+
class Meta:
|
6
|
+
model = ChatMessage
|
7
|
+
fields = '__all__'
|
8
|
+
|
9
|
+
class ChatConversationSerializer(serializers.ModelSerializer):
|
10
|
+
class Meta:
|
11
|
+
model = ChatConversation
|
12
|
+
fields = '__all__'
|
swarm/settings.py
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
"""
|
2
|
+
Django settings for swarm project.
|
3
|
+
Includes Pydantic base settings for Swarm Core.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
from enum import Enum
|
10
|
+
from pathlib import Path
|
11
|
+
from pydantic import Field # Import Field from Pydantic v2+
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
13
|
+
|
14
|
+
# --- Pydantic Settings for Swarm Core ---
|
15
|
+
class LogFormat(str, Enum):
|
16
|
+
standard = "[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s"
|
17
|
+
simple = "[%(levelname)s] %(name)s - %(message)s"
|
18
|
+
|
19
|
+
class Settings(BaseSettings):
|
20
|
+
model_config = SettingsConfigDict(env_prefix='SWARM_', case_sensitive=False)
|
21
|
+
|
22
|
+
log_level: str = Field(default='INFO', description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")
|
23
|
+
log_format: LogFormat = Field(default=LogFormat.standard, description="Logging format")
|
24
|
+
debug: bool = Field(default=False, description="Global debug flag")
|
25
|
+
|
26
|
+
# --- Standard Django Settings ---
|
27
|
+
|
28
|
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
29
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
30
|
+
PROJECT_ROOT = BASE_DIR.parent
|
31
|
+
if str(PROJECT_ROOT) not in sys.path:
|
32
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
33
|
+
|
34
|
+
BLUEPRINTS_DIR = PROJECT_ROOT / 'blueprints'
|
35
|
+
|
36
|
+
# --- Determine if running under pytest ---
|
37
|
+
TESTING = 'pytest' in sys.modules
|
38
|
+
|
39
|
+
# Quick-start development settings - unsuitable for production
|
40
|
+
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-YOUR_FALLBACK_KEY_HERE_CHANGE_ME')
|
41
|
+
# Use the Pydantic setting value for Django's DEBUG
|
42
|
+
DEBUG = Settings().debug # Read from instantiated Pydantic settings
|
43
|
+
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',')
|
44
|
+
|
45
|
+
# --- Application definition ---
|
46
|
+
INSTALLED_APPS = [
|
47
|
+
'django.contrib.admin',
|
48
|
+
'django.contrib.auth',
|
49
|
+
'django.contrib.contenttypes',
|
50
|
+
'django.contrib.sessions',
|
51
|
+
'django.contrib.messages',
|
52
|
+
'django.contrib.staticfiles',
|
53
|
+
# Third-party apps
|
54
|
+
'rest_framework',
|
55
|
+
'rest_framework.authtoken',
|
56
|
+
'drf_spectacular',
|
57
|
+
# Local apps
|
58
|
+
'swarm.apps.SwarmConfig',
|
59
|
+
]
|
60
|
+
|
61
|
+
# --- Conditionally add blueprint apps for TESTING ---
|
62
|
+
if TESTING:
|
63
|
+
_test_apps_to_add = ['blueprints.university']
|
64
|
+
for app in _test_apps_to_add:
|
65
|
+
if app not in INSTALLED_APPS:
|
66
|
+
INSTALLED_APPS.insert(0, app)
|
67
|
+
logging.info(f"Settings [TESTING]: Added '{app}' to INSTALLED_APPS.")
|
68
|
+
if 'SWARM_BLUEPRINTS' not in os.environ:
|
69
|
+
os.environ['SWARM_BLUEPRINTS'] = 'university'
|
70
|
+
logging.info(f"Settings [TESTING]: Set SWARM_BLUEPRINTS='university'")
|
71
|
+
else:
|
72
|
+
# --- Dynamic App Loading for Production/Development ---
|
73
|
+
_INITIAL_BLUEPRINT_APPS = []
|
74
|
+
_swarm_blueprints_env = os.getenv('SWARM_BLUEPRINTS')
|
75
|
+
_log_source = "Not Set"
|
76
|
+
if _swarm_blueprints_env:
|
77
|
+
_blueprint_names = [name.strip() for name in _swarm_blueprints_env.split(',') if name.strip()]
|
78
|
+
_INITIAL_BLUEPRINT_APPS = [f'blueprints.{name}' for name in _blueprint_names if name.replace('_', '').isidentifier()]
|
79
|
+
_log_source = "SWARM_BLUEPRINTS env var"
|
80
|
+
logging.info(f"Settings: Found blueprints from env var: {_INITIAL_BLUEPRINT_APPS}")
|
81
|
+
else:
|
82
|
+
_log_source = "directory scan"
|
83
|
+
try:
|
84
|
+
if BLUEPRINTS_DIR.is_dir():
|
85
|
+
for item in BLUEPRINTS_DIR.iterdir():
|
86
|
+
if item.is_dir() and (item / '__init__.py').exists():
|
87
|
+
if item.name.replace('_', '').isidentifier():
|
88
|
+
_INITIAL_BLUEPRINT_APPS.append(f'blueprints.{item.name}')
|
89
|
+
logging.info(f"Settings: Found blueprints from directory scan: {_INITIAL_BLUEPRINT_APPS}")
|
90
|
+
except Exception as e:
|
91
|
+
logging.error(f"Settings: Error discovering blueprint apps during initial load: {e}")
|
92
|
+
|
93
|
+
for app in _INITIAL_BLUEPRINT_APPS:
|
94
|
+
if app not in INSTALLED_APPS:
|
95
|
+
INSTALLED_APPS.append(app)
|
96
|
+
logging.info(f"Settings [{_log_source}]: Added '{app}' to INSTALLED_APPS.")
|
97
|
+
# --- End App Loading Logic ---
|
98
|
+
|
99
|
+
if isinstance(INSTALLED_APPS, tuple): INSTALLED_APPS = list(INSTALLED_APPS)
|
100
|
+
logging.info(f"Settings: Final INSTALLED_APPS = {INSTALLED_APPS}")
|
101
|
+
|
102
|
+
MIDDLEWARE = [
|
103
|
+
'django.middleware.security.SecurityMiddleware',
|
104
|
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
105
|
+
'django.middleware.common.CommonMiddleware',
|
106
|
+
'django.middleware.csrf.CsrfViewMiddleware',
|
107
|
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
108
|
+
'django.contrib.messages.middleware.MessageMiddleware',
|
109
|
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
110
|
+
]
|
111
|
+
|
112
|
+
ROOT_URLCONF = 'swarm.urls'
|
113
|
+
|
114
|
+
TEMPLATES = [
|
115
|
+
{
|
116
|
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
117
|
+
'DIRS': [BASE_DIR / 'templates'],
|
118
|
+
'APP_DIRS': True,
|
119
|
+
'OPTIONS': {
|
120
|
+
'context_processors': [
|
121
|
+
'django.template.context_processors.debug',
|
122
|
+
'django.template.context_processors.request',
|
123
|
+
'django.contrib.auth.context_processors.auth',
|
124
|
+
'django.contrib.messages.context_processors.messages',
|
125
|
+
],
|
126
|
+
},
|
127
|
+
},
|
128
|
+
]
|
129
|
+
|
130
|
+
WSGI_APPLICATION = 'swarm.wsgi.application'
|
131
|
+
|
132
|
+
SQLITE_DB_PATH = os.getenv('SQLITE_DB_PATH', BASE_DIR / 'db.sqlite3')
|
133
|
+
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': SQLITE_DB_PATH } }
|
134
|
+
DJANGO_DATABASE = DATABASES['default']
|
135
|
+
|
136
|
+
AUTH_PASSWORD_VALIDATORS = [
|
137
|
+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
138
|
+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
139
|
+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
140
|
+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
141
|
+
]
|
142
|
+
|
143
|
+
LANGUAGE_CODE = 'en-us'; TIME_ZONE = 'UTC'; USE_I18N = True; USE_TZ = True
|
144
|
+
|
145
|
+
STATIC_URL = '/static/'; STATIC_ROOT = BASE_DIR / 'staticfiles'
|
146
|
+
STATICFILES_DIRS = [ BASE_DIR / 'static', BASE_DIR / 'assets' ]
|
147
|
+
|
148
|
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
149
|
+
|
150
|
+
REST_FRAMEWORK = {
|
151
|
+
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
152
|
+
'DEFAULT_AUTHENTICATION_CLASSES': (
|
153
|
+
'swarm.auth.EnvOrTokenAuthentication',
|
154
|
+
'rest_framework.authentication.TokenAuthentication',
|
155
|
+
'rest_framework.authentication.SessionAuthentication',
|
156
|
+
),
|
157
|
+
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',)
|
158
|
+
}
|
159
|
+
|
160
|
+
SPECTACULAR_SETTINGS = {
|
161
|
+
'TITLE': 'Open Swarm API',
|
162
|
+
'DESCRIPTION': 'API for the Open Swarm multi-agent collaboration framework.',
|
163
|
+
'VERSION': '1.0.0',
|
164
|
+
'SERVE_INCLUDE_SCHEMA': False,
|
165
|
+
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
|
166
|
+
}
|
167
|
+
|
168
|
+
LOGGING = {
|
169
|
+
'version': 1, 'disable_existing_loggers': False,
|
170
|
+
'formatters': { 'standard': { 'format': '[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s' }, },
|
171
|
+
'handlers': { 'console': { 'level': 'DEBUG' if DEBUG else 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'standard', }, },
|
172
|
+
'loggers': {
|
173
|
+
'django': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, },
|
174
|
+
'django.request': { 'handlers': ['console'], 'level': 'WARNING', 'propagate': False, },
|
175
|
+
'swarm': { 'handlers': ['console'], 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, },
|
176
|
+
'swarm.extensions': { 'handlers': ['console'], 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, },
|
177
|
+
'blueprints': { 'handlers': ['console'], 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, },
|
178
|
+
},
|
179
|
+
}
|
180
|
+
|
181
|
+
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
|
182
|
+
LOGIN_URL = '/accounts/login/'; LOGIN_REDIRECT_URL = '/chatbot/'
|
183
|
+
|
184
|
+
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
185
|
+
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
186
|
+
|
187
|
+
if TESTING:
|
188
|
+
print("Pytest detected: Adjusting settings for testing.")
|
189
|
+
DATABASES['default']['NAME'] = ':memory:'
|