swarms 7.8.8__py3-none-any.whl → 7.9.0__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.
swarms/structs/agent.py CHANGED
@@ -56,7 +56,6 @@ from swarms.tools.base_tool import BaseTool
56
56
  from swarms.tools.py_func_to_openai_func_str import (
57
57
  convert_multiple_functions_to_openai_function_schema,
58
58
  )
59
- from swarms.utils.any_to_str import any_to_str
60
59
  from swarms.utils.data_to_text import data_to_text
61
60
  from swarms.utils.file_processing import create_file_in_folder
62
61
  from swarms.utils.formatter import formatter
@@ -72,8 +71,10 @@ from swarms.prompts.max_loop_prompt import generate_reasoning_prompt
72
71
  from swarms.prompts.safety_prompt import SAFETY_PROMPT
73
72
  from swarms.structs.ma_utils import set_random_models_for_agents
74
73
  from swarms.tools.mcp_client_call import (
74
+ execute_multiple_tools_on_multiple_mcp_servers_sync,
75
75
  execute_tool_call_simple,
76
76
  get_mcp_tools_sync,
77
+ get_tools_for_multiple_mcp_servers,
77
78
  )
78
79
  from swarms.schemas.mcp_schemas import (
79
80
  MCPConnection,
@@ -81,7 +82,6 @@ from swarms.schemas.mcp_schemas import (
81
82
  from swarms.utils.index import (
82
83
  exists,
83
84
  format_data_structure,
84
- format_dict_to_string,
85
85
  )
86
86
  from swarms.schemas.conversation_schema import ConversationSchema
87
87
  from swarms.utils.output_types import OutputType
@@ -287,6 +287,11 @@ class Agent:
287
287
  >>> print(response)
288
288
  >>> # Generate a report on the financials.
289
289
 
290
+ >>> # Real-time streaming example
291
+ >>> agent = Agent(llm=llm, max_loops=1, streaming_on=True)
292
+ >>> response = agent.run("Tell me a long story.") # Will stream in real-time
293
+ >>> print(response) # Final complete response
294
+
290
295
  """
291
296
 
292
297
  def __init__(
@@ -403,7 +408,7 @@ class Agent:
403
408
  llm_args: dict = None,
404
409
  load_state_path: str = None,
405
410
  role: agent_roles = "worker",
406
- no_print: bool = False,
411
+ print_on: bool = True,
407
412
  tools_list_dictionary: Optional[List[Dict[str, Any]]] = None,
408
413
  mcp_url: Optional[Union[str, MCPConnection]] = None,
409
414
  mcp_urls: List[str] = None,
@@ -417,7 +422,9 @@ class Agent:
417
422
  llm_base_url: Optional[str] = None,
418
423
  llm_api_key: Optional[str] = None,
419
424
  rag_config: Optional[RAGConfig] = None,
420
- tool_call_summary: bool = False,
425
+ tool_call_summary: bool = True,
426
+ output_raw_json_from_tool_call: bool = False,
427
+ summarize_multiple_images: bool = False,
421
428
  *args,
422
429
  **kwargs,
423
430
  ):
@@ -446,7 +453,10 @@ class Agent:
446
453
  self.system_prompt = system_prompt
447
454
  self.agent_name = agent_name
448
455
  self.agent_description = agent_description
449
- self.saved_state_path = f"{self.agent_name}_{generate_api_key(prefix='agent-')}_state.json"
456
+ # self.saved_state_path = f"{self.agent_name}_{generate_api_key(prefix='agent-')}_state.json"
457
+ self.saved_state_path = (
458
+ f"{generate_api_key(prefix='agent-')}_state.json"
459
+ )
450
460
  self.autosave = autosave
451
461
  self.response_filters = []
452
462
  self.self_healing_enabled = self_healing_enabled
@@ -535,7 +545,7 @@ class Agent:
535
545
  self.llm_args = llm_args
536
546
  self.load_state_path = load_state_path
537
547
  self.role = role
538
- self.no_print = no_print
548
+ self.print_on = print_on
539
549
  self.tools_list_dictionary = tools_list_dictionary
540
550
  self.mcp_url = mcp_url
541
551
  self.mcp_urls = mcp_urls
@@ -550,6 +560,10 @@ class Agent:
550
560
  self.llm_api_key = llm_api_key
551
561
  self.rag_config = rag_config
552
562
  self.tool_call_summary = tool_call_summary
563
+ self.output_raw_json_from_tool_call = (
564
+ output_raw_json_from_tool_call
565
+ )
566
+ self.summarize_multiple_images = summarize_multiple_images
553
567
 
554
568
  # self.short_memory = self.short_memory_init()
555
569
 
@@ -622,16 +636,20 @@ class Agent:
622
636
  )
623
637
 
624
638
  self.short_memory.add(
625
- role=f"{self.agent_name}",
626
- content=f"Tools available: {format_data_structure(self.tools_list_dictionary)}",
639
+ role=self.agent_name,
640
+ content=self.tools_list_dictionary,
627
641
  )
628
642
 
629
643
  def short_memory_init(self):
630
- if (
631
- self.agent_name is not None
632
- or self.agent_description is not None
633
- ):
634
- prompt = f"\n Your Name: {self.agent_name} \n\n Your Description: {self.agent_description} \n\n {self.system_prompt}"
644
+ prompt = ""
645
+
646
+ # Add agent name, description, and instructions to the prompt
647
+ if self.agent_name is not None:
648
+ prompt += f"\n Name: {self.agent_name}"
649
+ elif self.agent_description is not None:
650
+ prompt += f"\n Description: {self.agent_description}"
651
+ elif self.system_prompt is not None:
652
+ prompt += f"\n Instructions: {self.system_prompt}"
635
653
  else:
636
654
  prompt = self.system_prompt
637
655
 
@@ -692,6 +710,10 @@ class Agent:
692
710
 
693
711
  if exists(self.tools) and len(self.tools) >= 2:
694
712
  parallel_tool_calls = True
713
+ elif exists(self.mcp_url) or exists(self.mcp_urls):
714
+ parallel_tool_calls = True
715
+ elif exists(self.mcp_config):
716
+ parallel_tool_calls = True
695
717
  else:
696
718
  parallel_tool_calls = False
697
719
 
@@ -714,7 +736,7 @@ class Agent:
714
736
  parallel_tool_calls=parallel_tool_calls,
715
737
  )
716
738
 
717
- elif self.mcp_url is not None:
739
+ elif exists(self.mcp_url) or exists(self.mcp_urls):
718
740
  self.llm = LiteLLM(
719
741
  **common_args,
720
742
  tools_list_dictionary=self.add_mcp_tools_to_memory(),
@@ -752,15 +774,27 @@ class Agent:
752
774
  tools = get_mcp_tools_sync(server_path=self.mcp_url)
753
775
  elif exists(self.mcp_config):
754
776
  tools = get_mcp_tools_sync(connection=self.mcp_config)
755
- logger.info(f"Tools: {tools}")
777
+ # logger.info(f"Tools: {tools}")
778
+ elif exists(self.mcp_urls):
779
+ tools = get_tools_for_multiple_mcp_servers(
780
+ urls=self.mcp_urls,
781
+ output_type="str",
782
+ )
783
+ # print(f"Tools: {tools} for {self.mcp_urls}")
756
784
  else:
757
785
  raise AgentMCPConnectionError(
758
786
  "mcp_url must be either a string URL or MCPConnection object"
759
787
  )
760
- self.pretty_print(
761
- f"✨ [SYSTEM] Successfully integrated {len(tools)} MCP tools into agent: {self.agent_name} | Status: ONLINE | Time: {time.strftime('%H:%M:%S')} ✨",
762
- loop_count=0,
763
- )
788
+
789
+ if (
790
+ exists(self.mcp_url)
791
+ or exists(self.mcp_urls)
792
+ or exists(self.mcp_config)
793
+ ):
794
+ self.pretty_print(
795
+ f"✨ [SYSTEM] Successfully integrated {len(tools)} MCP tools into agent: {self.agent_name} | Status: ONLINE | Time: {time.strftime('%H:%M:%S')} ✨",
796
+ loop_count=0,
797
+ )
764
798
 
765
799
  return tools
766
800
  except AgentMCPConnectionError as e:
@@ -786,6 +820,29 @@ class Agent:
786
820
 
787
821
  return json.loads(self.tools_list_dictionary)
788
822
 
823
+ def check_model_supports_utilities(self, img: str = None) -> bool:
824
+ """
825
+ Check if the current model supports vision capabilities.
826
+
827
+ Args:
828
+ img (str, optional): Image input to check vision support for. Defaults to None.
829
+
830
+ Returns:
831
+ bool: True if model supports vision and image is provided, False otherwise.
832
+ """
833
+ from litellm.utils import supports_vision
834
+
835
+ # Only check vision support if an image is provided
836
+ if img is not None:
837
+ out = supports_vision(self.model_name)
838
+ if not out:
839
+ raise ValueError(
840
+ f"Model {self.model_name} does not support vision capabilities. Please use a vision-enabled model."
841
+ )
842
+ return out
843
+
844
+ return False
845
+
789
846
  def check_if_no_prompt_then_autogenerate(self, task: str = None):
790
847
  """
791
848
  Checks if auto_generate_prompt is enabled and generates a prompt by combining agent name, description and system prompt if available.
@@ -907,12 +964,7 @@ class Agent:
907
964
  self,
908
965
  task: Optional[Union[str, Any]] = None,
909
966
  img: Optional[str] = None,
910
- speech: Optional[str] = None,
911
- video: Optional[str] = None,
912
- is_last: Optional[bool] = False,
913
967
  print_task: Optional[bool] = False,
914
- generate_speech: Optional[bool] = False,
915
- correct_answer: Optional[str] = None,
916
968
  *args,
917
969
  **kwargs,
918
970
  ) -> Any:
@@ -937,9 +989,12 @@ class Agent:
937
989
 
938
990
  self.check_if_no_prompt_then_autogenerate(task)
939
991
 
992
+ if img is not None:
993
+ self.check_model_supports_utilities(img=img)
994
+
940
995
  self.short_memory.add(role=self.user_name, content=task)
941
996
 
942
- if self.plan_enabled:
997
+ if self.plan_enabled is True:
943
998
  self.plan(task)
944
999
 
945
1000
  # Set the loop count
@@ -1006,56 +1061,73 @@ class Agent:
1006
1061
  )
