@truedat/ai 8.14.2 → 8.14.3

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.
@@ -0,0 +1,336 @@
1
+ import _ from "lodash/fp";
2
+ import PropTypes from "prop-types";
3
+ import { useState } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import {
6
+ Button,
7
+ Checkbox,
8
+ Dropdown,
9
+ Form,
10
+ Header,
11
+ Label,
12
+ Message,
13
+ Table,
14
+ } from "semantic-ui-react";
15
+ import ConfirmModal from "@truedat/core/components/ConfirmModal";
16
+ import {
17
+ useIndices,
18
+ useIndexCreate,
19
+ useIndexDisable,
20
+ useIndexEnable,
21
+ } from "@truedat/ai/hooks/useIndices";
22
+ import { serverErrors } from "../../serverErrors";
23
+ import HelpIcon from "./HelpIcon";
24
+
25
+ const purposes = ["suggestions", "rag"];
26
+ const messagePrefix = "providers.embeddingModels";
27
+
28
+ const purposeColors = { suggestions: "teal", rag: "violet" };
29
+ const collectionPattern = /^[a-z][a-z0-9_]*$/;
30
+ const collectionMaxLength = 60;
31
+ const isValidCollection = (name) =>
32
+ name.length <= collectionMaxLength && collectionPattern.test(name);
33
+
34
+ const columns = [
35
+ { key: "embedding", label: `${messagePrefix}.form.embedding` },
36
+ { key: "collection", label: `${messagePrefix}.form.collection` },
37
+ { key: "purpose", label: `${messagePrefix}.form.purpose` },
38
+ { key: "dims", label: `${messagePrefix}.form.dims` },
39
+ { key: "enabled", label: "providers.form.enabled" },
40
+ ];
41
+
42
+ const firstError = (errors, key) => _.head(errors[key]);
43
+
44
+ function AddEmbeddingModel({ provider, onCreated, onCancel }) {
45
+ const { formatMessage } = useIntl();
46
+ const { trigger: createIndex, isMutating } = useIndexCreate();
47
+ const [embedding, setEmbedding] = useState("");
48
+ const [purpose, setPurpose] = useState("suggestions");
49
+ const [collection, setCollection] = useState("");
50
+ const [dims, setDims] = useState("");
51
+ const [errors, setErrors] = useState({});
52
+ const clearErrors = (...keys) => setErrors(_.omit(keys));
53
+
54
+ const purposeOptions = purposes.map((value) => ({
55
+ key: value,
56
+ value,
57
+ text: formatMessage({ id: `${messagePrefix}.purpose.${value}` }),
58
+ }));
59
+
60
+ const dimsNumber = parseInt(dims, 10);
61
+ const index = {
62
+ embedding,
63
+ collection_name: collection,
64
+ index_type: purpose,
65
+ provider_id: provider.id,
66
+ ...(_.isNaN(dimsNumber) ? {} : { index_params: { dims: dimsNumber } }),
67
+ };
68
+
69
+ const onSave = async () => {
70
+ try {
71
+ await createIndex({ index });
72
+ onCreated();
73
+ } catch (error) {
74
+ setErrors(serverErrors(error));
75
+ }
76
+ };
77
+
78
+ const fieldLabel = (key) => (
79
+ <label>
80
+ {formatMessage({ id: `${messagePrefix}.form.${key}` })}
81
+ <HelpIcon
82
+ id={`${key}`}
83
+ content={formatMessage({ id: `${messagePrefix}.form.${key}.hint` })}
84
+ />
85
+ </label>
86
+ );
87
+
88
+ const handledKeys = ["embedding", "collection_name", "index_params"];
89
+ const otherErrors = _.flow(_.omit(handledKeys), _.values, _.flatten)(errors);
90
+
91
+ return (
92
+ <Form as="div">
93
+ <Form.Input
94
+ required
95
+ autoComplete="off"
96
+ label={fieldLabel("embedding")}
97
+ placeholder={formatMessage({ id: `${messagePrefix}.form.embedding` })}
98
+ value={embedding}
99
+ error={firstError(errors, "embedding")}
100
+ onChange={(_e, { value }) => {
101
+ setEmbedding(value);
102
+ // The dimensions check depends on which model it is
103
+ clearErrors("embedding", "index_params");
104
+ }}
105
+ />
106
+ <Form.Input
107
+ required
108
+ autoComplete="off"
109
+ label={fieldLabel("collection")}
110
+ placeholder={formatMessage({
111
+ id: `${messagePrefix}.form.collection.placeholder`,
112
+ })}
113
+ value={collection}
114
+ error={firstError(errors, "collection_name")}
115
+ onChange={(_e, { value }) => {
116
+ setCollection(value);
117
+ clearErrors("collection_name");
118
+ }}
119
+ />
120
+ <Form.Field required>
121
+ {fieldLabel("purpose")}
122
+ <Dropdown
123
+ selection
124
+ options={purposeOptions}
125
+ value={purpose}
126
+ onChange={(_e, { value }) => {
127
+ setPurpose(value);
128
+ clearErrors("index_type");
129
+ }}
130
+ />
131
+ </Form.Field>
132
+ <Form.Input
133
+ type="number"
134
+ min={1}
135
+ autoComplete="off"
136
+ label={fieldLabel("dims")}
137
+ placeholder={formatMessage({ id: `${messagePrefix}.form.dims` })}
138
+ value={dims}
139
+ error={firstError(errors, "index_params")}
140
+ onChange={(_e, { value }) => {
141
+ setDims(value);
142
+ clearErrors("index_params");
143
+ }}
144
+ />
145
+ {_.isEmpty(otherErrors) ? null : (
146
+ <Message
147
+ negative
148
+ list={otherErrors}
149
+ onDismiss={() => setErrors(_.pick(handledKeys))}
150
+ />
151
+ )}
152
+ <Button
153
+ primary
154
+ loading={isMutating}
155
+ disabled={
156
+ _.isEmpty(embedding) || !isValidCollection(collection) || isMutating
157
+ }
158
+ onClick={onSave}
159
+ content={formatMessage({ id: "actions.save" })}
160
+ />
161
+ <Button
162
+ onClick={onCancel}
163
+ content={formatMessage({ id: "actions.cancel" })}
164
+ />
165
+ </Form>
166
+ );
167
+ }
168
+
169
+ AddEmbeddingModel.propTypes = {
170
+ provider: PropTypes.object.isRequired,
171
+ onCreated: PropTypes.func.isRequired,
172
+ onCancel: PropTypes.func.isRequired,
173
+ };
174
+
175
+ export default function EmbeddingModels({ provider }) {
176
+ const { formatMessage } = useIntl();
177
+ const { data, mutate } = useIndices();
178
+ const { trigger: enableIndex } = useIndexEnable();
179
+ const { trigger: disableIndex } = useIndexDisable();
180
+ const [adding, setAdding] = useState(false);
181
+ const [notices, setNotices] = useState([]);
182
+ const [disabling, setDisabling] = useState(null);
183
+ const [toggleErrors, setToggleErrors] = useState([]);
184
+
185
+ const unsupported = provider?.type === "anthropic";
186
+ const models = _.filter((i) => i.provider === provider?.id)(data);
187
+
188
+ const onEnable = async (model) => {
189
+ setToggleErrors([]);
190
+ try {
191
+ await enableIndex({ id: model.id });
192
+ setNotices((current) => [
193
+ ..._.reject({ id: model.id })(current),
194
+ { id: model.id, purpose: model.index_type },
195
+ ]);
196
+ } catch (error) {
197
+ setToggleErrors(_.flatten(_.values(serverErrors(error))));
198
+ }
199
+ mutate();
200
+ };
201
+ const onDisable = async (model) => {
202
+ setToggleErrors([]);
203
+ try {
204
+ await disableIndex({ id: model.id });
205
+ setNotices(_.reject({ id: model.id }));
206
+ } catch (error) {
207
+ setToggleErrors(_.flatten(_.values(serverErrors(error))));
208
+ }
209
+ mutate();
210
+ };
211
+ return (
212
+ <>
213
+ <Header as="h4" dividing>
214
+ {formatMessage({ id: `${messagePrefix}.title` })}
215
+ <HelpIcon
216
+ id="locked"
217
+ content={formatMessage({ id: `${messagePrefix}.locked` })}
218
+ />
219
+ </Header>
220
+ {unsupported ? (
221
+ <Message info>
222
+ {formatMessage({ id: `${messagePrefix}.unsupported` })}
223
+ </Message>
224
+ ) : (
225
+ <>
226
+ {notices.map(({ id, purpose }) => (
227
+ <Message
228
+ key={id}
229
+ warning
230
+ onDismiss={() => setNotices(_.reject({ id }))}
231
+ content={formatMessage({
232
+ id: `${messagePrefix}.backfill.${purpose}`,
233
+ })}
234
+ />
235
+ ))}
236
+ {_.isEmpty(toggleErrors) ? null : (
237
+ <Message
238
+ negative
239
+ list={toggleErrors}
240
+ onDismiss={() => setToggleErrors([])}
241
+ />
242
+ )}
243
+ {_.isEmpty(models) ? (
244
+ <p>{formatMessage({ id: `${messagePrefix}.empty` })}</p>
245
+ ) : (
246
+ <>
247
+ <Table basic="very" compact>
248
+ <Table.Header>
249
+ <Table.Row>
250
+ {columns.map(({ key, label }) => (
251
+ <Table.HeaderCell
252
+ key={key}
253
+ textAlign={key === "enabled" ? "right" : undefined}
254
+ >
255
+ {formatMessage({ id: label })}
256
+ </Table.HeaderCell>
257
+ ))}
258
+ </Table.Row>
259
+ </Table.Header>
260
+ <Table.Body>
261
+ {models.map((model) => (
262
+ <Table.Row key={model.id}>
263
+ <Table.Cell>{model.embedding}</Table.Cell>
264
+ <Table.Cell>{model.collection_name}</Table.Cell>
265
+ <Table.Cell>
266
+ <Label
267
+ size="small"
268
+ color={purposeColors[model.index_type]}
269
+ >
270
+ {formatMessage({
271
+ id: `${messagePrefix}.purpose.${model.index_type}`,
272
+ })}
273
+ </Label>
274
+ </Table.Cell>
275
+ <Table.Cell>{model.index_params?.dims}</Table.Cell>
276
+ <Table.Cell textAlign="right">
277
+ <Checkbox
278
+ toggle
279
+ checked={!!model.enabled_at}
280
+ aria-label={formatMessage({
281
+ id: `${messagePrefix}.${
282
+ model.enabled_at ? "disable" : "enable"
283
+ }`,
284
+ })}
285
+ onChange={() =>
286
+ model.enabled_at
287
+ ? setDisabling(model)
288
+ : onEnable(model)
289
+ }
290
+ />
291
+ </Table.Cell>
292
+ </Table.Row>
293
+ ))}
294
+ </Table.Body>
295
+ </Table>
296
+ </>
297
+ )}
298
+ <ConfirmModal
299
+ open={!!disabling}
300
+ header={formatMessage({
301
+ id: `${messagePrefix}.disable.confirm.header`,
302
+ })}
303
+ content={formatMessage({
304
+ id: `${messagePrefix}.disable.confirm.content`,
305
+ })}
306
+ onClose={() => setDisabling(null)}
307
+ onConfirm={() => {
308
+ const model = disabling;
309
+ setDisabling(null);
310
+ onDisable(model);
311
+ }}
312
+ />
313
+ {adding ? (
314
+ <AddEmbeddingModel
315
+ provider={provider}
316
+ onCancel={() => setAdding(false)}
317
+ onCreated={() => {
318
+ setAdding(false);
319
+ mutate();
320
+ }}
321
+ />
322
+ ) : (
323
+ <Button
324
+ onClick={() => setAdding(true)}
325
+ content={formatMessage({ id: `${messagePrefix}.add` })}
326
+ />
327
+ )}
328
+ </>
329
+ )}
330
+ </>
331
+ );
332
+ }
333
+
334
+ EmbeddingModels.propTypes = {
335
+ provider: PropTypes.object,
336
+ };
@@ -0,0 +1,38 @@
1
+ import PropTypes from "prop-types";
2
+ import { Icon, Popup } from "semantic-ui-react";
3
+
4
+ const triggerStyle = {
5
+ display: "inline-block",
6
+ marginLeft: "0.4em",
7
+ fontSize: "0.85rem",
8
+ fontWeight: "normal",
9
+ lineHeight: 1,
10
+ verticalAlign: "middle",
11
+ cursor: "help",
12
+ };
13
+
14
+ export default function HelpIcon({ id, content }) {
15
+ return (
16
+ <Popup
17
+ trigger={
18
+ <span style={triggerStyle}>
19
+ <Icon
20
+ className="field-label-popup"
21
+ name="question circle outline"
22
+ color="grey"
23
+ fitted
24
+ data-testid={`help-${id}`}
25
+ />
26
+ </span>
27
+ }
28
+ content={content}
29
+ on="hover"
30
+ hideOnScroll
31
+ />
32
+ );
33
+ }
34
+
35
+ HelpIcon.propTypes = {
36
+ id: PropTypes.string.isRequired,
37
+ content: PropTypes.string.isRequired,
38
+ };
@@ -1,5 +1,5 @@
1
1
  import _ from "lodash/fp";
