@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,177 @@
|
|
|
1
|
+
import type { AssistantContentBlock, ModelPerformance, ModelUsage } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
import type { Message } from '../types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MessageTransformer - Handles message transformation utilities
|
|
7
|
+
*
|
|
8
|
+
* Provides utilities for:
|
|
9
|
+
* 1. Converting Message to AssistantContentBlock
|
|
10
|
+
* 2. Splitting metadata into usage and performance
|
|
11
|
+
* 3. Aggregating metadata from multiple messages
|
|
12
|
+
*/
|
|
13
|
+
export class MessageTransformer {
|
|
14
|
+
/**
|
|
15
|
+
* Convert a Message to AssistantContentBlock
|
|
16
|
+
*/
|
|
17
|
+
messageToContentBlock(message: Message): AssistantContentBlock {
|
|
18
|
+
const { usage, performance } = this.splitMetadata(message.metadata);
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
content: message.content || '',
|
|
22
|
+
error: message.error,
|
|
23
|
+
id: message.id,
|
|
24
|
+
imageList: message.imageList,
|
|
25
|
+
performance,
|
|
26
|
+
reasoning: message.reasoning || undefined,
|
|
27
|
+
tools: message.tools as any,
|
|
28
|
+
usage,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Split metadata into usage and performance objects
|
|
34
|
+
*/
|
|
35
|
+
splitMetadata(metadata?: any): {
|
|
36
|
+
performance?: ModelPerformance;
|
|
37
|
+
usage?: ModelUsage;
|
|
38
|
+
} {
|
|
39
|
+
if (!metadata) return {};
|
|
40
|
+
|
|
41
|
+
const usage: ModelUsage = {};
|
|
42
|
+
const performance: ModelPerformance = {};
|
|
43
|
+
|
|
44
|
+
const usageFields = [
|
|
45
|
+
'acceptedPredictionTokens',
|
|
46
|
+
'cost',
|
|
47
|
+
'inputAudioTokens',
|
|
48
|
+
'inputCacheMissTokens',
|
|
49
|
+
'inputCachedTokens',
|
|
50
|
+
'inputCitationTokens',
|
|
51
|
+
'inputImageTokens',
|
|
52
|
+
'inputTextTokens',
|
|
53
|
+
'inputWriteCacheTokens',
|
|
54
|
+
'outputAudioTokens',
|
|
55
|
+
'outputImageTokens',
|
|
56
|
+
'outputReasoningTokens',
|
|
57
|
+
'outputTextTokens',
|
|
58
|
+
'rejectedPredictionTokens',
|
|
59
|
+
'totalInputTokens',
|
|
60
|
+
'totalOutputTokens',
|
|
61
|
+
'totalTokens',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
let hasUsage = false;
|
|
65
|
+
usageFields.forEach((field) => {
|
|
66
|
+
if (metadata[field] !== undefined) {
|
|
67
|
+
(usage as any)[field] = metadata[field];
|
|
68
|
+
hasUsage = true;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const performanceFields = ['duration', 'latency', 'tps', 'ttft'] as const;
|
|
73
|
+
let hasPerformance = false;
|
|
74
|
+
performanceFields.forEach((field) => {
|
|
75
|
+
if (metadata[field] !== undefined) {
|
|
76
|
+
(performance as any)[field] = metadata[field];
|
|
77
|
+
hasPerformance = true;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
performance: hasPerformance ? performance : undefined,
|
|
83
|
+
usage: hasUsage ? usage : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Aggregate metadata from multiple children
|
|
89
|
+
* - Sums token counts and costs
|
|
90
|
+
* - Takes first ttft
|
|
91
|
+
* - Averages tps
|
|
92
|
+
* - Sums duration and latency
|
|
93
|
+
*/
|
|
94
|
+
aggregateMetadata(children: AssistantContentBlock[]): {
|
|
95
|
+
performance?: ModelPerformance;
|
|
96
|
+
usage?: ModelUsage;
|
|
97
|
+
} {
|
|
98
|
+
const usage: ModelUsage = {};
|
|
99
|
+
const performance: ModelPerformance = {};
|
|
100
|
+
let hasUsageData = false;
|
|
101
|
+
let hasPerformanceData = false;
|
|
102
|
+
let tpsSum = 0;
|
|
103
|
+
let tpsCount = 0;
|
|
104
|
+
|
|
105
|
+
children.forEach((child) => {
|
|
106
|
+
if (child.usage) {
|
|
107
|
+
const tokenFields = [
|
|
108
|
+
'acceptedPredictionTokens',
|
|
109
|
+
'inputAudioTokens',
|
|
110
|
+
'inputCacheMissTokens',
|
|
111
|
+
'inputCachedTokens',
|
|
112
|
+
'inputCitationTokens',
|
|
113
|
+
'inputImageTokens',
|
|
114
|
+
'inputTextTokens',
|
|
115
|
+
'inputWriteCacheTokens',
|
|
116
|
+
'outputAudioTokens',
|
|
117
|
+
'outputImageTokens',
|
|
118
|
+
'outputReasoningTokens',
|
|
119
|
+
'outputTextTokens',
|
|
120
|
+
'rejectedPredictionTokens',
|
|
121
|
+
'totalInputTokens',
|
|
122
|
+
'totalOutputTokens',
|
|
123
|
+
'totalTokens',
|
|
124
|
+
] as const;
|
|
125
|
+
|
|
126
|
+
tokenFields.forEach((field) => {
|
|
127
|
+
if (typeof child.usage![field] === 'number') {
|
|
128
|
+
(usage as any)[field] = ((usage as any)[field] || 0) + child.usage![field]!;
|
|
129
|
+
hasUsageData = true;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (typeof child.usage.cost === 'number') {
|
|
134
|
+
usage.cost = (usage.cost || 0) + child.usage.cost;
|
|
135
|
+
hasUsageData = true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (child.performance) {
|
|
140
|
+
// Take first ttft (time to first token)
|
|
141
|
+
if (child.performance.ttft !== undefined && performance.ttft === undefined) {
|
|
142
|
+
performance.ttft = child.performance.ttft;
|
|
143
|
+
hasPerformanceData = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Average tps (tokens per second)
|
|
147
|
+
if (typeof child.performance.tps === 'number') {
|
|
148
|
+
tpsSum += child.performance.tps;
|
|
149
|
+
tpsCount += 1;
|
|
150
|
+
hasPerformanceData = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Sum duration
|
|
154
|
+
if (child.performance.duration !== undefined) {
|
|
155
|
+
performance.duration = (performance.duration || 0) + child.performance.duration;
|
|
156
|
+
hasPerformanceData = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Sum latency
|
|
160
|
+
if (child.performance.latency !== undefined) {
|
|
161
|
+
performance.latency = (performance.latency || 0) + child.performance.latency;
|
|
162
|
+
hasPerformanceData = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Calculate average tps
|
|
168
|
+
if (tpsCount > 0) {
|
|
169
|
+
performance.tps = tpsSum / tpsCount;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
performance: hasPerformanceData ? performance : undefined,
|
|
174
|
+
usage: hasUsageData ? usage : undefined,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { IdNode, Message } from '../../types';
|
|
4
|
+
import { BranchResolver } from '../BranchResolver';
|
|
5
|
+
|
|
6
|
+
describe('BranchResolver', () => {
|
|
7
|
+
const resolver = new BranchResolver();
|
|
8
|
+
|
|
9
|
+
describe('getActiveBranchId', () => {
|
|
10
|
+
it('should return branch by activeBranchIndex from metadata when present', () => {
|
|
11
|
+
const message: Message = {
|
|
12
|
+
content: 'test',
|
|
13
|
+
createdAt: 0,
|
|
14
|
+
id: 'msg-1',
|
|
15
|
+
meta: {},
|
|
16
|
+
metadata: { activeBranchIndex: 1 },
|
|
17
|
+
role: 'user',
|
|
18
|
+
updatedAt: 0,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const idNode: IdNode = {
|
|
22
|
+
children: [
|
|
23
|
+
{ children: [], id: 'msg-2' },
|
|
24
|
+
{ children: [], id: 'msg-3' },
|
|
25
|
+
],
|
|
26
|
+
id: 'msg-1',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-3');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should infer active branch from which branch has children', () => {
|
|
33
|
+
const message: Message = {
|
|
34
|
+
content: 'test',
|
|
35
|
+
createdAt: 0,
|
|
36
|
+
id: 'msg-1',
|
|
37
|
+
meta: {},
|
|
38
|
+
role: 'user',
|
|
39
|
+
updatedAt: 0,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const idNode: IdNode = {
|
|
43
|
+
children: [
|
|
44
|
+
{ children: [], id: 'msg-2' },
|
|
45
|
+
{ children: [{ children: [], id: 'msg-4' }], id: 'msg-3' },
|
|
46
|
+
],
|
|
47
|
+
id: 'msg-1',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-3');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should default to first branch when no hints available', () => {
|
|
54
|
+
const message: Message = {
|
|
55
|
+
content: 'test',
|
|
56
|
+
createdAt: 0,
|
|
57
|
+
id: 'msg-1',
|
|
58
|
+
meta: {},
|
|
59
|
+
role: 'user',
|
|
60
|
+
updatedAt: 0,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const idNode: IdNode = {
|
|
64
|
+
children: [
|
|
65
|
+
{ children: [], id: 'msg-2' },
|
|
66
|
+
{ children: [], id: 'msg-3' },
|
|
67
|
+
],
|
|
68
|
+
id: 'msg-1',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-2');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should ignore invalid activeBranchIndex', () => {
|
|
75
|
+
const message: Message = {
|
|
76
|
+
content: 'test',
|
|
77
|
+
createdAt: 0,
|
|
78
|
+
id: 'msg-1',
|
|
79
|
+
meta: {},
|
|
80
|
+
metadata: { activeBranchIndex: 5 }, // out of bounds
|
|
81
|
+
role: 'user',
|
|
82
|
+
updatedAt: 0,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const idNode: IdNode = {
|
|
86
|
+
children: [
|
|
87
|
+
{ children: [], id: 'msg-2' },
|
|
88
|
+
{ children: [], id: 'msg-3' },
|
|
89
|
+
],
|
|
90
|
+
id: 'msg-1',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Should default to first branch
|
|
94
|
+
expect(resolver.getActiveBranchId(message, idNode)).toBe('msg-2');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('getActiveBranchIdFromMetadata', () => {
|
|
99
|
+
it('should return branch by activeBranchIndex from metadata', () => {
|
|
100
|
+
const message: Message = {
|
|
101
|
+
content: 'test',
|
|
102
|
+
createdAt: 0,
|
|
103
|
+
id: 'msg-1',
|
|
104
|
+
meta: {},
|
|
105
|
+
metadata: { activeBranchIndex: 1 },
|
|
106
|
+
role: 'user',
|
|
107
|
+
updatedAt: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const childIds = ['msg-2', 'msg-3'];
|
|
111
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
112
|
+
|
|
113
|
+
expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-3');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should infer active branch from which child has descendants', () => {
|
|
117
|
+
const message: Message = {
|
|
118
|
+
content: 'test',
|
|
119
|
+
createdAt: 0,
|
|
120
|
+
id: 'msg-1',
|
|
121
|
+
meta: {},
|
|
122
|
+
role: 'user',
|
|
123
|
+
updatedAt: 0,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const childIds = ['msg-2', 'msg-3'];
|
|
127
|
+
const childrenMap = new Map<string | null, string[]>([
|
|
128
|
+
['msg-2', []],
|
|
129
|
+
['msg-3', ['msg-4']],
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-3');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should default to first child when no hints available', () => {
|
|
136
|
+
const message: Message = {
|
|
137
|
+
content: 'test',
|
|
138
|
+
createdAt: 0,
|
|
139
|
+
id: 'msg-1',
|
|
140
|
+
meta: {},
|
|
141
|
+
role: 'user',
|
|
142
|
+
updatedAt: 0,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const childIds = ['msg-2', 'msg-3'];
|
|
146
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
147
|
+
|
|
148
|
+
expect(resolver.getActiveBranchIdFromMetadata(message, childIds, childrenMap)).toBe('msg-2');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { IdNode, Message, MessageGroupMetadata } from '../../types';
|
|
4
|
+
import { BranchResolver } from '../BranchResolver';
|
|
5
|
+
import { ContextTreeBuilder } from '../ContextTreeBuilder';
|
|
6
|
+
import { MessageCollector } from '../MessageCollector';
|
|
7
|
+
|
|
8
|
+
describe('ContextTreeBuilder', () => {
|
|
9
|
+
const createBuilder = (
|
|
10
|
+
messageMap: Map<string, Message>,
|
|
11
|
+
messageGroupMap: Map<string, MessageGroupMetadata> = new Map(),
|
|
12
|
+
) => {
|
|
13
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
14
|
+
const branchResolver = new BranchResolver();
|
|
15
|
+
const messageCollector = new MessageCollector(messageMap, childrenMap);
|
|
16
|
+
let nodeIdCounter = 0;
|
|
17
|
+
const generateNodeId = (prefix: string, messageId: string) =>
|
|
18
|
+
`${prefix}-${messageId}-${nodeIdCounter++}`;
|
|
19
|
+
|
|
20
|
+
return new ContextTreeBuilder(
|
|
21
|
+
messageMap,
|
|
22
|
+
messageGroupMap,
|
|
23
|
+
branchResolver,
|
|
24
|
+
messageCollector,
|
|
25
|
+
generateNodeId,
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('transformAll', () => {
|
|
30
|
+
it('should transform regular message nodes', () => {
|
|
31
|
+
const messageMap = new Map<string, Message>([
|
|
32
|
+
[
|
|
33
|
+
'msg-1',
|
|
34
|
+
{
|
|
35
|
+
content: 'Hello',
|
|
36
|
+
createdAt: 0,
|
|
37
|
+
id: 'msg-1',
|
|
38
|
+
meta: {},
|
|
39
|
+
role: 'user',
|
|
40
|
+
updatedAt: 0,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
'msg-2',
|
|
45
|
+
{
|
|
46
|
+
content: 'Hi',
|
|
47
|
+
createdAt: 0,
|
|
48
|
+
id: 'msg-2',
|
|
49
|
+
meta: {},
|
|
50
|
+
role: 'assistant',
|
|
51
|
+
updatedAt: 0,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const builder = createBuilder(messageMap);
|
|
57
|
+
const idNodes: IdNode[] = [
|
|
58
|
+
{
|
|
59
|
+
children: [{ children: [], id: 'msg-2' }],
|
|
60
|
+
id: 'msg-1',
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const result = builder.transformAll(idNodes);
|
|
65
|
+
|
|
66
|
+
expect(result).toHaveLength(2);
|
|
67
|
+
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
|
|
68
|
+
expect(result[1]).toEqual({ id: 'msg-2', type: 'message' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should create branch node for multiple children', () => {
|
|
72
|
+
const messageMap = new Map<string, Message>([
|
|
73
|
+
[
|
|
74
|
+
'msg-1',
|
|
75
|
+
{
|
|
76
|
+
content: 'Hello',
|
|
77
|
+
createdAt: 0,
|
|
78
|
+
id: 'msg-1',
|
|
79
|
+
meta: {},
|
|
80
|
+
metadata: { activeBranchIndex: 0 },
|
|
81
|
+
role: 'user',
|
|
82
|
+
updatedAt: 0,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
'msg-2',
|
|
87
|
+
{
|
|
88
|
+
content: 'Response 1',
|
|
89
|
+
createdAt: 0,
|
|
90
|
+
id: 'msg-2',
|
|
91
|
+
meta: {},
|
|
92
|
+
role: 'assistant',
|
|
93
|
+
updatedAt: 0,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
'msg-3',
|
|
98
|
+
{
|
|
99
|
+
content: 'Response 2',
|
|
100
|
+
createdAt: 0,
|
|
101
|
+
id: 'msg-3',
|
|
102
|
+
meta: {},
|
|
103
|
+
role: 'assistant',
|
|
104
|
+
updatedAt: 0,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const builder = createBuilder(messageMap);
|
|
110
|
+
const idNodes: IdNode[] = [
|
|
111
|
+
{
|
|
112
|
+
children: [
|
|
113
|
+
{ children: [], id: 'msg-2' },
|
|
114
|
+
{ children: [], id: 'msg-3' },
|
|
115
|
+
],
|
|
116
|
+
id: 'msg-1',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const result = builder.transformAll(idNodes);
|
|
121
|
+
|
|
122
|
+
expect(result).toHaveLength(2);
|
|
123
|
+
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
|
|
124
|
+
expect(result[1]).toMatchObject({
|
|
125
|
+
activeBranchIndex: 0,
|
|
126
|
+
branches: [[{ id: 'msg-2', type: 'message' }], [{ id: 'msg-3', type: 'message' }]],
|
|
127
|
+
parentMessageId: 'msg-1',
|
|
128
|
+
type: 'branch',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should create assistant group node for assistant with tools', () => {
|
|
133
|
+
const messageMap = new Map<string, Message>([
|
|
134
|
+
[
|
|
135
|
+
'msg-1',
|
|
136
|
+
{
|
|
137
|
+
content: 'Assistant with tools',
|
|
138
|
+
createdAt: 0,
|
|
139
|
+
id: 'msg-1',
|
|
140
|
+
meta: {},
|
|
141
|
+
role: 'assistant',
|
|
142
|
+
tools: [
|
|
143
|
+
{
|
|
144
|
+
apiName: 'test',
|
|
145
|
+
arguments: '{}',
|
|
146
|
+
id: 'tool-1',
|
|
147
|
+
identifier: 'test',
|
|
148
|
+
type: 'default',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
updatedAt: 0,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
'tool-1',
|
|
156
|
+
{
|
|
157
|
+
content: 'Tool result',
|
|
158
|
+
createdAt: 0,
|
|
159
|
+
id: 'tool-1',
|
|
160
|
+
meta: {},
|
|
161
|
+
role: 'tool',
|
|
162
|
+
updatedAt: 0,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const builder = createBuilder(messageMap);
|
|
168
|
+
const idNodes: IdNode[] = [
|
|
169
|
+
{
|
|
170
|
+
children: [{ children: [], id: 'tool-1' }],
|
|
171
|
+
id: 'msg-1',
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const result = builder.transformAll(idNodes);
|
|
176
|
+
|
|
177
|
+
expect(result).toHaveLength(1);
|
|
178
|
+
expect(result[0]).toMatchObject({
|
|
179
|
+
children: [{ id: 'msg-1', tools: ['tool-1'], type: 'message' }],
|
|
180
|
+
id: 'msg-1',
|
|
181
|
+
type: 'assistantGroup',
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should create compare node from message group', () => {
|
|
186
|
+
const messageMap = new Map<string, Message>([
|
|
187
|
+
[
|
|
188
|
+
'msg-1',
|
|
189
|
+
{
|
|
190
|
+
content: 'Compare 1',
|
|
191
|
+
createdAt: 0,
|
|
192
|
+
groupId: 'group-1',
|
|
193
|
+
id: 'msg-1',
|
|
194
|
+
meta: {},
|
|
195
|
+
metadata: { activeColumn: true },
|
|
196
|
+
role: 'assistant',
|
|
197
|
+
updatedAt: 0,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
[
|
|
201
|
+
'msg-2',
|
|
202
|
+
{
|
|
203
|
+
content: 'Compare 2',
|
|
204
|
+
createdAt: 0,
|
|
205
|
+
groupId: 'group-1',
|
|
206
|
+
id: 'msg-2',
|
|
207
|
+
meta: {},
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
updatedAt: 0,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const messageGroupMap = new Map<string, MessageGroupMetadata>([
|
|
215
|
+
['group-1', { id: 'group-1', mode: 'compare' }],
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const builder = createBuilder(messageMap, messageGroupMap);
|
|
219
|
+
const idNodes: IdNode[] = [{ children: [], id: 'msg-1' }];
|
|
220
|
+
|
|
221
|
+
const result = builder.transformAll(idNodes);
|
|
222
|
+
|
|
223
|
+
expect(result).toHaveLength(1);
|
|
224
|
+
expect(result[0]).toMatchObject({
|
|
225
|
+
activeColumnId: 'msg-1',
|
|
226
|
+
columns: [[{ id: 'msg-1', type: 'message' }], [{ id: 'msg-2', type: 'message' }]],
|
|
227
|
+
messageId: 'msg-1',
|
|
228
|
+
type: 'compare',
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should create compare node from user message metadata', () => {
|
|
233
|
+
const messageMap = new Map<string, Message>([
|
|
234
|
+
[
|
|
235
|
+
'msg-1',
|
|
236
|
+
{
|
|
237
|
+
content: 'User message',
|
|
238
|
+
createdAt: 0,
|
|
239
|
+
id: 'msg-1',
|
|
240
|
+
meta: {},
|
|
241
|
+
metadata: { compare: true },
|
|
242
|
+
role: 'user',
|
|
243
|
+
updatedAt: 0,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
[
|
|
247
|
+
'msg-2',
|
|
248
|
+
{
|
|
249
|
+
content: 'Assistant 1',
|
|
250
|
+
createdAt: 0,
|
|
251
|
+
id: 'msg-2',
|
|
252
|
+
meta: {},
|
|
253
|
+
metadata: { activeColumn: true },
|
|
254
|
+
role: 'assistant',
|
|
255
|
+
updatedAt: 0,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
'msg-3',
|
|
260
|
+
{
|
|
261
|
+
content: 'Assistant 2',
|
|
262
|
+
createdAt: 0,
|
|
263
|
+
id: 'msg-3',
|
|
264
|
+
meta: {},
|
|
265
|
+
role: 'assistant',
|
|
266
|
+
updatedAt: 0,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const builder = createBuilder(messageMap);
|
|
272
|
+
const idNodes: IdNode[] = [
|
|
273
|
+
{
|
|
274
|
+
children: [
|
|
275
|
+
{ children: [], id: 'msg-2' },
|
|
276
|
+
{ children: [], id: 'msg-3' },
|
|
277
|
+
],
|
|
278
|
+
id: 'msg-1',
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const result = builder.transformAll(idNodes);
|
|
283
|
+
|
|
284
|
+
expect(result).toHaveLength(2);
|
|
285
|
+
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
|
|
286
|
+
expect(result[1]).toMatchObject({
|
|
287
|
+
activeColumnId: 'msg-2',
|
|
288
|
+
columns: [[{ id: 'msg-2', type: 'message' }], [{ id: 'msg-3', type: 'message' }]],
|
|
289
|
+
id: 'compare-msg-1-msg-2-msg-3',
|
|
290
|
+
messageId: 'msg-1',
|
|
291
|
+
type: 'compare',
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should handle empty node list', () => {
|
|
296
|
+
const messageMap = new Map<string, Message>();
|
|
297
|
+
const builder = createBuilder(messageMap);
|
|
298
|
+
|
|
299
|
+
const result = builder.transformAll([]);
|
|
300
|
+
|
|
301
|
+
expect(result).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should handle missing message in map', () => {
|
|
305
|
+
const messageMap = new Map<string, Message>();
|
|
306
|
+
const builder = createBuilder(messageMap);
|
|
307
|
+
const idNodes: IdNode[] = [{ children: [], id: 'missing' }];
|
|
308
|
+
|
|
309
|
+
const result = builder.transformAll(idNodes);
|
|
310
|
+
|
|
311
|
+
expect(result).toHaveLength(0);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should continue with active column children in compare mode', () => {
|
|
315
|
+
const messageMap = new Map<string, Message>([
|
|
316
|
+
[
|
|
317
|
+
'msg-1',
|
|
318
|
+
{
|
|
319
|
+
content: 'User',
|
|
320
|
+
createdAt: 0,
|
|
321
|
+
id: 'msg-1',
|
|
322
|
+
meta: {},
|
|
323
|
+
metadata: { compare: true },
|
|
324
|
+
role: 'user',
|
|
325
|
+
updatedAt: 0,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
[
|
|
329
|
+
'msg-2',
|
|
330
|
+
{
|
|
331
|
+
content: 'Assistant 1',
|
|
332
|
+
createdAt: 0,
|
|
333
|
+
id: 'msg-2',
|
|
334
|
+
meta: {},
|
|
335
|
+
metadata: { activeColumn: true },
|
|
336
|
+
role: 'assistant',
|
|
337
|
+
updatedAt: 0,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
[
|
|
341
|
+
'msg-3',
|
|
342
|
+
{
|
|
343
|
+
content: 'Assistant 2',
|
|
344
|
+
createdAt: 0,
|
|
345
|
+
id: 'msg-3',
|
|
346
|
+
meta: {},
|
|
347
|
+
role: 'assistant',
|
|
348
|
+
updatedAt: 0,
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
[
|
|
352
|
+
'msg-4',
|
|
353
|
+
{
|
|
354
|
+
content: 'Follow-up',
|
|
355
|
+
createdAt: 0,
|
|
356
|
+
id: 'msg-4',
|
|
357
|
+
meta: {},
|
|
358
|
+
role: 'user',
|
|
359
|
+
updatedAt: 0,
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
]);
|
|
363
|
+
|
|
364
|
+
const builder = createBuilder(messageMap);
|
|
365
|
+
const idNodes: IdNode[] = [
|
|
366
|
+
{
|
|
367
|
+
children: [
|
|
368
|
+
{ children: [{ children: [], id: 'msg-4' }], id: 'msg-2' },
|
|
369
|
+
{ children: [], id: 'msg-3' },
|
|
370
|
+
],
|
|
371
|
+
id: 'msg-1',
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
const result = builder.transformAll(idNodes);
|
|
376
|
+
|
|
377
|
+
expect(result).toHaveLength(3);
|
|
378
|
+
expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
|
|
379
|
+
expect(result[1]).toMatchObject({
|
|
380
|
+
type: 'compare',
|
|
381
|
+
});
|
|
382
|
+
expect(result[2]).toEqual({ id: 'msg-4', type: 'message' });
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|