wingbot 3.53.4 → 3.54.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wingbot",
3
- "version": "3.53.4",
3
+ "version": "3.54.0",
4
4
  "description": "Enterprise Messaging Bot Conversation Engine",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1 @@
1
+ > This plugin allows you to use chatGPT
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @author wingbot.ai
3
+ */
4
+ 'use strict';
5
+
6
+ const fetch = require('node-fetch').default;
7
+ const compileWithState = require('../../src/utils/compileWithState');
8
+
9
+ function chatgptPlugin (params) {
10
+
11
+ async function chatgpt (req, res) {
12
+ const content = req.text();
13
+
14
+ const token = compileWithState(req, res, params.token).trim();
15
+
16
+ // gpt-3.5-turbo-0301 gpt-3.5-turbo
17
+ const model = compileWithState(req, res, params.model) || 'gpt-3.5-turbo';
18
+
19
+ const system = compileWithState(req, res, params.system).trim();
20
+
21
+ // 0 - 2
22
+ const temperature = parseFloat(compileWithState(req, res, params.temperature).trim().replace(',', '.') || '1') || 1;
23
+ // presence_penalty between -2.0 and 2.0
24
+ const presence = parseFloat(compileWithState(req, res, params.presence).trim().replace(',', '.') || '0') || 0;
25
+
26
+ const maxTokens = parseFloat(compileWithState(req, res, params.maxTokens).trim() || '512') || 512;
27
+
28
+ const limit = parseInt(compileWithState(req, res, params.maxTokens).trim() || '10', 10) || 10;
29
+
30
+ const user = `${req.pageId}|${req.senderId}`;
31
+
32
+ const systemAfter = compileWithState(req, res, params.systemAfter).trim();
33
+
34
+ let body;
35
+
36
+ try {
37
+ res.typingOn();
38
+
39
+ body = {
40
+ model,
41
+ frequency_penalty: 0,
42
+ presence_penalty: presence,
43
+ max_tokens: maxTokens,
44
+ temperature,
45
+ user
46
+ };
47
+
48
+ const ts = await res.getTranscript(limit);
49
+
50
+ const messages = [
51
+ ...(system ? [{ role: 'system', content: system }] : []),
52
+ ...ts.map((t) => ({ role: t.fromBot ? 'assistant' : 'user', content: t.text })),
53
+ { role: 'user', content },
54
+ ...(systemAfter ? [{ role: 'system', content: systemAfter }] : [])
55
+ ];
56
+
57
+ Object.assign(body, { messages });
58
+
59
+ const useFetch = params.fetch || fetch;
60
+
61
+ const response = await useFetch('https://api.openai.com/v1/chat/completions', {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Content-Type': 'application/json',
65
+ Authorization: `Bearer ${token}`
66
+ },
67
+ body: JSON.stringify(body)
68
+ });
69
+
70
+ const data = await response.json();
71
+
72
+ if (response.status !== 200
73
+ || !Array.isArray(data.choices)) {
74
+ const { status, statusText } = response;
75
+ // eslint-disable-next-line no-console
76
+ console.log('chat gpt error', { status, statusText, data });
77
+ throw new Error(`Chat GPT ${status}`);
78
+ }
79
+
80
+ // eslint-disable-next-line no-console
81
+ console.log('chat gpt', JSON.stringify(data));
82
+
83
+ const sent = data.choices
84
+ .filter((ch) => ch.message && ch.message.role === 'assistant' && ch.message.content)
85
+ .map((ch) => {
86
+ let filtered = ch.message.content
87
+ .replace(/\n\n/g, '\n')
88
+ .split(/\n+(?!-)/g)
89
+ .filter((t) => !!t.trim());
90
+
91
+ if (filtered.length > 2) {
92
+ filtered = filtered.slice(0, filtered.length - 1);
93
+ }
94
+
95
+ filtered
96
+ .forEach((t) => res.text(t.trim()));
97
+
98
+ return ch;
99
+ });
100
+
101
+ if (sent.length === 0) {
102
+ const { status, statusText } = response;
103
+ // eslint-disable-next-line no-console
104
+ console.log('chat gpt nothing to send', { status, statusText, data });
105
+ throw new Error('Chat GPT empty');
106
+ }
107
+
108
+ } catch (e) {
109
+ // eslint-disable-next-line no-console
110
+ console.error('chat gpt fail', e, body);
111
+ await res.run('fallback');
112
+ }
113
+
114
+ }
115
+
116
+ return chatgpt;
117
+ }
118
+
119
+ module.exports = chatgptPlugin;
@@ -484,6 +484,86 @@
484
484
  ],
