Skip to content

Commit 588b0bb

Browse files
authored
PLU-445 - [AISAY-4] - Change file selector to Dropdown (#928)
### TL;DR Refactored AISAY actions to use a single file input instead of multiple attachments and centralized schema validation. **Note to reviewer** This initial rollout will only include the generalised model as there are pending issues to iron out for the specific model. ### What changed? - Changed the file input type from `multiselect` to `dropdown` in both generalised and specific model actions - Consolidated schema validation logic into a common `schema.ts` file - Added support for specific variable types in `ControlledAutocomplete` component - Used logger to log errors ### How to test? - [ ] Use the AISAY generalised model action with a single file input ### Why make this change? The AISAY actions were designed to work with a single file, but the actions were previously using the MultiSelect component. ### Screenshots ![Screenshot 2025-04-07 at 11.50.40 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/8c7d2d6f-7db2-47ba-86ab-c56af1c374bc.png)
1 parent c2a2fa4 commit 588b0bb

File tree

11 files changed

+210
-85
lines changed

11 files changed

+210
-85
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { assert, beforeEach, describe, it } from 'vitest'
2+
3+
import {
4+
generalisedModelSchema,
5+
specificModelSchema,
6+
} from '../../common/schema'
7+
8+
describe('AISAY schema', () => {
9+
let generalisedModelPayload: Record<string, unknown>
10+
let specificModelPayload: Record<string, unknown>
11+
12+
beforeEach(() => {
13+
generalisedModelPayload = {
14+
file: 's3:plumber-test-bucket:123456/789abc/plumber-logo.jpg',
15+
prompts: [
16+
{
17+
prompt: 'What is your name?',
18+
},
19+
{
20+
prompt: 'What is your age?',
21+
},
22+
],
23+
}
24+
25+
specificModelPayload = {
26+
file: 's3:plumber-test-bucket:123456/789abc/plumber-logo.jpg',
27+
documentType: 'BANK_STATEMENT',
28+
}
29+
})
30+
31+
describe('generalised model schema', () => {
32+
it('should validate generalised model schema', () => {
33+
const result = generalisedModelSchema.safeParse(generalisedModelPayload)
34+
assert(result.success === true)
35+
assert(
36+
result.data.file ===
37+
's3:plumber-test-bucket:123456/789abc/plumber-logo.jpg',
38+
)
39+
assert(Object.keys(result.data.prompts).length === 2)
40+
assert(
41+
result.data.prompts['additionalProp0'].description ===
42+
'Extract the What is your name?',
43+
)
44+
assert(
45+
result.data.prompts['additionalProp1'].description ===
46+
'Extract the What is your age?',
47+
)
48+
})
49+
50+
it('should fail if file is not a valid S3 ID', () => {
51+
generalisedModelPayload.file = '123'
52+
const result = generalisedModelSchema.safeParse(generalisedModelPayload)
53+
assert(result.success === false)
54+
})
55+
})
56+
57+
describe('specific model schema', () => {
58+
it('should validate specific model schema', () => {
59+
const result = specificModelSchema.safeParse(specificModelPayload)
60+
assert(result.success === true)
61+
})
62+
63+
it.each(['BANK_STATEMENT', 'CHEQUE', 'INVOICE', 'PASSPORT', 'RECEIPT'])(
64+
`should validate for valid document type: %s`,
65+
(documentType) => {
66+
specificModelPayload.documentType = documentType
67+
const result = specificModelSchema.safeParse(specificModelPayload)
68+
assert(result.success === true)
69+
},
70+
)
71+
72+
it('should fail if document type is not a valid document type', () => {
73+
specificModelPayload.documentType = 'INVALID_DOCUMENT_TYPE'
74+
const result = specificModelSchema.safeParse(specificModelPayload)
75+
assert(result.success === false)
76+
})
77+
})
78+
})

packages/backend/src/apps/aisay/actions/use-generalised-model/get-data-out-metadata.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { IDataOutMetadata, IExecutionStep } from '@plumber/types'
22

3-
import { getInfoToExtract } from '../../common/info-to-extract'
3+
import { getPrompts } from '../../common/get-prompts'
44

55
async function getDataOutMetadata(
66
executionStep: IExecutionStep,
@@ -11,23 +11,29 @@ async function getDataOutMetadata(
1111
return null
1212
}
1313

14-
const infoToExtract = await getInfoToExtract(stepId)
14+
const prompts = await getPrompts(stepId)
1515

1616
const fieldsMetadata: Record<string, IDataOutMetadata> = {}
1717
Object.keys(dataOut.fields).forEach((key) => {
1818
const index = parseInt(key.split('additionalProp')[1])
19-
const fieldName = infoToExtract[index]
19+
const fieldName = prompts[index]
2020
fieldsMetadata[key] = {
2121
label: fieldName,
2222
}
2323
})
2424

25+
const otherMetadata: Record<string, IDataOutMetadata> = {}
26+
Object.keys(dataOut).forEach((key) => {
27+
if (key !== 'fields') {
28+
otherMetadata[key] = {
29+
isHidden: true,
30+
}
31+
}
32+
})
33+
2534
return {
26-
quota: {
27-
label: 'Quota',
28-
isHidden: true,
29-
},
3035
fields: fieldsMetadata,
36+
...otherMetadata,
3137
}
3238
}
3339

packages/backend/src/apps/aisay/actions/use-generalised-model/index.ts

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { IJSONArray, IRawAction } from '@plumber/types'
1+
import { IRawAction } from '@plumber/types'
22

33
import StepError from '@/errors/step'
4+
import logger from '@/helpers/logger'
45
import Step from '@/models/step'
56

67
import { getToken } from '../../auth/get-token'
78
import { parseError } from '../../common/error-parser'
8-
import { getAttachmentsFromS3, getValidationError } from '../../common/utils'
9+
import { generalisedModelSchema } from '../../common/schema'
10+
import { getAttachmentFromS3, getValidationError } from '../../common/utils'
911

1012
import getDataOutMetadata from './get-data-out-metadata'
11-
import { requestSchema } from './schema'
1213

1314
const action: IRawAction = {
1415
name: 'Extract data from all document types',
@@ -17,8 +18,8 @@ const action: IRawAction = {
1718
arguments: [
1819
{
1920
label: 'File',
20-
key: 'attachments',
21-
type: 'multiselect' as const,
21+
key: 'file',
22+
type: 'dropdown',
2223
required: true,
2324
variables: true,
2425
variableTypes: ['file'],
@@ -27,15 +28,15 @@ const action: IRawAction = {
2728
label: 'Prompts',
2829
description:
2930
'Enter prompts to specify how the data should be interpreted and extracted',
30-
key: 'infoToExtract',
31+
key: 'prompts',
3132
type: 'multirow' as const,
3233
required: true,
3334
variables: true,
3435
addRowButtonText: 'Add prompt',
3536
subFields: [
3637
{
3738
placeholder: 'E.g. Return the price of individual line items',
38-
key: 'infoToExtract',
39+
key: 'prompt',
3940
type: 'string' as const,
4041
required: true,
4142
variables: false,
@@ -44,17 +45,14 @@ const action: IRawAction = {
4445
},
4546
],
4647
doesFileProcessing: (step: Step) => {
47-
return (
48-
step.parameters.attachments &&
49-
(step.parameters.attachments as IJSONArray).length > 0
50-
)
48+
return step.parameters.file && step.parameters.file !== ''
5149
},
5250
getDataOutMetadata,
5351

5452
async run($) {
55-
const { attachments, infoToExtract } = $.step.parameters as {
56-
attachments?: IJSONArray
57-
infoToExtract: Array<{ infoToExtract: string }>
53+
const { file, prompts } = $.step.parameters as {
54+
file: string
55+
prompts: Array<{ prompt: string }>
5856
}
5957

6058
if (!$.auth.data?.clientId || !$.auth.data?.clientSecret) {
@@ -66,7 +64,7 @@ const action: IRawAction = {
6664
)
6765
}
6866

69-
const result = requestSchema.safeParse({ attachments, infoToExtract })
67+
const result = generalisedModelSchema.safeParse({ file, prompts })
7068
if (!result.success) {
7169
const { stepErrorName, stepErrorSolution } = getValidationError(result)
7270

@@ -79,26 +77,12 @@ const action: IRawAction = {
7977
}
8078

8179
try {
82-
/**
83-
* FIXME (kevinkim-ogp): should only accept one attachment
84-
* use a different selector on the frontend or update the
85-
* multi-select to only allow one attachment
86-
*/
87-
// Pre-call get attachments from S3 first
88-
const attachmentFiles = await getAttachmentsFromS3(
89-
result.data.attachments,
90-
$.flow.id,
91-
)
92-
const attachment = attachmentFiles[0]
80+
// get attachment from S3 first
81+
const attachment = await getAttachmentFromS3(result.data.file, $.flow.id)
9382

9483
// Step 1: get AWS Cognito access token
9584
const token = await getToken($)
9685

97-
/**
98-
* TODO (kevinkim-ogp): first iteration of AISAY will only support synchronous calls
99-
* - add a check to ensure that the attachment is less than 9 MB (7 MB to be safe)
100-
* - add a check to ensure that the call to the model is less than 29 seconds
101-
*/
10286
// Step 2: Call the model to get the output
10387
const res = await $.http.request({
10488
url: `${$.app.baseUrl}/query`,
@@ -109,14 +93,14 @@ const action: IRawAction = {
10993
Authorization: `Bearer ${token}`,
11094
},
11195
data: {
112-
gpt_query: result.data.infoToExtract,
96+
gpt_query: result.data.prompts,
11397
image: attachment.data,
11498
},
11599
})
116100

117101
$.setActionItem({ raw: { ...res.data } })
118102
} catch (err) {
119-
console.error(err)
103+
logger.error(err)
120104
const { stepErrorName, stepErrorSolution } = parseError(err)
121105
throw new StepError(
122106
stepErrorName,

packages/backend/src/apps/aisay/actions/use-specific-model/index.ts

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { IJSONArray, IRawAction } from '@plumber/types'
1+
import { IRawAction } from '@plumber/types'
22

33
import appConfig from '@/config/app'
44
import StepError from '@/errors/step'
5+
import logger from '@/helpers/logger'
56
import Step from '@/models/step'
67

78
import { getToken } from '../../auth/get-token'
89
import { parseError } from '../../common/error-parser'
9-
import { getAttachmentsFromS3, getValidationError } from '../../common/utils'
10+
import { specificModelSchema } from '../../common/schema'
11+
import { getAttachmentFromS3, getValidationError } from '../../common/utils'
1012

1113
import getDataOutMetadata from './get-data-out-metadata'
12-
import { requestSchema } from './schema'
1314

1415
const action: IRawAction = {
1516
name: 'Extract data from specific document types',
@@ -19,8 +20,8 @@ const action: IRawAction = {
1920
arguments: [
2021
{
2122
label: 'File',
22-
key: 'attachments',
23-
type: 'multiselect' as const,
23+
key: 'file',
24+
type: 'dropdown',
2425
required: true,
2526
variables: true,
2627
variableTypes: ['file'],
@@ -41,16 +42,13 @@ const action: IRawAction = {
4142
},
4243
],
4344
doesFileProcessing: (step: Step) => {
44-
return (
45-
step.parameters.attachments &&
46-
(step.parameters.attachments as IJSONArray).length > 0
47-
)
45+
return step.parameters.file && step.parameters.file !== ''
4846
},
4947
getDataOutMetadata,
5048

5149
async run($) {
52-
const { attachments, documentType } = $.step.parameters as {
53-
attachments?: IJSONArray
50+
const { file, documentType } = $.step.parameters as {
51+
file: string
5452
documentType: string
5553
}
5654

@@ -63,7 +61,7 @@ const action: IRawAction = {
6361
)
6462
}
6563

66-
const result = requestSchema.safeParse({ attachments, documentType })
64+
const result = specificModelSchema.safeParse({ file, documentType })
6765
if (!result.success) {
6866
const { stepErrorName, stepErrorSolution } = getValidationError(result)
6967

@@ -76,19 +74,12 @@ const action: IRawAction = {
7674
}
7775

7876
try {
79-
// Pre-call get attachments from S3 first
80-
const attachmentFiles = await getAttachmentsFromS3(
81-
result.data.attachments,
82-
$.flow.id,
83-
)
84-
const attachment = attachmentFiles[0]
77+
// get attachment from S3 first
78+
const attachment = await getAttachmentFromS3(result.data.file, $.flow.id)
8579

8680
// Step 1: get AWS Cognito access token
8781
const token = await getToken($)
8882

89-
// Assuming we do a synchronous call to the model
90-
// which needs to be less than 29 seconds
91-
// and with a document size of less than 9 MB
9283
// Step 2: Call the model to get the output
9384
const aisayRes = await $.http.request({
9485
url: `${appConfig.aisayApiUrl}/query`,
@@ -108,7 +99,7 @@ const action: IRawAction = {
10899
raw: { ...aisayRes.data, documentType: result.data.documentType },
109100
})
110101
} catch (err) {
111-
console.error(err)
102+
logger.error(err)
112103
const { stepErrorName, stepErrorSolution } = parseError(err)
113104

114105
throw new StepError(
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
export const MAX_FILE_SIZE = 6 * 1024 * 1024 // 6 MB
1+
export const DOCUMENT_TYPES = [
2+
'BANK_STATEMENT',
3+
'CHEQUE',
4+
'INVOICE',
5+
'PASSPORT',
6+
'RECEIPT',
7+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Step from '@/models/step'
2+
3+
export const getPrompts = async (stepId: string) => {
4+
const step = await Step.query().findById(stepId)
5+
if (!Array.isArray(step.parameters.prompts)) {
6+
return []
7+
}
8+
return step.parameters.prompts.map((i: { prompt: string }) => i.prompt)
9+
}

0 commit comments

Comments
 (0)