@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.
- package/README.md +1088 -0
- package/dist/group-chat.d.ts +327 -0
- package/dist/group-chat.d.ts.map +1 -0
- package/dist/group-chat.js +724 -0
- package/dist/group-chat.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/nested-chat.d.ts +296 -0
- package/dist/nested-chat.d.ts.map +1 -0
- package/dist/nested-chat.js +600 -0
- package/dist/nested-chat.js.map +1 -0
- package/dist/speaker-selection.d.ts +195 -0
- package/dist/speaker-selection.d.ts.map +1 -0
- package/dist/speaker-selection.js +569 -0
- package/dist/speaker-selection.js.map +1 -0
- package/dist/termination.d.ts +237 -0
- package/dist/termination.d.ts.map +1 -0
- package/dist/termination.js +566 -0
- package/dist/termination.js.map +1 -0
- package/dist/types.d.ts +1248 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +201 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
- package/src/group-chat.ts +980 -0
- package/src/index.ts +145 -0
- package/src/nested-chat.ts +795 -0
- package/src/speaker-selection.ts +794 -0
- package/src/termination.ts +704 -0
- package/src/types.ts +876 -0
|
@@ -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
|
+
}
|