1007
1062
  self.memory_query(task_prompt)
1008
1063
 
1009
- # # Generate response using LLM
1010
- # response_args = (
1011
- # (task_prompt, *args)
1012
- # if img is None
1013
- # else (task_prompt, img, *args)
1014
- # )
1015
-
1016
- # # Call the LLM
1017
- # response = self.call_llm(
1018
- # *response_args, **kwargs
1019
- # )
1020
-
1021
- response = self.call_llm(
1022
- task=task_prompt, img=img, *args, **kwargs
1023
- )
1064
+ if img is not None:
1065
+ response = self.call_llm(
1066
+ task=task_prompt,
1067
+ img=img,
1068
+ current_loop=loop_count,
1069
+ *args,
1070
+ **kwargs,
1071
+ )
1072
+ else:
1073
+ response = self.call_llm(
1074
+ task=task_prompt,
1075
+ current_loop=loop_count,
1076
+ *args,
1077
+ **kwargs,
1078
+ )
1024
1079
 
1080
+ # Parse the response from the agent with the output type
1025
1081
  if exists(self.tools_list_dictionary):
1026
1082
  if isinstance(response, BaseModel):
1027
1083
  response = response.model_dump()
1028
1084
 
1029
- # # Convert to a str if the response is not a str
1030
- # if self.mcp_url is None or self.tools is None:
1085
+ # Parse the response from the agent with the output type
1031
1086
  response = self.parse_llm_output(response)
