node-red-contrib-ai-agent 0.4.1 → 0.5.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
@@ -19,6 +19,7 @@ Your feedback and contributions are highly appreciated!
19
19
  ## Features
20
20
 
21
21
  - **AI Agent Node**: Process messages with AI, maintaining conversation context
22
+ - **AI Agent Orchestrator Node**: Participates in orchestrated flows via Chain Discovery
22
23
  - **Memory Nodes**:
23
24
  - **In-Memory**: Store conversation context in memory (volatile)
24
25
  - **File-based**: Persist conversation context to disk
@@ -61,6 +62,14 @@ Processes messages using the configured AI model and maintains conversation cont
61
62
  - **System Prompt**: Initial instructions for the AI
62
63
  - **Response Type**: Format of the response (text or JSON object)
63
64
 
65
+ ### AI Agent Orchestrator
66
+ A specialized version of the AI Agent designed for multi-agent workflows. It "tags" messages in a pipeline so the **AI Orchestrator** can discover it.
67
+
68
+ **Properties:**
69
+ - **Name**: Display name for the node
70
+ - **Capabilities**: Comma-separated list of skills (e.g., `coding, research`)
71
+ - **System Prompt**: Instructions for this specific expert
72
+
64
73
  ### Memory (In-Memory)
65
74
  A configuration node that initializes the conversation context in memory. The agent node uses this configuration to manage the conversation context.
66
75
 
@@ -91,13 +100,13 @@ Configures the AI model and API settings.
91
100
  - **Name**: Display name for the node
92
101
 
93
102
  ### AI Orchestrator
94
- Coordinates multiple AI agents by creating and executing plans. It uses an autonomy loop (observe-think-act-reflect) to achieve complex goals.
103
+ Coordinates multiple AI agents by creating and executing plans. It uses **Chain Discovery** to identify available agents from its input message.
95
104
 
96
105
  **Key Features:**
106
+ - **Chain Discovery**: Implicitly discovers agents wired in a pipeline before it.
107
+ - **Zero-Wire Execution**: Calls discovered agents directly via code (no messy routing wires).
97
108
  - **Non-linear Planning**: Supports task dependencies (tasks wait for their predecessors).
98
- - **Task Prioritization**: Executes higher priority tasks first within dependency constraints.
99
- - **Dynamic Plan Revision**: Refines the plan based on task outcomes and agent feedback.
100
- - **Error Recovery**: Automatically handles task failures with recovery strategies (retry, pivot, or fail).
109
+ - **Error Recovery**: Automatically handles task failures with recovery strategies.
101
110
 
102
111
  **Properties:**
103
112
  - **Max Iterations**: Maximum cycles for the autonomy loop
@@ -186,17 +195,16 @@ For more complex scenarios, you can chain multiple agents to process messages in
186
195
 
187
196
  Each agent will maintain its own conversation context based on its memory configuration.
188
197
 
189
- ## Example: Autonomous Orchestration
198
+ ## Example: Autonomous Orchestration (Chain Discovery)
190
199
 
191
- The AI Orchestrator can manage complex, multi-step tasks:
200
+ The AI Orchestrator can manage complex, multi-step tasks by utilizing specialized agents in a pipeline:
192
201
 
193
- 1. Add an **AI Orchestrator** node
194
- 2. Connect its first output to an **AI Agent**
195
- 3. Connect the agent's output back to the **AI Orchestrator** input
196
- 4. Connect the orchestrator's second output to a **Debug** node
197
- 5. Configure the orchestrator with a goal (e.g., "Write a blog post and then translate it to Spanish")
202
+ 1. Add an **AI Orchestrator** node.
203
+ 2. Add one or more **AI Agent Orchestrator** nodes (e.g., "Coder", "Researcher").
204
+ 3. Connect them in a line: `[Inject Goal] --> [Coder] --> [Researcher] --> [Orchestrator] --> [Debug]`.
205
+ 4. The Orchestrator will automatically discover the "Coder" and "Researcher" via the message pipeline and call them as needed to achieve the goal.
198
206
 
199
- The orchestrator will create a plan (optionally with dependencies and priorities), dispatch the first available task to the agent, reflect on the result, and then dispatch the next task until completion. If a task fails, it can revise the plan to recover.
207
+ This linear architecture keeps your flows clean while allowing for powerful, multi-agent collaboration.
200
208
 
201
209
  ## Best Practices
202
210
 
