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.
@@ -1,5 +1,6 @@
1
1
  import re
2
- from typing import Callable, List, Union
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
- f"- {other['name']}: {other['description']}\n"
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 start_interactive_session(self):
725
+ def _get_speaking_order(
726
+ self, mentioned_agents: List[str]
727
+ ) -> List[str]:
206
728
  """
207
- Start an interactive terminal session for chatting with agents.
729
+ Determines the speaking order using the configured speaker function.
208
730
 
209
- This method creates a REPL (Read-Eval-Print Loop) that allows users to:
210
- - Chat with agents using @mentions
211
- - See available agents and their descriptions
212
- - Exit the session using 'exit' or 'quit'
213
- - Get help using 'help' or '?'
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 self.interactive:
216
- raise InteractiveGroupChatError(
217
- "Interactive mode is not enabled. Initialize with interactive=True"
218
- )
737
+ if not mentioned_agents:
738
+ return []
219
739
 
220
- print(f"\nWelcome to {self.name}!")
221
- print(f"Description: {self.description}")
222
- print("\nAvailable agents:")
223
- for name, agent in self.agent_map.items():
224
- if isinstance(agent, Agent):
225
- print(
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
- else:
229
- print(f"- @{name}: Custom callable function")
747
+ ordered_agents = []
230
748
 
231
- print("\nCommands:")
232
- print("- Type 'help' or '?' for help")
233
- print("- Type 'exit' or 'quit' to end the session")
234
- print("- Use @agent_name to mention agents")
235
- print("\nStart chatting:")
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
- while True:
238
- try:
239
- # Get user input
240
- user_input = input("\nYou: ").strip()
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
- # Handle special commands
243
- if user_input.lower() in ["exit", "quit"]:
244
- print("Goodbye!")
245
- break
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
- if user_input.lower() in ["help", "?"]:
248
- print("\nHelp:")
249
- print("1. Mention agents using @agent_name")
250
- print(
251
- "2. You can mention multiple agents in one task"
252
- )
253
- print("3. Available agents:")
254
- for name in self.agent_map:
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
- if not user_input:
262
- continue
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
- # Process the task and get responses
265
- try:
266
- response = self.run(user_input)
267
- print("\nChat:")
268
- print(response)
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
- except NoMentionedAgentsError:
271
- print(
272
- "\nError: Please mention at least one agent using @agent_name"
273
- )
274
- except AgentNotFoundError as e:
275
- print(f"\nError: {str(e)}")
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(self, task: str) -> str:
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
- # Get responses from mentioned agents
307
- for agent_name in mentioned_agents:
308
- agent = self.agent_map.get(agent_name)
309
- if not agent:
310
- raise AgentNotFoundError(
311
- f"Agent '{agent_name}' not found"
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
- try:
315
- # Get the complete conversation history
316
- context = (
317
- self.conversation.return_history_as_string()
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
- # Get response from agent
321
- if isinstance(agent, Agent):
322
- response = agent.run(
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
- except Exception as e:
337
- logger.error(
338
- f"Error getting response from {agent_name}: {e}"
339
- )
340
- self.conversation.add(
341
- role=agent_name,
342
- content=f"Error: Unable to generate response - {str(e)}",
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}")