@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,220 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { IdNode, Message } from '../../types';
|
|
4
|
+
import { MessageCollector } from '../MessageCollector';
|
|
5
|
+
|
|
6
|
+
describe('MessageCollector', () => {
|
|
7
|
+
describe('collectGroupMembers', () => {
|
|
8
|
+
it('should collect messages with matching groupId', () => {
|
|
9
|
+
const messageMap = new Map<string, Message>();
|
|
10
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
11
|
+
const collector = new MessageCollector(messageMap, childrenMap);
|
|
12
|
+
|
|
13
|
+
const messages: Message[] = [
|
|
14
|
+
{
|
|
15
|
+
content: '1',
|
|
16
|
+
createdAt: 0,
|
|
17
|
+
groupId: 'group-1',
|
|
18
|
+
id: 'msg-1',
|
|
19
|
+
meta: {},
|
|
20
|
+
role: 'assistant',
|
|
21
|
+
updatedAt: 0,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
content: '2',
|
|
25
|
+
createdAt: 0,
|
|
26
|
+
groupId: 'group-1',
|
|
27
|
+
id: 'msg-2',
|
|
28
|
+
meta: {},
|
|
29
|
+
role: 'assistant',
|
|
30
|
+
updatedAt: 0,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
content: '3',
|
|
34
|
+
createdAt: 0,
|
|
35
|
+
groupId: 'group-2',
|
|
36
|
+
id: 'msg-3',
|
|
37
|
+
meta: {},
|
|
38
|
+
role: 'assistant',
|
|
39
|
+
updatedAt: 0,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const result = collector.collectGroupMembers('group-1', messages);
|
|
44
|
+
|
|
45
|
+
expect(result).toHaveLength(2);
|
|
46
|
+
expect(result.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('collectToolMessages', () => {
|
|
51
|
+
it('should collect tool messages matching assistant tool call IDs', () => {
|
|
52
|
+
const messageMap = new Map<string, Message>();
|
|
53
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
54
|
+
const collector = new MessageCollector(messageMap, childrenMap);
|
|
55
|
+
|
|
56
|
+
const assistant: Message = {
|
|
57
|
+
content: 'test',
|
|
58
|
+
createdAt: 0,
|
|
59
|
+
id: 'msg-1',
|
|
60
|
+
meta: {},
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
tools: [
|
|
63
|
+
{ apiName: 'tool1', arguments: '{}', id: 'tool-1', identifier: 'test', type: 'default' },
|
|
64
|
+
{ apiName: 'tool2', arguments: '{}', id: 'tool-2', identifier: 'test', type: 'default' },
|
|
65
|
+
],
|
|
66
|
+
updatedAt: 0,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const messages: Message[] = [
|
|
70
|
+
{
|
|
71
|
+
content: 'result1',
|
|
72
|
+
createdAt: 0,
|
|
73
|
+
id: 'msg-2',
|
|
74
|
+
meta: {},
|
|
75
|
+
parentId: 'msg-1',
|
|
76
|
+
role: 'tool',
|
|
77
|
+
tool_call_id: 'tool-1',
|
|
78
|
+
updatedAt: 0,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
content: 'result2',
|
|
82
|
+
createdAt: 0,
|
|
83
|
+
id: 'msg-3',
|
|
84
|
+
meta: {},
|
|
85
|
+
parentId: 'msg-1',
|
|
86
|
+
role: 'tool',
|
|
87
|
+
tool_call_id: 'tool-2',
|
|
88
|
+
updatedAt: 0,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
content: 'other',
|
|
92
|
+
createdAt: 0,
|
|
93
|
+
id: 'msg-4',
|
|
94
|
+
meta: {},
|
|
95
|
+
parentId: 'msg-1',
|
|
96
|
+
role: 'tool',
|
|
97
|
+
tool_call_id: 'tool-3',
|
|
98
|
+
updatedAt: 0,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const result = collector.collectToolMessages(assistant, messages);
|
|
103
|
+
|
|
104
|
+
expect(result).toHaveLength(2);
|
|
105
|
+
expect(result.map((m) => m.id)).toEqual(['msg-2', 'msg-3']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('findLastNodeInAssistantGroup', () => {
|
|
110
|
+
it('should return the node itself if no tool children', () => {
|
|
111
|
+
const messageMap = new Map<string, Message>();
|
|
112
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
113
|
+
const collector = new MessageCollector(messageMap, childrenMap);
|
|
114
|
+
|
|
115
|
+
const idNode: IdNode = {
|
|
116
|
+
children: [],
|
|
117
|
+
id: 'msg-1',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = collector.findLastNodeInAssistantGroup(idNode);
|
|
121
|
+
|
|
122
|
+
expect(result).toEqual(idNode);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return last tool node if no assistant children', () => {
|
|
126
|
+
const messageMap = new Map<string, Message>([
|
|
127
|
+
[
|
|
128
|
+
'msg-1',
|
|
129
|
+
{
|
|
130
|
+
content: 'test',
|
|
131
|
+
createdAt: 0,
|
|
132
|
+
id: 'msg-1',
|
|
133
|
+
meta: {},
|
|
134
|
+
role: 'assistant',
|
|
135
|
+
updatedAt: 0,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
[
|
|
139
|
+
'tool-1',
|
|
140
|
+
{
|
|
141
|
+
content: 'result',
|
|
142
|
+
createdAt: 0,
|
|
143
|
+
id: 'tool-1',
|
|
144
|
+
meta: {},
|
|
145
|
+
parentId: 'msg-1',
|
|
146
|
+
role: 'tool',
|
|
147
|
+
updatedAt: 0,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
]);
|
|
151
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
152
|
+
const collector = new MessageCollector(messageMap, childrenMap);
|
|
153
|
+
|
|
154
|
+
const idNode: IdNode = {
|
|
155
|
+
children: [{ children: [], id: 'tool-1' }],
|
|
156
|
+
id: 'msg-1',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const result = collector.findLastNodeInAssistantGroup(idNode);
|
|
160
|
+
|
|
161
|
+
expect(result?.id).toBe('tool-1');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should follow assistant chain recursively', () => {
|
|
165
|
+
const messageMap = new Map<string, Message>([
|
|
166
|
+
[
|
|
167
|
+
'msg-1',
|
|
168
|
+
{
|
|
169
|
+
content: 'test1',
|
|
170
|
+
createdAt: 0,
|
|
171
|
+
id: 'msg-1',
|
|
172
|
+
meta: {},
|
|
173
|
+
role: 'assistant',
|
|
174
|
+
updatedAt: 0,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
[
|
|
178
|
+
'tool-1',
|
|
179
|
+
{
|
|
180
|
+
content: 'result1',
|
|
181
|
+
createdAt: 0,
|
|
182
|
+
id: 'tool-1',
|
|
183
|
+
meta: {},
|
|
184
|
+
parentId: 'msg-1',
|
|
185
|
+
role: 'tool',
|
|
186
|
+
updatedAt: 0,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
'msg-2',
|
|
191
|
+
{
|
|
192
|
+
content: 'test2',
|
|
193
|
+
createdAt: 0,
|
|
194
|
+
id: 'msg-2',
|
|
195
|
+
meta: {},
|
|
196
|
+
parentId: 'tool-1',
|
|
197
|
+
role: 'assistant',
|
|
198
|
+
updatedAt: 0,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
]);
|
|
202
|
+
const childrenMap = new Map<string | null, string[]>();
|
|
203
|
+
const collector = new MessageCollector(messageMap, childrenMap);
|
|
204
|
+
|
|
205
|
+
const idNode: IdNode = {
|
|
206
|
+
children: [
|
|
207
|
+
{
|
|
208
|
+
children: [{ children: [], id: 'msg-2' }],
|
|
209
|
+
id: 'tool-1',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
id: 'msg-1',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const result = collector.findLastNodeInAssistantGroup(idNode);
|
|
216
|
+
|
|
217
|
+
expect(result?.id).toBe('msg-2');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { AssistantContentBlock } from '@/types/index';
|
|
4
|
+
|
|
5
|
+
import type { Message } from '../../types';
|
|
6
|
+
import { MessageTransformer } from '../MessageTransformer';
|
|
7
|
+
|
|
8
|
+
describe('MessageTransformer', () => {
|
|
9
|
+
const transformer = new MessageTransformer();
|
|
10
|
+
|
|
11
|
+
describe('messageToContentBlock', () => {
|
|
12
|
+
it('should convert message to content block', () => {
|
|
13
|
+
const message: Message = {
|
|
14
|
+
content: 'Hello',
|
|
15
|
+
createdAt: 0,
|
|
16
|
+
id: 'msg-1',
|
|
17
|
+
meta: {},
|
|
18
|
+
metadata: {
|
|
19
|
+
cost: 0.001,
|
|
20
|
+
duration: 1000,
|
|
21
|
+
totalInputTokens: 10,
|
|
22
|
+
totalOutputTokens: 20,
|
|
23
|
+
totalTokens: 30,
|
|
24
|
+
tps: 20,
|
|
25
|
+
},
|
|
26
|
+
role: 'assistant',
|
|
27
|
+
updatedAt: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const result = transformer.messageToContentBlock(message);
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({
|
|
33
|
+
content: 'Hello',
|
|
34
|
+
error: undefined,
|
|
35
|
+
id: 'msg-1',
|
|
36
|
+
imageList: undefined,
|
|
37
|
+
performance: {
|
|
38
|
+
duration: 1000,
|
|
39
|
+
tps: 20,
|
|
40
|
+
},
|
|
41
|
+
reasoning: undefined,
|
|
42
|
+
tools: undefined,
|
|
43
|
+
usage: {
|
|
44
|
+
cost: 0.001,
|
|
45
|
+
totalInputTokens: 10,
|
|
46
|
+
totalOutputTokens: 20,
|
|
47
|
+
totalTokens: 30,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle message without metadata', () => {
|
|
53
|
+
const message: Message = {
|
|
54
|
+
content: 'Hello',
|
|
55
|
+
createdAt: 0,
|
|
56
|
+
id: 'msg-1',
|
|
57
|
+
meta: {},
|
|
58
|
+
role: 'assistant',
|
|
59
|
+
updatedAt: 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const result = transformer.messageToContentBlock(message);
|
|
63
|
+
|
|
64
|
+
expect(result.usage).toBeUndefined();
|
|
65
|
+
expect(result.performance).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('splitMetadata', () => {
|
|
70
|
+
it('should split metadata into usage and performance', () => {
|
|
71
|
+
const metadata = {
|
|
72
|
+
cost: 0.001,
|
|
73
|
+
duration: 1000,
|
|
74
|
+
latency: 1100,
|
|
75
|
+
totalInputTokens: 10,
|
|
76
|
+
totalOutputTokens: 20,
|
|
77
|
+
totalTokens: 30,
|
|
78
|
+
tps: 20,
|
|
79
|
+
ttft: 100,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = transformer.splitMetadata(metadata);
|
|
83
|
+
|
|
84
|
+
expect(result.usage).toEqual({
|
|
85
|
+
cost: 0.001,
|
|
86
|
+
totalInputTokens: 10,
|
|
87
|
+
totalOutputTokens: 20,
|
|
88
|
+
totalTokens: 30,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.performance).toEqual({
|
|
92
|
+
duration: 1000,
|
|
93
|
+
latency: 1100,
|
|
94
|
+
tps: 20,
|
|
95
|
+
ttft: 100,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle undefined metadata', () => {
|
|
100
|
+
const result = transformer.splitMetadata(undefined);
|
|
101
|
+
|
|
102
|
+
expect(result).toEqual({});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle empty metadata', () => {
|
|
106
|
+
const result = transformer.splitMetadata({});
|
|
107
|
+
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
performance: undefined,
|
|
110
|
+
usage: undefined,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle partial metadata', () => {
|
|
115
|
+
const result = transformer.splitMetadata({
|
|
116
|
+
cost: 0.001,
|
|
117
|
+
duration: 1000,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.usage).toEqual({
|
|
121
|
+
cost: 0.001,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.performance).toEqual({
|
|
125
|
+
duration: 1000,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('aggregateMetadata', () => {
|
|
131
|
+
it('should aggregate usage and performance from multiple children', () => {
|
|
132
|
+
const children: AssistantContentBlock[] = [
|
|
133
|
+
{
|
|
134
|
+
content: 'First',
|
|
135
|
+
id: 'msg-1',
|
|
136
|
+
performance: {
|
|
137
|
+
duration: 1000,
|
|
138
|
+
latency: 1100,
|
|
139
|
+
tps: 20,
|
|
140
|
+
ttft: 100,
|
|
141
|
+
},
|
|
142
|
+
usage: {
|
|
143
|
+
cost: 0.001,
|
|
144
|
+
totalInputTokens: 10,
|
|
145
|
+
totalOutputTokens: 20,
|
|
146
|
+
totalTokens: 30,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
content: 'Second',
|
|
151
|
+
id: 'msg-2',
|
|
152
|
+
performance: {
|
|
153
|
+
duration: 2000,
|
|
154
|
+
latency: 2100,
|
|
155
|
+
tps: 30,
|
|
156
|
+
},
|
|
157
|
+
usage: {
|
|
158
|
+
cost: 0.002,
|
|
159
|
+
totalInputTokens: 15,
|
|
160
|
+
totalOutputTokens: 25,
|
|
161
|
+
totalTokens: 40,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const result = transformer.aggregateMetadata(children);
|
|
167
|
+
|
|
168
|
+
expect(result.usage).toEqual({
|
|
169
|
+
cost: 0.003,
|
|
170
|
+
totalInputTokens: 25,
|
|
171
|
+
totalOutputTokens: 45,
|
|
172
|
+
totalTokens: 70,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.performance).toEqual({
|
|
176
|
+
duration: 3000,
|
|
177
|
+
latency: 3200,
|
|
178
|
+
tps: 25, // average of 20 and 30
|
|
179
|
+
ttft: 100, // first value
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should handle empty children array', () => {
|
|
184
|
+
const result = transformer.aggregateMetadata([]);
|
|
185
|
+
|
|
186
|
+
expect(result).toEqual({
|
|
187
|
+
performance: undefined,
|
|
188
|
+
usage: undefined,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle children without metadata', () => {
|
|
193
|
+
const children: AssistantContentBlock[] = [
|
|
194
|
+
{
|
|
195
|
+
content: 'First',
|
|
196
|
+
id: 'msg-1',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
content: 'Second',
|
|
200
|
+
id: 'msg-2',
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const result = transformer.aggregateMetadata(children);
|
|
205
|
+
|
|
206
|
+
expect(result).toEqual({
|
|
207
|
+
performance: undefined,
|
|
208
|
+
usage: undefined,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle mixed children (some with metadata, some without)', () => {
|
|
213
|
+
const children: AssistantContentBlock[] = [
|
|
214
|
+
{
|
|
215
|
+
content: 'First',
|
|
216
|
+
id: 'msg-1',
|
|
217
|
+
usage: {
|
|
218
|
+
cost: 0.001,
|
|
219
|
+
totalTokens: 30,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
content: 'Second',
|
|
224
|
+
id: 'msg-2',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
content: 'Third',
|
|
228
|
+
id: 'msg-3',
|
|
229
|
+
usage: {
|
|
230
|
+
cost: 0.002,
|
|
231
|
+
totalTokens: 40,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const result = transformer.aggregateMetadata(children);
|
|
237
|
+
|
|
238
|
+
expect(result.usage).toEqual({
|
|
239
|
+
cost: 0.003,
|
|
240
|
+
totalTokens: 70,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should average tps correctly', () => {
|
|
245
|
+
const children: AssistantContentBlock[] = [
|
|
246
|
+
{
|
|
247
|
+
content: 'First',
|
|
248
|
+
id: 'msg-1',
|
|
249
|
+
performance: { tps: 10 },
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
content: 'Second',
|
|
253
|
+
id: 'msg-2',
|
|
254
|
+
performance: { tps: 20 },
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
content: 'Third',
|
|
258
|
+
id: 'msg-3',
|
|
259
|
+
performance: { tps: 30 },
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const result = transformer.aggregateMetadata(children);
|
|
264
|
+
|
|
265
|
+
expect(result.performance?.tps).toBe(20); // average of 10, 20, 30
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should take first ttft value only', () => {
|
|
269
|
+
const children: AssistantContentBlock[] = [
|
|
270
|
+
{
|
|
271
|
+
content: 'First',
|
|
272
|
+
id: 'msg-1',
|
|
273
|
+
performance: { ttft: 100 },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
content: 'Second',
|
|
277
|
+
id: 'msg-2',
|
|
278
|
+
performance: { ttft: 200 },
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const result = transformer.aggregateMetadata(children);
|
|
283
|
+
|
|
284
|
+
expect(result.performance?.ttft).toBe(100); // first value only
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ContextNode, HelperMaps, IdNode, Message } from '../types';
|
|
2
|
+
import { BranchResolver } from './BranchResolver';
|
|
3
|
+
import { ContextTreeBuilder } from './ContextTreeBuilder';
|
|
4
|
+
import { FlatListBuilder } from './FlatListBuilder';
|
|
5
|
+
import { MessageCollector } from './MessageCollector';
|
|
6
|
+
import { MessageTransformer } from './MessageTransformer';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Phase 3: Transformation
|
|
10
|
+
* Converts structural idTree into semantic contextTree and flatList
|
|
11
|
+
*
|
|
12
|
+
* This is the main coordinator that delegates to specialized builders:
|
|
13
|
+
* - ContextTreeBuilder: Builds the tree structure for UI rendering
|
|
14
|
+
* - FlatListBuilder: Builds the flat list for API consumption
|
|
15
|
+
*/
|
|
16
|
+
export class Transformer {
|
|
17
|
+
private messageMap: Map<string, Message>;
|
|
18
|
+
private nodeIdCounter = 0;
|
|
19
|
+
|
|
20
|
+
// Utility classes
|
|
21
|
+
private branchResolver: BranchResolver;
|
|
22
|
+
private messageCollector: MessageCollector;
|
|
23
|
+
private messageTransformer: MessageTransformer;
|
|
24
|
+
|
|
25
|
+
// Builder classes
|
|
26
|
+
private contextTreeBuilder: ContextTreeBuilder;
|
|
27
|
+
private flatListBuilder: FlatListBuilder;
|
|
28
|
+
|
|
29
|
+
constructor(private helperMaps: HelperMaps) {
|
|
30
|
+
this.messageMap = helperMaps.messageMap;
|
|
31
|
+
|
|
32
|
+
// Initialize utility classes
|
|
33
|
+
this.branchResolver = new BranchResolver();
|
|
34
|
+
this.messageCollector = new MessageCollector(this.messageMap, helperMaps.childrenMap);
|
|
35
|
+
this.messageTransformer = new MessageTransformer();
|
|
36
|
+
|
|
37
|
+
// Initialize builder classes
|
|
38
|
+
this.contextTreeBuilder = new ContextTreeBuilder(
|
|
39
|
+
helperMaps.messageMap,
|
|
40
|
+
helperMaps.messageGroupMap,
|
|
41
|
+
this.branchResolver,
|
|
42
|
+
this.messageCollector,
|
|
43
|
+
this.generateNodeId.bind(this),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
this.flatListBuilder = new FlatListBuilder(
|
|
47
|
+
helperMaps.messageMap,
|
|
48
|
+
helperMaps.messageGroupMap,
|
|
49
|
+
helperMaps.childrenMap,
|
|
50
|
+
this.branchResolver,
|
|
51
|
+
this.messageCollector,
|
|
52
|
+
this.messageTransformer,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Generates a unique node ID
|
|
58
|
+
*/
|
|
59
|
+
private generateNodeId(prefix: string, messageId: string): string {
|
|
60
|
+
return `${prefix}-${messageId}-${this.nodeIdCounter++}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Transform all root nodes to contextTree
|
|
65
|
+
* Returns a linear array of context nodes for UI rendering
|
|
66
|
+
*/
|
|
67
|
+
transformAll(idNodes: IdNode[]): ContextNode[] {
|
|
68
|
+
return this.contextTreeBuilder.transformAll(idNodes);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate flatList from messages array
|
|
73
|
+
* Only includes messages in the active path for API consumption
|
|
74
|
+
*/
|
|
75
|
+
flatten(messages: Message[]): Message[] {
|
|
76
|
+
return this.flatListBuilder.flatten(messages);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Tree Types
|
|
3
|
+
*
|
|
4
|
+
* Tree structure for understanding conversation flow and navigation.
|
|
5
|
+
* Used for complex operations like branch switching and context understanding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base interface for all display nodes
|
|
10
|
+
*/
|
|
11
|
+
interface BaseNode {
|
|
12
|
+
/** Unique identifier for this node */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Type discriminator */
|
|
15
|
+
type: 'message' | 'assistantGroup' | 'compare' | 'branch';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Basic message node - leaf node representing a single message
|
|
20
|
+
*/
|
|
21
|
+
export interface MessageNode extends BaseNode {
|
|
22
|
+
/** Tool message IDs (for assistant messages with tool calls) */
|
|
23
|
+
tools?: string[];
|
|
24
|
+
type: 'message';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Assistant group node - aggregates an assistant message with its tool calls
|
|
29
|
+
*/
|
|
30
|
+
export interface AssistantGroupNode extends BaseNode {
|
|
31
|
+
/** Child nodes (assistant and tool messages) */
|
|
32
|
+
children: ContextNode[];
|
|
33
|
+
type: 'assistantGroup';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compare node - renders multiple parallel outputs side by side
|
|
38
|
+
*/
|
|
39
|
+
export interface CompareNode extends BaseNode {
|
|
40
|
+
/** ID of the active column that enters LLM context */
|
|
41
|
+
activeColumnId?: string;
|
|
42
|
+
/** Each column represents a parallel output tree */
|
|
43
|
+
columns: ContextNode[][];
|
|
44
|
+
/** The message that triggered the comparison */
|
|
45
|
+
messageId: string;
|
|
46
|
+
type: 'compare';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Branch node - represents multiple alternate conversation paths
|
|
51
|
+
*/
|
|
52
|
+
export interface BranchNode extends BaseNode {
|
|
53
|
+
/** Index of the currently active branch */
|
|
54
|
+
activeBranchIndex: number;
|
|
55
|
+
/** Each branch is a separate conversation tree */
|
|
56
|
+
branches: ContextNode[][];
|
|
57
|
+
/** The parent message that has multiple branches */
|
|
58
|
+
parentMessageId: string;
|
|
59
|
+
type: 'branch';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Union type of all display nodes
|
|
64
|
+
*/
|
|
65
|
+
export type ContextNode = MessageNode | AssistantGroupNode | CompareNode | BranchNode;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { UIChatMessage } from '@lobechat/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flat Message List Types
|
|
5
|
+
*
|
|
6
|
+
* Flattened array optimized for virtual list rendering.
|
|
7
|
+
* Contains virtual messages with extended role types for grouped display.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extended message role types for flat list rendering
|
|
12
|
+
*
|
|
13
|
+
* Standard roles from UIChatMessage:
|
|
14
|
+
* - 'user': User message
|
|
15
|
+
* - 'assistant': Assistant message (standalone, without tools)
|
|
16
|
+
* - 'tool': Tool execution result
|
|
17
|
+
* - 'system': System message
|
|
18
|
+
* - 'supervisor': Supervisor message in multi-agent
|
|
19
|
+
*
|
|
20
|
+
* Virtual roles created by parse():
|
|
21
|
+
* - 'assistantGroup': Assistant message + tool calls aggregation
|
|
22
|
+
* - 'messageGroup': Generic message group (manual/summary)
|
|
23
|
+
* - 'compare': Compare mode for parallel model outputs
|
|
24
|
+
*/
|
|
25
|
+
export type FlatMessageRole =
|
|
26
|
+
| 'user'
|
|
27
|
+
| 'assistant'
|
|
28
|
+
| 'tool'
|
|
29
|
+
| 'system'
|
|
30
|
+
| 'supervisor'
|
|
31
|
+
| 'assistantGroup'
|
|
32
|
+
| 'messageGroup'
|
|
33
|
+
| 'compare';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Message in flat list
|
|
37
|
+
*
|
|
38
|
+
* Can be either:
|
|
39
|
+
* 1. Original message from database
|
|
40
|
+
* 2. Virtual message created by parse() with extended role and children
|
|
41
|
+
*/
|
|
42
|
+
export type FlatMessage = UIChatMessage;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Branch metadata attached to user messages
|
|
46
|
+
*/
|
|
47
|
+
export interface BranchMetadata {
|
|
48
|
+
/** Active branch message ID */
|
|
49
|
+
activeId: string;
|
|
50
|
+
/** All branch message IDs */
|
|
51
|
+
branchIds: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Virtual message extra fields for flat list
|
|
56
|
+
*/
|
|
57
|
+
export interface FlatMessageExtra {
|
|
58
|
+
/** Branch information for user messages with multiple children */
|
|
59
|
+
branches?: BranchMetadata;
|
|
60
|
+
/** Optional description for groups */
|
|
61
|
+
description?: string;
|
|
62
|
+
/** Group mode for messageGroup and compare virtual messages */
|
|
63
|
+
groupMode?: 'compare' | 'manual' | 'summary';
|
|
64
|
+
/** Parent message ID that triggered this group */
|
|
65
|
+
parentMessageId?: string;
|
|
66
|
+
}
|