@truedat/ai 6.2.3 → 6.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/ai",
3
- "version": "6.2.3",
3
+ "version": "6.3.1",
4
4
  "description": "Truedat Web Artificial Intelligence package",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -84,7 +84,7 @@
84
84
  ]
85
85
  },
86
86
  "dependencies": {
87
- "@truedat/core": "6.2.3",
87
+ "@truedat/core": "6.3.1",
88
88
  "prop-types": "^15.8.1",
89
89
  "react-hook-form": "^7.45.4",
90
90
  "react-intl": "^5.20.10",
@@ -97,5 +97,5 @@
97
97
  "react-dom": ">= 16.8.6 < 17",
98
98
  "semantic-ui-react": ">= 2.0.3 < 2.2"
99
99
  },
100
- "gitHead": "b7dc4b095be9edb38abfc53fa6a6a274eee425e2"
100
+ "gitHead": "0c8183f86f4fe56211b82d2f2427c64f983f0b81"
101
101
  }
package/src/api.js CHANGED
@@ -3,6 +3,9 @@ const API_RESOURCE_MAPPING = "/api/resource_mappings/:id";
3
3
  const API_PROMPTS = "/api/prompts";
4
4
  const API_PROMPT = "/api/prompts/:id";
5
5
  const API_PROMPT_SET_ACTIVE = "/api/prompts/:id/set_active";
6
+ const API_PROVIDERS = "/api/providers";
7
+ const API_PROVIDER = "/api/providers/:id";
8
+ const API_PROVIDER_CHAT = "/api/providers/:id/chat_completion";
6
9
  const API_SUGGESTIONS_AVAILABILITY_CHECK =
7
10
  "/api/suggestions/availability_check";
8
11
  const API_SUGGESTIONS_REQUEST = "/api/suggestions/request";
@@ -13,6 +16,9 @@ export {
13
16
  API_PROMPTS,
14
17
  API_PROMPT,
15
18
  API_PROMPT_SET_ACTIVE,
19
+ API_PROVIDERS,
20
+ API_PROVIDER,
21
+ API_PROVIDER_CHAT,
16
22
  API_SUGGESTIONS_AVAILABILITY_CHECK,
17
23
  API_SUGGESTIONS_REQUEST,
18
24
  };
@@ -2,9 +2,16 @@ import React from "react";
2
2
  import { Route, Switch } from "react-router-dom";
3
3
  import { Unauthorized } from "@truedat/core/components";
4
4
  import { useAuthorized } from "@truedat/core/hooks";
5
- import { RESOURCE_MAPPINGS, PROMPTS } from "@truedat/core/routes";
5
+ import {
6
+ AI_SANDBOX,
7
+ RESOURCE_MAPPINGS,
8
+ PROMPTS,
9
+ PROVIDERS,
10
+ } from "@truedat/core/routes";
6
11
  import ResourceMappings from "./resourceMappings/ResourceMappings";
7
12
  import Prompts from "./prompts/Prompts";
13
+ import Providers from "./providers/Providers";
14
+ import AiSandbox from "./aiSandbox/AiSandbox";
8
15
 
9
16
  export default function AiRoutes() {
10
17
  const authorized = useAuthorized("manage_ai");
@@ -19,6 +26,14 @@ export default function AiRoutes() {
19
26
  path={PROMPTS}
20
27
  render={() => (authorized ? <Prompts /> : <Unauthorized />)}
21
28
  />
29
+ <Route
30
+ path={PROVIDERS}
31
+ render={() => (authorized ? <Providers /> : <Unauthorized />)}
32
+ />
33
+ <Route
34
+ path={AI_SANDBOX}
35
+ render={() => (authorized ? <AiSandbox /> : <Unauthorized />)}
36
+ />
22
37
  </Switch>
23
38
  );
24
39
  }
