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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.66.4",
3
+ "version": "3.67.1",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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 = null,
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') || 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') || 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 limit = parseInt(compileWithState(req, res, params.limit).trim() || '10', 10) || 10;
34
+ const requestTokens = parseFloat(compileWithState(req, res, params.maxTokens).trim() || '512') || undefined;
40
35
 
41
- const user = `${req.pageId}|${req.senderId}`;
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
- let body;
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
- res.setFlag('gpt');
60
- res.typingOn();
61
-
62
- body = {
63
- model,
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
- this._wordEntityDetector = wordEntityDetector;
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
- model.wordEntityDetector = wordEntityDetector;
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._wordEntityDetector = detector;
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 = ['email', /(?<=(\s|^|:))[a-zA-Z0-9!#$%&'*+\-=?^_`{|}~"][^@:\s]*@[^.@\s]+\.[^@\s,]+/, {
11
- anonymize: true,
12
- clearOverlaps: true
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
- /((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|$|[,!.?\-:]))/,
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, entities, startIndex, this.prefix);
407
+ const ents = this.wordEntityDetector(s, prevEnts, startIndex, this.prefix);
408
+ entities.push(...ents);
407
409
  }
408
410
  }
409
411