open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636259__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 (296) hide show
  1. open_swarm-0.1.1748636259.dist-info/METADATA +188 -0
  2. open_swarm-0.1.1748636259.dist-info/RECORD +82 -0
  3. {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636259.dist-info}/WHEEL +2 -1
  4. open_swarm-0.1.1748636259.dist-info/entry_points.txt +3 -0
  5. open_swarm-0.1.1748636259.dist-info/top_level.txt +1 -0
  6. swarm/agent/agent.py +49 -0
  7. swarm/auth.py +48 -113
  8. swarm/consumers.py +0 -19
  9. swarm/extensions/blueprint/__init__.py +16 -30
  10. swarm/{core → extensions/blueprint}/agent_utils.py +1 -1
  11. swarm/extensions/blueprint/blueprint_base.py +458 -0
  12. swarm/extensions/blueprint/blueprint_discovery.py +112 -0
  13. swarm/extensions/blueprint/output_utils.py +95 -0
  14. swarm/{core → extensions/blueprint}/spinner.py +21 -30
  15. swarm/extensions/cli/cli_args.py +0 -6
  16. swarm/extensions/cli/commands/blueprint_management.py +9 -47
  17. swarm/extensions/cli/commands/config_management.py +6 -5
  18. swarm/extensions/cli/commands/edit_config.py +7 -16
  19. swarm/extensions/cli/commands/list_blueprints.py +1 -1
  20. swarm/extensions/cli/commands/validate_env.py +4 -11
  21. swarm/extensions/cli/commands/validate_envvars.py +6 -6
  22. swarm/extensions/cli/interactive_shell.py +2 -16
  23. swarm/extensions/config/config_loader.py +201 -107
  24. swarm/{core → extensions/config}/config_manager.py +38 -50
  25. swarm/{core → extensions/config}/server_config.py +0 -32
  26. swarm/extensions/launchers/build_launchers.py +14 -0
  27. swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
  28. swarm/extensions/launchers/swarm_api.py +64 -8
  29. swarm/extensions/launchers/swarm_cli.py +300 -8
  30. swarm/llm/chat_completion.py +195 -0
  31. swarm/serializers.py +5 -96
  32. swarm/settings.py +111 -99
  33. swarm/urls.py +74 -57
  34. swarm/utils/context_utils.py +4 -10
  35. swarm/utils/general_utils.py +0 -21
  36. swarm/utils/redact.py +36 -23
  37. swarm/views/api_views.py +39 -48
  38. swarm/views/chat_views.py +70 -237
  39. swarm/views/core_views.py +87 -80
  40. swarm/views/model_views.py +121 -64
  41. swarm/views/utils.py +441 -65
  42. swarm/views/web_views.py +2 -2
  43. open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
  44. open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
  45. open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
  46. swarm/blueprints/README.md +0 -68
  47. swarm/blueprints/blueprint_audit_status.json +0 -27
  48. swarm/blueprints/chatbot/README.md +0 -40
  49. swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
  50. swarm/blueprints/chatbot/metadata.json +0 -23
  51. swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
  52. swarm/blueprints/chucks_angels/README.md +0 -11
  53. swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
  54. swarm/blueprints/chucks_angels/test_basic.py +0 -3
  55. swarm/blueprints/codey/CODEY.md +0 -15
  56. swarm/blueprints/codey/README.md +0 -115
  57. swarm/blueprints/codey/blueprint_codey.py +0 -1072
  58. swarm/blueprints/codey/codey_cli.py +0 -373
  59. swarm/blueprints/codey/instructions.md +0 -17
  60. swarm/blueprints/codey/metadata.json +0 -23
  61. swarm/blueprints/common/operation_box_utils.py +0 -83
  62. swarm/blueprints/digitalbutlers/README.md +0 -11
  63. swarm/blueprints/digitalbutlers/__init__.py +0 -1
  64. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
  65. swarm/blueprints/digitalbutlers/test_basic.py +0 -3
  66. swarm/blueprints/divine_code/README.md +0 -3
  67. swarm/blueprints/divine_code/__init__.py +0 -10
  68. swarm/blueprints/divine_code/apps.py +0 -11
  69. swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
  70. swarm/blueprints/django_chat/apps.py +0 -6
  71. swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
  72. swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
  73. swarm/blueprints/django_chat/urls.py +0 -8
  74. swarm/blueprints/django_chat/views.py +0 -32
  75. swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
  76. swarm/blueprints/flock/README.md +0 -11
  77. swarm/blueprints/flock/__init__.py +0 -8
  78. swarm/blueprints/flock/blueprint_flock.py +0 -7
  79. swarm/blueprints/flock/test_basic.py +0 -3
  80. swarm/blueprints/geese/README.md +0 -10
  81. swarm/blueprints/geese/__init__.py +0 -8
  82. swarm/blueprints/geese/blueprint_geese.py +0 -384
  83. swarm/blueprints/geese/geese_cli.py +0 -102
  84. swarm/blueprints/jeeves/README.md +0 -41
  85. swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
  86. swarm/blueprints/jeeves/jeeves_cli.py +0 -55
  87. swarm/blueprints/jeeves/metadata.json +0 -24
  88. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
  89. swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
  90. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
  91. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
  92. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
  93. swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
  94. swarm/blueprints/poets/blueprint_poets.py +0 -546
  95. swarm/blueprints/poets/poets_cli.py +0 -23
  96. swarm/blueprints/rue_code/README.md +0 -8
  97. swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
  98. swarm/blueprints/rue_code/rue_code_cli.py +0 -43
  99. swarm/blueprints/stewie/apps.py +0 -12
  100. swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
  101. swarm/blueprints/stewie/models.py +0 -19
  102. swarm/blueprints/stewie/serializers.py +0 -10
  103. swarm/blueprints/stewie/settings.py +0 -17
  104. swarm/blueprints/stewie/urls.py +0 -11
  105. swarm/blueprints/stewie/views.py +0 -26
  106. swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
  107. swarm/blueprints/whinge_surf/README.md +0 -22
  108. swarm/blueprints/whinge_surf/__init__.py +0 -1
  109. swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
  110. swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
  111. swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
  112. swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
  113. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
  114. swarm/blueprints/zeus/__init__.py +0 -2
  115. swarm/blueprints/zeus/apps.py +0 -4
  116. swarm/blueprints/zeus/blueprint_zeus.py +0 -270
  117. swarm/blueprints/zeus/zeus_cli.py +0 -13
  118. swarm/cli/async_input.py +0 -65
  119. swarm/cli/async_input_demo.py +0 -32
  120. swarm/core/blueprint_base.py +0 -769
  121. swarm/core/blueprint_discovery.py +0 -125
  122. swarm/core/blueprint_runner.py +0 -59
  123. swarm/core/blueprint_ux.py +0 -109
  124. swarm/core/build_launchers.py +0 -15
  125. swarm/core/cli/__init__.py +0 -1
  126. swarm/core/cli/commands/__init__.py +0 -1
  127. swarm/core/cli/commands/blueprint_management.py +0 -7
  128. swarm/core/cli/interactive_shell.py +0 -14
  129. swarm/core/cli/main.py +0 -50
  130. swarm/core/cli/utils/__init__.py +0 -1
  131. swarm/core/cli/utils/discover_commands.py +0 -18
  132. swarm/core/config_loader.py +0 -122
  133. swarm/core/output_utils.py +0 -193
  134. swarm/core/session_logger.py +0 -42
  135. swarm/core/slash_commands.py +0 -89
  136. swarm/core/swarm_api.py +0 -68
  137. swarm/core/swarm_cli.py +0 -216
  138. swarm/core/utils/__init__.py +0 -0
  139. swarm/extensions/blueprint/cli_handler.py +0 -197
  140. swarm/extensions/blueprint/runnable_blueprint.py +0 -42
  141. swarm/extensions/cli/utils/__init__.py +0 -1
  142. swarm/extensions/cli/utils/async_input.py +0 -46
  143. swarm/extensions/cli/utils/prompt_user.py +0 -3
  144. swarm/management/__init__.py +0 -0
  145. swarm/management/commands/__init__.py +0 -0
  146. swarm/management/commands/runserver.py +0 -58
  147. swarm/middleware.py +0 -65
  148. swarm/permissions.py +0 -38
  149. swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
  150. swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
  151. swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
  152. swarm/static/contrib/markedjs/marked.min.js +0 -6
  153. swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
  154. swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
  155. swarm/static/contrib/tabler-icons/archive.svg +0 -21
  156. swarm/static/contrib/tabler-icons/artboard.svg +0 -27
  157. swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
  158. swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
  159. swarm/static/contrib/tabler-icons/carambola.svg +0 -19
  160. swarm/static/contrib/tabler-icons/copy.svg +0 -20
  161. swarm/static/contrib/tabler-icons/download.svg +0 -21
  162. swarm/static/contrib/tabler-icons/edit.svg +0 -21
  163. swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
  164. swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
  165. swarm/static/contrib/tabler-icons/headset.svg +0 -22
  166. swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
  167. swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
  168. swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
  169. swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
  170. swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
  171. swarm/static/contrib/tabler-icons/message-star.svg +0 -22
  172. swarm/static/contrib/tabler-icons/message-x.svg +0 -23
  173. swarm/static/contrib/tabler-icons/message.svg +0 -21
  174. swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
  175. swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
  176. swarm/static/contrib/tabler-icons/robot.svg +0 -26
  177. swarm/static/contrib/tabler-icons/search.svg +0 -19
  178. swarm/static/contrib/tabler-icons/settings.svg +0 -20
  179. swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
  180. swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
  181. swarm/static/css/dropdown.css +0 -22
  182. swarm/static/htmx/htmx.min.js +0 -0
  183. swarm/static/js/dropdown.js +0 -23
  184. swarm/static/rest_mode/css/base.css +0 -470
  185. swarm/static/rest_mode/css/chat-history.css +0 -286
  186. swarm/static/rest_mode/css/chat.css +0 -251
  187. swarm/static/rest_mode/css/chatbot.css +0 -74
  188. swarm/static/rest_mode/css/chatgpt.css +0 -62
  189. swarm/static/rest_mode/css/colors/corporate.css +0 -74
  190. swarm/static/rest_mode/css/colors/pastel.css +0 -81
  191. swarm/static/rest_mode/css/colors/tropical.css +0 -82
  192. swarm/static/rest_mode/css/general.css +0 -142
  193. swarm/static/rest_mode/css/layout.css +0 -167
  194. swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
  195. swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
  196. swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
  197. swarm/static/rest_mode/css/messages.css +0 -84
  198. swarm/static/rest_mode/css/messenger.css +0 -135
  199. swarm/static/rest_mode/css/settings.css +0 -91
  200. swarm/static/rest_mode/css/simple.css +0 -44
  201. swarm/static/rest_mode/css/slack.css +0 -58
  202. swarm/static/rest_mode/css/style.css +0 -156
  203. swarm/static/rest_mode/css/theme.css +0 -30
  204. swarm/static/rest_mode/css/toast.css +0 -40
  205. swarm/static/rest_mode/js/auth.js +0 -9
  206. swarm/static/rest_mode/js/blueprint.js +0 -41
  207. swarm/static/rest_mode/js/blueprintUtils.js +0 -12
  208. swarm/static/rest_mode/js/chatLogic.js +0 -79
  209. swarm/static/rest_mode/js/debug.js +0 -63
  210. swarm/static/rest_mode/js/events.js +0 -98
  211. swarm/static/rest_mode/js/main.js +0 -19
  212. swarm/static/rest_mode/js/messages.js +0 -264
  213. swarm/static/rest_mode/js/messengerLogic.js +0 -355
  214. swarm/static/rest_mode/js/modules/apiService.js +0 -84
  215. swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
  216. swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
  217. swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
  218. swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
  219. swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
  220. swarm/static/rest_mode/js/modules/state.js +0 -7
  221. swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
  222. swarm/static/rest_mode/js/modules/validation.js +0 -23
  223. swarm/static/rest_mode/js/rendering.js +0 -119
  224. swarm/static/rest_mode/js/settings.js +0 -130
  225. swarm/static/rest_mode/js/sidebar.js +0 -94
  226. swarm/static/rest_mode/js/simpleLogic.js +0 -37
  227. swarm/static/rest_mode/js/slackLogic.js +0 -66
  228. swarm/static/rest_mode/js/splash.js +0 -76
  229. swarm/static/rest_mode/js/theme.js +0 -111
  230. swarm/static/rest_mode/js/toast.js +0 -36
  231. swarm/static/rest_mode/js/ui.js +0 -265
  232. swarm/static/rest_mode/js/validation.js +0 -57
  233. swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
  234. swarm/static/rest_mode/svg/arrow_down.svg +0 -5
  235. swarm/static/rest_mode/svg/arrow_left.svg +0 -5
  236. swarm/static/rest_mode/svg/arrow_right.svg +0 -5
  237. swarm/static/rest_mode/svg/arrow_up.svg +0 -5
  238. swarm/static/rest_mode/svg/attach.svg +0 -8
  239. swarm/static/rest_mode/svg/avatar.svg +0 -7
  240. swarm/static/rest_mode/svg/canvas.svg +0 -6
  241. swarm/static/rest_mode/svg/chat_history.svg +0 -4
  242. swarm/static/rest_mode/svg/close.svg +0 -5
  243. swarm/static/rest_mode/svg/copy.svg +0 -4
  244. swarm/static/rest_mode/svg/dark_mode.svg +0 -3
  245. swarm/static/rest_mode/svg/edit.svg +0 -5
  246. swarm/static/rest_mode/svg/layout.svg +0 -9
  247. swarm/static/rest_mode/svg/logo.svg +0 -29
  248. swarm/static/rest_mode/svg/logout.svg +0 -5
  249. swarm/static/rest_mode/svg/mobile.svg +0 -5
  250. swarm/static/rest_mode/svg/new_chat.svg +0 -4
  251. swarm/static/rest_mode/svg/not_visible.svg +0 -5
  252. swarm/static/rest_mode/svg/plus.svg +0 -7
  253. swarm/static/rest_mode/svg/run_code.svg +0 -6
  254. swarm/static/rest_mode/svg/save.svg +0 -4
  255. swarm/static/rest_mode/svg/search.svg +0 -6
  256. swarm/static/rest_mode/svg/settings.svg +0 -4
  257. swarm/static/rest_mode/svg/speaker.svg +0 -5
  258. swarm/static/rest_mode/svg/stop.svg +0 -6
  259. swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
  260. swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
  261. swarm/static/rest_mode/svg/toggle_off.svg +0 -6
  262. swarm/static/rest_mode/svg/toggle_on.svg +0 -6
  263. swarm/static/rest_mode/svg/trash.svg +0 -10
  264. swarm/static/rest_mode/svg/undo.svg +0 -3
  265. swarm/static/rest_mode/svg/visible.svg +0 -8
  266. swarm/static/rest_mode/svg/voice.svg +0 -10
  267. swarm/templates/account/login.html +0 -22
  268. swarm/templates/account/signup.html +0 -32
  269. swarm/templates/base.html +0 -30
  270. swarm/templates/chat.html +0 -43
  271. swarm/templates/index.html +0 -35
  272. swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
  273. swarm/templates/rest_mode/components/header.html +0 -45
  274. swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
  275. swarm/templates/rest_mode/components/settings_dialog.html +0 -97
  276. swarm/templates/rest_mode/components/splash_screen.html +0 -7
  277. swarm/templates/rest_mode/components/top_bar.html +0 -28
  278. swarm/templates/rest_mode/message_ui.html +0 -50
  279. swarm/templates/rest_mode/slackbot.html +0 -30
  280. swarm/templates/simple_blueprint_page.html +0 -24
  281. swarm/templates/websocket_partials/final_system_message.html +0 -3
  282. swarm/templates/websocket_partials/system_message.html +0 -4
  283. swarm/templates/websocket_partials/user_message.html +0 -5
  284. swarm/utils/ansi_box.py +0 -34
  285. swarm/utils/disable_tracing.py +0 -38
  286. swarm/utils/log_utils.py +0 -63
  287. swarm/utils/openai_patch.py +0 -33
  288. swarm/ux/ansi_box.py +0 -43
  289. swarm/ux/spinner.py +0 -53
  290. {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636259.dist-info}/licenses/LICENSE +0 -0
  291. /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
  292. /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
  293. /swarm/{core → extensions/config}/setup_wizard.py +0 -0
  294. /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
  295. /swarm/{core → extensions/config}/utils/logger.py +0 -0
  296. /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
