Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- [
- {
- "id": "5dc8d9f249f7cf93",
- "type": "group",
- "z": "b2a201c573251bb8",
- "name": "nr-assistant-self-hosted",
- "style": {
- "label": true
- },
- "nodes": [
- "90b85bbb169ebd01",
- "26a4798f99ba4c7f",
- "59c6c5bf27ebe683"
- ],
- "env": [
- {
- "name": "NODERED_URL",
- "value": "http://127.0.0.1:1880",
- "type": "str"
- },
- {
- "name": "NODERED_USER",
- "type": "cred"
- },
- {
- "name": "NODERED_PASS",
- "type": "cred"
- },
- {
- "name": "OPENAI_APIKEY",
- "type": "cred"
- }
- ],
- "x": 34,
- "y": 79,
- "w": 592,
- "h": 82
- },
- {
- "id": "90b85bbb169ebd01",
- "type": "http in",
- "z": "b2a201c573251bb8",
- "g": "5dc8d9f249f7cf93",
- "name": "",
- "url": "/admin/nr-assistant/:method",
- "method": "post",
- "upload": false,
- "skipBodyParsing": false,
- "swaggerDoc": "",
- "x": 200,
- "y": 120,
- "wires": [
- [
- "59c6c5bf27ebe683"
- ]
- ]
- },
- {
- "id": "26a4798f99ba4c7f",
- "type": "http response",
- "z": "b2a201c573251bb8",
- "g": "5dc8d9f249f7cf93",
- "name": "",
- "statusCode": "",
- "headers": {},
- "x": 550,
- "y": 120,
- "wires": []
- },
- {
- "id": "59c6c5bf27ebe683",
- "type": "function",
- "z": "b2a201c573251bb8",
- "g": "5dc8d9f249f7cf93",
- "name": "assistant",
- "func": "const request = msg.payload;\nconst flows = await getFlows();\n\nswitch (request.context.scope) {\n case \"node\":\n\n msg.payload =\n {\n \"data\": {\n ...await llmNodeFlow(request.prompt)\n }\n }\n break;\n\n case \"inline\":\n const flow = await flows.filter(f => f.id == request.transactionId.split(\"-\")[0]);\n switch (request.context.type) {\n case \"function\":\n msg.payload =\n {\n \"data\": {\n \"transactionId\": request.transactionId,\n \"data\": {\n ...await llmNodeInlineFunction(request.prompt, flow, request.context)\n }\n }\n }\n break;\n case \"json\":\n // not implemented\n msg.payload =\n {\n \"data\": {\n \"transactionId\": request.transactionId,\n \"data\": {\n \"json\": \"{\\n \\\"name\\\": \\\"example\\\",\\n \\\"enabled\\\": true\\n}\"\n }\n }\n }\n break;\n case \"css\":\n // not implemented\n msg.payload =\n {\n \"data\": {\n \"transactionId\": request.transactionId,\n \"data\": {\n \"css\": \".panel { display: grid; gap: 8px; }\"\n }\n }\n }\n break;\n case \"html\":\n // not implemented\n msg.payload =\n {\n \"data\": {\n \"transactionId\": request.transactionId,\n \"data\": {\n \"html\": \"<div id=\\\"app\\\">\\n <h3>{{ title }}</h3>\\n</div>\\n<script>\\nexport default { data(){ return { title: 'Hello' } } }\\n</script>\"\n }\n }\n }\n break;\n case \"sql\":\n // not implemented\n msg.payload =\n {\n \"data\": {\n \"transactionId\": request.transactionId,\n \"data\": {\n \"sql\": \"SELECT id, name FROM public.users WHERE active = true ORDER BY name;\"\n }\n }\n }\n break;\n }\n break;\n\n}\n\nreturn msg;\n\n\n\nasync function llmNodeFlow(prompt) {\n var messages = [\n {\n \"role\": \"system\",\n \"content\": \"You are a Node-RED expert and are asked to assist with generating code.\"\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ]\n var schema = {\n \"type\": \"object\",\n \"properties\": {\n \"flow\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^[0-9a-fA-F]{16}$\",\n \"minLength\": 16,\n \"maxLength\": 16,\n \"description\": \"Unique 16-character hex string\"\n },\n \"type\": {\n \"type\": \"string\"\n },\n \"z\": {\n \"type\": \"string\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"func\": {\n \"type\": \"string\"\n },\n \"outputs\": {\n \"type\": \"integer\"\n },\n \"noerr\": {\n \"type\": \"integer\"\n },\n \"initialize\": {\n \"type\": \"string\"\n },\n \"finalize\": {\n \"type\": \"string\"\n },\n \"libs\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"id\",\n \"type\",\n \"z\",\n \"name\",\n \"func\",\n \"outputs\",\n \"noerr\",\n \"initialize\",\n \"finalize\",\n \"libs\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\"flow\"],\n \"additionalProperties\": false\n }\n const response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${env.get(\"CREDENTIALS_OPENAI\")}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n max_completion_tokens: 3000,\n frequency_penalty: 0,\n presence_penalty: 0,\n model: \"gpt-5-mini\",\n reasoning_effort: \"minimal\",\n messages,\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"NodeRedFlow\",\n description: \"Author a Node-RED flow.\",\n strict: true,\n schema\n }\n }\n })\n });\n if (!response.ok) {\n const err = await response.json().catch(() => ({}));\n throw new Error(`OpenAI API error: ${response.status} ${response.statusText}\\n${JSON.stringify(err, null, 2)}`);\n }\n const data = await response.json();\n return JSON.parse(data.choices[0].message.content);\n}\n\nasync function llmNodeInlineFunction(prompt, flow, context) {\n var messages = [\n {\n \"role\": \"system\",\n \"content\": \"You are a Node-RED expert and are asked to assist with generating code.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"This is the current definition of the current node you need to provide code assistance on: \" + JSON.stringify(flow)\n },\n {\n \"role\": \"user\",\n \"content\": \"Here is some extra context: \" + JSON.stringify(context)\n },\n {\n \"role\": \"user\",\n \"content\": prompt\n }\n ]\n var schema = {\n \"type\": \"object\",\n \"properties\": {\n \"func\": {\n \"type\": \"string\",\n \"description\": \"Javascript code requested by the user.\"\n },\n \"outputs\": {\n \"type\": \"integer\"\n },\n \"node_modules\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"var\": {\n \"type\": \"string\"\n },\n \"module\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"var\",\n \"module\"\n ],\n \"additionalProperties\": false\n }\n }\n },\n \"required\": [\n \"func\",\n \"outputs\",\n \"node_modules\"\n ],\n \"additionalProperties\": false\n }\n\n const response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${env.get(\"CREDENTIALS_OPENAI\")}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n max_completion_tokens: 3000,\n frequency_penalty: 0,\n presence_penalty: 0,\n model: \"gpt-5-mini\",\n reasoning_effort: \"minimal\",\n messages,\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"NodeRedFlow\",\n description: \"Author a Node-RED flow.\",\n strict: true,\n schema\n }\n }\n })\n });\n if (!response.ok) {\n const err = await response.json().catch(() => ({}));\n throw new Error(`OpenAI API error: ${response.status} ${response.statusText}\\n${JSON.stringify(err, null, 2)}`);\n }\n const data = await response.json();\n return JSON.parse(data.choices[0].message.content);\n}\n\nasync function getFlows() {\n // would rather use RED.nodes.node(msg.payload.transactionId.split(\"-\")[0])\n const res = await fetch(`${env.get(\"NODERED_URL\")}/auth/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n client_id: 'node-red-admin',\n grant_type: 'password',\n scope: 'read',\n username: env.get(\"NODERED_USER\"),\n password: env.get(\"NODERED_PASS\")\n })\n });\n const { access_token, expires_in, token_type } = await res.json();\n const flows = await fetch(`${env.get(\"NODERED_URL\")}/flows`, {\n method: 'GET',\n headers: {\n 'Authorization': 'Bearer ' + access_token\n }\n });\n const flowsJson = await flows.json();\n return flowsJson;\n}",
- "outputs": 1,
- "timeout": 0,
- "noerr": 0,
- "initialize": "",
- "finalize": "",
- "libs": [
- {
- "var": "fetch",
- "module": "node-fetch"
- },
- {
- "var": "dayjs",
- "module": "dayjs"
- }
- ],
- "x": 420,
- "y": 120,
- "wires": [
- [
- "26a4798f99ba4c7f"
- ]
- ]
- }
- ]
Advertisement
Add Comment
Please, Sign In to add comment