cozy-search 2.2.29 → 2.3.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.
Files changed (36) hide show
  1. package/README.md +44 -0
  2. package/dist/components/Assistant/AssistantSelection.js +1 -1
  3. package/dist/components/CozyAssistantRuntimeProvider.js +20 -9
  4. package/dist/components/KnowledgeBase/KnowledgeBaseChip.d.ts +2 -1
  5. package/dist/components/KnowledgeBase/KnowledgeBaseChip.js +2 -1
  6. package/dist/components/KnowledgeBase/KnowledgeBaseSection.js +1 -1
  7. package/dist/components/KnowledgeBase/conversationAssistant.d.ts +6 -0
  8. package/dist/components/KnowledgeBase/conversationAssistant.js +42 -0
  9. package/dist/components/KnowledgeBase/conversationAssistant.spec.d.ts +1 -0
  10. package/dist/components/KnowledgeBase/conversationAssistant.spec.js +80 -0
  11. package/dist/components/KnowledgeBase/knowledgeBase.d.ts +8 -0
  12. package/dist/components/KnowledgeBase/knowledgeBase.js +57 -11
  13. package/dist/components/KnowledgeBase/knowledgeBase.spec.js +92 -0
  14. package/dist/components/KnowledgeBase/provisioning.d.ts +24 -0
  15. package/dist/components/KnowledgeBase/provisioning.js +645 -0
  16. package/dist/components/KnowledgeBase/provisioning.spec.d.ts +1 -0
  17. package/dist/components/KnowledgeBase/provisioning.spec.js +489 -0
  18. package/dist/components/KnowledgeBase/ragIndexing.d.ts +21 -0
  19. package/dist/components/KnowledgeBase/ragIndexing.js +415 -0
  20. package/dist/components/KnowledgeBase/ragIndexing.spec.d.ts +1 -0
  21. package/dist/components/KnowledgeBase/ragIndexing.spec.js +268 -0
  22. package/dist/components/KnowledgeBase/useDefaultAssistantId.d.ts +1 -0
  23. package/dist/components/KnowledgeBase/useDefaultAssistantId.js +38 -0
  24. package/dist/components/KnowledgeBase/useDefaultAssistantId.spec.d.ts +1 -0
  25. package/dist/components/KnowledgeBase/useDefaultAssistantId.spec.js +69 -0
  26. package/dist/components/KnowledgeBase/useSelectedAssistantKnowledgeBase.d.ts +1 -0
  27. package/dist/components/KnowledgeBase/useSelectedAssistantKnowledgeBase.js +6 -2
  28. package/dist/components/TwakeKnowledges/TwakeKnowledgeSelector.js +2 -0
  29. package/dist/components/Views/CreateAssistantDialog.js +6 -12
  30. package/dist/components/Views/DeleteAssistantDialog.js +20 -12
  31. package/dist/components/Views/EditAssistantDialog.js +13 -7
  32. package/dist/components/queries.d.ts +8 -0
  33. package/dist/components/queries.js +21 -2
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +35 -1
  36. package/package.json +3 -3
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.setupRagIndexing = exports.migrateAssistantsWithoutFolder = exports.makeRagIndexTriggerAttributes = exports.findRagIndexTriggers = exports.RAG_INDEX_WORKER = exports.RAG_INDEX_FILES_DEBOUNCE = void 0;
9
+
10
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
+
12
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
13
+
14
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
15
+
16
+ var _knowledgeBase = require("./knowledgeBase");
17
+
18
+ var _queries = require("../queries");
19
+
20
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
21
+
22
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
23
+
24
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
25
+
26
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
27
+
28
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
29
+
30
+ var RAG_INDEX_WORKER = 'rag-index';
31
+ exports.RAG_INDEX_WORKER = RAG_INDEX_WORKER;
32
+ var RAG_INDEX_FILES_DEBOUNCE = '30s';
33
+ exports.RAG_INDEX_FILES_DEBOUNCE = RAG_INDEX_FILES_DEBOUNCE;
34
+ var TRIGGERS_DOCTYPE = 'io.cozy.triggers';
35
+
36
+ var warn = function warn() {
37
+ var _console;
38
+
39
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
40
+ args[_key] = arguments[_key];
41
+ }
42
+
43
+ // eslint-disable-next-line no-console
44
+ (_console = console).warn.apply(_console, ['cozy-search rag indexing:'].concat(args));
45
+ };
46
+ /**
47
+ * The two rag-index triggers of an instance: one on the files (debounced,
48
+ * they change often), one on the assistants (a knowledge base change must
49
+ * be seen at once). Both carry the same message.
50
+ */
51
+
52
+
53
+ var makeRagIndexTriggerAttributes = function makeRagIndexTriggerAttributes(doctype) {
54
+ return _objectSpread(_objectSpread({
55
+ type: '@event',
56
+ arguments: doctype
57
+ }, doctype === _queries.FILES_DOCTYPE ? {
58
+ debounce: RAG_INDEX_FILES_DEBOUNCE
59
+ } : {}), {}, {
60
+ worker: RAG_INDEX_WORKER,
61
+ message: {
62
+ doctype: _queries.FILES_DOCTYPE
63
+ }
64
+ });
65
+ };
66
+ /**
67
+ * Lists the rag-index triggers of the instance by kind.
68
+ * @returns {Promise<{ files: object|null, assistants: object|null }>}
69
+ */
70
+
71
+
72
+ exports.makeRagIndexTriggerAttributes = makeRagIndexTriggerAttributes;
73
+
74
+ var findRagIndexTriggers = /*#__PURE__*/function () {
75
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
76
+ var _yield$client$collect, triggers, found, _iterator, _step, trigger;
77
+
78
+ return _regenerator.default.wrap(function _callee$(_context) {
79
+ while (1) {
80
+ switch (_context.prev = _context.next) {
81
+ case 0:
82
+ _context.next = 2;
83
+ return client.collection(TRIGGERS_DOCTYPE).find({
84
+ worker: RAG_INDEX_WORKER
85
+ });
86
+
87
+ case 2:
88
+ _yield$client$collect = _context.sent;
89
+ triggers = _yield$client$collect.data;
90
+ found = {
91
+ files: null,
92
+ assistants: null
93
+ };
94
+ _iterator = _createForOfIteratorHelper(triggers || []);
95
+ _context.prev = 6;
96
+
97
+ _iterator.s();
98
+
99
+ case 8:
100
+ if ((_step = _iterator.n()).done) {
101
+ _context.next = 15;
102
+ break;
103
+ }
104
+
105
+ trigger = _step.value;
106
+
107
+ if (!(trigger.type !== '@event')) {
108
+ _context.next = 12;
109
+ break;
110
+ }
111
+
112
+ return _context.abrupt("continue", 13);
113
+
114
+ case 12:
115
+ if (trigger.arguments === _queries.FILES_DOCTYPE && !found.files) {
116
+ found.files = trigger;
117
+ } else if (trigger.arguments === _queries.ASSISTANTS_DOCTYPE && !found.assistants) {
118
+ found.assistants = trigger;
119
+ }
120
+
121
+ case 13:
122
+ _context.next = 8;
123
+ break;
124
+
125
+ case 15:
126
+ _context.next = 20;
127
+ break;
128
+
129
+ case 17:
130
+ _context.prev = 17;
131
+ _context.t0 = _context["catch"](6);
132
+
133
+ _iterator.e(_context.t0);
134
+
135
+ case 20:
136
+ _context.prev = 20;
137
+
138
+ _iterator.f();
139
+
140
+ return _context.finish(20);
141
+
142
+ case 23:
143
+ return _context.abrupt("return", found);
144
+
145
+ case 24:
146
+ case "end":
147
+ return _context.stop();
148
+ }
149
+ }
150
+ }, _callee, null, [[6, 17, 20, 23]]);
151
+ }));
152
+
153
+ return function findRagIndexTriggers(_x) {
154
+ return _ref.apply(this, arguments);
155
+ };
156
+ }();
157
+ /**
158
+ * Gives the root folder to every assistant without a knowledge base
159
+ * folder (the rule is permanent: an assistant always has one). A conflict
160
+ * on one assistant means another session did it: skipped.
161
+ * @returns {Promise<string[]>} The ids of the migrated assistants.
162
+ */
163
+
164
+
165
+ exports.findRagIndexTriggers = findRagIndexTriggers;
166
+
167
+ var migrateAssistantsWithoutFolder = /*#__PURE__*/function () {
168
+ var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(client) {
169
+ var migrated, _buildAllAssistantsQu, definition, options, assistants, _iterator2, _step2, assistant, knowledgeBase;
170
+
171
+ return _regenerator.default.wrap(function _callee2$(_context2) {
172
+ while (1) {
173
+ switch (_context2.prev = _context2.next) {
174
+ case 0:
175
+ migrated = [];
176
+ _buildAllAssistantsQu = (0, _queries.buildAllAssistantsQuery)(), definition = _buildAllAssistantsQu.definition, options = _buildAllAssistantsQu.options;
177
+ _context2.next = 4;
178
+ return client.queryAll(definition(), options);
179
+
180
+ case 4:
181
+ assistants = _context2.sent;
182
+ _iterator2 = _createForOfIteratorHelper(assistants);
183
+ _context2.prev = 6;
184
+
185
+ _iterator2.s();
186
+
187
+ case 8:
188
+ if ((_step2 = _iterator2.n()).done) {
189
+ _context2.next = 25;
190
+ break;
191
+ }
192
+
193
+ assistant = _step2.value;
194
+ knowledgeBase = (0, _knowledgeBase.withRootFolderIfMissing)(assistant.knowledgeBase);
195
+
196
+ if (!(knowledgeBase === assistant.knowledgeBase)) {
197
+ _context2.next = 13;
198
+ break;
199
+ }
200
+
201
+ return _context2.abrupt("continue", 23);
202
+
203
+ case 13:
204
+ _context2.prev = 13;
205
+ _context2.next = 16;
206
+ return client.save(_objectSpread(_objectSpread({}, assistant), {}, {
207
+ knowledgeBase: knowledgeBase
208
+ }));
209
+
210
+ case 16:
211
+ migrated.push(assistant._id);
212
+ _context2.next = 23;
213
+ break;
214
+
215
+ case 19:
216
+ _context2.prev = 19;
217
+ _context2.t0 = _context2["catch"](13);
218
+
219
+ if (!((_context2.t0 === null || _context2.t0 === void 0 ? void 0 : _context2.t0.status) !== 409)) {
220
+ _context2.next = 23;
221
+ break;
222
+ }
223
+
224
+ throw _context2.t0;
225
+
226
+ case 23:
227
+ _context2.next = 8;
228
+ break;
229
+
230
+ case 25:
231
+ _context2.next = 30;
232
+ break;
233
+
234
+ case 27:
235
+ _context2.prev = 27;
236
+ _context2.t1 = _context2["catch"](6);
237
+
238
+ _iterator2.e(_context2.t1);
239
+
240
+ case 30:
241
+ _context2.prev = 30;
242
+
243
+ _iterator2.f();
244
+
245
+ return _context2.finish(30);
246
+
247
+ case 33:
248
+ return _context2.abrupt("return", migrated);
249
+
250
+ case 34:
251
+ case "end":
252
+ return _context2.stop();
253
+ }
254
+ }
255
+ }, _callee2, null, [[6, 27, 30, 33], [13, 19]]);
256
+ }));
257
+
258
+ return function migrateAssistantsWithoutFolder(_x2) {
259
+ return _ref2.apply(this, arguments);
260
+ };
261
+ }();
262
+ /**
263
+ * Creates the missing triggers, pushing the `arguments` of each one
264
+ * created into `created` as it goes (so a failure halfway still reports
265
+ * what exists).
266
+ */
267
+
268
+
269
+ exports.migrateAssistantsWithoutFolder = migrateAssistantsWithoutFolder;
270
+
271
+ var ensureTriggers = /*#__PURE__*/function () {
272
+ var _ref3 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(client, created) {
273
+ var _yield$findRagIndexTr, files, assistants, triggers, _yield$triggers$creat, trigger;
274
+
275
+ return _regenerator.default.wrap(function _callee3$(_context3) {
276
+ while (1) {
277
+ switch (_context3.prev = _context3.next) {
278
+ case 0:
279
+ _context3.next = 2;
280
+ return findRagIndexTriggers(client);
281
+
282
+ case 2:
283
+ _yield$findRagIndexTr = _context3.sent;
284
+ files = _yield$findRagIndexTr.files;
285
+ assistants = _yield$findRagIndexTr.assistants;
286
+ triggers = client.collection(TRIGGERS_DOCTYPE);
287
+
288
+ if (assistants) {
289
+ _context3.next = 10;
290
+ break;
291
+ }
292
+
293
+ _context3.next = 9;
294
+ return triggers.create(makeRagIndexTriggerAttributes(_queries.ASSISTANTS_DOCTYPE));
295
+
296
+ case 9:
297
+ created.push(_queries.ASSISTANTS_DOCTYPE);
298
+
299
+ case 10:
300
+ if (files) {
301
+ _context3.next = 26;
302
+ break;
303
+ }
304
+
305
+ _context3.next = 13;
306
+ return triggers.create(makeRagIndexTriggerAttributes(_queries.FILES_DOCTYPE));
307
+
308
+ case 13:
309
+ _yield$triggers$creat = _context3.sent;
310
+ trigger = _yield$triggers$creat.data;
311
+ _context3.prev = 15;
312
+ _context3.next = 18;
313
+ return triggers.launch(trigger);
314
+
315
+ case 18:
316
+ _context3.next = 25;
317
+ break;
318
+
319
+ case 20:
320
+ _context3.prev = 20;
321
+ _context3.t0 = _context3["catch"](15);
322
+ _context3.next = 24;
323
+ return triggers.destroy(trigger).catch(function () {});
324
+
325
+ case 24:
326
+ throw _context3.t0;
327
+
328
+ case 25:
329
+ created.push(_queries.FILES_DOCTYPE);
330
+
331
+ case 26:
332
+ case "end":
333
+ return _context3.stop();
334
+ }
335
+ }
336
+ }, _callee3, null, [[15, 20]]);
337
+ }));
338
+
339
+ return function ensureTriggers(_x3, _x4) {
340
+ return _ref3.apply(this, arguments);
341
+ };
342
+ }();
343
+ /**
344
+ * Makes the instance ready for the assistants-driven indexing: the files
345
+ * and assistants triggers exist (the files one is launched when created),
346
+ * and every assistant has a knowledge base folder. Idempotent and never throws: an app calls it once
347
+ * per session at startup. On a stack that still reserves the worker the
348
+ * trigger creation answers 403: logged, the assistants keep working with
349
+ * the stack's legacy indexing.
350
+ * @param {import('cozy-client').CozyClient} client - The cozy client.
351
+ * @returns {Promise<{triggers: string[], migrated: string[], errors: Error[]}>}
352
+ * The `arguments` of the triggers created, the ids of the migrated
353
+ * assistants, and the failures.
354
+ */
355
+
356
+
357
+ var setupRagIndexing = /*#__PURE__*/function () {
358
+ var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee4(client) {
359
+ var result;
360
+ return _regenerator.default.wrap(function _callee4$(_context4) {
361
+ while (1) {
362
+ switch (_context4.prev = _context4.next) {
363
+ case 0:
364
+ result = {
365
+ triggers: [],
366
+ migrated: [],
367
+ errors: []
368
+ };
369
+ _context4.prev = 1;
370
+ _context4.next = 4;
371
+ return ensureTriggers(client, result.triggers);
372
+
373
+ case 4:
374
+ _context4.next = 10;
375
+ break;
376
+
377
+ case 6:
378
+ _context4.prev = 6;
379
+ _context4.t0 = _context4["catch"](1);
380
+ warn('cannot set up the rag-index triggers', _context4.t0);
381
+ result.errors.push(_context4.t0);
382
+
383
+ case 10:
384
+ _context4.prev = 10;
385
+ _context4.next = 13;
386
+ return migrateAssistantsWithoutFolder(client);
387
+
388
+ case 13:
389
+ result.migrated = _context4.sent;
390
+ _context4.next = 20;
391
+ break;
392
+
393
+ case 16:
394
+ _context4.prev = 16;
395
+ _context4.t1 = _context4["catch"](10);
396
+ warn('cannot migrate the assistants without folder', _context4.t1);
397
+ result.errors.push(_context4.t1);
398
+
399
+ case 20:
400
+ return _context4.abrupt("return", result);
401
+
402
+ case 21:
403
+ case "end":
404
+ return _context4.stop();
405
+ }
406
+ }
407
+ }, _callee4, null, [[1, 6], [10, 16]]);
408
+ }));
409
+
410
+ return function setupRagIndexing(_x5) {
411
+ return _ref4.apply(this, arguments);
412
+ };
413
+ }();
414
+
415
+ exports.setupRagIndexing = setupRagIndexing;
@@ -0,0 +1,268 @@
1
+ import { Q } from 'cozy-client'
2
+
3
+ import { ROOT_DIR_ID } from './knowledgeBase'
4
+ import {
5
+ RAG_INDEX_FILES_DEBOUNCE,
6
+ findRagIndexTriggers,
7
+ makeRagIndexTriggerAttributes,
8
+ migrateAssistantsWithoutFolder,
9
+ setupRagIndexing
10
+ } from './ragIndexing'
11
+
12
+ const FILES = 'io.cozy.files'
13
+ const ASSISTANTS = 'io.cozy.ai.chat.assistants'
14
+
15
+ const filesTrigger = {
16
+ _id: 'trigger-files',
17
+ _type: 'io.cozy.triggers',
18
+ type: '@event',
19
+ worker: 'rag-index',
20
+ arguments: FILES,
21
+ debounce: '30s',
22
+ message: { doctype: FILES }
23
+ }
24
+ const assistantsTrigger = {
25
+ _id: 'trigger-assistants',
26
+ _type: 'io.cozy.triggers',
27
+ type: '@event',
28
+ worker: 'rag-index',
29
+ arguments: ASSISTANTS,
30
+ message: { doctype: FILES }
31
+ }
32
+ const cronFilesTrigger = {
33
+ _id: 'trigger-cron-files',
34
+ _type: 'io.cozy.triggers',
35
+ type: '@cron',
36
+ worker: 'rag-index',
37
+ arguments: FILES,
38
+ message: { doctype: FILES }
39
+ }
40
+
41
+ const makeClient = ({ triggers = [], assistants = [] } = {}) => {
42
+ const triggersCollection = {
43
+ find: jest.fn().mockResolvedValue({ data: triggers }),
44
+ create: jest.fn(async attributes => ({
45
+ data: { _id: `created-${attributes.arguments}`, ...attributes }
46
+ })),
47
+ launch: jest.fn().mockResolvedValue({ data: { _id: 'job-1' } }),
48
+ destroy: jest.fn(async trigger => ({ data: trigger }))
49
+ }
50
+ const client = {
51
+ collection: jest.fn(doctype => {
52
+ if (doctype === 'io.cozy.triggers') return triggersCollection
53
+ throw new Error(`unexpected collection ${doctype}`)
54
+ }),
55
+ queryAll: jest.fn(async () => assistants),
56
+ save: jest.fn(async doc => ({ data: doc }))
57
+ }
58
+ return { client, triggersCollection }
59
+ }
60
+
61
+ describe('makeRagIndexTriggerAttributes', () => {
62
+ it('debounces the files trigger only', () => {
63
+ expect(makeRagIndexTriggerAttributes(FILES)).toEqual({
64
+ type: '@event',
65
+ arguments: FILES,
66
+ debounce: RAG_INDEX_FILES_DEBOUNCE,
67
+ worker: 'rag-index',
68
+ message: { doctype: FILES }
69
+ })
70
+ expect(makeRagIndexTriggerAttributes(ASSISTANTS)).toEqual({
71
+ type: '@event',
72
+ arguments: ASSISTANTS,
73
+ worker: 'rag-index',
74
+ message: { doctype: FILES }
75
+ })
76
+ expect(RAG_INDEX_FILES_DEBOUNCE).toBe('30s')
77
+ })
78
+ })
79
+
80
+ describe('findRagIndexTriggers', () => {
81
+ it('sorts the triggers by kind', async () => {
82
+ const { client, triggersCollection } = makeClient({
83
+ triggers: [filesTrigger, assistantsTrigger]
84
+ })
85
+ const found = await findRagIndexTriggers(client)
86
+ expect(triggersCollection.find).toHaveBeenCalledWith({
87
+ worker: 'rag-index'
88
+ })
89
+ expect(found.files).toBe(filesTrigger)
90
+ expect(found.assistants).toBe(assistantsTrigger)
91
+ })
92
+
93
+ it('returns nulls when there is nothing', async () => {
94
+ const { client } = makeClient()
95
+ expect(await findRagIndexTriggers(client)).toEqual({
96
+ files: null,
97
+ assistants: null
98
+ })
99
+ })
100
+
101
+ it('ignores a rag-index trigger that is not an @event trigger', async () => {
102
+ const { client } = makeClient({ triggers: [cronFilesTrigger] })
103
+ const found = await findRagIndexTriggers(client)
104
+ expect(found.files).toBeNull()
105
+ expect(found.assistants).toBeNull()
106
+ })
107
+ })
108
+
109
+ describe('migrateAssistantsWithoutFolder', () => {
110
+ it('appends the root entry to assistants without folder, keeping other entries', async () => {
111
+ const { client } = makeClient({
112
+ assistants: [
113
+ {
114
+ _id: 'a1',
115
+ _type: ASSISTANTS,
116
+ knowledgeBase: [{ doctype: 'io.cozy.email' }]
117
+ },
118
+ {
119
+ _id: 'a2',
120
+ _type: ASSISTANTS,
121
+ knowledgeBase: [{ doctype: FILES, dirId: 'd' }]
122
+ },
123
+ { _id: 'a3', _type: ASSISTANTS }
124
+ ]
125
+ })
126
+ const migrated = await migrateAssistantsWithoutFolder(client)
127
+ expect(migrated).toEqual(['a1', 'a3'])
128
+ expect(client.queryAll).toHaveBeenCalledWith(Q(ASSISTANTS).limitBy(1000), {
129
+ as: `${ASSISTANTS}/all`
130
+ })
131
+ expect(client.save).toHaveBeenCalledTimes(2)
132
+ expect(client.save).toHaveBeenCalledWith({
133
+ _id: 'a1',
134
+ _type: ASSISTANTS,
135
+ knowledgeBase: [
136
+ { doctype: 'io.cozy.email' },
137
+ { doctype: FILES, dirId: ROOT_DIR_ID }
138
+ ]
139
+ })
140
+ expect(client.save).toHaveBeenCalledWith({
141
+ _id: 'a3',
142
+ _type: ASSISTANTS,
143
+ knowledgeBase: [{ doctype: FILES, dirId: ROOT_DIR_ID }]
144
+ })
145
+ })
146
+
147
+ it('ignores a conflict on one assistant and goes on', async () => {
148
+ const { client } = makeClient({
149
+ assistants: [
150
+ { _id: 'a1', _type: ASSISTANTS },
151
+ { _id: 'a2', _type: ASSISTANTS }
152
+ ]
153
+ })
154
+ client.save.mockImplementationOnce(async () => {
155
+ throw Object.assign(new Error('conflict'), { status: 409 })
156
+ })
157
+ expect(await migrateAssistantsWithoutFolder(client)).toEqual(['a2'])
158
+ })
159
+ })
160
+
161
+ describe('setupRagIndexing', () => {
162
+ let warn
163
+ beforeEach(() => {
164
+ warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
165
+ })
166
+ afterEach(() => warn.mockRestore())
167
+
168
+ it('creates the missing triggers and launches the files one', async () => {
169
+ const { client, triggersCollection } = makeClient({
170
+ triggers: [assistantsTrigger]
171
+ })
172
+ const result = await setupRagIndexing(client)
173
+
174
+ expect(triggersCollection.create).toHaveBeenCalledTimes(1)
175
+ expect(triggersCollection.create).toHaveBeenCalledWith(
176
+ makeRagIndexTriggerAttributes(FILES)
177
+ )
178
+ expect(triggersCollection.launch).toHaveBeenCalledTimes(1)
179
+ expect(triggersCollection.launch.mock.calls[0][0]._id).toBe(
180
+ 'created-io.cozy.files'
181
+ )
182
+ expect(result).toEqual({ triggers: [FILES], migrated: [], errors: [] })
183
+ })
184
+
185
+ it('removes the files trigger it could not launch, keeping the assistants one', async () => {
186
+ const { client, triggersCollection } = makeClient()
187
+ const launchError = new Error('launch failed')
188
+ triggersCollection.launch.mockRejectedValue(launchError)
189
+
190
+ const result = await setupRagIndexing(client)
191
+
192
+ expect(triggersCollection.destroy).toHaveBeenCalledTimes(1)
193
+ expect(triggersCollection.destroy.mock.calls[0][0]._id).toBe(
194
+ 'created-io.cozy.files'
195
+ )
196
+ expect(result.triggers).toEqual([ASSISTANTS])
197
+ expect(result.errors).toEqual([launchError])
198
+ })
199
+
200
+ it('creates the files trigger when the existing one is not an @event trigger', async () => {
201
+ const { client, triggersCollection } = makeClient({
202
+ triggers: [cronFilesTrigger, assistantsTrigger]
203
+ })
204
+ const result = await setupRagIndexing(client)
205
+
206
+ expect(triggersCollection.create).toHaveBeenCalledWith(
207
+ makeRagIndexTriggerAttributes(FILES)
208
+ )
209
+ expect(result.triggers).toEqual([FILES])
210
+ })
211
+
212
+ it('creates the assistants trigger without launching it', async () => {
213
+ const { client, triggersCollection } = makeClient({
214
+ triggers: [filesTrigger]
215
+ })
216
+ const result = await setupRagIndexing(client)
217
+ expect(triggersCollection.create).toHaveBeenCalledWith(
218
+ makeRagIndexTriggerAttributes(ASSISTANTS)
219
+ )
220
+ expect(triggersCollection.launch).not.toHaveBeenCalled()
221
+ expect(result.triggers).toEqual([ASSISTANTS])
222
+ })
223
+
224
+ it('touches nothing when both triggers exist', async () => {
225
+ const { client, triggersCollection } = makeClient({
226
+ triggers: [filesTrigger, assistantsTrigger]
227
+ })
228
+ const result = await setupRagIndexing(client)
229
+ expect(triggersCollection.create).not.toHaveBeenCalled()
230
+ expect(result).toEqual({ triggers: [], migrated: [], errors: [] })
231
+ })
232
+
233
+ it('migrates the assistants without folder', async () => {
234
+ const { client } = makeClient({
235
+ triggers: [filesTrigger, assistantsTrigger],
236
+ assistants: [{ _id: 'a1', _type: ASSISTANTS }]
237
+ })
238
+ const result = await setupRagIndexing(client)
239
+ expect(result.migrated).toEqual(['a1'])
240
+ expect(client.save).toHaveBeenCalledTimes(1)
241
+ })
242
+
243
+ it('logs a 403 from an older stack and still migrates', async () => {
244
+ const { client, triggersCollection } = makeClient({
245
+ assistants: [{ _id: 'a1', _type: ASSISTANTS }]
246
+ })
247
+ triggersCollection.create.mockRejectedValue(
248
+ Object.assign(new Error('forbidden'), { status: 403 })
249
+ )
250
+ const result = await setupRagIndexing(client)
251
+ expect(result.triggers).toEqual([])
252
+ // The first failing creation aborts the trigger setup: one error.
253
+ expect(result.errors).toHaveLength(1)
254
+ expect(result.errors[0].status).toBe(403)
255
+ expect(result.migrated).toEqual(['a1'])
256
+ expect(warn).toHaveBeenCalled()
257
+ })
258
+
259
+ it('never throws, even when the listing fails', async () => {
260
+ const { client, triggersCollection } = makeClient()
261
+ triggersCollection.find.mockRejectedValue(new Error('down'))
262
+ const result = await setupRagIndexing(client)
263
+ expect(result.triggers).toEqual([])
264
+ expect(result.migrated).toEqual([])
265
+ expect(result.errors.map(e => e.message)).toEqual(['down'])
266
+ expect(triggersCollection.create).not.toHaveBeenCalled()
267
+ })
268
+ })
@@ -0,0 +1 @@
1
+ export function useDefaultAssistantId(): string | null | undefined;