node-red-contrib-ai-agent 0.0.4 → 0.0.6
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/README.md +19 -2
- package/memory-file/memory-file.html +22 -1
- package/memory-file/memory-file.js +393 -30
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -36,6 +36,18 @@ Your feedback and contributions are highly appreciated!
|
|
|
36
36
|
5. Connect to an AI Agent node to process messages
|
|
37
37
|
6. (Optional) Connect more AI Agent nodes to process messages in a chain
|
|
38
38
|
|
|
39
|
+
## Example: Today's Joke
|
|
40
|
+
|
|
41
|
+
Here's an example flow that tells a joke related to today's date using a custom tool:
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
### Flow Output
|
|
46
|
+
|
|
47
|
+
When executed, the flow will generate a joke related to the current date:
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
39
51
|
##
|
|
40
52
|
|
|
41
53
|
## Node Types
|
|
@@ -59,8 +71,11 @@ A configuration node that initializes the conversation context in memory. The ag
|
|
|
59
71
|
A configuration node that initializes the conversation context with file-based persistence. The agent node uses this configuration to manage the conversation context across restarts.
|
|
60
72
|
|
|
61
73
|
**Properties:**
|
|
62
|
-
- **
|
|
63
|
-
- **
|
|
74
|
+
- **Filename**: Path to store the memory file (relative to Node-RED user directory)
|
|
75
|
+
- **Max Conversations**: Maximum number of conversations to store
|
|
76
|
+
- **Max Messages Per Conversation**: Maximum messages per conversation history
|
|
77
|
+
- **Backups**: Enable/disable automatic backups
|
|
78
|
+
- **Backup Count**: Number of backups to keep
|
|
64
79
|
- **Name**: Display name for the node
|
|
65
80
|
|
|
66
81
|
### AI Model
|
|
@@ -128,6 +143,8 @@ The AI Agent will automatically detect and use the tools in its processing. You
|
|
|
128
143
|
|
|
129
144
|
In this example, the AI Agent has access to both an HTTP tool for making external API calls and a function tool for custom logic.
|
|
130
145
|
|
|
146
|
+
|
|
147
|
+
|
|
131
148
|
## Example: Chained Agents
|
|
132
149
|
|
|
133
150
|
For more complex scenarios, you can chain multiple agents to process messages in sequence:
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
validate: function(v) {
|
|
12
12
|
return v.length > 0;
|
|
13
13
|
}
|
|
14
|
-
}
|
|
14
|
+
},
|
|
15
|
+
maxConversations: { value: 50, validate: RED.validators.number() },
|
|
16
|
+
maxMessagesPerConversation: { value: 100, validate: RED.validators.number() },
|
|
17
|
+
backupEnabled: { value: true },
|
|
18
|
+
backupCount: { value: 3, validate: RED.validators.number() }
|
|
15
19
|
},
|
|
16
20
|
inputs: 1,
|
|
17
21
|
outputs: 1,
|
|
@@ -43,6 +47,23 @@
|
|
|
43
47
|
<label for="node-input-filename"><i class="fa fa-file"></i> Filename</label>
|
|
44
48
|
<input type="text" id="node-input-filename" placeholder="ai-memories.json">
|
|
45
49
|
</div>
|
|
50
|
+
<div class="form-row">
|
|
51
|
+
<label for="node-input-maxConversations"><i class="fa fa-list"></i> Max Conversations</label>
|
|
52
|
+
<input type="number" id="node-input-maxConversations" placeholder="50">
|
|
53
|
+
</div>
|
|
54
|
+
<div class="form-row">
|
|
55
|
+
<label for="node-input-maxMessagesPerConversation"><i class="fa fa-commenting"></i> Max Messages/Conv</label>
|
|
56
|
+
<input type="number" id="node-input-maxMessagesPerConversation" placeholder="100">
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<label for="node-input-backupEnabled"><i class="fa fa-shield"></i> Backups</label>
|
|
60
|
+
<input type="checkbox" id="node-input-backupEnabled" style="display:inline-block; width:auto; vertical-align:top;">
|
|
61
|
+
<label for="node-input-backupEnabled" style="width: auto;">Enable automatic backups</label>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-row" id="backupCount-row">
|
|
64
|
+
<label for="node-input-backupCount"><i class="fa fa-history"></i> Backup Count</label>
|
|
65
|
+
<input type="number" id="node-input-backupCount" placeholder="3">
|
|
66
|
+
</div>
|
|
46
67
|
<div class="form-tips">
|
|
47
68
|
<p>Memories will be stored in Node-RED's user directory.</p>
|
|
48
69
|
</div>
|
|
@@ -1,55 +1,419 @@
|
|
|
1
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class SimpleFileStorage {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.filePath = options.filePath;
|
|
7
|
+
this.backupEnabled = options.backupEnabled !== false;
|
|
8
|
+
this.backupCount = options.backupCount || 3;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async save(data) {
|
|
12
|
+
try {
|
|
13
|
+
const dir = path.dirname(this.filePath);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
data.metadata = data.metadata || {};
|
|
19
|
+
data.metadata.lastUpdated = new Date().toISOString();
|
|
20
|
+
|
|
21
|
+
await fs.promises.writeFile(
|
|
22
|
+
this.filePath,
|
|
23
|
+
JSON.stringify(data, null, 2)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (this.backupEnabled) {
|
|
27
|
+
await this.createBackup();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
loadSync() {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(this.filePath)) {
|
|
39
|
+
const data = fs.readFileSync(this.filePath, 'utf8');
|
|
40
|
+
return JSON.parse(data);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Backup recovery is still async, but for initial load sync is safer
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async load() {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(this.filePath)) {
|
|
52
|
+
const data = await fs.promises.readFile(this.filePath, 'utf8');
|
|
53
|
+
return JSON.parse(data);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return await this.recoverFromBackup();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async createBackup() {
|
|
62
|
+
try {
|
|
63
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
64
|
+
const backupPath = `${this.filePath}.${timestamp}.bak`;
|
|
65
|
+
|
|
66
|
+
await fs.promises.copyFile(this.filePath, backupPath);
|
|
67
|
+
|
|
68
|
+
const backups = await this.listBackups();
|
|
69
|
+
if (backups.length > this.backupCount) {
|
|
70
|
+
const oldestBackups = backups
|
|
71
|
+
.sort((a, b) => a.time - b.time)
|
|
72
|
+
.slice(0, backups.length - this.backupCount);
|
|
73
|
+
|
|
74
|
+
for (const backup of oldestBackups) {
|
|
75
|
+
await fs.promises.unlink(backup.path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async listBackups() {
|
|
86
|
+
try {
|
|
87
|
+
const dir = path.dirname(this.filePath);
|
|
88
|
+
const base = path.basename(this.filePath);
|
|
89
|
+
|
|
90
|
+
const files = await fs.promises.readdir(dir);
|
|
91
|
+
|
|
92
|
+
return files
|
|
93
|
+
.filter(file => file.startsWith(`${base}.`) && file.endsWith('.bak'))
|
|
94
|
+
.map(file => {
|
|
95
|
+
const match = file.match(/\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)\.bak$/);
|
|
96
|
+
const timestamp = match ? match[1].replace(/-/g, ':').replace(/-(\d{3})Z$/, '.$1Z') : null;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
path: path.join(dir, file),
|
|
100
|
+
time: timestamp ? new Date(timestamp).getTime() : 0
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async recoverFromBackup() {
|
|
109
|
+
try {
|
|
110
|
+
const backups = await this.listBackups();
|
|
111
|
+
|
|
112
|
+
if (backups.length === 0) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const latestBackup = backups.sort((a, b) => b.time - a.time)[0];
|
|
117
|
+
const data = await fs.promises.readFile(latestBackup.path, 'utf8');
|
|
118
|
+
return JSON.parse(data);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class SimpleMemoryManager {
|
|
126
|
+
constructor(options = {}) {
|
|
127
|
+
this.maxConversations = options.maxConversations || 50;
|
|
128
|
+
this.maxMessagesPerConversation = options.maxMessagesPerConversation || 100;
|
|
129
|
+
this.conversations = [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
addMessage(conversationId, message) {
|
|
133
|
+
let conversation = this.conversations.find(c => c.id === conversationId);
|
|
134
|
+
|
|
135
|
+
if (!conversation) {
|
|
136
|
+
conversation = {
|
|
137
|
+
id: conversationId,
|
|
138
|
+
messages: [],
|
|
139
|
+
createdAt: new Date().toISOString(),
|
|
140
|
+
updatedAt: new Date().toISOString()
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.conversations.push(conversation);
|
|
144
|
+
|
|
145
|
+
if (this.conversations.length > this.maxConversations) {
|
|
146
|
+
this.conversations = this.conversations
|
|
147
|
+
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
|
148
|
+
.slice(0, this.maxConversations);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
conversation.messages.push({
|
|
153
|
+
...message,
|
|
154
|
+
timestamp: new Date().toISOString()
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
conversation.updatedAt = new Date().toISOString();
|
|
158
|
+
|
|
159
|
+
if (conversation.messages.length > this.maxMessagesPerConversation) {
|
|
160
|
+
conversation.messages = conversation.messages.slice(-this.maxMessagesPerConversation);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return conversation;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getConversation(conversationId) {
|
|
167
|
+
return this.conversations.find(c => c.id === conversationId) || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getConversationMessages(conversationId, limit = null) {
|
|
171
|
+
const conversation = this.getConversation(conversationId);
|
|
172
|
+
|
|
173
|
+
if (!conversation) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const messages = conversation.messages;
|
|
178
|
+
|
|
179
|
+
if (limit && messages.length > limit) {
|
|
180
|
+
return messages.slice(-limit);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return messages;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
searchConversations(query, options = {}) {
|
|
187
|
+
const results = [];
|
|
188
|
+
|
|
189
|
+
for (const conversation of this.conversations) {
|
|
190
|
+
const matchingMessages = conversation.messages.filter(message =>
|
|
191
|
+
message.content && message.content.toLowerCase().includes(query.toLowerCase())
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
if (matchingMessages.length > 0) {
|
|
195
|
+
results.push({
|
|
196
|
+
conversation,
|
|
197
|
+
matchingMessages: options.includeMessages ? matchingMessages : matchingMessages.length
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return results.sort((a, b) =>
|
|
203
|
+
new Date(b.conversation.updatedAt) - new Date(a.conversation.updatedAt)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
deleteConversation(conversationId) {
|
|
208
|
+
const index = this.conversations.findIndex(c => c.id === conversationId);
|
|
209
|
+
|
|
210
|
+
if (index !== -1) {
|
|
211
|
+
this.conversations.splice(index, 1);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
clearAllConversations() {
|
|
219
|
+
this.conversations = [];
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
toJSON() {
|
|
224
|
+
return {
|
|
225
|
+
conversations: this.conversations,
|
|
226
|
+
metadata: {
|
|
227
|
+
version: '1.0',
|
|
228
|
+
lastUpdated: new Date().toISOString(),
|
|
229
|
+
stats: {
|
|
230
|
+
conversationCount: this.conversations.length,
|
|
231
|
+
messageCount: this.conversations.reduce((count, conv) => count + conv.messages.length, 0)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fromJSON(data) {
|
|
238
|
+
if (data && data.conversations) {
|
|
239
|
+
this.conversations = data.conversations;
|
|
240
|
+
} else {
|
|
241
|
+
this.conversations = [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = function (RED) {
|
|
2
247
|
'use strict';
|
|
3
248
|
|
|
4
249
|
function MemoryFileNode(config) {
|
|
5
250
|
RED.nodes.createNode(this, config);
|
|
6
251
|
const node = this;
|
|
7
|
-
|
|
252
|
+
|
|
8
253
|
// Configuration
|
|
9
254
|
node.name = config.name || 'AI Memory (File)';
|
|
10
255
|
node.filename = config.filename || 'ai-memories.json';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
node.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
256
|
+
node.maxConversations = parseInt(config.maxConversations) || 50;
|
|
257
|
+
node.maxMessagesPerConversation = parseInt(config.maxMessagesPerConversation) || 100;
|
|
258
|
+
node.backupEnabled = config.backupEnabled !== false;
|
|
259
|
+
node.backupCount = parseInt(config.backupCount) || 3;
|
|
260
|
+
|
|
261
|
+
const userDir = (RED.settings && RED.settings.userDir) || process.cwd();
|
|
262
|
+
const filePath = path.join(userDir, node.filename);
|
|
263
|
+
|
|
264
|
+
// Create storage and memory manager
|
|
265
|
+
node.fileStorage = new SimpleFileStorage({
|
|
266
|
+
filePath,
|
|
267
|
+
backupEnabled: node.backupEnabled,
|
|
268
|
+
backupCount: node.backupCount
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
node.memoryManager = new SimpleMemoryManager({
|
|
272
|
+
maxConversations: node.maxConversations,
|
|
273
|
+
maxMessagesPerConversation: node.maxMessagesPerConversation
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Load existing memories synchronously at startup
|
|
20
277
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
node.
|
|
24
|
-
node.status({
|
|
278
|
+
const data = node.fileStorage.loadSync();
|
|
279
|
+
if (data) {
|
|
280
|
+
node.memoryManager.fromJSON(data);
|
|
281
|
+
node.status({
|
|
282
|
+
fill: "green",
|
|
283
|
+
shape: "dot",
|
|
284
|
+
text: `${node.memoryManager.conversations.length} conversations`
|
|
285
|
+
});
|
|
25
286
|
} else {
|
|
26
|
-
node.status({fill:"blue",shape:"ring",text:"New file will be created"});
|
|
287
|
+
node.status({ fill: "blue", shape: "ring", text: "New memory file will be created" });
|
|
27
288
|
}
|
|
28
289
|
} catch (err) {
|
|
29
290
|
node.error("Error loading memory file: " + err.message);
|
|
30
|
-
node.status({fill:"red",shape:"ring",text:"Error loading"});
|
|
291
|
+
node.status({ fill: "red", shape: "ring", text: "Error loading" });
|
|
31
292
|
}
|
|
32
293
|
|
|
33
294
|
// Handle incoming messages
|
|
34
|
-
node.on('input', function(msg) {
|
|
295
|
+
node.on('input', async function (msg, send, done) {
|
|
296
|
+
// Use send and done for Node-RED 1.0+ compatibility
|
|
297
|
+
send = send || function () { node.send.apply(node, arguments) };
|
|
298
|
+
|
|
35
299
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
300
|
+
msg.aimemory = msg.aimemory || {};
|
|
301
|
+
|
|
302
|
+
if (msg.command) {
|
|
303
|
+
await processCommand(node, msg);
|
|
304
|
+
} else {
|
|
305
|
+
const conversationId = msg.conversationId || 'default';
|
|
306
|
+
const messages = node.memoryManager.getConversationMessages(conversationId);
|
|
307
|
+
|
|
308
|
+
msg.aimemory = {
|
|
309
|
+
type: 'file',
|
|
310
|
+
conversationId,
|
|
311
|
+
context: messages
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
send(msg);
|
|
316
|
+
|
|
317
|
+
node.status({
|
|
318
|
+
fill: "green",
|
|
319
|
+
shape: "dot",
|
|
320
|
+
text: `${node.memoryManager.conversations.length} conversations`
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (done) done();
|
|
42
324
|
} catch (err) {
|
|
43
325
|
node.error("Error in memory node: " + err.message, msg);
|
|
44
|
-
node.status({fill:"red",shape:"ring",text:"Error"});
|
|
326
|
+
node.status({ fill: "red", shape: "ring", text: "Error" });
|
|
327
|
+
if (done) done(err);
|
|
45
328
|
}
|
|
46
329
|
});
|
|
47
330
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
331
|
+
async function processCommand(node, msg) {
|
|
332
|
+
const command = msg.command;
|
|
333
|
+
|
|
334
|
+
switch (command) {
|
|
335
|
+
case 'add':
|
|
336
|
+
if (!msg.message) {
|
|
337
|
+
throw new Error('No message content provided');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const conversationId = msg.conversationId || 'default';
|
|
341
|
+
const conversation = node.memoryManager.addMessage(conversationId, msg.message);
|
|
342
|
+
|
|
343
|
+
msg.result = {
|
|
344
|
+
success: true,
|
|
345
|
+
operation: 'add',
|
|
346
|
+
conversationId,
|
|
347
|
+
messageCount: conversation.messages.length
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
await node.fileStorage.save(node.memoryManager.toJSON());
|
|
351
|
+
break;
|
|
352
|
+
|
|
353
|
+
case 'get':
|
|
354
|
+
const getConversationId = msg.conversationId || 'default';
|
|
355
|
+
const limit = msg.limit || null;
|
|
356
|
+
|
|
357
|
+
msg.result = {
|
|
358
|
+
success: true,
|
|
359
|
+
operation: 'get',
|
|
360
|
+
conversationId: getConversationId,
|
|
361
|
+
messages: node.memoryManager.getConversationMessages(getConversationId, limit)
|
|
362
|
+
};
|
|
363
|
+
break;
|
|
364
|
+
|
|
365
|
+
case 'search':
|
|
366
|
+
if (!msg.query) {
|
|
367
|
+
throw new Error('No search query provided');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
msg.result = {
|
|
371
|
+
success: true,
|
|
372
|
+
operation: 'search',
|
|
373
|
+
query: msg.query,
|
|
374
|
+
results: node.memoryManager.searchConversations(msg.query, {
|
|
375
|
+
includeMessages: msg.includeMessages !== false
|
|
376
|
+
})
|
|
377
|
+
};
|
|
378
|
+
break;
|
|
379
|
+
|
|
380
|
+
case 'delete':
|
|
381
|
+
if (!msg.conversationId) {
|
|
382
|
+
throw new Error('No conversation ID provided');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const deleted = node.memoryManager.deleteConversation(msg.conversationId);
|
|
386
|
+
|
|
387
|
+
msg.result = {
|
|
388
|
+
success: deleted,
|
|
389
|
+
operation: 'delete',
|
|
390
|
+
conversationId: msg.conversationId
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (deleted) {
|
|
394
|
+
await node.fileStorage.save(node.memoryManager.toJSON());
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'clear':
|
|
399
|
+
node.memoryManager.clearAllConversations();
|
|
400
|
+
|
|
401
|
+
msg.result = {
|
|
402
|
+
success: true,
|
|
403
|
+
operation: 'clear'
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
await node.fileStorage.save(node.memoryManager.toJSON());
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
default:
|
|
410
|
+
throw new Error(`Unknown command: ${command}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
node.on('close', async function () {
|
|
51
415
|
try {
|
|
52
|
-
|
|
416
|
+
await node.fileStorage.save(node.memoryManager.toJSON());
|
|
53
417
|
} catch (err) {
|
|
54
418
|
node.error("Error saving memory file: " + err.message);
|
|
55
419
|
}
|
|
@@ -57,6 +421,5 @@ module.exports = function(RED) {
|
|
|
57
421
|
});
|
|
58
422
|
}
|
|
59
423
|
|
|
60
|
-
// Register the node type
|
|
61
424
|
RED.nodes.registerType("ai-memory-file", MemoryFileNode);
|
|
62
425
|
};
|
package/package.json
CHANGED