@tiledesk/tiledesk-server 2.28.2 → 3.0.0

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/app.js +6 -35
  3. package/config/native_mcp/native_mcp_servers.json +7 -0
  4. package/controllers/chatbot.controller.js +659 -0
  5. package/controllers/intents.controller.js +139 -0
  6. package/errorCodes.js +2 -1
  7. package/event/botEvent.js +43 -3
  8. package/middleware/errorHandler.js +41 -0
  9. package/models/chatbotTemplates.js +4 -0
  10. package/models/faq.js +10 -0
  11. package/models/faq_kb.js +49 -0
  12. package/package.json +2 -2
  13. package/pubmodules/aiGenerations/README.md +154 -0
  14. package/pubmodules/aiGenerations/index.js +7 -0
  15. package/pubmodules/aiGenerations/listeners.js +96 -0
  16. package/pubmodules/aiGenerations/models/faqKbRevision.js +51 -0
  17. package/pubmodules/aiGenerations/routes/aiGenerations.js +426 -0
  18. package/pubmodules/aiGenerations/services/agentWriter.js +286 -0
  19. package/pubmodules/aiGenerations/services/atomic.js +70 -0
  20. package/pubmodules/aiGenerations/services/references.js +78 -0
  21. package/pubmodules/aiGenerations/services/revisionService.js +169 -0
  22. package/pubmodules/aiGenerations/services/snapshot.js +109 -0
  23. package/pubmodules/aiGenerations/test/aiGenerationsRoute.js +503 -0
  24. package/pubmodules/pubModulesManager.js +17 -0
  25. package/routes/faq.js +10 -3
  26. package/routes/faq_kb.js +716 -411
  27. package/services/chatbotService.js +84 -5
  28. package/services/faqService.js +159 -45
  29. package/services/v3FlowService.js +416 -0
  30. package/template/chatbot/blank.js +21 -3
  31. package/template/chatbot/blank_subagent.js +139 -0
  32. package/template/chatbot/blank_voice.js +21 -4
  33. package/template/chatbot/blank_voice_twilio.js +21 -5
  34. package/template/chatbot/index.js +1 -0
  35. package/template/chatbot/subagent_notes.js +30 -0
  36. package/test/faqRoute.js +83 -0
  37. package/test/faqkbRoute.js +982 -523
  38. package/test/faqkbV3Route.js +436 -0
  39. package/test/fixtures/example-json-with-subagents.txt +1 -0
  40. package/test/nativemcp.test.js +6 -1
  41. package/test/v3FlowService.js +187 -0
  42. package/test/webhookRoute.js +2 -1
  43. package/utils/AppError.js +9 -0
  44. package/utils/asyncHandler.js +10 -0
