swarms 7.8.9__py3-none-any.whl → 7.9.1__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
@@ -5,6 +5,7 @@ import os
5
5
  import random
6
6
  import threading
7
7
  import time
8
+ import traceback
8
9
  import uuid
9
10
  from concurrent.futures import ThreadPoolExecutor
10
11
  from datetime import datetime
@@ -56,7 +57,6 @@ from swarms.tools.base_tool import BaseTool
56
57
  from swarms.tools.py_func_to_openai_func_str import (
57
58
  convert_multiple_functions_to_openai_function_schema,
58
59
  )
59
- from swarms.utils.any_to_str import any_to_str
60
60
  from swarms.utils.data_to_text import data_to_text
61
61
  from swarms.utils.file_processing import create_file_in_folder
62
62
  from swarms.utils.formatter import formatter
@@ -86,6 +86,7 @@ from swarms.utils.index import (
86
86
  )
87
87
  from swarms.schemas.conversation_schema import ConversationSchema
88
88
  from swarms.utils.output_types import OutputType
89
+ from swarms.utils.retry_func import retry_function
89
90
 
90
91
 
91
92
  def stop_when_repeats(response: str) -> bool:
@@ -154,6 +155,12 @@ class AgentLLMInitializationError(AgentError):
154
155
  pass
155
156
 
156
157
 
158
+ class AgentToolExecutionError(AgentError):
159
+ """Exception raised when the agent fails to execute a tool. Check the tool's configuration and availability."""
160
+
161
+ pass
162
+
163
+
157
164
  # [FEAT][AGENT]
158
165
  class Agent:
159
166
  """
@@ -288,6 +295,11 @@ class Agent:
288
295
  >>> print(response)
289
296
  >>> # Generate a report on the financials.
290
297
 
298
+ >>> # Real-time streaming example
299
+ >>> agent = Agent(llm=llm, max_loops=1, streaming_on=True)
300
+ >>> response = agent.run("Tell me a long story.") # Will stream in real-time
301
+ >>> print(response) # Final complete response
302
+
291
303
  """
292
304
 
293
305
  def __init__(
@@ -404,7 +416,7 @@ class Agent:
404
416
  llm_args: dict = None,
405
417
  load_state_path: str = None,
406
418
  role: agent_roles = "worker",
407
- no_print: bool = False,
419
+ print_on: bool = True,
408
420
  tools_list_dictionary: Optional[List[Dict[str, Any]]] = None,
409
421
  mcp_url: Optional[Union[str, MCPConnection]] = None,
410
422
  mcp_urls: List[str] = None,
@@ -420,6 +432,8 @@ class Agent:
420
432
  rag_config: Optional[RAGConfig] = None,
421
433
  tool_call_summary: bool = True,
422
434
  output_raw_json_from_tool_call: bool = False,
435
+ summarize_multiple_images: bool = False,
436
+ tool_retry_attempts: int = 3,
423
437
  *args,
424
438
  **kwargs,
425
439
  ):
@@ -540,7 +554,7 @@ class Agent:
540
554
  self.llm_args = llm_args
541
555
  self.load_state_path = load_state_path
542
556
  self.role = role
543
- self.no_print = no_print
557
+ self.print_on = print_on
544
558
  self.tools_list_dictionary = tools_list_dictionary
545
559
  self.mcp_url = mcp_url
546
560
  self.mcp_urls = mcp_urls
@@ -558,6 +572,8 @@ class Agent:
558
572
  self.output_raw_json_from_tool_call = (
559
573
  output_raw_json_from_tool_call
560
574
  )
575
+ self.summarize_multiple_images = summarize_multiple_images
576
+ self.tool_retry_attempts = tool_retry_attempts
561
577
 
562
578
  # self.short_memory = self.short_memory_init()
563
579
 
@@ -630,16 +646,20 @@ class Agent:
630
646
  )
631
647
 
632
648
  self.short_memory.add(
633
- role=f"{self.agent_name}",
649
+ role=self.agent_name,
634
650
  content=self.tools_list_dictionary,
635
651
  )
636
652
 
637
653
  def short_memory_init(self):
638
- if (
639
- self.agent_name is not None
640
- or self.agent_description is not None
641
- ):
642
- prompt = f"\n Your Name: {self.agent_name} \n\n Your Description: {self.agent_description} \n\n {self.system_prompt}"
654
+ prompt = ""
655
+
656
+ # Add agent name, description, and instructions to the prompt
657
+ if self.agent_name is not None:
658
+ prompt += f"\n Name: {self.agent_name}"
659
+ elif self.agent_description is not None:
660
+ prompt += f"\n Description: {self.agent_description}"
661
+ elif self.system_prompt is not None:
662
+ prompt += f"\n Instructions: {self.system_prompt}"
643
663
  else:
644
664
  prompt = self.system_prompt
645
665
 
@@ -781,10 +801,11 @@ class Agent:
781
801
  or exists(self.mcp_urls)
782
802
  or exists(self.mcp_config)
783
803
  ):
784
- self.pretty_print(
785
- f"✨ [SYSTEM] Successfully integrated {len(tools)} MCP tools into agent: {self.agent_name} | Status: ONLINE | Time: {time.strftime('%H:%M:%S')} ✨",
786
- loop_count=0,
787
- )
804
+ if self.print_on is True:
805
+ self.pretty_print(
806
+ f"✨ [SYSTEM] Successfully integrated {len(tools)} MCP tools into agent: {self.agent_name} | Status: ONLINE | Time: {time.strftime('%H:%M:%S')} ✨",
807
+ loop_count=0,
808
+ )
788
809
 
789
810
  return tools
790
811
  except AgentMCPConnectionError as e:
@@ -810,6 +831,29 @@ class Agent:
810
831
 
811
832
  return json.loads(self.tools_list_dictionary)
812
833
 
834
+ def check_model_supports_utilities(self, img: str = None) -> bool:
835
+ """
836
+ Check if the current model supports vision capabilities.
837
+
838
+ Args:
839
+ img (str, optional): Image input to check vision support for. Defaults to None.
840
+
841
+ Returns:
842
+ bool: True if model supports vision and image is provided, False otherwise.
843
+ """
844
+ from litellm.utils import supports_vision
845
+
846
+ # Only check vision support if an image is provided
847
+ if img is not None:
848
+ out = supports_vision(self.model_name)
849
+ if not out:
850
+ raise ValueError(
851
+ f"Model {self.model_name} does not support vision capabilities. Please use a vision-enabled model."
852
+ )
853
+ return out
854
+
855
+ return False
856
+
813
857
  def check_if_no_prompt_then_autogenerate(self, task: str = None):
