wingbot 3.77.0-alpha.0 → 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 +1 -1
- package/src/LLMSession.js +48 -1
- package/src/LLMToolRunner.js +580 -0
- package/src/Plugins.js +53 -0
- package/src/Responder.js +25 -0
- package/src/log/types.js +64 -1
- package/src/resolvers/index.js +2 -0
- package/src/resolvers/llmTool.js +41 -0
- package/src/resolvers/message.js +36 -3
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npx mocha:*)",
|
|
5
|
+
"Bash(npx tsc *)",
|
|
6
|
+
"Bash(npm test *)",
|
|
7
|
+
"Bash(node -e \"const p = require\\('./bot/plugins/BTMLLM'\\); console.log\\('factory type:', typeof p\\); console.log\\('factory name:', p.name\\);\")",
|
|
8
|
+
"Bash(grep -n \"prompt\\\\`\\\\|tagged\\\\|render\\\\|compile\\\\|hbs\" /Users/ondrejveres/Wingbot/wingbot-llm/src/prompt.js)"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
11
|
+
}
|
package/index.js
CHANGED
|
@@ -70,6 +70,7 @@ const LLM = require('./src/LLM');
|
|
|
70
70
|
const LLMSession = require('./src/LLMSession');
|
|
71
71
|
const LLMTool = require('./src/LLMTool');
|
|
72
72
|
const LLMType = require('./src/LLMType');
|
|
73
|
+
const LLMToolRunner = require('./src/LLMToolRunner');
|
|
73
74
|
const LLMRouter = require('./src/LLMRouter');
|
|
74
75
|
const prompt = require('./src/prompt');
|
|
75
76
|
|
|
@@ -167,6 +168,7 @@ module.exports = {
|
|
|
167
168
|
LLMSession,
|
|
168
169
|
LLMTool,
|
|
169
170
|
LLMType,
|
|
171
|
+
LLMToolRunner,
|
|
170
172
|
|
|
171
173
|
LLMRouter,
|
|
172
174
|
prompt
|
package/package.json
CHANGED
package/src/LLMSession.js
CHANGED
|
@@ -101,6 +101,7 @@ const stateData = require('./utils/stateData');
|
|
|
101
101
|
/**
|
|
102
102
|
* @callback ToolFnCallback
|
|
103
103
|
* @param {{[key: string]: any}} input
|
|
104
|
+
* @param {LLMSession} [session] - call `session.stop()` to end the generation after the tool
|
|
104
105
|
* @returns {string|Promise<string>}
|
|
105
106
|
*/
|
|
106
107
|
|
|
@@ -256,6 +257,8 @@ class LLMSession {
|
|
|
256
257
|
|
|
257
258
|
/** @type {PossiblyAsyncContent|null} */
|
|
258
259
|
this._fallbackMessage = null;
|
|
260
|
+
|
|
261
|
+
this._stopped = false;
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
/**
|
|
@@ -374,6 +377,15 @@ class LLMSession {
|
|
|
374
377
|
.then(onFulfilled, onRejected);
|
|
375
378
|
}
|
|
376
379
|
|
|
380
|
+
/**
|
|
381
|
+
* the preset (or the provider options), the session generates with by default
|
|
382
|
+
*
|
|
383
|
+
* @returns {string|object}
|
|
384
|
+
*/
|
|
385
|
+
get preset () {
|
|
386
|
+
return this._preset;
|
|
387
|
+
}
|
|
388
|
+
|
|
377
389
|
/**
|
|
378
390
|
* @returns {ToolFunction[]}
|
|
379
391
|
*/
|
|
@@ -903,6 +915,28 @@ class LLMSession {
|
|
|
903
915
|
return this;
|
|
904
916
|
}
|
|
905
917
|
|
|
918
|
+
/**
|
|
919
|
+
* Ends the generation right after the current round of tool calls - the model
|
|
920
|
+
* is not asked again and nothing generated in this session is sent.
|
|
921
|
+
*
|
|
922
|
+
* Meant to be called from within a tool, which takes over the conversation
|
|
923
|
+
* (e.g. makes a postback to an interaction). It's applied immediately
|
|
924
|
+
* (not queued), because the tool already runs inside a queued job.
|
|
925
|
+
*
|
|
926
|
+
* @returns {this}
|
|
927
|
+
*/
|
|
928
|
+
stop () {
|
|
929
|
+
this._stopped = true;
|
|
930
|
+
return this;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* @returns {boolean}
|
|
935
|
+
*/
|
|
936
|
+
isStoppedSync () {
|
|
937
|
+
return this._stopped;
|
|
938
|
+
}
|
|
939
|
+
|
|
906
940
|
/**
|
|
907
941
|
* @returns {Promise<string>}
|
|
908
942
|
*/
|
|
@@ -1024,6 +1058,16 @@ class LLMSession {
|
|
|
1024
1058
|
},
|
|
1025
1059
|
...results.filter((r) => !!r)
|
|
1026
1060
|
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (this._stopped) {
|
|
1064
|
+
// a tool took over the conversation - drop any text generated
|
|
1065
|
+
// alongside the tool calls and don't ask the model again
|
|
1066
|
+
result = { role: ROLE_ASSISTANT, content: null };
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (toolCalls.length) {
|
|
1027
1071
|
result = await this._llm.generate(this, providerOptions, logOptions);
|
|
1028
1072
|
} else {
|
|
1029
1073
|
// everything failed - force a final text answer without tools
|
|
@@ -1093,7 +1137,7 @@ class LLMSession {
|
|
|
1093
1137
|
let args;
|
|
1094
1138
|
try {
|
|
1095
1139
|
args = JSON.parse(toolCall.args);
|
|
1096
|
-
const fnResult = await Promise.resolve(tool.fn(args));
|
|
1140
|
+
const fnResult = await Promise.resolve(tool.fn(args, this));
|
|
1097
1141
|
|
|
1098
1142
|
/**
|
|
1099
1143
|
* {
|
|
@@ -1193,6 +1237,9 @@ class LLMSession {
|
|
|
1193
1237
|
*/
|
|
1194
1238
|
send (quickReplies = undefined) {
|
|
1195
1239
|
this._job(() => {
|
|
1240
|
+
if (this._stopped) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1196
1243
|
const messages = this.messagesToSendSync();
|
|
1197
1244
|
this._onSend(messages, quickReplies);
|
|
1198
1245
|
}, true);
|
|
@@ -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;
|
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/log/types.js
CHANGED
|
@@ -59,7 +59,70 @@
|
|
|
59
59
|
* @returns {void}
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
|
-
/**
|
|
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
|
+
*/
|
|
63
126
|
|
|
64
127
|
/**
|
|
65
128
|
* What a library needs from a logger: the five levels. `console`, the shared
|
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
|
|
@@ -459,10 +461,36 @@ function message (params, context = {}) {
|
|
|
459
461
|
let discard;
|
|
460
462
|
let llmError;
|
|
461
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
|
+
});
|
|
473
|
+
|
|
462
474
|
try {
|
|
463
475
|
session = res.llmSessionWithHistory(params.llmContextType);
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
}
|
|
466
494
|
|
|
467
495
|
evaluation = await res.llmEvaluate(session, params.llmContextType);
|
|
468
496
|
({ discard } = evaluation);
|
|
@@ -503,6 +531,11 @@ function message (params, context = {}) {
|
|
|
503
531
|
|
|
504
532
|
res.setFlag(LLM.GPT_FLAG);
|
|
505
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
|
+
|
|
506
539
|
const lastMessageI = messages.length - 1;
|
|
507
540
|
messages.forEach((m, i) => {
|
|
508
541
|
if (lastMessageI === i) {
|