485
485
  "items": [
486
486
  ]
487
+ },
488
+ {
489
+ "id": "ai.wingbot.openai",
490
+ "name": "OpenAI - Chat",
491
+ "description": "Use within fallback to attach ChatGPT to your chatbot",
492
+ "availableSince": 3.54,
493
+ "editable": false,
494
+ "isFactory": true,
495
+ "category": "conversation",
496
+ "inputs": [
497
+ {
498
+ "name": "token",
499
+ "label": "API Key",
500
+ "type": "text",
501
+ "validations": [
502
+ { "type": "regexp", "value": "^.+$", "message": "the token should be filled" }
503
+ ]
504
+ },
505
+ {
506
+ "type": "select",
507
+ "name": "model",
508
+ "label": "Chat GPT model",
509
+ "options": [
510
+ { "value": "gpt-3.5-turbo", "label": "gpt-3.5-turbo" },
511
+ { "value": "gpt-3.5-turbo-0301", "label": "gpt-3.5-turbo-0301" }
512
+ ]
513
+ },
514
+ {
515
+ "type": "textarea",
516
+ "name": "system",
517
+ "label": "Initial preset message (system)"
518
+ },
519
+ {
520
+ "type": "text",
521
+ "name": "temperature",
522
+ "label": "Temperature: 0.0 - 2.0 (1.0) - randomness of answers",
523
+ "validations": [
524
+ {
525
+ "type": "regexp",
526
+ "value": "^[0-9]*([.,][0-9]+)?$",
527
+ "message": "Temperature should be a valid float number"
528
+ }
529
+ ]
530
+ },
531
+ {
532
+ "type": "text",
533
+ "name": "presence",
534
+ "label": "Presence: -2.0 - 2.0 (0.0) - likelihood to talk about new topics",
535
+ "validations": [
536
+ {
537
+ "type": "regexp",
538
+ "value": "^-?[0-9]*([.,][0-9]+)?$",
539
+ "message": "Presence should be a valid float number"
540
+ }
541
+ ]
542
+ },
543
+ {
544
+ "type": "text",
545
+ "name": "maxTokens",
546
+ "label": "Max Tokens: 0 - 4096 (512) - max. number of returned tokens",
547
+ "validations": [
548
+ {
549
+ "type": "regexp",
550
+ "value": "^[0-9]*$",
551
+ "message": "Max tokens should be a valid number"
552
+ }
553
+ ]
554
+ },
555
+ {
556
+ "type": "textarea",
557
+ "name": "systemAfter",
558
+ "label": "Current message (system)"
559
+ }
560
+ ],
561
+ "items": [
562
+ {
563
+ "id": "fallback",
564
+ "description": "If API call fails"
565
+ }
566
+ ]
487
567
  }
488
568
  ],
