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/cli/onboarding_process.py +1 -3
- swarms/prompts/collaborative_prompts.py +177 -0
- swarms/structs/agent.py +434 -128
- swarms/structs/concurrent_workflow.py +70 -196
- swarms/structs/conversation.py +6 -0
- swarms/structs/csv_to_agent.py +1 -3
- swarms/structs/interactive_groupchat.py +319 -12
- swarms/structs/ma_utils.py +25 -6
- swarms/structs/mixture_of_agents.py +88 -113
- swarms/structs/swarm_router.py +148 -187
- swarms/telemetry/__init__.py +4 -22
- swarms/telemetry/log_executions.py +43 -0
- swarms/telemetry/main.py +63 -325
- swarms/tools/__init__.py +10 -0
- swarms/tools/base_tool.py +15 -6
- swarms/tools/mcp_client_call.py +508 -0
- swarms/tools/py_func_to_openai_func_str.py +0 -1
- swarms/utils/auto_download_check_packages.py +4 -3
- swarms/utils/formatter.py +130 -13
- swarms/utils/history_output_formatter.py +2 -0
- swarms/utils/litellm_wrapper.py +5 -1
- swarms/utils/output_types.py +1 -1
- swarms-7.9.0.dist-info/METADATA +626 -0
- {swarms-7.8.8.dist-info → swarms-7.9.0.dist-info}/RECORD +27 -25
- swarms-7.8.8.dist-info/METADATA +0 -2119
- {swarms-7.8.8.dist-info → swarms-7.9.0.dist-info}/LICENSE +0 -0
- {swarms-7.8.8.dist-info → swarms-7.9.0.dist-info}/WHEEL +0 -0
- {swarms-7.8.8.dist-info → swarms-7.9.0.dist-info}/entry_points.txt +0 -0
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
|
-
|
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 =
|
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.
|
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=
|
626
|
-
content=
|
639
|
+
role=self.agent_name,
|
640
|
+
content=self.tools_list_dictionary,
|
627
641
|
)
|
628
642
|
|
629
643
|
def short_memory_init(self):
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
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
|
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
|
-
|
761
|
-
|
762
|
-
|
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
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
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
|
-
#
|
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=
|
1090
|
+
content=response,
|
1036
1091
|
)
|
1037
1092
|
|
1038
1093
|
# Print
|
1039
1094
|
self.pretty_print(response, loop_count)
|
1040
1095
|
|
1041
|
-
#
|
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
|
-
|
1048
|
-
|
1049
|
-
|
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
|
1054
|
-
self.
|
1116
|
+
if (
|
1117
|
+
exists(self.mcp_url)
|
1118
|
+
or exists(self.mcp_config)
|
1119
|
+
or exists(self.mcp_urls)
|
1055
1120
|
):
|
1056
|
-
|
1057
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
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
|
-
#
|
1314
|
-
self.
|
1315
|
-
|
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(
|
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
|
-
|
1440
|
-
|
1441
|
-
|
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,
|
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
|
2410
|
-
|
2411
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
2483
|
-
|
2484
|
-
|
2485
|
-
|
2486
|
-
|
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.
|
2793
|
+
if self.print_on is False:
|
2628
2794
|
if self.streaming_on is True:
|
2629
|
-
#
|
2630
|
-
|
2631
|
-
|
2632
|
-
|
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
|
-
|
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
|
-
|
2678
|
-
out = format_data_structure(response)
|
2679
|
-
else:
|
2680
|
-
out = str(response)
|
2681
|
-
|
2682
|
-
return out
|
2841
|
+
return response
|
2683
2842
|
|
2684
|
-
except
|
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.
|
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=
|
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
|
+
)
|