814
858
  """
815
859
  Checks if auto_generate_prompt is enabled and generates a prompt by combining agent name, description and system prompt if available.
@@ -931,12 +975,7 @@ class Agent:
931
975
  self,
932
976
  task: Optional[Union[str, Any]] = None,
933
977
  img: Optional[str] = None,
934
- speech: Optional[str] = None,
935
- video: Optional[str] = None,
936
- is_last: Optional[bool] = False,
937
978
  print_task: Optional[bool] = False,
938
- generate_speech: Optional[bool] = False,
939
- correct_answer: Optional[str] = None,
940
979
  *args,
941
980
  **kwargs,
942
981
  ) -> Any:
@@ -961,9 +1000,12 @@ class Agent:
961
1000
 
962
1001
  self.check_if_no_prompt_then_autogenerate(task)
963
1002
 
1003
+ if img is not None:
1004
+ self.check_model_supports_utilities(img=img)
1005
+
964
1006
  self.short_memory.add(role=self.user_name, content=task)
965
1007
 
966
- if self.plan_enabled or self.planning_prompt is not None:
1008
+ if self.plan_enabled is True:
967
1009
  self.plan(task)
968
1010
 
969
1011
  # Set the loop count
@@ -984,8 +1026,8 @@ class Agent:
984
1026
  # Print the request
985
1027
  if print_task is True:
986
1028
  formatter.print_panel(
987
- f"\n User: {task}",
988
- f"Task Request for {self.agent_name}",
1029
+ content=f"\n User: {task}",
1030
+ title=f"Task Request for {self.agent_name}",
989
1031
  )
990
1032
 
991
1033
  while (
@@ -1030,12 +1072,25 @@ class Agent:
1030
1072
  )
1031
1073
  self.memory_query(task_prompt)
1032
1074
 
1033
- response = self.call_llm(
1034
- task=task_prompt, img=img, *args, **kwargs
1035
- )
1075
+ if img is not None:
1076
+ response = self.call_llm(
1077
+ task=task_prompt,
1078
+ img=img,
1079
+ current_loop=loop_count,
1080
+ *args,
1081
+ **kwargs,
1082
+ )
1083
+ else:
1084
+ response = self.call_llm(
1085
+ task=task_prompt,
1086
+ current_loop=loop_count,
1087
+ *args,
1088
+ **kwargs,
1089
+ )
1036
1090
 
1037
- print(f"Response: {response}")
1091
+ # If streaming is enabled, then don't print the response
1038
1092
 
1093
+ # Parse the response from the agent with the output type
1039
1094
  if exists(self.tools_list_dictionary):
1040
1095
  if isinstance(response, BaseModel):
1041
1096
  response = response.model_dump()
@@ -1049,22 +1104,24 @@ class Agent:
1049
1104
  )
1050
1105
 
1051
1106
  # Print
1052
- self.pretty_print(response, loop_count)
1107
+ if self.print_on is True:
1108
+ if isinstance(response, list):
1109
+ self.pretty_print(
1110
+ f"Structured Output - Attempting Function Call Execution [{time.strftime('%H:%M:%S')}] \n\n {format_data_structure(response)} ",
1111
+ loop_count,
1112
+ )
1113
+ elif self.streaming_on is True:
1114
+ pass
1115
+ else:
1116
+ self.pretty_print(
1117
+ response, loop_count
1118
+ )
1053
1119
 
1054
1120
  # Check and execute callable tools
1055
1121
  if exists(self.tools):
1056
-
1057
- if (
1058
- self.output_raw_json_from_tool_call
1059
- is True
1060
- ):
1061
- print(type(response))
1062
- response = response
1063
- else:
1064
- self.execute_tools(
1065
- response=response,
1066
- loop_count=loop_count,
1067
- )
1122
+ self.tool_execution_retry(
1123
+ response, loop_count
1124
+ )
1068
1125
 
1069
1126
  # Handle MCP tools
1070
1127
  if (
@@ -1072,10 +1129,16 @@ class Agent:
1072
1129
  or exists(self.mcp_config)
1073
1130
  or exists(self.mcp_urls)
1074
1131
  ):
1075
- self.mcp_tool_handling(
1076
- response=response,
1077
- current_loop=loop_count,
1078
- )
1132
+ # Only handle MCP tools if response is not None
1133
+ if response is not None:
1134
+ self.mcp_tool_handling(
1135
+ response=response,
1136
+ current_loop=loop_count,
1137
+ )
1138
+ else:
1139
+ logger.warning(
1140
+ f"LLM returned None response in loop {loop_count}, skipping MCP tool handling"
1141
+ )
1079
1142
 
1080
1143
  self.sentiment_and_evaluator(response)
1081
1144
 
@@ -1089,8 +1152,12 @@ class Agent:
1089
1152
  self.save()
1090
1153
 
1091
1154
  logger.error(
1092
- f"Attempt {attempt+1}: Error generating"
1093
- f" response: {e}"
1155
+ f"Attempt {attempt+1}/{self.max_retries}: Error generating response in loop {loop_count} for agent '{self.agent_name}': {str(e)} | "
1156
+ f"Error type: {type(e).__name__}, Error details: {e.__dict__ if hasattr(e, '__dict__') else 'No additional details'} | "
1157
+ f"Current task: '{task}', Agent state: max_loops={self.max_loops}, "
1158
+ f"model={getattr(self.llm, 'model_name', 'unknown')}, "
1159
+ f"temperature={getattr(self.llm, 'temperature', 'unknown')}"
1160
+ f"{f' | Traceback: {e.__traceback__}' if hasattr(e, '__traceback__') else ''}"
1094
1161
  )
1095
1162
  attempt += 1
1096
1163
 
@@ -1112,13 +1179,19 @@ class Agent:
1112
1179
  self.stopping_condition is not None
1113
1180
  and self._check_stopping_condition(response)
1114
1181
  ):
1115
- logger.info("Stopping condition met.")
1182
+ logger.info(
1183
+ f"Agent '{self.agent_name}' stopping condition met. "
1184
+ f"Loop: {loop_count}, Response length: {len(str(response)) if response else 0}"
1185
+ )
1116
1186
  break
1117
1187
  elif (
1118
1188
  self.stopping_func is not None
1119
1189
  and self.stopping_func(response)
1120
1190
  ):
1121
- logger.info("Stopping function met.")
1191
+ logger.info(
1192
+ f"Agent '{self.agent_name}' stopping function condition met. "
1193
+ f"Loop: {loop_count}, Response length: {len(str(response)) if response else 0}"
1194
+ )
1122
1195
  break
1123
1196
 
1124
1197
  if self.interactive:
@@ -1130,7 +1203,10 @@ class Agent:
1130
1203
  user_input.lower()
1131
1204
  == self.custom_exit_command.lower()
1132
1205
  ):
1133
- print("Exiting as per user request.")
1206
+ self.pretty_print(
1207
+ "Exiting as per user request.",
1208
+ loop_count=loop_count,
1209
+ )
1134
1210
  break
1135
1211
 
1136
1212
  self.short_memory.add(
@@ -1162,14 +1238,27 @@ class Agent:
1162
1238
  self._handle_run_error(error)
1163
1239
 
1164
1240
  def __handle_run_error(self, error: any):
1241
+ import traceback
1242
+
1165
1243
  log_agent_data(self.to_dict())
1166
1244
 
1167
1245
  if self.autosave is True:
1168
1246
  self.save()
1169
1247
 
1170
- logger.info(
1171
- f"Error detected running your agent {self.agent_name} \n Error {error} \n Optimize your input parameters and or add an issue on the swarms github and contact our team on discord for support ;) "
1248
+ # Get detailed error information
1249
+ error_type = type(error).__name__
1250
+ error_message = str(error)
1251
+ traceback_info = traceback.format_exc()
1252
+
1253
+ logger.error(
1254
+ f"Error detected running your agent {self.agent_name}\n"
1255
+ f"Error Type: {error_type}\n"
1256
+ f"Error Message: {error_message}\n"
1257
+ f"Traceback:\n{traceback_info}\n"
1258
+ f"Agent State: {self.to_dict()}\n"
1259
+ f"Optimize your input parameters and or add an issue on the swarms github and contact our team on discord for support ;)"
1172
1260
  )
1261
+
1173
1262
  raise error
1174
1263
 
1175
1264
  def _handle_run_error(self, error: any):
@@ -1231,12 +1320,6 @@ class Agent:
1231
1320
  self,
1232
1321
  task: Optional[str] = None,
1233
1322
  img: Optional[str] = None,
1234
- is_last: bool = False,
1235
- device: str = "cpu", # gpu
1236
- device_id: int = 1,
1237
- all_cores: bool = True,
1238
- do_not_use_cluster_ops: bool = True,
1239
- all_gpus: bool = False,
1240
1323
  *args,
1241
1324
  **kwargs,
1242
1325
  ) -> Any:
@@ -1245,10 +1328,6 @@ class Agent:
1245
1328
  Args:
1246
1329
  task (Optional[str]): The task to be performed. Defaults to None.
1247
1330
  img (Optional[str]): The image to be processed. Defaults to None.
1248
- is_last (bool): Indicates if this is the last task. Defaults to False.
1249
- device (str): The device to use for execution. Defaults to "cpu".
1250
- device_id (int): The ID of the GPU to use if device is set to "gpu". Defaults to 0.
1251
- all_cores (bool): If True, uses all available CPU cores. Defaults to True.
1252
1331
  """