489
569
  "categories": [
package/src/AiMatching.js CHANGED
@@ -23,6 +23,16 @@ const FULL_EMOJI_REGEX = /^#((?:[\u2600-\u27bf].?|(?:\ud83c[\udde6-\uddff]){2}|[
23
23
  const HAS_CLOSING_HASH = /^#(.+)#$/;
24
24
  const ENTITY_REGEX = /^@([^=><!?]+)(\?)?([!=><]{1,2})?([^=><!]+)?$/i;
25
25
 
26
+ /**
27
+ * @typedef {object} EntityMatchingResult
28
+ * @prop {number} score
29
+ * @prop {number} handicap
30
+ * @prop {number} fromState
31
+ * @prop {number} minScore
32
+ * @prop {number} metl
33
+ * @prop {Entity[]} matched
34
+ */
35
+
26
36
  /**
27
37
  * RegExp to test a string for a ISO 8601 Date spec
28
38
  * YYYY
@@ -449,6 +459,7 @@ class AiMatching {
449
459
 
450
460
  const noIntentHandicap = req.intents.length === 0 ? 0 : this.redundantIntentHandicap;
451
461
  const regexpScore = this._matchRegexp(req, regexps, noIntentHandicap);
462
+ const textLength = req.text().trim().length;
452
463
 
453
464
  if (regexpScore !== 0 || (intents.length === 0 && regexps.length === 0)) {
454
465
 
@@ -477,8 +488,11 @@ class AiMatching {
477
488
  useState = stateData(req);
478
489
  }
479
490
 
480
- const { score, handicap, matched } = this
491
+ const {
492
+ score, handicap, matched, metl
493
+ } = this
481
494
  ._entityMatching(
495
+ textLength,
482
496
  entities,
483
497
  reqEntities,
484
498
  useState
@@ -499,30 +513,19 @@ class AiMatching {
499
513
  ? score - noIntentHandicap
500
514
  : (regexpScore + score) / 2;
501
515
 
502
- const matchedEntitiesTextLength = matched.reduce((tot, entity) => (
503
- typeof entity.end === 'number' && typeof entity.start === 'number' && tot !== null
504
- ? (tot + (entity.end - entity.start))
505
- : null
506
- ), 0);
507
- const textLength = req.text().trim().length;
508
-
509
- const useHandicap = textLength <= matchedEntitiesTextLength
510
- ? Math.max(-this.redundantEntityHandicap, baseScore - 1)
511
- : handicap;
512
-
513
- let finalScore = (baseScore - useHandicap)
516
+ let finalScore = (baseScore - handicap)
514
517
  * (this.multiMatchGain ** countOfAdditionalItems);
515
518
 
516
- if (matchedEntitiesTextLength && textLength) {
519
+ if (metl && textLength) {
517
520
  const remainingScore = Math.max(0, Math.min(1, finalScore) - (
518
521
  this._ai.confidence + this.redundantEntityHandicap
519
522
  ));
520
523
 
521
- const remainingTextLen = (textLength - matchedEntitiesTextLength);
524
+ const remainingTextLen = (textLength - metl);
522
525
  const minus = (remainingTextLen / textLength) * remainingScore;
523
526
 
524
527
  // eslint-disable-next-line max-len,object-curly-newline
525
- // console.log({ minus, matchedEntitiesTextLength, textLength, remainingScore })
528
+ // console.log({ minus, metl, textLength, remainingScore })
526
529
 
527
530
  finalScore -= minus;
528
531
  }
@@ -552,7 +555,14 @@ class AiMatching {
552
555
  let max = total;
553
556
  for (const requestIntent of req.intents) {
554
557
  const { score, entities: matchedEntities } = this
555
- ._intentMatchingScore(wanted, requestIntent, entities, req, stateless);
558
+ ._intentMatchingScore(
559
+ textLength,
560
+ wanted,
561
+ requestIntent,
562
+ entities,
563
+ req,
564
+ stateless
565
+ );
556
566
 
557
567
  if (score > max) {
558
568
  max = score;
@@ -579,6 +589,7 @@ class AiMatching {
579
589
  /**
580
590
  *
581
591
  * @private
592
+ * @param {number} textLength
582
593
  * @param {string} wantedIntent
583
594
  * @param {Intent} requestIntent
584
595
  * @param {EntityExpression[]} wantedEntities
@@ -586,7 +597,14 @@ class AiMatching {
586
597
  * @param {boolean} stateless
587
598
  * @returns {{score:number,entities:Entity[]}}
588
599
  */
589
- _intentMatchingScore (wantedIntent, requestIntent, wantedEntities, req, stateless = false) {
600
+ _intentMatchingScore (
601
+ textLength,
602
+ wantedIntent,
603
+ requestIntent,
604
+ wantedEntities,
605
+ req,
606
+ stateless = false
607
+ ) {
590
608
  if (wantedIntent !== requestIntent.intent) {
591
609
  return { score: 0, entities: [] };
592
610
  }
@@ -604,12 +622,14 @@ class AiMatching {
604
622
  score: entitiesScore, handicap, matched, minScore, fromState
605
623
  } = this
606
624
  ._entityMatching(
625
+ textLength,
607
626
  wantedEntities,
608
627
  useEntities,
609
628
  stateless ? {} : req.state,
610
629
  requestIntent.entities
611
630
  ? (x) => Math.atan((x - 0.76) * 40) / Math.atan((1 - 0.76) * 40)
612
- : (x) => x
631
+ : (x) => x,
632
+ req.entities
613
633
  );
614
634
 
615
635
  // eslint-disable-next-line max-len,object-curly-newline
@@ -639,13 +659,23 @@ class AiMatching {
639
659
  /**
640
660
  *
641
661
  * @private
662
+ * @param {number} textLen
642
663
  * @param {EntityExpression[]} wantedEntities
643
664
  * @param {Entity[]} requestEntities
644
665
  * @param {object} [requestState]
645
666
  * @param {Function} [scoreFn]
646
- * @returns {{score:number,handicap:number, matched:Entity[],minScore:number,fromState:number}}
667
+ * @param {Entity[]} allEntities
668
+ *
669
+ * @returns {EntityMatchingResult}
647
670
  */
648
- _entityMatching (wantedEntities, requestEntities = [], requestState = {}, scoreFn = (x) => x) {
671
+ _entityMatching (
672
+ textLen,
673
+ wantedEntities,
674
+ requestEntities = [],
675
+ requestState = {},
676
+ scoreFn = (x) => x,
677
+ allEntities = requestEntities
678
+ ) {
649
679
  const occurences = new Map();
650
680
 
651
681
  const matched = [];
@@ -653,6 +683,7 @@ class AiMatching {
653
683
  let sum = 0;
654
684
  let minScore = 1;
655
685
  let fromState = 0;
686
+ let metl = 0;
656
687
 
657
688
  for (const wanted of wantedEntities) {
658
689
  const usedIndexes = occurences.has(wanted.entity)
@@ -705,7 +736,7 @@ class AiMatching {
705
736
 
706
737
  if (!matching && (!wanted.optional || entityExists)) {
707
738
  return {
708
- score: 0, handicap: 0, matched: [], minScore, fromState
739
+ score: 0, handicap: 0, matched: [], minScore, fromState, metl
709
740
  };
710
741
  }
711
742
 
@@ -731,6 +762,10 @@ class AiMatching {
731
762
  }
732
763
 
733
764
  if (requestEntity) {
765
+ if (typeof requestEntity.end === 'number' && typeof requestEntity.start === 'number') {
766
+ metl += requestEntity.end - requestEntity.start;
767
+ }
768
+
734
769
  matched.push(requestEntity);
735
770
  sum += scoreFn(requestEntity.score);
736
771
  if (index !== -1) {
@@ -747,16 +782,37 @@ class AiMatching {
747
782
  }
748
783
  }
749
784
 
785
+ const withCoveringEntity = textLen && textLen <= metl;
786
+
750
787
  // eslint-disable-next-line max-len
751
- // console.log({ wantedEntities, sum, handicap, rl: requestEntities.length, ml: matched.length });
788
+ // console.log({ metl, withCoveringEntity, wantedEntities, sum, handicap, rl: requestEntities.length, ml: matched.length });
789
+
790
+ if (withCoveringEntity) {
791
+ handicap -= this.redundantEntityHandicap;
792
+ } else {
793
+ const otherEntitiesTextLen = allEntities
794
+ .filter((re) => !matched.some((e) => e.entity === re.entity))
795
+ .reduce((tot, entity) => (
796
+ typeof entity.end === 'number' && typeof entity.start === 'number'
797
+ ? (tot + (entity.end - entity.start))
798
+ : 0
799
+ ), 0);
800
+
801
+ const coveringHandicap = textLen && otherEntitiesTextLen >= textLen
802
+ ? 1
803
+ : 0;
752
804
 
753
- // @todo - neni mozne, by doslo k negativnimu handicapu
754
- handicap += (requestEntities.length + fromState - matched.length)
755
- * this.redundantEntityHandicap;
805
+ handicap += (requestEntities.length + fromState - matched.length + coveringHandicap)
806
+ * this.redundantEntityHandicap;
807
+
808
+ // eslint-disable-next-line max-len
809
+ // console.log({ requestEntities, matched, handicap, coveringHandicap, otherEntitiesTextLen });
810
+
811
+ }
756
812
  const score = matched.length === 0 ? 0 : sum / matched.length;
757
813
 
758
814
  return {
759
- score, handicap, matched, minScore, fromState
815
+ score, handicap, matched, minScore, fromState, metl
760
816
  };
761
817
  }
762
818
 
package/src/Processor.js CHANGED
@@ -646,7 +646,8 @@ class Processor extends EventEmitter {
646
646
  const options = {
647
647
  ...this.options,
648
648
  state: Object.freeze({ ...state }),
649
- features
649
+ features,
650
+ pageId
650
651
  };
651
652
 
652
653
  res = new Responder(senderId, messageSender, token, options, responderData);
package/src/Responder.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  FEATURE_SSML,
17
17
  FEATURE_PHRASES
18
18
  } = require('./features');
19
+ const transcriptFromHistory = require('./transcript/transcriptFromHistory');
19
20
 
20
21
  const TYPE_RESPONSE = 'RESPONSE';
21
22
  const TYPE_UPDATE = 'UPDATE';
@@ -26,6 +27,7 @@ const EXCEPTION_HOPCOUNT_THRESHOLD = 5;
26
27
  /** @typedef {import('./ReturnSender').UploadResult} UploadResult */
27
28
  /** @typedef {import('./analytics/consts').TrackingCategory} TrackingCategory */
28
29
  /** @typedef {import('./analytics/consts').TrackingType} TrackingType */
30
+ /** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */
29
31
 
30
32
  /**
31
33
  * @enum {string} ExpectedInput
@@ -89,6 +91,7 @@ class Responder {
89
91
  constructor (senderId, messageSender, token = null, options = {}, data = {}) {
90
92
  this._messageSender = messageSender;
91
93
  this._senderId = senderId;
94
+ this._pageId = options.pageId;
92
95
  this.token = token;
93
96
 
94
97
  /**
@@ -178,6 +181,33 @@ class Responder {
178
181
  this._recipient = { id: senderId };
179
182
 
180
183
  this._textResponses = [];
184
+
185
+ this._typingSent = false;
186
+ }
187
+
188
+ /**
189
+ *
190
+ * Returns current conversation transcript
191
+ *
192
+ * @param {number} [limit]
193
+ * @returns {Promise<Transcript[]>}
194
+ */
195
+ async getTranscript (limit = 10) {
196
+ const { chatLogStorage } = this._messageSender;
197
+ if (!chatLogStorage) {
198
+ return [];
199
+ }
200
+ const transcript = await transcriptFromHistory(
201
+ chatLogStorage,
202
+ this._senderId,
203
+ this._pageId,
204
+ limit
205
+ );
206
+ const { responseTexts = [] } = chatLogStorage;
207
+ transcript.push(...responseTexts.map((text) => ({
208
+ fromBot: true, text
209
+ })));
210
+ return transcript;
181
211
  }
182
212
 
183
213
  /**
@@ -287,6 +317,7 @@ class Responder {
287
317
  });
288
318
  }
289
319
  this.startedOutput = true;
320
+ this._typingSent = data.sender_action === 'typing_on';
290
321
  this._messageSender.send(data);
291
322
  return this;
292
323
  }
@@ -979,10 +1010,11 @@ class Responder {
979
1010
  /**
980
1011
  * Sends "typing..." information
981
1012
  *
1013
+ * @param {boolean} [force] - send even if was recently sent
982
1014
  * @returns {this}
983
1015
  */
984
- typingOn () {
985
- return this._senderAction('typing_on');
1016
+ typingOn (force = false) {
1017
+ return this._senderAction('typing_on', force);
986
1018
  }
987
1019
 
988
1020
  /**
@@ -1243,7 +1275,10 @@ class Responder {
1243
1275
  return this._textResponses;
1244
1276
  }
1245
1277
 
1246
- _senderAction (action) {
1278
+ _senderAction (action, force = false) {
1279
+ if (action === 'typing_on' && this._typingSent && !force) {
1280
+ return this;
1281
+ }
1247
1282
  const messageData = {
1248
1283
  sender_action: action
1249
1284
  };
@@ -12,10 +12,21 @@ const extractText = require('./transcript/extractText');
12
12
  /** @typedef {import('./Responder')} Responder */
13
13
  /** @typedef {import('./Processor').TrackingObject} TrackingObject */
14
14
 
15
+ /**
16
+ * @callback GetInteractions
17
+ * @param {string} senderId
18
+ * @param {string} pageId
19
+ * @param {number} [limit]
20
+ * @param {number} [endAt] - iterate backwards to history
21
+ * @param {number} [startAt] - iterate forward to last interaction
22
+ * @returns {Promise<object[]>}
23
+ */
24
+
15
25
  /**
16
26
  * @typedef {object} ChatLogStorage
17
- * @property {Function} log
18
- * @property {Function} error
27
+ * @prop {Function} log
28
+ * @prop {Function} error
29
+ * @prop {GetInteractions} [getInteractions]
19
30
  */
20
31
 
21
32
  /**
@@ -165,6 +176,13 @@ class ReturnSender {
165
176
  .filter((t) => t && `${t}`.trim());
166
177
  }
167
178
 
179
+ /**
180
+ * @returns {ChatLogStorage}
181
+ */
182
+ get chatLogStorage () {
183
+ return this._logger;
184
+ }
185
+
168
186
  _gotAnotherEvent () {
169
187
  if (this._gotAnotherEventDefer) {
170
188
  this._gotAnotherEventDefer();
@@ -33,6 +33,9 @@ const extractText = require('./extractText');
33
33
  * @returns {Promise<Transcript[]>}
34
34
  */
35
35
  async function transcriptFromHistory (chatLogStorage, senderId, pageId, limit = 20) {
36
+ if (typeof chatLogStorage.getInteractions !== 'function') {
37
+ return [];
38
+ }
36
39
  const data = await chatLogStorage.getInteractions(senderId, pageId, limit);
37
40
 
38
41
  return data