Skip to content

Commit 282c1fb

Browse files
authored
πŸ› fix: fixed the group topic copy not right (#11730)
fix: update the group topic copy way
1 parent e3046c7 commit 282c1fb

File tree

2 files changed

+206
-21
lines changed

2 files changed

+206
-21
lines changed

β€Žpackages/database/src/models/__tests__/topics/topic.create.test.tsβ€Ž

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { eq, inArray } from 'drizzle-orm';
22
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
33

44
import { getTestDB } from '../../../core/getTestDB';
5-
import { agents, messages, sessions, topics, users } from '../../../schemas';
5+
import { agents, messagePlugins, messages, sessions, topics, users } from '../../../schemas';
66
import { LobeChatDatabase } from '../../../type';
77
import { CreateTopicParams, TopicModel } from '../../topic';
88

@@ -279,6 +279,129 @@ describe('TopicModel - Create', () => {
279279
expect(duplicatedMessages[1].content).toBe('Assistant message');
280280
});
281281

282+
it('should correctly map parentId references when duplicating messages', async () => {
283+
const topicId = 'topic-with-parent-refs';
284+
285+
await serverDB.transaction(async (tx) => {
286+
await tx.insert(topics).values({ id: topicId, sessionId, userId, title: 'Original Topic' });
287+
await tx.insert(messages).values([
288+
{ id: 'msg1', role: 'user', topicId, userId, content: 'First message', parentId: null },
289+
{
290+
id: 'msg2',
291+
role: 'assistant',
292+
topicId,
293+
userId,
294+
content: 'Reply to first',
295+
parentId: 'msg1',
296+
},
297+
{
298+
id: 'msg3',
299+
role: 'tool',
300+
topicId,
301+
userId,
302+
content: 'Tool response',
303+
parentId: 'msg2',
304+
},
305+
{
306+
id: 'msg4',
307+
role: 'assistant',
308+
topicId,
309+
userId,
310+
content: 'Final message',
311+
parentId: 'msg3',
312+
},
313+
]);
314+
});
315+
316+
const { topic: duplicatedTopic, messages: duplicatedMessages } = await topicModel.duplicate(
317+
topicId,
318+
'Duplicated Topic',
319+
);
320+
321+
expect(duplicatedMessages).toHaveLength(4);
322+
323+
const msgMap = new Map(duplicatedMessages.map((m) => [m.content, m]));
324+
const newMsg1 = msgMap.get('First message')!;
325+
const newMsg2 = msgMap.get('Reply to first')!;
326+
const newMsg3 = msgMap.get('Tool response')!;
327+
const newMsg4 = msgMap.get('Final message')!;
328+
329+
expect(newMsg1.parentId).toBeNull();
330+
expect(newMsg2.parentId).toBe(newMsg1.id);
331+
expect(newMsg3.parentId).toBe(newMsg2.id);
332+
expect(newMsg4.parentId).toBe(newMsg3.id);
333+
334+
expect(newMsg1.id).not.toBe('msg1');
335+
expect(newMsg2.id).not.toBe('msg2');
336+
expect(newMsg3.id).not.toBe('msg3');
337+
expect(newMsg4.id).not.toBe('msg4');
338+
});
339+
340+
it('should correctly map tool_call_id when duplicating messages with tools', async () => {
341+
const topicId = 'topic-with-tools';
342+
const originalToolId = 'toolu_original_123';
343+
344+
await serverDB.transaction(async (tx) => {
345+
await tx.insert(topics).values({ id: topicId, sessionId, userId, title: 'Original Topic' });
346+
347+
// Insert assistant message with tools
348+
await tx.insert(messages).values({
349+
id: 'msg1',
350+
role: 'assistant',
351+
topicId,
352+
userId,
353+
content: 'Using tool',
354+
parentId: null,
355+
tools: [{ id: originalToolId, type: 'builtin', apiName: 'broadcast' }],
356+
});
357+
358+
// Insert tool message
359+
await tx.insert(messages).values({
360+
id: 'msg2',
361+
role: 'tool',
362+
topicId,
363+
userId,
364+
content: 'Tool response',
365+
parentId: 'msg1',
366+
});
367+
368+
// Insert messagePlugins entry
369+
await tx.insert(messagePlugins).values({
370+
id: 'msg2',
371+
userId,
372+
toolCallId: originalToolId,
373+
apiName: 'broadcast',
374+
});
375+
});
376+
377+
const { topic: duplicatedTopic, messages: duplicatedMessages } = await topicModel.duplicate(
378+
topicId,
379+
'Duplicated Topic',
380+
);
381+
382+
expect(duplicatedMessages).toHaveLength(2);
383+
384+
const msgMap = new Map(duplicatedMessages.map((m) => [m.role, m]));
385+
const newAssistant = msgMap.get('assistant')!;
386+
const newTool = msgMap.get('tool')!;
387+
388+
// Check that tools array has new IDs
389+
expect(newAssistant.tools).toBeDefined();
390+
const newTools = newAssistant.tools as any[];
391+
expect(newTools).toHaveLength(1);
392+
expect(newTools[0].id).not.toBe(originalToolId);
393+
expect(newTools[0].id).toMatch(/^toolu_/);
394+
395+
// Check that messagePlugins was copied with new toolCallId
396+
const newPlugin = await serverDB.query.messagePlugins.findFirst({
397+
where: eq(messagePlugins.id, newTool.id),
398+
});
399+
400+
expect(newPlugin).toBeDefined();
401+
expect(newPlugin!.toolCallId).toBe(newTools[0].id);
402+
expect(newPlugin!.toolCallId).not.toBe(originalToolId);
403+
});
404+
282405
it('should throw an error if the topic to duplicate does not exist', async () => {
283406
const topicId = 'nonexistent-topic';
284407

β€Žpackages/database/src/models/topic.tsβ€Ž

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ import {
1717
sql,
1818
} from 'drizzle-orm';
1919

20-
import { TopicItem, agents, agentsToSessions, messages, topics } from '../schemas';
20+
import {
21+
TopicItem,
22+
agents,
23+
agentsToSessions,
24+
messagePlugins,
25+
messages,
26+
topics,
27+
} from '../schemas';
2128
import { LobeChatDatabase } from '../type';
2229
import { genEndDateWhere, genRangeWhere, genStartDateWhere, genWhere } from '../utils/genWhere';
2330
import { idGenerator } from '../utils/idGenerator';
@@ -498,28 +505,83 @@ export class TopicModel {
498505
})
499506
.returning();
500507

501-
// Find messages associated with the original topic
508+
// Find messages associated with the original topic, ordered by createdAt
502509
const originalMessages = await tx
503510
.select()
504511
.from(messages)
505-
.where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)));
506-
507-
// copy messages
508-
const duplicatedMessages = await Promise.all(
509-
originalMessages.map(async (message) => {
510-
const result = (await tx
511-
.insert(messages)
512-
.values({
513-
...message,
514-
clientId: null,
515-
id: idGenerator('messages'),
516-
topicId: duplicatedTopic.id,
517-
})
518-
.returning()) as DBMessageItem[];
519-
520-
return result[0];
521-
}),
522-
);
512+
.where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)))
513+
.orderBy(messages.createdAt);
514+
515+
// Find all messagePlugins for this topic
516+
const messageIds = originalMessages.map((m) => m.id);
517+
const originalPlugins =
518+
messageIds.length > 0
519+
? await tx
520+
.select()
521+
.from(messagePlugins)
522+
.where(inArray(messagePlugins.id, messageIds))
523+
: [];
524+
525+
// Build oldId -> newId mapping for messages
526+
const idMap = new Map<string, string>();
527+
originalMessages.forEach((message) => {
528+
idMap.set(message.id, idGenerator('messages'));
529+
});
530+
531+
// Build oldToolId -> newToolId mapping for tools
532+
const toolIdMap = new Map<string, string>();
533+
originalMessages.forEach((message) => {
534+
if (message.tools && Array.isArray(message.tools)) {
535+
(message.tools as any[]).forEach((tool: any) => {
536+
if (tool.id) {
537+
toolIdMap.set(tool.id, `toolu_${idGenerator('messages')}`);
538+
}
539+
});
540+
}
541+
});
542+
543+
// copy messages sequentially to respect foreign key constraints
544+
const duplicatedMessages: DBMessageItem[] = [];
545+
for (const message of originalMessages) {
546+
const newId = idMap.get(message.id)!;
547+
const newParentId = message.parentId ? idMap.get(message.parentId) || null : null;
548+
549+
// Update tool IDs in tools array
550+
let newTools = message.tools;
551+
if (newTools && Array.isArray(newTools)) {
552+
newTools = (newTools as any[]).map((tool: any) => ({
553+
...tool,
554+
id: tool.id ? toolIdMap.get(tool.id) || tool.id : tool.id,
555+
}));
556+
}
557+
558+
const result = (await tx
559+
.insert(messages)
560+
.values({
561+
...message,
562+
clientId: null,
563+
id: newId,
564+
parentId: newParentId,
565+
topicId: duplicatedTopic.id,
566+
tools: newTools,
567+
})
568+
.returning()) as DBMessageItem[];
569+
570+
duplicatedMessages.push(result[0]);
571+
572+
// Copy messagePlugins if exists for this message
573+
const plugin = originalPlugins.find((p) => p.id === message.id);
574+
if (plugin) {
575+
const newToolCallId = plugin.toolCallId ? toolIdMap.get(plugin.toolCallId) || null : null;
576+
577+
await tx.insert(messagePlugins).values({
578+
...plugin,
579+
id: newId,
580+
clientId: null,
581+
toolCallId: newToolCallId,
582+
});
583+
}
584+
}
523585

524586
return {
525587
messages: duplicatedMessages,

0 commit comments

Comments
Β (0)