diff --git a/frontend/src/features/admin-form/assistance/AssistanceService.ts b/frontend/src/features/admin-form/assistance/AssistanceService.ts index 97685e6664..786eae4662 100644 --- a/frontend/src/features/admin-form/assistance/AssistanceService.ts +++ b/frontend/src/features/admin-form/assistance/AssistanceService.ts @@ -5,21 +5,21 @@ import { ApiService } from '~services/ApiService' import { ADMIN_FORM_ENDPOINT } from '~features/admin-form/common/AdminViewFormService' export const generateQuestions = async ( - purpose: string, + type: string, + content: string, ): Promise => { return await ApiService.post(`${ADMIN_FORM_ENDPOINT}/assistance/questions`, { - purpose, + type, + content, }).then(({ data }) => data) } export const generateFormFields = async ( - type: string, content: string, ): Promise => { return await ApiService.post( `${ADMIN_FORM_ENDPOINT}/assistance/form-fields`, { - type, content, }, ).then(({ data }) => data) diff --git a/frontend/src/features/admin-form/assistance/mutations.ts b/frontend/src/features/admin-form/assistance/mutations.ts index 80bfc455f5..987aef4309 100644 --- a/frontend/src/features/admin-form/assistance/mutations.ts +++ b/frontend/src/features/admin-form/assistance/mutations.ts @@ -31,13 +31,12 @@ export const useAssistanceMutations = () => { const { onClose } = onCloseContext || {} const createFieldsFromPromptMutation = useMutation((prompt: string) => - generateQuestions(prompt) + generateQuestions(ContentTypes.PROMPT, prompt) .then((questions) => { if (!questions.content) { throw new Error('No content in questions') } - - return generateFormFields(ContentTypes.QUESTIONS, questions.content) + return generateFormFields(questions.content) }) .then((data) => { let formFields @@ -71,35 +70,42 @@ export const useAssistanceMutations = () => { ) const createFieldsFromPdfMutation = useMutation((pdfContent: string) => - generateFormFields(ContentTypes.PDF, pdfContent).then((data) => { - let formFields - if (data.content) { - try { - formFields = JSON.parse(parseModelOutput(data.content)) - } catch (e) { - toast({ - description: `Error creating form. Reason: ${e}`, - status: 'warning', - }) - return + generateQuestions(ContentTypes.PDF, pdfContent) + .then((questions) => { + if (!questions.content) { + throw new Error('No content in questions') } - } - return createFieldsMutation.mutate(formFields, { - onSuccess: () => { - queryClient.invalidateQueries(adminFormKeys.id(formId)) - onClose() - toast({ - description: 'Successfully created form', - }) - }, - onError: () => { - toast({ - description: 'Error creating form.', - status: 'warning', - }) - }, + return generateFormFields(questions.content) }) - }), + .then((data) => { + let formFields + if (data.content) { + try { + formFields = JSON.parse(parseModelOutput(data.content)) + } catch (e) { + toast({ + description: `Error creating form. Reason: ${e}`, + status: 'warning', + }) + return + } + } + return createFieldsMutation.mutate(formFields, { + onSuccess: () => { + queryClient.invalidateQueries(adminFormKeys.id(formId)) + onClose() + toast({ + description: 'Successfully created form', + }) + }, + onError: () => { + toast({ + description: 'Error creating form.', + status: 'warning', + }) + }, + }) + }), ) return { diff --git a/shared/types/assistance.ts b/shared/types/assistance.ts index b85debde8f..8ac53871a3 100644 --- a/shared/types/assistance.ts +++ b/shared/types/assistance.ts @@ -1,4 +1,4 @@ export enum ContentTypes { - QUESTIONS = 'questions', + PROMPT = 'prompt', PDF = 'pdf', } diff --git a/src/app/modules/form/admin-form/admin-form.assistance.controller.ts b/src/app/modules/form/admin-form/admin-form.assistance.controller.ts index c185426f86..2e7b5034fd 100644 --- a/src/app/modules/form/admin-form/admin-form.assistance.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.assistance.controller.ts @@ -16,9 +16,12 @@ import { export const handleGenerateQuestions: ControllerHandler< unknown, ChatCompletionMessage | ErrorDto, - { purpose: string } + { + type: string + content: string + } > = async (req, res) => { - const result = await generateQuestions(req.body.purpose) + const result = await generateQuestions(req.body) if (result.isErr()) { return res.status(500).json({ message: result.error.message }) } @@ -34,11 +37,10 @@ export const handleGenerateFormFields: ControllerHandler< unknown, ChatCompletionMessage | ErrorDto, { - type: string content: string } > = async (req, res) => { - const result = await generateFormFields(req.body) + const result = await generateFormFields(req.body.content) if (result.isErr()) { return res.status(500).json({ message: result.error.message }) } diff --git a/src/app/modules/form/admin-form/admin-form.assistance.service.ts b/src/app/modules/form/admin-form/admin-form.assistance.service.ts index 26bf5df03e..7c32a3d327 100644 --- a/src/app/modules/form/admin-form/admin-form.assistance.service.ts +++ b/src/app/modules/form/admin-form/admin-form.assistance.service.ts @@ -33,20 +33,38 @@ const openai = new OpenAI({ }) /** - * generates a list of questions based on the given purpose - * @param purpose the purpose of the questions, e.g. "registration", "login", etc. + * generates a list of questions based on the given type and content + * @param {string} param.type - The type of content provided. "prompt" or "pdf" + * @param {string} param.content - prompt or parsed pdf content * @returns a ResultAsync containing the generated questions or an AssistanceConnectionError if there was an error connecting to OpenAI */ -export const generateQuestions = ( - purpose: string, -): ResultAsync => { +export const generateQuestions = ({ + type, + content, +}: { + type: string + content: string +}): ResultAsync => { const messages: ChatCompletionMessageParam[] = [ { role: Roles.SYSTEM, content: schemaPromptBuilder(sampleFormFields) }, - { - role: Roles.USER, - content: questionListPromptBuilder(purpose), - }, ] + switch (type) { + case ContentTypes.PROMPT: + messages.push({ + role: Roles.USER, + content: questionListPromptBuilder(content), + }) + break + case ContentTypes.PDF: + messages.push({ + role: Roles.USER, + content: migratePromptBuilder(content), + }) + break + default: + return errAsync(new AssistanceModelTypeError()) + } + return ResultAsync.fromPromise( openai.chat.completions.create({ messages: messages, @@ -62,7 +80,8 @@ export const generateQuestions = ( message: `Error while generating questions: ${errorMessage}`, meta: { action: 'generateQuestions', - purpose, + type, + content, }, error, }) @@ -77,42 +96,21 @@ export const generateQuestions = ( /** * Generates form fields based on the given type and content. * - * @param {object} param - The type and content parameters. - * @param {string} param.type - The type of content provided. "questions" or "pdf" - * @param {string} param.content - List of questions or parsed pdf content + * @param {string} param.questions - List of questions * * @returns {ResultAsync} A ResultAsync with either a ChatCompletionMessage or an error. * Possible errors include AssistanceConnectionError and AssistanceModelTypeError. */ -export const generateFormFields = ({ - type, - content, -}: { - type: string - content: string -}): ResultAsync< +export const generateFormFields = ( + questions: string, +): ResultAsync< ChatCompletionMessage, AssistanceConnectionError | AssistanceModelTypeError > => { const messages: ChatCompletionMessageParam[] = [ { role: Roles.SYSTEM, content: schemaPromptBuilder(sampleFormFields) }, + { role: Roles.USER, content: formFieldsPromptBuilder(questions) }, ] - switch (type) { - case ContentTypes.QUESTIONS: - messages.push({ - role: Roles.USER, - content: formFieldsPromptBuilder(content), - }) - break - case ContentTypes.PDF: - messages.push({ - role: Roles.USER, - content: migratePromptBuilder(content), - }) - break - default: - return errAsync(new AssistanceModelTypeError()) - } return ResultAsync.fromPromise( openai.chat.completions.create({ messages: messages, @@ -127,7 +125,7 @@ export const generateFormFields = ({ message: `Error while generating form fields: ${errorMessage}`, meta: { action: 'generateFormFields', - type, + questions, }, error, }) diff --git a/src/app/modules/form/admin-form/admin-form.assistance.utils.ts b/src/app/modules/form/admin-form/admin-form.assistance.utils.ts index 4faa41c628..3ca9c020fe 100644 --- a/src/app/modules/form/admin-form/admin-form.assistance.utils.ts +++ b/src/app/modules/form/admin-form/admin-form.assistance.utils.ts @@ -16,7 +16,7 @@ const expectedQuestionsListFormat = '---\n1. | \n2. ...\n---' export const schemaPromptBuilder = (schema: any) => { - const prompt = `I am a form builder that has the possible form field schemas in the following list:\n + const prompt = `I am a FormSG, a form builder that has the possible form field schemas in the following list:\n ${JSON.stringify(schema)} Please keep any null values in the schema as null, and false values in the schema as false. Strictly include all keys in the schema, even if they are null or false.` return prompt @@ -33,13 +33,12 @@ export const formFieldsPromptBuilder = (questions: string) => { Present the questions as form fields in JSON (list of form field schemas), in the form of "${expectedFormFieldSchemaFormat}" as defined by the system, without any code blocks. Format the JSON as a single line.` return prompt } - export const migratePromptBuilder = (parsedContent: string) => { const prompt = `Help me generate the corresponding JSON form fields from content parsed from a PDF document. Here is the parsed content from the PDF document (wrapped in triple quotes): """ ${parsedContent} """ - Based on the parsed content, extract content that should be added to the form builder form and present them as a list, in the form of "${expectedFormFieldSchemaFormat}".` + Based on the parsed content, extract content that should be added to the form builder form and present them as a list, in the form of "${expectedQuestionsListFormat}".` return prompt }