@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.
- package/CHANGELOG.md +20 -0
- package/app.js +6 -35
- package/config/native_mcp/native_mcp_servers.json +7 -0
- package/controllers/chatbot.controller.js +659 -0
- package/controllers/intents.controller.js +139 -0
- package/errorCodes.js +2 -1
- package/event/botEvent.js +43 -3
- package/middleware/errorHandler.js +41 -0
- package/models/chatbotTemplates.js +4 -0
- package/models/faq.js +10 -0
- package/models/faq_kb.js +49 -0
- package/package.json +2 -2
- package/pubmodules/aiGenerations/README.md +154 -0
- package/pubmodules/aiGenerations/index.js +7 -0
- package/pubmodules/aiGenerations/listeners.js +96 -0
- package/pubmodules/aiGenerations/models/faqKbRevision.js +51 -0
- package/pubmodules/aiGenerations/routes/aiGenerations.js +426 -0
- package/pubmodules/aiGenerations/services/agentWriter.js +286 -0
- package/pubmodules/aiGenerations/services/atomic.js +70 -0
- package/pubmodules/aiGenerations/services/references.js +78 -0
- package/pubmodules/aiGenerations/services/revisionService.js +169 -0
- package/pubmodules/aiGenerations/services/snapshot.js +109 -0
- package/pubmodules/aiGenerations/test/aiGenerationsRoute.js +503 -0
- package/pubmodules/pubModulesManager.js +17 -0
- package/routes/faq.js +10 -3
- package/routes/faq_kb.js +716 -411
- package/services/chatbotService.js +84 -5
- package/services/faqService.js +159 -45
- package/services/v3FlowService.js +416 -0
- package/template/chatbot/blank.js +21 -3
- package/template/chatbot/blank_subagent.js +139 -0
- package/template/chatbot/blank_voice.js +21 -4
- package/template/chatbot/blank_voice_twilio.js +21 -5
- package/template/chatbot/index.js +1 -0
- package/template/chatbot/subagent_notes.js +30 -0
- package/test/faqRoute.js +83 -0
- package/test/faqkbRoute.js +982 -523
- package/test/faqkbV3Route.js +436 -0
- package/test/fixtures/example-json-with-subagents.txt +1 -0
- package/test/nativemcp.test.js +6 -1
- package/test/v3FlowService.js +187 -0
- package/test/webhookRoute.js +2 -1
- package/utils/AppError.js +9 -0
- package/utils/asyncHandler.js +10 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
process.env.NODE_ENV = 'test';
|
|
2
|
+
process.env.LOG_LEVEL = 'error';
|
|
3
|
+
|
|
4
|
+
const chai = require('chai');
|
|
5
|
+
const expect = chai.expect;
|
|
6
|
+
|
|
7
|
+
const v3 = require('../services/v3FlowService');
|
|
8
|
+
const templates = require('../template/chatbot');
|
|
9
|
+
|
|
10
|
+
/** Blocks written the V2 way with fixed ids, so the list can be compared with the Design Studio's. */
|
|
11
|
+
function fixedBlank() {
|
|
12
|
+
return [
|
|
13
|
+
{ intent_id: 'fallback-id', intent_display_name: 'defaultFallback', actions: [], attributes: { position: { x: 0, y: 0 }, nextBlockAction: { _tdActionId: 'dot-id', _tdActionType: 'intent', intentName: '#msg-id' } } },
|
|
14
|
+
{ intent_id: 'msg-id', intent_display_name: 'untitled_block_2', actions: [{ _tdActionId: 'reply-id', _tdActionType: 'reply', text: 'I did not understand', attributes: { commands: [] } }], attributes: { position: { x: 0, y: 0 } } },
|
|
15
|
+
{ intent_id: 'start-id', intent_display_name: 'start', question: '\\start', actions: [{ _tdActionId: 'intent-id', _tdActionType: 'intent', intentName: '#welcome-id' }], attributes: { position: { x: 0, y: 0 } } },
|
|
16
|
+
{ intent_id: 'welcome-id', intent_display_name: 'welcome', actions: [{ _tdActionId: 'welcome-reply-id', _tdActionType: 'reply', text: 'Hi', attributes: { commands: [] } }], attributes: { position: { x: 0, y: 0 } } }
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('v3FlowService', () => {
|
|
21
|
+
|
|
22
|
+
describe('toV3Flow', () => {
|
|
23
|
+
|
|
24
|
+
it('builds the list with the Design Studio ids and clears the V2 destinations', () => {
|
|
25
|
+
const flow = v3.toV3Flow(fixedBlank(), () => '2026-10-15T00:00:00.000Z');
|
|
26
|
+
expect(flow.connectors).to.deep.equal({
|
|
27
|
+
version: 1,
|
|
28
|
+
updatedAt: '2026-10-15T00:00:00.000Z',
|
|
29
|
+
items: [
|
|
30
|
+
{ id: 'fallback-id/dot-id/msg-id', from: { intentId: 'fallback-id', actionId: 'dot-id', slot: 'next' }, to: { intentId: 'msg-id' }, kind: 'block' },
|
|
31
|
+
{ id: 'start-id/intent-id/welcome-id', from: { intentId: 'start-id', actionId: 'intent-id', slot: 'default' }, to: { intentId: 'welcome-id' }, kind: 'action' }
|
|
32
|
+
]
|
|
33
|
+
});
|
|
34
|
+
const fallback = flow.intents[0];
|
|
35
|
+
expect(fallback.attributes.nextBlockAction).to.deep.equal({ _tdActionId: 'dot-id', _tdActionType: 'intent' });
|
|
36
|
+
expect(flow.intents[2].actions[0]).to.not.have.property('intentName');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('fills in the technical ids a template leaves out, so every record has an anchor', () => {
|
|
40
|
+
const flow = v3.toV3Flow(JSON.parse(JSON.stringify(templates.blank({}))));
|
|
41
|
+
flow.intents.forEach((intent) => {
|
|
42
|
+
expect(intent.intent_id).to.be.a('string').that.is.not.empty;
|
|
43
|
+
intent.actions.forEach((action) => expect(action._tdActionId).to.be.a('string').that.is.not.empty);
|
|
44
|
+
});
|
|
45
|
+
expect(flow.connectors.items).to.have.lengthOf(2);
|
|
46
|
+
flow.connectors.items.forEach((record) => {
|
|
47
|
+
expect(record.from.actionId).to.be.a('string').that.is.not.empty;
|
|
48
|
+
expect(flow.intents.some((intent) => intent.intent_id === record.to.intentId)).to.equal(true);
|
|
49
|
+
});
|
|
50
|
+
expect(v3.validateConnectors(flow.connectors)).to.deep.equal([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('converts every server template without errors', () => {
|
|
54
|
+
Object.keys(templates).forEach((name) => {
|
|
55
|
+
const flow = v3.toV3Flow(JSON.parse(JSON.stringify(templates[name]({}))));
|
|
56
|
+
expect(v3.validateConnectors(flow.connectors), name).to.deep.equal([]);
|
|
57
|
+
flow.connectors.items.forEach((record) => {
|
|
58
|
+
expect(flow.intents.some((intent) => intent.intent_id === record.to.intentId), name + ' ' + record.id).to.equal(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('covers true/false, cases, branches, buttons and the iteration, and keeps a button\'s params', () => {
|
|
64
|
+
const intents = [
|
|
65
|
+
{ intent_id: 'a', intent_display_name: 'a', actions: [
|
|
66
|
+
{ _tdActionId: 'c1', _tdActionType: 'jsoncondition2', trueIntent: '#b', falseIntent: '#c' },
|
|
67
|
+
{ _tdActionId: 'm1', _tdActionType: 'jsonconditionmulti', cases: [{ _tdCaseId: 'k1', intent: '#b' }, { _tdCaseId: 'k2', intent: '' }], elseIntent: '#c' },
|
|
68
|
+
{ _tdActionId: 'ai1', _tdActionType: 'ai_condition', intents: [{ label: 'yes', conditionIntentId: '#b' }], fallbackIntent: '#c', errorIntent: '' },
|
|
69
|
+
{ _tdActionId: 'it1', _tdActionType: 'iteration', goToIntent: '#b', fallbackIntent: '#c' },
|
|
70
|
+
{ _tdActionId: 'r1', _tdActionType: 'reply', attributes: { commands: [{ type: 'message', message: { attributes: { attachment: { buttons: [
|
|
71
|
+
{ uid: 'u1', type: 'action', value: 'go', action: '#b{"x":1}' },
|
|
72
|
+
{ uid: 'u2', type: 'text', value: 'plain' },
|
|
73
|
+
{ uid: 'u3', type: 'url', value: 'site', link: 'https://x' }
|
|
74
|
+
] } } } }] } }
|
|
75
|
+
], attributes: {} },
|
|
76
|
+
{ intent_id: 'b', intent_display_name: 'b', actions: [], attributes: {} },
|
|
77
|
+
{ intent_id: 'c', intent_display_name: 'c', actions: [], attributes: {} }
|
|
78
|
+
];
|
|
79
|
+
const flow = v3.toV3Flow(intents, () => 'now');
|
|
80
|
+
expect(flow.connectors.items.map((r) => r.id)).to.deep.equal([
|
|
81
|
+
'a/c1/true/b', 'a/c1/false/c',
|
|
82
|
+
'a/m1/case/k1/b', 'a/m1/else/c',
|
|
83
|
+
'a/ai1/yes/true/b', 'a/ai1/fallback/c',
|
|
84
|
+
'a/it1/goto/b', 'a/it1/fallback/c',
|
|
85
|
+
'a/r1/u1/b'
|
|
86
|
+
]);
|
|
87
|
+
expect(flow.connectors.items.map((r) => r.kind)).to.deep.equal(['action', 'action', 'case', 'action', 'branch', 'action', 'action', 'action', 'button']);
|
|
88
|
+
expect(flow.connectors.items.map((r) => r.from.slot)).to.deep.equal(['true', 'false', 'case:k1', 'else', 'branch:yes', 'fallback', 'goto', 'fallback', 'button:u1']);
|
|
89
|
+
expect(flow.connectors.items[8].props).to.deep.equal({ params: '{"x":1}' });
|
|
90
|
+
const a = flow.intents[0];
|
|
91
|
+
expect(a.actions[0]).to.not.have.property('trueIntent');
|
|
92
|
+
expect(a.actions[1].cases[1]).to.not.have.property('intent');
|
|
93
|
+
expect(a.actions[2]).to.not.have.property('errorIntent');
|
|
94
|
+
const buttons = a.actions[4].attributes.commands[0].message.attributes.attachment.buttons;
|
|
95
|
+
expect(buttons[0]).to.not.have.property('action');
|
|
96
|
+
expect(buttons[0].type).to.equal('action');
|
|
97
|
+
expect(buttons[2].link).to.equal('https://x');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('leaves a bare block name and a target to a block that does not exist out of the list', () => {
|
|
101
|
+
const intents = [
|
|
102
|
+
{ intent_id: 'a', intent_display_name: 'a', actions: [{ _tdActionId: 'i1', _tdActionType: 'intent', intentName: 'welcome' }], attributes: { nextBlockAction: { _tdActionId: 'd1', _tdActionType: 'intent', intentName: '#ghost' } } },
|
|
103
|
+
{ intent_id: 'w', intent_display_name: 'welcome', actions: [], attributes: {} }
|
|
104
|
+
];
|
|
105
|
+
const flow = v3.toV3Flow(intents);
|
|
106
|
+
expect(flow.connectors.items).to.deep.equal([]);
|
|
107
|
+
expect(flow.intents[0].actions[0].intentName).to.equal('welcome');
|
|
108
|
+
expect(flow.intents[0].attributes.nextBlockAction).to.not.have.property('intentName');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('does not give a "Return to parent agent" block a dot connector', () => {
|
|
112
|
+
const intents = [
|
|
113
|
+
{ intent_id: 'r', intent_display_name: 'return', actions: [{ _tdActionId: 'rs', _tdActionType: 'returnstack' }], attributes: { nextBlockAction: { _tdActionId: 'd', _tdActionType: 'intent', intentName: '#x' } } },
|
|
114
|
+
{ intent_id: 'x', intent_display_name: 'x', actions: [], attributes: {} }
|
|
115
|
+
];
|
|
116
|
+
expect(v3.toV3Flow(intents).connectors.items).to.deep.equal([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('validateConnectors', () => {
|
|
121
|
+
const good = { id: 'a/b/c', from: { intentId: 'a', actionId: 'b', slot: 'next' }, to: { intentId: 'c' }, kind: 'block' };
|
|
122
|
+
|
|
123
|
+
it('accepts a well formed list, with or without updatedAt and props', () => {
|
|
124
|
+
expect(v3.validateConnectors({ version: 1, items: [] })).to.deep.equal([]);
|
|
125
|
+
expect(v3.validateConnectors({ version: 1, updatedAt: '2026-10-15T10:00:00.000Z', items: [{ ...good, props: { label: 'x' } }] })).to.deep.equal([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('names what is wrong', () => {
|
|
129
|
+
expect(v3.validateConnectors(null)).to.have.lengthOf(1);
|
|
130
|
+
expect(v3.validateConnectors({ version: 2, items: [] })[0]).to.match(/version/);
|
|
131
|
+
expect(v3.validateConnectors({ version: 1 })[0]).to.match(/items/);
|
|
132
|
+
expect(v3.validateConnectors({ version: 1, updatedAt: 'yesterday', items: [] })[0]).to.match(/updatedAt/);
|
|
133
|
+
expect(v3.validateConnectors({ version: 1, items: [{ ...good, kind: 'line' }] })[0]).to.match(/kind/);
|
|
134
|
+
expect(v3.validateConnectors({ version: 1, items: [{ ...good, id: 'z/b/c' }] })[0]).to.match(/does not match/);
|
|
135
|
+
expect(v3.validateConnectors({ version: 1, items: [good, good] })[0]).to.match(/duplicated/);
|
|
136
|
+
expect(v3.validateConnectors({ version: 1, items: [{ ...good, from: { intentId: 'a' } }] })[0]).to.match(/from/);
|
|
137
|
+
expect(v3.validateConnectors({ version: 1, items: [{ ...good, props: 'x' }] })[0]).to.match(/props/);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('promoteLegacyJson', () => {
|
|
142
|
+
|
|
143
|
+
it('moves dsVersion and connectors out of attributes, for the sub agents too', () => {
|
|
144
|
+
const json = {
|
|
145
|
+
attributes: { dsVersion: ' V3 ', connectors: { version: 1, items: [] }, globals: [] },
|
|
146
|
+
subagents: [{ attributes: { dsVersion: 'v3' } }]
|
|
147
|
+
};
|
|
148
|
+
expect(v3.promoteLegacyJson(json)).to.equal(true);
|
|
149
|
+
expect(json.ds_version).to.equal('v3');
|
|
150
|
+
expect(json.connectors).to.deep.equal({ version: 1, items: [] });
|
|
151
|
+
expect(json.attributes).to.deep.equal({ globals: [] });
|
|
152
|
+
expect(json.subagents[0].ds_version).to.equal('v3');
|
|
153
|
+
expect(json.subagents[0].attributes).to.deep.equal({});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('keeps the fields when both forms are present, and drops an unknown label', () => {
|
|
157
|
+
const json = { ds_version: 'v3', connectors: { version: 1, items: [] }, attributes: { dsVersion: 'v2', connectors: { version: 1, items: [{}] } } };
|
|
158
|
+
v3.promoteLegacyJson(json);
|
|
159
|
+
expect(json.ds_version).to.equal('v3');
|
|
160
|
+
expect(json.connectors.items).to.deep.equal([]);
|
|
161
|
+
const unknown = { ds_version: 'v9', attributes: {} };
|
|
162
|
+
v3.promoteLegacyJson(unknown);
|
|
163
|
+
expect(unknown).to.not.have.property('ds_version');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('leaves a V2 JSON alone', () => {
|
|
167
|
+
const json = { name: 'v2', attributes: { globals: [] }, intents: [] };
|
|
168
|
+
expect(v3.promoteLegacyJson(json)).to.equal(false);
|
|
169
|
+
expect(json).to.deep.equal({ name: 'v2', attributes: { globals: [] }, intents: [] });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('normalizeDsVersion and legacyAttributeKeys', () => {
|
|
174
|
+
it('recognises the V3 label in any spelling and nothing else', () => {
|
|
175
|
+
expect(v3.normalizeDsVersion('v3')).to.equal('v3');
|
|
176
|
+
expect(v3.normalizeDsVersion(' V3 ')).to.equal('v3');
|
|
177
|
+
expect(v3.normalizeDsVersion('v4')).to.equal(undefined);
|
|
178
|
+
expect(v3.normalizeDsVersion(3)).to.equal(undefined);
|
|
179
|
+
expect(v3.normalizeDsVersion(undefined)).to.equal(undefined);
|
|
180
|
+
});
|
|
181
|
+
it('lists the legacy keys a patch of the attributes carries', () => {
|
|
182
|
+
expect(v3.legacyAttributeKeys({ dsVersion: 'v3', connectors: {}, notes: [] })).to.deep.equal(['dsVersion', 'connectors']);
|
|
183
|
+
expect(v3.legacyAttributeKeys({ notes: [] })).to.deep.equal([]);
|
|
184
|
+
expect(v3.legacyAttributeKeys(null)).to.deep.equal([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
package/test/webhookRoute.js
CHANGED
|
@@ -16,6 +16,7 @@ let server = require('../app');
|
|
|
16
16
|
let should = chai.should();
|
|
17
17
|
var fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
|
+
const { ChatbotService } = require('../services/chatbotService');
|
|
19
20
|
|
|
20
21
|
var expect = chai.expect;
|
|
21
22
|
var assert = chai.assert;
|
|
@@ -543,7 +544,7 @@ describe('WebhookRoute', () => {
|
|
|
543
544
|
userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
|
|
544
545
|
projectService.create("test-webhook-create", savedUser._id).then(function (savedProject) {
|
|
545
546
|
|
|
546
|
-
class chatbot_service {
|
|
547
|
+
class chatbot_service extends ChatbotService {
|
|
547
548
|
async fork(id_faq_kb, api_url, token, project_id) {
|
|
548
549
|
let forked_bot_id = id_faq_kb.substr(id_faq_kb, id_faq_kb.length - 4) + "1111";
|
|
549
550
|
return { bot_id: forked_bot_id }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps an async route handler so rejected promises are forwarded to Express error middleware.
|
|
3
|
+
*/
|
|
4
|
+
function asyncHandler(fn) {
|
|
5
|
+
return function (req, res, next) {
|
|
6
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = asyncHandler;
|