@lobehub/lobehub 2.0.0-next.40 → 2.0.0-next.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/chat.json +1 -0
- package/locales/bg-BG/chat.json +1 -0
- package/locales/de-DE/chat.json +1 -0
- package/locales/en-US/chat.json +1 -0
- package/locales/es-ES/chat.json +1 -0
- package/locales/fa-IR/chat.json +1 -0
- package/locales/fr-FR/chat.json +1 -0
- package/locales/it-IT/chat.json +1 -0
- package/locales/ja-JP/chat.json +1 -0
- package/locales/ko-KR/chat.json +1 -0
- package/locales/nl-NL/chat.json +1 -0
- package/locales/pl-PL/chat.json +1 -0
- package/locales/pt-BR/chat.json +1 -0
- package/locales/ru-RU/chat.json +1 -0
- package/locales/tr-TR/chat.json +1 -0
- package/locales/vi-VN/chat.json +1 -0
- package/locales/zh-CN/chat.json +1 -0
- package/locales/zh-TW/chat.json +1 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/messages/message.create.test.ts +549 -0
- package/packages/database/src/models/__tests__/messages/message.delete.test.ts +481 -0
- package/packages/database/src/models/__tests__/messages/message.query.test.ts +1187 -0
- package/packages/database/src/models/__tests__/messages/message.stats.test.ts +633 -0
- package/packages/database/src/models/__tests__/messages/message.update.test.ts +757 -0
- package/packages/database/src/models/message.ts +5 -55
- package/packages/utils/src/clientIP.ts +6 -6
- package/packages/utils/src/compressImage.ts +3 -3
- package/packages/utils/src/fetch/fetchSSE.ts +15 -15
- package/packages/utils/src/format.ts +2 -2
- package/packages/utils/src/merge.ts +3 -3
- package/packages/utils/src/parseModels.ts +3 -3
- package/packages/utils/src/sanitizeUTF8.ts +4 -4
- package/packages/utils/src/toolManifest.ts +4 -4
- package/packages/utils/src/trace.test.ts +359 -0
- package/packages/utils/src/uriParser.ts +4 -4
- package/src/features/ChatItem/components/Title.tsx +20 -16
- package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
- package/src/features/Conversation/Messages/Group/index.tsx +10 -3
- package/src/server/services/message/index.ts +14 -4
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
- package/packages/database/src/models/__tests__/message.test.ts +0 -2632
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { DBMessageItem } from '@lobechat/types';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { uuid } from '@/utils/uuid';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
chatGroups,
|
|
9
|
+
chunks,
|
|
10
|
+
embeddings,
|
|
11
|
+
files,
|
|
12
|
+
messagePlugins,
|
|
13
|
+
messageQueries,
|
|
14
|
+
messageQueryChunks,
|
|
15
|
+
messages,
|
|
16
|
+
messagesFiles,
|
|
17
|
+
sessions,
|
|
18
|
+
users,
|
|
19
|
+
} from '../../../schemas';
|
|
20
|
+
import { LobeChatDatabase } from '../../../type';
|
|
21
|
+
import { MessageModel } from '../../message';
|
|
22
|
+
import { getTestDB } from '../_util';
|
|
23
|
+
import { codeEmbedding } from '../fixtures/embedding';
|
|
24
|
+
|
|
25
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
26
|
+
|
|
27
|
+
const userId = 'message-create-test';
|
|
28
|
+
const otherUserId = 'message-create-test-other';
|
|
29
|
+
const messageModel = new MessageModel(serverDB, userId);
|
|
30
|
+
const embeddingsId = uuid();
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
// Clear tables before each test case
|
|
34
|
+
await serverDB.transaction(async (trx) => {
|
|
35
|
+
await trx.delete(users).where(eq(users.id, userId));
|
|
36
|
+
await trx.delete(users).where(eq(users.id, otherUserId));
|
|
37
|
+
await trx.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
38
|
+
|
|
39
|
+
await trx.insert(sessions).values([
|
|
40
|
+
// { id: 'session1', userId },
|
|
41
|
+
// { id: 'session2', userId },
|
|
42
|
+
{ id: '1', userId },
|
|
43
|
+
]);
|
|
44
|
+
await trx.insert(files).values({
|
|
45
|
+
id: 'f1',
|
|
46
|
+
userId: userId,
|
|
47
|
+
url: 'abc',
|
|
48
|
+
name: 'file-1',
|
|
49
|
+
fileType: 'image/png',
|
|
50
|
+
size: 1000,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await trx.insert(embeddings).values({
|
|
54
|
+
id: embeddingsId,
|
|
55
|
+
embeddings: codeEmbedding,
|
|
56
|
+
userId,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(async () => {
|
|
62
|
+
// Clear tables after each test case
|
|
63
|
+
await serverDB.delete(users).where(eq(users.id, userId));
|
|
64
|
+
await serverDB.delete(users).where(eq(users.id, otherUserId));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('MessageModel Create Tests', () => {
|
|
68
|
+
describe('createMessage', () => {
|
|
69
|
+
it('should create a new message', async () => {
|
|
70
|
+
// Call createMessage method
|
|
71
|
+
await messageModel.create({ role: 'user', content: 'new message', sessionId: '1' });
|
|
72
|
+
|
|
73
|
+
// Assert result
|
|
74
|
+
const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
|
|
75
|
+
expect(result).toHaveLength(1);
|
|
76
|
+
expect(result[0].content).toBe('new message');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should create a message', async () => {
|
|
80
|
+
const sessionId = 'session1';
|
|
81
|
+
await serverDB.insert(sessions).values([{ id: sessionId, userId }]);
|
|
82
|
+
|
|
83
|
+
const result = await messageModel.create({
|
|
84
|
+
content: 'message 1',
|
|
85
|
+
role: 'user',
|
|
86
|
+
sessionId: 'session1',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.id).toBeDefined();
|
|
90
|
+
expect(result.content).toBe('message 1');
|
|
91
|
+
expect(result.role).toBe('user');
|
|
92
|
+
expect(result.sessionId).toBe('session1');
|
|
93
|
+
expect(result.userId).toBe(userId);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should generate message ID automatically', async () => {
|
|
97
|
+
// Call createMessage method
|
|
98
|
+
await messageModel.create({
|
|
99
|
+
role: 'user',
|
|
100
|
+
content: 'new message',
|
|
101
|
+
sessionId: '1',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Assert result
|
|
105
|
+
const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
|
|
106
|
+
expect(result[0].id).toBeDefined();
|
|
107
|
+
expect(result[0].id).toHaveLength(18);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should create a tool message and insert into messagePlugins table', async () => {
|
|
111
|
+
// Call create method
|
|
112
|
+
const result = await messageModel.create({
|
|
113
|
+
content: 'message 1',
|
|
114
|
+
role: 'tool',
|
|
115
|
+
sessionId: '1',
|
|
116
|
+
tool_call_id: 'tool1',
|
|
117
|
+
plugin: {
|
|
118
|
+
apiName: 'api1',
|
|
119
|
+
arguments: 'arg1',
|
|
120
|
+
identifier: 'plugin1',
|
|
121
|
+
type: 'default',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Assert result
|
|
126
|
+
expect(result.id).toBeDefined();
|
|
127
|
+
expect(result.content).toBe('message 1');
|
|
128
|
+
expect(result.role).toBe('tool');
|
|
129
|
+
expect(result.sessionId).toBe('1');
|
|
130
|
+
|
|
131
|
+
const pluginResult = await serverDB
|
|
132
|
+
.select()
|
|
133
|
+
.from(messagePlugins)
|
|
134
|
+
.where(eq(messagePlugins.id, result.id));
|
|
135
|
+
expect(pluginResult).toHaveLength(1);
|
|
136
|
+
expect(pluginResult[0].identifier).toBe('plugin1');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should create tool message ', async () => {
|
|
140
|
+
// Call create method
|
|
141
|
+
const state = {
|
|
142
|
+
query: 'Composio',
|
|
143
|
+
answers: [],
|
|
144
|
+
results: [
|
|
145
|
+
{
|
|
146
|
+
url: 'https://www.composio.dev/',
|
|
147
|
+
score: 16,
|
|
148
|
+
title: 'Composio - Connect 90+ tools to your AI agents',
|
|
149
|
+
engine: 'bing',
|
|
150
|
+
content:
|
|
151
|
+
'Faster DevelopmentHigher ReliabilityBetter Integrations. Get Started Now. Our platform lets you ditch the specs and seamlessly integrate any tool you need in less than 5 mins.',
|
|
152
|
+
engines: ['bing', 'qwant', 'brave', 'duckduckgo'],
|
|
153
|
+
category: 'general',
|
|
154
|
+
template: 'default.html',
|
|
155
|
+
positions: [1, 1, 1, 1],
|
|
156
|
+
thumbnail: '',
|
|
157
|
+
parsed_url: ['https', 'www.composio.dev', '/', '', '', ''],
|
|
158
|
+
publishedDate: null,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
url: 'https://www.composio.co/',
|
|
162
|
+
score: 10.75,
|
|
163
|
+
title: 'Composio',
|
|
164
|
+
engine: 'bing',
|
|
165
|
+
content:
|
|
166
|
+
'Composio was created to help streamline the entire book creation process! Writing. Take time out to write / Make a schedule to write consistently. We have writing software that optimizes your books for printing or ebook format. Figure out what you want to write. Collaborate and write with others. Professional editing is a necessity.',
|
|
167
|
+
engines: ['qwant', 'duckduckgo', 'google', 'bing', 'brave'],
|
|
168
|
+
category: 'general',
|
|
169
|
+
template: 'default.html',
|
|
170
|
+
positions: [5, 2, 1, 5, 4],
|
|
171
|
+
thumbnail: null,
|
|
172
|
+
parsed_url: ['https', 'www.composio.co', '/', '', '', ''],
|
|
173
|
+
publishedDate: null,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
unresponsive_engines: [],
|
|
177
|
+
};
|
|
178
|
+
const result = await messageModel.create({
|
|
179
|
+
content: '[{}]',
|
|
180
|
+
plugin: {
|
|
181
|
+
apiName: 'searchWithSearXNG',
|
|
182
|
+
arguments: '{\n "query": "Composio"\n}',
|
|
183
|
+
identifier: 'lobe-web-browsing',
|
|
184
|
+
type: 'builtin',
|
|
185
|
+
},
|
|
186
|
+
pluginState: state,
|
|
187
|
+
role: 'tool',
|
|
188
|
+
tool_call_id: 'tool_call_ymxXC2J0',
|
|
189
|
+
sessionId: '1',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Assert result
|
|
193
|
+
expect(result.id).toBeDefined();
|
|
194
|
+
expect(result.content).toBe('[{}]');
|
|
195
|
+
expect(result.role).toBe('tool');
|
|
196
|
+
expect(result.sessionId).toBe('1');
|
|
197
|
+
|
|
198
|
+
const pluginResult = await serverDB
|
|
199
|
+
.select()
|
|
200
|
+
.from(messagePlugins)
|
|
201
|
+
.where(eq(messagePlugins.id, result.id));
|
|
202
|
+
expect(pluginResult).toHaveLength(1);
|
|
203
|
+
expect(pluginResult[0].identifier).toBe('lobe-web-browsing');
|
|
204
|
+
expect(pluginResult[0].state!).toMatchObject(state);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('create with advanced parameters', () => {
|
|
208
|
+
it('should create a message with custom ID', async () => {
|
|
209
|
+
const customId = 'custom-msg-id';
|
|
210
|
+
|
|
211
|
+
const result = await messageModel.create(
|
|
212
|
+
{
|
|
213
|
+
role: 'user',
|
|
214
|
+
content: 'message with custom ID',
|
|
215
|
+
sessionId: '1',
|
|
216
|
+
},
|
|
217
|
+
customId,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(result.id).toBe(customId);
|
|
221
|
+
|
|
222
|
+
// Verify database records
|
|
223
|
+
const dbResult = await serverDB.select().from(messages).where(eq(messages.id, customId));
|
|
224
|
+
expect(dbResult).toHaveLength(1);
|
|
225
|
+
expect(dbResult[0].id).toBe(customId);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should create a message with file chunks and RAG query ID', async () => {
|
|
229
|
+
// Create test data following proper order: message -> query -> message with chunks
|
|
230
|
+
const chunkId1 = uuid();
|
|
231
|
+
const chunkId2 = uuid();
|
|
232
|
+
const firstMessageId = uuid();
|
|
233
|
+
const secondMessageId = uuid();
|
|
234
|
+
|
|
235
|
+
// 1. Create chunks first
|
|
236
|
+
await serverDB.insert(chunks).values([
|
|
237
|
+
{ id: chunkId1, text: 'chunk text 1', userId },
|
|
238
|
+
{ id: chunkId2, text: 'chunk text 2', userId },
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
// 2. Create first message (required for messageQuery FK)
|
|
242
|
+
await serverDB.insert(messages).values({
|
|
243
|
+
id: firstMessageId,
|
|
244
|
+
userId,
|
|
245
|
+
role: 'user',
|
|
246
|
+
content: 'user query',
|
|
247
|
+
sessionId: '1',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// 3. Create message query linked to first message
|
|
251
|
+
const messageQuery = await messageModel.createMessageQuery({
|
|
252
|
+
messageId: firstMessageId,
|
|
253
|
+
rewriteQuery: 'test query',
|
|
254
|
+
userQuery: 'original query',
|
|
255
|
+
embeddingsId,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 4. Create second message with file chunks referencing the query
|
|
259
|
+
const result = await messageModel.create(
|
|
260
|
+
{
|
|
261
|
+
role: 'assistant',
|
|
262
|
+
content: 'message with file chunks',
|
|
263
|
+
fileChunks: [
|
|
264
|
+
{ id: chunkId1, similarity: 0.95 },
|
|
265
|
+
{ id: chunkId2, similarity: 0.85 },
|
|
266
|
+
],
|
|
267
|
+
ragQueryId: messageQuery.id,
|
|
268
|
+
sessionId: '1',
|
|
269
|
+
},
|
|
270
|
+
secondMessageId,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Verify message created successfully
|
|
274
|
+
expect(result.id).toBe(secondMessageId);
|
|
275
|
+
|
|
276
|
+
// Verify message query chunk associations created successfully
|
|
277
|
+
const queryChunks = await serverDB
|
|
278
|
+
.select()
|
|
279
|
+
.from(messageQueryChunks)
|
|
280
|
+
.where(eq(messageQueryChunks.messageId, result.id));
|
|
281
|
+
|
|
282
|
+
expect(queryChunks).toHaveLength(2);
|
|
283
|
+
expect(queryChunks[0].chunkId).toBe(chunkId1);
|
|
284
|
+
expect(queryChunks[0].queryId).toBe(messageQuery.id);
|
|
285
|
+
expect(queryChunks[0].similarity).toBe('0.95000');
|
|
286
|
+
expect(queryChunks[1].chunkId).toBe(chunkId2);
|
|
287
|
+
expect(queryChunks[1].similarity).toBe('0.85000');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should create a message with files', async () => {
|
|
291
|
+
// Create test data
|
|
292
|
+
await serverDB.insert(files).values([
|
|
293
|
+
{
|
|
294
|
+
id: 'file1',
|
|
295
|
+
name: 'file1.txt',
|
|
296
|
+
fileType: 'text/plain',
|
|
297
|
+
size: 100,
|
|
298
|
+
url: 'url1',
|
|
299
|
+
userId,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'file2',
|
|
303
|
+
name: 'file2.jpg',
|
|
304
|
+
fileType: 'image/jpeg',
|
|
305
|
+
size: 200,
|
|
306
|
+
url: 'url2',
|
|
307
|
+
userId,
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
|
|
311
|
+
// Call create method
|
|
312
|
+
const result = await messageModel.create({
|
|
313
|
+
role: 'user',
|
|
314
|
+
content: 'message with files',
|
|
315
|
+
files: ['file1', 'file2'],
|
|
316
|
+
sessionId: '1',
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Verify message created successfully
|
|
320
|
+
expect(result.id).toBeDefined();
|
|
321
|
+
|
|
322
|
+
// Verify message file associations created successfully
|
|
323
|
+
const messageFiles = await serverDB
|
|
324
|
+
.select()
|
|
325
|
+
.from(messagesFiles)
|
|
326
|
+
.where(eq(messagesFiles.messageId, result.id));
|
|
327
|
+
|
|
328
|
+
expect(messageFiles).toHaveLength(2);
|
|
329
|
+
expect(messageFiles[0].fileId).toBe('file1');
|
|
330
|
+
expect(messageFiles[1].fileId).toBe('file2');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should create a message with custom timestamps', async () => {
|
|
334
|
+
const customCreatedAt = '2022-05-15T10:30:00Z';
|
|
335
|
+
const customUpdatedAt = '2022-05-16T11:45:00Z';
|
|
336
|
+
|
|
337
|
+
const result = await messageModel.create({
|
|
338
|
+
role: 'user',
|
|
339
|
+
content: 'message with custom timestamps',
|
|
340
|
+
createdAt: customCreatedAt as any,
|
|
341
|
+
updatedAt: customUpdatedAt as any,
|
|
342
|
+
sessionId: '1',
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Verify database records
|
|
346
|
+
const dbResult = await serverDB.select().from(messages).where(eq(messages.id, result.id));
|
|
347
|
+
|
|
348
|
+
// Date comparison needs to consider timezone and formatting, so use toISOString for comparison
|
|
349
|
+
expect(new Date(dbResult[0].createdAt!).toISOString()).toBe(
|
|
350
|
+
new Date(customCreatedAt).toISOString(),
|
|
351
|
+
);
|
|
352
|
+
expect(new Date(dbResult[0].updatedAt!).toISOString()).toBe(
|
|
353
|
+
new Date(customUpdatedAt).toISOString(),
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('batchCreateMessages', () => {
|
|
360
|
+
it('should batch create messages', async () => {
|
|
361
|
+
// Prepare test data
|
|
362
|
+
const newMessages = [
|
|
363
|
+
{ id: '1', role: 'user', content: 'message 1' },
|
|
364
|
+
{ id: '2', role: 'assistant', content: 'message 2' },
|
|
365
|
+
] as DBMessageItem[];
|
|
366
|
+
|
|
367
|
+
// Call batchCreateMessages method
|
|
368
|
+
await messageModel.batchCreate(newMessages);
|
|
369
|
+
|
|
370
|
+
// Assert result
|
|
371
|
+
const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
|
|
372
|
+
expect(result).toHaveLength(2);
|
|
373
|
+
expect(result[0].content).toBe('message 1');
|
|
374
|
+
expect(result[1].content).toBe('message 2');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should handle messages with and without groupId', async () => {
|
|
378
|
+
await serverDB.insert(sessions).values({ id: 'session1', userId });
|
|
379
|
+
await serverDB.insert(chatGroups).values({ id: 'group1', userId, title: 'Group 1' });
|
|
380
|
+
|
|
381
|
+
// Message without groupId - should keep sessionId
|
|
382
|
+
const msgWithoutGroup = await messageModel.create({
|
|
383
|
+
role: 'user',
|
|
384
|
+
content: 'message without group',
|
|
385
|
+
sessionId: 'session1',
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Message with groupId - sessionId should be set to null
|
|
389
|
+
const msgWithGroup = await messageModel.create({
|
|
390
|
+
role: 'user',
|
|
391
|
+
content: 'message with group',
|
|
392
|
+
sessionId: 'session1',
|
|
393
|
+
groupId: 'group1',
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Verify from database
|
|
397
|
+
const dbMsgWithoutGroup = await serverDB.query.messages.findFirst({
|
|
398
|
+
where: eq(messages.id, msgWithoutGroup.id),
|
|
399
|
+
});
|
|
400
|
+
const dbMsgWithGroup = await serverDB.query.messages.findFirst({
|
|
401
|
+
where: eq(messages.id, msgWithGroup.id),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(dbMsgWithoutGroup?.sessionId).toBe('session1');
|
|
405
|
+
expect(dbMsgWithoutGroup?.groupId).toBeNull();
|
|
406
|
+
|
|
407
|
+
expect(dbMsgWithGroup?.sessionId).toBeNull();
|
|
408
|
+
expect(dbMsgWithGroup?.groupId).toBe('group1');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('createMessageQuery', () => {
|
|
413
|
+
it('should create a new message query', async () => {
|
|
414
|
+
// Create test data
|
|
415
|
+
await serverDB.insert(messages).values({
|
|
416
|
+
id: 'msg1',
|
|
417
|
+
userId,
|
|
418
|
+
role: 'user',
|
|
419
|
+
content: 'test message',
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// 调用 createMessageQuery 方法
|
|
423
|
+
const result = await messageModel.createMessageQuery({
|
|
424
|
+
messageId: 'msg1',
|
|
425
|
+
userQuery: 'original query',
|
|
426
|
+
rewriteQuery: 'rewritten query',
|
|
427
|
+
embeddingsId,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Assert result
|
|
431
|
+
expect(result).toBeDefined();
|
|
432
|
+
expect(result.id).toBeDefined();
|
|
433
|
+
expect(result.messageId).toBe('msg1');
|
|
434
|
+
expect(result.userQuery).toBe('original query');
|
|
435
|
+
expect(result.rewriteQuery).toBe('rewritten query');
|
|
436
|
+
expect(result.userId).toBe(userId);
|
|
437
|
+
|
|
438
|
+
// 验证数据库中的记录
|
|
439
|
+
const dbResult = await serverDB
|
|
440
|
+
.select()
|
|
441
|
+
.from(messageQueries)
|
|
442
|
+
.where(eq(messageQueries.id, result.id));
|
|
443
|
+
|
|
444
|
+
expect(dbResult).toHaveLength(1);
|
|
445
|
+
expect(dbResult[0].messageId).toBe('msg1');
|
|
446
|
+
expect(dbResult[0].userQuery).toBe('original query');
|
|
447
|
+
expect(dbResult[0].rewriteQuery).toBe('rewritten query');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should create a message query with embeddings ID', async () => {
|
|
451
|
+
// Create test data
|
|
452
|
+
await serverDB.insert(messages).values({
|
|
453
|
+
id: 'msg2',
|
|
454
|
+
userId,
|
|
455
|
+
role: 'user',
|
|
456
|
+
content: 'test message',
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// 调用 createMessageQuery 方法
|
|
460
|
+
const result = await messageModel.createMessageQuery({
|
|
461
|
+
messageId: 'msg2',
|
|
462
|
+
userQuery: 'test query',
|
|
463
|
+
rewriteQuery: 'test rewritten query',
|
|
464
|
+
embeddingsId,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Assert result
|
|
468
|
+
expect(result).toBeDefined();
|
|
469
|
+
expect(result.embeddingsId).toBe(embeddingsId);
|
|
470
|
+
|
|
471
|
+
// 验证数据库中的记录
|
|
472
|
+
const dbResult = await serverDB
|
|
473
|
+
.select()
|
|
474
|
+
.from(messageQueries)
|
|
475
|
+
.where(eq(messageQueries.id, result.id));
|
|
476
|
+
|
|
477
|
+
expect(dbResult[0].embeddingsId).toBe(embeddingsId);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should generate a unique ID for each message query', async () => {
|
|
481
|
+
// Create test data
|
|
482
|
+
await serverDB.insert(messages).values({
|
|
483
|
+
id: 'msg3',
|
|
484
|
+
userId,
|
|
485
|
+
role: 'user',
|
|
486
|
+
content: 'test message',
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// 连续创建两个消息查询
|
|
490
|
+
const result1 = await messageModel.createMessageQuery({
|
|
491
|
+
messageId: 'msg3',
|
|
492
|
+
userQuery: 'query 1',
|
|
493
|
+
rewriteQuery: 'rewritten query 1',
|
|
494
|
+
embeddingsId,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const result2 = await messageModel.createMessageQuery({
|
|
498
|
+
messageId: 'msg3',
|
|
499
|
+
userQuery: 'query 2',
|
|
500
|
+
rewriteQuery: 'rewritten query 2',
|
|
501
|
+
embeddingsId,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Assert result
|
|
505
|
+
expect(result1.id).not.toBe(result2.id);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('updateMessageRAG', () => {
|
|
510
|
+
it('should insert message query chunks for RAG', async () => {
|
|
511
|
+
// prepare message and query
|
|
512
|
+
const messageId = 'rag-msg-1';
|
|
513
|
+
const queryId = uuid();
|
|
514
|
+
const chunk1 = uuid();
|
|
515
|
+
const chunk2 = uuid();
|
|
516
|
+
|
|
517
|
+
await serverDB.transaction(async (trx) => {
|
|
518
|
+
await trx.insert(messages).values({ id: messageId, role: 'user', userId, content: 'c' });
|
|
519
|
+
await trx.insert(chunks).values([
|
|
520
|
+
{ id: chunk1, text: 'a' },
|
|
521
|
+
{ id: chunk2, text: 'b' },
|
|
522
|
+
]);
|
|
523
|
+
await trx
|
|
524
|
+
.insert(messageQueries)
|
|
525
|
+
.values({ id: queryId, messageId, userId, userQuery: 'q', rewriteQuery: 'rq' });
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
await messageModel.updateMessageRAG(messageId, {
|
|
529
|
+
ragQueryId: queryId,
|
|
530
|
+
fileChunks: [
|
|
531
|
+
{ id: chunk1, similarity: 0.9 },
|
|
532
|
+
{ id: chunk2, similarity: 0.8 },
|
|
533
|
+
],
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const rows = await serverDB
|
|
537
|
+
.select()
|
|
538
|
+
.from(messageQueryChunks)
|
|
539
|
+
.where(eq(messageQueryChunks.messageId, messageId));
|
|
540
|
+
|
|
541
|
+
expect(rows).toHaveLength(2);
|
|
542
|
+
const s1 = rows.find((r) => r.chunkId === chunk1)!;
|
|
543
|
+
const s2 = rows.find((r) => r.chunkId === chunk2)!;
|
|
544
|
+
expect(s1.queryId).toBe(queryId);
|
|
545
|
+
expect(s1.similarity).toBe('0.90000');
|
|
546
|
+
expect(s2.similarity).toBe('0.80000');
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|