@@ -0,0 +1,416 @@
1
+ const uuidv4 = require('uuid/v4');
2
+ const winston = require('../config/winston');
3
+
4
+ /**
5
+ * The V3 shape of an agent's flow (Design Studio V3, since 15/10/2026).
6
+ *
7
+ * A V3 agent (`faq_kb.ds_version === 'v3'`) keeps its links in the bot-level list
8
+ * `faq_kb.connectors`, not in the destination fields of its actions (`intentName`,
9
+ * `trueIntent`, `goToIntent`, `cases[].intent`, `attributes.nextBlockAction.intentName`, ...).
10
+ * Those fields are empty in the stored blocks: the list is the only truth, and both the
11
+ * Design Studio V3 and the engine (tiledesk-chatbot, ConnectorsV3) read it from there.
12
+ *
13
+ * This module is the server's own knowledge of that shape, so an agent born here (from a
14
+ * template) or coming in (import, fork, restore) is already in V3 form:
15
+ * - `toV3Flow(intents)`: the blocks of a template, written the V2 way, become V3 blocks plus
16
+ * their connectors list (technical ids filled in, list built from the fields, fields cleared);
17
+ * - `validateConnectors(collection)`: the checks the dedicated endpoint and the import apply;
18
+ * - `promoteLegacyJson(json)`: a JSON exported before the two fields existed carried them
19
+ * inside `attributes`; they are moved to the fields.
20
+ *
21
+ * The output catalog is a copy of `connectors/connector-catalog.ts` of the Design Studio and of
22
+ * `utils/ConnectorsV3.js` of the engine: same types, same fields, same id suffixes. A record is
23
+ * `{ id, from: { intentId, actionId, slot }, to: { intentId }, kind, props? }` and its id is
24
+ * `${intentId}/${actionId}${suffix}/${toIntentId}`. If the catalog changes there, it changes here.
25
+ *
26
+ * A V2 agent never passes through here: every caller checks `ds_version` first.
27
+ */
28
+
29
+ const DS_VERSION_V3 = 'v3';
30
+ const COLLECTION_VERSION = 1;
31
+ const RECORD_KINDS = ['block', 'action', 'button', 'case', 'branch'];
32
+ const RETURN_STACK = 'returnstack';
33
+ const BUTTON_URL = 'url';
34
+ const BUTTON_ACTION = 'action';
35
+
36
+ const field = (key, suffix, name) => ({ key, kind: 'action', path: 'field', suffix, field: name });
37
+ const nested = (key, suffix, path) => ({ key, kind: 'action', path: 'nested', suffix, steps: path });
38
+ const lastCommand = (key, suffix, name) => ({ key, kind: 'action', path: 'lastCommandSetting', suffix, field: name });
39
+ const BUTTONS = { key: 'button', kind: 'button', path: 'buttons' };
40
+ const array = (key, kind, suffixOf, arrayName, idField, name) => ({ key, kind, path: 'array', suffixOf, array: arrayName, idField, field: name });
41
+
42
+ const TRUE_FALSE = [field('true', '/true', 'trueIntent'), field('false', '/false', 'falseIntent')];
43
+ const VOICE_SETTINGS = [nested('noInput', '/noInput', ['settings', 'noInputIntent']), nested('noMatch', '/noMatch', ['settings', 'noMatchIntent'])];
44
+ const REPLY_V1 = [BUTTONS, ...VOICE_SETTINGS];
45
+
46
+ const OUTPUT_CATALOG = {
47
+ intent: [field('default', '', 'intentName')],
48
+ connect_block: [field('default', '', 'intentName')],
49
+ reply: REPLY_V1,
50
+ randomreply: REPLY_V1,
51
+ replyv2: [BUTTONS, field('noInput', '/noInput', 'noInputIntent'), field('noMatch', '/noMatch', 'noMatchIntent')],
52
+ ifonlineagents: TRUE_FALSE,
53
+ ifonlineagentsv2: TRUE_FALSE,
54
+ ifopenhours: TRUE_FALSE,
55
+ jsonconditionmulti: [array('case', 'case', (c) => '/case/' + c._tdCaseId, 'cases', '_tdCaseId', 'intent'), field('else', '/else', 'elseIntent')],
56
+ jsoncondition: TRUE_FALSE,
57
+ jsoncondition2: TRUE_FALSE,
58
+ invoke_subagent: TRUE_FALSE,
59
+ askgpt: TRUE_FALSE,
60
+ askgptv2: TRUE_FALSE,
61
+ gpt_task: TRUE_FALSE,
62
+ gpt_assistant: TRUE_FALSE,
63
+ ai_prompt: TRUE_FALSE,
64
+ ai_condition: [array('branch', 'branch', (b) => '/' + b.label + '/true', 'intents', 'label', 'conditionIntentId'), field('fallback', '/fallback', 'fallbackIntent'), field('error', '/error', 'errorIntent')],
65
+ webrequestv2: TRUE_FALSE,
66
+ data_table: TRUE_FALSE,
67
+ make: TRUE_FALSE,
68
+ hubspot: TRUE_FALSE,
69
+ customerio: TRUE_FALSE,
70
+ brevo: TRUE_FALSE,
71
+ n8n: TRUE_FALSE,
72
+ qapla: TRUE_FALSE,
73
+ capture_user_reply: [field('default', '', 'goToIntent')],
74
+ iteration: [field('goto', '/goto', 'goToIntent'), field('fallback', '/fallback', 'fallbackIntent')],
75
+ dtmf_menu: [BUTTONS, ...VOICE_SETTINGS, lastCommand('noInput', '/noInput', 'noInputIntent'), lastCommand('noMatch', '/noMatch', 'noMatchIntent')],
76
+ dtmf_form: [BUTTONS, ...VOICE_SETTINGS, lastCommand('noInput', '/noInput', 'noInputIntent'), lastCommand('noMatch', '/noMatch', 'noMatchIntent')],
77
+ speech_form: [lastCommand('noInput', '/noInput', 'noInputIntent'), lastCommand('noMatch', '/noMatch', 'noMatchIntent')],
78
+ blind_transfer: [BUTTONS, ...VOICE_SETTINGS, lastCommand('true', '/true', 'trueIntent'), lastCommand('false', '/false', 'falseIntent')],
79
+ send_whatsapp: TRUE_FALSE,
80
+ };
81
+
82
+ /** The block's own exit (the dot), stored in `attributes.nextBlockAction.intentName`. */
83
+ const BLOCK_SLOT = { key: 'next', kind: 'block', path: 'block', suffix: '' };
84
+
85
+ function isSet(value) {
86
+ return typeof value === 'string' && value !== '';
87
+ }
88
+
89
+ function hasValidUid(uid) {
90
+ return typeof uid === 'string' && uid !== '' && uid !== 'UUIDV4';
91
+ }
92
+
93
+ /** Every `buttons[]` inside the object, at any depth, in visiting order (as the Design Studio's `findButtons`). */
94
+ function findButtons(obj) {
95
+ let buttons = [];
96
+ if (Array.isArray(obj)) {
97
+ for (const element of obj) { buttons = buttons.concat(findButtons(element)); }
98
+ } else if (obj && typeof obj === 'object') {
99
+ if (Array.isArray(obj.buttons)) { buttons = buttons.concat(obj.buttons); }
100
+ for (const key in obj) { if (key !== 'buttons') { buttons = buttons.concat(findButtons(obj[key])); } }
101
+ }
102
+ return buttons;
103
+ }
104
+
105
+ function isReturnStackIntent(intent) {
106
+ const actions = intent && intent.actions;
107
+ return Array.isArray(actions) && actions.length === 1 && actions[0] && actions[0]._tdActionType === RETURN_STACK;
108
+ }
109
+
110
+ /** A button's target is `#<id>` or `#<id>{...params}`; a field's target is `#<id>` (a bare id in old flows). */
111
+ function parseTarget(raw, isButton) {
112
+ if (!isSet(raw)) { return { toId: null, attrs: null, hasHash: false }; }
113
+ if (isButton) {
114
+ const start = raw.indexOf('#') + 1;
115
+ const end = raw.indexOf('{');
116
+ const toId = end > -1 ? raw.substring(start, end) : raw.substring(start);
117
+ return { toId: toId !== '' ? toId : null, attrs: end > -1 ? raw.substring(end) : null, hasHash: raw.trim().startsWith('#') };
118
+ }
119
+ const toId = raw.replace('#', '');
120
+ return { toId: toId !== '' ? toId : null, attrs: null, hasHash: raw.trim().startsWith('#') };
121
+ }
122
+
123
+ function makeSlot(spec, intentId, actionId, holder, fieldName, item, keyOverride) {
124
+ const isButton = spec.path === 'buttons';
125
+ const raw = holder && isSet(holder[fieldName]) ? holder[fieldName] : '';
126
+ const suffix = spec.path === 'array' ? spec.suffixOf(item) : (isButton ? '/' + item.uid : spec.suffix);
127
+ return {
128
+ key: keyOverride || spec.key, kind: spec.kind,
129
+ intentId, actionId, fromId: intentId + '/' + actionId + suffix,
130
+ holder, field: fieldName, item, raw, ...parseTarget(raw, isButton)
131
+ };
132
+ }
133
+
134
+ /** The exits of one action, connected or empty, in catalog order. Never throws: a malformed action just has fewer exits. */
135
+ function outputsOf(intentId, action) {
136
+ const out = [];
137
+ if (!action || typeof action !== 'object') { return out; }
138
+ const actionId = action._tdActionId;
139
+ for (const spec of OUTPUT_CATALOG[action._tdActionType] || []) {
140
+ switch (spec.path) {
141
+ case 'field':
142
+ out.push(makeSlot(spec, intentId, actionId, action, spec.field));
143
+ break;
144
+ case 'nested': {
145
+ let holder = action;
146
+ for (let i = 0; i < spec.steps.length - 1 && holder; i++) { holder = holder[spec.steps[i]]; }
147
+ if (holder && typeof holder === 'object') { out.push(makeSlot(spec, intentId, actionId, holder, spec.steps[spec.steps.length - 1])); }
148
+ break;
149
+ }
150
+ case 'lastCommandSetting': {
151
+ const commands = action.attributes && action.attributes.commands;
152
+ const settings = Array.isArray(commands) && commands.length > 0 && commands[commands.length - 1] ? commands[commands.length - 1].settings : null;
153
+ if (settings && typeof settings === 'object') { out.push(makeSlot(spec, intentId, actionId, settings, spec.field)); }
154
+ break;
155
+ }
156
+ case 'buttons':
157
+ // A text button has an empty exit; only an `action` button with a target yields a record. A url button has no exit.
158
+ for (const button of findButtons(action)) {
159
+ if (!button || typeof button !== 'object' || button.type === BUTTON_URL) { continue; }
160
+ const slot = makeSlot(spec, intentId, actionId, button, 'action', button, 'button:' + (button.uid == null ? '' : button.uid));
161
+ if (button.type !== BUTTON_ACTION) { slot.raw = ''; slot.toId = null; slot.attrs = null; slot.hasHash = false; }
162
+ out.push(slot);
163
+ }
164
+ break;
165
+ case 'array': {
166
+ const list = action[spec.array];
167
+ if (!Array.isArray(list)) { break; }
168
+ for (const item of list) {
169
+ if (!item || typeof item !== 'object') { continue; }
170
+ out.push(makeSlot(spec, intentId, actionId, item, spec.field, item, spec.key + ':' + item[spec.idField]));
171
+ }
172
+ break;
173
+ }
174
+ default:
175
+ break;
176
+ }
177
+ }
178
+ return out;
179
+ }
180
+
181
+ function blockOutputOf(intent) {
182
+ const next = intent && intent.attributes && intent.attributes.nextBlockAction;
183
+ if (!next || typeof next !== 'object' || isReturnStackIntent(intent)) { return null; }
184
+ const slot = makeSlot(BLOCK_SLOT, intent.intent_id, next._tdActionId, next, 'intentName');
185
+ slot.fromId = intent.intent_id + '/' + next._tdActionId;
186
+ return slot;
187
+ }
188
+
189
+ /** Every exit of a block: the dot first, then the actions in their order. */
190
+ function slotsOfIntent(intent) {
191
+ const out = [];
192
+ const block = blockOutputOf(intent);
193
+ if (block) { out.push(block); }
194
+ for (const action of (intent && intent.actions) || []) { out.push(...outputsOf(intent.intent_id, action)); }
195
+ return out;
196
+ }
197
+
198
+ /**
199
+ * Fills in the technical ids a template may leave out: `intent_id`, `_tdActionId` of actions and of
200
+ * the block's dot, `uid` of buttons, `_tdCaseId` of the cases of a multi condition. The Design Studio
201
+ * did the same on open; a V3 block must have them before its list is built. Mutates; returns how many.
202
+ */
203
+ function ensureTechnicalIds(intents) {
204
+ let filled = 0;
205
+ for (const intent of intents || []) {
206
+ if (!intent || typeof intent !== 'object') { continue; }
207
+ if (!isSet(intent.intent_id)) { intent.intent_id = uuidv4(); filled++; }
208
+ const next = intent.attributes && intent.attributes.nextBlockAction;
209
+ if (next && typeof next === 'object' && !isSet(next._tdActionId)) { next._tdActionId = uuidv4(); filled++; }
210
+ for (const action of intent.actions || []) {
211
+ if (!action || typeof action !== 'object') { continue; }
212
+ if (!isSet(action._tdActionId)) { action._tdActionId = uuidv4(); filled++; }
213
+ for (const button of findButtons(action)) {
214
+ if (button && typeof button === 'object' && !hasValidUid(button.uid)) { button.uid = uuidv4(); filled++; }
215
+ }
216
+ if (Array.isArray(action.cases)) {
217
+ for (const c of action.cases) {
218
+ if (c && typeof c === 'object' && !isSet(c._tdCaseId)) { c._tdCaseId = uuidv4(); filled++; }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ return filled;
224
+ }
225
+
226
+ function indexFlow(intents) {
227
+ const ids = new Set();
228
+ for (const intent of intents || []) { if (intent && isSet(intent.intent_id)) { ids.add(intent.intent_id); } }
229
+ return ids;
230
+ }
231
+
232
+ /** A target that the list can hold: `#<id>` of an existing block, or a bare id that is one (the Design Studio added the `#`). */
233
+ function resolvedTarget(slot, ids) {
234
+ if (!slot.toId) { return null; }
235
+ if (slot.hasHash) { return ids.has(slot.toId) ? slot.toId : null; }
236
+ return ids.has(slot.toId) ? slot.toId : null;
237
+ }
238
+
239
+ /** A destination written the V2 way: the key exists and holds '' or a `#id` (a bare block name of old flows is left alone). */
240
+ function isV2Trace(slot, ids) {
241
+ if (!slot.holder || !Object.prototype.hasOwnProperty.call(slot.holder, slot.field)) { return false; }
242
+ if (slot.raw === '' || slot.hasHash) { return true; }
243
+ return !!slot.toId && ids.has(slot.toId);
244
+ }
245
+
246
+ function propsOf(slot, intent) {
247
+ const props = {};
248
+ if (slot.kind === 'button' && isSet(slot.attrs)) { props.params = slot.attrs; }
249
+ const looks = intent && intent.attributes && intent.attributes.connectors && intent.attributes.connectors[slot.fromId];
250
+ if (looks && typeof looks === 'object') {
251
+ if (looks.display !== undefined) { props.display = looks.display; }
252
+ if (looks.label !== undefined) { props.label = looks.label; }
253
+ if (looks.color !== undefined) { props.color = looks.color; }
254
+ }
255
+ return Object.keys(props).length > 0 ? props : null;
256
+ }
257
+
258
+ /**
259
+ * The connectors list the blocks describe: one record per connected exit towards a block that
260
+ * exists. Same ids and same order as the Design Studio's `buildCollection`.
261
+ */
262
+ function buildConnectors(intents, now) {
263
+ const ids = indexFlow(intents);
264
+ const items = [];
265
+ const seen = new Set();
266
+ for (const intent of intents || []) {
267
+ if (!intent) { continue; }
268
+ for (const slot of slotsOfIntent(intent)) {
269
+ const toId = resolvedTarget(slot, ids);
270
+ if (!toId) { continue; }
271
+ const id = slot.fromId + '/' + toId;
272
+ if (seen.has(id)) { continue; }
273
+ seen.add(id);
274
+ const record = { id, from: { intentId: slot.intentId, actionId: slot.actionId, slot: slot.key }, to: { intentId: toId }, kind: slot.kind };
275
+ const props = propsOf(slot, intent);
276
+ if (props) { record.props = props; }
277
+ items.push(record);
278
+ }
279
+ }
280
+ return { version: COLLECTION_VERSION, updatedAt: now ? now() : new Date().toISOString(), items };
281
+ }
282
+
283
+ /**
284
+ * Clears every V2 trace of the connectors from the blocks: destination keys of actions and of the
285
+ * dot, `button.action`, and the per-block visual attributes (`attributes.connectors`). Mirrors the
286
+ * Design Studio's `stripV2Connectors`. Mutates; returns how many it removed.
287
+ */
288
+ function stripDestinations(intents) {
289
+ const ids = indexFlow(intents);
290
+ let stripped = 0;
291
+ for (const intent of intents || []) {
292
+ if (!intent) { continue; }
293
+ for (const slot of slotsOfIntent(intent)) {
294
+ if (!isV2Trace(slot, ids)) { continue; }
295
+ delete slot.holder[slot.field];
296
+ stripped++;
297
+ }
298
+ if (intent.attributes && Object.prototype.hasOwnProperty.call(intent.attributes, 'connectors')) {
299
+ delete intent.attributes.connectors;
300
+ stripped++;
301
+ }
302
+ }
303
+ return stripped;
304
+ }
305
+
306
+ /**
307
+ * Blocks written the V2 way (a template, an old JSON) become V3 blocks plus their list.
308
+ * Mutates the intents it receives and returns `{ intents, connectors }`.
309
+ */
310
+ function toV3Flow(intents, now) {
311
+ const list = Array.isArray(intents) ? intents : [];
312
+ ensureTechnicalIds(list);
313
+ const connectors = buildConnectors(list, now);
314
+ stripDestinations(list);
315
+ return { intents: list, connectors };
316
+ }
317
+
318
+ /** The errors a connectors list has; empty when it is good. Shape only: targets are not checked against the blocks. */
319
+ function validateConnectors(collection) {
320
+ const errors = [];
321
+ if (!collection || typeof collection !== 'object' || Array.isArray(collection)) { return ['connectors must be an object']; }
322
+ if (collection.version !== COLLECTION_VERSION) { errors.push('connectors.version must be ' + COLLECTION_VERSION); }
323
+ if (collection.updatedAt !== undefined && collection.updatedAt !== null && isNaN(new Date(collection.updatedAt).getTime())) { errors.push('connectors.updatedAt must be a date'); }
324
+ if (!Array.isArray(collection.items)) { errors.push('connectors.items must be an array'); return errors; }
325
+ const ids = new Set();
326
+ collection.items.forEach((record, i) => {
327
+ const at = 'connectors.items[' + i + ']';
328
+ if (!record || typeof record !== 'object') { errors.push(at + ' must be an object'); return; }
329
+ if (!isSet(record.id)) { errors.push(at + '.id is required'); }
330
+ const from = record.from && typeof record.from === 'object' ? record.from : null;
331
+ if (!from || !isSet(from.intentId) || !isSet(from.actionId) || !isSet(from.slot)) { errors.push(at + '.from needs intentId, actionId and slot'); }
332
+ const to = record.to && typeof record.to === 'object' ? record.to : null;
333
+ if (!to || !isSet(to.intentId)) { errors.push(at + '.to.intentId is required'); }
334
+ if (!RECORD_KINDS.includes(record.kind)) { errors.push(at + '.kind must be one of ' + RECORD_KINDS.join(', ')); }
335
+ if (record.props !== undefined && (record.props === null || typeof record.props !== 'object' || Array.isArray(record.props))) { errors.push(at + '.props must be an object'); }
336
+ if (isSet(record.id) && from && to && isSet(from.intentId) && isSet(to.intentId)) {
337
+ if (!record.id.startsWith(from.intentId + '/') || !record.id.endsWith('/' + to.intentId)) { errors.push(at + '.id does not match from.intentId and to.intentId'); }
338
+ }
339
+ if (isSet(record.id)) {
340
+ if (ids.has(record.id)) { errors.push(at + '.id is duplicated: ' + record.id); }
341
+ ids.add(record.id);
342
+ }
343
+ });
344
+ return errors;
345
+ }
346
+
347
+ /** 'v3' for any spelling of the V3 label, otherwise undefined. */
348
+ function normalizeDsVersion(value) {
349
+ if (typeof value !== 'string') { return undefined; }
350
+ const label = value.trim().toLowerCase();
351
+ return label === DS_VERSION_V3 ? DS_VERSION_V3 : undefined;
352
+ }
353
+
354
+ function isV3(bot) {
355
+ return !!bot && bot.ds_version === DS_VERSION_V3;
356
+ }
357
+
358
+ /** The legacy keys a PATCH of the attributes must not carry any more. */
359
+ function legacyAttributeKeys(attributes) {
360
+ if (!attributes || typeof attributes !== 'object') { return []; }
361
+ return ['dsVersion', 'connectors'].filter((key) => Object.prototype.hasOwnProperty.call(attributes, key));
362
+ }
363
+
364
+ /**
365
+ * A JSON exported before 15/10/2026 carries `attributes.dsVersion` and `attributes.connectors`:
366
+ * they move to `ds_version` and `connectors` (the fields win if both are present). Applies to the
367
+ * sub agents too. Mutates; returns true if it moved anything.
368
+ */
369
+ function promoteLegacyJson(json) {
370
+ if (!json || typeof json !== 'object') { return false; }
371
+ let moved = false;
372
+ const attributes = json.attributes;
373
+ if (attributes && typeof attributes === 'object') {
374
+ if (Object.prototype.hasOwnProperty.call(attributes, 'dsVersion')) {
375
+ if (json.ds_version === undefined) {
376
+ const label = normalizeDsVersion(attributes.dsVersion);
377
+ if (label) { json.ds_version = label; }
378
+ }
379
+ delete attributes.dsVersion;
380
+ moved = true;
381
+ }
382
+ if (Object.prototype.hasOwnProperty.call(attributes, 'connectors')) {
383
+ if (json.connectors === undefined && attributes.connectors && typeof attributes.connectors === 'object') {
384
+ json.connectors = attributes.connectors;
385
+ }
386
+ delete attributes.connectors;
387
+ moved = true;
388
+ }
389
+ }
390
+ if (json.ds_version !== undefined) {
391
+ const label = normalizeDsVersion(json.ds_version);
392
+ if (label) { json.ds_version = label; } else { delete json.ds_version; }
393
+ }
394
+ for (const subagent of Array.isArray(json.subagents) ? json.subagents : []) {
395
+ if (promoteLegacyJson(subagent)) { moved = true; }
396
+ }
397
+ if (moved) { winston.verbose('(V3FlowService) legacy dsVersion/connectors moved out of attributes'); }
398
+ return moved;
399
+ }
400
+
401
+ module.exports = {
402
+ DS_VERSION_V3,
403
+ COLLECTION_VERSION,
404
+ RECORD_KINDS,
405
+ OUTPUT_CATALOG,
406
+ slotsOfIntent,
407
+ ensureTechnicalIds,
408
+ buildConnectors,
409
+ stripDestinations,
410
+ toV3Flow,
411
+ validateConnectors,
412
+ normalizeDsVersion,
413
+ isV3,
414
+ legacyAttributeKeys,
415
+ promoteLegacyJson
416
+ };
@@ -3,9 +3,26 @@ const uuidv4 = require('uuid/v4');
3
3
  module.exports = function generateTemplate(options) {
4
4
 
5
5
  const custom_intent_id = uuidv4();
6
-
6
+ const default_fallback_intent_id = uuidv4();
7
+
7
8
  return [
8
9
  {
10
+ "webhook_enabled": false,
11
+ "enabled": true,
12
+ "actions": [],
13
+ "intent_display_name": "defaultFallback",
14
+ "attributes": {
15
+ "position": {
16
+ "x": 714,
17
+ "y": 528
18
+ },
19
+ "nextBlockAction": {
20
+ "_tdActionId": "5b689996-317b-4761-9bc0-cc78d1f390cb",
21
+ "_tdActionType": "intent",
22
+ "intentName": "#" + default_fallback_intent_id
23
+ }
24
+ }
25
+ }, {
9
26
  "webhook_enabled": false,
10
27
  "enabled": true,
11
28
  "actions": [{
@@ -24,10 +41,11 @@ module.exports = function generateTemplate(options) {
24
41
  }]
25
42
  }
26
43
  }],
27
- "intent_display_name": "defaultFallback",
44
+ "intent_display_name": "untitled_block_2",
45
+ "intent_id": default_fallback_intent_id,
28
46
  "attributes": {
29
47
  "position": {
30
- "x": 714,
48
+ "x": 1114,
31
49
  "y": 528
32
50
  }
33
51
  }
@@ -0,0 +1,139 @@
1
+ const uuidv4 = require('uuid/v4');
2
+
3
+ module.exports = function generateTemplate(options) {
4
+
5
+ const custom_intent_id = uuidv4();
6
+ const return_custom_intent_id = uuidv4();
7
+ const default_fallback_intent_id = uuidv4();
8
+
9
+ return [
10
+ {
11
+ "webhook_enabled": false,
12
+ "enabled": true,
13
+ "actions": [{
14
+ "_tdActionType": "intent",
15
+ "intentName": "#" + custom_intent_id
16
+ }],
17
+ "question": "",
18
+ "intent_display_name": "start",
19
+ "attributes": {
20
+ "position": {
21
+ "x": 172,
22
+ "y": 384
23
+ }
24
+ },
25
+ "agents_available": false,
26
+ },
27
+ {
28
+ "webhook_enabled": false,
29
+ "enabled": true,
30
+ "actions": [{
31
+ "_tdActionTitle": "",
32
+ "_tdActionType": "reply",
33
+ "attributes": {
34
+ "disableInputMessage": false,
35
+ "commands": [
36
+ {
37
+ "type": "wait",
38
+ "time": 500
39
+ },
40
+ {
41
+ "type": "message",
42
+ "message": {
43
+ "type": "text",
44
+ "text": "A chat message will be sent to the visitor",
45
+ "attributes": {
46
+ "attachment": {
47
+ "type": "template",
48
+ "buttons": [],
49
+ "json_buttons": "",
50
+ "json_gallery": ""
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ]
56
+ },
57
+
58
+ }],
59
+ "intent_display_name": "untitled_block_1",
60
+ "intent_id": custom_intent_id,
61
+ "agents_available": false,
62
+ "attributes": {
63
+ "position": {
64
+ "x": 612,
65
+ "y": 312
66
+ },
67
+ "nextBlockAction": {
68
+ "_tdActionId": "5b689996-317b-4761-9bc0-cc78d1f390ca",
69
+ "_tdActionType": "intent",
70
+ "intentName": "#" + return_custom_intent_id
71
+ },
72
+ "connectors": {},
73
+ "color": "156,163,205",
74
+ "readonly": false
75
+ },
76
+ },
77
+ {
78
+ "webhook_enabled": false,
79
+ "enabled": true,
80
+ "actions": [{
81
+ "_tdActionType": "returnstack",
82
+ }],
83
+ "intent_display_name": "return",
84
+ "agents_available": false,
85
+ "intent_id": return_custom_intent_id,
86
+ "attributes": {
87
+ "position": {
88
+ "x": 1129,
89
+ "y": 361
90
+ }
91
+ },
92
+ "agents_available": false,
93
+ },
94
+ {
95
+ "webhook_enabled": false,
96
+ "enabled": true,
97
+ "actions": [],
98
+ "intent_display_name": "defaultFallback",
99
+ "attributes": {
100
+ "position": {
101
+ "x": 315,
102
+ "y": 699
103
+ },
104
+ "nextBlockAction": {
105
+ "_tdActionId": "5b689996-317b-4761-9bc0-cc78d1f390cb",
106
+ "_tdActionType": "intent",
107
+ "intentName": "#" + default_fallback_intent_id
108
+ }
109
+ }
110
+ }, {
111
+ "webhook_enabled": false,
112
+ "enabled": true,
113
+ "actions": [{
114
+ "_tdActionType": "reply",
115
+ "text": "I didn't understand. Can you rephrase your question?",
116
+ "attributes": {
117
+ "commands": [{
118
+ "type": "wait",
119
+ "time": 500
120
+ }, {
121
+ "type": "message",
122
+ "message": {
123
+ "type": "text",
124
+ "text": "I didn't understand. Can you rephrase your question?"
125
+ }
126
+ }]
127
+ }
128
+ }],
129
+ "intent_display_name": "untitled_block_2",
130
+ "intent_id": default_fallback_intent_id,
131
+ "attributes": {
132
+ "position": {
133
+ "x": 721,
134
+ "y": 699
135
+ }
136
+ }
137
+ }
138
+ ]
139
+ }
@@ -3,6 +3,7 @@ const uuidv4 = require('uuid/v4');
3
3
  module.exports = function generateTemplate(options) {
4
4
 
5
5
  const custom_intent_id = uuidv4();
6
+ const default_fallback_intent_id = uuidv4();
6
7
 
7
8
  return [
8
9
  {
@@ -24,6 +25,22 @@ module.exports = function generateTemplate(options) {
24
25
  }
25
26
  },
26
27
  {
28
+ "webhook_enabled": false,
29
+ "enabled": true,
30
+ "actions": [],
31
+ "intent_display_name": "defaultFallback",
32
+ "attributes": {
33
+ "position": {
34
+ "x": 714,
35
+ "y": 528
36
+ },
37
+ "nextBlockAction": {
38
+ "_tdActionId": "5b689996-317b-4761-9bc0-cc78d1f390cb",
39
+ "_tdActionType": "intent",
40
+ "intentName": "#" + default_fallback_intent_id
41
+ }
42
+ }
43
+ }, {
27
44
  "webhook_enabled": false,
28
45
  "enabled": true,
29
46
  "actions": [{
@@ -40,13 +57,13 @@ module.exports = function generateTemplate(options) {
40
57
  "text": "I didn't understand. Can you rephrase your question?"
41
58
  }
42
59
  }]
43
- },
60
+ }
44
61
  }],
45
- "intent_display_name": "defaultFallback",
62
+ "intent_display_name": "untitled_block_2",
63
+ "intent_id": default_fallback_intent_id,
46
64
  "attributes": {
47
- "readonly": true,
48
65
  "position": {
49
- "x": 714,
66
+ "x": 1114,
50
67
  "y": 528
51
68
  }
52
69
  }