chore: tweak migration script to handle message order (#969)
This commit is contained in:
parent
1a948afe12
commit
1fd2302914
2 changed files with 74 additions and 19 deletions
|
|
@ -3,13 +3,14 @@ import postgres from 'postgres';
|
||||||
import {
|
import {
|
||||||
chat,
|
chat,
|
||||||
message,
|
message,
|
||||||
|
type MessageDeprecated,
|
||||||
messageDeprecated,
|
messageDeprecated,
|
||||||
vote,
|
vote,
|
||||||
voteDeprecated,
|
voteDeprecated,
|
||||||
} from '../schema';
|
} from '../schema';
|
||||||
import { drizzle } from 'drizzle-orm/postgres-js';
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
||||||
import { inArray } from 'drizzle-orm';
|
import { inArray } from 'drizzle-orm';
|
||||||
import { appendResponseMessages, UIMessage } from 'ai';
|
import { appendResponseMessages, type UIMessage } from 'ai';
|
||||||
|
|
||||||
config({
|
config({
|
||||||
path: '.env.local',
|
path: '.env.local',
|
||||||
|
|
@ -22,8 +23,8 @@ if (!process.env.POSTGRES_URL) {
|
||||||
const client = postgres(process.env.POSTGRES_URL);
|
const client = postgres(process.env.POSTGRES_URL);
|
||||||
const db = drizzle(client);
|
const db = drizzle(client);
|
||||||
|
|
||||||
const BATCH_SIZE = 50; // Process 10 chats at a time
|
const BATCH_SIZE = 100; // Process 100 chats at a time
|
||||||
const INSERT_BATCH_SIZE = 100; // Insert 100 messages at a time
|
const INSERT_BATCH_SIZE = 1000; // Insert 1000 messages at a time
|
||||||
|
|
||||||
type NewMessageInsert = {
|
type NewMessageInsert = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -40,16 +41,66 @@ type NewVoteInsert = {
|
||||||
isUpvoted: boolean;
|
isUpvoted: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function createNewTable() {
|
interface MessageDeprecatedContentPart {
|
||||||
|
type: string;
|
||||||
|
content: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageRank(message: MessageDeprecated): number {
|
||||||
|
if (
|
||||||
|
message.role === 'assistant' &&
|
||||||
|
(message.content as MessageDeprecatedContentPart[]).some(
|
||||||
|
(contentPart) => contentPart.type === 'tool-call',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
message.role === 'tool' &&
|
||||||
|
(message.content as MessageDeprecatedContentPart[]).some(
|
||||||
|
(contentPart) => contentPart.type === 'tool-result',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === 'assistant') {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeParts<T extends { type: string; [k: string]: any }>(
|
||||||
|
parts: T[],
|
||||||
|
): T[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return parts.filter((p) => {
|
||||||
|
const key = `${p.type}|${JSON.stringify(p.content ?? p)}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeParts<T extends { type: string; [k: string]: any }>(
|
||||||
|
parts: T[],
|
||||||
|
): T[] {
|
||||||
|
return parts.filter(
|
||||||
|
(part) => !(part.type === 'reasoning' && part.reasoning === 'undefined'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateMessages() {
|
||||||
const chats = await db.select().from(chat);
|
const chats = await db.select().from(chat);
|
||||||
|
|
||||||
let processedCount = 0;
|
let processedCount = 0;
|
||||||
|
|
||||||
// Process chats in batches
|
|
||||||
for (let i = 0; i < chats.length; i += BATCH_SIZE) {
|
for (let i = 0; i < chats.length; i += BATCH_SIZE) {
|
||||||
const chatBatch = chats.slice(i, i + BATCH_SIZE);
|
const chatBatch = chats.slice(i, i + BATCH_SIZE);
|
||||||
const chatIds = chatBatch.map((chat) => chat.id);
|
const chatIds = chatBatch.map((chat) => chat.id);
|
||||||
|
|
||||||
// Fetch all messages and votes for the current batch of chats in bulk
|
|
||||||
const allMessages = await db
|
const allMessages = await db
|
||||||
.select()
|
.select()
|
||||||
.from(messageDeprecated)
|
.from(messageDeprecated)
|
||||||
|
|
@ -60,20 +111,25 @@ async function createNewTable() {
|
||||||
.from(voteDeprecated)
|
.from(voteDeprecated)
|
||||||
.where(inArray(voteDeprecated.chatId, chatIds));
|
.where(inArray(voteDeprecated.chatId, chatIds));
|
||||||
|
|
||||||
// Prepare batches for insertion
|
|
||||||
const newMessagesToInsert: NewMessageInsert[] = [];
|
const newMessagesToInsert: NewMessageInsert[] = [];
|
||||||
const newVotesToInsert: NewVoteInsert[] = [];
|
const newVotesToInsert: NewVoteInsert[] = [];
|
||||||
|
|
||||||
// Process each chat in the batch
|
|
||||||
for (const chat of chatBatch) {
|
for (const chat of chatBatch) {
|
||||||
processedCount++;
|
processedCount++;
|
||||||
console.info(`Processed ${processedCount}/${chats.length} chats`);
|
console.info(`Processed ${processedCount}/${chats.length} chats`);
|
||||||
|
|
||||||
// Filter messages and votes for this specific chat
|
const messages = allMessages
|
||||||
const messages = allMessages.filter((msg) => msg.chatId === chat.id);
|
.filter((message) => message.chatId === chat.id)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const differenceInTime =
|
||||||
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
|
if (differenceInTime !== 0) return differenceInTime;
|
||||||
|
|
||||||
|
return getMessageRank(a) - getMessageRank(b);
|
||||||
|
});
|
||||||
|
|
||||||
const votes = allVotes.filter((v) => v.chatId === chat.id);
|
const votes = allVotes.filter((v) => v.chatId === chat.id);
|
||||||
|
|
||||||
// Group messages into sections
|
|
||||||
const messageSection: Array<UIMessage> = [];
|
const messageSection: Array<UIMessage> = [];
|
||||||
const messageSections: Array<Array<UIMessage>> = [];
|
const messageSections: Array<Array<UIMessage>> = [];
|
||||||
|
|
||||||
|
|
@ -93,7 +149,6 @@ async function createNewTable() {
|
||||||
messageSections.push([...messageSection]);
|
messageSections.push([...messageSection]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each message section
|
|
||||||
for (const section of messageSections) {
|
for (const section of messageSections) {
|
||||||
const [userMessage, ...assistantMessages] = section;
|
const [userMessage, ...assistantMessages] = section;
|
||||||
|
|
||||||
|
|
@ -121,10 +176,14 @@ async function createNewTable() {
|
||||||
attachments: [],
|
attachments: [],
|
||||||
} as NewMessageInsert;
|
} as NewMessageInsert;
|
||||||
} else if (message.role === 'assistant') {
|
} else if (message.role === 'assistant') {
|
||||||
|
const cleanParts = sanitizeParts(
|
||||||
|
dedupeParts(message.parts || []),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
parts: message.parts || [],
|
parts: cleanParts,
|
||||||
role: message.role,
|
role: message.role,
|
||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
|
@ -134,7 +193,6 @@ async function createNewTable() {
|
||||||
})
|
})
|
||||||
.filter((msg): msg is NewMessageInsert => msg !== null);
|
.filter((msg): msg is NewMessageInsert => msg !== null);
|
||||||
|
|
||||||
// Add messages to batch
|
|
||||||
for (const msg of projectedUISection) {
|
for (const msg of projectedUISection) {
|
||||||
newMessagesToInsert.push(msg);
|
newMessagesToInsert.push(msg);
|
||||||
|
|
||||||
|
|
@ -155,11 +213,9 @@ async function createNewTable() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch insert messages
|
|
||||||
for (let j = 0; j < newMessagesToInsert.length; j += INSERT_BATCH_SIZE) {
|
for (let j = 0; j < newMessagesToInsert.length; j += INSERT_BATCH_SIZE) {
|
||||||
const messageBatch = newMessagesToInsert.slice(j, j + INSERT_BATCH_SIZE);
|
const messageBatch = newMessagesToInsert.slice(j, j + INSERT_BATCH_SIZE);
|
||||||
if (messageBatch.length > 0) {
|
if (messageBatch.length > 0) {
|
||||||
// Ensure all required fields are present
|
|
||||||
const validMessageBatch = messageBatch.map((msg) => ({
|
const validMessageBatch = messageBatch.map((msg) => ({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
chatId: msg.chatId,
|
chatId: msg.chatId,
|
||||||
|
|
@ -173,7 +229,6 @@ async function createNewTable() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch insert votes
|
|
||||||
for (let j = 0; j < newVotesToInsert.length; j += INSERT_BATCH_SIZE) {
|
for (let j = 0; j < newVotesToInsert.length; j += INSERT_BATCH_SIZE) {
|
||||||
const voteBatch = newVotesToInsert.slice(j, j + INSERT_BATCH_SIZE);
|
const voteBatch = newVotesToInsert.slice(j, j + INSERT_BATCH_SIZE);
|
||||||
if (voteBatch.length > 0) {
|
if (voteBatch.length > 0) {
|
||||||
|
|
@ -185,7 +240,7 @@ async function createNewTable() {
|
||||||
console.info(`Migration completed: ${processedCount} chats processed`);
|
console.info(`Migration completed: ${processedCount} chats processed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewTable()
|
migrateMessages()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.info('Script completed successfully');
|
console.info('Script completed successfully');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ai-chatbot",
|
"name": "ai-chatbot",
|
||||||
"version": "3.0.13",
|
"version": "3.0.14",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev --turbo",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue