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.
- open_swarm-0.1.1743070217.dist-info/METADATA +258 -0
- open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
- open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
- open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
- swarm/__init__.py +3 -0
- swarm/agent/__init__.py +7 -0
- swarm/agent/agent.py +49 -0
- swarm/apps.py +53 -0
- swarm/auth.py +56 -0
- swarm/consumers.py +141 -0
- swarm/core.py +326 -0
- swarm/extensions/__init__.py +1 -0
- swarm/extensions/blueprint/__init__.py +36 -0
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/blueprint_utils.py +17 -0
- swarm/extensions/blueprint/common_utils.py +12 -0
- swarm/extensions/blueprint/django_utils.py +203 -0
- swarm/extensions/blueprint/interactive_mode.py +102 -0
- swarm/extensions/blueprint/modes/rest_mode.py +37 -0
- swarm/extensions/blueprint/output_utils.py +95 -0
- swarm/extensions/blueprint/spinner.py +91 -0
- swarm/extensions/cli/__init__.py +0 -0
- swarm/extensions/cli/blueprint_runner.py +251 -0
- swarm/extensions/cli/cli_args.py +88 -0
- swarm/extensions/cli/commands/__init__.py +0 -0
- swarm/extensions/cli/commands/blueprint_management.py +31 -0
- swarm/extensions/cli/commands/config_management.py +15 -0
- swarm/extensions/cli/commands/edit_config.py +77 -0
- swarm/extensions/cli/commands/list_blueprints.py +22 -0
- swarm/extensions/cli/commands/validate_env.py +57 -0
- swarm/extensions/cli/commands/validate_envvars.py +39 -0
- swarm/extensions/cli/interactive_shell.py +41 -0
- swarm/extensions/cli/main.py +36 -0
- swarm/extensions/cli/selection.py +43 -0
- swarm/extensions/cli/utils/discover_commands.py +32 -0
- swarm/extensions/cli/utils/env_setup.py +15 -0
- swarm/extensions/cli/utils.py +105 -0
- swarm/extensions/config/__init__.py +6 -0
- swarm/extensions/config/config_loader.py +208 -0
- swarm/extensions/config/config_manager.py +258 -0
- swarm/extensions/config/server_config.py +49 -0
- swarm/extensions/config/setup_wizard.py +103 -0
- swarm/extensions/config/utils/__init__.py +0 -0
- swarm/extensions/config/utils/logger.py +36 -0
- swarm/extensions/launchers/__init__.py +1 -0
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
- swarm/extensions/launchers/swarm_api.py +68 -0
- swarm/extensions/launchers/swarm_cli.py +304 -0
- swarm/extensions/launchers/swarm_wrapper.py +29 -0
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +36 -0
- swarm/extensions/mcp/mcp_client.py +341 -0
- swarm/extensions/mcp/mcp_constants.py +7 -0
- swarm/extensions/mcp/mcp_tool_provider.py +110 -0
- swarm/llm/chat_completion.py +195 -0
- swarm/messages.py +132 -0
- swarm/migrations/0010_initial_chat_models.py +51 -0
- swarm/migrations/__init__.py +0 -0
- swarm/models.py +45 -0
- swarm/repl/__init__.py +1 -0
- swarm/repl/repl.py +87 -0
- swarm/serializers.py +12 -0
- swarm/settings.py +189 -0
- swarm/tool_executor.py +239 -0
- swarm/types.py +126 -0
- swarm/urls.py +89 -0
- swarm/util.py +124 -0
- swarm/utils/color_utils.py +40 -0
- swarm/utils/context_utils.py +272 -0
- swarm/utils/general_utils.py +162 -0
- swarm/utils/logger.py +61 -0
- swarm/utils/logger_setup.py +25 -0
- swarm/utils/message_sequence.py +173 -0
- swarm/utils/message_utils.py +95 -0
- swarm/utils/redact.py +68 -0
- swarm/views/__init__.py +41 -0
- swarm/views/api_views.py +46 -0
- swarm/views/chat_views.py +76 -0
- swarm/views/core_views.py +118 -0
- swarm/views/message_views.py +40 -0
- swarm/views/model_views.py +135 -0
- swarm/views/utils.py +457 -0
- swarm/views/web_views.py +149 -0
- 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}"
|