Skip to content

Commit

Permalink
feat: generate questions from prompt or pdf and use them to generate …
Browse files Browse the repository at this point in the history
…form fields
  • Loading branch information
sebastianwzq committed Jan 31, 2024
1 parent 001fd36 commit 23f203d
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatCompletionMessage> => {
return await ApiService.post(`${ADMIN_FORM_ENDPOINT}/assistance/questions`, {
purpose,
type,
content,
}).then(({ data }) => data)
}

export const generateFormFields = async (
type: string,
content: string,
): Promise<ChatCompletionMessage> => {
return await ApiService.post(
`${ADMIN_FORM_ENDPOINT}/assistance/form-fields`,
{
type,
content,
},
).then(({ data }) => data)
Expand Down
66 changes: 36 additions & 30 deletions frontend/src/features/admin-form/assistance/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion shared/types/assistance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum ContentTypes {
QUESTIONS = 'questions',
PROMPT = 'prompt',
PDF = 'pdf',
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand All @@ -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 })
}
Expand Down
72 changes: 35 additions & 37 deletions src/app/modules/form/admin-form/admin-form.assistance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatCompletionMessage, AssistanceConnectionError> => {
export const generateQuestions = ({
type,
content,
}: {
type: string
content: string
}): ResultAsync<ChatCompletionMessage, AssistanceConnectionError> => {
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,
Expand All @@ -62,7 +80,8 @@ export const generateQuestions = (
message: `Error while generating questions: ${errorMessage}`,
meta: {
action: 'generateQuestions',
purpose,
type,
content,
},
error,
})
Expand All @@ -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,
Expand All @@ -127,7 +125,7 @@ export const generateFormFields = ({
message: `Error while generating form fields: ${errorMessage}`,
meta: {
action: 'generateFormFields',
type,
questions,
},
error,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const expectedQuestionsListFormat =
'---\n1. <question> | <answer type>\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
Expand All @@ -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
}

0 comments on commit 23f203d

Please sign in to comment.