wingbot 3.76.17 → 3.77.0-alpha.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 +2 -2
- package/src/BuildRouter.js +31 -13
- package/src/CallbackAuditLog.js +1 -5
- package/src/ChatGpt.js +46 -18
- package/src/ConversationTester.js +1 -5
- package/src/LLM.js +73 -8
- package/src/LLMDispatcher.js +1 -5
- package/src/LLMSession.js +1 -1
- package/src/Processor.js +26 -10
- package/src/Request.js +11 -9
- package/src/analytics/GA4.js +4 -4
- package/src/graphApi/GraphApi.js +1 -5
- package/src/graphApi/gqlRules.js +3 -7
- package/src/log/types.js +98 -0
- package/src/notifications/Notifications.js +1 -5
- package/src/resolvers/message.js +15 -2
- package/src/utils/canaryLog.js +1 -6
- package/src/utils/logSpan.js +36 -0
- package/src/utils/pathUtils.js +8 -2
- package/src/wingbot/CachedModel.js +15 -3
- package/src/wingbot/CustomEntityDetectionModel.js +3 -3
- package/src/wingbot/WingbotModel.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wingbot",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.77.0-alpha.0",
|
|
4
4
|
"description": "Enterprise Messaging Bot Conversation Engine",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
},
|
|
76
76
|
"optionalDependencies": {
|
|
77
77
|
"axios": "^1.6.4",
|
|
78
|
-
"handlebars": "
|
|
78
|
+
"handlebars": "4.7.10"
|
|
79
79
|
},
|
|
80
80
|
"allowScripts": {
|
|
81
81
|
"fsevents@2.3.2": true,
|
package/src/BuildRouter.js
CHANGED
|
@@ -157,12 +157,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
|
|
|
157
157
|
* @returns {string}
|
|
158
158
|
*/
|
|
159
159
|
|
|
160
|
-
/**
|
|
161
|
-
* @typedef {object} ILogger
|
|
162
|
-
* @prop {Function} log
|
|
163
|
-
* @prop {Function} warn
|
|
164
|
-
* @prop {Function} error
|
|
165
|
-
*/
|
|
160
|
+
/** @typedef {import('./log/types').LevelLogger} ILogger */
|
|
166
161
|
|
|
167
162
|
/**
|
|
168
163
|
* @template {BaseConfiguration} [C=object]
|
|
@@ -206,11 +201,7 @@ const DUMMY_ROUTE = { id: 0, path: null, resolvers: [] };
|
|
|
206
201
|
* @prop {string|number} [resolverId] - only for text messages with random characters
|
|
207
202
|
*/
|
|
208
203
|
|
|
209
|
-
/**
|
|
210
|
-
* @typedef {object} Logger
|
|
211
|
-
* @prop {Function} log
|
|
212
|
-
* @prop {Function} error
|
|
213
|
-
*/
|
|
204
|
+
/** @typedef {import('./log/types').LevelLogger} Logger */
|
|
214
205
|
|
|
215
206
|
/**
|
|
216
207
|
* @typedef {object} RunningReq
|
|
@@ -400,6 +391,30 @@ class BuildRouter extends Router {
|
|
|
400
391
|
return this._botLoaded;
|
|
401
392
|
}
|
|
402
393
|
|
|
394
|
+
/**
|
|
395
|
+
* What an expired request was about, without the whole request and
|
|
396
|
+
* responder (they hold the state and the transcript).
|
|
397
|
+
*
|
|
398
|
+
* @param {any[]} args - the reduce() arguments: (req, res, ...)
|
|
399
|
+
* @param {number} timeoutMs
|
|
400
|
+
* @returns {object}
|
|
401
|
+
*/
|
|
402
|
+
static _expireInfo (args, timeoutMs) {
|
|
403
|
+
const [req] = Array.isArray(args) ? args : [];
|
|
404
|
+
let action = null;
|
|
405
|
+
try {
|
|
406
|
+
action = req && typeof req.action === 'function' ? req.action() : null;
|
|
407
|
+
} catch (e) {
|
|
408
|
+
// the action is only a hint
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
senderId: (req && req.senderId) || null,
|
|
412
|
+
pageId: (req && req.pageId) || null,
|
|
413
|
+
action,
|
|
414
|
+
timeoutMs
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
403
418
|
async reduce (...args) {
|
|
404
419
|
await this.preload();
|
|
405
420
|
|
|
@@ -411,7 +426,7 @@ class BuildRouter extends Router {
|
|
|
411
426
|
const timeoutPromise = new Promise((resolve) => {
|
|
412
427
|
timeout = setTimeout(() => {
|
|
413
428
|
timeout = null;
|
|
414
|
-
this._logger.error('BuildRouter EXPIRE',
|
|
429
|
+
this._logger.error('BuildRouter EXPIRE', BuildRouter._expireInfo(args, this.runningReqTimeout));
|
|
415
430
|
this._runningReqs = this._runningReqs
|
|
416
431
|
.filter((rr) => rr !== runningRequest);
|
|
417
432
|
resolve();
|
|
@@ -527,7 +542,10 @@ class BuildRouter extends Router {
|
|
|
527
542
|
this._runningReqs
|
|
528
543
|
.filter((req) => {
|
|
529
544
|
if (req.started < threshold) {
|
|
530
|
-
this._logger.error('BuildRouter EXPIRE filter',
|
|
545
|
+
this._logger.error('BuildRouter EXPIRE filter', {
|
|
546
|
+
...BuildRouter._expireInfo(req.args, this.runningReqTimeout),
|
|
547
|
+
startedAt: new Date(req.started).toISOString()
|
|
548
|
+
});
|
|
531
549
|
return false;
|
|
532
550
|
}
|
|
533
551
|
return true;
|
package/src/CallbackAuditLog.js
CHANGED
|
@@ -35,11 +35,7 @@ const TYPE_INFO = 'Info';
|
|
|
35
35
|
* @prop {string} [ro] - referrer || origin
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* @typedef {object} Logger
|
|
40
|
-
* @prop {Function} log
|
|
41
|
-
* @prop {Function} error
|
|
42
|
-
*/
|
|
38
|
+
/** @typedef {import('./log/types').LevelLogger} Logger */
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
41
|
* @typedef {object} AuditLogEntry
|
package/src/ChatGpt.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const nodeFetch = require('node-fetch').default;
|
|
7
|
-
const util = require('util');
|
|
8
7
|
const { PHONE_REGEX, EMAIL_REGEX } = require('./systemEntities/regexps');
|
|
9
8
|
const LLM = require('./LLM');
|
|
10
9
|
|
|
@@ -101,11 +100,7 @@ const LLM = require('./LLM');
|
|
|
101
100
|
* @prop {ChatGPTUsage} usage
|
|
102
101
|
*/
|
|
103
102
|
|
|
104
|
-
/**
|
|
105
|
-
* @typedef {object} Logger
|
|
106
|
-
* @prop {Function} log
|
|
107
|
-
* @prop {Function} error
|
|
108
|
-
*/
|
|
103
|
+
/** @typedef {import('./log/types').LevelLogger} Logger */
|
|
109
104
|
|
|
110
105
|
/**
|
|
111
106
|
* @typedef {object} GptToolCall
|
|
@@ -178,6 +173,9 @@ const LLM = require('./LLM');
|
|
|
178
173
|
/**
|
|
179
174
|
* @class ChatGpt
|
|
180
175
|
*/
|
|
176
|
+
// how much of the input and the output the info row of an LLM call keeps
|
|
177
|
+
const SUMMARY_TEXT_LENGTH = 2000;
|
|
178
|
+
|
|
181
179
|
class ChatGpt {
|
|
182
180
|
|
|
183
181
|
/**
|
|
@@ -242,15 +240,41 @@ class ChatGpt {
|
|
|
242
240
|
return this._options.model;
|
|
243
241
|
}
|
|
244
242
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The info row of one LLM call: the new input (the last message, not the
|
|
245
|
+
* whole transcript), the output and the usage. The whole request and
|
|
246
|
+
* response are in the debug rows.
|
|
247
|
+
*
|
|
248
|
+
* @param {object} body - the request body
|
|
249
|
+
* @param {ChatGPTChoice} choice
|
|
250
|
+
* @param {LLMUsage} usage
|
|
251
|
+
* @param {number} durationMs
|
|
252
|
+
* @returns {object}
|
|
253
|
+
*/
|
|
254
|
+
static _summary (body, choice, usage, durationMs) {
|
|
255
|
+
const { messages = [], tools = [] } = body;
|
|
256
|
+
const last = messages[messages.length - 1] || {};
|
|
257
|
+
const { message = {}, finish_reason: finishReason } = choice || {};
|
|
258
|
+
const cut = (text) => (typeof text === 'string' && text.length > SUMMARY_TEXT_LENGTH
|
|
259
|
+
? `${text.slice(0, SUMMARY_TEXT_LENGTH)}… (${text.length - SUMMARY_TEXT_LENGTH} more chars)`
|
|
260
|
+
: text);
|
|
261
|
+
return {
|
|
262
|
+
...usage,
|
|
263
|
+
durationMs,
|
|
264
|
+
finishReason,
|
|
265
|
+
messageCount: messages.length,
|
|
266
|
+
toolCount: tools.length,
|
|
267
|
+
input: { role: last.role, content: cut(last.content) },
|
|
268
|
+
output: cut(message.content),
|
|
269
|
+
...(message.tool_calls && message.tool_calls.length
|
|
270
|
+
? {
|
|
271
|
+
toolCalls: message.tool_calls.map((tc) => ({
|
|
272
|
+
name: tc.function && tc.function.name,
|
|
273
|
+
args: cut(tc.function && tc.function.arguments)
|
|
274
|
+
}))
|
|
275
|
+
}
|
|
276
|
+
: {})
|
|
277
|
+
};
|
|
254
278
|
}
|
|
255
279
|
|
|
256
280
|
/**
|
|
@@ -457,7 +481,8 @@ class ChatGpt {
|
|
|
457
481
|
|
|
458
482
|
const apiUrl = `${this._openAiEndpoint}/chat/completions${this._apiKey ? `?api-version=${encodeURIComponent(this._apiVersion)}` : ''}`;
|
|
459
483
|
|
|
460
|
-
|
|
484
|
+
// the whole prompt: verbose only (or replayed with an error)
|
|
485
|
+
this._logger.debug('#GPT request', body);
|
|
461
486
|
|
|
462
487
|
const startTime = Date.now();
|
|
463
488
|
const response = await this._fetch(apiUrl, {
|
|
@@ -508,11 +533,14 @@ class ChatGpt {
|
|
|
508
533
|
|
|
509
534
|
const durationMs = Date.now() - startTime;
|
|
510
535
|
|
|
511
|
-
this.
|
|
536
|
+
this._logger.debug('#GPT response body', { responseData, durationMs });
|
|
537
|
+
this._logger.info('#GPT response', ChatGpt._summary(body, choice, llmUsage, durationMs));
|
|
512
538
|
|
|
513
539
|
return { choice, usage: llmUsage };
|
|
514
540
|
} catch (e) {
|
|
515
|
-
|
|
541
|
+
// the caller logs the error once (LLMSession: the fallback message);
|
|
542
|
+
// the detail goes with it (replayed with the error)
|
|
543
|
+
this._logger.debug('#GPT failed', {
|
|
516
544
|
message: e.message, body, responseText, responseData, status, statusText
|
|
517
545
|
});
|
|
518
546
|
throw e;
|
|
@@ -95,11 +95,7 @@ const DEFAULT_TEXT_THRESHOLD = 0.8;
|
|
|
95
95
|
* @prop {number} stepCount
|
|
96
96
|
*/
|
|
97
97
|
|
|
98
|
-
/**
|
|
99
|
-
* @typedef {object} Logger
|
|
100
|
-
* @prop {Function} log
|
|
101
|
-
* @prop {Function} error
|
|
102
|
-
*/
|
|
98
|
+
/** @typedef {import('./log/types').LevelLogger} Logger */
|
|
103
99
|
|
|
104
100
|
/**
|
|
105
101
|
* Automated Conversation tests runner
|
package/src/LLM.js
CHANGED
|
@@ -60,6 +60,7 @@ const LLMSession = require('./LLMSession');
|
|
|
60
60
|
* @typedef {object} PrepocessedRuleData
|
|
61
61
|
* @prop {Function} condition
|
|
62
62
|
* @prop {PreprocessorOutput} rule
|
|
63
|
+
* @prop {string[]} [aiTags] - original rule definition (from the designer), for logs
|
|
63
64
|
*/
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -75,12 +76,24 @@ const LLMSession = require('./LLMSession');
|
|
|
75
76
|
* @typedef {RuleScore & PreprocessedRule} RuleWithScore
|
|
76
77
|
*/
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {object} EvaluatedRule
|
|
81
|
+
* @prop {string|number} [id]
|
|
82
|
+
* @prop {EvaluationRuleAction} [action]
|
|
83
|
+
* @prop {string[]} [aiTags]
|
|
84
|
+
* @prop {boolean} conditionPassed - false, when the rule was skipped by its condition
|
|
85
|
+
* @prop {number} score - 0, when the rule did not match at all
|
|
86
|
+
* @prop {boolean} matched - score reached the threshold
|
|
87
|
+
*/
|
|
88
|
+
|
|
78
89
|
/**
|
|
79
90
|
* @typedef {object} EvaluationResult
|
|
80
91
|
* @prop {string} action
|
|
81
92
|
* @prop {boolean} discard
|
|
82
93
|
* @prop {RuleWithScore[]} results
|
|
83
94
|
* @prop {object} setState
|
|
95
|
+
* @prop {number} [threshold] - minimal score of a matching rule
|
|
96
|
+
* @prop {EvaluatedRule[]} [evaluated] - every rule and the decision made about it
|
|
84
97
|
*/
|
|
85
98
|
|
|
86
99
|
/**
|
|
@@ -190,11 +203,7 @@ const LLMSession = require('./LLMSession');
|
|
|
190
203
|
* @prop {LogPrompt} logPrompt
|
|
191
204
|
*/
|
|
192
205
|
|
|
193
|
-
/**
|
|
194
|
-
* @typedef {object} Logger
|
|
195
|
-
* @prop {Function} log
|
|
196
|
-
* @prop {Function} error
|
|
197
|
-
*/
|
|
206
|
+
/** @typedef {import('./log/types').LevelLogger} Logger */
|
|
198
207
|
|
|
199
208
|
/**
|
|
200
209
|
* @typedef {object} VectorSearchDocument
|
|
@@ -527,6 +536,8 @@ class LLM {
|
|
|
527
536
|
|
|
528
537
|
return {
|
|
529
538
|
...rest,
|
|
539
|
+
// kept, so logs can show the rule as it was defined in the designer
|
|
540
|
+
aiTags,
|
|
530
541
|
condition,
|
|
531
542
|
rule
|
|
532
543
|
};
|
|
@@ -559,14 +570,27 @@ class LLM {
|
|
|
559
570
|
const setState = {};
|
|
560
571
|
|
|
561
572
|
const sAct = Object.values(LLM.EVALUATION_ACTIONS);
|
|
573
|
+
const { threshold } = this._ai;
|
|
574
|
+
|
|
575
|
+
/** @type {EvaluatedRule[]} */
|
|
576
|
+
const evaluated = [];
|
|
562
577
|
|
|
563
578
|
const results = rules
|
|
564
|
-
.filter((rule) =>
|
|
579
|
+
.filter((rule) => {
|
|
580
|
+
const conditionPassed = !!rule.condition(req, res);
|
|
581
|
+
if (!conditionPassed) {
|
|
582
|
+
evaluated.push(LLM._describeRule(rule, false, 0, threshold));
|
|
583
|
+
}
|
|
584
|
+
return conditionPassed;
|
|
585
|
+
})
|
|
565
586
|
.map((rule) => {
|
|
566
587
|
const matched = this._ai.matcher
|
|
567
588
|
.matchText(text, rule.rule, nlpResult, state);
|
|
568
589
|
|
|
569
|
-
|
|
590
|
+
const score = matched ? matched.score : 0;
|
|
591
|
+
evaluated.push(LLM._describeRule(rule, true, score, threshold));
|
|
592
|
+
|
|
593
|
+
if (!matched || matched.score < threshold) {
|
|
570
594
|
return null;
|
|
571
595
|
}
|
|
572
596
|
|
|
@@ -594,10 +618,51 @@ class LLM {
|
|
|
594
618
|
setState,
|
|
595
619
|
results,
|
|
596
620
|
discard,
|
|
597
|
-
action: discard ? null : topRankedAction
|
|
621
|
+
action: discard ? null : topRankedAction,
|
|
622
|
+
threshold,
|
|
623
|
+
evaluated
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Rule as a plain object for logs (RegExp objects serialize as {})
|
|
629
|
+
*
|
|
630
|
+
* @param {PreprocessedRule} rule
|
|
631
|
+
* @param {boolean} conditionPassed
|
|
632
|
+
* @param {number} score
|
|
633
|
+
* @param {number} threshold
|
|
634
|
+
* @returns {EvaluatedRule}
|
|
635
|
+
*/
|
|
636
|
+
static _describeRule (rule, conditionPassed, score, threshold) {
|
|
637
|
+
// @ts-ignore
|
|
638
|
+
const { id, action = null, aiTags } = rule;
|
|
639
|
+
return {
|
|
640
|
+
id,
|
|
641
|
+
action,
|
|
642
|
+
aiTags: aiTags || LLM._ruleTags(rule.rule),
|
|
643
|
+
conditionPassed,
|
|
644
|
+
score,
|
|
645
|
+
matched: conditionPassed && score >= threshold
|
|
598
646
|
};
|
|
599
647
|
}
|
|
600
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Fallback for rules added without their definition
|
|
651
|
+
*
|
|
652
|
+
* @param {PreprocessorOutput} rule
|
|
653
|
+
* @returns {string[]}
|
|
654
|
+
*/
|
|
655
|
+
static _ruleTags (rule) {
|
|
656
|
+
if (!rule) return [];
|
|
657
|
+
/** @type {any} */
|
|
658
|
+
const r = rule;
|
|
659
|
+
return [
|
|
660
|
+
...(r.regexps || []).map((x) => `regexp:${x.r instanceof RegExp ? x.r.source : x.r}`),
|
|
661
|
+
...(r.intents || []).map((i) => `intent:${i}`),
|
|
662
|
+
...(r.entities || []).map((e) => `@${e.entity}${e.optional ? '?' : ''}`)
|
|
663
|
+
];
|
|
664
|
+
}
|
|
665
|
+
|
|
601
666
|
}
|
|
602
667
|
|
|
603
668
|
module.exports = LLM;
|
package/src/LLMDispatcher.js
CHANGED
|
@@ -39,11 +39,7 @@ const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{
|
|
|
39
39
|
* @prop {RouterResolver} resolve
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {object} Logger
|
|
44
|
-
* @prop {Function} log
|
|
45
|
-
* @prop {Function} error
|
|
46
|
-
*/
|
|
42
|
+
/** @typedef {import('./log/types').LevelLogger} Logger */
|
|
47
43
|
|
|
48
44
|
/**
|
|
49
45
|
* @typedef {object} DispatcherOptions
|
package/src/LLMSession.js
CHANGED
|
@@ -429,7 +429,7 @@ class LLMSession {
|
|
|
429
429
|
} else if (errors.length) {
|
|
430
430
|
// @ts-ignore
|
|
431
431
|
const errs = errors.map((e) => e.error);
|
|
432
|
-
this._llm.log.
|
|
432
|
+
this._llm.log.debug('LLMSession failures', { errors: errs.map((e) => e.message) });
|
|
433
433
|
throw new Error(errs.map((e) => e.message).join(', '));
|
|
434
434
|
}
|
|
435
435
|
}
|
package/src/Processor.js
CHANGED
|
@@ -14,6 +14,7 @@ const { prepareState, mergeState, isUserInteraction } = require('./utils/stateVa
|
|
|
14
14
|
const LLM = require('./LLM');
|
|
15
15
|
const LLMMockProvider = require('./LLMMockProvider');
|
|
16
16
|
const LLMDispatcher = require('./LLMDispatcher');
|
|
17
|
+
const logSpan = require('./utils/logSpan');
|
|
17
18
|
|
|
18
19
|
/** @typedef {import('./wingbot/CustomEntityDetectionModel').Intent} Intent */
|
|
19
20
|
/** @typedef {import('./ReducerWrapper')} ReducerWrapper */
|
|
@@ -85,12 +86,7 @@ const LLMDispatcher = require('./LLMDispatcher');
|
|
|
85
86
|
* @type {InteractionEvent}
|
|
86
87
|
*/
|
|
87
88
|
|
|
88
|
-
/**
|
|
89
|
-
* @typedef {object} ILogger
|
|
90
|
-
* @prop {Function} log
|
|
91
|
-
* @prop {Function} warn
|
|
92
|
-
* @prop {Function} error
|
|
93
|
-
*/
|
|
89
|
+
/** @typedef {import('./log/types').LevelLogger} ILogger */
|
|
94
90
|
|
|
95
91
|
/**
|
|
96
92
|
*
|
|
@@ -314,7 +310,7 @@ class Processor extends EventEmitter {
|
|
|
314
310
|
|
|
315
311
|
async _reportError (pageId, err, event, senderId = null) {
|
|
316
312
|
if (err.code === 204) {
|
|
317
|
-
this.options.log.
|
|
313
|
+
this.options.log.debug('nothing sent', { message: err.message, senderId, pageId });
|
|
318
314
|
return;
|
|
319
315
|
}
|
|
320
316
|
if (err.code !== 403) {
|
|
@@ -335,7 +331,9 @@ class Processor extends EventEmitter {
|
|
|
335
331
|
|
|
336
332
|
await this.stateStorage.saveState(state);
|
|
337
333
|
} catch (e) {
|
|
338
|
-
this.options.log.error(
|
|
334
|
+
this.options.log.error('Failed to store the send error to the state', e, {
|
|
335
|
+
senderId, pageId, sendError: err.message
|
|
336
|
+
});
|
|
339
337
|
}
|
|
340
338
|
}
|
|
341
339
|
|
|
@@ -364,6 +362,20 @@ class Processor extends EventEmitter {
|
|
|
364
362
|
),
|
|
365
363
|
responderData = {}
|
|
366
364
|
) {
|
|
365
|
+
const senderId = message && ((message.sender && message.sender.id)
|
|
366
|
+
|| (message.optin && message.optin.user_ref));
|
|
367
|
+
|
|
368
|
+
// one span per processed event: its logs, LLM and storage calls share
|
|
369
|
+
// the operation, and the span carries the conversation (senderId)
|
|
370
|
+
return logSpan(
|
|
371
|
+
this.options.log,
|
|
372
|
+
'wingbot processMessage',
|
|
373
|
+
() => this._handleMessage(message, pageId, messageSender, responderData),
|
|
374
|
+
{ senderId, pageId }
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async _handleMessage (message, pageId, messageSender, responderData) {
|
|
367
379
|
const preloadPromise = this._preload();
|
|
368
380
|
|
|
369
381
|
try {
|
|
@@ -1052,7 +1064,9 @@ class Processor extends EventEmitter {
|
|
|
1052
1064
|
this.stateStorage
|
|
1053
1065
|
.getState(senderId, pageId)
|
|
1054
1066
|
.then((state) => {
|
|
1055
|
-
this.options.log.warn(
|
|
1067
|
+
this.options.log.warn('Locked state', {
|
|
1068
|
+
senderId, pageId, lock, state
|
|
1069
|
+
});
|
|
1056
1070
|
})
|
|
1057
1071
|
.catch(() => {});
|
|
1058
1072
|
|
|
@@ -1085,7 +1099,9 @@ class Processor extends EventEmitter {
|
|
|
1085
1099
|
const duration = Date.now() - now;
|
|
1086
1100
|
if (duration >= (this.options.waitForLockedState / 2)) {
|
|
1087
1101
|
try {
|
|
1088
|
-
this.options.log.warn(
|
|
1102
|
+
this.options.log.warn('Loading state took too long', {
|
|
1103
|
+
senderId, pageId, timeout, durationMs: duration
|
|
1104
|
+
});
|
|
1089
1105
|
} catch (e) {
|
|
1090
1106
|
// noop
|
|
1091
1107
|
}
|
package/src/Request.js
CHANGED
|
@@ -173,6 +173,11 @@ class Request {
|
|
|
173
173
|
*/
|
|
174
174
|
this.params = {};
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* @type {S} current state of the conversation
|
|
178
|
+
*/
|
|
179
|
+
this.state = state;
|
|
180
|
+
|
|
176
181
|
this.message = event.message || null;
|
|
177
182
|
|
|
178
183
|
let usePostBack;
|
|
@@ -222,11 +227,6 @@ class Request {
|
|
|
222
227
|
*/
|
|
223
228
|
this.pageId = pageId;
|
|
224
229
|
|
|
225
|
-
/**
|
|
226
|
-
* @type {S} current state of the conversation
|
|
227
|
-
*/
|
|
228
|
-
this.state = state;
|
|
229
|
-
|
|
230
230
|
/**
|
|
231
231
|
* @type {string[]} features supported messaging features
|
|
232
232
|
*/
|
|
@@ -1353,10 +1353,12 @@ It looks like the bot isn't connected to class BotApp or the Processor is used w
|
|
|
1353
1353
|
features = [FEATURE_TEXT]
|
|
1354
1354
|
) {
|
|
1355
1355
|
const postback = {
|
|
1356
|
-
payload:
|
|
1357
|
-
action
|
|
1358
|
-
|
|
1359
|
-
|
|
1356
|
+
payload: data === null
|
|
1357
|
+
? action
|
|
1358
|
+
: {
|
|
1359
|
+
action,
|
|
1360
|
+
data
|
|
1361
|
+
}
|
|
1360
1362
|
};
|
|
1361
1363
|
if (refAction) {
|
|
1362
1364
|
Object.assign(postback, {
|
package/src/analytics/GA4.js
CHANGED
|
@@ -126,7 +126,7 @@ class GA4 {
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
if (this._options.debug) {
|
|
129
|
-
this._logger.
|
|
129
|
+
this._logger.debug('GA4: starting session', event);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
const query = Object.entries(event)
|
|
@@ -255,15 +255,15 @@ class GA4 {
|
|
|
255
255
|
|
|
256
256
|
message = (validationMessages[0] || { description: message }).description;
|
|
257
257
|
} catch (e) {
|
|
258
|
-
this._logger.
|
|
258
|
+
this._logger.warn('GA4 debug failed', e);
|
|
259
259
|
message += ` +(${e.message})`;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
if (validationMessages.length || message) {
|
|
263
|
-
this._logger.
|
|
263
|
+
this._logger.debug('GA4: validationMessages', { validationMessages });
|
|
264
264
|
this._logger.error(`GA4: fail: ${message} [${res ? res.status : 0}]`, params.body);
|
|
265
265
|
} else {
|
|
266
|
-
this._logger.
|
|
266
|
+
this._logger.debug('GA4: debug', { status: res ? res.status : 0, body: params.body });
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
package/src/graphApi/GraphApi.js
CHANGED
|
@@ -28,11 +28,7 @@ const DEFAULT_CACHE = 86400000; // 24 hours
|
|
|
28
28
|
/** @typedef {import('../CallbackAuditLog')} AuditLog */
|
|
29
29
|
/** @typedef {import('graphql')} GqlLib */
|
|
30
30
|
|
|
31
|
-
/**
|
|
32
|
-
* @typedef {object} Logger
|
|
33
|
-
* @prop {Function} log
|
|
34
|
-
* @prop {Function} error
|
|
35
|
-
*/
|
|
31
|
+
/** @typedef {import('../log/types').LevelLogger} Logger */
|
|
36
32
|
|
|
37
33
|
class GraphApi {
|
|
38
34
|
|
package/src/graphApi/gqlRules.js
CHANGED
|
@@ -10,11 +10,7 @@ const {
|
|
|
10
10
|
|
|
11
11
|
/** @typedef {import('graphql').ValidationRule} ValidationRule */
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {object} Logger
|
|
15
|
-
* @prop {Function} log
|
|
16
|
-
* @prop {Function} error
|
|
17
|
-
*/
|
|
13
|
+
/** @typedef {import('../log/types').LevelLogger} Logger */
|
|
18
14
|
|
|
19
15
|
/**
|
|
20
16
|
*
|
|
@@ -58,7 +54,7 @@ function gqlRules (variables, isProduction, hideVerboseErrors, log = console) {
|
|
|
58
54
|
// This can be used for logging or to implement rate limiting
|
|
59
55
|
onComplete: (complexity) => {
|
|
60
56
|
if (!isProduction) {
|
|
61
|
-
log.
|
|
57
|
+
log.debug('Determined query complexity', { complexity });
|
|
62
58
|
}
|
|
63
59
|
},
|
|
64
60
|
|
|
@@ -70,7 +66,7 @@ function gqlRules (variables, isProduction, hideVerboseErrors, log = console) {
|
|
|
70
66
|
log.error(msg);
|
|
71
67
|
return new GraphQLError('');
|
|
72
68
|
}
|
|
73
|
-
log.
|
|
69
|
+
log.warn('GQL Query too complex', { actual, max });
|
|
74
70
|
return new GraphQLError(`Query is too complex: ${actual}. Maximum allowed complexity: ${max}`);
|
|
75
71
|
},
|
|
76
72
|
|
package/src/log/types.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// GENERATED COPY of wingbot-super/packages/log/src/types.js - do not edit here.
|
|
2
|
+
// Edit packages/log, then run: node packages/log/scripts/copy.mjs packages/wingbot-llm
|
|
3
|
+
/**
|
|
4
|
+
* @author wingbot.ai
|
|
5
|
+
*
|
|
6
|
+
* The types of a log call, shared by the logger and the libraries that get
|
|
7
|
+
* it (wingbot, the orchestrator). Types only, no code: `scripts/copy.mjs`
|
|
8
|
+
* copies this file into the apps (next to the logger) and into the
|
|
9
|
+
* libraries (targets.json, `files: ["types"]`), so they import
|
|
10
|
+
* `LevelLogger` instead of declaring their own `Logger`.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A log call: `(message, [Error], [data...])`, the arguments in any order.
|
|
16
|
+
* The first Error is the exception, plain objects are merged into the data,
|
|
17
|
+
* other values go to `data.args`.
|
|
18
|
+
*
|
|
19
|
+
* Rules for every log call:
|
|
20
|
+
* 1. **The message is a constant**, the values go to the data:
|
|
21
|
+
* `log.warn('SAP getList failed', { status, taskType })`, not
|
|
22
|
+
* `` `SAP getList failed ${status}` ``. Never put `e.stack` or
|
|
23
|
+
* `JSON.stringify(...)` into the message: pass the Error, pass the object.
|
|
24
|
+
* (Only the first line, max 200 chars, becomes the message / App Insights
|
|
25
|
+
* event name; the rest goes to `fullMessage`.)
|
|
26
|
+
* 2. **Log once, where the error is handled.** An adapter (SapApi,
|
|
27
|
+
* MicrosoftApi) does not log a failure it throws: it calls
|
|
28
|
+
* `log.annotate({...})` and throws an Error with the context
|
|
29
|
+
* (`new Error(msg, { cause })`). The code that decides what happens (retry
|
|
30
|
+
* later, show an error, return 500) logs it once. Logging the same Error
|
|
31
|
+
* object again is marked as a duplicate and not reported again.
|
|
32
|
+
* 3. **Never log** a raw request / event, headers, cookies, tokens or config:
|
|
33
|
+
* use `requestLogInfo(event)`. Secret-looking keys, the `queryKeys` and the
|
|
34
|
+
* configured secret values are redacted anyway, but do not rely on it.
|
|
35
|
+
* Personal data (emails, transcripts) is allowed. How much data:
|
|
36
|
+
* - `debug`: the detail, whole objects too (the task, the slot, the
|
|
37
|
+
* transcript). It is written with `verbose`, or with the error of the
|
|
38
|
+
* trace (`replayOnError`), and cut by `redact` (`maxString`, `maxDepth`,
|
|
39
|
+
* `maxArray`).
|
|
40
|
+
* - `info`: the ids and the small values that explain the event (statuses,
|
|
41
|
+
* reason, emails), not whole objects: it is always written.
|
|
42
|
+
* - `warn` / `error`: what is needed to fix it, whole objects too.
|
|
43
|
+
* 4. **Wrap non-HTTP work** (worker runs, sync runs, queues) in `log.span()`,
|
|
44
|
+
* and call `await log.sendAndClose()` at the end of a lambda or worker run
|
|
45
|
+
* (it flushes only on Lambda; `{ force: true }` before the process exits).
|
|
46
|
+
* 5. **One row per business event.** A storage read or write that must be
|
|
47
|
+
* audited is `log.audit()`, other detail is `debug`. The decision about a
|
|
48
|
+
* task or slot (created, rejected and why, status changed) is one `info`
|
|
49
|
+
* row with the ids.
|
|
50
|
+
*
|
|
51
|
+
* @callback LogFn
|
|
52
|
+
* @param {any} message - a CONSTANT text: values go to the data, never `e.stack`
|
|
53
|
+
* or `JSON.stringify(...)` in it (only its first line, max 200 chars, is kept as
|
|
54
|
+
* the message). An Error is accepted too.
|
|
55
|
+
* @param {...any} args - the Error (pass it, do not stringify it) and data objects
|
|
56
|
+
* (`slotId`, `taskId`, ...; whole objects in `debug`, `warn` and `error`).
|
|
57
|
+
* Never a raw request, event, headers, tokens or config: use
|
|
58
|
+
* `requestLogInfo(event)`.
|
|
59
|
+
* @returns {void}
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** @typedef {<T>(name: string, fn: () => T|Promise<T>, attrs?: object) => Promise<T>} SpanFn */
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What a library needs from a logger: the five levels. `console`, the shared
|
|
66
|
+
* logger (lib/log of an app) and the orchestrator's moduleLoggerFactory
|
|
67
|
+
* logger have them. `span` is optional: the shared logger has it, a library
|
|
68
|
+
* checks it before the call (`console` has none).
|
|
69
|
+
*
|
|
70
|
+
* Use it in a library as the typedef
|
|
71
|
+
* `{import('./log/types').LevelLogger} Logger`.
|
|
72
|
+
*
|
|
73
|
+
* @typedef {object} LevelLogger
|
|
74
|
+
* @prop {LogFn} error **Someone must act**: a bug, lost or wrong data, an
|
|
75
|
+
* upstream call that failed after the retries, a business result that was not
|
|
76
|
+
* saved. Goes to App Insights (AppEvents, AppExceptions) and to Sentry, and
|
|
77
|
+
* triggers the error alerts, so do NOT use it for client mistakes or for
|
|
78
|
+
* handled states. Log it once, where the error is handled: an adapter that
|
|
79
|
+
* throws calls `log.annotate()` instead.
|
|
80
|
+
* Examples: a result written to the wrong row, `Bot preloader is not set`,
|
|
81
|
+
* SAP setResult failed after the retries.
|
|
82
|
+
* @prop {LogFn} warn **Degraded, but handled**: a 4xx from a client (bad
|
|
83
|
+
* body, invalid token), a transient upstream error before a retry, a discarded
|
|
84
|
+
* LLM reply, a duplicate-key retry, a fallback that was used.
|
|
85
|
+
* Examples: `HTTP 400 POST /api`, SAP timeout (next run retries), #CANARY.
|
|
86
|
+
* @prop {LogFn} info **Business events and expected states**: a slot opened,
|
|
87
|
+
* a task synced, an email sent, a test whitelist block, a missing translation.
|
|
88
|
+
* Dropped unless `verbose` (verboseLog in config). AppTraces.
|
|
89
|
+
* @prop {LogFn} log like `info`, but always written (AppEvents). Kept for the
|
|
90
|
+
* old call sites; prefer `info` (an event worth seeing in production) or
|
|
91
|
+
* `debug` (detail).
|
|
92
|
+
* @prop {LogFn} debug **Detail**: payloads, request / response bodies, step by
|
|
93
|
+
* step flow. Dropped unless `verbose`.
|
|
94
|
+
* @prop {SpanFn} [span] Runs `fn` in a child span (the shared logger only):
|
|
95
|
+
* see `LogFunctions.span` of the shared logger.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
module.exports = {};
|
|
@@ -110,11 +110,7 @@ const DEFAULT_CAMPAIGN_DATA = {
|
|
|
110
110
|
* @prop {object} meta
|
|
111
111
|
*/
|
|
112
112
|
|
|
113
|
-
/**
|
|
114
|
-
* @typedef {object} Logger
|
|
115
|
-
* @prop {Function} log
|
|
116
|
-
* @prop {Function} error
|
|
117
|
-
*/
|
|
113
|
+
/** @typedef {import('../log/types').LevelLogger} Logger */
|
|
118
114
|
|
|
119
115
|
/** @typedef {import('./api/notificationsApiFactory').NotificationsApiOptions} ApiOptions */
|
|
120
116
|
|
package/src/resolvers/message.js
CHANGED
|
@@ -457,6 +457,7 @@ function message (params, context = {}) {
|
|
|
457
457
|
let session;
|
|
458
458
|
let evaluation;
|
|
459
459
|
let discard;
|
|
460
|
+
let llmError;
|
|
460
461
|
|
|
461
462
|
try {
|
|
462
463
|
session = res.llmSessionWithHistory(params.llmContextType);
|
|
@@ -467,13 +468,25 @@ function message (params, context = {}) {
|
|
|
467
468
|
({ discard } = evaluation);
|
|
468
469
|
} catch (e) {
|
|
469
470
|
(context.log || console).error(`LLM ERR: ${e.message} [${req.senderId}]`, e);
|
|
471
|
+
llmError = e;
|
|
470
472
|
discard = true;
|
|
471
473
|
}
|
|
472
474
|
|
|
473
475
|
if (discard) {
|
|
476
|
+
// short summary: where it happened, what the LLM generated,
|
|
477
|
+
// what is sent instead and why (every rule with its score)
|
|
474
478
|
canaryLog(context.log, context.canaryLogs, 'LLM discarded', {
|
|
475
|
-
|
|
476
|
-
|
|
479
|
+
reason: llmError ? 'llm-error' : 'rule-matched',
|
|
480
|
+
...(llmError && { error: llmError.message }),
|
|
481
|
+
source: {
|
|
482
|
+
resolverId,
|
|
483
|
+
action: req.action(),
|
|
484
|
+
contextType: params.llmContextType || null
|
|
485
|
+
},
|
|
486
|
+
generated: (llmError ? null : session?.lastResponseSync()) || null,
|
|
487
|
+
fallbackText: text,
|
|
488
|
+
threshold: evaluation?.threshold ?? null,
|
|
489
|
+
rules: evaluation?.evaluated ?? []
|
|
477
490
|
});
|
|
478
491
|
} else {
|
|
479
492
|
|
package/src/utils/canaryLog.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author wingbot.ai
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} SpanLogger
|
|
8
|
+
* @prop {<T>(name: string, fn: () => T|Promise<T>, attributes?: object) => Promise<T>} [span]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs `fn` in a span of the app logger, when the logger has `span()`
|
|
13
|
+
* (the shared wingbot logger: an OpenTelemetry span, App Insights and Sentry).
|
|
14
|
+
* Other loggers (console) only run `fn`.
|
|
15
|
+
*
|
|
16
|
+
* The attributes without a value are left out.
|
|
17
|
+
*
|
|
18
|
+
* @template T
|
|
19
|
+
* @param {SpanLogger|any} log
|
|
20
|
+
* @param {string} name
|
|
21
|
+
* @param {() => T|Promise<T>} fn
|
|
22
|
+
* @param {Record<string, any>} [attributes]
|
|
23
|
+
* @returns {Promise<T>}
|
|
24
|
+
*/
|
|
25
|
+
async function logSpan (log, name, fn, attributes = {}) {
|
|
26
|
+
if (!log || typeof log.span !== 'function') {
|
|
27
|
+
return fn();
|
|
28
|
+
}
|
|
29
|
+
const attrs = Object.keys(attributes)
|
|
30
|
+
.filter((key) => attributes[key] !== undefined && attributes[key] !== null)
|
|
31
|
+
.reduce((o, key) => Object.assign(o, { [key]: attributes[key] }), {});
|
|
32
|
+
|
|
33
|
+
return log.span(name, fn, attrs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = logSpan;
|
package/src/utils/pathUtils.js
CHANGED
|
@@ -45,8 +45,14 @@ function parseActionPayload (object, needRawData = false) {
|
|
|
45
45
|
let isObject = typeof payload === 'object' && payload !== null;
|
|
46
46
|
|
|
47
47
|
if (typeof payload === 'string' && payload.match(/^\s*\{.*\}\s*$/)) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
try {
|
|
49
|
+
payload = JSON.parse(payload);
|
|
50
|
+
isObject = true;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (!(e instanceof SyntaxError)) throw e;
|
|
53
|
+
// broken JSON from the client: an unknown action, not a 500
|
|
54
|
+
// (the action check below rejects the raw string)
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
if (isObject) {
|
|
@@ -98,7 +98,11 @@ class CachedModel extends CustomEntityDetectionModel {
|
|
|
98
98
|
.filter((e) => this._entityDetectors.has(e.entity)
|
|
99
99
|
&& this._entityDetectors.get(e.entity).clearOverlaps);
|
|
100
100
|
|
|
101
|
-
if (this._options.verbose)
|
|
101
|
+
if (this._options.verbose) {
|
|
102
|
+
this._log.debug('#NLP before', {
|
|
103
|
+
text, before, intents, entities
|
|
104
|
+
});
|
|
105
|
+
}
|
|
102
106
|
|
|
103
107
|
[intents, entities] = this._attachEntities(intents, entities, before, expectedEntities);
|
|
104
108
|
|
|
@@ -106,11 +110,19 @@ class CachedModel extends CustomEntityDetectionModel {
|
|
|
106
110
|
.filter((e) => !this._entityDetectors.has(e.entity)
|
|
107
111
|
|| !this._entityDetectors.get(e.entity).clearOverlaps);
|
|
108
112
|
|
|
109
|
-
if (this._options.verbose)
|
|
113
|
+
if (this._options.verbose) {
|
|
114
|
+
this._log.debug('#NLP after', {
|
|
115
|
+
text, after, intents, entities
|
|
116
|
+
});
|
|
117
|
+
}
|
|
110
118
|
|
|
111
119
|
[intents, entities] = this._attachEntities(intents, entities, after);
|
|
112
120
|
|
|
113
|
-
if (this._options.verbose)
|
|
121
|
+
if (this._options.verbose) {
|
|
122
|
+
this._log.debug('#NLP attached', {
|
|
123
|
+
text, intents, entities, expectedEntities
|
|
124
|
+
});
|
|
125
|
+
}
|
|
114
126
|
|
|
115
127
|
return {
|
|
116
128
|
text: local.text,
|
|
@@ -304,7 +304,7 @@ class CustomEntityDetectionModel {
|
|
|
304
304
|
return zLen - aLen;
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
-
if (this._options.verbose) this._log.
|
|
307
|
+
if (this._options.verbose) this._log.debug('#NLP [nonOverlapping]', { entities, expectedEntities, justDuplicates });
|
|
308
308
|
|
|
309
309
|
let res = [];
|
|
310
310
|
|
|
@@ -354,7 +354,7 @@ class CustomEntityDetectionModel {
|
|
|
354
354
|
const othersConflict = res.some((e) => putback === e
|
|
355
355
|
|| (e.start < putback.end && e.end > putback.start));
|
|
356
356
|
|
|
357
|
-
this._log.
|
|
357
|
+
this._log.debug(`#NLP (${i}|${k} [putBack: ${entity.entity}|${putback.entity}] (${i}|${k})`, {
|
|
358
358
|
putback, entity, currentConflict, othersConflict
|
|
359
359
|
});
|
|
360
360
|
|
|
@@ -366,7 +366,7 @@ class CustomEntityDetectionModel {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
if (this._options.verbose) {
|
|
369
|
-
this._log.
|
|
369
|
+
this._log.debug(`#NLP (${i}) [nonOverlapping| ${entity.entity}:${entity.value}]`, {
|
|
370
370
|
willRemoveEntity: overlapping, overlapping, duplicate, isExpected, entity
|
|
371
371
|
});
|
|
372
372
|
}
|
|
@@ -109,7 +109,7 @@ class WingbotModel extends CachedModel {
|
|
|
109
109
|
`matches=${encodeURIComponent(this._matches)}`
|
|
110
110
|
];
|
|
111
111
|
|
|
112
|
-
if (this._options.verbose) this._log.
|
|
112
|
+
if (this._options.verbose) this._log.debug('#NLP query', { text, matches: this._matches, localEntities: entities });
|
|
113
113
|
|
|
114
114
|
if (entities) {
|
|
115
115
|
const buf = await this._brotli(Buffer.from(JSON.stringify({ entities })));
|
|
@@ -124,7 +124,7 @@ class WingbotModel extends CachedModel {
|
|
|
124
124
|
|
|
125
125
|
if (res.status === 403) {
|
|
126
126
|
// model does not exist
|
|
127
|
-
this._log.
|
|
127
|
+
this._log.warn('NLP model does not exist or is not deployed yet', { model: this._model });
|
|
128
128
|
return { intents: [], entities: [] };
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -136,7 +136,7 @@ class WingbotModel extends CachedModel {
|
|
|
136
136
|
throw new Error(`[${res.status}] Invalid JSON response: ${responseText}`);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
if (this._options.verbose) this._log.
|
|
139
|
+
if (this._options.verbose) this._log.debug('#NLP response', { text, response });
|
|
140
140
|
|
|
141
141
|
if (res.status >= 300) {
|
|
142
142
|
this._log.warn(`AI query failed: ${response.message || res.statusText}`);
|