1253
1332
  try:
1254
1333
  return self.run(
@@ -1339,10 +1418,15 @@ class Agent:
1339
1418
  # Get the current conversation history
1340
1419
  history = self.short_memory.get_str()
1341
1420
 
1421
+ plan_prompt = f"Create a comprehensive step-by-step plan to complete the following task: \n\n {task}"
1422
+
1342
1423
  # Construct the planning prompt by combining history, planning prompt, and task
1343
- planning_prompt = (
1344
- f"{history}\n\n{self.planning_prompt}\n\nTask: {task}"
1345
- )
1424
+ if exists(self.planning_prompt):
1425
+ planning_prompt = f"{history}\n\n{self.planning_prompt}\n\nTask: {task}"
1426
+ else:
1427
+ planning_prompt = (
1428
+ f"{history}\n\n{plan_prompt}\n\nTask: {task}"
1429
+ )
1346
1430
 
1347
1431
  # Generate the plan using the LLM
1348
1432
  plan = self.llm.run(task=planning_prompt, *args, **kwargs)
@@ -1350,9 +1434,6 @@ class Agent:
1350
1434
  # Store the generated plan in short-term memory
1351
1435
  self.short_memory.add(role=self.agent_name, content=plan)
1352
1436
 
1353
- logger.info(
1354
- f"Successfully created plan for task: {task[:50]}..."
1355
- )
1356
1437
  return None
1357
1438
 
1358
1439
  except Exception as error:
@@ -1477,10 +1558,13 @@ class Agent:
1477
1558
  f"The model '{self.model_name}' does not support function calling. Please use a model that supports function calling."
1478
1559
  )
1479
1560
 
1480
- if self.max_tokens > get_max_tokens(self.model_name):
1481
- raise AgentInitializationError(
1482
- 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."
1483
- )
1561
+ try:
1562
+ if self.max_tokens > get_max_tokens(self.model_name):
1563
+ raise AgentInitializationError(
1564
+ 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."
1565
+ )
1566
+ except Exception:
1567
+ pass
1484
1568
 
1485
1569
  if self.model_name not in model_list:
