diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 4379e8a1d7b..16d7567a3ae 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -137,8 +137,6 @@ export type ChatHubConversationsResponse = ChatHubSessionDto[]; export interface ChatHubConversationDto { messages: Record; - rootIds: ChatMessageId[]; - activeMessageChain: ChatMessageId[]; } export interface ChatHubConversationResponse { diff --git a/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts b/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts index cbb8ac377ab..8359a4ab3ef 100644 --- a/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts +++ b/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts @@ -1,7 +1,6 @@ import { testDb, testModules } from '@n8n/backend-test-utils'; import type { User } from '@n8n/db'; import { Container } from '@n8n/di'; - import { createAdmin, createMember } from '@test-integration/db/users'; import { ChatHubService } from '../chat-hub.service'; @@ -176,16 +175,10 @@ describe('chatHub', () => { expect(response).toBeDefined(); const { - conversation: { rootIds, messages, activeMessageChain }, + conversation: { messages }, } = response; - expect(rootIds).toEqual([msg1.id]); expect(Object.keys(messages)).toHaveLength(4); - expect(activeMessageChain).toHaveLength(4); - expect(activeMessageChain[0]).toBe(msg1.id); - expect(activeMessageChain[1]).toBe(msg2.id); - expect(activeMessageChain[2]).toBe(msg3.id); - expect(activeMessageChain[3]).toBe(msg4.id); expect(messages[msg1.id].content).toBe('message 1'); expect(messages[msg1.id].type).toBe('human'); expect(messages[msg1.id].turnId).toBe(msg1.id); @@ -283,16 +276,10 @@ describe('chatHub', () => { expect(response).toBeDefined(); const { - conversation: { rootIds, messages, activeMessageChain }, + conversation: { messages }, } = response; - expect(rootIds).toEqual([msg1.id]); expect(Object.keys(messages)).toHaveLength(6); - expect(activeMessageChain).toHaveLength(4); - expect(activeMessageChain[0]).toBe(msg1.id); - expect(activeMessageChain[1]).toBe(msg2.id); - expect(activeMessageChain[2]).toBe(msg5.id); - expect(activeMessageChain[3]).toBe(msg6.id); expect(messages[msg1.id].content).toBe('message 1'); expect(messages[msg2.id].content).toBe('message 2'); expect(messages[msg3.id].content).toBe('message 3a'); @@ -346,7 +333,7 @@ describe('chatHub', () => { turnId: ids[2], createdAt: new Date('2025-01-03T00:10:00Z'), }); - const msg4 = await messagesRepository.createChatMessage({ + await messagesRepository.createChatMessage({ id: ids[3], sessionId: session.id, name: 'ChatGPT', @@ -362,14 +349,10 @@ describe('chatHub', () => { expect(response).toBeDefined(); const { - conversation: { rootIds, messages, activeMessageChain }, + conversation: { messages }, } = response; - expect(rootIds).toEqual([msg1.id, msg3.id]); expect(Object.keys(messages)).toHaveLength(4); - expect(activeMessageChain).toHaveLength(2); - expect(activeMessageChain[0]).toBe(msg3.id); - expect(activeMessageChain[1]).toBe(msg4.id); }); it('should get conversation with a retry branch at last message', async () => { @@ -445,16 +428,10 @@ describe('chatHub', () => { expect(response.session.id).toBe(session.id); const { - conversation: { rootIds, messages, activeMessageChain }, + conversation: { messages }, } = response; - expect(rootIds).toEqual([msg1.id]); expect(Object.keys(messages)).toHaveLength(5); - expect(activeMessageChain).toHaveLength(4); - expect(activeMessageChain[0]).toBe(msg1.id); - expect(activeMessageChain[1]).toBe(msg2.id); - expect(activeMessageChain[2]).toBe(msg3.id); - expect(activeMessageChain[3]).toBe(msg5.id); expect(messages[msg5.id].previousMessageId).toBe(msg3.id); expect(messages[msg5.id].retryOfMessageId).toBe(msg4.id); }); @@ -548,7 +525,7 @@ describe('chatHub', () => { turnId: ids[4], createdAt: new Date('2025-01-03T00:25:00Z'), }); - const msg1b = await messagesRepository.createChatMessage({ + await messagesRepository.createChatMessage({ id: ids[6], sessionId: session.id, name: 'Nathan', @@ -579,7 +556,7 @@ describe('chatHub', () => { turnId: ids[8], createdAt: new Date('2025-01-03T00:40:00Z'), }); - const msg4c = await messagesRepository.createChatMessage({ + await messagesRepository.createChatMessage({ id: crypto.randomUUID(), sessionId: session.id, name: 'ChatGPT', @@ -595,18 +572,11 @@ describe('chatHub', () => { expect(response.session.id).toBe(session.id); const { - conversation: { rootIds, messages, activeMessageChain }, + conversation: { messages }, } = response; - expect(rootIds).toEqual([msg1.id, msg1b.id]); expect(Object.keys(messages)).toHaveLength(10); - expect(activeMessageChain).toHaveLength(4); - expect(activeMessageChain[0]).toBe(msg1.id); - expect(activeMessageChain[1]).toBe(msg2r.id); - expect(activeMessageChain[2]).toBe(msg3d.id); - expect(activeMessageChain[3]).toBe(msg4c.id); - expect(messages[msg2r.id].previousMessageId).toBe(msg1.id); expect(messages[msg2r.id].retryOfMessageId).toBe(msg2.id); }); diff --git a/packages/cli/src/modules/chat-hub/chat-hub.service.ts b/packages/cli/src/modules/chat-hub/chat-hub.service.ts index ae159a7f9bc..3efd65efe97 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -38,14 +38,6 @@ import { } from 'n8n-workflow'; import { v4 as uuidv4 } from 'uuid'; -import { ActiveExecutions } from '@/active-executions'; -import { CredentialsService } from '@/credentials/credentials.service'; -import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -import { NotFoundError } from '@/errors/response-errors/not-found.error'; -import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; -import { getBase } from '@/workflow-execute-additional-data'; -import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; - import { ChatHubMessage } from './chat-hub-message.entity'; import type { HumanMessagePayload, @@ -57,6 +49,14 @@ import type { import { ChatHubMessageRepository } from './chat-message.repository'; import { ChatHubSessionRepository } from './chat-session.repository'; +import { ActiveExecutions } from '@/active-executions'; +import { CredentialsService } from '@/credentials/credentials.service'; +import { BadRequestError } from '@/errors/response-errors/bad-request.error'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; +import { DynamicNodeParametersService } from '@/services/dynamic-node-parameters.service'; +import { getBase } from '@/workflow-execute-additional-data'; +import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; + const providerNodeTypeMapping: Record = { openai: { name: '@n8n/n8n-nodes-langchain.lmChatOpenAi', @@ -348,11 +348,23 @@ export class ChatHubService { const session = await this.getChatSession(user, sessionId, selectedModel, true, message); + // Ensure that the previous message exists in the session + if (payload.previousMessageId) { + const previousMessage = await this.messageRepository.getOneById( + payload.previousMessageId, + sessionId, + ); + if (!previousMessage) { + throw new BadRequestError('The previous message does not exist in the session'); + } + } + + const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m])); + const history = this.buildMessageHistory(messages, payload.previousMessageId); + const turnId = messageId; await this.saveHumanMessage(payload, user, turnId, payload.previousMessageId, selectedModel); - const history = session.messages ?? []; - const workflow = await this.createChatWorkflow( user, session.id, @@ -376,31 +388,26 @@ export class ChatHubService { async editHumanMessage(res: Response, user: User, payload: EditMessagePayload) { const { sessionId, editId, messageId, message, replyId } = payload; - const selectedModel: ModelWithCredentials = { ...payload.model, credentialId: this.getCredentialId(payload.model.provider, payload.credentials), }; const session = await this.getChatSession(user, sessionId, selectedModel); - const messages = session.messages ?? []; const messageToEdit = await this.getChatMessage(session.id, editId); if (messageToEdit.type !== 'human') { throw new BadRequestError('Can only edit human messages'); } - const historyIds = messageToEdit.previousMessageId - ? this.buildActiveMessageChain(messages, messageToEdit.previousMessageId) - : []; - - const history = historyIds.flatMap((id) => { - return messages.find((m) => m.id === id) ?? []; - }); + const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m])); + const history = this.buildMessageHistory(messages, messageToEdit.previousMessageId); // If the message to edit isn't the original message, we want to point to the original message const revisionOfMessageId = messageToEdit.revisionOfMessageId ?? messageToEdit.id; - const otherRuns = messages.filter((m) => m.revisionOfMessageId === revisionOfMessageId); + const otherRuns = (session.messages ?? []).filter( + (m) => m.revisionOfMessageId === revisionOfMessageId, + ); const runIndex = otherRuns.length + 1; await this.messageRepository.updateChatMessage(revisionOfMessageId, { state: 'replaced' }); @@ -451,20 +458,14 @@ export class ChatHubService { }; const session = await this.getChatSession(user, sessionId, selectedModel); - const messages = session.messages ?? []; const messageToRetry = await this.getChatMessage(session.id, retryId); if (messageToRetry.type !== 'ai') { throw new BadRequestError('Can only retry AI messages'); } - const historyIds = messageToRetry.previousMessageId - ? this.buildActiveMessageChain(messages, messageToRetry.previousMessageId) - : []; - - const history = historyIds.flatMap((id) => { - return messages.find((m) => m.id === id) ?? []; - }); + const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m])); + const history = this.buildMessageHistory(messages, messageToRetry.previousMessageId); const lastHumanMessage = history.filter((m) => m.type === 'human').pop(); if (!lastHumanMessage) { @@ -489,7 +490,9 @@ export class ChatHubService { // If the message being retried is itself a retry, we want to point to the original message const retryOfMessageId = messageToRetry.retryOfMessageId ?? messageToRetry.id; - const otherRuns = messages.filter((m) => m.retryOfMessageId === retryOfMessageId); + const otherRuns = (session.messages ?? []).filter( + (m) => m.retryOfMessageId === retryOfMessageId, + ); const runIndex = otherRuns.length + 1; await this.messageRepository.updateChatMessage(retryOfMessageId, { state: 'replaced' }); @@ -867,15 +870,6 @@ export class ChatHubService { } const messages = await this.messageRepository.getManyBySessionId(sessionId); - const messagesGraph: Record = Object.fromEntries( - messages.map((m) => [m.id, this.convertMessageToDto(m)]), - ); - - const rootIds = messages.filter((r) => r.previousMessageId === null).map((r) => r.id); - const activeMessages = messages.filter((m) => m.state === 'active'); - const latest = activeMessages[activeMessages.length - 1]; // Messages are sorted by createdAt - - const activeMessageChain = latest ? this.buildActiveMessageChain(messages, latest.id) : []; return { session: { @@ -891,9 +885,7 @@ export class ChatHubService { updatedAt: session.updatedAt.toISOString(), }, conversation: { - messages: messagesGraph, - rootIds, - activeMessageChain, + messages: Object.fromEntries(messages.map((m) => [m.id, this.convertMessageToDto(m)])), }, }; } @@ -922,23 +914,27 @@ export class ChatHubService { } /** - * Build the active message chain ending to the message with ID `lastMessageId` + * Build the message history chain ending to the message with ID `lastMessageId` */ - private buildActiveMessageChain(messages: ChatHubMessage[], lastMessageId: ChatMessageId) { - const nodes = new Map(messages.map((m) => [m.id, m])); + private buildMessageHistory( + messages: Record, + lastMessageId: ChatMessageId | null, + ) { + if (!lastMessageId) return []; const visited = new Set(); - const activeMessageChain = []; + const historyIds = []; let current: ChatMessageId | null = lastMessageId; while (current && !visited.has(current)) { - activeMessageChain.unshift(current); + historyIds.unshift(current); visited.add(current); - current = nodes.get(current)?.previousMessageId ?? null; + current = messages[current]?.previousMessageId ?? null; } - return activeMessageChain; + const history = historyIds.flatMap((id) => messages[id] ?? []); + return history; } async deleteAllSessions() { diff --git a/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue index b3e6dfe7535..109383e07c5 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/chatHub/ChatView.vue @@ -284,7 +284,9 @@ function handleEditMessage(message: ChatHubMessageDto) { return; } - chatStore.editMessage(sessionId.value, message.id, message.content, selectedModel.value, { + const mesasgeToEdit = message.revisionOfMessageId ?? message.id; + + chatStore.editMessage(sessionId.value, mesasgeToEdit, message.content, selectedModel.value, { [PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: { id: credentialsId, name: '', @@ -304,13 +306,19 @@ function handleRegenerateMessage(message: ChatHubMessageDto) { return; } - chatStore.regenerateMessage(sessionId.value, message.id, selectedModel.value, { + const messageToRetry = message.retryOfMessageId ?? message.id; + + chatStore.regenerateMessage(sessionId.value, messageToRetry, selectedModel.value, { [PROVIDER_CREDENTIAL_TYPE_MAP[selectedModel.value.provider]]: { id: credentialsId, name: '', }, }); } + +function handleSwitchAlternative(messageId: string) { + chatStore.switchAlternative(sessionId.value, messageId); +} diff --git a/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessageActions.vue b/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessageActions.vue index be6ea0bba0c..b4390197963 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessageActions.vue +++ b/packages/frontend/editor-ui/src/features/chatHub/components/ChatMessageActions.vue @@ -1,26 +1,33 @@