node-red-contrib-vectorprime 0.1.41 → 0.1.43
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 +12 -53
- package/package.json +2 -2
- package/vectorprime.html +72 -103
- package/vectorprime.js +36 -63
package/README.md
CHANGED
|
@@ -1,70 +1,29 @@
|
|
|
1
1
|
# node-red-contrib-vectorprime
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
✅ **1-minute setup:** install the node → drag **Rank Decision (VectorPrime)** into a flow → open **VectorPrime Config** → get your API key → paste → Deploy.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
👉 **Get your API key:** https://vectorprime.tech/?utm_source=node-red&utm_medium=readme&utm_campaign=node_red_contrib&utm_content=top_cta
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
✅ Rank alerts by urgency
|
|
9
|
-
✅ Choose the best option from multiple choices
|
|
10
|
-
✅ Turn “too many choices” into a safe automation decision state
|
|
11
|
-
✅ Works inside Node-RED flows with a simple node
|
|
7
|
+
> Pricing note: This node is free to use. VectorPrime API usage may require a paid plan after free-tier limits.
|
|
12
8
|
|
|
13
9
|
---
|
|
14
10
|
|
|
15
|
-
## What
|
|
11
|
+
## What this node does
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
VectorPrime helps Node-RED flows choose the **best next action** when multiple options compete — without hard-coded rules.
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## Why developers install this
|
|
22
|
-
|
|
23
|
-
Most automation flows break down when you have **multiple possible actions** and don’t know which one to run.
|
|
24
|
-
|
|
25
|
-
VectorPrime solves that by turning this:
|
|
15
|
+
Use cases:
|
|
26
16
|
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- “Which fix should I deploy first?”
|
|
32
|
-
|
|
33
|
-
Into:
|
|
34
|
-
|
|
35
|
-
✅ A **ranked result** when the input supports a winner
|
|
36
|
-
✅ Or a **tie / no-dominant outcome** (so your flow can branch safely)
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Nodes Included
|
|
41
|
-
|
|
42
|
-
- **VectorPrime Config**
|
|
43
|
-
Stores your VectorPrime Base URL + API Key.
|
|
44
|
-
|
|
45
|
-
- **VectorPrime Rank**
|
|
46
|
-
Sends options to VectorPrime (`/v1/kernel/rank`) and returns a ranked result.
|
|
17
|
+
- Prioritize tasks
|
|
18
|
+
- Rank alerts by urgency/impact
|
|
19
|
+
- Choose the safest next step in automation flows
|
|
20
|
+
- Route tickets/jobs based on structured decision input
|
|
47
21
|
|
|
48
22
|
---
|
|
49
23
|
|
|
50
24
|
## Install
|
|
51
25
|
|
|
52
|
-
|
|
53
|
-
1) Open Node-RED in your browser
|
|
54
|
-
2) Click the menu (top-right) ☰
|
|
55
|
-
3) Click **Manage palette**
|
|
56
|
-
4) Click the **Install** tab
|
|
57
|
-
5) Search: `node-red-contrib-vectorprime`
|
|
58
|
-
6) Click **Install**
|
|
59
|
-
|
|
60
|
-
### Option B: Install from terminal (Node-RED user directory)
|
|
61
|
-
|
|
62
|
-
Open your Node-RED user directory:
|
|
63
|
-
|
|
64
|
-
- **Windows:** `C:\Users\<you>\.node-red`
|
|
65
|
-
- **macOS/Linux:** `~/.node-red`
|
|
66
|
-
|
|
67
|
-
Then run:
|
|
26
|
+
In your Node-RED user directory (usually `C:\Users\<you>\.node-red`):
|
|
68
27
|
|
|
69
28
|
```bash
|
|
70
|
-
npm install node-red-contrib-vectorprime
|
|
29
|
+
npm install node-red-contrib-vectorprime
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-vectorprime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Rank tasks, alerts, or actions inside Node-RED when multiple options compete. Choose the safest next step automatically instead of hard-coded rules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Faisal Khan",
|
|
7
7
|
"homepage": "https://gitlab.com/faisalkhan3000/vectorprime-node-red-node",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://gitlab.com/faisalkhan3000/vectorprime-node-red-node.git"
|
|
10
|
+
"url": "git+https://gitlab.com/faisalkhan3000/vectorprime-node-red-node.git"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://gitlab.com/faisalkhan3000/vectorprime-node-red-node/-/issues"
|
package/vectorprime.html
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
# 2) REPLACE: `vectorprime.html` (FULL FILE)
|
|
4
|
+
|
|
5
|
+
```html
|
|
1
6
|
<!-- =========================
|
|
2
|
-
VectorPrime
|
|
7
|
+
VectorPrime Nodes (Node-RED)
|
|
8
|
+
WEBSITE-ONLY KEYS (no minting inside Node-RED)
|
|
9
|
+
========================= -->
|
|
10
|
+
|
|
11
|
+
<!-- =========================
|
|
12
|
+
Config Node UI
|
|
3
13
|
========================= -->
|
|
4
14
|
<script type="text/x-red" data-template-name="vectorprime-config">
|
|
5
15
|
<div class="form-row">
|
|
@@ -9,26 +19,28 @@
|
|
|
9
19
|
<input type="text" id="node-config-input-baseUrl" placeholder="https://vectorprime-kernel-backend.onrender.com">
|
|
10
20
|
</div>
|
|
11
21
|
|
|
12
|
-
<!-- ✅ REQUIRED: email for /v1/billing/signup -->
|
|
13
22
|
<div class="form-row">
|
|
14
|
-
<label for="node-config-input-
|
|
15
|
-
<i class="fa fa-
|
|
23
|
+
<label for="node-config-input-apiKey">
|
|
24
|
+
<i class="fa fa-key"></i> API Key
|
|
16
25
|
</label>
|
|
17
|
-
<input type="
|
|
26
|
+
<input type="password" id="node-config-input-apiKey" placeholder="vp_live_...">
|
|
18
27
|
</div>
|
|
19
28
|
|
|
20
29
|
<div class="form-row">
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
<a
|
|
31
|
+
class="vp-cta-btn"
|
|
32
|
+
href="https://vectorprime.tech/?utm_source=node-red&utm_medium=config&utm_campaign=node_red_contrib&utm_content=get_key_button"
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="noopener noreferrer"
|
|
35
|
+
>
|
|
36
|
+
Get your API key on vectorprime.tech
|
|
37
|
+
</a>
|
|
25
38
|
</div>
|
|
26
39
|
|
|
27
40
|
<div class="form-row">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
</
|
|
31
|
-
<span id="vp-key-status" style="margin-left:10px; font-size:12px;"></span>
|
|
41
|
+
<div style="font-size:12px; color:#666; line-height:1.4;">
|
|
42
|
+
Then paste it into <b>API Key</b> above.
|
|
43
|
+
</div>
|
|
32
44
|
</div>
|
|
33
45
|
|
|
34
46
|
<div class="form-row">
|
|
@@ -36,6 +48,21 @@
|
|
|
36
48
|
✅ Key is stored securely in Node-RED credentials (not exported with flows).
|
|
37
49
|
</div>
|
|
38
50
|
</div>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
.vp-cta-btn{
|
|
54
|
+
display:block;
|
|
55
|
+
width:100%;
|
|
56
|
+
padding:10px 12px;
|
|
57
|
+
text-align:center;
|
|
58
|
+
background:#2563EB;
|
|
59
|
+
color:#fff !important;
|
|
60
|
+
border-radius:8px;
|
|
61
|
+
text-decoration:none;
|
|
62
|
+
font-weight:600;
|
|
63
|
+
}
|
|
64
|
+
.vp-cta-btn:hover{ filter:brightness(0.92); }
|
|
65
|
+
</style>
|
|
39
66
|
</script>
|
|
40
67
|
|
|
41
68
|
<script type="text/javascript">
|
|
@@ -50,8 +77,7 @@
|
|
|
50
77
|
RED.nodes.registerType("vectorprime-config", {
|
|
51
78
|
category: "config",
|
|
52
79
|
defaults: {
|
|
53
|
-
baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true }
|
|
54
|
-
email: { value: "", required: true }
|
|
80
|
+
baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true }
|
|
55
81
|
},
|
|
56
82
|
credentials: {
|
|
57
83
|
apiKey: { type: "password" }
|
|
@@ -60,65 +86,16 @@
|
|
|
60
86
|
return "VectorPrime Config";
|
|
61
87
|
},
|
|
62
88
|
oneditprepare: function () {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const emailInput = document.getElementById("node-config-input-email");
|
|
74
|
-
const apiKeyInput = document.getElementById("node-config-input-apiKey");
|
|
75
|
-
|
|
76
|
-
const baseUrl = normalizeBaseUrl(baseUrlInput.value);
|
|
77
|
-
const email = ((emailInput && emailInput.value) ? emailInput.value : "").trim();
|
|
78
|
-
|
|
79
|
-
if (!baseUrl.startsWith("http")) {
|
|
80
|
-
statusEl.textContent = "❌ Base URL missing/invalid";
|
|
81
|
-
statusEl.style.color = "red";
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!email || !email.includes("@")) {
|
|
86
|
-
statusEl.textContent = "❌ Enter a valid email";
|
|
87
|
-
statusEl.style.color = "red";
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ✅ Call REAL backend endpoint that exists
|
|
92
|
-
const resp = await fetch(`${baseUrl}/v1/billing/signup`, {
|
|
93
|
-
method: "POST",
|
|
94
|
-
headers: { "Content-Type": "application/json" },
|
|
95
|
-
body: JSON.stringify({ email: email })
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const text = await resp.text();
|
|
99
|
-
let data;
|
|
100
|
-
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
101
|
-
|
|
102
|
-
if (!resp.ok) {
|
|
103
|
-
statusEl.textContent = `❌ Failed (${resp.status}): ${data.message || data.error || data.detail || "unknown"}`;
|
|
104
|
-
statusEl.style.color = "red";
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!data.api_key) {
|
|
109
|
-
statusEl.textContent = "❌ Backend did not return api_key";
|
|
110
|
-
statusEl.style.color = "red";
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
apiKeyInput.value = data.api_key;
|
|
115
|
-
statusEl.textContent = "✅ Free key generated + saved";
|
|
116
|
-
statusEl.style.color = "green";
|
|
117
|
-
} catch (err) {
|
|
118
|
-
statusEl.textContent = `❌ Error: ${err.message}`;
|
|
119
|
-
statusEl.style.color = "red";
|
|
120
|
-
}
|
|
121
|
-
};
|
|
89
|
+
const baseUrlInput = document.getElementById("node-config-input-baseUrl");
|
|
90
|
+
if (baseUrlInput && baseUrlInput.value) {
|
|
91
|
+
baseUrlInput.value = normalizeBaseUrl(baseUrlInput.value);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
oneditsave: function () {
|
|
95
|
+
const baseUrlInput = document.getElementById("node-config-input-baseUrl");
|
|
96
|
+
if (baseUrlInput && baseUrlInput.value) {
|
|
97
|
+
baseUrlInput.value = normalizeBaseUrl(baseUrlInput.value);
|
|
98
|
+
}
|
|
122
99
|
}
|
|
123
100
|
});
|
|
124
101
|
})();
|
|
@@ -130,9 +107,9 @@
|
|
|
130
107
|
|
|
131
108
|
<h3>Quick Setup</h3>
|
|
132
109
|
<ol>
|
|
133
|
-
<li>
|
|
134
|
-
<li>
|
|
135
|
-
<li>
|
|
110
|
+
<li>Confirm <b>Base URL</b> (default is correct)</li>
|
|
111
|
+
<li>Click <b>Get your API key</b> (opens vectorprime.tech)</li>
|
|
112
|
+
<li>Paste the key into <b>API Key</b></li>
|
|
136
113
|
<li>Click <b>Done</b></li>
|
|
137
114
|
</ol>
|
|
138
115
|
|
|
@@ -143,16 +120,13 @@
|
|
|
143
120
|
<h3>Recommended Base URL</h3>
|
|
144
121
|
<pre>https://vectorprime-kernel-backend.onrender.com</pre>
|
|
145
122
|
|
|
146
|
-
<
|
|
147
|
-
<p>
|
|
148
|
-
You can also paste your own paid key here if you want higher limits.
|
|
149
|
-
</p>
|
|
123
|
+
<p><b>Website-only keys:</b> this node does not mint keys inside Node-RED.</p>
|
|
150
124
|
</script>
|
|
151
125
|
|
|
152
126
|
<hr />
|
|
153
127
|
|
|
154
128
|
<!-- =========================
|
|
155
|
-
|
|
129
|
+
Rank Node UI
|
|
156
130
|
========================= -->
|
|
157
131
|
<script type="text/x-red" data-template-name="vectorprime-rank">
|
|
158
132
|
<div class="form-row">
|
|
@@ -189,10 +163,7 @@
|
|
|
189
163
|
inputs: 1,
|
|
190
164
|
outputs: 1,
|
|
191
165
|
icon: "font-awesome/fa-bolt",
|
|
192
|
-
|
|
193
|
-
// ✅ This is the name users see in the palette
|
|
194
166
|
paletteLabel: "Rank Decision (VectorPrime)",
|
|
195
|
-
|
|
196
167
|
label: function () {
|
|
197
168
|
return this.name || "Rank Decision (VectorPrime)";
|
|
198
169
|
}
|
|
@@ -201,39 +172,37 @@
|
|
|
201
172
|
|
|
202
173
|
<!-- ✅ HELP TAB: vectorprime-rank -->
|
|
203
174
|
<script type="text/x-red" data-help-name="vectorprime-rank">
|
|
204
|
-
<p>
|
|
205
|
-
<b>Rank Decision (VectorPrime)</b> picks the best next action automatically.
|
|
206
|
-
Use it anywhere your flow needs <b>priority scoring</b> or <b>decision ranking</b>.
|
|
207
|
-
</p>
|
|
175
|
+
<p><b>Rank Decision (VectorPrime)</b> ranks options (tasks/actions/alerts) using the VectorPrime kernel.</p>
|
|
208
176
|
|
|
209
177
|
<h3>What it does</h3>
|
|
210
178
|
<ul>
|
|
211
179
|
<li>Reads options from <code>msg.payload</code></li>
|
|
212
|
-
<li>Calls the VectorPrime
|
|
180
|
+
<li>Calls the VectorPrime kernel rank API</li>
|
|
213
181
|
<li>Returns the ranked decision inside <code>msg.payload</code></li>
|
|
214
182
|
</ul>
|
|
215
183
|
|
|
216
|
-
<h3>Where developers use this most</h3>
|
|
217
|
-
<ul>
|
|
218
|
-
<li><b>Incident response:</b> rank alerts → fix the most critical first</li>
|
|
219
|
-
<li><b>Automation flows:</b> choose the best next action from multiple tasks</li>
|
|
220
|
-
<li><b>Ops / IoT:</b> prioritize device events + maintenance decisions</li>
|
|
221
|
-
<li><b>Ticketing & work queues:</b> rank jobs, leads, or support tickets</li>
|
|
222
|
-
</ul>
|
|
223
|
-
|
|
224
184
|
<h3>Input format (simple)</h3>
|
|
225
185
|
<p>Send a list to rank inside <code>msg.payload.items</code>:</p>
|
|
226
186
|
|
|
227
187
|
<pre><code>{
|
|
228
188
|
"items": [
|
|
229
|
-
{ "id": "
|
|
230
|
-
{ "id": "
|
|
231
|
-
{ "id": "
|
|
189
|
+
{ "id": "a", "label": "Fix production bug", "urgency": 9, "impact": 10, "effort": 3 },
|
|
190
|
+
{ "id": "b", "label": "Ship new feature", "urgency": 6, "impact": 7, "effort": 6 },
|
|
191
|
+
{ "id": "c", "label": "Refactor module", "urgency": 3, "impact": 4, "effort": 8 }
|
|
232
192
|
]
|
|
233
193
|
}</code></pre>
|
|
234
194
|
|
|
235
195
|
<p>
|
|
236
|
-
✅ The node automatically converts <code>items</code> into
|
|
237
|
-
|
|
196
|
+
✅ The node automatically converts <code>items</code> into:
|
|
197
|
+
<code>decision_id</code>, <code>prompt</code>, <code>options</code>.
|
|
238
198
|
</p>
|
|
199
|
+
|
|
200
|
+
<h3>Output</h3>
|
|
201
|
+
<p>On success, the node sets <code>msg.payload</code> to the kernel response, typically including:</p>
|
|
202
|
+
<ul>
|
|
203
|
+
<li><code>ranking</code> (best → worst)</li>
|
|
204
|
+
<li><code>probabilities</code> (per option id)</li>
|
|
205
|
+
<li><code>meta</code> (engine info / tie-break flags)</li>
|
|
206
|
+
<li><code>vp_input</code> (what was sent)</li>
|
|
207
|
+
</ul>
|
|
239
208
|
</script>
|
package/vectorprime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Node 18+ has global fetch. Keep fallback for older Node.
|
|
5
5
|
const fetchFn =
|
|
6
6
|
typeof fetch === "function"
|
|
7
7
|
? fetch
|
|
@@ -25,10 +25,6 @@ module.exports = function (RED) {
|
|
|
25
25
|
return Number.isFinite(n) ? n : null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function almostEqual(a, b, eps = 1e-9) {
|
|
29
|
-
return Math.abs(a - b) <= eps;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
function isFlatProbabilities(probObj) {
|
|
33
29
|
if (!probObj || typeof probObj !== "object") return false;
|
|
34
30
|
const keys = Object.keys(probObj);
|
|
@@ -62,10 +58,9 @@ module.exports = function (RED) {
|
|
|
62
58
|
});
|
|
63
59
|
|
|
64
60
|
// Convert to pseudo-probabilities (normalized non-negative)
|
|
65
|
-
// Make everything >= 0 by shifting if needed
|
|
66
61
|
const scores = scored.map(s => s.score);
|
|
67
62
|
const minScore = Math.min(...scores);
|
|
68
|
-
const shifted = scores.map(s => s - minScore); //
|
|
63
|
+
const shifted = scores.map(s => s - minScore); // min is 0
|
|
69
64
|
const sum = shifted.reduce((acc, v) => acc + v, 0);
|
|
70
65
|
|
|
71
66
|
const probabilities = {};
|
|
@@ -74,7 +69,6 @@ module.exports = function (RED) {
|
|
|
74
69
|
probabilities[s.id] = shifted[j] / sum;
|
|
75
70
|
});
|
|
76
71
|
} else {
|
|
77
|
-
// all zero (fully tied) -> uniform
|
|
78
72
|
const uniform = 1 / scored.length;
|
|
79
73
|
scored.forEach(s => { probabilities[s.id] = uniform; });
|
|
80
74
|
}
|
|
@@ -87,6 +81,7 @@ module.exports = function (RED) {
|
|
|
87
81
|
|
|
88
82
|
// -----------------------------
|
|
89
83
|
// Config Node (stores Base URL + API Key)
|
|
84
|
+
// WEBSITE-ONLY KEYS: no minting inside Node-RED
|
|
90
85
|
// -----------------------------
|
|
91
86
|
function VectorPrimeConfigNode(n) {
|
|
92
87
|
RED.nodes.createNode(this, n);
|
|
@@ -94,10 +89,7 @@ module.exports = function (RED) {
|
|
|
94
89
|
this.baseUrl =
|
|
95
90
|
(n.baseUrl && String(n.baseUrl).trim()) ||
|
|
96
91
|
"https://vectorprime-kernel-backend.onrender.com";
|
|
97
|
-
|
|
98
|
-
// NOTE: email is stored as a normal config property (not credentials).
|
|
99
|
-
// Runtime node doesn't need it; editor uses it to request keys.
|
|
100
|
-
this.email = (n.email && String(n.email).trim()) || "";
|
|
92
|
+
// apiKey is stored in credentials only.
|
|
101
93
|
}
|
|
102
94
|
|
|
103
95
|
RED.nodes.registerType("vectorprime-config", VectorPrimeConfigNode, {
|
|
@@ -124,9 +116,7 @@ module.exports = function (RED) {
|
|
|
124
116
|
try {
|
|
125
117
|
if (!cfg) {
|
|
126
118
|
node.status({ fill: "red", shape: "ring", text: "missing config" });
|
|
127
|
-
throw new Error(
|
|
128
|
-
"VectorPrime config missing. Open node settings and select/create a Config."
|
|
129
|
-
);
|
|
119
|
+
throw new Error("VectorPrime config missing. Open node settings and select/create a Config.");
|
|
130
120
|
}
|
|
131
121
|
|
|
132
122
|
const baseUrl = normalizeBaseUrl(cfg.baseUrl);
|
|
@@ -145,15 +135,14 @@ module.exports = function (RED) {
|
|
|
145
135
|
if (!storedKey) {
|
|
146
136
|
node.status({ fill: "red", shape: "ring", text: "missing api key" });
|
|
147
137
|
throw new Error(
|
|
148
|
-
"No API key.
|
|
138
|
+
"No API key. Get one at https://vectorprime.tech/?utm_source=node-red&utm_medium=runtime_error&utm_campaign=node_red_contrib&utm_content=missing_key then paste it into the VectorPrime Config."
|
|
149
139
|
);
|
|
150
140
|
}
|
|
151
141
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
142
|
+
// ------------------------------------
|
|
143
|
+
// AUTO-CONVERT items -> options (friendly input)
|
|
154
144
|
// Backend expects: decision_id, prompt, options
|
|
155
|
-
//
|
|
156
|
-
// -----------------------------
|
|
145
|
+
// ------------------------------------
|
|
157
146
|
const originalPayload = msg.payload;
|
|
158
147
|
let payload = msg.payload || {};
|
|
159
148
|
let inputOptionsForFallback = null;
|
|
@@ -185,12 +174,14 @@ module.exports = function (RED) {
|
|
|
185
174
|
inputOptionsForFallback = payload.options;
|
|
186
175
|
}
|
|
187
176
|
|
|
188
|
-
|
|
189
|
-
if (!payload.decision_id || !payload.prompt || !payload.options) {
|
|
177
|
+
if (!payload || typeof payload !== "object") {
|
|
190
178
|
node.status({ fill: "red", shape: "ring", text: "bad payload" });
|
|
191
|
-
throw new Error(
|
|
192
|
-
|
|
193
|
-
|
|
179
|
+
throw new Error("Invalid payload. Provide { items:[...] } or full { decision_id, prompt, options }.");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!payload.decision_id || !payload.prompt || !payload.options || !Array.isArray(payload.options)) {
|
|
183
|
+
node.status({ fill: "red", shape: "ring", text: "bad payload" });
|
|
184
|
+
throw new Error("Invalid payload. Provide { items:[...] } or full { decision_id, prompt, options }.");
|
|
194
185
|
}
|
|
195
186
|
|
|
196
187
|
node.status({ fill: "blue", shape: "dot", text: "ranking..." });
|
|
@@ -213,60 +204,42 @@ module.exports = function (RED) {
|
|
|
213
204
|
throw new Error(`VectorPrime API ${resp.status}: ${JSON.stringify(data)}`);
|
|
214
205
|
}
|
|
215
206
|
|
|
216
|
-
//
|
|
217
|
-
// ✅ FIX: If backend returns a tie/flat result, do deterministic local ranking
|
|
218
|
-
// This prevents users from seeing "random" / "input-order" ranking.
|
|
219
|
-
// -----------------------------
|
|
207
|
+
// If backend returns a tie/flat result, do deterministic local ranking
|
|
220
208
|
const tieBreak =
|
|
221
209
|
data && data.meta && typeof data.meta === "object"
|
|
222
210
|
? data.meta.tie_break
|
|
223
211
|
: null;
|
|
224
212
|
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
msg.vp_raw = data;
|
|
237
|
-
|
|
238
|
-
// replace with deterministic result
|
|
239
|
-
data = {
|
|
240
|
-
...data,
|
|
241
|
-
ranking: fallback.ranking,
|
|
242
|
-
probabilities: fallback.probabilities,
|
|
243
|
-
meta: {
|
|
244
|
-
...(data.meta || {}),
|
|
245
|
-
notes: (data.meta && data.meta.notes ? String(data.meta.notes) + " | " : "") +
|
|
246
|
-
"Node-RED fallback ranking applied (urgency/impact/effort) because backend result was tied/flat.",
|
|
247
|
-
numeric_source: "node_red_fallback",
|
|
248
|
-
tie_break: "fallback_score_then_preserve_input_order",
|
|
249
|
-
},
|
|
250
|
-
};
|
|
213
|
+
const hasProb = data && data.probabilities && typeof data.probabilities === "object";
|
|
214
|
+
const isFlat = hasProb ? isFlatProbabilities(data.probabilities) : false;
|
|
215
|
+
|
|
216
|
+
if ((tieBreak || isFlat) && Array.isArray(inputOptionsForFallback) && inputOptionsForFallback.length >= 2) {
|
|
217
|
+
const local = buildFallbackRankingFromOptions(inputOptionsForFallback);
|
|
218
|
+
data = data && typeof data === "object" ? data : {};
|
|
219
|
+
data.ranking = local.ranking;
|
|
220
|
+
data.probabilities = local.probabilities;
|
|
221
|
+
data.meta = data.meta && typeof data.meta === "object" ? data.meta : {};
|
|
222
|
+
data.meta.local_fallback = true;
|
|
223
|
+
data.meta.local_fallback_reason = tieBreak ? "backend_tie_break" : "flat_probabilities";
|
|
251
224
|
}
|
|
252
225
|
|
|
253
|
-
|
|
226
|
+
// Attach what we sent (useful for debugging + audits)
|
|
227
|
+
if (data && typeof data === "object") {
|
|
228
|
+
data.vp_input = payload;
|
|
229
|
+
data.vp_original_payload = originalPayload;
|
|
230
|
+
}
|
|
254
231
|
|
|
255
|
-
// ✅ Replace payload with ranked result
|
|
256
232
|
msg.payload = data;
|
|
257
233
|
|
|
258
|
-
|
|
259
|
-
msg._vp_input = originalPayload;
|
|
260
|
-
|
|
234
|
+
node.status({ fill: "green", shape: "dot", text: "ok" });
|
|
261
235
|
send(msg);
|
|
262
236
|
done();
|
|
263
237
|
} catch (err) {
|
|
264
|
-
node.status({ fill: "red", shape: "ring", text: "
|
|
265
|
-
node.error(err, msg);
|
|
238
|
+
node.status({ fill: "red", shape: "ring", text: "error" });
|
|
266
239
|
done(err);
|
|
267
240
|
}
|
|
268
241
|
});
|
|
269
242
|
}
|
|
270
243
|
|
|
271
244
|
RED.nodes.registerType("vectorprime-rank", VectorPrimeRankNode);
|
|
272
|
-
};
|
|
245
|
+
};
|