1486
1570
  logger.warning(
@@ -2424,7 +2508,12 @@ class Agent:
2424
2508
  return None
2425
2509
 
2426
2510
  def call_llm(
2427
- self, task: str, img: Optional[str] = None, *args, **kwargs
2511
+ self,
2512
+ task: str,
2513
+ img: Optional[str] = None,
2514
+ current_loop: int = 0,
2515
+ *args,
2516
+ **kwargs,
2428
2517
  ) -> str:
2429
2518
  """
2430
2519
  Calls the appropriate method on the `llm` object based on the given task.
@@ -2446,14 +2535,81 @@ class Agent:
2446
2535
  """
2447
2536
 
2448
2537
  try:
2449
- if img is not None:
2450
- out = self.llm.run(
2451
- task=task, img=img, *args, **kwargs
2452
- )
2538
+ # Set streaming parameter in LLM if streaming is enabled
2539
+ if self.streaming_on and hasattr(self.llm, "stream"):
2540
+ original_stream = self.llm.stream
2541
+ self.llm.stream = True
2542
+
2543
+ if img is not None:
2544
+ streaming_response = self.llm.run(
2545
+ task=task, img=img, *args, **kwargs
2546
+ )
2547
+ else:
2548
+ streaming_response = self.llm.run(
2549
+ task=task, *args, **kwargs
2550
+ )
2551
+
2552
+ # If we get a streaming response, handle it with the new streaming panel
2553
+ if hasattr(
2554
+ streaming_response, "__iter__"
2555
+ ) and not isinstance(streaming_response, str):
2556
+ # Check print_on parameter for different streaming behaviors
2557
+ if self.print_on is False:
2558
+ # Silent streaming - no printing, just collect chunks
2559
+ chunks = []
2560
+ for chunk in streaming_response:
2561
+ if (
2562
+ hasattr(chunk, "choices")
2563
+ and chunk.choices[0].delta.content
2564
+ ):
2565
+ content = chunk.choices[
2566
+ 0
2567
+ ].delta.content
2568
+ chunks.append(content)
2569
+ complete_response = "".join(chunks)
2570
+ else:
2571
+ # Collect chunks for conversation saving
2572
+ collected_chunks = []
2573
+
2574
+ def on_chunk_received(chunk: str):
2575
+ """Callback to collect chunks as they arrive"""
2576
+ collected_chunks.append(chunk)
2577
+ # Optional: Save each chunk to conversation in real-time
2578
+ # This creates a more detailed conversation history
2579
+ if self.verbose:
2580
+ logger.debug(
2581
+ f"Streaming chunk received: {chunk[:50]}..."
2582
+ )
2583
+
2584
+ # Use the streaming panel to display and collect the response
2585
+ complete_response = formatter.print_streaming_panel(
2586
+ streaming_response,
2587
+ title=f"🤖 Agent: {self.agent_name} Loops: {current_loop}",
2588
+ style=None, # Use random color like non-streaming approach
2589
+ collect_chunks=True,
2590
+ on_chunk_callback=on_chunk_received,
2591
+ )
2592
+
2593
+ # Restore original stream setting
2594
+ self.llm.stream = original_stream
2595
+
2596
+ # Return the complete response for further processing
2597
+ return complete_response
2598
+ else:
2599
+ # Restore original stream setting
2600
+ self.llm.stream = original_stream
2601
+ return streaming_response
2453
2602
  else:
2454
- out = self.llm.run(task=task, *args, **kwargs)
2603
+ # Non-streaming call
2604
+ if img is not None:
2605
+ out = self.llm.run(
2606
+ task=task, img=img, *args, **kwargs
2607
+ )
2608
+ else:
2609
+ out = self.llm.run(task=task, *args, **kwargs)
2610
+
2611
+ return out
2455
2612
 
2456
- return out
2457
2613
  except AgentLLMError as e:
2458
2614
  logger.error(
2459
2615
  f"Error calling LLM: {e}. Task: {task}, Args: {args}, Kwargs: {kwargs}"
@@ -2479,7 +2635,8 @@ class Agent:
2479
2635
  self,
2480
2636
  task: Optional[Union[str, Any]] = None,
2481
2637
  img: Optional[str] = None,
2482
- scheduled_run_date: Optional[datetime] = None,
2638
+ imgs: Optional[List[str]] = None,
2639
+ correct_answer: Optional[str] = None,
2483
2640
  *args,
2484
2641
  **kwargs,
2485
2642
  ) -> Any:
@@ -2493,11 +2650,7 @@ class Agent:
2493
2650
  Args:
2494
2651
  task (Optional[str], optional): The task to be executed. Defaults to None.
2495
2652
  img (Optional[str], optional): The image to be processed. Defaults to None.
2496
- device (str, optional): The device to use for execution. Defaults to "cpu".
2497
- device_id (int, optional): The ID of the GPU to use if device is set to "gpu". Defaults to 0.
2498
- all_cores (bool, optional): If True, uses all available CPU cores. Defaults to True.
2499
- scheduled_run_date (Optional[datetime], optional): The date and time to schedule the task. Defaults to None.
2500
- do_not_use_cluster_ops (bool, optional): If True, does not use cluster ops. Defaults to False.
2653
+ imgs (Optional[List[str]], optional): The list of images to be processed. Defaults to None.
2501
2654
  *args: Additional positional arguments to be passed to the execution method.
2502
2655
  **kwargs: Additional keyword arguments to be passed to the execution method.
2503
2656
 
@@ -2510,21 +2663,28 @@ class Agent:
2510
2663
  """
2511
2664
 
2512
2665
  if not isinstance(task, str):
2513
- task = any_to_str(task)
2514
-
2515
- if scheduled_run_date:
2516
- while datetime.now() < scheduled_run_date:
2517
- time.sleep(
2518
- 1
2519
- ) # Sleep for a short period to avoid busy waiting
2666
+ task = format_data_structure(task)
2520
2667
 
2521
2668
  try:
2522
- output = self._run(
2523
- task=task,
2524
- img=img,
2525
- *args,
2526
- **kwargs,
2527
- )
2669
+ if exists(imgs):
2670
+ output = self.run_multiple_images(
2671
+ task=task, imgs=imgs, *args, **kwargs
2672
+ )
2673
+ elif exists(correct_answer):
2674
+ output = self.continuous_run_with_answer(
2675
+ task=task,
2676
+ img=img,
2677
+ correct_answer=correct_answer,
2678
+ *args,
2679
+ **kwargs,
2680
+ )
2681
+ else:
2682
+ output = self._run(
2683
+ task=task,
2684
+ img=img,
2685
+ *args,
2686
+ **kwargs,
2687
+ )
2528
2688
 
2529
2689
  return output
2530
2690
 
@@ -2664,21 +2824,23 @@ class Agent:
2664
2824
  return self.role
2665
2825
 
2666
2826
  def pretty_print(self, response: str, loop_count: int):
2667
- if self.no_print is False:
2668
- if self.streaming_on is True:
2669
- # self.stream_response(response)
2670
- formatter.print_panel_token_by_token(
2671
- f"{self.agent_name}: {response}",
2672
- title=f"Agent Name: {self.agent_name} [Max Loops: {loop_count}]",
2673
- )
2674
- elif self.no_print is True:
2675
- pass
2676
- else:
2677
- # logger.info(f"Response: {response}")
2678
- formatter.print_panel(
2679
- f"{self.agent_name}: {response}",
2680
- f"Agent Name {self.agent_name} [Max Loops: {loop_count} ]",
2681
- )
2827
+ # if self.print_on is False:
2828
+ # if self.streaming_on is True:
2829
+ # # Skip printing here since real streaming is handled in call_llm
2830
+ # # This avoids double printing when streaming_on=True
2831
+ # pass
2832
+ # elif self.print_on is False:
2833
+ # pass
2834
+ # else:
2835
+ # # logger.info(f"Response: {response}")
2836
+ # formatter.print_panel(
2837
+ # response,
2838
+ # f"Agent Name {self.agent_name} [Max Loops: {loop_count} ]",
2839
+ # )
2840
+ formatter.print_panel(
2841
+ response,
2842
+ f"Agent Name {self.agent_name} [Max Loops: {loop_count} ]",
2843
+ )
2682
2844
 
2683
2845
  def parse_llm_output(self, response: Any):
2684
2846
  """Parse and standardize the output from the LLM.
@@ -2781,7 +2943,7 @@ class Agent:
2781
2943
  )
