node-red-contrib-ai-agent 0.0.3 → 0.0.5

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": "node-red-contrib-ai-agent",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "AI Agent for Node-RED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "main": "ai-agent.js",
17
17
  "scripts": {
18
- "test": "echo \"Error: no test specified\" && exit 1"
18
+ "test": "mocha \"test/**/*.js\""
19
19
  },
20
20
  "author": "Milan Lesichkov",
21
21
  "maintainers": [
@@ -28,15 +28,25 @@
28
28
  "files": [
29
29
  "agent/*",
30
30
  "model/*",
31
- "tool/*"
31
+ "tool/*",
32
+ "tool-function/*",
33
+ "tool-http/*",
34
+ "memory-file/*",
35
+ "memory-inmem/*"
32
36
  ],
33
37
  "dependencies": {
34
- "node-red": ">=1.0.0",
35
- "axios": "^1.6.0"
38
+ "axios": "^1.6.0",
39
+ "node-red": ">=1.0.0"
36
40
  },
37
41
  "devDependencies": {
42
+ "chai": "^4.3.0",
38
43
  "eslint": "^8.0.0",
39
- "prettier": "^3.0.0"
44
+ "mocha": "^10.0.0",
45
+ "node-red-node-test-helper": "^0.3.0",
46
+ "prettier": "^3.0.0",
47
+ "proxyquire": "^2.1.3",
48
+ "sinon": "^15.0.0",
49
+ "sinon-chai": "^3.7.0"
40
50
  },
41
51
  "engines": {
42
52
  "node": ">=14.0.0"
@@ -46,7 +56,11 @@
46
56
  "nodes": {
47
57
  "ai-agent": "./agent/ai-agent.js",
48
58
  "ai-model": "./model/ai-model.js",
49
- "ai-tool": "./tool/ai-tool.js"
59
+ "ai-tool": "./tool/ai-tool.js",
60
+ "ai-tool-function": "./tool-function/ai-tool-function.js",
61
+ "ai-tool-http": "./tool-http/ai-tool-http.js",
62
+ "ai-memory-file": "./memory-file/memory-file.js",
63
+ "ai-memory-inmem": "./memory-inmem/memory-inmem.js"
50
64
  }
51
65
  }
52
66
  }
