n8n-nodes-token-aware-memory 0.1.28 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-token-aware-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "Token aware memory node for n8n AI workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "",
|
|
@@ -23,13 +23,7 @@
|
|
|
23
23
|
"release": "n8n-node release",
|
|
24
24
|
"prepublishOnly": "n8n-node prerelease"
|
|
25
25
|
},
|
|
26
|
-
"files":
|
|
27
|
-
"dist/nodes/TokenAwareMemory/token-aware-memory.svg",
|
|
28
|
-
"dist/nodes/TokenAwareMemory/TokenAwareMemory.node.js",
|
|
29
|
-
"dist/nodes/TokenAwareMemory/TokenAwareMemory.node.json",
|
|
30
|
-
"dist/package.json",
|
|
31
|
-
"README.md"
|
|
32
|
-
],
|
|
26
|
+
"files": "dist",
|
|
33
27
|
"n8n": {
|
|
34
28
|
"n8nNodesApiVersion": 1,
|
|
35
29
|
"strict": false,
|
|
@@ -40,7 +34,7 @@
|
|
|
40
34
|
}
|
|
41
35
|
],
|
|
42
36
|
"nodes": [
|
|
43
|
-
"
|
|
37
|
+
"nodes/TokenAwareMemory/TokenAwareMemory.node.js"
|
|
44
38
|
]
|
|
45
39
|
},
|
|
46
40
|
"dependencies": {
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TokenAwareMemory = void 0;
|
|
4
|
-
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
-
const redis_1 = require("redis");
|
|
6
|
-
class TokenEstimator {
|
|
7
|
-
static estimateTokens(text) {
|
|
8
|
-
return Math.ceil(text.length / 4);
|
|
9
|
-
}
|
|
10
|
-
static estimateMessageTokens(message) {
|
|
11
|
-
return message.tokens || this.estimateTokens(message.content);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
class HierarchicalTokenMemory {
|
|
15
|
-
constructor(redisClient, workflowId, nodeId, sessionId, maxTokens = 8000) {
|
|
16
|
-
this.redisClient = redisClient;
|
|
17
|
-
this.compressionThreshold = Math.floor(maxTokens * 0.8);
|
|
18
|
-
this.shortTermKey = `memory:${workflowId}:${nodeId}:${sessionId}:short`;
|
|
19
|
-
this.midTermKey = `memory:${workflowId}:${nodeId}:${sessionId}:mid`;
|
|
20
|
-
this.longTermKey = `memory:${workflowId}:${nodeId}:${sessionId}:long`;
|
|
21
|
-
this.metadataKey = `memory:${workflowId}:${nodeId}:${sessionId}:meta`;
|
|
22
|
-
}
|
|
23
|
-
async initialize() {
|
|
24
|
-
try {
|
|
25
|
-
await this.redisClient.connect();
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async loadMemory() {
|
|
31
|
-
const [shortTerm, midTerm, longTerm] = await Promise.all([
|
|
32
|
-
this.redisClient.lRange(this.shortTermKey, 0, -1),
|
|
33
|
-
this.redisClient.lRange(this.midTermKey, 0, -1),
|
|
34
|
-
this.redisClient.lRange(this.longTermKey, 0, -1),
|
|
35
|
-
]);
|
|
36
|
-
const parseMessages = (rawMessages) => {
|
|
37
|
-
return rawMessages.map(msg => JSON.parse(msg)).filter(Boolean);
|
|
38
|
-
};
|
|
39
|
-
const shortTermMessages = parseMessages(shortTerm);
|
|
40
|
-
const midTermMessages = parseMessages(midTerm);
|
|
41
|
-
const longTermMessages = parseMessages(longTerm);
|
|
42
|
-
const calculateTotalTokens = (messages) => {
|
|
43
|
-
return messages.reduce((total, msg) => total + TokenEstimator.estimateMessageTokens(msg), 0);
|
|
44
|
-
};
|
|
45
|
-
return [
|
|
46
|
-
{ messages: longTermMessages, totalTokens: calculateTotalTokens(longTermMessages) },
|
|
47
|
-
{ messages: midTermMessages, totalTokens: calculateTotalTokens(midTermMessages) },
|
|
48
|
-
{ messages: shortTermMessages, totalTokens: calculateTotalTokens(shortTermMessages) },
|
|
49
|
-
];
|
|
50
|
-
}
|
|
51
|
-
async getTotalTokens() {
|
|
52
|
-
const levels = await this.loadMemory();
|
|
53
|
-
return levels.reduce((total, level) => total + level.totalTokens, 0);
|
|
54
|
-
}
|
|
55
|
-
async saveMessage(role, content) {
|
|
56
|
-
const message = {
|
|
57
|
-
role,
|
|
58
|
-
content,
|
|
59
|
-
timestamp: new Date().toISOString(),
|
|
60
|
-
tokens: TokenEstimator.estimateTokens(content),
|
|
61
|
-
};
|
|
62
|
-
await this.redisClient.rPush(this.shortTermKey, JSON.stringify(message));
|
|
63
|
-
}
|
|
64
|
-
async compressMemory(llmConnection, summarizationPrompt) {
|
|
65
|
-
const levels = await this.loadMemory();
|
|
66
|
-
const shortTerm = levels[2];
|
|
67
|
-
if (shortTerm.messages.length > 1) {
|
|
68
|
-
const messagesToCompress = shortTerm.messages.slice(0, -1);
|
|
69
|
-
await this.redisClient.del(this.shortTermKey);
|
|
70
|
-
if (shortTerm.messages.length > 0) {
|
|
71
|
-
const recentMessage = shortTerm.messages[shortTerm.messages.length - 1];
|
|
72
|
-
await this.redisClient.rPush(this.shortTermKey, JSON.stringify(recentMessage));
|
|
73
|
-
}
|
|
74
|
-
const conversationText = messagesToCompress
|
|
75
|
-
.map(msg => `${msg.role}: ${msg.content}`)
|
|
76
|
-
.join('\n\n');
|
|
77
|
-
const prompt = summarizationPrompt.replace('{{conversation}}', conversationText);
|
|
78
|
-
const compressedContent = await this.summarizeWithLLM(llmConnection, prompt);
|
|
79
|
-
const compressedMessage = {
|
|
80
|
-
role: 'assistant',
|
|
81
|
-
content: compressedContent,
|
|
82
|
-
timestamp: new Date().toISOString(),
|
|
83
|
-
tokens: TokenEstimator.estimateTokens(compressedContent),
|
|
84
|
-
};
|
|
85
|
-
await this.redisClient.rPush(this.midTermKey, JSON.stringify(compressedMessage));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
async summarizeWithLLM(llmConnection, prompt) {
|
|
89
|
-
return `[AI-SUMMARY] Conversation compressed for token efficiency. Original length: ${prompt.length} characters.`;
|
|
90
|
-
}
|
|
91
|
-
async getFullHistory() {
|
|
92
|
-
const levels = await this.loadMemory();
|
|
93
|
-
const [longTerm, midTerm, shortTerm] = levels;
|
|
94
|
-
return [...longTerm.messages, ...midTerm.messages, ...shortTerm.messages];
|
|
95
|
-
}
|
|
96
|
-
async clear() {
|
|
97
|
-
await Promise.all([
|
|
98
|
-
this.redisClient.del(this.shortTermKey),
|
|
99
|
-
this.redisClient.del(this.midTermKey),
|
|
100
|
-
this.redisClient.del(this.longTermKey),
|
|
101
|
-
this.redisClient.del(this.metadataKey),
|
|
102
|
-
]);
|
|
103
|
-
}
|
|
104
|
-
async disconnect() {
|
|
105
|
-
try {
|
|
106
|
-
await this.redisClient.disconnect();
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
class TokenAwareMemory {
|
|
113
|
-
constructor() {
|
|
114
|
-
this.description = {
|
|
115
|
-
displayName: 'Token Aware Memory',
|
|
116
|
-
name: 'tokenAwareMemory',
|
|
117
|
-
icon: 'file:token-aware-memory.svg',
|
|
118
|
-
group: ['transform'],
|
|
119
|
-
version: 1,
|
|
120
|
-
description: 'Token-aware memory node with hierarchical storage, Redis persistence, and automatic compression using LLM summarization.',
|
|
121
|
-
defaults: {
|
|
122
|
-
name: 'Token Aware Memory',
|
|
123
|
-
},
|
|
124
|
-
codex: {
|
|
125
|
-
categories: ['AI'],
|
|
126
|
-
subcategories: {
|
|
127
|
-
AI: ['Memory'],
|
|
128
|
-
Memory: ['Other memories'],
|
|
129
|
-
},
|
|
130
|
-
resources: {
|
|
131
|
-
primaryDocumentation: [
|
|
132
|
-
{
|
|
133
|
-
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/',
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
inputs: [n8n_workflow_1.NodeConnectionTypes.AiLanguageModel],
|
|
139
|
-
outputs: [n8n_workflow_1.NodeConnectionTypes.AiMemory],
|
|
140
|
-
outputNames: ['Memory'],
|
|
141
|
-
usableAsTool: true,
|
|
142
|
-
credentials: [
|
|
143
|
-
{
|
|
144
|
-
name: 'redisApi',
|
|
145
|
-
required: true,
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
properties: [
|
|
149
|
-
{
|
|
150
|
-
displayName: 'Max Tokens',
|
|
151
|
-
name: 'maxTokens',
|
|
152
|
-
type: 'number',
|
|
153
|
-
default: 8000,
|
|
154
|
-
description: 'Maximum total tokens allowed before triggering automatic compression',
|
|
155
|
-
typeOptions: {
|
|
156
|
-
minValue: 1000,
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
displayName: 'Summarization Prompt',
|
|
161
|
-
name: 'summarizationPrompt',
|
|
162
|
-
type: 'string',
|
|
163
|
-
default: 'Please summarize the following conversation history concisely while preserving key information and context:\n\n{{conversation}}',
|
|
164
|
-
description: 'Prompt template used when compressing messages. Use {{conversation}} as placeholder for the content to summarize.',
|
|
165
|
-
typeOptions: {
|
|
166
|
-
rows: 4,
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
displayName: 'Redis Credentials',
|
|
171
|
-
name: 'redisCredentials',
|
|
172
|
-
type: 'credentials',
|
|
173
|
-
default: '',
|
|
174
|
-
required: true,
|
|
175
|
-
description: 'Redis connection credentials for memory persistence',
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
displayName: 'Session ID',
|
|
179
|
-
name: 'sessionId',
|
|
180
|
-
type: 'string',
|
|
181
|
-
default: '',
|
|
182
|
-
description: 'Unique session identifier to separate memory between different conversations/executions (leave empty for auto-generated)',
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
async supplyData(itemIndex) {
|
|
188
|
-
const maxTokens = this.getNodeParameter('maxTokens', itemIndex, 8000);
|
|
189
|
-
const summarizationPrompt = this.getNodeParameter('summarizationPrompt', itemIndex, '');
|
|
190
|
-
let sessionId = this.getNodeParameter('sessionId', itemIndex, '');
|
|
191
|
-
if (!sessionId) {
|
|
192
|
-
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
193
|
-
}
|
|
194
|
-
const credentials = await this.getCredentials('redis', itemIndex);
|
|
195
|
-
try {
|
|
196
|
-
const redisOptions = {
|
|
197
|
-
socket: {
|
|
198
|
-
host: credentials.host,
|
|
199
|
-
port: credentials.port || 6379,
|
|
200
|
-
tls: credentials.ssl === true,
|
|
201
|
-
},
|
|
202
|
-
database: credentials.database || 0,
|
|
203
|
-
};
|
|
204
|
-
if (credentials.user) {
|
|
205
|
-
redisOptions.username = credentials.user;
|
|
206
|
-
}
|
|
207
|
-
if (credentials.password) {
|
|
208
|
-
redisOptions.password = credentials.password;
|
|
209
|
-
}
|
|
210
|
-
const redisClient = (0, redis_1.createClient)(redisOptions);
|
|
211
|
-
redisClient.on('error', async (error) => {
|
|
212
|
-
await redisClient.disconnect();
|
|
213
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Redis Error: ' + error.message);
|
|
214
|
-
});
|
|
215
|
-
const workflowId = this.getWorkflow().id || 'default-workflow';
|
|
216
|
-
const nodeId = this.getNode().id || 'default-node';
|
|
217
|
-
const memory = new HierarchicalTokenMemory(redisClient, workflowId, nodeId, sessionId, maxTokens);
|
|
218
|
-
await memory.initialize();
|
|
219
|
-
let llmConnection = null;
|
|
220
|
-
try {
|
|
221
|
-
llmConnection = null;
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
}
|
|
225
|
-
const memoryInterface = {
|
|
226
|
-
loadMemoryVariables: async () => {
|
|
227
|
-
const history = await memory.getFullHistory();
|
|
228
|
-
return {
|
|
229
|
-
history,
|
|
230
|
-
};
|
|
231
|
-
},
|
|
232
|
-
saveContext: async (input, output) => {
|
|
233
|
-
if (input) {
|
|
234
|
-
await memory.saveMessage('user', String(input));
|
|
235
|
-
}
|
|
236
|
-
if (output) {
|
|
237
|
-
await memory.saveMessage('assistant', String(output));
|
|
238
|
-
}
|
|
239
|
-
const totalTokens = await memory.getTotalTokens();
|
|
240
|
-
if (totalTokens >= memory['compressionThreshold']) {
|
|
241
|
-
if (!llmConnection || !summarizationPrompt) {
|
|
242
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'LLM connection and summarization prompt are required for memory compression');
|
|
243
|
-
}
|
|
244
|
-
await memory.compressMemory(llmConnection, summarizationPrompt);
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
clear: async () => {
|
|
248
|
-
await memory.clear();
|
|
249
|
-
},
|
|
250
|
-
getTotalTokens: async () => {
|
|
251
|
-
return memory.getTotalTokens();
|
|
252
|
-
},
|
|
253
|
-
getFullHistory: async () => {
|
|
254
|
-
return memory.getFullHistory();
|
|
255
|
-
},
|
|
256
|
-
disconnect: async () => {
|
|
257
|
-
await memory.disconnect();
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
return {
|
|
261
|
-
response: memoryInterface,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to create token-aware memory: ' + (error instanceof Error ? error.message : String(error)));
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
exports.TokenAwareMemory = TokenAwareMemory;
|
|
270
|
-
//# sourceMappingURL=TokenAwareMemory.node.js.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"node": "n8n-nodes-token-aware-memory",
|
|
3
|
-
"nodeVersion": "1.0",
|
|
4
|
-
"codexVersion": "1.0",
|
|
5
|
-
"categories": ["AI"],
|
|
6
|
-
"subcategories": {
|
|
7
|
-
"AI": ["Memory"]
|
|
8
|
-
},
|
|
9
|
-
"resources": {
|
|
10
|
-
"primaryDocumentation": [
|
|
11
|
-
{
|
|
12
|
-
"url": "https://github.com/org/repo?tab=readme-ov-file"
|
|
13
|
-
}
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
-
<circle cx="12" cy="12" r="3"></circle>
|
|
3
|
-
<path d="M12 1v6m0 6v6"></path>
|
|
4
|
-
<path d="M1 12h6m6 0h6"></path>
|
|
5
|
-
<circle cx="12" cy="12" r="10" fill="none"></circle>
|
|
6
|
-
</svg>
|
package/dist/package.json
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "n8n-nodes-token-aware-memory",
|
|
3
|
-
"version": "0.1.27",
|
|
4
|
-
"description": "Token aware memory node for n8n AI workflows",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"homepage": "",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"n8n-community-node-package"
|
|
9
|
-
],
|
|
10
|
-
"author": {
|
|
11
|
-
"name": "antspider"
|
|
12
|
-
},
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "https://github.com/example/n8n-nodes-token-aware-memory.git"
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "n8n-node build",
|
|
19
|
-
"build:watch": "tsc --watch",
|
|
20
|
-
"dev": "n8n-node dev",
|
|
21
|
-
"lint": "n8n-node lint",
|
|
22
|
-
"lint:fix": "n8n-node lint --fix",
|
|
23
|
-
"release": "n8n-node release",
|
|
24
|
-
"prepublishOnly": "n8n-node prerelease"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"dist/nodes/TokenAwareMemory/token-aware-memory.svg",
|
|
28
|
-
"dist/nodes/TokenAwareMemory/TokenAwareMemory.node.js",
|
|
29
|
-
"dist/nodes/TokenAwareMemory/TokenAwareMemory.node.json",
|
|
30
|
-
"dist/package.json",
|
|
31
|
-
"README.md"
|
|
32
|
-
],
|
|
33
|
-
"n8n": {
|
|
34
|
-
"n8nNodesApiVersion": 1,
|
|
35
|
-
"strict": false,
|
|
36
|
-
"credentials": [
|
|
37
|
-
{
|
|
38
|
-
"name": "redisApi",
|
|
39
|
-
"displayName": "Redis"
|
|
40
|
-
}
|
|
41
|
-
],
|
|
42
|
-
"nodes": [
|
|
43
|
-
"dist/nodes/TokenAwareMemory/TokenAwareMemory.node.js"
|
|
44
|
-
]
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"redis": "^4.6.13"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@n8n/node-cli": "*",
|
|
51
|
-
"eslint": "9.32.0",
|
|
52
|
-
"prettier": "3.6.2",
|
|
53
|
-
"release-it": "^19.0.4",
|
|
54
|
-
"typescript": "5.9.2",
|
|
55
|
-
"@types/node": "^20.0.0"
|
|
56
|
-
},
|
|
57
|
-
"peerDependencies": {
|
|
58
|
-
"n8n-workflow": "*"
|
|
59
|
-
}
|
|
60
|
-
}
|