@lobehub/lobehub 2.0.0-next.286 → 2.0.0-next.288
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/CHANGELOG.md +58 -0
- package/apps/desktop/src/main/const/theme.ts +0 -3
- package/apps/desktop/src/main/core/browser/Browser.ts +1 -1
- package/apps/desktop/src/main/core/browser/WindowThemeManager.ts +3 -2
- package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +0 -1
- package/apps/desktop/src/main/core/browser/__tests__/WindowThemeManager.test.ts +8 -5
- package/changelog/v1.json +14 -0
- package/locales/en-US/plugin.json +3 -5
- package/locales/zh-CN/plugin.json +3 -5
- package/locales/zh-CN/tool.json +2 -0
- package/package.json +1 -1
- package/packages/builtin-agents/src/agents/group-supervisor/index.ts +12 -1
- package/packages/builtin-agents/src/agents/group-supervisor/systemRole.ts +0 -7
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/EditLocalFile/index.tsx +93 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GlobLocalFiles/index.tsx +73 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/GrepContent/index.tsx +69 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ListLocalFiles/index.tsx +68 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/ReadLocalFile/index.tsx +74 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/SearchLocalFiles/index.tsx +70 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/WriteLocalFile/index.tsx +57 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Inspector/index.ts +14 -0
- package/packages/builtin-tool-cloud-sandbox/src/client/Render/WriteFile/index.tsx +54 -35
- package/packages/builtin-tool-cloud-sandbox/src/client/components/FilePathDisplay.tsx +52 -0
- package/packages/builtin-tool-group-management/src/client/Inspector/ExecuteTasks/index.tsx +90 -0
- package/packages/builtin-tool-group-management/src/client/Inspector/index.ts +2 -0
- package/packages/builtin-tool-group-management/src/client/Intervention/ExecuteTasks.tsx +237 -0
- package/packages/builtin-tool-group-management/src/client/Intervention/index.ts +4 -1
- package/packages/builtin-tool-group-management/src/client/Render/index.ts +1 -1
- package/packages/builtin-tool-group-management/src/client/Streaming/ExecuteTask/index.tsx +69 -0
- package/packages/builtin-tool-group-management/src/client/Streaming/ExecuteTasks/index.tsx +87 -0
- package/packages/builtin-tool-group-management/src/client/Streaming/index.ts +4 -0
- package/packages/builtin-tool-group-management/src/executor.test.ts +8 -311
- package/packages/builtin-tool-group-management/src/executor.ts +5 -160
- package/packages/builtin-tool-group-management/src/manifest.ts +50 -94
- package/packages/builtin-tool-group-management/src/systemRole.ts +251 -172
- package/packages/builtin-tool-group-management/src/types.ts +29 -40
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +6 -4
- package/packages/context-engine/src/engine/messages/types.ts +4 -4
- package/packages/context-engine/src/processors/GroupRoleTransform.ts +261 -0
- package/packages/context-engine/src/processors/__tests__/GroupRoleTransform.test.ts +553 -0
- package/packages/context-engine/src/processors/index.ts +2 -2
- package/packages/context-engine/src/providers/__tests__/GroupContextInjector.test.ts +4 -16
- package/packages/context-engine/src/providers/__tests__/__snapshots__/GroupContextInjector.test.ts.snap +23 -28
- package/packages/desktop-bridge/src/index.ts +3 -0
- package/packages/prompts/src/prompts/agentGroup/__snapshots__/index.test.ts.snap +0 -7
- package/packages/prompts/src/prompts/agentGroup/groupContext.ts +0 -7
- package/src/app/[variants]/(main)/group/features/Conversation/AgentWelcome/OpeningQuestions.tsx +4 -8
- package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/GroupChat.tsx +0 -3
- package/src/app/[variants]/(main)/group/features/Conversation/useGroupContext.ts +3 -0
- package/src/app/[variants]/(main)/settings/hooks/useCategory.tsx +15 -2
- package/src/features/ChatInput/Desktop/index.tsx +1 -3
- package/src/features/Conversation/store/slices/message/action/crud.ts +2 -2
- package/src/features/ElectronTitlebar/Connection/ConnectionMode.tsx +2 -2
- package/src/features/ElectronTitlebar/SimpleTitleBar.tsx +1 -2
- package/src/features/ElectronTitlebar/index.tsx +2 -2
- package/src/hooks/useUserAvatar.test.ts +23 -4
- package/src/locales/default/plugin.ts +3 -5
- package/src/locales/default/tool.ts +3 -0
- package/src/services/chat/mecha/agentConfigResolver.test.ts +160 -0
- package/src/services/chat/mecha/agentConfigResolver.ts +15 -3
- package/src/services/chat/mecha/contextEngineering.ts +2 -1
- package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +4 -2
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +2 -0
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -18
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +24 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +6 -1
- package/src/store/chat/slices/topic/action.test.ts +10 -4
- package/src/store/chat/slices/topic/action.ts +3 -2
- package/src/store/electron/selectors/sync.ts +17 -1
- package/packages/context-engine/src/processors/GroupMessageSender.ts +0 -138
- package/packages/context-engine/src/processors/__tests__/GroupMessageSender.test.ts +0 -274
- package/src/features/ElectronTitlebar/const.ts +0 -1
|
@@ -190,12 +190,13 @@ export const createGroupOrchestrationExecutors = (
|
|
|
190
190
|
|
|
191
191
|
// If instruction is provided, inject it as a virtual User Message
|
|
192
192
|
// This virtual message is not persisted to database, only used for model context
|
|
193
|
+
// Mark with <speaker> tag so the agent knows this instruction is from the Supervisor
|
|
193
194
|
const now = Date.now();
|
|
194
195
|
const messagesWithInstruction: UIChatMessage[] = agentInstruction
|
|
195
196
|
? [
|
|
196
197
|
...messages,
|
|
197
198
|
{
|
|
198
|
-
content: agentInstruction
|
|
199
|
+
content: `<speaker name="Supervisor" />\n${agentInstruction}`,
|
|
199
200
|
createdAt: now,
|
|
200
201
|
id: `virtual_speak_instruction_${now}`,
|
|
201
202
|
role: 'user',
|
|
@@ -266,12 +267,13 @@ export const createGroupOrchestrationExecutors = (
|
|
|
266
267
|
|
|
267
268
|
// If instruction is provided, inject it as a virtual User Message
|
|
268
269
|
// This virtual message is not persisted to database, only used for model context
|
|
270
|
+
// Mark with <speaker> tag so the agent knows this instruction is from the Supervisor
|
|
269
271
|
const now = Date.now();
|
|
270
272
|
const messagesWithInstruction: UIChatMessage[] = agentInstruction
|
|
271
273
|
? [
|
|
272
274
|
...messages,
|
|
273
275
|
{
|
|
274
|
-
content: agentInstruction
|
|
276
|
+
content: `<speaker name="Supervisor" />\n${agentInstruction}`,
|
|
275
277
|
createdAt: now,
|
|
276
278
|
id: `virtual_broadcast_instruction_${now}`,
|
|
277
279
|
role: 'user',
|
|
@@ -197,6 +197,8 @@ export const conversationLifecycle: StateCreator<
|
|
|
197
197
|
// if there is topicId,then add topicId to message
|
|
198
198
|
topicId: operationContext.topicId ?? undefined,
|
|
199
199
|
threadId: operationContext.threadId ?? undefined,
|
|
200
|
+
// Pass isSupervisor metadata for group orchestration (consistent with server)
|
|
201
|
+
metadata: operationContext.isSupervisor ? { isSupervisor: true } : undefined,
|
|
200
202
|
},
|
|
201
203
|
{ operationId, tempMessageId: tempAssistantId },
|
|
202
204
|
);
|
|
@@ -592,7 +592,7 @@ export const streamingExecutor: StateCreator<
|
|
|
592
592
|
// resolveAgentConfig handles:
|
|
593
593
|
// - Builtin agent runtime config merging
|
|
594
594
|
// - max_tokens/reasoning_effort based on chatConfig settings
|
|
595
|
-
const { agentConfig: agentConfigData
|
|
595
|
+
const { agentConfig: agentConfigData } = resolveAgentConfig({
|
|
596
596
|
agentId: effectiveAgentId || '',
|
|
597
597
|
scope: context.scope, // Pass scope from context parameter (available at line 883)
|
|
598
598
|
});
|
|
@@ -863,22 +863,5 @@ export const streamingExecutor: StateCreator<
|
|
|
863
863
|
console.error('Desktop notification error:', error);
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
|
-
|
|
867
|
-
// Summary history if context messages is larger than historyCount
|
|
868
|
-
const historyCount = chatConfig.historyCount ?? 0;
|
|
869
|
-
|
|
870
|
-
if (
|
|
871
|
-
chatConfig.enableHistoryCount &&
|
|
872
|
-
chatConfig.enableCompressHistory &&
|
|
873
|
-
messages.length > historyCount
|
|
874
|
-
) {
|
|
875
|
-
// after generation: [u1,a1,u2,a2,u3,a3]
|
|
876
|
-
// but the `messages` is still: [u1,a1,u2,a2,u3]
|
|
877
|
-
// So if historyCount=2, we need to summary [u1,a1,u2,a2]
|
|
878
|
-
// because user find UI is [u1,a1,u2,a2 | u3,a3]
|
|
879
|
-
const historyMessages = messages.slice(0, -historyCount + 1);
|
|
880
|
-
|
|
881
|
-
await get().internal_summaryHistory(historyMessages);
|
|
882
|
-
}
|
|
883
866
|
},
|
|
884
867
|
});
|
|
@@ -323,6 +323,30 @@ describe('displayMessageSelectors', () => {
|
|
|
323
323
|
const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
|
|
324
324
|
expect(result).toBe(messageMapKey({ agentId: '', topicId: undefined }));
|
|
325
325
|
});
|
|
326
|
+
|
|
327
|
+
it('should generate correct key with activeGroupId for group conversations', () => {
|
|
328
|
+
const state: Partial<ChatStore> = {
|
|
329
|
+
activeAgentId: 'testId',
|
|
330
|
+
activeGroupId: 'groupId',
|
|
331
|
+
activeTopicId: undefined,
|
|
332
|
+
};
|
|
333
|
+
const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
|
|
334
|
+
expect(result).toBe(
|
|
335
|
+
messageMapKey({ agentId: 'testId', groupId: 'groupId', topicId: undefined }),
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should generate correct key with activeGroupId and activeTopicId', () => {
|
|
340
|
+
const state: Partial<ChatStore> = {
|
|
341
|
+
activeAgentId: 'testId',
|
|
342
|
+
activeGroupId: 'groupId',
|
|
343
|
+
activeTopicId: 'topicId',
|
|
344
|
+
};
|
|
345
|
+
const result = displayMessageSelectors.currentDisplayChatKey(state as ChatStore);
|
|
346
|
+
expect(result).toBe(
|
|
347
|
+
messageMapKey({ agentId: 'testId', groupId: 'groupId', topicId: 'topicId' }),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
326
350
|
});
|
|
327
351
|
|
|
328
352
|
describe('activeDisplayMessages with group chat messages', () => {
|
|
@@ -24,9 +24,14 @@ import { messageMapKey } from '../../../utils/messageMapKey';
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Get the current chat key for accessing messagesMap
|
|
27
|
+
* For group conversations, uses groupId to generate the correct key
|
|
27
28
|
*/
|
|
28
29
|
export const currentDisplayChatKey = (s: ChatStoreState) =>
|
|
29
|
-
messageMapKey({
|
|
30
|
+
messageMapKey({
|
|
31
|
+
agentId: s.activeAgentId,
|
|
32
|
+
groupId: s.activeGroupId,
|
|
33
|
+
topicId: s.activeTopicId,
|
|
34
|
+
});
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* Get display messages by key
|
|
@@ -462,7 +462,7 @@ describe('topic action', () => {
|
|
|
462
462
|
});
|
|
463
463
|
});
|
|
464
464
|
|
|
465
|
-
it('should
|
|
465
|
+
it('should clear new key data when switching with undefined (same as null)', async () => {
|
|
466
466
|
const { result } = renderHook(() => useChatStore());
|
|
467
467
|
const activeAgentId = 'test-agent-id';
|
|
468
468
|
|
|
@@ -475,13 +475,19 @@ describe('topic action', () => {
|
|
|
475
475
|
|
|
476
476
|
const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
|
|
477
477
|
|
|
478
|
-
// Switch with undefined (should
|
|
478
|
+
// Switch with undefined (should clear because id == null matches both null and undefined)
|
|
479
479
|
await act(async () => {
|
|
480
480
|
await result.current.switchTopic(undefined, { skipRefreshMessage: true });
|
|
481
481
|
});
|
|
482
482
|
|
|
483
|
-
// replaceMessages
|
|
484
|
-
expect(replaceMessagesSpy).
|
|
483
|
+
// replaceMessages SHOULD be called when switching with undefined
|
|
484
|
+
expect(replaceMessagesSpy).toHaveBeenCalledWith([], {
|
|
485
|
+
context: expect.objectContaining({
|
|
486
|
+
agentId: activeAgentId,
|
|
487
|
+
topicId: null,
|
|
488
|
+
}),
|
|
489
|
+
action: expect.any(String),
|
|
490
|
+
});
|
|
485
491
|
});
|
|
486
492
|
|
|
487
493
|
it('should not clear new key data when switching to an existing topic', async () => {
|
|
@@ -532,10 +532,11 @@ export const chatTopic: StateCreator<
|
|
|
532
532
|
const { activeAgentId, activeGroupId } = get();
|
|
533
533
|
|
|
534
534
|
// Clear the _new key data in the following cases:
|
|
535
|
-
// 1. When id is
|
|
535
|
+
// 1. When id is null or undefined (switching to empty topic state)
|
|
536
536
|
// 2. When clearNewKey option is explicitly true
|
|
537
537
|
// This prevents stale data from previous conversations showing up
|
|
538
|
-
|
|
538
|
+
// Note: Use == null to match both null and undefined
|
|
539
|
+
const shouldClearNewKey = !id || opts.clearNewKey;
|
|
539
540
|
|
|
540
541
|
if (shouldClearNewKey && activeAgentId) {
|
|
541
542
|
// Determine scope: use explicit scope from options, or infer from activeGroupId
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { OFFICIAL_URL } from '@lobechat/const';
|
|
2
|
+
|
|
1
3
|
import { type ElectronState } from '../initialState';
|
|
2
4
|
|
|
3
5
|
const isSyncActive = (s: ElectronState) => s.dataSyncConfig.active;
|
|
4
6
|
|
|
5
7
|
const storageMode = (s: ElectronState) => s.dataSyncConfig.storageMode;
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the effective remote server URL based on storage mode:
|
|
11
|
+
* - Cloud mode: returns OFFICIAL_URL
|
|
12
|
+
* - SelfHost mode: returns the configured remoteServerUrl
|
|
13
|
+
*/
|
|
14
|
+
const remoteServerUrl = (s: ElectronState) =>
|
|
15
|
+
s.dataSyncConfig.storageMode === 'cloud' ? OFFICIAL_URL : s.dataSyncConfig.remoteServerUrl || '';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns the raw remoteServerUrl from config without transformation.
|
|
19
|
+
* Use this when you need the original configured value (e.g., for editing forms).
|
|
20
|
+
*/
|
|
21
|
+
const rawRemoteServerUrl = (s: ElectronState) => s.dataSyncConfig.remoteServerUrl || '';
|
|
7
22
|
|
|
8
23
|
export const electronSyncSelectors = {
|
|
9
24
|
isSyncActive,
|
|
25
|
+
rawRemoteServerUrl,
|
|
10
26
|
remoteServerUrl,
|
|
11
27
|
storageMode,
|
|
12
28
|
};
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
|
-
|
|
3
|
-
import { BaseProcessor } from '../base/BaseProcessor';
|
|
4
|
-
import type { Message, PipelineContext, ProcessorOptions } from '../types';
|
|
5
|
-
|
|
6
|
-
const log = debug('context-engine:processor:GroupMessageSenderProcessor');
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Agent info for message sender identification
|
|
10
|
-
*/
|
|
11
|
-
export interface AgentInfo {
|
|
12
|
-
name: string;
|
|
13
|
-
role: 'supervisor' | 'participant';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Configuration for GroupMessageSenderProcessor
|
|
18
|
-
*/
|
|
19
|
-
export interface GroupMessageSenderConfig {
|
|
20
|
-
/**
|
|
21
|
-
* Mapping from agentId to agent info
|
|
22
|
-
* Used to look up agent name and role for each message
|
|
23
|
-
*/
|
|
24
|
-
agentMap: Record<string, AgentInfo>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Group Message Sender Processor
|
|
29
|
-
*
|
|
30
|
-
* Responsible for injecting sender identity information into assistant messages
|
|
31
|
-
* in group chat scenarios. This helps the model understand which agent sent
|
|
32
|
-
* each message in a multi-agent conversation.
|
|
33
|
-
*
|
|
34
|
-
* The processor appends a system context block at the end of each assistant
|
|
35
|
-
* message that has an agentId, containing:
|
|
36
|
-
* - Agent name
|
|
37
|
-
* - Agent role (supervisor or agent)
|
|
38
|
-
* - Agent ID (with instruction not to expose it in responses)
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const processor = new GroupMessageSenderProcessor({
|
|
43
|
-
* agentMap: {
|
|
44
|
-
* 'agt_xxx': { name: 'Weather Expert', role: 'agent' },
|
|
45
|
-
* 'agt_yyy': { name: 'Supervisor', role: 'supervisor' },
|
|
46
|
-
* }
|
|
47
|
-
* });
|
|
48
|
-
* ```
|
|
49
|
-
*/
|
|
50
|
-
export class GroupMessageSenderProcessor extends BaseProcessor {
|
|
51
|
-
readonly name = 'GroupMessageSenderProcessor';
|
|
52
|
-
|
|
53
|
-
constructor(
|
|
54
|
-
private config: GroupMessageSenderConfig,
|
|
55
|
-
options: ProcessorOptions = {},
|
|
56
|
-
) {
|
|
57
|
-
super(options);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
|
|
61
|
-
const clonedContext = this.cloneContext(context);
|
|
62
|
-
|
|
63
|
-
// Skip if no agentMap provided
|
|
64
|
-
if (!this.config.agentMap || Object.keys(this.config.agentMap).length === 0) {
|
|
65
|
-
log('No agentMap provided, skipping processing');
|
|
66
|
-
return this.markAsExecuted(clonedContext);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let processedCount = 0;
|
|
70
|
-
|
|
71
|
-
clonedContext.messages = clonedContext.messages.map((msg: Message) => {
|
|
72
|
-
// Only process assistant messages with agentId
|
|
73
|
-
if (msg.role === 'assistant' && msg.agentId) {
|
|
74
|
-
const agentInfo = this.config.agentMap[msg.agentId];
|
|
75
|
-
|
|
76
|
-
if (agentInfo) {
|
|
77
|
-
// Build the sender tag
|
|
78
|
-
const senderTag = this.buildSenderContext(msg.agentId, agentInfo);
|
|
79
|
-
|
|
80
|
-
// Prepend to message content (at the beginning)
|
|
81
|
-
if (typeof msg.content === 'string') {
|
|
82
|
-
processedCount++;
|
|
83
|
-
log(
|
|
84
|
-
`Injecting sender info for message from agent: ${agentInfo.name} (${agentInfo.role})`,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
...msg,
|
|
89
|
-
content: senderTag + msg.content,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
// Handle array content (multimodal messages)
|
|
93
|
-
else if (Array.isArray(msg.content)) {
|
|
94
|
-
const firstTextIndex = msg.content.findIndex((part: any) => part.type === 'text');
|
|
95
|
-
|
|
96
|
-
if (firstTextIndex !== -1) {
|
|
97
|
-
processedCount++;
|
|
98
|
-
log(
|
|
99
|
-
`Injecting sender info for multimodal message from agent: ${agentInfo.name} (${agentInfo.role})`,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const newContent = [...msg.content];
|
|
103
|
-
newContent[firstTextIndex] = {
|
|
104
|
-
...newContent[firstTextIndex],
|
|
105
|
-
text: senderTag + newContent[firstTextIndex].text,
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
...msg,
|
|
110
|
-
content: newContent,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return msg;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Update metadata
|
|
121
|
-
clonedContext.metadata.groupMessageSenderProcessed = processedCount;
|
|
122
|
-
|
|
123
|
-
log(`Group message sender processing completed: ${processedCount} messages processed`);
|
|
124
|
-
|
|
125
|
-
return this.markAsExecuted(clonedContext);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Build the sender tag to prepend to assistant messages.
|
|
130
|
-
*
|
|
131
|
-
* Uses a self-closing XML tag at the beginning:
|
|
132
|
-
* - Placed at start so model sees "who is speaking" before the content
|
|
133
|
-
* - Self-closing tag is less likely to be reproduced as it's not a "wrapper"
|
|
134
|
-
*/
|
|
135
|
-
private buildSenderContext(_agentId: string, agentInfo: AgentInfo): string {
|
|
136
|
-
return `<speaker name="${agentInfo.name}" />\n`;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { PipelineContext } from '../../types';
|
|
4
|
-
import { GroupMessageSenderProcessor } from '../GroupMessageSender';
|
|
5
|
-
|
|
6
|
-
describe('GroupMessageSenderProcessor', () => {
|
|
7
|
-
const createContext = (messages: any[]): PipelineContext => ({
|
|
8
|
-
initialState: { messages: [] },
|
|
9
|
-
isAborted: false,
|
|
10
|
-
messages,
|
|
11
|
-
metadata: {},
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe('Basic Scenarios', () => {
|
|
15
|
-
it('should inject sender info into assistant message with agentId', async () => {
|
|
16
|
-
const processor = new GroupMessageSenderProcessor({
|
|
17
|
-
agentMap: {
|
|
18
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const input: any[] = [
|
|
23
|
-
{ role: 'user', content: 'What is the weather?' },
|
|
24
|
-
{ role: 'assistant', content: 'The weather is sunny.', agentId: 'agt_weather' },
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const context = createContext(input);
|
|
28
|
-
const result = await processor.process(context);
|
|
29
|
-
|
|
30
|
-
expect(result.messages).toHaveLength(2);
|
|
31
|
-
|
|
32
|
-
// User message should be unchanged
|
|
33
|
-
expect(result.messages[0].content).toBe('What is the weather?');
|
|
34
|
-
|
|
35
|
-
// Assistant message should have speaker tag prepended
|
|
36
|
-
const assistantContent = result.messages[1].content;
|
|
37
|
-
expect(assistantContent).toMatch(/^<speaker name="Weather Expert" \/>/);
|
|
38
|
-
expect(assistantContent).toContain('The weather is sunny.');
|
|
39
|
-
|
|
40
|
-
// Check metadata
|
|
41
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(1);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should inject sender info for supervisor role', async () => {
|
|
45
|
-
const processor = new GroupMessageSenderProcessor({
|
|
46
|
-
agentMap: {
|
|
47
|
-
agt_supervisor: { name: 'Group Supervisor', role: 'supervisor' },
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const input: any[] = [
|
|
52
|
-
{ role: 'assistant', content: 'I will coordinate the agents.', agentId: 'agt_supervisor' },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const context = createContext(input);
|
|
56
|
-
const result = await processor.process(context);
|
|
57
|
-
|
|
58
|
-
const assistantContent = result.messages[0].content;
|
|
59
|
-
expect(assistantContent).toMatch(/^<speaker name="Group Supervisor" \/>/);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should not modify assistant message without agentId', async () => {
|
|
63
|
-
const processor = new GroupMessageSenderProcessor({
|
|
64
|
-
agentMap: {
|
|
65
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const input: any[] = [{ role: 'assistant', content: 'Hello from assistant.' }];
|
|
70
|
-
|
|
71
|
-
const context = createContext(input);
|
|
72
|
-
const result = await processor.process(context);
|
|
73
|
-
|
|
74
|
-
// Should be unchanged
|
|
75
|
-
expect(result.messages[0].content).toBe('Hello from assistant.');
|
|
76
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should not modify assistant message with unknown agentId', async () => {
|
|
80
|
-
const processor = new GroupMessageSenderProcessor({
|
|
81
|
-
agentMap: {
|
|
82
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const input: any[] = [{ role: 'assistant', content: 'Hello.', agentId: 'agt_unknown' }];
|
|
87
|
-
|
|
88
|
-
const context = createContext(input);
|
|
89
|
-
const result = await processor.process(context);
|
|
90
|
-
|
|
91
|
-
// Should be unchanged because agentId not in map
|
|
92
|
-
expect(result.messages[0].content).toBe('Hello.');
|
|
93
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(0);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should not modify user or tool messages', async () => {
|
|
97
|
-
const processor = new GroupMessageSenderProcessor({
|
|
98
|
-
agentMap: {
|
|
99
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const input: any[] = [
|
|
104
|
-
{ role: 'user', content: 'Hello', agentId: 'agt_weather' },
|
|
105
|
-
{ role: 'tool', content: 'Tool result', agentId: 'agt_weather' },
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
const context = createContext(input);
|
|
109
|
-
const result = await processor.process(context);
|
|
110
|
-
|
|
111
|
-
expect(result.messages[0].content).toBe('Hello');
|
|
112
|
-
expect(result.messages[1].content).toBe('Tool result');
|
|
113
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(0);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('Multiple Messages', () => {
|
|
118
|
-
it('should process multiple assistant messages from different agents', async () => {
|
|
119
|
-
const processor = new GroupMessageSenderProcessor({
|
|
120
|
-
agentMap: {
|
|
121
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
122
|
-
agt_news: { name: 'News Reporter', role: 'participant' },
|
|
123
|
-
agt_supervisor: { name: 'Supervisor', role: 'supervisor' },
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const input: any[] = [
|
|
128
|
-
{ role: 'user', content: 'Give me updates' },
|
|
129
|
-
{ role: 'assistant', content: 'Weather is sunny.', agentId: 'agt_weather' },
|
|
130
|
-
{ role: 'assistant', content: 'Top news today.', agentId: 'agt_news' },
|
|
131
|
-
{ role: 'assistant', content: 'Summary complete.', agentId: 'agt_supervisor' },
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
const context = createContext(input);
|
|
135
|
-
const result = await processor.process(context);
|
|
136
|
-
|
|
137
|
-
expect(result.messages).toHaveLength(4);
|
|
138
|
-
|
|
139
|
-
// Check each assistant message has correct speaker tag prepended
|
|
140
|
-
expect(result.messages[1].content).toMatch(/^<speaker name="Weather Expert" \/>/);
|
|
141
|
-
expect(result.messages[2].content).toMatch(/^<speaker name="News Reporter" \/>/);
|
|
142
|
-
expect(result.messages[3].content).toMatch(/^<speaker name="Supervisor" \/>/);
|
|
143
|
-
|
|
144
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(3);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('Multimodal Messages', () => {
|
|
149
|
-
it('should handle array content (multimodal messages)', async () => {
|
|
150
|
-
const processor = new GroupMessageSenderProcessor({
|
|
151
|
-
agentMap: {
|
|
152
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const input: any[] = [
|
|
157
|
-
{
|
|
158
|
-
role: 'assistant',
|
|
159
|
-
content: [
|
|
160
|
-
{ type: 'text', text: 'Here is the weather chart.' },
|
|
161
|
-
{ type: 'image_url', image_url: { url: 'https://example.com/chart.png' } },
|
|
162
|
-
],
|
|
163
|
-
agentId: 'agt_weather',
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
|
|
167
|
-
const context = createContext(input);
|
|
168
|
-
const result = await processor.process(context);
|
|
169
|
-
|
|
170
|
-
const content = result.messages[0].content as any[];
|
|
171
|
-
expect(Array.isArray(content)).toBe(true);
|
|
172
|
-
|
|
173
|
-
// The first text part should have speaker tag prepended
|
|
174
|
-
const textPart = content.find((p: any) => p.type === 'text');
|
|
175
|
-
expect(textPart.text).toMatch(/^<speaker name="Weather Expert" \/>/);
|
|
176
|
-
expect(textPart.text).toContain('Here is the weather chart.');
|
|
177
|
-
|
|
178
|
-
// Image part should be unchanged
|
|
179
|
-
const imagePart = content.find((p: any) => p.type === 'image_url');
|
|
180
|
-
expect(imagePart.image_url.url).toBe('https://example.com/chart.png');
|
|
181
|
-
|
|
182
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(1);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should append to the last text part in multimodal content', async () => {
|
|
186
|
-
const processor = new GroupMessageSenderProcessor({
|
|
187
|
-
agentMap: {
|
|
188
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const input: any[] = [
|
|
193
|
-
{
|
|
194
|
-
role: 'assistant',
|
|
195
|
-
content: [
|
|
196
|
-
{ type: 'text', text: 'First text.' },
|
|
197
|
-
{ type: 'image_url', image_url: { url: 'https://example.com/img.png' } },
|
|
198
|
-
{ type: 'text', text: 'Last text.' },
|
|
199
|
-
],
|
|
200
|
-
agentId: 'agt_weather',
|
|
201
|
-
},
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
const context = createContext(input);
|
|
205
|
-
const result = await processor.process(context);
|
|
206
|
-
|
|
207
|
-
const content = result.messages[0].content;
|
|
208
|
-
|
|
209
|
-
// First text part should have speaker tag prepended
|
|
210
|
-
expect(content[0].text).toMatch(/^<speaker name="Weather Expert" \/>/);
|
|
211
|
-
expect(content[0].text).toContain('First text.');
|
|
212
|
-
|
|
213
|
-
// Last text should be unchanged
|
|
214
|
-
expect(content[2].text).toBe('Last text.');
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Edge Cases', () => {
|
|
219
|
-
it('should skip processing with empty agentMap', async () => {
|
|
220
|
-
const processor = new GroupMessageSenderProcessor({
|
|
221
|
-
agentMap: {},
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const input: any[] = [{ role: 'assistant', content: 'Hello', agentId: 'agt_weather' }];
|
|
225
|
-
|
|
226
|
-
const context = createContext(input);
|
|
227
|
-
const result = await processor.process(context);
|
|
228
|
-
|
|
229
|
-
expect(result.messages[0].content).toBe('Hello');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('should handle empty messages array', async () => {
|
|
233
|
-
const processor = new GroupMessageSenderProcessor({
|
|
234
|
-
agentMap: {
|
|
235
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
236
|
-
},
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const context = createContext([]);
|
|
240
|
-
const result = await processor.process(context);
|
|
241
|
-
|
|
242
|
-
expect(result.messages).toHaveLength(0);
|
|
243
|
-
expect(result.metadata.groupMessageSenderProcessed).toBe(0);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should preserve original message properties', async () => {
|
|
247
|
-
const processor = new GroupMessageSenderProcessor({
|
|
248
|
-
agentMap: {
|
|
249
|
-
agt_weather: { name: 'Weather Expert', role: 'participant' },
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const input: any[] = [
|
|
254
|
-
{
|
|
255
|
-
role: 'assistant',
|
|
256
|
-
content: 'Weather info.',
|
|
257
|
-
agentId: 'agt_weather',
|
|
258
|
-
id: 'msg-123',
|
|
259
|
-
createdAt: '2025-01-01T00:00:00Z',
|
|
260
|
-
customField: 'custom value',
|
|
261
|
-
},
|
|
262
|
-
];
|
|
263
|
-
|
|
264
|
-
const context = createContext(input);
|
|
265
|
-
const result = await processor.process(context);
|
|
266
|
-
|
|
267
|
-
const msg = result.messages[0];
|
|
268
|
-
expect(msg.id).toBe('msg-123');
|
|
269
|
-
expect(msg.createdAt).toBe('2025-01-01T00:00:00Z');
|
|
270
|
-
expect(msg.customField).toBe('custom value');
|
|
271
|
-
expect(msg.agentId).toBe('agt_weather');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const TITLE_BAR_HEIGHT = 30;
|