wingbot 3.71.6 → 3.72.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/.github/workflows/deploy.yml +2 -2
- package/.github/workflows/pullRequest.yml +2 -2
- package/package.json +4 -2
- package/src/Ai.js +44 -3
- package/src/AiMatching.js +66 -40
- package/src/BotAppSender.js +6 -4
- package/src/BuildRouter.js +2 -0
- package/src/LLM.js +183 -2
- package/src/LLMSession.js +90 -3
- package/src/Processor.js +1 -1
- package/src/Responder.js +143 -3
- package/src/ReturnSender.js +4 -4
- package/src/Tester.js +44 -2
- package/src/graphApi/GraphApi.js +31 -1
- package/src/graphApi/gqlRules.js +87 -0
- package/src/graphApi/schema.gql +15 -0
- package/src/resolvers/contextMessage.js +21 -4
- package/src/resolvers/message.js +43 -1
- package/src/resolvers/utils.js +14 -5
- package/src/testTools/PromptAssert.js +184 -0
- package/src/testTools/asserts.js +42 -4
- package/src/utils/customCondition.js +14 -1
- package/src/utils/getCondition.js +16 -4
- package/src/utils/getUpdate.js +4 -4
- package/src/utils/stateData.js +2 -0
|
@@ -15,7 +15,7 @@ jobs:
|
|
|
15
15
|
|
|
16
16
|
strategy:
|
|
17
17
|
matrix:
|
|
18
|
-
node-version: [
|
|
18
|
+
node-version: [20.x]
|
|
19
19
|
|
|
20
20
|
steps:
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ jobs:
|
|
|
23
23
|
uses: actions/checkout@v2
|
|
24
24
|
|
|
25
25
|
- name: Node.js ${{ matrix.node-version }} setup
|
|
26
|
-
uses: actions/setup-node@
|
|
26
|
+
uses: actions/setup-node@v3
|
|
27
27
|
with:
|
|
28
28
|
node-version: ${{ matrix.node-version }}
|
|
29
29
|
cache: 'npm'
|
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
|
|
12
12
|
strategy:
|
|
13
13
|
matrix:
|
|
14
|
-
node-version: [
|
|
14
|
+
node-version: [20.x]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
17
|
|
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
uses: actions/checkout@v2
|
|
20
20
|
|
|
21
21
|
- name: Node.js ${{ matrix.node-version }} setup
|
|
22
|
-
uses: actions/setup-node@
|
|
22
|
+
uses: actions/setup-node@v3
|
|
23
23
|
with:
|
|
24
24
|
node-version: ${{ matrix.node-version }}
|
|
25
25
|
cache: 'npm'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wingbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.72.1",
|
|
4
4
|
"description": "Enterprise Messaging Bot Conversation Engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
"compress-json": "^3.0.0",
|
|
59
59
|
"deep-extend": "^0.6.0",
|
|
60
60
|
"form-data": "^4.0.0",
|
|
61
|
-
"graphql": "^16.
|
|
61
|
+
"graphql": "^16.11.0",
|
|
62
|
+
"graphql-depth-limit": "^1.1.0",
|
|
63
|
+
"graphql-query-complexity": "^1.1.0",
|
|
62
64
|
"jsonwebtoken": "^9.0.2",
|
|
63
65
|
"node-fetch": "^2.6.7",
|
|
64
66
|
"path-to-regexp": "^6.3.0",
|
package/src/Ai.js
CHANGED
|
@@ -41,8 +41,10 @@ let uq = 1;
|
|
|
41
41
|
|
|
42
42
|
/** @typedef {import('./Request').IntentAction} IntentAction */
|
|
43
43
|
/** @typedef {import('./Request')} Request */
|
|
44
|
+
/** @typedef {import('./Request').TextAlternative} TextAlternative */
|
|
44
45
|
/** @typedef {import('./Responder')} Responder */
|
|
45
46
|
/** @typedef {import('./Router').Resolver} Resolver */
|
|
47
|
+
/** @typedef {import('./utils/stateData').IStateRequest} IStateRequest */
|
|
46
48
|
/** @typedef {import('./wingbot/CachedModel').Result} Result */
|
|
47
49
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').Phrases} Phrases */
|
|
48
50
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').EntityDetector} EntityDetector */
|
|
@@ -137,7 +139,7 @@ class Ai {
|
|
|
137
139
|
* The prefix translator - for request-specific prefixes
|
|
138
140
|
*
|
|
139
141
|
* @param {string} defaultModel
|
|
140
|
-
* @param {
|
|
142
|
+
* @param {IStateRequest} req
|
|
141
143
|
*/
|
|
142
144
|
this.getPrefix = (defaultModel, req) => req.state.lang || defaultModel; // eslint-disable-line
|
|
143
145
|
|
|
@@ -857,11 +859,22 @@ class Ai {
|
|
|
857
859
|
.filter((alt) => alt.score >= this.sttScoreThreshold)
|
|
858
860
|
.slice(0, this.sttMaxAlternatives);
|
|
859
861
|
|
|
860
|
-
|
|
862
|
+
return this._queryModelWithTexts(model, texts, req);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
*
|
|
867
|
+
* @param {CustomEntityDetectionModel} model
|
|
868
|
+
* @param {TextAlternative[]} texts
|
|
869
|
+
* @param {Request} [req]
|
|
870
|
+
* @returns {Promise<Result>}
|
|
871
|
+
*/
|
|
872
|
+
async _queryModelWithTexts (model, texts, req = null) {
|
|
861
873
|
const altKoef = (1 - this.confidence);
|
|
874
|
+
const altMax = Math.max(0, ...texts.map((t) => t.score));
|
|
862
875
|
|
|
863
876
|
const results = await Promise.all(
|
|
864
|
-
texts.map(({ text, score }) => model
|
|
877
|
+
texts.map(({ text, score = 1 }) => model
|
|
865
878
|
.resolve(this.textFilter(text), req)
|
|
866
879
|
.then((res) => ({
|
|
867
880
|
...res,
|
|
@@ -910,6 +923,34 @@ class Ai {
|
|
|
910
923
|
};
|
|
911
924
|
}
|
|
912
925
|
|
|
926
|
+
/**
|
|
927
|
+
*
|
|
928
|
+
* @param {string} text
|
|
929
|
+
* @param {string|IStateRequest} langOrReq
|
|
930
|
+
* @returns {Promise<Result>}
|
|
931
|
+
*/
|
|
932
|
+
async queryModel (text, langOrReq = this.DEFAULT_PREFIX) {
|
|
933
|
+
let model;
|
|
934
|
+
|
|
935
|
+
if (typeof langOrReq === 'string') {
|
|
936
|
+
model = this._keyworders.has(langOrReq)
|
|
937
|
+
? this._keyworders.get(langOrReq)
|
|
938
|
+
: this._keyworders.get(this.DEFAULT_PREFIX);
|
|
939
|
+
} else {
|
|
940
|
+
model = this._getModelForRequest(langOrReq);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!model) {
|
|
944
|
+
return {
|
|
945
|
+
text,
|
|
946
|
+
intents: [],
|
|
947
|
+
entities: []
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return this._queryModelWithTexts(model, [{ text, score: 1 }]);
|
|
952
|
+
}
|
|
953
|
+
|
|
913
954
|
/**
|
|
914
955
|
*
|
|
915
956
|
* @param {IntentAction[]} aiActions
|
package/src/AiMatching.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
const { replaceDiacritics } = require('./utils/tokenizer');
|
|
6
|
+
const { replaceDiacritics, tokenize } = require('./utils/tokenizer');
|
|
7
7
|
const { vars } = require('./utils/stateVariables');
|
|
8
8
|
const stateData = require('./utils/stateData');
|
|
9
9
|
|
|
10
10
|
/** @typedef {import('handlebars')} Handlebars */
|
|
11
|
+
/** @typedef {import('./Ai').Result} Result */
|
|
11
12
|
|
|
12
13
|
/** @type {Handlebars} */
|
|
13
14
|
let handlebars;
|
|
@@ -66,16 +67,18 @@ const COMPARE = {
|
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
69
|
* @typedef {object} Entity
|
|
69
|
-
* @
|
|
70
|
-
* @
|
|
71
|
-
* @
|
|
70
|
+
* @prop {string} entity
|
|
71
|
+
* @prop {string} value
|
|
72
|
+
* @prop {number} score
|
|
73
|
+
* @prop {number} [start]
|
|
74
|
+
* @prop {number} [end]
|
|
72
75
|
*/
|
|
73
76
|
|
|
74
77
|
/**
|
|
75
78
|
* @typedef {object} Intent
|
|
76
|
-
* @
|
|
77
|
-
* @
|
|
78
|
-
* @
|
|
79
|
+
* @prop {string} [intent]
|
|
80
|
+
* @prop {number} score
|
|
81
|
+
* @prop {Entity[]} [entities]
|
|
79
82
|
*/
|
|
80
83
|
|
|
81
84
|
/**
|
|
@@ -461,35 +464,35 @@ class AiMatching {
|
|
|
461
464
|
}
|
|
462
465
|
|
|
463
466
|
/**
|
|
464
|
-
* Calculate a matching score of preprocessed rule against the request
|
|
465
467
|
*
|
|
466
|
-
* @param {
|
|
468
|
+
* @param {string} text
|
|
467
469
|
* @param {PreprocessorOutput} rule
|
|
468
|
-
* @param {
|
|
469
|
-
* @param {
|
|
470
|
-
* @param {boolean} [noEntityThreshold]
|
|
470
|
+
* @param {Result} nlpResult
|
|
471
|
+
* @param {{}} state
|
|
471
472
|
* @returns {Intent|null}
|
|
472
473
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
474
|
+
matchText (text, rule, nlpResult, state = {}) {
|
|
475
|
+
return this._match(text, rule, state, nlpResult, true);
|
|
476
|
+
}
|
|
475
477
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const textLength = req.text().trim().length;
|
|
478
|
+
_match (text, rule, useState, nlpResult, stateless = false, noEntityThreshold = false) {
|
|
479
|
+
let state = useState;
|
|
479
480
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return o;
|
|
486
|
-
}
|
|
487
|
-
return Object.assign(o, { [k]: v });
|
|
488
|
-
}, {});
|
|
489
|
-
} else {
|
|
490
|
-
useState = stateData(req);
|
|
481
|
+
if (stateless) {
|
|
482
|
+
state = Object.fromEntries(
|
|
483
|
+
Object.entries(state)
|
|
484
|
+
.filter(([k]) => !k.startsWith('@'))
|
|
485
|
+
);
|
|
491
486
|
}
|
|
492
487
|
|
|
488
|
+
const { regexps, intents, entities } = rule;
|
|
489
|
+
const { entities: reqEntities = [], intents: reqIntents = [] } = nlpResult;
|
|
490
|
+
const tokenized = tokenize(text) || text.trim();
|
|
491
|
+
|
|
492
|
+
const noIntentHandicap = reqIntents.length === 0 ? 0 : this.redundantIntentHandicap;
|
|
493
|
+
const regexpScore = this._matchRegexp(text, tokenized, regexps, noIntentHandicap);
|
|
494
|
+
const textLength = text.length;
|
|
495
|
+
|
|
493
496
|
if (regexpScore !== 0 || (intents.length === 0 && regexps.length === 0)) {
|
|
494
497
|
|
|
495
498
|
if (entities.length === 0) {
|
|
@@ -511,7 +514,7 @@ class AiMatching {
|
|
|
511
514
|
textLength,
|
|
512
515
|
entities,
|
|
513
516
|
reqEntities,
|
|
514
|
-
|
|
517
|
+
state,
|
|
515
518
|
undefined,
|
|
516
519
|
undefined,
|
|
517
520
|
noEntityThreshold
|
|
@@ -563,7 +566,7 @@ class AiMatching {
|
|
|
563
566
|
};
|
|
564
567
|
}
|
|
565
568
|
|
|
566
|
-
if (
|
|
569
|
+
if (reqIntents.length === 0) {
|
|
567
570
|
return null;
|
|
568
571
|
}
|
|
569
572
|
|
|
@@ -572,15 +575,15 @@ class AiMatching {
|
|
|
572
575
|
intents
|
|
573
576
|
.reduce((total, wanted) => {
|
|
574
577
|
let max = total;
|
|
575
|
-
for (const requestIntent of
|
|
578
|
+
for (const requestIntent of reqIntents) {
|
|
576
579
|
const { score, entities: matchedEntities } = this
|
|
577
580
|
._intentMatchingScore(
|
|
578
581
|
textLength,
|
|
579
582
|
wanted,
|
|
580
583
|
requestIntent,
|
|
581
584
|
entities,
|
|
582
|
-
|
|
583
|
-
|
|
585
|
+
reqEntities,
|
|
586
|
+
state,
|
|
584
587
|
noEntityThreshold
|
|
585
588
|
);
|
|
586
589
|
|
|
@@ -602,6 +605,27 @@ class AiMatching {
|
|
|
602
605
|
return winningIntent;
|
|
603
606
|
}
|
|
604
607
|
|
|
608
|
+
/**
|
|
609
|
+
* Calculate a matching score of preprocessed rule against the request
|
|
610
|
+
*
|
|
611
|
+
* @param {AIRequest} req
|
|
612
|
+
* @param {PreprocessorOutput} rule
|
|
613
|
+
* @param {boolean} [stateless]
|
|
614
|
+
* @param {Entity[]} [reqEntities]
|
|
615
|
+
* @param {boolean} [noEntityThreshold]
|
|
616
|
+
* @returns {Intent|null}
|
|
617
|
+
*/
|
|
618
|
+
match (req, rule, stateless = false, reqEntities = req.entities, noEntityThreshold = false) {
|
|
619
|
+
const { intents } = rule;
|
|
620
|
+
|
|
621
|
+
const state = stateData(req);
|
|
622
|
+
|
|
623
|
+
return this._match(req.text(), rule, state, {
|
|
624
|
+
intents: req.intents,
|
|
625
|
+
entities: reqEntities
|
|
626
|
+
}, stateless || intents.length === 0, noEntityThreshold);
|
|
627
|
+
}
|
|
628
|
+
|
|
605
629
|
_getMultiMatchGain (entitiesScore, matchedCount, fromState = 0) {
|
|
606
630
|
return (this.multiMatchGain * entitiesScore) ** Math.max(matchedCount - fromState, 0);
|
|
607
631
|
}
|
|
@@ -613,7 +637,7 @@ class AiMatching {
|
|
|
613
637
|
* @param {string} wantedIntent
|
|
614
638
|
* @param {Intent} requestIntent
|
|
615
639
|
* @param {EntityExpression[]} wantedEntities
|
|
616
|
-
* @param {
|
|
640
|
+
* @param {Entity[]} reqEntities
|
|
617
641
|
* @param {object} useState
|
|
618
642
|
* @param {boolean} [noEntityThreshold]
|
|
619
643
|
* @returns {{score:number,entities:Entity[]}}
|
|
@@ -623,7 +647,7 @@ class AiMatching {
|
|
|
623
647
|
wantedIntent,
|
|
624
648
|
requestIntent,
|
|
625
649
|
wantedEntities,
|
|
626
|
-
|
|
650
|
+
reqEntities,
|
|
627
651
|
useState,
|
|
628
652
|
noEntityThreshold = false
|
|
629
653
|
) {
|
|
@@ -631,7 +655,7 @@ class AiMatching {
|
|
|
631
655
|
return { score: 0, entities: [] };
|
|
632
656
|
}
|
|
633
657
|
|
|
634
|
-
const useEntities = requestIntent.entities ||
|
|
658
|
+
const useEntities = requestIntent.entities || reqEntities;
|
|
635
659
|
|
|
636
660
|
if (wantedEntities.length === 0) {
|
|
637
661
|
return {
|
|
@@ -651,7 +675,7 @@ class AiMatching {
|
|
|
651
675
|
requestIntent.entities
|
|
652
676
|
? (x) => Math.atan((x - 0.76) * 40) / Math.atan((1 - 0.76) * 40)
|
|
653
677
|
: (x) => x,
|
|
654
|
-
|
|
678
|
+
reqEntities,
|
|
655
679
|
noEntityThreshold
|
|
656
680
|
);
|
|
657
681
|
|
|
@@ -941,18 +965,20 @@ class AiMatching {
|
|
|
941
965
|
|
|
942
966
|
/**
|
|
943
967
|
*
|
|
944
|
-
* @param {
|
|
968
|
+
* @param {string} text
|
|
969
|
+
* @param {string} tokenized
|
|
945
970
|
* @param {RegexpComparator[]} regexps
|
|
946
971
|
* @param {number} noIntentHandicap
|
|
947
972
|
* @returns {number}
|
|
948
973
|
*/
|
|
949
|
-
_matchRegexp (
|
|
974
|
+
_matchRegexp (text, tokenized, regexps, noIntentHandicap) {
|
|
950
975
|
if (regexps.length === 0) {
|
|
951
976
|
return 0;
|
|
952
977
|
}
|
|
953
978
|
|
|
954
979
|
const scores = regexps.map(({ r, t, f }) => {
|
|
955
|
-
const
|
|
980
|
+
const txt = t ? tokenized : text;
|
|
981
|
+
const m = txt.match(r);
|
|
956
982
|
|
|
957
983
|
if (!m) {
|
|
958
984
|
return 0;
|
package/src/BotAppSender.js
CHANGED
|
@@ -41,12 +41,12 @@ class BotAppSender extends ReturnSender {
|
|
|
41
41
|
/**
|
|
42
42
|
*
|
|
43
43
|
* @param {SenderOptions} options
|
|
44
|
-
* @param {string}
|
|
44
|
+
* @param {string} senderId
|
|
45
45
|
* @param {object} incommingMessage
|
|
46
46
|
* @param {ChatLogStorage} logger - console like logger
|
|
47
47
|
*/
|
|
48
|
-
constructor (options,
|
|
49
|
-
super(options,
|
|
48
|
+
constructor (options, senderId, incommingMessage, logger = null) {
|
|
49
|
+
super(options, senderId, incommingMessage, logger);
|
|
50
50
|
|
|
51
51
|
this.waits = true;
|
|
52
52
|
|
|
@@ -98,14 +98,16 @@ class BotAppSender extends ReturnSender {
|
|
|
98
98
|
* @param {Buffer} data
|
|
99
99
|
* @param {string} contentType
|
|
100
100
|
* @param {string} fileName
|
|
101
|
+
* @param {string} [senderId]
|
|
101
102
|
* @returns {Promise<UploadResult>}
|
|
102
103
|
*/
|
|
103
|
-
async upload (data, contentType, fileName) {
|
|
104
|
+
async upload (data, contentType, fileName, senderId = this._senderId) {
|
|
104
105
|
const formData = new FormData();
|
|
105
106
|
|
|
106
107
|
const nonce = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36).padEnd(11, '0');
|
|
107
108
|
|
|
108
109
|
formData.append('nonce', nonce);
|
|
110
|
+
formData.append('senderId', senderId || '');
|
|
109
111
|
formData.append('f0', data, { filename: fileName, contentType });
|
|
110
112
|
|
|
111
113
|
const [token, agent] = await Promise.all([
|
package/src/BuildRouter.js
CHANGED
|
@@ -162,6 +162,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
|
|
|
162
162
|
* @prop {Block[]} [blocks]
|
|
163
163
|
* @prop {NestedLinksMapFactory} [nestedLinksMapFactory]
|
|
164
164
|
* @prop {object} [BuildRouter]
|
|
165
|
+
* @prop {Ai} [ai]
|
|
165
166
|
* @prop {string|number} [resolverId] - only for text messages with random characters
|
|
166
167
|
*/
|
|
167
168
|
|
|
@@ -928,6 +929,7 @@ class BuildRouter extends Router {
|
|
|
928
929
|
return resolvers.map((resolver, i) => {
|
|
929
930
|
|
|
930
931
|
const context = {
|
|
932
|
+
ai: Ai.ai,
|
|
931
933
|
...this._resolvedContext,
|
|
932
934
|
isLastIndex: lastIndex === i && !buildInfo.expectedToAddResolver,
|
|
933
935
|
isLastMessage: lastMessageIndex === i && !buildInfo.notLastMessage,
|
package/src/LLM.js
CHANGED
|
@@ -3,16 +3,72 @@
|
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
+
const { getSetState } = require('./utils/getUpdate');
|
|
6
7
|
const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
|
|
8
|
+
const getCondition = require('./utils/getCondition');
|
|
9
|
+
const stateData = require('./utils/stateData');
|
|
10
|
+
const Ai = require('./Ai');
|
|
11
|
+
// const getCondition = require('./utils/getCondition');
|
|
7
12
|
|
|
8
13
|
/** @typedef {import('./Responder')} Responder */
|
|
14
|
+
/** @typedef {import('./AiMatching').PreprocessorOutput} PreprocessorOutput */
|
|
15
|
+
/** @typedef {import('./Request')} Request */
|
|
9
16
|
/** @typedef {import('./Responder').Persona} Persona */
|
|
10
17
|
/** @typedef {import('./Router').BaseConfiguration} BaseConfiguration */
|
|
11
18
|
/** @typedef {import('./LLMSession').LLMMessage<any>} LLMMessage */
|
|
12
19
|
/** @typedef {import('./LLMSession').ToolCall} ToolCall */
|
|
13
20
|
/** @typedef {import('./LLMSession').LLMRole} LLMRole */
|
|
21
|
+
/** @typedef {import('./LLMSession').FilterScope} FilterScope */
|
|
14
22
|
/** @typedef {import('./LLMSession')} LLMSession */
|
|
15
23
|
/** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */
|
|
24
|
+
/** @typedef {import('./utils/getCondition').ConditionDefinition} ConditionDefinition */
|
|
25
|
+
/** @typedef {import('./utils/getCondition').ConditionContext} ConditionContext */
|
|
26
|
+
/** @typedef {import('./utils/stateData').IStateRequest} IStateRequest */
|
|
27
|
+
|
|
28
|
+
/** @typedef {string|'_DISCARD'} EvaluationRuleAction */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} EvaluationRuleData
|
|
32
|
+
* @prop {EvaluationRuleAction} [action]
|
|
33
|
+
* @prop {object} [setState]
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} RuleDefinitionData
|
|
38
|
+
* @prop {string[]} aiTags
|
|
39
|
+
* @prop {EvaluationRuleAction} [targetRouteId]
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {EvaluationRuleData & RuleDefinitionData & ConditionDefinition} EvaluationRule
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {object} PrepocessedRuleData
|
|
48
|
+
* @prop {Function} condition
|
|
49
|
+
* @prop {PreprocessorOutput} rule
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {EvaluationRuleData & PrepocessedRuleData} PreprocessedRule
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} RuleScore
|
|
58
|
+
* @prop {number} score
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {RuleScore & PreprocessedRule} RuleWithScore
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {object} EvaluationResult
|
|
67
|
+
* @prop {string} action
|
|
68
|
+
* @prop {boolean} discard
|
|
69
|
+
* @prop {RuleWithScore[]} results
|
|
70
|
+
* @prop {object} setState
|
|
71
|
+
*/
|
|
16
72
|
|
|
17
73
|
/**
|
|
18
74
|
* @callback LLMChatProviderPrompt
|
|
@@ -82,6 +138,13 @@ class LLM {
|
|
|
82
138
|
|
|
83
139
|
static GPT_FLAG = 'gpt';
|
|
84
140
|
|
|
141
|
+
/** @type {FilterScope} */
|
|
142
|
+
static FILTER_SCOPE_CONVERSATION = 'conversation';
|
|
143
|
+
|
|
144
|
+
static EVALUATION_ACTIONS = {
|
|
145
|
+
DISCARD: '_DISCARD'
|
|
146
|
+
};
|
|
147
|
+
|
|
85
148
|
/** @type {AnonymizeRegexp[]} */
|
|
86
149
|
static anonymizeRegexps = [
|
|
87
150
|
{ replacement: '@PHONE', regex: new RegExp(PHONE_REGEX.source, 'g') },
|
|
@@ -91,8 +154,9 @@ class LLM {
|
|
|
91
154
|
/**
|
|
92
155
|
*
|
|
93
156
|
* @param {LLMConfiguration} configuration
|
|
157
|
+
* @param {Ai} ai
|
|
94
158
|
*/
|
|
95
|
-
constructor (configuration) {
|
|
159
|
+
constructor (configuration, ai) {
|
|
96
160
|
const { provider, ...rest } = configuration;
|
|
97
161
|
|
|
98
162
|
this._configuration = {
|
|
@@ -107,6 +171,25 @@ class LLM {
|
|
|
107
171
|
|
|
108
172
|
/** @type {LLMChatProvider} */
|
|
109
173
|
this._provider = provider;
|
|
174
|
+
|
|
175
|
+
this._ai = ai;
|
|
176
|
+
|
|
177
|
+
/** @type {LLMMessage} */
|
|
178
|
+
this._lastResult = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @returns {Ai}
|
|
183
|
+
*/
|
|
184
|
+
get ai () {
|
|
185
|
+
return this._ai;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @returns {LLMMessage}
|
|
190
|
+
*/
|
|
191
|
+
get lastResult () {
|
|
192
|
+
return this._lastResult;
|
|
110
193
|
}
|
|
111
194
|
|
|
112
195
|
/**
|
|
@@ -148,7 +231,7 @@ class LLM {
|
|
|
148
231
|
...options
|
|
149
232
|
};
|
|
150
233
|
|
|
151
|
-
const prompt = session.toArray();
|
|
234
|
+
const prompt = session.toArray(true);
|
|
152
235
|
const result = await this._provider.requestChat(prompt, opts);
|
|
153
236
|
this._logPrompt(prompt, result);
|
|
154
237
|
return result;
|
|
@@ -160,6 +243,7 @@ class LLM {
|
|
|
160
243
|
* @param {LLMMessage} result
|
|
161
244
|
*/
|
|
162
245
|
_logPrompt (prompt, result) {
|
|
246
|
+
this._lastResult = result;
|
|
163
247
|
this._configuration.logger.logPrompt({
|
|
164
248
|
prompt, result
|
|
165
249
|
});
|
|
@@ -188,6 +272,103 @@ class LLM {
|
|
|
188
272
|
}));
|
|
189
273
|
}
|
|
190
274
|
|
|
275
|
+
/**
|
|
276
|
+
*
|
|
277
|
+
* @param {EvaluationRule[]} rules
|
|
278
|
+
* @param {ConditionContext} [context]
|
|
279
|
+
* @returns {PreprocessedRule[]}
|
|
280
|
+
*/
|
|
281
|
+
static preprocessEvaluationRules (rules, context = {}) {
|
|
282
|
+
const {
|
|
283
|
+
linksMap = new Map(),
|
|
284
|
+
ai = Ai.ai
|
|
285
|
+
} = context;
|
|
286
|
+
|
|
287
|
+
return rules.map((evalRule) => {
|
|
288
|
+
const { aiTags, targetRouteId, ...rest } = evalRule;
|
|
289
|
+
|
|
290
|
+
const condition = getCondition(rest, context);
|
|
291
|
+
const rule = ai.matcher.preprocessRule(aiTags);
|
|
292
|
+
|
|
293
|
+
let { action = null } = evalRule;
|
|
294
|
+
|
|
295
|
+
if (!action && targetRouteId && linksMap.has(targetRouteId)) {
|
|
296
|
+
action = linksMap.get(targetRouteId);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
...rest,
|
|
301
|
+
condition,
|
|
302
|
+
rule
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Returns all actions, which has been recognized
|
|
309
|
+
* with higher score than threshold, but
|
|
310
|
+
*
|
|
311
|
+
* - _DISCARD action discards any other rules (will return all relevant _DISCARD actions)
|
|
312
|
+
* - only the TOP ranked "interaction" action will be returned
|
|
313
|
+
* - actions will come in THE SAME order, so the "setState" will be applied in the same order
|
|
314
|
+
*
|
|
315
|
+
*
|
|
316
|
+
* @param {LLMMessage|string} result
|
|
317
|
+
* @param {PreprocessedRule[]} rules
|
|
318
|
+
* @param {IStateRequest} req
|
|
319
|
+
* @param {Responder} res
|
|
320
|
+
* @returns {Promise<EvaluationResult>}
|
|
321
|
+
*/
|
|
322
|
+
async evaluateResultWithRules (result, rules, req, res) {
|
|
323
|
+
const text = typeof result === 'string' ? result : result.content;
|
|
324
|
+
const nlpResult = await this._ai.queryModel(text, req);
|
|
325
|
+
const state = stateData(req, res);
|
|
326
|
+
|
|
327
|
+
let topRankedAction = null;
|
|
328
|
+
let topActionScore = 0;
|
|
329
|
+
let discard = false;
|
|
330
|
+
const setState = {};
|
|
331
|
+
|
|
332
|
+
const sAct = Object.values(LLM.EVALUATION_ACTIONS);
|
|
333
|
+
|
|
334
|
+
const results = rules
|
|
335
|
+
.filter((rule) => rule.condition(req, res))
|
|
336
|
+
.map((rule) => {
|
|
337
|
+
const matched = this._ai.matcher
|
|
338
|
+
.matchText(text, rule.rule, nlpResult, state);
|
|
339
|
+
|
|
340
|
+
if (!matched || matched.score < this._ai.threshold) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (rule.action === LLM.EVALUATION_ACTIONS.DISCARD) {
|
|
345
|
+
discard = true;
|
|
346
|
+
} else if (rule.action && topActionScore < matched.score) {
|
|
347
|
+
topRankedAction = rule.action;
|
|
348
|
+
topActionScore = matched.score;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
...rule,
|
|
353
|
+
score: matched.score
|
|
354
|
+
};
|
|
355
|
+
})
|
|
356
|
+
.filter((rule) => rule !== null
|
|
357
|
+
&& (!discard || rule.action === LLM.EVALUATION_ACTIONS.DISCARD)
|
|
358
|
+
&& (!rule.action || rule.action === topRankedAction || sAct.includes(rule.action)));
|
|
359
|
+
|
|
360
|
+
results.forEach((rule) => {
|
|
361
|
+
Object.assign(setState, getSetState(rule.setState, req, res, setState));
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
setState,
|
|
366
|
+
results,
|
|
367
|
+
discard,
|
|
368
|
+
action: discard ? null : topRankedAction
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
191
372
|
}
|
|
192
373
|
|
|
193
374
|
module.exports = LLM;
|