swarm/utils/redact.py CHANGED
@@ -13,43 +13,56 @@ DEFAULT_SENSITIVE_KEYS = ["secret", "password", "api_key", "apikey", "token", "a
13
13
  def redact_sensitive_data(
14
14
  data: Union[str, Dict, List],
15
15
  sensitive_keys: Optional[List[str]] = None,
16
- reveal_chars: int = 0,
16
+ reveal_chars: int = 4,
17
17
  mask: str = "[REDACTED]"
18
18
  ) -> Union[str, Dict, List]:
19
19
  """
20
20
  Recursively redact sensitive information from dictionaries or lists based on keys.
21
- By default, fully masks sensitive values (returns only the mask).
22
- If reveal_chars > 0, partially masks (preserves reveal_chars at start/end).
23
- If a custom mask is provided, always use it (for test compatibility).
21
+ Applies partial redaction to string values associated with sensitive keys.
24
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.
25
32
  """
26
- keys_to_redact = set((k.lower() for k in (sensitive_keys or DEFAULT_SENSITIVE_KEYS)))
27
- def smart_mask(val: str) -> str:
28
- if not isinstance(val, str):
29
- return val
30
- if mask != "[REDACTED]":
31
- return mask
32
- if reveal_chars == 0:
33
- return mask
34
- if len(val) >= 2 * reveal_chars + 1:
35
- return val[:reveal_chars] + mask + val[-reveal_chars:]
36
- return mask
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
+
37
36
  if isinstance(data, dict):
38
37
  redacted_dict = {}
39
- for k, v in data.items():
40
- if isinstance(k, str) and k.lower() in keys_to_redact:
41
- redacted_dict[k] = smart_mask(v)
42
- elif isinstance(v, (dict, list)):
43
- redacted_dict[k] = redact_sensitive_data(v, sensitive_keys, reveal_chars, mask)
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]"
44
52
  else:
45
- redacted_dict[k] = v
53
+ # Recursively redact nested structures if key is not sensitive
54
+ redacted_dict[key] = redact_sensitive_data(value, keys_to_redact, reveal_chars, mask)
46
55
  return redacted_dict
56
+
47
57
  elif isinstance(data, list):
58
+ # Recursively redact items in a list ONLY if they are dicts or lists themselves.
48
59
  processed_list = []
49
60
  for item in data:
50
61
  if isinstance(item, (dict, list)):
51
- processed_list.append(redact_sensitive_data(item, sensitive_keys, reveal_chars, mask))
62
+ processed_list.append(redact_sensitive_data(item, keys_to_redact, reveal_chars, mask))
52
63
  else:
53
- processed_list.append(item)
64
+ processed_list.append(item) # Keep non-dict/list items (like strings) unchanged
54
65
  return processed_list
66
+
67
+ # Return data unchanged if it's not a dict or list (including standalone strings)
55
68
  return data
swarm/views/api_views.py CHANGED
@@ -1,55 +1,46 @@
1
- import time
2
- import logging
3
- from rest_framework.views import APIView
4
- from rest_framework.response import Response
5
- from rest_framework import status
1
+ """
2
+ API-specific views and viewsets for Open Swarm MCP Core.
3
+ """
4
+ from rest_framework.viewsets import ModelViewSet
6
5
  from rest_framework.permissions import AllowAny
