@tiledesk/tiledesk-server 2.28.1 → 2.29.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 +9 -0
- package/app.js +5 -34
- package/controllers/chatbot.controller.js +599 -0
- package/controllers/intents.controller.js +139 -0
- package/errorCodes.js +2 -1
- package/event/botEvent.js +43 -3
- package/jobsManager.js +4 -1
- package/middleware/errorHandler.js +41 -0
- package/models/chatbotTemplates.js +4 -0
- package/models/faq.js +10 -0
- package/models/faq_kb.js +14 -0
- package/package.json +4 -4
- package/routes/faq_kb.js +630 -410
- package/services/chatbotService.js +84 -5
- package/services/faqService.js +129 -43
- 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/faqkbRoute.js +982 -523
- package/test/fixtures/example-json-with-subagents.txt +1 -0
- package/test/webhookRoute.js +2 -1
- package/utils/AppError.js +9 -0
- package/utils/asyncHandler.js +10 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const Intent = require("../models/faq");
|
|
2
|
+
const AppError = require("../utils/AppError");
|
|
3
|
+
const faqBotEvent = require("../event/faqBotEvent");
|
|
4
|
+
const winston = require("../config/winston");
|
|
5
|
+
|
|
6
|
+
const INTENT_FIELDS = [
|
|
7
|
+
"intent_display_name",
|
|
8
|
+
"intent_id",
|
|
9
|
+
"question",
|
|
10
|
+
"answer",
|
|
11
|
+
"reply",
|
|
12
|
+
"form",
|
|
13
|
+
"enabled",
|
|
14
|
+
"webhook_enabled",
|
|
15
|
+
"language",
|
|
16
|
+
"actions",
|
|
17
|
+
"attributes"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
class IntentsController {
|
|
21
|
+
|
|
22
|
+
constructor() { }
|
|
23
|
+
|
|
24
|
+
_mapIntent(chatbot_id, id_project, user_id, intent) {
|
|
25
|
+
const mapped = {
|
|
26
|
+
id_faq_kb: chatbot_id,
|
|
27
|
+
id_project,
|
|
28
|
+
createdBy: user_id
|
|
29
|
+
};
|
|
30
|
+
for (const field of INTENT_FIELDS) {
|
|
31
|
+
if (intent[field] !== undefined) {
|
|
32
|
+
mapped[field] = intent[field];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return mapped;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async create(chatbot_id, id_project, user_id, intent, { skipDuplicates = false } = {}) {
|
|
39
|
+
try {
|
|
40
|
+
const faq = await Intent.create(
|
|
41
|
+
this._mapIntent(chatbot_id, id_project, user_id, intent)
|
|
42
|
+
);
|
|
43
|
+
faqBotEvent.emit("faq.create", faq);
|
|
44
|
+
return faq;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === 11000) {
|
|
47
|
+
if (skipDuplicates) {
|
|
48
|
+
winston.debug("Skip duplicated intent_display_name");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
throw new AppError(409, "Duplicate intent_display_name");
|
|
52
|
+
}
|
|
53
|
+
winston.error("(IntentsController) create error: ", err);
|
|
54
|
+
throw new AppError(500, "Error creating intent");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async createMany(chatbot_id, id_project, user_id, intents, options) {
|
|
59
|
+
if (!intents?.length) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const results = await Promise.allSettled(
|
|
63
|
+
intents.map((intent) =>
|
|
64
|
+
this.create(chatbot_id, id_project, user_id, intent, options)
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
return results
|
|
68
|
+
.filter((r) => r.status === "fulfilled" && r.value)
|
|
69
|
+
.map((r) => r.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async upsert(chatbot_id, id_project, user_id, intent) {
|
|
73
|
+
try {
|
|
74
|
+
const savingResult = await Intent.findOneAndUpdate(
|
|
75
|
+
{ id_faq_kb: chatbot_id, intent_display_name: intent.intent_display_name },
|
|
76
|
+
this._mapIntent(chatbot_id, id_project, user_id, intent),
|
|
77
|
+
{ new: true, upsert: true, rawResult: true }
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!savingResult?.value) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (savingResult.lastErrorObject?.updatedExisting === true) {
|
|
85
|
+
winston.verbose("updated existing intent");
|
|
86
|
+
faqBotEvent.emit("faq.update", savingResult.value);
|
|
87
|
+
} else {
|
|
88
|
+
winston.verbose("new intent created");
|
|
89
|
+
faqBotEvent.emit("faq.create", savingResult.value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return savingResult.value;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
winston.error("(IntentsController) upsert error: ", err);
|
|
95
|
+
throw new AppError(500, "Error upserting intent");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async upsertMany(chatbot_id, id_project, user_id, intents) {
|
|
100
|
+
if (!intents?.length) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const results = await Promise.allSettled(
|
|
104
|
+
intents.map((intent) =>
|
|
105
|
+
this.upsert(chatbot_id, id_project, user_id, intent)
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
return results
|
|
109
|
+
.filter((r) => r.status === "fulfilled" && r.value)
|
|
110
|
+
.map((r) => r.value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async deleteByChatbotId(chatbot_id) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await Intent.deleteMany({ id_faq_kb: chatbot_id });
|
|
116
|
+
winston.verbose("All faq for chatbot " + chatbot_id + " deleted successfully");
|
|
117
|
+
return result;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
winston.error("(IntentsController) deleteByChatbotId error: ", err);
|
|
120
|
+
throw new AppError(500, `Error deleting intents for chatbot ${chatbot_id}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getIntents(chatbot_id) {
|
|
125
|
+
try {
|
|
126
|
+
const intents = await Intent
|
|
127
|
+
.find({ id_faq_kb: chatbot_id })
|
|
128
|
+
.lean()
|
|
129
|
+
.exec();
|
|
130
|
+
return intents;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
winston.error("(IntentsController) getIntents error: ", err);
|
|
133
|
+
throw new AppError(500, `Error finding intents with chatbot id ${chatbot_id}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const intentsController = new IntentsController();
|
|
139
|
+
module.exports = intentsController;
|
package/errorCodes.js
CHANGED
package/event/botEvent.js
CHANGED
|
@@ -135,7 +135,9 @@ class BotEvent extends EventEmitter {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
const trashedAt = chatbot.trashedAt || new Date();
|
|
139
|
+
|
|
140
|
+
await Faq.updateMany({ id_faq_kb: chatbot._id }, { trashed: true, trashedAt: trashedAt }).catch((err) => {
|
|
139
141
|
winston.error("Event faqbot.update.virtual.delete error updating faqs ", err);
|
|
140
142
|
})
|
|
141
143
|
|
|
@@ -143,6 +145,9 @@ class BotEvent extends EventEmitter {
|
|
|
143
145
|
winston.error("Error deleting webhook on chatbot deleting: ", err);
|
|
144
146
|
})
|
|
145
147
|
|
|
148
|
+
// Soft-delete subagents of the draft chatbot (+ their intents) so TTL applies
|
|
149
|
+
await trashSubagentsByParentIds([chatbot._id], trashedAt);
|
|
150
|
+
|
|
146
151
|
let publishedChatbots = await Faq_kb.find({ root_id: chatbot._id }, { _id: 1 }).catch((err) => {
|
|
147
152
|
winston.error("Event faqbot.update.virtual.delete error getting all published chatbots ", err);
|
|
148
153
|
})
|
|
@@ -161,13 +166,16 @@ class BotEvent extends EventEmitter {
|
|
|
161
166
|
for (const batch of batches) {
|
|
162
167
|
await Faq_kb.updateMany(
|
|
163
168
|
{ _id: { $in: batch } },
|
|
164
|
-
{ $set: { trashed: true, trashedAt:
|
|
169
|
+
{ $set: { trashed: true, trashedAt: trashedAt } }
|
|
165
170
|
);
|
|
166
171
|
|
|
167
172
|
await Faq.updateMany(
|
|
168
173
|
{ id_faq_kb: { $in: batch } },
|
|
169
|
-
{ $set: { trashed: true, trashedAt:
|
|
174
|
+
{ $set: { trashed: true, trashedAt: trashedAt } }
|
|
170
175
|
);
|
|
176
|
+
|
|
177
|
+
// Soft-delete subagents linked to published copies (+ their intents)
|
|
178
|
+
await trashSubagentsByParentIds(batch, trashedAt);
|
|
171
179
|
|
|
172
180
|
await sleep(sleep_ms);
|
|
173
181
|
}
|
|
@@ -175,6 +183,38 @@ class BotEvent extends EventEmitter {
|
|
|
175
183
|
}
|
|
176
184
|
})
|
|
177
185
|
|
|
186
|
+
async function trashSubagentsByParentIds(parentIds, trashedAt) {
|
|
187
|
+
if (!parentIds || parentIds.length === 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const parentIdStrings = parentIds.map(id => id.toString());
|
|
192
|
+
let subagents = await Faq_kb.find({ parent_id: { $in: parentIdStrings } }, { _id: 1 }).catch((err) => {
|
|
193
|
+
winston.error("Event faqbot.update.virtual.delete error getting subagents ", err);
|
|
194
|
+
return [];
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!subagents || subagents.length === 0) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const subagentIds = subagents.map(s => s._id);
|
|
202
|
+
|
|
203
|
+
await Faq_kb.updateMany(
|
|
204
|
+
{ _id: { $in: subagentIds } },
|
|
205
|
+
{ $set: { trashed: true, trashedAt: trashedAt } }
|
|
206
|
+
).catch((err) => {
|
|
207
|
+
winston.error("Event faqbot.update.virtual.delete error updating subagents ", err);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await Faq.updateMany(
|
|
211
|
+
{ id_faq_kb: { $in: subagentIds } },
|
|
212
|
+
{ $set: { trashed: true, trashedAt: trashedAt } }
|
|
213
|
+
).catch((err) => {
|
|
214
|
+
winston.error("Event faqbot.update.virtual.delete error updating subagent faqs ", err);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
178
218
|
function sleep(ms) {
|
|
179
219
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
180
220
|
}
|
package/jobsManager.js
CHANGED
|
@@ -72,7 +72,10 @@ class JobsManager {
|
|
|
72
72
|
|
|
73
73
|
listenWhatsappQueue(whatsappQueue) {
|
|
74
74
|
this.start("Whatsapp Queue", () => {
|
|
75
|
-
|
|
75
|
+
this.whatsappQueue = whatsappQueue;
|
|
76
|
+
if (typeof this.whatsappQueue.start === "function") {
|
|
77
|
+
this.whatsappQueue.start();
|
|
78
|
+
}
|
|
76
79
|
});
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const AppError = require('../utils/AppError');
|
|
2
|
+
const winston = require('../config/winston');
|
|
3
|
+
|
|
4
|
+
function errorHandler(err, req, res, next) {
|
|
5
|
+
if (err instanceof AppError) {
|
|
6
|
+
winston.error(err.message, err);
|
|
7
|
+
return res.status(err.status).json({
|
|
8
|
+
success: false,
|
|
9
|
+
message: err.message
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
winston.debug("err.name", err.name);
|
|
14
|
+
if (err.name === "IpDeniedError") {
|
|
15
|
+
winston.debug("IpDeniedError");
|
|
16
|
+
return res.status(401).json({ err: "error ip filter" });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const realIp = req.headers['x-forwarded-for']?.split(',')[0] || req.headers['x-real-ip'] || req.ip;
|
|
20
|
+
|
|
21
|
+
// emitted by multer when the file is too big
|
|
22
|
+
if (err.code === "LIMIT_FILE_SIZE") {
|
|
23
|
+
winston.debug("LIMIT_FILE_SIZE");
|
|
24
|
+
winston.warn(`LIMIT_FILE_SIZE on ${req.originalUrl}`, {
|
|
25
|
+
limit: process.env.MAX_UPLOAD_FILE_SIZE,
|
|
26
|
+
ip: req.ip,
|
|
27
|
+
realIp: realIp
|
|
28
|
+
});
|
|
29
|
+
return res.status(413).json({ err: "Content Too Large", limit_file_size: process.env.MAX_UPLOAD_FILE_SIZE });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (err.type === "entity.too.large" || err.name === "PayloadTooLargeError") {
|
|
33
|
+
winston.warn("Payload too large", { expected: err.expected, limit: err.limit, length: err.length });
|
|
34
|
+
return res.status(413).json({ err: "Request entity too large", limit: err.limit });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
winston.error("General error: ", err);
|
|
38
|
+
return res.status(500).json({ err: "error" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = errorHandler;
|
package/models/faq.js
CHANGED
|
@@ -192,6 +192,16 @@ FaqSchema.index(
|
|
|
192
192
|
{ partialFilterExpression: { "actions.namespace": { $exists: true } } }
|
|
193
193
|
);
|
|
194
194
|
|
|
195
|
+
FaqSchema.index(
|
|
196
|
+
{ "actions.botId": 1 },
|
|
197
|
+
{ partialFilterExpression: { "actions.botId": { $exists: true } } }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
FaqSchema.index(
|
|
201
|
+
{ "actions.botSlug": 1 },
|
|
202
|
+
{ partialFilterExpression: { "actions.botSlug": { $exists: true } } }
|
|
203
|
+
);
|
|
204
|
+
|
|
195
205
|
FaqSchema.index(
|
|
196
206
|
{ trashedAt: 1 },
|
|
197
207
|
{ expireAfterSeconds: trashExpirationTime }
|
package/models/faq_kb.js
CHANGED
|
@@ -161,6 +161,10 @@ var Faq_kbSchema = new Schema({
|
|
|
161
161
|
release_note: {
|
|
162
162
|
type: String,
|
|
163
163
|
required: false
|
|
164
|
+
},
|
|
165
|
+
parent_id: {
|
|
166
|
+
type: String,
|
|
167
|
+
required: false
|
|
164
168
|
}
|
|
165
169
|
},{
|
|
166
170
|
timestamps: true
|
|
@@ -248,6 +252,16 @@ Faq_kbSchema.index(
|
|
|
248
252
|
{ unique: true, partialFilterExpression: { slug: { $exists: true } } }
|
|
249
253
|
);
|
|
250
254
|
|
|
255
|
+
Faq_kbSchema.index(
|
|
256
|
+
{ id_project: 1, subtype: 1 },
|
|
257
|
+
{ partialFilterExpression: { subtype: { $ne: "subagent" } } }
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
Faq_kbSchema.index(
|
|
261
|
+
{ id_project: 1, parent_id: 1 },
|
|
262
|
+
{ partialFilterExpression: { parent_id: { $exists: true } } }
|
|
263
|
+
);
|
|
264
|
+
|
|
251
265
|
Faq_kbSchema.index(
|
|
252
266
|
{ trashedAt: 1 },
|
|
253
267
|
{ expireAfterSeconds: trashExpirationTime }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiledesk/tiledesk-server",
|
|
3
3
|
"description": "The Tiledesk server module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.29.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"start": "node ./bin/www",
|
|
7
7
|
"pretest": "mongodb-runner start",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"@tiledesk/tiledesk-rasa-connector": "^1.0.10",
|
|
50
50
|
"@tiledesk/tiledesk-sms-connector": "^0.1.13",
|
|
51
51
|
"@tiledesk/tiledesk-telegram-connector": "^0.1.14",
|
|
52
|
-
"@tiledesk/tiledesk-tybot-connector": "^2.
|
|
52
|
+
"@tiledesk/tiledesk-tybot-connector": "^2.3.0",
|
|
53
53
|
"@tiledesk/tiledesk-voice-twilio-connector": "^0.3.2",
|
|
54
54
|
"@tiledesk/tiledesk-vxml-connector": "^0.1.91",
|
|
55
|
-
"@tiledesk/tiledesk-whatsapp-connector": "1.0.
|
|
56
|
-
"@tiledesk/tiledesk-whatsapp-jobworker": "^0.0.
|
|
55
|
+
"@tiledesk/tiledesk-whatsapp-connector": "1.0.31",
|
|
56
|
+
"@tiledesk/tiledesk-whatsapp-jobworker": "^0.0.14",
|
|
57
57
|
"amqplib": "^0.5.5",
|
|
58
58
|
"app-root-path": "^3.0.0",
|
|
59
59
|
"bcrypt-nodejs": "0.0.3",
|