@@ -0,0 +1,95 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ai-tool-function', {
3
+ category: 'AI Agent',
4
+ inputs: 1,
5
+ outputs: 1,
6
+ icon: 'function.svg',
7
+ paletteLabel: 'AI Tool (Function)',
8
+ label: function() {
9
+ return this.name || 'AI Tool (Function)';
10
+ },
11
+ color: '#a6bbcf',
12
+ defaults: {
13
+ name: { value: '' },
14
+ toolName: { value: '', required: true, validate: RED.validators.regex(/^[a-zA-Z0-9_-]+$/) },
15
+ description: { value: '', required: true },
16
+ functionCode: { value: '// Write your function here\n// Available variables: input, context, node\n// Remember to use "new Date()" instead of just "Date()" for date objects\n// Return value will be passed to the next tool or as agent output\nreturn input;' }
17
+ },
18
+ oneditprepare: function() {
19
+ const that = this;
20
+ this.editor = RED.editor.createEditor({
21
+ id: 'node-input-functionCode-editor',
22
+ mode: 'ace/mode/javascript',
23
+ value: $("#node-input-functionCode").val(),
24
+ globals: {
25
+ input: true,
26
+ context: true,
27
+ node: true
28
+ }
29
+ });
30
+
31
+ // Resize function editor when the window size changes
32
+ function resizeEditor() {
33
+ const rows = $('#dialog-form>div:not(.node-text-editor-row)');
34
+ let height = $('#dialog-form').height();
35
+ for (let i=0; i<rows.length; i++) {
36
+ height -= $(rows[i]).outerHeight(true);
37
+ }
38
+ height -= 40;
39
+ $('.node-text-editor').css('height', height+"px");
40
+ that.editor.resize();
41
+ };
42
+ $('#dialog-form').on("dialogresize", resizeEditor);
43
+ $('#dialog-form').on("dialogopen", resizeEditor);
44
+ $('#dialog-form').on("dialogclose", function() {
45
+ $('#dialog-form').off("dialogresize", resizeEditor);
46
+ });
47
+ this.editor.focus();
48
+ },
49
+ oneditsave: function() {
50
+ $("#node-input-functionCode").val(this.editor.getValue());
51
+ this.editor.destroy();
52
+ delete this.editor;
53
+ return true;
54
+ },
55
+ });
56
+ </script>
57
+
58
+ <script type="text/html" data-template-name="ai-tool-function">
59
+ <div class="form-row">
60
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
61
+ <input type="text" id="node-input-name" placeholder="Name">
62
+ </div>
63
+
64
+ <div class="form-row">
65
+ <label for="node-input-toolName">Tool Name</label>
66
+ <input type="text" id="node-input-toolName" placeholder="unique_tool_identifier" style="font-family: monospace;">
67
+ </div>
68
+
69
+ <div class="form-row">
70
+ <label for="node-input-description">Description</label>
71
+ <textarea id="node-input-description" rows="2" placeholder="What this tool does"></textarea>
72
+ </div>
73
+
74
+ <!-- Function Options -->
75
+ <div class="form-row node-text-editor-row">
76
+ <label for="node-input-functionCode">Function Code</label>
77
+ <div style="height: 300px;" class="node-text-editor" id="node-input-functionCode-editor"></div>
78
+ <input type="hidden" id="node-input-functionCode">
79
+ </div>
80
+ </script>
81
+
82
+ <script type="text/html" data-help-name="ai-tool-function">
83
+ <p>Configures a function tool that can be used by the AI Agent.</p>
84
+ <p>Function tools extend the capabilities of the AI Agent by allowing it to execute custom JavaScript code.</p>
85
+ <h3>Function Tool</h3>
86
+ <p>Define custom logic in JavaScript that will be executed when the AI Agent calls this tool.</p>
87
+ <p>Your function has access to:</p>
88
+ <ul>
89
+ <li><code>input</code>: The data passed to the tool by the AI Agent</li>
90
+ <li><code>context</code>: The context object from the AI Agent</li>
91
+ <li><code>node</code>: The Node-RED node object</li>
92
+ </ul>
93
+ <h3>Outputs</h3>
94
+ <p>Adds the function tool definition to <code>msg.aiagent.tools</code> for use by the AI Agent node.</p>
95
+ </script>
@@ -0,0 +1,104 @@
1
+ module.exports = function(RED) {
2
+ 'use strict';
3
+
4
+ function AIToolNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Configuration
9
+ node.name = config.name || 'function_tool';
10
+ node.toolName = config.toolName || `function_${Date.now()}`;
11
+ node.description = config.description || 'JavaScript function tool';
12
+
13
+ // Create a safe function that can be called later
14
+ try {
15
+ // Ensure the function code is properly formatted
16
+ let functionCode = config.functionCode || 'return input;';
17
+
18
+ // Check if the function code already returns a value
19
+ if (!functionCode.trim().startsWith('return') && !functionCode.includes('return ')) {
20
+ functionCode = `return (${functionCode});`;
21
+ }
22
+
23
+ // Create the function with proper error handling
24
+ node.fn = new Function('input', 'context', 'node',
25
+ `try {
26
+ ${functionCode}
27
+ } catch(e) {
28
+ return { error: e.message };
29
+ }`
30
+ );
31
+ } catch (e) {
32
+ node.error(`Error creating function: ${e.message}`);
33
+ node.fn = (input) => ({ error: 'Invalid function' });
34
+ }
35
+
36
+ // Process incoming messages
37
+ node.on('input', function(msg, send, done) {
38
+ try {
39
+ // Clone the message to avoid modifying the original
40
+ const newMsg = RED.util.cloneMessage(msg);
41
+
42
+ // Initialize msg.aiagent if it doesn't exist
43
+ newMsg.aiagent = newMsg.aiagent || {};
44
+ newMsg.aiagent.tools = newMsg.aiagent.tools || [];
45
+
46
+ // Create tool definition according to OpenAI API format
47
+ const toolDef = {
48
+ type: 'function',
49
+ function: {
50
+ name: node.toolName,
51
+ description: node.description,
52
+ parameters: {
53
+ type: 'object',
54
+ properties: {},
55
+ required: []
56
+ }
57
+ },
58
+ // Add execute method for our internal use
59
+ execute: (input, context) => executeFunctionTool(node.fn, input, context, node)
60
+ };
61
+
62
+ // Add tool to the list
63
+ newMsg.aiagent.tools.push(toolDef);
64
+
65
+ // Update node status
66
+ node.status({ fill: 'green', shape: 'dot', text: 'Ready' });
67
+
68
+ // Send the modified message
69
+ send([newMsg, null]);
70
+
71
+ // Complete the async operation
72
+ if (done) {
73
+ done();
74
+ }
75
+ } catch (error) {
76
+ node.status({ fill: 'red', shape: 'ring', text: 'Error' });
77
+ node.error('Error in AI Tool node: ' + error.message, msg);
78
+ if (done) done();
79
+ }
80
+ });
81
+
82
+ // Handle node cleanup
83
+ node.on('close', function(done) {
84
+ node.status({});
85
+ if (done) done();
86
+ });
87
+ }
88
+ // Helper function to execute JavaScript function tool
89
+ async function executeFunctionTool(fn, input, context, node) {
90
+ try {
91
+ return await fn(input, context, node);
92
+ } catch (error) {
93
+ node.error(`Function Tool Error: ${error.message}`, { payload: input });
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ // Register the node type
99
+ RED.nodes.registerType('ai-tool-function', AIToolNode, {
100
+ settings: {
101
+ // Any node settings can go here
102
+ }
103
+ });
104
+ };
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
3
+ <rect x="10" y="10" width="80" height="80" rx="10" fill="#a6bbcf" stroke="#7a9cbc" stroke-width="2"/>
4
+ <path d="M30,30 L70,30 L70,50 L30,50 Z" fill="white" stroke="#5a7a99" stroke-width="2"/>
5
+ <path d="M40,60 L60,60 L60,70 L40,70 Z" fill="white" stroke="#5a7a99" stroke-width="2"/>
6
+ <path d="M30,35 L40,35" stroke="#5a7a99" stroke-width="2"/>
7
+ <path d="M30,45 L50,45" stroke="#5a7a99" stroke-width="2"/>
8
+ </svg>
@@ -0,0 +1,174 @@
1
+ <!-- AI Tool HTTP Node -->
2
+ <script type="text/javascript">
3
+ RED.nodes.registerType('ai-tool-http', {
4
+ category: 'AI Agent',
5
+ color: '#a6bbcf',
6
+ defaults: {
7
+ name: { value: "" },
8
+ toolName: { value: "", required: true },
9
+ description: { value: "HTTP request tool" },
10
+ httpMethod: { value: "GET" },
11
+ httpUrl: { value: "", required: true },
12
+ httpHeaders: { value: "{}" },
13
+ httpBody: { value: "" }
14
+ },
15
+ inputs: 1,
16
+ outputs: 2,
17
+ outputLabels: ["success", "error"],
18
+ icon: "font-awesome/fa-globe",
19
+ label: function() {
20
+ return this.name || "HTTP Tool";
21
+ },
22
+ paletteLabel: "AI HTTP Tool",
23
+ oneditprepare: function() {
24
+ // Initialize the ACE editor for JSON headers
25
+ const headersEditor = RED.editor.createEditor({
26
+ id: 'node-input-headers-editor',
27
+ mode: 'ace/mode/json',
28
+ value: this.httpHeaders || '{}'
29
+ });
30
+
31
+ // Initialize the ACE editor for the body
32
+ const bodyEditor = RED.editor.createEditor({
33
+ id: 'node-input-body-editor',
34
+ mode: 'ace/mode/text',
35
+ value: this.httpBody || ''
36
+ });
37
+
38
+ // Handle resizing
39
+ function resizeEditors() {
40
+ const rows = $('#dialog-form>div:not(.node-text-editor-row)');
41
+ let height = $('#dialog-form').height();
42
+
43
+ for (let i=0; i<rows.length; i++) {
44
+ height -= $(rows[i]).outerHeight(true);
45
+ }
46
+
47
+ const editorRow = $('#dialog-form>div.node-text-editor-row');
48
+ height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
49
+
50
+ // Split the height between the two editors
51
+ const headersHeight = Math.floor(height/2);
52
+ const bodyHeight = height - headersHeight - 30; // 30px for the label between them
53
+
54
+ $('#node-input-headers-editor').css("height", headersHeight + "px");
55
+ headersEditor.resize();
56
+
57
+ $('#node-input-body-editor').css("height", bodyHeight + "px");
58
+ bodyEditor.resize();
59
+ }
60
+
61
+ $('#dialog-form').css("min-width", "500px").css("min-height", "400px");
62
+
63
+ // Do the resize after a delay to allow the dialog to be fully rendered
64
+ setTimeout(resizeEditors, 10);
65
+
66
+ // Handle form submission
67
+ $('#node-input-dialog-ok').on('click', () => {
68
+ this.httpHeaders = headersEditor.getValue();
69
+ this.httpBody = bodyEditor.getValue();
70
+ });
71
+
72
+ // Handle editor cleanup
73
+ this.editor = {
74
+ destroy: function() {
75
+ headersEditor.destroy();
76
+ bodyEditor.destroy();
77
+ }
78
+ };
79
+ },
80
+ oneditsave: function() {
81
+ // The values are already saved in oneditprepare
82
+ },
83
+ oneditcancel: function() {
84
+ // Clean up the editors
85
+ if (this.editor) {
86
+ this.editor.destroy();
87
+ }
88
+ }
89
+ });
90
+ </script>
91
+
92
+ <script type="text/html" data-template-name="ai-tool-http">
93
+ <div class="form-row">
94
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
95
+ <input type="text" id="node-input-name" placeholder="Name">
96
+ </div>
97
+ <div class="form-row">
98
+ <label for="node-input-toolName"><i class="fa fa-wrench"></i> Tool Name</label>
99
+ <input type="text" id="node-input-toolName" placeholder="http_request">
100
+ </div>
101
+ <div class="form-row">
102
+ <label for="node-input-description"><i class="fa fa-info-circle"></i> Description</label>
103
+ <input type="text" id="node-input-description" placeholder="HTTP request tool">
104
+ </div>
105
+ <div class="form-row">
106
+ <label for="node-input-httpMethod"><i class="fa fa-exchange"></i> Method</label>
107
+ <select id="node-input-httpMethod">
108
+ <option value="GET">GET</option>
109
+ <option value="POST">POST</option>
110
+ <option value="PUT">PUT</option>
111
+ <option value="DELETE">DELETE</option>
112
+ <option value="PATCH">PATCH</option>
113
+ <option value="HEAD">HEAD</option>
114
+ <option value="OPTIONS">OPTIONS</option>
115
+ </select>
116
+ </div>
117
+ <div class="form-row">
118
+ <label for="node-input-httpUrl"><i class="fa fa-globe"></i> URL</label>
119
+ <input type="text" id="node-input-httpUrl" placeholder="https://api.example.com/endpoint">
120
+ </div>
121
+ <div class="form-row">
122
+ <label for="node-input-headers-editor"><i class="fa fa-list"></i> Headers (JSON)</label>
123
+ <div style="height: 150px; min-height: 150px;" class="node-text-editor" id="node-input-headers-editor"></div>
124
+ <input type="hidden" id="node-input-httpHeaders">
125
+ </div>
126
+ <div class="form-row">
127
+ <label for="node-input-body-editor"><i class="fa fa-file-text-o"></i> Body</label>
128
+ <div style="height: 150px; min-height: 150px;" class="node-text-editor" id="node-input-body-editor"></div>
129
+ <input type="hidden" id="node-input-httpBody">
130
+ </div>
131
+ <div class="form-tips">
132
+ <p>Use <code>${input.property}</code> syntax to reference input properties in URL, headers, and body.</p>
133
+ <p>Example: <code>https://api.example.com/users/${input.userId}</code></p>
134
+ </div>
135
+ </script>
136
+
137
+ <script type="text/html" data-help-name="ai-tool-http">
138
+ <p>Creates an HTTP request tool for AI Agent.</p>
139
+
140
+ <h3>Inputs</h3>
141
+ <dl class="message-properties">
142
+ <dt>payload <span class="property-type">any</span></dt>
143
+ <dd>The input message payload.</dd>
144
+ </dl>
145
+
146
+ <h3>Outputs</h3>
147
+ <ol class="node-ports">
148
+ <li>Standard output
149
+ <dl class="message-properties">
150
+ <dt>payload <span class="property-type">object</span></dt>
151
+ <dd>The original payload.</dd>
152
+ <dt>aiagent.tools <span class="property-type">array</span></dt>
153
+ <dd>Array containing the HTTP tool definition.</dd>
154
+ </dl>
155
+ </li>
156
+ <li>Error output
157
+ <dl class="message-properties">
158
+ <dt>error <span class="property-type">object</span></dt>
159
+ <dd>Error information if the node fails.</dd>
160
+ </dl>
161
+ </li>
162
+ </ol>
163
+
164
+ <h3>Details</h3>
165
+ <p>This node creates an HTTP request tool that can be used by the AI Agent node to make HTTP requests.</p>
166
+ <p>The tool can be configured with a method, URL, headers, and body.</p>
167
+ <p>You can use template variables in the URL, headers, and body to reference properties from the input object that the AI provides when calling the tool.</p>
168
+ <p>Example: <code>https://api.example.com/users/${input.userId}</code></p>
169
+
170
+ <h3>References</h3>
171
+ <ul>
172
+ <li><a href="https://github.com/lesichkovm/node-red-contrib-ai-agent" target="_blank">GitHub</a> - the nodes github repository</li>
173
+ </ul>
174
+ </script>
@@ -0,0 +1,176 @@
1
+ module.exports = function(RED) {
2
+ 'use strict';
3
+
4
+ function AIToolHttpNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Configuration
9
+ node.name = config.name || 'http_tool';
10
+ node.toolName = config.toolName || `http_${Date.now()}`;
11
+ node.description = config.description || 'HTTP request tool';
12
+ node.httpMethod = config.httpMethod || 'GET';
13
+ node.httpUrl = config.httpUrl || '';
14
+ node.httpHeaders = config.httpHeaders || '{}';
15
+ node.httpBody = config.httpBody || '';
16
+
17
+ // Process incoming messages
18
+ node.on('input', function(msg, send, done) {
19
+ try {
20
+ // Clone the message to avoid modifying the original
21
+ const newMsg = RED.util.cloneMessage(msg);
22
+
23
+ // Initialize msg.aiagent if it doesn't exist
24
+ newMsg.aiagent = newMsg.aiagent || {};
25
+ newMsg.aiagent.tools = newMsg.aiagent.tools || [];
26
+
27
+ // Create tool definition according to OpenAI API format
28
+ const toolDef = {
29
+ type: 'function',
30
+ function: {
31
+ name: node.toolName,
32
+ description: node.description,
33
+ parameters: {
34
+ type: 'object',
35
+ properties: {},
36
+ required: []
37
+ }
38
+ },
39
+ // Add execute method for our internal use
40
+ execute: (input, context) => executeHttpTool({
41
+ method: node.httpMethod,
42
+ url: node.httpUrl,
43
+ headers: tryParseJson(node.httpHeaders),
44
+ body: node.httpBody
45
+ }, input, node)
46
+ };
47
+
48
+ // Add tool to the list
49
+ newMsg.aiagent.tools.push(toolDef);
50
+
51
+ // Update node status
52
+ node.status({ fill: 'green', shape: 'dot', text: 'Ready' });
53
+
54
+ // Send the modified message
55
+ send([newMsg, null]);
56
+
57
+ // Complete the async operation
58
+ if (done) {
59
+ done();
60
+ }
61
+ } catch (error) {
62
+ node.status({ fill: 'red', shape: 'ring', text: 'Error' });
63
+ node.error('Error in AI Tool HTTP node: ' + error.message, msg);
64
+ if (done) done();
65
+ }
66
+ });
67
+
68
+ // Handle node cleanup
69
+ node.on('close', function(done) {
70
+ node.status({});
71
+ if (done) done();
72
+ });
73
+ }
74
+
75
+ // Helper function to execute HTTP tool
76
+ async function executeHttpTool(config, input, node) {
77
+ // Import axios here so it can be more easily mocked in tests
78
+ const axios = require('axios');
79
+ return executeHttpRequest(axios, config, input, node);
80
+ }
81
+
82
+ // Separate function for better testability
83
+ async function executeHttpRequest(axios, config, input, node) {
84
+ const { method, url, headers, body: bodyTemplate } = config;
85
+
86
+ try {
87
+ // Process template strings in URL and headers
88
+ const processedUrl = processTemplate(url, input);
89
+ const processedHeaders = Object.fromEntries(
90
+ Object.entries(headers).map(([key, value]) => [
91
+ processTemplate(key, input),
92
+ typeof value === 'string' ? processTemplate(value, input) : value
93
+ ])
94
+ );
95
+
96
+ // Process request body if it's a template
97
+ let requestBody = bodyTemplate;
98
+ if (typeof bodyTemplate === 'string') {
99
+ try {
100
+ // First try to parse as JSON to handle JSON templates
101
+ requestBody = JSON.parse(processTemplate(bodyTemplate, input));
102
+ } catch (e) {
103
+ // If not valid JSON, use as plain string
104
+ requestBody = processTemplate(bodyTemplate, input);
105
+ }
106
+ }
107
+
108
+ // Make the HTTP request
109
+ const response = await axios.request({
110
+ method,
111
+ url: processedUrl,
112
+ headers: processedHeaders,
113
+ data: method !== 'GET' && method !== 'HEAD' ? requestBody : undefined,
114
+ validateStatus: () => true // Always resolve, never reject
115
+ });
116
+
117
+ return {
118
+ status: response.status,
119
+ headers: response.headers,
120
+ data: response.data
121
+ };
122
+
123
+ } catch (error) {
124
+ node.error(`HTTP Tool Error: ${error.message}`, { payload: input });
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ // Helper function to process template strings with input data
130
+ function processTemplate(template, data) {
131
+ if (typeof template !== 'string') return template;
132
+
133
+ return template.replace(/\$\{([^}]+)\}/g, (match, key) => {
134
+ try {
135
+ // Handle nested properties using path notation
136
+ const value = key.split('.').reduce((obj, k) => {
137
+ if (obj === null || obj === undefined) return undefined;
138
+ return obj[k];
139
+ }, data);
140
+
141
+ // Convert undefined/null to empty string to avoid 'undefined' in output
142
+ if (value === undefined || value === null) return match;
143
+
144
+ // Handle different types of values
145
+ if (typeof value === 'object') {
146
+ try {
147
+ return JSON.stringify(value);
148
+ } catch (e) {
149
+ return String(value);
150
+ }
151
+ }
152
+
153
+ return String(value);
154
+ } catch (e) {
155
+ // If any error occurs during processing, return the original template
156
+ return match;
157
+ }
158
+ });
159
+ }
160
+
161
+ // Helper function to safely parse JSON
162
+ function tryParseJson(jsonString) {
163
+ try {
164
+ return JSON.parse(jsonString);
165
+ } catch (e) {
166
+ return {};
167
+ }
168
+ }
169
+
170
+ // Register the node type
171
+ RED.nodes.registerType('ai-tool-http', AIToolHttpNode, {
172
+ settings: {
173
+ // Any node settings can go here
174
+ }
175
+ });
176
+ };