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/README.md +150 -13
- package/agent/ai-agent.js +298 -188
- package/memory-file/memory-file.html +63 -0
- package/memory-file/memory-file.js +62 -0
- package/memory-inmem/memory-inmem.html +149 -0
- package/memory-inmem/memory-inmem.js +41 -0
- package/model/ai-model.html +338 -338
- package/package.json +21 -7
- package/tool-function/ai-tool-function.html +95 -0
- package/tool-function/ai-tool-function.js +104 -0
- package/tool-http/ai-tool-http-icon.svg +8 -0
- package/tool-http/ai-tool-http.html +174 -0
- package/tool-http/ai-tool-http.js +176 -0
- package/tool/ai-tool.html +0 -159
- package/tool/ai-tool.js +0 -220
- /package/{tool/ai-tool-icon.svg → tool-function/ai-tool-function-icon.svg} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-ai-agent",
|
|
3
|
-
"version": "0.0.
|
|
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": "
|
|
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
|
-
"
|
|
35
|
-
"
|
|
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
|
-
"
|
|
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
|
+
};
|