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.
Files changed (217) hide show
  1. open_swarm-0.1.1743362777.dist-info/METADATA +217 -0
  2. open_swarm-0.1.1743362777.dist-info/RECORD +260 -0
  3. {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743362777.dist-info}/WHEEL +1 -2
  4. open_swarm-0.1.1743362777.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.1743362777.dist-info}/licenses/LICENSE +0 -0
swarm/views/utils.py CHANGED
@@ -1,457 +1,81 @@
1
- """
2
- Utility functions for Swarm views.
3
- """
4
- import json
5
- from swarm.types import ChatMessage
6
- import uuid
7
- import time
8
- import os
9
- import redis
10
1
  import logging
11
- from typing import Any, Dict, List, Optional, Tuple
12
- from pathlib import Path
13
-
14
2
  from django.conf import settings
15
- from rest_framework.response import Response
16
- from rest_framework import status
17
-
18
- # Project-specific imports
19
- from swarm.models import ChatConversation, ChatMessage
20
- from swarm.extensions.blueprint import discover_blueprints
21
- from swarm.extensions.blueprint.blueprint_base import BlueprintBase
22
- from swarm.extensions.config.config_loader import load_server_config, load_llm_config
23
- from swarm.utils.logger_setup import setup_logger
24
- from swarm.utils.redact import redact_sensitive_data
25
- from swarm.utils.general_utils import extract_chat_id
26
- from swarm.extensions.blueprint.blueprint_utils import filter_blueprints
27
- from swarm.settings import BASE_DIR, BLUEPRINTS_DIR # Import BLUEPRINTS_DIR
28
-
29
- logger = setup_logger(__name__)
30
-
31
- # --- Configuration Loading ---
32
- CONFIG_PATH = Path(settings.BASE_DIR) / 'swarm_config.json'
33
- config = {}
34
- llm_config = {}
35
- llm_model = "default"
36
- llm_provider = "unknown"
37
-
38
- def load_configs():
39
- """Load main and LLM configurations."""
40
- global config, llm_config, llm_model, llm_provider
41
- try:
42
- config = load_server_config(str(CONFIG_PATH))
43
- logger.info(f"Server config loaded from {CONFIG_PATH}")
44
- except FileNotFoundError:
45
- logger.warning(f"Configuration file not found at {CONFIG_PATH}. Using defaults.")
46
- config = {} # Use empty dict or default structure if needed
47
- except ValueError as e:
48
- logger.error(f"Error loading server config: {e}. Using defaults.")
49
- config = {}
50
- except Exception as e:
51
- logger.critical(f"Unexpected error loading server config: {e}", exc_info=True)
52
- config = {} # Critical error, use empty config
53
-
54
- try:
55
- llm_config = load_llm_config(config) # Load default LLM config
56
- llm_model = llm_config.get("model", "default")
57
- llm_provider = llm_config.get("provider", "unknown")
58
- logger.info(f"Default LLM config loaded: Provider={llm_provider}, Model={llm_model}")
59
- except ValueError as e:
60
- logger.error(f"Failed to load default LLM configuration: {e}. LLM features may fail.")
61
- llm_config = {} # Ensure llm_config is a dict even on error
62
- llm_model = "error"
63
- llm_provider = "error"
64
- except Exception as e:
65
- logger.critical(f"Unexpected error loading LLM config: {e}", exc_info=True)
66
- llm_config = {}
67
- llm_model = "error"
68
- llm_provider = "error"
69
-
70
- load_configs() # Load configs when module is imported
71
-
72
- # --- Blueprint Discovery ---
73
- blueprints_metadata = {}
74
- def discover_and_load_blueprints():
75
- """Discover blueprints from the configured directory."""
76
- global blueprints_metadata
77
- try:
78
- # Ensure BLUEPRINTS_DIR is a Path object
79
- bp_dir_path = Path(BLUEPRINTS_DIR) if isinstance(BLUEPRINTS_DIR, str) else BLUEPRINTS_DIR
80
- if not bp_dir_path.is_absolute():
81
- bp_dir_path = Path(settings.BASE_DIR).parent / bp_dir_path # Assuming relative to project root
82
- logger.info(f"Discovering blueprints in: {bp_dir_path}")
83
- discovered = discover_blueprints(directories=[str(bp_dir_path)])
84
- blueprints_metadata = discovered
85
- loaded_names = list(blueprints_metadata.keys())
86
- logger.info(f"Discovered blueprints: {loaded_names if loaded_names else 'None'}")
87
- except FileNotFoundError:
88
- logger.warning(f"Blueprints directory '{BLUEPRINTS_DIR}' not found. No blueprints discovered dynamically.")
89
- blueprints_metadata = {}
90
- except Exception as e:
91
- logger.error(f"Failed during blueprint discovery: {e}", exc_info=True)
92
- blueprints_metadata = {}
93
-
94
- discover_and_load_blueprints() # Discover blueprints when module is imported
95
-
96
- # --- Redis Client Initialization ---
97
- REDIS_AVAILABLE = bool(os.getenv("STATEFUL_CHAT_ID_PATH")) and hasattr(settings, 'REDIS_HOST') and hasattr(settings, 'REDIS_PORT')
98
- redis_client = None
99
- if REDIS_AVAILABLE:
100
- try:
101
- redis_client = redis.Redis(
102
- host=settings.REDIS_HOST,
103
- port=settings.REDIS_PORT,
104
- decode_responses=True
105
- )
106
- redis_client.ping()
107
- logger.info(f"Redis connection successful ({settings.REDIS_HOST}:{settings.REDIS_PORT}).")
108
- except Exception as e:
109
- logger.warning(f"Redis connection failed: {e}. Stateful chat history via Redis is disabled.")
110
- REDIS_AVAILABLE = False
111
- else:
112
- logger.info("Redis configuration not found or STATEFUL_CHAT_ID_PATH not set. Stateful chat history via Redis is disabled.")
113
-
114
- # --- Helper Functions ---
115
-
116
- def serialize_swarm_response(response: Any, model_name: str, context_variables: Dict[str, Any]) -> Dict[str, Any]:
117
- """Serialize a blueprint response into an OpenAI-compatible chat completion format."""
118
- logger.debug(f"Serializing Swarm response, type: {type(response)}, model: {model_name}")
119
-
120
- # Determine messages from response
121
- if hasattr(response, 'messages') and isinstance(response.messages, list):
122
- messages = response.messages
123
- elif isinstance(response, dict) and isinstance(response.get("messages"), list):
124
- messages = response.get("messages", [])
125
- elif isinstance(response, str):
126
- logger.warning(f"Received raw string response, wrapping as assistant message: {response[:100]}...")
127
- messages = [{"role": "assistant", "content": response}]
128
- else:
129
- logger.error(f"Unexpected response type for serialization: {type(response)}. Returning empty response.")
130
- messages = []
131
-
132
- # Create choices array based on assistant messages with content
133
- choices = []
134
- for i, msg in enumerate(messages):
135
- if isinstance(msg, dict) and msg.get("role") == "assistant" and msg.get("content") is not None:
136
- choice = {
137
- "index": len(choices),
138
- "message": {
139
- "role": "assistant",
140
- "content": msg["content"]
141
- },
142
- "finish_reason": "stop" # Assume stop for non-streaming
143
- }
144
- # Include tool_calls if present in the original message
145
- if msg.get("tool_calls") is not None:
146
- choice["message"]["tool_calls"] = msg["tool_calls"]
147
- choice["finish_reason"] = "tool_calls" # Adjust finish reason if tools are called
148
-
149
- choices.append(choice)
150
- logger.debug(f"Added choice {len(choices)-1}: role={choice['message']['role']}, finish={choice['finish_reason']}")
151
-
152
- if not choices and messages:
153
- # Fallback if no assistant message with content, maybe use last message?
154
- logger.warning("No assistant messages with content found for 'choices'. Using last message if possible.")
155
- # This part might need refinement based on expected behavior for tool-only responses
156
-
157
- # Estimate token usage (basic approximation)
158
- prompt_tokens = 0
159
- completion_tokens = 0
160
- total_tokens = 0
161
- # Need access to the *input* messages for prompt_tokens, which aren't passed here.
162
- # This usage calculation will be inaccurate without the original prompt.
163
- for msg in messages: # Calculating based on response messages only
164
- if isinstance(msg, dict):
165
- content_tokens = len(str(msg.get("content", "")).split())
166
- if msg.get("role") == "assistant":
167
- completion_tokens += content_tokens
168
- total_tokens += content_tokens
169
- if msg.get("tool_calls"): # Add rough estimate for tool call overhead
170
- total_tokens += len(json.dumps(msg["tool_calls"])) // 4
171
-
172
- logger.warning("Token usage calculation is approximate and based only on response messages.")
173
-
174
- # Basic serialization structure
175
- serialized_response = {
176
- "id": f"swarm-chat-{uuid.uuid4()}",
177
- "object": "chat.completion",
178
- "created": int(time.time()),
179
- "model": model_name,
180
- "choices": choices,
181
- "usage": {
182
- "prompt_tokens": prompt_tokens, # Inaccurate without input messages
183
- "completion_tokens": completion_tokens,
184
- "total_tokens": total_tokens # Inaccurate
185
- },
186
- # Optionally include context and full response for debugging/state
187
- # "context_variables": context_variables,
188
- # "full_response": response # Might contain non-serializable objects
189
- }
190
-
191
- logger.debug(f"Serialized response: id={serialized_response['id']}, choices={len(choices)}")
192
- return serialized_response
193
-
194
- def parse_chat_request(request: Any) -> Any:
195
- """Parse incoming chat completion request body into components."""
196
- try:
197
- body = json.loads(request.body)
198
- model = body.get("model", "default") # Default to 'default' if model not specified
199
- messages = body.get("messages", [])
200
-
201
- # Basic validation
202
- if not isinstance(messages, list) or not messages:
203
- # Try extracting single message if 'messages' is invalid/empty
204
- if "message" in body:
205
- single_msg = body["message"]
206
- if isinstance(single_msg, str): messages = [{"role": "user", "content": single_msg}]
207
- elif isinstance(single_msg, dict) and "content" in single_msg:
208
- if "role" not in single_msg: single_msg["role"] = "user"
209
- messages = [single_msg]
210
- else:
211
- return Response({"error": "'message' field is invalid."}, status=status.HTTP_400_BAD_REQUEST)
212
- else:
213
- return Response({"error": "'messages' field is required and must be a non-empty list."}, status=status.HTTP_400_BAD_REQUEST)
214
-
215
- # Ensure all messages have a role (default to user if missing)
216
- for msg in messages:
217
- if not isinstance(msg, dict) or "content" not in msg:
218
- # Allow tool calls without content
219
- if not (isinstance(msg, dict) and msg.get("role") == "tool" and msg.get("tool_call_id")):
220
- logger.error(f"Invalid message format found: {msg}")
221
- return Response({"error": f"Invalid message format: {msg}"}, status=status.HTTP_400_BAD_REQUEST)
222
- if "role" not in msg:
223
- msg["role"] = "user"
224
-
225
- context_variables = body.get("context_variables", {})
226
- if not isinstance(context_variables, dict):
227
- logger.warning("Invalid 'context_variables' format, using empty dict.")
228
- context_variables = {}
229
-
230
- conversation_id = extract_chat_id(body) or str(uuid.uuid4()) # Generate if not found/extracted
231
-
232
- # Extract last tool_call_id for potential context filtering (optional)
233
- tool_call_id = None
234
- if messages and isinstance(messages[-1], dict) and messages[-1].get("role") == "tool":
235
- tool_call_id = messages[-1].get("tool_call_id")
236
-
237
- logger.debug(f"Parsed request: model={model}, messages_count={len(messages)}, context_keys={list(context_variables.keys())}, conv_id={conversation_id}, tool_id={tool_call_id}")
238
- return (body, model, messages, context_variables, conversation_id, tool_call_id)
239
-
240
- except json.JSONDecodeError:
241
- logger.error("Invalid JSON payload received.")
242
- return Response({"error": "Invalid JSON payload."}, status=status.HTTP_400_BAD_REQUEST)
243
- except Exception as e:
244
- logger.error(f"Error parsing request: {e}", exc_info=True)
245
- return Response({"error": "Failed to parse request."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
246
-
247
-
248
- def get_blueprint_instance(model: str, context_vars: dict) -> Any:
249
- """Instantiate a blueprint instance based on the requested model."""
250
- logger.debug(f"Attempting to get blueprint instance for model: {model}")
251
- # Reload configs and discover blueprints on each request? Or rely on initial load?
252
- # Let's rely on initial load for now, assuming blueprints don't change often.
253
- # discover_and_load_blueprints() # Uncomment if dynamic reload is needed
254
-
255
- blueprint_meta = blueprints_metadata.get(model)
256
- if not blueprint_meta:
257
- # Check if it's an LLM passthrough defined in config
258
- llm_profile = config.get("llm", {}).get(model)
259
- if llm_profile and llm_profile.get("passthrough"):
260
- logger.warning(f"Model '{model}' is an LLM passthrough, not a blueprint. Returning None.")
261
- # This scenario should ideally be handled before calling get_blueprint_instance
262
- # Returning None might cause issues downstream. Consider raising an error
263
- # or having the caller handle LLM passthrough separately.
264
- # For now, returning None as a signal.
265
- return None # Signal it's not a blueprint
266
- else:
267
- logger.error(f"Model '{model}' not found in discovered blueprints or LLM config.")
268
- return Response({"error": f"Model '{model}' not found."}, status=status.HTTP_404_NOT_FOUND)
269
-
270
- blueprint_class = blueprint_meta.get("blueprint_class")
271
- if not blueprint_class or not issubclass(blueprint_class, BlueprintBase):
272
- logger.error(f"Blueprint class for model '{model}' is invalid or not found.")
273
- return Response({"error": f"Blueprint class for model '{model}' is invalid."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
274
-
275
- try:
276
- # Pass the initially loaded global 'config' to the blueprint instance
277
- blueprint_instance = blueprint_class(config=config, debug=settings.DEBUG)
278
- logger.info(f"Successfully instantiated blueprint: {model}")
279
- # Optionally set active agent based on context, if blueprint supports it
280
- # active_agent_name = context_vars.get("active_agent_name")
281
- # if active_agent_name and hasattr(blueprint_instance, 'set_active_agent'):
282
- # try:
283
- # blueprint_instance.set_active_agent(active_agent_name)
284
- # except ValueError as e:
285
- # logger.warning(f"Could not set active agent '{active_agent_name}': {e}")
286
- return blueprint_instance
287
- except Exception as e:
288
- logger.error(f"Error initializing blueprint '{model}': {e}", exc_info=True)
289
- return Response({"error": f"Error initializing blueprint: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
290
-
291
- def load_conversation_history(conversation_id: Optional[str], current_messages: List[dict], tool_call_id: Optional[str] = None) -> List[dict]:
292
- """Load past messages for a conversation from Redis or database, combined with current."""
293
- if not conversation_id:
294
- logger.debug("No conversation_id provided, returning only current messages.")
295
- return current_messages # Return only the messages from the current request
296
-
297
- past_messages = []
298
- # Try Redis first if available
299
- if REDIS_AVAILABLE and redis_client:
300
- try:
301
- history_raw = redis_client.lrange(conversation_id, 0, -1) # Get all items in list
302
- if history_raw:
303
- # Redis stores list items as strings, parse each JSON string
304
- past_messages = [json.loads(msg_str) for msg_str in history_raw]
305
- logger.debug(f"Retrieved {len(past_messages)} messages from Redis list for conversation: {conversation_id}")
306
- except redis.exceptions.RedisError as e:
307
- logger.error(f"Redis error retrieving history for {conversation_id}: {e}", exc_info=True)
308
- except json.JSONDecodeError as e:
309
- logger.error(f"Error decoding JSON from Redis for {conversation_id}: {e}. History may be corrupted.")
310
- # Potentially clear corrupted Redis key?
311
- # redis_client.delete(conversation_id)
312
- except Exception as e:
313
- logger.error(f"Unexpected error retrieving from Redis for {conversation_id}: {e}", exc_info=True)
314
-
315
- # Fallback to Database if Redis fails or history is empty
316
- if not past_messages:
317
- try:
318
- conversation = ChatConversation.objects.filter(conversation_id=conversation_id).first()
319
- if conversation:
320
- # Query messages related to the conversation
321
- query = conversation.messages.all().order_by("timestamp")
322
- # Convert DB messages to the expected dict format
323
- past_messages = [
324
- {"role": msg.sender, "content": msg.content, "tool_calls": json.loads(msg.tool_calls) if msg.tool_calls else None}
325
- for msg in query
326
- ]
327
- logger.debug(f"Retrieved {len(past_messages)} messages from DB for conversation: {conversation_id}")
328
- else:
329
- logger.debug(f"No existing conversation found in DB for ID: {conversation_id}")
330
- past_messages = [] # Ensure it's an empty list if no conversation found
331
- except Exception as e:
332
- logger.error(f"Error retrieving conversation history from DB for {conversation_id}: {e}", exc_info=True)
333
- past_messages = [] # Ensure empty list on error
334
-
335
- # Combine history with current request messages
336
- # Ensure roles are correct ('user' for human, 'assistant'/'tool' for AI/tools)
337
- # Filter out potential duplicates if necessary (e.g., if client resends last user message)
338
- combined_messages = past_messages + current_messages
339
- logger.debug(f"Combined history: {len(past_messages)} past + {len(current_messages)} current = {len(combined_messages)} total messages for {conversation_id}")
340
- return combined_messages
3
+ from asgiref.sync import sync_to_async, async_to_sync
341
4
 
5
+ # Assuming the discovery functions are correctly located now
6
+ from swarm.extensions.blueprint.blueprint_discovery import discover_blueprints
342
7
 
343
- def store_conversation_history(conversation_id: str, full_history: List[dict], response_obj: Optional[Any] = None):
344
- """Store conversation history (including latest response) in DB and/or Redis."""
345
- if not conversation_id:
346
- logger.error("Cannot store history: conversation_id is missing.")
347
- return False
8
+ logger = logging.getLogger(__name__)
348
9
 
349
- # Ensure response messages are included in the history to be stored
350
- history_to_store = list(full_history) # Make a copy
351
- if response_obj:
352
- response_messages = []
353
- if hasattr(response_obj, 'messages') and isinstance(response_obj.messages, list):
354
- response_messages = response_obj.messages
355
- elif isinstance(response_obj, dict) and isinstance(response_obj.get('messages'), list):
356
- response_messages = response_obj.get('messages', [])
10
+ # --- Caching ---
11
+ _blueprint_meta_cache = None # Cache for the {name: class} mapping
12
+ _blueprint_instance_cache = {} # Simple instance cache for no-param blueprints
357
13
 
358
- # Add only messages not already in full_history (prevent duplicates if run_conversation includes input)
359
- last_stored_content = json.dumps(history_to_store[-1]) if history_to_store else None
360
- for msg in response_messages:
361
- msg_dict = msg.model_dump(exclude_none=True) if isinstance(msg, ChatMessage) else msg
362
- if json.dumps(msg_dict) != last_stored_content:
363
- history_to_store.append(msg)
14
+ # --- Blueprint Metadata Loading ---
15
+ def _load_all_blueprint_metadata_sync():
16
+ """Synchronous helper to perform blueprint discovery."""
17
+ global _blueprint_meta_cache
18
+ logger.info("Discovering blueprint classes (sync)...")
19
+ blueprint_classes = discover_blueprints(settings.BLUEPRINT_DIRECTORY)
20
+ logger.info(f"Found blueprint classes: {list(blueprint_classes.keys())}")
21
+ _blueprint_meta_cache = blueprint_classes
22
+ return blueprint_classes
364
23
 
24
+ @sync_to_async
25
+ def get_available_blueprints():
26
+ """Asynchronously retrieves available blueprint classes."""
27
+ global _blueprint_meta_cache
28
+ if _blueprint_meta_cache is None:
29
+ _load_all_blueprint_metadata_sync()
30
+ return _blueprint_meta_cache
365
31
 
366
- # --- Store in Database ---
367
- try:
368
- # Use update_or_create for conversation to handle creation/retrieval atomically
369
- conversation, created = ChatConversation.objects.update_or_create(
370
- conversation_id=conversation_id,
371
- defaults={'user': None} # Add user association if available (e.g., from request.user)
372
- )
373
- if created:
374
- logger.debug(f"Created new ChatConversation in DB: {conversation_id}")
375
-
376
- # Efficiently store only the messages *added* in this turn (response messages)
377
- # Assume `full_history` contains the prompt messages, and `response_messages` contains the response
378
- messages_to_add_to_db = []
379
- if response_obj:
380
- response_msgs_from_obj = getattr(response_obj, 'messages', []) if hasattr(response_obj, 'messages') else response_obj.get('messages', []) if isinstance(response_obj, dict) else []
381
- for msg in response_msgs_from_obj:
382
- if isinstance(msg, dict):
383
- # Basic validation
384
- role = msg.get("role")
385
- content = msg.get("content")
386
- tool_calls = msg.get("tool_calls")
387
- if role and (content is not None or tool_calls is not None):
388
- messages_to_add_to_db.append(ChatMessage(
389
- conversation=conversation,
390
- sender=role,
391
- content=content,
392
- # Store tool_calls as JSON string
393
- tool_calls=json.dumps(tool_calls) if tool_calls else None
394
- ))
32
+ # --- Blueprint Instance Loading ---
33
+ # Removed _load_blueprint_class_sync
395
34
 
396
- if messages_to_add_to_db:
397
- ChatMessage.objects.bulk_create(messages_to_add_to_db)
398
- logger.debug(f"Stored {len(messages_to_add_to_db)} new messages in DB for conversation {conversation_id}")
399
- else:
400
- logger.debug(f"No new response messages to store in DB for conversation {conversation_id}")
401
-
402
- except Exception as e:
403
- logger.error(f"Error storing conversation history to DB for {conversation_id}: {e}", exc_info=True)
404
- # Continue to Redis even if DB fails? Or return False? Let's continue for now.
35
+ async def get_blueprint_instance(blueprint_id: str, params: dict = None):
36
+ """Asynchronously gets an instance of a specific blueprint."""
37
+ logger.debug(f"Getting instance for blueprint: {blueprint_id} with params: {params}")
38
+ cache_key = (blueprint_id, tuple(sorted(params.items())) if isinstance(params, dict) else params)
405
39
 
406
- # --- Store full history in Redis List ---
407
- if REDIS_AVAILABLE and redis_client:
408
- try:
409
- # Use a Redis list (LPUSH/LTRIM or RPUSH/LTRIM for potentially capped history)
410
- # For simplicity, let's replace the entire history for now.
411
- # Delete existing list and push all new items. Use pipeline for atomicity.
412
- pipe = redis_client.pipeline()
413
- pipe.delete(conversation_id)
414
- # Push each message as a separate JSON string onto the list
415
- for msg_dict in history_to_store:
416
- pipe.rpush(conversation_id, json.dumps(msg_dict))
417
- # Optionally cap the list size
418
- # max_redis_history = 100 # Example cap
419
- # pipe.ltrim(conversation_id, -max_redis_history, -1)
420
- pipe.execute()
421
- logger.debug(f"Stored {len(history_to_store)} messages in Redis list for conversation {conversation_id}")
422
- except redis.exceptions.RedisError as e:
423
- logger.error(f"Redis error storing history list for {conversation_id}: {e}", exc_info=True)
424
- return False # Indicate failure if Redis write fails
425
- except Exception as e:
426
- logger.error(f"Unexpected error storing to Redis for {conversation_id}: {e}", exc_info=True)
427
- return False
40
+ if params is None and blueprint_id in _blueprint_instance_cache:
41
+ logger.debug(f"Returning cached instance for {blueprint_id}")
42
+ return _blueprint_instance_cache[blueprint_id]
428
43
 
429
- return True
44
+ available_blueprint_classes = await get_available_blueprints()
430
45
 
46
+ if not isinstance(available_blueprint_classes, dict) or blueprint_id not in available_blueprint_classes:
47
+ logger.error(f"Blueprint ID '{blueprint_id}' not found in available blueprint classes.")
48
+ return None
431
49
 
432
- async def run_conversation(blueprint_instance: Any, messages_extended: List[dict], context_vars: dict) -> Tuple[Any, dict]:
433
- """Run a conversation turn with a blueprint instance asynchronously."""
434
- if not isinstance(blueprint_instance, BlueprintBase):
435
- # Handle LLM passthrough case if needed, or raise error
436
- # For now, assume it must be a BlueprintBase instance
437
- logger.error("run_conversation called with non-blueprint instance.")
438
- raise TypeError("run_conversation expects a BlueprintBase instance.")
50
+ blueprint_class = available_blueprint_classes[blueprint_id]
439
51
 
440
52
  try:
441
- # Directly await the async method
442
- result_dict = await blueprint_instance.run_with_context_async(messages_extended, context_vars)
443
-
444
- response_obj = result_dict.get("response")
445
- updated_context = result_dict.get("context_variables", context_vars) # Fallback to original context
446
-
447
- if response_obj is None:
448
- logger.error("Blueprint run returned None in response object.")
449
- # Create a default error response
450
- response_obj = {"messages": [{"role": "assistant", "content": "Error: Blueprint failed to return a response."}]}
451
-
452
- return response_obj, updated_context
53
+ # *** Instantiate the class WITHOUT the params argument ***
54
+ # If blueprints need params, they should handle it internally
55
+ # or the base class __init__ needs to accept **kwargs.
56
+ instance = blueprint_class()
57
+ logger.info(f"Successfully instantiated blueprint: {blueprint_id}")
58
+ # Optionally pass params later if needed, e.g., instance.set_params(params) if such a method exists
59
+ if hasattr(instance, 'set_params') and callable(getattr(instance, 'set_params')):
60
+ instance.set_params(params) # Example of setting params after init
61
+
62
+ if params is None:
63
+ _blueprint_instance_cache[blueprint_id] = instance
64
+ return instance
453
65
  except Exception as e:
454
- logger.error(f"Exception during blueprint run: {e}", exc_info=True)
455
- # Return an error structure
456
- error_response = {"messages": [{"role": "assistant", "content": f"Error processing request: {e}"}]}
457
- return error_response, context_vars # Return original context on error
66
+ # Catch potential TypeError during instantiation too
67
+ logger.error(f"Failed to instantiate blueprint class '{blueprint_id}': {e}", exc_info=True)
68
+ return None
69
+
70
+ # --- Model Access Validation ---
71
+ def validate_model_access(user, model_name):
72
+ """Synchronous permission check."""
73
+ logger.debug(f"Validating access for user '{user}' to model '{model_name}'...")
74
+ try:
75
+ available = async_to_sync(get_available_blueprints)()
76
+ is_available = model_name in available
77
+ logger.debug(f"Model '{model_name}' availability: {is_available}")
78
+ return is_available
79
+ except Exception as e:
80
+ logger.error(f"Error checking model availability during validation: {e}", exc_info=True)
81
+ return False