@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.
@@ -21,6 +21,7 @@ const default_engine_hybrid = require('../config/kb/engine.hybrid');
21
21
  const default_embedding = require('../config/kb/embedding');
22
22
  const integrationService = require('./integrationService');
23
23
  const situatedContext = require('../config/kb/situatedContext');
24
+ const { MODELS_MULTIPLIER } = require('../utils/aiUtils');
24
25
 
25
26
  // Job managers
26
27
  let jobManager = new JobManager(AMQP_MANAGER_URL, {
@@ -94,8 +95,7 @@ class AiManager {
94
95
  this.saveBulk(operations, kbs, namespace.id_project, namespace.id).then( async (result) => {
95
96
  let hybrid = namespace.hybrid;
96
97
  let engine = namespace.engine || default_engine;
97
- let embedding = namespace.embedding || Object.assign({}, default_embedding);
98
- embedding.api_key = process.env.EMBEDDING_API_KEY || process.env.GPTKEY;
98
+ let embedding = this.normalizeEmbedding(namespace.embedding);
99
99
 
100
100
  let situated_context;
101
101
  if (options.situated_context) {
@@ -145,7 +145,7 @@ class AiManager {
145
145
  namespace: namespace.id,
146
146
  refresh_rate: options.refresh_rate,
147
147
  engine: namespace.engine,
148
- embedding: namespace.embedding,
148
+ embedding: this.normalizeEmbedding(namespace.embedding),
149
149
  hybrid: namespace.hybrid,
150
150
  ...(options.situated_context && { situated_context: options.situated_context }),
151
151
  }
@@ -448,7 +448,7 @@ class AiManager {
448
448
  if (err) {
449
449
  winston.error("deleteSitemap: deleteSchedule error for kb " + kb._id, err);
450
450
  }
451
- console.log("deleteSitemap: deleteSchedule result for kb " + kb._id, result);
451
+ winston.info("deleteSitemap: deleteSchedule result for kb " + kb._id, result);
452
452
  resolve(result);
453
453
  });
454
454
  });
@@ -514,25 +514,6 @@ class AiManager {
514
514
  return true;
515
515
  }
516
516
 
517
- // from webhook
518
- // async scheduleScrape(resources) {
519
-
520
- // let scheduler = new Scheduler({ jobManager: jobManager });
521
-
522
- // resources.forEach(r => {
523
- // winston.debug("(Webhook) Schedule job with following data: ", r);
524
- // scheduler.trainSchedule(r, async (err, result) => {
525
- // if (err) {
526
- // winston.error("Scheduling error: ", err);
527
- // } else {
528
- // winston.info("Scheduling result: ", result);
529
- // }
530
- // });
531
- // })
532
-
533
- // return true;
534
- // }
535
-
536
517
  async startScrape(data) {
537
518
 
538
519
  if (!data.gptkey) {
@@ -615,6 +596,16 @@ class AiManager {
615
596
  })
616
597
  }
617
598
 
599
+ normalizeEmbedding(embedding) {
600
+ const normalizedEmbedding = (embedding && typeof embedding.toObject === 'function')
601
+ ? embedding.toObject()
602
+ : (embedding || default_embedding);
603
+ return {
604
+ ...normalizedEmbedding,
605
+ api_key: process.env.EMBEDDING_API_KEY || process.env.GPTKEY
606
+ };
607
+ }
608
+
618
609
  normalizeSituatedContext(enable = false) {
619
610
  situatedContext.enable = enable;
620
611
  return situatedContext.enable
@@ -625,6 +616,33 @@ class AiManager {
625
616
  : undefined;
626
617
  }
627
618
 
619
+ async getTrackingTokens({ qaResult, publicKey, model, obj, quoteManager, project }) {
620
+ if (publicKey !== true) {
621
+ return qaResult.prompt_token_size;
622
+ }
623
+ let modelKey;
624
+ if (typeof model === 'string') {
625
+ modelKey = model;
626
+ } else if (model && typeof model.name === 'string') {
627
+ modelKey = model.name;
628
+ }
629
+ let multiplier = MODELS_MULTIPLIER[modelKey];
630
+ if (!multiplier) {
631
+ multiplier = 1;
632
+ winston.info("No multiplier found for AI model (qa) " + modelKey);
633
+ }
634
+ obj.multiplier = multiplier;
635
+ obj.tokens = qaResult.prompt_token_size;
636
+ const incremented_key = await quoteManager.incrementTokenCount(project, obj);
637
+ winston.verbose("incremented_key: ", incremented_key);
638
+ const trackingTokens = obj.tokens * obj.multiplier;
639
+ if (qaResult != null && typeof qaResult.prompt_token_size === 'number') {
640
+ qaResult.legacy_prompt_token_size = qaResult.prompt_token_size;
641
+ qaResult.prompt_token_size = trackingTokens;
642
+ }
643
+ return trackingTokens;
644
+ }
645
+
628
646
  }
629
647
 
630
648
  const aiManager = new AiManager();
@@ -0,0 +1,98 @@
1
+ const { Namespace, AnsweredQuestion, UnansweredQuestion } = require('../models/kb_setting');
2
+ const winston = require('../config/winston');
3
+
4
+ class KbQuestionService {
5
+
6
+ constructor() {}
7
+
8
+ async createAnsweredQuestion(id_project, data) {
9
+ try {
10
+ const { namespace, question, answer, tokens, request_id } = data;
11
+
12
+ if (!namespace || !question || !answer) {
13
+ throw new Error({ status: 422, error: 'Missing required parameters: namespace, question and answer' });
14
+ }
15
+
16
+ const isValidNamespace = await this.validateNamespace(id_project, namespace);
17
+ if (!isValidNamespace) {
18
+ throw new Error({ status: 403, error: 'Not allowed. The namespace does not belong to the current project.' });
19
+ }
20
+
21
+ const answeredQuestion = new AnsweredQuestion({
22
+ id_project,
23
+ namespace,
24
+ question,
25
+ answer,
26
+ tokens,
27
+ request_id,
28
+ });
29
+
30
+ return await answeredQuestion.save();
31
+
32
+ } catch (error) {
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ async createUnansweredQuestion(id_project, data) {
38
+ try {
39
+ const { namespace, question, request_id, sender } = data;
40
+
41
+ if (!namespace || !question) {
42
+ throw new Error({ status: 422, error: 'Missing required parameters: namespace and question' });
43
+ }
44
+
45
+ const isValidNamespace = await this.validateNamespace(id_project, namespace);
46
+ if (!isValidNamespace) {
47
+ throw new Error({ status: 403, error: 'Not allowed. The namespace does not belong to the current project.' });
48
+ }
49
+
50
+ const unansweredQuestion = new UnansweredQuestion({
51
+ id_project,
52
+ namespace,
53
+ question,
54
+ request_id,
55
+ sender,
56
+ });
57
+ return await unansweredQuestion.save();
58
+
59
+ } catch (error) {
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ async validateNamespace(id_project, namespace_id) {
65
+ try {
66
+ const namespace = await Namespace.findOne({
67
+ id_project: id_project,
68
+ id: namespace_id
69
+ });
70
+ return !!namespace;
71
+ } catch (error) {
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async trackKbQaResult(id_project, { namespace, question }, answer, tokens) {
77
+ try {
78
+ if (answer.success === true) {
79
+ await this.createAnsweredQuestion(id_project, {
80
+ namespace,
81
+ question,
82
+ answer: answer.answer,
83
+ tokens,
84
+ });
85
+ } else if (answer.success === false) {
86
+ await this.createUnansweredQuestion(id_project, { namespace, question });
87
+ } else {
88
+ winston.warn('qa answer.success is not true or false: ', answer.success);
89
+ }
90
+ } catch (error) {
91
+ winston.error('qa err: ', error);
92
+ }
93
+ }
94
+
95
+ }
96
+
97
+ const kbQuestionService = new KbQuestionService();
98
+ module.exports = kbQuestionService;
package/test/kbRoute.js CHANGED
@@ -8,6 +8,7 @@ process.env.PINECONE_TYPE = "serverless";
8
8
  process.env.PINECONE_INDEX_HYBRID = "test-index-hybrid";
9
9
  process.env.PINECONE_TYPE_HYBRID = "serverless";
10
10
  process.env.ADMIN_EMAIL = "admin@tiledesk.com";
11
+ process.env.KB_ENDPOINT_TRAIN = "http://kb-train.test";
11
12
 
12
13
  var userService = require('../services/userService');
13
14
  var projectService = require('../services/projectService');
@@ -27,6 +28,8 @@ const path = require('path');
27
28
  const mongoose = require('mongoose');
28
29
  const nock = require('nock');
29
30
  const faq = require('../models/faq');
31
+ const { Namespace } = require('../models/kb_setting');
32
+ const default_engine = require('../config/kb/engine');
30
33
 
31
34
 
32
35
  // chai.config.includeStack = true;
@@ -85,6 +88,7 @@ describe('KbRoute', () => {
85
88
  expect(res.body[0].embedding.provider).to.equal('openai')
86
89
  expect(res.body[0].embedding.name).to.equal('text-embedding-ada-002')
87
90
  expect(res.body[0].embedding.dimension).to.equal(1536)
91
+ expect(res.body[0].embedding.api_key).to.equal('')
88
92
 
89
93
  done();
90
94
  })
@@ -125,6 +129,7 @@ describe('KbRoute', () => {
125
129
  expect(res.body.embedding.provider).to.equal('openai')
126
130
  expect(res.body.embedding.name).to.equal('text-embedding-ada-002')
127
131
  expect(res.body.embedding.dimension).to.equal(1536)
132
+ expect(res.body.embedding.api_key).to.equal('')
128
133
 
129
134
  // Get again all namespace. A new default namespace should not be created.
130
135
  chai.request(server)
@@ -140,6 +145,7 @@ describe('KbRoute', () => {
140
145
  expect(res.body.length).to.equal(1);
141
146
  should.not.exist(res.body[0]._id);
142
147
  should.exist(res.body[0].id);
148
+ expect(res.body[0].embedding.api_key).to.equal('');
143
149
 
144
150
  done();
145
151
  })
@@ -237,6 +243,7 @@ describe('KbRoute', () => {
237
243
  expect(res.body.embedding.provider).to.equal('openai')
238
244
  expect(res.body.embedding.name).to.equal('text-embedding-ada-002')
239
245
  expect(res.body.embedding.dimension).to.equal(1536)
246
+ expect(res.body.embedding.api_key).to.equal('')
240
247
 
241
248
  done();
242
249
  })
@@ -284,6 +291,9 @@ describe('KbRoute', () => {
284
291
  res.body.should.be.a('object');
285
292
  expect(res.body.success).to.equal(true);
286
293
  expect(res.body.message).to.equal("Contents imported successfully");
294
+ res.body.schedule_json.should.be.a('array');
295
+ expect(res.body.schedule_json.length).to.equal(3);
296
+ expect(res.body.schedule_json[0].embedding.api_key).to.equal('fakegptkey');
287
297
 
288
298
  chai.request(server)
289
299
  .get('/' + savedProject._id + '/kb/?namespace=' + namespace_id)
@@ -305,7 +315,22 @@ describe('KbRoute', () => {
305
315
  let content_without_tags = res.body.kbs.find(kb => kb.source !== 'Example content');
306
316
  expect(content_without_tags.tags).to.equal(undefined);
307
317
 
308
- done();
318
+ chai.request(server)
319
+ .get('/' + savedProject._id + '/kb/namespace/all')
320
+ .auth(email, pwd)
321
+ .end((err, res) => {
322
+ if (err) { console.error("err: ", err); }
323
+ if (log) { console.log("get all namespaces res.body: ", res.body); }
324
+
325
+ res.should.have.status(200);
326
+ res.body.should.be.a('array');
327
+ expect(res.body.length).to.equal(1);
328
+ expect(res.body[0].id).to.equal(namespace_id);
329
+ expect(res.body[0].name).to.equal("Default");
330
+ expect(res.body[0].embedding.api_key).to.equal('');
331
+
332
+ done();
333
+ })
309
334
  })
310
335
 
311
336
  })
@@ -370,6 +395,7 @@ describe('KbRoute', () => {
370
395
  expect(res.body.preview_settings.max_tokens).to.equal(256)
371
396
  expect(res.body.preview_settings.temperature).to.equal(0.3)
372
397
  expect(res.body.preview_settings.top_k).to.equal(6)
398
+ expect(res.body.embedding.api_key).to.equal('')
373
399
 
374
400
  done();
375
401
 
@@ -379,6 +405,43 @@ describe('KbRoute', () => {
379
405
  });
380
406
  })
381
407
 
408
+ it('embedding-api-key-never-persisted', (done) => {
409
+ const namespaceId = new mongoose.Types.ObjectId().toString();
410
+ const projectId = new mongoose.Types.ObjectId().toString();
411
+
412
+ const ns = new Namespace({
413
+ id_project: projectId,
414
+ id: namespaceId,
415
+ name: 'ApiKeyTest',
416
+ preview_settings: {
417
+ model: 'gpt-4o',
418
+ max_tokens: 256,
419
+ temperature: 0.7,
420
+ top_k: 4,
421
+ context: null
422
+ },
423
+ engine: default_engine,
424
+ embedding: {
425
+ provider: 'openai',
426
+ name: 'text-embedding-ada-002',
427
+ dimension: 1536,
428
+ api_key: 'must-not-persist'
429
+ }
430
+ });
431
+
432
+ ns.save()
433
+ .then((saved) => {
434
+ expect(saved.embedding.api_key).to.equal('');
435
+ return Namespace.findOne({ id: namespaceId }).lean();
436
+ })
437
+ .then((doc) => {
438
+ expect(doc.embedding.api_key).to.equal('');
439
+ return Namespace.deleteOne({ _id: doc._id });
440
+ })
441
+ .then(() => done())
442
+ .catch(done);
443
+ })
444
+
382
445
  /**
383
446
  * Delete default namespace - Forbidden
384
447
  */
@@ -515,7 +578,6 @@ describe('KbRoute', () => {
515
578
 
516
579
  userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
517
580
  projectService.create("test-faqkb-create", savedUser._id).then(function (savedProject) {
518
-
519
581
  chai.request(server)
520
582
  .get('/' + savedProject._id + '/kb/namespace/all')
521
583
  .auth(email, pwd)
@@ -579,6 +641,7 @@ describe('KbRoute', () => {
579
641
  expect(scheduleJson.tags[1]).to.equal("example");
580
642
  should.exist(scheduleJson.engine)
581
643
  should.exist(scheduleJson.embedding)
644
+ expect(scheduleJson.embedding.api_key).to.equal('fakegptkey');
582
645
 
583
646
  done();
584
647
  })
@@ -632,7 +695,6 @@ describe('KbRoute', () => {
632
695
  res.body.should.be.a('object');
633
696
 
634
697
  let realResponse = res.body.data;
635
- console.log("realResponse: ", realResponse);
636
698
  expect(realResponse.value.id_project).to.equal(namespace_id)
637
699
  expect(realResponse.value.name).to.equal("example_text1")
638
700
  expect(realResponse.value.type).to.equal("text")
@@ -652,6 +714,83 @@ describe('KbRoute', () => {
652
714
 
653
715
  })
654
716
 
717
+ it('update-content', (done) => {
718
+ const kbTrainUrl = process.env.KB_ENDPOINT_TRAIN;
719
+
720
+ const deleteScope = nock(kbTrainUrl)
721
+ .post('/delete/id')
722
+ .reply(200, { success: true });
723
+
724
+ var email = "test-signup-" + Date.now() + "@email.com";
725
+ var pwd = "pwd";
726
+
727
+ userService.signup(email, pwd, "Test Firstname", "Test lastname").then(function (savedUser) {
728
+ projectService.create("test-faqkb-create", savedUser._id).then(function (savedProject) {
729
+
730
+ chai.request(server)
731
+ .get('/' + savedProject._id + '/kb/namespace/all')
732
+ .auth(email, pwd)
733
+ .end((err, res) => {
734
+ if (err) { console.error("err: ", err); }
735
+
736
+ res.should.have.status(200);
737
+ let namespace_id = res.body[0].id;
738
+
739
+ let kb = {
740
+ name: "example_text_update",
741
+ type: "text",
742
+ source: "example_text_update",
743
+ content: "Original text",
744
+ namespace: namespace_id
745
+ };
746
+
747
+ chai.request(server)
748
+ .post('/' + savedProject._id + '/kb')
749
+ .auth(email, pwd)
750
+ .send(kb)
751
+ .end((err, res) => {
752
+ if (err) { console.error("err: ", err); }
753
+
754
+ res.should.have.status(200);
755
+ let kb_id = res.body.data.value._id;
756
+
757
+ chai.request(server)
758
+ .put('/' + savedProject._id + '/kb/' + kb_id)
759
+ .auth(email, pwd)
760
+ .send({
761
+ name: "example_text_update",
762
+ type: "text",
763
+ source: "example_text_update",
764
+ content: "Updated text",
765
+ namespace: namespace_id
766
+ })
767
+ .end((err, res) => {
768
+ if (err) { console.error("err: ", err); }
769
+ if (log) { console.log("update content res.body: ", res.body); }
770
+
771
+ res.should.have.status(200);
772
+ expect(res.body.success).to.equal(true);
773
+ expect(res.body.message).to.equal("Schedule scrape skipped in test environment");
774
+ expect(res.body.data.content).to.equal("Updated text");
775
+ should.exist(res.body.schedule_json.embedding);
776
+ expect(res.body.schedule_json.embedding.api_key).to.equal('fakegptkey');
777
+
778
+ chai.request(server)
779
+ .get('/' + savedProject._id + '/kb/namespace/all')
780
+ .auth(email, pwd)
781
+ .end((err, res) => {
782
+ expect(res.body[0].embedding.api_key).to.equal('');
783
+ expect(deleteScope.isDone()).to.equal(true);
784
+ nock.cleanAll();
785
+ done();
786
+ });
787
+ });
788
+ });
789
+ });
790
+ });
791
+ });
792
+ }).timeout(10000)
793
+
655
794
  it('get-content-chunks', (done) => {
656
795
 
657
796
  var email = "test-signup-" + Date.now() + "@email.com";
@@ -1018,7 +1157,6 @@ describe('KbRoute', () => {
1018
1157
 
1019
1158
  if (err) { console.error("err: ", err); }
1020
1159
  if (log) { console.log("res.body: ", res.body) }
1021
- console.log("res.body: ", res.body)
1022
1160
  res.should.have.status(200);
1023
1161
  res.body.should.be.a('object');
1024
1162
  expect(res.body.success).to.equal(true);
@@ -1026,7 +1164,6 @@ describe('KbRoute', () => {
1026
1164
 
1027
1165
  let realResponse = res.body.data;
1028
1166
  realResponse.should.be.a('array');
1029
- console.log("realResponse: ", realResponse[0]);
1030
1167
  expect(realResponse.length).to.equal(2);
1031
1168
  expect(realResponse[0].namespace).to.equal(namespace_id);
1032
1169
  expect(realResponse[0].type).to.equal('faq');
@@ -1050,6 +1187,7 @@ describe('KbRoute', () => {
1050
1187
  expect(scheduleJson[0].engine.index_name).to.equal(namespace.engine.index_name);
1051
1188
  expect(scheduleJson[0].embedding.provider).to.equal(namespace.embedding.provider);
1052
1189
  expect(scheduleJson[0].embedding.name).to.equal(namespace.embedding.name);
1190
+ expect(scheduleJson[0].embedding.api_key).to.equal('fakegptkey');
1053
1191
 
1054
1192
  expect(scheduleJson[1].namespace).to.equal(namespace_id);
1055
1193
  expect(scheduleJson[1].type).to.equal('faq');
@@ -1215,6 +1353,7 @@ describe('KbRoute', () => {
1215
1353
  expect(scheduleJson[0].engine.index_name).to.equal(namespace.engine.index_name);
1216
1354
  expect(scheduleJson[0].embedding.provider).to.equal(namespace.embedding.provider);
1217
1355
  expect(scheduleJson[0].embedding.name).to.equal(namespace.embedding.name);
1356
+ expect(scheduleJson[0].embedding.api_key).to.equal('fakegptkey');
1218
1357
 
1219
1358
  expect(scheduleJson[1].namespace).to.equal(namespace_id);
1220
1359
  expect(scheduleJson[1].source).to.equal('https://gethelp.tiledesk.com/articles/article2');
@@ -1407,6 +1546,7 @@ describe('KbRoute', () => {
1407
1546
  scheduleJson.should.be.a('array');
1408
1547
  should.exist(scheduleJson[0].engine)
1409
1548
  should.exist(scheduleJson[0].embedding)
1549
+ expect(scheduleJson[0].embedding.api_key).to.equal('fakegptkey');
1410
1550
 
1411
1551
  done();
1412
1552
  })
@@ -1485,6 +1625,7 @@ describe('KbRoute', () => {
1485
1625
  expect(res.body.data.embedding.provider).to.equal("openai");
1486
1626
  expect(res.body.data.embedding.name).to.equal("text-embedding-ada-002");
1487
1627
  expect(res.body.data.embedding.dimension).to.equal(1536);
1628
+ expect(res.body.data.embedding.api_key).to.equal('fakegptkey');
1488
1629
 
1489
1630
  done();
1490
1631
 
@@ -1759,7 +1900,7 @@ describe('KbRoute', () => {
1759
1900
  .get('/' + savedProject._id + '/kb/namespace/all')
1760
1901
  .auth(email, pwd)
1761
1902
  .end((err, res) => {
1762
-
1903
+
1763
1904
  if (err) { console.error("err: ", err); }
1764
1905
  if (log) { console.log("get namespaces res.body: ", res.body); }
1765
1906
 
@@ -1778,7 +1919,7 @@ describe('KbRoute', () => {
1778
1919
  .auth(email, pwd)
1779
1920
  .send(data)
1780
1921
  .end((err, res) => {
1781
-
1922
+
1782
1923
  if (err) { console.error("err: ", err); }
1783
1924
  if (log) { console.log("create unanswered question res.body: ", res.body); }
1784
1925