@manny-est/node-red-flowpilot 0.5.2 → 0.6.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 +85 -26
- package/README.md +10 -1
- package/USER-GUIDE.md +5 -1
- package/flowpilot-core.css +53 -10
- package/flowpilot-node-entry.js +15 -0
- package/flowpilot.js +1207 -187
- package/lib/agent-contract.js +50 -0
- package/lib/build-core-script.js +1 -0
- package/lib/chat-data.js +106 -0
- package/lib/core/apply-review.js +34 -11
- package/lib/core/graph-truth.js +63 -0
- package/lib/core/history.js +157 -6
- package/lib/core/init.js +223 -21
- package/lib/core/main.js +780 -38
- package/lib/core/modes.js +1098 -112
- package/lib/core/selection-context.js +44 -24
- package/lib/default-system-prompt.js +4 -3
- package/lib/document-system-prompt.js +7 -4
- package/lib/envelope.js +13 -7
- package/lib/generation-system-prompt.js +5 -4
- package/lib/modify-system-prompt.js +64 -12
- package/lib/persona-prompt.js +81 -54
- package/lib/prompt-fragments.js +17 -2
- package/lib/provider-anthropic.js +23 -10
- package/lib/provider-openai-compatible.js +51 -11
- package/lib/provider-shape-check.js +34 -0
- package/lib/storage.js +134 -21
- package/package.json +3 -2
|
@@ -46,6 +46,23 @@
|
|
|
46
46
|
return { nodes: expanded, groupCount: groupCount };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// The "all"/"instance" targetNodeIds scopes (suggestedAction chips,
|
|
50
|
+
// redirect_mode tool calls): every node in the active flow tab, or
|
|
51
|
+
// every node in the whole instance (every tab, enabled or disabled —
|
|
52
|
+
// RED.nodes.eachNode iterates all of them regardless of tab state).
|
|
53
|
+
function allActiveTabNodeIds() {
|
|
54
|
+
var activeTabId = RED.workspaces && RED.workspaces.active ? RED.workspaces.active() : null;
|
|
55
|
+
var ids = [];
|
|
56
|
+
RED.nodes.eachNode(function (n) { if (n.z === activeTabId) { ids.push(n.id); } });
|
|
57
|
+
return ids;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function allInstanceNodeIds() {
|
|
61
|
+
var ids = [];
|
|
62
|
+
RED.nodes.eachNode(function (n) { ids.push(n.id); });
|
|
63
|
+
return ids;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
function pinCurrentSelection() {
|
|
50
67
|
var sel = (RED.view && RED.view.selection) ? RED.view.selection() : null;
|
|
51
68
|
var ids = expandGroupSelection((sel && sel.nodes) ? sel.nodes : []).nodes
|
|
@@ -204,35 +221,38 @@
|
|
|
204
221
|
? linksTouchingNodes(rawNodes)
|
|
205
222
|
: ((sel && sel.links) ? sel.links : []);
|
|
206
223
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
var
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
var configNodes;
|
|
225
|
+
if (currentSettings.allowConfigContext === true) {
|
|
226
|
+
// Collect config nodes (mqtt-broker, tls-config, etc.) that the
|
|
227
|
+
// selected nodes reference. Config nodes are shared configuration
|
|
228
|
+
// objects not on the canvas — the model needs their ids and
|
|
229
|
+
// non-credential properties to reference or create them in Modify.
|
|
230
|
+
// Detected via each live node's _def.defaults: any property whose
|
|
231
|
+
// propDef.type is a type name (rather than a value type like "str")
|
|
232
|
+
// is a config-node reference; n[k] is the referenced node's id.
|
|
233
|
+
var configNodeMap = {};
|
|
234
|
+
rawNodes.forEach(function (n) {
|
|
235
|
+
var typeDef = n._def;
|
|
236
|
+
if (!typeDef || !typeDef.defaults) { return; }
|
|
237
|
+
Object.keys(typeDef.defaults).forEach(function (k) {
|
|
238
|
+
var propDef = typeDef.defaults[k];
|
|
239
|
+
if (propDef && propDef.type && typeof n[k] === "string" && n[k] &&
|
|
240
|
+
!configNodeMap[n[k]]) {
|
|
241
|
+
var configNode = RED.nodes.node(n[k]);
|
|
242
|
+
if (configNode && configNode.type) {
|
|
243
|
+
configNodeMap[n[k]] = configNode;
|
|
244
|
+
}
|
|
225
245
|
}
|
|
226
|
-
}
|
|
246
|
+
});
|
|
227
247
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
248
|
+
configNodes = Object.keys(configNodeMap).map(function (id) {
|
|
249
|
+
return sanitizeNode(configNodeMap[id]);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
232
252
|
|
|
233
253
|
return {
|
|
234
254
|
nodes: rawNodes.map(sanitizeNode),
|
|
235
255
|
connections: buildConnections(rawNodes, rawLinks),
|
|
236
|
-
configNodes: configNodes.length ? configNodes : undefined
|
|
256
|
+
configNodes: Array.isArray(configNodes) && configNodes.length ? configNodes : undefined
|
|
237
257
|
};
|
|
238
258
|
}
|
|
@@ -33,11 +33,12 @@ Whenever the user describes something they want built, changed, fixed, wired up,
|
|
|
33
33
|
To do this, end your response with a hidden data block: on its own, after all visible reply text, with nothing after it and not inside a code fence, write the literal marker on its own line followed immediately by a single JSON object:
|
|
34
34
|
|
|
35
35
|
<<<FLOWPILOT_DATA>>>
|
|
36
|
-
{"suggestedAction": {"mode": "generate" | "document" | "modify", "prompt": "...", "selectionHint": "..."}}
|
|
36
|
+
{"suggestedAction": {"mode": "generate" | "document" | "modify", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | "instance" | ["real-node-id", "..."]}}
|
|
37
37
|
|
|
38
38
|
- "mode": which FlowPilot action the chip switches to.
|
|
39
|
-
- "prompt": the exact instruction text to pre-fill in the user's compose box — written as a ready-to-send request to FlowPilot, in the user's voice.
|
|
39
|
+
- "prompt": the exact instruction text to pre-fill in the user's compose box — written as a ready-to-send request to FlowPilot, in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
|
|
40
40
|
- "selectionHint" (optional): plain-language description of which node(s) the user should select before sending (only useful for "modify"/"document", which act on a selection).
|
|
41
|
+
- "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current selection, active flow/tab, whole instance, or other provided context makes the target resolvable. Use "all" when the entire active flow/tab is the resolved target, "instance" when the user means every flow tab in the whole Node-RED instance (only ever valid for "document" — Modify always acts on one flow), or a non-empty array of real node ids when a specific subset is the resolved target. Omit it only when you genuinely cannot resolve the target from the current context — don't guess between "all" and "instance" when the wording doesn't make it clear. Modify and Document require a resolved node set: never say or imply in "selectionHint" that no selection is needed unless "targetNodeIds" supplies that target, and never emit an empty array.
|
|
41
42
|
|
|
42
43
|
The user reviews the prepared prompt and clicks the chip themselves — nothing is sent automatically. Skip the data block only when there's truly no actionable follow-up (e.g. plain factual Q&A, status checks).
|
|
43
44
|
|
|
@@ -48,7 +49,7 @@ For example, for "Add a debug node after the inject" (asked in Chat):
|
|
|
48
49
|
CORRECT:
|
|
49
50
|
Sure — that's a quick addition. Switch to Modify with the inject node selected and I'll wire a debug node after it for you to review.
|
|
50
51
|
<<<FLOWPILOT_DATA>>>
|
|
51
|
-
{"suggestedAction": {"mode": "modify", "prompt": "Add a debug node after the inject node.", "selectionHint": "Select the inject node first."}}
|
|
52
|
+
{"suggestedAction": {"mode": "modify", "prompt": "Add a debug node after the inject node.", "selectionHint": "Select the inject node first.", "targetNodeIds": ["the-real-inject-node-id"]}}
|
|
52
53
|
|
|
53
54
|
WRONG — do not do this, even partially:
|
|
54
55
|
I'll add a debug node right after the inject node so you can inspect its output.
|
|
@@ -3,7 +3,7 @@ const {
|
|
|
3
3
|
buildSuggestedActionFragment
|
|
4
4
|
} = require("./prompt-fragments");
|
|
5
5
|
|
|
6
|
-
const identity = `You are FlowPilot's documentation generator.
|
|
6
|
+
const identity = `You are FlowPilot's documentation generator. You are given a set of existing Node-RED nodes as context (sanitized configuration plus how they're wired together) — this may be a hand-picked selection, an entire flow tab, or every flow tab in the whole instance, depending on what the user asked for and what got resolved before you were called. Your job is to explain what that context does, in detail, and package the explanation as a single Node-RED comment node the user can drop onto their canvas as a "read me". If the context spans multiple flow tabs, structure the explanation and diagram around that (e.g. group by tab) rather than writing as if it's all one flow.`;
|
|
7
7
|
|
|
8
8
|
const modeRouting = `Before documenting — check this is actually a "document" request:
|
|
9
9
|
|
|
@@ -16,11 +16,14 @@ The user is currently in Document mode, which produces a single read-me comment
|
|
|
16
16
|
When one of these applies, do NOT produce the {"explanation", "flow"} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked, and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
|
|
17
17
|
|
|
18
18
|
<<<FLOWPILOT_DATA>>>
|
|
19
|
-
{"suggestedAction": {"mode": "generate" | "modify" | "chat", "prompt": "...", "selectionHint": "..."}}
|
|
19
|
+
{"suggestedAction": {"mode": "generate" | "modify" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
|
|
20
20
|
|
|
21
21
|
- "mode": "generate"/"modify" if their request matches one of those actions instead; "chat" if it's a question or remark with no further action needed.
|
|
22
|
-
- "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
|
|
22
|
+
- "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
|
|
23
23
|
- "selectionHint" (optional): for "modify", which node(s) to select first (Generate needs no selection).
|
|
24
|
+
- "targetNodeIds" (only for "modify"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit it only when you genuinely cannot resolve the target from the current context. Never imply no selection is needed for Modify unless this field supplies the target, and never emit an empty array.
|
|
25
|
+
|
|
26
|
+
This escape hatch is for redirecting AWAY from Document (to Generate/Modify/Chat) — it never applies to a request that legitimately wants documentation, including "document my whole flow" or "document this entire Node-RED instance". Those stay in Document; see below for how their scope gets resolved.
|
|
24
27
|
|
|
25
28
|
The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for documenting the selection, ignore this section entirely and proceed normally below.
|
|
26
29
|
|
|
@@ -52,7 +55,7 @@ What "info" should contain:
|
|
|
52
55
|
- A Mermaid diagram of the flow using a fenced code block: \`\`\`mermaid ... \`\`\` (e.g. a \`graph LR\` or \`flowchart LR\` showing each node as a labeled box and arrows for the wiring). Use the node names/types from the context, not raw ids.
|
|
53
56
|
- If the user added their own notes alongside the selection, treat those as instructions for emphasis or audience (e.g. "explain like I'm new to Node-RED") — fold them into how you write the explanation, not as a separate section.
|
|
54
57
|
|
|
55
|
-
Base everything on the actual
|
|
58
|
+
Base everything on the actual nodes given as context and their wiring — never invent nodes that aren't in the context. If you were given nothing useful to document, say so plainly in "explanation" and still return a single comment node whose "info" explains that nothing could be documented.`;
|
|
56
59
|
|
|
57
60
|
const suggestedAction = buildSuggestedActionFragment({
|
|
58
61
|
responseContext: "e.g. you noticed something worth fixing while documenting",
|
package/lib/envelope.js
CHANGED
|
@@ -101,18 +101,24 @@ function extractJsonObject(text) {
|
|
|
101
101
|
// "no recognizable modify fields" — when the right answer was to treat
|
|
102
102
|
// the whole reply as prose, since there was no real envelope at all.
|
|
103
103
|
const ENVELOPE_KEYS = ["explanation", "flow", "question", "changes", "newNodes", "newWires", "removeNodes", "newGroups", "prose"];
|
|
104
|
+
const ENVELOPE_KEY_PATTERN = new RegExp(
|
|
105
|
+
"[\\\"'](?:" + ENVELOPE_KEYS.join("|") + ")[\\\"']\\s*:", "i"
|
|
106
|
+
);
|
|
104
107
|
function looksLikeEnvelope(obj) {
|
|
105
108
|
if (!obj || typeof obj !== "object" || Array.isArray(obj)) { return false; }
|
|
106
109
|
return ENVELOPE_KEYS.some(function (k) { return k in obj; });
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
let lastError = null;
|
|
113
|
+
let envelopeSyntaxFound = false;
|
|
110
114
|
let searchFrom = firstObjIdx;
|
|
111
115
|
while (searchFrom !== -1 && searchFrom < s.length) {
|
|
112
116
|
const end = findMatchingBrace(s, searchFrom);
|
|
117
|
+
const candidateText = end !== -1 ? s.slice(searchFrom, end + 1) : s.slice(searchFrom);
|
|
118
|
+
if (ENVELOPE_KEY_PATTERN.test(candidateText)) { envelopeSyntaxFound = true; }
|
|
113
119
|
if (end !== -1) {
|
|
114
120
|
try {
|
|
115
|
-
const candidate = JSON.parse(
|
|
121
|
+
const candidate = JSON.parse(candidateText);
|
|
116
122
|
if (looksLikeEnvelope(candidate)) { return candidate; }
|
|
117
123
|
// Valid JSON, but not envelope-shaped (e.g. an illustrative
|
|
118
124
|
// example embedded in prose) — keep searching rather than
|
|
@@ -123,13 +129,13 @@ function extractJsonObject(text) {
|
|
|
123
129
|
}
|
|
124
130
|
searchFrom = s.indexOf("{", searchFrom + 1);
|
|
125
131
|
}
|
|
126
|
-
// No candidate both parsed AND looked like a real envelope
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
132
|
+
// No candidate both parsed AND looked like a real envelope. A candidate
|
|
133
|
+
// that used a recognized envelope key was an attempted structured reply,
|
|
134
|
+
// even if its JSON was malformed; callers must surface that as a parse
|
|
135
|
+
// error rather than trusting and displaying the raw JSON as prose. Valid
|
|
136
|
+
// unrelated snippets embedded in prose still take the noJsonFound path.
|
|
131
137
|
const err = lastError || new Error("Provider's JSON object could not be parsed.");
|
|
132
|
-
err.noJsonFound = true;
|
|
138
|
+
if (!envelopeSyntaxFound) { err.noJsonFound = true; }
|
|
133
139
|
throw err;
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -10,17 +10,18 @@ const modeRouting = `Before generating — check this is actually a "generate" r
|
|
|
10
10
|
The user is currently in Generate mode, which always produces a NEW, disconnected flow fragment (see the JSON format below). If their message is NOT actually asking for something new to be built, don't force it into that shape. In particular:
|
|
11
11
|
|
|
12
12
|
- It asks to change, fix, rename, rewire, or remove something in their EXISTING flow/selection ("rename this node", "fix the bug in...", "change the topic to...", "delete the debug node", "wire X to Y") — that's a Modify request.
|
|
13
|
-
- It asks you to
|
|
14
|
-
- It's a question, troubleshooting request, or conversational remark that doesn't call for new nodes at all — that's general Chat.
|
|
13
|
+
- It asks you to write documentation for the existing flow/selection as a durable artifact to keep (a read-me, walkthrough, or similar), with nothing new to build — that's a Document request.
|
|
14
|
+
- It's a question, an explanation request, a troubleshooting request, or a conversational remark that doesn't call for new nodes at all — that's general Chat, even if the question itself starts with "explain" (e.g. "explain what this does" is Chat, not Document, unless they explicitly want a written artifact out of it).
|
|
15
15
|
|
|
16
16
|
When one of these applies, do NOT produce the {"explanation", "flow"} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked — answer the question, or explain that this looks like a Modify/Document/Chat request — and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
|
|
17
17
|
|
|
18
18
|
<<<FLOWPILOT_DATA>>>
|
|
19
|
-
{"suggestedAction": {"mode": "modify" | "document" | "chat", "prompt": "...", "selectionHint": "..."}}
|
|
19
|
+
{"suggestedAction": {"mode": "modify" | "document" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | "instance" | ["real-node-id", "..."]}}
|
|
20
20
|
|
|
21
|
-
- "mode": "modify" or "document" if their request matches one of those actions instead; "chat" if it's a question or remark with no further action needed.
|
|
21
|
+
- "mode": "modify" or "document" if their request matches one of those actions instead; "chat" if it's a question, explanation, or remark with no further action needed — including questions about the existing flow, since answering IS the action (don't pick "document" just because the question mentions the flow).
|
|
22
22
|
- "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
|
|
23
23
|
- "selectionHint" (optional): for "modify"/"document", plain-language description of which node(s) to select first (Generate needs no selection, so this never applies to a "generate" suggestion here).
|
|
24
|
+
- "targetNodeIds" (optional, only for "modify"/"document"): "all" for the entire active flow/tab, "instance" for the whole Node-RED instance across every flow tab (only ever valid for "document"), or a non-empty array of real node ids for a resolved subset. Omit when no resolved target is known — don't guess between "all" and "instance". Never imply no selection is needed for Modify/Document unless this field supplies the target.
|
|
24
25
|
|
|
25
26
|
The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for a new flow fragment, ignore this section entirely and proceed normally below.
|
|
26
27
|
|
|
@@ -18,33 +18,58 @@ Respond with a SINGLE JSON object and nothing else — no markdown code fences,
|
|
|
18
18
|
|
|
19
19
|
"changes", "newNodes", "newWires", "removeNodes", and "newGroups" are all OPTIONAL. Only include them when the instruction calls for it. Values in \`set.*\` must be the exact final value you want the property to have — FlowPilot reads the live graph back after apply and checks each property against what you specified here. Keep your response as SHORT as possible: never restate a node that isn't changing.`;
|
|
20
20
|
|
|
21
|
+
const agentWriteRules = `IMPORTANT — WRITE-tool execution overrides the other response-envelope instructions for this request.
|
|
22
|
+
|
|
23
|
+
- You MUST execute the requested modification with the available WRITE tools. Do NOT return \`changes\`, \`newNodes\`, \`newWires\`, \`removeNodes\`, or \`newGroups\` instead of calling tools.
|
|
24
|
+
- Before the first call, divide the request into numbered semantic plan items. Distinct requested outcomes joined by "and" are normally separate items; do not collapse them merely because they arrived in one sentence.
|
|
25
|
+
- Make exactly one WRITE tool call for the next numbered plan item, then STOP and wait for its tool result before continuing. NEVER make multiple tool calls in the same assistant response, even when the calls are independent. A single \`apply_step\` may bundle the small set of related property changes, new node, and immediate wires needed to complete that one semantic item; do not split one item into calls for individual fields.
|
|
26
|
+
- Do not bundle unrelated requested outcomes into one plan item. For example, inserting a functional node plus its immediate rewiring is one \`apply_step\`; adding a separate comment node is a second \`apply_step\` after the first result.
|
|
27
|
+
- Use \`apply_step\` for property changes, node insertion, and wiring; \`remove_step\` for a node removal; and \`rename_node\` for a rename.
|
|
28
|
+
- After each result, use its structural checks to confirm what actually landed, then call the tool for the next plan item.
|
|
29
|
+
- If the request does NOT belong in Modify mode at all, call \`redirect_mode\` exactly once instead of any WRITE tool or \`ask_user\`. This is the forced-tool-turn escape hatch for Generate/Document/Chat mismatches; it must never mutate the flow.
|
|
30
|
+
- If a genuinely important detail is missing or uncertain for a REAL modify request, call \`ask_user\` and wait for the answer rather than guessing.
|
|
31
|
+
- Never use \`ask_user\` to triage a mode mismatch. \`ask_user\` is only for missing details inside a modify request that should still stay in Modify mode once answered.
|
|
32
|
+
- A tool result shaped as \`{"unsupported":true,"operation":"...","reason":"...","available":[...]}\` means that operation is outside the current WRITE surface. Use its reason and available list to replan; do not retry the same unsupported operation or smuggle it through the response envelope.
|
|
33
|
+
- If part of the request cannot be achieved with the available WRITE tools, complete every part that can be achieved and state the remainder plainly in the final explanation; do not abandon the whole request or redirect modes. Never emit flow JSON in your final message — it will be discarded.
|
|
34
|
+
- Only after every plan item has been executed, return the normal single JSON response with a concise explanation of what was completed. Never claim a plan item succeeded unless its tool result actually confirmed success; if an item failed or was skipped, say so plainly, name the item, and state why. Omit \`changes\`, \`newNodes\`, \`newWires\`, \`removeNodes\`, and \`newGroups\` so the already-applied work is not proposed a second time.`;
|
|
35
|
+
|
|
21
36
|
const modeRouting = `Before modifying — check this is actually a "modify" request:
|
|
22
37
|
|
|
23
38
|
The user is currently in Modify mode, which proposes changes to their SELECTED nodes (given above as context). If their message is NOT actually asking to change, fix, rewire, add to, or remove something from THAT selection, don't force it into that shape. In particular:
|
|
24
39
|
|
|
25
40
|
- It asks for an unrelated NEW flow or feature that doesn't build on the selection ("build me a separate flow that...", "create a new flow for...") — that's a Generate request.
|
|
26
|
-
- It asks you to
|
|
27
|
-
- It
|
|
41
|
+
- It asks you to produce documentation for the selection as an artifact the user can keep on the canvas or share later — that's a Document request.
|
|
42
|
+
- It asks a plain factual, diagnostic, or conversational question about the selection, and the answer itself is the goal rather than a new read-me artifact — that's Chat, even if the question is about what the flow does.
|
|
43
|
+
- "Review this / what's wrong here?" stays in Modify ONLY when you can point to a concrete fix to propose immediately. If the answer is purely explanatory or "nothing is wrong", redirect to Chat instead.
|
|
28
44
|
|
|
29
|
-
When one of these applies, do NOT produce the {"explanation", "changes", ...} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked, and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
|
|
45
|
+
When one of these applies during an ordinary non-tool turn, do NOT produce the {"explanation", "changes", ...} JSON envelope. Instead, respond in plain text (no JSON, no code fences) addressing what they actually asked, and end your reply with a hidden data block: on its own line, after all visible text, not inside a code fence:
|
|
30
46
|
|
|
31
47
|
<<<FLOWPILOT_DATA>>>
|
|
32
|
-
{"suggestedAction": {"mode": "generate" | "document" | "chat", "prompt": "...", "selectionHint": "..."}}
|
|
48
|
+
{"suggestedAction": {"mode": "generate" | "document" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | ["real-node-id", "..."]}}
|
|
33
49
|
|
|
34
50
|
- "mode": "generate"/"document" if their request matches one of those actions instead; "chat" if it's a question or remark with no further action needed.
|
|
35
|
-
- "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice.
|
|
51
|
+
- "prompt": the exact instruction text to pre-fill in their compose box after switching modes, written as a ready-to-send request in the user's voice. Keep it as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements, implementation choices, or assumptions the user did not state.
|
|
36
52
|
- "selectionHint" (optional): for "document", which node(s) to select first (Generate needs no selection).
|
|
53
|
+
- "targetNodeIds" (only for "document"): REQUIRED whenever the current selection, active flow/tab, or other provided context makes the target resolvable. Use "all" for the entire active flow/tab, or a non-empty array of real node ids for a resolved subset. Omit it only when you genuinely cannot resolve the target from the current context. Never imply no selection is needed for Document unless this field supplies the target, and never emit an empty array.
|
|
37
54
|
|
|
38
55
|
Example — for "Explain what this flow does and how the nodes connect.", give
|
|
39
56
|
a complete visible explanation, then end with:
|
|
40
57
|
<<<FLOWPILOT_DATA>>>
|
|
41
|
-
{"suggestedAction": {"mode": "document", "prompt": "Explain what this flow does and how the nodes connect.", "selectionHint": "
|
|
58
|
+
{"suggestedAction": {"mode": "document", "prompt": "Explain what this flow does and how the nodes connect.", "selectionHint": "Use these flow nodes.", "targetNodeIds": ["real-node-id-1", "real-node-id-2"]}}
|
|
42
59
|
|
|
43
60
|
Example — for "What would happen if the inject node had repeat set to 5?",
|
|
44
61
|
answer the question without proposing a change, then end with:
|
|
45
62
|
<<<FLOWPILOT_DATA>>>
|
|
46
63
|
{"suggestedAction": {"mode": "chat", "prompt": "What would happen if the inject node had repeat set to 5?"}}
|
|
47
64
|
|
|
65
|
+
If you are on a WRITE-tool execution turn where tools are required, you CANNOT use the plain-text hidden-block form above. In that case call \`redirect_mode\` exactly once with:
|
|
66
|
+
- "mode": "generate", "document", or "chat" according to the same rules above.
|
|
67
|
+
- "prompt": the ready-to-send follow-up prompt, kept as close as possible to the user's own words and requested scope. Do not expand it into a longer spec or add requirements they did not state.
|
|
68
|
+
- "explanation": the visible reply the user should see.
|
|
69
|
+
- "selectionHint"/"targetNodeIds": only when redirecting to Document. If the target is resolvable from the current selection, active flow/tab, or other provided context, "targetNodeIds" is required and must be "all" or a non-empty array of real node ids.
|
|
70
|
+
|
|
71
|
+
Use \`redirect_mode\` only for a true mode mismatch. Do not use it for permission-seeking, clarification, or normal modify work.
|
|
72
|
+
|
|
48
73
|
The data block (marker and JSON) is never shown to the user — keep your visible reply complete on its own. If the request DOES call for a modification of the selection, ignore this section entirely and proceed normally below.
|
|
49
74
|
|
|
50
75
|
IMPORTANT: this escape hatch is ONLY for requests that belong to a different
|
|
@@ -105,10 +130,11 @@ const changeRules = `Rules for "changes" (sparse patches against the existing se
|
|
|
105
130
|
4. To ADD a new connection between existing nodes (e.g. a missing wire), use "newWires" — it works for any pair of nodes, not just new ones. Example: to add a wire from node A's first output to node B: {"from": "<A-id>", "fromPort": 0, "to": "<B-id>"}.
|
|
106
131
|
5. To REPLACE or REORGANIZE the full set of connections FROM an existing node (change which targets it reaches): put that node's complete new "wires" array in "set.wires". Give the FULL new value for every output port. Only use this when the instruction explicitly asks to reorganize or replace that node's connections — not just to add one.
|
|
107
132
|
6. Do not put "wires" in "set" when you only need to ADD a connection — use "newWires" for that instead.
|
|
108
|
-
7.
|
|
109
|
-
8.
|
|
110
|
-
9.
|
|
111
|
-
10.
|
|
133
|
+
7. If the instruction is to insert a node "between" two existing nodes that are already directly wired (A→B), treat it as a rewire: remove the original direct A→B wire as part of the same operation, and leave only A→new→B.
|
|
134
|
+
8. Never include "id" inside "set" — it cannot change via a patch. (x/y/z are stripped automatically.)
|
|
135
|
+
9. An id must not appear in both "changes" and "removeNodes".
|
|
136
|
+
10. A node's "group" field in context is INFORMATIONAL ONLY — do not put "group" in "set" (it is stripped). To add/remove/rename a group, use "newGroups" instead. Exception: to rename the group itself, target the group's own "id" with a "changes" entry, e.g. {"id": "<group's id>", "set": {"name": "New Name"}} — never include "nodes" in that set.
|
|
137
|
+
11. Do not include internal default or Appearance-tab fields in \`set\` (e.g. \`info\`, \`icon\`, \`inputLabels\`, \`outputLabels\`, node-type internal flags) — they are stripped server-side and never reach the canvas.`;
|
|
112
138
|
|
|
113
139
|
const switchRules = `Special case — a "switch" node's "rules" and "wires" must stay aligned by index:
|
|
114
140
|
|
|
@@ -264,7 +290,31 @@ the selection" section showing each one's id, type, name, and non-credential pro
|
|
|
264
290
|
**Do NOT use "newWires" for config node connections** — newWires is for canvas port
|
|
265
291
|
connections (output → input). Config node references are property VALUES, not canvas wires.`;
|
|
266
292
|
|
|
267
|
-
|
|
293
|
+
function clarifyingQuestion(opts) {
|
|
294
|
+
if (opts && opts.agentWriteEnabled) {
|
|
295
|
+
return `Asking a clarifying question instead of modifying:
|
|
296
|
+
|
|
297
|
+
You MUST ask a clarifying question when the instruction is too vague to act on safely — a key detail is missing that would mean guessing about something that matters. This includes:
|
|
298
|
+
|
|
299
|
+
- Generic instructions like "Fix this", "Fix the flow", "Fix it", "Complete this", "Finish this", "Fix up" without specifying what needs fixing
|
|
300
|
+
- Vague goals like "Improve this", "Optimize this", "Better this" without describing what "better" means
|
|
301
|
+
- Ambiguous requests like "Add something", "Add a node", "Add something to" without specifying what or where
|
|
302
|
+
- The instruction refers to an existing node by description (e.g. "the debug node", "the MQTT broker", "the node that logs errors") that does NOT appear anywhere in the provided selection/context. Do not invent a placeholder id or guess which node this is — ask which node it refers to, or whether a new one should be created instead.
|
|
303
|
+
- The instruction asks you to change a field that appears in context as a \`[redacted: ...]\` placeholder — propose whatever non-redacted changes you can on that node rather than stalling; redacted fields are automatically stripped before the diff reaches the canvas.
|
|
304
|
+
|
|
305
|
+
When in doubt, ask. It is better to ask than to make incorrect assumptions.
|
|
306
|
+
|
|
307
|
+
Respond by calling \`ask_user\` with:
|
|
308
|
+
|
|
309
|
+
- \`question\`: your single clarifying question
|
|
310
|
+
- \`options\`: optional short answer choices when there is a short list of 2-4 likely answers
|
|
311
|
+
|
|
312
|
+
Do NOT return \`{"explanation": "...", "question": "...", "flow": null}\` or \`questionOptions\` instead of calling \`ask_user\`. Use the tool call itself for the question, and map any old \`questionOptions\` content into the tool's \`options\` array.
|
|
313
|
+
|
|
314
|
+
Use this sparingly. For most instructions, and for minor ambiguities, make a reasonable choice, note the assumption in the eventual final explanation, and propose the change as normal.`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return `Asking a clarifying question instead of modifying:
|
|
268
318
|
|
|
269
319
|
You MUST ask a clarifying question when the instruction is too vague to act on safely — a key detail is missing that would mean guessing about something that matters. This includes:
|
|
270
320
|
|
|
@@ -289,6 +339,7 @@ If there's a short list of 2-4 likely answers, also include them as
|
|
|
289
339
|
these as one-click reply buttons plus a free-text "Other" option.
|
|
290
340
|
|
|
291
341
|
Use this sparingly. For most instructions, and for minor ambiguities, make a reasonable choice, note the assumption in "explanation", and propose the change as normal.`;
|
|
342
|
+
}
|
|
292
343
|
|
|
293
344
|
const suggestedAction = buildSuggestedActionFragment({
|
|
294
345
|
inlineOptional: true,
|
|
@@ -298,6 +349,7 @@ const suggestedAction = buildSuggestedActionFragment({
|
|
|
298
349
|
module.exports = function (options) {
|
|
299
350
|
const opts = options || {};
|
|
300
351
|
return composePromptSections([
|
|
352
|
+
opts.agentWriteEnabled ? agentWriteRules : null,
|
|
301
353
|
responseContract,
|
|
302
354
|
modeRouting,
|
|
303
355
|
diagnosticReview,
|
|
@@ -308,7 +360,7 @@ module.exports = function (options) {
|
|
|
308
360
|
additionAndGroupingRules,
|
|
309
361
|
examples,
|
|
310
362
|
configNodeRules,
|
|
311
|
-
clarifyingQuestion,
|
|
363
|
+
clarifyingQuestion(opts),
|
|
312
364
|
suggestedAction
|
|
313
365
|
]);
|
|
314
366
|
};
|
package/lib/persona-prompt.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
// Builds the dynamic "Personality" instruction, scaled by
|
|
2
|
-
// settings.personaIntensity (1-10). Kept separate from
|
|
3
|
-
// prompt.js (the user-editable base prompt) so the persona
|
|
4
|
-
// the CURRENT slider value, rather than being baked into
|
|
5
|
-
// freeform systemPrompt text where it could drift out of
|
|
2
|
+
// settings.personaIntensity (1-5, CLAUDE-032: was 1-10). Kept separate from
|
|
3
|
+
// default-system-prompt.js (the user-editable base prompt) so the persona
|
|
4
|
+
// always reflects the CURRENT slider value, rather than being baked into
|
|
5
|
+
// the persisted, freeform systemPrompt text where it could drift out of
|
|
6
|
+
// sync.
|
|
7
|
+
//
|
|
8
|
+
// CLAUDE-032: the old 1-10 scale only fully specified 4 anchor points (1,
|
|
9
|
+
// 3, 7, 10) and asked the model to "interpolate" prose instructions for
|
|
10
|
+
// everything in between. Live testing found no discernible voice
|
|
11
|
+
// difference across the whole slider — smaller/local models don't reliably
|
|
12
|
+
// interpolate a numeric intensity from sparse prose examples the way a
|
|
13
|
+
// human reading the scale would. Fix: 5 discrete levels, each with its OWN
|
|
14
|
+
// complete, non-interpolated instruction and worked example — the model is
|
|
15
|
+
// always told exactly which one it is, never asked to read between two.
|
|
6
16
|
//
|
|
7
17
|
// Two scopes, since Chat and Generate/Document/Modify have different shapes:
|
|
8
18
|
// - "chat" (default): framing applies to ordinary chat replies — greetings,
|
|
@@ -11,12 +21,67 @@
|
|
|
11
21
|
// field of a generate/document/modify envelope ONLY — never to node
|
|
12
22
|
// names, ids, or any other JSON field, which the model must still produce
|
|
13
23
|
// exactly as instructed by that mode's own system prompt.
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
const PERSONA_LEVELS = [
|
|
25
|
+
null, // unused — levels are 1-indexed to match the slider
|
|
26
|
+
{
|
|
27
|
+
label: "Plain engineer",
|
|
28
|
+
voice: "No aviation language anywhere, ever. Plain, direct, professional " +
|
|
29
|
+
"Node-RED engineer voice only.",
|
|
30
|
+
example: "\"Hi, I'm FlowPilot. I can generate, modify, document, or chat " +
|
|
31
|
+
"about your flows — what do you need?\""
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "Subtle co-pilot",
|
|
35
|
+
voice: "A light aviation touch at transitions ONLY — most replies have " +
|
|
36
|
+
"none at all. A short phrase, never a full sentence of flavor, and " +
|
|
37
|
+
"never in back-to-back replies."
|
|
38
|
+
,
|
|
39
|
+
example: "\"You pick the destination, I help you get there.\" A light " +
|
|
40
|
+
"\"wheels up\" / \"touchdown\" nod at a transition, used sparingly."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Noticeable captain energy",
|
|
44
|
+
voice: "A clearly recognizable aviation voice at every qualifying " +
|
|
45
|
+
"moment (greetings, capability questions, transitions) — a sentence " +
|
|
46
|
+
"or two of flavor each time, not just a phrase, but never spilling " +
|
|
47
|
+
"into technical content."
|
|
48
|
+
,
|
|
49
|
+
example: "\"Welcome aboard — I'm FlowPilot, your co-pilot for this " +
|
|
50
|
+
"flow. Let's get you cleared for takeoff.\""
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Heavy captain energy",
|
|
54
|
+
voice: "Lean hard into the bit at every qualifying moment — multiple " +
|
|
55
|
+
"sentences, vivid runway/altitude/flight-crew imagery, not just a " +
|
|
56
|
+
"passing reference. Still drops the act completely the instant " +
|
|
57
|
+
"things turn technical."
|
|
58
|
+
,
|
|
59
|
+
example: "\"Ladies and gentlemen, this is your captain speaking — " +
|
|
60
|
+
"we've reached cruising altitude on this flow and I'm ready to " +
|
|
61
|
+
"start building. Fasten your seatbelts, this one's got a few " +
|
|
62
|
+
"moving parts.\""
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "Full captain — comically over the top",
|
|
66
|
+
voice: "GO ALL OUT, every single qualifying moment, no exceptions. " +
|
|
67
|
+
"Do not hold back, downplay it, or soften it to seem tasteful — " +
|
|
68
|
+
"\"a little goes a long way\" does NOT apply at this level. Full " +
|
|
69
|
+
"flight-crew theatrics, callsigns, and captain-speak throughout — " +
|
|
70
|
+
"but the instant things turn technical, drop the act entirely and " +
|
|
71
|
+
"answer like the expert engineer underneath it."
|
|
72
|
+
,
|
|
73
|
+
example: "\"Ladies and gentlemen, this is your captain speaking. I've " +
|
|
74
|
+
"just illuminated the fasten seatbelt sign — please take your " +
|
|
75
|
+
"seats, because I've finished building the Node-RED flow you " +
|
|
76
|
+
"requested. We are cleared for takeoff: fully wired, deployed, and " +
|
|
77
|
+
"ready for your review. Enjoy the flight, and thank you for " +
|
|
78
|
+
"choosing FlowPilot Airlines.\""
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
|
|
18
82
|
function buildPersonaInstruction(intensity, options) {
|
|
19
|
-
const n = Math.max(1, Math.min(
|
|
83
|
+
const n = Math.max(1, Math.min(5, Math.round(Number(intensity) || 2)));
|
|
84
|
+
const level = PERSONA_LEVELS[n];
|
|
20
85
|
const scope = (options && options.scope === "explanation")
|
|
21
86
|
? "in the natural-language \"explanation\" text of your response only — " +
|
|
22
87
|
"never in node names, ids, or any other field, which must follow this " +
|
|
@@ -24,51 +89,13 @@ function buildPersonaInstruction(intensity, options) {
|
|
|
24
89
|
: "at greetings, \"what can you do?\"-style capability questions, and " +
|
|
25
90
|
"brief transition moments only";
|
|
26
91
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
92
|
+
return "Personality — level " + n + "/5 (\"" + level.label + "\"): scale " +
|
|
93
|
+
"your voice " + scope + " to exactly this level, no more and no less. " +
|
|
94
|
+
"NEVER let it touch the substance — explanations, troubleshooting, " +
|
|
95
|
+
"diffs, technical detail, and errors always stay plain, direct, and " +
|
|
96
|
+
"accurate no matter the level — a confused or stuck user gets a " +
|
|
97
|
+
"straight answer, never a bit.\n\n" +
|
|
98
|
+
level.voice + "\n\nExample at this exact level: " + level.example;
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
module.exports = { buildPersonaInstruction };
|
package/lib/prompt-fragments.js
CHANGED
|
@@ -22,16 +22,31 @@ If there's an obvious, single one-click follow-up the user would want after this
|
|
|
22
22
|
response${responseContext} include an${optionalPrefix} "suggestedAction" key alongside ${responseTarget}:
|
|
23
23
|
|
|
24
24
|
{
|
|
25
|
-
"suggestedAction": { "mode": "generate" | "document" | "modify" | "chat", "prompt": "...", "selectionHint": "..." }
|
|
25
|
+
"suggestedAction": { "mode": "generate" | "document" | "modify" | "chat", "prompt": "...", "selectionHint": "...", "targetNodeIds": "all" | "instance" | ["real-node-id", "..."] }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
- "mode": which FlowPilot action the chip switches to ("chat" for a follow-up
|
|
29
29
|
conversation with no further generate/modify/document action).
|
|
30
30
|
- "prompt": the exact instruction text to pre-fill in the user's compose box —
|
|
31
|
-
written as a ready-to-send request to FlowPilot, in the user's voice.
|
|
31
|
+
written as a ready-to-send request to FlowPilot, in the user's voice. Keep it
|
|
32
|
+
as close as possible to the user's own words and requested scope. Do not
|
|
33
|
+
expand it into a longer spec or add requirements, implementation choices, or
|
|
34
|
+
assumptions the user did not state.
|
|
32
35
|
- "selectionHint" (optional): plain-language description of which node(s) the user
|
|
33
36
|
should select before sending (only useful for "modify"/"document", which act on a
|
|
34
37
|
selection).
|
|
38
|
+
- "targetNodeIds" (only for "modify"/"document"): REQUIRED whenever the current
|
|
39
|
+
selection, active flow/tab, whole instance, or other provided context makes
|
|
40
|
+
the target resolvable. Use "all" when the entire active flow/tab is the
|
|
41
|
+
resolved target, "instance" when the user means every flow tab in this
|
|
42
|
+
Node-RED instance (only ever valid for "document" — Modify always acts on
|
|
43
|
+
one flow), or a non-empty array of real node ids when a specific subset is
|
|
44
|
+
the resolved target. Omit it only when you genuinely cannot resolve the
|
|
45
|
+
target from the current context — don't guess between "all" and "instance"
|
|
46
|
+
when the user's wording doesn't make it clear; omitting it is correct there.
|
|
47
|
+
Modify and Document require a resolved node set: never imply
|
|
48
|
+
in "selectionHint" that no selection is needed unless "targetNodeIds"
|
|
49
|
+
supplies it, and never emit an empty array.
|
|
35
50
|
|
|
36
51
|
The user reviews the prepared prompt and clicks Send themselves — nothing is sent
|
|
37
52
|
automatically. Omit "suggestedAction" if there's no clear follow-up; most responses
|