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 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.3",
3
+ "version": "3.67.0",
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
@@ -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 {WordEntityDetector}
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 = 'default') {
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
- modelObj.wordEntityDetector = this._wordEntityDetector;
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 = 'default') {
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 = 'default') {
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
- _getModelForRequest (req, isConfident = req.isConfidentInput(), defaultModel = DEFAULT_PREFIX) {
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
@@ -333,6 +333,7 @@ class Processor extends EventEmitter {
333
333
 
334
334
  async _preload () {
335
335
  return Promise.all([
336
+ Ai.ai.preloadDetectors(),
336
337
  // @ts-ignore
337
338
  this.reducer && typeof this.reducer.preload === 'function'
338
339
  // @ts-ignore
@@ -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
+ };