node-red-contrib-ai-agent 0.0.1 → 0.0.3
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 +80 -9
- package/agent/ai-agent-icon.svg +11 -0
- package/agent/ai-agent.html +96 -0
- package/agent/ai-agent.js +216 -0
- package/model/ai-model-icon.svg +11 -0
- package/model/ai-model.html +338 -0
- package/model/ai-model.js +73 -0
- package/package.json +52 -36
- package/tool/ai-tool-icon.svg +8 -0
- package/tool/ai-tool.html +159 -0
- package/tool/ai-tool.js +220 -0
- package/.gitpod.yml +0 -10
- package/ai-agent.html +0 -35
- package/ai-agent.js +0 -18
- package/icons/icon.svg +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ai-tool', {
|
|
3
|
+
category: 'AI Agent',
|
|
4
|
+
inputs: 1,
|
|
5
|
+
outputs: 1,
|
|
6
|
+
icon: 'function.svg',
|
|
7
|
+
paletteLabel: 'AI Tool',
|
|
8
|
+
label: function() {
|
|
9
|
+
return this.name || 'AI Tool';
|
|
10
|
+
},
|
|
11
|
+
color: '#a6bbcf',
|
|
12
|
+
defaults: {
|
|
13
|
+
name: { value: '' },
|
|
14
|
+
toolType: { value: 'http', required: true },
|
|
15
|
+
toolName: { value: '', required: true, validate: RED.validators.regex(/^[a-zA-Z0-9_-]+$/) },
|
|
16
|
+
description: { value: '', required: true },
|
|
17
|
+
httpMethod: { value: 'GET' },
|
|
18
|
+
httpUrl: { value: '' },
|
|
19
|
+
httpHeaders: { value: '{}', validate: function(v) {
|
|
20
|
+
try {
|
|
21
|
+
JSON.parse(v);
|
|
22
|
+
return true;
|
|
23
|
+
} catch(e) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}},
|
|
27
|
+
httpBody: { value: '' },
|
|
28
|
+
functionCode: { value: '// Write your function here\n// Available variables: input, context, node\n// Return value will be passed to the next tool or as agent output\nreturn input;' },
|
|
29
|
+
nodeRedFlow: { value: 'flow' },
|
|
30
|
+
nodeRedNode: { value: '' },
|
|
31
|
+
nodeRedProperty: { value: 'payload' }
|
|
32
|
+
},
|
|
33
|
+
oneditprepare: function() {
|
|
34
|
+
// Show/hide fields based on tool type
|
|
35
|
+
$('#node-input-toolType').on('change', function() {
|
|
36
|
+
$('.tool-type-options').hide();
|
|
37
|
+
$(`#${this.value}-options`).show();
|
|
38
|
+
}).trigger('change');
|
|
39
|
+
|
|
40
|
+
// Initialize JSON editor for headers
|
|
41
|
+
const headersEditor = RED.editor.createEditor({
|
|
42
|
+
id: 'node-input-http-headers',
|
|
43
|
+
value: $('#node-input-http-headers').val() || '{}',
|
|
44
|
+
mode: 'application/json',
|
|
45
|
+
onComplete: function() {
|
|
46
|
+
$('#node-input-http-headers').val(headersEditor.getText());
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
oneditsave: function() {
|
|
51
|
+
// Validate and format JSON before saving
|
|
52
|
+
try {
|
|
53
|
+
if (this.httpHeaders) {
|
|
54
|
+
JSON.parse(this.httpHeaders);
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
alert('Invalid JSON in Headers');
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<script type="text/html" data-template-name="ai-tool">
|
|
66
|
+
<div class="form-row">
|
|
67
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
68
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="form-row">
|
|
72
|
+
<label for="node-input-toolType">Tool Type</label>
|
|
73
|
+
<select id="node-input-toolType" style="width: 70%;">
|
|
74
|
+
<option value="http">HTTP Request</option>
|
|
75
|
+
<option value="function">JavaScript Function</option>
|
|
76
|
+
<option value="node-red">Node-RED Node</option>
|
|
77
|
+
</select>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="form-row">
|
|
81
|
+
<label for="node-input-toolName">Tool Name</label>
|
|
82
|
+
<input type="text" id="node-input-toolName" placeholder="unique_tool_identifier" style="font-family: monospace;">
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="form-row">
|
|
86
|
+
<label for="node-input-description">Description</label>
|
|
87
|
+
<textarea id="node-input-description" rows="2" placeholder="What this tool does"></textarea>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- HTTP Options -->
|
|
91
|
+
<div id="http-options" class="tool-type-options">
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-input-httpMethod">Method</label>
|
|
94
|
+
<select id="node-input-httpMethod" style="width: 30%;">
|
|
95
|
+
<option value="GET">GET</option>
|
|
96
|
+
<option value="POST">POST</option>
|
|
97
|
+
<option value="PUT">PUT</option>
|
|
98
|
+
<option value="DELETE">DELETE</option>
|
|
99
|
+
<option value="PATCH">PATCH</option>
|
|
100
|
+
</select>
|
|
101
|
+
<input type="text" id="node-input-httpUrl" placeholder="https://example.com/endpoint" style="width: 70%;">
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="form-row">
|
|
105
|
+
<label for="node-input-httpHeaders">Headers (JSON)</label>
|
|
106
|
+
<div id="node-input-http-headers-editor" style="height: 100px; width: 100%;"></div>
|
|
107
|
+
<input type="hidden" id="node-input-http-headers">
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="form-row">
|
|
111
|
+
<label for="node-input-httpBody">Body (for POST/PUT/PATCH)</label>
|
|
112
|
+
<textarea id="node-input-httpBody" rows="3" placeholder="Request body (leave empty for GET/DELETE)"></textarea>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- Function Options -->
|
|
117
|
+
<div id="function-options" class="tool-type-options" style="display: none;">
|
|
118
|
+
<div class="form-row">
|
|
119
|
+
<label for="node-input-functionCode">Function Code</label>
|
|
120
|
+
<div style="height: 200px;">
|
|
121
|
+
<textarea id="node-input-functionCode" style="width: 100%; height: 100%; font-family: monospace;"></textarea>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- Node-RED Options -->
|
|
127
|
+
<div id="node-red-options" class="tool-type-options" style="display: none;">
|
|
128
|
+
<div class="form-row">
|
|
129
|
+
<label for="node-input-nodeRedFlow">Flow Context</label>
|
|
130
|
+
<select id="node-input-nodeRedFlow" style="width: 100%;">
|
|
131
|
+
<option value="flow">Current Flow</option>
|
|
132
|
+
<option value="global">Global</option>
|
|
133
|
+
</select>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div class="form-row">
|
|
137
|
+
<label for="node-input-nodeRedNode">Node ID</label>
|
|
138
|
+
<input type="text" id="node-input-nodeRedNode" placeholder="Node ID to call" style="width: 100%;">
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="form-row">
|
|
142
|
+
<label for="node-input-nodeRedProperty">Property</label>
|
|
143
|
+
<input type="text" id="node-input-nodeRedProperty" value="payload" placeholder="Property to set (default: payload)" style="width: 100%;">
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<script type="text/html" data-help-name="ai-tool">
|
|
149
|
+
<p>Configures a tool that can be used by the AI Agent.</p>
|
|
150
|
+
<p>Tools extend the capabilities of the AI Agent by allowing it to interact with external systems, perform calculations, or access Node-RED nodes.</p>
|
|
151
|
+
<h3>Tool Types</h3>
|
|
152
|
+
<ul>
|
|
153
|
+
<li><strong>HTTP Request</strong>: Call external APIs</li>
|
|
154
|
+
<li><strong>JavaScript Function</strong>: Define custom logic in JavaScript</li>
|
|
155
|
+
<li><strong>Node-RED Node</strong>: Interact with other nodes in your flow</li>
|
|
156
|
+
</ul>
|
|
157
|
+
<h3>Outputs</h3>
|
|
158
|
+
<p>Adds the tool definition to <code>msg.aiagent.tools</code> for use by the AI Agent node.</p>
|
|
159
|
+
</script>
|
package/tool/ai-tool.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
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 || `tool_${config.toolType}`;
|
|
10
|
+
node.toolType = config.toolType || 'function';
|
|
11
|
+
node.toolName = config.toolName || `${node.toolType}_${Date.now()}`;
|
|
12
|
+
node.description = config.description || `${node.toolType} tool`;
|
|
13
|
+
|
|
14
|
+
// Initialize based on tool type
|
|
15
|
+
if (node.toolType === 'function') {
|
|
16
|
+
// Create a safe function that can be called later
|
|
17
|
+
try {
|
|
18
|
+
node.fn = new Function('input', 'context', 'node',
|
|
19
|
+
`try { ${config.functionCode || 'return input;'} } catch(e) { return { error: e.message }; }`
|
|
20
|
+
);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
node.error(`Error creating function: ${e.message}`);
|
|
23
|
+
node.fn = (input) => ({ error: 'Invalid function' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Process incoming messages
|
|
28
|
+
node.on('input', function(msg, send, done) {
|
|
29
|
+
try {
|
|
30
|
+
// Clone the message to avoid modifying the original
|
|
31
|
+
const newMsg = RED.util.cloneMessage(msg);
|
|
32
|
+
|
|
33
|
+
// Initialize msg.aiagent if it doesn't exist
|
|
34
|
+
newMsg.aiagent = newMsg.aiagent || {};
|
|
35
|
+
newMsg.aiagent.tools = newMsg.aiagent.tools || [];
|
|
36
|
+
|
|
37
|
+
// Create tool definition
|
|
38
|
+
const toolDef = {
|
|
39
|
+
name: node.toolName,
|
|
40
|
+
description: node.description,
|
|
41
|
+
type: node.toolType
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Add tool-specific configuration
|
|
45
|
+
if (node.toolType === 'http') {
|
|
46
|
+
toolDef.config = {
|
|
47
|
+
method: config.httpMethod || 'GET',
|
|
48
|
+
url: config.httpUrl || '',
|
|
49
|
+
headers: tryParseJson(config.httpHeaders) || {},
|
|
50
|
+
body: config.httpBody || null
|
|
51
|
+
};
|
|
52
|
+
toolDef.execute = (input) => executeHttpTool(toolDef.config, input, node);
|
|
53
|
+
}
|
|
54
|
+
else if (node.toolType === 'function') {
|
|
55
|
+
toolDef.execute = (input, context) => executeFunctionTool(node.fn, input, context, node);
|
|
56
|
+
}
|
|
57
|
+
else if (node.toolType === 'node-red') {
|
|
58
|
+
toolDef.config = {
|
|
59
|
+
flowContext: config.nodeRedFlow || 'flow',
|
|
60
|
+
nodeId: config.nodeRedNode || '',
|
|
61
|
+
property: config.nodeRedProperty || 'payload'
|
|
62
|
+
};
|
|
63
|
+
toolDef.execute = (input) => executeNodeRedTool(toolDef.config, input, node, msg._msgid);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Add tool to the list
|
|
67
|
+
newMsg.aiagent.tools.push(toolDef);
|
|
68
|
+
|
|
69
|
+
// Update node status
|
|
70
|
+
node.status({ fill: 'green', shape: 'dot', text: 'Ready' });
|
|
71
|
+
|
|
72
|
+
// Send the modified message
|
|
73
|
+
send([newMsg, null]);
|
|
74
|
+
|
|
75
|
+
// Complete the async operation
|
|
76
|
+
if (done) {
|
|
77
|
+
done();
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
|
|
81
|
+
node.error('Error in AI Tool node: ' + error.message, msg);
|
|
82
|
+
if (done) done();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Handle node cleanup
|
|
87
|
+
node.on('close', function(done) {
|
|
88
|
+
node.status({});
|
|
89
|
+
if (done) done();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Helper function to execute HTTP tool
|
|
94
|
+
async function executeHttpTool(config, input, node) {
|
|
95
|
+
const axios = require('axios');
|
|
96
|
+
const { method, url, headers, body: bodyTemplate } = config;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Process template strings in URL and headers
|
|
100
|
+
const processedUrl = processTemplate(url, input);
|
|
101
|
+
const processedHeaders = Object.fromEntries(
|
|
102
|
+
Object.entries(headers).map(([key, value]) => [
|
|
103
|
+
processTemplate(key, input),
|
|
104
|
+
typeof value === 'string' ? processTemplate(value, input) : value
|
|
105
|
+
])
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Process request body if it's a template
|
|
109
|
+
let requestBody = bodyTemplate;
|
|
110
|
+
if (typeof bodyTemplate === 'string') {
|
|
111
|
+
try {
|
|
112
|
+
// First try to parse as JSON to handle JSON templates
|
|
113
|
+
requestBody = JSON.parse(processTemplate(bodyTemplate, input));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// If not valid JSON, use as plain string
|
|
116
|
+
requestBody = processTemplate(bodyTemplate, input);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Make the HTTP request
|
|
121
|
+
const response = await axios({
|
|
122
|
+
method,
|
|
123
|
+
url: processedUrl,
|
|
124
|
+
headers: processedHeaders,
|
|
125
|
+
data: method !== 'GET' && method !== 'HEAD' ? requestBody : undefined,
|
|
126
|
+
validateStatus: () => true // Always resolve, never reject
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
status: response.status,
|
|
131
|
+
headers: response.headers,
|
|
132
|
+
data: response.data
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
node.error(`HTTP Tool Error: ${error.message}`, { payload: input });
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Helper function to execute JavaScript function tool
|
|
142
|
+
async function executeFunctionTool(fn, input, context, node) {
|
|
143
|
+
try {
|
|
144
|
+
return await fn(input, context, node);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
node.error(`Function Tool Error: ${error.message}`, { payload: input });
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Helper function to execute Node-RED node tool
|
|
152
|
+
async function executeNodeRedTool(config, input, node, msgId) {
|
|
153
|
+
const { flowContext, nodeId, property } = config;
|
|
154
|
+
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
try {
|
|
157
|
+
// Get the target node
|
|
158
|
+
const targetNode = RED.nodes.getNode(nodeId);
|
|
159
|
+
if (!targetNode) {
|
|
160
|
+
throw new Error(`Node ${nodeId} not found`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create a new message to send to the target node
|
|
164
|
+
const msg = {
|
|
165
|
+
_msgid: msgId || RED.util.generateId(),
|
|
166
|
+
[property]: input,
|
|
167
|
+
_toolContext: {
|
|
168
|
+
sourceNode: node.id,
|
|
169
|
+
timestamp: Date.now()
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Set up a one-time listener for the response
|
|
174
|
+
const responseHandler = (response) => {
|
|
175
|
+
// Clean up the listener to prevent memory leaks
|
|
176
|
+
RED.events.off('node:' + nodeId, responseHandler);
|
|
177
|
+
|
|
178
|
+
// Resolve with the response
|
|
179
|
+
resolve(response[property] || response.payload || response);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Listen for the response
|
|
183
|
+
RED.events.on('node:' + nodeId, responseHandler);
|
|
184
|
+
|
|
185
|
+
// Send the message to the target node
|
|
186
|
+
targetNode.receive(msg);
|
|
187
|
+
|
|
188
|
+
} catch (error) {
|
|
189
|
+
node.error(`Node-RED Tool Error: ${error.message}`, { payload: input });
|
|
190
|
+
reject(error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Helper function to process template strings with input data
|
|
196
|
+
function processTemplate(template, data) {
|
|
197
|
+
if (typeof template !== 'string') return template;
|
|
198
|
+
|
|
199
|
+
return template.replace(/\$\{([^}]+)\}/g, (match, key) => {
|
|
200
|
+
const value = key.split('.').reduce((obj, k) => (obj || {})[k], data);
|
|
201
|
+
return value !== undefined ? value : match;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Helper function to safely parse JSON
|
|
206
|
+
function tryParseJson(jsonString) {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(jsonString);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Register the node type
|
|
215
|
+
RED.nodes.registerType('ai-tool', AIToolNode, {
|
|
216
|
+
settings: {
|
|
217
|
+
// Any node settings can go here
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
};
|
package/.gitpod.yml
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# This configuration file was automatically generated by Gitpod.
|
|
2
|
-
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
|
|
3
|
-
# and commit this file to your remote git repository to share the goodness with others.
|
|
4
|
-
|
|
5
|
-
# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart
|
|
6
|
-
|
|
7
|
-
tasks:
|
|
8
|
-
- init: yarn install
|
|
9
|
-
|
|
10
|
-
|
package/ai-agent.html
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<script type="text/javascript">
|
|
2
|
-
RED.nodes.registerType('ai-agent', {
|
|
3
|
-
category: 'storage',
|
|
4
|
-
color: '#a6bbcf',
|
|
5
|
-
defaults: {
|
|
6
|
-
name: { value: '' },
|
|
7
|
-
},
|
|
8
|
-
inputs: 1,
|
|
9
|
-
outputs: 1,
|
|
10
|
-
icon: 'icon.svg',
|
|
11
|
-
label: function () {
|
|
12
|
-
return this.name || 'AI Agent'
|
|
13
|
-
},
|
|
14
|
-
oneditprepare: function () {},
|
|
15
|
-
oneditsave: function () {},
|
|
16
|
-
oneditresize: function (size) {},
|
|
17
|
-
})
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<!-- START: Template -->
|
|
21
|
-
<script type="text/x-red" data-template-name="ai-agent">
|
|
22
|
-
<div class="form-row">
|
|
23
|
-
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
24
|
-
<input type="text" id="node-input-name">
|
|
25
|
-
</div>
|
|
26
|
-
</script>
|
|
27
|
-
<!-- END: Template -->
|
|
28
|
-
|
|
29
|
-
<!-- START: Help -->
|
|
30
|
-
<script type="text/x-red" data-help-name="ai-agent">
|
|
31
|
-
<p>
|
|
32
|
-
A "batteries included" AI Agent for Node-RED designed to be flexible, portable, and fun to use.
|
|
33
|
-
</p>
|
|
34
|
-
</script>
|
|
35
|
-
<!-- END: Help -->
|
package/ai-agent.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module.exports = function (RED) {
|
|
2
|
-
function AiAgentNode(config) {
|
|
3
|
-
RED.nodes.createNode(this, config)
|
|
4
|
-
var node = this
|
|
5
|
-
|
|
6
|
-
node.on('close', (done) => {
|
|
7
|
-
node.done()
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
node.on('input', (msg, send, done) => {
|
|
11
|
-
send(msg)
|
|
12
|
-
done()
|
|
13
|
-
})
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Registering the node-red type
|
|
17
|
-
RED.nodes.registerType('ai-agent', AiAgentNode)
|
|
18
|
-
}
|
package/icons/icon.svg
DELETED
|
Binary file
|