@truedat/ai 6.0.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.
Files changed (27) hide show
  1. package/LICENSE +685 -0
  2. package/README.md +13 -0
  3. package/package.json +101 -0
  4. package/src/api.js +6 -0
  5. package/src/components/AiRoutes.js +24 -0
  6. package/src/components/constants.js +20 -0
  7. package/src/components/index.js +2 -0
  8. package/src/components/prompts/PromptEditor.js +365 -0
  9. package/src/components/prompts/Prompts.js +136 -0
  10. package/src/components/prompts/__tests__/Prompt.spec.js +77 -0
  11. package/src/components/prompts/__tests__/PromptEditor.spec.js +208 -0
  12. package/src/components/prompts/__tests__/__snapshots__/Prompt.spec.js.snap +77 -0
  13. package/src/components/prompts/__tests__/__snapshots__/PromptEditor.spec.js.snap +896 -0
  14. package/src/components/resourceMappings/ResourceMappingEditor.js +203 -0
  15. package/src/components/resourceMappings/ResourceMappingFields.js +119 -0
  16. package/src/components/resourceMappings/ResourceMappings.js +140 -0
  17. package/src/components/resourceMappings/__tests__/ResourceMappingEditor.spec.js +204 -0
  18. package/src/components/resourceMappings/__tests__/ResourceMappings.spec.js +79 -0
  19. package/src/components/resourceMappings/__tests__/__snapshots__/ResourceMappingEditor.spec.js.snap +748 -0
  20. package/src/components/resourceMappings/__tests__/__snapshots__/ResourceMappings.spec.js.snap +77 -0
  21. package/src/components/resourceMappings/selectors/DataStructureSelector.js +80 -0
  22. package/src/components/resourceMappings/selectors/index.js +11 -0
  23. package/src/hooks/__tests__/usePrompts.spec.js +101 -0
  24. package/src/hooks/__tests__/useResourceMappings.spec.js +101 -0
  25. package/src/hooks/usePrompts.js +31 -0
  26. package/src/hooks/useResourceMappings.js +33 -0
  27. package/src/index.js +3 -0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Truedat Web Artificial Intelligence