@@ -0,0 +1,106 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { useIntl, FormattedMessage } from "react-intl";
4
+ import {
5
+ Header,
6
+ Icon,
7
+ Segment,
8
+ Form,
9
+ Dropdown,
10
+ Message,
11
+ MessageHeader,
12
+ Comment,
13
+ CommentContent,
14
+ CommentAuthor,
15
+ CommentMetadata,
16
+ CommentText,
17
+ CommentGroup,
18
+ } from "semantic-ui-react";
19
+
20
+ import { useProviders } from "@truedat/ai/hooks/useProviders";
21
+
22
+ import RequestForm from "./RequestForm";
23
+
24
+ export default function AiSandbox() {
25
+ const { formatMessage } = useIntl();
26
+ const { data, loading } = useProviders();
27
+ const [selectedProvider, setSelectedProvider] = useState();
28
+ const [messages, setMessages] = useState([]);
29
+
30
+ const selectProvider = (provider) => {
31
+ setMessages([]);
32
+ setSelectedProvider(provider);
33
+ };
34
+
35
+ const providerOptions = _.flow(
36
+ _.propOr([], "data"),
37
+ _.orderBy(["id"], ["asc"]),
38
+ _.map(({ id, name, type }) => ({
39
+ key: id,
40
+ text: `${name} - ${type}`,
41
+ value: id,
42
+ }))
43
+ )(data);
44
+
45
+ return (
46
+ <Segment loading={loading}>
47
+ <Header as="h2">
48
+ <Icon circular name="magic" />
49
+ <Header.Content>
50
+ <FormattedMessage id="ai_sandbox.header" />
51
+ <Header.Subheader>
52
+ <FormattedMessage id="ai_sandbox.subheader" />
53
+ </Header.Subheader>
54
+ </Header.Content>
55
+ </Header>
56
+ <Form>
57
+ <Form.Field required>
58
+ <label>{formatMessage({ id: "ai_sandbox.form.provider" })}</label>
59
+ <Dropdown
60
+ selection
61
+ options={providerOptions}
62
+ onChange={(_e, { value }) => selectProvider(value)}
63
+ value={selectedProvider}
64
+ />
65
+ </Form.Field>
66
+ </Form>
67
+
68
+ <CommentGroup style={{ marginTop: 20 }}>
69
+ {messages.map(({ role, content }, key) =>
70
+ role == "system" ? (
71
+ <Message key={key} info>
72
+ <MessageHeader>
73
+ {formatMessage({ id: "ai_sandbox.role.system" })}
74
+ </MessageHeader>
75
+ <p>{content}</p>
76
+ </Message>
77
+ ) : role == "error" ? (
78
+ <Message key={key} error>
79
+ <MessageHeader>
80
+ {formatMessage({ id: "ai_sandbox.error" })}
81
+ </MessageHeader>
82
+ <p>{content}</p>
83
+ </Message>
84
+ ) : (
85
+ <Comment key={key}>
86
+ <CommentContent>
87
+ <CommentAuthor as="a">
88
+ {formatMessage({ id: `ai_sandbox.role.${role}` })}
89
+ </CommentAuthor>
90
+ <CommentText>{content}</CommentText>
91
+ </CommentContent>
92
+ </Comment>
93
+ )
94
+ )}
95
+ </CommentGroup>
96
+
97
+ {selectedProvider ? (
98
+ <RequestForm
99
+ provider={selectedProvider}
100
+ messages={messages}
101
+ setMessages={setMessages}
102
+ />
103
+ ) : null}
104
+ </Segment>
105
+ );
106
+ }
@@ -0,0 +1,70 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { useIntl } from "react-intl";
4
+ import { Button, Form, Dropdown, Input } from "semantic-ui-react";
5
+
6
+ import { useProviderChatCompletion } from "@truedat/ai/hooks/useProviders";
7
+
8
+ export default function AiSandbox({ provider, messages, setMessages }) {
9
+ const { formatMessage } = useIntl();
10
+ const [selectedRole, setSelectedRole] = useState("user");
11
+ const [content, setContent] = useState("");
12
+
13
+ const { trigger: chatCompletion, isMutating: isCompleting } =
14
+ useProviderChatCompletion(provider);
15
+
16
+ const roleOptions = _.map((role) => ({
17
+ key: role,
18
+ text: formatMessage({ id: `ai_sandbox.role.${role}` }),
19
+ value: role,
20
+ }))(["user", "system", "assistant"]);
21
+
22
+ const requestCompletion = () => {
23
+ if (!content) return;
24
+ const requestMessages = [
25
+ ..._.reject({ role: "error" })(messages),
26
+ { role: selectedRole, content },
27
+ ];
28
+ if (selectedRole == "user")
29
+ chatCompletion({ messages: requestMessages })
30
+ .then(({ data }) =>
31
+ setMessages([
32
+ ...requestMessages,
33
+ { role: "assistant", content: data },
34
+ ])
35
+ )
36
+ .catch((response) => {
37
+ const content = _.propOr("Error", "response.data.error")(response);
38
+ setMessages([{ role: "error", content }]);
39
+ });
40
+ else setMessages(requestMessages);
41
+ setContent("");
42
+ };
43
+
44
+ return (
45
+ <Form
46
+ loading={isCompleting}
47
+ onSubmit={requestCompletion}
48
+ style={{ display: "flex", marginTop: 20 }}
49
+ autoComplete="off"
50
+ >
51
+ <Dropdown
52
+ selection
53
+ options={roleOptions}
54
+ onChange={(_e, { value }) => setSelectedRole(value)}
55
+ value={selectedRole}
56
+ />
57
+ <Input
58
+ name="content"
59
+ style={{ flexGrow: 1 }}
60
+ value={content}
61
+ onChange={(_e, { value }) => setContent(value)}
62
+ />
63
+ <Button
64
+ size="small"
65
+ icon={selectedRole == "user" ? "send" : "write"}
66
+ disabled={isCompleting || !content}
67
+ />
68
+ </Form>
69
+ );
70
+ }
@@ -3,12 +3,12 @@ import { useIntl } from "react-intl";
3
3
 
