swarms 7.8.3__py3-none-any.whl → 7.8.7__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 (60) hide show
  1. swarms/agents/ape_agent.py +5 -22
  2. swarms/agents/consistency_agent.py +1 -1
  3. swarms/agents/i_agent.py +1 -1
  4. swarms/agents/reasoning_agents.py +99 -3
  5. swarms/agents/reasoning_duo.py +1 -1
  6. swarms/cli/main.py +1 -1
  7. swarms/communication/__init__.py +1 -0
  8. swarms/communication/duckdb_wrap.py +32 -2
  9. swarms/communication/pulsar_struct.py +45 -19
  10. swarms/communication/redis_wrap.py +56 -11
  11. swarms/communication/supabase_wrap.py +1659 -0
  12. swarms/prompts/agent_conversation_aggregator.py +38 -0
  13. swarms/prompts/prompt.py +0 -3
  14. swarms/schemas/agent_completion_response.py +71 -0
  15. swarms/schemas/agent_rag_schema.py +7 -0
  16. swarms/schemas/conversation_schema.py +9 -0
  17. swarms/schemas/llm_agent_schema.py +99 -81
  18. swarms/schemas/swarms_api_schemas.py +164 -0
  19. swarms/structs/__init__.py +15 -9
  20. swarms/structs/agent.py +219 -199
  21. swarms/structs/agent_rag_handler.py +685 -0
  22. swarms/structs/base_swarm.py +2 -1
  23. swarms/structs/conversation.py +832 -264
  24. swarms/structs/csv_to_agent.py +153 -100
  25. swarms/structs/deep_research_swarm.py +197 -193
  26. swarms/structs/dynamic_conversational_swarm.py +18 -7
  27. swarms/structs/hiearchical_swarm.py +1 -1
  28. swarms/structs/hybrid_hiearchical_peer_swarm.py +2 -18
  29. swarms/structs/image_batch_processor.py +261 -0
  30. swarms/structs/interactive_groupchat.py +356 -0
  31. swarms/structs/ma_blocks.py +159 -0
  32. swarms/structs/majority_voting.py +1 -1
  33. swarms/structs/mixture_of_agents.py +1 -1
  34. swarms/structs/multi_agent_exec.py +25 -26
  35. swarms/structs/multi_agent_router.py +3 -2
  36. swarms/structs/rearrange.py +3 -3
  37. swarms/structs/sequential_workflow.py +3 -3
  38. swarms/structs/swarm_matcher.py +499 -408
  39. swarms/structs/swarm_router.py +15 -97
  40. swarms/structs/swarming_architectures.py +1 -1
  41. swarms/tools/mcp_client_call.py +3 -0
  42. swarms/utils/__init__.py +10 -2
  43. swarms/utils/check_all_model_max_tokens.py +43 -0
  44. swarms/utils/generate_keys.py +0 -27
  45. swarms/utils/history_output_formatter.py +5 -20
  46. swarms/utils/litellm_wrapper.py +208 -60
  47. swarms/utils/output_types.py +24 -0
  48. swarms/utils/vllm_wrapper.py +14 -13
  49. swarms/utils/xml_utils.py +37 -2
  50. {swarms-7.8.3.dist-info → swarms-7.8.7.dist-info}/METADATA +31 -55
  51. {swarms-7.8.3.dist-info → swarms-7.8.7.dist-info}/RECORD +55 -48
  52. swarms/structs/multi_agent_collab.py +0 -242
  53. swarms/structs/output_types.py +0 -6
  54. swarms/utils/markdown_message.py +0 -21
  55. swarms/utils/visualizer.py +0 -510
  56. swarms/utils/wrapper_clusterop.py +0 -127
  57. /swarms/{tools → schemas}/tool_schema_base_model.py +0 -0
  58. {swarms-7.8.3.dist-info → swarms-7.8.7.dist-info}/LICENSE +0 -0
  59. {swarms-7.8.3.dist-info → swarms-7.8.7.dist-info}/WHEEL +0 -0
  60. {swarms-7.8.3.dist-info → swarms-7.8.7.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,8 @@
1
+ import traceback
1
2
  from typing import Optional
2
3
  import base64
3
4
  import requests
5
+ from pathlib import Path
4
6
 
5
7
  import asyncio
6
8
  from typing import List
@@ -9,11 +11,7 @@ from loguru import logger
9
11
  import litellm
10
12
  from pydantic import BaseModel
11
13
 
12
- from litellm import completion, acompletion
13
-
14
- litellm.set_verbose = True
15
- litellm.ssl_verify = False
16
- # litellm._turn_on_debug()
14
+ from litellm import completion, acompletion, supports_vision
17
15
 
18
16
 
19
17
  class LiteLLMException(Exception):
@@ -53,6 +51,35 @@ def get_audio_base64(audio_source: str) -> str:
53
51
  return encoded_string
54
52
 
55
53
 
54
+ def get_image_base64(image_source: str) -> str:
55
+ """
56
+ Convert image from a given source to a base64 encoded string.
57
+ Handles URLs, local file paths, and data URIs.
58
+ """
59
+ # If already a data URI, return as is
60
+ if image_source.startswith("data:image"):
61
+ return image_source
62
+
63
+ # Handle URL
64
+ if image_source.startswith(("http://", "https://")):
65
+ response = requests.get(image_source)
66
+ response.raise_for_status()
67
+ image_data = response.content
68
+ # Handle local file
69
+ else:
70
+ with open(image_source, "rb") as file:
71
+ image_data = file.read()
72
+
73
+ # Get file extension for mime type
74
+ extension = Path(image_source).suffix.lower()
75
+ mime_type = (
76
+ f"image/{extension[1:]}" if extension else "image/jpeg"
77
+ )
78
+
79
+ encoded_string = base64.b64encode(image_data).decode("utf-8")
80
+ return f"data:{mime_type};base64,{encoded_string}"
81
+
82
+
56
83
  class LiteLLM:
57
84
  """
58
85
  This class represents a LiteLLM.
@@ -72,12 +99,15 @@ class LiteLLM:
72
99
  tool_choice: str = "auto",
73
100
  parallel_tool_calls: bool = False,
74
101
  audio: str = None,
75
- retries: int = 3,
102
+ retries: int = 0,
76
103
  verbose: bool = False,
77
104
  caching: bool = False,
78
105
  mcp_call: bool = False,
79
106
  top_p: float = 1.0,
80
107
  functions: List[dict] = None,
108
+ return_all: bool = False,
109
+ base_url: str = None,
110
+ api_key: str = None,
81
111
  *args,
82
112
  **kwargs,
83
113
  ):
@@ -105,8 +135,11 @@ class LiteLLM:
105
135
  self.mcp_call = mcp_call
106
136
  self.top_p = top_p
107
137
  self.functions = functions
138
+ self.audio = audio
139
+ self.return_all = return_all
140
+ self.base_url = base_url
141
+ self.api_key = api_key
108
142
  self.modalities = []
109
- self._cached_messages = {} # Cache for prepared messages
110
143
  self.messages = [] # Initialize messages list
111
144
 
112
145
  # Configure litellm settings
@@ -135,7 +168,11 @@ class LiteLLM:
135
168
  out = out.model_dump()
136
169
  return out
137
170
 
138
- def _prepare_messages(self, task: str) -> list:
171
+ def _prepare_messages(
172
+ self,
173
+ task: str,
174
+ img: str = None,
175
+ ):
139
176
  """
140
177
  Prepare the messages for the given task.
141
178
 
@@ -145,91 +182,201 @@ class LiteLLM:
145
182
  Returns:
146
183
  list: A list of messages prepared for the task.
147
184
  """
148
- # Check cache first
149
- cache_key = f"{self.system_prompt}:{task}"
150
- if cache_key in self._cached_messages:
151
- return self._cached_messages[cache_key].copy()
185
+ self.check_if_model_supports_vision(img=img)
152
186
 
187
+ # Initialize messages
153
188
  messages = []
154
- if self.system_prompt:
189
+
190
+ # Add system prompt if present
191
+ if self.system_prompt is not None:
155
192
  messages.append(
156
193
  {"role": "system", "content": self.system_prompt}
157
194
  )
158
- messages.append({"role": "user", "content": task})
159
195
 
160
- # Cache the prepared messages
161
- self._cached_messages[cache_key] = messages.copy()
196
+ # Handle vision case
197
+ if img is not None:
198
+ messages = self.vision_processing(
199
+ task=task, image=img, messages=messages
200
+ )
201
+ else:
202
+ messages.append({"role": "user", "content": task})
203
+
162
204
  return messages
163
205
 
164
- def audio_processing(self, task: str, audio: str):
206
+ def anthropic_vision_processing(
207
+ self, task: str, image: str, messages: list
208
+ ) -> list:
165
209
  """
166
- Process the audio for the given task.
167
-
168
- Args:
169
- task (str): The task to be processed.
170
- audio (str): The path or identifier for the audio file.
210
+ Process vision input specifically for Anthropic models.
211
+ Handles Anthropic's specific image format requirements.
171
212
  """
172
- self.modalities.append("audio")
173
-
174
- encoded_string = get_audio_base64(audio)
213
+ # Get base64 encoded image
214
+ image_url = get_image_base64(image)
215
+
216
+ # Extract mime type from the data URI or use default
217
+ mime_type = "image/jpeg" # default
218
+ if "data:" in image_url and ";base64," in image_url:
219
+ mime_type = image_url.split(";base64,")[0].split("data:")[
220
+ 1
221
+ ]
222
+
223
+ # Ensure mime type is one of the supported formats
224
+ supported_formats = [
225
+ "image/jpeg",
226
+ "image/png",
227
+ "image/gif",
228
+ "image/webp",
229
+ ]
230
+ if mime_type not in supported_formats:
231
+ mime_type = (
232
+ "image/jpeg" # fallback to jpeg if unsupported
233
+ )
175
234
 
176
- # Append messages
177
- self.messages.append(
235
+ # Construct Anthropic vision message
236
+ messages.append(
178
237
  {
179
238
  "role": "user",
180
239
  "content": [
181
240
  {"type": "text", "text": task},
182
241
  {
183
- "type": "input_audio",
184
- "input_audio": {
185
- "data": encoded_string,
186
- "format": "wav",
242
+ "type": "image_url",
243
+ "image_url": {
244
+ "url": image_url,
245
+ "format": mime_type,
187
246
  },
188
247
  },
189
248
  ],
190
249
  }
191
250
  )
192
251
 
193
- def vision_processing(self, task: str, image: str):
252
+ return messages
253
+
254
+ def openai_vision_processing(
255
+ self, task: str, image: str, messages: list
256
+ ) -> list:
257
+ """
258
+ Process vision input specifically for OpenAI models.
259
+ Handles OpenAI's specific image format requirements.
260
+ """
261
+ # Get base64 encoded image with proper format
262
+ image_url = get_image_base64(image)
263
+
264
+ # Prepare vision message
265
+ vision_message = {
266
+ "type": "image_url",
267
+ "image_url": {"url": image_url},
268
+ }
269
+
270
+ # Add format for specific models
271
+ extension = Path(image).suffix.lower()
272
+ mime_type = (
273
+ f"image/{extension[1:]}" if extension else "image/jpeg"
274
+ )
275
+ vision_message["image_url"]["format"] = mime_type
276
+
277
+ # Append vision message
278
+ messages.append(
279
+ {
280
+ "role": "user",
281
+ "content": [
282
+ {"type": "text", "text": task},
283
+ vision_message,
284
+ ],
285
+ }
286
+ )
287
+
288
+ return messages
289
+
290
+ def vision_processing(
291
+ self, task: str, image: str, messages: Optional[list] = None
292
+ ):
194
293
  """
195
294
  Process the image for the given task.
295
+ Handles different image formats and model requirements.
296
+ """
297
+ # # # Handle Anthropic models separately
298
+ # # if "anthropic" in self.model_name.lower() or "claude" in self.model_name.lower():
299
+ # # messages = self.anthropic_vision_processing(task, image, messages)
300
+ # # return messages
301
+
302
+ # # Get base64 encoded image with proper format
303
+ # image_url = get_image_base64(image)
304
+
305
+ # # Prepare vision message
306
+ # vision_message = {
307
+ # "type": "image_url",
308
+ # "image_url": {"url": image_url},
309
+ # }
310
+
311
+ # # Add format for specific models
312
+ # extension = Path(image).suffix.lower()
313
+ # mime_type = f"image/{extension[1:]}" if extension else "image/jpeg"
314
+ # vision_message["image_url"]["format"] = mime_type
315
+
316
+ # # Append vision message
317
+ # messages.append(
318
+ # {
319
+ # "role": "user",
320
+ # "content": [
321
+ # {"type": "text", "text": task},
322
+ # vision_message,
323
+ # ],
324
+ # }
325
+ # )
326
+
327
+ # return messages
328
+ if (
329
+ "anthropic" in self.model_name.lower()
330
+ or "claude" in self.model_name.lower()
331
+ ):
332
+ messages = self.anthropic_vision_processing(
333
+ task, image, messages
334
+ )
335
+ return messages
336
+ else:
337
+ messages = self.openai_vision_processing(
338
+ task, image, messages
339
+ )
340
+ return messages
341
+
342
+ def audio_processing(self, task: str, audio: str):
196
343
  """
197
- self.modalities.append("vision")
344
+ Process the audio for the given task.
198
345
 
199
- # Append messages
346
+ Args:
347
+ task (str): The task to be processed.
348
+ audio (str): The path or identifier for the audio file.
349
+ """
350
+ encoded_string = get_audio_base64(audio)
351
+
352
+ # Append audio message
200
353
  self.messages.append(
201
354
  {
202
355
  "role": "user",
203
356
  "content": [
204
357
  {"type": "text", "text": task},
205
358
  {
206
- "type": "image_url",
207
- "image_url": {
208
- "url": image,
209
- # "detail": "high"
210
- # "format": "image",
359
+ "type": "input_audio",
360
+ "input_audio": {
361
+ "data": encoded_string,
362
+ "format": "wav",
211
363
  },
212
364
  },
213
365
  ],
214
366
  }
215
367
  )
216
368
 
217
- def handle_modalities(
218
- self, task: str, audio: str = None, img: str = None
219
- ):
369
+ def check_if_model_supports_vision(self, img: str = None):
220
370
  """
221
- Handle the modalities for the given task.
371
+ Check if the model supports vision.
222
372
  """
223
- self.messages = [] # Reset messages
224
- self.modalities.append("text")
225
-
226
- if audio is not None:
227
- self.audio_processing(task=task, audio=audio)
228
- self.modalities.append("audio")
229
-
230
373
  if img is not None:
231
- self.vision_processing(task=task, image=img)
232
- self.modalities.append("vision")
374
+ out = supports_vision(model=self.model_name)
375
+
376
+ if out is False:
377
+ raise ValueError(
378
+ f"Model {self.model_name} does not support vision"
379
+ )
233
380
 
234
381
  def run(
235
382
  self,
@@ -256,13 +403,7 @@ class LiteLLM:
256
403
  Exception: If there is an error in processing the request.
257
404
  """
258
405
  try:
259
- messages = self._prepare_messages(task)
260
-
261
- if audio is not None or img is not None:
262
- self.handle_modalities(
263
- task=task, audio=audio, img=img
264
- )
265
- messages = self.messages
406
+ messages = self._prepare_messages(task=task, img=img)
266
407
 
267
408
  # Base completion parameters
268
409
  completion_params = {
@@ -298,6 +439,9 @@ class LiteLLM:
298
439
  {"functions": self.functions}
299
440
  )
300
441
 
442
+ if self.base_url is not None:
443
+ completion_params["base_url"] = self.base_url
444
+
301
445
  # Add modalities if needed
302
446
  if self.modalities and len(self.modalities) >= 2:
303
447
  completion_params["modalities"] = self.modalities
@@ -308,12 +452,16 @@ class LiteLLM:
308
452
  # Handle tool-based response
309
453
  if self.tools_list_dictionary is not None:
310
454
  return self.output_for_tools(response)
455
+ elif self.return_all is True:
456
+ return response.model_dump()
311
457
  else:
312
458
  # Return standard response content
313
459
  return response.choices[0].message.content
314
460
 
315
461
  except LiteLLMException as error:
316
- logger.error(f"Error in LiteLLM run: {str(error)}")
462
+ logger.error(
463
+ f"Error in LiteLLM run: {str(error)} Traceback: {traceback.format_exc()}"
464
+ )
317
465
  if "rate_limit" in str(error).lower():
318
466
  logger.warning(
319
467
  "Rate limit hit, retrying with exponential backoff..."
@@ -0,0 +1,24 @@
1
+ from typing import Literal
2
+
3
+ HistoryOutputType = Literal[
4
+ "list",
5
+ "dict",
6
+ "dictionary",
7
+ "string",
8
+ "str",
9
+ "final",
10
+ "last",
11
+ "json",
12
+ "all",
13
+ "yaml",
14
+ "xml",
15
+ # "dict-final",
16
+ "dict-all-except-first",
17
+ "str-all-except-first",
18
+ "basemodel",
19
+ "dict-final",
20
+ ]
21
+
22
+ OutputType = HistoryOutputType
23
+
24
+ output_type: HistoryOutputType # OutputType now includes 'xml'
@@ -1,3 +1,5 @@
1
+ import os
2
+ import concurrent.futures
1
3
  from typing import List, Optional, Dict, Any
2
4
  from loguru import logger
3
5
 
@@ -131,16 +133,15 @@ class VLLMWrapper:
131
133
  Returns:
132
134
  List[str]: List of model responses.
133
135
  """
134
- logger.info(
135
- f"Running tasks in batches of size {batch_size}. Total tasks: {len(tasks)}"
136
- )
137
- results = []
138
-
139
- for i in range(0, len(tasks), batch_size):
140
- batch = tasks[i : i + batch_size]
141
- for task in batch:
142
- logger.info(f"Running task: {task}")
143
- results.append(self.run(task))
144
-
145
- logger.info("Completed all tasks.")
146
- return results
136
+ # Calculate the worker count based on 95% of available CPU cores
137
+ num_workers = max(1, int((os.cpu_count() or 1) * 0.95))
138
+ with concurrent.futures.ThreadPoolExecutor(
139
+ max_workers=num_workers
140
+ ) as executor:
141
+ futures = [
142
+ executor.submit(self.run, task) for task in tasks
143
+ ]
144
+ return [
145
+ future.result()
146
+ for future in concurrent.futures.as_completed(futures)
147
+ ]
swarms/utils/xml_utils.py CHANGED
@@ -3,7 +3,22 @@ from typing import Any
3
3
 
4
4
 
5
5
  def dict_to_xml(tag: str, d: dict) -> ET.Element:
6
- """Convert a dictionary to an XML Element."""
6
+ """
7
+ Convert a dictionary to an XML Element.
8
+
9
+ Args:
10
+ tag (str): The tag name for the root element
11
+ d (dict): The dictionary to convert to XML
12
+
13
+ Returns:
14
+ ET.Element: An XML Element representing the dictionary structure
15
+
16
+ Example:
17
+ >>> data = {"person": {"name": "John", "age": 30}}
18
+ >>> elem = dict_to_xml("root", data)
19
+ >>> ET.tostring(elem, encoding="unicode")
20
+ '<root><person><name>John</name><age>30</age></person></root>'
21
+ """
7
22
  elem = ET.Element(tag)
8
23
  for key, val in d.items():
9
24
  child = ET.Element(str(key))
@@ -24,7 +39,27 @@ def dict_to_xml(tag: str, d: dict) -> ET.Element:
24
39
 
25
40
 
26
41
  def to_xml_string(data: Any, root_tag: str = "root") -> str:
27
- """Convert a dict or list to an XML string."""
42
+ """
43
+ Convert a dict or list to an XML string.
44
+
45
+ Args:
46
+ data (Any): The data to convert to XML. Can be a dictionary, list, or other value
47
+ root_tag (str, optional): The tag name for the root element. Defaults to "root"
48
+
49
+ Returns:
50
+ str: An XML string representation of the input data
51
+
52
+ Example:
53
+ >>> data = {"person": {"name": "John", "age": 30}}
54
+ >>> xml_str = to_xml_string(data)
55
+ >>> print(xml_str)
56
+ <root><person><name>John</name><age>30</age></person></root>
57
+
58
+ >>> data = [1, 2, 3]
59
+ >>> xml_str = to_xml_string(data)
60
+ >>> print(xml_str)
61
+ <root><item>1</item><item>2</item><item>3</item></root>
62
+ """
28
63
  if isinstance(data, dict):
29
64
  elem = dict_to_xml(root_tag, data)
30
65
  elif isinstance(data, list):