1032
1087
 
1033
1088
  self.short_memory.add(
1034
1089
  role=self.agent_name,
1035
- content=format_dict_to_string(response),
1090
+ content=response,
1036
1091
  )
1037
1092
 
1038
1093
  # Print
1039
1094
  self.pretty_print(response, loop_count)
1040
1095
 
1041
- # # Output Cleaner
1042
- # self.output_cleaner_op(response)
1043
-
1044
- # Check and execute tools
1096
+ # Check and execute callable tools
1045
1097
  if exists(self.tools):
1046
-
1047
- self.execute_tools(
1048
- response=response,
1049
- loop_count=loop_count,
1050
- )
1098
+ if (
1099
+ self.output_raw_json_from_tool_call
1100
+ is True
1101
+ ):
1102
+ response = response
1103
+ else:
1104
+ # Only execute tools if response is not None
1105
+ if response is not None:
1106
+ self.execute_tools(
1107
+ response=response,
1108
+ loop_count=loop_count,
1109
+ )
1110
+ else:
1111
+ logger.warning(
1112
+ f"LLM returned None response in loop {loop_count}, skipping tool execution"
1113
+ )
1051
1114
 
1052
1115
  # Handle MCP tools
1053
- if exists(self.mcp_url) or exists(
1054
- self.mcp_config
1116
+ if (
1117
+ exists(self.mcp_url)
1118
+ or exists(self.mcp_config)
1119
+ or exists(self.mcp_urls)
1055
1120
  ):
1056
- self.mcp_tool_handling(
1057
- response, loop_count
1058
- )
1121
+ # Only handle MCP tools if response is not None
1122
+ if response is not None:
1123
+ self.mcp_tool_handling(
1124
+ response=response,
1125
+ current_loop=loop_count,
1126
+ )
1127
+ else:
1128
+ logger.warning(
1129
+ f"LLM returned None response in loop {loop_count}, skipping MCP tool handling"
1130
+ )
1059
1131
 
1060
1132
  self.sentiment_and_evaluator(response)
1061
1133
 
@@ -1110,7 +1182,10 @@ class Agent:
1110
1182
  user_input.lower()
1111
1183
  == self.custom_exit_command.lower()
1112
1184
  ):