4
4
  export const resourceTypes = ["data_structure", "business_concept"];
5
5
 
6
- export const providers = ["openai"];
6
+ export const providerTypes = ["openai", "azure_openai", "bedrock_claude"];
7
7
 
8
8
  export const useResourceTypeOptions = () =>
9
9
  useOptions(resourceTypes, "resourceMappings.resourceType");
10
- export const useProvidersOptions = () =>
11
- useOptions(providers, "prompts.provider");
10
+ export const useProviderTypeOptions = () =>
11
+ useOptions(providerTypes, "providers.type");
12
12
 
13
13
  const useOptions = (list, formatPrefix) => {
14
14
  const { formatMessage } = useIntl();
@@ -13,7 +13,7 @@ import {
13
13
  import { FormProvider, useForm, Controller } from "react-hook-form";
14
14
  import { ConfirmModal } from "@truedat/core/components";
15
15
  import { useLocales } from "@truedat/core/hooks";
16
- import { useResourceTypeOptions, useProvidersOptions } from "../constants";
16
+ import { useResourceTypeOptions } from "../constants";
17
17
 
18
18
  export default function PromptEditor({
19
19
  selectedPrompt,
@@ -23,10 +23,10 @@ export default function PromptEditor({
23
23
  onDelete,
24
24
  isSubmitting,
25
25
  setDirty,
26
+ providersOptions,
26
27
  }) {
27
28
  const { formatMessage } = useIntl();
28
29
  const resourceTypeOptions = useResourceTypeOptions();
29
- const providersOptions = useProvidersOptions();
30
30
  const locales = useLocales();
31
31
 
32
32
  const localeOptions = _.flow(
@@ -58,6 +58,8 @@ export default function PromptEditor({
58
58
  id: "prompts.form.name.new",
59
59
  });
60
60
 
61
+ const provider = watch("provider");
62
+
61
63
  return (
62
64
  <Fragment>
63
65
  <FormProvider {...form}>
@@ -232,7 +234,7 @@ export default function PromptEditor({
232
234
 
233
235
  <Controller
234
236
  control={control}
235
- name="provider"
237
+ name="provider_id"
236
238
  rules={{
237
239
  required: formatMessage(
238
240
  { id: "form.validation.required" },
@@ -261,40 +263,6 @@ export default function PromptEditor({
261
263
  )}
262
264
  />
263
265
 
264
- <Controller
265
- control={control}
266
- name="model"
267
- rules={{
268
- required: formatMessage(
269
- { id: "form.validation.required" },
270
- {
271
- prop: formatMessage({
272
- id: "prompts.form.model",
273
- }),
274
- }
275
- ),
276
- }}
277
- render={({
278
- field: { onBlur, onChange, value },
279
- fieldState: { error },
280
- }) => (
281
- <Form.Input
282
- autoComplete="off"
283
- placeholder={formatMessage({
284
- id: "prompts.form.model",
285
- })}
286
- error={error?.message}
287
- label={formatMessage({
288
- id: "prompts.form.model",
289
- })}
290
- onBlur={onBlur}
291
- onChange={(_e, { value }) => onChange(value)}
292
- value={value}
293
- required
294
- />
295
- )}
296
- />
297
-
298
266
  <Divider hidden />
299
267
  <Container textAlign="right">
300
268
  <Button
@@ -372,4 +340,5 @@ PromptEditor.propTypes = {
372
340
  onDelete: PropTypes.func,
373
341
  isSubmitting: PropTypes.bool,
374
342
  setDirty: PropTypes.func,
343
+ providersOptions: PropTypes.array,
375
344
  };
@@ -19,6 +19,7 @@ import {
19
19
  usePromptUpdate,
20
20
  usePromptSetActive,
21
21
  } from "@truedat/ai/hooks/usePrompts";
22
+ import { useProviders } from "@truedat/ai/hooks/useProviders";
22
23
  import PromptEditor from "./PromptEditor";
23
24
 
24
25
  const NEW_PROMPT = {
@@ -33,12 +34,20 @@ const NEW_PROMPT = {
33
34
 
34
35
  export default function Prompts() {
35
36
  const { formatMessage } = useIntl();
36
- const { data, loading, mutate } = usePrompts();
37
+ const { data, loading: loadingPrompts, mutate } = usePrompts();
38
+ const { data: providers, loading: loadingProviders } = useProviders();
37
39
  const [selectedPrompt, setSelectedPrompt] = useState();
38
40
  const [isDirty, setDirty] = useState(false);
39
41
 
42
+ const loading = loadingPrompts || loadingProviders;
43
+
40
44
  const prompts = data?.data;
41
45
 
46
+ const providersOptions = _.flow(
47
+ _.prop("data"),
48
+ _.map(({ id, name }) => ({ key: id, value: id, text: name }))
49
+ )(providers);
50
+
42
51
  const setStateNewPrompt = () => setSelectedPrompt(NEW_PROMPT);
43
52
  const clearForm = () => {
44
53
  setSelectedPrompt(null);
@@ -139,6 +148,7 @@ export default function Prompts() {
139
148
  onDelete={selectedPrompt?.id ? onPromptDelete : null}
140
149
  isSubmitting={isSubmitting}
141
150
  setDirty={setDirty}
151
+ providersOptions={providersOptions}
142
152
  />
143
153
  ) : (
144
154
  <Header as="h2" icon textAlign="center">
@@ -56,6 +56,7 @@ const renderOpts = {
56
56
  };
57
57
 
58
58
  const props = {
59
+ providersOptions: [{ key: 1, value: 1, text: "Provider1" }],
59
60
  selectedPrompt: {
60
61
  id: 1,
61
62
  name: "prompt1",
@@ -63,8 +64,7 @@ const props = {
63
64
  language: "en",
64
65
  system_prompt: "system_prompt",
65
66
  user_prompt_template: "user_prompt",
66
- provider: "openai",
67
- model: "model1",
67
+ provider_id: 1,
68
68
  },
69
69
  prompts: [
70
70
  {
@@ -74,8 +74,7 @@ const props = {
74
74
  language: "en",
75
75
  system_prompt: "system_prompt",
76
76
  user_prompt_template: "user_prompt",
77
- provider: "openai",
78
- model: "model1",
77
+ provider_id: 1,
79
78
  },
80
79
  ],
81
80
  onSubmit: jest.fn(),
@@ -200,9 +199,8 @@ describe("<PromptEditor />", () => {
200
199
  expect(onSubmit.mock.calls[0][0]).toEqual({
201
200
  id: 1,
202
201
  language: "en",
203
- model: "model1",
204
202
  name: "prompt1name",
205
- provider: "openai",
203
+ provider_id: 1,
206
204
  resource_type: "data_structure",
207
205
  system_prompt: "system_prompt",
208
206
  user_prompt_template: "user_prompt",
@@ -37,6 +37,24 @@ jest.mock("@truedat/ai/hooks/usePrompts", () => {
37
37
  };
38
38
  });
39
39
 
40
+ jest.mock("@truedat/ai/hooks/useProviders", () => {
41
+ const originalModule = jest.requireActual("@truedat/ai/hooks/useProviders");
42
+
43
+ return {
44
+ __esModule: true,
45
+ ...originalModule,
46
+ useProviders: jest.fn(() => ({
47
+ data: {
48
+ data: [
49
+ {
50
+ name: "provider1",
51
+ },
52
+ ],
53
+ },
54
+ loading: false,
55
+ })),
56
+ };
57
+ });
40
58
  describe("<Prompts />", () => {
41
59
  const renderOpts = {
42
60
  messages: {
@@ -167,7 +167,7 @@ exports[`<PromptEditor /> matches snapshot without onDelete 1`] = `
167
167
  class="divider text"
168
168
  role="alert"
169
169
  >
170
- prompts.provider.openai
170
+ Provider1
171
171
  </div>
172
172
  <i
173
173
  aria-hidden="true"
@@ -186,30 +186,12 @@ exports[`<PromptEditor /> matches snapshot without onDelete 1`] = `
186
186
  <span
187
187
  class="text"
188
188
  >
189
- prompts.provider.openai
189
+ Provider1
190
190
  </span>
191
191
  </div>
192
192
  </div>
193
193
  </div>
194
194
  </div>
195
- <div
196
- class="required field"
197
- >
198
- <label>
199
- prompts.form.model
200
- </label>
201
- <div
202
- class="ui input"
203
- >
204
- <input
205
- autocomplete="off"
206
- placeholder="prompts.form.model"
207
- required=""
208
- type="text"
209
- value="model1"
210
- />
211
- </div>
212
- </div>
213
195
  <div
214
196
  class="ui hidden divider"
215
197
  />
@@ -407,7 +389,7 @@ exports[`<PromptEditor /> matches the latest snapshot 1`] = `
407
389
  class="divider text"
408
390
  role="alert"
409
391
  >
410
- prompts.provider.openai
392
+ Provider1
411
393
  </div>
412
394
  <i
413
395
  aria-hidden="true"
@@ -426,30 +408,12 @@ exports[`<PromptEditor /> matches the latest snapshot 1`] = `
426
408
  <span
427
409
  class="text"
428
410
  >
429
- prompts.provider.openai
411
+ Provider1
430
412
  </span>
431
413
  </div>
432
414
  </div>
433
415
  </div>
434
416
  </div>
435
- <div
436
- class="required field"
437
- >
438
- <label>
439
- prompts.form.model
440
- </label>
441
- <div
442
- class="ui input"
443
- >
444
- <input
445
- autocomplete="off"
446
- placeholder="prompts.form.model"
447
- required=""
448
- type="text"
449
- value="model1"
450
- />
451
- </div>
452
- </div>
453
417
  <div
454
418
  class="ui hidden divider"
455
419
  />
@@ -650,7 +614,7 @@ exports[`<PromptEditor /> test cancel button with confirm 1`] = `
650
614
  class="divider text"
651
615
  role="alert"
652
616
  >
653
- prompts.provider.openai
617
+ Provider1
654
618
  </div>
655
619
  <i
656
620
  aria-hidden="true"
@@ -669,30 +633,12 @@ exports[`<PromptEditor /> test cancel button with confirm 1`] = `
669
633
  <span
670
634
  class="text"
671
635
  >
672
- prompts.provider.openai
636
+ Provider1
673
637
  </span>
674
638
  </div>
675
639
  </div>
676
640
  </div>
677
641
  </div>
678
- <div
679
- class="required field"
680
- >
681
- <label>
682
- prompts.form.model
683
- </label>
684
- <div
685
- class="ui input"
686
- >
687
- <input
688
- autocomplete="off"
689
- placeholder="prompts.form.model"
690
- required=""
691
- type="text"
692
- value="model1"
693
- />
694
- </div>
695
- </div>
696
642
  <div
697
643
  class="ui hidden divider"
698
644
  />
@@ -893,7 +839,7 @@ exports[`<PromptEditor /> test delete button 1`] = `
893
839
  class="divider text"
894
840
  role="alert"
895
841
  >
896
- prompts.provider.openai
842
+ Provider1
897
843
  </div>
898
844
  <i
899
845
  aria-hidden="true"
@@ -912,30 +858,12 @@ exports[`<PromptEditor /> test delete button 1`] = `
912
858
  <span
913
859
  class="text"
914
860
  >
915
- prompts.provider.openai
861
+ Provider1
916
862
  </span>
917
863
  </div>
918
864
  </div>
919
865
  </div>
920
866
  </div>
921
- <div
922
- class="required field"
923
- >
924
- <label>
925
- prompts.form.model
926
- </label>
927
- <div
928
- class="ui input"
929
- >
930
- <input
931
- autocomplete="off"
932
- placeholder="prompts.form.model"
933
- required=""
934
- type="text"
935
- value="model1"
936
- />
937
- </div>
938
- </div>
939
867
  <div
940
868
  class="ui hidden divider"
941
869
  />