@manny-est/node-red-flowpilot 0.2.2 → 0.4.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/CHANGELOG.md +62 -0
- package/PROJECT-OVERVIEW.md +42 -7
- package/README.md +19 -0
- package/USER-GUIDE.md +45 -1
- package/flowpilot-core.css +1189 -0
- package/flowpilot-core.js +6416 -0
- package/flowpilot.html +9 -5679
- package/flowpilot.js +292 -37
- package/lib/build-system-prompt.js +32 -0
- package/lib/default-system-prompt.js +0 -2
- package/lib/generation-system-prompt.js +17 -2
- package/lib/modify-system-prompt.js +16 -6
- package/lib/persona-prompt.js +74 -0
- package/lib/popout/view.html +33 -0
- package/lib/provider-openai-compatible.js +13 -10
- package/lib/storage.js +48 -4
- package/package.json +4 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Builds the dynamic "Personality" instruction, scaled by
|
|
2
|
+
// settings.personaIntensity (1-10). Kept separate from default-system-
|
|
3
|
+
// prompt.js (the user-editable base prompt) so the persona always reflects
|
|
4
|
+
// the CURRENT slider value, rather than being baked into the persisted,
|
|
5
|
+
// freeform systemPrompt text where it could drift out of sync.
|
|
6
|
+
//
|
|
7
|
+
// Two scopes, since Chat and Generate/Document/Modify have different shapes:
|
|
8
|
+
// - "chat" (default): framing applies to ordinary chat replies — greetings,
|
|
9
|
+
// capability questions, brief transitions.
|
|
10
|
+
// - "explanation": framing applies to the natural-language "explanation"
|
|
11
|
+
// field of a generate/document/modify envelope ONLY — never to node
|
|
12
|
+
// names, ids, or any other JSON field, which the model must still produce
|
|
13
|
+
// exactly as instructed by that mode's own system prompt.
|
|
14
|
+
//
|
|
15
|
+
// Reference-point anchors (not just an abstract 1-10 rule) because smaller/
|
|
16
|
+
// local models follow concrete worked examples far more reliably than prose
|
|
17
|
+
// instructions alone — the same lesson learned fixing Generate's "wires" bug.
|
|
18
|
+
function buildPersonaInstruction(intensity, options) {
|
|
19
|
+
const n = Math.max(1, Math.min(10, Math.round(Number(intensity) || 3)));
|
|
20
|
+
const scope = (options && options.scope === "explanation")
|
|
21
|
+
? "in the natural-language \"explanation\" text of your response only — " +
|
|
22
|
+
"never in node names, ids, or any other field, which must follow this " +
|
|
23
|
+
"mode's own format rules exactly"
|
|
24
|
+
: "at greetings, \"what can you do?\"-style capability questions, and " +
|
|
25
|
+
"brief transition moments only";
|
|
26
|
+
|
|
27
|
+
// The "hold back" instruction must NOT be blanket — at high intensity the
|
|
28
|
+
// whole point is to NOT hold back. Scaling this by n keeps "go all out at
|
|
29
|
+
// 10" from being undercut by a one-size-fits-all caution at the end.
|
|
30
|
+
const restraint = n >= 8
|
|
31
|
+
? "At this intensity, go all the way in: every qualifying moment gets " +
|
|
32
|
+
"the FULL treatment — multiple sentences of in-character captain-" +
|
|
33
|
+
"speak, not one sprinkled word. Do not hold back, downplay it, or " +
|
|
34
|
+
"soften it to seem tasteful — \"a little goes a long way\" does NOT " +
|
|
35
|
+
"apply at this intensity; lean all the way in, every time."
|
|
36
|
+
: (n >= 5
|
|
37
|
+
? "Use it often enough to be a clearly recognizable voice, but " +
|
|
38
|
+
"don't overdo it — a sentence or two of flavor per qualifying " +
|
|
39
|
+
"moment is plenty."
|
|
40
|
+
: "A little goes a long way: a short phrase, or nothing at all, is " +
|
|
41
|
+
"usually enough — don't repeat it in every single reply.");
|
|
42
|
+
|
|
43
|
+
return "Personality (intensity " + n + "/10, where 1 is a plain, no-frills " +
|
|
44
|
+
"Node-RED engineer and 10 is a comically over-the-top airline captain " +
|
|
45
|
+
"who happens to be a Node-RED expert): scale your voice " + scope + " " +
|
|
46
|
+
"to this intensity. NEVER let it touch the substance — explanations, " +
|
|
47
|
+
"troubleshooting, diffs, technical detail, and errors always stay " +
|
|
48
|
+
"plain, direct, and accurate no matter the intensity — a confused or " +
|
|
49
|
+
"stuck user gets a straight answer, never a bit.\n\n" +
|
|
50
|
+
"Reference points to interpolate between:\n" +
|
|
51
|
+
"- 1 (plain engineer): \"Hi, I'm FlowPilot. I can generate, modify, " +
|
|
52
|
+
"document, or chat about your flows — what do you need?\" No aviation " +
|
|
53
|
+
"language anywhere, ever.\n" +
|
|
54
|
+
"- 3 (subtle co-pilot): \"You pick the destination, I help you get " +
|
|
55
|
+
"there.\" A light \"wheels up\" / \"touchdown\" nod at a transition, " +
|
|
56
|
+
"used sparingly — most replies have no aviation language at all.\n" +
|
|
57
|
+
"- 7 (noticeable captain energy): \"Welcome aboard — I'm FlowPilot, " +
|
|
58
|
+
"your co-pilot for this flow. Let's get you cleared for takeoff.\" " +
|
|
59
|
+
"Aviation framing shows up more often and more colorfully, but still " +
|
|
60
|
+
"backs off completely once things turn technical.\n" +
|
|
61
|
+
"- 10 (full captain, comic — GO ALL OUT): \"Ladies and gentlemen, this " +
|
|
62
|
+
"is your captain speaking. I've just illuminated the fasten seatbelt " +
|
|
63
|
+
"sign — please take your seats, because I've finished building the " +
|
|
64
|
+
"Node-RED flow you requested. We are cleared for takeoff: fully wired, " +
|
|
65
|
+
"deployed, and ready for your review. Enjoy the flight, and thank you " +
|
|
66
|
+
"for choosing FlowPilot Airlines.\" At 10, EVERY qualifying moment gets " +
|
|
67
|
+
"a full announcement like this one, with callsigns, runway/altitude " +
|
|
68
|
+
"metaphors, and flight-crew theatrics throughout — not a passing " +
|
|
69
|
+
"reference — but the instant things turn technical, drop the act " +
|
|
70
|
+
"entirely and answer like the expert engineer underneath it.\n\n" +
|
|
71
|
+
restraint;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { buildPersonaInstruction };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>FlowPilot</title>
|
|
6
|
+
<link rel="stylesheet" href="/red/style.min.css">
|
|
7
|
+
<link rel="stylesheet" href="/vendor/font-awesome/css/font-awesome.min.css">
|
|
8
|
+
<link rel="stylesheet" href="/flowpilot/core.css">
|
|
9
|
+
<style>
|
|
10
|
+
/* Minimal page chrome — the real layout lives in flowpilot-core.css's
|
|
11
|
+
#fp-root rules, same as the sidebar. This pop-out has no settings/
|
|
12
|
+
history panels in v1, so #fp-root just needs to fill the window. */
|
|
13
|
+
html, body { height: 100%; margin: 0; }
|
|
14
|
+
#fp-popout-root { height: 100%; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body class="red-ui-editor">
|
|
18
|
+
<div id="fp-popout-root"></div>
|
|
19
|
+
<script src="/vendor/vendor.js"></script>
|
|
20
|
+
<script src="/red/red.min.js"></script>
|
|
21
|
+
<script src="/flowpilot/core.js"></script>
|
|
22
|
+
<script type="text/javascript">
|
|
23
|
+
// This window never connects to the runtime or loads a live flow
|
|
24
|
+
// (RED.nodes/RED.view here are inert, disconnected shells, exactly like
|
|
25
|
+
// Node-RED's own debug pop-out) — every canvas-touching action is
|
|
26
|
+
// proxied back to the main window via postMessage. See
|
|
27
|
+
// flowpilot-core.js's initPopout() for the actual logic (including
|
|
28
|
+
// applying the user's dark/light theme preference, the same way
|
|
29
|
+
// Node-RED's own debug pop-out's debug.js does).
|
|
30
|
+
FlowPilotCore.initPopout();
|
|
31
|
+
</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -48,8 +48,9 @@ function postJson(urlString, headers, body, timeoutMs) {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
req.on("error", reject);
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const effectiveTimeoutMs = timeoutMs || 180000;
|
|
52
|
+
req.setTimeout(effectiveTimeoutMs, () => {
|
|
53
|
+
req.destroy(new Error(`Provider request timed out after ${effectiveTimeoutMs}ms — increase the request timeout in Settings → Behavior for slower hardware.`));
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
req.write(payload);
|
|
@@ -100,8 +101,9 @@ function getJson(urlString, headers, timeoutMs) {
|
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
req.on("error", reject);
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
const effectiveTimeoutMs = timeoutMs || 30000;
|
|
105
|
+
req.setTimeout(effectiveTimeoutMs, () => {
|
|
106
|
+
req.destroy(new Error(`Provider request timed out after ${effectiveTimeoutMs}ms — increase the request timeout in Settings → Behavior for slower hardware.`));
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
req.end();
|
|
@@ -127,7 +129,7 @@ async function listModels(settings) {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
try {
|
|
130
|
-
const response = await getJson(`${baseUrl}/v1/models`, headers, 30000);
|
|
132
|
+
const response = await getJson(`${baseUrl}/v1/models`, headers, settings.requestTimeoutMs || 30000);
|
|
131
133
|
const data = response && Array.isArray(response.data) ? response.data : [];
|
|
132
134
|
const models = data
|
|
133
135
|
.map(function (m) { return m && m.id; })
|
|
@@ -169,7 +171,7 @@ async function chat(settings, messages, options) {
|
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
const startedAt = Date.now();
|
|
172
|
-
const response = await postJson(`${baseUrl}/v1/chat/completions`, headers, body, 180000);
|
|
174
|
+
const response = await postJson(`${baseUrl}/v1/chat/completions`, headers, body, settings.requestTimeoutMs || 180000);
|
|
173
175
|
const totalMs = Date.now() - startedAt;
|
|
174
176
|
|
|
175
177
|
const message = response && response.choices && response.choices[0] && response.choices[0].message;
|
|
@@ -275,8 +277,9 @@ function postStream(urlString, headers, body, timeoutMs, onDelta) {
|
|
|
275
277
|
});
|
|
276
278
|
|
|
277
279
|
req.on("error", reject);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
const effectiveTimeoutMs = timeoutMs || 180000;
|
|
281
|
+
req.setTimeout(effectiveTimeoutMs, () => {
|
|
282
|
+
req.destroy(new Error(`Provider request timed out after ${effectiveTimeoutMs}ms — increase the request timeout in Settings → Behavior for slower hardware.`));
|
|
280
283
|
});
|
|
281
284
|
|
|
282
285
|
req.write(payload);
|
|
@@ -306,7 +309,7 @@ async function chatStream(settings, messages, onDelta) {
|
|
|
306
309
|
temperature,
|
|
307
310
|
stream: true,
|
|
308
311
|
stream_options: { include_usage: true }
|
|
309
|
-
}, 180000, onDelta);
|
|
312
|
+
}, settings.requestTimeoutMs || 180000, onDelta);
|
|
310
313
|
|
|
311
314
|
return {
|
|
312
315
|
content: result.content || "",
|
|
@@ -359,7 +362,7 @@ async function probeTools(settings) {
|
|
|
359
362
|
tool_choice: "auto",
|
|
360
363
|
temperature: 0,
|
|
361
364
|
stream: false
|
|
362
|
-
}, 30000);
|
|
365
|
+
}, settings.requestTimeoutMs || 30000);
|
|
363
366
|
|
|
364
367
|
const message = response && response.choices && response.choices[0] && response.choices[0].message;
|
|
365
368
|
const toolCalls = message && Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
package/lib/storage.js
CHANGED
|
@@ -45,9 +45,34 @@ function createStorage(userDir) {
|
|
|
45
45
|
// includes as history with each request. Older turns are dropped
|
|
46
46
|
// client-side and the model is told when that happened.
|
|
47
47
|
historyMaxExchanges: 10,
|
|
48
|
+
// How long to wait for a provider response before giving up. Slow local
|
|
49
|
+
// hardware (e.g. Ollama on a big model with no GPU) can take much longer
|
|
50
|
+
// than cloud providers; users on that hardware raise this in Behavior
|
|
51
|
+
// settings rather than living with a hardcoded ceiling.
|
|
52
|
+
requestTimeoutMs: 180000,
|
|
53
|
+
// Max build->deploy->test->fix cycles the /build agentic loop will run
|
|
54
|
+
// before stopping with an honest "couldn't fully verify" instead of
|
|
55
|
+
// proposing another fix. Bounds against a non-converging loop burning
|
|
56
|
+
// tokens forever; user-configurable since "reasonable" varies by
|
|
57
|
+
// provider speed/cost. Unrelated to the read-only tool-calling loop's
|
|
58
|
+
// own hardcoded AGENT_LOOP_MAX_STEPS (a different bound, for a
|
|
59
|
+
// different loop).
|
|
60
|
+
agentLoopMaxIterations: 5,
|
|
48
61
|
// Lets the user silence the recurring secrets/size reminder bar after
|
|
49
62
|
// typing an explicit acknowledgement in settings.
|
|
50
63
|
suppressContextWarnings: false,
|
|
64
|
+
// Secret-shaped-value scrubbing (password/token/apiKey-looking fields in
|
|
65
|
+
// node config and debug output) — on by default. Local/private-AI users
|
|
66
|
+
// can turn it off via a separate type-to-confirm gate in Settings; the
|
|
67
|
+
// dedicated Node-RED credentials field is dropped by the frontend
|
|
68
|
+
// regardless of this setting, via a different, always-on mechanism.
|
|
69
|
+
redactionEnabled: true,
|
|
70
|
+
// Chat-only persona slider, 1-10: 1 is a plain Node-RED engineer, 10 is
|
|
71
|
+
// a comically over-the-top airline captain who happens to be a Node-RED
|
|
72
|
+
// expert. 3 matches the original "subtle co-pilot" voice this replaced.
|
|
73
|
+
// See lib/persona-prompt.js — generated fresh per request, never baked
|
|
74
|
+
// into the persisted systemPrompt below.
|
|
75
|
+
personaIntensity: 3,
|
|
51
76
|
// User-defined intent buttons: array of { label, text }.
|
|
52
77
|
customIntents: [],
|
|
53
78
|
systemPrompt: require("./default-system-prompt")
|
|
@@ -60,6 +85,15 @@ function createStorage(userDir) {
|
|
|
60
85
|
// unique to it) and swap in the current default instead. Applied both when
|
|
61
86
|
// reading settings AND when saving them, so a browser tab that still has
|
|
62
87
|
// the stale text loaded in the System Prompt textarea can't re-persist it.
|
|
88
|
+
// The old default also baked a static "Personality:" paragraph into the
|
|
89
|
+
// persisted systemPrompt; that's now generated fresh every request from
|
|
90
|
+
// personaIntensity (lib/persona-prompt.js) instead, scaled by a slider, so
|
|
91
|
+
// a leftover copy from before this change is redundant rather than wrong.
|
|
92
|
+
// Unlike the READ-ONLY case above, only this one paragraph is stale — the
|
|
93
|
+
// rest of any customization the user made should survive — so this is a
|
|
94
|
+
// surgical removal (exact match) rather than swapping the whole prompt.
|
|
95
|
+
const STALE_PERSONALITY_PARAGRAPH = "Personality: you have a subtle co-pilot voice, used ONLY for greetings, \"what can you do?\"-style capability questions, and brief transition moments — e.g. \"You pick the destination, I help you get there,\" or a light \"wheels up\" / \"touchdown\" nod when handing off to a review or confirming a change landed. For everything else — explanations, troubleshooting, diffs, technical detail, errors — stay plain and direct; never let the persona obscure, delay, or replace a real answer. A little goes a long way: do not repeat aviation phrasing in every reply.\n\n";
|
|
96
|
+
|
|
63
97
|
function fixStaleSystemPrompt(systemPrompt) {
|
|
64
98
|
if (typeof systemPrompt !== "string" || !systemPrompt.trim()) {
|
|
65
99
|
return defaultSettings.systemPrompt;
|
|
@@ -67,6 +101,9 @@ function createStorage(userDir) {
|
|
|
67
101
|
if (systemPrompt.indexOf("operating in READ-ONLY mode") !== -1) {
|
|
68
102
|
return defaultSettings.systemPrompt;
|
|
69
103
|
}
|
|
104
|
+
if (systemPrompt.indexOf(STALE_PERSONALITY_PARAGRAPH) !== -1) {
|
|
105
|
+
return systemPrompt.split(STALE_PERSONALITY_PARAGRAPH).join("");
|
|
106
|
+
}
|
|
70
107
|
return systemPrompt;
|
|
71
108
|
}
|
|
72
109
|
|
|
@@ -100,12 +137,19 @@ function createStorage(userDir) {
|
|
|
100
137
|
return parsed;
|
|
101
138
|
}
|
|
102
139
|
|
|
103
|
-
// Returns the currently active provider profile (or the first, or a
|
|
140
|
+
// Returns the currently active provider profile (or the first, or a
|
|
141
|
+
// default), with the app-level requestTimeoutMs folded in. Every
|
|
142
|
+
// provider.chat/chatStream/listModels/probeTools call takes this object as
|
|
143
|
+
// its `settings` argument, so merging the timeout in here is what threads
|
|
144
|
+
// it through all of them without touching each call site.
|
|
104
145
|
function getActiveProvider(settings) {
|
|
105
146
|
const list = Array.isArray(settings.providers) ? settings.providers : [];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
147
|
+
const base = list.length
|
|
148
|
+
? (list.filter(function (p) { return p.id === settings.activeProviderId; })[0] || list[0])
|
|
149
|
+
: defaultProvider();
|
|
150
|
+
const requestTimeoutMs = settings.requestTimeoutMs !== undefined
|
|
151
|
+
? settings.requestTimeoutMs : defaultSettings.requestTimeoutMs;
|
|
152
|
+
return Object.assign({}, base, { requestTimeoutMs: requestTimeoutMs });
|
|
109
153
|
}
|
|
110
154
|
|
|
111
155
|
function init() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manny-est/node-red-flowpilot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "FlowPilot for Node-RED - an AI-powered development assistant sidebar",
|
|
5
5
|
"main": "flowpilot.js",
|
|
6
6
|
"keywords": [
|
|
@@ -38,12 +38,15 @@
|
|
|
38
38
|
"files": [
|
|
39
39
|
"flowpilot.js",
|
|
40
40
|
"flowpilot.html",
|
|
41
|
+
"flowpilot-core.js",
|
|
42
|
+
"flowpilot-core.css",
|
|
41
43
|
"lib",
|
|
42
44
|
"icons",
|
|
43
45
|
"examples",
|
|
44
46
|
"README.md",
|
|
45
47
|
"USER-GUIDE.md",
|
|
46
48
|
"PROJECT-OVERVIEW.md",
|
|
49
|
+
"CHANGELOG.md",
|
|
47
50
|
"LICENSE"
|
|
48
51
|
]
|
|
49
52
|
}
|