@@ -1,96 +1,96 @@
1
- <script type="text/javascript">
2
- RED.nodes.registerType('ai-agent', {
3
- category: 'AI Agent',
4
- color: '#a6bbcf',
5
- defaults: {
6
- name: { value: '' },
7
- systemPrompt: {
8
- value: 'You are a helpful AI assistant.',
9
- required: true
10
- },
11
- responseType: {
12
- value: 'text',
13
- required: true,
14
- validate: function(val) {
15
- return ['text', 'object'].includes(val);
16
- }
17
- }
18
- },
19
- inputs: 1,
20
- outputs: 1,
21
- icon: 'agent/ai-agent-icon.svg',
22
- label: function() {
23
- return this.name || 'AI Agent';
24
- },
25
- paletteLabel: 'AI Agent',
26
- oneditprepare: function() {
27
- // Always show OpenRouter help since we only support OpenRouter now
28
- $('#openrouter-help').show();
29
- }
30
- });
31
- </script>
32
-
33
- <!-- START: Template -->
34
- <script type="text/x-red" data-template-name="ai-agent">
35
- <div class="form-row">
36
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
37
- <input type="text" id="node-input-name" placeholder="AI Agent">
38
- </div>
39
-
40
- <div class="form-row">
41
- <label for="node-input-systemPrompt"><i class="fa fa-comment"></i> System Prompt</label>
42
- <textarea type="text" id="node-input-systemPrompt" placeholder="You are a helpful AI assistant." style="width: 100%; height: 80px; resize: vertical; font-family: monospace;"></textarea>
43
- </div>
44
-
45
- <div class="form-row">
46
- <label><i class="fa fa-robot"></i> AI Agent</label>
47
- <div class="form-tips" style="width: 70%;">
48
- <p>This node uses OpenRouter for AI responses. Connect an <b>AI Model</b> node to configure the model and API key.</p>
49
- </div>
50
- </div>
51
-
52
- <div class="form-row">
53
- <label for="node-input-responseType"><i class="fa fa-reply"></i> Response Format</label>
54
- <select id="node-input-responseType" style="width: 100%;">
55
- <option value="text">Text Only</option>
56
- <option value="object">Structured Object</option>
57
- </select>
58
- </div>
59
-
60
- <div id="openrouter-help" class="form-tips" style="display: none; margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #3b78e7;">
61
- <p><i class="fa fa-info-circle"></i> <strong>Note:</strong> When using OpenRouter, connect an AI Model node to provide the model and API key configuration.</p>
62
- <p>The AI Model node will add an <code>aiagent</code> property to the message with the required configuration.</p>
63
- </div>
64
- </script>
65
- <!-- END: Template -->
66
-
67
- <!-- START: Help -->
68
- <script type="text/x-red" data-help-name="ai-agent">
69
- <h3>AI Agent Node</h3>
70
- <p>An intelligent agent that processes and responds to messages with configurable behavior.</p>
71
-
72
- <h4>Features</h4>
73
- <ul>
74
- <li>Multiple agent types (Assistant, Chatbot)</li>
75
- <li>Configurable response formats</li>
76
- <li>Conversation context tracking</li>
77
- <li>Error handling and status reporting</li>
78
- </ul>
79
-
80
- <h4>Usage</h4>
81
- <ol>
82
- <li>Connect the node to a message source (e.g., HTTP input, inject node)</li>
83
- <li>Configure the agent type and response format</li>
84
- <li>Process the response in your flow</li>
85
- </ol>
86
-
87
- <h4>Output Formats</h4>
88
- <p><b>Text Only:</b> Simple string response</p>
89
- <p><b>Structured Object:</b> Detailed response including metadata and context</p>
90
-
91
- <h4>Examples</h4>
92
- <p><b>Input:</b> "Hello"</p>
93
- <p><b>Assistant Output:</b> "Hello! How can I assist you today?"</p>
94
- <p><b>Chatbot Output:</b> "Hi there! What would you like to chat about?"</p>
95
- </script>
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('ai-agent', {
3
+ category: 'AI Agent',
4
+ color: '#a6bbcf',
5
+ defaults: {
6
+ name: { value: '' },
7
+ systemPrompt: {
8
+ value: 'You are a helpful AI assistant.',
9
+ required: true
10
+ },
11
+ responseType: {
12
+ value: 'text',
13
+ required: true,
14
+ validate: function(val) {
15
+ return ['text', 'object'].includes(val);
16
+ }
17
+ }
18
+ },
19
+ inputs: 1,
20
+ outputs: 1,
21
+ icon: 'agent/ai-agent-icon.svg',
22
+ label: function() {
23
+ return this.name || 'AI Agent';
24
+ },
25
+ paletteLabel: 'AI Agent',
26
+ oneditprepare: function() {
27
+ // Always show OpenRouter help since we only support OpenRouter now
28
+ $('#openrouter-help').show();
29
+ }
30
+ });
31
+ </script>
32
+
33
+ <!-- START: Template -->
34
+ <script type="text/x-red" data-template-name="ai-agent">
35
+ <div class="form-row">
36
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
37
+ <input type="text" id="node-input-name" placeholder="AI Agent">
38
+ </div>
39
+
40
+ <div class="form-row">
41
+ <label for="node-input-systemPrompt"><i class="fa fa-comment"></i> System Prompt</label>
42
+ <textarea type="text" id="node-input-systemPrompt" placeholder="You are a helpful AI assistant." style="width: 100%; height: 80px; resize: vertical; font-family: monospace;"></textarea>
43
+ </div>
44
+
45
+ <div class="form-row">
46
+ <label><i class="fa fa-robot"></i> AI Agent</label>
47
+ <div class="form-tips" style="width: 70%;">
48
+ <p>This node uses OpenRouter for AI responses. Connect an <b>AI Model</b> node to configure the model and API key.</p>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="form-row">
53
+ <label for="node-input-responseType"><i class="fa fa-reply"></i> Response Format</label>
54
+ <select id="node-input-responseType" style="width: 100%;">
55
+ <option value="text">Text Only</option>
56
+ <option value="object">Structured Object</option>
57
+ </select>
58
+ </div>
59
+
60
+ <div id="openrouter-help" class="form-tips" style="display: none; margin-top: 15px; padding: 10px; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #3b78e7;">
61
+ <p><i class="fa fa-info-circle"></i> <strong>Note:</strong> When using OpenRouter, connect an AI Model node to provide the model and API key configuration.</p>
62
+ <p>The AI Model node will add an <code>aiagent</code> property to the message with the required configuration.</p>
63
+ </div>
64
+ </script>
65
+ <!-- END: Template -->
66
+
67
+ <!-- START: Help -->
68
+ <script type="text/x-red" data-help-name="ai-agent">
69
+ <h3>AI Agent Node</h3>
70
+ <p>An intelligent agent that processes and responds to messages with configurable behavior.</p>
71
+
72
+ <h4>Features</h4>
73
+ <ul>
74
+ <li>Multiple agent types (Assistant, Chatbot)</li>
75
+ <li>Configurable response formats</li>
76
+ <li>Conversation context tracking</li>
77
+ <li>Error handling and status reporting</li>
78
+ </ul>
79
+
80
+ <h4>Usage</h4>
81
+ <ol>
82
+ <li>Connect the node to a message source (e.g., HTTP input, inject node)</li>
83
+ <li>Configure the agent type and response format</li>
84
+ <li>Process the response in your flow</li>
85
+ </ol>
86
+
87
+ <h4>Output Formats</h4>
88
+ <p><b>Text Only:</b> Simple string response</p>
89
+ <p><b>Structured Object:</b> Detailed response including metadata and context</p>
90
+
91
+ <h4>Examples</h4>
92
+ <p><b>Input:</b> "Hello"</p>
93
+ <p><b>Assistant Output:</b> "Hello! How can I assist you today?"</p>
94
+ <p><b>Chatbot Output:</b> "Hi there! What would you like to chat about?"</p>
95
+ </script>
96
96
  <!-- END: Help -->
