first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
import type { BufferWindowMemoryInput } from '@langchain/classic/memory';
|
||||
import { BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import {
|
||||
sessionIdOption,
|
||||
sessionKeyProperty,
|
||||
contextWindowLengthProperty,
|
||||
expressionSessionKeyProperty,
|
||||
} from '../descriptions';
|
||||
|
||||
class MemoryChatBufferSingleton {
|
||||
private static instance: MemoryChatBufferSingleton;
|
||||
|
||||
private memoryBuffer: Map<
|
||||
string,
|
||||
{ buffer: BufferWindowMemory; created: Date; last_accessed: Date }
|
||||
>;
|
||||
|
||||
private constructor() {
|
||||
this.memoryBuffer = new Map();
|
||||
}
|
||||
|
||||
static getInstance(): MemoryChatBufferSingleton {
|
||||
if (!MemoryChatBufferSingleton.instance) {
|
||||
MemoryChatBufferSingleton.instance = new MemoryChatBufferSingleton();
|
||||
}
|
||||
return MemoryChatBufferSingleton.instance;
|
||||
}
|
||||
|
||||
async getMemory(
|
||||
sessionKey: string,
|
||||
memoryParams: BufferWindowMemoryInput,
|
||||
): Promise<BufferWindowMemory> {
|
||||
await this.cleanupStaleBuffers();
|
||||
|
||||
let memoryInstance = this.memoryBuffer.get(sessionKey);
|
||||
if (memoryInstance) {
|
||||
memoryInstance.last_accessed = new Date();
|
||||
} else {
|
||||
const newMemory = new BufferWindowMemory(memoryParams);
|
||||
|
||||
memoryInstance = {
|
||||
buffer: newMemory,
|
||||
created: new Date(),
|
||||
last_accessed: new Date(),
|
||||
};
|
||||
this.memoryBuffer.set(sessionKey, memoryInstance);
|
||||
}
|
||||
return memoryInstance.buffer;
|
||||
}
|
||||
|
||||
private async cleanupStaleBuffers(): Promise<void> {
|
||||
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
||||
|
||||
for (const [key, memoryInstance] of this.memoryBuffer.entries()) {
|
||||
if (memoryInstance.last_accessed < oneHourAgo) {
|
||||
await this.memoryBuffer.get(key)?.buffer.clear();
|
||||
this.memoryBuffer.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryBufferWindow implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Simple Memory',
|
||||
name: 'memoryBufferWindow',
|
||||
icon: 'fa:database',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'Stores in n8n memory, so no credentials required',
|
||||
defaults: {
|
||||
name: 'Simple Memory',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['For beginners'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorybufferwindow/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
builderHint: {
|
||||
message:
|
||||
'Reuse with multiple agents in the same workflow by connecting to multiple agent nodes so agents have a shared context.',
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
{
|
||||
displayName:
|
||||
'This node stores memory locally in the n8n instance. It is not compatible with Queue Mode or Multi-Main setups, as memory will not be shared across workers. For production use with scaling, consider using an external memory store such as Redis, Postgres, or another persistent memory node.',
|
||||
name: 'scalingNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Session Key',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: 'chat_history',
|
||||
description: 'The key to use to store the memory in the workflow data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...sessionIdOption,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
expressionSessionKeyProperty(1.3),
|
||||
sessionKeyProperty,
|
||||
contextWindowLengthProperty,
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const contextWindowLength = this.getNodeParameter('contextWindowLength', itemIndex) as number;
|
||||
const workflowId = this.getWorkflow().id;
|
||||
const memoryInstance = MemoryChatBufferSingleton.getInstance();
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
let sessionId;
|
||||
|
||||
if (nodeVersion >= 1.2) {
|
||||
sessionId = getSessionId(this, itemIndex);
|
||||
} else {
|
||||
sessionId = this.getNodeParameter('sessionKey', itemIndex) as string;
|
||||
}
|
||||
|
||||
const memory = await memoryInstance.getMemory(`${workflowId}__${sessionId}`, {
|
||||
k: contextWindowLength,
|
||||
inputKey: 'input',
|
||||
memoryKey: 'chat_history',
|
||||
outputKey: 'output',
|
||||
returnMessages: true,
|
||||
});
|
||||
|
||||
return {
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
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 {
|
||||
json: simplified,
|
||||
};
|
||||
});
|
||||
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',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
hidden: true,
|
||||
version: 1,
|
||||
description: 'Retrieve chat messages from memory and use them in the workflow',
|
||||
defaults: {
|
||||
name: 'Chat Messages Retriever',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Miscellaneous'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorymanager/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [
|
||||
NodeConnectionTypes.Main,
|
||||
{
|
||||
displayName: 'Memory',
|
||||
maxConnections: 1,
|
||||
type: NodeConnectionTypes.AiMemory,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: "This node is deprecated. Use 'Chat Memory Manager' node instead.",
|
||||
type: 'notice',
|
||||
default: '',
|
||||
name: 'deprecatedNotice',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Output',
|
||||
name: 'simplifyOutput',
|
||||
type: 'boolean',
|
||||
description: 'Whether to simplify the output to only include the sender and the text',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
this.logger.debug('Executing Chat Memory Retriever');
|
||||
|
||||
const memory = (await this.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
|
||||
| BaseChatMemory
|
||||
| undefined;
|
||||
const simplifyOutput = this.getNodeParameter('simplifyOutput', 0) as boolean;
|
||||
|
||||
const messages = await memory?.chatHistory.getMessages();
|
||||
|
||||
if (simplifyOutput && messages) {
|
||||
return [simplifyMessages(messages)];
|
||||
}
|
||||
|
||||
const serializedMessages =
|
||||
messages?.map((message) => {
|
||||
const serializedMessage = message.toJSON();
|
||||
return { json: serializedMessage as unknown as IDataObject };
|
||||
}) ?? [];
|
||||
|
||||
return [serializedMessages];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
||||
import type { MessageContent, BaseMessage } from '@langchain/core/messages';
|
||||
import { AIMessage, SystemMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
type MessageRole = 'ai' | 'system' | 'user';
|
||||
interface MessageRecord {
|
||||
type: MessageRole;
|
||||
message: string;
|
||||
hideFromUI: boolean;
|
||||
}
|
||||
|
||||
export function simplifyMessages(messages: BaseMessage[]): Array<Record<string, MessageContent>> {
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
const result: Array<Record<string, MessageContent>> = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < messages.length) {
|
||||
const currentGroup: Record<string, MessageContent> = {};
|
||||
|
||||
do {
|
||||
const message = messages[index];
|
||||
const messageType = message.getType();
|
||||
|
||||
if (messageType in currentGroup) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentGroup[messageType] = message.content;
|
||||
index++;
|
||||
} while (index < messages.length);
|
||||
|
||||
result.push(currentGroup);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const prepareOutputSetup = (ctx: IExecuteFunctions, version: number, memory: BaseChatMemory) => {
|
||||
if (version === 1) {
|
||||
//legacy behavior of insert and delete for version 1
|
||||
return async (i: number) => {
|
||||
const messages = await memory.chatHistory.getMessages();
|
||||
|
||||
const serializedMessages = messages?.map((message) => message.toJSON()) ?? [];
|
||||
|
||||
const executionData = ctx.helpers.constructExecutionMetaData(
|
||||
ctx.helpers.returnJsonArray(serializedMessages as unknown as IDataObject[]),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
};
|
||||
}
|
||||
return async (i: number) => {
|
||||
return [
|
||||
{
|
||||
json: { success: true },
|
||||
pairedItem: { item: i },
|
||||
},
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
export class MemoryManager implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Chat Memory Manager',
|
||||
name: 'memoryManager',
|
||||
icon: 'fa:database',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1],
|
||||
description: 'Manage chat messages memory and use it in the workflow',
|
||||
defaults: {
|
||||
name: 'Chat Memory Manager',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Miscellaneous', 'Root Nodes'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorymanager/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [
|
||||
{
|
||||
displayName: '',
|
||||
type: NodeConnectionTypes.Main,
|
||||
},
|
||||
{
|
||||
displayName: 'Memory',
|
||||
type: NodeConnectionTypes.AiMemory,
|
||||
required: true,
|
||||
maxConnections: 1,
|
||||
},
|
||||
],
|
||||
|
||||
outputs: [
|
||||
{
|
||||
displayName: '',
|
||||
type: NodeConnectionTypes.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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Group Messages',
|
||||
name: 'groupMessages',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to group messages into a single item or return each message as a separate item',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['load'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
const items = this.getInputData();
|
||||
const mode = this.getNodeParameter('mode', 0, 'load') as 'load' | 'insert' | 'delete';
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const memory = (await this.getInputConnectionData(
|
||||
NodeConnectionTypes.AiMemory,
|
||||
i,
|
||||
)) as BaseChatMemory;
|
||||
|
||||
const prepareOutput = prepareOutputSetup(this, nodeVersion, memory);
|
||||
const 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();
|
||||
}
|
||||
|
||||
returnData.push(...(await prepareOutput(i)));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
returnData.push(...(await prepareOutput(i)));
|
||||
}
|
||||
|
||||
if (mode === 'load') {
|
||||
const simplifyOutput = this.getNodeParameter('simplifyOutput', i, false) as boolean;
|
||||
const options = this.getNodeParameter('options', i);
|
||||
|
||||
//Load mode, legacy behavior for version 1, buggy - outputs only for single input item
|
||||
if (simplifyOutput && messages.length && nodeVersion === 1) {
|
||||
const groupMessages = options.groupMessages as boolean;
|
||||
const output = simplifyMessages(messages);
|
||||
|
||||
return [
|
||||
this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(
|
||||
groupMessages ? [{ messages: output, messagesCount: output.length }] : output,
|
||||
),
|
||||
{ itemData: { item: i } },
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
let groupMessages = true;
|
||||
//disable grouping if explicitly set to false
|
||||
if (options.groupMessages === false) {
|
||||
groupMessages = false;
|
||||
}
|
||||
//disable grouping if not set and node version is 1 (legacy behavior)
|
||||
if (options.groupMessages === undefined && nodeVersion === 1) {
|
||||
groupMessages = false;
|
||||
}
|
||||
|
||||
let output: IDataObject[] =
|
||||
(simplifyOutput
|
||||
? simplifyMessages(messages)
|
||||
: (messages?.map((message) => message.toJSON()) as unknown as IDataObject[])) ?? [];
|
||||
|
||||
if (groupMessages) {
|
||||
output = [{ messages: output, messagesCount: output.length }];
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(output),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/unbound-method, @typescript-eslint/no-explicit-any */
|
||||
import type { BaseChatMemory } from '@langchain/community/memory/chat_memory';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { SystemMessage } from '@langchain/core/messages';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
|
||||
import { MemoryManager } from '../MemoryManager.node';
|
||||
|
||||
/**
|
||||
* These tests verify that the Memory Manager resolves sub-node expressions
|
||||
* (e.g. Session ID) per input item, not just once for item 0.
|
||||
*
|
||||
* See: https://github.com/n8n-io/n8n/issues/23890
|
||||
*/
|
||||
|
||||
interface MockMemory {
|
||||
memory: BaseChatMemory;
|
||||
getMessages: jest.Mock;
|
||||
addMessage: jest.Mock;
|
||||
clear: jest.Mock;
|
||||
}
|
||||
|
||||
function createMockMemory(messages: BaseMessage[] = []): MockMemory {
|
||||
const getMessages = jest.fn().mockResolvedValue([...messages]);
|
||||
const addMessage = jest.fn().mockResolvedValue(undefined);
|
||||
const clear = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const memory = { chatHistory: { getMessages, addMessage, clear } } as unknown as BaseChatMemory;
|
||||
|
||||
return { memory, getMessages, addMessage, clear };
|
||||
}
|
||||
|
||||
function createMockContext() {
|
||||
const mockHelpers = mock<IExecuteFunctions['helpers']>();
|
||||
(mockHelpers.constructExecutionMetaData as any).mockImplementation(
|
||||
(items: INodeExecutionData[], meta: { itemData: any }) =>
|
||||
items.map((item) => ({ ...item, pairedItem: meta.itemData })),
|
||||
);
|
||||
mockHelpers.returnJsonArray.mockImplementation(
|
||||
(data) =>
|
||||
(Array.isArray(data) ? data : [data]).map((d) => ({
|
||||
json: d,
|
||||
})) as INodeExecutionData[],
|
||||
);
|
||||
|
||||
const ctx = mock<IExecuteFunctions>({ helpers: mockHelpers });
|
||||
ctx.getNode.mockReturnValue({
|
||||
name: 'Chat Memory Manager',
|
||||
typeVersion: 1.1,
|
||||
parameters: {},
|
||||
} as INode);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
describe('MemoryManager.execute - multi-item session context', () => {
|
||||
let node: MemoryManager;
|
||||
|
||||
beforeEach(() => {
|
||||
node = new MemoryManager();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('insert mode', () => {
|
||||
it('should request memory with correct itemIndex for each input item', async () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
ctx.getInputData.mockReturnValue([
|
||||
{ json: { name: 'First item', code: 1 } },
|
||||
{ json: { name: 'Second item', code: 2 } },
|
||||
{ json: { name: 'Third item', code: 3 } },
|
||||
]);
|
||||
|
||||
ctx.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'mode') return 'insert';
|
||||
if (param === 'insertMode') return 'insert';
|
||||
if (param === 'messages.messageValues')
|
||||
return [{ type: 'system', message: 'test', hideFromUI: false }];
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const memories = [createMockMemory(), createMockMemory(), createMockMemory()];
|
||||
ctx.getInputConnectionData
|
||||
.mockResolvedValueOnce(memories[0].memory)
|
||||
.mockResolvedValueOnce(memories[1].memory)
|
||||
.mockResolvedValueOnce(memories[2].memory);
|
||||
|
||||
await node.execute.call(ctx);
|
||||
|
||||
expect(ctx.getInputConnectionData).toHaveBeenCalledTimes(3);
|
||||
expect(ctx.getInputConnectionData).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
NodeConnectionTypes.AiMemory,
|
||||
0,
|
||||
);
|
||||
expect(ctx.getInputConnectionData).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
NodeConnectionTypes.AiMemory,
|
||||
1,
|
||||
);
|
||||
expect(ctx.getInputConnectionData).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
NodeConnectionTypes.AiMemory,
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
it('should insert messages into separate sessions for each item', async () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
ctx.getInputData.mockReturnValue([
|
||||
{ json: { name: 'First item', code: 1 } },
|
||||
{ json: { name: 'Second item', code: 2 } },
|
||||
]);
|
||||
|
||||
ctx.getNodeParameter.mockImplementation((param, i, defaultValue) => {
|
||||
if (param === 'mode') return 'insert';
|
||||
if (param === 'insertMode') return 'insert';
|
||||
if (param === 'messages.messageValues')
|
||||
return [{ type: 'system', message: `Message for item ${i}`, hideFromUI: false }];
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mock1 = createMockMemory();
|
||||
const mock2 = createMockMemory();
|
||||
|
||||
ctx.getInputConnectionData
|
||||
.mockResolvedValueOnce(mock1.memory)
|
||||
.mockResolvedValueOnce(mock2.memory);
|
||||
|
||||
await node.execute.call(ctx);
|
||||
|
||||
expect(mock1.addMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mock2.addMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not cross-contaminate sessions in override mode with multiple items', async () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
ctx.getInputData.mockReturnValue([
|
||||
{ json: { name: 'First item', code: 1 } },
|
||||
{ json: { name: 'Second item', code: 2 } },
|
||||
]);
|
||||
|
||||
ctx.getNodeParameter.mockImplementation((param, i, defaultValue) => {
|
||||
if (param === 'mode') return 'insert';
|
||||
if (param === 'insertMode') return 'override';
|
||||
if (param === 'messages.messageValues') {
|
||||
const names = ['First item', 'Second item'];
|
||||
return [{ type: 'system', message: names[i], hideFromUI: false }];
|
||||
}
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mock1 = createMockMemory();
|
||||
const mock2 = createMockMemory();
|
||||
|
||||
ctx.getInputConnectionData
|
||||
.mockResolvedValueOnce(mock1.memory)
|
||||
.mockResolvedValueOnce(mock2.memory);
|
||||
|
||||
await node.execute.call(ctx);
|
||||
|
||||
// Each memory should be cleared and written to exactly once
|
||||
expect(mock1.clear).toHaveBeenCalledTimes(1);
|
||||
expect(mock2.clear).toHaveBeenCalledTimes(1);
|
||||
expect(mock1.addMessage).toHaveBeenCalledTimes(1);
|
||||
expect(mock2.addMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('load mode', () => {
|
||||
it('should load messages from the correct session for each item', async () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
ctx.getInputData.mockReturnValue([
|
||||
{ json: { sessionId: 'session-A' } },
|
||||
{ json: { sessionId: 'session-B' } },
|
||||
]);
|
||||
|
||||
ctx.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'mode') return 'load';
|
||||
if (param === 'simplifyOutput') return true;
|
||||
if (param === 'options') return { groupMessages: false };
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mockA = createMockMemory([new SystemMessage('Message from session A')]);
|
||||
const mockB = createMockMemory([new SystemMessage('Message from session B')]);
|
||||
|
||||
ctx.getInputConnectionData
|
||||
.mockResolvedValueOnce(mockA.memory)
|
||||
.mockResolvedValueOnce(mockB.memory);
|
||||
|
||||
const result = await node.execute.call(ctx);
|
||||
|
||||
expect(result[0]).toHaveLength(2);
|
||||
|
||||
const item0Message = result[0][0].json;
|
||||
const item1Message = result[0][1].json;
|
||||
|
||||
expect(item0Message).toHaveProperty('system', 'Message from session A');
|
||||
expect(item1Message).toHaveProperty('system', 'Message from session B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete mode', () => {
|
||||
it('should delete messages from the correct session for each item', async () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
ctx.getInputData.mockReturnValue([{ json: { code: 1 } }, { json: { code: 2 } }]);
|
||||
|
||||
ctx.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'mode') return 'delete';
|
||||
if (param === 'deleteMode') return 'all';
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mock1 = createMockMemory([new SystemMessage('Session 1 msg')]);
|
||||
const mock2 = createMockMemory([new SystemMessage('Session 2 msg')]);
|
||||
|
||||
ctx.getInputConnectionData
|
||||
.mockResolvedValueOnce(mock1.memory)
|
||||
.mockResolvedValueOnce(mock2.memory);
|
||||
|
||||
await node.execute.call(ctx);
|
||||
|
||||
expect(mock1.clear).toHaveBeenCalledTimes(1);
|
||||
expect(mock2.clear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should delete last N messages from the correct session for each item', async () => {
|
||||
const ctx = createMockContext();
|
||||
|
||||
ctx.getInputData.mockReturnValue([{ json: { code: 1 } }, { json: { code: 2 } }]);
|
||||
|
||||
ctx.getNodeParameter.mockImplementation((param, _i, defaultValue) => {
|
||||
if (param === 'mode') return 'delete';
|
||||
if (param === 'deleteMode') return 'lastN';
|
||||
if (param === 'lastMessagesCount') return 1;
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const mock1 = createMockMemory([
|
||||
new SystemMessage('Session 1 keep'),
|
||||
new SystemMessage('Session 1 delete'),
|
||||
]);
|
||||
const mock2 = createMockMemory([
|
||||
new SystemMessage('Session 2 keep'),
|
||||
new SystemMessage('Session 2 delete'),
|
||||
]);
|
||||
|
||||
ctx.getInputConnectionData
|
||||
.mockResolvedValueOnce(mock1.memory)
|
||||
.mockResolvedValueOnce(mock2.memory);
|
||||
|
||||
await node.execute.call(ctx);
|
||||
|
||||
expect(mock1.clear).toHaveBeenCalledTimes(1);
|
||||
expect(mock2.clear).toHaveBeenCalledTimes(1);
|
||||
|
||||
// After deleting last 1 message, each memory should have its "keep" message re-added
|
||||
expect(mock1.addMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: 'Session 1 keep' }),
|
||||
);
|
||||
expect(mock2.addMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: 'Session 2 keep' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { AIMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
|
||||
import { simplifyMessages } from '../MemoryManager.node';
|
||||
|
||||
describe('simplifyMessages', () => {
|
||||
it('should handle single message', () => {
|
||||
const messages = [new HumanMessage('Hello')];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([{ human: 'Hello' }]);
|
||||
});
|
||||
|
||||
it('should group different message types together', () => {
|
||||
const messages = [
|
||||
new HumanMessage('Hello, how are you?'),
|
||||
new AIMessage("I'm doing well, thank you for asking! How about you?"),
|
||||
];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
human: 'Hello, how are you?',
|
||||
ai: "I'm doing well, thank you for asking! How about you?",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should separate consecutive messages of same type into different groups', () => {
|
||||
const messages = [
|
||||
new HumanMessage('First human message'),
|
||||
new HumanMessage('Second human message'),
|
||||
new AIMessage('AI response'),
|
||||
];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([
|
||||
{ human: 'First human message' },
|
||||
{ human: 'Second human message', ai: 'AI response' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle three consecutive messages of same type', () => {
|
||||
const messages = [new HumanMessage('1'), new HumanMessage('2'), new HumanMessage('3')];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([{ human: '1' }, { human: '2' }, { human: '3' }]);
|
||||
});
|
||||
|
||||
it('should handle mixed message types with grouping', () => {
|
||||
const messages = [
|
||||
new SystemMessage('System message'),
|
||||
new HumanMessage('Hello'),
|
||||
new AIMessage('Hi there'),
|
||||
new HumanMessage('Another human message'),
|
||||
new AIMessage('Another AI message'),
|
||||
];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
system: 'System message',
|
||||
human: 'Hello',
|
||||
ai: 'Hi there',
|
||||
},
|
||||
{
|
||||
human: 'Another human message',
|
||||
ai: 'Another AI message',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle system messages correctly', () => {
|
||||
const messages = [
|
||||
new SystemMessage('System instruction'),
|
||||
new HumanMessage('User question'),
|
||||
new AIMessage('AI response'),
|
||||
];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
system: 'System instruction',
|
||||
human: 'User question',
|
||||
ai: 'AI response',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle alternating same types correctly', () => {
|
||||
const messages = [
|
||||
new HumanMessage('Human 1'),
|
||||
new AIMessage('AI 1'),
|
||||
new HumanMessage('Human 2'),
|
||||
new AIMessage('AI 2'),
|
||||
];
|
||||
const result = simplifyMessages(messages);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
human: 'Human 1',
|
||||
ai: 'AI 1',
|
||||
},
|
||||
{
|
||||
human: 'Human 2',
|
||||
ai: 'AI 2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { MongoDBChatMessageHistory } from '@langchain/mongodb';
|
||||
import { BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import { MongoClient } from 'mongodb';
|
||||
import type {
|
||||
ISupplyDataFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import {
|
||||
sessionIdOption,
|
||||
sessionKeyProperty,
|
||||
expressionSessionKeyProperty,
|
||||
contextWindowLengthProperty,
|
||||
} from '../descriptions';
|
||||
|
||||
export class MemoryMongoDbChat implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'MongoDB Chat Memory',
|
||||
name: 'memoryMongoDbChat',
|
||||
icon: 'file:mongodb.svg',
|
||||
group: ['transform'],
|
||||
version: [1],
|
||||
description: 'Stores the chat history in MongoDB collection.',
|
||||
defaults: {
|
||||
name: 'MongoDB Chat Memory',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'mongoDb',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['Other memories'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorymongochat/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
sessionIdOption,
|
||||
expressionSessionKeyProperty(1),
|
||||
sessionKeyProperty,
|
||||
{
|
||||
displayName: 'Collection Name',
|
||||
name: 'collectionName',
|
||||
type: 'string',
|
||||
default: 'n8n_chat_histories',
|
||||
description:
|
||||
'The collection name to store the chat history in. If collection does not exist, it will be created.',
|
||||
},
|
||||
{
|
||||
displayName: 'Database Name',
|
||||
name: 'databaseName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The database name to store the chat history in. If not provided, the database from credentials will be used.',
|
||||
},
|
||||
contextWindowLengthProperty,
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials<{
|
||||
configurationType: string;
|
||||
connectionString: string;
|
||||
database: string;
|
||||
host: string;
|
||||
user: string;
|
||||
port: number;
|
||||
password: string;
|
||||
tls: boolean;
|
||||
}>('mongoDb');
|
||||
const collectionName = this.getNodeParameter(
|
||||
'collectionName',
|
||||
itemIndex,
|
||||
'n8n_chat_histories',
|
||||
) as string;
|
||||
const databaseName = this.getNodeParameter('databaseName', itemIndex, '') as string;
|
||||
const sessionId = getSessionId(this, itemIndex);
|
||||
|
||||
let connectionString: string;
|
||||
let dbName: string;
|
||||
|
||||
if (credentials.configurationType === 'connectionString') {
|
||||
connectionString = credentials.connectionString;
|
||||
dbName = databaseName || credentials.database;
|
||||
} else {
|
||||
// Build connection string from individual fields
|
||||
const host = credentials.host;
|
||||
const port = credentials.port;
|
||||
const user = credentials.user ? encodeURIComponent(credentials.user) : '';
|
||||
const password = credentials.password ? encodeURIComponent(credentials.password) : '';
|
||||
const authString = user && password ? `${user}:${password}@` : '';
|
||||
const tls = credentials.tls;
|
||||
|
||||
connectionString = `mongodb://${authString}${host}:${port}/?appname=n8n`;
|
||||
if (tls) {
|
||||
connectionString += '&ssl=true';
|
||||
}
|
||||
|
||||
dbName = databaseName || credentials.database;
|
||||
}
|
||||
|
||||
if (!dbName) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Database name must be provided either in credentials or in node parameters',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new MongoClient(connectionString);
|
||||
await client.connect();
|
||||
|
||||
const db = client.db(dbName);
|
||||
const collection = db.collection(collectionName);
|
||||
|
||||
const mongoDBChatHistory = new MongoDBChatMessageHistory({
|
||||
collection,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
const memory = new BufferWindowMemory({
|
||||
memoryKey: 'chat_history',
|
||||
chatHistory: mongoDBChatHistory,
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
k: this.getNodeParameter('contextWindowLength', itemIndex, 5) as number,
|
||||
});
|
||||
|
||||
async function closeFunction() {
|
||||
await client.close();
|
||||
}
|
||||
|
||||
return {
|
||||
closeFunction,
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(this.getNode(), `MongoDB connection error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="120" height="258" viewBox="0 0 120 258" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M83.0089 28.7559C72.1328 15.9086 62.7673 2.86053 60.8539 0.150554C60.6525 -0.0501848 60.3503 -0.0501848 60.1489 0.150554C58.2355 2.86053 48.8699 15.9086 37.9938 28.7559C-55.3594 147.292 52.6968 227.287 52.6968 227.287L53.6031 227.889C54.4087 240.235 56.4228 258 56.4228 258H60.451H64.4792C64.4792 258 66.4934 240.335 67.299 227.889L68.2052 227.187C68.306 227.187 176.362 147.292 83.0089 28.7559ZM60.451 225.48C60.451 225.48 55.6172 221.365 54.3081 219.257V219.057L60.1489 89.9813C60.1489 89.5798 60.7532 89.5798 60.7532 89.9813L66.594 219.057V219.257C65.2848 221.365 60.451 225.48 60.451 225.48Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 728 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="120" height="258" viewBox="0 0 120 258" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M83.0089 28.7559C72.1328 15.9086 62.7673 2.86053 60.8539 0.150554C60.6525 -0.0501848 60.3503 -0.0501848 60.1489 0.150554C58.2355 2.86053 48.8699 15.9086 37.9938 28.7559C-55.3594 147.292 52.6968 227.287 52.6968 227.287L53.6031 227.889C54.4087 240.235 56.4228 258 56.4228 258H60.451H64.4792C64.4792 258 66.4934 240.335 67.299 227.889L68.2052 227.187C68.306 227.187 176.362 147.292 83.0089 28.7559ZM60.451 225.48C60.451 225.48 55.6172 221.365 54.3081 219.257V219.057L60.1489 89.9813C60.1489 89.5798 60.7532 89.5798 60.7532 89.9813L66.594 219.057V219.257C65.2848 221.365 60.451 225.48 60.451 225.48Z" fill="#00684A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 730 B |
@@ -0,0 +1,128 @@
|
||||
import { MotorheadMemory } from '@langchain/community/memory/motorhead_memory';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import { expressionSessionKeyProperty, sessionIdOption, sessionKeyProperty } from '../descriptions';
|
||||
|
||||
export class MemoryMotorhead implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Motorhead',
|
||||
name: 'memoryMotorhead',
|
||||
icon: 'fa:file-export',
|
||||
iconColor: 'black',
|
||||
hidden: true,
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'Use Motorhead Memory',
|
||||
defaults: {
|
||||
name: 'Motorhead',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['Other memories'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorymotorhead/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'motorheadApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
{
|
||||
displayName:
|
||||
'The Motorhead project is no longer maintained. This node is deprecated and will be removed in a future version.',
|
||||
name: 'deprecationNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...sessionIdOption,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
expressionSessionKeyProperty(1.3),
|
||||
sessionKeyProperty,
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials('motorheadApi');
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
let sessionId;
|
||||
|
||||
if (nodeVersion >= 1.2) {
|
||||
sessionId = getSessionId(this, itemIndex);
|
||||
} else {
|
||||
sessionId = this.getNodeParameter('sessionId', itemIndex) as string;
|
||||
}
|
||||
|
||||
const memory = new MotorheadMemory({
|
||||
sessionId,
|
||||
url: `${credentials.host as string}/motorhead`,
|
||||
clientId: credentials.clientId as string,
|
||||
apiKey: credentials.apiKey as string,
|
||||
memoryKey: 'chat_history',
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
});
|
||||
|
||||
await memory.init();
|
||||
|
||||
return {
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { PostgresChatMessageHistory } from '@langchain/community/stores/message/postgres';
|
||||
import { BufferMemory, BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import { configurePostgres } from 'n8n-nodes-base/dist/nodes/Postgres/transport/index';
|
||||
import type { PostgresNodeCredentials } from 'n8n-nodes-base/dist/nodes/Postgres/v2/helpers/interfaces';
|
||||
import { postgresConnectionTest } from 'n8n-nodes-base/dist/nodes/Postgres/v2/methods/credentialTest';
|
||||
import type {
|
||||
ISupplyDataFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type pg from 'pg';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import {
|
||||
sessionIdOption,
|
||||
sessionKeyProperty,
|
||||
contextWindowLengthProperty,
|
||||
expressionSessionKeyProperty,
|
||||
} from '../descriptions';
|
||||
|
||||
export class MemoryPostgresChat implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Postgres Chat Memory',
|
||||
name: 'memoryPostgresChat',
|
||||
icon: 'file:postgres.svg',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'Stores the chat history in Postgres table.',
|
||||
defaults: {
|
||||
name: 'Postgres Chat Memory',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'postgres',
|
||||
required: true,
|
||||
testedBy: 'postgresConnectionTest',
|
||||
},
|
||||
],
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['Other memories'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memorypostgreschat/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
sessionIdOption,
|
||||
expressionSessionKeyProperty(1.2),
|
||||
sessionKeyProperty,
|
||||
{
|
||||
displayName: 'Table Name',
|
||||
name: 'tableName',
|
||||
type: 'string',
|
||||
default: 'n8n_chat_histories',
|
||||
description:
|
||||
'The table name to store the chat history in. If table does not exist, it will be created.',
|
||||
},
|
||||
{
|
||||
...contextWindowLengthProperty,
|
||||
displayOptions: { hide: { '@version': [{ _cnd: { lt: 1.1 } }] } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
postgresConnectionTest,
|
||||
},
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials<PostgresNodeCredentials>('postgres');
|
||||
const tableName = this.getNodeParameter('tableName', itemIndex, 'n8n_chat_histories') as string;
|
||||
const sessionId = getSessionId(this, itemIndex);
|
||||
|
||||
const pgConf = await configurePostgres.call(this, credentials);
|
||||
const pool = pgConf.db.$pool as unknown as pg.Pool;
|
||||
|
||||
const pgChatHistory = new PostgresChatMessageHistory({
|
||||
pool,
|
||||
sessionId,
|
||||
tableName,
|
||||
});
|
||||
|
||||
const memClass = this.getNode().typeVersion < 1.1 ? BufferMemory : BufferWindowMemory;
|
||||
const kOptions =
|
||||
this.getNode().typeVersion < 1.1
|
||||
? {}
|
||||
: { k: this.getNodeParameter('contextWindowLength', itemIndex) };
|
||||
|
||||
const memory = new memClass({
|
||||
memoryKey: 'chat_history',
|
||||
chatHistory: pgChatHistory,
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
...kOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,185 @@
|
||||
import type { RedisChatMessageHistoryInput } from '@langchain/redis';
|
||||
import { RedisChatMessageHistory } from '@langchain/redis';
|
||||
import { BufferMemory, BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import {
|
||||
NodeOperationError,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
NodeConnectionTypes,
|
||||
} from 'n8n-workflow';
|
||||
import type { RedisClientOptions } from 'redis';
|
||||
import { createClient } from 'redis';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import {
|
||||
sessionIdOption,
|
||||
sessionKeyProperty,
|
||||
contextWindowLengthProperty,
|
||||
expressionSessionKeyProperty,
|
||||
} from '../descriptions';
|
||||
|
||||
export class MemoryRedisChat implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Redis Chat Memory',
|
||||
name: 'memoryRedisChat',
|
||||
icon: 'file:redis.svg',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5],
|
||||
description: 'Stores the chat history in Redis.',
|
||||
defaults: {
|
||||
name: 'Redis Chat Memory',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'redis',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['Other memories'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memoryredischat/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
{
|
||||
displayName: 'Session Key',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: 'chat_history',
|
||||
description: 'The key to use to store the memory in the workflow data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...sessionIdOption,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
expressionSessionKeyProperty(1.4),
|
||||
sessionKeyProperty,
|
||||
{
|
||||
displayName: 'Session Time To Live',
|
||||
name: 'sessionTTL',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description:
|
||||
'For how long the session should be stored in seconds. If set to 0 it will not expire.',
|
||||
},
|
||||
{
|
||||
...contextWindowLengthProperty,
|
||||
displayOptions: { hide: { '@version': [{ _cnd: { lt: 1.3 } }] } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials('redis');
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
const sessionTTL = this.getNodeParameter('sessionTTL', itemIndex, 0) as number;
|
||||
|
||||
let sessionId;
|
||||
|
||||
if (nodeVersion >= 1.2) {
|
||||
sessionId = getSessionId(this, itemIndex);
|
||||
} else {
|
||||
sessionId = this.getNodeParameter('sessionKey', itemIndex) as string;
|
||||
}
|
||||
|
||||
const redisOptions: RedisClientOptions = {
|
||||
socket: {
|
||||
host: credentials.host as string,
|
||||
port: credentials.port as number,
|
||||
tls: credentials.ssl === true,
|
||||
},
|
||||
database: credentials.database as number,
|
||||
};
|
||||
|
||||
if (credentials.user && nodeVersion >= 1.5) {
|
||||
redisOptions.username = credentials.user as string;
|
||||
}
|
||||
if (credentials.password) {
|
||||
redisOptions.password = credentials.password as string;
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
...redisOptions,
|
||||
});
|
||||
|
||||
client.on('error', async (error: Error) => {
|
||||
await client.quit();
|
||||
throw new NodeOperationError(this.getNode(), 'Redis Error: ' + error.message);
|
||||
});
|
||||
|
||||
const redisChatConfig: RedisChatMessageHistoryInput = {
|
||||
client,
|
||||
sessionId,
|
||||
};
|
||||
|
||||
if (sessionTTL > 0) {
|
||||
redisChatConfig.sessionTTL = sessionTTL;
|
||||
}
|
||||
const redisChatHistory = new RedisChatMessageHistory(redisChatConfig);
|
||||
|
||||
const memClass = this.getNode().typeVersion < 1.3 ? BufferMemory : BufferWindowMemory;
|
||||
const kOptions =
|
||||
this.getNode().typeVersion < 1.3
|
||||
? {}
|
||||
: { k: this.getNodeParameter('contextWindowLength', itemIndex) };
|
||||
|
||||
const memory = new memClass({
|
||||
memoryKey: 'chat_history',
|
||||
chatHistory: redisChatHistory,
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
...kOptions,
|
||||
});
|
||||
|
||||
async function closeFunction() {
|
||||
void client.disconnect();
|
||||
}
|
||||
|
||||
return {
|
||||
closeFunction,
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><g fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path fill="#A41E11" d="M57.656 43.99c-3.201 1.683-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.494c-2.79-1.343-20.449-8.535-23.629-10.067C.834 44.066.002 43.422.002 42.811v-6.117s22.98-5.045 26.69-6.388 4.995-1.39 8.154-.225c3.16 1.165 22.035 4.603 25.154 5.756v6.032c0 .605-.72 1.283-2.35 2.124z"/><path fill="#D82C20" d="M57.656 37.872c-3.201 1.685-19.787 8.56-23.318 10.417s-5.494 1.838-8.283.494c-2.79-1.343-20.449-8.534-23.63-10.068s-3.243-2.588-.122-3.82l24.388-9.52c3.71-1.34 4.994-1.39 8.153-.225s19.643 7.78 22.747 8.951c3.103 1.17 3.24 2.086.037 3.786z"/><path fill="#A41E11" d="M57.656 34.015c-3.201 1.683-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.536-23.629-10.067C.834 34.092.002 33.447.002 32.836V26.72s22.98-5.045 26.69-6.387c3.711-1.343 4.995-1.39 8.154-.225 3.16 1.165 22.035 4.602 25.154 5.756v6.032c0 .605-.72 1.283-2.35 2.123z"/><path fill="#D82C20" d="M57.656 27.898c-3.201 1.685-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.534-23.63-10.067-3.18-1.534-3.243-2.588-.122-3.82l24.388-9.52c3.71-1.343 4.994-1.39 8.153-.225 3.16 1.166 19.644 7.785 22.765 8.935s3.24 2.085.038 3.785z"/><path fill="#A41E11" d="M57.656 23.671c-3.201 1.683-19.787 8.561-23.318 10.419s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.535-23.629-10.069-1.592-.765-2.424-1.411-2.424-2.02v-6.11s22.98-5.045 26.69-6.388 4.995-1.39 8.154-.225c3.16 1.165 22.035 4.591 25.154 5.745v6.032c0 .605-.72 1.283-2.35 2.123z"/><path fill="#D82C20" d="M57.656 17.553c-3.201 1.685-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.534-23.63-10.068s-3.243-2.587-.122-3.82l24.388-9.52c3.71-1.343 4.994-1.39 8.153-.226 3.16 1.165 19.643 7.785 22.765 8.936s3.24 2.085.038 3.785z"/><path fill="#FFF" d="m31.497 15.032-1.88-3.153-6.002-.545 4.48-1.63L26.75 7.2l4.192 1.653 3.955-1.305-1.07 2.586 4.032 1.524-5.198.546zm-10.014 6.275 13.903-2.153-4.2 6.211zm-11.17-5.167c0-1.61 3.314-2.906 7.431-2.906 4.118 0 7.432 1.296 7.432 2.906s-3.314 2.905-7.432 2.905c-4.117 0-7.431-1.295-7.431-2.905"/><path fill="#7A0C00" d="m52.233 15.714-8.224 3.276-.007-6.556z"/><path fill="#AD2115" d="m44.01 18.991-.89.353-8.217-3.276 9.094-3.63z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,158 @@
|
||||
import { XataChatMessageHistory } from '@langchain/community/stores/message/xata';
|
||||
import { BaseClient } from '@xata.io/client';
|
||||
import { BufferMemory, BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
import type {
|
||||
ISupplyDataFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import {
|
||||
sessionIdOption,
|
||||
sessionKeyProperty,
|
||||
contextWindowLengthProperty,
|
||||
expressionSessionKeyProperty,
|
||||
} from '../descriptions';
|
||||
|
||||
export class MemoryXata implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Xata',
|
||||
name: 'memoryXata',
|
||||
icon: 'file:xata.svg',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4],
|
||||
description: 'Use Xata Memory',
|
||||
defaults: {
|
||||
name: 'Xata',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-non-core-color-present
|
||||
color: '#1321A7',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['Other memories'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memoryxata/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'xataApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...sessionIdOption,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
sessionKeyProperty,
|
||||
expressionSessionKeyProperty(1.4),
|
||||
{
|
||||
...contextWindowLengthProperty,
|
||||
displayOptions: { hide: { '@version': [{ _cnd: { lt: 1.3 } }] } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials('xataApi');
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
let sessionId;
|
||||
|
||||
if (nodeVersion >= 1.2) {
|
||||
sessionId = getSessionId(this, itemIndex);
|
||||
} else {
|
||||
sessionId = this.getNodeParameter('sessionId', itemIndex) as string;
|
||||
}
|
||||
|
||||
const xataClient = new BaseClient({
|
||||
apiKey: credentials.apiKey as string,
|
||||
branch: (credentials.branch as string) || 'main',
|
||||
databaseURL: credentials.databaseEndpoint as string,
|
||||
});
|
||||
|
||||
const table = (credentials.databaseEndpoint as string).match(
|
||||
/https:\/\/[^.]+\.[^.]+\.xata\.sh\/db\/([^\/:]+)/,
|
||||
);
|
||||
|
||||
if (table === null) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'It was not possible to extract the table from the Database Endpoint.',
|
||||
);
|
||||
}
|
||||
|
||||
const chatHistory = new XataChatMessageHistory({
|
||||
table: table[1],
|
||||
sessionId,
|
||||
client: xataClient,
|
||||
apiKey: credentials.apiKey as string,
|
||||
});
|
||||
|
||||
const memClass = this.getNode().typeVersion < 1.3 ? BufferMemory : BufferWindowMemory;
|
||||
const kOptions =
|
||||
this.getNode().typeVersion < 1.3
|
||||
? {}
|
||||
: { k: this.getNodeParameter('contextWindowLength', itemIndex) };
|
||||
|
||||
const memory = new memClass({
|
||||
chatHistory,
|
||||
memoryKey: 'chat_history',
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
...kOptions,
|
||||
});
|
||||
|
||||
return {
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1600" fill="none"><path fill="#7D7D87" d="M1250.12 576.498c-.11 89.997-36 176.267-99.79 239.83l-.01-.007-226.282 225.489c-7.841 7.82-20.58 7.84-27.927-.44-55.015-61.995-85.587-142.175-85.49-225.478.106-89.997 36-176.267 99.787-239.83l.007.007 206.745-206.014c18.63-18.569 49.12-18.702 64.92 2.324a339.1 339.1 0 0 1 68.04 204.119M440.552 817.702c-63.787-63.563-99.682-149.833-99.787-239.83-.087-74.03 24.048-145.594 68.035-204.119 15.803-21.026 46.294-20.893 64.928-2.324l206.741 206.016.006-.007c63.787 63.564 99.681 149.833 99.787 239.831.097 83.302-30.475 163.483-85.49 225.471-7.347 8.28-20.086 8.26-27.927.45L440.558 817.696zm701.268 403.488c-16.63 20.39-47.04 20.21-65.63 1.59l-127.698-127.84c-7.836-7.85-7.821-20.56.033-28.39l212.095-211.345c7.84-7.813 20.62-7.859 27.54.784 36.81 45.996 51.29 109.566 40.34 179.551-10.01 64.06-40.65 129.19-86.68 185.65m-627.124 2.97c-18.594 18.61-49.002 18.79-65.626-1.6-46.036-56.46-76.672-121.58-86.687-185.64-10.943-69.992 3.531-133.562 40.342-179.558 6.916-8.642 19.703-8.597 27.544-.784l212.092 211.352c7.854 7.82 7.868 20.54.033 28.38z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,172 @@
|
||||
import type { BaseChatMemory } from '@langchain/community/dist/memory/chat_memory';
|
||||
import { ZepMemory } from '@langchain/community/memory/zep';
|
||||
import { ZepCloudMemory } from '@langchain/community/memory/zep_cloud';
|
||||
import type { InputValues, MemoryVariables } from '@langchain/core/memory';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type ISupplyDataFunctions,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
type SupplyData,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getSessionId } from '@utils/helpers';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
import { expressionSessionKeyProperty, sessionIdOption, sessionKeyProperty } from '../descriptions';
|
||||
|
||||
// Extend ZepCloudMemory to trim white space in messages.
|
||||
class WhiteSpaceTrimmedZepCloudMemory extends ZepCloudMemory {
|
||||
override async loadMemoryVariables(values: InputValues): Promise<MemoryVariables> {
|
||||
const memoryVariables = await super.loadMemoryVariables(values);
|
||||
memoryVariables.chat_history = memoryVariables.chat_history.filter((m: BaseMessage) =>
|
||||
m.content.toString().trim(),
|
||||
);
|
||||
return memoryVariables;
|
||||
}
|
||||
}
|
||||
|
||||
export class MemoryZep implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Zep',
|
||||
name: 'memoryZep',
|
||||
hidden: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
|
||||
icon: 'file:zep.png',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'Use Zep Memory',
|
||||
defaults: {
|
||||
name: 'Zep',
|
||||
},
|
||||
codex: {
|
||||
categories: ['AI'],
|
||||
subcategories: {
|
||||
AI: ['Memory'],
|
||||
Memory: ['Other memories'],
|
||||
},
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.memoryzep/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
inputs: [],
|
||||
|
||||
outputs: [NodeConnectionTypes.AiMemory],
|
||||
outputNames: ['Memory'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'zepApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'This Zep integration is deprecated and will be removed in a future version.',
|
||||
name: 'deprecationNotice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
|
||||
{
|
||||
displayName: 'Only works with Zep Cloud and Community edition <= v0.27.2',
|
||||
name: 'supportedVersions',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...sessionIdOption,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
expressionSessionKeyProperty(1.3),
|
||||
sessionKeyProperty,
|
||||
],
|
||||
};
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials<{
|
||||
apiKey?: string;
|
||||
apiUrl?: string;
|
||||
cloud?: boolean;
|
||||
}>('zepApi');
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
let sessionId;
|
||||
|
||||
if (nodeVersion >= 1.2) {
|
||||
sessionId = getSessionId(this, itemIndex);
|
||||
} else {
|
||||
sessionId = this.getNodeParameter('sessionId', itemIndex) as string;
|
||||
}
|
||||
|
||||
let memory: BaseChatMemory;
|
||||
|
||||
if (credentials.cloud) {
|
||||
if (!credentials.apiKey) {
|
||||
throw new NodeOperationError(this.getNode(), 'API key is required to use Zep Cloud');
|
||||
}
|
||||
memory = new WhiteSpaceTrimmedZepCloudMemory({
|
||||
sessionId,
|
||||
apiKey: credentials.apiKey,
|
||||
memoryType: 'perpetual',
|
||||
memoryKey: 'chat_history',
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
separateMessages: false,
|
||||
});
|
||||
} else {
|
||||
if (!credentials.apiUrl) {
|
||||
throw new NodeOperationError(this.getNode(), 'API url is required to use Zep Open Source');
|
||||
}
|
||||
memory = new ZepMemory({
|
||||
sessionId,
|
||||
baseURL: credentials.apiUrl,
|
||||
apiKey: credentials.apiKey,
|
||||
memoryKey: 'chat_history',
|
||||
returnMessages: true,
|
||||
inputKey: 'input',
|
||||
outputKey: 'output',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
response: logWrapper(memory, this),
|
||||
};
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -0,0 +1,61 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const sessionIdOption: INodeProperties = {
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionIdType',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Connected Chat Trigger Node',
|
||||
value: 'fromInput',
|
||||
description:
|
||||
"Looks for an input field called 'sessionId' that is coming from a directly connected Chat Trigger",
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'Define below',
|
||||
value: 'customKey',
|
||||
description: 'Use an expression to reference data in previous nodes or enter static text',
|
||||
},
|
||||
],
|
||||
default: 'fromInput',
|
||||
builderHint: {
|
||||
message:
|
||||
"Use 'Connected Chat Trigger Node' (fromInput) if there is a Chat Trigger node earlier in the workflow. Otherwise use 'Define below' (customKey).",
|
||||
},
|
||||
};
|
||||
|
||||
export const expressionSessionKeyProperty = (fromVersion: number): INodeProperties => ({
|
||||
displayName: 'Session Key From Previous Node',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
disabledOptions: { show: { sessionIdType: ['fromInput'] } },
|
||||
displayOptions: {
|
||||
show: {
|
||||
sessionIdType: ['fromInput'],
|
||||
'@version': [{ _cnd: { gte: fromVersion } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const sessionKeyProperty: INodeProperties = {
|
||||
displayName: 'Key',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The key to use to store session ID in the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
sessionIdType: ['customKey'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const contextWindowLengthProperty: INodeProperties = {
|
||||
displayName: 'Context Window Length',
|
||||
name: 'contextWindowLength',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
hint: 'How many past interactions the model receives as context',
|
||||
};
|
||||
Reference in New Issue
Block a user