2782
2944
  # tool_response = format_data_structure(tool_response)
2783
2945
 
2784
- print(f"Multiple MCP Tool Response: {tool_response}")
2946
+ # print(f"Multiple MCP Tool Response: {tool_response}")
2785
2947
  else:
2786
2948
  raise AgentMCPConnectionError(
2787
2949
  "mcp_url must be either a string URL or MCPConnection object"
@@ -2791,10 +2953,10 @@ class Agent:
2791
2953
  # execute_tool_call_simple returns a string directly, not an object with content attribute
2792
2954
  text_content = f"MCP Tool Response: \n\n {json.dumps(tool_response, indent=2)}"
2793
2955
 
2794
- if self.no_print is False:
2956
+ if self.print_on is True:
2795
2957
  formatter.print_panel(
2796
- text_content,
2797
- "MCP Tool Response: 🛠️",
2958
+ content=text_content,
2959
+ title="MCP Tool Response: 🛠️",
2798
2960
  style="green",
2799
2961
  )
2800
2962
 
@@ -2818,7 +2980,8 @@ class Agent:
2818
2980
  # Fallback: provide a default summary
2819
2981
  summary = "I successfully executed the MCP tool and retrieved the information above."
2820
2982
 
2821
- self.pretty_print(summary, loop_count=current_loop)
2983
+ if self.print_on is True:
2984
+ self.pretty_print(summary, loop_count=current_loop)
2822
2985
 
2823
2986
  # Add to the memory
2824
2987
  self.short_memory.add(
@@ -2834,7 +2997,7 @@ class Agent:
2834
2997
  temperature=self.temperature,
2835
2998
  max_tokens=self.max_tokens,
2836
2999
  system_prompt=self.system_prompt,
2837
- stream=self.streaming_on,
3000
+ stream=False, # Always disable streaming for tool summaries
2838
3001
  tools_list_dictionary=None,
2839
3002
  parallel_tool_calls=False,
2840
3003
  base_url=self.llm_base_url,
@@ -2842,22 +3005,38 @@ class Agent:
2842
3005
  )
2843
3006
 
2844
3007
  def execute_tools(self, response: any, loop_count: int):
3008
+ # Handle None response gracefully
3009
+ if response is None:
3010
+ logger.warning(
3011
+ f"Cannot execute tools with None response in loop {loop_count}. "
3012
+ "This may indicate the LLM did not return a valid response."
3013
+ )
3014
+ return
2845
3015
 
2846
- output = (
2847
- self.tool_struct.execute_function_calls_from_api_response(
3016
+ try:
3017
+ output = self.tool_struct.execute_function_calls_from_api_response(
3018
+ response
3019
+ )
3020
+ except Exception as e:
3021
+ # Retry the tool call
3022
+ output = self.tool_struct.execute_function_calls_from_api_response(
2848
3023
  response
2849
3024
  )
2850
- )
3025
+
3026
+ if output is None:
3027
+ logger.error(f"Error executing tools: {e}")
3028
+ raise e
2851
3029
 
2852
3030
  self.short_memory.add(
2853
3031
  role="Tool Executor",
2854
3032
  content=format_data_structure(output),
2855
3033
  )
2856
3034
 
2857
- self.pretty_print(
2858
- f"{format_data_structure(output)}",
2859
- loop_count,
2860
- )
3035
+ if self.print_on is True:
3036
+ self.pretty_print(
3037
+ f"Tool Executed Successfully [{time.strftime('%H:%M:%S')}]",
3038
+ loop_count,
3039
+ )
2861
3040
 
2862
3041
  # Now run the LLM again without tools - create a temporary LLM instance
2863
3042
  # instead of modifying the cached one
@@ -2881,10 +3060,192 @@ class Agent:
2881
3060
  content=tool_response,
2882
3061
  )
2883
3062
 
2884
- self.pretty_print(
2885
- f"{tool_response}",
2886
- loop_count,
2887
- )
3063
+ if self.print_on is True:
3064
+ self.pretty_print(
3065
+ tool_response,
3066
+ loop_count,
3067
+ )
2888
3068
 
2889
3069
  def list_output_types(self):
2890
3070
  return OutputType
3071
+
3072
+ def run_multiple_images(
3073
+ self, task: str, imgs: List[str], *args, **kwargs
3074
+ ):
3075
+ """
3076
+ Run the agent with multiple images using concurrent processing.
3077
+
3078
+ Args:
3079
+ task (str): The task to be performed on each image.
3080
+ imgs (List[str]): List of image paths or URLs to process.
3081
+ *args: Additional positional arguments to pass to the agent's run method.
3082
+ **kwargs: Additional keyword arguments to pass to the agent's run method.
3083
+
3084
+ Returns:
3085
+ List[Any]: A list of outputs generated for each image in the same order as the input images.
3086
+
3087
+ Examples:
3088
+ >>> agent = Agent()
3089
+ >>> outputs = agent.run_multiple_images(
3090
+ ... task="Describe what you see in this image",
3091
+ ... imgs=["image1.jpg", "image2.png", "image3.jpeg"]
3092
+ ... )
3093
+ >>> print(f"Processed {len(outputs)} images")
3094
+ Processed 3 images
3095
+
3096
+ Raises:
3097
+ Exception: If an error occurs while processing any of the images.
3098
+ """
3099
+ # Calculate number of workers as 95% of available CPU cores
3100
+ cpu_count = os.cpu_count()
3101
+ max_workers = max(1, int(cpu_count * 0.95))
3102
+
3103
+ # Use ThreadPoolExecutor for concurrent processing
3104
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
3105
+ # Submit all image processing tasks
3106
+ future_to_img = {
3107
+ executor.submit(
3108
+ self.run, task=task, img=img, *args, **kwargs
3109
+ ): img
3110
+ for img in imgs
3111
+ }
3112
+
3113
+ # Collect results in order
3114
+ outputs = []
3115
+ for future in future_to_img:
3116
+ try:
3117
+ output = future.result()
3118
+ outputs.append(output)
3119
+ except Exception as e:
3120
+ logger.error(f"Error processing image: {e}")
3121
+ outputs.append(
3122
+ None
3123
+ ) # or raise the exception based on your preference
3124
+
3125
+ # Combine the outputs into a single string if summarization is enabled
3126
+ if self.summarize_multiple_images is True:
3127
+ output = "\n".join(outputs)
3128
+
3129
+ prompt = f"""
3130
+ You have already analyzed {len(outputs)} images and provided detailed descriptions for each one.
3131
+ Now, based on your previous analysis of these images, create a comprehensive report that:
3132
+
3133
+ 1. Synthesizes the key findings across all images
3134
+ 2. Identifies common themes, patterns, or relationships between the images
3135
+ 3. Provides an overall summary that captures the most important insights
3136
+ 4. Highlights any notable differences or contrasts between the images
3137
+
3138
+ Here are your previous analyses of the images:
3139
+ {output}
3140
+
3141
+ Please create a well-structured report that brings together your insights from all {len(outputs)} images.
3142
+ """
3143
+
3144
+ outputs = self.run(task=prompt, *args, **kwargs)
3145
+
3146
+ return outputs
3147
+
3148
+ def continuous_run_with_answer(
3149
+ self,
3150
+ task: str,
3151
+ img: Optional[str] = None,
3152
+ correct_answer: str = None,
3153
+ max_attempts: int = 10,
3154
+ ):
3155
+ """
3156
+ Run the agent with the task until the correct answer is provided.
3157
+
3158
+ Args:
3159
+ task (str): The task to be performed
3160
+ correct_answer (str): The correct answer that must be found in the response
3161
+ max_attempts (int): Maximum number of attempts before giving up (default: 10)
3162
+
3163
+ Returns:
3164
+ str: The response containing the correct answer
3165
+
3166
+ Raises:
3167
+ Exception: If max_attempts is reached without finding the correct answer
3168
+ """
3169
+ attempts = 0
3170
+
3171
+ while attempts < max_attempts:
3172
+ attempts += 1
3173
+
3174
+ if self.verbose:
3175
+ logger.info(
3176
+ f"Attempt {attempts}/{max_attempts} to find correct answer"
3177
+ )
3178
+
3179
+ response = self._run(task=task, img=img)
3180
+
3181
+ # Check if the correct answer is in the response (case-insensitive)
3182
+ if correct_answer.lower() in response.lower():
3183
+ if self.verbose:
3184
+ logger.info(
3185
+ f"Correct answer found on attempt {attempts}"
3186
+ )
3187
+ return response
3188
+ else:
3189
+ # Add feedback to help guide the agent
3190
+ feedback = "Your previous response was incorrect. Think carefully about the question and ensure your response directly addresses what was asked."
3191
+ self.short_memory.add(role="User", content=feedback)
3192
+
3193
+ if self.verbose:
3194
+ logger.info(
3195
+ f"Correct answer not found. Expected: '{correct_answer}'"
3196
+ )
3197
+
3198
+ # If we reach here, we've exceeded max_attempts
3199
+ raise Exception(
3200
+ f"Failed to find correct answer '{correct_answer}' after {max_attempts} attempts"
3201
+ )
3202
+
3203
+ def tool_execution_retry(self, response: any, loop_count: int):
3204
+ """
3205
+ Execute tools with retry logic for handling failures.
3206
+
3207
+ This method attempts to execute tools based on the LLM response. If the response
3208
+ is None, it logs a warning and skips execution. If an exception occurs during
3209
+ tool execution, it logs the error with full traceback and retries the operation
3210
+ using the configured retry attempts.
3211
+
3212
+ Args:
3213
+ response (any): The response from the LLM that may contain tool calls to execute.
3214
+ Can be None if the LLM failed to provide a valid response.
3215
+ loop_count (int): The current iteration loop number for logging and debugging purposes.
3216
+
3217
+ Returns:
3218
+ None
3219
+
3220
+ Raises:
3221
+ Exception: Re-raises any exception that occurs during tool execution after
3222
+ all retry attempts have been exhausted.
3223
+
3224
+ Note:
3225
+ - Uses self.tool_retry_attempts for the maximum number of retry attempts
3226
+ - Logs detailed error information including agent name and loop count
3227
+ - Skips execution gracefully if response is None
3228
+ """
3229
+ try:
3230
+ if response is not None:
3231
+ self.execute_tools(
3232
+ response=response,
3233
+ loop_count=loop_count,
3234
+ )
3235
+ else:
3236
+ logger.warning(
3237
+ f"Agent '{self.agent_name}' received None response from LLM in loop {loop_count}. "
3238
+ f"This may indicate an issue with the model or prompt. Skipping tool execution."
3239
+ )
3240
+ except Exception as e:
3241
+ logger.error(
3242
+ f"Agent '{self.agent_name}' encountered error during tool execution in loop {loop_count}: {str(e)}. "
3243
+ f"Full traceback: {traceback.format_exc()}. "
3244
+ f"Attempting to retry tool execution with 3 attempts"
3245
+ )
3246
+ retry_function(
3247
+ self.execute_tools,
3248
+ response=response,
3249
+ loop_count=loop_count,
3250
+ max_retries=self.tool_retry_attempts,
3251
+ )