node-red-contrib-vectorprime 0.1.9 → 0.1.11
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 +24 -47
- package/package.json +1 -1
- package/vectorprime.html +30 -50
- package/vectorprime.js +68 -75
package/README.md
CHANGED
|
@@ -1,68 +1,45 @@
|
|
|
1
1
|
# node-red-contrib-vectorprime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**VectorPrime Rank** — automatically rank tasks, alerts, or decisions using the VectorPrime Decision Kernel.
|
|
4
4
|
|
|
5
|
-
This node
|
|
5
|
+
This node helps you pick the **best next action** when you have multiple choices and need to choose the optimal one fast.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **Output:** `msg.payload` (VectorPrime response JSON)
|
|
7
|
+
✅ Prioritize tasks
|
|
8
|
+
✅ Rank alerts by urgency
|
|
9
|
+
✅ Choose the best option from multiple choices
|
|
10
|
+
✅ Turn “too many choices” into a single clear answer
|
|
11
|
+
✅ Works inside Node-RED flows with 1 click
|
|
13
12
|
|
|
14
13
|
---
|
|
15
14
|
|
|
16
|
-
##
|
|
15
|
+
## What it does (in 1 sentence)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`/v1/kernel/rank`
|
|
17
|
+
**Send a list of options → get them ranked best-to-worst with a recommended top choice.**
|
|
21
18
|
|
|
22
19
|
---
|
|
23
20
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
### 1) Drag these nodes into your flow:
|
|
21
|
+
## Why developers install this
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
- **VectorPrime**
|
|
30
|
-
- **Debug**
|
|
23
|
+
Most automation flows break down when you have **multiple possible actions** and don’t know which one to run.
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
VectorPrime solves that by turning this:
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
- “Which alert should fire first?”
|
|
28
|
+
- “Which ticket should I do next?”
|
|
29
|
+
- “Which customer should get priority?”
|
|
30
|
+
- “Which action gives the best outcome with least risk?”
|
|
31
|
+
- “Which fix should I deploy first?”
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
Into:
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
✅ Paste your VectorPrime API key into the node field
|
|
41
|
-
(or use the **Get Free Key** button if enabled)
|
|
42
|
-
|
|
43
|
-
### 4) Run it
|
|
44
|
-
|
|
45
|
-
1. Click **Deploy**
|
|
46
|
-
2. Press the **Inject** button
|
|
47
|
-
3. View output in the **Debug** sidebar
|
|
35
|
+
✅ **One ranked result** (best option on top)
|
|
48
36
|
|
|
49
37
|
---
|
|
50
38
|
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
### IMPORTANT:
|
|
54
|
-
Your Inject node must send a JSON object inside `msg.payload`.
|
|
39
|
+
## Quick Start (60 seconds)
|
|
55
40
|
|
|
56
|
-
|
|
41
|
+
### ✅ 1) Install the node
|
|
42
|
+
In your Node-RED user directory (usually `C:\Users\mxz\.node-red`):
|
|
57
43
|
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
"decision_id": "node-red-demo-001",
|
|
61
|
-
"prompt": "Pick one.",
|
|
62
|
-
"options": [
|
|
63
|
-
{ "id": "a", "label": "Option A" },
|
|
64
|
-
{ "id": "b", "label": "Option B" }
|
|
65
|
-
],
|
|
66
|
-
"engine": "classical",
|
|
67
|
-
"seed": 123
|
|
68
|
-
}
|
|
44
|
+
```bash
|
|
45
|
+
npm install node-red-contrib-vectorprime
|
package/package.json
CHANGED
package/vectorprime.html
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- =========================
|
|
2
|
+
VectorPrime Config Template
|
|
3
|
+
========================= -->
|
|
4
|
+
<script type="text/x-red" data-template-name="vectorprime-config">
|
|
2
5
|
<div class="form-row">
|
|
3
6
|
<label for="node-config-input-baseUrl">
|
|
4
7
|
<i class="fa fa-globe"></i> Base URL
|
|
@@ -105,7 +108,7 @@
|
|
|
105
108
|
</script>
|
|
106
109
|
|
|
107
110
|
<!-- ✅ HELP TAB: vectorprime-config -->
|
|
108
|
-
<script type="text/
|
|
111
|
+
<script type="text/x-red" data-help-name="vectorprime-config">
|
|
109
112
|
<p><b>VectorPrime Config</b> stores your Base URL and API key securely.</p>
|
|
110
113
|
|
|
111
114
|
<h3>Quick Setup</h3>
|
|
@@ -130,7 +133,10 @@
|
|
|
130
133
|
|
|
131
134
|
<hr />
|
|
132
135
|
|
|
133
|
-
|
|
136
|
+
<!-- =========================
|
|
137
|
+
VectorPrime Rank Template
|
|
138
|
+
========================= -->
|
|
139
|
+
<script type="text/x-red" data-template-name="vectorprime-rank">
|
|
134
140
|
<div class="form-row">
|
|
135
141
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
136
142
|
<input type="text" id="node-input-name" placeholder="Rank Decision (VectorPrime)">
|
|
@@ -169,9 +175,6 @@
|
|
|
169
175
|
// ✅ This is the name users see in the palette
|
|
170
176
|
paletteLabel: "Rank Decision (VectorPrime)",
|
|
171
177
|
|
|
172
|
-
// ✅ This is the short description used in the tooltip card
|
|
173
|
-
description: "Automatically ranks tasks, alerts, or choices in msg.payload so your flow can pick the best next action.",
|
|
174
|
-
|
|
175
178
|
label: function () {
|
|
176
179
|
return this.name || "Rank Decision (VectorPrime)";
|
|
177
180
|
}
|
|
@@ -179,29 +182,29 @@
|
|
|
179
182
|
</script>
|
|
180
183
|
|
|
181
184
|
<!-- ✅ HELP TAB: vectorprime-rank -->
|
|
182
|
-
<script type="text/
|
|
185
|
+
<script type="text/x-red" data-help-name="vectorprime-rank">
|
|
183
186
|
<p>
|
|
184
|
-
<b>Rank Decision (VectorPrime)</b>
|
|
185
|
-
Use it anywhere
|
|
187
|
+
<b>Rank Decision (VectorPrime)</b> picks the best next action automatically.
|
|
188
|
+
Use it anywhere your flow needs <b>priority scoring</b> or <b>decision ranking</b>.
|
|
186
189
|
</p>
|
|
187
190
|
|
|
188
191
|
<h3>What it does</h3>
|
|
189
192
|
<ul>
|
|
190
|
-
<li>Reads
|
|
191
|
-
<li>Calls VectorPrime Rank API</li>
|
|
192
|
-
<li>Returns the ranked decision
|
|
193
|
+
<li>Reads options from <code>msg.payload</code></li>
|
|
194
|
+
<li>Calls the VectorPrime Decision Kernel Rank API</li>
|
|
195
|
+
<li>Returns the ranked decision inside <code>msg.payload</code></li>
|
|
193
196
|
</ul>
|
|
194
197
|
|
|
195
|
-
<h3>
|
|
198
|
+
<h3>Where developers use this most</h3>
|
|
196
199
|
<ul>
|
|
197
|
-
<li><b>Incident response:</b> rank alerts
|
|
198
|
-
<li><b>
|
|
199
|
-
<li><b>Ops
|
|
200
|
-
<li><b>
|
|
200
|
+
<li><b>Incident response:</b> rank alerts → fix the most critical first</li>
|
|
201
|
+
<li><b>Automation flows:</b> choose the best next action from multiple tasks</li>
|
|
202
|
+
<li><b>Ops / IoT:</b> prioritize device events + maintenance decisions</li>
|
|
203
|
+
<li><b>Ticketing & work queues:</b> rank jobs, leads, or support tickets</li>
|
|
201
204
|
</ul>
|
|
202
205
|
|
|
203
|
-
<h3>Input format (
|
|
204
|
-
<p>Send a list
|
|
206
|
+
<h3>Input format (simple)</h3>
|
|
207
|
+
<p>Send a list to rank inside <code>msg.payload.items</code>:</p>
|
|
205
208
|
|
|
206
209
|
<pre><code>{
|
|
207
210
|
"items": [
|
|
@@ -211,17 +214,14 @@
|
|
|
211
214
|
]
|
|
212
215
|
}</code></pre>
|
|
213
216
|
|
|
214
|
-
<h3>Output</h3>
|
|
215
217
|
<p>
|
|
216
|
-
The
|
|
217
|
-
|
|
218
|
+
✅ The node automatically converts <code>items</code> into the backend format
|
|
219
|
+
(<code>decision_id</code>, <code>prompt</code>, <code>options</code>) so flows stay easy.
|
|
218
220
|
</p>
|
|
219
221
|
|
|
220
222
|
<h3>1-Click Example Flow (Import This)</h3>
|
|
221
223
|
<p>
|
|
222
|
-
Copy the JSON below
|
|
223
|
-
<br/>
|
|
224
|
-
<b>Node-RED Menu → Import → Clipboard → Paste → Import</b>
|
|
224
|
+
Copy the JSON below → Node-RED Menu → Import → Clipboard → Paste → Import
|
|
225
225
|
</p>
|
|
226
226
|
|
|
227
227
|
<pre><code>[
|
|
@@ -237,11 +237,7 @@
|
|
|
237
237
|
"type": "inject",
|
|
238
238
|
"z": "a111111111111111",
|
|
239
239
|
"name": "Run VectorPrime Rank (1-click)",
|
|
240
|
-
"props": [
|
|
241
|
-
{
|
|
242
|
-
"p": "payload"
|
|
243
|
-
}
|
|
244
|
-
],
|
|
240
|
+
"props": [{ "p": "payload" }],
|
|
245
241
|
"payload": "{\"items\":[{\"id\":\"fix-prod-bug\",\"label\":\"Fix production bug\",\"urgency\":10,\"impact\":10},{\"id\":\"ship-feature\",\"label\":\"Ship new feature\",\"urgency\":7,\"impact\":8},{\"id\":\"refactor\",\"label\":\"Refactor module\",\"urgency\":3,\"impact\":5}]}",
|
|
246
242
|
"payloadType": "json",
|
|
247
243
|
"repeat": "",
|
|
@@ -249,13 +245,9 @@
|
|
|
249
245
|
"once": false,
|
|
250
246
|
"onceDelay": 0.1,
|
|
251
247
|
"topic": "",
|
|
252
|
-
"x":
|
|
248
|
+
"x": 230,
|
|
253
249
|
"y": 160,
|
|
254
|
-
"wires": [
|
|
255
|
-
[
|
|
256
|
-
"c111111111111111"
|
|
257
|
-
]
|
|
258
|
-
]
|
|
250
|
+
"wires": [["c111111111111111"]]
|
|
259
251
|
},
|
|
260
252
|
{
|
|
261
253
|
"id": "c111111111111111",
|
|
@@ -264,13 +256,9 @@
|
|
|
264
256
|
"name": "VectorPrime Rank",
|
|
265
257
|
"config": "d111111111111111",
|
|
266
258
|
"endpoint": "/v1/kernel/rank",
|
|
267
|
-
"x":
|
|
259
|
+
"x": 470,
|
|
268
260
|
"y": 160,
|
|
269
|
-
"wires": [
|
|
270
|
-
[
|
|
271
|
-
"e111111111111111"
|
|
272
|
-
]
|
|
273
|
-
]
|
|
261
|
+
"wires": [["e111111111111111"]]
|
|
274
262
|
},
|
|
275
263
|
{
|
|
276
264
|
"id": "e111111111111111",
|
|
@@ -283,8 +271,6 @@
|
|
|
283
271
|
"tostatus": false,
|
|
284
272
|
"complete": "payload",
|
|
285
273
|
"targetType": "msg",
|
|
286
|
-
"statusVal": "",
|
|
287
|
-
"statusType": "auto",
|
|
288
274
|
"x": 700,
|
|
289
275
|
"y": 160,
|
|
290
276
|
"wires": []
|
|
@@ -296,10 +282,4 @@
|
|
|
296
282
|
"baseUrl": "https://vectorprime-kernel-backend.onrender.com"
|
|
297
283
|
}
|
|
298
284
|
]</code></pre>
|
|
299
|
-
|
|
300
|
-
<h3>Advanced tip: override Authorization</h3>
|
|
301
|
-
<p>
|
|
302
|
-
If you set <code>msg.headers.Authorization</code> before this node runs, it will use your header instead
|
|
303
|
-
of the stored key.
|
|
304
|
-
</p>
|
|
305
285
|
</script>
|
package/vectorprime.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// ✅ Node 22 has global fetch, but keep fallback for safety
|
|
5
|
+
const fetchFn =
|
|
6
|
+
typeof fetch === "function"
|
|
7
|
+
? fetch
|
|
8
|
+
: (...args) => import("node-fetch").then(({ default: f }) => f(...args));
|
|
5
9
|
|
|
6
10
|
function normalizeBaseUrl(url) {
|
|
7
11
|
url = (url || "").trim();
|
|
@@ -12,27 +16,30 @@ module.exports = function (RED) {
|
|
|
12
16
|
|
|
13
17
|
function normalizeEndpoint(path) {
|
|
14
18
|
path = (path || "").trim();
|
|
15
|
-
if (!path) return "/v1/kernel/rank";
|
|
16
19
|
if (!path.startsWith("/")) path = "/" + path;
|
|
17
20
|
return path;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
//
|
|
23
|
+
// -----------------------------
|
|
24
|
+
// Config Node (stores Base URL + API Key)
|
|
25
|
+
// -----------------------------
|
|
21
26
|
function VectorPrimeConfigNode(n) {
|
|
22
27
|
RED.nodes.createNode(this, n);
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
this.baseUrl =
|
|
30
|
+
(n.baseUrl && String(n.baseUrl).trim()) ||
|
|
31
|
+
"https://vectorprime-kernel-backend.onrender.com";
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
// ✅ IMPORTANT: credentials must be declared here too
|
|
29
34
|
RED.nodes.registerType("vectorprime-config", VectorPrimeConfigNode, {
|
|
30
35
|
credentials: {
|
|
31
36
|
apiKey: { type: "password" },
|
|
32
37
|
},
|
|
33
38
|
});
|
|
34
39
|
|
|
35
|
-
//
|
|
40
|
+
// -----------------------------
|
|
41
|
+
// Rank Node (calls VectorPrime backend)
|
|
42
|
+
// -----------------------------
|
|
36
43
|
function VectorPrimeRankNode(config) {
|
|
37
44
|
RED.nodes.createNode(this, config);
|
|
38
45
|
const node = this;
|
|
@@ -40,19 +47,17 @@ module.exports = function (RED) {
|
|
|
40
47
|
node.name = config.name;
|
|
41
48
|
node.endpoint = config.endpoint || "/v1/kernel/rank";
|
|
42
49
|
|
|
50
|
+
const cfg = RED.nodes.getNode(config.config);
|
|
51
|
+
|
|
43
52
|
node.on("input", async function (msg, send, done) {
|
|
44
53
|
send = send || function () { node.send.apply(node, arguments); };
|
|
45
54
|
|
|
46
55
|
try {
|
|
47
|
-
const cfg = RED.nodes.getNode(config.config);
|
|
48
56
|
if (!cfg) {
|
|
49
57
|
node.status({ fill: "red", shape: "ring", text: "missing config" });
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
send(msg);
|
|
54
|
-
if (done) done();
|
|
55
|
-
return;
|
|
58
|
+
throw new Error(
|
|
59
|
+
"VectorPrime config missing. Open node settings and select/create a Config."
|
|
60
|
+
);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
const baseUrl = normalizeBaseUrl(cfg.baseUrl);
|
|
@@ -60,94 +65,82 @@ module.exports = function (RED) {
|
|
|
60
65
|
|
|
61
66
|
if (!baseUrl.startsWith("http")) {
|
|
62
67
|
node.status({ fill: "red", shape: "ring", text: "invalid base url" });
|
|
63
|
-
|
|
64
|
-
send(msg);
|
|
65
|
-
if (done) done();
|
|
66
|
-
return;
|
|
68
|
+
throw new Error(`Base URL missing/invalid: ${baseUrl}`);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
// ✅ Ensure headers object exists
|
|
70
|
-
msg.headers = msg.headers || {};
|
|
71
|
-
|
|
72
|
-
// ✅ Allow incoming Authorization override (advanced users)
|
|
73
|
-
const incomingAuth =
|
|
74
|
-
msg.headers.authorization ||
|
|
75
|
-
msg.headers.Authorization ||
|
|
76
|
-
"";
|
|
77
|
-
|
|
78
71
|
const storedKey =
|
|
79
72
|
cfg.credentials && cfg.credentials.apiKey
|
|
80
73
|
? String(cfg.credentials.apiKey).trim()
|
|
81
74
|
: "";
|
|
82
75
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
error: "No API key. Open VectorPrime Config and click 'Get Free Key'.",
|
|
89
|
-
};
|
|
90
|
-
send(msg);
|
|
91
|
-
if (done) done();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
msg.headers.Authorization = `Bearer ${storedKey}`;
|
|
76
|
+
if (!storedKey) {
|
|
77
|
+
node.status({ fill: "red", shape: "ring", text: "missing api key" });
|
|
78
|
+
throw new Error(
|
|
79
|
+
"No API key. Open VectorPrime Config and click 'Get Free Key' or paste your paid key."
|
|
80
|
+
);
|
|
95
81
|
}
|
|
96
82
|
|
|
97
|
-
//
|
|
98
|
-
|
|
83
|
+
// -----------------------------
|
|
84
|
+
// ✅ AUTO-CONVERT ITEMS → OPTIONS (YOUR REQUIRED FIX)
|
|
85
|
+
// Backend expects: decision_id, prompt, options
|
|
86
|
+
// Users send: {items:[...]}
|
|
87
|
+
// -----------------------------
|
|
88
|
+
let payload = msg.payload || {};
|
|
89
|
+
|
|
90
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.items) && !payload.options) {
|
|
91
|
+
const decision_id = payload.decision_id || `node-red-${Date.now()}`;
|
|
92
|
+
const prompt = payload.prompt || "Rank the best next action from these options.";
|
|
93
|
+
|
|
94
|
+
const options = payload.items.map((it, idx) => ({
|
|
95
|
+
id: it.id || `opt-${idx + 1}`,
|
|
96
|
+
label: it.label || it.title || it.id || `Option ${idx + 1}`,
|
|
97
|
+
urgency: it.urgency,
|
|
98
|
+
impact: it.impact,
|
|
99
|
+
effort: it.effort,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
payload = { decision_id, prompt, options };
|
|
103
|
+
}
|
|
99
104
|
|
|
100
|
-
|
|
105
|
+
// Validate final expected shape
|
|
106
|
+
if (!payload.decision_id || !payload.prompt || !payload.options) {
|
|
107
|
+
node.status({ fill: "red", shape: "ring", text: "bad payload" });
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Invalid payload. Provide { items:[...] } or full { decision_id, prompt, options }."
|
|
110
|
+
);
|
|
111
|
+
}
|
|
101
112
|
|
|
102
113
|
node.status({ fill: "blue", shape: "dot", text: "ranking..." });
|
|
103
114
|
|
|
104
|
-
const resp = await
|
|
115
|
+
const resp = await fetchFn(`${baseUrl}${endpoint}`, {
|
|
105
116
|
method: "POST",
|
|
106
|
-
headers:
|
|
117
|
+
headers: {
|
|
118
|
+
"Authorization": `Bearer ${storedKey}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
107
121
|
body: JSON.stringify(payload),
|
|
108
122
|
});
|
|
109
123
|
|
|
110
124
|
const text = await resp.text();
|
|
111
125
|
let data;
|
|
112
|
-
try {
|
|
113
|
-
data = JSON.parse(text);
|
|
114
|
-
} catch {
|
|
115
|
-
data = { raw: text };
|
|
116
|
-
}
|
|
126
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
117
127
|
|
|
118
|
-
// ✅ Free tier limit hit → return upgrade url inside Node-RED
|
|
119
128
|
if (!resp.ok) {
|
|
120
|
-
const upgradeUrl = data.upgrade_url || data.checkout_url || null;
|
|
121
|
-
|
|
122
|
-
if (resp.status === 402 && upgradeUrl) {
|
|
123
|
-
node.status({
|
|
124
|
-
fill: "yellow",
|
|
125
|
-
shape: "ring",
|
|
126
|
-
text: "limit reached → upgrade",
|
|
127
|
-
});
|
|
128
|
-
msg.payload = data;
|
|
129
|
-
msg.vectorprime = { upgrade_url: upgradeUrl };
|
|
130
|
-
send(msg);
|
|
131
|
-
if (done) done();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
129
|
node.status({ fill: "red", shape: "ring", text: `error ${resp.status}` });
|
|
136
|
-
|
|
137
|
-
send(msg);
|
|
138
|
-
if (done) done();
|
|
139
|
-
return;
|
|
130
|
+
throw new Error(`VectorPrime API ${resp.status}: ${JSON.stringify(data)}`);
|
|
140
131
|
}
|
|
141
132
|
|
|
142
133
|
node.status({ fill: "green", shape: "dot", text: "ok" });
|
|
134
|
+
|
|
135
|
+
// ✅ Replace payload with ranked result
|
|
143
136
|
msg.payload = data;
|
|
137
|
+
|
|
144
138
|
send(msg);
|
|
145
|
-
|
|
139
|
+
done();
|
|
146
140
|
} catch (err) {
|
|
147
141
|
node.status({ fill: "red", shape: "ring", text: "failed" });
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (done) done(err);
|
|
142
|
+
node.error(err, msg);
|
|
143
|
+
done(err);
|
|
151
144
|
}
|
|
152
145
|
});
|
|
153
146
|
}
|