2
+
3
+ Artificial Intelligence for Truedat Web.
4
+
5
+ ## How To...
6
+
7
+ ### Install depencencies
8
+
9
+ `yarn`
10
+
11
+ ### Run the tests
12
+
13
+ `yarn test`
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@truedat/ai",
3
+ "version": "6.0.1",
4
+ "description": "Truedat Web Artificial Intelligence package",
5
+ "sideEffects": false,
6
+ "jsnext:main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "author": "Bluetab Solutions",
12
+ "license": "GPL-3.0",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "clean": "rimraf yarn-error.log",
18
+ "debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
19
+ "test": "TZ=UTC jest --coverage",
20
+ "test:watch": "TZ=UTC jest --watch",
21
+ "eslint": "eslint src/**",
22
+ "eslint:fix": "eslint --fix src/**"
23
+ },
24
+ "devDependencies": {
25
+ "@babel/cli": "^7.19.3",
26
+ "@babel/core": "^7.20.5",
27
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
28
+ "@babel/plugin-proposal-object-rest-spread": "^7.20.2",
29
+ "@babel/plugin-proposal-optional-chaining": "^7.18.9",
30
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
31
+ "@babel/plugin-transform-modules-commonjs": "^7.19.6",
32
+ "@babel/preset-env": "^7.20.2",
33
+ "@babel/preset-react": "^7.18.6",
34
+ "@testing-library/jest-dom": "^5.16.5",
35
+ "@testing-library/react": "^12.0.0",
36
+ "@testing-library/react-hooks": "^8.0.1",
37
+ "@testing-library/user-event": "^13.2.1",
38
+ "@truedat/test": "5.12.2",
39
+ "babel-jest": "^28.1.0",
40
+ "babel-plugin-dynamic-import-node": "^2.3.3",
41
+ "babel-plugin-lodash": "^3.3.4",
42
+ "babel-plugin-react-intl": "^5.1.18",
43
+ "babel-plugin-transform-semantic-ui-react-imports": "^1.4.1",
44
+ "identity-obj-proxy": "^3.0.0",
45
+ "jest": "^29.0.0",
46
+ "jest-environment-jsdom": "^29.0.0",
47
+ "react": "^16.14.0",
48
+ "react-dom": "^16.14.0",
49
+ "rimraf": "^3.0.2",
50
+ "semantic-ui-react": "^2.1.4"
51
+ },
52
+ "jest": {
53
+ "maxWorkers": "50%",
54
+ "testTimeout": 10000,
55
+ "moduleDirectories": [
56
+ "<rootDir>/src",
57
+ "../../node_modules"
58
+ ],
59
+ "setupFilesAfterEnv": [
60
+ "@truedat/test/setup"
61
+ ],
62
+ "moduleNameMapper": {
63
+ "\\.(css|less)$": "identity-obj-proxy",
64
+ "^@truedat/([^/]+)$": "<rootDir>/../$1/src/index",
65
+ "^@truedat/([^/]+)/(.*)$": "<rootDir>/../$1/src/$2"
66
+ },
67
+ "snapshotSerializers": [
68
+ "enzyme-to-json/serializer"
69
+ ],
70
+ "testEnvironment": "jsdom",
71
+ "testPathIgnorePatterns": [
72
+ "<rootDir>/node_modules/"
73
+ ],
74
+ "transform": {
75
+ "\\.js$": [
76
+ "babel-jest",
77
+ {
78
+ "rootMode": "upward"
79
+ }
80
+ ]
81
+ },
82
+ "transformIgnorePatterns": [
83
+ "/node_modules/(?!@truedat).*"
84
+ ]
85
+ },
86
+ "dependencies": {
87
+ "@truedat/core": "6.0.1",
88
+ "prop-types": "^15.8.1",
89
+ "react-hook-form": "^7.45.4",
90
+ "react-intl": "^5.20.10",
91
+ "react-router-dom": "^5.2.0",
92
+ "semantic-ui-react": "^2.1.4",
93
+ "swr": "^2.0.0"
94
+ },
95
+ "peerDependencies": {
96
+ "react": ">= 16.8.6 < 17",
97
+ "react-dom": ">= 16.8.6 < 17",
98
+ "semantic-ui-react": ">= 2.0.3 < 2.2"
99
+ },
100
+ "gitHead": "2ce73d263401e0a7959f9111ba3370b6810b5420"
101
+ }
package/src/api.js ADDED
@@ -0,0 +1,6 @@
1
+ const API_RESOURCE_MAPPINGS = "/api/resource_mappings";
2
+ const API_RESOURCE_MAPPING = "/api/resource_mappings/:id";
3
+ const API_PROMPTS = "/api/prompts";
4
+ const API_PROMPT = "/api/prompts/:id";
5
+
6
+ export { API_RESOURCE_MAPPINGS, API_RESOURCE_MAPPING, API_PROMPTS, API_PROMPT };
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ import { Route, Switch } from "react-router-dom";
3
+ import { Unauthorized } from "@truedat/core/components";
4
+ import { useAuthorized } from "@truedat/core/hooks";
5
+ import { RESOURCE_MAPPINGS, PROMPTS } from "@truedat/core/routes";
6
+ import ResourceMappings from "./resourceMappings/ResourceMappings";
7
+ import Prompts from "./prompts/Prompts";
8
+
9
+ export default function AiRoutes() {
10
+ const authorized = useAuthorized("manage_ai");
11
+
12
+ return (
13
+ <Switch>
14
+ <Route
15
+ path={RESOURCE_MAPPINGS}
16
+ render={() => (authorized ? <ResourceMappings /> : <Unauthorized />)}
17
+ />
18
+ <Route
19
+ path={PROMPTS}
20
+ render={() => (authorized ? <Prompts /> : <Unauthorized />)}
21
+ />
22
+ </Switch>
23
+ );
24
+ }
@@ -0,0 +1,20 @@
1
+ import _ from "lodash/fp";
2
+ import { useIntl } from "react-intl";
3
+
4
+ export const resourceTypes = ["data_structure"];
5
+
6
+ export const providers = ["openai"];
7
+
8
+ export const useResourceTypeOptions = () =>
9
+ useOptions(resourceTypes, "resourceMappings.resourceType");
10
+ export const useProvidersOptions = () =>
11
+ useOptions(providers, "prompts.provider");
12
+
13
+ const useOptions = (list, formatPrefix) => {
14
+ const { formatMessage } = useIntl();
15
+ return _.map((value) => ({
16
+ key: value,
17
+ text: formatMessage({ id: `${formatPrefix}.${value}` }),
18
+ value,
19
+ }))(list);
20
+ };
@@ -0,0 +1,2 @@
1
+ import AiRoutes from "./AiRoutes";
2
+ export { AiRoutes };
@@ -0,0 +1,365 @@
1
+ import _ from "lodash/fp";
2
+ import React, { Fragment, useEffect } from "react";
3
+ import PropTypes from "prop-types";
4
+ import { useIntl } from "react-intl";
5
+ import {
6
+ Button,
7
+ Container,
8
+ Divider,
9
+ Header,
10
+ Form,
11
+ Dropdown,
12
+ } from "semantic-ui-react";
13
+ import { FormProvider, useForm, Controller } from "react-hook-form";
14
+ import { ConfirmModal } from "@truedat/core/components";
15
+ import { useLocales } from "@truedat/core/hooks";
16
+ import { useResourceTypeOptions, useProvidersOptions } from "../constants";
17
+
18
+ export default function PromptEditor({
19
+ selectedPrompt,
20
+ onSubmit,
21
+ onCancel,
22
+ onDelete,
23
+ isSubmitting,
24
+ setDirty,
25
+ }) {
26
+ const { formatMessage } = useIntl();
27
+ const resourceTypeOptions = useResourceTypeOptions();
28
+ const providersOptions = useProvidersOptions();
29
+ const locales = useLocales();
30
+
31
+ const localeOptions = _.flow(
32
+ _.prop("locales"),
33
+ _.map("lang"),
34
+ _.map((key) => ({
35
+ key,
36
+ text: key,
37
+ value: key,
38
+ }))
39
+ )(locales);
40
+
41
+ const form = useForm({
42
+ mode: "onTouched",
43
+ defaultValues: selectedPrompt,
44
+ });
45
+ const { control, handleSubmit, watch, formState } = form;
46
+ const { isDirty, isValid } = formState;
47
+
48
+ useEffect(() => {
49
+ setDirty(isDirty);
50
+ }, [setDirty, isDirty]);
51
+
52
+ if (!selectedPrompt) return null;
53
+
54
+ const name =
55
+ watch("name") ||
56
+ formatMessage({
57
+ id: "prompts.form.name.new",
58
+ });
59
+
60
+ return (
61
+ <Fragment>
62
+ <FormProvider {...form}>
63
+ <Form>
64
+ <Header as="h3" dividing>
65
+ {name}
66
+ </Header>
67
+
68
+ <Controller
69
+ control={control}
70
+ name="name"
71
+ rules={{
72
+ required: formatMessage(
73
+ { id: "form.validation.required" },
74
+ {
75
+ prop: formatMessage({
76
+ id: "prompts.form.name",
77
+ }),
78
+ }
79
+ ),
80
+ }}
81
+ render={({
82
+ field: { onBlur, onChange, value },
83
+ fieldState: { error },
84
+ }) => (
85
+ <Form.Input
86
+ autoComplete="off"
87
+ placeholder={formatMessage({
88
+ id: "prompts.form.name",
89
+ })}
90
+ error={error?.message}
91
+ label={formatMessage({
92
+ id: "prompts.form.name",
93
+ })}
94
+ onBlur={onBlur}
95
+ onChange={(_e, { value }) => onChange(value)}
96
+ value={value}
97
+ required
98
+ />
99
+ )}
100
+ />
101
+
102
+ <Controller
103
+ control={control}
104
+ name="resource_type"
105
+ rules={{
106
+ required: formatMessage(
107
+ { id: "form.validation.required" },
108
+ {
109
+ prop: formatMessage({
110
+ id: "prompts.form.resource_type",
111
+ }),
112
+ }
113
+ ),
114
+ }}
115
+ render={({ field: { onBlur, onChange, value } }) => (
116
+ <Form.Field required>
117
+ <label>
118
+ {formatMessage({
119
+ id: "prompts.form.resource_type",
120
+ })}
121
+ </label>
122
+ <Dropdown
123
+ selection
124
+ onBlur={onBlur}
125
+ options={resourceTypeOptions}
126
+ onChange={(_e, { value }) => onChange(value)}
127
+ value={value}
128
+ />
129
+ </Form.Field>
130
+ )}
131
+ />
132
+
133
+ <Controller
134
+ control={control}
135
+ name="language"
136
+ rules={{
137
+ required: formatMessage(
138
+ { id: "form.validation.required" },
139
+ {
140
+ prop: formatMessage({
141
+ id: "prompts.form.language",
142
+ }),
143
+ }
144
+ ),
145
+ }}
146
+ render={({ field: { onBlur, onChange, value } }) => (
147
+ <Form.Field required>
148
+ <label>
149
+ {formatMessage({
150
+ id: "prompts.form.language",
151
+ })}
152
+ </label>
153
+ <Dropdown
154
+ selection
155
+ onBlur={onBlur}
156
+ options={localeOptions}
157
+ onChange={(_e, { value }) => onChange(value)}
158
+ value={value}
159
+ />
160
+ </Form.Field>
161
+ )}
162
+ />
163
+
164
+ <Controller
165
+ control={control}
166
+ name="system_prompt"
167
+ rules={{
168
+ required: formatMessage(
169
+ { id: "form.validation.required" },
170
+ {
171
+ prop: formatMessage({
172
+ id: "prompts.form.system_prompt",
173
+ }),
174
+ }
175
+ ),
176
+ }}
177
+ render={({
178
+ field: { onBlur, onChange, value },
179
+ fieldState: { error },
180
+ }) => (
181
+ <Form.TextArea
182
+ autoComplete="off"
183
+ placeholder={formatMessage({
184
+ id: "prompts.form.system_prompt",
185
+ })}
186
+ error={error?.message}
187
+ label={formatMessage({
188
+ id: "prompts.form.system_prompt",
189
+ })}
190
+ onBlur={onBlur}
191
+ onChange={(_e, { value }) => onChange(value)}
192
+ value={value}
193
+ required
194
+ />
195
+ )}
196
+ />
197
+
198
+ <Controller
199
+ control={control}
200
+ name="user_prompt_template"
201
+ rules={{
202
+ required: formatMessage(
203
+ { id: "form.validation.required" },
204
+ {
205
+ prop: formatMessage({
206
+ id: "prompts.form.user_prompt_template",
207
+ }),
208
+ }
209
+ ),
210
+ }}
211
+ render={({
212
+ field: { onBlur, onChange, value },
213
+ fieldState: { error },
214
+ }) => (
215
+ <Form.TextArea
216
+ autoComplete="off"
217
+ placeholder={formatMessage({
218
+ id: "prompts.form.user_prompt_template",
219
+ })}
220
+ error={error?.message}
221
+ label={formatMessage({
222
+ id: "prompts.form.user_prompt_template",
223
+ })}
224
+ onBlur={onBlur}
225
+ onChange={(_e, { value }) => onChange(value)}
226
+ value={value}
227
+ required
228
+ />
229
+ )}
230
+ />
231
+
232
+ <Controller
233
+ control={control}
234
+ name="provider"
235
+ rules={{
236
+ required: formatMessage(
237
+ { id: "form.validation.required" },
238
+ {
239
+ prop: formatMessage({
240
+ id: "prompts.form.provider",
241
+ }),
242
+ }
243
+ ),
244
+ }}
245
+ render={({ field: { onBlur, onChange, value } }) => (
246
+ <Form.Field required>
247
+ <label>
248
+ {formatMessage({
249
+ id: "prompts.form.provider",
250
+ })}
251
+ </label>
252
+ <Dropdown
253
+ selection
254
+ onBlur={onBlur}
255
+ options={providersOptions}
256
+ onChange={(_e, { value }) => onChange(value)}
257
+ value={value}
258
+ />
259
+ </Form.Field>
260
+ )}
261
+ />
262
+
263
+ <Controller
264
+ control={control}
265
+ name="model"
266
+ rules={{
267
+ required: formatMessage(
268
+ { id: "form.validation.required" },
269
+ {
270
+ prop: formatMessage({
271
+ id: "prompts.form.model",
272
+ }),
273
+ }
274
+ ),
275
+ }}
276
+ render={({
277
+ field: { onBlur, onChange, value },
278
+ fieldState: { error },
279
+ }) => (
280
+ <Form.Input
281
+ autoComplete="off"
282
+ placeholder={formatMessage({
283
+ id: "prompts.form.model",
284
+ })}
285
+ error={error?.message}
286
+ label={formatMessage({
287
+ id: "prompts.form.model",
288
+ })}
289
+ onBlur={onBlur}
290
+ onChange={(_e, { value }) => onChange(value)}
291
+ value={value}
292
+ required
293
+ />
294
+ )}
295
+ />
296
+
297
+ <Divider hidden />
298
+ <Container textAlign="right">
299
+ <Button
300
+ onClick={handleSubmit(onSubmit)}
301
+ primary
302
+ loading={isSubmitting}
303
+ disabled={!isValid || !isDirty}
304
+ content={formatMessage({ id: "actions.save" })}
305
+ />
306
+ {isDirty ? (
307
+ <ConfirmModal
308
+ trigger={
309
+ <Button
310
+ content={formatMessage({ id: "actions.cancel" })}
311
+ disabled={isSubmitting}
312
+ />
313
+ }
314
+ header={formatMessage({
315
+ id: "actions.discard.confirmation.header",
316
+ })}
317
+ content={formatMessage({
318
+ id: "actions.discard.confirmation.content",
319
+ })}
320
+ onConfirm={onCancel}
321
+ onOpen={(e) => e.stopPropagation()}
322
+ onClose={(e) => e.stopPropagation()}
323
+ />
324
+ ) : (
325
+ <Button
326
+ content={formatMessage({ id: "actions.cancel" })}
327
+ disabled={isSubmitting}
328
+ onClick={onCancel}
329
+ />
330
+ )}
331
+ {onDelete ? (
332
+ <ConfirmModal
333
+ trigger={
334
+ <Button
335
+ color="red"
336
+ content={formatMessage({ id: "actions.delete" })}
337
+ disabled={isSubmitting}
338
+ />
339
+ }
340
+ header={formatMessage({
341
+ id: "functions.action.delete.header",
342
+ })}
343
+ content={formatMessage({
344
+ id: "functions.action.delete.content",
345
+ })}
346
+ onConfirm={onDelete}
347
+ onOpen={(e) => e.stopPropagation()}
348
+ onClose={(e) => e.stopPropagation()}
349
+ />
350
+ ) : null}
351
+ </Container>
352
+ </Form>
353
+ </FormProvider>
354
+ </Fragment>
355
+ );
356
+ }
357
+
358
+ PromptEditor.propTypes = {
359
+ selectedPrompt: PropTypes.object,
360
+ onSubmit: PropTypes.func,
361
+ onCancel: PropTypes.func,
362
+ onDelete: PropTypes.func,
363
+ isSubmitting: PropTypes.bool,
364
+ setDirty: PropTypes.func,
365
+ };
@@ -0,0 +1,136 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useState } from "react";
3
+ import { useIntl, FormattedMessage } from "react-intl";
4
+ import {
5
+ Button,
6
+ Grid,
7
+ GridColumn,
8
+ Header,
9
+ Icon,
10
+ List,
11
+ Segment,
12
+ } from "semantic-ui-react";
13
+
14
+ import {
15
+ usePrompts,
16
+ usePromptCreate,
17
+ usePromptDelete,
18
+ usePromptUpdate,
19
+ } from "@truedat/ai/hooks/usePrompts";
20
+ import PromptEditor from "./PromptEditor";
21
+
22
+ const NEW_PROMPT = {
23
+ name: "",
24
+ resource_type: "data_structure",
25
+ language: "en",
26
+ system_prompt: "",
27
+ user_prompt_template: "",
28
+ provider: "openai",
29
+ model: "",
30
+ };
31
+
32
+ export default function Prompts() {
33
+ const { formatMessage } = useIntl();
34
+ const { data, loading, mutate } = usePrompts();
35
+ const [selectedPrompt, setSelectedPrompt] = useState();
36
+ const [isDirty, setDirty] = useState(false);
37
+
38
+ const prompts = data?.data;
39
+
40
+ const setStateNewPrompt = () => setSelectedPrompt(NEW_PROMPT);
41
+ const clearForm = () => {
42
+ setSelectedPrompt(null);
43
+ setDirty(false);
44
+ };
45
+
46
+ const { trigger: createPrompt, isMutating: isCreating } = usePromptCreate();
47
+
48
+ const { trigger: updatePrompt, isMutating: isUpdating } =
49
+ usePromptUpdate(selectedPrompt);
50
+
51
+ const { trigger: deletePrompt, isMutating: isDeleting } =
52
+ usePromptDelete(selectedPrompt);
53
+
54
+ const isSubmitting = isCreating || isUpdating || isDeleting;
55
+ const onPromptCreate = async (prompt) => {
56
+ const mutatePrompt = selectedPrompt?.id ? updatePrompt : createPrompt;
57
+ await mutatePrompt({ prompt });
58
+ clearForm();
59
+ mutate();
60
+ };
61
+ const onPromptDelete = async (prompt) => {
62
+ await deletePrompt({ prompt });
63
+ clearForm();
64
+ mutate();
65
+ };
66
+
67
+ return (
68
+ <Segment loading={loading}>
69
+ <Header as="h2">
70
+ <Icon circular name="map signs" />
71
+ <Header.Content>
72
+ <FormattedMessage id="prompts.header" />
73
+ <Header.Subheader>
74
+ <FormattedMessage id="prompts.subheader" />
75
+ </Header.Subheader>
76
+ </Header.Content>
77
+ </Header>
78
+
79
+ <Grid>
80
+ <GridColumn width={4}>
81
+ <Button fluid onClick={setStateNewPrompt} disabled={isDirty}>
82
+ {formatMessage({ id: "prompts.action.new" })}
83
+ </Button>
84
+ <List divided selection={!isDirty}>
85
+ {!_.isEmpty(prompts) ? (
86
+ prompts.map((prompt, key) => (
87
+ <List.Item
88
+ key={key}
89
+ onClick={() => !isDirty && setSelectedPrompt(prompt)}
90
+ >
91
+ <List.Content>
92
+ <List.Header>
93
+ {prompt.name ||
94
+ formatMessage({
95
+ id: "prompts.form.name.new",
96
+ })}
97
+ </List.Header>
98
+ </List.Content>
99
+ </List.Item>
100
+ ))
101
+ ) : (
102
+ <List.Item>
103
+ <List.Content>
104
+ <List.Header>
105
+ {formatMessage({ id: "prompts.empty_list" })}
106
+ </List.Header>
107
+ </List.Content>
108
+ </List.Item>
109
+ )}
110
+ </List>
111
+ </GridColumn>
112
+ <GridColumn width={11}>
113
+ {selectedPrompt ? (
114
+ <PromptEditor
115
+ key={selectedPrompt?.id || "new"}
116
+ selectedPrompt={selectedPrompt}
117
+ prompts={prompts}
118
+ onCancel={clearForm}
119
+ onSubmit={onPromptCreate}
120
+ onDelete={selectedPrompt?.id ? onPromptDelete : null}
121
+ isSubmitting={isSubmitting}
122
+ setDirty={setDirty}
123
+ />
124
+ ) : (
125
+ <Header as="h2" icon textAlign="center">
126
+ <Icon name="hand pointer outline" />
127
+ <Header.Subheader>
128
+ {formatMessage({ id: "prompts.no_selection" })}
129
+ </Header.Subheader>
130
+ </Header>
131
+ )}
132
+ </GridColumn>
133
+ </Grid>
134
+ </Segment>
135
+ );
136
+ }