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/cli/onboarding_process.py +1 -3
- swarms/prompts/collaborative_prompts.py +177 -0
- swarms/structs/__init__.py +11 -1
- swarms/structs/agent.py +488 -127
- swarms/structs/concurrent_workflow.py +70 -196
- swarms/structs/conversation.py +103 -25
- swarms/structs/interactive_groupchat.py +815 -108
- swarms/structs/ma_utils.py +25 -6
- swarms/structs/mixture_of_agents.py +88 -113
- swarms/structs/swarm_router.py +155 -195
- swarms/telemetry/__init__.py +4 -18
- swarms/telemetry/log_executions.py +43 -0
- swarms/telemetry/main.py +53 -217
- swarms/tools/base_tool.py +8 -3
- swarms/utils/formatter.py +130 -13
- swarms/utils/litellm_wrapper.py +7 -1
- swarms/utils/retry_func.py +66 -0
- swarms-7.9.1.dist-info/METADATA +626 -0
- {swarms-7.8.9.dist-info → swarms-7.9.1.dist-info}/RECORD +22 -19
- swarms-7.8.9.dist-info/METADATA +0 -2119
- {swarms-7.8.9.dist-info → swarms-7.9.1.dist-info}/LICENSE +0 -0
- {swarms-7.8.9.dist-info → swarms-7.9.1.dist-info}/WHEEL +0 -0
- {swarms-7.8.9.dist-info → swarms-7.9.1.dist-info}/entry_points.txt +0 -0
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
|
-
|
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.
|
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=
|
649
|
+
role=self.agent_name,
|
634
650
|
content=self.tools_list_dictionary,
|
635
651
|
)
|
636
652
|
|
637
653
|
def short_memory_init(self):
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
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.
|
785
|
-
|
786
|
-
|
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
|
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
|
-
|
1034
|
-
|
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
|
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.
|
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
|
-
|
1058
|
-
|
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
|
-
|
1076
|
-
|
1077
|
-
|
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"
|
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(
|
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(
|
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
|
-
|
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
|
-
|
1171
|
-
|
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
|
-
|
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
|
-
|
1481
|
-
|
1482
|
-
|
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,
|
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
|
2450
|
-
|
2451
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
2523
|
-
|
2524
|
-
|
2525
|
-
|
2526
|
-
|
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.
|
2668
|
-
|
2669
|
-
|
2670
|
-
|
2671
|
-
|
2672
|
-
|
2673
|
-
|
2674
|
-
|
2675
|
-
|
2676
|
-
|
2677
|
-
|
2678
|
-
|
2679
|
-
|
2680
|
-
|
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.
|
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.
|
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=
|
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
|
-
|
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.
|
2858
|
-
|
2859
|
-
|
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.
|
2885
|
-
|
2886
|
-
|
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
|
+
)
|