7
- # *** Import async_to_sync ***
8
- from asgiref.sync import async_to_sync
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
9
11
 
10
- from swarm.views.utils import get_available_blueprints
12
+ logger = setup_logger(__name__)
11
13
 
12
- logger = logging.getLogger(__name__)
14
+ class HiddenSpectacularAPIView(BaseSpectacularAPIView):
15
+ exclude_from_schema = True
13
16
 
14
- class ModelsListView(APIView):
15
- """
16
- API view to list available models (blueprints) compatible with OpenAI's /v1/models format.
17
- """
17
+ class ChatMessageViewSet(ModelViewSet):
18
+ """API viewset for managing chat messages."""
19
+ authentication_classes = []
18
20
  permission_classes = [AllowAny]
21
+ queryset = ChatMessage.objects.all()
22
+ serializer_class = ChatMessageSerializer
19
23
 
20
- def get(self, request, *args, **kwargs):
21
- try:
22
- # *** Use async_to_sync to call the async function ***
23
- available_blueprints = async_to_sync(get_available_blueprints)()
24
-
25
- models_data = []
26
- current_time = int(time.time())
27
- if isinstance(available_blueprints, dict):
28
- blueprint_ids = available_blueprints.keys()
29
- elif isinstance(available_blueprints, list):
30
- blueprint_ids = available_blueprints
31
- else:
32
- logger.error(f"Unexpected type from get_available_blueprints: {type(available_blueprints)}")
33
- blueprint_ids = []
34
-
35
- for blueprint_id in blueprint_ids:
36
- models_data.append({
37
- "id": blueprint_id,
38
- "object": "model",
39
- "created": current_time,
40
- "owned_by": "open-swarm",
41
- })
42
-
43
- response_payload = {
44
- "object": "list",
45
- "data": models_data,
46
- }
47
- return Response(response_payload, status=status.HTTP_200_OK)
48
-
49
- except Exception as e:
50
- logger.exception("Error retrieving available models.")
51
- return Response(
52
- {"error": "Failed to retrieve models list."},
53
- status=status.HTTP_500_INTERNAL_SERVER_ERROR
54
- )
24
+ @extend_schema(summary="List all chat messages")
25
+ def list(self, request, *args, **kwargs):
26
+ return super().list(request, *args, **kwargs)
55
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)
swarm/views/chat_views.py CHANGED
@@ -1,243 +1,76 @@
1
-
2
- # --- Content for src/swarm/views/chat_views.py ---
1
+ """
2
+ Chat-related views for Open Swarm MCP Core.
3
+ """
4
+ import asyncio
3
5
  import logging
