open-swarm 0.1.1743070217__py3-none-any.whl → 0.1.1743364176__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. open_swarm-0.1.1743364176.dist-info/METADATA +286 -0
  2. open_swarm-0.1.1743364176.dist-info/RECORD +260 -0
  3. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/WHEEL +1 -2
  4. open_swarm-0.1.1743364176.dist-info/entry_points.txt +2 -0
  5. swarm/__init__.py +0 -2
  6. swarm/auth.py +53 -49
  7. swarm/blueprints/README.md +67 -0
  8. swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
  9. swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
  10. swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
  11. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
  12. swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
  13. swarm/blueprints/divine_code/__init__.py +0 -0
  14. swarm/blueprints/divine_code/apps.py +11 -0
  15. swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
  16. swarm/blueprints/django_chat/apps.py +6 -0
  17. swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
  18. swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
  19. swarm/blueprints/django_chat/urls.py +8 -0
  20. swarm/blueprints/django_chat/views.py +32 -0
  21. swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
  22. swarm/blueprints/family_ties/apps.py +11 -0
  23. swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
  24. swarm/blueprints/family_ties/models.py +19 -0
  25. swarm/blueprints/family_ties/serializers.py +7 -0
  26. swarm/blueprints/family_ties/settings.py +16 -0
  27. swarm/blueprints/family_ties/urls.py +10 -0
  28. swarm/blueprints/family_ties/views.py +26 -0
  29. swarm/blueprints/flock/__init__.py +0 -0
  30. swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
  31. swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
  32. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
  33. swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
  34. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
  35. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
  36. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
  37. swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
  38. swarm/blueprints/rue_code/__init__.py +0 -0
  39. swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
  40. swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
  41. swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
  42. swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
  43. swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
  44. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
  45. swarm/extensions/blueprint/__init__.py +30 -15
  46. swarm/extensions/blueprint/agent_utils.py +16 -40
  47. swarm/extensions/blueprint/blueprint_base.py +141 -543
  48. swarm/extensions/blueprint/blueprint_discovery.py +112 -98
  49. swarm/extensions/blueprint/cli_handler.py +185 -0
  50. swarm/extensions/blueprint/config_loader.py +122 -0
  51. swarm/extensions/blueprint/django_utils.py +181 -79
  52. swarm/extensions/blueprint/interactive_mode.py +1 -1
  53. swarm/extensions/config/config_loader.py +83 -200
  54. swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
  55. swarm/extensions/launchers/swarm_cli.py +199 -287
  56. swarm/llm/chat_completion.py +26 -55
  57. swarm/management/__init__.py +0 -0
  58. swarm/management/commands/__init__.py +0 -0
  59. swarm/management/commands/runserver.py +58 -0
  60. swarm/permissions.py +38 -0
  61. swarm/serializers.py +96 -5
  62. swarm/settings.py +95 -110
  63. swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
  64. swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
  65. swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
  66. swarm/static/contrib/markedjs/marked.min.js +6 -0
  67. swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
  68. swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
  69. swarm/static/contrib/tabler-icons/archive.svg +21 -0
  70. swarm/static/contrib/tabler-icons/artboard.svg +27 -0
  71. swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
  72. swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
  73. swarm/static/contrib/tabler-icons/carambola.svg +19 -0
  74. swarm/static/contrib/tabler-icons/copy.svg +20 -0
  75. swarm/static/contrib/tabler-icons/download.svg +21 -0
  76. swarm/static/contrib/tabler-icons/edit.svg +21 -0
  77. swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
  78. swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
  79. swarm/static/contrib/tabler-icons/headset.svg +22 -0
  80. swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
  81. swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
  82. swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
  83. swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
  84. swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
  85. swarm/static/contrib/tabler-icons/message-star.svg +22 -0
  86. swarm/static/contrib/tabler-icons/message-x.svg +23 -0
  87. swarm/static/contrib/tabler-icons/message.svg +21 -0
  88. swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
  89. swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
  90. swarm/static/contrib/tabler-icons/robot.svg +26 -0
  91. swarm/static/contrib/tabler-icons/search.svg +19 -0
  92. swarm/static/contrib/tabler-icons/settings.svg +20 -0
  93. swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
  94. swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
  95. swarm/static/css/dropdown.css +22 -0
  96. swarm/static/htmx/htmx.min.js +0 -0
  97. swarm/static/js/dropdown.js +23 -0
  98. swarm/static/rest_mode/css/base.css +470 -0
  99. swarm/static/rest_mode/css/chat-history.css +286 -0
  100. swarm/static/rest_mode/css/chat.css +251 -0
  101. swarm/static/rest_mode/css/chatbot.css +74 -0
  102. swarm/static/rest_mode/css/chatgpt.css +62 -0
  103. swarm/static/rest_mode/css/colors/corporate.css +74 -0
  104. swarm/static/rest_mode/css/colors/pastel.css +81 -0
  105. swarm/static/rest_mode/css/colors/tropical.css +82 -0
  106. swarm/static/rest_mode/css/general.css +142 -0
  107. swarm/static/rest_mode/css/layout.css +167 -0
  108. swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
  109. swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
  110. swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
  111. swarm/static/rest_mode/css/messages.css +84 -0
  112. swarm/static/rest_mode/css/messenger.css +135 -0
  113. swarm/static/rest_mode/css/settings.css +91 -0
  114. swarm/static/rest_mode/css/simple.css +44 -0
  115. swarm/static/rest_mode/css/slack.css +58 -0
  116. swarm/static/rest_mode/css/style.css +156 -0
  117. swarm/static/rest_mode/css/theme.css +30 -0
  118. swarm/static/rest_mode/css/toast.css +40 -0
  119. swarm/static/rest_mode/js/auth.js +9 -0
  120. swarm/static/rest_mode/js/blueprint.js +41 -0
  121. swarm/static/rest_mode/js/blueprintUtils.js +12 -0
  122. swarm/static/rest_mode/js/chatLogic.js +79 -0
  123. swarm/static/rest_mode/js/debug.js +63 -0
  124. swarm/static/rest_mode/js/events.js +98 -0
  125. swarm/static/rest_mode/js/main.js +19 -0
  126. swarm/static/rest_mode/js/messages.js +264 -0
  127. swarm/static/rest_mode/js/messengerLogic.js +355 -0
  128. swarm/static/rest_mode/js/modules/apiService.js +84 -0
  129. swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
  130. swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
  131. swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
  132. swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
  133. swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
  134. swarm/static/rest_mode/js/modules/state.js +7 -0
  135. swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
  136. swarm/static/rest_mode/js/modules/validation.js +23 -0
  137. swarm/static/rest_mode/js/rendering.js +119 -0
  138. swarm/static/rest_mode/js/settings.js +130 -0
  139. swarm/static/rest_mode/js/sidebar.js +94 -0
  140. swarm/static/rest_mode/js/simpleLogic.js +37 -0
  141. swarm/static/rest_mode/js/slackLogic.js +66 -0
  142. swarm/static/rest_mode/js/splash.js +76 -0
  143. swarm/static/rest_mode/js/theme.js +111 -0
  144. swarm/static/rest_mode/js/toast.js +36 -0
  145. swarm/static/rest_mode/js/ui.js +265 -0
  146. swarm/static/rest_mode/js/validation.js +57 -0
  147. swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
  148. swarm/static/rest_mode/svg/arrow_down.svg +5 -0
  149. swarm/static/rest_mode/svg/arrow_left.svg +5 -0
  150. swarm/static/rest_mode/svg/arrow_right.svg +5 -0
  151. swarm/static/rest_mode/svg/arrow_up.svg +5 -0
  152. swarm/static/rest_mode/svg/attach.svg +8 -0
  153. swarm/static/rest_mode/svg/avatar.svg +7 -0
  154. swarm/static/rest_mode/svg/canvas.svg +6 -0
  155. swarm/static/rest_mode/svg/chat_history.svg +4 -0
  156. swarm/static/rest_mode/svg/close.svg +5 -0
  157. swarm/static/rest_mode/svg/copy.svg +4 -0
  158. swarm/static/rest_mode/svg/dark_mode.svg +3 -0
  159. swarm/static/rest_mode/svg/edit.svg +5 -0
  160. swarm/static/rest_mode/svg/layout.svg +9 -0
  161. swarm/static/rest_mode/svg/logo.svg +29 -0
  162. swarm/static/rest_mode/svg/logout.svg +5 -0
  163. swarm/static/rest_mode/svg/mobile.svg +5 -0
  164. swarm/static/rest_mode/svg/new_chat.svg +4 -0
  165. swarm/static/rest_mode/svg/not_visible.svg +5 -0
  166. swarm/static/rest_mode/svg/plus.svg +7 -0
  167. swarm/static/rest_mode/svg/run_code.svg +6 -0
  168. swarm/static/rest_mode/svg/save.svg +4 -0
  169. swarm/static/rest_mode/svg/search.svg +6 -0
  170. swarm/static/rest_mode/svg/settings.svg +4 -0
  171. swarm/static/rest_mode/svg/speaker.svg +5 -0
  172. swarm/static/rest_mode/svg/stop.svg +6 -0
  173. swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
  174. swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
  175. swarm/static/rest_mode/svg/toggle_off.svg +6 -0
  176. swarm/static/rest_mode/svg/toggle_on.svg +6 -0
  177. swarm/static/rest_mode/svg/trash.svg +10 -0
  178. swarm/static/rest_mode/svg/undo.svg +3 -0
  179. swarm/static/rest_mode/svg/visible.svg +8 -0
  180. swarm/static/rest_mode/svg/voice.svg +10 -0
  181. swarm/templates/account/login.html +22 -0
  182. swarm/templates/account/signup.html +32 -0
  183. swarm/templates/base.html +30 -0
  184. swarm/templates/chat.html +43 -0
  185. swarm/templates/index.html +35 -0
  186. swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
  187. swarm/templates/rest_mode/components/header.html +45 -0
  188. swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
  189. swarm/templates/rest_mode/components/settings_dialog.html +97 -0
  190. swarm/templates/rest_mode/components/splash_screen.html +7 -0
  191. swarm/templates/rest_mode/components/top_bar.html +28 -0
  192. swarm/templates/rest_mode/message_ui.html +50 -0
  193. swarm/templates/rest_mode/slackbot.html +30 -0
  194. swarm/templates/simple_blueprint_page.html +24 -0
  195. swarm/templates/websocket_partials/final_system_message.html +3 -0
  196. swarm/templates/websocket_partials/system_message.html +4 -0
  197. swarm/templates/websocket_partials/user_message.html +5 -0
  198. swarm/urls.py +57 -74
  199. swarm/utils/log_utils.py +63 -0
  200. swarm/views/api_views.py +48 -39
  201. swarm/views/chat_views.py +156 -70
  202. swarm/views/core_views.py +85 -90
  203. swarm/views/model_views.py +64 -121
  204. swarm/views/utils.py +65 -441
  205. open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
  206. open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
  207. open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
  208. open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
  209. swarm/agent/agent.py +0 -49
  210. swarm/core.py +0 -326
  211. swarm/extensions/mcp/__init__.py +0 -1
  212. swarm/extensions/mcp/cache_utils.py +0 -36
  213. swarm/extensions/mcp/mcp_client.py +0 -341
  214. swarm/extensions/mcp/mcp_constants.py +0 -7
  215. swarm/extensions/mcp/mcp_tool_provider.py +0 -110
  216. swarm/types.py +0 -126
  217. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/licenses/LICENSE +0 -0
@@ -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 # Added 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
- # Make sure ChatCompletionMessage is correctly imported if it's defined elsewhere
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, # <-- Added tools parameter
46
- tool_choice: Optional[str] = "auto", # <-- Added tool_choice parameter
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]]: # Adjusted return type hint
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
- messages = repair_message_payload([{"role": "system", "content": instructions}], debug=debug)
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
- messages = repair_message_payload(messages, debug=debug) # Ensure tool calls are paired post-truncation
110
- # Final content None -> "" check after repair
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, # Only set tool_choice if tools are provided
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} # Clean None values
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 # Return stream object directly
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 # Return the message dictionary
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"} # Return dict
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: # Catch broader errors during API call
136
+ except Exception as e:
159
137
  logger.error(f"Unexpected error during chat completion for '{agent.name}': {e}", exc_info=True)
160
- raise # Re-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, # <-- Added tools
172
- tool_choice: Optional[str] = "auto", # <-- Added tool_choice
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]]: # Return dict or stream
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, # Pass through
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, ChatConversation
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__'