@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,195 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("Register Intent", {
3
+ category: 'AI Intent',
4
+ color: '#1abc9c',
5
+ icon:"register-icon.svg",
6
+ defaults: {
7
+ name: { value: "", required: true, validate: RED.validators.regex(/^[a-zA-Z0-9_-]{1,64}$/) },
8
+ description: { value: "", required: true},
9
+ advanceMode: {value: "false"},
10
+ excludeFromOpenAi: { value: false },
11
+ code: {
12
+ value: "",
13
+ validate: function(code){
14
+ if(this.advanceMode === "true"){
15
+ try{
16
+ return code.trim() && !!JSON.parse(code)
17
+ }catch(e){
18
+ return false
19
+ }
20
+ }
21
+ return true
22
+ }
23
+ }
24
+ },
25
+ inputs: 0,
26
+ outputs: 1,
27
+ label: function () {
28
+ return this.name || "Register Intent";
29
+ },
30
+ oneditprepare: function(){
31
+ this.editor = RED.editor.createEditor({
32
+ id: 'node-code',
33
+ mode: 'ace/mode/json',
34
+ value: this.code
35
+ });
36
+
37
+ $("#node-input-advanceMode").typedInput({
38
+ type:"bool",
39
+ types:["bool"],
40
+ typeField: "#node-input-advanceMode-type"
41
+ }).on("change",function () {
42
+ this.advanceMode = $(this).val()
43
+ if ($(this).val() === "true") {
44
+ $("#code").slideDown()
45
+ } else {
46
+ $("#code").slideUp();
47
+ }
48
+ })
49
+ },
50
+ oneditsave: function() {
51
+ this.code = this.editor.getValue();
52
+ this.editor.destroy();
53
+ delete this.editor;
54
+ },
55
+ oneditcancel: function() {
56
+ this.editor.destroy();
57
+ delete this.editor;
58
+ }
59
+
60
+ });
61
+
62
+
63
+ </script>
64
+
65
+ <script type="text/html" data-template-name="Register Intent">
66
+
67
+ <div style="display: flex; justify-content: center; margin-bottom: 25px;">
68
+ <a href="https://youtu.be/FvP04OToeLQ" target="_blank" referrerpolicy="no-referrer"
69
+ style="color: #f53b57"><i class="fa fa-youtube"></i><span style="padding-left: 10px;">Watch
70
+ Register Intent Node Tutorial</span></a>
71
+ </div>
72
+
73
+ <div>
74
+
75
+ <div class="form-row">
76
+ <label for="node-input-name">Name</label>
77
+ <input type="text" id="node-input-name" placeholder="Name">
78
+ </div>
79
+ <div class="form-tips"><b>Tip:</b> Name should be alphanumeric including underscores and hyphens (no spaces).</div>
80
+ </div>
81
+
82
+
83
+ <div class="form-row">
84
+ <label for="node-input-description">Intent Description</label>
85
+ <input type="text" id="node-input-description" placeholder="Description">
86
+ </div>
87
+
88
+ <div class="form-row">
89
+ <label for="node-input-advanceMode">Advance Mode</label>
90
+ <input type="text" id="node-input-advanceMode">
91
+ <input type="hidden" id="node-input-advanceMode-type">
92
+ </div>
93
+
94
+ <div class="form-row" id="code">
95
+ <label for="node-code"><i class="fa fa-code"></i>Tool Schema (parameters)</label>
96
+ <div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-code"></div>
97
+ </div>
98
+
99
+
100
+ <div style="display: flex; justify-content: space-around;">
101
+
102
+ <label for="node-input-excludeFromOpenAi" style="display: flex;">
103
+ <i class="fa fa-tag"></i>
104
+ <span style="margin-left: 5px;">Exclude from OpenAI (Deprecated)</span>
105
+ <input style="display: inline-block;margin-left: 1rem;" type="checkbox" id="node-input-excludeFromOpenAi"
106
+ name="enable conversation"></label>
107
+ </div>
108
+
109
+ </script>
110
+
111
+ <script type="text/html" data-help-name="Register Intent">
112
+ <p>Registers an intent for use with the <code>Call Intent</code> node. This allows you to define custom actions that can be triggered by other flows or, optionally, by an AI assistant like OpenAI.</p>
113
+
114
+ <h3>Inputs</h3>
115
+ <dl class="message-properties">
116
+ <dt>Name <span class="property-type">string</span></dt>
117
+ <dd>A unique name for this intent. This name is used to identify the intent when calling it from other nodes. It should be alphanumeric and can include underscores and hyphens (no spaces). Maximum length is 64 characters.</dd>
118
+
119
+ <dt>Description <span class="property-type">string</span></dt>
120
+ <dd>A clear and concise description of the intent's purpose. This description is used by AI assistants (if enabled) to understand when the intent should be triggered. A good description helps the AI choose the appropriate function to call.</dd>
121
+
122
+ <dt>Advanced Mode <span class="property-type">boolean</span></dt>
123
+ <dd>Enables/Disables advanced configuration of the intent. If checked, the Tool Schema field will be displayed and required.</dd>
124
+
125
+ <dt>Tool Schema <span class="property-type">JSON (when Advanced Mode is enabled)</span></dt>
126
+ <dd>A JSON object defining the schema of the tool, enabling more structured interactions with AI assistants. This schema describes the parameters the tool expects, allowing the AI to provide the correct inputs. Must be valid JSON.</dd>
127
+
128
+ <dt>Exclude from OpenAI (Deprecated) <span class="property-type">boolean</span></dt>
129
+ <dd><em>Deprecated: This option is no longer used.</em> Previously, this option controlled whether the intent was exposed to OpenAI as a callable function. This is now handled automatically based on your flow configuration.
130
+ </dd>
131
+ </dl>
132
+
133
+ <h3>Details</h3>
134
+ <p>Place this node at the beginning of a flow to register an intent. The registered intent can then be called by a <code>Call Intent</code> node. This mechanism allows you to create modular and reusable flows that can be triggered by various events or AI assistants.</p>
135
+
136
+ <p>When integrating with AI assistants, the <strong>Description</strong> field is crucial. It provides the AI with the context needed to determine when to call this intent. Therefore, write descriptive and concise descriptions that clearly explain the intent's functionality.</p>
137
+
138
+ <p>The <strong>Advanced Mode</strong> and <strong>Tool Schema</strong> fields allow you to define a structured interface for AI interactions. The schema describes the input parameters expected by the intent, ensuring that the AI provides valid data when calling the associated flow.</p>
139
+
140
+ <p><strong>Example:</strong> Imagine you have a flow that controls your living room lights. You could create a "Living Room Lights" intent with a description like "Controls the living room lights. Accepts 'on', 'off', and 'dim' commands." Then, in your AI assistant, you could configure it to call this intent when the user says something like "Turn on the living room lights."</p>
141
+
142
+ <h3>Important</h3>
143
+ <p>The JSON provide will vary greatly based on the LLM platform. Some Schema features in OpenAI is not support in Gemini while many models in Ollama are not supported at all.
144
+ Please consult the various function calling documentation for your specified platform</p>
145
+
146
+ <h4>Example</h4>
147
+ The following is an example of a OpenAI function definition.
148
+ You only need to provide the `parameters` property of the schema.
149
+ A typical full function schema looks like this:
150
+ <pre>
151
+ {
152
+ type: "function",
153
+ function:{
154
+ name: "",
155
+ description: "",
156
+ parameters: {
157
+ type: "object"
158
+ properties: {...}
159
+ }
160
+ }
161
+ </pre>
162
+
163
+ <p> You only need to provide the `parameters` property for your schema</p>
164
+ <pre>
165
+ {
166
+ "type": "object",
167
+ "properties": {
168
+ "eventName": {
169
+ "type": "string",
170
+ "description": "A unique name for the event. Used to identify the event in the schedule."
171
+ },
172
+ "eventTime": {
173
+ "type": "string",
174
+ "format": "date-time",
175
+ "description": "The time for the one-off event in ISO8601 format (e.g., 2025-01-26T18:30:00)."
176
+ },
177
+ "eventPayload": {
178
+ "type": "string",
179
+ "description": "Command to give the smart home when the timer finishes"
180
+ }
181
+ },
182
+ "required": [
183
+ "eventName",
184
+ "eventTime"
185
+ ],
186
+ "additionalProperties": false
187
+ }
188
+ </pre>
189
+ <p>Check the function calling documentation for your platform of choice</p>
190
+ <ul>
191
+ <li><a href="https://platform.openai.com/docs/guides/function-calling">OpenAI</a></li>
192
+ <li><a href="https://ai.google.dev/gemini-api/docs/function-calling">Gemini</a></li>
193
+ <li><a href="https://ollama.com/blog/tool-support">Ollama</a></li>
194
+ </ul>
195
+ </script>
@@ -0,0 +1,72 @@
1
+ const PubSub = require("pubsub-js");
2
+ const { getErrorMessagesForConfig } = require("./utils");
3
+ const { end, ContextDatabase } = require("../globalUtils");
4
+ const {validateOpenAISchema} = require("../utilities/validateSchema")
5
+
6
+ module.exports = function (RED) {
7
+ function RegisterIntentHandlerNode(config) {
8
+ RED.nodes.createNode(this, config);
9
+
10
+ const errorMessage = getErrorMessagesForConfig(config);
11
+ const node = this;
12
+ try{
13
+ if(config.advanceMode === "true"){
14
+ const schema = JSON.parse(config.code)
15
+ const result = validateOpenAISchema(schema)
16
+ if(!result.isValid){
17
+ node.status({fill:"red",shape:"dot",text:`${result.errorMsg}`});
18
+ node.error(result.errorMsg)
19
+ console.log(`RESULT: ${config.name} - `,result.errorMsg)
20
+ }else{
21
+ node.status({fill:"blue",shape:"dot",text:"Ready (Advanced)"});
22
+ }
23
+ }else{
24
+ node.status({fill:"blue",shape:"dot",text:"Ready (Simple)"});
25
+ }
26
+ }catch(e){
27
+ node.status({fill:"red",shape:"dot",text:"Advance mode json invalid"});
28
+ node.error(e)
29
+
30
+ }
31
+
32
+ const nodeDB = new ContextDatabase(RED);
33
+ if (errorMessage) {
34
+ // There was an error. Stop.
35
+ node.status({fill:"red",shape:"dot",text:"Error"});
36
+ return this.error(errorMessage);
37
+ }
38
+ else {
39
+ // create a new entry in global context for the given node id
40
+ nodeDB.saveIntent({
41
+ nodeId: node.id,
42
+ ...config,
43
+ });
44
+ }
45
+
46
+ // When Call Intent node publishes an event,
47
+ // this node will only listen for its own intent
48
+ const token = PubSub.subscribe(config.id, function (msg, data) {
49
+ const nodeStore = nodeDB.getNodeStore();
50
+ const { name, description, excludeFromOpenAi, code } = nodeStore[node.id];
51
+ node.status({fill:"green",shape:"dot",text:`Received data ${new Date()}`});
52
+ node.send([
53
+ { ...data, _config: { name, description, excludeFromOpenAi, code } },
54
+ ]);
55
+ });
56
+
57
+ // We need to clean up on close otherwise
58
+ // more than one message is sent when a call is published
59
+ this.on("close", function (removed, done) {
60
+ if (removed) {
61
+ nodeDB.removeIntent(config);
62
+ PubSub.unsubscribe(token);
63
+ end(done);
64
+ } else {
65
+ PubSub.unsubscribe(token);
66
+ end(done);
67
+ }
68
+ });
69
+ }
70
+
71
+ RED.nodes.registerType("Register Intent", RegisterIntentHandlerNode);
72
+ };
@@ -0,0 +1,10 @@
1
+ const getErrorMessagesForConfig = (config) => {
2
+ const { name, description } = config;
3
+ if (!name) {
4
+ return "Name is required";
5
+ } else if (!description) {
6
+ return "Description is required";
7
+ }
8
+ };
9
+
10
+ module.exports = { getErrorMessagesForConfig };
@@ -0,0 +1,249 @@
1
+ const { TYPES, TOOL_CHOICE } = require("../constants");
2
+ const { ContextDatabase } = require("../globalUtils");
3
+ const { ChatLedger } = require("./chat-ledger");
4
+
5
+ class ChatController {
6
+ constructor(node, config, msg, RED) {
7
+ this.msg = msg;
8
+ this.node = node;
9
+ this.config = config;
10
+ this.apiProperties = getChatCompletionProps(msg, config, node);
11
+
12
+ const registeredIntentFunctions = this.getRegisteredIntentFunctions(RED);
13
+ const rawIntents = this.getRawIntents(RED);
14
+
15
+ this.tools = [
16
+ ...this.apiProperties.tools,
17
+ ...registeredIntentFunctions,
18
+ ].filter(Boolean);
19
+ const toolProperties = determineToolProperties(
20
+ rawIntents,
21
+ this.tools,
22
+ this.apiProperties.tool_choice
23
+ );
24
+
25
+ this.toolProperties = validateToolProperties(toolProperties, node);
26
+ }
27
+
28
+ mergeResponseWithMessage = (response, request) => {
29
+ const { user, system, tools, ...rest } = this.msg;
30
+ const ledger = new ChatLedger(this.config.conversation_id, this.node);
31
+ const fullConversation = ledger.addResponseToConversationAndSave(
32
+ request,
33
+ response,
34
+ this.node.type
35
+ );
36
+
37
+ return {
38
+ ...rest,
39
+ payload: response,
40
+ _debug: {
41
+ ...request,
42
+ type: this.node.type,
43
+ fullConversation,
44
+ conversation_id: this.config.conversation_id,
45
+ },
46
+ };
47
+ };
48
+
49
+ /**
50
+ * Converts the raw intents into functions that the LLM can use.
51
+ * @param {*} node
52
+ * @returns
53
+ */
54
+ getRegisteredIntentFunctions = (RED) => {
55
+ const intents = this.getRawIntents(RED);
56
+ return createFunctionsFromContext(intents);
57
+ };
58
+
59
+ /**
60
+ * This will return all stored Registered Intents throughout the entire system
61
+ * and Tool Nodes that are attached directly to this flow
62
+ * This will return:
63
+ * type RawIntent = {
64
+ * [node_id]: node // could be Registered Intent or Tool node
65
+ * }
66
+ */
67
+ getRawIntents = (RED) => {
68
+ const context = new ContextDatabase(RED);
69
+ return context.getNodeStore() || {};
70
+ };
71
+ }
72
+
73
+ /**
74
+ * If no tools exist, then remove tools and toolChoice from the payload
75
+ */
76
+ const validateToolProperties = (toolProperties, node) => {
77
+ if (!toolProperties.tools?.length) {
78
+ const { tools, toolChoice, ...rest } = toolProperties;
79
+
80
+ if (toolChoice && toolChoice !== TOOL_CHOICE.None) {
81
+ node.warn(
82
+ "Removing tools from payload since no tools are available. Flow will continue."
83
+ );
84
+ }
85
+
86
+ return rest;
87
+ }
88
+ return toolProperties;
89
+ };
90
+
91
+ /**
92
+ * combines various properties from `msg` and `config` to return all the properties needed for OpenAI API request
93
+ * @param {Record<string,any>} msg
94
+ * @param {Record<string, any>} config
95
+ * @returns
96
+ */
97
+ const getChatCompletionProps = (msg, config, node) => {
98
+ const model = msg.payload?.model || config.model;
99
+ const temperature = Number(msg.payload?.temperature || config.temperature);
100
+ const max_tokens = Number(msg.payload?.max_tokens || config.max_tokens);
101
+ const top_p = Number(msg.payload?.top_p || config.top_p);
102
+ const frequency_penalty = Number(
103
+ msg.payload?.frequency_penalty || config.frequency_penalty
104
+ );
105
+ const presence_penalty = Number(
106
+ msg.payload?.presence_penalty || config.presence_penalty
107
+ );
108
+ const _format = { format: msg.payload?.json || config.json };
109
+ const tools = msg?.tools || [];
110
+ const tool_choice =
111
+ msg.payload?.tool_choice || config?.tool_choice || TOOL_CHOICE.Auto;
112
+ const ledger = new ChatLedger(config.conversation_id, node);
113
+ const messages = ledger.combineExistingMessages(msg.user, msg.system);
114
+
115
+ if (!_format.format) {
116
+ delete _format.format;
117
+ } else {
118
+ _format.format = "json";
119
+ }
120
+
121
+ return {
122
+ model,
123
+ temperature,
124
+ max_tokens,
125
+ top_p,
126
+ frequency_penalty,
127
+ presence_penalty,
128
+ messages,
129
+ tool_choice,
130
+ tools,
131
+ ..._format,
132
+ };
133
+ };
134
+
135
+ /**
136
+ * converts the registered intents stored in the context into functions that can be used by the LLM.
137
+ * The registered intent will be ignored if excludeFromOpenAi is set to true.
138
+ * rawIntents may have tool nodes included so the values need to be filtered by the node type.
139
+ * rawIntents have the following shape:
140
+ *
141
+ * type RawIntents = {
142
+ * [node_id]: node // node could be Registered Intent or Tool node
143
+ * }
144
+ */
145
+ const createFunctionsFromContext = (rawIntents = {}) => {
146
+ return (
147
+ Object.values(rawIntents)
148
+ .filter((payload) => {
149
+ return payload.type === "Register Intent";
150
+ })
151
+ .map((payload) => {
152
+ if (payload.excludeFromOpenAi) {
153
+ return undefined;
154
+ }
155
+
156
+ return {
157
+ type: "function",
158
+ function: {
159
+ name: payload.name,
160
+ description: payload.description,
161
+ parameters: {
162
+ type: "object",
163
+ properties: {
164
+ isRegisteredIntent: { type:"boolean", const: true },
165
+ response: {
166
+ type: "string",
167
+ description: "A friendly response to the given command",
168
+ },
169
+ },
170
+ required: ["isRegisteredIntent", "response"],
171
+ },
172
+ },
173
+ };
174
+ })
175
+ .filter(Boolean) || []
176
+ );
177
+ };
178
+
179
+ /**
180
+ * Based on the tool_choice the tool properties will be created. This is based off
181
+ * https://cookbook.openai.com/examples/how_to_call_functions_with_chat_models
182
+ *
183
+ * @param {Object} context - contains all the saved nodes that represents functions
184
+ * @param {*} tools - All the tools to be sent as functions
185
+ * @param {*} toolChoice - Specifies which tools to use
186
+ * @returns
187
+ */
188
+ const determineToolProperties = (
189
+ context = {},
190
+ tools = [],
191
+ toolChoice = TOOL_CHOICE.Auto
192
+ ) => {
193
+ const props = {
194
+ tools,
195
+ tool_choice: toolChoice,
196
+ };
197
+ if (toolChoice === TOOL_CHOICE.None || tools.length === 0) {
198
+ // No tools chosen
199
+ return {};
200
+ } else if (toolChoice === TOOL_CHOICE.Auto) {
201
+ // set the choice to auto
202
+ return props;
203
+ } else if (context[toolChoice]?.type === TYPES.OpenAITool) {
204
+ // User chose a specific tool from the dropdown list
205
+ const tool = JSON.parse(context[toolChoice].tool);
206
+ props.tool_choice = {
207
+ type: tool.type,
208
+ function: { name: tool.function.name },
209
+ };
210
+
211
+ const doesExist = tools.some((_tool) => {
212
+ return _tool.function.name === tool.function.name;
213
+ });
214
+
215
+ if (!doesExist) {
216
+ throw new Error(
217
+ `The OpenAI Tool node "${context[toolChoice].name}" is missing from the flow.`
218
+ );
219
+ }
220
+
221
+ return props;
222
+ } else if (context[toolChoice]?.name) {
223
+ // ???
224
+ props.tool_choice = {
225
+ type: "function",
226
+ function: { name: context[toolChoice].name },
227
+ };
228
+
229
+ return props;
230
+ }
231
+ // Something funky happened so we will use auto instead
232
+ return {
233
+ tools,
234
+ tool_choice: TOOL_CHOICE.Auto,
235
+ };
236
+ };
237
+
238
+ const createChatEndpoint = (baseURL = "") => {
239
+ return stripTrailingSlash(baseURL) + "/api/chat";
240
+ };
241
+
242
+ const stripTrailingSlash = (str) => {
243
+ return str.endsWith("/") ? str.slice(0, -1) : str;
244
+ };
245
+
246
+ module.exports = {
247
+ ChatController,
248
+ createChatEndpoint,
249
+ };
@@ -0,0 +1,122 @@
1
+ const { CONVERSATION_CONTEXT, TYPES } = require("../constants");
2
+ const { GlobalContext } = require("./global-context");
3
+
4
+ class ChatLedger {
5
+ constructor(id = "", node) {
6
+ this.id = id;
7
+ this.node = node;
8
+ }
9
+
10
+ addResponseToConversationAndSave = (
11
+ request,
12
+ response,
13
+ type = TYPES.OpenAIChat
14
+ ) => {
15
+ const conversation = [];
16
+
17
+ request.messages.forEach((message) => {
18
+ conversation.push(message);
19
+ });
20
+
21
+ if (type === TYPES.OpenAIChat) {
22
+ response.choices.forEach(({ message }) => {
23
+ const { role, content = "", tool_calls } = message;
24
+
25
+ if (content) {
26
+ conversation.push({ role, content });
27
+ } else if (tool_calls) {
28
+ const toolMessage = tool_calls.reduce((message, tool) => {
29
+ return `${message} ${JSON.stringify(
30
+ tool.function.arguments,
31
+ null,
32
+ 2
33
+ )}`;
34
+ }, "");
35
+ conversation.push({ role, content: toolMessage });
36
+ }
37
+ });
38
+ } else {
39
+ const { role, content = "" } = response.message;
40
+ conversation.push({ role, content });
41
+ }
42
+
43
+ return this.saveConversation(conversation);
44
+ };
45
+
46
+ combineExistingMessages = (userOrAssistant, system) => {
47
+ const existingConversation = this.getConversation();
48
+ let combined = [...existingConversation, userOrAssistant];
49
+
50
+ if (existingConversation[0]?.role === "system" && system) {
51
+ //Replace the existing system with the new system
52
+ combined[0] = system;
53
+ } else if (system) {
54
+ // Add the system to the beginning of the array
55
+ combined = [system, ...combined];
56
+ }
57
+
58
+ // Remove entries with empty content. API breaks if content is empty
59
+ return combined
60
+ .map((conversation) => {
61
+ if (conversation.content) {
62
+ return conversation;
63
+ }
64
+ })
65
+ .filter(Boolean);
66
+ };
67
+
68
+ clearConversation = () => {
69
+ const { id, node } = this;
70
+ if (!id) {
71
+ this.node.warn(
72
+ `No conversation id is present on the . Cannot clear conversation`
73
+ );
74
+ return [];
75
+ }
76
+ const globalContext = new GlobalContext(node);
77
+ const conversations =
78
+ globalContext.getValueFromGlobalContext(CONVERSATION_CONTEXT) || {};
79
+
80
+ conversations[id] = [];
81
+
82
+ globalContext.setValueToGlobalContext(conversations, CONVERSATION_CONTEXT);
83
+
84
+ return conversations[id];
85
+ };
86
+
87
+ getConversation = () => {
88
+ const { id = "", node } = this;
89
+
90
+ if (!id) {
91
+ return [];
92
+ }
93
+
94
+ const globalContext = new GlobalContext(node);
95
+ const conversations =
96
+ globalContext.getValueFromGlobalContext(CONVERSATION_CONTEXT) || {};
97
+
98
+ return conversations[id] || [];
99
+ };
100
+
101
+ saveConversation = (conversation = []) => {
102
+ const { id, node } = this;
103
+
104
+ if (!id) {
105
+ return conversation;
106
+ }
107
+
108
+ const globalContext = new GlobalContext(node);
109
+ const conversations =
110
+ globalContext.getValueFromGlobalContext(CONVERSATION_CONTEXT) || {};
111
+
112
+ conversations[id] = conversation;
113
+
114
+ globalContext.setValueToGlobalContext(conversations, CONVERSATION_CONTEXT);
115
+
116
+ return conversation;
117
+ };
118
+ }
119
+
120
+ module.exports = {
121
+ ChatLedger,
122
+ };