wingbot 3.71.6 → 3.72.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.
@@ -8,30 +8,47 @@ const Router = require('../Router');
8
8
  const Responder = require('../Responder');
9
9
  const { getLanguageText } = require('./utils');
10
10
  const compileWithState = require('../utils/compileWithState');
11
+ const LLM = require('../LLM');
11
12
 
12
13
  /** @typedef {import('../BuildRouter').BotContext<any>} BotContext */
13
14
  /** @typedef {import('../Router').Resolver<any>} Resolver */
14
15
  /** @typedef {import('./utils').Translations} Translations */
16
+ /** @typedef {import('../LLM').EvaluationRule} EvaluationRule */
17
+
18
+ /**
19
+ * @typedef {object} ContextMessageParams
20
+ * @property {Translations} context
21
+ * @property {string} [type]
22
+ * @property {EvaluationRule[]} [rules]
23
+ */
15
24
 
16
25
  /**
17
26
  *
18
- * @param {object} params
19
- * @param {Translations} params.context
20
- * @param {string} [params.type]
27
+ * @param {ContextMessageParams} params
21
28
  * @param {BotContext} context
22
29
  * @returns {Resolver}
23
30
  */
24
31
  // eslint-disable-next-line no-unused-vars
25
32
  function contextMessage (params, context) {
26
33
 
34
+ const { rules = [] } = params;
35
+ const preprocessedRules = LLM.preprocessEvaluationRules(rules, context);
36
+
27
37
  return async (req, res) => {
28
- res.llmAddSystemPrompt((r) => {
38
+ res.llmAddInstructions((r) => {
29
39
  const translated = getLanguageText(params.context, req.state.lang);
30
40
  const translatedText = Array.isArray(translated) ? translated[0] : translated;
31
41
  const statefulPrompt = compileWithState(req, r, translatedText);
32
42
  return statefulPrompt;
33
43
  }, params.type);
34
44
 
45
+ preprocessedRules.forEach((rule) => res.llmAddResultRule(
46
+ rule,
47
+ undefined,
48
+ undefined,
49
+ params.type
50
+ ));
51
+
35
52
  return Router.CONTINUE;
36
53
  };
37
54
  }
@@ -23,8 +23,11 @@ const { vars, VAR_TYPES } = require('../utils/stateVariables');
23
23
  const LLM = require('../LLM');
24
24
 
25
25
  /** @typedef {import('../Responder').VoiceControl} VoiceControl */
26
+ /** @typedef {import('../BuildRouter').LinksMap} LinksMap */
26
27
  /** @typedef {import('./utils').Translations} Translations */
27
28
  /** @typedef {import('./utils').TextObject} TextObject */
29
+ /** @typedef {import('../utils/getCondition').ConditionDefinition} ConditionDefinition */
30
+ /** @typedef {import('../utils/getCondition').ConditionContext} ConditionContext */
28
31
  /**
29
32
  * Returns voice control props from params
30
33
  *
@@ -68,6 +71,30 @@ function getVoiceControlFromParams (params, lang = null) {
68
71
  return Object.keys(voiceControl).length > 0 ? voiceControl : null;
69
72
  }
70
73
 
74
+ /**
75
+ * @typedef {object} QuickReplyData
76
+ * @prop {boolean} [isLocation]
77
+ * @prop {boolean} [isEmail]
78
+ * @prop {boolean} [isPhone]
79
+ * @prop {boolean} [trackAsNegative]
80
+ * @prop {string} [action]
81
+ * @prop {string} [targetRouteId]
82
+ * @prop {{l:string,t:string[]}[] | string} [title]
83
+ * @prop {object} [setState]
84
+ * @prop {string[]} [aiTags]
85
+ * @prop {{l:string,t:string[]}[] | string} [aiTitle]
86
+ */
87
+
88
+ /**
89
+ * @typedef {ConditionDefinition & QuickReplyData} QuickReply
90
+ */
91
+
92
+ /**
93
+ *
94
+ * @param {QuickReply[]} replies
95
+ * @param {LinksMap} linksMap
96
+ * @param {ConditionContext} context
97
+ */
71
98
  function parseReplies (replies, linksMap, context) {
72
99
  return replies.map((reply) => {
73
100
 
@@ -314,8 +341,9 @@ function message (params, context = {}) {
314
341
  /**
315
342
  * @param {Request} req
316
343
  * @param {Responder} res
344
+ * @param {Function } postBack
317
345
  */
318
- return async (req, res) => {
346
+ return async (req, res, postBack) => {
319
347
  if (condition === undefined) {
320
348
  condition = getCondition(params, context, 'Message condition');
321
349
  }
@@ -425,6 +453,20 @@ function message (params, context = {}) {
425
453
  await session.systemPrompt(text)
426
454
  .generate();
427
455
 
456
+ const evaluation = await res.llmEvaluate(session, params.llmContextType);
457
+
458
+ if (evaluation.discard) {
459
+ if (isLastMessage && !req.actionData()._resolverTag) {
460
+ res.finalMessageSent = true;
461
+ }
462
+ return ret;
463
+ }
464
+
465
+ if (evaluation.action) {
466
+ postBack(evaluation.action);
467
+ return Router.END;
468
+ }
469
+
428
470
  // if (!response.content) {
429
471
  // // no response?
430
472
  // }
@@ -31,17 +31,26 @@ const WEBVIEW_COMPACT = 'compact';
31
31
  * @prop {string|null} p - purpose
32
32
  */
33
33
 
34
+ /**
35
+ * null = text+voice
36
+ * t = text only
37
+ * v = voice only
38
+ * s = ssml
39
+ *
40
+ * @typedef {"t"|"v"|"s"|null} Purpose
41
+ */
42
+
34
43
  /**
35
44
  * @typedef Translation
36
45
  * @property {string|string[]} t - text alternatives
37
46
  * @property {string | null} l - language
38
- * @property {string|string[]} [p] - purposes
39
- * null = default + voice
40
- * t = text
41
- * v = voice
42
- * s = ssml
47
+ * @property {Purpose | Purpose[]} [p] - purposes
43
48
  */
44
49
 
50
+ /**
51
+ * @param {string | string[] | Translation | Translation[]} translations
52
+ * @returns {translations is Translation[]}
53
+ */
45
54
  function isArrayOfObjects (translations) {
46
55
  return Array.isArray(translations)
47
56
  && typeof translations[0] === 'object'
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @author David Menger
3
+ */
4
+ 'use strict';
5
+
6
+ const assert = require('assert');
7
+ const asserts = require('./asserts');
8
+
9
+ const { ex } = asserts;
10
+
11
+ /** @typedef {import('../LLM').PromptInfo} PromptInfo */
12
+ /** @typedef {import('../LLM').LLMMessage} LLMMessage */
13
+ /** @typedef {import('../LLM').LLMRole} LLMRole */
14
+
15
+ /**
16
+ * @class PromptAssert
17
+ */
18
+ class PromptAssert {
19
+
20
+ /**
21
+ *
22
+ * @param {PromptInfo[]} prompts
23
+ */
24
+ constructor (prompts) {
25
+ this._prompts = prompts;
26
+ }
27
+
28
+ /**
29
+ * Check if recent instruction contains selected string
30
+ *
31
+ * @param {string} search
32
+ * @returns {this}
33
+ */
34
+ instructionContains (search) {
35
+ const messages = this._flatPrompt((m) => m.role === 'system');
36
+ this._promptContains(search, messages, false, 'of role "system"');
37
+ return this;
38
+ }
39
+
40
+ /**
41
+ * Check if recent prompt input contains selected string
42
+ *
43
+ * @param {string} search
44
+ * @returns {this}
45
+ */
46
+ promptContains (search) {
47
+ this._promptContains(search, this._flatPrompt());
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * Check if recent results contains selected string
53
+ *
54
+ * @param {string} search
55
+ * @returns {this}
56
+ */
57
+ resultContains (search) {
58
+ this._promptContains(search, this._flatResults());
59
+ return this;
60
+ }
61
+
62
+ /**
63
+ *
64
+ * @param {(value: LLMMessage, index: number, array: LLMMessage[]) => unknown} filter
65
+ * @returns {LLMMessage[]}
66
+ */
67
+ _flatPrompt (filter = () => true) {
68
+ return this._prompts
69
+ .flatMap((prompt) => prompt.prompt
70
+ .filter(filter));
71
+ }
72
+
73
+ /**
74
+ *
75
+ * @returns {LLMMessage[]}
76
+ */
77
+ _flatResults () {
78
+ return this._prompts
79
+ .map((prompt) => prompt.result);
80
+ }
81
+
82
+ _promptContains (search, messages, notContains = false, addMessage = 'No LLM message found') {
83
+ if (messages.length === 0) {
84
+ PromptAssert.debug(this._prompts, true);
85
+ assert.fail(ex(`No LLM message ${addMessage}`, search));
86
+ }
87
+
88
+ const found = messages.some((m) => asserts.llmContains(m, search, false));
89
+ // console.log({ found, messages, notContains });
90
+ if (notContains === found) {
91
+ PromptAssert.debug(this._prompts, true);
92
+ assert.fail(ex(
93
+ `Text${notContains ? '' : ' not'} found in LLM messages ${addMessage}`,
94
+ search
95
+ ));
96
+ }
97
+ }
98
+
99
+ /**
100
+ *
101
+ * @param {LLMMessage[]} prompt
102
+ * @returns {string}
103
+ */
104
+ static promptStats (prompt) {
105
+ const stats = prompt.reduce((o, m) => Object.assign(o, {
106
+ [m.role]: (o[m.role] || 0) + 1
107
+ }), {
108
+ system: 0, assistant: 0, user: 0, tool: 0
109
+ });
110
+
111
+ return `[ ${Object.entries(stats).map(([k, v]) => `${k.toUpperCase()}: ${v}`).join(' ')} ]`;
112
+ }
113
+
114
+ /**
115
+ *
116
+ * @param {LLMRole} role
117
+ * @return {string}
118
+ */
119
+ static _marker (role) {
120
+ switch (role) {
121
+ case 'user':
122
+ return '>';
123
+ case 'system':
124
+ return '*';
125
+ case 'assistant':
126
+ return '<';
127
+ default:
128
+ return '-';
129
+ }
130
+ }
131
+
132
+ static _content (msg, full) {
133
+ if (typeof msg.content !== 'string') {
134
+ return msg.content === null ? '<null>' : typeof msg.content;
135
+ }
136
+ const txt = msg.content.replace(/\n+/g, ' ').replace(/\s\s+/g, ' ');
137
+ return `'${txt.length <= 100 || full ? txt : `${txt.substring(0, 100)}...`}'`;
138
+ }
139
+
140
+ /**
141
+ *
142
+ * @param {PromptInfo[]} prompts
143
+ * @param {boolean} [full=false]
144
+ * @param {boolean} [silent=false]
145
+ * @returns {string[]}
146
+ */
147
+ static debug (prompts, full = false, silent = false) {
148
+ let out;
149
+ if (prompts.length === 0) {
150
+ out = ['no LLM prompts occured'];
151
+ } else {
152
+ out = prompts.map((p, i) => {
153
+ const lastIndexOfUser = full
154
+ ? 0
155
+ : p.prompt.reduce((li, m, c) => (m.role === 'user' ? c : li), 0);
156
+
157
+ const prompt = p.prompt.reduce((a, m, c) => {
158
+ if (['user', 'assistant'].includes(m.role) && c < lastIndexOfUser) {
159
+ const prev = a[a.length - 1];
160
+ if (prev === '...') {
161
+ return a;
162
+ }
163
+ return [...a, '...'];
164
+ }
165
+ return [...a, `${PromptAssert._marker(m.role)} ${PromptAssert._content(m, full)}`];
166
+ }, []);
167
+
168
+ return [
169
+ `${i + 1}) ${PromptAssert.promptStats(p.prompt)}`,
170
+ `${prompt.join('\n ')}`,
171
+ `# ${PromptAssert._content(p.result)}`
172
+ ].join('\n ');
173
+ });
174
+ }
175
+ if (!silent) {
176
+ // eslint-disable-next-line no-console
177
+ console.log(...out);
178
+ }
179
+ return out;
180
+ }
181
+
182
+ }
183
+
184
+ module.exports = PromptAssert;
@@ -7,6 +7,9 @@ const assert = require('assert');
7
7
  const deepExtend = require('deep-extend');
8
8
  const { actionMatches, parseActionPayload } = require('../utils');
9
9
 
10
+ /** @typedef {import('../LLM').PromptInfo} PromptInfo */
11
+ /** @typedef {import('../LLM').LLMMessage} LLMMessage */
12
+
10
13
  /**
11
14
  * Format message
12
15
  *
@@ -29,9 +32,21 @@ function m (text, actual = null, expected = null) {
29
32
  return `${text}${result}`;
30
33
  }
31
34
 
32
- function ex (message, expected, actual) {
33
- const actuals = Array.isArray(actual) ? actual : [actual];
34
- return `${message}\n + expected: "${expected}"\n - actual: ${actuals
35
+ function ex (message, expected, actual = null) {
36
+ let actuals;
37
+ if (Array.isArray(actual)) {
38
+ actuals = actual;
39
+ } else {
40
+ actuals = actual === null ? [] : [actual];
41
+ }
42
+
43
+ const msg = `${message}\n + expected: "${expected}"`;
44
+
45
+ if (actuals.length === 0) {
46
+ return msg;
47
+ }
48
+
49
+ return `${msg}\n - actual: ${actuals
35
50
  .map((a) => `"${a}"`).join('\n ')}`;
36
51
  }
37
52
 
@@ -144,6 +159,28 @@ function contains (response, search, message = 'Should contain a text') {
144
159
  return true;
145
160
  }
146
161
 
162
+ /**
163
+ *
164
+ * @param {LLMMessage} llmMsg
165
+ * @param {string} search
166
+ * @param {string|false} message
167
+ */
168
+ function llmContains (llmMsg, search, message = 'Should contain a text') {
169
+ const text = llmMsg.content;
170
+
171
+ const typeIsText = typeof text === 'string';
172
+ if (message === false && !typeIsText) {
173
+ return false;
174
+ }
175
+ assert.ok(typeIsText, m(message, search, 'not a text message'));
176
+ const match = searchMatchesText(search, text);
177
+ if (message === false) {
178
+ return match;
179
+ }
180
+ assert.ok(match, m(message, text, search));
181
+ return true;
182
+ }
183
+
147
184
  /**
148
185
  * Checks quick response action
149
186
  *
@@ -374,5 +411,6 @@ module.exports = {
374
411
  quickReplyText,
375
412
  getText,
376
413
  parseActionPayload,
377
- ex
414
+ ex,
415
+ llmContains
378
416
  };
@@ -175,9 +175,22 @@ const compare = (variable, operator, value = undefined) => {
175
175
  }
176
176
  };
177
177
 
178
+ /** @typedef {'if'|'it'|'c'|'nc'|'mr'|'nmr'|'st'|'gt'|'set'|'get'|'eq'|'neq'} ConditionOperator */
179
+
180
+ /**
181
+ * @typedef {object} EditableConditionRule
182
+ * @prop {string} value
183
+ * @prop {ConditionOperator} operator
184
+ * @prop {string} variable
185
+ */
186
+
187
+ /**
188
+ * @typedef {EditableConditionRule[][]} EditableCondition
189
+ */
190
+
178
191
  /**
179
192
  *
180
- * @param {{value:string, operator:string, variable:string}[][]} condition
193
+ * @param {EditableCondition} condition
181
194
  * @param {object} configuration
182
195
  * @param {string} description
183
196
  */
@@ -7,11 +7,23 @@ const customCondition = require('./customCondition');
7
7
  const customFn = require('./customFn');
8
8
 
9
9
  /** @typedef {import('../BuildRouter').BotContext} BotContext */
10
+ /** @typedef {import('./customCondition').EditableCondition} EditableCondition */
11
+
12
+ // eslint-disable-next-line max-len
13
+ /** @typedef {Pick<BotContext,'configuration'|'allowForbiddenSnippetWords'|'linksMap'|'ai'>} ConditionContext */
14
+
15
+ /**
16
+ * @typedef {object} ConditionDefinition
17
+ * @prop {boolean} [hasCondition]
18
+ * @prop {string} [conditionFn]
19
+ * @prop {boolean} [hasEditableCondition]
20
+ * @prop {EditableCondition} [editableCondition]
21
+ */
10
22
 
11
23
  /**
12
24
  *
13
- * @param {object} params
14
- * @param {BotContext} context
25
+ * @param {ConditionDefinition} params
26
+ * @param {ConditionContext} context
15
27
  * @param {string} description
16
28
  * @returns {Function}
17
29
  */
@@ -27,7 +39,7 @@ module.exports = function getCondition (params, context, description = '') {
27
39
  editableCondition = []
28
40
  } = params;
29
41
 
30
- let condition = null;
42
+ let condition;
31
43
 
32
44
  if (hasCondition) {
33
45
  if (hasEditableCondition) {
@@ -36,7 +48,7 @@ module.exports = function getCondition (params, context, description = '') {
36
48
  condition = customFn(conditionFn, description, allowForbiddenSnippetWords);
37
49
  }
38
50
  } else {
39
- condition = customFn('(req,res)=>true', description, allowForbiddenSnippetWords);
51
+ condition = () => true;
40
52
  }
41
53
  return condition;
42
54
  };
@@ -88,7 +88,7 @@ function toArray (previousValue) {
88
88
  const ENTITY_HBS_REGEXP = /^\s*\{\{\[?@([^@[\]{}\s]+)(\])?\}\}\s*$/;
89
89
  const VARIABLE_HBS_REGEXP = /^\s*\{\{\[?([^$@[\]{}\s]+)\]?\}\}\s*$/;
90
90
 
91
- function getSetState (setState, req, res = null, useState = null, configuration = null) {
91
+ function getSetState (setState, req, res = null, stateOverride = null, configuration = null) {
92
92
  if (!setState) {
93
93
  return {};
94
94
  }
@@ -96,7 +96,7 @@ function getSetState (setState, req, res = null, useState = null, configuration
96
96
  .filter((k) => k !== '_');
97
97
 
98
98
  let obj = {};
99
- let state = stateData(req, res, configuration, useState);
99
+ let state = stateData(req, res, configuration, stateOverride);
100
100
 
101
101
  keys.forEach((k) => {
102
102
  const val = setState[k];
@@ -166,8 +166,8 @@ function getSetState (setState, req, res = null, useState = null, configuration
166
166
  if (entity && (!rear || req.entities.some((e) => e.entity === entity))) {
167
167
  values = req.entities.filter((e) => e.entity === entity)
168
168
  .map((e) => e.value);
169
- if (values.length === 0 && useState && useState[`@${entity}`]) {
170
- values = [useState[`@${entity}`]];
169
+ if (values.length === 0 && stateOverride && stateOverride[`@${entity}`]) {
170
+ values = [stateOverride[`@${entity}`]];
171
171
  }
172
172
  } else if (variable) {
173
173
  values = toArray(getValue(variable, state));
@@ -16,6 +16,7 @@ const { dateToISO8601String, zeroHourDate } = require('./datetime');
16
16
  * @prop {object} [configuration]
17
17
  * @prop {Function} text
18
18
  * @prop {Function} [actionData]
19
+ * @prop {Function} [isConfidentInput]
19
20
  */
20
21
 
21
22
  /**
@@ -68,6 +69,7 @@ module.exports = function stateData (
68
69
  $today,
69
70
  $tomorrow,
70
71
  $yesterday,
72
+ $llmResult: res?.llm?.lastResult?.content,
71
73
  // yes - res because of circular dependency
72
74
  ...(res && req && req.actionData()),
73
75
  ...(res ? res.data : {}),