@lobehub/lobehub 2.0.0-next.32 → 2.0.0-next.33

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.
Files changed (81) hide show
  1. package/.github/workflows/test.yml +1 -0
  2. package/CHANGELOG.md +33 -0
  3. package/apps/desktop/package.json +1 -1
  4. package/changelog/v1.json +12 -0
  5. package/docker-compose/local/.env.example +3 -0
  6. package/docs/self-hosting/server-database/docker-compose.mdx +29 -0
  7. package/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +29 -0
  8. package/package.json +1 -1
  9. package/packages/const/src/hotkeys.ts +3 -3
  10. package/packages/const/src/models.ts +2 -2
  11. package/packages/const/src/utils/merge.ts +3 -3
  12. package/packages/conversation-flow/package.json +13 -0
  13. package/packages/conversation-flow/src/__tests__/fixtures/index.ts +48 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-chain-with-followup.json +56 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/assistant-with-tools.json +144 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/active-index-1.json +131 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-branch.json +96 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-user-branch.json +123 -0
  19. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/conversation.json +128 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +14 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/nested.json +179 -0
  22. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/index.ts +8 -0
  23. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/simple.json +85 -0
  24. package/packages/conversation-flow/src/__tests__/fixtures/inputs/compare/with-tools.json +169 -0
  25. package/packages/conversation-flow/src/__tests__/fixtures/inputs/complex-scenario.json +107 -0
  26. package/packages/conversation-flow/src/__tests__/fixtures/inputs/index.ts +14 -0
  27. package/packages/conversation-flow/src/__tests__/fixtures/inputs/linear-conversation.json +59 -0
  28. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-chain-with-followup.json +135 -0
  29. package/packages/conversation-flow/src/__tests__/fixtures/outputs/assistant-with-tools.json +340 -0
  30. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +242 -0
  31. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-branch.json +208 -0
  32. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-user-branch.json +254 -0
  33. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +260 -0
  34. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +14 -0
  35. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +389 -0
  36. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/index.ts +8 -0
  37. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/simple.json +224 -0
  38. package/packages/conversation-flow/src/__tests__/fixtures/outputs/compare/with-tools.json +418 -0
  39. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +239 -0
  40. package/packages/conversation-flow/src/__tests__/fixtures/outputs/linear-conversation.json +138 -0
  41. package/packages/conversation-flow/src/__tests__/parse.test.ts +97 -0
  42. package/packages/conversation-flow/src/index.ts +17 -0
  43. package/packages/conversation-flow/src/indexing.ts +58 -0
  44. package/packages/conversation-flow/src/parse.ts +53 -0
  45. package/packages/conversation-flow/src/structuring.ts +38 -0
  46. package/packages/conversation-flow/src/transformation/BranchResolver.ts +66 -0
  47. package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +292 -0
  48. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +421 -0
  49. package/packages/conversation-flow/src/transformation/MessageCollector.ts +166 -0
  50. package/packages/conversation-flow/src/transformation/MessageTransformer.ts +177 -0
  51. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +151 -0
  52. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +385 -0
  53. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +511 -0
  54. package/packages/conversation-flow/src/transformation/__tests__/MessageCollector.test.ts +220 -0
  55. package/packages/conversation-flow/src/transformation/__tests__/MessageTransformer.test.ts +287 -0
  56. package/packages/conversation-flow/src/transformation/index.ts +78 -0
  57. package/packages/conversation-flow/src/types/contextTree.ts +65 -0
  58. package/packages/conversation-flow/src/types/flatMessageList.ts +66 -0
  59. package/packages/conversation-flow/src/types/shared.ts +63 -0
  60. package/packages/conversation-flow/src/types.ts +36 -0
  61. package/packages/conversation-flow/vitest.config.mts +10 -0
  62. package/packages/types/src/message/common/metadata.ts +5 -1
  63. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +3 -4
  64. package/src/envs/__tests__/app.test.ts +47 -13
  65. package/src/envs/app.ts +6 -0
  66. package/src/server/routers/async/__tests__/caller.test.ts +333 -0
  67. package/src/server/routers/async/caller.ts +2 -1
  68. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +57 -57
  69. package/src/server/routers/lambda/message.ts +2 -2
  70. package/src/server/services/message/__tests__/index.test.ts +4 -4
  71. package/src/server/services/message/index.ts +1 -1
  72. package/src/services/message/index.ts +2 -3
  73. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +8 -8
  74. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +8 -8
  75. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +1 -1
  76. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +1 -1
  77. package/src/store/chat/slices/message/action.test.ts +7 -7
  78. package/src/store/chat/slices/message/action.ts +2 -2
  79. package/src/store/chat/slices/plugin/action.test.ts +7 -7
  80. package/src/store/chat/slices/plugin/action.ts +1 -1
  81. package/packages/context-engine/ARCHITECTURE.md +0 -425
