open-swarm 0.1.1743070217__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. open_swarm-0.1.1743070217.dist-info/METADATA +258 -0
  2. open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
  3. open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
  4. open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
  5. open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
  6. open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
  7. swarm/__init__.py +3 -0
  8. swarm/agent/__init__.py +7 -0
  9. swarm/agent/agent.py +49 -0
  10. swarm/apps.py +53 -0
  11. swarm/auth.py +56 -0
  12. swarm/consumers.py +141 -0
  13. swarm/core.py +326 -0
  14. swarm/extensions/__init__.py +1 -0
  15. swarm/extensions/blueprint/__init__.py +36 -0
  16. swarm/extensions/blueprint/agent_utils.py +45 -0
  17. swarm/extensions/blueprint/blueprint_base.py +562 -0
  18. swarm/extensions/blueprint/blueprint_discovery.py +112 -0
  19. swarm/extensions/blueprint/blueprint_utils.py +17 -0
  20. swarm/extensions/blueprint/common_utils.py +12 -0
  21. swarm/extensions/blueprint/django_utils.py +203 -0
  22. swarm/extensions/blueprint/interactive_mode.py +102 -0
  23. swarm/extensions/blueprint/modes/rest_mode.py +37 -0
  24. swarm/extensions/blueprint/output_utils.py +95 -0
  25. swarm/extensions/blueprint/spinner.py +91 -0
  26. swarm/extensions/cli/__init__.py +0 -0
  27. swarm/extensions/cli/blueprint_runner.py +251 -0
  28. swarm/extensions/cli/cli_args.py +88 -0
  29. swarm/extensions/cli/commands/__init__.py +0 -0
  30. swarm/extensions/cli/commands/blueprint_management.py +31 -0
  31. swarm/extensions/cli/commands/config_management.py +15 -0
  32. swarm/extensions/cli/commands/edit_config.py +77 -0
  33. swarm/extensions/cli/commands/list_blueprints.py +22 -0
  34. swarm/extensions/cli/commands/validate_env.py +57 -0
  35. swarm/extensions/cli/commands/validate_envvars.py +39 -0
  36. swarm/extensions/cli/interactive_shell.py +41 -0
  37. swarm/extensions/cli/main.py +36 -0
  38. swarm/extensions/cli/selection.py +43 -0
  39. swarm/extensions/cli/utils/discover_commands.py +32 -0
  40. swarm/extensions/cli/utils/env_setup.py +15 -0
  41. swarm/extensions/cli/utils.py +105 -0
  42. swarm/extensions/config/__init__.py +6 -0
  43. swarm/extensions/config/config_loader.py +208 -0
  44. swarm/extensions/config/config_manager.py +258 -0
  45. swarm/extensions/config/server_config.py +49 -0
  46. swarm/extensions/config/setup_wizard.py +103 -0
  47. swarm/extensions/config/utils/__init__.py +0 -0
  48. swarm/extensions/config/utils/logger.py +36 -0
  49. swarm/extensions/launchers/__init__.py +1 -0
  50. swarm/extensions/launchers/build_launchers.py +14 -0
  51. swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
  52. swarm/extensions/launchers/swarm_api.py +68 -0
  53. swarm/extensions/launchers/swarm_cli.py +304 -0
  54. swarm/extensions/launchers/swarm_wrapper.py +29 -0
  55. swarm/extensions/mcp/__init__.py +1 -0
  56. swarm/extensions/mcp/cache_utils.py +36 -0
  57. swarm/extensions/mcp/mcp_client.py +341 -0
  58. swarm/extensions/mcp/mcp_constants.py +7 -0
  59. swarm/extensions/mcp/mcp_tool_provider.py +110 -0
  60. swarm/llm/chat_completion.py +195 -0
  61. swarm/messages.py +132 -0
  62. swarm/migrations/0010_initial_chat_models.py +51 -0
  63. swarm/migrations/__init__.py +0 -0
  64. swarm/models.py +45 -0
  65. swarm/repl/__init__.py +1 -0
  66. swarm/repl/repl.py +87 -0
  67. swarm/serializers.py +12 -0
  68. swarm/settings.py +189 -0
  69. swarm/tool_executor.py +239 -0
  70. swarm/types.py +126 -0
  71. swarm/urls.py +89 -0
  72. swarm/util.py +124 -0
  73. swarm/utils/color_utils.py +40 -0
  74. swarm/utils/context_utils.py +272 -0
  75. swarm/utils/general_utils.py +162 -0
  76. swarm/utils/logger.py +61 -0
  77. swarm/utils/logger_setup.py +25 -0
  78. swarm/utils/message_sequence.py +173 -0
  79. swarm/utils/message_utils.py +95 -0
  80. swarm/utils/redact.py +68 -0
  81. swarm/views/__init__.py +41 -0
  82. swarm/views/api_views.py +46 -0
  83. swarm/views/chat_views.py +76 -0
  84. swarm/views/core_views.py +118 -0
  85. swarm/views/message_views.py +40 -0
  86. swarm/views/model_views.py +135 -0
  87. swarm/views/utils.py +457 -0
  88. swarm/views/web_views.py +149 -0
  89. swarm/wsgi.py +16 -0
