cozy-search 2.3.3 → 2.5.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.
package/README.md CHANGED
@@ -59,9 +59,23 @@ of verbs (cozy-home and cozy-drive use `ALL`).
59
59
 
60
60
  ### RAG indexing setup
61
61
 
62
- Call `setupRagIndexing(client)` once per session at startup (before
63
- `ensureProvisionedAssistants` if your app provisions assistants from the
64
- `rag.assistants.autoprovision` flag). It makes sure the instance's two
62
+ Opening the assistant (`AssistantView` or `AssistantDialog`) calls
63
+ `autoprovisionAssistants(client)` once per session: when the
64
+ `cozy.assistant.autoprovision` flag lists assistants, it makes sure the
65
+ `rag-index` triggers exist, gives the root folder to the assistants that
66
+ have no knowledge base folder, then runs
67
+ `ensureProvisionedAssistants(client, entries)`.
68
+
69
+ A host app should only call the `useRagIndexTriggers` hook (or
70
+ `ensureRagIndexTriggers(client)`) at startup: one request when the
71
+ triggers already exist, and the knowledge base folders get indexed as
72
+ they change before the assistant is ever opened. The assistants are
73
+ provisioned when the assistant opens; a host app that wants that earlier
74
+ can call `autoprovisionAssistants(client)` or the
75
+ `useAssistantsAutoprovision` hook. Every call shares the first one's
76
+ promise, the triggers step included, so nothing runs twice.
77
+
78
+ `setupRagIndexing(client)` makes sure the instance's two
65
79
  `rag-index` triggers exist (one on `io.cozy.files`, one on
66
80
  `io.cozy.ai.chat.assistants`) and gives the root folder to any assistant
67
81
  that has no knowledge base folder yet — the stack's `rag-index` worker
@@ -76,7 +90,7 @@ never throws: on a stack whose `rag-index` worker is still reserved, the
76
90
  additionally creates one `io.cozy.accounts` document per provisioned
77
91
  assistant, and the `dirName` folder in `io.cozy.files`.
78
92
 
79
- An entry of `rag.assistants.autoprovision` can carry `"default": true`
93
+ An entry of `cozy.assistant.autoprovision` can carry `"default": true`
80
94
  (the first flagged entry wins if there are several). Every NEW
81
95
  conversation then starts on that assistant once its document exists —
82
96
  from the session after it was provisioned onward. Existing conversations
@@ -21,6 +21,8 @@ var _Conversation = _interopRequireDefault(require("../Conversations/Conversatio
21
21
 
22
22
  var _CozyAssistantRuntimeProvider = _interopRequireDefault(require("../CozyAssistantRuntimeProvider"));
23
23
 
24
+ var _useAssistantsAutoprovision = require("../KnowledgeBase/useAssistantsAutoprovision");
25
+
24
26
  var _SearchConversation = _interopRequireDefault(require("../Search/SearchConversation"));
25
27
 
26
28
  var _Sidebar = _interopRequireDefault(require("../Sidebar"));
@@ -43,6 +45,7 @@ var AssistantContainer = function AssistantContainer() {
43
45
  var _useAssistant = (0, _AssistantProvider.useAssistant)(),
44
46
  isOpenSearchConversation = _useAssistant.isOpenSearchConversation;
45
47
 
48
+ (0, _useAssistantsAutoprovision.useAssistantsAutoprovision)();
46
49
  return /*#__PURE__*/_react.default.createElement("div", {
47
50
  className: (0, _classnames.default)('u-flex u-ov-hidden u-h-100', styles['assistant-container'])
48
51
  }, /*#__PURE__*/_react.default.createElement(_Sidebar.default, {
@@ -0,0 +1,15 @@
1
+ export function resetAutoprovisionForTests(): void;
2
+ export function autoprovisionEntries(): Array<object> | null;
3
+ export function ensureRagIndexTriggers(client: import("cozy-client").CozyClient): Promise<{
4
+ triggers: string[];
5
+ error: Error | null;
6
+ }>;
7
+ export function autoprovisionAssistants(client: import("cozy-client").CozyClient): Promise<null | {
8
+ setup: object;
9
+ created: string[];
10
+ ensured: string[];
11
+ skipped: {
12
+ id: string;
13
+ reason: string;
14
+ }[];
15
+ }>;
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ exports.resetAutoprovisionForTests = exports.ensureRagIndexTriggers = exports.autoprovisionEntries = exports.autoprovisionAssistants = void 0;
9
+
10
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
+
12
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
13
+
14
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
15
+
16
+ var _cozyFlags = _interopRequireDefault(require("cozy-flags"));
17
+
18
+ var _provisioning = require("./provisioning");
19
+
20
+ var _ragIndexing = require("./ragIndexing");
21
+
22
+ 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; }
23
+
24
+ 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; }
25
+
26
+ var warn = function warn() {
27
+ var _console;
28
+
29
+ for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
30
+ args[_key] = arguments[_key];
31
+ }
32
+
33
+ // eslint-disable-next-line no-console
34
+ (_console = console).warn.apply(_console, ['cozy-search autoprovision:'].concat(args));
35
+ };
36
+
37
+ var triggersPending = null;
38
+ var pending = null;
39
+
40
+ var resetAutoprovisionForTests = function resetAutoprovisionForTests() {
41
+ triggersPending = null;
42
+ pending = null;
43
+ };
44
+ /**
45
+ * The entries of the autoprovision flag, or null when it lists nothing.
46
+ * @returns {Array<object>|null}
47
+ */
48
+
49
+
50
+ exports.resetAutoprovisionForTests = resetAutoprovisionForTests;
51
+
52
+ var autoprovisionEntries = function autoprovisionEntries() {
53
+ var entries = (0, _cozyFlags.default)(_provisioning.AUTOPROVISION_FLAG);
54
+ return Array.isArray(entries) && entries.length > 0 ? entries : null;
55
+ };
56
+ /**
57
+ * Makes sure the instance has its rag-index triggers, so the files of the
58
+ * knowledge base folders get indexed as they change. Runs once per session
59
+ * and never rejects: a host app can call it at startup, it costs a single
60
+ * request when the triggers already exist.
61
+ * @param {import('cozy-client').CozyClient} client - The cozy client.
62
+ * @returns {Promise<{triggers: string[], error: Error|null}>} The
63
+ * `arguments` of the triggers created, and the failure if any.
64
+ */
65
+
66
+
67
+ exports.autoprovisionEntries = autoprovisionEntries;
68
+
69
+ var ensureRagIndexTriggers = function ensureRagIndexTriggers(client) {
70
+ if (!triggersPending) {
71
+ triggersPending = (0, _ragIndexing.createRagIndexTriggers)(client).then(function (triggers) {
72
+ return {
73
+ triggers: triggers,
74
+ error: null
75
+ };
76
+ }, function (error) {
77
+ warn('cannot set up the rag-index triggers', error);
78
+ return {
79
+ triggers: [],
80
+ error: error
81
+ };
82
+ });
83
+ }
84
+
85
+ return triggersPending;
86
+ };
87
+
88
+ exports.ensureRagIndexTriggers = ensureRagIndexTriggers;
89
+
90
+ var run = /*#__PURE__*/function () {
91
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client) {
92
+ var entries, setup, _yield$ensureRagIndex, triggers, error, assistants, result;
93
+
94
+ return _regenerator.default.wrap(function _callee$(_context) {
95
+ while (1) {
96
+ switch (_context.prev = _context.next) {
97
+ case 0:
98
+ entries = autoprovisionEntries();
99
+
100
+ if (entries) {
101
+ _context.next = 3;
102
+ break;
103
+ }
104
+
105
+ return _context.abrupt("return", null);
106
+
107
+ case 3:
108
+ setup = {
109
+ triggers: [],
110
+ migrated: [],
111
+ errors: []
112
+ };
113
+ _context.next = 6;
114
+ return ensureRagIndexTriggers(client);
115
+
116
+ case 6:
117
+ _yield$ensureRagIndex = _context.sent;
118
+ triggers = _yield$ensureRagIndex.triggers;
119
+ error = _yield$ensureRagIndex.error;
120
+ setup.triggers = triggers;
121
+ if (error) setup.errors.push(error);
122
+ assistants = null;
123
+ _context.prev = 12;
124
+ _context.next = 15;
125
+ return (0, _ragIndexing.fetchAssistants)(client);
126
+
127
+ case 15:
128
+ assistants = _context.sent;
129
+ _context.next = 18;
130
+ return (0, _ragIndexing.migrateAssistantsWithoutFolder)(client, assistants);
131
+
132
+ case 18:
133
+ setup.migrated = _context.sent;
134
+ _context.next = 25;
135
+ break;
136
+
137
+ case 21:
138
+ _context.prev = 21;
139
+ _context.t0 = _context["catch"](12);
140
+ warn('cannot migrate the assistants without folder', _context.t0);
141
+ setup.errors.push(_context.t0);
142
+
143
+ case 25:
144
+ _context.next = 27;
145
+ return (0, _provisioning.ensureProvisionedAssistants)(client, entries, {
146
+ assistants: assistants
147
+ });
148
+
149
+ case 27:
150
+ result = _context.sent;
151
+
152
+ if (result.skipped.length > 0) {
153
+ warn('skipped entries', result.skipped);
154
+ }
155
+
156
+ return _context.abrupt("return", _objectSpread({
157
+ setup: setup
158
+ }, result));
159
+
160
+ case 30:
161
+ case "end":
162
+ return _context.stop();
163
+ }
164
+ }
165
+ }, _callee, null, [[12, 21]]);
166
+ }));
167
+
168
+ return function run(_x) {
169
+ return _ref.apply(this, arguments);
170
+ };
171
+ }();
172
+ /**
173
+ * Sets up the rag-index triggers, gives a folder to the assistants that
174
+ * lack one, then provisions the assistants listed in the autoprovision
175
+ * flag. Runs once per session: every call shares the first one's promise,
176
+ * and the triggers step is shared with ensureRagIndexTriggers, so a host
177
+ * app that ensured them at startup does not pay for them twice. Never
178
+ * rejects.
179
+ * @param {import('cozy-client').CozyClient} client - The cozy client.
180
+ * @returns {Promise<null|{setup: object, created: string[], ensured: string[], skipped: {id: string, reason: string}[]}>} Null when the flag lists nothing.
181
+ */
182
+
183
+
184
+ var autoprovisionAssistants = function autoprovisionAssistants(client) {
185
+ if (!pending) {
186
+ pending = run(client).catch(function (error) {
187
+ warn('failed', error);
188
+ return null;
189
+ });
190
+ }
191
+
192
+ return pending;
193
+ };
194
+
195
+ exports.autoprovisionAssistants = autoprovisionAssistants;
@@ -0,0 +1,199 @@
1
+ import flag from 'cozy-flags'
2
+
3
+ import {
4
+ autoprovisionAssistants,
5
+ autoprovisionEntries,
6
+ ensureRagIndexTriggers,
7
+ resetAutoprovisionForTests
8
+ } from './autoprovision'
9
+ import { ensureProvisionedAssistants } from './provisioning'
10
+ import {
11
+ createRagIndexTriggers,
12
+ fetchAssistants,
13
+ migrateAssistantsWithoutFolder
14
+ } from './ragIndexing'
15
+
16
+ jest.mock('cozy-flags', () => jest.fn())
17
+ jest.mock('./provisioning', () => ({
18
+ AUTOPROVISION_FLAG: 'cozy.assistant.autoprovision',
19
+ ensureProvisionedAssistants: jest.fn()
20
+ }))
21
+ jest.mock('./ragIndexing', () => ({
22
+ createRagIndexTriggers: jest.fn(),
23
+ fetchAssistants: jest.fn(),
24
+ migrateAssistantsWithoutFolder: jest.fn()
25
+ }))
26
+
27
+ const client = { id: 'client' }
28
+ const entries = [{ name: 'Docs', dirName: 'Docs' }]
29
+ const assistants = [{ _id: 'legacy' }]
30
+ let warnSpy
31
+
32
+ beforeEach(() => {
33
+ resetAutoprovisionForTests()
34
+ flag.mockReset()
35
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
36
+ createRagIndexTriggers.mockReset().mockResolvedValue(['io.cozy.files'])
37
+ fetchAssistants.mockReset().mockResolvedValue(assistants)
38
+ migrateAssistantsWithoutFolder.mockReset().mockResolvedValue([])
39
+ ensureProvisionedAssistants
40
+ .mockReset()
41
+ .mockResolvedValue({ created: ['docs'], ensured: [], skipped: [] })
42
+ })
43
+
44
+ afterEach(() => {
45
+ warnSpy.mockRestore()
46
+ })
47
+
48
+ describe('autoprovisionEntries', () => {
49
+ it('returns the flag entries', () => {
50
+ flag.mockReturnValue(entries)
51
+ expect(autoprovisionEntries()).toBe(entries)
52
+ expect(flag).toHaveBeenCalledWith('cozy.assistant.autoprovision')
53
+ })
54
+
55
+ it.each([null, undefined, [], 'docs', {}])('returns null for %p', value => {
56
+ flag.mockReturnValue(value)
57
+ expect(autoprovisionEntries()).toBeNull()
58
+ })
59
+ })
60
+
61
+ describe('ensureRagIndexTriggers', () => {
62
+ it('creates the missing triggers once per session', async () => {
63
+ const first = ensureRagIndexTriggers(client)
64
+ expect(ensureRagIndexTriggers(client)).toBe(first)
65
+ await expect(first).resolves.toEqual({
66
+ triggers: ['io.cozy.files'],
67
+ error: null
68
+ })
69
+ await ensureRagIndexTriggers(client)
70
+
71
+ expect(createRagIndexTriggers).toHaveBeenCalledTimes(1)
72
+ expect(createRagIndexTriggers).toHaveBeenCalledWith(client)
73
+ })
74
+
75
+ it('reports a failure without rejecting', async () => {
76
+ const error = new Error('forbidden')
77
+ createRagIndexTriggers.mockRejectedValue(error)
78
+ await expect(ensureRagIndexTriggers(client)).resolves.toEqual({
79
+ triggers: [],
80
+ error
81
+ })
82
+ expect(warnSpy).toHaveBeenCalledWith(
83
+ 'cozy-search autoprovision:',
84
+ 'cannot set up the rag-index triggers',
85
+ error
86
+ )
87
+ })
88
+ })
89
+
90
+ describe('autoprovisionAssistants', () => {
91
+ it('does nothing without flag entries', async () => {
92
+ flag.mockReturnValue(null)
93
+ await expect(autoprovisionAssistants(client)).resolves.toBeNull()
94
+ expect(createRagIndexTriggers).not.toHaveBeenCalled()
95
+ expect(fetchAssistants).not.toHaveBeenCalled()
96
+ expect(migrateAssistantsWithoutFolder).not.toHaveBeenCalled()
97
+ expect(ensureProvisionedAssistants).not.toHaveBeenCalled()
98
+ })
99
+
100
+ it('lets the provisioning query the assistants when listing them failed', async () => {
101
+ flag.mockReturnValue(entries)
102
+ fetchAssistants.mockRejectedValue(new Error('nope'))
103
+ const result = await autoprovisionAssistants(client)
104
+
105
+ expect(migrateAssistantsWithoutFolder).not.toHaveBeenCalled()
106
+ expect(ensureProvisionedAssistants).toHaveBeenCalledWith(client, entries, {
107
+ assistants: null
108
+ })
109
+ expect(result.setup.errors.map(e => e.message)).toEqual(['nope'])
110
+ })
111
+
112
+ it('ensures the triggers, migrates, then provisions the flag entries', async () => {
113
+ flag.mockReturnValue(entries)
114
+ migrateAssistantsWithoutFolder.mockResolvedValue(['legacy'])
115
+ const result = await autoprovisionAssistants(client)
116
+
117
+ expect(createRagIndexTriggers).toHaveBeenCalledWith(client)
118
+ expect(fetchAssistants).toHaveBeenCalledTimes(1)
119
+ expect(migrateAssistantsWithoutFolder).toHaveBeenCalledWith(
120
+ client,
121
+ assistants
122
+ )
123
+ expect(ensureProvisionedAssistants).toHaveBeenCalledWith(client, entries, {
124
+ assistants
125
+ })
126
+ const order = [
127
+ createRagIndexTriggers,
128
+ migrateAssistantsWithoutFolder,
129
+ ensureProvisionedAssistants
130
+ ].map(fn => fn.mock.invocationCallOrder[0])
131
+ expect(order).toEqual([...order].sort((a, b) => a - b))
132
+ expect(result).toEqual({
133
+ setup: { triggers: ['io.cozy.files'], migrated: ['legacy'], errors: [] },
134
+ created: ['docs'],
135
+ ensured: [],
136
+ skipped: []
137
+ })
138
+ })
139
+
140
+ it('runs once per session, sharing the promise', async () => {
141
+ flag.mockReturnValue(entries)
142
+ const first = autoprovisionAssistants(client)
143
+ expect(autoprovisionAssistants(client)).toBe(first)
144
+ await first
145
+ await autoprovisionAssistants(client)
146
+
147
+ expect(createRagIndexTriggers).toHaveBeenCalledTimes(1)
148
+ expect(ensureProvisionedAssistants).toHaveBeenCalledTimes(1)
149
+ })
150
+
151
+ it('shares the triggers step with ensureRagIndexTriggers', async () => {
152
+ flag.mockReturnValue(entries)
153
+ await ensureRagIndexTriggers(client)
154
+ await autoprovisionAssistants(client)
155
+
156
+ expect(createRagIndexTriggers).toHaveBeenCalledTimes(1)
157
+ expect(ensureProvisionedAssistants).toHaveBeenCalledTimes(1)
158
+ })
159
+
160
+ it('still provisions when the setup steps failed', async () => {
161
+ flag.mockReturnValue(entries)
162
+ createRagIndexTriggers.mockRejectedValue(new Error('forbidden'))
163
+ migrateAssistantsWithoutFolder.mockRejectedValue(new Error('nope'))
164
+ const result = await autoprovisionAssistants(client)
165
+
166
+ expect(ensureProvisionedAssistants).toHaveBeenCalledTimes(1)
167
+ expect(result.setup.errors.map(e => e.message)).toEqual([
168
+ 'forbidden',
169
+ 'nope'
170
+ ])
171
+ })
172
+
173
+ it('logs the skipped entries', async () => {
174
+ flag.mockReturnValue(entries)
175
+ ensureProvisionedAssistants.mockResolvedValue({
176
+ created: [],
177
+ ensured: [],
178
+ skipped: [{ id: 'docs', reason: 'nope' }]
179
+ })
180
+ await autoprovisionAssistants(client)
181
+
182
+ expect(warnSpy).toHaveBeenCalledWith(
183
+ 'cozy-search autoprovision:',
184
+ 'skipped entries',
185
+ [{ id: 'docs', reason: 'nope' }]
186
+ )
187
+ })
188
+
189
+ it('never rejects', async () => {
190
+ flag.mockReturnValue(entries)
191
+ ensureProvisionedAssistants.mockRejectedValue(new Error('boom'))
192
+ await expect(autoprovisionAssistants(client)).resolves.toBeNull()
193
+ expect(warnSpy).toHaveBeenCalledWith(
194
+ 'cozy-search autoprovision:',
195
+ 'failed',
196
+ expect.any(Error)
197
+ )
198
+ })
199
+ })
@@ -1,4 +1,4 @@
1
- export const AUTOPROVISION_FLAG: "rag.assistants.autoprovision";
1
+ export const AUTOPROVISION_FLAG: "cozy.assistant.autoprovision";
2
2
  export function assistantIdFromName(name: any): string;
3
3
  export function getDefaultProvisionedAssistantId(): string | null;
4
4
  export function isMagicFolderId(dirId: any): boolean;
@@ -14,7 +14,9 @@ export function ensureProvisionedAssistants(client: import("cozy-client").CozyCl
14
14
  prompt?: string;
15
15
  icon?: string | null;
16
16
  default?: boolean;
17
- }>): Promise<{
17
+ }>, { assistants }?: {
18
+ assistants?: object[] | undefined;
19
+ } | undefined): Promise<{
18
20
  created: string[];
19
21
  ensured: string[];
20
22
  skipped: {