@flowfuse/nr-assistant 0.3.1-8f3f1da-202507071743.0 → 0.3.1-92d1581-202507160925.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/completions.html +406 -0
- package/index.html +16 -17
- package/index.js +7 -242
- package/lib/assistant.js +667 -0
- package/lib/completions/Labeller.js +56 -0
- package/lib/flowGraph.js +65 -0
- package/lib/settings.js +18 -0
- package/lib/utils.js +21 -0
- package/package.json +11 -6
- package/resources/sharedUtils.js +70 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class CompletionsLabeller {
|
|
2
|
+
constructor ({ inputFeatureLabels, classifierLabels, nodeLabels }) {
|
|
3
|
+
this.inputFeatureLabels = inputFeatureLabels
|
|
4
|
+
this.classifierLabels = classifierLabels
|
|
5
|
+
this.nodeLabels = nodeLabels
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
ohe (node) {
|
|
9
|
+
return this.inputFeatureLabels.map(cat => (cat === node ? 1 : 0))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
countNodes (sequence) {
|
|
13
|
+
return this.nodeLabels.map(cat => sequence.filter(n => n === cat).length)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Encode a sequence of nodes into a feature vector.
|
|
18
|
+
* @param {string[]} userInput - Array of node type names.
|
|
19
|
+
* @returns {number[]} Encoded feature vector.
|
|
20
|
+
*/
|
|
21
|
+
encode_sequence (userInput) {
|
|
22
|
+
const inputNode = userInput[0]
|
|
23
|
+
const recentNode = userInput[userInput.length - 1]
|
|
24
|
+
const sequenceLength = userInput.length
|
|
25
|
+
|
|
26
|
+
const inputOhe = this.ohe(inputNode)
|
|
27
|
+
const recentOhe = this.ohe(recentNode)
|
|
28
|
+
const counts = this.countNodes(userInput)
|
|
29
|
+
|
|
30
|
+
// Concatenate all features (order must match training)
|
|
31
|
+
// [sequence_length, ...input_ohe, ...recent_ohe, ...counts]
|
|
32
|
+
return [
|
|
33
|
+
sequenceLength,
|
|
34
|
+
...inputOhe,
|
|
35
|
+
...recentOhe,
|
|
36
|
+
...counts
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Decode model predictions into human-readable labels.
|
|
42
|
+
* @param {Float32Array} predictions - Array of model predictions (probabilities).
|
|
43
|
+
* @param {number} [topN=5] - Number of top predictions to return.
|
|
44
|
+
* @returns {{ className: string, confidence: number, classIndex: number }[]}
|
|
45
|
+
*/
|
|
46
|
+
decode_predictions (predictions, topN = 5) {
|
|
47
|
+
return [...predictions]
|
|
48
|
+
.map((confidence, classIndex) => {
|
|
49
|
+
return { confidence, classIndex, className: this.classifierLabels[classIndex] }
|
|
50
|
+
}).sort((a, b) => b.confidence - a.confidence)
|
|
51
|
+
.slice(0, topN) // Get top N predictions
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports.CompletionsLabeller = CompletionsLabeller
|
|
56
|
+
module.exports.default = CompletionsLabeller
|
package/lib/flowGraph.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function buildReverseGraph (flow) {
|
|
2
|
+
const reverseGraph = new Map()
|
|
3
|
+
for (const node of flow) {
|
|
4
|
+
const outputs = (node && node.wires && node.wires.flat()) || []
|
|
5
|
+
for (const output of outputs) {
|
|
6
|
+
if (!reverseGraph.has(output)) {
|
|
7
|
+
reverseGraph.set(output, [])
|
|
8
|
+
}
|
|
9
|
+
reverseGraph.get(output).push(node.id) // output is downstream, node.id is upstream
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return reverseGraph
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find the longest upstream path in a flow graph.
|
|
17
|
+
*
|
|
18
|
+
* Interesting parts:
|
|
19
|
+
* - Uses DFS (depth-first search) to find the longest upstream path
|
|
20
|
+
* - Handles circular references by using a Set to track visited nodes
|
|
21
|
+
* @param {Array} graph - The reverse graph where keys are node IDs and values are arrays of upstream node IDs.
|
|
22
|
+
* @param {string} startId - The ID of the node from which to start the search.
|
|
23
|
+
* @returns {Array} - An array of node IDs representing the longest upstream path.
|
|
24
|
+
* @throws {Error} - Throws an error if a circular reference is detected.
|
|
25
|
+
*/
|
|
26
|
+
function findLongestUpstreamPath (graph, startId) {
|
|
27
|
+
const visited = new Set()
|
|
28
|
+
function dfs (current, path) {
|
|
29
|
+
if (visited.has(current)) {
|
|
30
|
+
throw new Error(`Circular reference detected at node ${current}`)
|
|
31
|
+
}
|
|
32
|
+
visited.add(current)
|
|
33
|
+
let maxSubPath = []
|
|
34
|
+
for (const parent of graph.get(current) || []) {
|
|
35
|
+
const subPath = dfs(parent, path)
|
|
36
|
+
if (subPath.length > maxSubPath.length) {
|
|
37
|
+
maxSubPath = subPath
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
visited.delete(current)
|
|
41
|
+
|
|
42
|
+
return [...maxSubPath, current]
|
|
43
|
+
}
|
|
44
|
+
return dfs(startId, [])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the longest upstream path in a flow graph.
|
|
49
|
+
* @param {Array<{id: string, type: string, wires: Array<Array<string>>, [key: string]: any}>} flow - The flow graph.
|
|
50
|
+
* @param {string} finalNodeId - The ID of the final node.
|
|
51
|
+
* @returns {Array} - An array of nodes representing the longest upstream path.
|
|
52
|
+
*/
|
|
53
|
+
function getLongestUpstreamPath (flow, finalNodeId) {
|
|
54
|
+
const reverseGraph = buildReverseGraph(flow)
|
|
55
|
+
const idToNodeLookup = Object.fromEntries(flow.map(node => [node.id, node]))
|
|
56
|
+
const pathIds = findLongestUpstreamPath(reverseGraph, finalNodeId)
|
|
57
|
+
const result = pathIds.map(id => idToNodeLookup[id])
|
|
58
|
+
return result.filter(node => node !== undefined && node.id !== finalNodeId)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
getLongestUpstreamPath,
|
|
63
|
+
buildReverseGraph,
|
|
64
|
+
findLongestUpstreamPath
|
|
65
|
+
}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
getSettings: (RED) => {
|
|
3
|
+
const assistantSettings = (RED.settings.flowforge && RED.settings.flowforge.assistant) || {}
|
|
4
|
+
if (assistantSettings.enabled !== true) {
|
|
5
|
+
assistantSettings.enabled = false
|
|
6
|
+
assistantSettings.completions = null // if the assistant is not enabled, completions should not be enabled
|
|
7
|
+
}
|
|
8
|
+
assistantSettings.mcp = assistantSettings.mcp || {
|
|
9
|
+
enabled: true // default to enabled
|
|
10
|
+
}
|
|
11
|
+
assistantSettings.completions = assistantSettings.completions || {
|
|
12
|
+
enabled: true, // default to enabled
|
|
13
|
+
modelUrl: null,
|
|
14
|
+
vocabularyUrl: null
|
|
15
|
+
}
|
|
16
|
+
return assistantSettings
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const tryJsonParse = (str, defaultValue = undefined) => {
|
|
2
|
+
try {
|
|
3
|
+
return JSON.parse(str)
|
|
4
|
+
} catch (e) {
|
|
5
|
+
return defaultValue
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const hasProperty = (obj, prop) => {
|
|
10
|
+
return isObject(obj) && Object.prototype.hasOwnProperty.call(obj, prop)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const isObject = (obj) => {
|
|
14
|
+
return obj !== null && typeof obj === 'object' && !Array.isArray(obj)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
tryJsonParse,
|
|
19
|
+
hasProperty,
|
|
20
|
+
isObject
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowfuse/nr-assistant",
|
|
3
|
-
"version": "0.3.1-
|
|
3
|
+
"version": "0.3.1-92d1581-202507160925.0",
|
|
4
4
|
"description": "FlowFuse Node-RED assistant plugin",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "mocha --exit \"test/**/*.test.js\"",
|
|
8
8
|
"lint": "eslint -c .eslintrc --ext js,html \"*.js\" \"*.html\"",
|
|
9
9
|
"lint:fix": "eslint -c .eslintrc --ext js,html \"*.js\" \"*.html\" --fix"
|
|
10
10
|
},
|
|
@@ -29,20 +29,25 @@
|
|
|
29
29
|
"node-red": {
|
|
30
30
|
"version": ">=2.2.0",
|
|
31
31
|
"plugins": {
|
|
32
|
-
"flowfuse-nr-assistant": "index.js"
|
|
32
|
+
"flowfuse-nr-assistant": "index.js",
|
|
33
|
+
"ff-assistant-completions": "completions.html"
|
|
33
34
|
}
|
|
34
35
|
},
|
|
35
36
|
"engines": {
|
|
36
37
|
"node": ">=16.x"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
-
"got": "^11.8.6"
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.13.1",
|
|
41
|
+
"got": "^11.8.6",
|
|
42
|
+
"onnxruntime-web": "^1.22.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"eslint": "^8.48.0",
|
|
44
46
|
"eslint-config-standard": "^17.1.0",
|
|
45
47
|
"eslint-plugin-html": "7.1.0",
|
|
46
|
-
"eslint-plugin-no-only-tests": "^3.1.0"
|
|
48
|
+
"eslint-plugin-no-only-tests": "^3.1.0",
|
|
49
|
+
"mocha": "^11.6.0",
|
|
50
|
+
"should": "^13.2.3",
|
|
51
|
+
"sinon": "^18.0.0"
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FFA Assistant Utils
|
|
3
|
+
* Shared utility functions for the FlowFuse Assistant
|
|
4
|
+
* To import this in js backend code, use:
|
|
5
|
+
* const { cleanFlow } = require('flowfuse-nr-assistant/resources/sharedUtils.js')
|
|
6
|
+
* To import this in frontend code, use:
|
|
7
|
+
* <script src="/resources/@flowfuse/nr-assistant/sharedUtils.js"></script>
|
|
8
|
+
* To use this in the browser, you can access it via:
|
|
9
|
+
* FFAssistantUtils.cleanFlow(nodeArray)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
(function (root, factory) {
|
|
15
|
+
if (typeof module === 'object' && module.exports) {
|
|
16
|
+
// Node.js / CommonJS
|
|
17
|
+
module.exports = factory()
|
|
18
|
+
} else {
|
|
19
|
+
// Browser
|
|
20
|
+
root.FFAssistantUtils = root.FFAssistantUtils || {}
|
|
21
|
+
Object.assign(root.FFAssistantUtils, factory())
|
|
22
|
+
}
|
|
23
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
24
|
+
'use strict'
|
|
25
|
+
/**
|
|
26
|
+
* Cleans a single or an array nodes by removing internal properties and circular references.
|
|
27
|
+
* @param {Array<Object> | Object} flow - The node or array of nodes to clean
|
|
28
|
+
* @returns {{nodes: Array, totalNodeCount: number}} - The cleaned nodes and the total node count
|
|
29
|
+
*/
|
|
30
|
+
function cleanFlow (flow) {
|
|
31
|
+
if (!flow) return { flow: [], nodeCount: 0 }
|
|
32
|
+
const nodeArray = Array.isArray(flow) ? flow : [flow]
|
|
33
|
+
const nodes = [...nodeArray] // make a shallow copy of the array
|
|
34
|
+
let totalNodeCount = 0
|
|
35
|
+
const MAX_DEPTH = 10 // maximum depth to recurse into groups
|
|
36
|
+
const visited = new Set() // to avoid circular references
|
|
37
|
+
// the input is an array of node. each node is an object.
|
|
38
|
+
// if the .type is 'group' and it has a .nodes array, we need to clean those nodes as well (and any nested groups).
|
|
39
|
+
const recursiveClean = (node, depth = 0) => {
|
|
40
|
+
if (!node || typeof node !== 'object' || !node.id) {
|
|
41
|
+
return null // if the node is not an object or doesn't have an id, return null
|
|
42
|
+
}
|
|
43
|
+
if (visited.has(node.id) || depth > MAX_DEPTH) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
totalNodeCount += 1
|
|
47
|
+
visited.add(node.id)
|
|
48
|
+
const cleaned = { ...node }
|
|
49
|
+
delete cleaned._ // remove the internal _ property
|
|
50
|
+
delete cleaned._def // remove the definition
|
|
51
|
+
delete cleaned._config // remove the config
|
|
52
|
+
delete cleaned.validationErrors // remove validation errors
|
|
53
|
+
if (node.type === 'group') {
|
|
54
|
+
delete cleaned._childGroups // this can cause circular references
|
|
55
|
+
delete cleaned._parentGroup // this can cause circular references
|
|
56
|
+
cleaned.nodes = []
|
|
57
|
+
if (Array.isArray(node.nodes) && node.nodes.length > 0) {
|
|
58
|
+
cleaned.nodes = node.nodes.map(childNode => recursiveClean(childNode, depth + 1))
|
|
59
|
+
.filter(childNode => childNode !== null) // filter out any null nodes
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return cleaned
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
flow: nodes.map(node => recursiveClean(node)).filter(node => node !== null), // filter out any null nodes
|
|
66
|
+
nodeCount: totalNodeCount
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { cleanFlow }
|
|
70
|
+
}))
|