node-red-contrib-ai-agent 0.0.6 → 0.1.0

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 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
- - **Tool Integration**: Extend functionality with custom tools (Function and HTTP)
194
- - **Context Management**: Maintain conversation history
195
- - **Flexible Configuration**: Customize model parameters and behavior
196
- - **Template Variables**: Use dynamic values in HTTP requests
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
- // Initialize any UI components here
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
- oneditsave: function() {
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.0',
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 && data.conversations) {
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} conversations`
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ai-agent",
3
- "version": "0.0.6",
3
+ "version": "0.1.0",
4
4
  "description": "AI Agent for Node-RED",
5
5
  "repository": {
6
6
  "type": "git",