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 +20 -12
- package/agent/ai-agent.html +95 -95
- package/agent/ai-agent.js +326 -326
- package/orchestrator/orchestrator.html +2 -2
- package/orchestrator/orchestrator.js +98 -26
- package/package.json +2 -1
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
|
|
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
|
-
- **
|
|
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.
|
|
195
|
-
3. Connect
|
|
196
|
-
4.
|
|
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
|
-
|
|
207
|
+
This linear architecture keeps your flows clean while allowing for powerful, multi-agent collaboration.
|
|
200
208
|
|
|
201
209
|
## Best Practices
|
|
202
210
|
|
package/agent/ai-agent.html
CHANGED
|
@@ -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:
|
|
13
|
-
outputLabels: ["
|
|
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(
|
|
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
|
-
//
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
}
|