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
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @author Ondrej Veres
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const LLMType = require('./LLMType');
|
|
7
|
+
const compileWithState = require('./utils/compileWithState');
|
|
8
|
+
const stateData = require('./utils/stateData');
|
|
9
|
+
const getCondition = require('./utils/getCondition');
|
|
10
|
+
const { compare, ConditionOperators } = require('./utils/customCondition');
|
|
11
|
+
const { getValue } = require('./utils/getUpdate');
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('./Request')} Request */
|
|
14
|
+
/** @typedef {import('./Responder')} Responder */
|
|
15
|
+
/** @typedef {import('./Plugins')} Plugins */
|
|
16
|
+
/** @typedef {import('./LLMSession')} LLMSession */
|
|
17
|
+
/** @typedef {import('./Responder').QuickReply} QuickReply */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* texts in languages as the designer exports them ([{ l, t }], as the multilang inputs),
|
|
21
|
+
* or { [lang]: text }
|
|
22
|
+
*
|
|
23
|
+
* @typedef {{ l: string|null, t: string }[]|{ [lang: string]: string }} ToolTexts
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {object} ToolParameterDefinition
|
|
28
|
+
* @prop {string} name
|
|
29
|
+
* @prop {'string'|'number'|'boolean'|'enum'|'array'} type
|
|
30
|
+
* @prop {boolean} [optional]
|
|
31
|
+
* @prop {string} [description]
|
|
32
|
+
* @prop {{ value: string, description: string }[]} [values]
|
|
33
|
+
* @prop {ToolParameterDefinition[]} [items] - array: fields of the objects in the array
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A button of a situation - a prefilled call of a tool (or just a text the user sends)
|
|
38
|
+
*
|
|
39
|
+
* @typedef {object} ToolButtonDefinition
|
|
40
|
+
* @prop {ToolTexts} title
|
|
41
|
+
* @prop {string|null} tool - the tool the click calls (null - the title goes to the LLM as a text)
|
|
42
|
+
* @prop {object|null} [args] - arguments of the tool, strings can use the values
|
|
43
|
+
* of the situation ("{{botCounterOffer}}" keeps the number)
|
|
44
|
+
* @prop {boolean} [hasCondition] - the condition (as the conditions of the designer):
|
|
45
|
+
* the variables are the values of the situation and the state
|
|
46
|
+
* @prop {boolean} [hasEditableCondition]
|
|
47
|
+
* @prop {any[][]} [editableCondition]
|
|
48
|
+
* @prop {string} [conditionFn]
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What happens in a situation - the designer sets either the instruction for the LLM
|
|
53
|
+
* (the conversation with the LLM continues), or the target (a postback)
|
|
54
|
+
*
|
|
55
|
+
* @typedef {object} ToolSituationDefinition
|
|
56
|
+
* @prop {ToolTexts} [instruction]
|
|
57
|
+
* @prop {string|null} [action] - target interaction
|
|
58
|
+
* @prop {object} [setState] - set before the postback to the target
|
|
59
|
+
* (strings can use the values of the situation)
|
|
60
|
+
* @prop {ToolButtonDefinition[]} [buttons] - offered with the answer of the LLM
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tool as it comes from the designer (Tools tab, on the root block of the snapshot)
|
|
65
|
+
*
|
|
66
|
+
* @typedef {object} ToolDefinition
|
|
67
|
+
* @prop {string} id
|
|
68
|
+
* @prop {string} description
|
|
69
|
+
* @prop {ToolParameterDefinition[]} [parameters]
|
|
70
|
+
* @prop {{ [id: string]: ToolSituationDefinition }} [situations]
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What a tool (registered with plugins.registerTool()) returns - the situation, the code
|
|
75
|
+
* found out. What follows (an instruction for the LLM, or a target) is set in the designer.
|
|
76
|
+
*
|
|
77
|
+
* @typedef {object} ToolResult
|
|
78
|
+
* @prop {string} situation - id of the situation
|
|
79
|
+
* @prop {object} [data] - values for the texts, the set state and the buttons
|
|
80
|
+
* @prop {string} [asTool] - the situation is defined on another tool
|
|
81
|
+
* (e.g. a refusal, which turned out to be a repeated offer)
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @typedef {object} LLMToolRunnerOptions
|
|
86
|
+
* @prop {object[]} [blocks] - blocks of the snapshot (the tools are on the root block)
|
|
87
|
+
* @prop {Plugins} [plugins] - code of the tools (plugins.registerTool())
|
|
88
|
+
* @prop {Console} [log]
|
|
89
|
+
* @prop {boolean} [allowForbiddenSnippetWords] - for the code conditions of the buttons
|
|
90
|
+
* (as in the bot)
|
|
91
|
+
* @prop {string} [source] - for the logs and the tracking (e.g. "prompt", "mainLoop")
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {ToolTexts} texts
|
|
96
|
+
* @param {string} lang
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function textForLang (texts, lang) {
|
|
100
|
+
if (!texts) {
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
// a text can have alternatives (["Accept", "Take it"]) - the first filled one is used
|
|
104
|
+
const first = (t) => (Array.isArray(t) ? t.find((a) => `${a || ''}`.trim()) || '' : t || '');
|
|
105
|
+
const byLang = Array.isArray(texts)
|
|
106
|
+
? Object.fromEntries(texts.map((tr) => [tr.l || '', first(tr.t)]))
|
|
107
|
+
: Object.fromEntries(Object.entries(texts).map(([l, t]) => [l, first(t)]));
|
|
108
|
+
if (lang && `${byLang[lang] || ''}`.trim()) {
|
|
109
|
+
return byLang[lang];
|
|
110
|
+
}
|
|
111
|
+
return byLang.en || Object.values(byLang).find((t) => `${t || ''}`.trim()) || '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {ToolParameterDefinition} param
|
|
116
|
+
* @returns {LLMType}
|
|
117
|
+
*/
|
|
118
|
+
function parameterType (param) {
|
|
119
|
+
let type;
|
|
120
|
+
let { description } = param;
|
|
121
|
+
|
|
122
|
+
switch (param.type) {
|
|
123
|
+
case 'number':
|
|
124
|
+
type = LLMType.number();
|
|
125
|
+
break;
|
|
126
|
+
case 'boolean':
|
|
127
|
+
type = LLMType.boolean();
|
|
128
|
+
break;
|
|
129
|
+
case 'enum':
|
|
130
|
+
type = LLMType.enum(param.values.map((v) => v.value));
|
|
131
|
+
description = [
|
|
132
|
+
description,
|
|
133
|
+
...param.values
|
|
134
|
+
.filter((v) => v.description)
|
|
135
|
+
.map((v) => `- ${v.value}: ${v.description}`)
|
|
136
|
+
].filter((d) => !!d).join('\n');
|
|
137
|
+
break;
|
|
138
|
+
case 'array':
|
|
139
|
+
// array of objects - the fields are parameters too
|
|
140
|
+
type = LLMType.array(LLMType.object(Object.fromEntries(
|
|
141
|
+
(param.items || []).map((item) => [item.name, parameterType(item)])
|
|
142
|
+
)));
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
type = LLMType.string();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (description) {
|
|
149
|
+
type.description(description);
|
|
150
|
+
}
|
|
151
|
+
return param.optional ? type.optional() : type;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {ToolDefinition} def
|
|
156
|
+
* @returns {LLMType}
|
|
157
|
+
*/
|
|
158
|
+
function parametersType (def) {
|
|
159
|
+
return LLMType.object(Object.fromEntries(
|
|
160
|
+
(def.parameters || []).map((param) => [param.name, parameterType(param)])
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// a template, which is just one value - "{{price}}"
|
|
165
|
+
const SINGLE_VALUE = /^\{\{\s*([a-zA-Z0-9_$]+)\s*\}\}$/;
|
|
166
|
+
|
|
167
|
+
// the click of a button of a situation - in the data of the quick reply
|
|
168
|
+
const BUTTON_KEY = '_llmToolButton';
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} template
|
|
172
|
+
* @param {(template: string) => string} compile
|
|
173
|
+
* @param {object} values
|
|
174
|
+
* @returns {any}
|
|
175
|
+
*/
|
|
176
|
+
function compileValue (template, compile, values) {
|
|
177
|
+
const single = template.trim().match(SINGLE_VALUE);
|
|
178
|
+
if (single && Object.prototype.hasOwnProperty.call(values, single[1])) {
|
|
179
|
+
return values[single[1]];
|
|
180
|
+
}
|
|
181
|
+
return compile(template);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set state from the designer - texts are filled with the values of the situation now
|
|
186
|
+
* (the values won't be in the state later), special values (increment, entity, ...)
|
|
187
|
+
* are kept. A template with just one value keeps its type ("{{price}}" stays a number).
|
|
188
|
+
*
|
|
189
|
+
* @param {object} setState
|
|
190
|
+
* @param {(template: string) => string} compile
|
|
191
|
+
* @param {object} [values]
|
|
192
|
+
* @returns {object}
|
|
193
|
+
*/
|
|
194
|
+
function quickReplySetState (setState, compile, values = {}) {
|
|
195
|
+
return Object.fromEntries(
|
|
196
|
+
Object.entries(setState || {})
|
|
197
|
+
.map(([key, value]) => [
|
|
198
|
+
key,
|
|
199
|
+
typeof value === 'string' ? compileValue(value, compile, values) : value
|
|
200
|
+
])
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Arguments of a tool for a button - every string can use the values of the situation,
|
|
206
|
+
* a template with just one value keeps its type
|
|
207
|
+
*
|
|
208
|
+
* @param {any} args
|
|
209
|
+
* @param {(template: string) => string} compile
|
|
210
|
+
* @param {object} [values]
|
|
211
|
+
* @returns {any}
|
|
212
|
+
*/
|
|
213
|
+
function compileArgs (args, compile, values = {}) {
|
|
214
|
+
if (typeof args === 'string') {
|
|
215
|
+
return compileValue(args, compile, values);
|
|
216
|
+
}
|
|
217
|
+
if (Array.isArray(args)) {
|
|
218
|
+
return args.map((a) => compileArgs(a, compile, values));
|
|
219
|
+
}
|
|
220
|
+
if (args && typeof args === 'object') {
|
|
221
|
+
return Object.fromEntries(Object.entries(args)
|
|
222
|
+
.map(([key, value]) => [key, compileArgs(value, compile, values)]));
|
|
223
|
+
}
|
|
224
|
+
return args;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Runs the tools placed in the interactions ("LLM Tool" blocks the request passed)
|
|
229
|
+
* within an LLM session, which talks to the user:
|
|
230
|
+
*
|
|
231
|
+
* - the LLM calls a tool by what the user did, the code of the tool
|
|
232
|
+
* (plugins.registerTool()) returns the situation
|
|
233
|
+
* - what follows is in the designer (Tools tab): the instruction for the LLM
|
|
234
|
+
* with the buttons, or the target interaction (a postback)
|
|
235
|
+
* - a button is a prefilled call of a tool - the click runs the same code
|
|
236
|
+
* as when the user writes it (handleButton())
|
|
237
|
+
*/
|
|
238
|
+
class LLMToolRunner {
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @param {Request} req
|
|
242
|
+
* @param {Responder} res
|
|
243
|
+
* @param {LLMToolRunnerOptions} [options]
|
|
244
|
+
*/
|
|
245
|
+
constructor (req, res, options = {}) {
|
|
246
|
+
const {
|
|
247
|
+
blocks = [], plugins = null, log = console, source = 'llm',
|
|
248
|
+
allowForbiddenSnippetWords = false
|
|
249
|
+
} = options;
|
|
250
|
+
|
|
251
|
+
this._req = req;
|
|
252
|
+
this._res = res;
|
|
253
|
+
this._plugins = plugins;
|
|
254
|
+
this._log = log;
|
|
255
|
+
this._source = source;
|
|
256
|
+
this._allowForbiddenSnippetWords = allowForbiddenSnippetWords;
|
|
257
|
+
|
|
258
|
+
const rootBlock = (blocks || []).find((b) => b && b.isRoot);
|
|
259
|
+
|
|
260
|
+
/** @type {ToolDefinition[]} */
|
|
261
|
+
this._definitions = (rootBlock && rootBlock.tools) || [];
|
|
262
|
+
|
|
263
|
+
/** @type {Map<string, (args: object, session: LLMSession) => Promise<string>>} */
|
|
264
|
+
this._attached = new Map();
|
|
265
|
+
|
|
266
|
+
/** @type {string|null} */
|
|
267
|
+
this.postBackAction = null;
|
|
268
|
+
|
|
269
|
+
/** @type {QuickReply[]} */
|
|
270
|
+
this.quickReplies = [];
|
|
271
|
+
|
|
272
|
+
/** number of tools attached to the session */
|
|
273
|
+
this.count = 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* the current state - with the changes of the tools (e.g. a new language
|
|
278
|
+
* is used for the texts right after the tool switched it)
|
|
279
|
+
*
|
|
280
|
+
* @returns {object}
|
|
281
|
+
*/
|
|
282
|
+
_stateData () {
|
|
283
|
+
return stateData(this._req, this._res);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {string} template
|
|
288
|
+
* @param {object} [values]
|
|
289
|
+
* @returns {string}
|
|
290
|
+
*/
|
|
291
|
+
_compile (template, values = {}) {
|
|
292
|
+
return compileWithState(this._req, this._res, template, {
|
|
293
|
+
...this._stateData(),
|
|
294
|
+
...values
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {string} action
|
|
300
|
+
* @param {LLMSession} session
|
|
301
|
+
* @returns {string}
|
|
302
|
+
*/
|
|
303
|
+
_makePostBack (action, session) {
|
|
304
|
+
if (this.postBackAction) {
|
|
305
|
+
return 'Ignored - the conversation already continues elsewhere.';
|
|
306
|
+
}
|
|
307
|
+
this.postBackAction = action;
|
|
308
|
+
session.stop();
|
|
309
|
+
return 'OK';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @param {ToolDefinition} def
|
|
314
|
+
* @param {string} situation
|
|
315
|
+
* @param {object} values
|
|
316
|
+
* @returns {string}
|
|
317
|
+
*/
|
|
318
|
+
_instructionText (def, situation, values) {
|
|
319
|
+
const { lang } = this._stateData();
|
|
320
|
+
const sit = (def.situations || {})[situation] || {};
|
|
321
|
+
const template = textForLang(sit.instruction, lang);
|
|
322
|
+
|
|
323
|
+
if (!template) {
|
|
324
|
+
this._log.error(`LLM tool "${def.id}" - situation "${situation}" has no instruction in the designer`);
|
|
325
|
+
return `${situation}: ${JSON.stringify(values)}`;
|
|
326
|
+
}
|
|
327
|
+
return this._compile(template, values);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The condition of the button - over the values of the situation and the state
|
|
332
|
+
*
|
|
333
|
+
* @param {ToolDefinition} def
|
|
334
|
+
* @param {ToolButtonDefinition} button
|
|
335
|
+
* @param {object} values
|
|
336
|
+
* @returns {boolean}
|
|
337
|
+
*/
|
|
338
|
+
_buttonShown (def, button, values) {
|
|
339
|
+
if (!button.hasCondition) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
if (!button.hasEditableCondition) {
|
|
344
|
+
// a code condition - (req, res, values)
|
|
345
|
+
const fn = getCondition(button, {
|
|
346
|
+
configuration: this._req.configuration,
|
|
347
|
+
allowForbiddenSnippetWords: this._allowForbiddenSnippetWords
|
|
348
|
+
}, `LLM tool "${def.id}" button`);
|
|
349
|
+
const shown = fn(this._req, this._res, values);
|
|
350
|
+
if (shown && typeof shown.then === 'function') {
|
|
351
|
+
// the buttons are made synchronously - an async condition can't be used
|
|
352
|
+
throw new Error('the condition must not be async');
|
|
353
|
+
}
|
|
354
|
+
return !!shown;
|
|
355
|
+
}
|
|
356
|
+
const data = { ...this._stateData(), ...values };
|
|
357
|
+
const raw = [
|
|
358
|
+
ConditionOperators['matches regexp'],
|
|
359
|
+
ConditionOperators['not matches regexp'],
|
|
360
|
+
ConditionOperators['is true'],
|
|
361
|
+
ConditionOperators['is false']
|
|
362
|
+
];
|
|
363
|
+
return (button.editableCondition || []).some((list) => list.every((cond) => compare(
|
|
364
|
+
getValue(cond.variable, data),
|
|
365
|
+
cond.operator,
|
|
366
|
+
raw.includes(cond.operator) ? cond.value : this._compile(cond.value, values)
|
|
367
|
+
)));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
this._log.error(`LLM tool "${def.id}" - invalid condition of a button: ${e.message}`, e);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The buttons of the situation - with the answer of the LLM
|
|
376
|
+
*
|
|
377
|
+
* @param {ToolDefinition} def
|
|
378
|
+
* @param {string} situation
|
|
379
|
+
* @param {object} values
|
|
380
|
+
*/
|
|
381
|
+
_addButtons (def, situation, values) {
|
|
382
|
+
const sit = (def.situations || {})[situation] || {};
|
|
383
|
+
const { lang } = this._stateData();
|
|
384
|
+
const compile = (t) => this._compile(t, values);
|
|
385
|
+
const action = this._res.currentAction();
|
|
386
|
+
|
|
387
|
+
(sit.buttons || [])
|
|
388
|
+
.filter((button) => this._buttonShown(def, button, values))
|
|
389
|
+
.forEach((button) => {
|
|
390
|
+
const title = compile(textForLang(button.title, lang)).trim();
|
|
391
|
+
|
|
392
|
+
if (!title) {
|
|
393
|
+
this._log.error(`LLM tool "${def.id}" - a button of "${situation}" has no title in the designer`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.quickReplies.push({
|
|
397
|
+
action,
|
|
398
|
+
title,
|
|
399
|
+
...(button.tool && {
|
|
400
|
+
data: {
|
|
401
|
+
[BUTTON_KEY]: {
|
|
402
|
+
tool: button.tool,
|
|
403
|
+
args: compileArgs(button.args || {}, compile, values)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Runs the code of the tool and what follows the situation
|
|
413
|
+
*
|
|
414
|
+
* @param {ToolDefinition} def
|
|
415
|
+
* @param {Function} fn
|
|
416
|
+
* @param {object} args
|
|
417
|
+
* @param {LLMSession} session
|
|
418
|
+
* @returns {Promise<string>}
|
|
419
|
+
*/
|
|
420
|
+
async _runTool (def, fn, args, session) {
|
|
421
|
+
const { _req: req, _res: res } = this;
|
|
422
|
+
|
|
423
|
+
/** @type {ToolResult} */
|
|
424
|
+
const result = await fn(args, req, res, session);
|
|
425
|
+
|
|
426
|
+
if (!result || !result.situation) {
|
|
427
|
+
// no situation - the result goes to the LLM as it is (as in LLMSession)
|
|
428
|
+
if (result === undefined || result === null) {
|
|
429
|
+
return 'OK';
|
|
430
|
+
}
|
|
431
|
+
return typeof result === 'string' ? result : JSON.stringify(result);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const { situation, data: values = {} } = result;
|
|
435
|
+
res.trackEvent('tool', this._source, def.id, situation);
|
|
436
|
+
|
|
437
|
+
const sitDef = result.asTool
|
|
438
|
+
? this._definitions.find((d) => d.id === result.asTool) || def
|
|
439
|
+
: def;
|
|
440
|
+
const sit = (sitDef.situations || {})[situation];
|
|
441
|
+
|
|
442
|
+
if (!sit) {
|
|
443
|
+
this._log.error(`LLM tool "${def.id}" - situation "${situation}" is not in the designer (tools.json)`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (sit && sit.action) {
|
|
447
|
+
if (this.postBackAction) {
|
|
448
|
+
// another tool has already moved the conversation - its state stays
|
|
449
|
+
return this._makePostBack(sit.action, session);
|
|
450
|
+
}
|
|
451
|
+
// the conversation continues in the target interaction
|
|
452
|
+
const setState = quickReplySetState(
|
|
453
|
+
sit.setState,
|
|
454
|
+
(t) => this._compile(t, values),
|
|
455
|
+
values
|
|
456
|
+
);
|
|
457
|
+
if (Object.keys(setState).length) {
|
|
458
|
+
res.setState(setState);
|
|
459
|
+
}
|
|
460
|
+
return this._makePostBack(sit.action, session);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// only the buttons of the last situation are offered (the earlier ones are outdated)
|
|
464
|
+
this.quickReplies = [];
|
|
465
|
+
this._addButtons(sitDef, situation, values);
|
|
466
|
+
return this._instructionText(sitDef, situation, values);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Adds the tools placed in the interactions, the request passed, to the session
|
|
471
|
+
*
|
|
472
|
+
* @param {LLMSession} session
|
|
473
|
+
* @returns {this}
|
|
474
|
+
*/
|
|
475
|
+
attach (session) {
|
|
476
|
+
this._res.llmTools()
|
|
477
|
+
.map((id) => {
|
|
478
|
+
const def = this._definitions.find((d) => d.id === id);
|
|
479
|
+
if (!def) {
|
|
480
|
+
this._log.error(`LLM tool "${id}" is placed in an interaction, but it's not in the Tools tab`);
|
|
481
|
+
}
|
|
482
|
+
return def;
|
|
483
|
+
})
|
|
484
|
+
.filter((def) => !!def)
|
|
485
|
+
.forEach((def) => {
|
|
486
|
+
const fn = this._plugins && this._plugins.getTool(def.id);
|
|
487
|
+
|
|
488
|
+
if (!fn) {
|
|
489
|
+
this._log.error(`LLM tool "${def.id}" is in the designer, but it's not registered in the code`);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const run = (args, s) => this._runTool(def, fn, args, s);
|
|
494
|
+
this._attached.set(def.id, run);
|
|
495
|
+
this.count += 1;
|
|
496
|
+
session.tool({
|
|
497
|
+
name: def.id,
|
|
498
|
+
description: def.description,
|
|
499
|
+
parameters: parametersType(def),
|
|
500
|
+
fn: run
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* The click of a button of a situation - calls its tool the same way, as when
|
|
509
|
+
* the LLM calls it (the call is in the conversation for the LLM).
|
|
510
|
+
* Then generate the answer - unless the situation made a postback (postBackAction).
|
|
511
|
+
*
|
|
512
|
+
* Only the tools placed on the way can be called (the data of the click come
|
|
513
|
+
* from the user - the same as writing it).
|
|
514
|
+
*
|
|
515
|
+
* @param {LLMSession} session
|
|
516
|
+
* @returns {Promise<boolean>} - a button was clicked
|
|
517
|
+
*/
|
|
518
|
+
async handleButton (session) {
|
|
519
|
+
const button = this._req.actionData()[BUTTON_KEY];
|
|
520
|
+
|
|
521
|
+
if (!button || typeof button.tool !== 'string') {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const run = this._attached.get(button.tool);
|
|
526
|
+
if (!run) {
|
|
527
|
+
this._log.error(`LLM tool "${button.tool}" of a button is not placed on the way to the interaction`);
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const args = button.args && typeof button.args === 'object' ? button.args : {};
|
|
532
|
+
const id = `button_${button.tool}`;
|
|
533
|
+
let content;
|
|
534
|
+
try {
|
|
535
|
+
content = await run(args, session);
|
|
536
|
+
} catch (e) {
|
|
537
|
+
// the same as a failed call of the LLM - not in the conversation, the LLM answers
|
|
538
|
+
this._log.error(`LLM tool "${button.tool}" of a button: ${e.message}`, e, { args });
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
session.push(
|
|
543
|
+
{
|
|
544
|
+
role: 'assistant',
|
|
545
|
+
toolCalls: [{
|
|
546
|
+
id, type: 'function', name: button.tool, args: JSON.stringify(args)
|
|
547
|
+
}]
|
|
548
|
+
},
|
|
549
|
+
{ role: 'tool', content, toolCallId: id }
|
|
550
|
+
);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Provider options for the generation - one tool call at a time, when there are tools
|
|
556
|
+
* (the tools change the state - e.g. two offers in one message would both be recorded)
|
|
557
|
+
*
|
|
558
|
+
* @param {LLMSession} session
|
|
559
|
+
* @returns {string|object}
|
|
560
|
+
*/
|
|
561
|
+
generateOptions (session) {
|
|
562
|
+
const { preset } = session;
|
|
563
|
+
if (this.count === 0) {
|
|
564
|
+
return preset;
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
...(typeof preset === 'string' || !preset ? { preset } : preset),
|
|
568
|
+
parallelToolCalls: false
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
LLMToolRunner.BUTTON_KEY = BUTTON_KEY;
|
|
574
|
+
LLMToolRunner.textForLang = textForLang;
|
|
575
|
+
LLMToolRunner.parameterType = parameterType;
|
|
576
|
+
LLMToolRunner.parametersType = parametersType;
|
|
577
|
+
LLMToolRunner.quickReplySetState = quickReplySetState;
|
|
578
|
+
LLMToolRunner.compileArgs = compileArgs;
|
|
579
|
+
|
|
580
|
+
module.exports = LLMToolRunner;
|
package/src/Plugins.js
CHANGED
|
@@ -46,6 +46,20 @@ const { compileWithState } = require('./utils');
|
|
|
46
46
|
* @prop {string[]} [notLastMessageItems]
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Tool, which can be offered to the LLM - its description and parameters are
|
|
51
|
+
* defined in the designer (tools.json)
|
|
52
|
+
*
|
|
53
|
+
* @template {object} [S=object]
|
|
54
|
+
* @template {BaseConfiguration} [C=object]
|
|
55
|
+
* @callback PluginTool
|
|
56
|
+
* @param {object} args - arguments filled by the LLM
|
|
57
|
+
* @param {Request<S,C>} req
|
|
58
|
+
* @param {Responder} res
|
|
59
|
+
* @param {import('./LLMSession')} session
|
|
60
|
+
* @returns {any|Promise<any>}
|
|
61
|
+
*/
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* Custom code plugins for BuildRouter and wingbot.ai
|
|
51
65
|
*
|
|
@@ -57,6 +71,9 @@ class Plugins {
|
|
|
57
71
|
|
|
58
72
|
constructor () {
|
|
59
73
|
this._plugins = new Map();
|
|
74
|
+
|
|
75
|
+
/** @type {Map<string,PluginTool<S,C>>} */
|
|
76
|
+
this._tools = new Map();
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
/**
|
|
@@ -214,6 +231,10 @@ class Plugins {
|
|
|
214
231
|
name._plugins.forEach((el, key) => {
|
|
215
232
|
this._plugins.set(key, el);
|
|
216
233
|
});
|
|
234
|
+
// a plugins object of an older wingbot has no tools
|
|
235
|
+
(name._tools || new Map()).forEach((el, key) => {
|
|
236
|
+
this._tools.set(key, el);
|
|
237
|
+
});
|
|
217
238
|
}
|
|
218
239
|
|
|
219
240
|
/**
|
|
@@ -231,6 +252,38 @@ class Plugins {
|
|
|
231
252
|
this._plugins.set(name, { pluginFactory, options });
|
|
232
253
|
}
|
|
233
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Register tool for the LLM
|
|
257
|
+
*
|
|
258
|
+
* @param {string} id - tool id (has to match the id in tools.json)
|
|
259
|
+
* @param {PluginTool<S,C>} tool
|
|
260
|
+
*/
|
|
261
|
+
registerTool (id, tool) {
|
|
262
|
+
if (typeof tool !== 'function') {
|
|
263
|
+
throw new Error(`Tool should be a function, ${typeof tool} given (tool: ${id})`);
|
|
264
|
+
}
|
|
265
|
+
this._tools.set(id, tool);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get registered tool
|
|
270
|
+
*
|
|
271
|
+
* @param {string} id
|
|
272
|
+
* @returns {PluginTool<S,C>|null}
|
|
273
|
+
*/
|
|
274
|
+
getTool (id) {
|
|
275
|
+
return this._tools.get(id) || null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Ids of all registered tools
|
|
280
|
+
*
|
|
281
|
+
* @returns {string[]}
|
|
282
|
+
*/
|
|
283
|
+
getToolIds () {
|
|
284
|
+
return Array.from(this._tools.keys());
|
|
285
|
+
}
|
|
286
|
+
|
|
234
287
|
}
|
|
235
288
|
|
|
236
289
|
module.exports = Plugins;
|