@wundr.io/autogen-orchestrator 1.0.3

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.
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Speaker Selection Strategies for AutoGen-style Group Chat
3
+ *
4
+ * Implements various strategies for selecting the next speaker in a
5
+ * multi-agent conversation, including round-robin, LLM-selected, and priority-based.
6
+ */
7
+
8
+ import type {
9
+ ChatParticipant,
10
+ Message,
11
+ ChatContext,
12
+ SpeakerSelectionConfig,
13
+ SpeakerSelectionResult,
14
+ SpeakerSelectionStrategy,
15
+ SpeakerSelectionMethod,
16
+ TransitionRule,
17
+ } from './types';
18
+
19
+ /**
20
+ * Factory function to create speaker selection strategies
21
+ * @param method - The speaker selection method to use
22
+ * @returns The appropriate speaker selection strategy
23
+ */
24
+ export function createSpeakerSelector(
25
+ method: SpeakerSelectionMethod,
26
+ ): SpeakerSelectionStrategy {
27
+ switch (method) {
28
+ case 'round_robin':
29
+ return new RoundRobinSelector();
30
+ case 'random':
31
+ return new RandomSelector();
32
+ case 'llm_selected':
33
+ return new LLMSelector();
34
+ case 'priority':
35
+ return new PrioritySelector();
36
+ case 'manual':
37
+ return new ManualSelector();
38
+ case 'auto':
39
+ return new AutoSelector();
40
+ default:
41
+ return new RoundRobinSelector();
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Round-robin speaker selection - cycles through participants in order
47
+ */
48
+ export class RoundRobinSelector implements SpeakerSelectionStrategy {
49
+ private currentIndex = 0;
50
+
51
+ /**
52
+ * Select the next speaker using round-robin ordering
53
+ * @param participants - Available participants
54
+ * @param messages - Message history
55
+ * @param context - Current chat context
56
+ * @param _config - Optional configuration (unused)
57
+ * @returns Speaker selection result
58
+ */
59
+ async selectSpeaker(
60
+ participants: ChatParticipant[],
61
+ messages: Message[],
62
+ _context: ChatContext,
63
+ _config?: SpeakerSelectionConfig,
64
+ ): Promise<SpeakerSelectionResult> {
65
+ const activeParticipants = participants.filter(
66
+ p => p.status === 'active' || p.status === 'idle',
67
+ );
68
+
69
+ if (activeParticipants.length === 0) {
70
+ throw new Error('No active participants available for selection');
71
+ }
72
+
73
+ // Find the last speaker and get the next one
74
+ const lastMessage = messages[messages.length - 1];
75
+ if (lastMessage) {
76
+ const lastSpeakerIndex = activeParticipants.findIndex(
77
+ p => p.name === lastMessage.name,
78
+ );
79
+ if (lastSpeakerIndex !== -1) {
80
+ this.currentIndex = (lastSpeakerIndex + 1) % activeParticipants.length;
81
+ }
82
+ }
83
+
84
+ const selectedParticipant = activeParticipants[this.currentIndex];
85
+ if (!selectedParticipant) {
86
+ throw new Error('Failed to select participant in round-robin');
87
+ }
88
+
89
+ return {
90
+ speaker: selectedParticipant.name,
91
+ reason: `Round-robin selection: position ${this.currentIndex + 1} of ${activeParticipants.length}`,
92
+ confidence: 1.0,
93
+ alternatives: activeParticipants
94
+ .filter(p => p.name !== selectedParticipant.name)
95
+ .map(p => p.name),
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Reset the round-robin index
101
+ */
102
+ reset(): void {
103
+ this.currentIndex = 0;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Random speaker selection - randomly selects from available participants
109
+ */
110
+ export class RandomSelector implements SpeakerSelectionStrategy {
111
+ /**
112
+ * Select the next speaker randomly
113
+ * @param participants - Available participants
114
+ * @param messages - Message history
115
+ * @param context - Current chat context
116
+ * @param config - Optional configuration
117
+ * @returns Speaker selection result
118
+ */
119
+ async selectSpeaker(
120
+ participants: ChatParticipant[],
121
+ messages: Message[],
122
+ context: ChatContext,
123
+ config?: SpeakerSelectionConfig,
124
+ ): Promise<SpeakerSelectionResult> {
125
+ const activeParticipants = participants.filter(
126
+ p => p.status === 'active' || p.status === 'idle',
127
+ );
128
+
129
+ if (activeParticipants.length === 0) {
130
+ throw new Error('No active participants available for selection');
131
+ }
132
+
133
+ // Optionally exclude the last speaker
134
+ const lastMessage = messages[messages.length - 1];
135
+ let eligibleParticipants = activeParticipants;
136
+
137
+ if (lastMessage && activeParticipants.length > 1) {
138
+ eligibleParticipants = activeParticipants.filter(
139
+ p => p.name !== lastMessage.name,
140
+ );
141
+ }
142
+
143
+ // Apply weights if configured
144
+ let selectedParticipant: ChatParticipant;
145
+ if (config?.weights && Object.keys(config.weights).length > 0) {
146
+ selectedParticipant = this.weightedSelection(
147
+ eligibleParticipants,
148
+ config.weights,
149
+ );
150
+ } else {
151
+ const randomIndex = Math.floor(
152
+ Math.random() * eligibleParticipants.length,
153
+ );
154
+ selectedParticipant = eligibleParticipants[randomIndex]!;
155
+ }
156
+
157
+ return {
158
+ speaker: selectedParticipant.name,
159
+ reason: 'Random selection from eligible participants',
160
+ confidence: 1 / eligibleParticipants.length,
161
+ alternatives: eligibleParticipants
162
+ .filter(p => p.name !== selectedParticipant.name)
163
+ .map(p => p.name),
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Perform weighted random selection
169
+ * @param participants - Participants to select from
170
+ * @param weights - Weight for each participant
171
+ * @returns Selected participant
172
+ */
173
+ private weightedSelection(
174
+ participants: ChatParticipant[],
175
+ weights: Record<string, number>,
176
+ ): ChatParticipant {
177
+ const totalWeight = participants.reduce(
178
+ (sum, p) => sum + (weights[p.name] || 1),
179
+ 0,
180
+ );
181
+
182
+ let random = Math.random() * totalWeight;
183
+
184
+ for (const participant of participants) {
185
+ const weight = weights[participant.name] || 1;
186
+ random -= weight;
187
+ if (random <= 0) {
188
+ return participant;
189
+ }
190
+ }
191
+
192
+ // Fallback to last participant
193
+ return participants[participants.length - 1]!;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * LLM-based speaker selection - uses an LLM to determine the best next speaker
199
+ */
200
+ export class LLMSelector implements SpeakerSelectionStrategy {
201
+ /**
202
+ * Select the next speaker using LLM reasoning
203
+ * @param participants - Available participants
204
+ * @param messages - Message history
205
+ * @param context - Current chat context
206
+ * @param config - Configuration including LLM settings
207
+ * @returns Speaker selection result
208
+ */
209
+ async selectSpeaker(
210
+ participants: ChatParticipant[],
211
+ messages: Message[],
212
+ context: ChatContext,
213
+ config?: SpeakerSelectionConfig,
214
+ ): Promise<SpeakerSelectionResult> {
215
+ const activeParticipants = participants.filter(
216
+ p => p.status === 'active' || p.status === 'idle',
217
+ );
218
+
219
+ if (activeParticipants.length === 0) {
220
+ throw new Error('No active participants available for selection');
221
+ }
222
+
223
+ // Build the selection prompt
224
+ const selectionPrompt = this.buildSelectionPrompt(
225
+ activeParticipants,
226
+ messages,
227
+ context,
228
+ config,
229
+ );
230
+
231
+ // Simulate LLM selection (in real implementation, call actual LLM)
232
+ const selectedName = await this.simulateLLMSelection(
233
+ selectionPrompt,
234
+ activeParticipants,
235
+ messages,
236
+ );
237
+
238
+ const selectedParticipant = activeParticipants.find(
239
+ p => p.name === selectedName,
240
+ );
241
+
242
+ if (!selectedParticipant) {
243
+ // Fallback to first active participant
244
+ const fallback = activeParticipants[0]!;
245
+ return {
246
+ speaker: fallback.name,
247
+ reason: 'LLM selection fallback: invalid selection returned',
248
+ confidence: 0.5,
249
+ alternatives: activeParticipants
250
+ .filter(p => p.name !== fallback.name)
251
+ .map(p => p.name),
252
+ };
253
+ }
254
+
255
+ return {
256
+ speaker: selectedParticipant.name,
257
+ reason:
258
+ 'LLM selected based on conversation context and participant capabilities',
259
+ confidence: 0.85,
260
+ alternatives: activeParticipants
261
+ .filter(p => p.name !== selectedParticipant.name)
262
+ .map(p => p.name),
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Build the prompt for LLM speaker selection
268
+ * @param participants - Available participants
269
+ * @param messages - Message history
270
+ * @param context - Chat context
271
+ * @param config - Selection configuration
272
+ * @returns Formatted prompt string
273
+ */
274
+ private buildSelectionPrompt(
275
+ participants: ChatParticipant[],
276
+ messages: Message[],
277
+ context: ChatContext,
278
+ config?: SpeakerSelectionConfig,
279
+ ): string {
280
+ const participantDescriptions = participants
281
+ .map(
282
+ p => `- ${p.name}: ${p.description || p.systemPrompt.slice(0, 100)}...`,
283
+ )
284
+ .join('\n');
285
+
286
+ const recentMessages = messages
287
+ .slice(-5)
288
+ .map(m => `${m.name}: ${m.content.slice(0, 200)}`)
289
+ .join('\n');
290
+
291
+ const basePrompt =
292
+ config?.selectorPrompt ||
293
+ 'You are a conversation moderator. Select the most appropriate next speaker.';
294
+
295
+ return `${basePrompt}
296
+
297
+ ## Available Participants:
298
+ ${participantDescriptions}
299
+
300
+ ## Recent Conversation:
301
+ ${recentMessages}
302
+
303
+ ## Context:
304
+ - Current round: ${context.currentRound}
305
+ - Previous speaker: ${context.previousSpeaker || 'None'}
306
+
307
+ Based on the conversation flow and participant expertise, who should speak next?
308
+ Return only the participant name.`;
309
+ }
310
+
311
+ /**
312
+ * Simulate LLM selection (placeholder for actual LLM call)
313
+ * @param prompt - Selection prompt
314
+ * @param participants - Available participants
315
+ * @param messages - Message history
316
+ * @returns Selected participant name
317
+ */
318
+ private async simulateLLMSelection(
319
+ _prompt: string,
320
+ participants: ChatParticipant[],
321
+ messages: Message[],
322
+ ): Promise<string> {
323
+ // In a real implementation, this would call an actual LLM
324
+ // For now, use heuristics to simulate intelligent selection
325
+
326
+ const lastMessage = messages[messages.length - 1];
327
+
328
+ // Find participant most relevant to the last message content
329
+ if (lastMessage) {
330
+ const relevantParticipant = this.findMostRelevantParticipant(
331
+ lastMessage.content,
332
+ participants,
333
+ );
334
+ if (relevantParticipant) {
335
+ return relevantParticipant.name;
336
+ }
337
+ }
338
+
339
+ // Exclude last speaker and select randomly
340
+ const eligibleParticipants = lastMessage
341
+ ? participants.filter(p => p.name !== lastMessage.name)
342
+ : participants;
343
+
344
+ const randomIndex = Math.floor(Math.random() * eligibleParticipants.length);
345
+ return eligibleParticipants[randomIndex]?.name || participants[0]!.name;
346
+ }
347
+
348
+ /**
349
+ * Find the participant most relevant to the given content
350
+ * @param content - Message content to analyze
351
+ * @param participants - Available participants
352
+ * @returns Most relevant participant or null
353
+ */
354
+ private findMostRelevantParticipant(
355
+ content: string,
356
+ participants: ChatParticipant[],
357
+ ): ChatParticipant | null {
358
+ const contentLower = content.toLowerCase();
359
+
360
+ // Score each participant based on capability match
361
+ let bestMatch: ChatParticipant | null = null;
362
+ let bestScore = 0;
363
+
364
+ for (const participant of participants) {
365
+ let score = 0;
366
+
367
+ // Check capabilities
368
+ for (const capability of participant.capabilities) {
369
+ if (contentLower.includes(capability.toLowerCase())) {
370
+ score += 2;
371
+ }
372
+ }
373
+
374
+ // Check if participant is mentioned
375
+ if (contentLower.includes(participant.name.toLowerCase())) {
376
+ score += 5;
377
+ }
378
+
379
+ // Check description keywords
380
+ if (participant.description) {
381
+ const descWords = participant.description.toLowerCase().split(/\s+/);
382
+ for (const word of descWords) {
383
+ if (word.length > 4 && contentLower.includes(word)) {
384
+ score += 1;
385
+ }
386
+ }
387
+ }
388
+
389
+ if (score > bestScore) {
390
+ bestScore = score;
391
+ bestMatch = participant;
392
+ }
393
+ }
394
+
395
+ return bestScore > 0 ? bestMatch : null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Priority-based speaker selection - selects based on configured priority order
401
+ */
402
+ export class PrioritySelector implements SpeakerSelectionStrategy {
403
+ /**
404
+ * Select the next speaker based on priority order
405
+ * @param participants - Available participants
406
+ * @param messages - Message history
407
+ * @param context - Current chat context
408
+ * @param config - Configuration with priority order
409
+ * @returns Speaker selection result
410
+ */
411
+ async selectSpeaker(
412
+ participants: ChatParticipant[],
413
+ messages: Message[],
414
+ context: ChatContext,
415
+ config?: SpeakerSelectionConfig,
416
+ ): Promise<SpeakerSelectionResult> {
417
+ const activeParticipants = participants.filter(
418
+ p => p.status === 'active' || p.status === 'idle',
419
+ );
420
+
421
+ if (activeParticipants.length === 0) {
422
+ throw new Error('No active participants available for selection');
423
+ }
424
+
425
+ const priorityOrder = config?.priorityOrder || [];
426
+ const lastMessage = messages[messages.length - 1];
427
+
428
+ // Check transition rules first
429
+ if (config?.transitionRules && lastMessage) {
430
+ const nextSpeaker = this.applyTransitionRules(
431
+ lastMessage.name,
432
+ config.transitionRules,
433
+ activeParticipants,
434
+ );
435
+ if (nextSpeaker) {
436
+ return {
437
+ speaker: nextSpeaker.name,
438
+ reason: `Transition rule: ${lastMessage.name} -> ${nextSpeaker.name}`,
439
+ confidence: 0.9,
440
+ alternatives: activeParticipants
441
+ .filter(p => p.name !== nextSpeaker.name)
442
+ .map(p => p.name),
443
+ };
444
+ }
445
+ }
446
+
447
+ // Check allowed transitions
448
+ if (config?.allowedTransitions && lastMessage) {
449
+ const allowed = config.allowedTransitions[lastMessage.name];
450
+ if (allowed && allowed.length > 0) {
451
+ const eligibleParticipants = activeParticipants.filter(p =>
452
+ allowed.includes(p.name),
453
+ );
454
+ if (eligibleParticipants.length > 0) {
455
+ const selected = eligibleParticipants[0]!;
456
+ return {
457
+ speaker: selected.name,
458
+ reason: `Allowed transition from ${lastMessage.name}`,
459
+ confidence: 0.85,
460
+ alternatives: eligibleParticipants.slice(1).map(p => p.name),
461
+ };
462
+ }
463
+ }
464
+ }
465
+
466
+ // Select based on priority order
467
+ for (const priorityName of priorityOrder) {
468
+ const participant = activeParticipants.find(p => p.name === priorityName);
469
+ if (
470
+ participant &&
471
+ (!lastMessage || participant.name !== lastMessage.name)
472
+ ) {
473
+ return {
474
+ speaker: participant.name,
475
+ reason: `Priority selection: rank ${priorityOrder.indexOf(priorityName) + 1}`,
476
+ confidence: 0.95,
477
+ alternatives: activeParticipants
478
+ .filter(p => p.name !== participant.name)
479
+ .map(p => p.name),
480
+ };
481
+ }
482
+ }
483
+
484
+ // Fallback to first available participant
485
+ const fallback =
486
+ activeParticipants.find(
487
+ p => !lastMessage || p.name !== lastMessage.name,
488
+ ) || activeParticipants[0]!;
489
+
490
+ return {
491
+ speaker: fallback.name,
492
+ reason: 'Priority fallback: no priority match found',
493
+ confidence: 0.5,
494
+ alternatives: activeParticipants
495
+ .filter(p => p.name !== fallback.name)
496
+ .map(p => p.name),
497
+ };
498
+ }
499
+
500
+ /**
501
+ * Apply transition rules to determine next speaker
502
+ * @param fromSpeaker - Current speaker name
503
+ * @param rules - Transition rules
504
+ * @param participants - Available participants
505
+ * @returns Next speaker or null
506
+ */
507
+ private applyTransitionRules(
508
+ fromSpeaker: string,
509
+ rules: TransitionRule[],
510
+ participants: ChatParticipant[],
511
+ ): ChatParticipant | null {
512
+ // Find rules matching the current speaker
513
+ const matchingRules = rules.filter(rule => rule.from === fromSpeaker);
514
+
515
+ if (matchingRules.length === 0) {
516
+ return null;
517
+ }
518
+
519
+ // Sort by weight if available
520
+ matchingRules.sort((a, b) => (b.weight || 0) - (a.weight || 0));
521
+
522
+ // Find the first valid transition
523
+ for (const rule of matchingRules) {
524
+ for (const toName of rule.to) {
525
+ const participant = participants.find(
526
+ p =>
527
+ p.name === toName && (p.status === 'active' || p.status === 'idle'),
528
+ );
529
+ if (participant) {
530
+ return participant;
531
+ }
532
+ }
533
+ }
534
+
535
+ return null;
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Manual speaker selection - expects explicit selection from context
541
+ */
542
+ export class ManualSelector implements SpeakerSelectionStrategy {
543
+ /**
544
+ * Select the next speaker from manual specification
545
+ * @param participants - Available participants
546
+ * @param messages - Message history
547
+ * @param context - Current chat context with manual selection
548
+ * @returns Speaker selection result
549
+ */
550
+ async selectSpeaker(
551
+ participants: ChatParticipant[],
552
+ _messages: Message[],
553
+ context: ChatContext,
554
+ ): Promise<SpeakerSelectionResult> {
555
+ const activeParticipants = participants.filter(
556
+ p => p.status === 'active' || p.status === 'idle',
557
+ );
558
+
559
+ if (activeParticipants.length === 0) {
560
+ throw new Error('No active participants available for selection');
561
+ }
562
+
563
+ // Check for manually specified next speaker in context state
564
+ const manualSelection = context.state['nextSpeaker'] as string | undefined;
565
+
566
+ if (manualSelection) {
567
+ const participant = activeParticipants.find(
568
+ p => p.name === manualSelection,
569
+ );
570
+ if (participant) {
571
+ return {
572
+ speaker: participant.name,
573
+ reason: 'Manual selection from context',
574
+ confidence: 1.0,
575
+ alternatives: activeParticipants
576
+ .filter(p => p.name !== participant.name)
577
+ .map(p => p.name),
578
+ };
579
+ }
580
+ }
581
+
582
+ // If no manual selection, wait or use fallback
583
+ throw new Error(
584
+ 'Manual selection mode requires nextSpeaker in context state',
585
+ );
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Auto speaker selection - intelligently chooses selection strategy based on context
591
+ */
592
+ export class AutoSelector implements SpeakerSelectionStrategy {
593
+ private roundRobin = new RoundRobinSelector();
594
+ private llm = new LLMSelector();
595
+ private priority = new PrioritySelector();
596
+
597
+ /**
598
+ * Automatically select the best strategy and next speaker
599
+ * @param participants - Available participants
600
+ * @param messages - Message history
601
+ * @param context - Current chat context
602
+ * @param config - Selection configuration
603
+ * @returns Speaker selection result
604
+ */
605
+ async selectSpeaker(
606
+ participants: ChatParticipant[],
607
+ messages: Message[],
608
+ context: ChatContext,
609
+ config?: SpeakerSelectionConfig,
610
+ ): Promise<SpeakerSelectionResult> {
611
+ const activeParticipants = participants.filter(
612
+ p => p.status === 'active' || p.status === 'idle',
613
+ );
614
+
615
+ if (activeParticipants.length === 0) {
616
+ throw new Error('No active participants available for selection');
617
+ }
618
+
619
+ // Determine best strategy based on context
620
+ const strategy = this.determineStrategy(
621
+ activeParticipants,
622
+ messages,
623
+ context,
624
+ config,
625
+ );
626
+
627
+ let result: SpeakerSelectionResult;
628
+
629
+ switch (strategy) {
630
+ case 'priority':
631
+ result = await this.priority.selectSpeaker(
632
+ participants,
633
+ messages,
634
+ context,
635
+ config,
636
+ );
637
+ break;
638
+ case 'llm':
639
+ result = await this.llm.selectSpeaker(
640
+ participants,
641
+ messages,
642
+ context,
643
+ config,
644
+ );
645
+ break;
646
+ case 'round_robin':
647
+ default:
648
+ result = await this.roundRobin.selectSpeaker(
649
+ participants,
650
+ messages,
651
+ context,
652
+ config,
653
+ );
654
+ break;
655
+ }
656
+
657
+ return {
658
+ ...result,
659
+ reason: `Auto-selected ${strategy} strategy: ${result.reason}`,
660
+ };
661
+ }
662
+
663
+ /**
664
+ * Determine the best selection strategy for current context
665
+ * @param participants - Active participants
666
+ * @param messages - Message history
667
+ * @param context - Chat context
668
+ * @param config - Selection configuration
669
+ * @returns Strategy name to use
670
+ */
671
+ private determineStrategy(
672
+ participants: ChatParticipant[],
673
+ messages: Message[],
674
+ context: ChatContext,
675
+ config?: SpeakerSelectionConfig,
676
+ ): 'priority' | 'llm' | 'round_robin' {
677
+ // Use priority if transition rules or priority order are configured
678
+ if (
679
+ config?.transitionRules?.length ||
680
+ config?.priorityOrder?.length ||
681
+ config?.allowedTransitions
682
+ ) {
683
+ return 'priority';
684
+ }
685
+
686
+ // Use LLM for complex conversations with many participants
687
+ if (participants.length > 3 && messages.length > 5) {
688
+ return 'llm';
689
+ }
690
+
691
+ // Use LLM if participants have distinct capabilities
692
+ const uniqueCapabilities = new Set(
693
+ participants.flatMap(p => p.capabilities),
694
+ );
695
+ if (uniqueCapabilities.size > participants.length * 2) {
696
+ return 'llm';
697
+ }
698
+
699
+ // Default to round-robin for simple cases
700
+ return 'round_robin';
701
+ }
702
+ }
703
+
704
+ /**
705
+ * Speaker selection manager that wraps all strategies
706
+ */
707
+ export class SpeakerSelectionManager {
708
+ private strategies: Map<SpeakerSelectionMethod, SpeakerSelectionStrategy> =
709
+ new Map();
710
+ private currentStrategy: SpeakerSelectionStrategy;
711
+ private method: SpeakerSelectionMethod;
712
+
713
+ /**
714
+ * Create a new speaker selection manager
715
+ * @param method - Initial selection method
716
+ */
717
+ constructor(method: SpeakerSelectionMethod = 'round_robin') {
718
+ this.method = method;
719
+ this.currentStrategy = createSpeakerSelector(method);
720
+ this.initializeStrategies();
721
+ }
722
+
723
+ /**
724
+ * Initialize all available strategies
725
+ */
726
+ private initializeStrategies(): void {
727
+ this.strategies.set('round_robin', new RoundRobinSelector());
728
+ this.strategies.set('random', new RandomSelector());
729
+ this.strategies.set('llm_selected', new LLMSelector());
730
+ this.strategies.set('priority', new PrioritySelector());
731
+ this.strategies.set('manual', new ManualSelector());
732
+ this.strategies.set('auto', new AutoSelector());
733
+ }
734
+
735
+ /**
736
+ * Select the next speaker using the current strategy
737
+ * @param participants - Available participants
738
+ * @param messages - Message history
739
+ * @param context - Chat context
740
+ * @param config - Selection configuration
741
+ * @returns Speaker selection result
742
+ */
743
+ async selectSpeaker(
744
+ participants: ChatParticipant[],
745
+ messages: Message[],
746
+ context: ChatContext,
747
+ config?: SpeakerSelectionConfig,
748
+ ): Promise<SpeakerSelectionResult> {
749
+ return this.currentStrategy.selectSpeaker(
750
+ participants,
751
+ messages,
752
+ context,
753
+ config,
754
+ );
755
+ }
756
+
757
+ /**
758
+ * Change the selection method
759
+ * @param method - New selection method
760
+ */
761
+ setMethod(method: SpeakerSelectionMethod): void {
762
+ this.method = method;
763
+ const strategy = this.strategies.get(method);
764
+ if (strategy) {
765
+ this.currentStrategy = strategy;
766
+ } else {
767
+ this.currentStrategy = createSpeakerSelector(method);
768
+ this.strategies.set(method, this.currentStrategy);
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Get the current selection method
774
+ * @returns Current method
775
+ */
776
+ getMethod(): SpeakerSelectionMethod {
777
+ return this.method;
778
+ }
779
+
780
+ /**
781
+ * Get a specific strategy instance
782
+ * @param method - Selection method
783
+ * @returns Strategy instance
784
+ */
785
+ getStrategy(method: SpeakerSelectionMethod): SpeakerSelectionStrategy {
786
+ const strategy = this.strategies.get(method);
787
+ if (!strategy) {
788
+ const newStrategy = createSpeakerSelector(method);
789
+ this.strategies.set(method, newStrategy);
790
+ return newStrategy;
791
+ }
792
+ return strategy;
793
+ }
794
+ }