node-red-contrib-ai-agent 0.5.0 → 0.5.2
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.
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ai-orchestrator-agent', {
|
|
3
|
+
category: 'AI Agent',
|
|
4
|
+
color: '#deb887',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
systemPrompt: {
|
|
8
|
+
value: 'You are a helpful AI assistant.',
|
|
9
|
+
required: true
|
|
10
|
+
},
|
|
11
|
+
capabilities: {
|
|
12
|
+
value: '',
|
|
13
|
+
required: true
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
inputs: 1,
|
|
17
|
+
outputs: 1,
|
|
18
|
+
icon: 'agent/ai-agent-icon.svg',
|
|
19
|
+
label: function () {
|
|
20
|
+
return this.name || 'AI Agent Orchestrator';
|
|
21
|
+
},
|
|
22
|
+
paletteLabel: 'Agent Orchestrator',
|
|
23
|
+
oneditprepare: function () {
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script type="text/x-red" data-template-name="ai-orchestrator-agent">
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
31
|
+
<input type="text" id="node-input-name" placeholder="AI Agent Orchestrator">
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="form-row">
|
|
35
|
+
<label for="node-input-capabilities"><i class="fa fa-list"></i> Capabilities</label>
|
|
36
|
+
<input type="text" id="node-input-capabilities" placeholder="e.g. coding, research, translation">
|
|
37
|
+
<div class="form-tips" style="margin-top: 5px;">
|
|
38
|
+
Comma-separated list of skills this agent provides to its Orchestrator.
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="form-row">
|
|
43
|
+
<label for="node-input-systemPrompt"><i class="fa fa-comment"></i> System Prompt</label>
|
|
44
|
+
<textarea type="text" id="node-input-systemPrompt" style="width: 100%; height: 80px; resize: vertical; font-family: monospace;"></textarea>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="form-tips" style="background: #f8f9fa; border-left: 3px solid #3b78e7; padding: 10px;">
|
|
48
|
+
<p><b>Pipeline Node:</b> This node tags messages with its identity so a connected Orchestrator can use it.</p>
|
|
49
|
+
<p><b>Output:</b> Discovery Pipeline (Connect to Orchestrator).</p>
|
|
50
|
+
</div>
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<script type="text/x-red" data-help-name="ai-orchestrator-agent">
|
|
54
|
+
<h3>AI Agent Orchestrator Node</h3>
|
|
55
|
+
<p>A specialized version of the AI Agent designed for multi-agent workflows.</p>
|
|
56
|
+
|
|
57
|
+
<h4>Features</h4>
|
|
58
|
+
<ul>
|
|
59
|
+
<li><b>Discovery Pipeline</b>: Appends its metadata to `msg.agents` for the AI Orchestrator.</li>
|
|
60
|
+
<li><b>Direct Invocation</b>: Can be called directly by the Orchestrator without wires.</li>
|
|
61
|
+
</ul>
|
|
62
|
+
|
|
63
|
+
<h4>Usage</h4>
|
|
64
|
+
<ol>
|
|
65
|
+
<li>Place the node in front of an AI Orchestrator node.</li>
|
|
66
|
+
<li>Connect the output of this node to the input of the Orchestrator.</li>
|
|
67
|
+
<li>Define the agent's specific capabilities in the config.</li>
|
|
68
|
+
</ol>
|
|
69
|
+
</script>
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper functions for AI Agent Node
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Validate AI configuration
|
|
8
|
+
function validateAIConfig(aiagent) {
|
|
9
|
+
if (!aiagent) return 'AI configuration missing. Ensure an AI Model node is connected.';
|
|
10
|
+
if (!aiagent.model) return 'AI model not specified. Please configure the AI Model node with a valid model.';
|
|
11
|
+
if (!aiagent.apiKey) return 'API key not found. Please configure the AI Model node with a valid API key.';
|
|
12
|
+
return null; // No errors
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates and returns a message object
|
|
17
|
+
*/
|
|
18
|
+
function createMessage(role, content) {
|
|
19
|
+
return {
|
|
20
|
+
role: role,
|
|
21
|
+
content: content,
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
type: 'conversation'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Prepares the prompt with context if memory is available
|
|
29
|
+
*/
|
|
30
|
+
function preparePrompt(node, msg, inputText) {
|
|
31
|
+
const messages = [{ role: 'system', content: node.systemPrompt }];
|
|
32
|
+
let userMessage = null;
|
|
33
|
+
|
|
34
|
+
if (msg.aimemory) {
|
|
35
|
+
if (!msg.aimemory.context) {
|
|
36
|
+
throw new Error('Memory not properly initialized. Ensure a memory node is connected.');
|
|
37
|
+
}
|
|
38
|
+
msg.aimemory.context = msg.aimemory.context || [];
|
|
39
|
+
msg.aimemory.maxItems = msg.aimemory.maxItems || 1000;
|
|
40
|
+
messages.push(...msg.aimemory.context);
|
|
41
|
+
userMessage = createMessage('user', inputText);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
messages.push({ role: 'user', content: inputText });
|
|
45
|
+
|
|
46
|
+
return { messages, userMessage };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Updates the conversation context with new messages
|
|
51
|
+
*/
|
|
52
|
+
function updateContext(msg, userMessage, assistantResponse) {
|
|
53
|
+
if (!msg.aimemory?.context) return;
|
|
54
|
+
|
|
55
|
+
const assistantMessage = createMessage('assistant', assistantResponse);
|
|
56
|
+
const newContext = [...msg.aimemory.context, userMessage, assistantMessage];
|
|
57
|
+
const maxItems = msg.aimemory.maxItems || 1000;
|
|
58
|
+
|
|
59
|
+
msg.aimemory.context = newContext.slice(-maxItems);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handles errors consistently
|
|
64
|
+
*/
|
|
65
|
+
function handleError(node, msg, error) {
|
|
66
|
+
const errorMsg = error.response?.data?.error?.message || error.message || 'Unknown error';
|
|
67
|
+
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
|
|
68
|
+
node.error('AI Agent Orchestrator Error: ' + errorMsg, msg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Formats tools for the OpenAI/OpenRouter API
|
|
73
|
+
*/
|
|
74
|
+
function formatToolsForAPI(tools) {
|
|
75
|
+
return tools.map(tool => {
|
|
76
|
+
const type = tool.type || 'function';
|
|
77
|
+
const fn = tool.function || {};
|
|
78
|
+
return {
|
|
79
|
+
type: type,
|
|
80
|
+
function: {
|
|
81
|
+
name: fn.name || 'function',
|
|
82
|
+
description: fn.description || 'function',
|
|
83
|
+
parameters: fn.parameters || {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {},
|
|
86
|
+
required: []
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Calls the AI with proper error handling
|
|
95
|
+
*/
|
|
96
|
+
async function callAI(node, aiConfig, messages) {
|
|
97
|
+
const hasTools = aiConfig.tools && Array.isArray(aiConfig.tools) && aiConfig.tools.length > 0;
|
|
98
|
+
const tools = hasTools ? aiConfig.tools : [];
|
|
99
|
+
const toolChoice = hasTools ? 'auto' : 'none';
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
node.status({ fill: 'blue', shape: 'dot', text: `Calling ${aiConfig.model}...` });
|
|
103
|
+
|
|
104
|
+
const requestPayload = {
|
|
105
|
+
model: aiConfig.model,
|
|
106
|
+
temperature: aiConfig.temperature,
|
|
107
|
+
messages: messages,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (hasTools) {
|
|
111
|
+
requestPayload.tools = formatToolsForAPI(tools);
|
|
112
|
+
requestPayload.tool_choice = toolChoice;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const response = await axios.post(
|
|
116
|
+
'https://openrouter.ai/api/v1/chat/completions',
|
|
117
|
+
requestPayload,
|
|
118
|
+
{
|
|
119
|
+
headers: {
|
|
120
|
+
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'HTTP-Referer': 'https://nodered.org/',
|
|
123
|
+
'X-Title': 'Node-RED AI Orchestrator-Agent'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const responseMessage = response.data.choices[0]?.message;
|
|
129
|
+
|
|
130
|
+
if (responseMessage?.tool_calls && aiConfig.tools) {
|
|
131
|
+
return await processToolCalls(node, responseMessage, aiConfig.tools, messages, aiConfig);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return responseMessage?.content?.trim() || '';
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const errorMsg = error.response?.data?.error?.message || error.message;
|
|
138
|
+
throw new Error(`AI API Error: ${errorMsg}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Helper function to process tool calls
|
|
144
|
+
*/
|
|
145
|
+
async function processToolCalls(node, responseMessage, tools, messages, aiConfig) {
|
|
146
|
+
const toolCalls = responseMessage.tool_calls || [];
|
|
147
|
+
let toolResults = [];
|
|
148
|
+
|
|
149
|
+
for (const toolCall of toolCalls) {
|
|
150
|
+
const { id, function: fn } = toolCall;
|
|
151
|
+
const { name, arguments: args } = fn;
|
|
152
|
+
|
|
153
|
+
const tool = tools.find(t => t.function?.name === name);
|
|
154
|
+
if (!tool) {
|
|
155
|
+
toolResults.push({
|
|
156
|
+
tool_call_id: id,
|
|
157
|
+
role: 'tool',
|
|
158
|
+
name,
|
|
159
|
+
content: JSON.stringify({ error: `Tool '${name}' not found` })
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
|
|
166
|
+
const result = await tool.execute(parsedArgs);
|
|
167
|
+
|
|
168
|
+
toolResults.push({
|
|
169
|
+
tool_call_id: id,
|
|
170
|
+
role: 'tool',
|
|
171
|
+
name,
|
|
172
|
+
content: typeof result === 'string' ? result : JSON.stringify(result)
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
toolResults.push({
|
|
176
|
+
tool_call_id: id,
|
|
177
|
+
role: 'tool',
|
|
178
|
+
name,
|
|
179
|
+
content: JSON.stringify({ error: error.message })
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const updatedMessages = [...messages, responseMessage, ...toolResults];
|
|
185
|
+
return await callAI(node, { ...aiConfig, tools: null }, updatedMessages);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = function (RED) {
|
|
189
|
+
function AIOrchestratorAgent(config) {
|
|
190
|
+
RED.nodes.createNode(this, config);
|
|
191
|
+
this.name = config.name || 'AI Agent Orchestrator';
|
|
192
|
+
this.systemPrompt = config.systemPrompt || 'You are a helpful AI assistant.';
|
|
193
|
+
this.capabilities = (config.capabilities || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
194
|
+
|
|
195
|
+
// AI Orchestrator direct call API
|
|
196
|
+
this.executeTask = async function (taskInput, msg) {
|
|
197
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'executing...' });
|
|
198
|
+
try {
|
|
199
|
+
const validationError = validateAIConfig(msg.aiagent);
|
|
200
|
+
if (validationError) throw new Error(validationError);
|
|
201
|
+
|
|
202
|
+
const inputText = typeof taskInput === 'string' ? taskInput : JSON.stringify(taskInput);
|
|
203
|
+
const { messages, userMessage } = preparePrompt(node, msg, inputText);
|
|
204
|
+
const response = await callAI(node, msg.aiagent, messages);
|
|
205
|
+
|
|
206
|
+
if (msg.aimemory && userMessage) {
|
|
207
|
+
updateContext(msg, userMessage, response);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
node.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
211
|
+
return response;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
handleError(node, msg, error);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const node = this;
|
|
219
|
+
|
|
220
|
+
node.on('close', function (done) {
|
|
221
|
+
node.status({});
|
|
222
|
+
if (done) done();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Discovery Pipeline Logic
|
|
226
|
+
node.on('input', function (msg, send, done) {
|
|
227
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'tagging...' });
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
// Enforce agents array
|
|
231
|
+
msg.agents = msg.agents || [];
|
|
232
|
+
|
|
233
|
+
// Push metadata for the Orchestrator to see
|
|
234
|
+
msg.agents.push({
|
|
235
|
+
id: node.id,
|
|
236
|
+
name: node.name,
|
|
237
|
+
capabilities: node.capabilities,
|
|
238
|
+
type: 'agent'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Pass through to Output 2 (Pipeline)
|
|
242
|
+
send([null, msg]);
|
|
243
|
+
node.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
244
|
+
|
|
245
|
+
} catch (error) {
|
|
246
|
+
node.error(error.message, msg);
|
|
247
|
+
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
|
|
248
|
+
} finally {
|
|
249
|
+
if (done) done();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
RED.nodes.registerType('ai-orchestrator-agent', AIOrchestratorAgent);
|
|
255
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-ai-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "AI Agent for Node-RED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
"license": "AGPL-3.0",
|
|
29
29
|
"files": [
|
|
30
30
|
"agent/*",
|
|
31
|
+
"memory-file/*",
|
|
32
|
+
"memory-inmem/*",
|
|
31
33
|
"model/*",
|
|
34
|
+
"orchestrator/*",
|
|
35
|
+
"orchestrator-agent/*",
|
|
32
36
|
"tool/*",
|
|
33
37
|
"tool-function/*",
|
|
34
|
-
"tool-http/*"
|
|
35
|
-
"memory-file/*",
|
|
36
|
-
"memory-inmem/*",
|
|
37
|
-
"orchestrator/*"
|
|
38
|
+
"tool-http/*"
|
|
38
39
|
],
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"axios": "^1.6.0",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"ai-memory-file": "./memory-file/memory-file.js",
|
|
65
66
|
"ai-memory-inmem": "./memory-inmem/memory-inmem.js",
|
|
66
67
|
"ai-orchestrator": "./orchestrator/orchestrator.js",
|
|
67
|
-
"ai-agent
|
|
68
|
+
"ai-orchestrator-agent": "./orchestrator-agent/orchestrator-agent.js",
|
|
68
69
|
"ai-tool-approval": "./tool-approval/ai-tool-approval.js"
|
|
69
70
|
}
|
|
70
71
|
}
|