4
6
  import json
5
- import uuid
6
- import time
7
- import asyncio
8
- from typing import Dict, Any, AsyncGenerator, List, Optional
9
-
10
- from django.shortcuts import render
11
- from django.http import StreamingHttpResponse, JsonResponse, Http404, HttpRequest, HttpResponse, HttpResponseBase
12
- from django.views import View
13
- from django.utils.decorators import method_decorator
14
7
  from django.views.decorators.csrf import csrf_exempt
15
- from django.contrib.auth.decorators import login_required
16
- from django.conf import settings
17
- from django.urls import reverse # Needed for reverse() used in tests
18
-
19
- from rest_framework import status
20
- from rest_framework.views import APIView
21
8
  from rest_framework.response import Response
22
- from rest_framework.permissions import IsAuthenticated, AllowAny
23
- from rest_framework.exceptions import ValidationError, PermissionDenied, NotFound, APIException, ParseError, NotAuthenticated
24
- from rest_framework.request import Request # Import DRF Request
25
-
26
- # Utility to wrap sync functions for async execution
27
- from asgiref.sync import sync_to_async
28
-
29
- # Assuming serializers are in the same app
30
- from swarm.serializers import ChatCompletionRequestSerializer
31
- # Assuming utils are in the same app/directory level
32
- # Make sure these utils are async-safe or wrapped if they perform sync I/O
33
- from .utils import get_blueprint_instance, validate_model_access, get_available_blueprints
34
- # Import custom permission
35
- from swarm.auth import HasValidTokenOrSession # Keep this import
36
-
37
- logger = logging.getLogger(__name__)
38
- # Specific logger for debug prints, potentially configured differently
39
- print_logger = logging.getLogger('print_debug')
40
-
41
- # ==============================================================================
42
- # API Views (DRF based)
43
- # ==============================================================================
44
-
45
- class HealthCheckView(APIView):
46
- """ Simple health check endpoint. """
47
- permission_classes = [AllowAny]
48
- def get(self, request, *args, **kwargs):
49
- """ Returns simple 'ok' status. """
50
- return Response({"status": "ok"})
51
-
52
- class ChatCompletionsView(APIView):
53
- """
54
- Handles chat completion requests (/v1/chat/completions), compatible with OpenAI API spec.
55
- Supports both streaming and non-streaming responses.
56
- Uses asynchronous handling for potentially long-running blueprint operations.
57
- """
58
- # Default serializer class for request validation.
59
- serializer_class = ChatCompletionRequestSerializer
60
- # Default permission classes are likely set in settings.py
61
- # permission_classes = [IsAuthenticated] # Example default
62
-
63
- # --- Internal Helper Methods (Unchanged) ---
64
-
65
- async def _handle_non_streaming(self, blueprint_instance, messages: List[Dict[str, str]], request_id: str, model_name: str) -> Response:
66
- """ Handles non-streaming requests. """
67
- logger.info(f"[ReqID: {request_id}] Processing non-streaming request for model '{model_name}'.")
68
- final_response_data = None; start_time = time.time()
69
- try:
70
- # The blueprint's run method should be an async generator.
71
- async_generator = blueprint_instance.run(messages)
72
- async for chunk in async_generator:
73
- # Check if the chunk contains the expected final message list.
74
- if isinstance(chunk, dict) and "messages" in chunk and isinstance(chunk["messages"], list):
75
- final_response_data = chunk["messages"]
76
- logger.debug(f"[ReqID: {request_id}] Received final data chunk.")
77
- break # Stop after getting the final data
78
- else:
79
- logger.warning(f"[ReqID: {request_id}] Unexpected chunk format during non-streaming run: {chunk}")
80
-
81
- if not final_response_data or not isinstance(final_response_data, list) or not final_response_data:
82
- logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' did not return a valid final message list. Got: {final_response_data}")
83
- raise APIException("Blueprint did not return valid data.", code=status.HTTP_500_INTERNAL_SERVER_ERROR)
84
-
85
- if not isinstance(final_response_data[0], dict) or 'role' not in final_response_data[0]:
86
- logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' returned invalid message structure. Got: {final_response_data[0]}")
87
- raise APIException("Blueprint returned invalid message structure.", code=status.HTTP_500_INTERNAL_SERVER_ERROR)
88
-
89
- response_payload = { "id": f"chatcmpl-{request_id}", "object": "chat.completion", "created": int(time.time()), "model": model_name, "choices": [{"index": 0, "message": final_response_data[0], "logprobs": None, "finish_reason": "stop"}], "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}, "system_fingerprint": None }
90
- end_time = time.time(); logger.info(f"[ReqID: {request_id}] Non-streaming request completed in {end_time - start_time:.2f}s.")
91
- return Response(response_payload, status=status.HTTP_200_OK)
92
- except APIException: raise
93
- except Exception as e: logger.error(f"[ReqID: {request_id}] Unexpected error during non-streaming blueprint execution: {e}", exc_info=True); raise APIException(f"Internal server error during generation: {e}", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
94
-
95
- async def _handle_streaming(self, blueprint_instance, messages: List[Dict[str, str]], request_id: str, model_name: str) -> StreamingHttpResponse:
96
- """ Handles streaming requests using SSE. """
97
- logger.info(f"[ReqID: {request_id}] Processing streaming request for model '{model_name}'.")
98
- async def event_stream():
99
- start_time = time.time(); chunk_index = 0
100
- try:
101
- logger.debug(f"[ReqID: {request_id}] Getting async generator from blueprint.run()..."); async_generator = blueprint_instance.run(messages); logger.debug(f"[ReqID: {request_id}] Got async generator. Starting iteration...")
102
- async for chunk in async_generator:
103
- logger.debug(f"[ReqID: {request_id}] Received stream chunk {chunk_index}: {chunk}")
104
- if not isinstance(chunk, dict) or "messages" not in chunk or not isinstance(chunk["messages"], list) or not chunk["messages"] or not isinstance(chunk["messages"][0], dict): logger.warning(f"[ReqID: {request_id}] Skipping invalid chunk format: {chunk}"); continue
105
- delta_content = chunk["messages"][0].get("content"); delta = {"role": "assistant"}
106
- if delta_content is not None: delta["content"] = delta_content
107
- response_chunk = { "id": f"chatcmpl-{request_id}", "object": "chat.completion.chunk", "created": int(time.time()), "model": model_name, "choices": [{"index": 0, "delta": delta, "logprobs": None, "finish_reason": None}] }
108
- logger.debug(f"[ReqID: {request_id}] Sending SSE chunk {chunk_index}"); yield f"data: {json.dumps(response_chunk)}\n\n"; chunk_index += 1; await asyncio.sleep(0.01)
109
- logger.debug(f"[ReqID: {request_id}] Finished iterating stream. Sending [DONE]."); yield "data: [DONE]\n\n"; end_time = time.time(); logger.info(f"[ReqID: {request_id}] Streaming request completed in {end_time - start_time:.2f}s.")
110
- except APIException as e: logger.error(f"[ReqID: {request_id}] API error during streaming: {e}", exc_info=True); error_msg = f"API error: {e.detail}"; error_chunk = {"error": {"message": error_msg, "type": "api_error", "code": e.status_code}}; yield f"data: {json.dumps(error_chunk)}\n\n"; yield "data: [DONE]\n\n"
111
- except Exception as e: logger.error(f"[ReqID: {request_id}] Unexpected error during streaming: {e}", exc_info=True); error_msg = f"Internal server error: {str(e)}"; error_chunk = {"error": {"message": error_msg, "type": "internal_error"}}; yield f"data: {json.dumps(error_chunk)}\n\n"; yield "data: [DONE]\n\n"
112
- return StreamingHttpResponse(event_stream(), content_type="text/event-stream")
113
-
114
- # --- Restore Custom dispatch method (wrapping perform_authentication) ---
115
- @method_decorator(csrf_exempt)
116
- async def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponseBase:
117
- """
118
- Override DRF's dispatch method to specifically wrap the authentication step.
119
- """
120
- self.args = args
121
- self.kwargs = kwargs
122
- drf_request: Request = self.initialize_request(request, *args, **kwargs)
123
- self.request = drf_request
124
- self.headers = self.default_response_headers
125
-
126
- response = None
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.
127
47
  try:
128
- # --- Wrap ONLY perform_authentication ---
129
- print_logger.debug(f"User before perform_authentication: {getattr(drf_request, 'user', 'N/A')}, Auth: {getattr(drf_request, 'auth', 'N/A')}")
130
- # This forces the synchronous DB access within perform_authentication into a thread
131
- await sync_to_async(self.perform_authentication)(drf_request)
132
- print_logger.debug(f"User after perform_authentication: {getattr(drf_request, 'user', 'N/A')}, Auth: {getattr(drf_request, 'auth', 'N/A')}")
133
- # --- End wrapping ---
134
-
135
- # Run permission and throttle checks synchronously after auth.
136
- # These checks operate on the now-populated request.user/auth attributes.
137
- self.check_permissions(drf_request)
138
- print_logger.debug("Permissions check passed.")
139
- self.check_throttles(drf_request)
140
- print_logger.debug("Throttles check passed.")
141
-
142
- # Find and execute the handler (e.g., post).
143
- if drf_request.method.lower() in self.http_method_names:
144
- handler = getattr(self, drf_request.method.lower(), self.http_method_not_allowed)
145
- else:
146
- handler = self.http_method_not_allowed
147
-
148
- # IMPORTANT: Await the handler if it's async (like self.post)
149
- if asyncio.iscoroutinefunction(handler):
150
- response = await handler(drf_request, *args, **kwargs)
151
- else:
152
- # Wrap sync handlers if any exist (like GET, OPTIONS).
153
- response = await sync_to_async(handler)(drf_request, *args, **kwargs)
154
-
155
- except Exception as exc:
156
- # Let DRF handle exceptions to generate appropriate responses
157
- response = self.handle_exception(exc)
158
-
159
- # Finalize response should now receive a valid Response/StreamingHttpResponse
160
- self.response = self.finalize_response(drf_request, response, *args, **kwargs)
161
- return self.response
162
-
163
- # --- POST Handler (Keep sync_to_async wrappers here too) ---
164
- async def post(self, request: Request, *args: Any, **kwargs: Any) -> HttpResponseBase:
165
- """
166
- Handles POST requests for chat completions. Assumes dispatch has handled auth/perms.
167
- """
168
- request_id = str(uuid.uuid4())
169
- logger.info(f"[ReqID: {request_id}] Processing POST request.")
170
- print_logger.debug(f"[ReqID: {request_id}] User in post: {getattr(request, 'user', 'N/A')}, Auth: {getattr(request, 'auth', 'N/A')}")
171
-
172
- # --- Request Body Parsing & Validation ---
173
- try: request_data = request.data
174
- except ParseError as e: logger.error(f"[ReqID: {request_id}] Invalid request body format: {e.detail}"); raise e
175
- except json.JSONDecodeError as e: logger.error(f"[ReqID: {request_id}] JSON Decode Error: {e}"); raise ParseError(f"Invalid JSON body: {e}")
176
-
177
- # --- Serialization and Validation ---
178
- serializer = self.serializer_class(data=request_data)
179
- try:
180
- print_logger.debug(f"[ReqID: {request_id}] Validating request data: {request_data}")
181
- # Wrap sync is_valid call as it *might* do DB lookups
182
- await sync_to_async(serializer.is_valid)(raise_exception=True)
183
- print_logger.debug(f"[ReqID: {request_id}] Request data validation successful.")
184
- except ValidationError as e: print_logger.error(f"[ReqID: {request_id}] Request data validation FAILED: {e.detail}"); raise e
185
- except Exception as e: print_logger.error(f"[ReqID: {request_id}] Unexpected error during serializer validation: {e}", exc_info=True); raise APIException(f"Internal error during request validation: {e}", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
186
-
187
- validated_data = serializer.validated_data
188
- model_name = validated_data['model']
189
- messages = validated_data['messages']
190
- stream = validated_data.get('stream', False)
191
- blueprint_params = validated_data.get('params', None)
192
-
193
- # --- Model Access Validation ---
194
- # This function likely performs sync DB lookups, so wrap it.
195
- print_logger.debug(f"[ReqID: {request_id}] Checking model access for user '{request.user}' and model '{model_name}'")
196
- try:
197
- access_granted = await sync_to_async(validate_model_access)(request.user, model_name)
198
- except Exception as e:
199
- logger.error(f"[ReqID: {request_id}] Error during model access validation for model '{model_name}': {e}", exc_info=True)
200
- raise APIException("Error checking model permissions.", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
201
-
202
- if not access_granted:
203
- logger.warning(f"[ReqID: {request_id}] User '{request.user}' denied access to model '{model_name}'.")
204
- raise PermissionDenied(f"You do not have permission to access the model '{model_name}'.")
205
- print_logger.debug(f"[ReqID: {request_id}] Model access granted.")
206
-
207
- # --- Get Blueprint Instance ---
208
- # This function should ideally be async or sync-safe.
209
- print_logger.debug(f"[ReqID: {request_id}] Getting blueprint instance for '{model_name}' with params: {blueprint_params}")
210
- try:
211
- blueprint_instance = await get_blueprint_instance(model_name, params=blueprint_params)
212
- except Exception as e:
213
- logger.error(f"[ReqID: {request_id}] Error getting blueprint instance for '{model_name}': {e}", exc_info=True)
214
- raise APIException(f"Failed to load model '{model_name}': {e}", code=status.HTTP_500_INTERNAL_SERVER_ERROR) from e
215
-
216
- if blueprint_instance is None:
217
- logger.error(f"[ReqID: {request_id}] Blueprint '{model_name}' not found or failed to initialize (get_blueprint_instance returned None).")
218
- raise NotFound(f"The requested model (blueprint) '{model_name}' was not found or could not be initialized.")
219
-
220
- # --- Handle Streaming or Non-Streaming Response ---
221
- if stream:
222
- return await self._handle_streaming(blueprint_instance, messages, request_id, model_name)
223
- else:
224
- return await self._handle_non_streaming(blueprint_instance, messages, request_id, model_name)
225
-
226
-
227
- # ==============================================================================
228
- # Simple Django Views (Example for Web UI - if ENABLE_WEBUI=True)
229
- # ==============================================================================
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)
230
76
 
231
- @method_decorator(csrf_exempt, name='dispatch') # Apply csrf_exempt if needed
232
- @method_decorator(login_required, name='dispatch') # Require login
233
- class IndexView(View):
234
- """ Renders the main chat interface page. """
235
- def get(self, request):
236
- """ Handles GET requests to render the index page. """
237
- # Assuming get_available_blueprints is sync safe
238
- available_blueprints = get_available_blueprints()
239
- context = {
240
- 'available_blueprints': available_blueprints,
241
- 'user': request.user, # User should be available here
242
- }
243
- return render(request, 'index.html', context)