node-red-contrib-vectorprime 0.1.3 → 0.1.5
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/package.json +1 -1
- package/vectorprime.html +144 -91
- package/vectorprime.js +34 -16
package/package.json
CHANGED
package/vectorprime.html
CHANGED
|
@@ -28,87 +28,86 @@
|
|
|
28
28
|
</script>
|
|
29
29
|
|
|
30
30
|
<script type="text/javascript">
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Call backend directly from the editor
|
|
73
|
-
const resp = await fetch(`${baseUrl}/v1/keys/free`, {
|
|
74
|
-
method: "POST",
|
|
75
|
-
headers: { "Content-Type": "application/json" },
|
|
76
|
-
body: JSON.stringify({ source: "node-red" })
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const text = await resp.text();
|
|
80
|
-
let data;
|
|
81
|
-
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
82
|
-
|
|
83
|
-
if (!resp.ok) {
|
|
84
|
-
statusEl.textContent = `❌ Failed (${resp.status}): ${data.message || data.error || "unknown"}`;
|
|
85
|
-
statusEl.style.color = "red";
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!data.api_key) {
|
|
90
|
-
statusEl.textContent = "❌ Backend did not return api_key";
|
|
91
|
-
statusEl.style.color = "red";
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
apiKeyInput.value = data.api_key;
|
|
96
|
-
statusEl.textContent = "✅ Free key generated + saved";
|
|
97
|
-
statusEl.style.color = "green";
|
|
98
|
-
} catch (err) {
|
|
99
|
-
statusEl.textContent = `❌ Error: ${err.message}`;
|
|
100
|
-
statusEl.style.color = "red";
|
|
101
|
-
}
|
|
102
|
-
};
|
|
31
|
+
(function () {
|
|
32
|
+
function normalizeBaseUrl(url) {
|
|
33
|
+
url = (url || "").trim();
|
|
34
|
+
if (!url) return "";
|
|
35
|
+
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
36
|
+
return url;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
RED.nodes.registerType("vectorprime-config", {
|
|
40
|
+
category: "config",
|
|
41
|
+
defaults: {
|
|
42
|
+
baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true }
|
|
43
|
+
},
|
|
44
|
+
credentials: {
|
|
45
|
+
apiKey: { type: "password" }
|
|
46
|
+
},
|
|
47
|
+
label: function () {
|
|
48
|
+
return "VectorPrime Config";
|
|
49
|
+
},
|
|
50
|
+
oneditprepare: function () {
|
|
51
|
+
const statusEl = document.getElementById("vp-key-status");
|
|
52
|
+
const btn = document.getElementById("vp-get-free-key");
|
|
53
|
+
|
|
54
|
+
btn.onclick = async function (e) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
statusEl.textContent = "Requesting free key...";
|
|
57
|
+
statusEl.style.color = "#444";
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const baseUrlInput = document.getElementById("node-config-input-baseUrl");
|
|
61
|
+
const apiKeyInput = document.getElementById("node-config-input-apiKey");
|
|
62
|
+
|
|
63
|
+
const baseUrl = normalizeBaseUrl(baseUrlInput.value);
|
|
64
|
+
|
|
65
|
+
if (!baseUrl.startsWith("http")) {
|
|
66
|
+
statusEl.textContent = "❌ Base URL missing/invalid";
|
|
67
|
+
statusEl.style.color = "red";
|
|
68
|
+
return;
|
|
103
69
|
}
|
|
104
|
-
|
|
105
|
-
|
|
70
|
+
|
|
71
|
+
const resp = await fetch(`${baseUrl}/v1/keys/free`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({ source: "node-red" })
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const text = await resp.text();
|
|
78
|
+
let data;
|
|
79
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
80
|
+
|
|
81
|
+
if (!resp.ok) {
|
|
82
|
+
statusEl.textContent = `❌ Failed (${resp.status}): ${data.message || data.error || "unknown"}`;
|
|
83
|
+
statusEl.style.color = "red";
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!data.api_key) {
|
|
88
|
+
statusEl.textContent = "❌ Backend did not return api_key";
|
|
89
|
+
statusEl.style.color = "red";
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
apiKeyInput.value = data.api_key;
|
|
94
|
+
statusEl.textContent = "✅ Free key generated + saved";
|
|
95
|
+
statusEl.style.color = "green";
|
|
96
|
+
} catch (err) {
|
|
97
|
+
statusEl.textContent = `❌ Error: ${err.message}`;
|
|
98
|
+
statusEl.style.color = "red";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
})();
|
|
106
104
|
</script>
|
|
107
105
|
|
|
106
|
+
|
|
108
107
|
<script type="text/html" data-template-name="vectorprime-rank">
|
|
109
108
|
<div class="form-row">
|
|
110
109
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
111
|
-
<input type="text" id="node-input-name" placeholder="VectorPrime
|
|
110
|
+
<input type="text" id="node-input-name" placeholder="Rank Decision (VectorPrime)">
|
|
112
111
|
</div>
|
|
113
112
|
|
|
114
113
|
<div class="form-row">
|
|
@@ -123,25 +122,79 @@
|
|
|
123
122
|
|
|
124
123
|
<div class="form-row">
|
|
125
124
|
<div style="font-size:12px; color:#666;">
|
|
126
|
-
✅
|
|
125
|
+
✅ Sends <code>msg.payload</code> to VectorPrime and returns the response in <code>msg.payload</code>.
|
|
127
126
|
</div>
|
|
128
127
|
</div>
|
|
129
128
|
</script>
|
|
130
129
|
|
|
131
130
|
<script type="text/javascript">
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
RED.nodes.registerType("vectorprime-rank", {
|
|
132
|
+
category: "network", // ✅ CHANGED: Network category
|
|
133
|
+
color: "#2563EB",
|
|
134
|
+
defaults: {
|
|
135
|
+
name: { value: "" },
|
|
136
|
+
config: { type: "vectorprime-config", required: true },
|
|
137
|
+
endpoint: { value: "/v1/kernel/rank", required: true }
|
|
138
|
+
},
|
|
139
|
+
inputs: 1,
|
|
140
|
+
outputs: 1,
|
|
141
|
+
icon: "font-awesome/fa-bolt",
|
|
142
|
+
|
|
143
|
+
paletteLabel: "Rank Decision (VectorPrime)", // ✅ 1-second clarity
|
|
144
|
+
|
|
145
|
+
label: function () {
|
|
146
|
+
return this.name || "Rank Decision (VectorPrime)";
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
labelStyle: function () {
|
|
150
|
+
return this.name ? "node_label_italic" : "";
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
<!-- ✅ Help docs (fixes “no information available”) -->
|
|
157
|
+
<script type="text/html" data-help-name="vectorprime-config">
|
|
158
|
+
<p><b>VectorPrime Config</b></p>
|
|
159
|
+
<p>
|
|
160
|
+
Stores your VectorPrime backend URL + API Key securely using Node-RED credentials.
|
|
161
|
+
</p>
|
|
162
|
+
|
|
163
|
+
<h3>Fields</h3>
|
|
164
|
+
<ul>
|
|
165
|
+
<li><b>Base URL</b> — your VectorPrime backend (Render URL by default).</li>
|
|
166
|
+
<li><b>API Key</b> — stored securely (not exported with flows).</li>
|
|
167
|
+
</ul>
|
|
168
|
+
|
|
169
|
+
<p>
|
|
170
|
+
Click <b>Get Free Key</b> to generate a free-tier key automatically (requires backend support).
|
|
171
|
+
</p>
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<script type="text/html" data-help-name="vectorprime-rank">
|
|
175
|
+
<p><b>Rank Decision (VectorPrime)</b></p>
|
|
176
|
+
|
|
177
|
+
<p>
|
|
178
|
+
Sends <code>msg.payload</code> to VectorPrime for ranking and returns the result in <code>msg.payload</code>.
|
|
179
|
+
</p>
|
|
180
|
+
|
|
181
|
+
<h3>Input Example</h3>
|
|
182
|
+
<pre>{
|
|
183
|
+
"decision_id": "node-red-demo-001",
|
|
184
|
+
"prompt": "Pick one.",
|
|
185
|
+
"options": [{"id":"a","label":"A"},{"id":"b","label":"B"}],
|
|
186
|
+
"engine": "classical",
|
|
187
|
+
"seed": 123
|
|
188
|
+
}</pre>
|
|
189
|
+
|
|
190
|
+
<h3>Output</h3>
|
|
191
|
+
<p>
|
|
192
|
+
Returns ranked options, probabilities, and metadata in <code>msg.payload</code>.
|
|
193
|
+
</p>
|
|
194
|
+
|
|
195
|
+
<h3>Authentication</h3>
|
|
196
|
+
<ul>
|
|
197
|
+
<li>If <code>msg.headers.Authorization</code> is provided, it uses that.</li>
|
|
198
|
+
<li>Otherwise it uses the key stored in <b>VectorPrime Config</b>.</li>
|
|
199
|
+
</ul>
|
|
147
200
|
</script>
|
package/vectorprime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module.exports = function (RED) {
|
|
2
|
+
// ✅ Works with node-fetch v2 or v3
|
|
2
3
|
const fetch = (...args) =>
|
|
3
|
-
import("node-fetch").then((
|
|
4
|
+
import("node-fetch").then((mod) => (mod.default ? mod.default(...args) : mod(...args)));
|
|
4
5
|
|
|
5
6
|
function normalizeBaseUrl(url) {
|
|
6
7
|
url = (url || "").trim();
|
|
@@ -11,6 +12,7 @@ module.exports = function (RED) {
|
|
|
11
12
|
|
|
12
13
|
function normalizeEndpoint(path) {
|
|
13
14
|
path = (path || "").trim();
|
|
15
|
+
if (!path) return "/v1/kernel/rank";
|
|
14
16
|
if (!path.startsWith("/")) path = "/" + path;
|
|
15
17
|
return path;
|
|
16
18
|
}
|
|
@@ -38,15 +40,19 @@ module.exports = function (RED) {
|
|
|
38
40
|
node.name = config.name;
|
|
39
41
|
node.endpoint = config.endpoint || "/v1/kernel/rank";
|
|
40
42
|
|
|
41
|
-
const cfg = RED.nodes.getNode(config.config);
|
|
42
|
-
|
|
43
43
|
node.on("input", async function (msg, send, done) {
|
|
44
|
+
send = send || function () { node.send.apply(node, arguments); };
|
|
45
|
+
|
|
44
46
|
try {
|
|
47
|
+
const cfg = RED.nodes.getNode(config.config);
|
|
45
48
|
if (!cfg) {
|
|
46
49
|
node.status({ fill: "red", shape: "ring", text: "missing config" });
|
|
47
|
-
|
|
48
|
-
"VectorPrime config missing. Open node settings and set Config."
|
|
49
|
-
|
|
50
|
+
msg.payload = {
|
|
51
|
+
error: "VectorPrime config missing. Open node settings and set Config.",
|
|
52
|
+
};
|
|
53
|
+
send(msg);
|
|
54
|
+
if (done) done();
|
|
55
|
+
return;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
const baseUrl = normalizeBaseUrl(cfg.baseUrl);
|
|
@@ -54,10 +60,16 @@ module.exports = function (RED) {
|
|
|
54
60
|
|
|
55
61
|
if (!baseUrl.startsWith("http")) {
|
|
56
62
|
node.status({ fill: "red", shape: "ring", text: "invalid base url" });
|
|
57
|
-
|
|
63
|
+
msg.payload = { error: `Base URL missing/invalid: ${baseUrl}` };
|
|
64
|
+
send(msg);
|
|
65
|
+
if (done) done();
|
|
66
|
+
return;
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
// ✅ Ensure headers object exists
|
|
60
70
|
msg.headers = msg.headers || {};
|
|
71
|
+
|
|
72
|
+
// ✅ Allow incoming Authorization override (advanced users)
|
|
61
73
|
const incomingAuth =
|
|
62
74
|
msg.headers.authorization ||
|
|
63
75
|
msg.headers.Authorization ||
|
|
@@ -68,17 +80,21 @@ module.exports = function (RED) {
|
|
|
68
80
|
? String(cfg.credentials.apiKey).trim()
|
|
69
81
|
: "";
|
|
70
82
|
|
|
71
|
-
// If user didn't provide Authorization, use stored key
|
|
83
|
+
// ✅ If user didn't provide Authorization, use stored key from config credentials
|
|
72
84
|
if (!incomingAuth) {
|
|
73
85
|
if (!storedKey) {
|
|
74
86
|
node.status({ fill: "red", shape: "ring", text: "missing api key" });
|
|
75
|
-
|
|
76
|
-
"No API key. Open VectorPrime Config and click 'Get Free Key'."
|
|
77
|
-
|
|
87
|
+
msg.payload = {
|
|
88
|
+
error: "No API key. Open VectorPrime Config and click 'Get Free Key'.",
|
|
89
|
+
};
|
|
90
|
+
send(msg);
|
|
91
|
+
if (done) done();
|
|
92
|
+
return;
|
|
78
93
|
}
|
|
79
94
|
msg.headers.Authorization = `Bearer ${storedKey}`;
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
// ✅ Force JSON
|
|
82
98
|
msg.headers["Content-Type"] = "application/json";
|
|
83
99
|
|
|
84
100
|
const payload = msg.payload || {};
|
|
@@ -99,7 +115,7 @@ module.exports = function (RED) {
|
|
|
99
115
|
data = { raw: text };
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
//
|
|
118
|
+
// ✅ Free tier limit hit → return upgrade url inside Node-RED
|
|
103
119
|
if (!resp.ok) {
|
|
104
120
|
const upgradeUrl = data.upgrade_url || data.checkout_url || null;
|
|
105
121
|
|
|
@@ -112,24 +128,26 @@ module.exports = function (RED) {
|
|
|
112
128
|
msg.payload = data;
|
|
113
129
|
msg.vectorprime = { upgrade_url: upgradeUrl };
|
|
114
130
|
send(msg);
|
|
115
|
-
done();
|
|
131
|
+
if (done) done();
|
|
116
132
|
return;
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
node.status({ fill: "red", shape: "ring", text: `error ${resp.status}` });
|
|
120
136
|
msg.payload = data;
|
|
121
137
|
send(msg);
|
|
122
|
-
done();
|
|
138
|
+
if (done) done();
|
|
123
139
|
return;
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
node.status({ fill: "green", shape: "dot", text: "ok" });
|
|
127
143
|
msg.payload = data;
|
|
128
144
|
send(msg);
|
|
129
|
-
done();
|
|
145
|
+
if (done) done();
|
|
130
146
|
} catch (err) {
|
|
131
147
|
node.status({ fill: "red", shape: "ring", text: "failed" });
|
|
132
|
-
|
|
148
|
+
msg.payload = { error: err.message || String(err) };
|
|
149
|
+
send(msg);
|
|
150
|
+
if (done) done(err);
|
|
133
151
|
}
|
|
134
152
|
});
|
|
135
153
|
}
|