wingbot 3.66.4 → 3.67.1
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/index.js +3 -1
- package/package.json +1 -1
- package/plugins/ai.wingbot.openai/plugin.js +23 -160
- package/src/Ai.js +29 -10
- package/src/ChatGpt.js +486 -0
- package/src/systemEntities/email.js +10 -4
- package/src/systemEntities/phone.js +3 -1
- package/src/systemEntities/regexps.js +12 -0
- package/src/wingbot/CustomEntityDetectionModel.js +3 -1
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
/** @typedef {import('./src/Processor').ProcessorOptions} ProcessorOptions */
|
|
6
|
+
/** @typedef {import('./src/Processor').ProcessorOptions<Router|BuildRouter>} ProcessorOptions */
|
|
7
7
|
|
|
8
8
|
const Processor = require('./src/Processor');
|
|
9
9
|
const Router = require('./src/Router');
|
|
@@ -19,6 +19,7 @@ const CustomEntityDetectionModel = require('./src/wingbot/CustomEntityDetectionM
|
|
|
19
19
|
const ConversationTester = require('./src/ConversationTester');
|
|
20
20
|
const { asserts } = require('./src/testTools');
|
|
21
21
|
const BuildRouter = require('./src/BuildRouter');
|
|
22
|
+
const ChatGpt = require('./src/ChatGpt');
|
|
22
23
|
const MockAiModel = require('./src/MockAiModel');
|
|
23
24
|
const ReturnSender = require('./src/ReturnSender');
|
|
24
25
|
const CallbackAuditLog = require('./src/CallbackAuditLog');
|
|
@@ -126,6 +127,7 @@ module.exports = {
|
|
|
126
127
|
ButtonTemplate,
|
|
127
128
|
GenericTemplate,
|
|
128
129
|
BaseTemplate,
|
|
130
|
+
ChatGpt,
|
|
129
131
|
|
|
130
132
|
// tests
|
|
131
133
|
ConversationTester,
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fetch = require('node-fetch').default;
|
|
7
7
|
const Responder = require('../../src/Responder');
|
|
8
|
+
const ChatGpt = require('../../src/ChatGpt');
|
|
8
9
|
const compileWithState = require('../../src/utils/compileWithState');
|
|
9
10
|
|
|
10
11
|
const MSG_REPLACE = '#MSG-REPLACE#';
|
|
@@ -13,15 +14,11 @@ const CHAR_LIM = 4096;
|
|
|
13
14
|
|
|
14
15
|
function chatgptPlugin (params, configuration = {}) {
|
|
15
16
|
const {
|
|
16
|
-
openAiEndpoint =
|
|
17
|
+
openAiEndpoint = undefined,
|
|
17
18
|
openAiApiKey = null
|
|
18
19
|
} = configuration;
|
|
19
20
|
|
|
20
21
|
async function chatgpt (req, res) {
|
|
21
|
-
const content = req.text();
|
|
22
|
-
|
|
23
|
-
const charLim = params.charLim || CHAR_LIM;
|
|
24
|
-
|
|
25
22
|
const token = compileWithState(req, res, params.token).trim();
|
|
26
23
|
|
|
27
24
|
// gpt-3.5-turbo-0301 gpt-3.5-turbo
|
|
@@ -30,17 +27,13 @@ function chatgptPlugin (params, configuration = {}) {
|
|
|
30
27
|
const system = compileWithState(req, res, params.system).trim();
|
|
31
28
|
|
|
32
29
|
// 0 - 2
|
|
33
|
-
const temperature = parseFloat(compileWithState(req, res, params.temperature).trim().replace(',', '.') || '1') ||
|
|
30
|
+
const temperature = parseFloat(compileWithState(req, res, params.temperature).trim().replace(',', '.') || '1') || undefined;
|
|
34
31
|
// presence_penalty between -2.0 and 2.0
|
|
35
|
-
const presence = parseFloat(compileWithState(req, res, params.presence).trim().replace(',', '.') || '0') ||
|
|
36
|
-
|
|
37
|
-
const maxTokens = parseFloat(compileWithState(req, res, params.maxTokens).trim() || '512') || 512;
|
|
32
|
+
const presence = parseFloat(compileWithState(req, res, params.presence).trim().replace(',', '.') || '0') || undefined;
|
|
38
33
|
|
|
39
|
-
const
|
|
34
|
+
const requestTokens = parseFloat(compileWithState(req, res, params.maxTokens).trim() || '512') || undefined;
|
|
40
35
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const systemAfter = compileWithState(req, res, params.systemAfter).trim();
|
|
36
|
+
const transcriptLength = parseInt(compileWithState(req, res, params.limit).trim() || '10', 10) || 10;
|
|
44
37
|
|
|
45
38
|
const replacedAnnotation = `${params.annotation || ''}`.replace(/\{\{message\}\}/g, MSG_REPLACE);
|
|
46
39
|
const annotation = compileWithState(req, res, replacedAnnotation).trim();
|
|
@@ -53,157 +46,27 @@ function chatgptPlugin (params, configuration = {}) {
|
|
|
53
46
|
|| continueConfig.find((c) => ['default', '']
|
|
54
47
|
.includes(`${c.lang || ''}`.trim().toLowerCase()));
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
const gpt = new ChatGpt({
|
|
50
|
+
fetch: params.fetch,
|
|
51
|
+
model,
|
|
52
|
+
presencePenalty: presence,
|
|
53
|
+
requestTokens,
|
|
54
|
+
temperature,
|
|
55
|
+
transcriptLength,
|
|
56
|
+
openAiEndpoint,
|
|
57
|
+
...(openAiEndpoint
|
|
58
|
+
? { apiKey: token || openAiApiKey }
|
|
59
|
+
: { authorization: token || openAiApiKey })
|
|
60
|
+
});
|
|
57
61
|
|
|
58
62
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
frequency_penalty: 0,
|
|
65
|
-
presence_penalty: presence,
|
|
66
|
-
max_tokens: maxTokens,
|
|
67
|
-
temperature,
|
|
68
|
-
user
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const onlyFlag = Math.sign(limit) === -1 ? 'gpt' : null;
|
|
72
|
-
|
|
73
|
-
const ts = await res.getTranscript(Math.abs(limit), onlyFlag);
|
|
74
|
-
|
|
75
|
-
let total = (system ? system.length : 0)
|
|
76
|
-
+ (systemAfter ? systemAfter.length : 0)
|
|
77
|
-
+ maxTokens
|
|
78
|
-
+ content.length;
|
|
79
|
-
|
|
80
|
-
for (let i = ts.length - 1; i >= 0; i--) {
|
|
81
|
-
total += ts[i].text.length;
|
|
82
|
-
if (total > charLim) {
|
|
83
|
-
ts.splice(i, 1);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const messages = [
|
|
88
|
-
...(system ? [{ role: 'system', content: system }] : []),
|
|
89
|
-
...ts.map((t) => ({ role: t.fromBot ? 'assistant' : 'user', content: t.text })),
|
|
90
|
-
{ role: 'user', content },
|
|
91
|
-
...(systemAfter ? [{ role: 'system', content: systemAfter }] : [])
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
Object.assign(body, { messages });
|
|
95
|
-
|
|
96
|
-
const useFetch = params.fetch || fetch;
|
|
97
|
-
|
|
98
|
-
const apiUrl = openAiEndpoint
|
|
99
|
-
? `${openAiEndpoint}/chat/completions?api-version=2023-03-15-preview`
|
|
100
|
-
: 'https://api.openai.com/v1/chat/completions';
|
|
101
|
-
|
|
102
|
-
const response = await useFetch(apiUrl, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
headers: {
|
|
105
|
-
'Content-Type': 'application/json',
|
|
106
|
-
...(openAiEndpoint
|
|
107
|
-
? { 'api-key': token || openAiApiKey }
|
|
108
|
-
: { Authorization: `Bearer ${token || openAiApiKey}` })
|
|
109
|
-
},
|
|
110
|
-
body: JSON.stringify(body)
|
|
63
|
+
await gpt.respond(req, res, {
|
|
64
|
+
system,
|
|
65
|
+
persona,
|
|
66
|
+
continueReply,
|
|
67
|
+
annotation
|
|
111
68
|
});
|
|
112
|
-
|
|
113
|
-
const data = await response.json();
|
|
114
|
-
|
|
115
|
-
if (response.status !== 200
|
|
116
|
-
|| !Array.isArray(data.choices)) {
|
|
117
|
-
const { status, statusText } = response;
|
|
118
|
-
// eslint-disable-next-line no-console
|
|
119
|
-
console.log('chat gpt error', {
|
|
120
|
-
status, statusText, data, apiUrl
|
|
121
|
-
});
|
|
122
|
-
throw new Error(`Chat GPT ${status}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// eslint-disable-next-line no-console
|
|
126
|
-
console.log('chat gpt', JSON.stringify(data));
|
|
127
|
-
|
|
128
|
-
const sent = data.choices
|
|
129
|
-
.filter((ch) => ch.message && ch.message.role === 'assistant' && ch.message.content)
|
|
130
|
-
.map((ch) => {
|
|
131
|
-
|
|
132
|
-
let sliced = data.usage && data.usage.completion_tokens >= maxTokens;
|
|
133
|
-
|
|
134
|
-
let filtered = ch.message.content
|
|
135
|
-
.replace(/\n\n/g, '\n')
|
|
136
|
-
.split(/\n+(?!-)/g)
|
|
137
|
-
.filter((t) => !!t.trim());
|
|
138
|
-
|
|
139
|
-
if (filtered.length > 2) {
|
|
140
|
-
filtered = filtered.slice(0, filtered.length - 1);
|
|
141
|
-
sliced = true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (persona) {
|
|
145
|
-
res.setPersona({ name: persona });
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
filtered
|
|
149
|
-
.forEach((t, fi) => {
|
|
150
|
-
let trim = t.trim();
|
|
151
|
-
|
|
152
|
-
if (annotation) {
|
|
153
|
-
// replace the annotation first
|
|
154
|
-
|
|
155
|
-
const replacements = annotation.split(MSG_REPLACE);
|
|
156
|
-
const last = replacements.length > 1
|
|
157
|
-
? replacements.length - 1
|
|
158
|
-
: replacements.length;
|
|
159
|
-
replacements.forEach((r, i) => {
|
|
160
|
-
const foundI = trim.indexOf(r.trim(), i === last
|
|
161
|
-
? trim.length - r.trim().length
|
|
162
|
-
: 0);
|
|
163
|
-
|
|
164
|
-
if (foundI === -1
|
|
165
|
-
|| (i === 0 && foundI > 0)
|
|
166
|
-
|| (i === last && (trim.length - foundI - r.length) > 0)) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
trim = trim.replace(r.trim(), '').trim();
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (annotation && annotation.includes(MSG_REPLACE)) {
|
|
175
|
-
trim = annotation.replace(MSG_REPLACE, trim);
|
|
176
|
-
} else if (annotation) {
|
|
177
|
-
trim = `${annotation} ${trim}`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
res.text(trim, sliced && fi === (filtered.length - 1) && continueReply
|
|
181
|
-
? [
|
|
182
|
-
{
|
|
183
|
-
title: continueReply.title,
|
|
184
|
-
action: res.currentAction()
|
|
185
|
-
}
|
|
186
|
-
]
|
|
187
|
-
: null);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
if (persona) {
|
|
191
|
-
res.setPersona({ name: null });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return ch;
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
if (sent.length === 0) {
|
|
198
|
-
const { status, statusText } = response;
|
|
199
|
-
// eslint-disable-next-line no-console
|
|
200
|
-
console.log('chat gpt nothing to send', { status, statusText, data });
|
|
201
|
-
throw new Error('Chat GPT empty');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
69
|
} catch (e) {
|
|
205
|
-
// eslint-disable-next-line no-console
|
|
206
|
-
console.error('chat gpt fail', e, body);
|
|
207
70
|
await res.run('fallback');
|
|
208
71
|
}
|
|
209
72
|
|
package/src/Ai.js
CHANGED
|
@@ -51,9 +51,15 @@ let uq = 1;
|
|
|
51
51
|
// eslint-disable-next-line max-len
|
|
52
52
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').WordEntityDetector} WordEntityDetector */
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} WordDetectorData
|
|
56
|
+
* @prop {WordEntityDetector} detector
|
|
57
|
+
* @prop {number} [maxWordCount]
|
|
58
|
+
*/
|
|
59
|
+
|
|
54
60
|
/**
|
|
55
61
|
* @callback WordEntityDetectorFactory
|
|
56
|
-
* @returns {Promise<WordEntityDetector>}
|
|
62
|
+
* @returns {Promise<WordEntityDetector|WordDetectorData>}
|
|
57
63
|
*/
|
|
58
64
|
|
|
59
65
|
/** @typedef {[string,EntityDetector|RegExp,DetectorOptions]} DetectorArgs */
|
|
@@ -90,6 +96,8 @@ class Ai {
|
|
|
90
96
|
*/
|
|
91
97
|
this._wordEntityDetector = null;
|
|
92
98
|
|
|
99
|
+
this._wordEntityDetectorMaxWordCount = 0;
|
|
100
|
+
|
|
93
101
|
/**
|
|
94
102
|
* Upper threshold - for match method and for navigate method
|
|
95
103
|
*
|
|
@@ -289,19 +297,33 @@ class Ai {
|
|
|
289
297
|
|
|
290
298
|
/**
|
|
291
299
|
*
|
|
292
|
-
* @param {WordEntityDetector|WordEntityDetectorFactory} wordEntityDetector
|
|
300
|
+
* @param {WordEntityDetector|WordEntityDetectorFactory|WordDetectorData} wordEntityDetector
|
|
293
301
|
*/
|
|
294
302
|
setWordEntityDetector (wordEntityDetector) {
|
|
295
|
-
if (wordEntityDetector.length === 0) {
|
|
303
|
+
if (typeof wordEntityDetector === 'function' && wordEntityDetector.length === 0) {
|
|
296
304
|
// @ts-ignore
|
|
297
305
|
this._wordEntityDetectorFactory = wordEntityDetector;
|
|
298
306
|
return this;
|
|
299
307
|
}
|
|
300
308
|
|
|
301
|
-
|
|
309
|
+
let detector;
|
|
310
|
+
if (typeof wordEntityDetector === 'object') {
|
|
311
|
+
({ detector } = wordEntityDetector);
|
|
312
|
+
this._wordEntityDetectorMaxWordCount = Math.max(
|
|
313
|
+
this._wordEntityDetectorMaxWordCount,
|
|
314
|
+
wordEntityDetector.maxWordCount || 0
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
detector = wordEntityDetector;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// @ts-ignore
|
|
321
|
+
this._wordEntityDetector = detector;
|
|
302
322
|
|
|
303
323
|
for (const model of this._keyworders.values()) {
|
|
304
|
-
|
|
324
|
+
// @ts-ignore
|
|
325
|
+
model.wordEntityDetector = detector;
|
|
326
|
+
model.maxWordCount = Math.max(model.maxWordCount, this._wordEntityDetectorMaxWordCount);
|
|
305
327
|
}
|
|
306
328
|
return this;
|
|
307
329
|
}
|
|
@@ -684,15 +706,12 @@ class Ai {
|
|
|
684
706
|
*/
|
|
685
707
|
preloadDetectors () {
|
|
686
708
|
if (this._wordEntityDetectorFactory === null || this._wordEntityDetector) {
|
|
687
|
-
return Promise.resolve();
|
|
709
|
+
return Promise.resolve(this._wordEntityDetector);
|
|
688
710
|
}
|
|
689
711
|
|
|
690
712
|
const promise = this._wordEntityDetectorFactory()
|
|
691
713
|
.then((detector) => {
|
|
692
|
-
this.
|
|
693
|
-
for (const model of this._keyworders.values()) {
|
|
694
|
-
model.wordEntityDetector = detector;
|
|
695
|
-
}
|
|
714
|
+
this.setWordEntityDetector(detector);
|
|
696
715
|
return detector;
|
|
697
716
|
})
|
|
698
717
|
.catch((e) => {
|
package/src/ChatGpt.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author David Menger
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const nodeFetch = require('node-fetch').default;
|
|
7
|
+
const util = require('util');
|
|
8
|
+
const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
|
|
9
|
+
|
|
10
|
+
/** @typedef {import('node-fetch').default} Fetch */
|
|
11
|
+
/** @typedef {import('./Request')} Request */
|
|
12
|
+
/** @typedef {import('./Responder')} Responder */
|
|
13
|
+
/** @typedef {import('./Responder').QuickReply} QuickReply */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} Transcript
|
|
17
|
+
* @prop {string} text
|
|
18
|
+
* @prop {boolean} fromBot
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} GeneralOptions
|
|
23
|
+
* @prop {Fetch} [fetch]
|
|
24
|
+
* @prop {string} [defaultUser]
|
|
25
|
+
* @prop {string} [openAiEndpoint]
|
|
26
|
+
* @prop {string} [apiVersion]
|
|
27
|
+
* @prop {string} [apiKey] // for microsoft services
|
|
28
|
+
* @prop {string} [authorization] // for chat gpt api
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** @typedef {'gpt-3.5-turbo'|'gpt-4'|'gpt-4-32k'|'gpt-3.5-turbo-16k'|string} ChatGPTModel */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} DefaultRequestOptions
|
|
35
|
+
* @prop {ChatGPTModel} [model]
|
|
36
|
+
* @prop {number} [presencePenalty=0.0]
|
|
37
|
+
* @prop {number} [requestTokens=256]
|
|
38
|
+
* @prop {number} [tokensLimit=4096]
|
|
39
|
+
* @prop {number} [temperature=1.0]
|
|
40
|
+
* @prop {number} [transcriptLength=-5]
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} OptionsExtension
|
|
45
|
+
* @prop {FNAnnotation[]} [functions]
|
|
46
|
+
*
|
|
47
|
+
* @typedef {OptionsExtension & DefaultRequestOptions} RequestOptions
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {GeneralOptions & DefaultRequestOptions} ChatGptOptions
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {object} Message
|
|
56
|
+
* @prop {'system'|'user'|'assistant'|string} role
|
|
57
|
+
* @prop {string} content
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {object} ChatGPTChoice
|
|
62
|
+
* @prop {'stop'|'length'|'function_call'|'content_filter'|null} finish_reason
|
|
63
|
+
* @prop {number} index
|
|
64
|
+
* @prop {Message} message
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {object} ChatGPTUsage
|
|
69
|
+
* @prop {number} completion_tokens
|
|
70
|
+
* @prop {number} prompt_tokens
|
|
71
|
+
* @prop {number} total_tokens
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @typedef {object} ChatGPTResponse
|
|
76
|
+
* @prop {ChatGPTChoice[]} choices
|
|
77
|
+
* @prop {number} created
|
|
78
|
+
* @prop {ChatGPTModel} model
|
|
79
|
+
* @prop {'text_completion'} object
|
|
80
|
+
* @prop {ChatGPTUsage} usage
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} Logger
|
|
85
|
+
* @prop {Function} log
|
|
86
|
+
* @prop {Function} error
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} SlicedAnnotation
|
|
91
|
+
* @prop {boolean} [sliced]
|
|
92
|
+
*
|
|
93
|
+
* @typedef {string[] & SlicedAnnotation} StringArrayWithSliced
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @typedef {object} ContinueReply
|
|
98
|
+
* @prop {string} title
|
|
99
|
+
* @prop {string} [action]
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @typedef {object} Persona
|
|
104
|
+
* @prop {string} [profile_pic_url]
|
|
105
|
+
* @prop {string} [name]
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {object} ReplyConfiguration
|
|
110
|
+
* @prop {string} [system]
|
|
111
|
+
* @prop {string} [annotation]
|
|
112
|
+
* @prop {ContinueReply|boolean} [continueReply]
|
|
113
|
+
* @prop {string|Persona} [persona]
|
|
114
|
+
* @prop {boolean} [anonymize=true]
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @typedef {object} FNScalarParam
|
|
119
|
+
* @prop {'string'|'number'|'boolean'} type
|
|
120
|
+
* @prop {string[]} [enum]
|
|
121
|
+
* @prop {string} [description]
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @typedef {object} FNArrayParam
|
|
126
|
+
* @prop {'array'} type
|
|
127
|
+
* @prop {string} [description]
|
|
128
|
+
* @prop {FNParam} items
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @typedef {object} FNObjectParam
|
|
133
|
+
* @prop {'object'} type
|
|
134
|
+
* @prop {{ [key: string]: FNParam }} properties
|
|
135
|
+
* @prop {string[]} [required]
|
|
136
|
+
* @prop {string} [description]
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
/** @typedef {FNScalarParam|FNObjectParam|FNArrayParam} FNParam */
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @typedef {object} FNAnnotation
|
|
143
|
+
* @prop {string} name
|
|
144
|
+
* @prop {string} description
|
|
145
|
+
* @prop {FNParam} parameters
|
|
146
|
+
*/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @class ChatGpt
|
|
150
|
+
*/
|
|
151
|
+
class ChatGpt {
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
*
|
|
155
|
+
* @param {ChatGptOptions} options
|
|
156
|
+
* @param {Logger} log
|
|
157
|
+
*/
|
|
158
|
+
constructor (options, log = console) {
|
|
159
|
+
const {
|
|
160
|
+
fetch = nodeFetch,
|
|
161
|
+
defaultUser = null,
|
|
162
|
+
openAiEndpoint = 'https://api.openai.com/v1',
|
|
163
|
+
apiKey,
|
|
164
|
+
authorization,
|
|
165
|
+
...rest
|
|
166
|
+
} = options;
|
|
167
|
+
|
|
168
|
+
this._apiKey = apiKey;
|
|
169
|
+
this._authorization = authorization;
|
|
170
|
+
|
|
171
|
+
this._fetch = fetch;
|
|
172
|
+
|
|
173
|
+
this._openAiEndpoint = openAiEndpoint;
|
|
174
|
+
|
|
175
|
+
this._defaultUser = defaultUser;
|
|
176
|
+
|
|
177
|
+
/** @type {Required<DefaultRequestOptions>} */
|
|
178
|
+
this._options = {
|
|
179
|
+
requestTokens: 256,
|
|
180
|
+
tokensLimit: 4096,
|
|
181
|
+
presencePenalty: 0.0, // -2.0-2.0
|
|
182
|
+
temperature: 1.0,
|
|
183
|
+
model: 'gpt-3.5-turbo',
|
|
184
|
+
transcriptLength: -5,
|
|
185
|
+
...rest
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
this._logger = log;
|
|
189
|
+
|
|
190
|
+
this.MSG_REPLACE = '#MSG-REPLACE#';
|
|
191
|
+
|
|
192
|
+
this.GPT_FLAG = 'gpt';
|
|
193
|
+
|
|
194
|
+
this._anonymizeRegexps = [
|
|
195
|
+
{ replacement: '@PHONE', regex: new RegExp(PHONE_REGEX.source, 'g') },
|
|
196
|
+
{ replacement: '@EMAIL', regex: new RegExp(EMAIL_REGEX.source, 'g') }
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_log (msg, ...args) {
|
|
201
|
+
if (this._logger === console) {
|
|
202
|
+
|
|
203
|
+
this._logger.log(msg, ...args.map((arg) => util.inspect(arg, {
|
|
204
|
+
showHidden: false, depth: null, colors: true
|
|
205
|
+
})));
|
|
206
|
+
} else {
|
|
207
|
+
this._logger.log(msg, ...args);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
*
|
|
213
|
+
* @param {Responder} res
|
|
214
|
+
* @param {number} limit
|
|
215
|
+
* @param {boolean} anonymize
|
|
216
|
+
* @returns {Promise<Transcript[]>}
|
|
217
|
+
*/
|
|
218
|
+
async getTranscript (
|
|
219
|
+
res,
|
|
220
|
+
limit = this._options.transcriptLength,
|
|
221
|
+
anonymize = true
|
|
222
|
+
) {
|
|
223
|
+
const onlyFlag = Math.sign(limit) === -1 ? 'gpt' : null;
|
|
224
|
+
const transcript = await res.getTranscript(Math.abs(limit), onlyFlag);
|
|
225
|
+
|
|
226
|
+
if (!anonymize) {
|
|
227
|
+
return transcript;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return transcript.map((t) => ({
|
|
231
|
+
...t,
|
|
232
|
+
text: this._anonymizeRegexps
|
|
233
|
+
.reduce((text, { replacement, regex }) => {
|
|
234
|
+
const replaced = text.replace(regex, replacement);
|
|
235
|
+
return replaced;
|
|
236
|
+
}, t.text)
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
*
|
|
242
|
+
* @param {string} content
|
|
243
|
+
* @param {string} [system]
|
|
244
|
+
* @param {Transcript[]} [transcript]
|
|
245
|
+
* @param {RequestOptions} [requestOptions]
|
|
246
|
+
* @param {string|Request} [user]
|
|
247
|
+
* @returns {Promise<ChatGPTChoice>}
|
|
248
|
+
*/
|
|
249
|
+
async request (content, system = null, transcript = [], requestOptions = {}, user = null) {
|
|
250
|
+
const {
|
|
251
|
+
requestTokens,
|
|
252
|
+
tokensLimit,
|
|
253
|
+
model,
|
|
254
|
+
presencePenalty,
|
|
255
|
+
temperature,
|
|
256
|
+
functions = []
|
|
257
|
+
} = {
|
|
258
|
+
...this._options,
|
|
259
|
+
...requestOptions
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const maxTokens = Math.min(requestTokens, tokensLimit);
|
|
263
|
+
|
|
264
|
+
let body;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
body = {
|
|
268
|
+
model,
|
|
269
|
+
frequency_penalty: 0,
|
|
270
|
+
presence_penalty: presencePenalty,
|
|
271
|
+
max_tokens: maxTokens,
|
|
272
|
+
temperature,
|
|
273
|
+
functions
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (typeof user === 'string') {
|
|
277
|
+
Object.assign(body, { user });
|
|
278
|
+
} else if (user) {
|
|
279
|
+
Object.assign(body, { user: `${user.pageId}|${user.senderId}` });
|
|
280
|
+
} else if (this._defaultUser) {
|
|
281
|
+
Object.assign(body, { user: this._defaultUser });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let total = (system ? system.length : 0)
|
|
285
|
+
+ maxTokens
|
|
286
|
+
+ content.length;
|
|
287
|
+
|
|
288
|
+
const ts = transcript.slice();
|
|
289
|
+
|
|
290
|
+
for (let i = ts.length - 1; i >= 0; i--) {
|
|
291
|
+
total += ts[i].text.length;
|
|
292
|
+
if (total > tokensLimit) {
|
|
293
|
+
ts.splice(i, 1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @type {Message[]} */
|
|
298
|
+
const messages = [
|
|
299
|
+
...(system ? [{ role: 'system', content: system }] : []),
|
|
300
|
+
...ts.map((t) => ({ role: t.fromBot ? 'assistant' : 'user', content: t.text })),
|
|
301
|
+
{ role: 'user', content }
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
Object.assign(body, { messages });
|
|
305
|
+
|
|
306
|
+
const apiUrl = `${this._openAiEndpoint}/chat/completions${this._apiKey ? '?api-version=2023-03-15-preview' : ''}`;
|
|
307
|
+
|
|
308
|
+
this._log('#GPT request', body);
|
|
309
|
+
|
|
310
|
+
const response = await this._fetch(apiUrl, {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: {
|
|
313
|
+
'Content-Type': 'application/json',
|
|
314
|
+
...(this._apiKey
|
|
315
|
+
? { 'api-key': this._apiKey }
|
|
316
|
+
: { Authorization: `Bearer ${this._authorization}` })
|
|
317
|
+
},
|
|
318
|
+
body: JSON.stringify(body)
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
/** @type {ChatGPTResponse} */
|
|
322
|
+
const data = await response.json();
|
|
323
|
+
|
|
324
|
+
if (response.status !== 200
|
|
325
|
+
|| !Array.isArray(data.choices)) {
|
|
326
|
+
const { status, statusText } = response;
|
|
327
|
+
|
|
328
|
+
this._logger.error('#GPT failed', {
|
|
329
|
+
status, statusText, data, body
|
|
330
|
+
});
|
|
331
|
+
throw new Error(`Chat GPT ${status}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const [choice] = data.choices;
|
|
335
|
+
|
|
336
|
+
this._log('#GPT response', { choice, data });
|
|
337
|
+
|
|
338
|
+
return choice;
|
|
339
|
+
} catch (e) {
|
|
340
|
+
this._logger.error('#GPT failed', e, body);
|
|
341
|
+
throw e;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
*
|
|
347
|
+
* @param {ChatGPTChoice} choice
|
|
348
|
+
* @param {string} [annotation]
|
|
349
|
+
* @returns {StringArrayWithSliced}
|
|
350
|
+
*/
|
|
351
|
+
toMessages (choice, annotation = null) {
|
|
352
|
+
let sliced = choice.finish_reason === 'length';
|
|
353
|
+
|
|
354
|
+
let filtered = choice.message.content
|
|
355
|
+
.replace(/\n\n/g, '\n')
|
|
356
|
+
.split(/\n+(?!-)/g)
|
|
357
|
+
.filter((t) => !!t.trim());
|
|
358
|
+
|
|
359
|
+
if (sliced && filtered.length > 1) {
|
|
360
|
+
filtered = filtered.slice(0, filtered.length - 1);
|
|
361
|
+
sliced = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
filtered = filtered
|
|
365
|
+
.map((t) => {
|
|
366
|
+
let trim = t.trim();
|
|
367
|
+
|
|
368
|
+
if (annotation) {
|
|
369
|
+
// replace the annotation first
|
|
370
|
+
|
|
371
|
+
const replacements = annotation.split(this.MSG_REPLACE);
|
|
372
|
+
|
|
373
|
+
const last = replacements.length > 1
|
|
374
|
+
? replacements.length - 1
|
|
375
|
+
: replacements.length;
|
|
376
|
+
|
|
377
|
+
replacements.forEach((r, i) => {
|
|
378
|
+
const foundI = trim.indexOf(r.trim(), i === last
|
|
379
|
+
? trim.length - r.trim().length
|
|
380
|
+
: 0);
|
|
381
|
+
|
|
382
|
+
if (foundI === -1
|
|
383
|
+
|| (i === 0 && foundI > 0)
|
|
384
|
+
|| (i === last && (trim.length - foundI - r.length) > 0)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
trim = trim.replace(r.trim(), '').trim();
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (annotation && annotation.includes(this.MSG_REPLACE)) {
|
|
393
|
+
trim = annotation.replace(this.MSG_REPLACE, trim);
|
|
394
|
+
} else if (annotation) {
|
|
395
|
+
trim = `${annotation} ${trim}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return trim;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return Object.assign(filtered, { sliced });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
*
|
|
406
|
+
* @param {Responder} res
|
|
407
|
+
* @param {string[]} messages
|
|
408
|
+
* @param {QuickReply[]} [quickReplies]
|
|
409
|
+
*/
|
|
410
|
+
sendMessages (res, messages, quickReplies = null) {
|
|
411
|
+
messages.forEach((text, i) => {
|
|
412
|
+
const addQuickReply = i === (messages.length - 1);
|
|
413
|
+
res.text(text, addQuickReply ? quickReplies : null);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
*
|
|
419
|
+
* @param {Request} req
|
|
420
|
+
* @param {Responder} res
|
|
421
|
+
* @param {ReplyConfiguration} [replyConfig]
|
|
422
|
+
* @param {RequestOptions} [options]
|
|
423
|
+
*/
|
|
424
|
+
async respond (req, res, replyConfig = {}, options = {}) {
|
|
425
|
+
res.setFlag(this.GPT_FLAG);
|
|
426
|
+
res.typingOn();
|
|
427
|
+
|
|
428
|
+
const {
|
|
429
|
+
transcriptLength
|
|
430
|
+
} = {
|
|
431
|
+
...this._options,
|
|
432
|
+
...options
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const { persona, continueReply, anonymize } = replyConfig;
|
|
436
|
+
|
|
437
|
+
const content = req.text();
|
|
438
|
+
|
|
439
|
+
const transcript = await this.getTranscript(res, transcriptLength, anonymize);
|
|
440
|
+
const choice = await this.request(content, replyConfig.system, transcript, options);
|
|
441
|
+
|
|
442
|
+
const messages = this.toMessages(choice, replyConfig.annotation);
|
|
443
|
+
|
|
444
|
+
if (typeof persona === 'string') {
|
|
445
|
+
res.setPersona({ name: persona });
|
|
446
|
+
} else if (persona) {
|
|
447
|
+
res.setPersona(persona);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const { sliced } = messages;
|
|
451
|
+
|
|
452
|
+
let qrs;
|
|
453
|
+
|
|
454
|
+
if (!continueReply) {
|
|
455
|
+
qrs = null;
|
|
456
|
+
} else if (continueReply === true) {
|
|
457
|
+
qrs = [];
|
|
458
|
+
} else if (typeof continueReply === 'object') {
|
|
459
|
+
qrs = [{
|
|
460
|
+
title: continueReply.title,
|
|
461
|
+
action: continueReply.action || res.currentAction()
|
|
462
|
+
}];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (messages.length === 0) {
|
|
466
|
+
const err = new Error('#GPT nothing to send');
|
|
467
|
+
this._logger.error('#GPT nothing to send', err, { choice, content });
|
|
468
|
+
throw err;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this.sendMessages(res, messages, qrs);
|
|
472
|
+
|
|
473
|
+
if (persona) {
|
|
474
|
+
res.setPersona({ name: null });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
messages,
|
|
479
|
+
sliced,
|
|
480
|
+
choice
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
module.exports = ChatGpt;
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
+
const { EMAIL_REGEX } = require('./regexps');
|
|
7
|
+
|
|
6
8
|
/** @typedef {import('../wingbot/CustomEntityDetectionModel').EntityDetector} EntityDetector */
|
|
7
9
|
/** @typedef {import('../wingbot/CustomEntityDetectionModel').DetectorOptions} DetectorOptions */
|
|
8
10
|
|
|
9
11
|
/** @type {[string,EntityDetector|RegExp,DetectorOptions]} */
|
|
10
|
-
module.exports = [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
module.exports = [
|
|
13
|
+
'email',
|
|
14
|
+
EMAIL_REGEX,
|
|
15
|
+
{
|
|
16
|
+
anonymize: true,
|
|
17
|
+
clearOverlaps: true
|
|
18
|
+
}
|
|
19
|
+
];
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
+
const { PHONE_REGEX } = require('./regexps');
|
|
7
|
+
|
|
6
8
|
/** @typedef {import('../wingbot/CustomEntityDetectionModel').EntityDetector} EntityDetector */
|
|
7
9
|
/** @typedef {import('../wingbot/CustomEntityDetectionModel').DetectorOptions} DetectorOptions */
|
|
8
10
|
|
|
9
11
|
/** @type {[string,EntityDetector|RegExp,DetectorOptions]} */
|
|
10
12
|
module.exports = [
|
|
11
13
|
'phone',
|
|
12
|
-
|
|
14
|
+
PHONE_REGEX,
|
|
13
15
|
{
|
|
14
16
|
anonymize: true,
|
|
15
17
|
clearOverlaps: true,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author David Menger
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const PHONE_REGEX = /((00|\+)[\s-]?[0-9]{1,4}[\s-]?)?([0-9]{3,4}[\s-]?([0-9]{2,3}[\s-]?[0-9]{2}[\s-]?[0-9]{2,3}|[0-9]{3,4}[\s-]?[0-9]{3,4}))(?=(\s|$|[,!.?\-:]))/;
|
|
7
|
+
const EMAIL_REGEX = /(?<=(\s|^|:))[a-zA-Z0-9!#$%&'*+\-=?^_`{|}~"][^@:\s]*@[^.@\s]+\.[^@\s,]+/;
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
PHONE_REGEX,
|
|
11
|
+
EMAIL_REGEX
|
|
12
|
+
};
|
|
@@ -68,6 +68,7 @@ const { iterateThroughWords } = require('../utils/ai');
|
|
|
68
68
|
* @param {DetectedEntity[]} entities
|
|
69
69
|
* @param {number} startIndex
|
|
70
70
|
* @param {string} prefix
|
|
71
|
+
* @returns {DetectedEntity[]}
|
|
71
72
|
*/
|
|
72
73
|
|
|
73
74
|
/**
|
|
@@ -403,7 +404,8 @@ class CustomEntityDetectionModel {
|
|
|
403
404
|
let entities = prevEnts.slice();
|
|
404
405
|
if (this.wordEntityDetector) {
|
|
405
406
|
for (const [s, startIndex] of iterateThroughWords(text, this.maxWordCount)) {
|
|
406
|
-
this.wordEntityDetector(s,
|
|
407
|
+
const ents = this.wordEntityDetector(s, prevEnts, startIndex, this.prefix);
|
|
408
|
+
entities.push(...ents);
|
|
407
409
|
}
|
|
408
410
|
}
|
|
409
411
|
|