feat: Implement Chat Memory Manager node (#8127)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
oleg 2024-01-15 09:13:54 +01:00 committed by GitHub
parent 851060dd3f
commit 464be93323
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 346 additions and 3 deletions

View file

@ -72,6 +72,7 @@ function getInputs(
'@n8n/n8n-nodes-langchain.lmChatOllama',
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
'@n8n/n8n-nodes-langchain.lmChatGooglePalm',
'@n8n/n8n-nodes-langchain.lmChatMistralCloud',
],
},
},

View file

@ -32,12 +32,14 @@ function simplifyMessages(messages: BaseMessage[]) {
return transformedMessages;
}
// This node is deprecated. Use MemoryManager instead.
export class MemoryChatRetriever implements INodeType {
description: INodeTypeDescription = {
displayName: 'Chat Messages Retriever',
name: 'memoryChatRetriever',
icon: 'fa:database',
group: ['transform'],
hidden: true,
version: 1,
description: 'Retrieve chat messages from memory and use them in the workflow',
defaults: {
@ -69,6 +71,12 @@ export class MemoryChatRetriever implements INodeType {
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs: [NodeConnectionType.Main],
properties: [
{
displayName: "This node is deprecated. Use 'Chat Memory Manager' node instead.",
type: 'notice',
default: '',
name: 'deprecatedNotice',
},
{
displayName: 'Simplify Output',
name: 'simplifyOutput',

View file

@ -0,0 +1,329 @@
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
import {
NodeConnectionType,
type IDataObject,
type IExecuteFunctions,
type INodeExecutionData,
type INodeType,
type INodeTypeDescription,
} from 'n8n-workflow';
import type { BaseChatMemory } from 'langchain/memory';
import { AIMessage, SystemMessage, HumanMessage, type BaseMessage } from 'langchain/schema';
type MessageRole = 'ai' | 'system' | 'user';
interface MessageRecord {
type: MessageRole;
message: string;
hideFromUI: boolean;
}
function simplifyMessages(messages: BaseMessage[]) {
const chunkedMessages = [];
for (let i = 0; i < messages.length; i += 2) {
chunkedMessages.push([messages[i], messages[i + 1]]);
}
const transformedMessages = chunkedMessages.map((exchange) => {
const simplified = {
[exchange[0]._getType()]: exchange[0].content,
};
if (exchange[1]) {
simplified[exchange[1]._getType()] = exchange[1].content;
}
return simplified;
});
return transformedMessages;
}
export class MemoryManager implements INodeType {
description: INodeTypeDescription = {
displayName: 'Chat Memory Manager',
name: 'memoryManager',
icon: 'fa:database',
group: ['transform'],
version: 1,
description: 'Manage chat messages memory and use it in the workflow',
defaults: {
name: 'Chat Memory Manager',
},
codex: {
categories: ['AI'],
subcategories: {
AI: ['Miscellaneous'],
},
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorymanager/',
},
],
},
},
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs: [
{
displayName: '',
type: NodeConnectionType.Main,
},
{
displayName: 'Memory',
type: NodeConnectionType.AiMemory,
required: true,
maxConnections: 1,
},
],
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs: [
{
displayName: '',
type: NodeConnectionType.Main,
},
],
properties: [
{
displayName: 'Operation Mode',
name: 'mode',
type: 'options',
noDataExpression: true,
default: 'load',
options: [
{
name: 'Get Many Messages',
description: 'Retrieve chat messages from connected memory',
value: 'load',
},
{
name: 'Insert Messages',
description: 'Insert chat messages into connected memory',
value: 'insert',
},
{
name: 'Delete Messages',
description: 'Delete chat messages from connected memory',
value: 'delete',
},
],
},
{
displayName: 'Insert Mode',
name: 'insertMode',
type: 'options',
description: 'Choose how new messages are inserted into the memory',
noDataExpression: true,
default: 'insert',
options: [
{
name: 'Insert Messages',
value: 'insert',
description: 'Add messages alongside existing ones',
},
{
name: 'Override All Messages',
value: 'override',
description: 'Replace the current memory with new messages',
},
],
displayOptions: {
show: {
mode: ['insert'],
},
},
},
{
displayName: 'Delete Mode',
name: 'deleteMode',
type: 'options',
description: 'How messages are deleted from memory',
noDataExpression: true,
default: 'lastN',
options: [
{
name: 'Last N',
value: 'lastN',
description: 'Delete the last N messages',
},
{
name: 'All Messages',
value: 'all',
description: 'Clear all messages from memory',
},
],
displayOptions: {
show: {
mode: ['delete'],
},
},
},
{
displayName: 'Chat Messages',
name: 'messages',
description: 'Chat messages to insert into memory',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
placeholder: 'Add message',
options: [
{
name: 'messageValues',
displayName: 'Message',
values: [
{
displayName: 'Type Name or ID',
name: 'type',
type: 'options',
options: [
{
name: 'AI',
value: 'ai',
},
{
name: 'System',
value: 'system',
},
{
name: 'User',
value: 'user',
},
],
default: 'system',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Hide Message in Chat',
name: 'hideFromUI',
type: 'boolean',
required: true,
default: false,
description: 'Whether to hide the message from the chat UI',
},
],
},
],
displayOptions: {
show: {
mode: ['insert'],
},
},
},
{
displayName: 'Messages Count',
name: 'lastMessagesCount',
type: 'number',
description: 'The amount of last messages to delete',
default: 2,
displayOptions: {
show: {
mode: ['delete'],
deleteMode: ['lastN'],
},
},
},
{
displayName: 'Simplify Output',
name: 'simplifyOutput',
type: 'boolean',
description: 'Whether to simplify the output to only include the sender and the text',
default: true,
displayOptions: {
show: {
mode: ['load'],
},
},
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const memory = (await this.getInputConnectionData(
NodeConnectionType.AiMemory,
0,
)) as BaseChatMemory;
const items = this.getInputData();
const result = [];
for (let i = 0; i < items.length; i++) {
const mode = this.getNodeParameter('mode', i) as 'load' | 'insert' | 'delete';
let messages = [...(await memory.chatHistory.getMessages())];
if (mode === 'delete') {
const deleteMode = this.getNodeParameter('deleteMode', i) as 'lastN' | 'all';
if (deleteMode === 'lastN') {
const lastMessagesCount = this.getNodeParameter('lastMessagesCount', i) as number;
if (messages.length >= lastMessagesCount) {
const newMessages = messages.slice(0, messages.length - lastMessagesCount);
await memory.chatHistory.clear();
for (const message of newMessages) {
await memory.chatHistory.addMessage(message);
}
}
} else {
await memory.chatHistory.clear();
}
}
if (mode === 'insert') {
const insertMode = this.getNodeParameter('insertMode', i) as 'insert' | 'override';
const messagesToInsert = this.getNodeParameter(
'messages.messageValues',
i,
[],
) as MessageRecord[];
const templateMapper = {
ai: AIMessage,
system: SystemMessage,
user: HumanMessage,
};
if (insertMode === 'override') {
await memory.chatHistory.clear();
}
for (const message of messagesToInsert) {
const MessageClass = new templateMapper[message.type](message.message);
if (message.hideFromUI) {
MessageClass.additional_kwargs.hideFromUI = true;
}
await memory.chatHistory.addMessage(MessageClass);
}
}
// Refresh messages from memory
messages = await memory.chatHistory.getMessages();
const simplifyOutput = this.getNodeParameter('simplifyOutput', i, false) as boolean;
if (simplifyOutput && messages) {
return [
this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(simplifyMessages(messages)),
{ itemData: { item: i } },
),
];
}
const serializedMessages = messages?.map((message) => message.toJSON()) ?? [];
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(serializedMessages as unknown as IDataObject[]),
{ itemData: { item: i } },
);
result.push(...executionData);
}
return this.prepareOutputData(result);
}
}

View file

@ -87,6 +87,8 @@ export class MemoryMotorhead implements INodeType {
apiKey: credentials.apiKey as string,
memoryKey: 'chat_history',
returnMessages: true,
inputKey: 'input',
outputKey: 'output',
});
await memory.init();

View file

@ -103,6 +103,8 @@ export class MemoryXata implements INodeType {
}),
memoryKey: 'chat_history',
returnMessages: true,
inputKey: 'input',
outputKey: 'output',
});
return {
response: logWrapper(memory, this),

View file

@ -404,9 +404,9 @@ export class ChatTrigger implements INodeType {
const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
| BaseChatMemory
| undefined;
const messages = ((await memory?.chatHistory.getMessages()) ?? []).map(
(message) => message?.toJSON(),
);
const messages = ((await memory?.chatHistory.getMessages()) ?? [])
.filter((message) => !message?.additional_kwargs?.hideFromUI)
.map((message) => message?.toJSON());
return {
webhookResponse: { data: messages },
};

View file

@ -71,6 +71,7 @@
"dist/nodes/memory/MemoryBufferWindow/MemoryBufferWindow.node.js",
"dist/nodes/memory/MemoryMotorhead/MemoryMotorhead.node.js",
"dist/nodes/memory/MemoryRedisChat/MemoryRedisChat.node.js",
"dist/nodes/memory/MemoryManager/MemoryManager.node.js",
"dist/nodes/memory/MemoryChatRetriever/MemoryChatRetriever.node.js",
"dist/nodes/memory/MemoryXata/MemoryXata.node.js",
"dist/nodes/memory/MemoryZep/MemoryZep.node.js",