fix(core): Files are not included in memory in chat (no-changelog) (#22922)

This commit is contained in:
Suguru Inoue
2025-12-10 08:38:29 +01:00
committed by GitHub
parent c5a31a3be8
commit c7fd47d31f
6 changed files with 737 additions and 34 deletions
@@ -0,0 +1,576 @@
import type { WorkflowRepository, SharedWorkflowRepository } from '@n8n/db';
import type { Logger } from '@n8n/backend-common';
import { mock } from 'jest-mock-extended';
import type { BinaryDataService } from 'n8n-core';
import type { IBinaryData } from 'n8n-workflow';
import { ChatHubWorkflowService } from '../chat-hub-workflow.service';
import { ChatHubMessage } from '../chat-hub-message.entity';
import { ChatHubSession } from '../chat-hub-session.entity';
import { ChatHubAttachmentService } from '../chat-hub.attachment.service';
import type { ChatHubMessageRepository } from '../chat-message.repository';
describe('ChatHubWorkflowService', () => {
const logger = mock<Logger>();
const workflowRepository = mock<WorkflowRepository>();
const sharedWorkflowRepository = mock<SharedWorkflowRepository>();
const binaryDataService = mock<BinaryDataService>();
const messageRepository = mock<ChatHubMessageRepository>();
let chatHubAttachmentService: ChatHubAttachmentService;
let service: ChatHubWorkflowService;
beforeEach(() => {
jest.resetAllMocks();
// Create real ChatHubAttachmentService with mocked dependencies
chatHubAttachmentService = new ChatHubAttachmentService(binaryDataService, messageRepository);
service = new ChatHubWorkflowService(
logger,
workflowRepository,
sharedWorkflowRepository,
chatHubAttachmentService,
);
// Mock repository methods
const mockEntityManager = {
save: jest.fn().mockImplementation(async (entity) => {
// Return the entity with an ID added
return { ...entity, id: 'workflow-123' };
}),
} as any;
Object.defineProperty(workflowRepository, 'manager', {
value: {
transaction: jest.fn((cb) => cb(mockEntityManager)),
},
writable: true,
});
(sharedWorkflowRepository.create as jest.Mock) = jest.fn().mockReturnValue({});
});
describe('createChatWorkflow', () => {
describe('message history handling', () => {
it('should handle empty history', async () => {
const mockHistory: ChatHubMessage[] = [];
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
const messageValues = (restoreMemoryNode?.parameters?.messages as any)?.messageValues;
expect(messageValues).toHaveLength(0);
});
it('should handle multiple messages', async () => {
const message1 = new ChatHubMessage();
message1.id = 'msg-1';
message1.content = 'First message';
message1.type = 'human';
message1.attachments = [];
message1.sessionId = 'session-456';
message1.session = new ChatHubSession();
message1.status = 'running';
const message2 = new ChatHubMessage();
message2.id = 'msg-2';
message2.content = 'Second message';
message2.type = 'ai';
message2.attachments = [];
message2.sessionId = 'session-456';
message2.session = new ChatHubSession();
message2.status = 'running';
const message3 = new ChatHubMessage();
message3.id = 'msg-3';
message3.content = 'Third message';
message3.type = 'human';
message3.attachments = [];
message3.sessionId = 'session-456';
message3.session = new ChatHubSession();
message3.status = 'running';
const mockHistory: ChatHubMessage[] = [message1, message2, message3];
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
const messageValues = (restoreMemoryNode?.parameters?.messages as any)?.messageValues;
expect(messageValues).toHaveLength(3);
expect(messageValues[0]).toEqual({
type: 'user',
message: 'First message',
hideFromUI: false,
});
expect(messageValues[1]).toEqual({
type: 'ai',
message: 'Second message',
hideFromUI: false,
});
expect(messageValues[2]).toEqual({
type: 'user',
message: 'Third message',
hideFromUI: false,
});
});
});
describe('attachment files', () => {
it('should convert binary data to data URL when attachment has id', async () => {
const mockAttachment: IBinaryData = {
id: 'filesystem-v2:chat-hub/sessions/session-456/messages/msg-1/binary_data/bin-1',
data: 'filesystem-v2',
mimeType: 'image/png',
fileName: 'test.png',
fileSize: '100 kB',
fileExtension: 'png',
};
const mockMessage = new ChatHubMessage();
mockMessage.id = 'msg-1';
mockMessage.content = 'Check this image';
mockMessage.type = 'human';
mockMessage.attachments = [mockAttachment];
mockMessage.sessionId = 'session-456';
mockMessage.session = new ChatHubSession();
mockMessage.status = 'running';
const mockHistory: ChatHubMessage[] = [mockMessage];
const mockImageBuffer = Buffer.from('fake-image-data', 'base64');
binaryDataService.getAsBuffer.mockResolvedValue(mockImageBuffer);
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
expect(binaryDataService.getAsBuffer).toHaveBeenCalledWith(mockAttachment);
const expectedDataUrl = `data:${mockAttachment.mimeType};base64,${mockImageBuffer.toString('base64')}`;
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
expect((restoreMemoryNode?.parameters?.messages as any)?.messageValues[0].message).toEqual([
{ type: 'text', text: 'Check this image' },
{ type: 'image_url', image_url: expectedDataUrl },
]);
});
it('should use data URL directly when attachment has data but no id', async () => {
const mockAttachment: IBinaryData = {
data: 'data:image/png;base64,iVBORw0KGgoAAAANS',
mimeType: 'image/png',
fileName: 'test.png',
};
const mockMessage = new ChatHubMessage();
mockMessage.id = 'msg-1';
mockMessage.content = 'Check this image';
mockMessage.type = 'human';
mockMessage.attachments = [mockAttachment];
mockMessage.sessionId = 'session-456';
mockMessage.session = new ChatHubSession();
mockMessage.status = 'running';
const mockHistory: ChatHubMessage[] = [mockMessage];
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
expect((restoreMemoryNode?.parameters?.messages as any)?.messageValues[0].message).toEqual([
{ type: 'text', text: 'Check this image' },
{ type: 'image_url', image_url: mockAttachment.data },
]);
});
it('should handle multiple attachments correctly', async () => {
const mockAttachmentWithId: IBinaryData = {
id: 'filesystem-v2:chat-hub/sessions/session-456/messages/msg-1/binary_data/bin-2',
data: 'filesystem-v2',
mimeType: 'image/png',
fileName: 'test1.png',
fileSize: '150 kB',
fileExtension: 'png',
};
const mockAttachmentWithData: IBinaryData = {
data: 'data:image/jpeg;base64,/9j/4AAQ',
mimeType: 'image/jpeg',
fileName: 'test2.jpg',
};
const mockMessage = new ChatHubMessage();
mockMessage.id = 'msg-1';
mockMessage.content = 'Check these images';
mockMessage.type = 'human';
mockMessage.attachments = [mockAttachmentWithId, mockAttachmentWithData];
mockMessage.sessionId = 'session-456';
mockMessage.session = new ChatHubSession();
mockMessage.status = 'running';
const mockHistory: ChatHubMessage[] = [mockMessage];
const mockImageBuffer = Buffer.from('fake-image-data-1', 'base64');
binaryDataService.getAsBuffer.mockResolvedValue(mockImageBuffer);
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
expect(binaryDataService.getAsBuffer).toHaveBeenCalledTimes(1);
expect(binaryDataService.getAsBuffer).toHaveBeenCalledWith(mockAttachmentWithId);
expect(binaryDataService.createSignedToken).not.toHaveBeenCalled();
const expectedDataUrl = `data:${mockAttachmentWithId.mimeType};base64,${mockImageBuffer.toString('base64')}`;
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
expect((restoreMemoryNode?.parameters?.messages as any)?.messageValues[0].message).toEqual([
{ type: 'text', text: 'Check these images' },
{ type: 'image_url', image_url: expectedDataUrl },
{ type: 'image_url', image_url: mockAttachmentWithData.data },
]);
});
it('should omit attachments that exceed maxTotalPayloadSize limit', async () => {
// Create a large data URL that, when added to another attachment, will exceed the 20MB limit (90% of 20MB = 18MB)
const largeDataUrl = 'data:image/png;base64,' + 'A'.repeat(10 * 1024 * 1024);
const smallDataUrl = 'data:image/png;base64,' + 'B'.repeat(9 * 1024 * 1024);
const mockAttachment1: IBinaryData = {
data: largeDataUrl,
mimeType: 'image/png',
fileName: 'large.png',
};
const mockAttachment2: IBinaryData = {
data: smallDataUrl,
mimeType: 'image/png',
fileName: 'small.png',
};
const mockMessage = new ChatHubMessage();
mockMessage.id = 'msg-1';
mockMessage.content = 'Check these images';
mockMessage.type = 'human';
mockMessage.attachments = [mockAttachment1, mockAttachment2];
mockMessage.sessionId = 'session-456';
mockMessage.session = new ChatHubSession();
mockMessage.status = 'running';
const mockHistory: ChatHubMessage[] = [mockMessage];
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
const messageContent = (restoreMemoryNode?.parameters?.messages as any)?.messageValues[0]
.message;
// Should include text and only the first attachment since the second would exceed the limit
expect(messageContent).toEqual([
{ type: 'text', text: 'Check these images' },
{ type: 'image_url', image_url: largeDataUrl },
{ type: 'text', text: 'File: small.png\n(Content omitted due to size limit)' },
]);
});
it('should omit attachments across multiple messages when total size exceeds limit', async () => {
const attachment1DataUrl = 'data:image/png;base64,' + 'A'.repeat(5 * 1024 * 1024);
const mockAttachment1: IBinaryData = {
data: attachment1DataUrl,
mimeType: 'image/png',
fileName: 'first.png',
};
const message1 = new ChatHubMessage();
message1.id = 'msg-1';
message1.content = 'First message with attachment';
message1.type = 'human';
message1.attachments = [mockAttachment1];
message1.sessionId = 'session-456';
message1.session = new ChatHubSession();
message1.status = 'running';
const attachment2DataUrl = 'data:image/jpeg;base64,' + 'B'.repeat(8 * 1024 * 1024);
const mockAttachment2: IBinaryData = {
data: attachment2DataUrl,
mimeType: 'image/jpeg',
fileName: 'second.jpg',
};
const message2 = new ChatHubMessage();
message2.id = 'msg-2';
message2.content = 'Second message';
message2.type = 'ai';
message2.attachments = [mockAttachment2];
message2.sessionId = 'session-456';
message2.session = new ChatHubSession();
message2.status = 'running';
const attachment3DataUrl = 'data:image/png;base64,' + 'C'.repeat(6 * 1024 * 1024);
const mockAttachment3: IBinaryData = {
data: attachment3DataUrl,
mimeType: 'image/png',
fileName: 'third.png',
};
const message3 = new ChatHubMessage();
message3.id = 'msg-3';
message3.content = 'Third message';
message3.type = 'human';
message3.attachments = [mockAttachment3];
message3.sessionId = 'session-456';
message3.session = new ChatHubSession();
message3.status = 'running';
const mockHistory: ChatHubMessage[] = [message1, message2, message3];
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
const messageValues = (restoreMemoryNode?.parameters?.messages as any)?.messageValues;
expect(messageValues).toHaveLength(3);
// First message attachment is skipped due to cumulative size limit (processed last, size limit reached)
expect(messageValues[0].message).toEqual([
{ type: 'text', text: 'First message with attachment' },
{ type: 'text', text: 'File: first.png\n(Content omitted due to size limit)' },
]);
// Second message includes attachment
expect(messageValues[1].message).toEqual([
{ type: 'text', text: 'Second message' },
{ type: 'image_url', image_url: attachment2DataUrl },
]);
// Third message includes attachment (processed first)
expect(messageValues[2].message).toEqual([
{ type: 'text', text: 'Third message' },
{ type: 'image_url', image_url: attachment3DataUrl },
]);
});
it('should include omitted content message when attachment exceeds size limit', async () => {
const largeDataUrl = 'data:image/png;base64,' + 'A'.repeat(17 * 1024 * 1024);
const mockAttachment: IBinaryData = {
data: largeDataUrl,
mimeType: 'image/png',
fileName: 'large-image.png',
};
const mockMessage = new ChatHubMessage();
mockMessage.id = 'msg-1';
mockMessage.content = 'Check this large image';
mockMessage.type = 'human';
mockMessage.attachments = [mockAttachment];
mockMessage.sessionId = 'session-456';
mockMessage.session = new ChatHubSession();
mockMessage.status = 'running';
const anotherAttachment: IBinaryData = {
data: 'data:image/jpeg;base64,' + 'B'.repeat(2 * 1024 * 1024),
mimeType: 'image/jpeg',
fileName: 'small-image.jpg',
};
const mockMessage2 = new ChatHubMessage();
mockMessage2.id = 'msg-2';
mockMessage2.content = 'And this small one';
mockMessage2.type = 'human';
mockMessage2.attachments = [anotherAttachment];
mockMessage2.sessionId = 'session-456';
mockMessage2.session = new ChatHubSession();
mockMessage2.status = 'running';
const mockHistory: ChatHubMessage[] = [mockMessage, mockMessage2];
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
const messageValues = (restoreMemoryNode?.parameters?.messages as any)?.messageValues;
expect(messageValues[0].message).toEqual([
{ type: 'text', text: 'Check this large image' },
{ type: 'text', text: 'File: large-image.png\n(Content omitted due to size limit)' },
]);
expect(messageValues[1].message).toEqual([
{ type: 'text', text: 'And this small one' },
{ type: 'image_url', image_url: anotherAttachment.data },
]);
});
it('should handle text file attachments as text blocks', async () => {
const textContent = 'This is the content of the text file.\nIt has multiple lines.';
const mockAttachment: IBinaryData = {
id: 'filesystem-v2:chat-hub/sessions/session-456/messages/msg-1/binary_data/text-1',
data: 'filesystem-v2',
mimeType: 'text/plain',
fileName: 'document.txt',
fileSize: '60',
fileExtension: 'txt',
};
const mockMessage = new ChatHubMessage();
mockMessage.id = 'msg-1';
mockMessage.content = 'Here is a text file';
mockMessage.type = 'human';
mockMessage.attachments = [mockAttachment];
mockMessage.sessionId = 'session-456';
mockMessage.session = new ChatHubSession();
mockMessage.status = 'running';
const mockHistory: ChatHubMessage[] = [mockMessage];
// Mock getAsBuffer to return the text content
binaryDataService.getAsBuffer.mockResolvedValue(Buffer.from(textContent, 'utf-8'));
const result = await service.createChatWorkflow(
'user-123',
'session-456',
'project-789',
mockHistory,
'Hello',
[],
{ openAiApi: { id: 'cred-123', name: 'OpenAI' } },
{ provider: 'openai', model: 'gpt-4' },
undefined,
[],
'UTC',
);
expect(binaryDataService.getAsBuffer).toHaveBeenCalledWith(mockAttachment);
const restoreMemoryNode = result.workflowData.nodes.find(
(node) => node.name === 'Restore Chat Memory',
);
expect(restoreMemoryNode?.parameters?.messages).toBeDefined();
const messageValues = (restoreMemoryNode?.parameters?.messages as any)?.messageValues;
expect(messageValues[0].message).toEqual([
{ type: 'text', text: 'Here is a text file' },
{ type: 'text', text: `File: document.txt\nContent: \n${textContent}` },
]);
});
});
});
});
@@ -26,13 +26,15 @@ import {
NodeConnectionTypes,
OperationalError,
type IBinaryData,
type NodeParameterValueType,
} from 'n8n-workflow';
import { v4 as uuidv4 } from 'uuid';
import { ChatHubMessage } from './chat-hub-message.entity';
import { NODE_NAMES, PROVIDER_NODE_TYPE_MAP } from './chat-hub.constants';
import { MessageRecord, type ChatTriggerResponseMode } from './chat-hub.types';
import { MessageRecord, type ContentBlock, type ChatTriggerResponseMode } from './chat-hub.types';
import { getMaxContextWindowTokens } from './context-limits';
import { ChatHubAttachmentService } from './chat-hub.attachment.service';
@Service()
export class ChatHubWorkflowService {
@@ -40,6 +42,7 @@ export class ChatHubWorkflowService {
private readonly logger: Logger,
private readonly workflowRepository: WorkflowRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly chatHubAttachmentService: ChatHubAttachmentService,
) {}
async createChatWorkflow(
@@ -65,7 +68,7 @@ export class ChatHubWorkflowService {
`Creating chat workflow for user ${userId} and session ${sessionId}, provider ${model.provider}`,
);
const { nodes, connections, executionData } = this.buildChatWorkflow({
const { nodes, connections, executionData } = await this.buildChatWorkflow({
userId,
sessionId,
history,
@@ -255,7 +258,7 @@ export class ChatHubWorkflowService {
return uniqueName;
}
private buildChatWorkflow({
private async buildChatWorkflow({
userId,
sessionId,
history,
@@ -280,7 +283,7 @@ export class ChatHubWorkflowService {
const toolsAgentNode = this.buildToolsAgentNode(model, systemMessage);
const modelNode = this.buildModelNode(credentials, model);
const memoryNode = this.buildMemoryNode(20);
const restoreMemoryNode = this.buildRestoreMemoryNode(history);
const restoreMemoryNode = await this.buildRestoreMemoryNode(history);
const clearMemoryNode = this.buildClearMemoryNode();
const mergeNode = this.buildMergeNode();
@@ -673,29 +676,15 @@ ${this.getSystemMessageMetadata(timeZone)}`;
};
}
private buildRestoreMemoryNode(history: ChatHubMessage[]): INode {
private async buildRestoreMemoryNode(history: ChatHubMessage[]): Promise<INode> {
const messageValues = await this.buildMessageValuesWithAttachments(history);
return {
parameters: {
mode: 'insert',
insertMode: 'override',
messages: {
messageValues: history
// Empty messages can't be restored by the memory manager
.filter((message) => message.content.length > 0)
.map((message) => {
const typeMap: Record<string, MessageRecord['type']> = {
human: 'user',
ai: 'ai',
system: 'system',
};
// TODO: Tool messages etc?
return {
type: typeMap[message.type] || 'system',
message: message.content,
hideFromUI: false,
};
}),
messageValues: messageValues as unknown as NodeParameterValueType,
},
},
type: MEMORY_MANAGER_NODE_TYPE,
@@ -706,6 +695,128 @@ ${this.getSystemMessageMetadata(timeZone)}`;
};
}
private async buildMessageValuesWithAttachments(
history: ChatHubMessage[],
): Promise<MessageRecord[]> {
// Gemini has 20MB limit, the value should also be what n8n instance can safely handle
const maxTotalPayloadSize = 20 * 1024 * 1024 * 0.9;
const typeMap: Record<string, MessageRecord['type']> = {
human: 'user',
ai: 'ai',
system: 'system',
};
const messageValues: MessageRecord[] = [];
let currentTotalSize = 0;
const messages = history.slice().reverse(); // Traversing messages from last to prioritize newer attachments
for (const message of messages) {
// Empty messages can't be restored by the memory manager
if (message.content.length === 0) {
continue;
}
const attachments = message.attachments ?? [];
const type = typeMap[message.type] || 'system';
// TODO: Tool messages etc?
const textSize = message.content.length;
currentTotalSize += textSize;
if (attachments.length === 0) {
messageValues.push({
type,
message: message.content,
hideFromUI: false,
});
continue;
}
const blocks: ContentBlock[] = [{ type: 'text', text: message.content }];
// Add attachments if within size limit
for (const attachment of attachments) {
const block = await this.buildContentBlockForAttachment(
attachment,
currentTotalSize,
maxTotalPayloadSize,
);
blocks.push(block);
currentTotalSize += block.type === 'text' ? block.text.length : block.image_url.length;
}
messageValues.push({
type,
message: blocks,
hideFromUI: false,
});
}
// Reverse to restore original order
messageValues.reverse();
return messageValues;
}
private async buildContentBlockForAttachment(
attachment: IBinaryData,
currentTotalSize: number,
maxTotalPayloadSize: number,
): Promise<ContentBlock> {
class TotalFileSizeExceededError extends Error {}
try {
if (currentTotalSize >= maxTotalPayloadSize) {
throw new TotalFileSizeExceededError();
}
if (this.isTextFile(attachment.mimeType)) {
const buffer = await this.chatHubAttachmentService.getAsBuffer(attachment);
const content = buffer.toString('utf-8');
if (currentTotalSize + content.length > maxTotalPayloadSize) {
throw new TotalFileSizeExceededError();
}
return {
type: 'text',
text: `File: ${attachment.fileName ?? 'attachment'}\nContent: \n${content}`,
};
}
const url = await this.chatHubAttachmentService.getDataUrl(attachment);
if (currentTotalSize + url.length > maxTotalPayloadSize) {
throw new TotalFileSizeExceededError();
}
return { type: 'image_url', image_url: url };
} catch (e) {
if (e instanceof TotalFileSizeExceededError) {
return {
type: 'text',
text: `File: ${attachment.fileName ?? 'attachment'}\n(Content omitted due to size limit)`,
};
}
throw e;
}
}
private isTextFile(mimeType: string): boolean {
return (
mimeType.startsWith('text/') ||
mimeType === 'application/json' ||
mimeType === 'application/xml' ||
mimeType === 'application/csv' ||
mimeType === 'application/x-yaml' ||
mimeType === 'application/yaml'
);
}
private buildClearMemoryNode(): INode {
return {
parameters: {
@@ -126,6 +126,22 @@ export class ChatHubAttachmentService {
);
}
async getDataUrl(binaryData: IBinaryData): Promise<string> {
if (binaryData.data.startsWith('data:')) {
return binaryData.data;
}
const buffer = await this.binaryDataService.getAsBuffer(binaryData);
const base64Data = buffer.toString(BINARY_ENCODING);
const mimeType = binaryData.mimeType || 'application/octet-stream';
return `data:${mimeType};base64,${base64Data}`;
}
async getAsBuffer(binaryData: IBinaryData): Promise<Buffer<ArrayBufferLike>> {
return await this.binaryDataService.getAsBuffer(binaryData);
}
/**
* Processes a single attachment by populating metadata and storing it.
*/
@@ -43,11 +43,16 @@ export interface EditMessagePayload extends BaseMessagePayload {
message: string;
}
// From @langchain/core
export type ContentBlock =
| { type: 'text'; text: string }
| { type: 'image_url'; image_url: string };
// From packages/@n8n/nodes-langchain/nodes/memory/MemoryManager/MemoryManager.node.ts
export type MessageRole = 'ai' | 'system' | 'user';
export interface MessageRecord {
type: MessageRole;
message: string;
message: string | ContentBlock[];
hideFromUI: boolean;
}