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
package/README.md CHANGED
@@ -34,15 +34,59 @@ import 'cozy-search/dist/stylesheet.css'
34
34
  "description": "Required by the cozy Assistant",
35
35
  "type": "io.cozy.ai.chat.events",
36
36
  "verbs": ["GET"]
37
+ },
38
+ "chatAssistants": {
39
+ "description": "Required by the cozy Assistant",
40
+ "type": "io.cozy.ai.chat.assistants",
41
+ "verbs": ["GET", "POST", "PUT", "DELETE"]
42
+ },
43
+ "accounts": {
44
+ "description": "Required by the cozy Assistant (provider accounts)",
45
+ "type": "io.cozy.accounts",
46
+ "verbs": ["GET", "POST", "PUT", "DELETE"]
37
47
  }
38
48
  ```
39
49
 
50
+ The assistants and their provider accounts are created, edited and
51
+ deleted from the library's dialogs, so the host app declares the full set
52
+ of verbs (cozy-home and cozy-drive use `ALL`).
53
+
40
54
  2. Add realtime queries for chat conversations in your tree :
41
55
 
42
56
  ```jsx
43
57
  <RealTimeQueries doctype="io.cozy.ai.chat.conversations" />
44
58
  ```
45
59
 
60
+ ### RAG indexing setup
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
65
+ `rag-index` triggers exist (one on `io.cozy.files`, one on
66
+ `io.cozy.ai.chat.assistants`) and gives the root folder to any assistant
67
+ that has no knowledge base folder yet — the stack's `rag-index` worker
68
+ reads the assistants to know what to index, cozy-search only has to keep
69
+ the triggers and the assistants' `knowledgeBase` in shape.
70
+
71
+ It needs the following permissions: `io.cozy.triggers` and `io.cozy.jobs`
72
+ (to create and launch the triggers), `io.cozy.ai.chat.assistants` and
73
+ `io.cozy.files` (to read and migrate the assistants). It is idempotent and
74
+ never throws: on a stack whose `rag-index` worker is still reserved, the
75
+ 403 is logged and the app keeps working. `ensureProvisionedAssistants`
76
+ additionally creates one `io.cozy.accounts` document per provisioned
77
+ assistant, and the `dirName` folder in `io.cozy.files`.
78
+
79
+ An entry of `rag.assistants.autoprovision` can carry `"default": true`
80
+ (the first flagged entry wins if there are several). Every NEW
81
+ conversation then starts on that assistant once its document exists —
82
+ from the session after it was provisioned onward. Existing conversations
83
+ are unaffected: they keep their own assistant, or stay unscoped if they
84
+ had none.
85
+
86
+ ```json
87
+ { "name": "Mes documents", "dirName": "Documents", "default": true }
88
+ ```
89
+
46
90
  ### On desktop
47
91
 
48
92
  You can add the search bar like this :
@@ -165,7 +165,7 @@ var AssistantSelection = function AssistantSelection(_ref) {
165
165
  assistant: _constants.DEFAULT_ASSISTANT,
166
166
  onClose: handleClose,
167
167
  onSelect: function onSelect() {
168
- return setSelectedAssistantId(_constants.DEFAULT_ASSISTANT.id);
168
+ return setSelectedAssistantId(_constants.DEFAULT_ASSISTANT._id);
169
169
  },
170
170
  selectedAssistant: selectedAssistant,
171
171
  disableActions: true
@@ -43,12 +43,14 @@ var _twakeI18n = require("twake-i18n");
43
43
 
44
44
  var _AssistantProvider = require("./AssistantProvider");
45
45
 
46
+ var _conversationAssistant = require("./KnowledgeBase/conversationAssistant");
47
+
48
+ var _useDefaultAssistantId = require("./KnowledgeBase/useDefaultAssistantId");
49
+
46
50
  var _CozyRealtimeChatAdapter = require("./adapters/CozyRealtimeChatAdapter");
47
51
 
48
52
  var _StreamBridge = require("./adapters/StreamBridge");
49
53
 
50
- var _constants = require("./constants");
51
-
52
54
  var _helpers = require("./helpers");
53
55
 
54
56
  var _queries = require("./queries");
@@ -80,7 +82,7 @@ var convertMessagesToThreadMessages = function convertMessagesToThreadMessages(m
80
82
  };
81
83
 
82
84
  var ConversationLoader = function ConversationLoader(_ref) {
83
- var _conversation$relatio4, _conversation$relatio5, _conversation$relatio6;
85
+ var _conversation$relatio, _conversation$relatio2, _conversation$relatio3;
84
86
 
85
87
  var children = _ref.children,
86
88
  conversationId = _ref.conversationId;
@@ -95,16 +97,25 @@ var ConversationLoader = function ConversationLoader(_ref) {
95
97
  var queryResult = (0, _cozyClient.useQuery)(conversationQuery.definition, conversationQuery.options);
96
98
  var conversation = queryResult.data;
97
99
  var isLoading = (0, _cozyClient.isQueryLoading)(queryResult);
100
+ var boundId = conversation === null || conversation === void 0 ? void 0 : (_conversation$relatio = conversation.relationships) === null || _conversation$relatio === void 0 ? void 0 : (_conversation$relatio2 = _conversation$relatio.assistant) === null || _conversation$relatio2 === void 0 ? void 0 : (_conversation$relatio3 = _conversation$relatio2.data) === null || _conversation$relatio3 === void 0 ? void 0 : _conversation$relatio3._id;
101
+ var defaultId = (0, _useDefaultAssistantId.useDefaultAssistantId)();
98
102
  var initialMessages = (0, _react2.useMemo)(function () {
99
103
  return convertMessagesToThreadMessages(conversation === null || conversation === void 0 ? void 0 : conversation.messages, t);
100
- }, [conversation === null || conversation === void 0 ? void 0 : conversation.messages, t]);
104
+ }, [conversation === null || conversation === void 0 ? void 0 : conversation.messages, t]); // Undefined until the conversation, and the default a new one starts
105
+ // on, are known: the runtime waits, or a message could go to the
106
+ // sentinel assistant before the configured default is selected.
107
+
108
+ var resolvedAssistantId = (0, _conversationAssistant.resolveConversationAssistantId)({
109
+ boundId: boundId,
110
+ conversation: conversation,
111
+ isLoading: isLoading,
112
+ defaultId: defaultId
113
+ });
101
114
  (0, _react2.useEffect)(function () {
102
- var _conversation$relatio, _conversation$relatio2, _conversation$relatio3;
103
-
104
- setSelectedAssistantId((conversation === null || conversation === void 0 ? void 0 : (_conversation$relatio = conversation.relationships) === null || _conversation$relatio === void 0 ? void 0 : (_conversation$relatio2 = _conversation$relatio.assistant) === null || _conversation$relatio2 === void 0 ? void 0 : (_conversation$relatio3 = _conversation$relatio2.data) === null || _conversation$relatio3 === void 0 ? void 0 : _conversation$relatio3._id) || _constants.DEFAULT_ASSISTANT._id);
105
- }, [conversation === null || conversation === void 0 ? void 0 : (_conversation$relatio4 = conversation.relationships) === null || _conversation$relatio4 === void 0 ? void 0 : (_conversation$relatio5 = _conversation$relatio4.assistant) === null || _conversation$relatio5 === void 0 ? void 0 : (_conversation$relatio6 = _conversation$relatio5.data) === null || _conversation$relatio6 === void 0 ? void 0 : _conversation$relatio6._id, setSelectedAssistantId]);
115
+ if (resolvedAssistantId) setSelectedAssistantId(resolvedAssistantId);
116
+ }, [resolvedAssistantId, setSelectedAssistantId]);
106
117
 
107
- if (isLoading) {
118
+ if (resolvedAssistantId === undefined) {
108
119
  return /*#__PURE__*/_react2.default.createElement("div", {
109
120
  className: "u-flex u-flex-items-center u-flex-justify-center u-h-100 u-w-100"
110
121
  }, /*#__PURE__*/_react2.default.createElement(_Spinner.default, {
@@ -7,9 +7,10 @@ export default KnowledgeBaseChip;
7
7
  * open the folder in Drive (new tab) or change the knowledge base through
8
8
  * the Drive folder picker (persisted on the assistant immediately).
9
9
  */
10
- declare function KnowledgeBaseChip({ dirId, folder, isUnavailable, isLast, onChangeFolder }: {
10
+ declare function KnowledgeBaseChip({ dirId, folder, isRoot, isUnavailable, isLast, onChangeFolder }: {
11
11
  dirId: any;
12
12
  folder: any;
13
+ isRoot: any;
13
14
  isUnavailable: any;
14
15
  isLast: any;
15
16
  onChangeFolder: any;
@@ -48,6 +48,7 @@ var KnowledgeBaseChip = function KnowledgeBaseChip(_ref) {
48
48
 
49
49
  var dirId = _ref.dirId,
50
50
  folder = _ref.folder,
51
+ isRoot = _ref.isRoot,
51
52
  isUnavailable = _ref.isUnavailable,
52
53
  isLast = _ref.isLast,
53
54
  onChangeFolder = _ref.onChangeFolder;
@@ -83,7 +84,7 @@ var KnowledgeBaseChip = function KnowledgeBaseChip(_ref) {
83
84
  subDomainType: client === null || client === void 0 ? void 0 : client.getInstanceOptions().subdomain,
84
85
  hash: "/folder/".concat(dirId)
85
86
  });
86
- var label = isUnavailable ? t('assistant.knowledge_base.unavailable') : (_folder$name = folder === null || folder === void 0 ? void 0 : folder.name) !== null && _folder$name !== void 0 ? _folder$name : '…';
87
+ var label = isUnavailable ? t('assistant.knowledge_base.unavailable') : isRoot ? t('assistant.twake_knowledges.drive') : (_folder$name = folder === null || folder === void 0 ? void 0 : folder.name) !== null && _folder$name !== void 0 ? _folder$name : '…';
87
88
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", {
88
89
  ref: chipRef,
89
90
  className: (0, _classnames.default)({
@@ -102,7 +102,7 @@ var KnowledgeBaseSection = function KnowledgeBaseSection(_ref) {
102
102
  src: _tdrive.default,
103
103
  width: 16
104
104
  }),
105
- label: isUnavailable ? t('assistant.knowledge_base.unavailable') : (_folder$name = folder === null || folder === void 0 ? void 0 : folder.name) !== null && _folder$name !== void 0 ? _folder$name : '…',
105
+ label: isUnavailable ? t('assistant.knowledge_base.unavailable') : (0, _knowledgeBase.isRootDirId)(dirId) ? t('assistant.twake_knowledges.drive') : (_folder$name = folder === null || folder === void 0 ? void 0 : folder.name) !== null && _folder$name !== void 0 ? _folder$name : '…',
106
106
  deleteIcon: /*#__PURE__*/_react.default.createElement(_twakeIcons.Icon, {
107
107
  icon: _twakeIcons.Cross,
108
108
  size: 10 // inline width/height beats MUI v4's fixed deleteIcon class size
@@ -0,0 +1,6 @@
1
+ export function resolveConversationAssistantId({ boundId, conversation, isLoading, defaultId }: {
2
+ boundId: string | undefined;
3
+ conversation: object | undefined;
4
+ isLoading: boolean;
5
+ defaultId: string | null | undefined;
6
+ }): string | undefined;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.resolveConversationAssistantId = void 0;
7
+
8
+ var _constants = require("../constants");
9
+
10
+ /**
11
+ * Which assistant a loaded conversation selects. Undefined means "leave the
12
+ * selection alone" (the conversation, or the default a new one starts on,
13
+ * is still loading).
14
+ * @param {object} params
15
+ * @param {string|undefined} params.boundId - The conversation's bound
16
+ * assistant id (`conversation?.relationships?.assistant?.data?._id`).
17
+ * @param {object|undefined} params.conversation - The conversation document,
18
+ * or undefined when it does not exist (yet).
19
+ * @param {boolean} params.isLoading - Whether the conversation query is
20
+ * still loading.
21
+ * @param {string|null|undefined} params.defaultId - The configured default
22
+ * assistant id, only when its document is known to exist; undefined while
23
+ * that is being checked.
24
+ * @returns {string|undefined}
25
+ */
26
+ var resolveConversationAssistantId = function resolveConversationAssistantId(_ref) {
27
+ var boundId = _ref.boundId,
28
+ conversation = _ref.conversation,
29
+ isLoading = _ref.isLoading,
30
+ defaultId = _ref.defaultId;
31
+ if (boundId) return boundId;
32
+ if (isLoading) return undefined;
33
+
34
+ if (!conversation) {
35
+ if (defaultId === undefined) return undefined;
36
+ return defaultId || _constants.DEFAULT_ASSISTANT._id;
37
+ }
38
+
39
+ return _constants.DEFAULT_ASSISTANT._id;
40
+ };
41
+
42
+ exports.resolveConversationAssistantId = resolveConversationAssistantId;
@@ -0,0 +1,80 @@
1
+ import { resolveConversationAssistantId } from './conversationAssistant'
2
+
3
+ describe('resolveConversationAssistantId', () => {
4
+ it('selects the bound assistant, regardless of anything else', () => {
5
+ expect(
6
+ resolveConversationAssistantId({
7
+ boundId: 'bound',
8
+ conversation: { _id: 'c1' },
9
+ isLoading: false,
10
+ defaultId: 'docs'
11
+ })
12
+ ).toBe('bound')
13
+ })
14
+
15
+ it('leaves the selection alone while the conversation query is loading', () => {
16
+ expect(
17
+ resolveConversationAssistantId({
18
+ boundId: undefined,
19
+ conversation: undefined,
20
+ isLoading: true,
21
+ defaultId: 'docs'
22
+ })
23
+ ).toBeUndefined()
24
+ })
25
+
26
+ it('leaves the selection alone while the default of a new conversation is being checked', () => {
27
+ expect(
28
+ resolveConversationAssistantId({
29
+ boundId: undefined,
30
+ conversation: undefined,
31
+ isLoading: false,
32
+ defaultId: undefined
33
+ })
34
+ ).toBeUndefined()
35
+ })
36
+
37
+ it('does not wait for the default on an existing conversation', () => {
38
+ expect(
39
+ resolveConversationAssistantId({
40
+ boundId: undefined,
41
+ conversation: { _id: 'c1' },
42
+ isLoading: false,
43
+ defaultId: undefined
44
+ })
45
+ ).toBe('default_ai_assistant')
46
+ })
47
+
48
+ it('selects the default for a new conversation when one is configured', () => {
49
+ expect(
50
+ resolveConversationAssistantId({
51
+ boundId: undefined,
52
+ conversation: undefined,
53
+ isLoading: false,
54
+ defaultId: 'docs'
55
+ })
56
+ ).toBe('docs')
57
+ })
58
+
59
+ it('selects the sentinel for a new conversation without a configured default', () => {
60
+ expect(
61
+ resolveConversationAssistantId({
62
+ boundId: undefined,
63
+ conversation: undefined,
64
+ isLoading: false,
65
+ defaultId: null
66
+ })
67
+ ).toBe('default_ai_assistant')
68
+ })
69
+
70
+ it('selects the sentinel for an existing conversation without an assistant, even with a default configured', () => {
71
+ expect(
72
+ resolveConversationAssistantId({
73
+ boundId: undefined,
74
+ conversation: { _id: 'c1' },
75
+ isLoading: false,
76
+ defaultId: 'docs'
77
+ })
78
+ ).toBe('default_ai_assistant')
79
+ })
80
+ })
@@ -1,3 +1,11 @@
1
+ /** The root folder of the instance: the assistant covers the whole Drive. */
2
+ export const ROOT_DIR_ID: "io.cozy.files.root-dir";
3
+ export function isRootDirId(dirId: any): boolean;
4
+ export function makeRootKnowledgeBaseEntry(): {
5
+ doctype: string;
6
+ dirId: string;
7
+ };
8
+ export function withRootFolderIfMissing(knowledgeBase?: any[]): any[];
1
9
  export function makeKnowledgeBaseEntry(pickedFolder: any): {
2
10
  doctype: string;
3
11
  dirId: any;
@@ -5,7 +5,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
5
5
  Object.defineProperty(exports, "__esModule", {
6
6
  value: true
7
7
  });
8
- exports.withoutKnowledgeBaseDoctype = exports.withKnowledgeBaseEntry = exports.saveKnowledgeBase = exports.makeKnowledgeBaseEntry = exports.makeEmailKnowledgeBaseEntry = exports.hasEmailKnowledgeBase = exports.getKnowledgeBaseDirId = void 0;
8
+ exports.withoutKnowledgeBaseDoctype = exports.withRootFolderIfMissing = exports.withKnowledgeBaseEntry = exports.saveKnowledgeBase = exports.makeRootKnowledgeBaseEntry = exports.makeKnowledgeBaseEntry = exports.makeEmailKnowledgeBaseEntry = exports.isRootDirId = exports.hasEmailKnowledgeBase = exports.getKnowledgeBaseDirId = exports.ROOT_DIR_ID = void 0;
9
9
 
10
10
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
11
11
 
@@ -15,15 +15,54 @@ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/
15
15
 
16
16
  var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
17
17
 
18
- var _cozyClient = require("cozy-client");
19
-
20
18
  var _queries = require("../queries");
21
19
 
22
20
  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
21
 
24
22
  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
23
 
26
- var ASSISTANTS_DOCTYPE = 'io.cozy.ai.chat.assistants';
24
+ /** The root folder of the instance: the assistant covers the whole Drive. */
25
+ var ROOT_DIR_ID = 'io.cozy.files.root-dir';
26
+ exports.ROOT_DIR_ID = ROOT_DIR_ID;
27
+
28
+ var isRootDirId = function isRootDirId(dirId) {
29
+ return dirId === ROOT_DIR_ID;
30
+ };
31
+
32
+ exports.isRootDirId = isRootDirId;
33
+
34
+ var makeRootKnowledgeBaseEntry = function makeRootKnowledgeBaseEntry() {
35
+ return {
36
+ doctype: _queries.FILES_DOCTYPE,
37
+ dirId: ROOT_DIR_ID
38
+ };
39
+ };
40
+
41
+ exports.makeRootKnowledgeBaseEntry = makeRootKnowledgeBaseEntry;
42
+
43
+ var isFolderEntry = function isFolderEntry(entry) {
44
+ return entry.doctype === _queries.FILES_DOCTYPE && !!entry.dirId;
45
+ };
46
+
47
+ var hasFolderEntry = function hasFolderEntry(knowledgeBase) {
48
+ return (knowledgeBase || []).some(isFolderEntry);
49
+ };
50
+ /**
51
+ * An assistant always has a knowledge base folder: without one, it covers
52
+ * the whole Drive. Adds the root entry when no folder entry exists,
53
+ * replacing a files entry without dirId (getKnowledgeBaseDirId would find
54
+ * that one first); returns the very same array otherwise.
55
+ */
56
+
57
+
58
+ var withRootFolderIfMissing = function withRootFolderIfMissing() {
59
+ var knowledgeBase = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
60
+ return hasFolderEntry(knowledgeBase) ? knowledgeBase : [].concat((0, _toConsumableArray2.default)(knowledgeBase.filter(function (entry) {
61
+ return entry.doctype !== _queries.FILES_DOCTYPE;
62
+ })), [makeRootKnowledgeBaseEntry()]);
63
+ };
64
+
65
+ exports.withRootFolderIfMissing = withRootFolderIfMissing;
27
66
 
28
67
  var makeKnowledgeBaseEntry = function makeKnowledgeBaseEntry(pickedFolder) {
29
68
  return {
@@ -90,6 +129,10 @@ var getKnowledgeBaseDirId = function getKnowledgeBaseDirId(assistant) {
90
129
  * applied to the freshly fetched doc's knowledgeBase. Computing the update
91
130
  * from the fresh doc rather than a value the caller may have cached avoids
92
131
  * dropping concurrent changes (stale-read race).
132
+ *
133
+ * The saved knowledge base always has a folder entry (the root when none
134
+ * was chosen): the stack's rag-index worker reads the assistants to know
135
+ * what to index, there is nothing else to do here.
93
136
  */
94
137
 
95
138
 
@@ -97,25 +140,28 @@ exports.getKnowledgeBaseDirId = getKnowledgeBaseDirId;
97
140
 
98
141
  var saveKnowledgeBase = /*#__PURE__*/function () {
99
142
  var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(client, assistantId, knowledgeBaseOrUpdater) {
100
- var _yield$client$query, assistant, knowledgeBase;
143
+ var _buildAssistantByIdQu, definition, options, _yield$client$query, assistant, knowledgeBase;
101
144
 
102
145
  return _regenerator.default.wrap(function _callee$(_context) {
103
146
  while (1) {
104
147
  switch (_context.prev = _context.next) {
105
148
  case 0:
106
- _context.next = 2;
107
- return client.query((0, _cozyClient.Q)(ASSISTANTS_DOCTYPE).getById(assistantId));
149
+ _buildAssistantByIdQu = (0, _queries.buildAssistantByIdQuery)(assistantId), definition = _buildAssistantByIdQu.definition, options = _buildAssistantByIdQu.options;
150
+ _context.next = 3;
151
+ return client.query(definition(), {
152
+ as: options.as
153
+ });
108
154
 
109
- case 2:
155
+ case 3:
110
156
  _yield$client$query = _context.sent;
111
157
  assistant = _yield$client$query.data;
112
158
  knowledgeBase = typeof knowledgeBaseOrUpdater === 'function' ? knowledgeBaseOrUpdater(assistant === null || assistant === void 0 ? void 0 : assistant.knowledgeBase) : knowledgeBaseOrUpdater;
113
- _context.next = 7;
159
+ _context.next = 8;
114
160
  return client.save(_objectSpread(_objectSpread({}, assistant), {}, {
115
- knowledgeBase: knowledgeBase
161
+ knowledgeBase: withRootFolderIfMissing(knowledgeBase)
116
162
  }));
117
163
 
118
- case 7:
164
+ case 8:
119
165
  case "end":
120
166
  return _context.stop();
121
167
  }
@@ -5,6 +5,9 @@ import {
5
5
  withKnowledgeBaseEntry,
6
6
  withoutKnowledgeBaseDoctype,
7
7
  getKnowledgeBaseDirId,
8
+ ROOT_DIR_ID,
9
+ isRootDirId,
10
+ withRootFolderIfMissing,
8
11
  saveKnowledgeBase
9
12
  } from './knowledgeBase'
10
13
 
@@ -125,6 +128,43 @@ describe('getKnowledgeBaseDirId', () => {
125
128
  })
126
129
  })
127
130
 
131
+ describe('withRootFolderIfMissing', () => {
132
+ it('appends the root entry when there is no files entry', () => {
133
+ expect(withRootFolderIfMissing([{ doctype: 'io.cozy.email' }])).toEqual([
134
+ { doctype: 'io.cozy.email' },
135
+ { doctype: 'io.cozy.files', dirId: ROOT_DIR_ID }
136
+ ])
137
+ expect(withRootFolderIfMissing()).toEqual([
138
+ { doctype: 'io.cozy.files', dirId: ROOT_DIR_ID }
139
+ ])
140
+ })
141
+
142
+ it('returns the same array when a files entry exists', () => {
143
+ const kb = [{ doctype: 'io.cozy.files', dirId: 'folder-1' }]
144
+ expect(withRootFolderIfMissing(kb)).toBe(kb)
145
+ })
146
+
147
+ it('replaces a files entry without dirId, keeping the other entries', () => {
148
+ expect(
149
+ withRootFolderIfMissing([
150
+ { doctype: 'io.cozy.email' },
151
+ { doctype: 'io.cozy.files' }
152
+ ])
153
+ ).toEqual([
154
+ { doctype: 'io.cozy.email' },
155
+ { doctype: 'io.cozy.files', dirId: ROOT_DIR_ID }
156
+ ])
157
+ })
158
+ })
159
+
160
+ describe('isRootDirId', () => {
161
+ it('recognizes the root folder id only', () => {
162
+ expect(isRootDirId(ROOT_DIR_ID)).toBe(true)
163
+ expect(isRootDirId('folder-1')).toBe(false)
164
+ expect(isRootDirId(null)).toBe(false)
165
+ })
166
+ })
167
+
128
168
  describe('saveKnowledgeBase', () => {
129
169
  it('refetches the assistant and saves it with the new knowledgeBase', async () => {
130
170
  const assistantDoc = {
@@ -174,4 +214,56 @@ describe('saveKnowledgeBase', () => {
174
214
  ]
175
215
  })
176
216
  })
217
+
218
+ it('saves the root entry when the knowledge base has no folder', async () => {
219
+ const assistantDoc = {
220
+ _id: 'assistant-1',
221
+ _type: 'io.cozy.ai.chat.assistants'
222
+ }
223
+ const client = {
224
+ query: jest.fn().mockResolvedValue({ data: assistantDoc }),
225
+ save: jest.fn().mockResolvedValue({ data: assistantDoc })
226
+ }
227
+
228
+ await saveKnowledgeBase(client, 'assistant-1', [
229
+ { doctype: 'io.cozy.email' }
230
+ ])
231
+
232
+ expect(client.save).toHaveBeenCalledWith({
233
+ ...assistantDoc,
234
+ knowledgeBase: [
235
+ { doctype: 'io.cozy.email' },
236
+ { doctype: 'io.cozy.files', dirId: ROOT_DIR_ID }
237
+ ]
238
+ })
239
+ })
240
+
241
+ it('applies the root invariant to the updater result too', async () => {
242
+ const assistantDoc = {
243
+ _id: 'assistant-1',
244
+ _type: 'io.cozy.ai.chat.assistants',
245
+ knowledgeBase: [{ doctype: 'io.cozy.files', dirId: 'folder-1' }]
246
+ }
247
+ const client = {
248
+ query: jest.fn().mockResolvedValue({ data: assistantDoc }),
249
+ save: jest.fn().mockResolvedValue({ data: assistantDoc })
250
+ }
251
+
252
+ await saveKnowledgeBase(client, 'assistant-1', () => [])
253
+
254
+ expect(client.save).toHaveBeenCalledWith({
255
+ ...assistantDoc,
256
+ knowledgeBase: [{ doctype: 'io.cozy.files', dirId: ROOT_DIR_ID }]
257
+ })
258
+ })
259
+
260
+ it('propagates a save failure', async () => {
261
+ const client = {
262
+ query: jest.fn().mockResolvedValue({ data: { _id: 'assistant-1' } }),
263
+ save: jest.fn().mockRejectedValue(new Error('boom'))
264
+ }
265
+ await expect(saveKnowledgeBase(client, 'assistant-1', [])).rejects.toThrow(
266
+ 'boom'
267
+ )
268
+ })
177
269
  })
@@ -0,0 +1,24 @@
1
+ export const AUTOPROVISION_FLAG: "rag.assistants.autoprovision";
2
+ export function assistantIdFromName(name: any): string;
3
+ export function getDefaultProvisionedAssistantId(): string | null;
4
+ export function isMagicFolderId(dirId: any): boolean;
5
+ export function resolveProvisionedFolder(client: any, { dirId, dirName }: {
6
+ dirId: any;
7
+ dirName: any;
8
+ }): Promise<any>;
9
+ export function createOpenragAccount(client: any): Promise<any>;
10
+ export function ensureProvisionedAssistants(client: import("cozy-client").CozyClient, configs: Array<{
11
+ name: string;
12
+ dirName?: string;
13
+ dirId?: string;
14
+ prompt?: string;
15
+ icon?: string | null;
16
+ default?: boolean;
17
+ }>): Promise<{
18
+ created: string[];
19
+ ensured: string[];
20
+ skipped: {
21
+ id: string;
22
+ reason: string;
23
+ }[];
24
+ }>;