@tiledesk/tiledesk-server 2.19.4 → 2.19.6

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.
@@ -3,338 +3,319 @@ const router = express.Router();
3
3
  const { Namespace, UnansweredQuestion } = require('../models/kb_setting');
4
4
  var winston = require('../config/winston');
5
5
  var fastCsv = require('fast-csv');
6
+ const kbQuestionService = require('../services/kbQuestionService');
6
7
 
7
8
  // Add a new unanswered question
8
9
  router.post('/', async (req, res) => {
9
- try {
10
- const { namespace, question, request_id, sender } = req.body;
11
- const id_project = req.projectid;
12
-
13
- if (!namespace || !question) {
14
- return res.status(400).json({
15
- success: false,
16
- error: "Missing required parameters: namespace and question"
17
- });
18
- }
19
-
20
- // Check if namespace 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 unansweredQuestion = new UnansweredQuestion({
30
- id_project,
31
- namespace,
32
- question,
33
- request_id,
34
- sender
35
- });
36
-
37
- const savedQuestion = await unansweredQuestion.save();
38
- res.status(200).json(savedQuestion);
39
-
40
- } catch (error) {
41
- winston.error('Error adding unanswered question:', error);
42
- res.status(500).json({
43
- success: false,
44
- error: "Error adding unanswered question"
45
- });
46
- }
10
+ try {
11
+ const data = req.body;
12
+ const id_project = req.projectid;
13
+
14
+ const savedQuestion = await kbQuestionService.createUnansweredQuestion(
15
+ id_project,
16
+ data
17
+ );
18
+
19
+ res.status(200).json(savedQuestion);
20
+
21
+ } catch (error) {
22
+ winston.error('Error adding unanswered question:', error);
23
+ res.status(error.status || 500).json({
24
+ success: false,
25
+ error: error.error || "Error adding unanswered question"
26
+ });
27
+ }
47
28
  });
48
29
 
49
30
  // Get all unanswered questions for a namespace