package/agent/ai-agent.js CHANGED
@@ -1,326 +1,326 @@
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
- * @param {string} role - The role of the message (e.g., 'user', 'assistant', 'system')
18
- * @param {string} content - The content of the message
19
- * @returns {Object} - The message object
20
- */
21
- function createMessage(role, content) {
22
- return {
23
- role: role,
24
- content: content,
25
- timestamp: new Date().toISOString(),
26
- type: 'conversation'
27
- };
28
- }
29
-
30
- /**
31
- * Prepares the prompt with context if memory is available
32
- * @param {Object} node - The AI Agent node
33
- * @param {Object} msg - The input message
34
- * @param {string} inputText - The input text
35
- * @returns {Object} - The prepared prompt object
36
- */
37
- function preparePrompt(node, msg, inputText) {
38
- const messages = [{ role: 'system', content: node.systemPrompt }];
39
- let userMessage = null;
40
-
41
- // Add context if using memory
42
- if (msg.aimemory) {
43
- if (!msg.aimemory.context) {
44
- throw new Error('Memory not properly initialized. Ensure a memory node is connected.');
45
- }
46
-
47
- // Ensure memory has required structure
48
- msg.aimemory.context = msg.aimemory.context || [];
49
- msg.aimemory.maxItems = msg.aimemory.maxItems || 1000;
50
-
51
- // Add conversation context
52
- messages.push(...msg.aimemory.context);
53
-
54
- // Create and store user message for later context update
55
- userMessage = createMessage('user', inputText);
56
- }
57
-
58
- // Add current user input
59
- messages.push({ role: 'user', content: inputText });
60
-
61
- return { messages, userMessage };
62
- }
63
-
64
- /**
65
- * Updates the conversation context with new messages
66
- * @param {Object} msg - The input message
67
- * @param {Object} userMessage - The user message
68
- * @param {string} assistantResponse - The assistant response
69
- */
70
- function updateContext(msg, userMessage, assistantResponse) {
71
- if (!msg.aimemory?.context) return;
72
-
73
- const assistantMessage = createMessage('assistant', assistantResponse);
74
- const newContext = [...msg.aimemory.context, userMessage, assistantMessage];
75
- const maxItems = msg.aimemory.maxItems || 1000;
76
-
77
- msg.aimemory.context = newContext.slice(-maxItems);
78
- }
79
-
80
- /**
81
- * Handles errors consistently
82
- * @param {Object} node - The AI Agent node
83
- * @param {Object} msg - The input message
84
- * @param {Error} error - The error object
85
- */
86
- function handleError(node, msg, error) {
87
- const errorMsg = error.response?.data?.error?.message || error.message || 'Unknown error';
88
- node.status({ fill: 'red', shape: 'ring', text: 'Error' });
89
- node.error('AI Agent Error: ' + errorMsg, msg);
90
- }
91
-
92
- /**
93
- * Formats tools for the OpenAI/OpenRouter API
94
- * @param {Array} tools - Array of tool definitions
95
- * @returns {Array} - Formatted tools for the API
96
- */
97
- function formatToolsForAPI(tools) {
98
- return tools.map(tool => {
99
- const type = tool.type || 'function';
100
- const fn = tool.function || {};
101
- const name = fn.name || 'function';
102
- const description = fn.description || 'function';
103
- const parameters = fn.parameters || {};
104
- return {
105
- type: type,
106
- function: {
107
- name: name,
108
- description: description,
109
- parameters: parameters || {
110
- type: 'object',
111
- properties: {},
112
- required: []
113
- }
114
- }
115
- };
116
- });
117
- }
118
-
119
- /**
120
- * Calls the AI with proper error handling
121
- * @param {Object} node - The AI Agent node
122
- * @param {Object} aiConfig - The AI configuration object
123
- * @param {Array} messages - The messages to send to the AI
124
- * @returns {Promise<string>} - The AI response
125
- */
126
- async function callAI(node, aiConfig, messages) {
127
- const hasTools = aiConfig.tools && Array.isArray(aiConfig.tools) && aiConfig.tools.length > 0;
128
- const tools = hasTools ? aiConfig.tools : [];
129
- const toolChoice = hasTools ? 'auto' : 'none';
130
-
131
- node.warn(`Calling ${aiConfig.model} with ${tools.length} tools and ${toolChoice} tool choice`);
132
-
133
- try {
134
- node.status({ fill: 'blue', shape: 'dot', text: `Calling ${aiConfig.model}...` });
135
-
136
- // Prepare request payload
137
- const requestPayload = {
138
- model: aiConfig.model,
139
- temperature: aiConfig.temperature,
140
- // max_tokens: aiConfig.maxTokens,
141
- messages: messages,
142
- };
143
-
144
- // Add tools if available
145
- if (hasTools) {
146
- node.warn('Adding tools: ' + JSON.stringify(tools, null, 2));
147
- requestPayload.tools = formatToolsForAPI(tools);
148
- requestPayload.tool_choice = toolChoice;
149
- }
150
-
151
- node.warn(JSON.stringify(requestPayload, null, 2));
152
-
153
- const response = await axios.post(
154
- 'https://openrouter.ai/api/v1/chat/completions',
155
- requestPayload,
156
- {
157
- headers: {
158
- 'Authorization': `Bearer ${aiConfig.apiKey}`,
159
- 'Content-Type': 'application/json',
160
- 'HTTP-Referer': 'https://nodered.org/',
161
- 'X-Title': 'Node-RED AI Agent'
162
- }
163
- }
164
- );
165
-
166
- // Check if the response contains tool calls
167
- const responseMessage = response.data.choices[0]?.message;
168
-
169
- node.warn(JSON.stringify(responseMessage, null, 2));
170
-
171
- if (responseMessage?.tool_calls && aiConfig.tools) {
172
- // Process tool calls
173
- if (node.warn) node.warn('Processing tool calls');
174
- return await processToolCalls(node, responseMessage, aiConfig.tools, messages, aiConfig);
175
- }
176
-
177
- node.warn('Processing response');
178
- return responseMessage?.content?.trim() || '';
179
-
180
- } catch (error) {
181
- const errorMsg = error.response?.data?.error?.message || error.message;
182
- throw new Error(`AI API Error: ${errorMsg}`);
183
- }
184
- }
185
-
186
- /**
187
- * Helper function to process tool calls from AI response
188
- * @param {Object} node - The Node-RED node instance
189
- * @param {Object} responseMessage - The AI response message containing tool calls
190
- * @param {Array} tools - Array of available tools
191
- * @param {Array} messages - Conversation messages
192
- * @returns {Promise<string>} - Result of tool executions
193
- */
194
- async function processToolCalls(node, responseMessage, tools, messages, aiConfig) {
195
- try {
196
- const toolCalls = responseMessage.tool_calls || [];
197
- let toolResults = [];
198
- if (node && node.warn) {
199
- node.warn('Processing tool calls: ' + JSON.stringify(toolCalls, null, 2));
200
- }
201
-
202
- // Process each tool call
203
- for (const toolCall of toolCalls) {
204
- const { id, function: fn } = toolCall;
205
- const { name, arguments: args } = fn;
206
-
207
- // Find the matching tool
208
- const tool = tools.find(t => t.function?.name === name);
209
- if (!tool) {
210
- toolResults.push({
211
- tool_call_id: id,
212
- role: 'tool',
213
- name,
214
- content: JSON.stringify({ error: `Tool '${name}' not found` })
215
- });
216
- continue;
217
- }
218
-
219
- // Execute the tool
220
- try {
221
- const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
222
- const result = await tool.execute(parsedArgs);
223
-
224
- toolResults.push({
225
- tool_call_id: id,
226
- role: 'tool',
227
- name,
228
- content: typeof result === 'string' ? result : JSON.stringify(result)
229
- });
230
- } catch (error) {
231
- toolResults.push({
232
- tool_call_id: id,
233
- role: 'tool',
234
- name,
235
- content: JSON.stringify({ error: error.message })
236
- });
237
- }
238
- }
239
-
240
- // Add tool results to the messages array
241
- const updatedMessages = [...messages, responseMessage, ...toolResults];
242
-
243
- // Make a new API call to let the AI process the tool results
244
- const aiResponse = await callAI(node, { ...aiConfig, tools: null }, updatedMessages);
245
-
246
- // Return the final AI response
247
- return aiResponse;
248
- } catch (error) {
249
- return `Error processing tool calls: ${error.message}`;
250
- }
251
- }
252
-
253
- module.exports = function (RED) {
254
- /**
255
- * AI Agent Node
256
- * @param {Object} config - The node configuration object
257
- */
258
- function AiAgentNode(config) {
259
- RED.nodes.createNode(this, config);
260
- const node = this;
261
-
262
- // Configuration
263
- this.agentName = config.name || 'AI Agent';
264
- this.systemPrompt = config.systemPrompt || 'You are a helpful AI assistant.';
265
- this.responseType = config.responseType || 'text';
266
-
267
- // Handle node cleanup
268
- node.on('close', function (done) {
269
- node.status({});
270
- if (done) done();
271
- });
272
-
273
- // Process incoming messages
274
- node.on('input', async function (msg, send, done) {
275
- node.status({ fill: 'blue', shape: 'dot', text: 'processing...' });
276
-
277
- try {
278
- // 1. Validate AI configuration
279
- const validationError = validateAIConfig(msg.aiagent);
280
- if (validationError) {
281
- throw new Error(validationError);
282
- }
283
-
284
- // 2. Get input
285
- const input = msg.payload || {};
286
- const inputText = typeof input === 'string' ? input : JSON.stringify(input);
287
-
288
- // 3. Prepare prompt with context
289
- const { messages, userMessage } = preparePrompt(node, msg, inputText);
290
-
291
- // 4. Execute the prompt and get response
292
- const response = await callAI(node, msg.aiagent, messages);
293
-
294
- // 5. Update context if using memory
295
- if (msg.aimemory && userMessage) {
296
- updateContext(msg, userMessage, response);
297
- }
298
-
299
- // 6. Format and send response
300
- msg.payload = node.responseType === 'object' ? {
301
- agent: node.agentName,
302
- type: 'ai',
303
- input: input,
304
- response: response,
305
- timestamp: new Date().toISOString(),
306
- context: {
307
- conversationLength: msg.aimemory?.context?.length || 0,
308
- lastInteraction: new Date().toISOString(),
309
- ...(msg.aimemory && { aimemory: msg.aimemory })
310
- }
311
- } : response;
312
-
313
- send(msg);
314
- node.status({ fill: 'green', shape: 'dot', text: 'ready' });
315
-
316
- } catch (error) {
317
- handleError(node, msg, error);
318
- } finally {
319
- if (done) done();
320
- }
321
- });
322
- }
323
-
324
- // Register the node type
325
- RED.nodes.registerType('ai-agent', AiAgentNode);
326
- };
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
+ * @param {string} role - The role of the message (e.g., 'user', 'assistant', 'system')
18
+ * @param {string} content - The content of the message
19
+ * @returns {Object} - The message object
20
+ */
21
+ function createMessage(role, content) {
22
+ return {
23
+ role: role,
24
+ content: content,
25
+ timestamp: new Date().toISOString(),
26
+ type: 'conversation'
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Prepares the prompt with context if memory is available
32
+ * @param {Object} node - The AI Agent node
33
+ * @param {Object} msg - The input message
34
+ * @param {string} inputText - The input text
35
+ * @returns {Object} - The prepared prompt object
36
+ */
37
+ function preparePrompt(node, msg, inputText) {
38
+ const messages = [{ role: 'system', content: node.systemPrompt }];
39
+ let userMessage = null;
40
+
41
+ // Add context if using memory
42
+ if (msg.aimemory) {
43
+ if (!msg.aimemory.context) {
44
+ throw new Error('Memory not properly initialized. Ensure a memory node is connected.');
45
+ }
46
+
47
+ // Ensure memory has required structure
48
+ msg.aimemory.context = msg.aimemory.context || [];
49
+ msg.aimemory.maxItems = msg.aimemory.maxItems || 1000;
50
+
51
+ // Add conversation context
52
+ messages.push(...msg.aimemory.context);
53
+
54
+ // Create and store user message for later context update
55
+ userMessage = createMessage('user', inputText);
56
+ }
57
+
58
+ // Add current user input
59
+ messages.push({ role: 'user', content: inputText });
60
+
61
+ return { messages, userMessage };
62
+ }
63
+
64
+ /**
65
+ * Updates the conversation context with new messages
66
+ * @param {Object} msg - The input message
67
+ * @param {Object} userMessage - The user message
68
+ * @param {string} assistantResponse - The assistant response
69
+ */
70
+ function updateContext(msg, userMessage, assistantResponse) {
71
+ if (!msg.aimemory?.context) return;
72
+
73
+ const assistantMessage = createMessage('assistant', assistantResponse);
74
+ const newContext = [...msg.aimemory.context, userMessage, assistantMessage];
75
+ const maxItems = msg.aimemory.maxItems || 1000;
76
+
77
+ msg.aimemory.context = newContext.slice(-maxItems);
78
+ }
79
+
80
+ /**
81
+ * Handles errors consistently
82
+ * @param {Object} node - The AI Agent node
83
+ * @param {Object} msg - The input message
84
+ * @param {Error} error - The error object
85
+ */
86
+ function handleError(node, msg, error) {
87
+ const errorMsg = error.response?.data?.error?.message || error.message || 'Unknown error';
88
+ node.status({ fill: 'red', shape: 'ring', text: 'Error' });
89
+ node.error('AI Agent Error: ' + errorMsg, msg);
90
+ }
91
+
92
+ /**
93
+ * Formats tools for the OpenAI/OpenRouter API
94
+ * @param {Array} tools - Array of tool definitions
95
+ * @returns {Array} - Formatted tools for the API
96
+ */
97
+ function formatToolsForAPI(tools) {
98
+ return tools.map(tool => {
99
+ const type = tool.type || 'function';
100
+ const fn = tool.function || {};
101
+ const name = fn.name || 'function';
102
+ const description = fn.description || 'function';
103
+ const parameters = fn.parameters || {};
104
+ return {
105
+ type: type,
106
+ function: {
107
+ name: name,
108
+ description: description,
109
+ parameters: parameters || {
110
+ type: 'object',
111
+ properties: {},
112
+ required: []
113
+ }
114
+ }
115
+ };
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Calls the AI with proper error handling
121
+ * @param {Object} node - The AI Agent node
122
+ * @param {Object} aiConfig - The AI configuration object
123
+ * @param {Array} messages - The messages to send to the AI
124
+ * @returns {Promise<string>} - The AI response
125
+ */
126
+ async function callAI(node, aiConfig, messages) {
127
+ const hasTools = aiConfig.tools && Array.isArray(aiConfig.tools) && aiConfig.tools.length > 0;
128
+ const tools = hasTools ? aiConfig.tools : [];
129
+ const toolChoice = hasTools ? 'auto' : 'none';
130
+
131
+ node.warn(`Calling ${aiConfig.model} with ${tools.length} tools and ${toolChoice} tool choice`);
132
+
133
+ try {
134
+ node.status({ fill: 'blue', shape: 'dot', text: `Calling ${aiConfig.model}...` });
135
+
136
+ // Prepare request payload
137
+ const requestPayload = {
138
+ model: aiConfig.model,
139
+ temperature: aiConfig.temperature,
140
+ // max_tokens: aiConfig.maxTokens,
141
+ messages: messages,
142
+ };
143
+
144
+ // Add tools if available
145
+ if (hasTools) {
146
+ node.warn('Adding tools: ' + JSON.stringify(tools, null, 2));
147
+ requestPayload.tools = formatToolsForAPI(tools);
148
+ requestPayload.tool_choice = toolChoice;
149
+ }
150
+
151
+ node.warn(JSON.stringify(requestPayload, null, 2));
152
+
153
+ const response = await axios.post(
154
+ 'https://openrouter.ai/api/v1/chat/completions',
155
+ requestPayload,
156
+ {
157
+ headers: {
158
+ 'Authorization': `Bearer ${aiConfig.apiKey}`,
159
+ 'Content-Type': 'application/json',
160
+ 'HTTP-Referer': 'https://nodered.org/',
161
+ 'X-Title': 'Node-RED AI Agent'
162
+ }
163
+ }
164
+ );
165
+
166
+ // Check if the response contains tool calls
167
+ const responseMessage = response.data.choices[0]?.message;
168
+
169
+ node.warn(JSON.stringify(responseMessage, null, 2));
170
+
171
+ if (responseMessage?.tool_calls && aiConfig.tools) {
172
+ // Process tool calls
173
+ if (node.warn) node.warn('Processing tool calls');
174
+ return await processToolCalls(node, responseMessage, aiConfig.tools, messages, aiConfig);
175
+ }
176
+
177
+ node.warn('Processing response');
178
+ return responseMessage?.content?.trim() || '';
179
+
180
+ } catch (error) {
181
+ const errorMsg = error.response?.data?.error?.message || error.message;
182
+ throw new Error(`AI API Error: ${errorMsg}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Helper function to process tool calls from AI response
188
+ * @param {Object} node - The Node-RED node instance
189
+ * @param {Object} responseMessage - The AI response message containing tool calls
190
+ * @param {Array} tools - Array of available tools
191
+ * @param {Array} messages - Conversation messages
192
+ * @returns {Promise<string>} - Result of tool executions
193
+ */
194
+ async function processToolCalls(node, responseMessage, tools, messages, aiConfig) {
195
+ try {
196
+ const toolCalls = responseMessage.tool_calls || [];
197
+ let toolResults = [];
198
+ if (node && node.warn) {
199
+ node.warn('Processing tool calls: ' + JSON.stringify(toolCalls, null, 2));
200
+ }
201
+
202
+ // Process each tool call
203
+ for (const toolCall of toolCalls) {
204
+ const { id, function: fn } = toolCall;
205
+ const { name, arguments: args } = fn;
206
+
207
+ // Find the matching tool
208
+ const tool = tools.find(t => t.function?.name === name);
209
+ if (!tool) {
210
+ toolResults.push({
211
+ tool_call_id: id,
212
+ role: 'tool',
213
+ name,
214
+ content: JSON.stringify({ error: `Tool '${name}' not found` })
215
+ });
216
+ continue;
217
+ }
218
+
219
+ // Execute the tool
220
+ try {
221
+ const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args;
222
+ const result = await tool.execute(parsedArgs);
223
+
224
+ toolResults.push({
225
+ tool_call_id: id,
226
+ role: 'tool',
227
+ name,
228
+ content: typeof result === 'string' ? result : JSON.stringify(result)
229
+ });
230
+ } catch (error) {
231
+ toolResults.push({
232
+ tool_call_id: id,
233
+ role: 'tool',
234
+ name,
235
+ content: JSON.stringify({ error: error.message })
236
+ });
237
+ }
238
+ }
239
+
240
+ // Add tool results to the messages array
241
+ const updatedMessages = [...messages, responseMessage, ...toolResults];
242
+
243
+ // Make a new API call to let the AI process the tool results
244
+ const aiResponse = await callAI(node, { ...aiConfig, tools: null }, updatedMessages);
245
+
246
+ // Return the final AI response
247
+ return aiResponse;
248
+ } catch (error) {
249
+ return `Error processing tool calls: ${error.message}`;
250
+ }
251
+ }
252
+
253
+ module.exports = function (RED) {
254
+ /**
255
+ * AI Agent Node
256
+ * @param {Object} config - The node configuration object
257
+ */
258
+ function AiAgentNode(config) {
259
+ RED.nodes.createNode(this, config);
260
+ const node = this;
261
+
262
+ // Configuration
263
+ this.agentName = config.name || 'AI Agent';
264
+ this.systemPrompt = config.systemPrompt || 'You are a helpful AI assistant.';
265
+ this.responseType = config.responseType || 'text';
266
+
267
+ // Handle node cleanup
268
+ node.on('close', function (done) {
269
+ node.status({});
270
+ if (done) done();
271
+ });
272
+
273
+ // Process incoming messages
274
+ node.on('input', async function (msg, send, done) {
275
+ node.status({ fill: 'blue', shape: 'dot', text: 'processing...' });
276
+
277
+ try {
278
+ // 1. Validate AI configuration
279
+ const validationError = validateAIConfig(msg.aiagent);
280
+ if (validationError) {
281
+ throw new Error(validationError);
282
+ }
283
+
284
+ // 2. Get input
285
+ const input = msg.payload || {};
286
+ const inputText = typeof input === 'string' ? input : JSON.stringify(input);
287
+
288
+ // 3. Prepare prompt with context
289
+ const { messages, userMessage } = preparePrompt(node, msg, inputText);
290
+
291
+ // 4. Execute the prompt and get response
292
+ const response = await callAI(node, msg.aiagent, messages);
293
+
294
+ // 5. Update context if using memory
295
+ if (msg.aimemory && userMessage) {
296
+ updateContext(msg, userMessage, response);
297
+ }
298
+
299
+ // 6. Format and send response
300
+ msg.payload = node.responseType === 'object' ? {
301
+ agent: node.agentName,
302
+ type: 'ai',
303
+ input: input,
304
+ response: response,
305
+ timestamp: new Date().toISOString(),
306
+ context: {
307
+ conversationLength: msg.aimemory?.context?.length || 0,
308
+ lastInteraction: new Date().toISOString(),
309
+ ...(msg.aimemory && { aimemory: msg.aimemory })
310
+ }
311
+ } : response;
312
+
313
+ send(msg);
314
+ node.status({ fill: 'green', shape: 'dot', text: 'ready' });
315
+
316
+ } catch (error) {
317
+ handleError(node, msg, error);
318
+ } finally {
319
+ if (done) done();
320
+ }
321
+ });
322
+ }
323
+
324
+ // Register the node type
325
+ RED.nodes.registerType('ai-agent', AiAgentNode);
326
+ };
@@ -9,8 +9,8 @@
9
9
  defaultGoal: { value: "" }
10
10
  },
11
11
  inputs: 1,
12
- outputs: 2,
13
- outputLabels: ["Task Dispatch", "Final Result"],
12
+ outputs: 1,
13
+ outputLabels: ["Final Result"],
14
14
  icon: "font-awesome/fa-sitemap",
15
15
  label: function () {
16
16
  return this.name || "ai orchestrator";
@@ -1,5 +1,13 @@
1
1
  const axios = require('axios');
2
2
 
3
+ /**
4
+ * AI Orchestrator Node - Manages multi-agent task execution with planning and reflection
5
+ * @param {Object} config - Node configuration object
6
+ * @param {string} config.name - Node name
7
+ * @param {number} config.maxIterations - Maximum number of planning/execution iterations
8
+ * @param {string} config.planningStrategy - Strategy for plan creation ('simple' or 'advanced')
9
+ * @param {string} config.defaultGoal - Default goal if none provided in message
10
+ */
3
11
  module.exports = function (RED) {
4
12
  function AiOrchestratorNode(config) {
5
13
  RED.nodes.createNode(this, config);
@@ -12,16 +20,21 @@ module.exports = function (RED) {
12
20
 
13
21
  node.on('input', async function (msg, send, done) {
14
22
  send = send || function () { node.send.apply(node, arguments) };
15
- node.status({ fill: 'blue', shape: 'dot', text: 'thinking...' });
16
23
 
17
24
  try {
18
25
  // Initialize orchestration state if not present
19
26
  if (!msg.orchestration) {
27
+ node.status({ fill: 'blue', shape: 'dot', text: 'initializing team...' });
28
+
29
+ // Pipeline Discovery: Extract agents from upstream chain
30
+ const availableAgents = msg.agents || [];
31
+
20
32
  msg.orchestration = {
21
33
  planId: 'plan-' + Date.now(),
22
34
  iterations: 0,
23
35
  goal: msg.payload || node.defaultGoal,
24
36
  status: 'planning',
37
+ availableAgents: availableAgents,
25
38
  history: [],
26
39
  plan: null
27
40
  };
@@ -35,7 +48,7 @@ module.exports = function (RED) {
35
48
  msg.orchestration.status = 'failed';
36
49
  msg.orchestration.error = 'Max iterations reached';
37
50
  node.status({ fill: 'red', shape: 'dot', text: 'max iterations' });
38
- send([null, msg]); // Output 2 for final result
51
+ send(msg);
39
52
  if (done) done();
40
53
  return;
41
54
  }
@@ -45,32 +58,61 @@ module.exports = function (RED) {
45
58
  throw new Error('AI Model configuration missing or API key not found.');
46
59
  }
47
60
 
48
- // Logic based on current status
49
- if (msg.orchestration.status === 'planning' || !msg.orchestration.plan) {
50
- await createInitialPlan(node, msg);
51
- } else if (msg.orchestration.currentTaskId) {
52
- await reflectAndRefine(node, msg);
53
- }
61
+ // Inner loop for Zero-Wire execution
62
+ while (msg.orchestration.status !== 'completed' && msg.orchestration.status !== 'failed') {
54
63
 
55
- // Dispatch or Finalize
56
- if (msg.orchestration.status === 'completed' || msg.orchestration.status === 'failed') {
57
- node.status({ fill: 'green', shape: 'dot', text: msg.orchestration.status });
58
- send([null, msg]); // Output 2
59
- } else {
64
+ // 1. Planning Phase
65
+ if (msg.orchestration.status === 'planning' || !msg.orchestration.plan) {
66
+ node.status({ fill: 'blue', shape: 'dot', text: 'planning...' });
67
+ await createInitialPlan(node, msg);
68
+ }
69
+
70
+ // 2. Find Next Task
60
71
  const nextTask = getNextTask(msg.orchestration.plan);
61
- if (nextTask) {
62
- msg.payload = nextTask.input;
63
- msg.topic = nextTask.type;
64
- msg.orchestration.currentTaskId = nextTask.id;
65
- node.status({ fill: 'blue', shape: 'ring', text: `dispatching: ${nextTask.id}` });
66
- send([msg, null]); // Output 1
67
- } else {
72
+ if (!nextTask) {
68
73
  msg.orchestration.status = 'completed';
69
- node.status({ fill: 'green', shape: 'dot', text: 'completed' });
70
- send([null, msg]); // Output 2
74
+ break;
75
+ }
76
+
77
+ // 3. Execution Phase (Direct Call)
78
+ msg.orchestration.currentTaskId = nextTask.id;
79
+ const agentInfo = msg.orchestration.availableAgents.find(a =>
80
+ a.capabilities.some(cap => cap.toLowerCase() === nextTask.type.toLowerCase())
81
+ );
82
+
83
+ if (!agentInfo) {
84
+ node.warn(`No registered agent found for capability: ${nextTask.type}`);
85
+ throw new Error(`Capability not provided by any wired agent: ${nextTask.type}`);
86
+ }
87
+
88
+ const agentNode = RED.nodes.getNode(agentInfo.id);
89
+ if (!agentNode || typeof agentNode.executeTask !== 'function') {
90
+ throw new Error(`Agent node ${agentInfo.name} [${agentInfo.id}] is not an AI Agent Orchestrator or is missing executeTask API.`);
91
+ }
92
+
93
+ node.status({ fill: 'blue', shape: 'ring', text: `agent: ${agentInfo.name}` });
94
+ try {
95
+ const result = await agentNode.executeTask(nextTask.input, msg);
96
+ msg.payload = result;
97
+ msg.error = null;
98
+ } catch (err) {
99
+ // Strip 'AI API Error: ' prefix if present to match test expectations
100
+ let errorMessage = err.message;
101
+ if (errorMessage.startsWith('AI API Error: ')) {
102
+ errorMessage = errorMessage.substring('AI API Error: '.length);
103
+ }
104
+ msg.error = errorMessage;
71
105
  }
106
+
107
+ // 4. Reflection Phase
108
+ node.status({ fill: 'blue', shape: 'dot', text: 'reflecting...' });
109
+ await reflectAndRefine(node, msg);
72
110
  }
73
111
 
112
+ // Final Output
113
+ node.status({ fill: 'green', shape: 'dot', text: msg.orchestration.status });
114
+ send(msg);
115
+
74
116
  if (done) done();
75
117
  } catch (error) {
76
118
  node.status({ fill: 'red', shape: 'ring', text: 'error' });
@@ -80,20 +122,27 @@ module.exports = function (RED) {
80
122
  });
81
123
  }
82
124
 
125
+ /**
126
+ * Creates an initial execution plan using AI
127
+ * @param {Object} node - The orchestrator node instance
128
+ * @param {Object} msg - The message object containing orchestration state
129
+ * @throws {Error} If planning fails
130
+ */
83
131
  async function createInitialPlan(node, msg) {
84
132
  const goal = msg.orchestration.goal;
85
133
  const strategy = node.planningStrategy;
134
+ const agents = msg.orchestration.availableAgents || [];
135
+ const agentManifest = agents.map(a => `- ${a.name}: [${a.capabilities.join(', ')}]`).join('\n');
86
136
 
87
- let prompt = `Goal: ${goal}\n\nDecompose this goal into a series of tasks for AI agents.
137
+ let prompt = `Goal: ${goal}\n\nAvailable Agents and their Capabilities:\n${agentManifest}\n\nDecompose this goal into a series of tasks. You MUST ONLY use capabilities provided by the available agents listed above.
88
138
  Return a JSON object with a "tasks" array. Each task should have:
89
139
  - "id": a short string id (e.g., "t1", "t2")
90
- - "type": the type of task (e.g., "research", "code", "review")
140
+ - "type": the name of the REQUIRED capability from the list above
91
141
  - "input": detailed instruction for the agent
92
142
  - "status": "pending"
93
143
  - "priority": a number (1-10, default 5)
94
144
  - "dependsOn": an array of IDs of tasks that must be completed BEFORE this task can start (empty array if none)
95
-
96
- Note: You can use "human_approval" as a task type if you need a human to verify something before proceeding.`;
145
+ `;
97
146
 
98
147
  if (strategy === 'advanced') {
99
148
  prompt += `\n\nThink about parallel execution. Group related tasks and identify bottlenecks. Ensure dependencies are logical.`;
@@ -117,6 +166,11 @@ Note: You can use "human_approval" as a task type if you need a human to verify
117
166
  }
118
167
  }
119
168
 
169
+ /**
170
+ * Reflects on task execution results and refines the plan
171
+ * @param {Object} node - The orchestrator node instance
172
+ * @param {Object} msg - The message object containing orchestration state and task results
173
+ */
120
174
  async function reflectAndRefine(node, msg) {
121
175
  const currentTaskId = msg.orchestration.currentTaskId;
122
176
  const taskResult = msg.payload;
@@ -174,6 +228,11 @@ Return a JSON object:
174
228
  }
175
229
  }
176
230
 
231
+ /**
232
+ * Gets the next executable task from the plan based on dependencies and priority
233
+ * @param {Object} plan - The execution plan containing tasks
234
+ * @returns {Object|null} The next task to execute or null if no eligible tasks
235
+ */
177
236
  function getNextTask(plan) {
178
237
  if (!plan || !plan.tasks) return null;
179
238
 
@@ -202,6 +261,14 @@ Return a JSON object:
202
261
  return eligibleTasks[0];
203
262
  }
204
263
 
264
+ /**
265
+ * Makes an API call to the AI model
266
+ * @param {Object} aiConfig - AI configuration containing model and API key
267
+ * @param {string} prompt - The user prompt to send
268
+ * @param {string} systemPrompt - The system prompt for context
269
+ * @returns {Promise<string>} The AI response content
270
+ * @throws {Error} If API call fails
271
+ */
205
272
  async function callAI(aiConfig, prompt, systemPrompt) {
206
273
  const response = await axios.post(
207
274
  'https://openrouter.ai/api/v1/chat/completions',
@@ -223,6 +290,11 @@ Return a JSON object:
223
290
  return response.data.choices[0]?.message?.content || '';
224
291
  }
225
292
 
293
+ /**
294
+ * Extracts JSON from a text response
295
+ * @param {string} text - The text containing JSON
296
+ * @returns {string} The extracted JSON string
297
+ */
226
298
  function extractJson(text) {
227
299
  const match = text.match(/\{[\s\S]*\}/);
228
300
  return match ? match[0] : text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ai-agent",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI Agent for Node-RED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -64,6 +64,7 @@
64
64
  "ai-memory-file": "./memory-file/memory-file.js",
65
65
  "ai-memory-inmem": "./memory-inmem/memory-inmem.js",
66
66
  "ai-orchestrator": "./orchestrator/orchestrator.js",
67
+ "ai-agent-orchestrator": "./agent-orchestrator/agent-orchestrator.js",
67
68
  "ai-tool-approval": "./tool-approval/ai-tool-approval.js"
68
69
  }
69
70
  }