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/.claude/settings.local.json +11 -0
- package/index.js +2 -0
- 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 +49 -2
- package/src/LLMToolRunner.js +580 -0
- package/src/Plugins.js +53 -0
- package/src/Processor.js +26 -10
- package/src/Responder.js +25 -0
- 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 +161 -0
- package/src/notifications/Notifications.js +1 -5
- package/src/resolvers/index.js +2 -0
- package/src/resolvers/llmTool.js +41 -0
- package/src/resolvers/message.js +51 -5
- 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/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/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
|
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,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
|
|
package/src/resolvers/index.js
CHANGED
|
@@ -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;
|
package/src/resolvers/message.js
CHANGED
|
@@ -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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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) {
|
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
|
}
|