@lobehub/lobehub 2.0.0-next.32 → 2.0.0-next.34
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/.github/workflows/test.yml +1 -0
- package/CHANGELOG.md +58 -0
- package/apps/desktop/package.json +1 -1
- package/changelog/v1.json +21 -0
- package/docker-compose/local/.env.example +3 -0
- package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
- package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
- package/package.json +1 -1
- package/packages/const/src/hotkeys.ts +3 -3
- package/packages/const/src/models.ts +2 -2
- package/packages/const/src/utils/merge.ts +3 -3
- package/packages/conversation-flow/package.json +13 -0
- package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
- package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
- package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
- package/packages/conversation-flow/src/index.ts +17 -0
- package/packages/conversation-flow/src/indexing.ts +58 -0
- package/packages/conversation-flow/src/parse.ts +53 -0
- package/packages/conversation-flow/src/structuring.ts +38 -0
- package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
- package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
- package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
- package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
- package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
- package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
- package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
- package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
- package/packages/conversation-flow/src/transformation/index.ts +78 -0
- package/packages/conversation-flow/src/types/contextTree.ts +65 -0
- package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
- package/packages/conversation-flow/src/types/shared.ts +63 -0
- package/packages/conversation-flow/src/types.ts +36 -0
- package/packages/conversation-flow/vitest.config.mts +10 -0
- package/packages/model-bank/src/aiModels/google.ts +1 -1
- package/packages/types/src/message/common/metadata.ts +5 -1
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
- package/src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx +97 -7
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/DisabledModels.tsx +144 -8
- package/src/envs/__tests__/app.test.ts +47 -13
- package/src/envs/app.ts +6 -0
- package/src/locales/default/modelProvider.ts +15 -1
- package/src/server/routers/async/__tests__/caller.test.ts +333 -0
- package/src/server/routers/async/caller.ts +2 -1
- package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
- package/src/server/routers/lambda/message.ts +2 -2
- package/src/server/services/mcp/deps/checkers/ManualInstallationChecker.test.ts +162 -0
- package/src/server/services/mcp/deps/checkers/NpmInstallationChecker.test.ts +374 -0
- package/src/server/services/mcp/deps/checkers/PythonInstallationChecker.test.ts +368 -0
- package/src/server/services/message/__tests__/index.test.ts +4 -4
- package/src/server/services/message/index.ts +1 -1
- package/src/services/message/index.ts +2 -3
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
- package/src/store/chat/slices/message/action.test.ts +7 -7
- package/src/store/chat/slices/message/action.ts +2 -2
- package/src/store/chat/slices/plugin/action.test.ts +7 -7
- package/src/store/chat/slices/plugin/action.ts +1 -1
- package/src/store/global/initialState.ts +4 -0
- package/src/store/global/selectors/systemStatus.ts +6 -0
- package/packages/context-engine/ARCHITECTURE.md +0 -425
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import type { AssistantContentBlock, ChatToolPayloadWithResult } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
import type { Message, MessageGroupMetadata } from '../types';
|
|
4
|
+
import type { BranchResolver } from './BranchResolver';
|
|
5
|
+
import type { MessageCollector } from './MessageCollector';
|
|
6
|
+
import type { MessageTransformer } from './MessageTransformer';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FlatListBuilder - Builds flat message list following the active path
|
|
10
|
+
*
|
|
11
|
+
* Handles:
|
|
12
|
+
* 1. Recursive traversal following active branches
|
|
13
|
+
* 2. Creating virtual messages for Compare and AssistantGroup
|
|
14
|
+
* 3. Processing different message types with priority
|
|
15
|
+
*/
|
|
16
|
+
export class FlatListBuilder {
|
|
17
|
+
constructor(
|
|
18
|
+
private messageMap: Map<string, Message>,
|
|
19
|
+
private messageGroupMap: Map<string, MessageGroupMetadata>,
|
|
20
|
+
private childrenMap: Map<string | null, string[]>,
|
|
21
|
+
private branchResolver: BranchResolver,
|
|
22
|
+
private messageCollector: MessageCollector,
|
|
23
|
+
private messageTransformer: MessageTransformer,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate flatList from messages array
|
|
28
|
+
* Only includes messages in the active path
|
|
29
|
+
*/
|
|
30
|
+
flatten(messages: Message[]): Message[] {
|
|
31
|
+
const flatList: Message[] = [];
|
|
32
|
+
const processedIds = new Set<string>();
|
|
33
|
+
|
|
34
|
+
// Build the active path by traversing from root
|
|
35
|
+
this.buildFlatListRecursive(null, flatList, processedIds, messages);
|
|
36
|
+
|
|
37
|
+
return flatList;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Recursively build flatList following the active path
|
|
42
|
+
*/
|
|
43
|
+
private buildFlatListRecursive(
|
|
44
|
+
parentId: string | null,
|
|
45
|
+
flatList: Message[],
|
|
46
|
+
processedIds: Set<string>,
|
|
47
|
+
allMessages: Message[],
|
|
48
|
+
): void {
|
|
49
|
+
const children = this.childrenMap.get(parentId) ?? [];
|
|
50
|
+
|
|
51
|
+
for (const childId of children) {
|
|
52
|
+
if (processedIds.has(childId)) continue;
|
|
53
|
+
|
|
54
|
+
const message = this.messageMap.get(childId);
|
|
55
|
+
if (!message) continue;
|
|
56
|
+
|
|
57
|
+
// Priority 1: Compare message group
|
|
58
|
+
const messageGroup = message.groupId ? this.messageGroupMap.get(message.groupId) : undefined;
|
|
59
|
+
|
|
60
|
+
if (messageGroup && messageGroup.mode === 'compare' && !processedIds.has(messageGroup.id)) {
|
|
61
|
+
const groupMembers = this.messageCollector.collectGroupMembers(
|
|
62
|
+
message.groupId!,
|
|
63
|
+
allMessages,
|
|
64
|
+
);
|
|
65
|
+
const compareMessage = this.createCompareMessage(messageGroup, groupMembers);
|
|
66
|
+
flatList.push(compareMessage);
|
|
67
|
+
groupMembers.forEach((m) => processedIds.add(m.id));
|
|
68
|
+
processedIds.add(messageGroup.id);
|
|
69
|
+
|
|
70
|
+
// Continue with active column's children (if any)
|
|
71
|
+
if ((compareMessage as any).activeColumnId) {
|
|
72
|
+
this.buildFlatListRecursive(
|
|
73
|
+
(compareMessage as any).activeColumnId,
|
|
74
|
+
flatList,
|
|
75
|
+
processedIds,
|
|
76
|
+
allMessages,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Priority 2: AssistantGroup (assistant + tools)
|
|
83
|
+
if (message.role === 'assistant' && message.tools && message.tools.length > 0) {
|
|
84
|
+
// Collect the entire assistant group chain
|
|
85
|
+
const assistantChain: Message[] = [];
|
|
86
|
+
const allToolMessages: Message[] = [];
|
|
87
|
+
this.messageCollector.collectAssistantChain(
|
|
88
|
+
message,
|
|
89
|
+
allMessages,
|
|
90
|
+
assistantChain,
|
|
91
|
+
allToolMessages,
|
|
92
|
+
processedIds,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Create assistantGroup virtual message
|
|
96
|
+
const groupMessage = this.createAssistantGroupMessage(
|
|
97
|
+
assistantChain[0],
|
|
98
|
+
assistantChain,
|
|
99
|
+
allToolMessages,
|
|
100
|
+
);
|
|
101
|
+
flatList.push(groupMessage);
|
|
102
|
+
|
|
103
|
+
// Mark all as processed
|
|
104
|
+
assistantChain.forEach((m) => processedIds.add(m.id));
|
|
105
|
+
allToolMessages.forEach((m) => processedIds.add(m.id));
|
|
106
|
+
|
|
107
|
+
// Continue after the assistant chain
|
|
108
|
+
// Priority 1: If last assistant has non-tool children, continue from it
|
|
109
|
+
// Priority 2: Otherwise continue from tools (for cases where user replies to tool)
|
|
110
|
+
const lastAssistant = assistantChain.at(-1);
|
|
111
|
+
const toolIds = new Set(allToolMessages.map((t) => t.id));
|
|
112
|
+
|
|
113
|
+
const lastAssistantNonToolChildren = lastAssistant
|
|
114
|
+
? this.childrenMap.get(lastAssistant.id)?.filter((childId) => !toolIds.has(childId))
|
|
115
|
+
: undefined;
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
lastAssistantNonToolChildren &&
|
|
119
|
+
lastAssistantNonToolChildren.length > 0 &&
|
|
120
|
+
lastAssistant
|
|
121
|
+
) {
|
|
122
|
+
// Follow-up messages exist after the last assistant (not tools)
|
|
123
|
+
this.buildFlatListRecursive(lastAssistant.id, flatList, processedIds, allMessages);
|
|
124
|
+
} else {
|
|
125
|
+
// No non-tool children of last assistant, check tools for children
|
|
126
|
+
for (const toolMsg of allToolMessages) {
|
|
127
|
+
this.buildFlatListRecursive(toolMsg.id, flatList, processedIds, allMessages);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Priority 3a: Compare mode from user message metadata
|
|
134
|
+
const childMessages = this.childrenMap.get(message.id) ?? [];
|
|
135
|
+
if (this.isCompareMode(message) && childMessages.length > 1) {
|
|
136
|
+
// Add user message
|
|
137
|
+
flatList.push(message);
|
|
138
|
+
processedIds.add(message.id);
|
|
139
|
+
|
|
140
|
+
// Create compare virtual message with proper handling of AssistantGroups
|
|
141
|
+
const compareMessage = this.createCompareMessageFromChildIds(
|
|
142
|
+
message,
|
|
143
|
+
childMessages,
|
|
144
|
+
allMessages,
|
|
145
|
+
processedIds,
|
|
146
|
+
);
|
|
147
|
+
flatList.push(compareMessage);
|
|
148
|
+
|
|
149
|
+
// Continue with active column's children (if any)
|
|
150
|
+
if ((compareMessage as any).activeColumnId) {
|
|
151
|
+
this.buildFlatListRecursive(
|
|
152
|
+
(compareMessage as any).activeColumnId,
|
|
153
|
+
flatList,
|
|
154
|
+
processedIds,
|
|
155
|
+
allMessages,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Priority 3b: User message with branches
|
|
162
|
+
if (message.role === 'user' && childMessages.length > 1) {
|
|
163
|
+
const activeBranchId = this.branchResolver.getActiveBranchIdFromMetadata(
|
|
164
|
+
message,
|
|
165
|
+
childMessages,
|
|
166
|
+
this.childrenMap,
|
|
167
|
+
);
|
|
168
|
+
const userWithBranches = this.createUserMessageWithBranches(message);
|
|
169
|
+
flatList.push(userWithBranches);
|
|
170
|
+
processedIds.add(message.id);
|
|
171
|
+
|
|
172
|
+
// Continue with active branch and process its message
|
|
173
|
+
const activeBranchMsg = this.messageMap.get(activeBranchId);
|
|
174
|
+
if (activeBranchMsg) {
|
|
175
|
+
flatList.push(activeBranchMsg);
|
|
176
|
+
processedIds.add(activeBranchId);
|
|
177
|
+
|
|
178
|
+
// Continue with active branch's children
|
|
179
|
+
this.buildFlatListRecursive(activeBranchId, flatList, processedIds, allMessages);
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Priority 3c: Assistant message with branches
|
|
185
|
+
if (message.role === 'assistant' && childMessages.length > 1) {
|
|
186
|
+
const activeBranchId = this.branchResolver.getActiveBranchIdFromMetadata(
|
|
187
|
+
message,
|
|
188
|
+
childMessages,
|
|
189
|
+
this.childrenMap,
|
|
190
|
+
);
|
|
191
|
+
// Add the assistant message itself
|
|
192
|
+
flatList.push(message);
|
|
193
|
+
processedIds.add(message.id);
|
|
194
|
+
|
|
195
|
+
// Continue with active branch and process its message
|
|
196
|
+
const activeBranchMsg = this.messageMap.get(activeBranchId);
|
|
197
|
+
if (activeBranchMsg) {
|
|
198
|
+
flatList.push(activeBranchMsg);
|
|
199
|
+
processedIds.add(activeBranchId);
|
|
200
|
+
|
|
201
|
+
// Continue with active branch's children
|
|
202
|
+
this.buildFlatListRecursive(activeBranchId, flatList, processedIds, allMessages);
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Priority 4: Regular message
|
|
208
|
+
flatList.push(message);
|
|
209
|
+
processedIds.add(message.id);
|
|
210
|
+
|
|
211
|
+
// Continue with children
|
|
212
|
+
this.buildFlatListRecursive(message.id, flatList, processedIds, allMessages);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if message has compare mode in metadata
|
|
218
|
+
*/
|
|
219
|
+
private isCompareMode(message: Message): boolean {
|
|
220
|
+
return (message.metadata as any)?.compare === true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create compare virtual message from child IDs with AssistantGroup support
|
|
225
|
+
*/
|
|
226
|
+
private createCompareMessageFromChildIds(
|
|
227
|
+
parentMessage: Message,
|
|
228
|
+
childIds: string[],
|
|
229
|
+
allMessages: Message[],
|
|
230
|
+
processedIds: Set<string>,
|
|
231
|
+
): Message {
|
|
232
|
+
const columns: Message[][] = [];
|
|
233
|
+
const columnFirstIds: string[] = [];
|
|
234
|
+
let activeColumnId: string | undefined;
|
|
235
|
+
|
|
236
|
+
// Process each child (column)
|
|
237
|
+
for (const childId of childIds) {
|
|
238
|
+
const childMessage = this.messageMap.get(childId);
|
|
239
|
+
if (!childMessage) continue;
|
|
240
|
+
|
|
241
|
+
columnFirstIds.push(childId);
|
|
242
|
+
|
|
243
|
+
// Check if this message is marked as active column
|
|
244
|
+
if ((childMessage.metadata as any)?.activeColumn === true) {
|
|
245
|
+
activeColumnId = childId;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check if this child is an AssistantGroup
|
|
249
|
+
if (
|
|
250
|
+
childMessage.role === 'assistant' &&
|
|
251
|
+
childMessage.tools &&
|
|
252
|
+
childMessage.tools.length > 0
|
|
253
|
+
) {
|
|
254
|
+
// Collect the entire assistant group chain for this column
|
|
255
|
+
const assistantChain: Message[] = [];
|
|
256
|
+
const allToolMessages: Message[] = [];
|
|
257
|
+
const columnProcessedIds = new Set<string>();
|
|
258
|
+
|
|
259
|
+
this.messageCollector.collectAssistantChain(
|
|
260
|
+
childMessage,
|
|
261
|
+
allMessages,
|
|
262
|
+
assistantChain,
|
|
263
|
+
allToolMessages,
|
|
264
|
+
columnProcessedIds,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// Create assistantGroup virtual message for this column
|
|
268
|
+
const groupMessage = this.createAssistantGroupMessage(
|
|
269
|
+
assistantChain[0],
|
|
270
|
+
assistantChain,
|
|
271
|
+
allToolMessages,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
columns.push([groupMessage]);
|
|
275
|
+
|
|
276
|
+
// Mark all as processed
|
|
277
|
+
assistantChain.forEach((m) => processedIds.add(m.id));
|
|
278
|
+
allToolMessages.forEach((m) => processedIds.add(m.id));
|
|
279
|
+
} else {
|
|
280
|
+
// Regular message (not an AssistantGroup)
|
|
281
|
+
columns.push([childMessage]);
|
|
282
|
+
processedIds.add(childId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Generate ID with all column first message IDs
|
|
287
|
+
const columnIdsStr = columnFirstIds.join('-');
|
|
288
|
+
const compareId = `compare-${parentMessage.id}-${columnIdsStr}`;
|
|
289
|
+
|
|
290
|
+
// Calculate timestamps from first column's messages
|
|
291
|
+
const firstColumnMessages = childIds.map((id) => this.messageMap.get(id)).filter(Boolean);
|
|
292
|
+
const createdAt =
|
|
293
|
+
firstColumnMessages.length > 0
|
|
294
|
+
? Math.min(...firstColumnMessages.map((m) => m!.createdAt))
|
|
295
|
+
: parentMessage.createdAt;
|
|
296
|
+
const updatedAt =
|
|
297
|
+
firstColumnMessages.length > 0
|
|
298
|
+
? Math.max(...firstColumnMessages.map((m) => m!.updatedAt))
|
|
299
|
+
: parentMessage.updatedAt;
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
activeColumnId,
|
|
303
|
+
columns: columns as any,
|
|
304
|
+
content: '',
|
|
305
|
+
createdAt,
|
|
306
|
+
extra: {
|
|
307
|
+
parentMessageId: parentMessage.id,
|
|
308
|
+
},
|
|
309
|
+
id: compareId,
|
|
310
|
+
meta: parentMessage.meta || {},
|
|
311
|
+
role: 'compare' as any,
|
|
312
|
+
updatedAt,
|
|
313
|
+
} as Message;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Create compare virtual message (for group-based compare)
|
|
318
|
+
*/
|
|
319
|
+
private createCompareMessage(group: MessageGroupMetadata, members: Message[]): Message {
|
|
320
|
+
// Find active column ID from members metadata
|
|
321
|
+
const activeColumnId = members.find((msg) => (msg.metadata as any)?.activeColumn === true)?.id;
|
|
322
|
+
|
|
323
|
+
// columns contain full Message objects
|
|
324
|
+
const columns: Message[][] = members.map((msg) => [msg]);
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
activeColumnId,
|
|
328
|
+
columns: columns as any,
|
|
329
|
+
content: '',
|
|
330
|
+
createdAt: Math.min(...members.map((m) => m.createdAt)),
|
|
331
|
+
extra: {
|
|
332
|
+
groupMode: group.mode,
|
|
333
|
+
parentMessageId: group.parentMessageId,
|
|
334
|
+
},
|
|
335
|
+
id: group.id,
|
|
336
|
+
meta: members[0]?.meta || {},
|
|
337
|
+
role: 'compare' as any,
|
|
338
|
+
updatedAt: Math.max(...members.map((m) => m.updatedAt)),
|
|
339
|
+
} as Message;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create assistant group virtual message from entire chain
|
|
344
|
+
*/
|
|
345
|
+
private createAssistantGroupMessage(
|
|
346
|
+
firstAssistant: Message,
|
|
347
|
+
assistantChain: Message[],
|
|
348
|
+
allToolMessages: Message[],
|
|
349
|
+
): Message {
|
|
350
|
+
const children: AssistantContentBlock[] = [];
|
|
351
|
+
|
|
352
|
+
// Create tool map for lookup
|
|
353
|
+
const toolMap = new Map<string, Message>();
|
|
354
|
+
allToolMessages.forEach((tm) => {
|
|
355
|
+
if (tm.tool_call_id) {
|
|
356
|
+
toolMap.set(tm.tool_call_id, tm);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Process each assistant in the chain
|
|
361
|
+
for (const assistant of assistantChain) {
|
|
362
|
+
// Build toolsWithResults for this assistant
|
|
363
|
+
const toolsWithResults: ChatToolPayloadWithResult[] =
|
|
364
|
+
assistant.tools?.map((tool) => {
|
|
365
|
+
const toolMsg = toolMap.get(tool.id);
|
|
366
|
+
if (toolMsg) {
|
|
367
|
+
return {
|
|
368
|
+
...tool,
|
|
369
|
+
result: {
|
|
370
|
+
content: toolMsg.content || '',
|
|
371
|
+
error: toolMsg.error,
|
|
372
|
+
id: toolMsg.id,
|
|
373
|
+
state: toolMsg.pluginState,
|
|
374
|
+
},
|
|
375
|
+
result_msg_id: toolMsg.id,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return tool;
|
|
379
|
+
}) || [];
|
|
380
|
+
|
|
381
|
+
const { usage: msgUsage, performance: msgPerformance } =
|
|
382
|
+
this.messageTransformer.splitMetadata(assistant.metadata);
|
|
383
|
+
|
|
384
|
+
children.push({
|
|
385
|
+
content: assistant.content || '',
|
|
386
|
+
error: assistant.error,
|
|
387
|
+
id: assistant.id,
|
|
388
|
+
imageList:
|
|
389
|
+
assistant.imageList && assistant.imageList.length > 0 ? assistant.imageList : undefined,
|
|
390
|
+
performance: msgPerformance,
|
|
391
|
+
reasoning: assistant.reasoning || undefined,
|
|
392
|
+
tools: toolsWithResults.length > 0 ? toolsWithResults : undefined,
|
|
393
|
+
usage: msgUsage,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const aggregated = this.messageTransformer.aggregateMetadata(children);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
...firstAssistant,
|
|
401
|
+
children,
|
|
402
|
+
content: '',
|
|
403
|
+
imageList: undefined,
|
|
404
|
+
metadata: undefined,
|
|
405
|
+
performance: aggregated.performance,
|
|
406
|
+
reasoning: undefined,
|
|
407
|
+
role: 'assistantGroup' as any,
|
|
408
|
+
tools: undefined,
|
|
409
|
+
usage: aggregated.usage,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Create user message with branch metadata
|
|
415
|
+
*/
|
|
416
|
+
private createUserMessageWithBranches(user: Message): Message {
|
|
417
|
+
// Just return the original user message with its metadata.activeBranchId
|
|
418
|
+
// No need to add extra.branches
|
|
419
|
+
return { ...user };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ContextNode, IdNode, Message, MessageNode } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MessageCollector - Handles collection of related messages
|
|
5
|
+
*
|
|
6
|
+
* Provides utilities for:
|
|
7
|
+
* 1. Collecting messages in a group
|
|
8
|
+
* 2. Collecting tool messages
|
|
9
|
+
* 3. Collecting assistant chains
|
|
10
|
+
* 4. Finding next messages in sequences
|
|
11
|
+
*/
|
|
12
|
+
export class MessageCollector {
|
|
13
|
+
constructor(
|
|
14
|
+
private messageMap: Map<string, Message>,
|
|
15
|
+
private childrenMap: Map<string | null, string[]>,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Collect all messages belonging to a message group
|
|
20
|
+
*/
|
|
21
|
+
collectGroupMembers(groupId: string, messages: Message[]): Message[] {
|
|
22
|
+
return messages.filter((m) => m.groupId === groupId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Collect tool messages related to an assistant message
|
|
27
|
+
*/
|
|
28
|
+
collectToolMessages(assistant: Message, messages: Message[]): Message[] {
|
|
29
|
+
const toolCallIds = new Set(assistant.tools?.map((t) => t.id) || []);
|
|
30
|
+
return messages.filter(
|
|
31
|
+
(m) => m.role === 'tool' && m.tool_call_id && toolCallIds.has(m.tool_call_id),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Recursively collect the entire assistant chain
|
|
37
|
+
* (assistant -> tools -> assistant -> tools -> ...)
|
|
38
|
+
*/
|
|
39
|
+
collectAssistantChain(
|
|
40
|
+
currentAssistant: Message,
|
|
41
|
+
allMessages: Message[],
|
|
42
|
+
assistantChain: Message[],
|
|
43
|
+
allToolMessages: Message[],
|
|
44
|
+
processedIds: Set<string>,
|
|
45
|
+
): void {
|
|
46
|
+
if (processedIds.has(currentAssistant.id)) return;
|
|
47
|
+
|
|
48
|
+
// Add current assistant to chain
|
|
49
|
+
assistantChain.push(currentAssistant);
|
|
50
|
+
|
|
51
|
+
// Collect its tool messages
|
|
52
|
+
const toolMessages = this.collectToolMessages(currentAssistant, allMessages);
|
|
53
|
+
allToolMessages.push(...toolMessages);
|
|
54
|
+
|
|
55
|
+
// Find next assistant after tools
|
|
56
|
+
for (const toolMsg of toolMessages) {
|
|
57
|
+
const nextMessages = allMessages.filter((m) => m.parentId === toolMsg.id);
|
|
58
|
+
|
|
59
|
+
for (const nextMsg of nextMessages) {
|
|
60
|
+
if (nextMsg.role === 'assistant' && nextMsg.tools && nextMsg.tools.length > 0) {
|
|
61
|
+
// Continue the chain
|
|
62
|
+
this.collectAssistantChain(
|
|
63
|
+
nextMsg,
|
|
64
|
+
allMessages,
|
|
65
|
+
assistantChain,
|
|
66
|
+
allToolMessages,
|
|
67
|
+
processedIds,
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
} else if (nextMsg.role === 'assistant') {
|
|
71
|
+
// Final assistant without tools
|
|
72
|
+
assistantChain.push(nextMsg);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Recursively collect assistant messages for an AssistantGroup (contextTree version)
|
|
81
|
+
*/
|
|
82
|
+
collectAssistantGroupMessages(
|
|
83
|
+
message: Message,
|
|
84
|
+
idNode: IdNode,
|
|
85
|
+
children: ContextNode[],
|
|
86
|
+
): void {
|
|
87
|
+
// Get tool message IDs if this assistant has tools
|
|
88
|
+
const toolIds = idNode.children
|
|
89
|
+
.filter((child) => {
|
|
90
|
+
const childMsg = this.messageMap.get(child.id);
|
|
91
|
+
return childMsg?.role === 'tool';
|
|
92
|
+
})
|
|
93
|
+
.map((child) => child.id);
|
|
94
|
+
|
|
95
|
+
// Add current assistant message node
|
|
96
|
+
const messageNode: MessageNode = {
|
|
97
|
+
id: message.id,
|
|
98
|
+
type: 'message',
|
|
99
|
+
};
|
|
100
|
+
if (toolIds.length > 0) {
|
|
101
|
+
messageNode.tools = toolIds;
|
|
102
|
+
}
|
|
103
|
+
children.push(messageNode);
|
|
104
|
+
|
|
105
|
+
// Find next assistant message after tools
|
|
106
|
+
for (const toolNode of idNode.children) {
|
|
107
|
+
const toolMsg = this.messageMap.get(toolNode.id);
|
|
108
|
+
if (toolMsg?.role !== 'tool') continue;
|
|
109
|
+
|
|
110
|
+
// Check if tool has an assistant child
|
|
111
|
+
if (toolNode.children.length > 0) {
|
|
112
|
+
const nextChild = toolNode.children[0];
|
|
113
|
+
const nextMsg = this.messageMap.get(nextChild.id);
|
|
114
|
+
|
|
115
|
+
if (nextMsg?.role === 'assistant') {
|
|
116
|
+
// Recursively collect this assistant and its descendants
|
|
117
|
+
this.collectAssistantGroupMessages(nextMsg, nextChild, children);
|
|
118
|
+
return; // Only follow one path
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find next message after tools in an assistant group
|
|
126
|
+
*/
|
|
127
|
+
findNextAfterTools(assistantMsg: Message, idNode: IdNode): IdNode | null {
|
|
128
|
+
// Recursively find the last message in the assistant group
|
|
129
|
+
const lastNode = this.findLastNodeInAssistantGroup(idNode);
|
|
130
|
+
if (lastNode && lastNode.children.length > 0) {
|
|
131
|
+
return lastNode.children[0];
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Find the last node in an AssistantGroup sequence
|
|
138
|
+
*/
|
|
139
|
+
findLastNodeInAssistantGroup(idNode: IdNode): IdNode | null {
|
|
140
|
+
// Check if has tool children
|
|
141
|
+
const toolChildren = idNode.children.filter((child) => {
|
|
142
|
+
const childMsg = this.messageMap.get(child.id);
|
|
143
|
+
return childMsg?.role === 'tool';
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (toolChildren.length === 0) {
|
|
147
|
+
return idNode;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if any tool has an assistant child
|
|
151
|
+
for (const toolNode of toolChildren) {
|
|
152
|
+
if (toolNode.children.length > 0) {
|
|
153
|
+
const nextChild = toolNode.children[0];
|
|
154
|
+
const nextMsg = this.messageMap.get(nextChild.id);
|
|
155
|
+
|
|
156
|
+
if (nextMsg?.role === 'assistant') {
|
|
157
|
+
// Continue following the assistant chain
|
|
158
|
+
return this.findLastNodeInAssistantGroup(nextChild);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// No more assistant messages, return the last tool node
|
|
164
|
+
return toolChildren.at(-1) ?? null;
|
|
165
|
+
}
|
|
166
|
+
}
|