@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.
- package/.eslintrc +49 -0
- package/.idea/modules.xml +8 -0
- package/.idea/node-red-contrib-ai-intent.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/call-intent/icons/promotion-icon.svg +8 -0
- package/call-intent/index.html +114 -0
- package/call-intent/index.js +110 -0
- package/constants.js +31 -0
- package/database.js +9 -0
- package/examples/home-assistant-automation.json +167 -0
- package/examples/llm-chat-node-example.json +208 -0
- package/examples/openai-call-registered-intent-example.json +174 -0
- package/examples/openai-system-node-example.json +178 -0
- package/examples/openai-tool-node-example.json +120 -0
- package/examples/openai-user-node-exampe.json +234 -0
- package/geminiai-chat/geminiai-configuration/index.html +18 -0
- package/geminiai-chat/geminiai-configuration/index.js +7 -0
- package/geminiai-chat/icons/diamond.svg +8 -0
- package/geminiai-chat/icons/gemini-icon.svg +1 -0
- package/geminiai-chat/icons/gemini.svg +8 -0
- package/geminiai-chat/index.html +189 -0
- package/geminiai-chat/index.js +92 -0
- package/globalUtils.js +39 -0
- package/images/call_register_intent.jpeg +0 -0
- package/images/finally.jpg +0 -0
- package/images/set-config-node.gif +0 -0
- package/llm-chat/AzureOpenAIHelper.js +204 -0
- package/llm-chat/ChatGPTHelper.js +197 -0
- package/llm-chat/GeminiHelper.js +260 -0
- package/llm-chat/OllamaHelper.js +196 -0
- package/llm-chat/icons/bot-message-square.svg +1 -0
- package/llm-chat/icons/brain-circuit.svg +1 -0
- package/llm-chat/icons/chatgpt-icon.svg +7 -0
- package/llm-chat/index.html +205 -0
- package/llm-chat/index.js +73 -0
- package/llm-chat/platform-configuration/index.html +136 -0
- package/llm-chat/platform-configuration/index.js +16 -0
- package/localai-chat/icons/gem-icon.svg +1 -0
- package/localai-chat/icons/llama.svg +8 -0
- package/localai-chat/index.html +244 -0
- package/localai-chat/index.js +108 -0
- package/localai-chat/localai-configuration/index.html +18 -0
- package/localai-chat/localai-configuration/index.js +7 -0
- package/openai-chat/icons/chatgpt-icon.svg +7 -0
- package/openai-chat/index.html +196 -0
- package/openai-chat/index.js +58 -0
- package/openai-chat/openai-configuration/index.html +18 -0
- package/openai-chat/openai-configuration/index.js +7 -0
- package/openai-response/index.html +66 -0
- package/openai-response/index.js +154 -0
- package/openai-system/index.html +68 -0
- package/openai-system/index.js +28 -0
- package/openai-tool/index.html +57 -0
- package/openai-tool/index.js +50 -0
- package/openai-user/index.html +76 -0
- package/openai-user/index.js +26 -0
- package/package.json +49 -0
- package/register-intent/icons/register-icon.svg +8 -0
- package/register-intent/index.html +195 -0
- package/register-intent/index.js +72 -0
- package/register-intent/utils.js +10 -0
- package/utilities/chat-controller.js +249 -0
- package/utilities/chat-ledger.js +122 -0
- package/utilities/conversationHistory.js +68 -0
- package/utilities/format.js +94 -0
- package/utilities/gemini-controller.js +243 -0
- package/utilities/global-context.js +30 -0
- 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,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
|
+
};
|