open-swarm 0.1.1743070217__py3-none-any.whl → 0.1.1743362777__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_swarm-0.1.1743362777.dist-info/METADATA +217 -0
- open_swarm-0.1.1743362777.dist-info/RECORD +260 -0
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743362777.dist-info}/WHEEL +1 -2
- open_swarm-0.1.1743362777.dist-info/entry_points.txt +2 -0
- swarm/__init__.py +0 -2
- swarm/auth.py +53 -49
- swarm/blueprints/README.md +67 -0
- swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
- swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
- swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
- swarm/blueprints/divine_code/__init__.py +0 -0
- swarm/blueprints/divine_code/apps.py +11 -0
- swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
- swarm/blueprints/django_chat/apps.py +6 -0
- swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
- swarm/blueprints/django_chat/urls.py +8 -0
- swarm/blueprints/django_chat/views.py +32 -0
- swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
- swarm/blueprints/family_ties/apps.py +11 -0
- swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
- swarm/blueprints/family_ties/models.py +19 -0
- swarm/blueprints/family_ties/serializers.py +7 -0
- swarm/blueprints/family_ties/settings.py +16 -0
- swarm/blueprints/family_ties/urls.py +10 -0
- swarm/blueprints/family_ties/views.py +26 -0
- swarm/blueprints/flock/__init__.py +0 -0
- swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
- swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
- swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
- swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
- swarm/blueprints/rue_code/__init__.py +0 -0
- swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
- swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
- swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
- swarm/extensions/blueprint/__init__.py +30 -15
- swarm/extensions/blueprint/agent_utils.py +16 -40
- swarm/extensions/blueprint/blueprint_base.py +141 -543
- swarm/extensions/blueprint/blueprint_discovery.py +112 -98
- swarm/extensions/blueprint/cli_handler.py +185 -0
- swarm/extensions/blueprint/config_loader.py +122 -0
- swarm/extensions/blueprint/django_utils.py +181 -79
- swarm/extensions/blueprint/interactive_mode.py +1 -1
- swarm/extensions/config/config_loader.py +83 -200
- swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_cli.py +199 -287
- swarm/llm/chat_completion.py +26 -55
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +58 -0
- swarm/permissions.py +38 -0
- swarm/serializers.py +96 -5
- swarm/settings.py +95 -110
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
- swarm/static/contrib/markedjs/marked.min.js +6 -0
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
- swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
- swarm/static/contrib/tabler-icons/archive.svg +21 -0
- swarm/static/contrib/tabler-icons/artboard.svg +27 -0
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
- swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
- swarm/static/contrib/tabler-icons/carambola.svg +19 -0
- swarm/static/contrib/tabler-icons/copy.svg +20 -0
- swarm/static/contrib/tabler-icons/download.svg +21 -0
- swarm/static/contrib/tabler-icons/edit.svg +21 -0
- swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
- swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
- swarm/static/contrib/tabler-icons/headset.svg +22 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
- swarm/static/contrib/tabler-icons/message-star.svg +22 -0
- swarm/static/contrib/tabler-icons/message-x.svg +23 -0
- swarm/static/contrib/tabler-icons/message.svg +21 -0
- swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
- swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
- swarm/static/contrib/tabler-icons/robot.svg +26 -0
- swarm/static/contrib/tabler-icons/search.svg +19 -0
- swarm/static/contrib/tabler-icons/settings.svg +20 -0
- swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
- swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
- swarm/static/css/dropdown.css +22 -0
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +23 -0
- swarm/static/rest_mode/css/base.css +470 -0
- swarm/static/rest_mode/css/chat-history.css +286 -0
- swarm/static/rest_mode/css/chat.css +251 -0
- swarm/static/rest_mode/css/chatbot.css +74 -0
- swarm/static/rest_mode/css/chatgpt.css +62 -0
- swarm/static/rest_mode/css/colors/corporate.css +74 -0
- swarm/static/rest_mode/css/colors/pastel.css +81 -0
- swarm/static/rest_mode/css/colors/tropical.css +82 -0
- swarm/static/rest_mode/css/general.css +142 -0
- swarm/static/rest_mode/css/layout.css +167 -0
- swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
- swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
- swarm/static/rest_mode/css/messages.css +84 -0
- swarm/static/rest_mode/css/messenger.css +135 -0
- swarm/static/rest_mode/css/settings.css +91 -0
- swarm/static/rest_mode/css/simple.css +44 -0
- swarm/static/rest_mode/css/slack.css +58 -0
- swarm/static/rest_mode/css/style.css +156 -0
- swarm/static/rest_mode/css/theme.css +30 -0
- swarm/static/rest_mode/css/toast.css +40 -0
- swarm/static/rest_mode/js/auth.js +9 -0
- swarm/static/rest_mode/js/blueprint.js +41 -0
- swarm/static/rest_mode/js/blueprintUtils.js +12 -0
- swarm/static/rest_mode/js/chatLogic.js +79 -0
- swarm/static/rest_mode/js/debug.js +63 -0
- swarm/static/rest_mode/js/events.js +98 -0
- swarm/static/rest_mode/js/main.js +19 -0
- swarm/static/rest_mode/js/messages.js +264 -0
- swarm/static/rest_mode/js/messengerLogic.js +355 -0
- swarm/static/rest_mode/js/modules/apiService.js +84 -0
- swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
- swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
- swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
- swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
- swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
- swarm/static/rest_mode/js/modules/state.js +7 -0
- swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
- swarm/static/rest_mode/js/modules/validation.js +23 -0
- swarm/static/rest_mode/js/rendering.js +119 -0
- swarm/static/rest_mode/js/settings.js +130 -0
- swarm/static/rest_mode/js/sidebar.js +94 -0
- swarm/static/rest_mode/js/simpleLogic.js +37 -0
- swarm/static/rest_mode/js/slackLogic.js +66 -0
- swarm/static/rest_mode/js/splash.js +76 -0
- swarm/static/rest_mode/js/theme.js +111 -0
- swarm/static/rest_mode/js/toast.js +36 -0
- swarm/static/rest_mode/js/ui.js +265 -0
- swarm/static/rest_mode/js/validation.js +57 -0
- swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
- swarm/static/rest_mode/svg/arrow_down.svg +5 -0
- swarm/static/rest_mode/svg/arrow_left.svg +5 -0
- swarm/static/rest_mode/svg/arrow_right.svg +5 -0
- swarm/static/rest_mode/svg/arrow_up.svg +5 -0
- swarm/static/rest_mode/svg/attach.svg +8 -0
- swarm/static/rest_mode/svg/avatar.svg +7 -0
- swarm/static/rest_mode/svg/canvas.svg +6 -0
- swarm/static/rest_mode/svg/chat_history.svg +4 -0
- swarm/static/rest_mode/svg/close.svg +5 -0
- swarm/static/rest_mode/svg/copy.svg +4 -0
- swarm/static/rest_mode/svg/dark_mode.svg +3 -0
- swarm/static/rest_mode/svg/edit.svg +5 -0
- swarm/static/rest_mode/svg/layout.svg +9 -0
- swarm/static/rest_mode/svg/logo.svg +29 -0
- swarm/static/rest_mode/svg/logout.svg +5 -0
- swarm/static/rest_mode/svg/mobile.svg +5 -0
- swarm/static/rest_mode/svg/new_chat.svg +4 -0
- swarm/static/rest_mode/svg/not_visible.svg +5 -0
- swarm/static/rest_mode/svg/plus.svg +7 -0
- swarm/static/rest_mode/svg/run_code.svg +6 -0
- swarm/static/rest_mode/svg/save.svg +4 -0
- swarm/static/rest_mode/svg/search.svg +6 -0
- swarm/static/rest_mode/svg/settings.svg +4 -0
- swarm/static/rest_mode/svg/speaker.svg +5 -0
- swarm/static/rest_mode/svg/stop.svg +6 -0
- swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
- swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
- swarm/static/rest_mode/svg/toggle_off.svg +6 -0
- swarm/static/rest_mode/svg/toggle_on.svg +6 -0
- swarm/static/rest_mode/svg/trash.svg +10 -0
- swarm/static/rest_mode/svg/undo.svg +3 -0
- swarm/static/rest_mode/svg/visible.svg +8 -0
- swarm/static/rest_mode/svg/voice.svg +10 -0
- swarm/templates/account/login.html +22 -0
- swarm/templates/account/signup.html +32 -0
- swarm/templates/base.html +30 -0
- swarm/templates/chat.html +43 -0
- swarm/templates/index.html +35 -0
- swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
- swarm/templates/rest_mode/components/header.html +45 -0
- swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
- swarm/templates/rest_mode/components/settings_dialog.html +97 -0
- swarm/templates/rest_mode/components/splash_screen.html +7 -0
- swarm/templates/rest_mode/components/top_bar.html +28 -0
- swarm/templates/rest_mode/message_ui.html +50 -0
- swarm/templates/rest_mode/slackbot.html +30 -0
- swarm/templates/simple_blueprint_page.html +24 -0
- swarm/templates/websocket_partials/final_system_message.html +3 -0
- swarm/templates/websocket_partials/system_message.html +4 -0
- swarm/templates/websocket_partials/user_message.html +5 -0
- swarm/urls.py +57 -74
- swarm/utils/log_utils.py +63 -0
- swarm/views/api_views.py +48 -39
- swarm/views/chat_views.py +156 -70
- swarm/views/core_views.py +85 -90
- swarm/views/model_views.py +64 -121
- swarm/views/utils.py +65 -441
- open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
- open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
- open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
- swarm/agent/agent.py +0 -49
- swarm/core.py +0 -326
- swarm/extensions/mcp/__init__.py +0 -1
- swarm/extensions/mcp/cache_utils.py +0 -36
- swarm/extensions/mcp/mcp_client.py +0 -341
- swarm/extensions/mcp/mcp_constants.py +0 -7
- swarm/extensions/mcp/mcp_tool_provider.py +0 -110
- swarm/types.py +0 -126
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743362777.dist-info}/licenses/LICENSE +0 -0
swarm/llm/chat_completion.py
CHANGED
@@ -8,25 +8,21 @@ tool call repair, and interaction with the OpenAI API. Located in llm/ for LLM-s
|
|
8
8
|
import os
|
9
9
|
import json
|
10
10
|
import logging
|
11
|
-
from typing import List, Optional, Dict, Any, Union, AsyncGenerator
|
11
|
+
from typing import List, Optional, Dict, Any, Union, AsyncGenerator
|
12
12
|
from collections import defaultdict
|
13
13
|
|
14
14
|
import asyncio
|
15
15
|
from openai import AsyncOpenAI, OpenAIError
|
16
|
-
|
17
|
-
# Assuming it might be part of the base model or a common types module
|
18
|
-
# For now, let's assume it's implicitly handled or use a dict directly
|
19
|
-
# from ..types import ChatCompletionMessage, Agent # If defined in types
|
20
|
-
from ..types import Agent # Import Agent
|
16
|
+
from ..types import Agent
|
21
17
|
from ..utils.redact import redact_sensitive_data
|
22
18
|
from ..utils.general_utils import serialize_datetime
|
23
19
|
from ..utils.message_utils import filter_duplicate_system_messages, update_null_content
|
24
20
|
from ..utils.context_utils import get_token_count, truncate_message_history
|
25
|
-
from ..utils.message_sequence import repair_message_payload
|
21
|
+
# --- REMOVED import: from ..utils.message_sequence import repair_message_payload ---
|
26
22
|
|
27
23
|
# Configure module-level logging
|
28
24
|
logger = logging.getLogger(__name__)
|
29
|
-
logger.setLevel(logging.DEBUG)
|
25
|
+
# logger.setLevel(logging.DEBUG) # Keep level controlled by main setup
|
30
26
|
if not logger.handlers:
|
31
27
|
stream_handler = logging.StreamHandler()
|
32
28
|
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
@@ -42,31 +38,15 @@ async def get_chat_completion(
|
|
42
38
|
current_llm_config: Dict[str, Any],
|
43
39
|
max_context_tokens: int,
|
44
40
|
max_context_messages: int,
|
45
|
-
tools: Optional[List[Dict[str, Any]]] = None,
|
46
|
-
tool_choice: Optional[str] = "auto",
|
41
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
42
|
+
tool_choice: Optional[str] = "auto",
|
47
43
|
model_override: Optional[str] = None,
|
48
44
|
stream: bool = False,
|
49
45
|
debug: bool = False
|
50
|
-
) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]:
|
46
|
+
) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]:
|
51
47
|
"""
|
52
48
|
Retrieve a chat completion from the LLM for the given agent and history.
|
53
|
-
|
54
|
-
Args:
|
55
|
-
client: AsyncOpenAI client instance.
|
56
|
-
agent: The agent processing the completion.
|
57
|
-
history: List of previous messages in the conversation.
|
58
|
-
context_variables: Variables to include in the agent's context.
|
59
|
-
current_llm_config: Current LLM configuration dictionary.
|
60
|
-
max_context_tokens: Maximum token limit for context.
|
61
|
-
max_context_messages: Maximum message limit for context.
|
62
|
-
tools: Optional list of tools in OpenAI format.
|
63
|
-
tool_choice: Tool choice mode (e.g., "auto", "none").
|
64
|
-
model_override: Optional model to use instead of default.
|
65
|
-
stream: If True, stream the response; otherwise, return complete.
|
66
|
-
debug: If True, log detailed debugging information.
|
67
|
-
|
68
|
-
Returns:
|
69
|
-
Union[Dict[str, Any], AsyncGenerator[Any, None]]: The LLM's response message (as dict) or stream.
|
49
|
+
Relies on openai-agents Runner for actual execution, this might become deprecated.
|
70
50
|
"""
|
71
51
|
if not agent:
|
72
52
|
logger.error("Cannot generate chat completion: Agent is None")
|
@@ -87,7 +67,9 @@ async def get_chat_completion(
|
|
87
67
|
if not isinstance(instructions, str):
|
88
68
|
logger.warning(f"Invalid instructions type for '{agent.name}': {type(instructions)}. Converting to string.")
|
89
69
|
instructions = str(instructions)
|
90
|
-
|
70
|
+
|
71
|
+
# --- REMOVED call to repair_message_payload for system message ---
|
72
|
+
messages = [{"role": "system", "content": instructions}]
|
91
73
|
|
92
74
|
if not isinstance(history, list):
|
93
75
|
logger.error(f"Invalid history type for '{agent.name}': {type(history)}. Expected list.")
|
@@ -100,15 +82,14 @@ async def get_chat_completion(
|
|
100
82
|
if "tool_calls" in msg and msg["tool_calls"] is not None and not isinstance(msg["tool_calls"], list):
|
101
83
|
logger.warning(f"Invalid tool_calls in history for '{msg.get('sender', 'unknown')}': {msg['tool_calls']}. Setting to None.")
|
102
84
|
msg["tool_calls"] = None
|
103
|
-
# Ensure content: None becomes content: "" for API compatibility
|
104
85
|
if "content" in msg and msg["content"] is None:
|
105
86
|
msg["content"] = ""
|
106
87
|
messages.append(msg)
|
88
|
+
|
107
89
|
messages = filter_duplicate_system_messages(messages)
|
108
90
|
messages = truncate_message_history(messages, active_model, max_context_tokens, max_context_messages)
|
109
|
-
|
110
|
-
|
111
|
-
messages = update_null_content(messages)
|
91
|
+
# --- REMOVED call to repair_message_payload after truncation ---
|
92
|
+
messages = update_null_content(messages) # Keep null content update
|
112
93
|
|
113
94
|
logger.debug(f"Prepared {len(messages)} messages for '{agent.name}'")
|
114
95
|
if debug:
|
@@ -119,45 +100,42 @@ async def get_chat_completion(
|
|
119
100
|
"messages": messages,
|
120
101
|
"stream": stream,
|
121
102
|
"temperature": current_llm_config.get("temperature", 0.7),
|
122
|
-
# --- Pass tools and tool_choice ---
|
123
103
|
"tools": tools if tools else None,
|
124
|
-
"tool_choice": tool_choice if tools else None,
|
104
|
+
"tool_choice": tool_choice if tools else None,
|
125
105
|
}
|
126
106
|
if getattr(agent, "response_format", None):
|
127
107
|
create_params["response_format"] = agent.response_format
|
128
|
-
create_params = {k: v for k, v in create_params.items() if v is not None}
|
108
|
+
create_params = {k: v for k, v in create_params.items() if v is not None}
|
129
109
|
|
130
110
|
tool_info_log = f", tools_count={len(tools)}" if tools else ", tools=None"
|
131
111
|
logger.debug(f"Chat completion params: model='{active_model}', messages_count={len(messages)}, stream={stream}{tool_info_log}, tool_choice={create_params.get('tool_choice')}")
|
132
112
|
|
133
113
|
try:
|
134
114
|
logger.debug(f"Calling OpenAI API for '{agent.name}' with model='{active_model}'")
|
135
|
-
# Temporary workaround for potential env var conflicts if client doesn't isolate well
|
136
115
|
prev_openai_api_key = os.environ.pop("OPENAI_API_KEY", None)
|
137
116
|
try:
|
138
117
|
completion = await client.chat.completions.create(**create_params)
|
139
118
|
if stream:
|
140
|
-
return completion
|
119
|
+
return completion
|
141
120
|
|
142
|
-
# --- Handle Non-Streaming Response ---
|
143
121
|
if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
|
144
122
|
message_dict = completion.choices[0].message.model_dump(exclude_none=True)
|
145
123
|
log_msg = message_dict.get("content", "No content")[:50] if message_dict.get("content") else "No content"
|
146
124
|
if message_dict.get("tool_calls"): log_msg += f" (+{len(message_dict['tool_calls'])} tool calls)"
|
147
125
|
logger.debug(f"OpenAI completion received for '{agent.name}': {log_msg}...")
|
148
|
-
return message_dict
|
126
|
+
return message_dict
|
149
127
|
else:
|
150
128
|
logger.warning(f"No valid message in completion for '{agent.name}'")
|
151
|
-
return {"role": "assistant", "content": "No response generated"}
|
129
|
+
return {"role": "assistant", "content": "No response generated"}
|
152
130
|
finally:
|
153
131
|
if prev_openai_api_key is not None:
|
154
132
|
os.environ["OPENAI_API_KEY"] = prev_openai_api_key
|
155
133
|
except OpenAIError as e:
|
156
134
|
logger.error(f"Chat completion failed for '{agent.name}': {e}")
|
157
135
|
raise
|
158
|
-
except Exception as e:
|
136
|
+
except Exception as e:
|
159
137
|
logger.error(f"Unexpected error during chat completion for '{agent.name}': {e}", exc_info=True)
|
160
|
-
raise
|
138
|
+
raise
|
161
139
|
|
162
140
|
|
163
141
|
async def get_chat_completion_message(
|
@@ -168,28 +146,21 @@ async def get_chat_completion_message(
|
|
168
146
|
current_llm_config: Dict[str, Any],
|
169
147
|
max_context_tokens: int,
|
170
148
|
max_context_messages: int,
|
171
|
-
tools: Optional[List[Dict[str, Any]]] = None,
|
172
|
-
tool_choice: Optional[str] = "auto",
|
149
|
+
tools: Optional[List[Dict[str, Any]]] = None,
|
150
|
+
tool_choice: Optional[str] = "auto",
|
173
151
|
model_override: Optional[str] = None,
|
174
152
|
stream: bool = False,
|
175
153
|
debug: bool = False
|
176
|
-
) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]:
|
154
|
+
) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]:
|
177
155
|
"""
|
178
156
|
Wrapper to retrieve and validate a chat completion message (returns dict or stream).
|
179
|
-
|
180
|
-
Args:
|
181
|
-
Same as get_chat_completion.
|
182
|
-
|
183
|
-
Returns:
|
184
|
-
Union[Dict[str, Any], AsyncGenerator[Any, None]]: Validated LLM response message as dict or the stream.
|
157
|
+
Relies on openai-agents Runner for actual execution, this might become deprecated.
|
185
158
|
"""
|
186
159
|
logger.debug(f"Fetching chat completion message for '{agent.name}'")
|
187
160
|
completion_result = await get_chat_completion(
|
188
161
|
client, agent, history, context_variables, current_llm_config,
|
189
162
|
max_context_tokens, max_context_messages,
|
190
|
-
tools=tools, tool_choice=tool_choice,
|
163
|
+
tools=tools, tool_choice=tool_choice,
|
191
164
|
model_override=model_override, stream=stream, debug=debug
|
192
165
|
)
|
193
|
-
# If streaming, completion_result is already the generator
|
194
|
-
# If not streaming, it's the message dictionary
|
195
166
|
return completion_result
|
File without changes
|
File without changes
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import os
|
2
|
+
import logging
|
3
|
+
from django.core.management.commands.runserver import Command as RunserverCommand
|
4
|
+
from django.conf import settings
|
5
|
+
from dotenv import load_dotenv
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
# Load .env from project root relative to this file's location
|
9
|
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent
|
10
|
+
# Check if .env exists before trying to load
|
11
|
+
dotenv_path = BASE_DIR / '.env'
|
12
|
+
if dotenv_path.is_file():
|
13
|
+
load_dotenv(dotenv_path=dotenv_path)
|
14
|
+
else:
|
15
|
+
# Optionally log if .env is missing, but don't require it
|
16
|
+
# logger = logging.getLogger(__name__) # Get logger if needed here
|
17
|
+
# logger.debug(".env file not found in project root, relying solely on environment variables.")
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__) # Get logger for command messages
|
22
|
+
|
23
|
+
class Command(RunserverCommand):
|
24
|
+
help = 'Starts a lightweight Web server for development, with an option to enable Swarm API authentication.'
|
25
|
+
|
26
|
+
def add_arguments(self, parser):
|
27
|
+
super().add_arguments(parser)
|
28
|
+
parser.add_argument(
|
29
|
+
'--enable-auth',
|
30
|
+
action='store_true',
|
31
|
+
dest='enable_auth',
|
32
|
+
help='Enable Swarm API Key authentication using API_AUTH_TOKEN from environment/.env.', # Updated help text
|
33
|
+
)
|
34
|
+
|
35
|
+
def handle(self, *args, **options):
|
36
|
+
enable_auth_flag = options.get('enable_auth', False)
|
37
|
+
api_key = None # Keep internal variable name simple
|
38
|
+
|
39
|
+
if enable_auth_flag:
|
40
|
+
settings.ENABLE_API_AUTH = True # Override setting
|
41
|
+
# *** Use API_AUTH_TOKEN from environment ***
|
42
|
+
api_key = os.getenv('API_AUTH_TOKEN')
|
43
|
+
if api_key:
|
44
|
+
settings.SWARM_API_KEY = api_key # Store the key in settings
|
45
|
+
logger.info("Swarm API authentication ENABLED via --enable-auth flag. API_AUTH_TOKEN found.")
|
46
|
+
else:
|
47
|
+
settings.SWARM_API_KEY = None # Ensure it's None if not found
|
48
|
+
logger.warning("Swarm API authentication ENABLED via --enable-auth flag, but API_AUTH_TOKEN not found in environment/.env. API will allow anonymous access if session auth fails.")
|
49
|
+
else:
|
50
|
+
# Keep defaults from settings.py (ENABLE_API_AUTH=False, SWARM_API_KEY=None)
|
51
|
+
# Ensure SWARM_API_KEY is explicitly None if auth is disabled
|
52
|
+
settings.ENABLE_API_AUTH = False
|
53
|
+
settings.SWARM_API_KEY = None
|
54
|
+
logger.info("Swarm API authentication DISABLED (run with --enable-auth and set API_AUTH_TOKEN to activate).")
|
55
|
+
|
56
|
+
# Call the original runserver command handler
|
57
|
+
super().handle(*args, **options)
|
58
|
+
|
swarm/permissions.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import logging
|
2
|
+
from rest_framework.permissions import BasePermission
|
3
|
+
from django.conf import settings
|
4
|
+
from swarm.auth import StaticTokenAuthentication # Import for type checking
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
class HasValidTokenOrSession(BasePermission):
|
9
|
+
"""
|
10
|
+
Allows access if the user is authenticated via a valid session
|
11
|
+
OR if a valid static API token was provided (indicated by request.auth).
|
12
|
+
"""
|
13
|
+
|
14
|
+
def has_permission(self, request, view):
|
15
|
+
# Check if standard Django user authentication succeeded (Session)
|
16
|
+
# This user comes from AuthenticationMiddleware + CustomSessionAuthentication
|
17
|
+
is_session_authenticated = request.user and request.user.is_authenticated
|
18
|
+
if is_session_authenticated:
|
19
|
+
logger.debug("[Permission] Access granted via authenticated session user.")
|
20
|
+
return True
|
21
|
+
|
22
|
+
# Check if StaticTokenAuthentication succeeded.
|
23
|
+
# We modified StaticTokenAuthentication to return (AnonymousUser(), token)
|
24
|
+
# DRF populates request.auth with the second element of the tuple (the token).
|
25
|
+
# We also check the authenticator type for robustness.
|
26
|
+
is_static_token_auth = (
|
27
|
+
request.successful_authenticator and
|
28
|
+
isinstance(request.successful_authenticator, StaticTokenAuthentication) and
|
29
|
+
request.auth is not None # Check if request.auth (the token) was set
|
30
|
+
)
|
31
|
+
|
32
|
+
if is_static_token_auth:
|
33
|
+
logger.debug("[Permission] Access granted via valid static API token.")
|
34
|
+
return True
|
35
|
+
|
36
|
+
logger.debug("[Permission] Access denied. No valid session or static token found.")
|
37
|
+
return False
|
38
|
+
|
swarm/serializers.py
CHANGED
@@ -1,12 +1,103 @@
|
|
1
1
|
from rest_framework import serializers
|
2
|
-
from swarm.models import ChatMessage
|
2
|
+
from swarm.models import ChatMessage
|
3
|
+
import logging
|
4
|
+
|
5
|
+
logger = logging.getLogger(__name__)
|
6
|
+
print_logger = logging.getLogger('print_debug')
|
7
|
+
|
8
|
+
class MessageSerializer(serializers.Serializer):
|
9
|
+
role = serializers.ChoiceField(choices=["system", "user", "assistant", "tool"])
|
10
|
+
# Content is CharField, allows null/blank by default
|
11
|
+
content = serializers.CharField(allow_null=True, required=False, allow_blank=True)
|
12
|
+
name = serializers.CharField(required=False, allow_blank=True)
|
13
|
+
|
14
|
+
# Removed validate_content
|
15
|
+
|
16
|
+
def validate(self, data):
|
17
|
+
"""Validate message structure based on role."""
|
18
|
+
print_logger.debug(f"MessageSerializer.validate received data: {data}")
|
19
|
+
role = data.get('role')
|
20
|
+
content = data.get('content', None)
|
21
|
+
name = data.get('name')
|
22
|
+
|
23
|
+
# Role validation
|
24
|
+
if 'role' not in data:
|
25
|
+
raise serializers.ValidationError({"role": ["This field is required."]})
|
26
|
+
|
27
|
+
# Content requiredness validation (based on role)
|
28
|
+
content_required = role in ['system', 'user', 'assistant', 'tool']
|
29
|
+
content_present = 'content' in data
|
30
|
+
|
31
|
+
if content_required:
|
32
|
+
if not content_present:
|
33
|
+
raise serializers.ValidationError({"content": ["This field is required."]})
|
34
|
+
# Null/Blank checks are handled by field definition (allow_null/allow_blank)
|
35
|
+
# Type check will happen in ChatCompletionRequestSerializer.validate_messages
|
36
|
+
|
37
|
+
# Name validation for tool role
|
38
|
+
if role == 'tool' and not name:
|
39
|
+
raise serializers.ValidationError({"name": ["This field is required for role 'tool'."]})
|
40
|
+
|
41
|
+
print_logger.debug(f"MessageSerializer.validate PASSED for data: {data}")
|
42
|
+
return data
|
43
|
+
|
44
|
+
class ChatCompletionRequestSerializer(serializers.Serializer):
|
45
|
+
model = serializers.CharField(max_length=255)
|
46
|
+
messages = MessageSerializer(many=True, min_length=1)
|
47
|
+
stream = serializers.BooleanField(default=False)
|
48
|
+
params = serializers.JSONField(required=False, allow_null=True)
|
49
|
+
|
50
|
+
def validate(self, data):
|
51
|
+
"""Perform object-level validation."""
|
52
|
+
model_value = self.initial_data.get('model')
|
53
|
+
logger.debug(f"Top-level validate checking model type. Got: {type(model_value)}, value: {model_value}")
|
54
|
+
if model_value is not None and not isinstance(model_value, str):
|
55
|
+
raise serializers.ValidationError({"model": ["Field 'model' must be a string."]})
|
56
|
+
# Messages validation (including content type) happens in validate_messages
|
57
|
+
return data
|
58
|
+
|
59
|
+
def validate_messages(self, value):
|
60
|
+
"""
|
61
|
+
Validate the messages list itself and perform raw type checks.
|
62
|
+
'value' here is the list *after* MessageSerializer has run on each item.
|
63
|
+
We need to inspect `self.initial_data` for the raw types.
|
64
|
+
"""
|
65
|
+
if not value:
|
66
|
+
raise serializers.ValidationError("Messages list cannot be empty.")
|
67
|
+
|
68
|
+
# Access raw message data from initial_data for type checking
|
69
|
+
raw_messages = self.initial_data.get('messages', [])
|
70
|
+
if not isinstance(raw_messages, list):
|
71
|
+
# This case is handled by ListField implicitly, but good to be explicit
|
72
|
+
raise serializers.ValidationError("Expected a list of message items.")
|
73
|
+
|
74
|
+
errors = []
|
75
|
+
for i, raw_msg in enumerate(raw_messages):
|
76
|
+
msg_errors = {}
|
77
|
+
if not isinstance(raw_msg, dict):
|
78
|
+
# If the item itself isn't a dict, add error and skip further checks for it
|
79
|
+
errors.append({f"item_{i}": "Each message must be a dictionary."})
|
80
|
+
continue
|
81
|
+
|
82
|
+
# *** Check raw content type here ***
|
83
|
+
content = raw_msg.get('content', None)
|
84
|
+
if 'content' in raw_msg and content is not None and not isinstance(content, str):
|
85
|
+
msg_errors['content'] = ["Content must be a string or null."] # Match test assertion
|
86
|
+
|
87
|
+
# Add other raw checks if needed (e.g., role type)
|
88
|
+
|
89
|
+
if msg_errors:
|
90
|
+
errors.append(msg_errors) # Append errors for this specific message index
|
91
|
+
|
92
|
+
if errors:
|
93
|
+
# Raise a single validation error containing all message-specific errors
|
94
|
+
raise serializers.ValidationError(errors)
|
95
|
+
|
96
|
+
# Return the processed 'value' which passed MessageSerializer validation
|
97
|
+
return value
|
3
98
|
|
4
99
|
class ChatMessageSerializer(serializers.ModelSerializer):
|
5
100
|
class Meta:
|
6
101
|
model = ChatMessage
|
7
102
|
fields = '__all__'
|
8
103
|
|
9
|
-
class ChatConversationSerializer(serializers.ModelSerializer):
|
10
|
-
class Meta:
|
11
|
-
model = ChatConversation
|
12
|
-
fields = '__all__'
|