1113
- print("Exiting as per user request.")
1185
+ self.pretty_print(
1186
+ "Exiting as per user request.",
1187
+ loop_count=loop_count,
1188
+ )
1114
1189
  break
1115
1190
 
1116
1191
  self.short_memory.add(
@@ -1211,12 +1286,6 @@ class Agent:
1211
1286
  self,
1212
1287
  task: Optional[str] = None,
1213
1288
  img: Optional[str] = None,
1214
- is_last: bool = False,
1215
- device: str = "cpu", # gpu
1216
- device_id: int = 1,
1217
- all_cores: bool = True,
1218
- do_not_use_cluster_ops: bool = True,
1219
- all_gpus: bool = False,
1220
1289
  *args,
1221
1290
  **kwargs,
1222
1291
  ) -> Any:
@@ -1225,10 +1294,6 @@ class Agent:
1225
1294
  Args:
1226
1295
  task (Optional[str]): The task to be performed. Defaults to None.
1227
1296
  img (Optional[str]): The image to be processed. Defaults to None.
1228
- is_last (bool): Indicates if this is the last task. Defaults to False.
1229
- device (str): The device to use for execution. Defaults to "cpu".
1230
- device_id (int): The ID of the GPU to use if device is set to "gpu". Defaults to 0.
1231
- all_cores (bool): If True, uses all available CPU cores. Defaults to True.
1232
1297
  """
1233
1298
  try:
1234
1299
  return self.run(
@@ -1298,26 +1363,49 @@ class Agent:
1298
1363
 
1299
1364
  def plan(self, task: str, *args, **kwargs) -> None:
1300
1365
  """
1301
- Plan the task
1366
+ Create a strategic plan for executing the given task.
1367
+
1368
+ This method generates a step-by-step plan by combining the conversation
1369
+ history, planning prompt, and current task. The plan is then added to
1370
+ the agent's short-term memory for reference during execution.
1302
1371
 
1303
1372
  Args:
