wingbot 3.74.8 → 3.75.9-alpha.2

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.
@@ -0,0 +1,325 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const GlobalIntents = require('./GlobalIntents');
7
+ const LLMType = require('./LLMType');
8
+
9
+ /** @typedef {import('./GlobalIntents').GlobalIntentsMap} GlobalIntentsMap */
10
+ /** @typedef {import('./GlobalIntents').ResolvedGlobalIntentsMap} ResolvedGlobalIntentsMap */
11
+ /** @typedef {import('./GlobalIntents').GlobalIntentResolved} GlobalIntentResolved */
12
+ /** @typedef {import('./GlobalIntents').MatchingResolver} MatchingResolver */
13
+ /** @typedef {import('./GlobalIntents').MatcherResult} MatcherResult */
14
+ /** @typedef {import('./LLMDispatcher').ILLMRouter} ILLMRouter */
15
+ /** @typedef {import('./Request')} Request */
16
+ /** @typedef {import('./LLM')} LLM */
17
+ /** @typedef {import('./prompt').Prompt} Prompt */
18
+ /** @typedef {import('./Router').RoutingInstruction} RoutingInstruction */
19
+
20
+ /**
21
+ * @typedef {object} NLPActionInfo
22
+ * @prop {MatcherResult} intent
23
+ * @prop {boolean} aboveConfidence
24
+ * @prop {boolean} winner
25
+ * @prop {string} [title]
26
+ * @prop {string|string[]} [match]
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} BaseRouteAction
31
+ * @prop {string} action
32
+ * @prop {object} [data]
33
+ * @prop {object} [setState]
34
+ * @prop {object} [_aiKeys]
35
+ */
36
+
37
+ /**
38
+ * @typedef {object} LLMRouting
39
+ * @prop {LLMRoutingCache} globals
40
+ * @prop {ILLMRouter|null} router
41
+ * @prop {Map<string, LLMRoutingCache>} locals
42
+ */
43
+
44
+ /**
45
+ * @typedef {Pick<GlobalIntentResolved, 'action'|'classification'>} LLMAction
46
+ */
47
+
48
+ /**
49
+ * @typedef {BaseRouteAction & Partial<NLPActionInfo>} RoutingAction
50
+ */
51
+
52
+ /**
53
+ * @callback RouterResolver
54
+ * @param {LLM} llm
55
+ * @param {Request} req
56
+ * @param {LLMRoutingInput} routing
57
+ *
58
+ * @returns {Promise<RoutingAction|null>}
59
+ */
60
+
61
+ /**
62
+ * @typedef {Pick<LLMRoutingCache, 'actionList'|'actionListString'|'byAction'>} LLMRoutingInput
63
+ */
64
+
65
+ /**
66
+ * @typedef {object} LLMRoutingCache
67
+ * @prop {boolean} [keepUserInInteractionsWithBounceAllowed]
68
+ * @prop {string} actionListString
69
+ * @prop {LLMAction[]} actionList
70
+ * @prop {ILLMRouter|null} router
71
+ * @prop {Map<string, GlobalIntentResolved>} byAction
72
+ */
73
+
74
+ /**
75
+ * @typedef {object} LLMRouterOptions
76
+ * @prop {boolean} [keepUserInInteractionsWithBounceAllowed]
77
+ * @prop {boolean} [global]
78
+ */
79
+
80
+ /**
81
+ * @typedef {Omit<LLMRouterOptions, 'global'>} GILLMExtension
82
+ */
83
+
84
+ /**
85
+ * @class LLMRouter
86
+ */
87
+ class LLMRouter {
88
+
89
+ // lets give it "reduce" method? - can forward that info to dispatcher
90
+
91
+ /**
92
+ *
93
+ * @param {RouterResolver} routeResolver
94
+ * @param {Omit<LLMRouterOptions, 'global'>} [options]
95
+ */
96
+ static global (routeResolver, options = {}) {
97
+ return new LLMRouter(routeResolver, {
98
+ ...options,
99
+ global: true
100
+ });
101
+ }
102
+
103
+ /**
104
+ *
105
+ * @param {RouterResolver} [routeResolver]
106
+ * @param {LLMRouterOptions} [options]
107
+ */
108
+ constructor (routeResolver = null, options = {}) {
109
+ const { global = false, ...rest } = options;
110
+ this._route = routeResolver;
111
+ this._global = global;
112
+
113
+ /** @typedef {GlobalIntentsMap} */
114
+ this.globalIntents = new Map();
115
+
116
+ if (global) {
117
+ const gi = GlobalIntents.create({
118
+ router: this._route ? this : null,
119
+ ...rest
120
+ });
121
+ this.globalIntents.set(gi.id, gi);
122
+ } else if (Object.keys(rest).length) {
123
+ const gi = GlobalIntents.create({
124
+ local: true,
125
+ ...rest
126
+ });
127
+ this.globalIntents.set(gi.id, gi);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @type {object}
133
+ */
134
+ static _cachedStructuredOutput = null;
135
+
136
+ /**
137
+ *
138
+ * @returns {object}
139
+ */
140
+ static structuredOutput () {
141
+ if (LLMRouter._cachedStructuredOutput === null) {
142
+ LLMRouter._cachedStructuredOutput = LLMType
143
+ .object({
144
+ action: LLMType.string()
145
+ .description('recommended action')
146
+ }, 'conversation routing information')
147
+ .toJSON();
148
+ }
149
+ return LLMRouter._cachedStructuredOutput;
150
+ }
151
+
152
+ /**
153
+ *
154
+ * prompt wi
155
+ *
156
+ * @param {Prompt} prompt
157
+ * @returns {RouterResolver}
158
+ */
159
+ static defaultRoute (prompt) {
160
+ /** @type {RouterResolver} */
161
+ const route = async (llm, req, routing) => {
162
+ const res = await llm.session('routing')
163
+ .setData(routing)
164
+ .systemPrompt(prompt)
165
+ .generateStructured(LLMRouter.structuredOutput());
166
+
167
+ if (!routing.byAction.has(LLMRouter._normByAction(res.action))) {
168
+ return null;
169
+ }
170
+
171
+ return res;
172
+ };
173
+
174
+ return route;
175
+ }
176
+
177
+ /**
178
+ *
179
+ * @returns {RoutingInstruction}
180
+ */
181
+ reduce () {
182
+ return false;
183
+ }
184
+
185
+ on () {
186
+ // acually does nothing too
187
+ }
188
+
189
+ /**
190
+ *
191
+ * @param {LLM} llm
192
+ * @param {Request} req
193
+ * @param {LLMRoutingInput} routing
194
+ * @returns {Promise<RoutingAction|null>}
195
+ */
196
+ async resolve (llm, req, routing) { // eslint-disable-line no-unused-vars
197
+ if (!this._route) {
198
+ return null;
199
+ }
200
+ return this._route(llm, req, routing);
201
+ }
202
+
203
+ /**
204
+ *
205
+ * @param {string} path
206
+ * @param {string} classification
207
+ * @returns {MatchingResolver}
208
+ */
209
+ route (path, classification) {
210
+ return GlobalIntents.resolver(path, {
211
+ classification,
212
+ router: this._route ? this : null,
213
+ local: !this._global
214
+ });
215
+ }
216
+
217
+ /**
218
+ *
219
+ * @param {string} path
220
+ * @param {string} classification
221
+ * @returns {MatchingResolver}
222
+ */
223
+ static route (path, classification) {
224
+ return GlobalIntents.resolver(path, {
225
+ classification
226
+ });
227
+ }
228
+
229
+ static _normByAction (act) {
230
+ return act.replace(/^\/|\/$/g, '');
231
+ }
232
+
233
+ /**
234
+ *
235
+ * @param {GlobalIntentResolved[]} resolvedLlm
236
+ * @returns {LLMRoutingCache}
237
+ */
238
+ static _prepareRoutingInput (resolvedLlm) {
239
+ const byAction = new Map();
240
+ let router = null;
241
+ let keepUserInInteractionsWithBounceAllowed;
242
+
243
+ const actionList = resolvedLlm.map((gi) => {
244
+ const { action, classification } = gi;
245
+ byAction.set(LLMRouter._normByAction(gi.action), gi);
246
+
247
+ if (typeof gi.keepUserInInteractionsWithBounceAllowed === 'boolean') {
248
+ keepUserInInteractionsWithBounceAllowed = true;
249
+ }
250
+
251
+ if (router && router !== gi.router) {
252
+ throw new Error(`Multiple local routers on "${gi.localPath}"`);
253
+ } else {
254
+ router = gi.router;
255
+ }
256
+
257
+ return { action, classification };
258
+ });
259
+
260
+ const actionListString = actionList
261
+ .map((gi) => `- \`${gi.action}\`: ${gi.classification}`)
262
+ .join('\n');
263
+
264
+ return {
265
+ ...(typeof keepUserInInteractionsWithBounceAllowed === 'boolean'
266
+ ? { keepUserInInteractionsWithBounceAllowed }
267
+ : {}),
268
+ actionListString,
269
+ actionList,
270
+ byAction,
271
+ router
272
+ };
273
+ }
274
+
275
+ /**
276
+ *
277
+ * @param {ResolvedGlobalIntentsMap} globalIntents
278
+ * @returns {LLMRouting}
279
+ */
280
+ static prepareRouting (globalIntents) {
281
+ /** @type {Map<string, GlobalIntentResolved[]>} */
282
+ const locals = new Map();
283
+ /** @type {GlobalIntentResolved[]} */
284
+ const globals = [];
285
+ /** @type {ILLMRouter} */
286
+ let router = null;
287
+
288
+ for (const gi of globalIntents.values()) {
289
+ if (!gi.classification
290
+ && !gi.router
291
+ && typeof gi.keepUserInInteractionsWithBounceAllowed === 'undefined') continue;
292
+
293
+ if (gi.local) {
294
+ let local = locals.get(gi.localPath);
295
+ if (!local) {
296
+ local = [];
297
+ locals.set(gi.localPath, local);
298
+ }
299
+ local.push(gi);
300
+ } else if (gi.classification) {
301
+ globals.push(gi);
302
+ } else if (gi.router) { // public default router without classification
303
+ if (router !== null) {
304
+ throw new Error(`Detected more than one global router on '${gi.localPath}'. Only single global router allowed.`);
305
+ }
306
+ router = gi.router;
307
+ }
308
+ }
309
+
310
+ return {
311
+ router,
312
+ locals: new Map(
313
+ Array.from(locals)
314
+ .map(([localPath, locs]) => [
315
+ localPath,
316
+ LLMRouter._prepareRoutingInput(locs)
317
+ ])
318
+ ),
319
+ globals: LLMRouter._prepareRoutingInput(globals)
320
+ };
321
+ }
322
+
323
+ }
324
+
325
+ module.exports = LLMRouter;
package/src/LLMSession.js CHANGED
@@ -6,12 +6,16 @@
6
6
  const {
7
7
  FILTER_SCOPE_CONVERSATION, ROLE_ASSISTANT, ROLE_USER, ROLE_SYSTEM
8
8
  } = require('./LLMConsts');
9
+ const stateData = require('./utils/stateData');
9
10
 
10
11
  /** @typedef {import('./Responder').QuickReply} QuickReply */
11
12
  /** @typedef {import('./LLM').LLMCallPreset} LLMCallPreset */
12
13
  /** @typedef {import('./LLM')} LLM */
13
14
  /** @typedef {import('./LLM').LLMLogOptions} LLMLogOptions */
14
15
 
16
+ /** @typedef {import('./Request')} Request */
17
+ /** @typedef {import('./Responder')} Responder */
18
+
15
19
  /** @typedef {'user'|'assistant'} LLMChatRole */
16
20
  /** @typedef {'system'} LLMSystemRole */
17
21
  /** @typedef {'tool'} LLMToolRole */
@@ -29,7 +33,9 @@ const {
29
33
  * @prop {string} args - JSON string
30
34
  */
31
35
 
32
- /** @typedef {string|Promise<string>} PossiblyAsyncContent */
36
+ /** @typedef {import('./prompt').Renderer} Renderer */
37
+
38
+ /** @typedef {string|Promise<string>|Renderer} PossiblyAsyncContent */
33
39
 
34
40
  /**
35
41
  * @template {LLMRole} [R=LLMRole]
@@ -112,7 +118,6 @@ const {
112
118
  * @prop {string} name - The function name
113
119
  * @prop {string} [description] - What the function does
114
120
  * @prop {FnParamsObject} [parameters] - Parameter schema
115
- * @prop {boolean} [strict]
116
121
  */
117
122
 
118
123
  /**
@@ -155,6 +160,16 @@ const {
155
160
  * - toJson
156
161
  */
157
162
 
163
+ /**
164
+ * @typedef {object} SessionOptions
165
+ * @prop {LLMFilter[]} [filters]
166
+ * @prop {SendCallback} [onSend]
167
+ * @prop {Request} [req]
168
+ * @prop {Responder} [res]
169
+ * @prop {object} [data]
170
+ * @prop {LLMCallPreset} [preset]
171
+ */
172
+
158
173
  /**
159
174
  * @class LLMSession
160
175
  * @implements {PromiseLike<LLMMessage<any>>}
@@ -165,10 +180,18 @@ class LLMSession {
165
180
  *
166
181
  * @param {LLM} llm
167
182
  * @param {(PossiblyAsyncLLMMessage|AsyncLLMMessage)[]} [chat]
168
- * @param {SendCallback} [onSend]
169
- * @param {LLMFilter[]} [filters=[]]
183
+ * @param {SessionOptions} [options]
170
184
  */
171
- constructor (llm, chat = [], onSend = null, filters = []) {
185
+ constructor (llm, chat = [], options = {}) {
186
+ const {
187
+ onSend = null,
188
+ filters = [],
189
+ data = {},
190
+ req = null,
191
+ res = null,
192
+ preset = undefined
193
+ } = options;
194
+
172
195
  this._llm = llm;
173
196
 
174
197
  this._onSend = onSend;
@@ -194,6 +217,36 @@ class LLMSession {
194
217
 
195
218
  /** @type {Map<string,ObjectTool>} */
196
219
  this._tools = new Map();
220
+
221
+ this._data = { ...data };
222
+
223
+ this._req = req || llm?.req;
224
+ this._res = res || llm?.res;
225
+
226
+ this._preset = preset;
227
+ }
228
+
229
+ /**
230
+ *
231
+ * @param {object} data
232
+ * @returns {this}
233
+ */
234
+ setData (data) {
235
+ this._job(() => {
236
+ Object.assign(this._data, data);
237
+ }, true);
238
+ return this;
239
+ }
240
+
241
+ _resolveData () {
242
+ const dataFromReqRes = this._req || this._res
243
+ ? stateData(this._req, this._res)
244
+ : {};
245
+
246
+ return {
247
+ ...dataFromReqRes,
248
+ ...this._data
249
+ };
197
250
  }
198
251
 
199
252
  _job (task, runNowAndSyncWhenQueueIsEmpty = false) {
@@ -297,19 +350,15 @@ class LLMSession {
297
350
  .map(({
298
351
  name,
299
352
  description = null,
300
- parameters = {},
301
- strict = true
353
+ parameters = {}
302
354
  }) => ({
303
355
  name,
304
356
  ...(description && { description }),
305
- ...(typeof strict === 'boolean' ? { strict } : {}),
357
+ strict: true,
306
358
  parameters: {
307
359
  type: 'object',
308
360
  properties: {},
309
361
  additionalProperties: false,
310
- required: 'properties' in parameters
311
- ? Object.keys(parameters.properties)
312
- : [],
313
362
  ...parameters
314
363
  }
315
364
  }));
@@ -447,11 +496,15 @@ class LLMSession {
447
496
  return;
448
497
  }
449
498
  if ('content' in m
499
+ && typeof m.content !== 'function'
450
500
  && (m.content instanceof Promise
451
501
  || (typeof m.content !== 'string' && m.content && 'then' in m.content))) {
452
502
  return;
453
503
  }
454
- if (filtered && this._filters.length >= 0 && 'content' in m && typeof m.content === 'string') {
504
+ if (filtered
505
+ && this._filters.length >= 0
506
+ && 'content' in m
507
+ && (typeof m.content === 'string' || typeof m.content === 'function')) {
455
508
  const content = this._filters.reduce((text, filter) => {
456
509
  if (!text) {
457
510
  return text;
@@ -463,7 +516,7 @@ class LLMSession {
463
516
  }
464
517
  const res = filter.filter(text, m.role);
465
518
  return res === true ? text : res;
466
- }, m.content);
519
+ }, typeof m.content === 'function' ? m.content.toString() : m.content);
467
520
 
468
521
  if (typeof content === 'string') {
469
522
  // @ts-ignore
@@ -472,6 +525,11 @@ class LLMSession {
472
525
  content
473
526
  });
474
527
  }
528
+ } else if ('content' in m && typeof m.content === 'function') {
529
+ ret.push({
530
+ ...m,
531
+ content: m.content.toString()
532
+ });
475
533
  } else {
476
534
  // @ts-ignore
477
535
  ret.push(m);
@@ -520,6 +578,9 @@ class LLMSession {
520
578
  if (typeof m.content === 'string') {
521
579
  return m.content;
522
580
  }
581
+ if (typeof m.content === 'function') {
582
+ return m.content.toString();
583
+ }
523
584
  return '<Promise>';
524
585
  }
525
586
 
@@ -627,12 +688,17 @@ class LLMSession {
627
688
  })();
628
689
  return wrapped;
629
690
  }
630
- if (!('content' in msg) || !this._contentIsPromise(msg.content)) {
691
+ if (!('content' in msg) || (!this._contentIsPromise(msg.content) && typeof msg.content !== 'function')) {
631
692
  return msg;
632
693
  }
694
+
695
+ const contentPromise = typeof msg.content === 'function'
696
+ ? msg.content(this._resolveData())
697
+ : msg.content;
698
+
633
699
  const ret = {
634
700
  ...msg,
635
- content: Promise.resolve(msg.content)
701
+ content: Promise.resolve(contentPromise)
636
702
  .then((r) => {
637
703
  // @ts-ignore
638
704
  ret.content = r;
@@ -688,7 +754,7 @@ class LLMSession {
688
754
 
689
755
  /**
690
756
  *
691
- * @param {string|Promise<string>} content
757
+ * @param {string|Promise<string>|Renderer} content
692
758
  * @returns {this}
693
759
  */
694
760
  user (content) {
@@ -701,7 +767,7 @@ class LLMSession {
701
767
 
702
768
  /**
703
769
  *
704
- * @param {string|Promise<string>} content
770
+ * @param {string|Promise<string>|Renderer} content
705
771
  * @returns {this}
706
772
  */
707
773
  assistant (content) {
@@ -714,7 +780,7 @@ class LLMSession {
714
780
 
715
781
  /**
716
782
  *
717
- * @param {string|Promise<string>} content
783
+ * @param {string|Promise<string>|Renderer} content
718
784
  * @returns {this}
719
785
  */
720
786
  systemPrompt (content) {
@@ -747,7 +813,7 @@ class LLMSession {
747
813
  * @param {LLMLogOptions} [logOptions]
748
814
  * @returns {this}
749
815
  */
750
- generate (providerOptions = undefined, logOptions = {}) {
816
+ generate (providerOptions = this._preset, logOptions = {}) {
751
817
  this._job(() => this._generate(providerOptions, logOptions));
752
818
  return this;
753
819
  }
@@ -759,7 +825,7 @@ class LLMSession {
759
825
  * @param {LLMLogOptions} [logOptions]
760
826
  * @returns {this}
761
827
  */
762
- generateStructured (output, providerOptions = undefined, logOptions = {}) {
828
+ generateStructured (output, providerOptions = this._preset, logOptions = {}) {
763
829
 
764
830
  const responseFormat = 'toJSON' in output && typeof output.toJSON === 'function'
765
831
  ? output.toJSON()
@@ -787,7 +853,7 @@ class LLMSession {
787
853
  * @param {LLMLogOptions} [logOptions]
788
854
  * @returns {Promise<LLMMessage<any>>}
789
855
  */
790
- async _generate (providerOptions = undefined, logOptions = {}) {
856
+ async _generate (providerOptions = this._preset, logOptions = {}) {
791
857
  let result = await this._llm.generate(this, providerOptions, logOptions);
792
858
 
793
859
  if (result.toolCalls?.length) {
package/src/LLMType.js CHANGED
@@ -196,10 +196,11 @@ class LLMType {
196
196
  }
197
197
 
198
198
  /**
199
- * Marks field as optional when used as an object property.
199
+ * Marks field as optional when used as an object property. In the
200
+ * generated schema this surfaces as a nullable type (e.g. `['string', 'null']`)
201
+ * — strict mode still requires the key to be present, just allows `null`.
200
202
  *
201
- * When used outside object-property context, it has no practical effect,
202
- * because requiredness is evaluated by parent object schemas.
203
+ * Outside an object-property context it has no practical effect.
203
204
  *
204
205
  * @example
205
206
  * const input = LLMType.object({
@@ -215,10 +216,9 @@ class LLMType {
215
216
  }
216
217
 
217
218
  /**
218
- * Returns JSON Schema representation.
219
- *
220
- * For object schemas, nested properties are resolved recursively by calling
221
- * `toJSON()` on each nested `LLMType` instance.
219
+ * Returns JSON Schema representation in OpenAI strict-mode form:
220
+ * every property is listed in `required`, and properties marked
221
+ * `optional()` are made nullable instead of being dropped from `required`.
222
222
  *
223
223
  * @example
224
224
  * const schema = LLMType.object({
@@ -246,18 +246,18 @@ class LLMType {
246
246
  const properties = {};
247
247
  const required = [];
248
248
  Object.entries(this._properties).forEach(([key, child]) => {
249
- properties[key] = child.toJSON();
250
- if (child._required) {
251
- required.push(key);
249
+ const childSchema = child.toJSON();
250
+ if (!child._required) {
251
+ childSchema.type = [childSchema.type, 'null'];
252
252
  }
253
+ properties[key] = childSchema;
254
+ required.push(key);
253
255
  });
254
256
 
255
257
  schema.type = 'object';
256
258
  schema.properties = properties;
257
259
  schema.additionalProperties = false;
258
- if (required.length > 0) {
259
- schema.required = required;
260
- }
260
+ schema.required = required;
261
261
  break;
262
262
  }
263
263