@@ -0,0 +1,66 @@
1
+ import type { IdNode, Message } from '../types';
2
+
3
+ /**
4
+ * BranchResolver - Handles branch resolution logic
5
+ *
6
+ * Determines which branch should be active based on:
7
+ * 1. metadata.activeBranchIndex (explicit index)
8
+ * 2. Inferred from which branch has children
9
+ * 3. Default to first branch
10
+ */
11
+ export class BranchResolver {
12
+ /**
13
+ * Get active branch ID from IdNode structure (used in contextTree building)
14
+ */
15
+ getActiveBranchId(message: Message, idNode: IdNode): string {
16
+ // Priority 1: Try to get from metadata.activeBranchIndex (index-based)
17
+ const activeBranchIndex = (message.metadata as any)?.activeBranchIndex;
18
+ if (
19
+ typeof activeBranchIndex === 'number' &&
20
+ activeBranchIndex >= 0 &&
21
+ activeBranchIndex < idNode.children.length
22
+ ) {
23
+ return idNode.children[activeBranchIndex].id;
24
+ }
25
+
26
+ // Priority 2: Infer from which branch has children
27
+ for (const child of idNode.children) {
28
+ if (child.children.length > 0) {
29
+ return child.id;
30
+ }
31
+ }
32
+
33
+ // Default to first branch
34
+ return idNode.children[0].id;
35
+ }
36
+
37
+ /**
38
+ * Get active branch ID from flat list (used in flatList building)
39
+ */
40
+ getActiveBranchIdFromMetadata(
41
+ message: Message,
42
+ childIds: string[],
43
+ childrenMap: Map<string | null, string[]>,
44
+ ): string {
45
+ // Priority 1: Try to get from metadata.activeBranchIndex (index-based)
46
+ const activeBranchIndex = (message.metadata as any)?.activeBranchIndex;
47
+ if (
48
+ typeof activeBranchIndex === 'number' &&
49
+ activeBranchIndex >= 0 &&
50
+ activeBranchIndex < childIds.length
51
+ ) {
52
+ return childIds[activeBranchIndex];
53
+ }
54
+
55
+ // Priority 2: Infer from which child has descendants
56
+ for (const childId of childIds) {
57
+ const descendants = childrenMap.get(childId);
58
+ if (descendants && descendants.length > 0) {
59
+ return childId;
60
+ }
61
+ }
62
+
63
+ // Default to first child
64
+ return childIds[0];
65
+ }
66
+ }
@@ -0,0 +1,292 @@
1
+ import type {
2
+ AssistantGroupNode,
3
+ BranchNode,
4
+ CompareNode,
5
+ ContextNode,
6
+ IdNode,
7
+ Message,
8
+ MessageGroupMetadata,
9
+ MessageNode,
10
+ } from '../types';
11
+ import type { BranchResolver } from './BranchResolver';
12
+ import type { MessageCollector } from './MessageCollector';
13
+
14
+ /**
15
+ * ContextTreeBuilder - Transforms IdNode tree into ContextNode tree
16
+ *
17
+ * Handles:
18
+ * 1. Tree traversal with priority-based node type detection
19
+ * 2. Creating different types of ContextNodes (Message, Branch, Compare, AssistantGroup)
20
+ * 3. Linear array output of the tree structure
21
+ */
22
+ export class ContextTreeBuilder {
23
+ constructor(
24
+ private messageMap: Map<string, Message>,
25
+ private messageGroupMap: Map<string, MessageGroupMetadata>,
26
+ private branchResolver: BranchResolver,
27
+ private messageCollector: MessageCollector,
28
+ private generateNodeId: (prefix: string, messageId: string) => string,
29
+ ) {}
30
+
31
+ /**
32
+ * Transform all root nodes to contextTree
33
+ * Returns a linear array of context nodes
34
+ */
35
+ transformAll(idNodes: IdNode[]): ContextNode[] {
36
+ const contextTree: ContextNode[] = [];
37
+
38
+ for (const idNode of idNodes) {
39
+ this.transformToLinear(idNode, contextTree);
40
+ }
41
+
42
+ return contextTree;
43
+ }
44
+
45
+ /**
46
+ * Transform a single IdNode and append to contextTree array
47
+ */
48
+ private transformToLinear(idNode: IdNode, contextTree: ContextNode[]): void {
49
+ const message = this.messageMap.get(idNode.id);
50
+ if (!message) return;
51
+
52
+ // Priority 1: Compare mode from user message metadata
53
+ if (this.isCompareMode(message) && idNode.children.length > 1) {
54
+ // Add user message node
55
+ const messageNode = this.createMessageNode(message);
56
+ contextTree.push(messageNode);
57
+
58
+ // Create compare node with children
59
+ const compareNode = this.createCompareNodeFromChildren(message, idNode);
60
+ contextTree.push(compareNode);
61
+
62
+ // Continue with active column's children (if any)
63
+ if (compareNode.activeColumnId) {
64
+ // Find the active column's IdNode in the children
65
+ const activeColumnIdNode = idNode.children.find(
66
+ (child) => child.id === compareNode.activeColumnId,
67
+ );
68
+ if (activeColumnIdNode && activeColumnIdNode.children.length > 0) {
69
+ this.transformToLinear(activeColumnIdNode.children[0], contextTree);
70
+ }
71
+ }
72
+ return;
73
+ }
74
+
75
+ // Priority 2: Compare mode (from messageGroup metadata)
76
+ const messageGroup = message.groupId ? this.messageGroupMap.get(message.groupId) : undefined;
77
+
78
+ if (messageGroup && messageGroup.mode === 'compare') {
79
+ // Create compare node
80
+ const compareNode = this.createCompareNode(messageGroup, message);
81
+ contextTree.push(compareNode);
82
+
83
+ // Continue with active column's children (if any)
84
+ if (compareNode.activeColumnId) {
85
+ // Find the active column's IdNode in the children
86
+ const activeColumnIdNode = idNode.children.find(
87
+ (child) => child.id === compareNode.activeColumnId,
88
+ );
89
+ if (activeColumnIdNode && activeColumnIdNode.children.length > 0) {
90
+ this.transformToLinear(activeColumnIdNode.children[0], contextTree);
91
+ }
92
+ }
93
+ return;
94
+ }
95
+
96
+ // Priority 3: AssistantGroup (assistant + tools)
97
+ if (this.isAssistantGroupNode(message, idNode)) {
98
+ const assistantGroupNode = this.createAssistantGroupNode(message, idNode);
99
+ contextTree.push(assistantGroupNode);
100
+
101
+ // Find the next message after tools
102
+ const nextMessage = this.messageCollector.findNextAfterTools(message, idNode);
103
+ if (nextMessage) {
104
+ this.transformToLinear(nextMessage, contextTree);
105
+ }
106
+ return;
107
+ }
108
+
109
+ // Priority 4: Branch (multiple children)
110
+ if (idNode.children.length > 1) {
111
+ // Add current message node
112
+ const messageNode = this.createMessageNode(message);
113
+ contextTree.push(messageNode);
114
+
115
+ // Create branch node
116
+ const branchNode = this.createBranchNode(message, idNode);
117
+ contextTree.push(branchNode);
118
+
119
+ // Don't continue after branch - branch is an end point
120
+ return;
121
+ }
122
+
123
+ // Priority 5: Regular message
124
+ const messageNode = this.createMessageNode(message);
125
+ contextTree.push(messageNode);
126
+
127
+ // Continue with single child
128
+ if (idNode.children.length === 1) {
129
+ this.transformToLinear(idNode.children[0], contextTree);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Check if message has compare mode in metadata
135
+ */
136
+ private isCompareMode(message: Message): boolean {
137
+ return (message.metadata as any)?.compare === true;
138
+ }
139
+
140
+ /**
141
+ * Check if this is Assistant + Tools pattern
142
+ */
143
+ private isAssistantGroupNode(message: Message, idNode: IdNode): boolean {
144
+ if (message.role !== 'assistant') return false;
145
+
146
+ return (
147
+ idNode.children.length > 0 &&
148
+ idNode.children.every((child) => {
149
+ const childMsg = this.messageMap.get(child.id);
150
+ return childMsg?.role === 'tool';
151
+ })
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Create MessageNode (leaf node)
157
+ * Uses the message's own id directly
158
+ */
159
+ private createMessageNode(message: Message): MessageNode {
160
+ return {
161
+ id: message.id,
162
+ type: 'message',
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Create AssistantGroupNode
168
+ * Collects all assistant messages in the sequence (with or without tools)
169
+ */
170
+ private createAssistantGroupNode(message: Message, idNode: IdNode): AssistantGroupNode {
171
+ const children: ContextNode[] = [];
172
+
173
+ // Recursively collect all assistant messages in this group
174
+ this.messageCollector.collectAssistantGroupMessages(message, idNode, children);
175
+
176
+ return {
177
+ children,
178
+ id: message.id,
179
+ type: 'assistantGroup',
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Create BranchNode
185
+ */
186
+ private createBranchNode(message: Message, idNode: IdNode): BranchNode {
187
+ const activeBranchId = this.branchResolver.getActiveBranchId(message, idNode);
188
+ const activeBranchIndex = idNode.children.findIndex((child) => child.id === activeBranchId);
189
+
190
+ // Each branch is a tree starting from that child
191
+ const branches = idNode.children.map((child) => {
192
+ const branchTree: ContextNode[] = [];
193
+ this.transformToLinear(child, branchTree);
194
+ return branchTree;
195
+ });
196
+
197
+ return {
198
+ activeBranchIndex: activeBranchIndex >= 0 ? activeBranchIndex : 0,
199
+ branches,
200
+ id: this.generateNodeId('branch', message.id),
201
+ parentMessageId: message.id,
202
+ type: 'branch',
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Create CompareNode from children messages
208
+ */
209
+ private createCompareNodeFromChildren(message: Message, idNode: IdNode): CompareNode {
210
+ // Find active column ID from children metadata
211
+ let activeColumnId: string | undefined;
212
+
213
+ // Each child is a column - need to recursively process to handle AssistantGroup
214
+ const columns = idNode.children.map((child) => {
215
+ const childMessage = this.messageMap.get(child.id);
216
+ if (!childMessage) {
217
+ return [
218
+ {
219
+ id: child.id,
220
+ type: 'message',
221
+ } as MessageNode,
222
+ ];
223
+ }
224
+
225
+ // Check if this message is marked as active column
226
+ if ((childMessage.metadata as any)?.activeColumn === true) {
227
+ activeColumnId = child.id;
228
+ }
229
+
230
+ // Check if this column should be an AssistantGroup
231
+ if (this.isAssistantGroupNode(childMessage, child)) {
232
+ const assistantGroupNode = this.createAssistantGroupNode(childMessage, child);
233
+ return [assistantGroupNode];
234
+ }
235
+
236
+ // Otherwise, just a simple MessageNode
237
+ return [
238
+ {
239
+ id: child.id,
240
+ type: 'message',
241
+ } as MessageNode,
242
+ ];
243
+ });
244
+
245
+ // Generate ID by joining parent message id and all column message ids
246
+ const columnIds = idNode.children.map((child) => child.id).join('-');
247
+ const compareId = `compare-${message.id}-${columnIds}`;
248
+
249
+ return {
250
+ activeColumnId,
251
+ columns,
252
+ id: compareId,
253
+ messageId: message.id,
254
+ type: 'compare',
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Create CompareNode from message group
260
+ */
261
+ private createCompareNode(group: MessageGroupMetadata, message: Message): CompareNode {
262
+ // Collect all messages in this group
263
+ const groupMessages: Message[] = [];
264
+ for (const msg of this.messageMap.values()) {
265
+ if (msg.groupId === group.id) {
266
+ groupMessages.push(msg);
267
+ }
268
+ }
269
+
270
+ // Find active column ID from group messages metadata
271
+ const activeColumnId = groupMessages.find(
272
+ (msg) => (msg.metadata as any)?.activeColumn === true,
273
+ )?.id;
274
+
275
+ // Each column is a message tree
276
+ const columns = groupMessages.map((msg) => {
277
+ const messageNode: MessageNode = {
278
+ id: msg.id,
279
+ type: 'message',
280
+ };
281
+ return [messageNode];
282
+ });
283
+
284
+ return {
285
+ activeColumnId,
286
+ columns,
287
+ id: this.generateNodeId('compare', group.id),
288
+ messageId: group.parentMessageId || message.id,
289
+ type: 'compare',
290
+ };
291
+ }
292
+ }