50
31
  router.get('/:namespace', async (req, res) => {
51
- try {
52
- const { namespace } = req.params;
53
- const id_project = req.projectid;
54
-
55
- if (!namespace) {
56
- return res.status(400).json({
57
- success: false,
58
- error: "Missing required parameter: namespace"
59
- });
60
- }
61
-
62
- // Check if namespace belongs to project
63
- const isValidNamespace = await validateNamespace(id_project, namespace);
64
- if (!isValidNamespace) {
65
- return res.status(403).json({
66
- success: false,
67
- error: "Not allowed. The namespace does not belong to the current project."
68
- });
69
- }
70
-
71
- const page = parseInt(req.query.page) || 0;
72
- const limit = parseInt(req.query.limit) || 20;
73
- const sortField = req.query.sortField || 'createdAt';
74
- const direction = parseInt(req.query.direction) || -1;
75
-
76
- const filter = { id_project, namespace };
77
-
78
- let projection = undefined;
79
-
80
- if (req.query.search) {
81
- filter.$text = { $search: req.query.search };
82
- // Add score to projection if it's a text search
83
- projection = { score: { $meta: "textScore" } };
84
- }
85
-
86
- let sortObj;
87
- if (projection && projection.score) {
88
- sortObj = { score: { $meta: "textScore" } };
89
- } else {
90
- sortObj = { [sortField]: direction };
91
- }
92
-
93
- const questions = await UnansweredQuestion.find(filter, projection)
94
- .sort(sortObj)
95
- .skip(page * limit)
96
- .limit(limit);
97
-
98
- const count = await UnansweredQuestion.countDocuments(filter);
99
-
100
- res.status(200).json({
101
- count,
102
- questions,
103
- query: {
104
- page,
105
- limit,
106
- sortField,
107
- direction,
108
- search: req.query.search || undefined
109
- }
110
- });
111
-
112
- } catch (error) {
113
- winston.error('Error getting unanswered questions:', error);
114
- res.status(500).json({
115
- success: false,
116
- error: "Error getting unanswered questions"
117
- });
32
+ try {
33
+ const { namespace } = req.params;
34
+ const id_project = req.projectid;
35
+
36
+ if (!namespace) {
37
+ return res.status(400).json({
38
+ success: false,
39
+ error: "Missing required parameter: namespace"
40
+ });
41
+ }
42
+
43
+ // Check if namespace belongs to project
44
+ const isValidNamespace = await validateNamespace(id_project, namespace);
45
+ if (!isValidNamespace) {
46
+ return res.status(403).json({
47
+ success: false,
48
+ error: "Not allowed. The namespace does not belong to the current project."
49
+ });
50
+ }
51
+
52
+ const page = parseInt(req.query.page) || 0;
53
+ const limit = parseInt(req.query.limit) || 20;
54
+ const sortField = req.query.sortField || 'createdAt';
55
+ const direction = parseInt(req.query.direction) || -1;
56
+
57
+ const filter = { id_project, namespace };
58
+
59
+ let projection = undefined;
60
+
61
+ if (req.query.search) {
62
+ filter.$text = { $search: req.query.search };
63
+ // Add score to projection if it's a text search
64
+ projection = { score: { $meta: "textScore" } };
118
65
  }
66
+
67
+ let sortObj;
68
+ if (projection && projection.score) {
69
+ sortObj = { score: { $meta: "textScore" } };
70
+ } else {
71
+ sortObj = { [sortField]: direction };
72
+ }
73
+
74
+ const questions = await UnansweredQuestion.find(filter, projection)
75
+ .sort(sortObj)
76
+ .skip(page * limit)
77
+ .limit(limit);
78
+
79
+ const count = await UnansweredQuestion.countDocuments(filter);
80
+
81
+ res.status(200).json({
82
+ count,
83
+ questions,
84
+ query: {
85
+ page,
86
+ limit,
87
+ sortField,
88
+ direction,
89
+ search: req.query.search || undefined
90
+ }
91
+ });
92
+
93
+ } catch (error) {
94
+ winston.error('Error getting unanswered questions:', error);
95
+ res.status(500).json({
96
+ success: false,
97
+ error: "Error getting unanswered questions"
98
+ });
99
+ }
119
100
  });
120
101
 
121
102
  // Delete a specific unanswered question
122
103
  router.delete('/:id', async (req, res) => {
123
- try {
124
- const { id } = req.params;
125
- const id_project = req.projectid;
126
-
127
- const deleted = await UnansweredQuestion.findOneAndDelete({ _id: id, id_project });
128
- if (!deleted) {
129
- return res.status(404).json({
130
- success: false,
131
- error: "Question not found"
132
- });
133
- }
134
-
135
- res.status(200).json({
136
- success: true,
137
- message: "Question deleted successfully"
138
- });
139
-
140
- } catch (error) {
141
- winston.error('Error deleting unanswered question:', error);
142
- res.status(500).json({
143
- success: false,
144
- error: "Error deleting unanswered question"
145
- });
104
+ try {
105
+ const { id } = req.params;
106
+ const id_project = req.projectid;
107
+
108
+ const deleted = await UnansweredQuestion.findOneAndDelete({ _id: id, id_project });
109
+ if (!deleted) {
110
+ return res.status(404).json({
111
+ success: false,
112
+ error: "Question not found"
113
+ });
146
114
  }
115
+
116
+ res.status(200).json({
117
+ success: true,
118
+ message: "Question deleted successfully"
119
+ });
120
+
121
+ } catch (error) {
122
+ winston.error('Error deleting unanswered question:', error);
123
+ res.status(500).json({
124
+ success: false,
125
+ error: "Error deleting unanswered question"
126
+ });
127
+ }
147
128
  });
148
129
 
149
130
  // Delete all unanswered questions for a namespace
150
131
  router.delete('/namespace/:namespace', async (req, res) => {
151
- try {
152
- const { namespace } = req.params;
153
- const id_project = req.projectid;
154
-
155
- // Check if namespace belongs to project
156
- const isValidNamespace = await validateNamespace(id_project, namespace);
157
- if (!isValidNamespace) {
158
- return res.status(403).json({
159
- success: false,
160
- error: "Not allowed. The namespace does not belong to the current project."
161
- });
162
- }
163
-
164
- const result = await UnansweredQuestion.deleteMany({ id_project, namespace });
165
- res.status(200).json({
166
- success: true,
167
- count: result.deletedCount,
168
- message: "All questions deleted successfully"
169
- });
170
-
171
- } catch (error) {
172
- winston.error('Error deleting unanswered questions:', error);
173
- res.status(500).json({
174
- success: false,
175
- error: "Error deleting unanswered questions"
176
- });
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
+ });
177
143
  }
144
+
145
+ const result = await UnansweredQuestion.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 unanswered questions:', error);
154
+ res.status(500).json({
155
+ success: false,
156
+ error: "Error deleting unanswered questions"
157
+ });
158
+ }
178
159
  });
179
160
 
180
161
  // Update an unanswered question
