@tiledesk/tiledesk-server 2.19.3 → 2.19.5

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 CHANGED
@@ -5,6 +5,16 @@
5
5
  🚀 IN PRODUCTION 🚀
6
6
  (https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
7
7
 
8
+ # 2.19.5
9
+ - Improved management of answered and unanswered questions, duration and token consumption
10
+
11
+ # 2.19.4
12
+ - Added missing id_project in data sent for qa and single scrape
13
+
14
+ # 2.19.3
15
+ - Fixed bug on AiPrompt action with vllm models
16
+ - Updated tybot-connector to 2.1.2
17
+
8
18
  # 2.19.2
9
19
  - Improved vLLM integration in order to support multiple servers
10
20
  - Added custom_regex option for .txt and .md file uploaded in a Knowledge Base
@@ -20,6 +20,67 @@ let expireAnsweredAfterSeconds = ttlSecondsFromEnv(
20
20
  DEFAULT_ANSWERED_TTL_SEC
21
21
  );
22
22
 
23
+ const EMPTY_EMBEDDING_API_KEY = '';
24
+
25
+ function logEmbeddingApiKeyWriteAttempt(source, attemptedValue, meta = {}) {
26
+ if (attemptedValue === EMPTY_EMBEDDING_API_KEY || attemptedValue == null) {
27
+ return;
28
+ }
29
+ winston.warn('[Namespace.embedding.api_key] non-empty value blocked on ' + source, {
30
+ ...meta,
31
+ attemptedValue,
32
+ stack: new Error().stack
33
+ });
34
+ }
35
+
36
+ function sanitizeEmbeddingApiKeyObject(embedding, source, meta = {}) {
37
+ if (!embedding || typeof embedding !== 'object') {
38
+ return false;
39
+ }
40
+ if (!Object.prototype.hasOwnProperty.call(embedding, 'api_key')) {
41
+ return false;
42
+ }
43
+ if (embedding.api_key === EMPTY_EMBEDDING_API_KEY || embedding.api_key == null) {
44
+ embedding.api_key = EMPTY_EMBEDDING_API_KEY;
45
+ return false;
46
+ }
47
+ logEmbeddingApiKeyWriteAttempt(source, embedding.api_key, meta);
48
+ embedding.api_key = EMPTY_EMBEDDING_API_KEY;
49
+ return true;
50
+ }
51
+
52
+ function sanitizeEmbeddingApiKeyUpdate(update, source, meta = {}) {
53
+ if (!update || typeof update !== 'object') {
54
+ return false;
55
+ }
56
+
57
+ let changed = false;
58
+
59
+ const sanitizeContainer = (container) => {
60
+ if (!container || typeof container !== 'object') {
61
+ return;
62
+ }
63
+ if (sanitizeEmbeddingApiKeyObject(container.embedding, source, meta)) {
64
+ changed = true;
65
+ }
66
+ if (Object.prototype.hasOwnProperty.call(container, 'embedding.api_key')
67
+ && container['embedding.api_key'] !== EMPTY_EMBEDDING_API_KEY) {
68
+ logEmbeddingApiKeyWriteAttempt(source, container['embedding.api_key'], meta);
69
+ container['embedding.api_key'] = EMPTY_EMBEDDING_API_KEY;
70
+ changed = true;
71
+ }
72
+ };
73
+
74
+ sanitizeContainer(update);
75
+ ['$set', '$setOnInsert'].forEach((key) => {
76
+ if (update[key]) {
77
+ sanitizeContainer(update[key]);
78
+ }
79
+ });
80
+
81
+ return changed;
82
+ }
83
+
23
84
 
24
85
  const EngineSchema = new Schema({
25
86
  name: {
@@ -77,7 +138,12 @@ const EmbeddingSchema = new Schema({
77
138
  },
78
139
  api_key: {
79
140
  type: String,
80
- required: false
141
+ required: false,
142
+ default: EMPTY_EMBEDDING_API_KEY,
143
+ set: function (value) {
144
+ logEmbeddingApiKeyWriteAttempt('embedding.api_key setter', value);
145
+ return EMPTY_EMBEDDING_API_KEY;
146
+ }
81
147
  }
82
148
  }, {
83
149
  _id: false // This is schema is always used as an embedded object inside NamespaceSchema
@@ -120,6 +186,40 @@ const NamespaceSchema = new Schema({
120
186
  timestamps: true
121
187
  })
122
188
 
189
+ NamespaceSchema.pre('save', function (next) {
190
+ if (this.isModified('embedding.api_key')) {
191
+ const attemptedValue = this.get('embedding.api_key');
192
+ logEmbeddingApiKeyWriteAttempt('save', attemptedValue, {
193
+ namespaceId: this.id,
194
+ id_project: this.id_project
195
+ });
196
+ this.set('embedding.api_key', EMPTY_EMBEDDING_API_KEY);
197
+ }
198
+ next();
199
+ });
200
+
201
+ ['updateOne', 'updateMany', 'findOneAndUpdate'].forEach((hook) => {
202
+ NamespaceSchema.pre(hook, function (next) {
203
+ const update = this.getUpdate();
204
+ if (sanitizeEmbeddingApiKeyUpdate(update, hook, { query: this.getQuery() })) {
205
+ this.setUpdate(update);
206
+ }
207
+ next();
208
+ });
209
+ });
210
+
211
+ NamespaceSchema.pre('insertMany', function (next, docs) {
212
+ if (Array.isArray(docs)) {
213
+ docs.forEach((doc) => {
214
+ sanitizeEmbeddingApiKeyObject(doc.embedding, 'insertMany', {
215
+ namespaceId: doc.id,
216
+ id_project: doc.id_project
217
+ });
218
+ });
219
+ }
220
+ next();
221
+ });
222
+
123
223
  var KBSchema = new Schema({
124
224
  id_project: {
125
225
  type: String,
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.19.3",
4
+ "version": "2.19.5",
5
5
  "scripts": {
6
6
  "start": "node ./bin/www",
7
7
  "pretest": "mongodb-runner start",
@@ -6,295 +6,274 @@ var fastCsv = require('fast-csv');
6
6
 
7
7
  // Add a new unanswerd question
8
8
  router.post('/', async (req, res) => {
9
- try {
10
- const { namespace, question, answer, tokens, request_id } = req.body;
11
- const id_project = req.projectid;
12
-
13
- if (!namespace || !question || !answer) {
14
- return res.status(400).json({
15
- success: false,
16
- error: "Missing required parameters: namespace, question and answer"
17
- })
18
- }
19
-
20
- // Check if namespae belongs to project
21
- const isValidNamespace = await validateNamespace(id_project, namespace);
22
- if (!isValidNamespace) {
23
- return res.status(403).json({
24
- success: false,
25
- error: "Not allowed. The namespace does not belong to the current project."
26
- })
27
- }
28
-
29
- const answeredQuestion = new AnsweredQuestion({
30
- id_project,
31
- namespace,
32
- question,
33
- answer,
34
- tokens,
35
- request_id,
36
- });
37
-
38
- const savedQuestion = await answeredQuestion.save();
39
- res.status(200).json(savedQuestion);
40
-
41
- } catch (error) {
42
- winston.error('Error adding answered question:', error);
43
- res.status(500).json({
44
- success: false,
45
- error: "Error adding answered question"
46
- });
47
- }
9
+ try {
10
+
11
+ const data = req.body;
12
+ const id_project = req.projectid;
13
+
14
+ const savedQuestion = await KbQuestionService.createAnsweredQuestion(
15
+ id_project,
16
+ data
17
+ );
18
+
19
+ res.status(200).json(savedQuestion);
20
+
21
+ } catch (error) {
22
+ winston.error('Error adding answered question:', error);
23
+ res.status(error.status || 500).json({
24
+ success: false,
25
+ error: error.error || "Error adding answered question"
26
+ });
27
+ }
48
28
  })
49
29
 
50
30
  // Get all answered questions for a namespace
51
31
  router.get('/:namespace', async (req, res) => {
52
32
 
53
- try {
54
- const { namespace } = req.params;
55
- const id_project = req.projectid;
56
-
57
- if (!namespace) {
58
- return res.status(400).json({
59
- success: false,
60
- error: "Missing required parameter: namespace"
61
- })
62
- }
63
-
64
- // Check if namespace belongs to project
65
- const isValidNamespace = await validateNamespace(id_project, namespace);
66
- if (!isValidNamespace) {
67
- return res.status(403).json({
68
- success: false,
69
- error: "Not allowed. The namespace does not belong to the current project."
70
- })
71
- }
72
-
73
- const page = parseInt(req.query.page) || 0;
74
- const limit = parseInt(req.query.limit) || 20;
75
- const sortField = req.query.sortField || 'created_at';
76
- const direction = parseInt(req.query.direction) || -1;
77
-
78
- const filter = { id_project, namespace };
79
-
80
- let projection = undefined;
81
-
82
- if (req.query.search) {
83
- filter.$text = { $search: req.query.search };
84
- // Add score to projection if it's a text search
85
- projection = { score: { $meta: "textScore" } };
86
- }
87
-
88
- let sortObj;
89
- if (projection && projection.score) {
90
- sortObj = { score: { $meta: "textScore" } };
91
- } else {
92
- sortObj = { [sortField]: direction };
93
- }
94
-
95
- const questions = await AnsweredQuestion.find(filter, projection)
96
- .sort(sortObj)
97
- .skip(page * limit)
98
- .limit(limit);
99
-
100
- const count = await AnsweredQuestion.countDocuments(filter);
101
-
102
- res.status(200).json({
103
- count,
104
- questions,
105
- query: {
106
- page,
107
- limit,
108
- sortField,
109
- direction,
110
- search: req.query.search || undefined
111
- }
112
- });
113
-
114
- } catch (error) {
115
- winston.error('Error getting answered questions:', error);
116
- res.status(500).json({
117
- success: false,
118
- error: "Error getting answered questions"
119
- });
33
+ try {
34
+ const { namespace } = req.params;
35
+ const id_project = req.projectid;
36
+
37
+ if (!namespace) {
38
+ return res.status(400).json({
39
+ success: false,
40
+ error: "Missing required parameter: namespace"
41
+ })
42
+ }
43
+
44
+ // Check if namespace belongs to project
45
+ const isValidNamespace = await validateNamespace(id_project, namespace);
46
+ if (!isValidNamespace) {
47
+ return res.status(403).json({
48
+ success: false,
49
+ error: "Not allowed. The namespace does not belong to the current project."
50
+ })
120
51
  }
121
52
 
53
+ const page = parseInt(req.query.page) || 0;
54
+ const limit = parseInt(req.query.limit) || 20;
55
+ const sortField = req.query.sortField || 'createdAt';
56
+ const direction = parseInt(req.query.direction) || -1;
57
+
58
+ const filter = { id_project, namespace };
59
+
60
+ let projection = undefined;
61
+
62
+ if (req.query.search) {
63
+ filter.$text = { $search: req.query.search };
64
+ // Add score to projection if it's a text search
65
+ projection = { score: { $meta: "textScore" } };
66
+ }
67
+
68
+ let sortObj;
69
+ if (projection && projection.score) {
70
+ sortObj = { score: { $meta: "textScore" } };
71
+ } else {
72
+ sortObj = { [sortField]: direction };
73
+ }
74
+
75
+ const questions = await AnsweredQuestion.find(filter, projection)
76
+ .sort(sortObj)
77
+ .skip(page * limit)
78
+ .limit(limit);
79
+
80
+ const count = await AnsweredQuestion.countDocuments(filter);
81
+
82
+ res.status(200).json({
83
+ count,
84
+ questions,
85
+ query: {
86
+ page,
87
+ limit,
88
+ sortField,
89
+ direction,
90
+ search: req.query.search || undefined
91
+ }
92
+ });
93
+
94
+ } catch (error) {
95
+ winston.error('Error getting answered questions:', error);
96
+ res.status(500).json({
97
+ success: false,
98
+ error: "Error getting answered questions"
99
+ });
100
+ }
101
+
122
102
  })
123
103
 
124
104
  router.delete('/:id', async (req, res) => {
125
- try {
126
- const { id } = req.params;
127
- const id_project = req.projectid;
128
-
129
- const deleted = await AnsweredQuestion.findOneAndDelete({ _id: id, id_project });
130
- if (!deleted) {
131
- return res.status(404).json({
132
- success: false,
133
- error: "Question not found"
134
- });
135
- }
136
-
137
- res.status(200).json({
138
- success: true,
139
- message: "Question deleted successfully"
140
- });
141
-
142
- } catch (error) {
143
- winston.error('Error deleting answered question:', error);
144
- res.status(500).json({
145
- success: false,
146
- error: "Error deleting answered question"
147
- });
105
+ try {
106
+ const { id } = req.params;
107
+ const id_project = req.projectid;
108
+
109
+ const deleted = await AnsweredQuestion.findOneAndDelete({ _id: id, id_project });
110
+ if (!deleted) {
111
+ return res.status(404).json({
112
+ success: false,
113
+ error: "Question not found"
114
+ });
148
115
  }
116
+
117
+ res.status(200).json({
118
+ success: true,
119
+ message: "Question deleted successfully"
120
+ });
121
+
122
+ } catch (error) {
123
+ winston.error('Error deleting answered question:', error);
124
+ res.status(500).json({
125
+ success: false,
126
+ error: "Error deleting answered question"
127
+ });
128
+ }
149
129
  })
150
130
 
151
131
  router.delete('/namespace/:namespace', async (req, res) => {
152
- try {
153
- const { namespace } = req.params;
154
- const id_project = req.projectid;
155
-
156
- // Check if namespace belongs to project
157
- const isValidNamespace = await validateNamespace(id_project, namespace);
158
- if (!isValidNamespace) {
159
- return res.status(403).json({
160
- success: false,
161
- error: "Not allowed. The namespace does not belong to the current project."
162
- });
163
- }
164
-
165
- const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
166
- res.status(200).json({
167
- success: true,
168
- count: result.deletedCount,
169
- message: "All questions deleted successfully"
170
- });
171
-
172
- } catch (error) {
173
- winston.error('Error deleting answered questions:', error);
174
- res.status(500).json({
175
- success: false,
176
- error: "Error deleting answered questions"
177
- });
132
+ try {
133
+ const { namespace } = req.params;
134
+ const id_project = req.projectid;
135
+
136
+ // Check if namespace belongs to project
137
+ const isValidNamespace = await validateNamespace(id_project, namespace);
138
+ if (!isValidNamespace) {
139
+ return res.status(403).json({
140
+ success: false,
141
+ error: "Not allowed. The namespace does not belong to the current project."
142
+ });
178
143
  }
144
+
145
+ const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
146
+ res.status(200).json({
147
+ success: true,
148
+ count: result.deletedCount,
149
+ message: "All questions deleted successfully"
150
+ });
151
+
152
+ } catch (error) {
153
+ winston.error('Error deleting answered questions:', error);
154
+ res.status(500).json({
155
+ success: false,
156
+ error: "Error deleting answered questions"
157
+ });
158
+ }
179
159
  })
180
160
 
181
161
  router.get('/count/:namespace', async (req, res) => {
182
- try {
183
- const { namespace } = req.params;
184
- const id_project = req.projectid;
185
-
186
- if (!namespace) {
187
- return res.status(400).json({
188
- success: false,
189
- error: "Missing required parameter: namespace"
190
- });
191
- }
192
-
193
- // Check if namespace belongs to project
194
- const isValidNamespace = await validateNamespace(id_project, namespace);
195
- if (!isValidNamespace) {
196
- return res.status(403).json({
197
- success: false,
198
- error: "Not allowed. The namespace does not belong to the current project."
199
- });
200
- }
201
-
202
- const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
203
- res.status(200).json({ count });
204
-
205
- } catch (error) {
206
- winston.error('Error counting answered questions:', error);
207
- res.status(500).json({
208
- success: false,
209
- error: "Error counting answered questions"
210
- });
162
+ try {
163
+ const { namespace } = req.params;
164
+ const id_project = req.projectid;
165
+
166
+ if (!namespace) {
167
+ return res.status(400).json({
168
+ success: false,
169
+ error: "Missing required parameter: namespace"
170
+ });
171
+ }
172
+
173
+ // Check if namespace belongs to project
174
+ const isValidNamespace = await validateNamespace(id_project, namespace);
175
+ if (!isValidNamespace) {
176
+ return res.status(403).json({
177
+ success: false,
178
+ error: "Not allowed. The namespace does not belong to the current project."
179
+ });
211
180
  }
181
+
182
+ const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
183
+ res.status(200).json({ count });
184
+
185
+ } catch (error) {
186
+ winston.error('Error counting answered questions:', error);
187
+ res.status(500).json({
188
+ success: false,
189
+ error: "Error counting answered questions"
190
+ });
191
+ }
212
192
  })
213
193
 
214
194
  router.get('/:namespace/export', async (req, res) => {
215
- try {
216
- const { namespace } = req.params;
217
- const id_project = req.projectid;
218
- const mode = String(req.query.mode || 'csv').toLowerCase();
219
-
220
- if (mode !== 'csv' && mode !== 'json') {
221
- return res.status(400).json({
222
- success: false,
223
- error: 'Invalid format. Use mode=json or mode=csv'
224
- });
225
- }
226
-
227
- const isValidNamespace = await validateNamespace(id_project, namespace);
228
- if (!isValidNamespace) {
229
- return res.status(403).json({
230
- success: false,
231
- error: "Not allowed. The namespace does not belong to the current project."
232
- });
233
- }
234
-
235
- const questions = await AnsweredQuestion.find({ id_project, namespace })
236
- .sort({ createdAt: -1 })
237
- .lean();
238
-
239
- const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
240
-
241
- if (mode === 'json') {
242
- const questionsJson = questions.map((q) => {
243
- const { __v, updatedAt, ...doc } = q;
244
- return doc;
245
- });
246
- const payload = {
247
- namespace,
248
- exportedAt: new Date().toISOString(),
249
- count: questionsJson.length,
250
- questions: questionsJson
251
- };
252
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
253
- res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.json"`);
254
- return res.send(JSON.stringify(payload, null, 2));
255
- }
256
-
257
- res.setHeader('Content-Type', 'text/csv; charset=utf-8');
258
- res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.csv"`);
259
-
260
- const csvStream = fastCsv.format({ headers: true });
261
- csvStream.pipe(res);
262
-
263
- for (const q of questions) {
264
- csvStream.write({
265
- id: String(q._id),
266
- namespace: q.namespace,
267
- question: q.question,
268
- answer: q.answer,
269
- tokens: q.tokens != null ? q.tokens : '',
270
- request_id: q.request_id || '',
271
- createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
272
- });
273
- }
274
- csvStream.end();
275
- } catch (error) {
276
- winston.error('Error exporting answered questions:', error);
277
- if (!res.headersSent) {
278
- res.status(500).json({
279
- success: false,
280
- error: "Error exporting answered questions"
281
- });
282
- }
195
+ try {
196
+ const { namespace } = req.params;
197
+ const id_project = req.projectid;
198
+ const mode = String(req.query.mode || 'csv').toLowerCase();
199
+
200
+ if (mode !== 'csv' && mode !== 'json') {
201
+ return res.status(400).json({
202
+ success: false,
203
+ error: 'Invalid format. Use mode=json or mode=csv'
204
+ });
205
+ }
206
+
207
+ const isValidNamespace = await validateNamespace(id_project, namespace);
208
+ if (!isValidNamespace) {
209
+ return res.status(403).json({
210
+ success: false,
211
+ error: "Not allowed. The namespace does not belong to the current project."
212
+ });
283
213
  }
214
+
215
+ const questions = await AnsweredQuestion.find({ id_project, namespace })
216
+ .sort({ createdAt: -1 })
217
+ .lean();
218
+
219
+ const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
220
+
221
+ if (mode === 'json') {
222
+ const questionsJson = questions.map((q) => {
223
+ const { __v, updatedAt, ...doc } = q;
224
+ return doc;
225
+ });
226
+ const payload = {
227
+ namespace,
228
+ exportedAt: new Date().toISOString(),
229
+ count: questionsJson.length,
230
+ questions: questionsJson
231
+ };
232
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
233
+ res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.json"`);
234
+ return res.send(JSON.stringify(payload, null, 2));
235
+ }
236
+
237
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
238
+ res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.csv"`);
239
+
240
+ const csvStream = fastCsv.format({ headers: true });
241
+ csvStream.pipe(res);
242
+
243
+ for (const q of questions) {
244
+ csvStream.write({
245
+ id: String(q._id),
246
+ namespace: q.namespace,
247
+ question: q.question,
248
+ answer: q.answer,
249
+ tokens: q.tokens != null ? q.tokens : '',
250
+ request_id: q.request_id || '',
251
+ createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
252
+ });
253
+ }
254
+ csvStream.end();
255
+ } catch (error) {
256
+ winston.error('Error exporting answered questions:', error);
257
+ if (!res.headersSent) {
258
+ res.status(500).json({
259
+ success: false,
260
+ error: "Error exporting answered questions"
261
+ });
262
+ }
263
+ }
284
264
  });
285
265
 
286
266
  // Helper function to validate namespace
287
267
  async function validateNamespace(id_project, namespace_id) {
288
- try {
289
- const namespace = await Namespace.findOne({
290
- id_project: id_project,
291
- id: namespace_id
292
- });
293
- return !!namespace;
294
- } catch (err) {
295
- winston.error('validate namespace error: ', err);
296
- throw err;
297
- }
268
+ try {
269
+ const namespace = await Namespace.findOne({
270
+ id_project: id_project,
271
+ id: namespace_id
272
+ });
273
+ return !!namespace;
274
+ } catch (err) {
275
+ winston.error('validate namespace error: ', err);
276
+ throw err;
277
+ }
298
278
  }
299
-
300
- module.exports = router;
279
+ module.exports = router;