1304
- task (str): The task to plan
1373
+ task (str): The task to create a plan for
1374
+ *args: Additional positional arguments passed to the LLM
1375
+ **kwargs: Additional keyword arguments passed to the LLM
1376
+
1377
+ Returns:
1378
+ None: The plan is stored in memory rather than returned
1379
+
1380
+ Raises:
1381
+ Exception: If planning fails, the original exception is re-raised
1305
1382
  """
1306
1383
  try:
1384
+ # Get the current conversation history
1385
+ history = self.short_memory.get_str()
1386
+
1387
+ plan_prompt = f"Create a comprehensive step-by-step plan to complete the following task: \n\n {task}"
1388
+
1389
+ # Construct the planning prompt by combining history, planning prompt, and task
1307
1390
  if exists(self.planning_prompt):
1308
- # Join the plan and the task
1309
- planning_prompt = f"{self.planning_prompt} {task}"
1310
- plan = self.llm(planning_prompt, *args, **kwargs)
1311
- logger.info(f"Plan: {plan}")
1391
+ planning_prompt = f"{history}\n\n{self.planning_prompt}\n\nTask: {task}"
1392
+ else:
1393
+ planning_prompt = (
1394
+ f"{history}\n\n{plan_prompt}\n\nTask: {task}"
1395
+ )
1312
1396
 
1313
- # Add the plan to the memory
1314
- self.short_memory.add(
1315
- role=self.agent_name, content=str(plan)
1316
- )
1397
+ # Generate the plan using the LLM
1398
+ plan = self.llm.run(task=planning_prompt, *args, **kwargs)
1399
+
1400
+ # Store the generated plan in short-term memory
1401
+ self.short_memory.add(role=self.agent_name, content=plan)
1317
1402
 
1318
1403
  return None
1404
+
1319
1405
  except Exception as error:
1320
- logger.error(f"Error planning task: {error}")
1406
+ logger.error(
1407
+ f"Failed to create plan for task '{task}': {error}"
1408
+ )
1321
1409
  raise error
1322
1410
 
1323
1411
  async def run_concurrent(self, task: str, *args, **kwargs):
@@ -1436,11 +1524,13 @@ class Agent:
1436
1524
  f"The model '{self.model_name}' does not support function calling. Please use a model that supports function calling."
1437
1525
  )
1438
1526
 
1439
- if self.max_tokens > get_max_tokens(self.model_name):
1440
- raise AgentInitializationError(
1441
- f"Max tokens is set to {self.max_tokens}, but the model '{self.model_name}' only supports {get_max_tokens(self.model_name)} tokens. Please set max tokens to {get_max_tokens(self.model_name)} or less."
1442
- )
1443
-
1527
+ try:
1528
+ if self.max_tokens > get_max_tokens(self.model_name):
1529
+ raise AgentInitializationError(
1530
+ f"Max tokens is set to {self.max_tokens}, but the model '{self.model_name}' only supports {get_max_tokens(self.model_name)} tokens. Please set max tokens to {get_max_tokens(self.model_name)} or less."
1531
+ )
1532
+ except Exception:
1533
+ pass
1444
1534
 
1445
1535
  if self.model_name not in model_list:
1446
1536
  logger.warning(
@@ -2384,7 +2474,12 @@ class Agent:
2384
2474
  return None
2385
2475
 
2386
2476
  def call_llm(
2387
- self, task: str, img: Optional[str] = None, *args, **kwargs
2477
+ self,
2478
+ task: str,
2479
+ img: Optional[str] = None,
2480
+ current_loop: int = 0,
2481
+ *args,
2482
+ **kwargs,
2388
2483
  ) -> str:
2389
2484
  """
2390
2485
  Calls the appropriate method on the `llm` object based on the given task.
