@cpwc/node-red-contrib-ai-intent 3.1.0-alpha

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.
Files changed (70) hide show
  1. package/.eslintrc +49 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/node-red-contrib-ai-intent.iml +12 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/LICENSE +21 -0
  6. package/README.md +233 -0
  7. package/call-intent/icons/promotion-icon.svg +8 -0
  8. package/call-intent/index.html +114 -0
  9. package/call-intent/index.js +110 -0
  10. package/constants.js +31 -0
  11. package/database.js +9 -0
  12. package/examples/home-assistant-automation.json +167 -0
  13. package/examples/llm-chat-node-example.json +208 -0
  14. package/examples/openai-call-registered-intent-example.json +174 -0
  15. package/examples/openai-system-node-example.json +178 -0
  16. package/examples/openai-tool-node-example.json +120 -0
  17. package/examples/openai-user-node-exampe.json +234 -0
  18. package/geminiai-chat/geminiai-configuration/index.html +18 -0
  19. package/geminiai-chat/geminiai-configuration/index.js +7 -0
  20. package/geminiai-chat/icons/diamond.svg +8 -0
  21. package/geminiai-chat/icons/gemini-icon.svg +1 -0
  22. package/geminiai-chat/icons/gemini.svg +8 -0
  23. package/geminiai-chat/index.html +189 -0
  24. package/geminiai-chat/index.js +92 -0
  25. package/globalUtils.js +39 -0
  26. package/images/call_register_intent.jpeg +0 -0
  27. package/images/finally.jpg +0 -0
  28. package/images/set-config-node.gif +0 -0
  29. package/llm-chat/AzureOpenAIHelper.js +204 -0
  30. package/llm-chat/ChatGPTHelper.js +197 -0
  31. package/llm-chat/GeminiHelper.js +260 -0
  32. package/llm-chat/OllamaHelper.js +196 -0
  33. package/llm-chat/icons/bot-message-square.svg +1 -0
  34. package/llm-chat/icons/brain-circuit.svg +1 -0
  35. package/llm-chat/icons/chatgpt-icon.svg +7 -0
  36. package/llm-chat/index.html +205 -0
  37. package/llm-chat/index.js +73 -0
  38. package/llm-chat/platform-configuration/index.html +136 -0
  39. package/llm-chat/platform-configuration/index.js +16 -0
  40. package/localai-chat/icons/gem-icon.svg +1 -0
  41. package/localai-chat/icons/llama.svg +8 -0
  42. package/localai-chat/index.html +244 -0
  43. package/localai-chat/index.js +108 -0
  44. package/localai-chat/localai-configuration/index.html +18 -0
  45. package/localai-chat/localai-configuration/index.js +7 -0
  46. package/openai-chat/icons/chatgpt-icon.svg +7 -0
  47. package/openai-chat/index.html +196 -0
  48. package/openai-chat/index.js +58 -0
  49. package/openai-chat/openai-configuration/index.html +18 -0
  50. package/openai-chat/openai-configuration/index.js +7 -0
  51. package/openai-response/index.html +66 -0
  52. package/openai-response/index.js +154 -0
  53. package/openai-system/index.html +68 -0
  54. package/openai-system/index.js +28 -0
  55. package/openai-tool/index.html +57 -0
  56. package/openai-tool/index.js +50 -0
  57. package/openai-user/index.html +76 -0
  58. package/openai-user/index.js +26 -0
  59. package/package.json +49 -0
  60. package/register-intent/icons/register-icon.svg +8 -0
  61. package/register-intent/index.html +195 -0
  62. package/register-intent/index.js +72 -0
  63. package/register-intent/utils.js +10 -0
  64. package/utilities/chat-controller.js +249 -0
  65. package/utilities/chat-ledger.js +122 -0
  66. package/utilities/conversationHistory.js +68 -0
  67. package/utilities/format.js +94 -0
  68. package/utilities/gemini-controller.js +243 -0
  69. package/utilities/global-context.js +30 -0
  70. package/utilities/validateSchema.js +74 -0
