node-red-contrib-vectorprime 0.1.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 +68 -0
- package/index.js +0 -0
- package/package.json +22 -0
- package/vectorprime.html +147 -0
- package/vectorprime.js +138 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# node-red-contrib-vectorprime
|
|
2
|
+
|
|
3
|
+
Official VectorPrime Decision Kernel node for Node-RED.
|
|
4
|
+
|
|
5
|
+
This node sends a decision request to VectorPrime and returns a ranked result.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✅ What this node does
|
|
10
|
+
|
|
11
|
+
- **Input:** `msg.payload` (your decision request JSON)
|
|
12
|
+
- **Output:** `msg.payload` (VectorPrime response JSON)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✅ Quick Start
|
|
17
|
+
|
|
18
|
+
### Endpoint
|
|
19
|
+
|
|
20
|
+
`/v1/kernel/rank`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ✅ How to use in Node-RED
|
|
25
|
+
|
|
26
|
+
### 1) Drag these nodes into your flow:
|
|
27
|
+
|
|
28
|
+
- **Inject**
|
|
29
|
+
- **VectorPrime**
|
|
30
|
+
- **Debug**
|
|
31
|
+
|
|
32
|
+
### 2) Connect them like this:
|
|
33
|
+
|
|
34
|
+
Inject → VectorPrime → Debug
|
|
35
|
+
|
|
36
|
+
### 3) Configure the VectorPrime node
|
|
37
|
+
|
|
38
|
+
Double-click the **VectorPrime** node, then:
|
|
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
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## ✅ Example Inject Payload
|
|
52
|
+
|
|
53
|
+
### IMPORTANT:
|
|
54
|
+
Your Inject node must send a JSON object inside `msg.payload`.
|
|
55
|
+
|
|
56
|
+
Set your Inject node payload type to **JSON** and paste this:
|
|
57
|
+
|
|
58
|
+
```json
|
|
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
|
+
}
|
package/index.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-vectorprime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "VectorPrime Decision Kernel node for Node-RED",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"node-red",
|
|
9
|
+
"vectorprime",
|
|
10
|
+
"decision",
|
|
11
|
+
"ranking",
|
|
12
|
+
"kernel"
|
|
13
|
+
],
|
|
14
|
+
"node-red": {
|
|
15
|
+
"nodes": {
|
|
16
|
+
"vectorprime": "vectorprime.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"node-fetch": "^2.6.7"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/vectorprime.html
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="vectorprime-config">
|
|
2
|
+
<div class="form-row">
|
|
3
|
+
<label for="node-config-input-baseUrl">
|
|
4
|
+
<i class="fa fa-globe"></i> Base URL
|
|
5
|
+
</label>
|
|
6
|
+
<input type="text" id="node-config-input-baseUrl" placeholder="https://vectorprime-kernel-backend.onrender.com">
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="form-row">
|
|
10
|
+
<label for="node-config-input-apiKey">
|
|
11
|
+
<i class="fa fa-key"></i> API Key
|
|
12
|
+
</label>
|
|
13
|
+
<input type="password" id="node-config-input-apiKey" placeholder="vp_free_...">
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="form-row">
|
|
17
|
+
<button id="vp-get-free-key" class="red-ui-button">
|
|
18
|
+
Get Free Key
|
|
19
|
+
</button>
|
|
20
|
+
<span id="vp-key-status" style="margin-left:10px; font-size:12px;"></span>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="form-row">
|
|
24
|
+
<div style="font-size:12px; color:#666;">
|
|
25
|
+
✅ Key is stored securely in Node-RED credentials (not exported with flows).
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/javascript">
|
|
31
|
+
(function () {
|
|
32
|
+
function normalizeBaseUrl(url) {
|
|
33
|
+
url = (url || "").trim();
|
|
34
|
+
if (!url) return "";
|
|
35
|
+
// Remove trailing slash
|
|
36
|
+
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
RED.nodes.registerType("vectorprime-config", {
|
|
41
|
+
category: "config",
|
|
42
|
+
defaults: {
|
|
43
|
+
baseUrl: { value: "https://vectorprime-kernel-backend.onrender.com", required: true }
|
|
44
|
+
},
|
|
45
|
+
credentials: {
|
|
46
|
+
apiKey: { type: "password" }
|
|
47
|
+
},
|
|
48
|
+
label: function () {
|
|
49
|
+
return "VectorPrime Config";
|
|
50
|
+
},
|
|
51
|
+
oneditprepare: function () {
|
|
52
|
+
const statusEl = document.getElementById("vp-key-status");
|
|
53
|
+
const btn = document.getElementById("vp-get-free-key");
|
|
54
|
+
|
|
55
|
+
btn.onclick = async function (e) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
statusEl.textContent = "Requesting free key...";
|
|
58
|
+
statusEl.style.color = "#444";
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const baseUrlInput = document.getElementById("node-config-input-baseUrl");
|
|
62
|
+
const apiKeyInput = document.getElementById("node-config-input-apiKey");
|
|
63
|
+
|
|
64
|
+
const baseUrl = normalizeBaseUrl(baseUrlInput.value);
|
|
65
|
+
|
|
66
|
+
if (!baseUrl.startsWith("http")) {
|
|
67
|
+
statusEl.textContent = "❌ Base URL missing/invalid";
|
|
68
|
+
statusEl.style.color = "red";
|
|
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
|
+
};
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
})();
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<script type="text/html" data-template-name="vectorprime-rank">
|
|
109
|
+
<div class="form-row">
|
|
110
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
111
|
+
<input type="text" id="node-input-name" placeholder="VectorPrime Rank">
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="form-row">
|
|
115
|
+
<label for="node-input-config"><i class="fa fa-cog"></i> Config</label>
|
|
116
|
+
<input type="text" id="node-input-config">
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="form-row">
|
|
120
|
+
<label for="node-input-endpoint"><i class="fa fa-link"></i> Endpoint</label>
|
|
121
|
+
<input type="text" id="node-input-endpoint" placeholder="/v1/kernel/rank">
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="form-row">
|
|
125
|
+
<div style="font-size:12px; color:#666;">
|
|
126
|
+
✅ This node sends <code>msg.payload</code> to VectorPrime and returns the response in <code>msg.payload</code>.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<script type="text/javascript">
|
|
132
|
+
RED.nodes.registerType("vectorprime-rank", {
|
|
133
|
+
category: "VectorPrime",
|
|
134
|
+
color: "#2563EB", // ✅ FIX: readable (NOT black)
|
|
135
|
+
defaults: {
|
|
136
|
+
name: { value: "" },
|
|
137
|
+
config: { type: "vectorprime-config", required: true },
|
|
138
|
+
endpoint: { value: "/v1/kernel/rank", required: true }
|
|
139
|
+
},
|
|
140
|
+
inputs: 1,
|
|
141
|
+
outputs: 1,
|
|
142
|
+
icon: "font-awesome/fa-bolt",
|
|
143
|
+
label: function () {
|
|
144
|
+
return this.name || "VectorPrime Rank";
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
</script>
|
package/vectorprime.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
const fetch = (...args) =>
|
|
3
|
+
import("node-fetch").then(({ default: fetch }) => fetch(...args));
|
|
4
|
+
|
|
5
|
+
function normalizeBaseUrl(url) {
|
|
6
|
+
url = (url || "").trim();
|
|
7
|
+
if (!url) return "";
|
|
8
|
+
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
9
|
+
return url;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeEndpoint(path) {
|
|
13
|
+
path = (path || "").trim();
|
|
14
|
+
if (!path.startsWith("/")) path = "/" + path;
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ✅ REQUIRED: runtime config node registration
|
|
19
|
+
function VectorPrimeConfigNode(n) {
|
|
20
|
+
RED.nodes.createNode(this, n);
|
|
21
|
+
|
|
22
|
+
// baseUrl stored in node settings (non-credential)
|
|
23
|
+
this.baseUrl = n.baseUrl || "https://vectorprime-kernel-backend.onrender.com";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ✅ IMPORTANT: credentials must be declared here too
|
|
27
|
+
RED.nodes.registerType("vectorprime-config", VectorPrimeConfigNode, {
|
|
28
|
+
credentials: {
|
|
29
|
+
apiKey: { type: "password" },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ✅ Main node
|
|
34
|
+
function VectorPrimeRankNode(config) {
|
|
35
|
+
RED.nodes.createNode(this, config);
|
|
36
|
+
const node = this;
|
|
37
|
+
|
|
38
|
+
node.name = config.name;
|
|
39
|
+
node.endpoint = config.endpoint || "/v1/kernel/rank";
|
|
40
|
+
|
|
41
|
+
const cfg = RED.nodes.getNode(config.config);
|
|
42
|
+
|
|
43
|
+
node.on("input", async function (msg, send, done) {
|
|
44
|
+
try {
|
|
45
|
+
if (!cfg) {
|
|
46
|
+
node.status({ fill: "red", shape: "ring", text: "missing config" });
|
|
47
|
+
throw new Error(
|
|
48
|
+
"VectorPrime config missing. Open node settings and set Config."
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const baseUrl = normalizeBaseUrl(cfg.baseUrl);
|
|
53
|
+
const endpoint = normalizeEndpoint(node.endpoint);
|
|
54
|
+
|
|
55
|
+
if (!baseUrl.startsWith("http")) {
|
|
56
|
+
node.status({ fill: "red", shape: "ring", text: "invalid base url" });
|
|
57
|
+
throw new Error(`Base URL missing/invalid: ${baseUrl}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
msg.headers = msg.headers || {};
|
|
61
|
+
const incomingAuth =
|
|
62
|
+
msg.headers.authorization ||
|
|
63
|
+
msg.headers.Authorization ||
|
|
64
|
+
"";
|
|
65
|
+
|
|
66
|
+
const storedKey =
|
|
67
|
+
cfg.credentials && cfg.credentials.apiKey
|
|
68
|
+
? String(cfg.credentials.apiKey).trim()
|
|
69
|
+
: "";
|
|
70
|
+
|
|
71
|
+
// If user didn't provide Authorization, use stored key
|
|
72
|
+
if (!incomingAuth) {
|
|
73
|
+
if (!storedKey) {
|
|
74
|
+
node.status({ fill: "red", shape: "ring", text: "missing api key" });
|
|
75
|
+
throw new Error(
|
|
76
|
+
"No API key. Open VectorPrime Config and click 'Get Free Key'."
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
msg.headers.Authorization = `Bearer ${storedKey}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
msg.headers["Content-Type"] = "application/json";
|
|
83
|
+
|
|
84
|
+
const payload = msg.payload || {};
|
|
85
|
+
|
|
86
|
+
node.status({ fill: "blue", shape: "dot", text: "ranking..." });
|
|
87
|
+
|
|
88
|
+
const resp = await fetch(`${baseUrl}${endpoint}`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: msg.headers,
|
|
91
|
+
body: JSON.stringify(payload),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const text = await resp.text();
|
|
95
|
+
let data;
|
|
96
|
+
try {
|
|
97
|
+
data = JSON.parse(text);
|
|
98
|
+
} catch {
|
|
99
|
+
data = { raw: text };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If free tier limit hit → return upgrade url inside Node-RED
|
|
103
|
+
if (!resp.ok) {
|
|
104
|
+
const upgradeUrl = data.upgrade_url || data.checkout_url || null;
|
|
105
|
+
|
|
106
|
+
if (resp.status === 402 && upgradeUrl) {
|
|
107
|
+
node.status({
|
|
108
|
+
fill: "yellow",
|
|
109
|
+
shape: "ring",
|
|
110
|
+
text: "limit reached → upgrade",
|
|
111
|
+
});
|
|
112
|
+
msg.payload = data;
|
|
113
|
+
msg.vectorprime = { upgrade_url: upgradeUrl };
|
|
114
|
+
send(msg);
|
|
115
|
+
done();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
node.status({ fill: "red", shape: "ring", text: `error ${resp.status}` });
|
|
120
|
+
msg.payload = data;
|
|
121
|
+
send(msg);
|
|
122
|
+
done();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
node.status({ fill: "green", shape: "dot", text: "ok" });
|
|
127
|
+
msg.payload = data;
|
|
128
|
+
send(msg);
|
|
129
|
+
done();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
node.status({ fill: "red", shape: "ring", text: "failed" });
|
|
132
|
+
done(err);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
RED.nodes.registerType("vectorprime-rank", VectorPrimeRankNode);
|
|
138
|
+
};
|