@lobehub/lobehub 2.0.0-next.40 → 2.0.0-next.41

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.
@@ -0,0 +1,492 @@
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
+ chunks,
9
+ embeddings,
10
+ files,
11
+ messagePlugins,
12
+ messageQueries,
13
+ messageQueryChunks,
14
+ messages,
15
+ messagesFiles,
16
+ sessions,
17
+ users,
18
+ } from '../../../schemas';
19
+ import { LobeChatDatabase } from '../../../type';
20
+ import { MessageModel } from '../../message';
21
+ import { getTestDB } from '../_util';
22
+ import { codeEmbedding } from '../fixtures/embedding';
23
+
24
+ const serverDB: LobeChatDatabase = await getTestDB();
25
+
26
+ const userId = 'message-create-test';
27
+ const otherUserId = 'message-create-test-other';
28
+ const messageModel = new MessageModel(serverDB, userId);
29
+ const embeddingsId = uuid();
30
+
31
+ beforeEach(async () => {
32
+ // Clear tables before each test case
33
+ await serverDB.transaction(async (trx) => {
34
+ await trx.delete(users).where(eq(users.id, userId));
35
+ await trx.delete(users).where(eq(users.id, otherUserId));
36
+ await trx.insert(users).values([{ id: userId }, { id: otherUserId }]);
37
+
38
+ await trx.insert(sessions).values([
39
+ // { id: 'session1', userId },
40
+ // { id: 'session2', userId },
41
+ { id: '1', userId },
42
+ ]);
43
+ await trx.insert(files).values({
44
+ id: 'f1',
45
+ userId: userId,
46
+ url: 'abc',
47
+ name: 'file-1',
48
+ fileType: 'image/png',
49
+ size: 1000,
50
+ });
51
+
52
+ await trx.insert(embeddings).values({
53
+ id: embeddingsId,
54
+ embeddings: codeEmbedding,
55
+ userId,
56
+ });
57
+ });
58
+ });
59
+
60
+ afterEach(async () => {
61
+ // Clear tables after each test case
62
+ await serverDB.delete(users).where(eq(users.id, userId));
63
+ await serverDB.delete(users).where(eq(users.id, otherUserId));
64
+ });
65
+
66
+ describe('MessageModel Create Tests', () => {
67
+ describe('createMessage', () => {
68
+ it('should create a new message', async () => {
69
+ // Call createMessage method
70
+ await messageModel.create({ role: 'user', content: 'new message', sessionId: '1' });
71
+
72
+ // Assert result
73
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
74
+ expect(result).toHaveLength(1);
75
+ expect(result[0].content).toBe('new message');
76
+ });
77
+
78
+ it('should create a message', async () => {
79
+ const sessionId = 'session1';
80
+ await serverDB.insert(sessions).values([{ id: sessionId, userId }]);
81
+
82
+ const result = await messageModel.create({
83
+ content: 'message 1',
84
+ role: 'user',
85
+ sessionId: 'session1',
86
+ });
87
+
88
+ expect(result.id).toBeDefined();
89
+ expect(result.content).toBe('message 1');
90
+ expect(result.role).toBe('user');
91
+ expect(result.sessionId).toBe('session1');
92
+ expect(result.userId).toBe(userId);
93
+ });
94
+
95
+ it('should generate message ID automatically', async () => {
96
+ // Call createMessage method
97
+ await messageModel.create({
98
+ role: 'user',
99
+ content: 'new message',
100
+ sessionId: '1',
101
+ });
102
+
103
+ // Assert result
104
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
105
+ expect(result[0].id).toBeDefined();
106
+ expect(result[0].id).toHaveLength(18);
107
+ });
108
+
109
+ it('should create a tool message and insert into messagePlugins table', async () => {
110
+ // Call create method
111
+ const result = await messageModel.create({
112
+ content: 'message 1',
113
+ role: 'tool',
114
+ sessionId: '1',
115
+ tool_call_id: 'tool1',
116
+ plugin: {
117
+ apiName: 'api1',
118
+ arguments: 'arg1',
119
+ identifier: 'plugin1',
120
+ type: 'default',
121
+ },
122
+ });
123
+
124
+ // Assert result
125
+ expect(result.id).toBeDefined();
126
+ expect(result.content).toBe('message 1');
127
+ expect(result.role).toBe('tool');
128
+ expect(result.sessionId).toBe('1');
129
+
130
+ const pluginResult = await serverDB
131
+ .select()
132
+ .from(messagePlugins)
133
+ .where(eq(messagePlugins.id, result.id));
134
+ expect(pluginResult).toHaveLength(1);
135
+ expect(pluginResult[0].identifier).toBe('plugin1');
136
+ });
137
+
138
+ it('should create tool message ', async () => {
139
+ // Call create method
140
+ const state = {
141
+ query: 'Composio',
142
+ answers: [],
143
+ results: [
144
+ {
145
+ url: 'https://www.composio.dev/',
146
+ score: 16,
147
+ title: 'Composio - Connect 90+ tools to your AI agents',
148
+ engine: 'bing',
149
+ content:
150
+ '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.',
151
+ engines: ['bing', 'qwant', 'brave', 'duckduckgo'],
152
+ category: 'general',
153
+ template: 'default.html',
154
+ positions: [1, 1, 1, 1],
155
+ thumbnail: '',
156
+ parsed_url: ['https', 'www.composio.dev', '/', '', '', ''],
157
+ publishedDate: null,
158
+ },
159
+ {
160
+ url: 'https://www.composio.co/',
161
+ score: 10.75,
162
+ title: 'Composio',
163
+ engine: 'bing',
164
+ content:
165
+ '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.',
166
+ engines: ['qwant', 'duckduckgo', 'google', 'bing', 'brave'],
167
+ category: 'general',
168
+ template: 'default.html',
169
+ positions: [5, 2, 1, 5, 4],
170
+ thumbnail: null,
171
+ parsed_url: ['https', 'www.composio.co', '/', '', '', ''],
172
+ publishedDate: null,
173
+ },
174
+ ],
175
+ unresponsive_engines: [],
176
+ };
177
+ const result = await messageModel.create({
178
+ content: '[{}]',
179
+ plugin: {
180
+ apiName: 'searchWithSearXNG',
181
+ arguments: '{\n "query": "Composio"\n}',
182
+ identifier: 'lobe-web-browsing',
183
+ type: 'builtin',
184
+ },
185
+ pluginState: state,
186
+ role: 'tool',
187
+ tool_call_id: 'tool_call_ymxXC2J0',
188
+ sessionId: '1',
189
+ });
190
+
191
+ // Assert result
192
+ expect(result.id).toBeDefined();
193
+ expect(result.content).toBe('[{}]');
194
+ expect(result.role).toBe('tool');
195
+ expect(result.sessionId).toBe('1');
196
+
197
+ const pluginResult = await serverDB
198
+ .select()
199
+ .from(messagePlugins)
200
+ .where(eq(messagePlugins.id, result.id));
201
+ expect(pluginResult).toHaveLength(1);
202
+ expect(pluginResult[0].identifier).toBe('lobe-web-browsing');
203
+ expect(pluginResult[0].state!).toMatchObject(state);
204
+ });
205
+
206
+ describe('create with advanced parameters', () => {
207
+ it('should create a message with custom ID', async () => {
208
+ const customId = 'custom-msg-id';
209
+
210
+ const result = await messageModel.create(
211
+ {
212
+ role: 'user',
213
+ content: 'message with custom ID',
214
+ sessionId: '1',
215
+ },
216
+ customId,
217
+ );
218
+
219
+ expect(result.id).toBe(customId);
220
+
221
+ // Verify database records
222
+ const dbResult = await serverDB.select().from(messages).where(eq(messages.id, customId));
223
+ expect(dbResult).toHaveLength(1);
224
+ expect(dbResult[0].id).toBe(customId);
225
+ });
226
+
227
+ it.skip('should create a message with file chunks and RAG query ID', async () => {
228
+ // Create test data
229
+ const chunkId1 = uuid();
230
+ const chunkId2 = uuid();
231
+ const ragQueryId = uuid();
232
+
233
+ await serverDB.insert(chunks).values([
234
+ { id: chunkId1, text: 'chunk text 1' },
235
+ { id: chunkId2, text: 'chunk text 2' },
236
+ ]);
237
+
238
+ // Call create method
239
+ const result = await messageModel.create({
240
+ role: 'assistant',
241
+ content: 'message with file chunks',
242
+ fileChunks: [
243
+ { id: chunkId1, similarity: 0.95 },
244
+ { id: chunkId2, similarity: 0.85 },
245
+ ],
246
+ ragQueryId,
247
+ sessionId: '1',
248
+ });
249
+
250
+ // Verify message created successfully
251
+ expect(result.id).toBeDefined();
252
+
253
+ // Verify message query chunk associations created successfully
254
+ const queryChunks = await serverDB
255
+ .select()
256
+ .from(messageQueryChunks)
257
+ .where(eq(messageQueryChunks.messageId, result.id));
258
+
259
+ expect(queryChunks).toHaveLength(2);
260
+ expect(queryChunks[0].chunkId).toBe(chunkId1);
261
+ expect(queryChunks[0].queryId).toBe(ragQueryId);
262
+ expect(queryChunks[0].similarity).toBe('0.95');
263
+ expect(queryChunks[1].chunkId).toBe(chunkId2);
264
+ expect(queryChunks[1].similarity).toBe('0.85');
265
+ });
266
+
267
+ it('should create a message with files', async () => {
268
+ // Create test data
269
+ await serverDB.insert(files).values([
270
+ {
271
+ id: 'file1',
272
+ name: 'file1.txt',
273
+ fileType: 'text/plain',
274
+ size: 100,
275
+ url: 'url1',
276
+ userId,
277
+ },
278
+ {
279
+ id: 'file2',
280
+ name: 'file2.jpg',
281
+ fileType: 'image/jpeg',
282
+ size: 200,
283
+ url: 'url2',
284
+ userId,
285
+ },
286
+ ]);
287
+
288
+ // Call create method
289
+ const result = await messageModel.create({
290
+ role: 'user',
291
+ content: 'message with files',
292
+ files: ['file1', 'file2'],
293
+ sessionId: '1',
294
+ });
295
+
296
+ // Verify message created successfully
297
+ expect(result.id).toBeDefined();
298
+
299
+ // Verify message file associations created successfully
300
+ const messageFiles = await serverDB
301
+ .select()
302
+ .from(messagesFiles)
303
+ .where(eq(messagesFiles.messageId, result.id));
304
+
305
+ expect(messageFiles).toHaveLength(2);
306
+ expect(messageFiles[0].fileId).toBe('file1');
307
+ expect(messageFiles[1].fileId).toBe('file2');
308
+ });
309
+
310
+ it('should create a message with custom timestamps', async () => {
311
+ const customCreatedAt = '2022-05-15T10:30:00Z';
312
+ const customUpdatedAt = '2022-05-16T11:45:00Z';
313
+
314
+ const result = await messageModel.create({
315
+ role: 'user',
316
+ content: 'message with custom timestamps',
317
+ createdAt: customCreatedAt as any,
318
+ updatedAt: customUpdatedAt as any,
319
+ sessionId: '1',
320
+ });
321
+
322
+ // Verify database records
323
+ const dbResult = await serverDB.select().from(messages).where(eq(messages.id, result.id));
324
+
325
+ // Date comparison needs to consider timezone and formatting, so use toISOString for comparison
326
+ expect(new Date(dbResult[0].createdAt!).toISOString()).toBe(
327
+ new Date(customCreatedAt).toISOString(),
328
+ );
329
+ expect(new Date(dbResult[0].updatedAt!).toISOString()).toBe(
330
+ new Date(customUpdatedAt).toISOString(),
331
+ );
332
+ });
333
+ });
334
+ });
335
+
336
+ describe('batchCreateMessages', () => {
337
+ it('should batch create messages', async () => {
338
+ // Prepare test data
339
+ const newMessages = [
340
+ { id: '1', role: 'user', content: 'message 1' },
341
+ { id: '2', role: 'assistant', content: 'message 2' },
342
+ ] as DBMessageItem[];
343
+
344
+ // Call batchCreateMessages method
345
+ await messageModel.batchCreate(newMessages);
346
+
347
+ // Assert result
348
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
349
+ expect(result).toHaveLength(2);
350
+ expect(result[0].content).toBe('message 1');
351
+ expect(result[1].content).toBe('message 2');
352
+ });
353
+ });
354
+
355
+ describe('createMessageQuery', () => {
356
+ it('should create a new message query', async () => {
357
+ // Create test data
358
+ await serverDB.insert(messages).values({
359
+ id: 'msg1',
360
+ userId,
361
+ role: 'user',
362
+ content: 'test message',
363
+ });
364
+
365
+ // 调用 createMessageQuery 方法
366
+ const result = await messageModel.createMessageQuery({
367
+ messageId: 'msg1',
368
+ userQuery: 'original query',
369
+ rewriteQuery: 'rewritten query',
370
+ embeddingsId,
371
+ });
372
+
373
+ // Assert result
374
+ expect(result).toBeDefined();
375
+ expect(result.id).toBeDefined();
376
+ expect(result.messageId).toBe('msg1');
377
+ expect(result.userQuery).toBe('original query');
378
+ expect(result.rewriteQuery).toBe('rewritten query');
379
+ expect(result.userId).toBe(userId);
380
+
381
+ // 验证数据库中的记录
382
+ const dbResult = await serverDB
383
+ .select()
384
+ .from(messageQueries)
385
+ .where(eq(messageQueries.id, result.id));
386
+
387
+ expect(dbResult).toHaveLength(1);
388
+ expect(dbResult[0].messageId).toBe('msg1');
389
+ expect(dbResult[0].userQuery).toBe('original query');
390
+ expect(dbResult[0].rewriteQuery).toBe('rewritten query');
391
+ });
392
+
393
+ it('should create a message query with embeddings ID', async () => {
394
+ // Create test data
395
+ await serverDB.insert(messages).values({
396
+ id: 'msg2',
397
+ userId,
398
+ role: 'user',
399
+ content: 'test message',
400
+ });
401
+
402
+ // 调用 createMessageQuery 方法
403
+ const result = await messageModel.createMessageQuery({
404
+ messageId: 'msg2',
405
+ userQuery: 'test query',
406
+ rewriteQuery: 'test rewritten query',
407
+ embeddingsId,
408
+ });
409
+
410
+ // Assert result
411
+ expect(result).toBeDefined();
412
+ expect(result.embeddingsId).toBe(embeddingsId);
413
+
414
+ // 验证数据库中的记录
415
+ const dbResult = await serverDB
416
+ .select()
417
+ .from(messageQueries)
418
+ .where(eq(messageQueries.id, result.id));
419
+
420
+ expect(dbResult[0].embeddingsId).toBe(embeddingsId);
421
+ });
422
+
423
+ it('should generate a unique ID for each message query', async () => {
424
+ // Create test data
425
+ await serverDB.insert(messages).values({
426
+ id: 'msg3',
427
+ userId,
428
+ role: 'user',
429
+ content: 'test message',
430
+ });
431
+
432
+ // 连续创建两个消息查询
433
+ const result1 = await messageModel.createMessageQuery({
434
+ messageId: 'msg3',
435
+ userQuery: 'query 1',
436
+ rewriteQuery: 'rewritten query 1',
437
+ embeddingsId,
438
+ });
439
+
440
+ const result2 = await messageModel.createMessageQuery({
441
+ messageId: 'msg3',
442
+ userQuery: 'query 2',
443
+ rewriteQuery: 'rewritten query 2',
444
+ embeddingsId,
445
+ });
446
+
447
+ // Assert result
448
+ expect(result1.id).not.toBe(result2.id);
449
+ });
450
+ });
451
+
452
+ describe('updateMessageRAG', () => {
453
+ it('should insert message query chunks for RAG', async () => {
454
+ // prepare message and query
455
+ const messageId = 'rag-msg-1';
456
+ const queryId = uuid();
457
+ const chunk1 = uuid();
458
+ const chunk2 = uuid();
459
+
460
+ await serverDB.transaction(async (trx) => {
461
+ await trx.insert(messages).values({ id: messageId, role: 'user', userId, content: 'c' });
462
+ await trx.insert(chunks).values([
463
+ { id: chunk1, text: 'a' },
464
+ { id: chunk2, text: 'b' },
465
+ ]);
466
+ await trx
467
+ .insert(messageQueries)
468
+ .values({ id: queryId, messageId, userId, userQuery: 'q', rewriteQuery: 'rq' });
469
+ });
470
+
471
+ await messageModel.updateMessageRAG(messageId, {
472
+ ragQueryId: queryId,
473
+ fileChunks: [
474
+ { id: chunk1, similarity: 0.9 },
475
+ { id: chunk2, similarity: 0.8 },
476
+ ],
477
+ });
478
+
479
+ const rows = await serverDB
480
+ .select()
481
+ .from(messageQueryChunks)
482
+ .where(eq(messageQueryChunks.messageId, messageId));
483
+
484
+ expect(rows).toHaveLength(2);
485
+ const s1 = rows.find((r) => r.chunkId === chunk1)!;
486
+ const s2 = rows.find((r) => r.chunkId === chunk2)!;
487
+ expect(s1.queryId).toBe(queryId);
488
+ expect(s1.similarity).toBe('0.90000');
489
+ expect(s2.similarity).toBe('0.80000');
490
+ });
491
+ });
492
+ });