wingbot 3.66.3 → 3.67.0
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 +92 -9
- package/src/ChatGpt.js +434 -0
- package/src/Processor.js +1 -0
- package/src/systemEntities/email.js +10 -4
- package/src/systemEntities/phone.js +3 -1
- package/src/systemEntities/regexps.js +12 -0
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
|
@@ -10,8 +10,6 @@ const { deepEqual } = require('./utils/deepMapTools');
|
|
|
10
10
|
const systemEntities = require('./systemEntities');
|
|
11
11
|
const CustomEntityDetectionModel = require('./wingbot/CustomEntityDetectionModel');
|
|
12
12
|
|
|
13
|
-
const DEFAULT_PREFIX = 'default';
|
|
14
|
-
|
|
15
13
|
let uq = 1;
|
|
16
14
|
|
|
17
15
|
/** @typedef {import('./AiMatching').Compare} Compare */
|
|
@@ -49,9 +47,15 @@ let uq = 1;
|
|
|
49
47
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').Phrases} Phrases */
|
|
50
48
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').EntityDetector} EntityDetector */
|
|
51
49
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').DetectorOptions} DetectorOptions */
|
|
50
|
+
/** @typedef {import('./wingbot/CustomEntityDetectionModel').Entity} Entity */
|
|
52
51
|
// eslint-disable-next-line max-len
|
|
53
52
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').WordEntityDetector} WordEntityDetector */
|
|
54
53
|
|
|
54
|
+
/**
|
|
55
|
+
* @callback WordEntityDetectorFactory
|
|
56
|
+
* @returns {Promise<WordEntityDetector>}
|
|
57
|
+
*/
|
|
58
|
+
|
|
55
59
|
/** @typedef {[string,EntityDetector|RegExp,DetectorOptions]} DetectorArgs */
|
|
56
60
|
|
|
57
61
|
/**
|
|
@@ -76,7 +80,13 @@ class Ai {
|
|
|
76
80
|
|
|
77
81
|
/**
|
|
78
82
|
* @private
|
|
79
|
-
* @type {
|
|
83
|
+
* @type {WordEntityDetectorFactory}
|
|
84
|
+
*/
|
|
85
|
+
this._wordEntityDetectorFactory = null;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @private
|
|
89
|
+
* @type {WordEntityDetector|Promise<WordEntityDetector>}
|
|
80
90
|
*/
|
|
81
91
|
this._wordEntityDetector = null;
|
|
82
92
|
|
|
@@ -140,6 +150,37 @@ class Ai {
|
|
|
140
150
|
* @type {AiMatching}
|
|
141
151
|
*/
|
|
142
152
|
this.matcher = new AiMatching(this);
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @type {string}
|
|
156
|
+
*/
|
|
157
|
+
this.DEFAULT_PREFIX = 'default';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
*
|
|
162
|
+
* @param {string} text
|
|
163
|
+
* @param {string|Request} prefix
|
|
164
|
+
* @returns {Promise<Entity[]>}
|
|
165
|
+
*/
|
|
166
|
+
async detectEntities (text, prefix = this.DEFAULT_PREFIX) {
|
|
167
|
+
let model;
|
|
168
|
+
|
|
169
|
+
if (typeof prefix === 'string') {
|
|
170
|
+
model = this._keyworders.get(prefix);
|
|
171
|
+
} else {
|
|
172
|
+
const usePrefix = this.getPrefix(this.DEFAULT_PREFIX, prefix);
|
|
173
|
+
model = this._keyworders.get(usePrefix);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!model) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const entities = await model.resolveEntities(text);
|
|
181
|
+
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
return entities;
|
|
143
184
|
}
|
|
144
185
|
|
|
145
186
|
/**
|
|
@@ -192,7 +233,7 @@ class Ai {
|
|
|
192
233
|
* @returns {T}
|
|
193
234
|
* @memberOf Ai
|
|
194
235
|
*/
|
|
195
|
-
register (model, prefix =
|
|
236
|
+
register (model, prefix = this.DEFAULT_PREFIX) {
|
|
196
237
|
/** @type {T} */
|
|
197
238
|
let modelObj;
|
|
198
239
|
|
|
@@ -210,7 +251,10 @@ class Ai {
|
|
|
210
251
|
|
|
211
252
|
this._keyworders.set(prefix, modelObj);
|
|
212
253
|
|
|
213
|
-
|
|
254
|
+
if (typeof this._wordEntityDetector === 'function') {
|
|
255
|
+
modelObj.wordEntityDetector = this._wordEntityDetector;
|
|
256
|
+
}
|
|
257
|
+
|
|
214
258
|
for (const entityArgs of this._detectors.values()) {
|
|
215
259
|
modelObj.setEntityDetector(...entityArgs);
|
|
216
260
|
}
|
|
@@ -245,9 +289,15 @@ class Ai {
|
|
|
245
289
|
|
|
246
290
|
/**
|
|
247
291
|
*
|
|
248
|
-
* @param {WordEntityDetector} wordEntityDetector
|
|
292
|
+
* @param {WordEntityDetector|WordEntityDetectorFactory} wordEntityDetector
|
|
249
293
|
*/
|
|
250
294
|
setWordEntityDetector (wordEntityDetector) {
|
|
295
|
+
if (wordEntityDetector.length === 0) {
|
|
296
|
+
// @ts-ignore
|
|
297
|
+
this._wordEntityDetectorFactory = wordEntityDetector;
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
|
|
251
301
|
this._wordEntityDetector = wordEntityDetector;
|
|
252
302
|
|
|
253
303
|
for (const model of this._keyworders.values()) {
|
|
@@ -286,7 +336,7 @@ class Ai {
|
|
|
286
336
|
*
|
|
287
337
|
* @param {string} [prefix]
|
|
288
338
|
*/
|
|
289
|
-
deregister (prefix =
|
|
339
|
+
deregister (prefix = this.DEFAULT_PREFIX) {
|
|
290
340
|
this._keyworders.delete(prefix);
|
|
291
341
|
}
|
|
292
342
|
|
|
@@ -298,7 +348,7 @@ class Ai {
|
|
|
298
348
|
* @returns {CustomEntityDetectionModel}
|
|
299
349
|
* @memberOf Ai
|
|
300
350
|
*/
|
|
301
|
-
getModel (prefix =
|
|
351
|
+
getModel (prefix = this.DEFAULT_PREFIX) {
|
|
302
352
|
const model = this._keyworders.get(prefix);
|
|
303
353
|
if (!model) {
|
|
304
354
|
throw new Error(`Model ${prefix} not registered yet. Register the model first.`);
|
|
@@ -592,7 +642,8 @@ class Ai {
|
|
|
592
642
|
};
|
|
593
643
|
}
|
|
594
644
|
|
|
595
|
-
|
|
645
|
+
// eslint-disable-next-line max-len
|
|
646
|
+
_getModelForRequest (req, isConfident = req.isConfidentInput(), defaultModel = this.DEFAULT_PREFIX) {
|
|
596
647
|
if (isConfident) {
|
|
597
648
|
return null;
|
|
598
649
|
}
|
|
@@ -627,6 +678,35 @@ class Ai {
|
|
|
627
678
|
};
|
|
628
679
|
}
|
|
629
680
|
|
|
681
|
+
/**
|
|
682
|
+
*
|
|
683
|
+
* @returns {Promise}
|
|
684
|
+
*/
|
|
685
|
+
preloadDetectors () {
|
|
686
|
+
if (this._wordEntityDetectorFactory === null || this._wordEntityDetector) {
|
|
687
|
+
return Promise.resolve();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const promise = this._wordEntityDetectorFactory()
|
|
691
|
+
.then((detector) => {
|
|
692
|
+
this._wordEntityDetector = detector;
|
|
693
|
+
for (const model of this._keyworders.values()) {
|
|
694
|
+
model.wordEntityDetector = detector;
|
|
695
|
+
}
|
|
696
|
+
return detector;
|
|
697
|
+
})
|
|
698
|
+
.catch((e) => {
|
|
699
|
+
// eslint-disable-next-line no-console
|
|
700
|
+
console.error('AI.preloadDetectors FAILED', e);
|
|
701
|
+
this._wordEntityDetector = null;
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// @ts-ignore
|
|
705
|
+
this._wordEntityDetector = promise;
|
|
706
|
+
|
|
707
|
+
return promise;
|
|
708
|
+
}
|
|
709
|
+
|
|
630
710
|
/**
|
|
631
711
|
*
|
|
632
712
|
* @param {Request} req
|
|
@@ -710,6 +790,7 @@ class Ai {
|
|
|
710
790
|
req.intents = [];
|
|
711
791
|
return;
|
|
712
792
|
}
|
|
793
|
+
|
|
713
794
|
await this._loadIntents(req, res, model);
|
|
714
795
|
} else {
|
|
715
796
|
req.intents = [];
|
|
@@ -734,6 +815,8 @@ class Ai {
|
|
|
734
815
|
}
|
|
735
816
|
}
|
|
736
817
|
|
|
818
|
+
await this.preloadDetectors();
|
|
819
|
+
|
|
737
820
|
const texts = req.textAlternatives()
|
|
738
821
|
.filter((alt) => alt.score >= this.sttScoreThreshold)
|
|
739
822
|
.slice(0, this.sttMaxAlternatives);
|
package/src/ChatGpt.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author David Menger
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const nodeFetch = require('node-fetch').default;
|
|
7
|
+
const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('node-fetch').default} Fetch */
|
|
10
|
+
/** @typedef {import('./Request')} Request */
|
|
11
|
+
/** @typedef {import('./Responder')} Responder */
|
|
12
|
+
/** @typedef {import('./Responder').QuickReply} QuickReply */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} Transcript
|
|
16
|
+
* @prop {string} text
|
|
17
|
+
* @prop {boolean} fromBot
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} GeneralOptions
|
|
22
|
+
* @prop {Fetch} [fetch]
|
|
23
|
+
* @prop {string} [defaultUser]
|
|
24
|
+
* @prop {string} [openAiEndpoint]
|
|
25
|
+
* @prop {string} [apiVersion]
|
|
26
|
+
* @prop {string} [apiKey] // for microsoft services
|
|
27
|
+
* @prop {string} [authorization] // for chat gpt api
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** @typedef {'gpt-3.5-turbo'|'gpt-4'|'gpt-4-32k'|'gpt-3.5-turbo-16k'|string} ChatGPTModel */
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} RequestOptions
|
|
34
|
+
* @prop {ChatGPTModel} [model]
|
|
35
|
+
* @prop {number} [presencePenalty=0.0]
|
|
36
|
+
* @prop {number} [requestTokens=256]
|
|
37
|
+
* @prop {number} [tokensLimit=4096]
|
|
38
|
+
* @prop {number} [temperature=1.0]
|
|
39
|
+
* @prop {number} [transcriptLength=-5]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {GeneralOptions & RequestOptions} ChatGptOptions
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} Message
|
|
48
|
+
* @prop {'system'|'user'|'assistant'|string} role
|
|
49
|
+
* @prop {string} content
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} ChatGPTChoice
|
|
54
|
+
* @prop {'stop'|'length'|'function_call'|'content_filter'|null} finish_reason
|
|
55
|
+
* @prop {number} index
|
|
56
|
+
* @prop {Message} message
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {object} ChatGPTUsage
|
|
61
|
+
* @prop {number} completion_tokens
|
|
62
|
+
* @prop {number} prompt_tokens
|
|
63
|
+
* @prop {number} total_tokens
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} ChatGPTResponse
|
|
68
|
+
* @prop {ChatGPTChoice[]} choices
|
|
69
|
+
* @prop {number} created
|
|
70
|
+
* @prop {ChatGPTModel} model
|
|
71
|
+
* @prop {'text_completion'} object
|
|
72
|
+
* @prop {ChatGPTUsage} usage
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @typedef {object} Logger
|
|
77
|
+
* @prop {Function} log
|
|
78
|
+
* @prop {Function} error
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {object} SlicedAnnotation
|
|
83
|
+
* @prop {boolean} [sliced]
|
|
84
|
+
*
|
|
85
|
+
* @typedef {string[] & SlicedAnnotation} StringArrayWithSliced
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {object} ContinueReply
|
|
90
|
+
* @prop {string} title
|
|
91
|
+
* @prop {string} [action]
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @typedef {object} Persona
|
|
96
|
+
* @prop {string} [profile_pic_url]
|
|
97
|
+
* @prop {string} [name]
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @typedef {object} ReplyConfiguration
|
|
102
|
+
* @prop {string} [system]
|
|
103
|
+
* @prop {string} [annotation]
|
|
104
|
+
* @prop {ContinueReply|boolean} [continueReply]
|
|
105
|
+
* @prop {string|Persona} [persona]
|
|
106
|
+
* @prop {boolean} [anonymize=true]
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @class ChatGpt
|
|
111
|
+
*/
|
|
112
|
+
class ChatGpt {
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
*
|
|
116
|
+
* @param {ChatGptOptions} options
|
|
117
|
+
* @param {Logger} log
|
|
118
|
+
*/
|
|
119
|
+
constructor (options, log = console) {
|
|
120
|
+
const {
|
|
121
|
+
fetch = nodeFetch,
|
|
122
|
+
defaultUser = null,
|
|
123
|
+
openAiEndpoint = 'https://api.openai.com/v1',
|
|
124
|
+
apiKey,
|
|
125
|
+
authorization,
|
|
126
|
+
...rest
|
|
127
|
+
} = options;
|
|
128
|
+
|
|
129
|
+
this._apiKey = apiKey;
|
|
130
|
+
this._authorization = authorization;
|
|
131
|
+
|
|
132
|
+
this._fetch = fetch;
|
|
133
|
+
|
|
134
|
+
this._openAiEndpoint = openAiEndpoint;
|
|
135
|
+
|
|
136
|
+
this._defaultUser = defaultUser;
|
|
137
|
+
|
|
138
|
+
/** @type {Required<RequestOptions>} */
|
|
139
|
+
this._options = {
|
|
140
|
+
requestTokens: 256,
|
|
141
|
+
tokensLimit: 4096,
|
|
142
|
+
presencePenalty: 0.0, // -2.0-2.0
|
|
143
|
+
temperature: 1.0,
|
|
144
|
+
model: 'gpt-3.5-turbo',
|
|
145
|
+
transcriptLength: -5,
|
|
146
|
+
...rest
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
this._log = log;
|
|
150
|
+
|
|
151
|
+
this.MSG_REPLACE = '#MSG-REPLACE#';
|
|
152
|
+
|
|
153
|
+
this.GPT_FLAG = 'gpt';
|
|
154
|
+
|
|
155
|
+
this._anonymizeRegexps = [
|
|
156
|
+
{ replacement: '@PHONE', regex: new RegExp(PHONE_REGEX.source, 'g') },
|
|
157
|
+
{ replacement: '@EMAIL', regex: new RegExp(EMAIL_REGEX.source, 'g') }
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
*
|
|
163
|
+
* @param {Responder} res
|
|
164
|
+
* @param {number} limit
|
|
165
|
+
* @param {boolean} anonymize
|
|
166
|
+
* @returns {Promise<Transcript[]>}
|
|
167
|
+
*/
|
|
168
|
+
async getTranscript (
|
|
169
|
+
res,
|
|
170
|
+
limit = this._options.transcriptLength,
|
|
171
|
+
anonymize = true
|
|
172
|
+
) {
|
|
173
|
+
const onlyFlag = Math.sign(limit) === -1 ? 'gpt' : null;
|
|
174
|
+
const transcript = await res.getTranscript(Math.abs(limit), onlyFlag);
|
|
175
|
+
|
|
176
|
+
if (!anonymize) {
|
|
177
|
+
return transcript;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return transcript.map((t) => ({
|
|
181
|
+
...t,
|
|
182
|
+
text: this._anonymizeRegexps
|
|
183
|
+
.reduce((text, { replacement, regex }) => {
|
|
184
|
+
const replaced = text.replace(regex, replacement);
|
|
185
|
+
return replaced;
|
|
186
|
+
}, t.text)
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
*
|
|
192
|
+
* @param {string} content
|
|
193
|
+
* @param {string} [system]
|
|
194
|
+
* @param {Transcript[]} [transcript]
|
|
195
|
+
* @param {RequestOptions} [requestOptions]
|
|
196
|
+
* @param {string|Request} [user]
|
|
197
|
+
* @returns {Promise<ChatGPTChoice>}
|
|
198
|
+
*/
|
|
199
|
+
async request (content, system = null, transcript = [], requestOptions = {}, user = null) {
|
|
200
|
+
const {
|
|
201
|
+
requestTokens,
|
|
202
|
+
tokensLimit,
|
|
203
|
+
model,
|
|
204
|
+
presencePenalty,
|
|
205
|
+
temperature
|
|
206
|
+
} = {
|
|
207
|
+
...this._options,
|
|
208
|
+
...requestOptions
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const maxTokens = Math.min(requestTokens, tokensLimit);
|
|
212
|
+
|
|
213
|
+
let body;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
body = {
|
|
217
|
+
model,
|
|
218
|
+
frequency_penalty: 0,
|
|
219
|
+
presence_penalty: presencePenalty,
|
|
220
|
+
max_tokens: maxTokens,
|
|
221
|
+
temperature
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (typeof user === 'string') {
|
|
225
|
+
Object.assign(body, { user });
|
|
226
|
+
} else if (user) {
|
|
227
|
+
Object.assign(body, { user: `${user.pageId}|${user.senderId}` });
|
|
228
|
+
} else if (this._defaultUser) {
|
|
229
|
+
Object.assign(body, { user: this._defaultUser });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let total = (system ? system.length : 0)
|
|
233
|
+
+ maxTokens
|
|
234
|
+
+ content.length;
|
|
235
|
+
|
|
236
|
+
const ts = transcript.slice();
|
|
237
|
+
|
|
238
|
+
for (let i = ts.length - 1; i >= 0; i--) {
|
|
239
|
+
total += ts[i].text.length;
|
|
240
|
+
if (total > tokensLimit) {
|
|
241
|
+
ts.splice(i, 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** @type {Message[]} */
|
|
246
|
+
const messages = [
|
|
247
|
+
...(system ? [{ role: 'system', content: system }] : []),
|
|
248
|
+
...ts.map((t) => ({ role: t.fromBot ? 'assistant' : 'user', content: t.text })),
|
|
249
|
+
{ role: 'user', content }
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
Object.assign(body, { messages });
|
|
253
|
+
|
|
254
|
+
const apiUrl = `${this._openAiEndpoint}/chat/completions${this._apiKey ? '?api-version=2023-03-15-preview' : ''}`;
|
|
255
|
+
|
|
256
|
+
this._log.log('#GPT request', body);
|
|
257
|
+
|
|
258
|
+
const response = await this._fetch(apiUrl, {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
'Content-Type': 'application/json',
|
|
262
|
+
...(this._apiKey
|
|
263
|
+
? { 'api-key': this._apiKey }
|
|
264
|
+
: { Authorization: `Bearer ${this._authorization}` })
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify(body)
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
/** @type {ChatGPTResponse} */
|
|
270
|
+
const data = await response.json();
|
|
271
|
+
|
|
272
|
+
if (response.status !== 200
|
|
273
|
+
|| !Array.isArray(data.choices)) {
|
|
274
|
+
const { status, statusText } = response;
|
|
275
|
+
|
|
276
|
+
this._log.error('#GPT failed', {
|
|
277
|
+
status, statusText, data, body
|
|
278
|
+
});
|
|
279
|
+
throw new Error(`Chat GPT ${status}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const [choice] = data.choices;
|
|
283
|
+
|
|
284
|
+
this._log.log('#GPT response', { choice, data });
|
|
285
|
+
|
|
286
|
+
return choice;
|
|
287
|
+
} catch (e) {
|
|
288
|
+
this._log.error('#GPT failed', e, body);
|
|
289
|
+
throw e;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
*
|
|
295
|
+
* @param {ChatGPTChoice} choice
|
|
296
|
+
* @param {string} [annotation]
|
|
297
|
+
* @returns {StringArrayWithSliced}
|
|
298
|
+
*/
|
|
299
|
+
toMessages (choice, annotation = null) {
|
|
300
|
+
let sliced = choice.finish_reason === 'length';
|
|
301
|
+
|
|
302
|
+
let filtered = choice.message.content
|
|
303
|
+
.replace(/\n\n/g, '\n')
|
|
304
|
+
.split(/\n+(?!-)/g)
|
|
305
|
+
.filter((t) => !!t.trim());
|
|
306
|
+
|
|
307
|
+
if (sliced && filtered.length > 1) {
|
|
308
|
+
filtered = filtered.slice(0, filtered.length - 1);
|
|
309
|
+
sliced = true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
filtered = filtered
|
|
313
|
+
.map((t) => {
|
|
314
|
+
let trim = t.trim();
|
|
315
|
+
|
|
316
|
+
if (annotation) {
|
|
317
|
+
// replace the annotation first
|
|
318
|
+
|
|
319
|
+
const replacements = annotation.split(this.MSG_REPLACE);
|
|
320
|
+
|
|
321
|
+
const last = replacements.length > 1
|
|
322
|
+
? replacements.length - 1
|
|
323
|
+
: replacements.length;
|
|
324
|
+
|
|
325
|
+
replacements.forEach((r, i) => {
|
|
326
|
+
const foundI = trim.indexOf(r.trim(), i === last
|
|
327
|
+
? trim.length - r.trim().length
|
|
328
|
+
: 0);
|
|
329
|
+
|
|
330
|
+
if (foundI === -1
|
|
331
|
+
|| (i === 0 && foundI > 0)
|
|
332
|
+
|| (i === last && (trim.length - foundI - r.length) > 0)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
trim = trim.replace(r.trim(), '').trim();
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (annotation && annotation.includes(this.MSG_REPLACE)) {
|
|
341
|
+
trim = annotation.replace(this.MSG_REPLACE, trim);
|
|
342
|
+
} else if (annotation) {
|
|
343
|
+
trim = `${annotation} ${trim}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return trim;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return Object.assign(filtered, { sliced });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
*
|
|
354
|
+
* @param {Responder} res
|
|
355
|
+
* @param {string[]} messages
|
|
356
|
+
* @param {QuickReply[]} [quickReplies]
|
|
357
|
+
*/
|
|
358
|
+
sendMessages (res, messages, quickReplies = null) {
|
|
359
|
+
messages.forEach((text, i) => {
|
|
360
|
+
const addQuickReply = i === (messages.length - 1);
|
|
361
|
+
res.text(text, addQuickReply ? quickReplies : null);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
*
|
|
367
|
+
* @param {Request} req
|
|
368
|
+
* @param {Responder} res
|
|
369
|
+
* @param {ReplyConfiguration} [replyConfig]
|
|
370
|
+
* @param {RequestOptions} [options]
|
|
371
|
+
*/
|
|
372
|
+
async respond (req, res, replyConfig = {}, options = {}) {
|
|
373
|
+
res.setFlag(this.GPT_FLAG);
|
|
374
|
+
res.typingOn();
|
|
375
|
+
|
|
376
|
+
const {
|
|
377
|
+
transcriptLength
|
|
378
|
+
} = {
|
|
379
|
+
...this._options,
|
|
380
|
+
...options
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const { persona, continueReply, anonymize } = replyConfig;
|
|
384
|
+
|
|
385
|
+
const content = req.text();
|
|
386
|
+
|
|
387
|
+
const transcript = await this.getTranscript(res, transcriptLength, anonymize);
|
|
388
|
+
const choice = await this.request(content, replyConfig.system, transcript, options);
|
|
389
|
+
|
|
390
|
+
const messages = this.toMessages(choice, replyConfig.annotation);
|
|
391
|
+
|
|
392
|
+
if (typeof persona === 'string') {
|
|
393
|
+
res.setPersona({ name: persona });
|
|
394
|
+
} else if (persona) {
|
|
395
|
+
res.setPersona(persona);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const { sliced } = messages;
|
|
399
|
+
|
|
400
|
+
let qrs;
|
|
401
|
+
|
|
402
|
+
if (!continueReply) {
|
|
403
|
+
qrs = null;
|
|
404
|
+
} else if (continueReply === true) {
|
|
405
|
+
qrs = [];
|
|
406
|
+
} else if (typeof continueReply === 'object') {
|
|
407
|
+
qrs = [{
|
|
408
|
+
title: continueReply.title,
|
|
409
|
+
action: continueReply.action || res.currentAction()
|
|
410
|
+
}];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (messages.length === 0) {
|
|
414
|
+
const err = new Error('#GPT nothing to send');
|
|
415
|
+
this._log.error('#GPT nothing to send', err, { choice, content });
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this.sendMessages(res, messages, qrs);
|
|
420
|
+
|
|
421
|
+
if (persona) {
|
|
422
|
+
res.setPersona({ name: null });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
messages,
|
|
427
|
+
sliced,
|
|
428
|
+
choice
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = ChatGpt;
|
package/src/Processor.js
CHANGED
|
@@ -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
|
+
};
|