node-red-contrib-ai-agent 0.0.6 → 0.0.7
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 +21 -4
- package/memory-file/memory-file.html +48 -10
- package/memory-file/memory-file.js +194 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,9 @@ A configuration node that initializes the conversation context with file-based p
|
|
|
76
76
|
- **Max Messages Per Conversation**: Maximum messages per conversation history
|
|
77
77
|
- **Backups**: Enable/disable automatic backups
|
|
78
78
|
- **Backup Count**: Number of backups to keep
|
|
79
|
+
- **Consolidation**: Threshold of messages to trigger auto-summarization
|
|
80
|
+
- **Long-Term Memory**: Enable/disable vector-based storage
|
|
81
|
+
- **Embedding Model**: The model used for semantic embeddings (e.g., text-embedding-ada-002)
|
|
79
82
|
- **Name**: Display name for the node
|
|
80
83
|
|
|
81
84
|
### AI Model
|
|
@@ -190,10 +193,24 @@ This allows for complex conversation flows where different agents handle differe
|
|
|
190
193
|
|
|
191
194
|
## Advanced Features
|
|
192
195
|
|
|
193
|
-
|
|
194
|
-
- **
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
### 1. Vector Storage (Long-Term Memory)
|
|
197
|
+
The `AI Memory (File)` node supports vector-based storage. When enabled, it can store embeddings of summaries or key information. This allows for **semantic search** using the `query` command.
|
|
198
|
+
|
|
199
|
+
### 2. Memory Consolidation
|
|
200
|
+
Automatically (or manually) summarize conversation threads to save space and maintain long-term context. After a threshold of messages is reached, the node can use an AI model to summarize the history and store it in the vector database.
|
|
201
|
+
|
|
202
|
+
### 3. Memory Commands
|
|
203
|
+
Memory nodes support the following commands via `msg.command`:
|
|
204
|
+
- **add**: Add a message to a conversation (`msg.message` required).
|
|
205
|
+
- **get**: Retrieve messages for a conversation (`msg.conversationId` optional).
|
|
206
|
+
- **search**: Plain-text search across conversations (`msg.query` required).
|
|
207
|
+
- **query**: Semantic (vector) search in long-term memory (`msg.query` text or vector required).
|
|
208
|
+
- **consolidate**: Manually trigger summarization and long-term storage.
|
|
209
|
+
- **clear**: Clear short-term, long-term, or all memory.
|
|
210
|
+
- **delete**: Delete a specific conversation (`msg.conversationId` required).
|
|
211
|
+
|
|
212
|
+
### 4. Template Variables
|
|
213
|
+
Use dynamic values in HTTP requests via `${input.property}` syntax.
|
|
197
214
|
|
|
198
215
|
## Contributing
|
|
199
216
|
|
|
@@ -5,34 +5,46 @@
|
|
|
5
5
|
color: '#a6bbcf',
|
|
6
6
|
defaults: {
|
|
7
7
|
name: { value: "" },
|
|
8
|
-
filename: {
|
|
8
|
+
filename: {
|
|
9
9
|
value: "ai-memories.json",
|
|
10
10
|
required: true,
|
|
11
|
-
validate: function(v) {
|
|
11
|
+
validate: function (v) {
|
|
12
12
|
return v.length > 0;
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
maxConversations: { value: 50, validate: RED.validators.number() },
|
|
16
16
|
maxMessagesPerConversation: { value: 100, validate: RED.validators.number() },
|
|
17
17
|
backupEnabled: { value: true },
|
|
18
|
-
backupCount: { value: 3, validate: RED.validators.number() }
|
|
18
|
+
backupCount: { value: 3, validate: RED.validators.number() },
|
|
19
|
+
vectorEnabled: { value: false },
|
|
20
|
+
embeddingModel: { value: "text-embedding-ada-002" },
|
|
21
|
+
consolidationThreshold: { value: 10, validate: RED.validators.number() }
|
|
19
22
|
},
|
|
23
|
+
|
|
20
24
|
inputs: 1,
|
|
21
25
|
outputs: 1,
|
|
22
26
|
icon: "file.png",
|
|
23
|
-
label: function() {
|
|
27
|
+
label: function () {
|
|
24
28
|
return this.name || "AI Memory (File)";
|
|
25
29
|
},
|
|
26
|
-
labelStyle: function() {
|
|
30
|
+
labelStyle: function () {
|
|
27
31
|
return this.name ? "node_label_italic" : "";
|
|
28
32
|
},
|
|
29
|
-
oneditprepare: function() {
|
|
30
|
-
|
|
33
|
+
oneditprepare: function () {
|
|
34
|
+
$("#node-input-vectorEnabled").on("change", function () {
|
|
35
|
+
if ($(this).is(":checked")) {
|
|
36
|
+
$(".vector-row").show();
|
|
37
|
+
} else {
|
|
38
|
+
$(".vector-row").hide();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
$("#node-input-vectorEnabled").trigger("change");
|
|
31
42
|
},
|
|
32
|
-
|
|
43
|
+
|
|
44
|
+
oneditsave: function () {
|
|
33
45
|
// Handle save if needed
|
|
34
46
|
},
|
|
35
|
-
oneditcancel: function() {
|
|
47
|
+
oneditcancel: function () {
|
|
36
48
|
// Cleanup if needed
|
|
37
49
|
}
|
|
38
50
|
});
|
|
@@ -64,6 +76,32 @@
|
|
|
64
76
|
<label for="node-input-backupCount"><i class="fa fa-history"></i> Backup Count</label>
|
|
65
77
|
<input type="number" id="node-input-backupCount" placeholder="3">
|
|
66
78
|
</div>
|
|
79
|
+
|
|
80
|
+
<hr>
|
|
81
|
+
<h4>AI Context & Consolidation</h4>
|
|
82
|
+
<div class="form-row">
|
|
83
|
+
<label for="node-input-consolidationThreshold"><i class="fa fa-compress"></i> Consolidation</label>
|
|
84
|
+
<input type="number" id="node-input-consolidationThreshold" placeholder="10">
|
|
85
|
+
<div style="margin-left: 105px; font-size: 0.8em; color: #666;">
|
|
86
|
+
Threshold of messages to trigger auto-consolidation.
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="form-row">
|
|
91
|
+
<label for="node-input-vectorEnabled"><i class="fa fa-cube"></i> Long-Term</label>
|
|
92
|
+
<input type="checkbox" id="node-input-vectorEnabled" style="display:inline-block; width:auto; vertical-align:top;">
|
|
93
|
+
<label for="node-input-vectorEnabled" style="width: auto;">Enable Long-Term Vector Memory</label>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="form-row vector-row">
|
|
97
|
+
<label for="node-input-embeddingModel"><i class="fa fa-braille"></i> Embedding</label>
|
|
98
|
+
<select id="node-input-embeddingModel">
|
|
99
|
+
<option value="text-embedding-ada-002">OpenAI Ada 002</option>
|
|
100
|
+
<option value="text-embedding-3-small">OpenAI 3 Small</option>
|
|
101
|
+
<option value="text-embedding-3-large">OpenAI 3 Large</option>
|
|
102
|
+
</select>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
67
105
|
<div class="form-tips">
|
|
68
106
|
<p>Memories will be stored in Node-RED's user directory.</p>
|
|
69
107
|
</div>
|
|
@@ -81,4 +119,4 @@
|
|
|
81
119
|
<dt>payload <span>object|string</span></dt>
|
|
82
120
|
<dd>The processed message with memory operations applied.</dd>
|
|
83
121
|
</dl>
|
|
84
|
-
</script>
|
|
122
|
+
</script>
|
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
|
|
5
|
+
class VectorStorage {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.vectors = [];
|
|
8
|
+
this.metadata = [];
|
|
9
|
+
this.dimensions = options.dimensions || 1536;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
addItem(text, vector, metadata = {}) {
|
|
13
|
+
const id = Math.random().toString(36).substring(7);
|
|
14
|
+
this.vectors.push({ id, vector, text });
|
|
15
|
+
this.metadata.push({ id, ...metadata, timestamp: new Date().toISOString() });
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
search(queryVector, limit = 5) {
|
|
20
|
+
if (!queryVector || this.vectors.length === 0) return [];
|
|
21
|
+
|
|
22
|
+
const results = this.vectors.map((item, index) => {
|
|
23
|
+
return {
|
|
24
|
+
id: item.id,
|
|
25
|
+
text: item.text,
|
|
26
|
+
similarity: this.calculateSimilarity(queryVector, item.vector),
|
|
27
|
+
metadata: this.metadata[index]
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return results
|
|
32
|
+
.sort((a, b) => b.similarity - a.similarity)
|
|
33
|
+
.slice(0, limit);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
calculateSimilarity(vec1, vec2) {
|
|
37
|
+
if (vec1.length !== vec2.length) return 0;
|
|
38
|
+
let dotProduct = 0;
|
|
39
|
+
let normA = 0;
|
|
40
|
+
let normB = 0;
|
|
41
|
+
for (let i = 0; i < vec1.length; i++) {
|
|
42
|
+
dotProduct += vec1[i] * vec2[i];
|
|
43
|
+
normA += vec1[i] * vec1[i];
|
|
44
|
+
normB += vec2[i] * vec2[i];
|
|
45
|
+
}
|
|
46
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toJSON() {
|
|
50
|
+
return {
|
|
51
|
+
vectors: this.vectors,
|
|
52
|
+
metadata: this.metadata,
|
|
53
|
+
dimensions: this.dimensions
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fromJSON(data) {
|
|
58
|
+
if (data) {
|
|
59
|
+
this.vectors = data.vectors || [];
|
|
60
|
+
this.metadata = data.metadata || [];
|
|
61
|
+
this.dimensions = data.dimensions || 1536;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
3
66
|
|
|
4
67
|
class SimpleFileStorage {
|
|
5
68
|
constructor(options = {}) {
|
|
@@ -127,8 +190,10 @@ class SimpleMemoryManager {
|
|
|
127
190
|
this.maxConversations = options.maxConversations || 50;
|
|
128
191
|
this.maxMessagesPerConversation = options.maxMessagesPerConversation || 100;
|
|
129
192
|
this.conversations = [];
|
|
193
|
+
this.longTerm = new VectorStorage();
|
|
130
194
|
}
|
|
131
195
|
|
|
196
|
+
|
|
132
197
|
addMessage(conversationId, message) {
|
|
133
198
|
let conversation = this.conversations.find(c => c.id === conversationId);
|
|
134
199
|
|
|
@@ -220,14 +285,72 @@ class SimpleMemoryManager {
|
|
|
220
285
|
return true;
|
|
221
286
|
}
|
|
222
287
|
|
|
288
|
+
async consolidate(node, msg, aiConfig) {
|
|
289
|
+
if (!msg.conversationId) return { success: false, error: "No conversationId" };
|
|
290
|
+
const conversation = this.getConversation(msg.conversationId);
|
|
291
|
+
if (!conversation || conversation.messages.length < 2) return { success: false, error: "Not enough messages to consolidate" };
|
|
292
|
+
|
|
293
|
+
const textToSummarize = conversation.messages.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const prompt = `Summarize the following conversation for long-term memory storage. Focus on key facts, decisions, and preferences. Keep it concise:\n\n${textToSummarize}`;
|
|
297
|
+
|
|
298
|
+
const response = await axios.post(
|
|
299
|
+
'https://openrouter.ai/api/v1/chat/completions',
|
|
300
|
+
{
|
|
301
|
+
model: aiConfig.model,
|
|
302
|
+
messages: [{ role: 'system', content: 'You are a memory consolidation assistant.' }, { role: 'user', content: prompt }]
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
headers: {
|
|
306
|
+
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
307
|
+
'Content-Type': 'application/json'
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const summary = response.data.choices[0]?.message?.content?.trim();
|
|
313
|
+
if (summary) {
|
|
314
|
+
// Generate embedding for the summary
|
|
315
|
+
const embeddingResponse = await axios.post(
|
|
316
|
+
'https://openrouter.ai/api/v1/embeddings',
|
|
317
|
+
{
|
|
318
|
+
model: 'text-embedding-ada-002', // Default embedding model
|
|
319
|
+
input: summary
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
headers: {
|
|
323
|
+
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
324
|
+
'Content-Type': 'application/json'
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const vector = embeddingResponse.data.data[0].embedding;
|
|
330
|
+
this.longTerm.addItem(summary, vector, {
|
|
331
|
+
conversationId: msg.conversationId,
|
|
332
|
+
type: 'summary',
|
|
333
|
+
originalMessageCount: conversation.messages.length
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return { success: true, summary };
|
|
337
|
+
}
|
|
338
|
+
} catch (error) {
|
|
339
|
+
node.error("Consolidation error: " + error.message);
|
|
340
|
+
return { success: false, error: error.message };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
223
344
|
toJSON() {
|
|
224
345
|
return {
|
|
225
346
|
conversations: this.conversations,
|
|
347
|
+
longTerm: this.longTerm.toJSON(),
|
|
226
348
|
metadata: {
|
|
227
|
-
version: '1.
|
|
349
|
+
version: '1.1',
|
|
228
350
|
lastUpdated: new Date().toISOString(),
|
|
229
351
|
stats: {
|
|
230
352
|
conversationCount: this.conversations.length,
|
|
353
|
+
longTermItemCount: this.longTerm.vectors.length,
|
|
231
354
|
messageCount: this.conversations.reduce((count, conv) => count + conv.messages.length, 0)
|
|
232
355
|
}
|
|
233
356
|
}
|
|
@@ -235,14 +358,18 @@ class SimpleMemoryManager {
|
|
|
235
358
|
}
|
|
236
359
|
|
|
237
360
|
fromJSON(data) {
|
|
238
|
-
if (data
|
|
239
|
-
this.conversations = data.conversations;
|
|
361
|
+
if (data) {
|
|
362
|
+
this.conversations = data.conversations || [];
|
|
363
|
+
if (data.longTerm) {
|
|
364
|
+
this.longTerm.fromJSON(data.longTerm);
|
|
365
|
+
}
|
|
240
366
|
} else {
|
|
241
367
|
this.conversations = [];
|
|
242
368
|
}
|
|
243
369
|
}
|
|
244
370
|
}
|
|
245
371
|
|
|
372
|
+
|
|
246
373
|
module.exports = function (RED) {
|
|
247
374
|
'use strict';
|
|
248
375
|
|
|
@@ -257,6 +384,10 @@ module.exports = function (RED) {
|
|
|
257
384
|
node.maxMessagesPerConversation = parseInt(config.maxMessagesPerConversation) || 100;
|
|
258
385
|
node.backupEnabled = config.backupEnabled !== false;
|
|
259
386
|
node.backupCount = parseInt(config.backupCount) || 3;
|
|
387
|
+
node.vectorEnabled = config.vectorEnabled === true;
|
|
388
|
+
node.embeddingModel = config.embeddingModel || 'text-embedding-ada-002';
|
|
389
|
+
node.consolidationThreshold = parseInt(config.consolidationThreshold) || 10;
|
|
390
|
+
|
|
260
391
|
|
|
261
392
|
const userDir = (RED.settings && RED.settings.userDir) || process.cwd();
|
|
262
393
|
const filePath = path.join(userDir, node.filename);
|
|
@@ -305,10 +436,16 @@ module.exports = function (RED) {
|
|
|
305
436
|
const conversationId = msg.conversationId || 'default';
|
|
306
437
|
const messages = node.memoryManager.getConversationMessages(conversationId);
|
|
307
438
|
|
|
439
|
+
// Auto-consolidate if threshold reached
|
|
440
|
+
if (messages.length >= node.consolidationThreshold && msg.aiagent) {
|
|
441
|
+
node.memoryManager.consolidate(node, msg, msg.aiagent);
|
|
442
|
+
}
|
|
443
|
+
|
|
308
444
|
msg.aimemory = {
|
|
309
445
|
type: 'file',
|
|
310
446
|
conversationId,
|
|
311
|
-
context: messages
|
|
447
|
+
context: messages,
|
|
448
|
+
longTermEnabled: node.vectorEnabled
|
|
312
449
|
};
|
|
313
450
|
}
|
|
314
451
|
|
|
@@ -317,11 +454,12 @@ module.exports = function (RED) {
|
|
|
317
454
|
node.status({
|
|
318
455
|
fill: "green",
|
|
319
456
|
shape: "dot",
|
|
320
|
-
text: `${node.memoryManager.conversations.length}
|
|
457
|
+
text: `${node.memoryManager.conversations.length} convs, ${node.memoryManager.longTerm.vectors.length} long-term`
|
|
321
458
|
});
|
|
322
459
|
|
|
323
460
|
if (done) done();
|
|
324
461
|
} catch (err) {
|
|
462
|
+
|
|
325
463
|
node.error("Error in memory node: " + err.message, msg);
|
|
326
464
|
node.status({ fill: "red", shape: "ring", text: "Error" });
|
|
327
465
|
if (done) done(err);
|
|
@@ -397,6 +535,7 @@ module.exports = function (RED) {
|
|
|
397
535
|
|
|
398
536
|
case 'clear':
|
|
399
537
|
node.memoryManager.clearAllConversations();
|
|
538
|
+
node.memoryManager.longTerm = new VectorStorage();
|
|
400
539
|
|
|
401
540
|
msg.result = {
|
|
402
541
|
success: true,
|
|
@@ -406,11 +545,61 @@ module.exports = function (RED) {
|
|
|
406
545
|
await node.fileStorage.save(node.memoryManager.toJSON());
|
|
407
546
|
break;
|
|
408
547
|
|
|
548
|
+
case 'consolidate':
|
|
549
|
+
if (!msg.aiagent) {
|
|
550
|
+
throw new Error('AI Agent configuration (msg.aiagent) required for consolidation');
|
|
551
|
+
}
|
|
552
|
+
msg.result = await node.memoryManager.consolidate(node, msg, msg.aiagent);
|
|
553
|
+
await node.fileStorage.save(node.memoryManager.toJSON());
|
|
554
|
+
break;
|
|
555
|
+
|
|
556
|
+
case 'query':
|
|
557
|
+
if (!node.vectorEnabled) {
|
|
558
|
+
throw new Error('Vector storage not enabled for this node');
|
|
559
|
+
}
|
|
560
|
+
if (!msg.query) {
|
|
561
|
+
throw new Error('No query text/vector provided');
|
|
562
|
+
}
|
|
563
|
+
if (!msg.aiagent) {
|
|
564
|
+
throw new Error('AI Agent configuration (msg.aiagent) required for semantic search');
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
// Generate embedding for query if it's text
|
|
569
|
+
let queryVector = msg.query;
|
|
570
|
+
if (typeof msg.query === 'string') {
|
|
571
|
+
const embeddingResponse = await axios.post(
|
|
572
|
+
'https://openrouter.ai/api/v1/embeddings',
|
|
573
|
+
{
|
|
574
|
+
model: node.embeddingModel,
|
|
575
|
+
input: msg.query
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
headers: {
|
|
579
|
+
'Authorization': `Bearer ${msg.aiagent.apiKey}`,
|
|
580
|
+
'Content-Type': 'application/json'
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
queryVector = embeddingResponse.data.data[0].embedding;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
msg.result = {
|
|
588
|
+
success: true,
|
|
589
|
+
operation: 'query',
|
|
590
|
+
results: node.memoryManager.longTerm.search(queryVector, msg.limit || 5)
|
|
591
|
+
};
|
|
592
|
+
} catch (error) {
|
|
593
|
+
throw new Error("Semantic search error: " + error.message);
|
|
594
|
+
}
|
|
595
|
+
break;
|
|
596
|
+
|
|
409
597
|
default:
|
|
410
598
|
throw new Error(`Unknown command: ${command}`);
|
|
411
599
|
}
|
|
412
600
|
}
|
|
413
601
|
|
|
602
|
+
|
|
414
603
|
node.on('close', async function () {
|
|
415
604
|
try {
|
|
416
605
|
await node.fileStorage.save(node.memoryManager.toJSON());
|