@@ -0,0 +1,189 @@
1
+ <script type="text/javascript">
2
+ (() => {
3
+ const DYNAMIC_OPTIONS = [
4
+ { value: "auto", label: "Automatic" },
5
+ { value: "any", label: "Any" },
6
+ { value: "none", label: "None" }
7
+ ]
8
+ const removeDuplicates = (data) => {
9
+ const intents = {}
10
+ const tools = []
11
+
12
+ data.forEach((intent => {
13
+ if (intent.type === "OpenAI Tool") {
14
+ if (!intents[intent.name]) {
15
+ intents[intent.name] = true
16
+ tools.push(intent)
17
+ }
18
+ } else {
19
+ tools.push(intent)
20
+ }
21
+ }))
22
+ return tools
23
+ }
24
+
25
+ $.getJSON('registered-intents', function (data = RED.settings.callIntentRegistry) {
26
+ const tools = removeDuplicates(data)
27
+ window.__tools = getToolOptions(tools)
28
+ initialize()
29
+ });
30
+
31
+ const getToolOptions = (intents = []) => {
32
+ const options = intents.map(intent => {
33
+ const suffix = intent.type === "Register Intent" ? " (Registered Intent)" : ""
34
+ return { value: intent.id, label: `${intent.name}${suffix}` }
35
+ })
36
+
37
+ return [...DYNAMIC_OPTIONS, ...options]
38
+ }
39
+
40
+ RED.nodes.registerType("GeminiAI Chat", {
41
+ category: 'AI Intent',
42
+ color: 'rgba(255, 0, 119, .75)',
43
+ icon: "gemini-icon.svg",
44
+ defaults: {
45
+ name: { value: "" },
46
+ tool_choice: {
47
+ value: getToolOptions(RED.settings.callIntentRegistry),
48
+ },
49
+ conversation_id: { value: "" },
50
+ token: { value: "", type: "geminiai-configuration", required: false },
51
+ model: { value: "gemini-1.5-flash", required: true },
52
+ temperature: { value: .7, required: true },
53
+ max_tokens: { value: 1200, required: true },
54
+ top_p: { value: 1, required: true },
55
+ top_k: { value: 16, required: true },
56
+ },
57
+ inputs: 1,
58
+ outputs: 1,
59
+ paletteLabel: "Gemini (Deprecated)",
60
+ label: function () {
61
+ return this.name +" (Deprecated)" || "GeminiAI (Deprecated)";
62
+ },
63
+
64
+ oneditprepare: function (x) {
65
+ $("#node-input-temperature").typedInput({
66
+ type: "num",
67
+ })
68
+ $("#node-input-max_tokens").typedInput({
69
+ type: "num",
70
+ })
71
+ $("#node-input-top_p").typedInput({
72
+ type: "num",
73
+ })
74
+ $("#node-input-top_k").typedInput({
75
+ type: "num",
76
+ })
77
+
78
+ $.getJSON('registered-intents', function (data = RED.settings.callIntentRegistry) {
79
+ const tools = removeDuplicates(data)
80
+ window.__tools = getToolOptions(tools)
81
+
82
+ $("#node-input-tool_choice").typedInput({
83
+ types: [
84
+ {
85
+ value: "",
86
+ options: window.__tools
87
+ }
88
+ ]
89
+ })
90
+ });
91
+ }
92
+ });
93
+ })()
94
+
95
+ </script>
96
+
97
+ <script type="text/html" data-template-name="GeminiAI Chat">
98
+ <div class="form-row">
99
+ <label for="node-input-name"> Name</label>
100
+ <input type="text" id="node-input-name" placeholder="Name">
101
+ </div>
102
+ <div class="form-row">
103
+ <label for="node-input-token"> Token</label>
104
+ <input type="text" id="node-input-token" placeholder="0a1b2c3b4d5e6f">
105
+ </div>
106
+ <div class="form-row">
107
+ <label for="node-input-conversation_id"><i class="fa fa-tag"></i> Conversation Id</label>
108
+ <input type="text" id="node-input-conversation_id" placeholder="any arbitrary name">
109
+ </div>
110
+ <div class="form-row">
111
+ <label for="node-input-model"> Model</label>
112
+ <input type="text" id="node-input-model" placeholder="gpt-3.5-turbo">
113
+ </div>
114
+ <div class="form-row">
115
+ <label for="node-input-tool_choice"> Tool Choice</label>
116
+ <input type="text" id="node-input-tool_choice" placeholder="Automatic">
117
+ </div>
118
+ <div class="form-row">
119
+ <label for="node-input-temperature"> Temperature</label>
120
+ <input type="number" id="node-input-temperature" placeholder="0.7">
121
+ </div>
122
+ <div class="form-row">
123
+ <label for="node-input-max_tokens"> Max Tokens</label>
124
+ <input type="number" id="node-input-max_tokens" placeholder="1200">
125
+ </div>
126
+ <div class="form-row">
127
+ <label for="node-input-top_p"> Top P</label>
128
+ <input type="number" id="node-input-top_p" placeholder="0">
129
+ </div>
130
+ <div class="form-row">
131
+ <label for="node-input-top_k">Top K</label>
132
+ <input type="number" id="node-input-top_k" placeholder="16">
133
+ </div>
134
+
135
+
136
+ </script>
137
+
138
+ <script type="text/html" data-help-name="GeminiAI Chat">
139
+ <p>Calls Gemini and returns the response</p>
140
+
141
+ <h3>Important</h3>
142
+ <p>To use this node you need an API Key from Gemini. Add the API Key to the settings.js file in the node-red folder under
143
+ the functonGlobalContext section using the key "geminiaiAPIKey"</p>
144
+ <pre>
145
+ functionGlobalContext: {
146
+ geminiaiAPIKey: "Your Key Goes Here",
147
+ }
148
+ </pre>
149
+
150
+ Alternatively, you can add the token via the token configuration dropdown.
151
+ See the <a href="https://github.com/montaque22/node-red-contrib-ai-intent" target="_blank">Read Me</a> for more information
152
+
153
+ <h3>Inputs</h3>
154
+ <dl class="message-properties">
155
+ <dt class="optional">Conversation ID
156
+ <span class="property-type">string</span>
157
+ </dt>
158
+ <dd> By including this identifier, AI-Intent will save the conversation in the global context
159
+ and will pass the entire conversation to the LLM when any Chat node with the same conversation id
160
+ is triggered. This mean that you can have many Chat nodes in different flows with the same conversation id
161
+ and AI-Intent will ensure that conversation context is maintained between them. It should be noted that only
162
+ <b>one</b> System message will be maintained. As a result the flow with the current System node will
163
+ overwrite any system prompt that was previously saved.</dd>
164
+ </dl>
165
+ <dl class="message-properties">
166
+ <dt class="required">model
167
+ <span class="property-type">string</span>
168
+ </dt>
169
+ <dd> Required field that dictates the model to use when calling Gemini </dd>
170
+ </dl>
171
+
172
+ <h3>Details</h3>
173
+ <p>At minimum the User node should be used before this node. The OpenAI User node adds necessary
174
+ information to the msg object to allow the chat to work.</p>
175
+
176
+ <dl class="message-properties">
177
+ <dt class="required">Tool Choice
178
+ <span class="property-type">Dropdown</span>
179
+ </dt>
180
+ <dd> This setting controls how GPT uses the provided functions to respond.
181
+ If you want to force GPT to use a specific function, selected it from the dropdown.
182
+ <b>Automatic</b> lets GPT decides what is best (This is the default). <b>None</b> disables the use of functions.
183
+ to further optimize the API call, AI-Intent will not pass any of the tools
184
+ to save tokens if "None" is selected. Check documentation for more detail on
185
+ how tools work: <a href="https://ai.google.dev/gemini-api/docs/function-calling" target="_blank">Function Calling & Tools</a>
186
+ </dd>
187
+ </dl>
188
+
189
+ </script>
@@ -0,0 +1,92 @@
1
+ const { GEMINI_AI_KEY, TYPES } = require("../constants");
2
+ const { end } = require("../globalUtils");
3
+ const { GoogleGenerativeAI } = require("@google/generative-ai");
4
+ const { GlobalContext } = require("../utilities/global-context");
5
+ const { GeminiController } = require("../utilities/gemini-controller");
6
+
7
+ module.exports = function (RED) {
8
+ function GeminiAIChatHandlerNode(config) {
9
+ RED.nodes.createNode(this, config);
10
+ // Retrieve the config node with API token data.
11
+ this.token = RED.nodes.getNode(config.token);
12
+ const node = this;
13
+
14
+ this.on("input", function (msg, send, done = () => {}) {
15
+ const controller = new GeminiController(node, config, msg, RED);
16
+ const nodeDB = new GlobalContext(node);
17
+ const apiKey =
18
+ node.token?.api || nodeDB.getValueFromGlobalContext(GEMINI_AI_KEY);
19
+
20
+ send =
21
+ send ||
22
+ function () {
23
+ config.send.apply(node, arguments);
24
+ };
25
+
26
+ if (!apiKey) {
27
+ return end(
28
+ done,
29
+ `Api key missing for Gemini. Please add ${GEMINI_AI_KEY} key-value pair to the functionGlobalContext.
30
+ Or add it to the config within the GeminiAI-Chat node`
31
+ );
32
+ }
33
+
34
+ const { apiProperties, toolProperties } = controller;
35
+
36
+ const generationConfig = {
37
+ maxOutputTokens: apiProperties.max_tokens,
38
+ temperature: apiProperties.temperature,
39
+ topP: apiProperties.top_p,
40
+ topK: apiProperties.top_k,
41
+ };
42
+
43
+ const finalProps = {
44
+ ...apiProperties,
45
+ ...toolProperties,
46
+ };
47
+
48
+ // Access your API key as an environment variable (see "Set up your API key" above)
49
+ const genAI = new GoogleGenerativeAI(apiKey);
50
+
51
+ // ...
52
+
53
+ const modelParams = {
54
+ model: finalProps.model,
55
+ generationConfig,
56
+ tools: finalProps.tools,
57
+ tool_config: {
58
+ function_calling_config: {
59
+ mode: finalProps.tool_choice.toUpperCase(),
60
+ allowed_function_names: finalProps.allowFunctionNames,
61
+ },
62
+ },
63
+ };
64
+
65
+ // The Gemini 1.5 models are versatile and work with most use cases
66
+ const model = genAI.getGenerativeModel(modelParams);
67
+
68
+ const chat = model.startChat({
69
+ history: finalProps.history,
70
+ });
71
+
72
+ chat
73
+ .sendMessage(finalProps.message)
74
+ .then((result) => result.response)
75
+ .then((response) => {
76
+ return {
77
+ functions: response.functionCalls(),
78
+ text: response.text(),
79
+ };
80
+ })
81
+ .then((payload) => {
82
+ send(controller.mergeResponseWithMessage(payload, finalProps));
83
+ end(done);
84
+ })
85
+ .catch((err) => {
86
+ end(done, err);
87
+ });
88
+ });
89
+ }
90
+
91
+ RED.nodes.registerType(TYPES.GeminiaiChat, GeminiAIChatHandlerNode);
92
+ };
package/globalUtils.js ADDED
@@ -0,0 +1,39 @@
1
+ const { INTENT_STORE, LOCAL_STORAGE_PATH} = require("./constants");
2
+ const { getStorageAtLocation } = require("./database");
3
+
4
+ function end(done, error) {
5
+ if (done) {
6
+ done(error);
7
+ }
8
+ }
9
+
10
+ class ContextDatabase {
11
+ constructor(RED) {
12
+ const {functionGlobalContext = {} } = RED.settings
13
+ const path = functionGlobalContext[LOCAL_STORAGE_PATH]
14
+
15
+ this.globalContext = getStorageAtLocation(path);
16
+ }
17
+
18
+ getNodeStore() {
19
+ const stringStore = this.globalContext.getItem(INTENT_STORE) || "{}";
20
+
21
+ return JSON.parse(stringStore);
22
+ }
23
+
24
+ saveIntent(config) {
25
+ const nodeStore = this.getNodeStore();
26
+ nodeStore[config.id] = config;
27
+
28
+ this.globalContext.setItem(INTENT_STORE, JSON.stringify(nodeStore));
29
+ }
30
+
31
+ removeIntent(config) {
32
+ const nodeStore = this.getNodeStore();
33
+ delete nodeStore[config.id];
34
+
35
+ this.globalContext.setItem(INTENT_STORE, JSON.stringify(nodeStore));
36
+ }
37
+ }
38
+
39
+ module.exports = { end, ContextDatabase };
Binary file
Binary file
Binary file
@@ -0,0 +1,204 @@
1
+ const { GlobalContext } = require("../utilities/global-context");
2
+ const { TOOL_CHOICE } = require("../constants");
3
+ const AzureOpenAI = require("openai");
4
+ const { ContextDatabase } = require("../globalUtils");
5
+ const { ConversationHistory } = require("../utilities/conversationHistory");
6
+ const { Format } = require("../utilities/format");
7
+
8
+ const azureOpenAIHelper = (props, callback) => {
9
+ const { node, config, msg, RED } = props
10
+ const nodeDB = new GlobalContext(node);
11
+ const { model, credentials } = node.platform
12
+ const apiKey = credentials.api
13
+ const endpoint = credentials.endpoint
14
+ const apiVersion = "2025-01-01-preview"
15
+
16
+ if (!apiKey) {
17
+ node.status({ fill: "red", shape: "dot", text: "Error" });
18
+ return callback("API key missing for AzureOpenAI. Please add openaiAPIKey key-value pair to the functionGlobalContext.");
19
+ }
20
+ if (!endpoint) {
21
+ node.status({ fill: "red", shape: "dot", text: "Error" });
22
+ return callback("Endpoint missing for AzureOpenAI. Please add openaiAPIKey key-value pair to the functionGlobalContext.");
23
+ }
24
+
25
+ const openai = new AzureOpenAI({ endpoint, apiKey, apiVersion });
26
+ const { options = {}, system = "", user = "" } = msg?.payload || {}
27
+ const conversation_id = config.conversation_id;
28
+ const conversationHistory = new ConversationHistory(nodeDB, conversation_id)
29
+
30
+ if (msg.clearChatHistory) {
31
+ conversationHistory.clearHistory()
32
+ node.warn("Conversation history cleared")
33
+ }
34
+
35
+ if (!user) {
36
+ node.status({ fill: "red", shape: "dot", text: "Stopped" });
37
+ return node.warn("payload.user is empty. Stopping the flow ")
38
+ }
39
+
40
+ conversationHistory.addSystemMessage(system)
41
+ conversationHistory.addUserMessage(user)
42
+
43
+ if (conversation_id) {
44
+ conversationHistory.saveHistory()
45
+ }
46
+
47
+ const toolProperties = getToolProperties(config, msg.tools, RED)
48
+ const finalProps = {
49
+ ...options,
50
+ ...toolProperties,
51
+ model,
52
+ messages: conversationHistory.conversation
53
+ };
54
+
55
+ openai.chat.completions
56
+ .create(finalProps)
57
+ .then((response) => {
58
+
59
+ response.choices.forEach(choice => {
60
+ conversationHistory.addAssistantMessage(choice.message.content)
61
+ })
62
+ conversationHistory.saveHistory()
63
+
64
+ return createPayload(finalProps, response, msg, conversationHistory.conversation)
65
+ })
66
+ .then(msg => {
67
+ callback(null, msg)
68
+ })
69
+ .catch((err) => {
70
+ callback(err)
71
+ });
72
+ }
73
+
74
+ const createPayload = (request, response, previousMsg, conversationHistory) => {
75
+ const format = new Format()
76
+ const payload = format.formatPayloadForOpenAI(response.choices)
77
+
78
+ return {
79
+ ...previousMsg,
80
+ payload,
81
+ apiResponse: response,
82
+ _debug: {
83
+ ...request,
84
+ messages: conversationHistory
85
+ },
86
+ }
87
+ }
88
+
89
+ const getAllTools = (RED) => {
90
+ const context = new ContextDatabase(RED);
91
+ const intents = context.getNodeStore() || {};
92
+ return createFunctionsFromContext(intents)
93
+ }
94
+
95
+ /**
96
+ * Converts the raw intents into functions that the LLM can use.
97
+ * @param {*} node
98
+ * @returns
99
+ */
100
+ getRegisteredIntentFunctions = (RED) => {
101
+ const intents = getRawIntents(RED);
102
+ return createFunctionsFromContext(intents);
103
+ };
104
+
105
+ /**
106
+ * This will return all stored Registered Intents throughout the entire system
107
+ * and Tool Nodes that are attached directly to this flow
108
+ * This will return:
109
+ * type RawIntent = {
110
+ * [node_id]: node // could be Registered Intent or Tool node
111
+ * }
112
+ */
113
+ getRawIntents = (RED) => {
114
+ const context = new ContextDatabase(RED);
115
+ return context.getNodeStore() || {};
116
+ };
117
+
118
+ /**
119
+ * converts the registered intents stored in the context into functions that can be used by the LLM.
120
+ * The registered intent will be ignored if excludeFromOpenAi is set to true.
121
+ * rawIntents may have tool nodes included so the values need to be filtered by the node type.
122
+ * rawIntents have the following shape:
123
+ *
124
+ * type RawIntents = {
125
+ * [node_id]: node // node could be Registered Intent or Tool node
126
+ * }
127
+ */
128
+ const createFunctionsFromContext = (rawIntents = {}) => {
129
+ return (
130
+ Object.values(rawIntents)
131
+ .filter((payload) => {
132
+ return payload.type === "Register Intent";
133
+ })
134
+ .map((payload) => {
135
+ if (payload.excludeFromOpenAi) {
136
+ return undefined;
137
+ }
138
+
139
+ const parameters = payload.code?.trim() ?
140
+ JSON.parse(payload.code) : { type: "object", properties: {}, required: [] };
141
+
142
+
143
+ //TODO - Remove after all the old versions are deprecated
144
+ const { properties = {}, required = [] } = parameters
145
+ required.push("isRegisteredIntent")
146
+ properties.isRegisteredIntent = { type: "boolean", const: true }
147
+
148
+ return {
149
+ type: "function",
150
+ function: {
151
+ name: payload.name,
152
+ description: payload.description,
153
+ parameters: {
154
+ ...parameters,
155
+ properties,
156
+ required,
157
+ additionalProperties: false
158
+ },
159
+ strict: true
160
+ },
161
+ };
162
+ })
163
+ .filter(Boolean) || []
164
+ );
165
+ };
166
+
167
+
168
+ /**
169
+ *
170
+ * @param config
171
+ * @param deprecatedTools
172
+ * @returns {{}}
173
+ */
174
+ const getToolProperties = (
175
+ config,
176
+ deprecatedTools = [],
177
+ deprecatedTools = [],
178
+ RED
179
+ ) => {
180
+
181
+ const tool_choice = config.tool_choice
182
+ const tool_string_ids = config.tools;
183
+ const tool_ids = tool_string_ids.split(",");
184
+ const toolProperties = {}
185
+ const allTools = getAllTools(RED)
186
+ const tools = []
187
+
188
+ if (tool_choice !== TOOL_CHOICE.None) {
189
+ [...deprecatedTools, ...allTools].forEach(tool => {
190
+ if (tool_ids.includes(tool.function.name)) {
191
+ tools.push(tool);
192
+ }
193
+ })
194
+
195
+ toolProperties.tools = tools
196
+ toolProperties.tool_choice = tool_choice
197
+ }
198
+
199
+ return toolProperties
200
+ }
201
+
202
+ module.exports = {
203
+ azureOpenAIHelper
204
+ }