@@ -2406,14 +2501,81 @@ class Agent:
2406
2501
  """
2407
2502
 
2408
2503
  try:
2409
- if img is not None:
2410
- out = self.llm.run(
2411
- task=task, img=img, *args, **kwargs
2412
- )
2504
+ # Set streaming parameter in LLM if streaming is enabled
2505
+ if self.streaming_on and hasattr(self.llm, "stream"):
2506
+ original_stream = self.llm.stream
2507
+ self.llm.stream = True
2508
+
2509
+ if img is not None:
2510
+ streaming_response = self.llm.run(
2511
+ task=task, img=img, *args, **kwargs
2512
+ )
2513
+ else:
2514
+ streaming_response = self.llm.run(
2515
+ task=task, *args, **kwargs
2516
+ )
2517
+
2518
+ # If we get a streaming response, handle it with the new streaming panel
2519
+ if hasattr(
2520
+ streaming_response, "__iter__"
2521
+ ) and not isinstance(streaming_response, str):
2522
+ # Check print_on parameter for different streaming behaviors
2523
+ if self.print_on is False:
2524
+ # Silent streaming - no printing, just collect chunks
2525
+ chunks = []
2526
+ for chunk in streaming_response:
2527
+ if (
2528
+ hasattr(chunk, "choices")
2529
+ and chunk.choices[0].delta.content
2530
+ ):
2531
+ content = chunk.choices[
2532
+ 0
2533
+ ].delta.content
2534
+ chunks.append(content)
2535
+ complete_response = "".join(chunks)
2536
+ else:
2537
+ # Collect chunks for conversation saving
2538
+ collected_chunks = []
2539
+
2540
+ def on_chunk_received(chunk: str):
2541
+ """Callback to collect chunks as they arrive"""
2542
+ collected_chunks.append(chunk)
2543
+ # Optional: Save each chunk to conversation in real-time
2544
+ # This creates a more detailed conversation history
2545
+ if self.verbose:
2546
+ logger.debug(
2547
+ f"Streaming chunk received: {chunk[:50]}..."
2548
+ )
2549
+
2550
+ # Use the streaming panel to display and collect the response
2551
+ complete_response = formatter.print_streaming_panel(
2552
+ streaming_response,
2553
+ title=f"🤖 Agent: {self.agent_name} Loops: {current_loop}",
2554
+ style=None, # Use random color like non-streaming approach
2555
+ collect_chunks=True,
2556
+ on_chunk_callback=on_chunk_received,
2557
+ )
2558
+
2559
+ # Restore original stream setting
2560
+ self.llm.stream = original_stream
2561
+
2562
+ # Return the complete response for further processing
2563
+ return complete_response
2564
+ else:
2565
+ # Restore original stream setting
2566
+ self.llm.stream = original_stream
2567
+ return streaming_response
2413
2568
  else:
2414
- out = self.llm.run(task=task, *args, **kwargs)
2569
+ # Non-streaming call
2570
+ if img is not None:
2571
+ out = self.llm.run(
2572
+ task=task, img=img, *args, **kwargs
2573
+ )
2574
+ else:
2575
+ out = self.llm.run(task=task, *args, **kwargs)
2576
+
2577
+ return out
2415
2578
 
2416
- return out
2417
2579
  except AgentLLMError as e:
2418
2580
  logger.error(
2419
2581
  f"Error calling LLM: {e}. Task: {task}, Args: {args}, Kwargs: {kwargs}"
@@ -2439,7 +2601,8 @@ class Agent:
2439
2601
  self,
2440
2602
  task: Optional[Union[str, Any]] = None,
2441
2603
  img: Optional[str] = None,
2442
- scheduled_run_date: Optional[datetime] = None,
2604
+ imgs: Optional[List[str]] = None,
2605
+ correct_answer: Optional[str] = None,
2443
2606
  *args,
2444
2607
  **kwargs,
2445
2608
  ) -> Any:
@@ -2453,11 +2616,7 @@ class Agent:
2453
2616
  Args:
2454
2617
  task (Optional[str], optional): The task to be executed. Defaults to None.
2455
2618
  img (Optional[str], optional): The image to be processed. Defaults to None.
2456
- device (str, optional): The device to use for execution. Defaults to "cpu".
2457
- device_id (int, optional): The ID of the GPU to use if device is set to "gpu". Defaults to 0.
2458
- all_cores (bool, optional): If True, uses all available CPU cores. Defaults to True.
2459
- scheduled_run_date (Optional[datetime], optional): The date and time to schedule the task. Defaults to None.
2460
- do_not_use_cluster_ops (bool, optional): If True, does not use cluster ops. Defaults to False.
2619
+ imgs (Optional[List[str]], optional): The list of images to be processed. Defaults to None.
2461
2620
  *args: Additional positional arguments to be passed to the execution method.
2462
2621
  **kwargs: Additional keyword arguments to be passed to the execution method.
2463
2622
 
@@ -2470,21 +2629,28 @@ class Agent:
2470
2629
  """
2471
2630
 
2472
2631
  if not isinstance(task, str):
2473
- task = any_to_str(task)
2474
-
2475
- if scheduled_run_date:
2476
- while datetime.now() < scheduled_run_date:
2477
- time.sleep(
2478
- 1
2479
- ) # Sleep for a short period to avoid busy waiting
2632
+ task = format_data_structure(task)
2480
2633
 
2481
2634
  try:
2482
- output = self._run(
2483
- task=task,
2484
- img=img,
2485
- *args,
2486
- **kwargs,
2487
- )
2635
+ if exists(imgs):
2636
+ output = self.run_multiple_images(
2637
+ task=task, imgs=imgs, *args, **kwargs
2638
+ )
2639
+ elif exists(correct_answer):
2640
+ output = self.continuous_run_with_answer(
2641
+ task=task,
2642
+ img=img,
2643
+ correct_answer=correct_answer,
2644
+ *args,
2645
+ **kwargs,
2646
+ )
2647
+ else:
2648
+ output = self._run(
2649
+ task=task,
2650
+ img=img,
2651
+ *args,
2652
+ **kwargs,
2653
+ )
2488
2654
 
2489
2655
  return output
2490
2656
 
@@ -2624,14 +2790,12 @@ class Agent:
2624
2790
  return self.role
2625
2791
 
2626
2792
  def pretty_print(self, response: str, loop_count: int):
2627
- if self.no_print is False:
2793
+ if self.print_on is False:
2628
2794
  if self.streaming_on is True:
2629
- # self.stream_response(response)
2630
- formatter.print_panel_token_by_token(
2631
- f"{self.agent_name}: {response}",
2632
- title=f"Agent Name: {self.agent_name} [Max Loops: {loop_count}]",
2633
- )
2634
- elif self.no_print is True:
2795
+ # Skip printing here since real streaming is handled in call_llm
2796
+ # This avoids double printing when streaming_on=True
2797
+ pass
2798
+ elif self.print_on is False:
2635
2799
  pass
