node-red-contrib-ai-agent 0.1.0 → 0.3.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 +28 -0
- package/orchestrator/orchestrator.html +82 -0
- package/orchestrator/orchestrator.js +229 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Your feedback and contributions are highly appreciated!
|
|
|
23
23
|
- **In-Memory**: Store conversation context in memory (volatile)
|
|
24
24
|
- **File-based**: Persist conversation context to disk
|
|
25
25
|
- **AI Model Node**: Configure AI models and API settings
|
|
26
|
+
- **AI Orchestrator Node**: Coordinate multiple agents and create autonomous plans
|
|
26
27
|
- **Tool Integration**: Extend functionality with custom tools
|
|
27
28
|
- **Stateless Design**: Memory nodes are stateless, making them more reliable and scalable
|
|
28
29
|
- **Context Management**: Automatic conversation history management with configurable retention
|
|
@@ -89,6 +90,21 @@ Configures the AI model and API settings.
|
|
|
89
90
|
- **API Key**: Your OpenRouter API key
|
|
90
91
|
- **Name**: Display name for the node
|
|
91
92
|
|
|
93
|
+
### 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.
|
|
95
|
+
|
|
96
|
+
**Key Features:**
|
|
97
|
+
- **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).
|
|
101
|
+
|
|
102
|
+
**Properties:**
|
|
103
|
+
- **Max Iterations**: Maximum cycles for the autonomy loop
|
|
104
|
+
- **Planning Strategy**: Simple (linear) or Advanced (dependency-aware)
|
|
105
|
+
- **Default Goal**: Optional fallback goal
|
|
106
|
+
- **Name**: Display name for the node
|
|
107
|
+
|
|
92
108
|
### AI Tool Function
|
|
93
109
|
Creates a JavaScript function tool that can be used by the AI Agent.
|
|
94
110
|
|
|
@@ -159,6 +175,18 @@ For more complex scenarios, you can chain multiple agents to process messages in
|
|
|
159
175
|
|
|
160
176
|
Each agent will maintain its own conversation context based on its memory configuration.
|
|
161
177
|
|
|
178
|
+
## Example: Autonomous Orchestration
|
|
179
|
+
|
|
180
|
+
The AI Orchestrator can manage complex, multi-step tasks:
|
|
181
|
+
|
|
182
|
+
1. Add an **AI Orchestrator** node
|
|
183
|
+
2. Connect its first output to an **AI Agent**
|
|
184
|
+
3. Connect the agent's output back to the **AI Orchestrator** input
|
|
185
|
+
4. Connect the orchestrator's second output to a **Debug** node
|
|
186
|
+
5. Configure the orchestrator with a goal (e.g., "Write a blog post and then translate it to Spanish")
|
|
187
|
+
|
|
188
|
+
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.
|
|
189
|
+
|
|
162
190
|
## Best Practices
|
|
163
191
|
|
|
164
192
|
### Memory Management
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('ai-orchestrator', {
|
|
3
|
+
category: 'ai agent',
|
|
4
|
+
color: '#E2D96E',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
maxIterations: { value: 5, validate: RED.validators.number() },
|
|
8
|
+
planningStrategy: { value: "simple" },
|
|
9
|
+
defaultGoal: { value: "" }
|
|
10
|
+
},
|
|
11
|
+
inputs: 1,
|
|
12
|
+
outputs: 2,
|
|
13
|
+
outputLabels: ["Task Dispatch", "Final Result"],
|
|
14
|
+
icon: "font-awesome/fa-sitemap",
|
|
15
|
+
label: function () {
|
|
16
|
+
return this.name || "ai orchestrator";
|
|
17
|
+
},
|
|
18
|
+
paletteLabel: "ai orchestrator"
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script type="text/html" data-template-name="ai-orchestrator">
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
25
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
26
|
+
</div>
|
|
27
|
+
<div class="form-row">
|
|
28
|
+
<label for="node-input-maxIterations"><i class="fa fa-repeat"></i> Max Iterations</label>
|
|
29
|
+
<input type="number" id="node-input-maxIterations" placeholder="5">
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-input-planningStrategy"><i class="fa fa-cog"></i> Planning Strategy</label>
|
|
33
|
+
<select id="node-input-planningStrategy">
|
|
34
|
+
<option value="simple">Simple (Linear)</option>
|
|
35
|
+
<option value="advanced">Advanced (LLM-optimized)</option>
|
|
36
|
+
</select>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="form-row">
|
|
39
|
+
<label for="node-input-defaultGoal"><i class="fa fa-bullseye"></i> Default Goal</label>
|
|
40
|
+
<textarea id="node-input-defaultGoal" style="width: 100%" rows="3" placeholder="Optional default goal for the orchestrator"></textarea>
|
|
41
|
+
</div>
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<script type="text/html" data-content-help-name="ai-orchestrator">
|
|
45
|
+
<p>The AI Orchestrator node manages complex tasks by decomposing them into a plan and dispatching individual tasks to other AI agents.</p>
|
|
46
|
+
|
|
47
|
+
<h3>Inputs</h3>
|
|
48
|
+
<dl class="message-properties">
|
|
49
|
+
<dt>payload <span class="property-type">string | object</span></dt>
|
|
50
|
+
<dd>The goal or task description if starting a new plan.</dd>
|
|
51
|
+
<dt>orchestration <span class="property-type">object</span></dt>
|
|
52
|
+
<dd>The internal state of the orchestration loop (passed back from agents).</dd>
|
|
53
|
+
<dt>aiagent <span class="property-type">object</span></dt>
|
|
54
|
+
<dd>The AI model configuration (required for planning and reflection).</dd>
|
|
55
|
+
</dl>
|
|
56
|
+
|
|
57
|
+
<h3>Outputs</h3>
|
|
58
|
+
<ol class="node-ports">
|
|
59
|
+
<li>Task Dispatch
|
|
60
|
+
<dl class="message-properties">
|
|
61
|
+
<dt>msg.payload <span class="property-type">string</span></dt>
|
|
62
|
+
<dd>The instruction for the next task.</dd>
|
|
63
|
+
</dl>
|
|
64
|
+
</li>
|
|
65
|
+
<li>Final Result
|
|
66
|
+
<dl class="message-properties">
|
|
67
|
+
<dt>msg.orchestration <span class="property-type">object</span></dt>
|
|
68
|
+
<dd>The full orchestration history and final result status.</dd>
|
|
69
|
+
</dl>
|
|
70
|
+
</li>
|
|
71
|
+
</ol>
|
|
72
|
+
|
|
73
|
+
<h3>Details</h3>
|
|
74
|
+
<p>Connect this node to one or more AI agents. The first output should flow into an agent, and the agent's output should loop back to this node's input.</p>
|
|
75
|
+
<p>The orchestrator supports advanced planning features in "Advanced" mode:</p>
|
|
76
|
+
<ul>
|
|
77
|
+
<li><strong>Dependencies:</strong> Tasks can wait for other tasks to complete (<code>dependsOn</code> array).</li>
|
|
78
|
+
<li><strong>Priorities:</strong> Tasks with higher <code>priority</code> numbers are executed first.</li>
|
|
79
|
+
<li><strong>Error Recovery:</strong> If a task fails, the orchestrator reflects on the error and can revise the plan to retry or try an alternative approach.</li>
|
|
80
|
+
</ul>
|
|
81
|
+
<p>When the goal is achieved or max iterations are reached, the final message is sent to the second output.</p>
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
module.exports = function (RED) {
|
|
4
|
+
function AiOrchestratorNode(config) {
|
|
5
|
+
RED.nodes.createNode(this, config);
|
|
6
|
+
const node = this;
|
|
7
|
+
|
|
8
|
+
this.name = config.name || 'AI Orchestrator';
|
|
9
|
+
this.maxIterations = parseInt(config.maxIterations) || 5;
|
|
10
|
+
this.planningStrategy = config.planningStrategy || 'simple';
|
|
11
|
+
this.defaultGoal = config.defaultGoal || '';
|
|
12
|
+
|
|
13
|
+
node.on('input', async function (msg, send, done) {
|
|
14
|
+
send = send || function () { node.send.apply(node, arguments) };
|
|
15
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'thinking...' });
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Initialize orchestration state if not present
|
|
19
|
+
if (!msg.orchestration) {
|
|
20
|
+
msg.orchestration = {
|
|
21
|
+
planId: 'plan-' + Date.now(),
|
|
22
|
+
iterations: 0,
|
|
23
|
+
goal: msg.payload || node.defaultGoal,
|
|
24
|
+
status: 'planning',
|
|
25
|
+
history: [],
|
|
26
|
+
plan: null
|
|
27
|
+
};
|
|
28
|
+
} else {
|
|
29
|
+
msg.orchestration.iterations++;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check for max iterations
|
|
33
|
+
if (msg.orchestration.iterations >= node.maxIterations) {
|
|
34
|
+
node.warn('Max iterations reached');
|
|
35
|
+
msg.orchestration.status = 'failed';
|
|
36
|
+
msg.orchestration.error = 'Max iterations reached';
|
|
37
|
+
node.status({ fill: 'red', shape: 'dot', text: 'max iterations' });
|
|
38
|
+
send([null, msg]); // Output 2 for final result
|
|
39
|
+
if (done) done();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// AI Configuration check
|
|
44
|
+
if (!msg.aiagent || !msg.aiagent.apiKey) {
|
|
45
|
+
throw new Error('AI Model configuration missing or API key not found.');
|
|
46
|
+
}
|
|
47
|
+
|
|
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
|
+
}
|
|
54
|
+
|
|
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 {
|
|
60
|
+
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 {
|
|
68
|
+
msg.orchestration.status = 'completed';
|
|
69
|
+
node.status({ fill: 'green', shape: 'dot', text: 'completed' });
|
|
70
|
+
send([null, msg]); // Output 2
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (done) done();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
node.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
77
|
+
node.error(error.message, msg);
|
|
78
|
+
if (done) done(error);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function createInitialPlan(node, msg) {
|
|
84
|
+
const goal = msg.orchestration.goal;
|
|
85
|
+
const strategy = node.planningStrategy;
|
|
86
|
+
|
|
87
|
+
let prompt = `Goal: ${goal}\n\nDecompose this goal into a series of tasks for AI agents.
|
|
88
|
+
Return a JSON object with a "tasks" array. Each task should have:
|
|
89
|
+
- "id": a short string id (e.g., "t1", "t2")
|
|
90
|
+
- "type": the type of task (e.g., "research", "code", "review")
|
|
91
|
+
- "input": detailed instruction for the agent
|
|
92
|
+
- "status": "pending"
|
|
93
|
+
- "priority": a number (1-10, default 5)
|
|
94
|
+
- "dependsOn": an array of IDs of tasks that must be completed BEFORE this task can start (empty array if none)`;
|
|
95
|
+
|
|
96
|
+
if (strategy === 'advanced') {
|
|
97
|
+
prompt += `\n\nThink about parallel execution. Group related tasks and identify bottlenecks. Ensure dependencies are logical.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
prompt += `\n\nExample:
|
|
101
|
+
{
|
|
102
|
+
"tasks": [
|
|
103
|
+
{"id": "t1", "type": "research", "input": "...", "status": "pending", "priority": 10, "dependsOn": []},
|
|
104
|
+
{"id": "t2", "type": "implementation", "input": "...", "status": "pending", "priority": 5, "dependsOn": ["t1"]}
|
|
105
|
+
]
|
|
106
|
+
}`;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await callAI(msg.aiagent, prompt, "You are an AI Orchestrator that creates non-linear plans with dependencies.");
|
|
110
|
+
const planData = JSON.parse(extractJson(response));
|
|
111
|
+
msg.orchestration.plan = planData;
|
|
112
|
+
msg.orchestration.status = 'executing';
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new Error(`Planning failed: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function reflectAndRefine(node, msg) {
|
|
119
|
+
const currentTaskId = msg.orchestration.currentTaskId;
|
|
120
|
+
const taskResult = msg.payload;
|
|
121
|
+
const isError = msg.error ? true : false;
|
|
122
|
+
|
|
123
|
+
// Update history
|
|
124
|
+
msg.orchestration.history.push({
|
|
125
|
+
taskId: currentTaskId,
|
|
126
|
+
result: taskResult,
|
|
127
|
+
error: msg.error,
|
|
128
|
+
timestamp: new Date().toISOString()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Update task status in plan
|
|
132
|
+
const task = msg.orchestration.plan.tasks.find(t => t.id === currentTaskId);
|
|
133
|
+
if (task) {
|
|
134
|
+
if (isError) {
|
|
135
|
+
task.status = 'failed';
|
|
136
|
+
task.error = msg.error;
|
|
137
|
+
} else {
|
|
138
|
+
task.status = 'completed';
|
|
139
|
+
task.output = taskResult;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const prompt = `Current Goal: ${msg.orchestration.goal}
|
|
144
|
+
Current Plan: ${JSON.stringify(msg.orchestration.plan)}
|
|
145
|
+
Last Task ID: ${currentTaskId}
|
|
146
|
+
Last Task ${isError ? 'Error' : 'Result'}: ${JSON.stringify(isError ? msg.error : taskResult)}
|
|
147
|
+
|
|
148
|
+
Evaluate the progress.
|
|
149
|
+
1. If the last task failed, propose a recovery strategy (retry, alternative task, or fail the goal).
|
|
150
|
+
2. If the goal is achieved, set status to "completed".
|
|
151
|
+
3. Otherwise, continue execution. You may refine the plan by adding, removing, or modifying tasks.
|
|
152
|
+
|
|
153
|
+
Return a JSON object:
|
|
154
|
+
{
|
|
155
|
+
"analysis": "detailed evaluation of progress and next steps",
|
|
156
|
+
"status": "executing" | "completed" | "failed",
|
|
157
|
+
"updatedPlan": { "tasks": [...] }
|
|
158
|
+
}`;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const response = await callAI(msg.aiagent, prompt, "You are an AI Orchestrator that reflects on progress and manages plan revisions.");
|
|
162
|
+
const reflection = JSON.parse(extractJson(response));
|
|
163
|
+
|
|
164
|
+
msg.orchestration.status = reflection.status;
|
|
165
|
+
if (reflection.updatedPlan) {
|
|
166
|
+
msg.orchestration.plan = reflection.updatedPlan;
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
node.warn(`Reflection failed, continuing with current plan state: ${error.message}`);
|
|
170
|
+
// Fallback: stay in executing status if it was executing, let getNextTask decide
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getNextTask(plan) {
|
|
175
|
+
if (!plan || !plan.tasks) return null;
|
|
176
|
+
|
|
177
|
+
// Find tasks that are pending AND all their dependencies are completed
|
|
178
|
+
const eligibleTasks = plan.tasks.filter(t => {
|
|
179
|
+
if (t.status !== 'pending') return false;
|
|
180
|
+
|
|
181
|
+
if (!t.dependsOn || t.dependsOn.length === 0) return true;
|
|
182
|
+
|
|
183
|
+
return t.dependsOn.every(depId => {
|
|
184
|
+
const depTask = plan.tasks.find(pt => pt.id === depId);
|
|
185
|
+
return depTask && depTask.status === 'completed';
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (eligibleTasks.length === 0) return null;
|
|
190
|
+
|
|
191
|
+
// Sort by priority (descending) then by ID
|
|
192
|
+
eligibleTasks.sort((a, b) => {
|
|
193
|
+
const priorityA = a.priority || 5;
|
|
194
|
+
const priorityB = b.priority || 5;
|
|
195
|
+
if (priorityB !== priorityA) return priorityB - priorityA;
|
|
196
|
+
return a.id.localeCompare(b.id);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return eligibleTasks[0];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function callAI(aiConfig, prompt, systemPrompt) {
|
|
203
|
+
const response = await axios.post(
|
|
204
|
+
'https://openrouter.ai/api/v1/chat/completions',
|
|
205
|
+
{
|
|
206
|
+
model: aiConfig.model,
|
|
207
|
+
messages: [
|
|
208
|
+
{ role: 'system', content: systemPrompt },
|
|
209
|
+
{ role: 'user', content: prompt }
|
|
210
|
+
],
|
|
211
|
+
response_format: { type: 'json_object' }
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
headers: {
|
|
215
|
+
'Authorization': `Bearer ${aiConfig.apiKey}`,
|
|
216
|
+
'Content-Type': 'application/json'
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
return response.data.choices[0]?.message?.content || '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function extractJson(text) {
|
|
224
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
225
|
+
return match ? match[0] : text;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
RED.nodes.registerType('ai-orchestrator', AiOrchestratorNode);
|
|
229
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-ai-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI Agent for Node-RED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"tool-function/*",
|
|
33
33
|
"tool-http/*",
|
|
34
34
|
"memory-file/*",
|
|
35
|
-
"memory-inmem/*"
|
|
35
|
+
"memory-inmem/*",
|
|
36
|
+
"orchestrator/*"
|
|
36
37
|
],
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"axios": "^1.6.0",
|
|
@@ -60,7 +61,8 @@
|
|
|
60
61
|
"ai-tool-function": "./tool-function/ai-tool-function.js",
|
|
61
62
|
"ai-tool-http": "./tool-http/ai-tool-http.js",
|
|
62
63
|
"ai-memory-file": "./memory-file/memory-file.js",
|
|
63
|
-
"ai-memory-inmem": "./memory-inmem/memory-inmem.js"
|
|
64
|
+
"ai-memory-inmem": "./memory-inmem/memory-inmem.js",
|
|
65
|
+
"ai-orchestrator": "./orchestrator/orchestrator.js"
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
}
|