181
162
  router.put('/:id', async (req, res) => {
182
- try {
183
- const { id } = req.params;
184
- const { question } = req.body;
185
- const id_project = req.projectid;
186
-
187
- if (!question) {
188
- return res.status(400).json({
189
- success: false,
190
- error: "Missing required parameter: question"
191
- });
192
- }
193
-
194
- const updatedQuestion = await UnansweredQuestion.findOneAndUpdate(
195
- { _id: id, id_project },
196
- { question },
197
- { new: true }
198
- );
199
-
200
- if (!updatedQuestion) {
201
- return res.status(404).json({
202
- success: false,
203
- error: "Question not found"
204
- });
205
- }
206
-
207
- res.status(200).json(updatedQuestion);
208
-
209
- } catch (error) {
210
- winston.error('Error updating unanswered question:', error);
211
- res.status(500).json({
212
- success: false,
213
- error: "Error updating unanswered question"
214
- });
163
+ try {
164
+ const { id } = req.params;
165
+ const { question } = req.body;
166
+ const id_project = req.projectid;
167
+
168
+ if (!question) {
169
+ return res.status(400).json({
170
+ success: false,
171
+ error: "Missing required parameter: question"
172
+ });
173
+ }
174
+
175
+ const updatedQuestion = await UnansweredQuestion.findOneAndUpdate(
176
+ { _id: id, id_project },
177
+ { question },
178
+ { new: true }
179
+ );
180
+
181
+ if (!updatedQuestion) {
182
+ return res.status(404).json({
183
+ success: false,
184
+ error: "Question not found"
185
+ });
215
186
  }
187
+
188
+ res.status(200).json(updatedQuestion);
189
+
190
+ } catch (error) {
191
+ winston.error('Error updating unanswered question:', error);
192
+ res.status(500).json({
193
+ success: false,
194
+ error: "Error updating unanswered question"
195
+ });
196
+ }
216
197
  });
217
198
 
218
199
  // Count unanswered questions for a namespace
219
200
  router.get('/count/:namespace', async (req, res) => {
220
- try {
221
- const { namespace } = req.params;
222
- const id_project = req.projectid;
223
-
224
- if (!namespace) {
225
- return res.status(400).json({
226
- success: false,
227
- error: "Missing required parameter: namespace"
228
- });
229
- }
230
-
231
- // Check if namespace belongs to project
232
- const isValidNamespace = await validateNamespace(id_project, namespace);
233
- if (!isValidNamespace) {
234
- return res.status(403).json({
235
- success: false,
236
- error: "Not allowed. The namespace does not belong to the current project."
237
- });
238
- }
239
-
240
- const count = await UnansweredQuestion.countDocuments({
241
- id_project,
242
- namespace
243
- });
244
-
245
- res.status(200).json({ count });
246
-
247
- } catch (error) {
248
- winston.error('Error counting unanswered questions:', error);
249
- res.status(500).json({
250
- success: false,
251
- error: "Error counting unanswered questions"
252
- });
201
+ try {
202
+ const { namespace } = req.params;
203
+ const id_project = req.projectid;
204
+
205
+ if (!namespace) {
206
+ return res.status(400).json({
207
+ success: false,
208
+ error: "Missing required parameter: namespace"
209
+ });
210
+ }
211
+
212
+ // Check if namespace belongs to project
213
+ const isValidNamespace = await validateNamespace(id_project, namespace);
214
+ if (!isValidNamespace) {
215
+ return res.status(403).json({
216
+ success: false,
217
+ error: "Not allowed. The namespace does not belong to the current project."
218
+ });
253
219
  }
220
+
221
+ const count = await UnansweredQuestion.countDocuments({
222
+ id_project,
223
+ namespace
224
+ });
225
+
226
+ res.status(200).json({ count });
227
+
228
+ } catch (error) {
229
+ winston.error('Error counting unanswered questions:', error);
230
+ res.status(500).json({
231
+ success: false,
232
+ error: "Error counting unanswered questions"
233
+ });
234
+ }
254
235
  });
255
236
 
256
237
  router.get('/:namespace/export', async (req, res) => {
257
- try {
258
- const { namespace } = req.params;
259
- const id_project = req.projectid;
260
- const mode = String(req.query.mode || 'csv').toLowerCase();
261
-
262
- if (mode !== 'csv' && mode !== 'json') {
263
- return res.status(400).json({
264
- success: false,
265
- error: 'Invalid format. Use mode=json or mode=csv'
266
- });
267
- }
268
-
269
- const isValidNamespace = await validateNamespace(id_project, namespace);
270
- if (!isValidNamespace) {
271
- return res.status(403).json({
272
- success: false,
273
- error: "Not allowed. The namespace does not belong to the current project."
274
- });
275
- }
276
-
277
- const questions = await UnansweredQuestion.find({ id_project, namespace })
278
- .sort({ createdAt: -1 })
279
- .lean();
280
-
281
- const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
282
-
283
- if (mode === 'json') {
284
- const questionsJson = questions.map((q) => {
285
- const { __v, updatedAt, ...doc } = q;
286
- return doc;
287
- });
288
- const payload = {
289
- namespace,
290
- exportedAt: new Date().toISOString(),
291
- count: questionsJson.length,
292
- questions: questionsJson
293
- };
294
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
295
- res.setHeader('Content-Disposition', `attachment; filename="unanswered-${safeFilename}.json"`);
296
- return res.send(JSON.stringify(payload, null, 2));
297
- }
298
-
299
- res.setHeader('Content-Type', 'text/csv; charset=utf-8');
300
- res.setHeader('Content-Disposition', `attachment; filename="unanswered-${safeFilename}.csv"`);
301
-
302
- const csvStream = fastCsv.format({ headers: true });
303
- csvStream.pipe(res);
304
-
305
- for (const q of questions) {
306
- csvStream.write({
307
- id: String(q._id),
308
- namespace: q.namespace,
309
- question: q.question,
310
- request_id: q.request_id || '',
311
- createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
312
- });
313
- }
314
- csvStream.end();
315
- } catch (error) {
316
- winston.error('Error exporting unanswered questions:', error);
317
- if (!res.headersSent) {
318
- res.status(500).json({
319
- success: false,
320
- error: "Error exporting unanswered questions"
321
- });
322
- }
238
+ try {
239
+ const { namespace } = req.params;
240
+ const id_project = req.projectid;
241
+ const mode = String(req.query.mode || 'csv').toLowerCase();
242
+
243
+ if (mode !== 'csv' && mode !== 'json') {
244
+ return res.status(400).json({
245
+ success: false,
246
+ error: 'Invalid format. Use mode=json or mode=csv'
247
+ });
248
+ }
249
+
250
+ const isValidNamespace = await validateNamespace(id_project, namespace);
251
+ if (!isValidNamespace) {
252
+ return res.status(403).json({
253
+ success: false,
254
+ error: "Not allowed. The namespace does not belong to the current project."
255
+ });
323
256
  }
257
+
258
+ const questions = await UnansweredQuestion.find({ id_project, namespace })
259
+ .sort({ createdAt: -1 })
260
+ .lean();
261
+
262
+ const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
263
+
264
+ if (mode === 'json') {
265
+ const questionsJson = questions.map((q) => {
266
+ const { __v, updatedAt, ...doc } = q;
267
+ return doc;
268
+ });
269
+ const payload = {
270
+ namespace,
271
+ exportedAt: new Date().toISOString(),
272
+ count: questionsJson.length,
273
+ questions: questionsJson
274
+ };
275
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
276
+ res.setHeader('Content-Disposition', `attachment; filename="unanswered-${safeFilename}.json"`);
277
+ return res.send(JSON.stringify(payload, null, 2));
278
+ }
279
+
280
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
281
+ res.setHeader('Content-Disposition', `attachment; filename="unanswered-${safeFilename}.csv"`);
282
+
283
+ const csvStream = fastCsv.format({ headers: true });
284
+ csvStream.pipe(res);
285
+
286
+ for (const q of questions) {
287
+ csvStream.write({
288
+ id: String(q._id),
289
+ namespace: q.namespace,
290
+ question: q.question,
291
+ request_id: q.request_id || '',
292
+ createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
293
+ });
294
+ }
295
+ csvStream.end();
296
+ } catch (error) {
297
+ winston.error('Error exporting unanswered questions:', error);
298
+ if (!res.headersSent) {
299
+ res.status(500).json({
300
+ success: false,
301
+ error: "Error exporting unanswered questions"
302
+ });
303
+ }
304
+ }
324
305
  });
325
306
 
326
307
  // Helper function to validate namespace
327
308
  async function validateNamespace(id_project, namespace_id) {
328
- try {
329
- const namespace = await Namespace.findOne({
330
- id_project: id_project,
331
- id: namespace_id
332
- });
333
- return !!namespace; // return true if namespace exists, false otherwise
334
- } catch (err) {
335
- winston.error("validate namespace error: ", err);
336
- throw err;
337
- }
309
+ try {
310
+ const namespace = await Namespace.findOne({
311
+ id_project: id_project,
312
+ id: namespace_id
313
+ });
314
+ return !!namespace; // return true if namespace exists, false otherwise
315
+ } catch (err) {
316
+ winston.error("validate namespace error: ", err);
317
+ throw err;
318
+ }
338
319
  }
339
320
 
340
321
  module.exports = router;
package/routes/webhook.js CHANGED
@@ -199,8 +199,7 @@ router.post('/kb/reindex', async (req, res) => {
199
199
  json.engine = namespace.engine || (namespace.hybrid ? default_engine_hybrid : default_engine);
200
200
  json.hybrid = namespace.hybrid;
201
201
 
202
- let embedding = namespace.embedding || Object.assign({}, default_embedding);
203
- embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
202
+ let embedding = aiManager.normalizeEmbedding(namespace.embedding);
204
203
  json.embedding = embedding;
205
204
 
206
205
  const situated_context_obj = aiManager.normalizeSituatedContext(kb.situated_context);