swarms 7.9.0__py3-none-any.whl → 7.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swarms/structs/__init__.py +11 -1
- swarms/structs/agent.py +156 -57
- swarms/structs/conversation.py +103 -25
- swarms/structs/interactive_groupchat.py +570 -170
- swarms/structs/swarm_router.py +7 -8
- swarms/utils/litellm_wrapper.py +2 -0
- swarms/utils/retry_func.py +66 -0
- {swarms-7.9.0.dist-info → swarms-7.9.1.dist-info}/METADATA +1 -1
- {swarms-7.9.0.dist-info → swarms-7.9.1.dist-info}/RECORD +12 -11
- {swarms-7.9.0.dist-info → swarms-7.9.1.dist-info}/LICENSE +0 -0
- {swarms-7.9.0.dist-info → swarms-7.9.1.dist-info}/WHEEL +0 -0
- {swarms-7.9.0.dist-info → swarms-7.9.1.dist-info}/entry_points.txt +0 -0
@@ -121,6 +121,71 @@ def priority_speaker(
|
|
121
121
|
return available_agents[-1] # Fallback
|
122
122
|
|
123
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
|
+
|
124
189
|
class InteractiveGroupChat:
|
125
190
|
"""
|
126
191
|
An interactive group chat system that enables conversations with multiple agents using @mentions.
|
@@ -145,11 +210,38 @@ class InteractiveGroupChat:
|
|
145
210
|
max_loops (int, optional): Maximum conversation turns. Defaults to 1.
|
146
211
|
output_type (str, optional): Type of output format. Defaults to "string".
|
147
212
|
interactive (bool, optional): Whether to enable interactive terminal mode. Defaults to False.
|
148
|
-
speaker_function (Callable, optional): Function to determine speaking order.
|
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)
|
149
217
|
speaker_state (dict, optional): Initial state for speaker function. Defaults to empty dict.
|
150
218
|
|
151
219
|
Raises:
|
152
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")
|
153
245
|
"""
|
154
246
|
|
155
247
|
def __init__(
|
@@ -161,7 +253,7 @@ class InteractiveGroupChat:
|
|
161
253
|
max_loops: int = 1,
|
162
254
|
output_type: str = "string",
|
163
255
|
interactive: bool = False,
|
164
|
-
speaker_function: Optional[Callable] = None,
|
256
|
+
speaker_function: Optional[Union[str, Callable]] = None,
|
165
257
|
speaker_state: Optional[dict] = None,
|
166
258
|
):
|
167
259
|
self.id = id
|
@@ -173,9 +265,27 @@ class InteractiveGroupChat:
|
|
173
265
|
self.interactive = interactive
|
174
266
|
|
175
267
|
# Speaker function configuration
|
176
|
-
|
177
|
-
speaker_function
|
178
|
-
)
|
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
|
+
|
179
289
|
self.speaker_state = speaker_state or {"current_index": 0}
|
180
290
|
|
181
291
|
# Validate speaker function
|
@@ -197,6 +307,230 @@ class InteractiveGroupChat:
|
|
197
307
|
self._setup_conversation_context()
|
198
308
|
self._update_agent_prompts()
|
199
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
|
+
|
200
534
|
def _validate_speaker_function(self) -> None:
|
201
535
|
"""
|
202
536
|
Validates the speaker function.
|
@@ -287,6 +621,7 @@ IMPORTANT: You are part of a collaborative group chat where you can interact wit
|
|
287
621
|
2. ACKNOWLEDGE: Reference and acknowledge what other agents have said
|
288
622
|
3. BUILD UPON: Add your perspective while building upon their insights
|
289
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
|
290
625
|
|
291
626
|
HOW TO MENTION OTHER AGENTS:
|
292
627
|
- Use @agent_name to mention another agent in your response
|
@@ -325,24 +660,33 @@ COLLABORATION GUIDELINES:
|
|
325
660
|
- ASK CLARIFYING QUESTIONS if you need more information from other agents
|
326
661
|
- DELEGATE appropriately: "Let me ask @expert_agent to verify this" or "@specialist, can you elaborate on this point?"
|
327
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
|
+
|
328
670
|
RESPONSE STRUCTURE:
|
329
671
|
1. ACKNOWLEDGE: "I've reviewed the responses from @agent1 and @agent2..."
|
330
672
|
2. BUILD: "Building on @agent1's analysis of the data..."
|
331
673
|
3. CONTRIBUTE: "From my perspective, I would add..."
|
332
674
|
4. COLLABORATE: "To get a complete picture, let me ask @agent3 to..."
|
333
|
-
5.
|
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..."
|
334
677
|
|
335
678
|
EXAMPLES OF GOOD COLLABORATION:
|
336
|
-
- "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."
|
337
|
-
- "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."
|
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."
|
338
681
|
|
339
682
|
AVOID:
|
340
683
|
- Ignoring other agents' responses
|
341
684
|
- Repeating what others have already said
|
342
685
|
- Making assumptions without consulting relevant experts
|
343
686
|
- Responding in isolation without considering the group's collective knowledge
|
687
|
+
- Not acknowledging task completion status
|
344
688
|
|
345
|
-
Remember: You are part of a team. Your response should reflect that you've read, understood, and
|
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.
|
346
690
|
"""
|
347
691
|
|
348
692
|
# Update the agent's system prompt
|
@@ -438,6 +782,12 @@ Remember: You are part of a team. Your response should reflect that you've read,
|
|
438
782
|
)
|
439
783
|
return sorted_agents
|
440
784
|
|
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
|
790
|
+
|
441
791
|
else:
|
442
792
|
# Custom speaker function
|
443
793
|
# For custom functions, we'll use the first agent returned
|
@@ -460,120 +810,12 @@ Remember: You are part of a team. Your response should reflect that you've read,
|
|
460
810
|
# Fallback to original order
|
461
811
|
return mentioned_agents
|
462
812
|
|
463
|
-
def
|
813
|
+
def run(
|
464
814
|
self,
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
Set a custom speaker function and optional state.
|
470
|
-
|
471
|
-
Args:
|
472
|
-
speaker_function: Function that determines speaking order
|
473
|
-
speaker_state: Optional state for the speaker function
|
474
|
-
"""
|
475
|
-
self.speaker_function = speaker_function
|
476
|
-
if speaker_state:
|
477
|
-
self.speaker_state.update(speaker_state)
|
478
|
-
self._validate_speaker_function()
|
479
|
-
logger.info(
|
480
|
-
f"Speaker function updated to: {speaker_function.__name__}"
|
481
|
-
)
|
482
|
-
|
483
|
-
def set_priorities(self, priorities: dict) -> None:
|
484
|
-
"""
|
485
|
-
Set agent priorities for priority-based speaking order.
|
486
|
-
|
487
|
-
Args:
|
488
|
-
priorities: Dictionary mapping agent names to priority weights
|
489
|
-
"""
|
490
|
-
self.speaker_state["priorities"] = priorities
|
491
|
-
logger.info(f"Agent priorities set: {priorities}")
|
492
|
-
|
493
|
-
def start_interactive_session(self):
|
494
|
-
"""
|
495
|
-
Start an interactive terminal session for chatting with agents.
|
496
|
-
|
497
|
-
This method creates a REPL (Read-Eval-Print Loop) that allows users to:
|
498
|
-
- Chat with agents using @mentions
|
499
|
-
- See available agents and their descriptions
|
500
|
-
- Exit the session using 'exit' or 'quit'
|
501
|
-
- Get help using 'help' or '?'
|
502
|
-
"""
|
503
|
-
if not self.interactive:
|
504
|
-
raise InteractiveGroupChatError(
|
505
|
-
"Interactive mode is not enabled. Initialize with interactive=True"
|
506
|
-
)
|
507
|
-
|
508
|
-
print(f"\nWelcome to {self.name}!")
|
509
|
-
print(f"Description: {self.description}")
|
510
|
-
print("\nAvailable agents:")
|
511
|
-
for name, agent in self.agent_map.items():
|
512
|
-
if isinstance(agent, Agent):
|
513
|
-
print(
|
514
|
-
f"- @{name}: {agent.system_prompt.splitlines()[0]}"
|
515
|
-
)
|
516
|
-
else:
|
517
|
-
print(f"- @{name}: Custom callable function")
|
518
|
-
|
519
|
-
print("\nCommands:")
|
520
|
-
print("- Type 'help' or '?' for help")
|
521
|
-
print("- Type 'exit' or 'quit' to end the session")
|
522
|
-
print("- Use @agent_name to mention agents")
|
523
|
-
print("\nStart chatting:")
|
524
|
-
|
525
|
-
while True:
|
526
|
-
try:
|
527
|
-
# Get user input
|
528
|
-
user_input = input("\nYou: ").strip()
|
529
|
-
|
530
|
-
# Handle special commands
|
531
|
-
if user_input.lower() in ["exit", "quit"]:
|
532
|
-
print("Goodbye!")
|
533
|
-
break
|
534
|
-
|
535
|
-
if user_input.lower() in ["help", "?"]:
|
536
|
-
print("\nHelp:")
|
537
|
-
print("1. Mention agents using @agent_name")
|
538
|
-
print(
|
539
|
-
"2. You can mention multiple agents in one task"
|
540
|
-
)
|
541
|
-
print("3. Available agents:")
|
542
|
-
for name in self.agent_map:
|
543
|
-
print(f" - @{name}")
|
544
|
-
print(
|
545
|
-
"4. Type 'exit' or 'quit' to end the session"
|
546
|
-
)
|
547
|
-
continue
|
548
|
-
|
549
|
-
if not user_input:
|
550
|
-
continue
|
551
|
-
|
552
|
-
# Process the task and get responses
|
553
|
-
try:
|
554
|
-
self.run(user_input)
|
555
|
-
print("\nChat:")
|
556
|
-
# print(response)
|
557
|
-
|
558
|
-
except NoMentionedAgentsError:
|
559
|
-
print(
|
560
|
-
"\nError: Please mention at least one agent using @agent_name"
|
561
|
-
)
|
562
|
-
except AgentNotFoundError as e:
|
563
|
-
print(f"\nError: {str(e)}")
|
564
|
-
except Exception as e:
|
565
|
-
print(f"\nAn error occurred: {str(e)}")
|
566
|
-
|
567
|
-
except KeyboardInterrupt:
|
568
|
-
print("\nSession terminated by user. Goodbye!")
|
569
|
-
break
|
570
|
-
except Exception as e:
|
571
|
-
print(f"\nAn unexpected error occurred: {str(e)}")
|
572
|
-
print(
|
573
|
-
"The session will continue. You can type 'exit' to end it."
|
574
|
-
)
|
575
|
-
|
576
|
-
def run(self, task: str) -> str:
|
815
|
+
task: str,
|
816
|
+
img: Optional[str] = None,
|
817
|
+
imgs: Optional[List[str]] = None,
|
818
|
+
) -> str:
|
577
819
|
"""
|
578
820
|
Process a task and get responses from mentioned agents.
|
579
821
|
If interactive mode is enabled, this will be called by start_interactive_session().
|
@@ -591,31 +833,171 @@ Remember: You are part of a team. Your response should reflect that you've read,
|
|
591
833
|
# Add user task to conversation
|
592
834
|
self.conversation.add(role="User", content=task)
|
593
835
|
|
594
|
-
#
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
)
|
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
|
+
)
|
601
842
|
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
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,
|
608
861
|
)
|
609
862
|
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
863
|
+
# Handle both single agent and multiple agents
|
864
|
+
if isinstance(next_speakers, str):
|
865
|
+
next_speakers = [next_speakers]
|
866
|
+
|
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
|
614
956
|
)
|
615
957
|
|
616
|
-
|
617
|
-
|
618
|
-
|
958
|
+
return history_output_formatter(
|
959
|
+
self.conversation, self.output_type
|
960
|
+
)
|
961
|
+
|
962
|
+
except InteractiveGroupChatError as e:
|
963
|
+
logger.error(f"GroupChat error: {e}")
|
964
|
+
raise
|
965
|
+
except Exception as e:
|
966
|
+
logger.error(f"Unexpected error: {e}")
|
967
|
+
raise InteractiveGroupChatError(
|
968
|
+
f"Unexpected error occurred: {str(e)}"
|
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}
|
619
1001
|
|
620
1002
|
COLLABORATIVE TASK: Please respond to the latest task as {agent_name}.
|
621
1003
|
|
@@ -626,38 +1008,56 @@ IMPORTANT INSTRUCTIONS:
|
|
626
1008
|
4. If you need input from other agents, mention them using @agent_name
|
627
1009
|
5. Provide your unique expertise while showing you understand the group's collective knowledge
|
628
1010
|
|
629
|
-
|
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]"
|
630
1016
|
|
631
|
-
|
632
|
-
else:
|
633
|
-
# For callable functions
|
634
|
-
response = agent(context)
|
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."""
|
635
1018
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
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)
|
642
1027
|
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
)
|
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
|
651
1035
|
|
652
|
-
|
653
|
-
|
1036
|
+
except Exception as e:
|
1037
|
+
logger.error(
|
1038
|
+
f"Error getting response from {agent_name}: {e}"
|
654
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)}"
|
655
1045
|
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
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'"
|
663
1060
|
)
|
1061
|
+
|
1062
|
+
self.speaker_state["strategy"] = strategy
|
1063
|
+
logger.info(f"Dynamic speaker strategy set to: {strategy}")
|