2636
2800
  else:
2637
2801
  # logger.info(f"Response: {response}")
@@ -2664,7 +2828,7 @@ class Agent:
2664
2828
  ) # Convert other dicts to string
2665
2829
 
2666
2830
  elif isinstance(response, BaseModel):
2667
- out = response.model_dump()
2831
+ response = response.model_dump()
2668
2832
 
2669
2833
  # Handle List[BaseModel] responses
2670
2834
  elif (
@@ -2674,14 +2838,9 @@ class Agent:
2674
2838
  ):
2675
2839
  return [item.model_dump() for item in response]
2676
2840
 
2677
- elif isinstance(response, list):
2678
- out = format_data_structure(response)
2679
- else:
2680
- out = str(response)
2681
-
2682
- return out
2841
+ return response
2683
2842
 
2684
- except Exception as e:
2843
+ except AgentChatCompletionResponse as e:
2685
2844
  logger.error(f"Error parsing LLM output: {e}")
2686
2845
  raise ValueError(
2687
2846
  f"Failed to parse LLM output: {type(response)}"
@@ -2738,6 +2897,15 @@ class Agent:
2738
2897
  connection=self.mcp_config,
2739
2898
  )
2740
2899
  )
2900
+ elif exists(self.mcp_urls):
2901
+ tool_response = execute_multiple_tools_on_multiple_mcp_servers_sync(
2902
+ responses=response,
2903
+ urls=self.mcp_urls,
2904
+ output_type="json",
2905
+ )
2906
+ # tool_response = format_data_structure(tool_response)
2907
+
2908
+ # print(f"Multiple MCP Tool Response: {tool_response}")
2741
2909
  else:
2742
2910
  raise AgentMCPConnectionError(
2743
2911
  "mcp_url must be either a string URL or MCPConnection object"
@@ -2745,9 +2913,9 @@ class Agent:
2745
2913
 
2746
2914
  # Get the text content from the tool response
2747
2915
  # execute_tool_call_simple returns a string directly, not an object with content attribute
2748
- text_content = f"MCP Tool Response: \n{json.dumps(tool_response, indent=2)}"
2916
+ text_content = f"MCP Tool Response: \n\n {json.dumps(tool_response, indent=2)}"
2749
2917
 
2750
- if self.no_print is False:
2918
+ if self.print_on is False:
2751
2919
  formatter.print_panel(
2752
2920
  text_content,
2753
2921
  "MCP Tool Response: 🛠️",
@@ -2790,7 +2958,7 @@ class Agent:
2790
2958
  temperature=self.temperature,
2791
2959
  max_tokens=self.max_tokens,
2792
2960
  system_prompt=self.system_prompt,
2793
- stream=self.streaming_on,
2961
+ stream=False, # Always disable streaming for tool summaries
2794
2962
  tools_list_dictionary=None,
2795
2963
  parallel_tool_calls=False,
2796
2964
  base_url=self.llm_base_url,
@@ -2798,6 +2966,13 @@ class Agent:
2798
2966
  )
2799
2967
 
2800
2968
  def execute_tools(self, response: any, loop_count: int):
2969
+ # Handle None response gracefully
2970
+ if response is None:
2971
+ logger.warning(
2972
+ f"Cannot execute tools with None response in loop {loop_count}. "
2973
+ "This may indicate the LLM did not return a valid response."
2974
+ )
2975
+ return
2801
2976
 
2802
2977
  output = (
2803
2978
  self.tool_struct.execute_function_calls_from_api_response(
@@ -2844,3 +3019,134 @@ class Agent:
2844
3019
 
2845
3020
  def list_output_types(self):
2846
3021
  return OutputType
3022
+
3023
+ def run_multiple_images(
3024
+ self, task: str, imgs: List[str], *args, **kwargs
3025
+ ):
3026
+ """
3027
+ Run the agent with multiple images using concurrent processing.
3028
+
3029
+ Args:
3030
+ task (str): The task to be performed on each image.
3031
+ imgs (List[str]): List of image paths or URLs to process.
3032
+ *args: Additional positional arguments to pass to the agent's run method.
3033
+ **kwargs: Additional keyword arguments to pass to the agent's run method.
3034
+
3035
+ Returns:
3036
+ List[Any]: A list of outputs generated for each image in the same order as the input images.
3037
+
3038
+ Examples:
3039
+ >>> agent = Agent()
3040
+ >>> outputs = agent.run_multiple_images(
3041
+ ... task="Describe what you see in this image",
3042
+ ... imgs=["image1.jpg", "image2.png", "image3.jpeg"]
3043
+ ... )
3044
+ >>> print(f"Processed {len(outputs)} images")
3045
+ Processed 3 images
3046
+
3047
+ Raises:
3048
+ Exception: If an error occurs while processing any of the images.
3049
+ """
3050
+ # Calculate number of workers as 95% of available CPU cores
3051
+ cpu_count = os.cpu_count()
3052
+ max_workers = max(1, int(cpu_count * 0.95))
3053
+
3054
+ # Use ThreadPoolExecutor for concurrent processing
3055
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
3056
+ # Submit all image processing tasks
3057
+ future_to_img = {
3058
+ executor.submit(
3059
+ self.run, task=task, img=img, *args, **kwargs
3060
+ ): img
3061
+ for img in imgs
3062
+ }
3063
+
3064
+ # Collect results in order
3065
+ outputs = []
3066
+ for future in future_to_img:
3067
+ try:
3068
+ output = future.result()
3069
+ outputs.append(output)
3070
+ except Exception as e:
3071
+ logger.error(f"Error processing image: {e}")
3072
+ outputs.append(
3073
+ None
3074
+ ) # or raise the exception based on your preference
3075
+
3076
+ # Combine the outputs into a single string if summarization is enabled
3077
+ if self.summarize_multiple_images is True:
3078
+ output = "\n".join(outputs)
3079
+
3080
+ prompt = f"""
3081
+ You have already analyzed {len(outputs)} images and provided detailed descriptions for each one.
3082
+ Now, based on your previous analysis of these images, create a comprehensive report that:
3083
+
3084
+ 1. Synthesizes the key findings across all images
3085
+ 2. Identifies common themes, patterns, or relationships between the images
3086
+ 3. Provides an overall summary that captures the most important insights
3087
+ 4. Highlights any notable differences or contrasts between the images
3088
+
3089
+ Here are your previous analyses of the images:
3090
+ {output}
3091
+
3092
+ Please create a well-structured report that brings together your insights from all {len(outputs)} images.
3093
+ """
3094
+
3095
+ outputs = self.run(task=prompt, *args, **kwargs)
3096
+
3097
+ return outputs
3098
+
3099
+ def continuous_run_with_answer(
3100
+ self,
3101
+ task: str,
3102
+ img: Optional[str] = None,
3103
+ correct_answer: str = None,
3104
+ max_attempts: int = 10,
3105
+ ):
3106
+ """
3107
+ Run the agent with the task until the correct answer is provided.
3108
+
3109
+ Args:
3110
+ task (str): The task to be performed
3111
+ correct_answer (str): The correct answer that must be found in the response
3112
+ max_attempts (int): Maximum number of attempts before giving up (default: 10)
3113
+
3114
+ Returns:
3115
+ str: The response containing the correct answer
3116
+
3117
+ Raises:
3118
+ Exception: If max_attempts is reached without finding the correct answer
3119
+ """
3120
+ attempts = 0
3121
+
3122
+ while attempts < max_attempts:
3123
+ attempts += 1
3124
+
3125
+ if self.verbose:
3126
+ logger.info(
3127
+ f"Attempt {attempts}/{max_attempts} to find correct answer"
3128
+ )
3129
+
3130
+ response = self._run(task=task, img=img)
3131
+
3132
+ # Check if the correct answer is in the response (case-insensitive)
3133
+ if correct_answer.lower() in response.lower():
3134
+ if self.verbose:
3135
+ logger.info(
3136
+ f"Correct answer found on attempt {attempts}"
3137
+ )
3138
+ return response
3139
+ else:
3140
+ # Add feedback to help guide the agent
3141
+ feedback = "Your previous response was incorrect. Think carefully about the question and ensure your response directly addresses what was asked."
3142
+ self.short_memory.add(role="User", content=feedback)
3143
+
3144
+ if self.verbose:
3145
+ logger.info(
3146
+ f"Correct answer not found. Expected: '{correct_answer}'"
3147
+ )
3148
+
3149
+ # If we reach here, we've exceeded max_attempts
3150
+ raise Exception(
3151
+ f"Failed to find correct answer '{correct_answer}' after {max_attempts} attempts"
3152
+ )