2025-08-08 06:36:01 +08:00
|
|
|
import type { ModelMessage } from 'ai';
|
|
|
|
import { type SQL, and, eq, isNull, sql } from 'drizzle-orm';
|
|
|
|
import { db } from '../../connection';
|
|
|
|
import { messages } from '../../schema';
|
|
|
|
|
|
|
|
export interface UpdateMessageEntriesParams {
|
|
|
|
messageId: string;
|
2025-08-14 00:48:07 +08:00
|
|
|
toolCallId: string;
|
2025-08-08 06:36:01 +08:00
|
|
|
rawLlmMessage?: ModelMessage;
|
2025-08-08 07:10:24 +08:00
|
|
|
responseEntry?: unknown;
|
|
|
|
reasoningEntry?: unknown;
|
2025-08-08 06:36:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const MESSAGE_FIELD_MAPPING = {
|
|
|
|
rawLlmMessages: messages.rawLlmMessages,
|
|
|
|
responseMessages: messages.responseMessages,
|
|
|
|
reasoning: messages.reasoning,
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
type MessageFieldName = keyof typeof MESSAGE_FIELD_MAPPING;
|
|
|
|
|
|
|
|
/**
|
2025-08-14 01:50:56 +08:00
|
|
|
* Generates SQL for upserting entries in a JSONB array field using jsonb_set.
|
2025-08-08 06:36:01 +08:00
|
|
|
*/
|
2025-08-14 00:48:07 +08:00
|
|
|
function generateJsonbArrayUpsertSql(
|
2025-08-08 06:36:01 +08:00
|
|
|
fieldName: MessageFieldName,
|
2025-08-13 06:38:20 +08:00
|
|
|
jsonString: string,
|
2025-08-14 00:48:07 +08:00
|
|
|
toolCallId: string
|
2025-08-08 06:36:01 +08:00
|
|
|
): SQL {
|
|
|
|
const field = MESSAGE_FIELD_MAPPING[fieldName];
|
|
|
|
|
2025-08-14 04:19:32 +08:00
|
|
|
// For rawLlmMessages, we need to check inside the content array for toolCallId
|
|
|
|
// Structure: { role: 'assistant', content: [{ type: 'tool-call', toolCallId: '...', ... }] }
|
|
|
|
const whereClause =
|
|
|
|
fieldName === 'rawLlmMessages'
|
|
|
|
? sql`EXISTS (
|
|
|
|
SELECT 1
|
|
|
|
FROM jsonb_array_elements(elem.value->'content') AS content_elem
|
|
|
|
WHERE content_elem->>'toolCallId' = ${toolCallId}
|
|
|
|
)`
|
|
|
|
: sql`elem.value->>'id' = ${toolCallId} OR elem.value->>'toolCallId' = ${toolCallId}`;
|
|
|
|
|
2025-08-08 06:36:01 +08:00
|
|
|
return sql`
|
2025-08-14 00:48:07 +08:00
|
|
|
CASE
|
|
|
|
WHEN EXISTS (
|
|
|
|
SELECT 1
|
2025-08-14 01:50:56 +08:00
|
|
|
FROM jsonb_array_elements(COALESCE(${field}, '[]'::jsonb)) WITH ORDINALITY AS elem(value, pos)
|
2025-08-14 04:19:32 +08:00
|
|
|
WHERE ${whereClause}
|
2025-08-14 00:48:07 +08:00
|
|
|
) THEN
|
2025-08-14 01:50:56 +08:00
|
|
|
jsonb_set(
|
|
|
|
COALESCE(${field}, '[]'::jsonb),
|
|
|
|
ARRAY[(
|
|
|
|
SELECT (elem.pos - 1)::text
|
|
|
|
FROM jsonb_array_elements(COALESCE(${field}, '[]'::jsonb)) WITH ORDINALITY AS elem(value, pos)
|
2025-08-14 04:19:32 +08:00
|
|
|
WHERE ${whereClause}
|
2025-08-14 01:50:56 +08:00
|
|
|
LIMIT 1
|
|
|
|
)],
|
2025-08-08 06:36:01 +08:00
|
|
|
${jsonString}::jsonb,
|
|
|
|
false
|
|
|
|
)
|
|
|
|
ELSE
|
2025-08-14 00:48:07 +08:00
|
|
|
COALESCE(${field}, '[]'::jsonb) || ${jsonString}::jsonb
|
2025-08-08 06:36:01 +08:00
|
|
|
END
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-08-14 01:50:56 +08:00
|
|
|
* Updates message entries atomically, ensuring only one entry per toolCallId exists.
|
2025-08-08 06:36:01 +08:00
|
|
|
*/
|
|
|
|
export async function updateMessageEntries({
|
|
|
|
messageId,
|
2025-08-14 00:48:07 +08:00
|
|
|
toolCallId,
|
2025-08-08 06:36:01 +08:00
|
|
|
rawLlmMessage,
|
|
|
|
responseEntry,
|
|
|
|
reasoningEntry,
|
|
|
|
}: UpdateMessageEntriesParams): Promise<{ success: boolean }> {
|
|
|
|
try {
|
|
|
|
const setValues: Record<string, SQL | string> = {
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (rawLlmMessage) {
|
2025-08-14 00:48:07 +08:00
|
|
|
setValues.rawLlmMessages = generateJsonbArrayUpsertSql(
|
2025-08-13 08:32:56 +08:00
|
|
|
'rawLlmMessages',
|
|
|
|
JSON.stringify(rawLlmMessage),
|
2025-08-14 00:48:07 +08:00
|
|
|
toolCallId
|
2025-08-13 08:32:56 +08:00
|
|
|
);
|
2025-08-08 06:36:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (responseEntry) {
|
2025-08-14 00:48:07 +08:00
|
|
|
setValues.responseMessages = generateJsonbArrayUpsertSql(
|
2025-08-13 08:32:56 +08:00
|
|
|
'responseMessages',
|
|
|
|
JSON.stringify(responseEntry),
|
2025-08-14 00:48:07 +08:00
|
|
|
toolCallId
|
2025-08-13 08:32:56 +08:00
|
|
|
);
|
2025-08-08 06:36:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (reasoningEntry) {
|
2025-08-14 00:48:07 +08:00
|
|
|
setValues.reasoning = generateJsonbArrayUpsertSql(
|
2025-08-13 08:32:56 +08:00
|
|
|
'reasoning',
|
|
|
|
JSON.stringify(reasoningEntry),
|
2025-08-14 00:48:07 +08:00
|
|
|
toolCallId
|
2025-08-13 08:32:56 +08:00
|
|
|
);
|
2025-08-08 06:36:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
await db
|
|
|
|
.update(messages)
|
|
|
|
.set(setValues)
|
|
|
|
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
|
|
|
|
|
|
|
return { success: true };
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to update message entries:', error);
|
|
|
|
throw new Error(`Failed to update message entries for message ${messageId}`);
|
|
|
|
}
|
|
|
|
}
|