@tiledesk/tiledesk-server 2.19.4 → 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,9 @@
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
+
8
11
  # 2.19.4
9
12
  - Added missing id_project in data sent for qa and single scrape
10
13
 
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.4",
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;