wingbot 3.76.18 → 3.77.0-alpha.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/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.log(`nothing sent: ${err.message}`, event);
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(`failed to log error "${err.message}" because: ${e.message}`, e, err);
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(`Locked state: ${senderId}, lock: ${lock}, at ${Date.now()}`, state);
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(`Loading state (${senderId}) for timeout ${timeout} took too long (${duration}ms).`);
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/Responder.js CHANGED
@@ -253,6 +253,9 @@ class Responder {
253
253
 
254
254
  /** @typedef {PromptSource|Promise<string>} PromptContextItem */
255
255
 
256
+ /** @type {Set<string>} - ids of the tools placed in the interactions, the request passed */
257
+ this._llmTools = new Set();
258
+
256
259
  /** @type {Map<string,PromptContextItem[]>} */
257
260
  this._llmContext = new Map([
258
261
  [this.LLM_CTX_DEFAULT, []],
@@ -301,6 +304,28 @@ class Responder {
301
304
  return this;
302
305
  }
303
306
 
307
+ /**
308
+ * Makes a tool (registered with plugins.registerTool()) available to the LLM,
309
+ * which talks to the user in this request (prompt messages, plugins using LLMToolRunner)
310
+ * - like the instructions, the tools are collected on the way to the interaction
311
+ *
312
+ * @param {string} toolId
313
+ * @returns {this}
314
+ */
315
+ llmAddTool (toolId) {
316
+ this._llmTools.add(toolId);
317
+ return this;
318
+ }
319
+
320
+ /**
321
+ * Ids of the tools added in this request (in the order they were added)
322
+ *
323
+ * @returns {string[]}
324
+ */
325
+ llmTools () {
326
+ return [...this._llmTools];
327
+ }
328
+
304
329
  /**
305
330
  *
306
331
  * @param {LLMFilter|LLMFilterFn} filter
@@ -126,7 +126,7 @@ class GA4 {
126
126
  };
127
127
 
128
128
  if (this._options.debug) {
129
- this._logger.log('GA4: starting session', event);
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.log('GA4 debug failed', e);
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.log('GA4: validationMessages', validationMessages);
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.log(`GA4: debug [${res ? res.status : 0}]`, params.body);
266
+ this._logger.debug('GA4: debug', { status: res ? res.status : 0, body: params.body });
267
267
  }
268
268
  }
269
269
 
@@ -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
 
@@ -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.log('Determined query complexity: ', complexity);
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.log(msg);
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
 
@@ -0,0 +1,161 @@
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
+ /**
63
+ * The role of a span (OpenTelemetry SpanKind). App Insights picks the table
64
+ * by it: `server` and `consumer` are AppRequests (the start of an operation:
65
+ * an HTTP request, a worker run taken from a timer or a queue), the others are
66
+ * AppDependencies (`client`: a call to another system, `internal`: a step).
67
+ *
68
+ * @typedef {'internal'|'server'|'client'|'producer'|'consumer'} SpanKindName
69
+ */
70
+
71
+ /**
72
+ * @typedef {object} SpanOptions
73
+ * @prop {SpanKindName} [kind] default `internal`
74
+ */
75
+
76
+ /**
77
+ * @typedef {<T>(
78
+ * name: string, fn: () => T|Promise<T>, attrs?: object, options?: SpanOptions
79
+ * ) => Promise<T>} SpanFn
80
+ */
81
+
82
+ /**
83
+ * A call to another system (SAP, Microsoft Graph, KSRM ...), see `log.httpCall()`.
84
+ *
85
+ * @typedef {object} HttpCall
86
+ * @prop {string} [method] `GET`, `POST` ...
87
+ * @prop {string|null} [url] the full URL (query secrets are redacted)
88
+ * @prop {Record<string, any>} [params] the small values that tell what the
89
+ * call is about (query parameters, ids, type, language)
90
+ * @prop {Record<string, any>|null} [headers] the request headers, when
91
+ * `send()` is not axios (axios: the headers it sent are read from the
92
+ * response); secret headers are redacted
93
+ * @prop {any} [body] the request body (redacted, cut at 8000 characters)
94
+ * @prop {Record<string, any>} [data] more fields of the row (ECS
95
+ * `eventAction` ...)
96
+ */
97
+
98
+ /**
99
+ * What `toResponse` reads; what it leaves out is read like an axios response
100
+ * (`status`, `data`, `headers`, `config.headers`).
101
+ *
102
+ * @typedef {object} HttpCallResponse
103
+ * @prop {number|null} [status] the HTTP status
104
+ * @prop {any} [body] the response body
105
+ * @prop {Record<string, any>|null} [headers] the response headers
106
+ * @prop {Record<string, any>|null} [requestHeaders] the headers that were sent
107
+ */
108
+
109
+ /**
110
+ * @template R
111
+ * @typedef {object} HttpCallOptions
112
+ * @prop {(res: R) => HttpCallResponse} [toResponse] reads the status, the body
113
+ * and the headers of what `send()` returns (and of `error.response`), or
114
+ * changes the body for the log
115
+ */
116
+
117
+ /**
118
+ * `httpCall(name, send, call, options)`, like `span(name, fn, data, options)`.
119
+ * The name is the span name and the message of the row, a constant per
120
+ * operation: `SAP getListVA`, `Graph sendMail` (not the URL, it has ids).
121
+ *
122
+ * @typedef {<R>(
123
+ * name: string, send: () => Promise<R>, call?: HttpCall, options?: HttpCallOptions<R>
124
+ * ) => Promise<R>} HttpCallFn
125
+ */
126
+
127
+ /**
128
+ * What a library needs from a logger: the five levels. `console`, the shared
129
+ * logger (lib/log of an app) and the orchestrator's moduleLoggerFactory
130
+ * logger have them. `span` is optional: the shared logger has it, a library
131
+ * checks it before the call (`console` has none).
132
+ *
133
+ * Use it in a library as the typedef
134
+ * `{import('./log/types').LevelLogger} Logger`.
135
+ *
136
+ * @typedef {object} LevelLogger
137
+ * @prop {LogFn} error **Someone must act**: a bug, lost or wrong data, an
138
+ * upstream call that failed after the retries, a business result that was not
139
+ * saved. Goes to App Insights (AppEvents, AppExceptions) and to Sentry, and
140
+ * triggers the error alerts, so do NOT use it for client mistakes or for
141
+ * handled states. Log it once, where the error is handled: an adapter that
142
+ * throws calls `log.annotate()` instead.
143
+ * Examples: a result written to the wrong row, `Bot preloader is not set`,
144
+ * SAP setResult failed after the retries.
145
+ * @prop {LogFn} warn **Degraded, but handled**: a 4xx from a client (bad
146
+ * body, invalid token), a transient upstream error before a retry, a discarded
147
+ * LLM reply, a duplicate-key retry, a fallback that was used.
148
+ * Examples: `HTTP 400 POST /api`, SAP timeout (next run retries), #CANARY.
149
+ * @prop {LogFn} info **Business events and expected states**: a slot opened,
150
+ * a task synced, an email sent, a test whitelist block, a missing translation.
151
+ * Dropped unless `verbose` (verboseLog in config). AppTraces.
152
+ * @prop {LogFn} log like `info`, but always written (AppEvents). Kept for the
153
+ * old call sites; prefer `info` (an event worth seeing in production) or
154
+ * `debug` (detail).
155
+ * @prop {LogFn} debug **Detail**: payloads, request / response bodies, step by
156
+ * step flow. Dropped unless `verbose`.
157
+ * @prop {SpanFn} [span] Runs `fn` in a child span (the shared logger only):
158
+ * see `LogFunctions.span` of the shared logger.
159
+ */
160
+
161
+ 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
 
@@ -18,11 +18,13 @@ const setState = require('./setState');
18
18
  const expectedInput = require('./expectedInput');
19
19
  const skip = require('./skip');
20
20
  const contextMessage = require('./contextMessage');
21
+ const llmTool = require('./llmTool');
21
22
 
22
23
  module.exports = {
23
24
  path,
24
25
  message,
25
26
  contextMessage,
27
+ llmTool,
26
28
  include,
27
29
  postback,
28
30
  expected,
@@ -0,0 +1,41 @@
1
+ /*
2
+ * @author Ondrej Veres
3
+ */
4
+ 'use strict';
5
+
6
+ const Router = require('../Router');
7
+ const getCondition = require('../utils/getCondition');
8
+
9
+ /** @typedef {import('../BuildRouter').BotContext<any>} BotContext */
10
+ /** @typedef {import('../Router').Resolver<any>} Resolver */
11
+ /** @typedef {import('../utils/getCondition').ConditionDefinition} ConditionDefinition */
12
+
13
+ /**
14
+ * @typedef {object} LlmToolParamsBase
15
+ * @property {string} toolId - id of a tool registered with plugins.registerTool()
16
+ */
17
+
18
+ /** @typedef {LlmToolParamsBase & ConditionDefinition} LlmToolParams */
19
+
20
+ /**
21
+ * Makes a tool available to the LLM, which talks to the user - like the instructions
22
+ * (contextMessage), it's collected on the way to the interaction
23
+ * (in the root - everywhere, in a dialog - only there)
24
+ *
25
+ * @param {LlmToolParams} params
26
+ * @param {BotContext} context
27
+ * @returns {Resolver}
28
+ */
29
+ function llmTool (params, context) {
30
+ const { toolId } = params;
31
+ const condition = getCondition(params, context, `LLM tool "${toolId}"`);
32
+
33
+ return async (req, res) => {
34
+ if (toolId && await condition(req, res)) {
35
+ res.llmAddTool(toolId);
36
+ }
37
+ return Router.CONTINUE;
38
+ };
39
+ }
40
+
41
+ module.exports = llmTool;
@@ -22,6 +22,7 @@ const {
22
22
  const { vars, VAR_TYPES } = require('../utils/stateVariables');
23
23
  const LLM = require('../LLM');
24
24
  const canaryLog = require('../utils/canaryLog');
25
+ const LLMToolRunner = require('../LLMToolRunner');
25
26
 
26
27
  /** @typedef {import('../Responder').VoiceControl} VoiceControl */
27
28
  /** @typedef {import('../BuildRouter').LinksMap} LinksMap */
@@ -317,9 +318,10 @@ function selectTranslation (resolverId, params, texts, data) {
317
318
  * @param {"message" | "prompt"} [params.type]
318
319
  * @param {string} [params.llmContextType]
319
320
  * @param {BotContext} context
321
+ * @param {import('../Plugins')} [plugins] - code of the LLM tools placed in the interactions
320
322
  * @returns {Resolver}
321
323
  */
322
- function message (params, context = {}) {
324
+ function message (params, context = {}, plugins = null) {
323
325
  const {
324
326
  // @ts-ignore
325
327
  isLastIndex, isLastMessage, linksMap, configuration, resolverId
@@ -457,23 +459,62 @@ function message (params, context = {}) {
457
459
  let session;
458
460
  let evaluation;
459
461
  let discard;
462
+ let llmError;
463
+
464
+ // the tools placed on the way to this interaction ("LLM Tool" blocks)
465
+ const tools = new LLMToolRunner(req, res, {
466
+ // @ts-ignore
467
+ blocks: context.blocks,
468
+ plugins,
469
+ log: context.log,
470
+ allowForbiddenSnippetWords: context.allowForbiddenSnippetWords,
471
+ source: 'prompt'
472
+ });
460
473
 
461
474
  try {
462
475
  session = res.llmSessionWithHistory(params.llmContextType);
463
- await session.systemPrompt(text)
464
- .generate();
476
+ tools.attach(session);
477
+ // a button of a tool - its tool runs the same way, as when the LLM calls it
478
+ await tools.handleButton(session);
479
+ if (!tools.postBackAction && !session.isStoppedSync()) {
480
+ await session.systemPrompt(text)
481
+ .generate(tools.generateOptions(session));
482
+ }
483
+
484
+ if (tools.postBackAction) {
485
+ // a situation with a target - the conversation continues in its interaction
486
+ postBack(tools.postBackAction);
487
+ return Router.END;
488
+ }
489
+
490
+ if (session.isStoppedSync()) {
491
+ // a tool took over the conversation (session.stop()) - nothing more is sent
492
+ return Router.END;
493
+ }
465
494
 
466
495
  evaluation = await res.llmEvaluate(session, params.llmContextType);
467
496
  ({ discard } = evaluation);
468
497
  } catch (e) {
469
498
  (context.log || console).error(`LLM ERR: ${e.message} [${req.senderId}]`, e);
499
+ llmError = e;
470
500
  discard = true;
471
501
  }
472
502
 
473
503
  if (discard) {
504
+ // short summary: where it happened, what the LLM generated,
505
+ // what is sent instead and why (every rule with its score)
474
506
  canaryLog(context.log, context.canaryLogs, 'LLM discarded', {
475
- evaluation,
476
- session
507
+ reason: llmError ? 'llm-error' : 'rule-matched',
508
+ ...(llmError && { error: llmError.message }),
509
+ source: {
510
+ resolverId,
511
+ action: req.action(),
512
+ contextType: params.llmContextType || null
513
+ },
514
+ generated: (llmError ? null : session?.lastResponseSync()) || null,
515
+ fallbackText: text,
516
+ threshold: evaluation?.threshold ?? null,
517
+ rules: evaluation?.evaluated ?? []
477
518
  });
478
519
  } else {
479
520
 
@@ -490,6 +531,11 @@ function message (params, context = {}) {
490
531
 
491
532
  res.setFlag(LLM.GPT_FLAG);
492
533
 
534
+ if (tools.quickReplies.length) {
535
+ // quick replies of the tools go with the answer of the LLM
536
+ sendReplies = [...(sendReplies || []), ...tools.quickReplies];
537
+ }
538
+
493
539
  const lastMessageI = messages.length - 1;
494
540
  messages.forEach((m, i) => {
495
541
  if (lastMessageI === i) {
@@ -3,12 +3,7 @@
3
3
  */
4
4
  'use strict';
5
5
 
6
- /**
7
- * @typedef {object} ILogger
8
- * @prop {Function} log
9
- * @prop {Function} warn
10
- * @prop {Function} error
11
- */
6
+ /** @typedef {import('../log/types').LevelLogger} ILogger */
12
7
 
13
8
  /**
14
9
  *
@@ -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;
@@ -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
- payload = JSON.parse(payload);
49
- isObject = true;
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) this._log.log(`#NLP: ${text} [before]`, { before, intents, entities });
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) this._log.log(`#NLP: ${text} [after]`, { after, intents, entities });
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) this._log.log(`#NLP: ${text} [attached]`, { intents, entities, expectedEntities });
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.log('#NLP [nonOverlapping]', { entities, expectedEntities, justDuplicates });
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.log(`#NLP (${i}|${k} [putBack: ${entity.entity}|${putback.entity}] (${i}|${k})`, {
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.log(`#NLP (${i}) [nonOverlapping| ${entity.entity}:${entity.value}]`, {
369
+ this._log.debug(`#NLP (${i}) [nonOverlapping| ${entity.entity}:${entity.value}]`, {
370
370
  willRemoveEntity: overlapping, overlapping, duplicate, isExpected, entity
371
371
  });
372
372
  }