swarm/tool_executor.py ADDED
@@ -0,0 +1,239 @@
1
+ """
2
+ Tool execution utilities for the Swarm framework.
3
+ Handles invoking agent functions/tools based on LLM requests.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import inspect # To check for awaitables
9
+ from typing import List, Dict, Any, Optional, Union
10
+
11
+ # Import necessary types from the Swarm framework
12
+ from .types import (
13
+ ChatCompletionMessageToolCall,
14
+ Agent,
15
+ AgentFunction, # Type hint for functions/tools
16
+ Response, # Structure for returning results of multiple tool calls
17
+ Result # Structure for returning result of a single tool call
18
+ )
19
+ # Utility to convert function signatures to JSON schema (if needed, though less common now with direct calls)
20
+ # from .util import function_to_json # Commented out if not used directly here
21
+
22
+ # Configure module-level logging
23
+ logger = logging.getLogger(__name__)
24
+ # logger.setLevel(logging.DEBUG) # Uncomment for verbose logging
25
+ if not logger.handlers:
26
+ stream_handler = logging.StreamHandler()
27
+ formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s")
28
+ stream_handler.setFormatter(formatter)
29
+ logger.addHandler(stream_handler)
30
+
31
+ # Standard name used for injecting context variables into tool calls
32
+ __CTX_VARS_NAME__ = "context_variables"
33
+
34
+
35
+ def handle_function_result(result: Any, debug: bool) -> Result:
36
+ """
37
+ Process the raw result returned by an agent function/tool into a standardized Result object.
38
+ Handles agent handoffs if the result is an Agent instance.
39
+
40
+ Args:
41
+ result: The raw return value from the executed function/tool.
42
+ debug: If True, log detailed information about the result processing.
43
+
44
+ Returns:
45
+ Result: A standardized Result object containing the processed value,
46
+ potential agent handoff, and context variable updates.
47
+
48
+ Raises:
49
+ TypeError: If the raw result cannot be cast to a string for the Result value.
50
+ """
51
+ if debug:
52
+ # Log raw result type and a preview (truncated for brevity)
53
+ try:
54
+ result_preview = str(result)[:100] + ('...' if len(str(result)) > 100 else '')
55
+ except Exception:
56
+ result_preview = "[Could not convert result to string for preview]"
57
+ logger.debug(f"Processing function result. Type: {type(result)}, Preview: {result_preview}")
58
+
59
+ # Check if the result is already a Result object
60
+ if isinstance(result, Result):
61
+ if debug: logger.debug("Result is already a Result object. Returning as is.")
62
+ return result
63
+ # Check if the result indicates an agent handoff
64
+ elif isinstance(result, Agent):
65
+ agent_name = getattr(result, 'name', 'UnnamedAgent')
66
+ if debug: logger.debug(f"Result is an Agent handoff to: '{agent_name}'")
67
+ # Create a Result object indicating the handoff
68
+ # The 'value' might represent the confirmation or status of the handoff itself
69
+ return Result(value=json.dumps({"status": f"Handoff to agent {agent_name} initiated."}), agent=result)
70
+ # Handle other types (attempt to serialize to string)
71
+ else:
72
+ try:
73
+ # Convert the result to a JSON string if possible, otherwise just stringify
74
+ # JSON is generally preferred for structured tool responses
75
+ if isinstance(result, (dict, list, tuple)):
76
+ result_str = json.dumps(result)
77
+ else:
78
+ result_str = str(result)
79
+
80
+ if debug: logger.debug(f"Converted result to string/JSON: {result_str[:100]}{'...' if len(result_str) > 100 else ''}")
81
+ # Return a Result object with the stringified value
82
+ return Result(value=result_str)
83
+ except (TypeError, ValueError) as e:
84
+ logger.error(f"Failed to serialize or cast function result to string/JSON: {e}", exc_info=debug)
85
+ # Raise a TypeError if conversion fails, indicating an issue with the tool's return type
86
+ raise TypeError(f"Tool function returned a result of type {type(result)} that could not be serialized to string/JSON: {result}") from e
87
+
88
+
89
+ async def handle_tool_calls(
90
+ tool_calls: List[ChatCompletionMessageToolCall], # Expect list of Pydantic models
91
+ functions: List[AgentFunction], # Available functions/tools for the agent
92
+ context_variables: dict, # Current context
93
+ debug: bool # Debug logging flag
94
+ ) -> Response:
95
+ """
96
+ Execute a list of tool calls requested by the LLM and aggregate their results.
97
+
98
+ Args:
99
+ tool_calls: A list of ChatCompletionMessageToolCall objects requested by the LLM.
100
+ functions: A list of available functions/tools (callables or dicts) for the current agent.
101
+ context_variables: A dictionary containing the current context variables.
102
+ debug: If True, enable detailed debugging logs.
103
+
104
+ Returns:
105
+ Response: An object containing a list of messages (tool results) to be added
106
+ to the conversation history, the potentially changed agent (due to handoff),
107
+ and any updates to context variables from the tool calls.
108
+ """
109
+ # Basic validation of input
110
+ if not tool_calls or not isinstance(tool_calls, list):
111
+ logger.debug("No valid tool calls provided to handle_tool_calls.")
112
+ # Return an empty Response if there's nothing to process
113
+ return Response(messages=[], agent=None, context_variables={})
114
+
115
+ logger.debug(f"Handling {len(tool_calls)} tool calls.")
116
+
117
+ # Create a mapping from function/tool name to the actual callable object
118
+ function_map: Dict[str, AgentFunction] = {}
119
+ for func in functions:
120
+ # Get name robustly (prefer 'name' attribute, fallback to __name__)
121
+ func_name = getattr(func, 'name', getattr(func, '__name__', None))
122
+ if func_name:
123
+ if func_name in function_map:
124
+ logger.warning(f"Duplicate function/tool name '{func_name}' detected. Overwriting previous entry.")
125
+ function_map[func_name] = func
126
+ else:
127
+ logger.warning(f"Available function/tool object {func} is missing a valid name. Skipping.")
128
+
129
+ # Initialize Response object to aggregate results
130
+ aggregated_response = Response(messages=[], agent=None, context_variables={})
131
+
132
+ # Process each requested tool call
133
+ for tool_call in tool_calls:
134
+ # Ensure it's the expected Pydantic model type
135
+ if not isinstance(tool_call, ChatCompletionMessageToolCall):
136
+ logger.warning(f"Skipping invalid item in tool_calls list: Expected ChatCompletionMessageToolCall, got {type(tool_call)}.")
137
+ continue
138
+
139
+ # Extract necessary info from the tool call object
140
+ tool_name = getattr(tool_call.function, 'name', None)
141
+ tool_call_id = getattr(tool_call, 'id', None)
142
+ raw_arguments = getattr(tool_call.function, 'arguments', '{}') # Default to empty JSON object string
143
+
144
+ # Validate essential components
145
+ if not tool_name or not tool_call_id:
146
+ logger.error(f"Invalid tool call data: Missing name ('{tool_name}') or id ('{tool_call_id}'). Skipping.")
147
+ # Optionally add an error message to the response
148
+ aggregated_response.messages.append({
149
+ "role": "tool", "tool_call_id": tool_call_id or "missing_id", "name": tool_name or "missing_name",
150
+ "content": json.dumps({"error": "Invalid tool call data received from LLM."})
151
+ })
152
+ continue
153
+
154
+ # Find the corresponding function/tool in the map
155
+ func_to_call = function_map.get(tool_name)
156
+ if not func_to_call:
157
+ logger.error(f"Tool '{tool_name}' requested by LLM (ID: '{tool_call_id}') not found in agent's available functions.")
158
+ # Add error message to history
159
+ aggregated_response.messages.append({
160
+ "role": "tool", "tool_call_id": tool_call_id, "name": tool_name,
161
+ "content": json.dumps({"error": f"Tool '{tool_name}' is not available."}) # Use JSON for content
162
+ })
163
+ continue
164
+
165
+ # Parse arguments string into a dictionary
166
+ try:
167
+ args: Dict[str, Any] = json.loads(raw_arguments)
168
+ if not isinstance(args, dict):
169
+ logger.warning(f"Parsed arguments for tool '{tool_name}' is not a dictionary ({type(args)}). Using empty dict.")
170
+ args = {}
171
+ except json.JSONDecodeError as e:
172
+ logger.error(f"Failed to parse JSON arguments for tool '{tool_name}' (ID: '{tool_call_id}'): {e}. Raw args: '{raw_arguments}'. Using empty dict.")
173
+ args = {}
174
+
175
+ # Inject context variables if the function expects them
176
+ try:
177
+ sig = inspect.signature(func_to_call)
178
+ if __CTX_VARS_NAME__ in sig.parameters:
179
+ args[__CTX_VARS_NAME__] = context_variables
180
+ if debug: logger.debug(f"Injecting context variables into tool '{tool_name}'.")
181
+ except (ValueError, TypeError) as e:
182
+ # Handle cases where signature cannot be inspected (e.g., built-ins)
183
+ logger.warning(f"Could not inspect signature for tool '{tool_name}': {e}. Cannot inject context automatically.")
184
+
185
+
186
+ # --- Execute the function/tool ---
187
+ try:
188
+ logger.info(f"Executing tool '{tool_name}' (ID: '{tool_call_id}') with args: {redact_sensitive_data(args)}")
189
+ # Execute the function with parsed arguments
190
+ raw_result = func_to_call(**args)
191
+
192
+ # Handle asynchronous functions/tools if necessary
193
+ if inspect.isawaitable(raw_result):
194
+ if debug: logger.debug(f"Awaiting async result for tool '{tool_name}' (ID: '{tool_call_id}')")
195
+ raw_result = await raw_result
196
+ # else: (sync function executed directly)
197
+
198
+ # Process the raw result (handles handoffs, serialization)
199
+ processed_result: Result = handle_function_result(raw_result, debug)
200
+
201
+ # Add the processed result message to the response
202
+ # Ensure content is a JSON string as expected by OpenAI 'tool' role message
203
+ result_content_json = processed_result.value if isinstance(processed_result.value, str) else json.dumps(processed_result.value)
204
+ aggregated_response.messages.append({
205
+ "role": "tool",
206
+ "tool_call_id": tool_call_id,
207
+ "name": tool_name,
208
+ "content": result_content_json
209
+ })
210
+
211
+ # Update context variables from the result
212
+ if processed_result.context_variables:
213
+ aggregated_response.context_variables.update(processed_result.context_variables)
214
+ if debug: logger.debug(f"Updated context variables from tool '{tool_name}': {processed_result.context_variables.keys()}")
215
+
216
+ # Handle potential agent handoff indicated by the result
217
+ if processed_result.agent:
218
+ # If multiple tool calls try to handoff, the last one 'wins' here
219
+ if aggregated_response.agent and aggregated_response.agent != processed_result.agent:
220
+ logger.warning(f"Multiple agent handoffs detected in one turn. Last handoff to '{getattr(processed_result.agent, 'name', 'UnnamedAgent')}' takes precedence.")
221
+ aggregated_response.agent = processed_result.agent
222
+ # Update context immediately for subsequent steps within this turn if needed
223
+ context_variables["active_agent_name"] = getattr(processed_result.agent, 'name', None)
224
+ logger.debug(f"Agent handoff triggered by tool '{tool_name}' to agent '{context_variables['active_agent_name']}'.")
225
+
226
+ except Exception as e:
227
+ # Catch errors during function execution
228
+ logger.error(f"Error executing tool '{tool_name}' (ID: '{tool_call_id}'): {e}", exc_info=debug)
229
+ # Add error message to the response history
230
+ aggregated_response.messages.append({
231
+ "role": "tool",
232
+ "tool_call_id": tool_call_id,
233
+ "name": tool_name,
234
+ "content": json.dumps({"error": f"Execution failed: {str(e)}"}) # Provide error in JSON content
235
+ })
236
+
237
+ # Return the aggregated response containing all tool result messages and potential updates
238
+ logger.debug(f"Finished handling tool calls. {len(aggregated_response.messages)} result messages generated.")
239
+ return aggregated_response
swarm/types.py ADDED
@@ -0,0 +1,126 @@
1
+ from openai.types.chat import ChatCompletionMessage
2
+ from openai.types.chat.chat_completion_message_tool_call import (
3
+ ChatCompletionMessageToolCall,
4
+ Function as OpenAIFunction, # Renamed to avoid clash
5
+ )
6
+ from typing import List, Callable, Union, Optional, Dict, Any
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+ from pydantic_settings import BaseSettings, SettingsConfigDict
10
+ import uuid
11
+ from enum import Enum
12
+
13
+ # --- Pydantic Settings for Swarm Core ---
14
+ class LogFormat(str, Enum):
15
+ standard = "[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s"
16
+ simple = "[%(levelname)s] %(name)s - %(message)s"
17
+
18
+ class Settings(BaseSettings):
19
+ model_config = SettingsConfigDict(env_prefix='SWARM_', case_sensitive=False)
20
+
21
+ log_level: str = Field(default='INFO', description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")
22
+ log_format: LogFormat = Field(default=LogFormat.standard, description="Logging format")
23
+ debug: bool = Field(default=False, description="Global debug flag")
24
+
25
+ # --- LLMConfig ---
26
+ class LLMConfig(BaseModel):
27
+ """Configuration for a specific LLM profile."""
28
+ provider: Optional[str] = "openai"
29
+ model: Optional[str] = None
30
+ api_key: Optional[str] = None
31
+ base_url: Optional[str] = None
32
+ max_tokens: Optional[int] = None # Max tokens supported by model
33
+ temperature: Optional[float] = 0.7
34
+ cost: Optional[float] = None
35
+ speed: Optional[float] = None
36
+ intelligence: Optional[float] = None
37
+ passthrough: Optional[bool] = False
38
+
39
+ model_config = ConfigDict(extra='allow')
40
+
41
+ # --- Moved Tool Types Definition Higher ---
42
+ class ToolFunction(BaseModel):
43
+ name: str
44
+ arguments: str # Should be a JSON string
45
+
46
+ class ToolCall(BaseModel):
47
+ id: str
48
+ type: str = "function"
49
+ function: ToolFunction
50
+
51
+ class ToolResult(BaseModel):
52
+ tool_call_id: str
53
+ role: str = "tool" # Added role for consistency
54
+ name: Optional[str] = None # Name of the function that was called
55
+ content: str
56
+ # --- End Tool Types ---
57
+
58
+ # AgentFunction needs Agent defined, so keep it below Agent
59
+ # AgentFunction = Callable[[], Union[str, "Agent", dict]]
60
+ AgentFunction = Callable[..., Union[str, "Agent", dict]]
61
+
62
+ class Agent(BaseModel):
63
+ name: str = "Agent"
64
+ model: str = "default" # LLM profile name to use
65
+ instructions: Union[str, Callable[[], str]] = "You are a helpful agent."
66
+ functions: List[AgentFunction] = []
67
+ resources: List[Dict[str, Any]] = []
68
+ tool_choice: Optional[str] = None
69
+ parallel_tool_calls: bool = False
70
+ mcp_servers: Optional[List[str]] = None
71
+ env_vars: Optional[Dict[str, str]] = None
72
+ response_format: Optional[Dict[str, Any]] = None
73
+
74
+ # --- ChatMessage Definition (Now ToolCall is defined) ---
75
+ class ChatMessage(BaseModel):
76
+ """Represents a message in the chat history, potentially with tool calls."""
77
+ role: str
78
+ content: Optional[str] = None
79
+ tool_calls: Optional[List[ToolCall]] = None
80
+ tool_call_id: Optional[str] = None # For tool results
81
+ name: Optional[str] = None # For tool results or function name
82
+ sender: Optional[str] = None # Track the agent sending the message
83
+
84
+ model_config = ConfigDict(extra="allow")
85
+ # --- End ChatMessage ---
86
+
87
+ class Response(BaseModel):
88
+ id: Optional[str] = Field(default_factory=lambda: f"response-{uuid.uuid4()}")
89
+ messages: List[ChatMessage] = [] # Use ChatMessage type hint
90
+ agent: Optional[Agent] = None
91
+ context_variables: dict = {}
92
+
93
+ class Result(BaseModel):
94
+ """
95
+ Encapsulates the possible return values for an agent function.
96
+ """
97
+ value: str = ""
98
+ agent: Optional[Agent] = None
99
+ context_variables: dict = {}
100
+
101
+ # Re-defined Tool class
102
+ class Tool:
103
+ def __init__(
104
+ self,
105
+ name: str,
106
+ func: Callable,
107
+ description: str = "",
108
+ input_schema: Optional[Dict[str, Any]] = None,
109
+ dynamic: bool = False,
110
+ ):
111
+ self.name = name
112
+ self.func = func
113
+ self.description = description
114
+ self.input_schema = input_schema or {"type": "object", "properties": {}} # Default schema
115
+ self.dynamic = dynamic
116
+
117
+ @property
118
+ def __name__(self): return self.name
119
+ @property
120
+ def __code__(self): return getattr(self.func, "__code__", None)
121
+ def __call__(self, *args, **kwargs): return self.func(*args, **kwargs)
122
+
123
+ # Type alias for tool definitions used in discovery
124
+ ToolDefinition = Dict[str, Any] # A dictionary representing a tool's schema
125
+ Resource = Dict[str, Any] # A dictionary representing a resource
126
+
swarm/urls.py ADDED
@@ -0,0 +1,89 @@
1
+ from django.contrib import admin
2
+ from django.urls import path, re_path, include
3
+ from django.http import HttpResponse
4
+ from django.conf import settings
5
+ from django.conf.urls.static import static
6
+ import os
7
+ import logging
8
+
9
+ # Import specific views from their modules
10
+ from swarm.views.core_views import index as core_index_view, serve_swarm_config, custom_login
11
+ from swarm.views.chat_views import chat_completions
12
+ from swarm.views.model_views import list_models
13
+ from swarm.views.message_views import ChatMessageViewSet
14
+ from drf_spectacular.views import SpectacularSwaggerView, SpectacularAPIView as HiddenSpectacularAPIView
15
+ from rest_framework.routers import DefaultRouter
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ def favicon(request):
20
+ favicon_path = settings.BASE_DIR / 'assets' / 'images' / 'favicon.ico'
21
+ try:
22
+ with open(favicon_path, 'rb') as f:
23
+ favicon_data = f.read()
24
+ return HttpResponse(favicon_data, content_type="image/x-icon")
25
+ except FileNotFoundError:
26
+ logger.warning("Favicon not found.")
27
+ return HttpResponse(status=404)
28
+
29
+ ENABLE_ADMIN = os.getenv("ENABLE_ADMIN", "false").lower() in ("true", "1", "t")
30
+ ENABLE_WEBUI = os.getenv("ENABLE_WEBUI", "true").lower() in ("true", "1", "t")
31
+
32
+ logger.debug(f"ENABLE_WEBUI={'true' if ENABLE_WEBUI else 'false'}")
33
+ logger.debug(f"ENABLE_ADMIN={'true' if ENABLE_ADMIN else 'false'}")
34
+
35
+ router = DefaultRouter()
36
+ # Ensure ChatMessageViewSet is available before registering
37
+ if ChatMessageViewSet:
38
+ router.register(r'v1/chat/messages', ChatMessageViewSet, basename='chatmessage')
39
+ else:
40
+ logger.warning("ChatMessageViewSet not imported correctly, skipping API registration.")
41
+
42
+ # Base URL patterns required by Swarm core
43
+ # Use the imported view functions directly
44
+ base_urlpatterns = [
45
+ re_path(r'^health/?$', lambda request: HttpResponse("OK"), name='health_check'),
46
+ re_path(r'^v1/chat/completions/?$', chat_completions, name='chat_completions'),
47
+ re_path(r'^v1/models/?$', list_models, name='list_models'),
48
+ re_path(r'^schema/?$', HiddenSpectacularAPIView.as_view(), name='schema'),
49
+ re_path(r'^swagger-ui/?$', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
50
+ ]
51
+
52
+ # Optional Admin URLs
53
+ admin_urlpatterns = [path('admin/', admin.site.urls)] if ENABLE_ADMIN else []
54
+
55
+ # Optional Web UI URLs
56
+ webui_urlpatterns = []
57
+ if ENABLE_WEBUI:
58
+ webui_urlpatterns = [
59
+ path('', core_index_view, name='index'),
60
+ path('favicon.ico', favicon, name='favicon'),
61
+ path('config/swarm_config.json', serve_swarm_config, name='serve_swarm_config'),
62
+ path('accounts/login/', custom_login, name='custom_login'),
63
+ ]
64
+ if settings.DEBUG:
65
+ if settings.STATIC_URL and settings.STATIC_ROOT:
66
+ webui_urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
67
+ else:
68
+ logger.warning("STATIC_URL or STATIC_ROOT not configured, static files may not serve correctly in DEBUG mode.")
69
+
70
+ # --- Blueprint URLs are now added dynamically via blueprint_base.py -> django_utils.py ---
71
+ blueprint_urlpatterns = [] # Start with empty list, populated by utils
72
+
73
+ # Combine all URL patterns
74
+ urlpatterns = webui_urlpatterns + admin_urlpatterns + base_urlpatterns + blueprint_urlpatterns + router.urls
75
+
76
+ # Log final URL patterns (consider moving this to where patterns are finalized if issues persist)
77
+ if settings.DEBUG:
78
+ try:
79
+ from django.urls import get_resolver
80
+ # Note: get_resolver(None) might not reflect dynamically added URLs perfectly here.
81
+ # Logging within django_utils might be more accurate for dynamic additions.
82
+ final_patterns = get_resolver(None).url_patterns
83
+ logger.debug(f"Initial resolved URL patterns ({len(final_patterns)} total):")
84
+ # for pattern in final_patterns:
85
+ # try: pattern_repr = str(pattern)
86
+ # except: pattern_repr = f"[Pattern for {getattr(pattern, 'name', 'unnamed')}]"
87
+ # logger.debug(f" {pattern_repr}")
88
+ except Exception as e:
89
+ logger.error(f"Could not log initial URL patterns: {e}")
swarm/util.py ADDED
@@ -0,0 +1,124 @@
1
+ """
2
+ Utility functions for the Swarm framework.
3
+
4
+ This module provides helper functions for serializing functions/tools to JSON and merging streaming response chunks,
5
+ ensuring compatibility with OpenAI API requirements and robust handling of agent interactions.
6
+ """
7
+
8
+ import inspect
9
+ import json
10
+ from datetime import datetime
11
+ from .types import Tool # Adjust import as needed if 'Tool' is in a different location
12
+
13
+
14
+ def merge_fields(target: dict, source: dict) -> None:
15
+ """
16
+ Recursively merge fields from source into target, appending strings and updating nested dictionaries.
17
+
18
+ Args:
19
+ target (dict): The dictionary to update.
20
+ source (dict): The dictionary with new values to merge.
21
+ """
22
+ for key, value in source.items():
23
+ if isinstance(value, str):
24
+ target[key] = target.get(key, "") + value
25
+ elif value is not None and isinstance(value, dict):
26
+ if key not in target:
27
+ target[key] = {}
28
+ merge_fields(target[key], value)
29
+
30
+
31
+ def merge_chunk(final_response: dict, delta: dict) -> None:
32
+ """
33
+ Merge a delta update into a response dictionary, handling tool calls and content incrementally.
34
+
35
+ Args:
36
+ final_response (dict): The cumulative response being built.
37
+ delta (dict): The delta update from a streaming response.
38
+ """
39
+ delta.pop("role", None)
40
+ merge_fields(final_response, delta)
41
+
42
+ tool_calls = delta.get("tool_calls")
43
+ if tool_calls and len(tool_calls) > 0:
44
+ index = tool_calls[0].pop("index", 0)
45
+ if "tool_calls" not in final_response:
46
+ final_response["tool_calls"] = {}
47
+ if index not in final_response["tool_calls"]:
48
+ final_response["tool_calls"][index] = {"function": {"arguments": "", "name": ""}, "id": "", "type": ""}
49
+ merge_fields(final_response["tool_calls"][index], tool_calls[0])
50
+
51
+
52
+ def function_to_json(func, truncate_desc: bool = False) -> dict:
53
+ """
54
+ Convert a Python callable or Tool instance to a JSON-serializable dictionary for OpenAI API.
55
+
56
+ Supports both reflection-based serialization for raw functions and schema-based serialization for Tool objects.
57
+ Optionally truncates descriptions to 1024 characters to meet API limits.
58
+
59
+ Args:
60
+ func: The function or Tool object to serialize.
61
+ truncate_desc (bool): If True, truncate the description to 1024 characters.
62
+
63
+ Returns:
64
+ dict: A dictionary with 'type', 'function', 'name', 'description', and 'parameters'.
65
+
66
+ Raises:
67
+ ValueError: If function signature cannot be inspected (for raw functions).
68
+ """
69
+ # Handle Tool instances from MCP servers
70
+ if isinstance(func, Tool):
71
+ name = func.name
72
+ description = func.description or ""
73
+ tool_schema = func.input_schema or {}
74
+ final_type = tool_schema.get("type", "object")
75
+ final_properties = tool_schema.get("properties", {})
76
+ final_required = tool_schema.get("required", [])
77
+ # Handle raw Python callables via reflection
78
+ else:
79
+ try:
80
+ signature = inspect.signature(func)
81
+ except ValueError as e:
82
+ raise ValueError(f"Failed to get signature for function {func.__name__}: {str(e)}")
83
+
84
+ name = getattr(func, "__name__", "unnamed_function")
85
+ description = (func.__doc__ or "").strip() or f"Calls {name}"
86
+ type_map = {
87
+ str: "string",
88
+ int: "integer",
89
+ float: "number",
90
+ bool: "boolean",
91
+ list: "array",
92
+ dict: "object",
93
+ type(None): "null",
94
+ }
95
+ parameters = {}
96
+ required = []
97
+ for param in signature.parameters.values():
98
+ ann = param.annotation if param.annotation != inspect.Parameter.empty else str
99
+ param_type = type_map.get(ann, "string")
100
+ parameters[param.name] = {"type": param_type}
101
+ if param.default == inspect.Parameter.empty:
102
+ required.append(param.name)
103
+
104
+ final_type = "object"
105
+ final_properties = parameters
106
+ final_required = required
107
+
108
+ # Truncate description if requested
109
+ if truncate_desc and len(description) > 1024:
110
+ description = description[:1024]
111
+ # logger.debug(f"Truncated description for '{name}': {len(description)} -> 1024 characters")
112
+
113
+ return {
114
+ "type": "function",
115
+ "function": {
116
+ "name": name,
117
+ "description": description,
118
+ "parameters": {
119
+ "type": final_type,
120
+ "properties": final_properties,
121
+ "required": final_required,
122
+ },
123
+ },
124
+ }
@@ -0,0 +1,40 @@
1
+ # src/swarm/utils/color_utils.py
2
+
3
+ from colorama import Fore, Style, init as colorama_init
4
+
5
+ def initialize_colorama():
6
+ """
7
+ Initialize colorama for colored terminal outputs.
8
+
9
+ This function should be called at the start of your application to ensure
10
+ that ANSI color codes are interpreted correctly across different platforms.
11
+ """
12
+ colorama_init(autoreset=True)
13
+
14
+ def color_text(text: str, color: str) -> str:
15
+ """
16
+ Return the text string wrapped in the specified color codes.
17
+
18
+ Args:
19
+ text (str): The text to color.
20
+ color (str): The color name. Supported colors: red, green, yellow, blue, magenta, cyan, white.
21
+
22
+ Returns:
23
+ str: Colored text string.
24
+
25
+ Example:
26
+ >>> print(color_text("Hello, World!", "green"))
27
+ Hello, World! # (in green color)
28
+ """
29
+ color_mapping = {
30
+ "red": Fore.RED,
31
+ "green": Fore.GREEN,
32
+ "yellow": Fore.YELLOW,
33
+ "blue": Fore.BLUE,
34
+ "magenta": Fore.MAGENTA,
35
+ "cyan": Fore.CYAN,
36
+ "white": Fore.WHITE,
37
+ }
38
+
39
+ color_code = color_mapping.get(color.lower(), Fore.WHITE)
40
+ return f"{color_code}{text}{Style.RESET_ALL}"