2
- import { Fragment, useEffect } from "react";
2
+ import { Fragment, useEffect, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useIntl } from "react-intl";
5
5
  import {
@@ -9,10 +9,18 @@ import {
9
9
  Header,
10
10
  Form,
11
11
  Dropdown,
12
+ Message,
12
13
  } from "semantic-ui-react";
13
14
  import { FormProvider, useForm, Controller } from "react-hook-form";
14
15
  import ConfirmModal from "@truedat/core/components/ConfirmModal";
15
- import { useAgentTypeOptions, useProviderTypeOptions } from "../constants";
16
+ import {
17
+ useProviderEnable,
18
+ useProviderDisable,
19
+ } from "@truedat/ai/hooks/useProviders";
20
+ import { serverErrors } from "../../serverErrors";
21
+ import { useProviderTypeOptions } from "../constants";
22
+ import EmbeddingModels from "./EmbeddingModels";
23
+ import HelpIcon from "./HelpIcon";
16
24
  import {
17
25
  Openai,
18
26
  AzureOpenai,
@@ -38,21 +46,13 @@ export default function ProviderEditor({
38
46
  onCancel,
39
47
  onDelete,
40
48
  isSubmitting,
49
+ deleteError,
50
+ onDismissDeleteError,
41
51
  setDirty,
52
+ onRefresh,
42
53
  }) {
43
54
  const { formatMessage } = useIntl();
44
55
  const typeOptions = useProviderTypeOptions();
45
- const allAgentOptions = useAgentTypeOptions();
46
-
47
- const usedAgents = _.flow(
48
- _.filter((p) => p.id !== selectedProvider?.id),
49
- _.map(_.prop("agents")),
50
- _.filter(Boolean),
51
- )(providers);
52
-
53
- const availableAgentOptions = _.filter(
54
- (o) => !_.includes(o.value)(usedAgents),
55
- )(allAgentOptions);
56
56
 
57
57
  const form = useForm({
58
58
  mode: "onTouched",
@@ -60,6 +60,38 @@ export default function ProviderEditor({
60
60
  });
61
61
  const { control, handleSubmit, watch, formState } = form;
62
62
  const { isDirty, isValid } = formState;
63
+ const [enableOnCreate, setEnableOnCreate] = useState(_.isEmpty(providers));
64
+ const submit = (values) =>
65
+ onSubmit(
66
+ _.omit(["is_default", "agents", "enabled", "enabled_at"], values),
67
+ { enable: enableOnCreate },
68
+ );
69
+
70
+ const { trigger: enableProvider, isMutating: isEnabling } =
71
+ useProviderEnable();
72
+ const { trigger: disableProvider, isMutating: isDisabling } =
73
+ useProviderDisable();
74
+ const [confirmingDisable, setConfirmingDisable] = useState(false);
75
+ const [enabledErrors, setEnabledErrors] = useState([]);
76
+ const [isRefreshing, setRefreshing] = useState(false);
77
+
78
+ const savedProvider = _.find({ id: selectedProvider?.id })(providers);
79
+ const isEnabled = !!savedProvider?.enabled_at;
80
+
81
+ const changeEnabled = async (trigger) => {
82
+ setEnabledErrors([]);
83
+ setRefreshing(true);
84
+ try {
85
+ await trigger({ id: selectedProvider.id });
86
+ } catch (error) {
87
+ setEnabledErrors(_.flatten(_.values(serverErrors(error))));
88
+ }
89
+ try {
90
+ await onRefresh?.();
91
+ } finally {
92
+ setRefreshing(false);
93
+ }
94
+ };
63
95
 
64
96
  useEffect(() => {
65
97
  setDirty(isDirty);
@@ -75,6 +107,16 @@ export default function ProviderEditor({
75
107
 
76
108
  const type = watch("type");
77
109
 
110
+ const enabledLabel = (
111
+ <label>
112
+ {formatMessage({ id: "providers.form.enabled" })}
113
+ <HelpIcon
114
+ id="provider-enabled"
115
+ content={formatMessage({ id: "providers.form.enabled.hint" })}
116
+ />
117
+ </label>
118
+ );
119
+
78
120
  return (
79
121
  <Fragment>
80
122
  <FormProvider {...form}>
@@ -145,32 +187,67 @@ export default function ProviderEditor({
145
187
  </Form.Field>
146
188
  )}
147
189
  />
148
- <Controller
149
- control={control}
150
- name="agents"
151
- render={({ field: { onBlur, onChange, value } }) => (
152
- <Form.Field>
153
- <label>{formatMessage({ id: "providers.form.agents" })}</label>
154
- <Dropdown
155
- clearable
156
- selection
157
- disabled={availableAgentOptions.length === 0 && !value}
158
- onBlur={onBlur}
159
- options={availableAgentOptions}
160
- onChange={(_e, { value: v }) => onChange(v === "" ? null : v)}
161
- value={value ?? ""}
162
- placeholder={formatMessage({
163
- id: "providers.form.agents.placeholder",
164
- })}
190
+ {selectedProvider.id ? null : (
191
+ <Form.Field>
192
+ <Form.Checkbox
193
+ toggle
194
+ label={enabledLabel}
195
+ checked={enableOnCreate}
196
+ onChange={() => setEnableOnCreate(!enableOnCreate)}
197
+ />
198
+ </Form.Field>
199
+ )}
200
+ {selectedProvider.id ? (
201
+ <Form.Field>
202
+ <Form.Checkbox
203
+ toggle
204
+ label={enabledLabel}
205
+ checked={isEnabled}
206
+ disabled={isEnabling || isDisabling || isRefreshing}
207
+ onChange={() =>
208
+ isEnabled
209
+ ? setConfirmingDisable(true)
210
+ : changeEnabled(enableProvider)
211
+ }
212
+ />
213
+ {_.isEmpty(enabledErrors) ? null : (
214
+ <Message
215
+ negative
216
+ list={enabledErrors}
217
+ onDismiss={() => setEnabledErrors([])}
165
218
  />
166
- </Form.Field>
167
- )}
168
- />
219
+ )}
220
+ <ConfirmModal
221
+ open={confirmingDisable}
222
+ header={formatMessage({
223
+ id: "providers.enabled.disable.confirm.header",
224
+ })}
225
+ content={formatMessage({
226
+ id: "providers.enabled.disable.confirm.content",
227
+ })}
228
+ onClose={() => setConfirmingDisable(false)}
229
+ onConfirm={() => {
230
+ setConfirmingDisable(false);
231
+ changeEnabled(disableProvider);
232
+ }}
233
+ />
234
+ </Form.Field>
235
+ ) : null}
169
236
  {providerPropertiesComponents[type]}
237
+ {selectedProvider.id ? (
238
+ <EmbeddingModels provider={selectedProvider} />
239
+ ) : null}
170
240
  <Divider hidden />
241
+ {deleteError ? (
242
+ <Message
243
+ negative
244
+ list={deleteError}
245
+ onDismiss={onDismissDeleteError}
246
+ />
247
+ ) : null}
171
248
  <Container textAlign="right">
172
249
  <Button
173
- onClick={handleSubmit(onSubmit)}
250
+ onClick={handleSubmit(submit)}
174
251
  primary
175
252
  loading={isSubmitting}
176
253
  disabled={!isValid || !isDirty}
@@ -235,5 +312,8 @@ ProviderEditor.propTypes = {
235
312
  onCancel: PropTypes.func,
236
313
  onDelete: PropTypes.func,
237
314
  isSubmitting: PropTypes.bool,
315
+ deleteError: PropTypes.array,
316
+ onDismissDeleteError: PropTypes.func,
238
317
  setDirty: PropTypes.func,
318
+ onRefresh: PropTypes.func,
239
319
  };