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
@@ -1,5 +1,6 @@
|
|
1
1
|
import re
|
2
|
-
|
2
|
+
import random
|
3
|
+
from typing import Callable, List, Union, Optional
|
3
4
|
|
4
5
|
from loguru import logger
|
5
6
|
|
@@ -35,6 +36,156 @@ class InvalidTaskFormatError(InteractiveGroupChatError):
|
|
35
36
|
pass
|
36
37
|
|
37
38
|
|
39
|
+
class InvalidSpeakerFunctionError(InteractiveGroupChatError):
|
40
|
+
"""Raised when an invalid speaker function is provided"""
|
41
|
+
|
42
|
+
pass
|
43
|
+
|
44
|
+
|
45
|
+
# Built-in speaker functions
|
46
|
+
def round_robin_speaker(
|
47
|
+
agents: List[str], current_index: int = 0
|
48
|
+
) -> str:
|
49
|
+
"""
|
50
|
+
Round robin speaker function that cycles through agents in order.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
agents: List of agent names
|
54
|
+
current_index: Current position in the cycle
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Next agent name in the round robin sequence
|
58
|
+
"""
|
59
|
+
if not agents:
|
60
|
+
raise ValueError("No agents provided for round robin")
|
61
|
+
return agents[current_index % len(agents)]
|
62
|
+
|
63
|
+
|
64
|
+
def random_speaker(agents: List[str], **kwargs) -> str:
|
65
|
+
"""
|
66
|
+
Random speaker function that selects agents randomly.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
agents: List of agent names
|
70
|
+
**kwargs: Additional arguments (ignored)
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
Randomly selected agent name
|
74
|
+
"""
|
75
|
+
if not agents:
|
76
|
+
raise ValueError("No agents provided for random selection")
|
77
|
+
return random.choice(agents)
|
78
|
+
|
79
|
+
|
80
|
+
def priority_speaker(
|
81
|
+
agents: List[str], priorities: dict, **kwargs
|
82
|
+
) -> str:
|
83
|
+
"""
|
84
|
+
Priority-based speaker function that selects agents based on priority weights.
|
85
|
+
|
86
|
+
Args:
|
87
|
+
agents: List of agent names
|
88
|
+
priorities: Dictionary mapping agent names to priority weights
|
89
|
+
**kwargs: Additional arguments (ignored)
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Selected agent name based on priority weights
|
93
|
+
"""
|
94
|
+
if not agents:
|
95
|
+
raise ValueError("No agents provided for priority selection")
|
96
|
+
|
97
|
+
# Filter agents that exist in the priorities dict
|
98
|
+
available_agents = [
|
99
|
+
agent for agent in agents if agent in priorities
|
100
|
+
]
|
101
|
+
if not available_agents:
|
102
|
+
# Fallback to random if no priorities match
|
103
|
+
return random.choice(agents)
|
104
|
+
|
105
|
+
# Calculate total weight
|
106
|
+
total_weight = sum(
|
107
|
+
priorities[agent] for agent in available_agents
|
108
|
+
)
|
109
|
+
if total_weight == 0:
|
110
|
+
return random.choice(available_agents)
|
111
|
+
|
112
|
+
# Select based on weighted probability
|
113
|
+
rand_val = random.uniform(0, total_weight)
|
114
|
+
current_weight = 0
|
115
|
+
|
116
|
+
for agent in available_agents:
|
117
|
+
current_weight += priorities[agent]
|
118
|
+
if rand_val <= current_weight:
|
119
|
+
return agent
|
120
|
+
|
121
|
+
return available_agents[-1] # Fallback
|
122
|
+
|
123
|
+
|
124
|
+
def random_dynamic_speaker(
|
125
|
+
agents: List[str],
|
126
|
+
response: str = "",
|
127
|
+
strategy: str = "parallel",
|
128
|
+
**kwargs,
|
129
|
+
) -> Union[str, List[str]]:
|
130
|
+
"""
|
131
|
+
Random dynamic speaker function that selects agents based on @mentions in responses.
|
132
|
+
|
133
|
+
This function works in two phases:
|
134
|
+
1. If no response is provided (first call), randomly selects an agent
|
135
|
+
2. If a response is provided, extracts @mentions and returns agent(s) based on strategy
|
136
|
+
|
137
|
+
Args:
|
138
|
+
agents: List of available agent names
|
139
|
+
response: The response from the previous agent (may contain @mentions)
|
140
|
+
strategy: How to handle multiple mentions - "sequential" or "parallel"
|
141
|
+
**kwargs: Additional arguments (ignored)
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
For sequential strategy: str (single agent name)
|
145
|
+
For parallel strategy: List[str] (list of agent names)
|
146
|
+
"""
|
147
|
+
if not agents:
|
148
|
+
raise ValueError(
|
149
|
+
"No agents provided for random dynamic selection"
|
150
|
+
)
|
151
|
+
|
152
|
+
# If no response provided, randomly select first agent
|
153
|
+
if not response:
|
154
|
+
return random.choice(agents)
|
155
|
+
|
156
|
+
# Extract @mentions from the response
|
157
|
+
mentions = re.findall(r"@(\w+)", response)
|
158
|
+
|
159
|
+
# Filter mentions to only include valid agents
|
160
|
+
valid_mentions = [
|
161
|
+
mention for mention in mentions if mention in agents
|
162
|
+
]
|
163
|
+
|
164
|
+
if not valid_mentions:
|
165
|
+
# If no valid mentions, randomly select from all agents
|
166
|
+
return random.choice(agents)
|
167
|
+
|
168
|
+
# Handle multiple mentions based on strategy
|
169
|
+
if strategy == "sequential":
|
170
|
+
# Return the first mentioned agent for sequential execution
|
171
|
+
return valid_mentions[0]
|
172
|
+
elif strategy == "parallel":
|
173
|
+
# Return all mentioned agents for parallel execution
|
174
|
+
return valid_mentions
|
175
|
+
else:
|
176
|
+
raise ValueError(
|
177
|
+
f"Invalid strategy: {strategy}. Must be 'sequential' or 'parallel'"
|
178
|
+
)
|
179
|
+
|
180
|
+
|
181
|
+
speaker_functions = {
|
182
|
+
"round-robin-speaker": round_robin_speaker,
|
183
|
+
"random-speaker": random_speaker,
|
184
|
+
"priority-speaker": priority_speaker,
|
185
|
+
"random-dynamic-speaker": random_dynamic_speaker,
|
186
|
+
}
|
187
|
+
|
188
|
+
|
38
189
|
class InteractiveGroupChat:
|
39
190
|
"""
|
40
191
|
An interactive group chat system that enables conversations with multiple agents using @mentions.
|
@@ -49,6 +200,8 @@ class InteractiveGroupChat:
|
|
49
200
|
max_loops (int): Maximum number of conversation turns
|
50
201
|
conversation (Conversation): Stores the chat history
|
51
202
|
agent_map (Dict[str, Union[Agent, Callable]]): Mapping of agent names to their instances
|
203
|
+
speaker_function (Callable): Function to determine speaking order
|
204
|
+
speaker_state (dict): State for speaker functions that need it
|
52
205
|
|
53
206
|
Args:
|
54
207
|
name (str, optional): Name of the group chat. Defaults to "InteractiveGroupChat".
|
@@ -57,9 +210,38 @@ class InteractiveGroupChat:
|
|
57
210
|
max_loops (int, optional): Maximum conversation turns. Defaults to 1.
|
58
211
|
output_type (str, optional): Type of output format. Defaults to "string".
|
59
212
|
interactive (bool, optional): Whether to enable interactive terminal mode. Defaults to False.
|
213
|
+
speaker_function (Union[str, Callable], optional): Function to determine speaking order. Can be:
|
214
|
+
- A string name: "round-robin-speaker", "random-speaker", "priority-speaker", "random-dynamic-speaker"
|
215
|
+
- A custom callable function
|
216
|
+
- None (defaults to round_robin_speaker)
|
217
|
+
speaker_state (dict, optional): Initial state for speaker function. Defaults to empty dict.
|
60
218
|
|
61
219
|
Raises:
|
62
220
|
ValueError: If invalid initialization parameters are provided
|
221
|
+
InvalidSpeakerFunctionError: If the speaker function is invalid
|
222
|
+
|
223
|
+
Examples:
|
224
|
+
# Initialize with string-based speaker function
|
225
|
+
group_chat = InteractiveGroupChat(
|
226
|
+
agents=[agent1, agent2, agent3],
|
227
|
+
speaker_function="random-speaker"
|
228
|
+
)
|
229
|
+
|
230
|
+
# Initialize with priority speaker function
|
231
|
+
group_chat = InteractiveGroupChat(
|
232
|
+
agents=[agent1, agent2, agent3],
|
233
|
+
speaker_function="priority-speaker",
|
234
|
+
speaker_state={"priorities": {"agent1": 3, "agent2": 2, "agent3": 1}}
|
235
|
+
)
|
236
|
+
|
237
|
+
# Initialize with dynamic speaker function (agents mention each other)
|
238
|
+
group_chat = InteractiveGroupChat(
|
239
|
+
agents=[agent1, agent2, agent3],
|
240
|
+
speaker_function="random-dynamic-speaker"
|
241
|
+
)
|
242
|
+
|
243
|
+
# Change speaker function during runtime
|
244
|
+
group_chat.set_speaker_function("round-robin-speaker")
|
63
245
|
"""
|
64
246
|
|
65
247
|
def __init__(
|
@@ -71,6 +253,8 @@ class InteractiveGroupChat:
|
|
71
253
|
max_loops: int = 1,
|
72
254
|
output_type: str = "string",
|
73
255
|
interactive: bool = False,
|
256
|
+
speaker_function: Optional[Union[str, Callable]] = None,
|
257
|
+
speaker_state: Optional[dict] = None,
|
74
258
|
):
|
75
259
|
self.id = id
|
76
260
|
self.name = name
|
@@ -80,6 +264,33 @@ class InteractiveGroupChat:
|
|
80
264
|
self.output_type = output_type
|
81
265
|
self.interactive = interactive
|
82
266
|
|
267
|
+
# Speaker function configuration
|
268
|
+
if speaker_function is None:
|
269
|
+
self.speaker_function = round_robin_speaker
|
270
|
+
elif isinstance(speaker_function, str):
|
271
|
+
if speaker_function not in speaker_functions:
|
272
|
+
available_functions = ", ".join(
|
273
|
+
speaker_functions.keys()
|
274
|
+
)
|
275
|
+
raise InvalidSpeakerFunctionError(
|
276
|
+
f"Invalid speaker function: '{speaker_function}'. "
|
277
|
+
f"Available functions: {available_functions}"
|
278
|
+
)
|
279
|
+
self.speaker_function = speaker_functions[
|
280
|
+
speaker_function
|
281
|
+
]
|
282
|
+
elif callable(speaker_function):
|
283
|
+
self.speaker_function = speaker_function
|
284
|
+
else:
|
285
|
+
raise InvalidSpeakerFunctionError(
|
286
|
+
"Speaker function must be either a string, callable, or None"
|
287
|
+
)
|
288
|
+
|
289
|
+
self.speaker_state = speaker_state or {"current_index": 0}
|
290
|
+
|
291
|
+
# Validate speaker function
|
292
|
+
self._validate_speaker_function()
|
293
|
+
|
83
294
|
# Initialize conversation history
|
84
295
|
self.conversation = Conversation(time_enabled=True)
|
85
296
|
|
@@ -96,6 +307,256 @@ class InteractiveGroupChat:
|
|
96
307
|
self._setup_conversation_context()
|
97
308
|
self._update_agent_prompts()
|
98
309
|
|
310
|
+
def set_speaker_function(
|
311
|
+
self,
|
312
|
+
speaker_function: Union[str, Callable],
|
313
|
+
speaker_state: Optional[dict] = None,
|
314
|
+
) -> None:
|
315
|
+
"""
|
316
|
+
Set the speaker function using either a string name or a custom callable.
|
317
|
+
|
318
|
+
Args:
|
319
|
+
speaker_function: Either a string name of a predefined function or a custom callable
|
320
|
+
String options:
|
321
|
+
- "round-robin-speaker": Cycles through agents in order
|
322
|
+
- "random-speaker": Selects agents randomly
|
323
|
+
- "priority-speaker": Selects based on priority weights
|
324
|
+
- "random-dynamic-speaker": Randomly selects first agent, then follows @mentions in responses
|
325
|
+
Callable: Custom function that takes (agents: List[str], **kwargs) -> str
|
326
|
+
speaker_state: Optional state for the speaker function
|
327
|
+
|
328
|
+
Raises:
|
329
|
+
InvalidSpeakerFunctionError: If the speaker function is invalid
|
330
|
+
"""
|
331
|
+
if isinstance(speaker_function, str):
|
332
|
+
# Handle string-based speaker function
|
333
|
+
if speaker_function not in speaker_functions:
|
334
|
+
available_functions = ", ".join(
|
335
|
+
speaker_functions.keys()
|
336
|
+
)
|
337
|
+
raise InvalidSpeakerFunctionError(
|
338
|
+
f"Invalid speaker function: '{speaker_function}'. "
|
339
|
+
f"Available functions: {available_functions}"
|
340
|
+
)
|
341
|
+
self.speaker_function = speaker_functions[
|
342
|
+
speaker_function
|
343
|
+
]
|
344
|
+
logger.info(
|
345
|
+
f"Speaker function set to: {speaker_function}"
|
346
|
+
)
|
347
|
+
elif callable(speaker_function):
|
348
|
+
# Handle callable speaker function
|
349
|
+
self.speaker_function = speaker_function
|
350
|
+
logger.info(
|
351
|
+
f"Custom speaker function set to: {speaker_function.__name__}"
|
352
|
+
)
|
353
|
+
else:
|
354
|
+
raise InvalidSpeakerFunctionError(
|
355
|
+
"Speaker function must be either a string or a callable"
|
356
|
+
)
|
357
|
+
|
358
|
+
# Update speaker state if provided
|
359
|
+
if speaker_state:
|
360
|
+
self.speaker_state.update(speaker_state)
|
361
|
+
|
362
|
+
# Validate the speaker function
|
363
|
+
self._validate_speaker_function()
|
364
|
+
|
365
|
+
def set_priorities(self, priorities: dict) -> None:
|
366
|
+
"""
|
367
|
+
Set agent priorities for priority-based speaking order.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
priorities: Dictionary mapping agent names to priority weights
|
371
|
+
"""
|
372
|
+
self.speaker_state["priorities"] = priorities
|
373
|
+
logger.info(f"Agent priorities set: {priorities}")
|
374
|
+
|
375
|
+
def get_available_speaker_functions(self) -> List[str]:
|
376
|
+
"""
|
377
|
+
Get a list of available speaker function names.
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
List[str]: List of available speaker function names
|
381
|
+
"""
|
382
|
+
return list(speaker_functions.keys())
|
383
|
+
|
384
|
+
def get_current_speaker_function(self) -> str:
|
385
|
+
"""
|
386
|
+
Get the name of the current speaker function.
|
387
|
+
|
388
|
+
Returns:
|
389
|
+
str: Name of the current speaker function, or "custom" if it's a custom function
|
390
|
+
"""
|
391
|
+
for name, func in speaker_functions.items():
|
392
|
+
if self.speaker_function == func:
|
393
|
+
return name
|
394
|
+
return "custom"
|
395
|
+
|
396
|
+
def start_interactive_session(self):
|
397
|
+
"""
|
398
|
+
Start an interactive terminal session for chatting with agents.
|
399
|
+
|
400
|
+
This method creates a REPL (Read-Eval-Print Loop) that allows users to:
|
401
|
+
- Chat with agents using @mentions
|
402
|
+
- See available agents and their descriptions
|
403
|
+
- Exit the session using 'exit' or 'quit'
|
404
|
+
- Get help using 'help' or '?'
|
405
|
+
"""
|
406
|
+
if not self.interactive:
|
407
|
+
raise InteractiveGroupChatError(
|
408
|
+
"Interactive mode is not enabled. Initialize with interactive=True"
|
409
|
+
)
|
410
|
+
|
411
|
+
print(f"\nWelcome to {self.name}!")
|
412
|
+
print(f"Description: {self.description}")
|
413
|
+
print(
|
414
|
+
f"Current speaker function: {self.get_current_speaker_function()}"
|
415
|
+
)
|
416
|
+
print("\nAvailable agents:")
|
417
|
+
for name, agent in self.agent_map.items():
|
418
|
+
if isinstance(agent, Agent):
|
419
|
+
print(
|
420
|
+
f"- @{name}: {agent.system_prompt.splitlines()[0]}"
|
421
|
+
)
|
422
|
+
else:
|
423
|
+
print(f"- @{name}: Custom callable function")
|
424
|
+
|
425
|
+
print("\nCommands:")
|
426
|
+
print("- Type 'help' or '?' for help")
|
427
|
+
print("- Type 'exit' or 'quit' to end the session")
|
428
|
+
print("- Type 'speaker' to change speaker function")
|
429
|
+
print("- Use @agent_name to mention agents")
|
430
|
+
print("\nStart chatting:")
|
431
|
+
|
432
|
+
while True:
|
433
|
+
try:
|
434
|
+
# Get user input
|
435
|
+
user_input = input("\nYou: ").strip()
|
436
|
+
|
437
|
+
# Handle special commands
|
438
|
+
if user_input.lower() in ["exit", "quit"]:
|
439
|
+
print("Goodbye!")
|
440
|
+
break
|
441
|
+
|
442
|
+
if user_input.lower() in ["help", "?"]:
|
443
|
+
print("\nHelp:")
|
444
|
+
print("1. Mention agents using @agent_name")
|
445
|
+
print(
|
446
|
+
"2. You can mention multiple agents in one task"
|
447
|
+
)
|
448
|
+
print("3. Available agents:")
|
449
|
+
for name in self.agent_map:
|
450
|
+
print(f" - @{name}")
|
451
|
+
print(
|
452
|
+
"4. Type 'speaker' to change speaker function"
|
453
|
+
)
|
454
|
+
print(
|
455
|
+
"5. Type 'exit' or 'quit' to end the session"
|
456
|
+
)
|
457
|
+
continue
|
458
|
+
|
459
|
+
if user_input.lower() == "speaker":
|
460
|
+
print(
|
461
|
+
f"\nCurrent speaker function: {self.get_current_speaker_function()}"
|
462
|
+
)
|
463
|
+
print("Available speaker functions:")
|
464
|
+
for i, func_name in enumerate(
|
465
|
+
self.get_available_speaker_functions(), 1
|
466
|
+
):
|
467
|
+
print(f" {i}. {func_name}")
|
468
|
+
|
469
|
+
try:
|
470
|
+
choice = input(
|
471
|
+
"\nEnter the number or name of the speaker function: "
|
472
|
+
).strip()
|
473
|
+
|
474
|
+
# Try to parse as number first
|
475
|
+
try:
|
476
|
+
func_index = int(choice) - 1
|
477
|
+
if (
|
478
|
+
0
|
479
|
+
<= func_index
|
480
|
+
< len(
|
481
|
+
self.get_available_speaker_functions()
|
482
|
+
)
|
483
|
+
):
|
484
|
+
selected_func = self.get_available_speaker_functions()[
|
485
|
+
func_index
|
486
|
+
]
|
487
|
+
else:
|
488
|
+
print(
|
489
|
+
"Invalid number. Please try again."
|
490
|
+
)
|
491
|
+
continue
|
492
|
+
except ValueError:
|
493
|
+
# Try to parse as name
|
494
|
+
selected_func = choice
|
495
|
+
|
496
|
+
self.set_speaker_function(selected_func)
|
497
|
+
print(
|
498
|
+
f"Speaker function changed to: {self.get_current_speaker_function()}"
|
499
|
+
)
|
500
|
+
|
501
|
+
except InvalidSpeakerFunctionError as e:
|
502
|
+
print(f"Error: {e}")
|
503
|
+
except Exception as e:
|
504
|
+
print(f"An error occurred: {e}")
|
505
|
+
continue
|
506
|
+
|
507
|
+
if not user_input:
|
508
|
+
continue
|
509
|
+
|
510
|
+
# Process the task and get responses
|
511
|
+
try:
|
512
|
+
self.run(user_input)
|
513
|
+
print("\nChat:")
|
514
|
+
# print(response)
|
515
|
+
|
516
|
+
except NoMentionedAgentsError:
|
517
|
+
print(
|
518
|
+
"\nError: Please mention at least one agent using @agent_name"
|
519
|
+
)
|
520
|
+
except AgentNotFoundError as e:
|
521
|
+
print(f"\nError: {str(e)}")
|
522
|
+
except Exception as e:
|
523
|
+
print(f"\nAn error occurred: {str(e)}")
|
524
|
+
|
525
|
+
except KeyboardInterrupt:
|
526
|
+
print("\nSession terminated by user. Goodbye!")
|
527
|
+
break
|
528
|
+
except Exception as e:
|
529
|
+
print(f"\nAn unexpected error occurred: {str(e)}")
|
530
|
+
print(
|
531
|
+
"The session will continue. You can type 'exit' to end it."
|
532
|
+
)
|
533
|
+
|
534
|
+
def _validate_speaker_function(self) -> None:
|
535
|
+
"""
|
536
|
+
Validates the speaker function.
|
537
|
+
|
538
|
+
Raises:
|
539
|
+
InvalidSpeakerFunctionError: If the speaker function is invalid
|
540
|
+
"""
|
541
|
+
if not callable(self.speaker_function):
|
542
|
+
raise InvalidSpeakerFunctionError(
|
543
|
+
"Speaker function must be callable"
|
544
|
+
)
|
545
|
+
|
546
|
+
# Test the speaker function with a dummy list
|
547
|
+
try:
|
548
|
+
test_result = self.speaker_function(
|
549
|
+
["test_agent"], **self.speaker_state
|
550
|
+
)
|
551
|
+
if not isinstance(test_result, str):
|
552
|
+
raise InvalidSpeakerFunctionError(
|
553
|
+
"Speaker function must return a string"
|
554
|
+
)
|
555
|
+
except Exception as e:
|
556
|
+
raise InvalidSpeakerFunctionError(
|
557
|
+
f"Speaker function validation failed: {e}"
|
558
|
+
)
|
559
|
+
|
99
560
|
def _validate_initialization(self) -> None:
|
100
561
|
"""
|
101
562
|
Validates the group chat configuration.
|
@@ -150,6 +611,27 @@ class InteractiveGroupChat:
|
|
150
611
|
}
|
151
612
|
)
|
152
613
|
|
614
|
+
# Create the enhanced prompt that teaches agents how to use @mentions
|
615
|
+
mention_instruction = """
|
616
|
+
|
617
|
+
IMPORTANT: You are part of a collaborative group chat where you can interact with other agents using @mentions.
|
618
|
+
|
619
|
+
-COLLABORATIVE RESPONSE PROTOCOL:
|
620
|
+
1. FIRST: Read and understand all previous responses from other agents
|
621
|
+
2. ACKNOWLEDGE: Reference and acknowledge what other agents have said
|
622
|
+
3. BUILD UPON: Add your perspective while building upon their insights
|
623
|
+
4. MENTION: Use @agent_name to call on other agents when needed
|
624
|
+
5. COMPLETE: Acknowledge when your part is done and what still needs to be done
|
625
|
+
|
626
|
+
HOW TO MENTION OTHER AGENTS:
|
627
|
+
- Use @agent_name to mention another agent in your response
|
628
|
+
- You can mention multiple agents: @agent1 @agent2
|
629
|
+
- When you mention an agent, they will be notified and can respond
|
630
|
+
- Example: "I think @analyst should review this data" or "Let's ask @researcher to investigate this further"
|
631
|
+
|
632
|
+
AVAILABLE AGENTS TO MENTION:
|
633
|
+
"""
|
634
|
+
|
153
635
|
group_context = (
|
154
636
|
f"\n\nYou are part of a group chat named '{self.name}' with the following description: {self.description}\n"
|
155
637
|
f"Other participants in this chat:\n"
|
@@ -163,11 +645,49 @@ class InteractiveGroupChat:
|
|
163
645
|
for info in agent_info
|
164
646
|
if info["name"] != agent.agent_name
|
165
647
|
]
|
166
|
-
agent_context = group_context
|
648
|
+
agent_context = group_context + mention_instruction
|
167
649
|
for other in other_agents:
|
168
|
-
agent_context +=
|
169
|
-
|
170
|
-
|
650
|
+
agent_context += f"- @{other['name']}: {other['description']}\n"
|
651
|
+
|
652
|
+
# Add final instruction
|
653
|
+
agent_context += """
|
654
|
+
|
655
|
+
COLLABORATION GUIDELINES:
|
656
|
+
- ALWAYS read the full conversation history before responding
|
657
|
+
- ACKNOWLEDGE other agents' contributions: "Building on @analyst's data insights..." or "I agree with @researcher's findings that..."
|
658
|
+
- BUILD UPON previous responses rather than repeating information
|
659
|
+
- SYNTHESIZE multiple perspectives when possible
|
660
|
+
- ASK CLARIFYING QUESTIONS if you need more information from other agents
|
661
|
+
- DELEGATE appropriately: "Let me ask @expert_agent to verify this" or "@specialist, can you elaborate on this point?"
|
662
|
+
|
663
|
+
TASK COMPLETION GUIDELINES:
|
664
|
+
- ACKNOWLEDGE when you are done with your part of the task
|
665
|
+
- CLEARLY STATE what still needs to be done before the overall task is finished
|
666
|
+
- If you mention other agents, explain what specific input you need from them
|
667
|
+
- Use phrases like "I have completed [specific part]" or "The task still requires [specific actions]"
|
668
|
+
- Provide a clear status update: "My analysis is complete. The task now needs @writer to create content and @reviewer to validate the approach."
|
669
|
+
|
670
|
+
RESPONSE STRUCTURE:
|
671
|
+
1. ACKNOWLEDGE: "I've reviewed the responses from @agent1 and @agent2..."
|
672
|
+
2. BUILD: "Building on @agent1's analysis of the data..."
|
673
|
+
3. CONTRIBUTE: "From my perspective, I would add..."
|
674
|
+
4. COLLABORATE: "To get a complete picture, let me ask @agent3 to..."
|
675
|
+
5. COMPLETE: "I have completed [my part]. The task still requires [specific next steps]"
|
676
|
+
6. SYNTHESIZE: "Combining our insights, the key findings are..."
|
677
|
+
|
678
|
+
EXAMPLES OF GOOD COLLABORATION:
|
679
|
+
- "I've reviewed @analyst's data analysis and @researcher's market insights. The data shows strong growth potential, and I agree with @researcher that we should focus on emerging markets. Let me add that from a content perspective, we should @writer to create targeted messaging for these markets. I have completed my market analysis. The task now requires @writer to develop content and @reviewer to validate our approach."
|
680
|
+
- "Building on @researcher's findings about customer behavior, I can see that @analyst's data supports this trend. To get a complete understanding, let me ask @writer to help us craft messaging that addresses these specific customer needs. My data analysis is complete. The task still needs @writer to create messaging and @reviewer to approve the final strategy."
|
681
|
+
|
682
|
+
AVOID:
|
683
|
+
- Ignoring other agents' responses
|
684
|
+
- Repeating what others have already said
|
685
|
+
- Making assumptions without consulting relevant experts
|
686
|
+
- Responding in isolation without considering the group's collective knowledge
|
687
|
+
- Not acknowledging task completion status
|
688
|
+
|
689
|
+
Remember: You are part of a team. Your response should reflect that you've read, understood, and are building upon the contributions of others, and clearly communicate your task completion status.
|
690
|
+
"""
|
171
691
|
|
172
692
|
# Update the agent's system prompt
|
173
693
|
agent.system_prompt = (
|
@@ -202,90 +722,100 @@ class InteractiveGroupChat:
|
|
202
722
|
logger.error(f"Error extracting mentions: {e}")
|
203
723
|
raise InvalidTaskFormatError(f"Invalid task format: {e}")
|
204
724
|
|
205
|
-
def
|
725
|
+
def _get_speaking_order(
|
726
|
+
self, mentioned_agents: List[str]
|
727
|
+
) -> List[str]:
|
206
728
|
"""
|
207
|
-
|
729
|
+
Determines the speaking order using the configured speaker function.
|
208
730
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
731
|
+
Args:
|
732
|
+
mentioned_agents: List of agent names that were mentioned
|
733
|
+
|
734
|
+
Returns:
|
735
|
+
List of agent names in the order they should speak
|
214
736
|
"""
|
215
|
-
if not
|
216
|
-
|
217
|
-
"Interactive mode is not enabled. Initialize with interactive=True"
|
218
|
-
)
|
737
|
+
if not mentioned_agents:
|
738
|
+
return []
|
219
739
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
f"- @{name}: {agent.system_prompt.splitlines()[0]}"
|
740
|
+
# Use the speaker function to determine order
|
741
|
+
try:
|
742
|
+
if self.speaker_function == round_robin_speaker:
|
743
|
+
# For round robin, we need to maintain state
|
744
|
+
current_index = self.speaker_state.get(
|
745
|
+
"current_index", 0
|
227
746
|
)
|
228
|
-
|
229
|
-
print(f"- @{name}: Custom callable function")
|
747
|
+
ordered_agents = []
|
230
748
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
749
|
+
# Create the order starting from current index
|
750
|
+
for i in range(len(mentioned_agents)):
|
751
|
+
agent = round_robin_speaker(
|
752
|
+
mentioned_agents, current_index + i
|
753
|
+
)
|
754
|
+
ordered_agents.append(agent)
|
236
755
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
756
|
+
# Update state for next round
|
757
|
+
self.speaker_state["current_index"] = (
|
758
|
+
current_index + len(mentioned_agents)
|
759
|
+
) % len(mentioned_agents)
|
760
|
+
return ordered_agents
|
241
761
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
762
|
+
elif self.speaker_function == random_speaker:
|
763
|
+
# For random, shuffle the list
|
764
|
+
shuffled = mentioned_agents.copy()
|
765
|
+
random.shuffle(shuffled)
|
766
|
+
return shuffled
|
246
767
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
)
|
253
|
-
|
254
|
-
|
255
|
-
print(f" - @{name}")
|
256
|
-
print(
|
257
|
-
"4. Type 'exit' or 'quit' to end the session"
|
258
|
-
)
|
259
|
-
continue
|
768
|
+
elif self.speaker_function == priority_speaker:
|
769
|
+
# For priority, we need priorities in speaker_state
|
770
|
+
priorities = self.speaker_state.get("priorities", {})
|
771
|
+
if not priorities:
|
772
|
+
# Fallback to random if no priorities set
|
773
|
+
shuffled = mentioned_agents.copy()
|
774
|
+
random.shuffle(shuffled)
|
775
|
+
return shuffled
|
260
776
|
|
261
|
-
|
262
|
-
|
777
|
+
# Sort by priority (higher priority first)
|
778
|
+
sorted_agents = sorted(
|
779
|
+
mentioned_agents,
|
780
|
+
key=lambda x: priorities.get(x, 0),
|
781
|
+
reverse=True,
|
782
|
+
)
|
783
|
+
return sorted_agents
|
263
784
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
785
|
+
elif self.speaker_function == random_dynamic_speaker:
|
786
|
+
# For dynamic speaker, we need to handle it differently
|
787
|
+
# The dynamic speaker will be called during the run method
|
788
|
+
# For now, just return the original order
|
789
|
+
return mentioned_agents
|
269
790
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
except Exception as e:
|
277
|
-
print(f"\nAn error occurred: {str(e)}")
|
278
|
-
|
279
|
-
except KeyboardInterrupt:
|
280
|
-
print("\nSession terminated by user. Goodbye!")
|
281
|
-
break
|
282
|
-
except Exception as e:
|
283
|
-
print(f"\nAn unexpected error occurred: {str(e)}")
|
284
|
-
print(
|
285
|
-
"The session will continue. You can type 'exit' to end it."
|
791
|
+
else:
|
792
|
+
# Custom speaker function
|
793
|
+
# For custom functions, we'll use the first agent returned
|
794
|
+
# and then process the rest in original order
|
795
|
+
first_speaker = self.speaker_function(
|
796
|
+
mentioned_agents, **self.speaker_state
|
286
797
|
)
|
798
|
+
if first_speaker in mentioned_agents:
|
799
|
+
remaining = [
|
800
|
+
agent
|
801
|
+
for agent in mentioned_agents
|
802
|
+
if agent != first_speaker
|
803
|
+
]
|
804
|
+
return [first_speaker] + remaining
|
805
|
+
else:
|
806
|
+
return mentioned_agents
|
807
|
+
|
808
|
+
except Exception as e:
|
809
|
+
logger.error(f"Error in speaker function: {e}")
|
810
|
+
# Fallback to original order
|
811
|
+
return mentioned_agents
|
287
812
|
|
288
|
-
def run(
|
813
|
+
def run(
|
814
|
+
self,
|
815
|
+
task: str,
|
816
|
+
img: Optional[str] = None,
|
817
|
+
imgs: Optional[List[str]] = None,
|
818
|
+
) -> str:
|
289
819
|
"""
|
290
820
|
Process a task and get responses from mentioned agents.
|
291
821
|
If interactive mode is enabled, this will be called by start_interactive_session().
|
@@ -303,43 +833,126 @@ class InteractiveGroupChat:
|
|
303
833
|
# Add user task to conversation
|
304
834
|
self.conversation.add(role="User", content=task)
|
305
835
|
|
306
|
-
#
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
)
|
836
|
+
# Handle dynamic speaker function differently
|
837
|
+
if self.speaker_function == random_dynamic_speaker:
|
838
|
+
# Get strategy from speaker state (default to sequential)
|
839
|
+
strategy = self.speaker_state.get(
|
840
|
+
"strategy", "sequential"
|
841
|
+
)
|
313
842
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
843
|
+
# For dynamic speaker, we'll determine the next speaker after each response
|
844
|
+
# Track which agents have spoken to ensure all get a chance
|
845
|
+
spoken_agents = set()
|
846
|
+
last_response = ""
|
847
|
+
max_iterations = (
|
848
|
+
len(mentioned_agents) * 3
|
849
|
+
) # Allow more iterations for parallel
|
850
|
+
iteration = 0
|
851
|
+
|
852
|
+
while iteration < max_iterations and len(
|
853
|
+
spoken_agents
|
854
|
+
) < len(mentioned_agents):
|
855
|
+
# Determine next speaker(s) using dynamic function
|
856
|
+
next_speakers = self.speaker_function(
|
857
|
+
mentioned_agents, # Use all mentioned agents, not remaining_agents
|
858
|
+
last_response,
|
859
|
+
strategy=strategy,
|
860
|
+
**self.speaker_state,
|
318
861
|
)
|
319
862
|
|
320
|
-
#
|
321
|
-
if isinstance(
|
322
|
-
|
323
|
-
task=f"{context}\nPlease respond to the latest task as {agent_name}."
|
324
|
-
)
|
325
|
-
else:
|
326
|
-
# For callable functions
|
327
|
-
response = agent(context)
|
328
|
-
|
329
|
-
# Add response to conversation
|
330
|
-
if response and not response.isspace():
|
331
|
-
self.conversation.add(
|
332
|
-
role=agent_name, content=response
|
333
|
-
)
|
334
|
-
logger.info(f"Agent {agent_name} responded")
|
863
|
+
# Handle both single agent and multiple agents
|
864
|
+
if isinstance(next_speakers, str):
|
865
|
+
next_speakers = [next_speakers]
|
335
866
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
867
|
+
# Filter out invalid agents
|
868
|
+
valid_next_speakers = [
|
869
|
+
agent
|
870
|
+
for agent in next_speakers
|
871
|
+
if agent in mentioned_agents
|
872
|
+
]
|
873
|
+
|
874
|
+
if not valid_next_speakers:
|
875
|
+
# If no valid mentions found, randomly select from unspoken agents
|
876
|
+
unspoken_agents = [
|
877
|
+
agent
|
878
|
+
for agent in mentioned_agents
|
879
|
+
if agent not in spoken_agents
|
880
|
+
]
|
881
|
+
if unspoken_agents:
|
882
|
+
valid_next_speakers = [
|
883
|
+
random.choice(unspoken_agents)
|
884
|
+
]
|
885
|
+
else:
|
886
|
+
# All agents have spoken, break the loop
|
887
|
+
break
|
888
|
+
|
889
|
+
# Process agents based on strategy
|
890
|
+
if strategy == "sequential":
|
891
|
+
# Process one agent at a time
|
892
|
+
for next_speaker in valid_next_speakers:
|
893
|
+
if next_speaker in spoken_agents:
|
894
|
+
continue # Skip if already spoken
|
895
|
+
|
896
|
+
response = self._get_agent_response(
|
897
|
+
next_speaker, img, imgs
|
898
|
+
)
|
899
|
+
if response:
|
900
|
+
last_response = response
|
901
|
+
spoken_agents.add(next_speaker)
|
902
|
+
break # Only process one agent in sequential mode
|
903
|
+
|
904
|
+
elif strategy == "parallel":
|
905
|
+
# Process all mentioned agents in parallel
|
906
|
+
import concurrent.futures
|
907
|
+
|
908
|
+
# Get responses from all valid agents
|
909
|
+
responses = []
|
910
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
911
|
+
future_to_agent = {
|
912
|
+
executor.submit(
|
913
|
+
self._get_agent_response,
|
914
|
+
agent,
|
915
|
+
img,
|
916
|
+
imgs,
|
917
|
+
): agent
|
918
|
+
for agent in valid_next_speakers
|
919
|
+
if agent not in spoken_agents
|
920
|
+
}
|
921
|
+
|
922
|
+
for (
|
923
|
+
future
|
924
|
+
) in concurrent.futures.as_completed(
|
925
|
+
future_to_agent
|
926
|
+
):
|
927
|
+
agent = future_to_agent[future]
|
928
|
+
try:
|
929
|
+
response = future.result()
|
930
|
+
if response:
|
931
|
+
responses.append(response)
|
932
|
+
spoken_agents.add(agent)
|
933
|
+
except Exception as e:
|
934
|
+
logger.error(
|
935
|
+
f"Error getting response from {agent}: {e}"
|
936
|
+
)
|
937
|
+
|
938
|
+
# Combine responses for next iteration
|
939
|
+
if responses:
|
940
|
+
last_response = "\n\n".join(responses)
|
941
|
+
|
942
|
+
iteration += 1
|
943
|
+
else:
|
944
|
+
# For non-dynamic speaker functions, use the original logic
|
945
|
+
speaking_order = self._get_speaking_order(
|
946
|
+
mentioned_agents
|
947
|
+
)
|
948
|
+
logger.info(
|
949
|
+
f"Speaking order determined: {speaking_order}"
|
950
|
+
)
|
951
|
+
|
952
|
+
# Get responses from mentioned agents in the determined order
|
953
|
+
for agent_name in speaking_order:
|
954
|
+
response = self._get_agent_response(
|
955
|
+
agent_name, img, imgs
|
343
956
|
)
|
344
957
|
|
345
958
|
return history_output_formatter(
|
@@ -354,3 +967,97 @@ class InteractiveGroupChat:
|
|
354
967
|
raise InteractiveGroupChatError(
|
355
968
|
f"Unexpected error occurred: {str(e)}"
|
356
969
|
)
|
970
|
+
|
971
|
+
def _get_agent_response(
|
972
|
+
self,
|
973
|
+
agent_name: str,
|
974
|
+
img: Optional[str] = None,
|
975
|
+
imgs: Optional[List[str]] = None,
|
976
|
+
) -> Optional[str]:
|
977
|
+
"""
|
978
|
+
Get response from a specific agent.
|
979
|
+
|
980
|
+
Args:
|
981
|
+
agent_name: Name of the agent to get response from
|
982
|
+
img: Optional image for the task
|
983
|
+
imgs: Optional list of images for the task
|
984
|
+
|
985
|
+
Returns:
|
986
|
+
The agent's response or None if error
|
987
|
+
"""
|
988
|
+
agent = self.agent_map.get(agent_name)
|
989
|
+
if not agent:
|
990
|
+
raise AgentNotFoundError(
|
991
|
+
f"Agent '{agent_name}' not found"
|
992
|
+
)
|
993
|
+
|
994
|
+
try:
|
995
|
+
# Get the complete conversation history
|
996
|
+
context = self.conversation.return_history_as_string()
|
997
|
+
|
998
|
+
# Get response from agent
|
999
|
+
if isinstance(agent, Agent):
|
1000
|
+
collaborative_task = f"""{context}
|
1001
|
+
|
1002
|
+
COLLABORATIVE TASK: Please respond to the latest task as {agent_name}.
|
1003
|
+
|
1004
|
+
IMPORTANT INSTRUCTIONS:
|
1005
|
+
1. Read the ENTIRE conversation history above
|
1006
|
+
2. Acknowledge what other agents have said before adding your perspective
|
1007
|
+
3. Build upon their insights rather than repeating information
|
1008
|
+
4. If you need input from other agents, mention them using @agent_name
|
1009
|
+
5. Provide your unique expertise while showing you understand the group's collective knowledge
|
1010
|
+
|
1011
|
+
TASK COMPLETION GUIDELINES:
|
1012
|
+
- Acknowledge when you are done with your part of the task
|
1013
|
+
- Clearly state what still needs to be done before the overall task is finished
|
1014
|
+
- If you mention other agents, explain what specific input you need from them
|
1015
|
+
- Use phrases like "I have completed [specific part]" or "The task still requires [specific actions]"
|
1016
|
+
|
1017
|
+
Remember: You are part of a collaborative team. Your response should demonstrate that you've read, understood, and are building upon the contributions of others."""
|
1018
|
+
|
1019
|
+
response = agent.run(
|
1020
|
+
task=collaborative_task,
|
1021
|
+
img=img,
|
1022
|
+
imgs=imgs,
|
1023
|
+
)
|
1024
|
+
else:
|
1025
|
+
# For callable functions
|
1026
|
+
response = agent(context)
|
1027
|
+
|
1028
|
+
# Add response to conversation
|
1029
|
+
if response and not response.isspace():
|
1030
|
+
self.conversation.add(
|
1031
|
+
role=agent_name, content=response
|
1032
|
+
)
|
1033
|
+
logger.info(f"Agent {agent_name} responded")
|
1034
|
+
return response
|
1035
|
+
|
1036
|
+
except Exception as e:
|
1037
|
+
logger.error(
|
1038
|
+
f"Error getting response from {agent_name}: {e}"
|
1039
|
+
)
|
1040
|
+
self.conversation.add(
|
1041
|
+
role=agent_name,
|
1042
|
+
content=f"Error: Unable to generate response - {str(e)}",
|
1043
|
+
)
|
1044
|
+
return f"Error: Unable to generate response - {str(e)}"
|
1045
|
+
|
1046
|
+
return None
|
1047
|
+
|
1048
|
+
def set_dynamic_strategy(self, strategy: str) -> None:
|
1049
|
+
"""
|
1050
|
+
Set the strategy for the random-dynamic-speaker function.
|
1051
|
+
|
1052
|
+
Args:
|
1053
|
+
strategy: Either "sequential" or "parallel"
|
1054
|
+
- "sequential": Process one agent at a time based on @mentions
|
1055
|
+
- "parallel": Process all mentioned agents simultaneously
|
1056
|
+
"""
|
1057
|
+
if strategy not in ["sequential", "parallel"]:
|
1058
|
+
raise ValueError(
|
1059
|
+
"Strategy must be either 'sequential' or 'parallel'"
|
1060
|
+
)
|
1061
|
+
|
1062
|
+
self.speaker_state["strategy"] = strategy
|
1063
|
+
logger.info(f"Dynamic speaker strategy set to: {strategy}")
|