diff --git a/docs/cspell.dict.txt b/docs/cspell.dict.txt index c1af56b..387b3fa 100644 --- a/docs/cspell.dict.txt +++ b/docs/cspell.dict.txt @@ -11,6 +11,7 @@ langchain libphonenumber listbox listitem +Multicorrect nodenext ollama openai diff --git a/src/docFillerCore/engines/fillerEngine.ts b/src/docFillerCore/engines/fillerEngine.ts index 305f459..53382d7 100644 --- a/src/docFillerCore/engines/fillerEngine.ts +++ b/src/docFillerCore/engines/fillerEngine.ts @@ -10,93 +10,72 @@ export class FillerEngine { public async fill( fieldType: QType, fieldValue: ExtractedValue, - value: string + value: any ): Promise { if (fieldType === null) return false; switch (fieldType) { case QType.TEXT: - return this.fillText(fieldValue, '1000'); + return this.fillText(fieldValue, value); case QType.TEXT_EMAIL: - return this.fillEmail(fieldValue, 'harshit123@gmail.com'); + return this.fillEmail(fieldValue, value); case QType.TEXT_URL: - return this.fillTextURL(fieldValue, 'https://google.com'); + return this.fillTextURL(fieldValue, value); case QType.PARAGRAPH: - return this.fillParagraph(fieldValue, 'this is a paragraph \new'); + return this.fillParagraph(fieldValue, value); case QType.LINEAR_SCALE: - return await this.fillLinearScale(fieldValue, '1'); + return await this.fillLinearScale(fieldValue, value); case QType.DROPDOWN: - return await this.fillDropDown(fieldValue, 'Option 3'); + return await this.fillDropDown(fieldValue, value); case QType.CHECKBOX_GRID: - return await this.fillCheckboxGrid(fieldValue, [ - { - row: 'Row 1', - cols: [{ data: 'Column 1' }, { data: 'Column 2' }], - }, - { row: 'Row 2', cols: [{ data: 'Column 2' }] }, - ] as RowColumnOption[]); + return await this.fillCheckboxGrid(fieldValue, value); case QType.MULTIPLE_CHOICE_GRID: - return await this.fillMultipleChoiceGrid(fieldValue, [ - { row: 'Row 1', selectedColumn: 'Column 1' }, - { row: 'Row 2', selectedColumn: 'Column 2' }, - { row: 'Brooooo', selectedColumn: 'Column 2' }, - ]); + return await this.fillMultipleChoiceGrid(fieldValue, value); case QType.DATE: - return this.fillDate(fieldValue, '11-11-2111'); + return this.fillDate(fieldValue, value); case QType.DATE_AND_TIME: - return await this.fillDateAndTime(fieldValue, '01-01-2003-01-11'); + return await this.fillDateAndTime(fieldValue, value); case QType.DATE_TIME_WITH_MERIDIEM: - return await this.fillDateAndTimeWithMeridiem( - fieldValue, - '11-11-2023-11-39-PM' - ); + return await this.fillDateAndTimeWithMeridiem(fieldValue, value); case QType.DATE_TIME_WITH_MERIDIEM_WITHOUT_YEAR: return await this.fillDateTimeWithMeridiemWithoutYear( fieldValue, - '11-11-11-39-PM' + value ); case QType.TIME_WITH_MERIDIEM: - return await this.fillTimeWithMeridiem(fieldValue, '11-39-PM'); + return await this.fillTimeWithMeridiem(fieldValue, value); case QType.TIME: - return this.fillTime(fieldValue, '02-02'); + return this.fillTime(fieldValue, value); case QType.DURATION: - return this.fillDuration(fieldValue, '11-11-11'); + return this.fillDuration(fieldValue, value); case QType.DATE_WITHOUT_YEAR: - return this.fillDateWithoutYear(fieldValue, '11-11'); + return this.fillDateWithoutYear(fieldValue, value); case QType.DATE_TIME_WITHOUT_YEAR: - return await this.fillDateTimeWithoutYear(fieldValue, '22-01-01-01'); + return await this.fillDateTimeWithoutYear(fieldValue, value); case QType.MULTI_CORRECT_WITH_OTHER: case QType.MULTI_CORRECT: - return await this.fillMultiCorrectWithOther(fieldValue, [ - { optionText: 'Sightseeing' }, - { optionText: 'Day 2' }, - { isOther: true, otherOptionValue: 'My name is Andrew!' }, - ] as MultiCorrectOrMultipleOption[]); + return await this.fillMultiCorrectWithOther(fieldValue, value); case QType.MULTIPLE_CHOICE_WITH_OTHER: case QType.MULTIPLE_CHOICE: - return await this.fillMultipleChoiceWithOther(fieldValue, { - // optionText: 'Option 2', - isOther: true, - otherOptionValue: 'Random', - } as MultiCorrectOrMultipleOption); + return await this.fillMultipleChoiceWithOther(fieldValue, value); } } @@ -110,50 +89,47 @@ export class FillerEngine { return false; } - private fillEmail(fieldValue: ExtractedValue, value: string): boolean { - return this.fillText(fieldValue, value); + private fillEmail( + fieldValue: ExtractedValue, + value: GenericLLMResponse + ): boolean { + return this.fillText(fieldValue, value?.answer); } - private fillTextURL(fieldValue: ExtractedValue, value: string): boolean { - return this.fillText(fieldValue, value); + private fillTextURL( + fieldValue: ExtractedValue, + value: GenericLLMResponse + ): boolean { + return this.fillText(fieldValue, value?.answer); } private fillParagraph(fieldValue: ExtractedValue, value: string): boolean { return this.fillText(fieldValue, value); } - private fillDate(fieldValue: ExtractedValue, value: string): boolean { - const datePattern = /^(\d{2})-(\d{2})-(\d{4})$/; + private fillDate(fieldValue: ExtractedValue, value: Date): boolean { + if (!(value instanceof Date)) return false; - if (!datePattern.test(value)) return false; + const date = value; - const [day, month, year] = value.split('-'); - const date = new Date(`${year}-${month}-${day}`); - - // Invalid date format as raised by date constructor - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#return_value if (isNaN(date.valueOf())) return false; - if ( - !( - date.getDate() === Number(day) && - // Month numbering start from 0...11 - date.getMonth() + 1 === Number(month) && - date.getFullYear() === Number(year) - ) - ) { - return false; - } + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString(); const inputEvent = new Event('input', { bubbles: true }); + if (fieldValue.date) { fieldValue.date.value = day; fieldValue.date.dispatchEvent(inputEvent); } + if (fieldValue.month) { fieldValue.month.value = month; fieldValue.month.dispatchEvent(inputEvent); } + if (fieldValue.year) { fieldValue.year.value = year; fieldValue.year.dispatchEvent(inputEvent); @@ -164,30 +140,20 @@ export class FillerEngine { private async fillDateAndTime( fieldValue: ExtractedValue, - value: string + value: Date ): Promise { await sleep(SLEEP_DURATION); + if (!(value instanceof Date)) return false; - const datePattern = /^(\d{2})-(\d{2})-(\d{4})-(\d{2})-(\d{2})$/; - if (!datePattern.test(value)) return false; + const date = value; - const [day, month, year, hours, minutes] = value.split('-'); - const date = new Date(`${year}-${month}-${day}`); - - // Invalid date format as raised by date constructor - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#return_value if (isNaN(date.valueOf())) return false; - if ( - !( - date.getDate() === Number(day) && - // Month numbering start from 0...11 - date.getMonth() + 1 === Number(month) && - date.getFullYear() === Number(year) - ) - ) { - return false; - } + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString(); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); const inputEvent = new Event('input', { bubbles: true }); @@ -195,35 +161,49 @@ export class FillerEngine { fieldValue.date.value = day; fieldValue.date.dispatchEvent(inputEvent); } + if (fieldValue.month) { fieldValue.month.value = month; fieldValue.month.dispatchEvent(inputEvent); } + if (fieldValue.year) { fieldValue.year.value = year; fieldValue.year.dispatchEvent(inputEvent); } + if (fieldValue.hour) { fieldValue.hour.value = hours; fieldValue.hour.dispatchEvent(inputEvent); } + if (fieldValue.minute) { fieldValue.minute.value = minutes; fieldValue.minute.dispatchEvent(inputEvent); } + return true; } private async fillDateTimeWithMeridiemWithoutYear( fieldValue: ExtractedValue, - value: string + value: Date ): Promise { await sleep(SLEEP_DURATION); - const dateTimePattern = /^(\d{2})-(\d{2})-(\d{2})-(\d{2})-(AM|PM)$/; - if (!dateTimePattern.test(value)) return false; + if (!(value instanceof Date)) return false; + + const date = value; + + if (isNaN(date.valueOf())) return false; + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const hours24 = date.getHours(); + const minutes = date.getMinutes().toString().padStart(2, '0'); - const [day, month, hours, minutes, meridiem] = value.split('-'); + const meridiem = hours24 >= 12 ? 'PM' : 'AM'; + const hours = (hours24 % 12 || 12).toString().padStart(2, '0'); const inputEvent = new Event('input', { bubbles: true }); @@ -231,14 +211,17 @@ export class FillerEngine { fieldValue.date.value = day; fieldValue.date.dispatchEvent(inputEvent); } + if (fieldValue.month) { fieldValue.month.value = month; fieldValue.month.dispatchEvent(inputEvent); } + if (fieldValue.hour) { fieldValue.hour.value = hours; fieldValue.hour.dispatchEvent(inputEvent); } + if (fieldValue.minute) { fieldValue.minute.value = minutes; fieldValue.minute.dispatchEvent(inputEvent); @@ -275,24 +258,32 @@ export class FillerEngine { } } } + return false; } private async fillTimeWithMeridiem( fieldValue: ExtractedValue, - value: string + value: Date ): Promise { await sleep(SLEEP_DURATION); - const timePattern = /^(\d{2})-(\d{2})-(AM|PM)$/; - if (!timePattern.test(value)) return false; + if (!(value instanceof Date)) return false; - const [hours, minutes, meridiem] = value.split('-'); + const date = value; + + if (isNaN(date.valueOf())) return false; + + let hours = date.getHours(); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + const meridiem = hours >= 12 ? 'PM' : 'AM'; + const hours12 = (hours % 12 || 12).toString().padStart(2, '0'); const inputEvent = new Event('input', { bubbles: true }); if (fieldValue.hour) { - fieldValue.hour.value = hours; + fieldValue.hour.value = hours12; fieldValue.hour.dispatchEvent(inputEvent); } if (fieldValue.minute) { @@ -335,24 +326,25 @@ export class FillerEngine { private async fillDateAndTimeWithMeridiem( fieldValue: ExtractedValue, - value: string + value: Date ): Promise { await sleep(SLEEP_DURATION); - const dateTimePattern = /^(\d{2})-(\d{2})-(\d{4})-(\d{2})-(\d{2})-(AM|PM)$/; - if (!dateTimePattern.test(value)) return false; + if (!(value instanceof Date)) return false; - const [day, month, year, hours, minutes, meridiem] = value.split('-'); - const date = new Date(`${year}-${month}-${day}`); + const date = value; - if ( - isNaN(date.valueOf()) || - date.getDate() !== Number(day) || - date.getMonth() + 1 !== Number(month) || - date.getFullYear() !== Number(year) - ) { - return false; - } + if (isNaN(date.valueOf())) return false; + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const year = date.getFullYear().toString(); + + let hours = date.getHours(); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + const meridiem = hours >= 12 ? 'PM' : 'AM'; + const hours12 = (hours % 12 || 12).toString().padStart(2, '0'); const inputEvent = new Event('input', { bubbles: true }); @@ -368,8 +360,9 @@ export class FillerEngine { fieldValue.year.value = year; fieldValue.year.dispatchEvent(inputEvent); } + if (fieldValue.hour) { - fieldValue.hour.value = hours; + fieldValue.hour.value = hours12; fieldValue.hour.dispatchEvent(inputEvent); } if (fieldValue.minute) { @@ -410,18 +403,28 @@ export class FillerEngine { return false; } - private fillTime(fieldValue: ExtractedValue, value: string): boolean { - const [hours, minutes] = value.split('-'); + private fillTime(fieldValue: ExtractedValue, value: Date): boolean { + if (!(value instanceof Date)) return false; + + const date = value; + + if (isNaN(date.valueOf())) return false; + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const inputEvent = new Event('input', { bubbles: true }); if (fieldValue.hour) { fieldValue.hour.value = hours; fieldValue.hour.dispatchEvent(inputEvent); } + if (fieldValue.minute) { fieldValue.minute.value = minutes; fieldValue.minute.dispatchEvent(inputEvent); } + return true; } @@ -447,15 +450,24 @@ export class FillerEngine { private fillDateWithoutYear( fieldValue: ExtractedValue, - value: string + value: Date ): boolean { - const [day, month] = value.split('-'); + if (!(value instanceof Date)) return false; + + const date = value; + + if (isNaN(date.valueOf())) return false; + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const inputEvent = new Event('input', { bubbles: true }); if (fieldValue.date) { fieldValue.date.value = day; fieldValue.date.dispatchEvent(inputEvent); } + if (fieldValue.month) { fieldValue.month.value = month; fieldValue.month.dispatchEvent(inputEvent); @@ -466,25 +478,38 @@ export class FillerEngine { private async fillDateTimeWithoutYear( fieldValue: ExtractedValue, - value: string + value: Date ): Promise { await sleep(SLEEP_DURATION); - const [day, month, hours, minutes] = value.split('-'); + if (!(value instanceof Date)) return false; + + const date = value; + + if (isNaN(date.valueOf())) return false; + + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const inputEvent = new Event('input', { bubbles: true }); if (fieldValue.date) { fieldValue.date.value = day; fieldValue.date.dispatchEvent(inputEvent); } + if (fieldValue.month) { fieldValue.month.value = month; fieldValue.month.dispatchEvent(inputEvent); } + if (fieldValue.hour) { fieldValue.hour.value = hours; fieldValue.hour.dispatchEvent(inputEvent); } + if (fieldValue.minute) { fieldValue.minute.value = minutes; fieldValue.minute.dispatchEvent(inputEvent); @@ -567,21 +592,23 @@ export class FillerEngine { private async fillLinearScale( fieldValue: ExtractedValue, - value: string + value: GenericLLMResponse ): Promise { await sleep(SLEEP_DURATION); + for (const index in fieldValue.options) { + if (fieldValue.options?.hasOwnProperty(index)) { + const scale = fieldValue.options[Number(index)]; - fieldValue.options?.forEach((scale) => { - if (scale.data === value) { - if (scale.dom?.getAttribute('aria-checked') !== 'true') { - (scale.dom as HTMLInputElement)?.dispatchEvent( - new Event('click', { bubbles: true }) - ); + if (scale.data.toString() === value?.answer?.toString()) { + if (scale.dom?.getAttribute('aria-checked') !== 'true') { + (scale.dom as HTMLInputElement)?.dispatchEvent( + new Event('click', { bubbles: true }) + ); + } return true; } } - }); - + } return false; } @@ -649,11 +676,12 @@ export class FillerEngine { private async fillDropDown( fieldValue: ExtractedValue, - value: string + value: GenericLLMResponse ): Promise { await sleep(SLEEP_DURATION); + for (const option of fieldValue.options || []) { - if (option.data === value) { + if (option.data === value?.answer) { const dropdown = option.dom; if (fieldValue.dom) { if (fieldValue.dom?.getAttribute('aria-expanded') !== 'true') { @@ -662,10 +690,13 @@ export class FillerEngine { ?.dispatchEvent(new Event('click', { bubbles: true })); await sleep(SLEEP_DURATION); } + const allOptions = fieldValue.dom.querySelectorAll(`div[role=option]`); allOptions.forEach((possibleOption) => { - if (possibleOption.querySelector('span')?.textContent === value) { + if ( + possibleOption.querySelector('span')?.textContent === value.answer + ) { possibleOption.dispatchEvent( new Event('click', { bubbles: true }) ); diff --git a/src/docFillerCore/engines/gptEngine.ts b/src/docFillerCore/engines/gptEngine.ts index 7f68f1e..cc251cc 100644 --- a/src/docFillerCore/engines/gptEngine.ts +++ b/src/docFillerCore/engines/gptEngine.ts @@ -155,9 +155,16 @@ export class LLMEngine { ): StructuredOutputParser | DatetimeOutputParser | StringOutputParser { switch (questionType) { case QType.TEXT: + return new StringOutputParser(); case QType.TEXT_EMAIL: + return StructuredOutputParser.fromNamesAndDescriptions({ + answer: + 'Give Correct Email corresponding to given question or give random@gmail.com', + }); case QType.TEXT_URL: - return new StringOutputParser(); + return StructuredOutputParser.fromNamesAndDescriptions({ + answer: 'Give Correct Url corresponding to given question', + }); case QType.DATE: case QType.TIME: @@ -171,6 +178,10 @@ export class LLMEngine { return new DatetimeOutputParser(); case QType.LINEAR_SCALE: + return StructuredOutputParser.fromNamesAndDescriptions({ + answer: + "The integer answer to the user's question as the key corresponding to the calculated answer", + }); case QType.DROPDOWN: return StructuredOutputParser.fromNamesAndDescriptions({ answer: "answer to the user's question", diff --git a/src/docFillerCore/engines/parserEngine.ts b/src/docFillerCore/engines/parserEngine.ts deleted file mode 100644 index d6c1708..0000000 --- a/src/docFillerCore/engines/parserEngine.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { QType } from '@utils/questionTypes'; -import ValidationUtils from '@utils/validationUtils'; - -export class ParserEngine { - private validationUtil: ValidationUtils; - - constructor() { - this.validationUtil = new ValidationUtils(); - } - - public parse( - fieldType: QType, - extractedValue: ExtractedValue, - response: string - ): boolean | null { - // TODO: Just for testing, remove this later - const testResponse = { - TEXT: 'Andrew', - EMAIL: 'abc@gmail.com', - PARAGRAPH: - "Please enjoy these great stories, fairy-tales, fables, and nursery rhymes for children. They help kids learn to read and make excellent bedtime stories! We have hundreds of great children's stories for you to share.", - MULTI_CORRECT: 'Swimming\nHiking', - MULTI_CORRECT_WITH_OTHER: 'Day 1\nDay 2', - DATE: '22-12-2022', - TIME: '10-12', - DATE_AND_TIME: '22-12-2022-12-12', - DURATION: '21-34-58', - DATE_WITHOUT_YEAR: '31-12', - DATE_TIME_WITHOUT_YEAR: '31-12-17-12', - MULTIPLE_CHOICE: 'Tanzania', - MULTIPLE_CHOICE_WITH_OTHER: 'Tanzania', - LINEAR_SCALE: '1', - MULTIPLE_CHOICE_GRID: - 'Strongly disagree\nAgree\nStrongly agree\nDisagree', - CHECKBOX_GRID: 'Japan\nCanada', - DROPDOWN: 'Asia', - }; - - const validatedResponse = this.validateResponse(response); - - if (validatedResponse === false) { - return false; - } - - if (fieldType !== null && extractedValue !== null) { - switch (fieldType) { - case QType.TEXT: - return this.validateText(testResponse.TEXT); - case QType.TEXT_EMAIL: - return this.validateEmail(testResponse.EMAIL); - case QType.MULTI_CORRECT_WITH_OTHER: - return this.validateMultiCorrectWithOther( - extractedValue, - testResponse.MULTI_CORRECT_WITH_OTHER - ); - case QType.MULTI_CORRECT: - return this.validateMultiCorrect( - extractedValue, - testResponse.MULTI_CORRECT - ); - case QType.PARAGRAPH: - return this.validateParagraph(testResponse.PARAGRAPH); - case QType.DATE: - return this.validateDate(testResponse.DATE); - case QType.DATE_AND_TIME: - return this.validateDateAndTime(testResponse.DATE_AND_TIME); - case QType.TIME: - return this.validateTime(testResponse.TIME); - case QType.DURATION: - return this.validateDuration(testResponse.DURATION); - case QType.DATE_WITHOUT_YEAR: - return this.validateDateWithoutYear(testResponse.DATE_WITHOUT_YEAR); - case QType.DATE_TIME_WITHOUT_YEAR: - return this.validateDateTimeWithoutYear( - testResponse.DATE_TIME_WITHOUT_YEAR - ); - case QType.MULTIPLE_CHOICE: - return this.validateMultipleChoice( - extractedValue, - testResponse.MULTIPLE_CHOICE - ); - case QType.MULTIPLE_CHOICE_WITH_OTHER: - return this.validateMultipleChoiceWithOther( - extractedValue, - testResponse.MULTIPLE_CHOICE_WITH_OTHER - ); - case QType.LINEAR_SCALE: - return this.validateLinearScale( - extractedValue, - testResponse.LINEAR_SCALE - ); - case QType.MULTIPLE_CHOICE_GRID: - return this.validateMultipleChoiceGrid( - extractedValue, - testResponse.MULTIPLE_CHOICE_GRID - ); - // case QType.CHECKBOX_GRID: - // return this.validateCheckBoxGrid(extractedValue, testResponse.CHECKBOX_GRID); - case QType.DROPDOWN: - return this.validateDropdown(extractedValue, testResponse.DROPDOWN); - default: - return null; - } - } else { - return false; - } - } - - // TODO: Check if it not one of those LLM error messages like those on safety etc - - private validateResponse(response: string): boolean { - return !response.startsWith('As an AI model'); - } - - private validateText(response: string): boolean { - const text = response.trim(); - return text.length > 0 && !(text.includes('\n') || text.includes('\r')); - } - - private validateParagraph(response: string): boolean { - return response.trim().length > 0; - } - - private validateEmail(response: string): boolean { - // Checking valid email in accord to RFC 5322 - // https://stackoverflow.com/questions/201323/how-can-i-validate-an-email-address-using-a-regular-expression/201378#201378 - - const mailRegex = - /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/; - return Boolean(response && response.match(mailRegex)); - } - - private validateDate(response: string): boolean { - // Accepts: dd-MM-yyyy - const dateValid = - /^([0][1-9]|[1-2][0-9]|[3][0-1])-([0][1-9]|[1][0-2])-(\d{4})$/; - const dateArr = response.match(dateValid); - if (dateArr) { - const [_, date, month, year] = dateArr; - return this.validationUtil.validateDate(date, month, year); - } - return false; - } - - private validateDateAndTime(response: string): boolean { - // Accepts: dd-MM-yyyy-hh-mm - const dateTimeValid = - /^([0][1-9]|[1-2][0-9]|[3][0-1])-([0][1-9]|[1][0-2])-(\d{4})-([01][0-9]|[2][0-3])-([0-5][0-9])$/; - const dateArr = response.match(dateTimeValid); - if (dateArr) { - const [_, date, month, year, hour, minute] = dateArr; - return this.validationUtil.validateDate(date, month, year); - } - return false; - } - - private validateTime(response: string): boolean { - // Accepts: hh-mm - const timeValid = /^([01][0-9]|[2][0-3])-([0-5][0-9])$/; - return Boolean(response && response.match(timeValid)); - } - - private validateDuration(response: string): boolean { - // Accepts: hh-mm-ss - const durationValid = /^([01][0-9]|[2][0-3])(-[0-5][0-9]){2}$/; - return Boolean(response && response.match(durationValid)); - } - - private validateDateWithoutYear(response: string): boolean { - // Accepts: dd-MM - const dateWYearValid = - /^([0][1-9]|[1-2][0-9]|[3][0-1])-([0][1-9]|[1][0-2])$/; - const dateArr = response.match(dateWYearValid); - if (dateArr) { - const [_, date, month] = dateArr; - return this.validationUtil.validateDate(date, month); - } - return false; - } - - private validateDateTimeWithoutYear(response: string): boolean { - // Accepts: dd-MM-hh-mm - const dateTimeWYearValid = - /^([0][1-9]|[1-2][0-9]|[3][0-1])-([0][1-9]|[1][0-2])-([01][0-9]|[2][0-3])-([0-5][0-9])$/; - const dateArr = response.match(dateTimeWYearValid); - if (dateArr) { - const [_, date, month, hour, minute] = dateArr; - return this.validationUtil.validateDate(date, month); - } - return false; - } - - private validateMultiCorrect( - extractedValue: ExtractedValue, - response: string - ): boolean { - const responseOptions = response - .split(/\r?\n/) - .map((option) => option.trim()); - const actualOptions = extractedValue.options?.map((option) => - option.data.trim() - ); - - return responseOptions.every((option) => - actualOptions?.some( - (actualOption) => option.toLowerCase() === actualOption.toLowerCase() - ) - ); - } - - private validateMultiCorrectWithOther( - extractedValue: ExtractedValue, - response: string - ): boolean { - return ( - this.validateMultiCorrect(extractedValue, response) || - (response.startsWith('Other') && this.validateText(response)) - ); - } - - private validateMultipleChoice( - extractedValue: ExtractedValue, - response: string - ): boolean { - const trimmedResponse = response.trim(); - const actualOptions = extractedValue.options?.map((option) => - option.data.trim() - ); - - return Boolean( - actualOptions?.some( - (option) => option.toLowerCase() === trimmedResponse.toLowerCase() - ) - ); - } - - private validateMultipleChoiceWithOther( - extractedValue: ExtractedValue, - response: string - ): boolean { - return ( - this.validateMultipleChoice(extractedValue, response) || - (response.startsWith('Other') && this.validateText(response)) - ); - } - - private validateLinearScale( - extractedValue: ExtractedValue, - response: string - ): boolean { - // Validations works the same as for MultipleChoice behind the hood! - return this.validateMultipleChoice(extractedValue, response); - } - - private validateMultipleChoiceGrid( - extractedValue: ExtractedValue, - response: string - ): boolean { - const responseOptions = response - .trim() - .split(/\r?\n/) - .map((option) => option.trim()); - - if (responseOptions.length !== (extractedValue.rowArray || []).length) { - return false; - } - - const actualOptions = extractedValue.columnArray || []; - - return responseOptions.every((option) => - actualOptions.some( - (actualOption) => option.toLowerCase() === actualOption.toLowerCase() - ) - ); - } - - // TODO: Implement this - private validateCheckBoxGrid( - extractedValue: ExtractedValue, - response: string - ): boolean { - return false; - // return this.validateMultipleChoiceGrid(extractedValue, response); - } - - private validateDropdown( - extractedValue: ExtractedValue, - response: string - ): boolean { - // Validations works the same as for MultipleChoice behind the hood! - - return this.validateMultipleChoice(extractedValue, response); - } -} diff --git a/src/docFillerCore/engines/promptEngine.ts b/src/docFillerCore/engines/promptEngine.ts index 93104b1..ce22026 100644 --- a/src/docFillerCore/engines/promptEngine.ts +++ b/src/docFillerCore/engines/promptEngine.ts @@ -140,9 +140,7 @@ export class PromptEngine { `represents "${ bounds?.upperBound ?? EMPTY_STRING }" with uniform distribution between.\n` + - `Instructions: Return only the integer answer and nothing else. ` + - `Only return the key corresponding to the calculated answer\n\nQuestion: ` + - `${title} ${description}` + `\nQuestion: ${title} ${description}` ); } @@ -281,90 +279,50 @@ export class PromptEngine { } private getDatePrompt(title: string, description: string): string { - return ( - `Please provide the date (in format dd-MM-yyyy) - (only return dd-MM-yyyy date, nothing ` + - `else, don't give any other sentence) that best corresponds to your response for the following ` + - `question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time, nothing else, and do not include any other text.`; } private getTimePrompt(title: string, description: string): string { - return ( - `Please provide the time (in format hh-mm) - (only return hh-mm time, ` + - `nothing else, don't include any other text) that best corresponds to ` + - `your response for the following question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time, nothing else, and do not include any other text.`; } private getTimeWithMeridiemPrompt( title: string, description: string ): string { - return ( - `Please provide the time (in format hh-mm (AM or PM)) - (only return ` + - `hh:mm (AM or PM) time, nothing else, don't include any other text) that ` + - `best corresponds to your response for the following question: ` + - `${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time, nothing else, and do not include any other text.`; } private getDateTimeWithMeridiemPrompt( title: string, description: string ): string { - return ( - `Please strictly provide the date and time (in format dd-MM-yyyy-hh-mm (AM or PM)) - ` + - `(only return dd-MM-yyyy-hh-mm date and time, nothing else and please don't give any prompt, ` + - `just give the exact answer, include no string in answer) that best corresponds to your response ` + - `for the following question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time (fill in the required fields among year, month, date, hour, minute, seconds as per the question; keep the rest as "00"), nothing else, and do not include any other text.`; } private getDateTimePrompt(title: string, description: string): string { - return ( - `Please strictly provide the date and time (in format dd-MM-yyyy-hh-mm) - (only return ` + - `dd-MM-yyyy-hh-mm date and time, nothing else and please don't give any prompt, just give the ` + - `exact answer, include no string in answer) that best corresponds to your response for the ` + - `following question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time (fill in the required fields among year, month, date, hour, minute, seconds as per the question; keep the rest as "00"), nothing else, and do not include any other text.`; } private getDurationPrompt(title: string, description: string): string { - return ( - `Please provide the duration that best corresponds to your response (give only answer ` + - `and nothing else in this format hh-mm-ss) for the following question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time (fill in the required fields among year, month, date, hour, minute, seconds as per the question; keep the rest as "00"), nothing else, and do not include any other text.`; } private getDateWithoutYearPrompt(title: string, description: string): string { - return ( - `Please provide the date (month and day) - (in format dd-MM) - (only return dd-MM date, ` + - `nothing else) that best corresponds to your response for the following question: ${description} ` + - `${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time (fill in the required fields among year, month, date, hour, minute, seconds as per the question; keep the rest as "00"), nothing else, and do not include any other text.`; } private getDateTimeWithoutYearPrompt( title: string, description: string ): string { - return ( - `Please strictly provide the date without year and time (in format dd-MM-hh-mm) - ` + - `(only return dd-MM-hh-mm date without year and time, nothing else and please don't give any ` + - `prompt, just give the exact answer, include no string in answer) that best corresponds to ` + - `your response for the following question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time (fill in the required fields among year, month, date, hour, minute, seconds as per the question; keep the rest as "00"), nothing else, and do not include any other text.`; } private getDateTimeWithMeridiemWithoutYear( title: string, description: string ): string { - return ( - `Please strictly provide the date without year and time (in format dd-MM-hh-mm (AM or PM)) - ` + - `(only return dd-MM-hh-mm date without year and time, nothing else and please don't give any ` + - `prompt, just give the exact answer, include no string in answer) that best corresponds to ` + - `your response for the following question: ${description} ${title}` - ); + return `Please provide the correct time that best corresponds to your response for the following question: ${description} ${title}. Only return the correct time as per question(fill in the required fields among year, month, date, hour, minute, seconds as per the question; keep the rest as "00"), nothing else, and do not include any other text.`; } } diff --git a/src/docFillerCore/engines/validatorEngine.ts b/src/docFillerCore/engines/validatorEngine.ts new file mode 100644 index 0000000..0e7ded2 --- /dev/null +++ b/src/docFillerCore/engines/validatorEngine.ts @@ -0,0 +1,259 @@ +import { EMPTY_STRING } from '@utils/constant'; +import { QType } from '@utils/questionTypes'; + +export class ValidatorEngine { + constructor() {} + + public validate( + fieldType: QType, + extractedValue: ExtractedValue, + response: any + ): boolean | null { + if (!response) { + return false; + } + + if (fieldType !== null && extractedValue !== null) { + switch (fieldType) { + case QType.TEXT: + return this.validateText(response); + case QType.TEXT_EMAIL: + return this.validateEmail(response); + case QType.MULTI_CORRECT_WITH_OTHER: + return this.validateMultiCorrectWithOther(extractedValue, response); + case QType.MULTI_CORRECT: + return this.validateMultiCorrect(extractedValue, response); + case QType.PARAGRAPH: + return this.validateParagraph(response); + case QType.DATE: + return this.validateDate(response); + case QType.DATE_AND_TIME: + return this.validateDateAndTime(response); + case QType.TIME: + return this.validateTime(response); + case QType.DURATION: + return this.validateDuration(response); + case QType.DATE_WITHOUT_YEAR: + return this.validateDateWithoutYear(response); + case QType.DATE_TIME_WITHOUT_YEAR: + return this.validateDateTimeWithoutYear(response); + case QType.MULTIPLE_CHOICE: + return this.validateMultipleChoice(extractedValue, response); + case QType.MULTIPLE_CHOICE_WITH_OTHER: + return this.validateMultipleChoiceWithOther(extractedValue, response); + case QType.LINEAR_SCALE: + return this.validateLinearScale(extractedValue, response); + + case QType.MULTIPLE_CHOICE_GRID: + return this.validateMultipleChoiceGrid(extractedValue, response); + + case QType.CHECKBOX_GRID: + return this.validateCheckBoxGrid(extractedValue, response); + case QType.DROPDOWN: + return this.validateDropdown(extractedValue, response); + case QType.DATE_TIME_WITH_MERIDIEM: + return this.validateDateTimeWithMeridiem(response); + case QType.DATE_TIME_WITH_MERIDIEM_WITHOUT_YEAR: + return this.validateDateTimeWithMeridiemWithoutYear(response); + case QType.TEXT_URL: + return this.validateTextUrl(response); + case QType.TIME_WITH_MERIDIEM: + return this.validateTimeWithMeridiem(response); + } + } else { + return false; + } + } + + private validateText(response: string): boolean { + const text = response.trim(); + return text.length > 0 && !(text.includes('\n') || text.includes('\r')); + } + + private validateTextUrl(response: string): boolean { + return this.validateText(response); + } + + private validateParagraph(response: string): boolean { + return response?.trim().length > 0; + } + + private validateEmail(response: GenericLLMResponse): boolean { + // Checking valid email in accord to RFC 5322 + // https://stackoverflow.com/questions/201323/how-can-i-validate-an-email-address-using-a-regular-expression/201378#201378 + + const mailRegex = + /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/; + return Boolean(response && response?.answer?.match(mailRegex)); + } + + private validateDate(response: Date): boolean { + return response instanceof Date && !isNaN(response.valueOf()); + } + + private validateDateAndTime(response: Date): boolean { + return this.validateDate(response); + } + + private validateTime(response: Date): boolean { + return this.validateDate(response); + } + + private validateTimeWithMeridiem(response: Date): boolean { + return this.validateDate(response); + } + + private validateDuration(response: string): boolean { + // Accepts: hh-mm-ss + const durationValid = /^([01][0-9]|[2][0-3])(-[0-5][0-9]){2}$/; + return Boolean(response && response.match(durationValid)); + } + + private validateDateWithoutYear(response: Date): boolean { + return this.validateDate(response); + } + + private validateDateTimeWithoutYear(response: Date): boolean { + return this.validateDate(response); + } + + private validateDateTimeWithMeridiem(response: Date): boolean { + return this.validateDate(response); + } + + private validateDateTimeWithMeridiemWithoutYear(response: Date): boolean { + return this.validateDate(response); + } + + private validateMultiCorrect( + extractedValue: ExtractedValue, + response: MultiCorrectOrMultipleOption[] + ): boolean { + const responseOptions = response + .map((option) => option.optionText?.trim()) + .filter((text) => Boolean(text)); + + const actualOptions = extractedValue.options?.map((option) => + option.data.trim().toLowerCase() + ); + + return responseOptions.every( + (option) => option && actualOptions?.includes(option.toLowerCase()) + ); + } + + private validateMultiCorrectWithOther( + extractedValue: ExtractedValue, + response: MultiCorrectOrMultipleOption[] + ): boolean { + const isMulticorrect = this.validateMultiCorrect(extractedValue, response); + const isValidOther = response.some( + (option) => + option.isOther && + option.otherOptionValue && + this.validateText(option?.otherOptionValue) + ); + return isMulticorrect || !(isMulticorrect || isValidOther); + } + + private validateMultipleChoice( + extractedValue: ExtractedValue, + response: MultiCorrectOrMultipleOption + ): boolean { + if (typeof response.optionText !== 'string') { + return false; + } + + const trimmedResponse = response.optionText.trim().toLowerCase(); + + const actualOptions = + extractedValue.options?.map((option) => + option.data.trim().toLowerCase() + ) || []; + + const isValid = actualOptions.includes(trimmedResponse); + return isValid; + } + + private validateMultipleChoiceWithOther( + extractedValue: ExtractedValue, + response: MultiCorrectOrMultipleOption + ): boolean { + const isValidChoice = this.validateMultipleChoice(extractedValue, response); + + const isOtherOption = + (response.isOther ?? false) && + response.otherOptionValue && + this.validateText(response.otherOptionValue); + + return isValidChoice || Boolean(isOtherOption); + } + + private validateLinearScale( + extractedValue: ExtractedValue, + response: GenericLLMResponse + ): boolean { + const validOptions = (extractedValue.options ?? []).map( + (option) => option.data + ); + + return validOptions.includes(response?.answer ?? EMPTY_STRING); + } + + private validateMultipleChoiceGrid( + extractedValue: ExtractedValue, + response: RowColumn[] + ): boolean { + if (response.length !== (extractedValue.rowColumnOption || []).length) { + return false; + } + + return response.every((responseRow, rowIndex) => { + const actualRow = extractedValue.rowColumnOption?.[rowIndex]; + if (!actualRow) return false; + + const selectedColumn = responseRow.selectedColumn.trim().toLowerCase(); + + const actualColumns = actualRow.cols.map((col) => + col.data.trim().toLowerCase() + ); + + return actualColumns.includes(selectedColumn); + }); + } + + // TODO: Implement this : DONE + private validateCheckBoxGrid( + extractedValue: ExtractedValue, + response: RowColumnOption[] + ): boolean { + if (response.length !== (extractedValue.rowColumnOption || []).length) { + return false; + } + + return response.every((responseRow, rowIndex) => { + const actualRow = extractedValue.rowColumnOption?.[rowIndex]; + if (!actualRow) return false; + + const selectedColumns = responseRow.cols.map((col) => + col.data.trim().toLowerCase() + ); + + const actualColumns = actualRow.cols.map((col) => + col.data.trim().toLowerCase() + ); + + return selectedColumns.every((selectedCol) => + actualColumns.includes(selectedCol) + ); + }); + } + + private validateDropdown( + extractedValue: ExtractedValue, + response: GenericLLMResponse + ): boolean { + return true; + // return this.validateMultipleChoice(extractedValue, response); + } +} diff --git a/src/docFillerCore/index.ts b/src/docFillerCore/index.ts index e9efc08..8ce9ec8 100644 --- a/src/docFillerCore/index.ts +++ b/src/docFillerCore/index.ts @@ -1,6 +1,6 @@ import { DetectBoxType } from '@docFillerCore/detectors/detectBoxType'; import { FieldExtractorEngine } from '@docFillerCore/engines/fieldExtractorEngine'; -import { ParserEngine } from '@docFillerCore/engines/parserEngine'; +import { ValidatorEngine } from '@docFillerCore/engines/validatorEngine'; import { QuestionExtractorEngine } from '@docFillerCore/engines/questionExtractorEngine'; import { PromptEngine } from '@docFillerCore/engines/promptEngine'; import { FillerEngine } from '@docFillerCore/engines/fillerEngine'; @@ -15,7 +15,7 @@ async function runDocFillerEngine() { const fields = new FieldExtractorEngine(); const prompts = new PromptEngine(); const llm = LLMEngine.getInstance(CURRENT_LLM_MODEL); - const parser = new ParserEngine(); + const validator = new ValidatorEngine(); const filler = new FillerEngine(); for (const question of questions) { @@ -40,11 +40,17 @@ async function runDocFillerEngine() { console.log('LLM Response ↴'); console.log(response); - const parsed_response = parser.parse(fieldType, fieldValue, 'response'); + const parsed_response = validator.validate( + fieldType, + fieldValue, + response + ); console.log(`Parsed Response : ${parsed_response}`); - const fillerStatus = await filler.fill(fieldType, fieldValue, 'response'); - console.log(`Filler Status ${fillerStatus}`); + if (parsed_response) { + const fillerStatus = await filler.fill(fieldType, fieldValue, response); + console.log(`Filler Status ${fillerStatus}`); + } console.log(); } diff --git a/src/utils/constant.ts b/src/utils/constant.ts index ae27043..5df63ae 100644 --- a/src/utils/constant.ts +++ b/src/utils/constant.ts @@ -3,5 +3,5 @@ import LLMEngineType from '@utils/llmEngineTypes'; const SLEEP_DURATION: number = 2000; const EMPTY_STRING: string = ''; const DEFAULT_LLM_MODEL = LLMEngineType.ChatGPT; -const CURRENT_LLM_MODEL = LLMEngineType.Ollama; +const CURRENT_LLM_MODEL = LLMEngineType.Gemini; export { EMPTY_STRING, SLEEP_DURATION, DEFAULT_LLM_MODEL, CURRENT_LLM_MODEL }; diff --git a/types.d.ts b/types.d.ts index c153bdf..f53c542 100644 --- a/types.d.ts +++ b/types.d.ts @@ -95,3 +95,7 @@ interface RowColumn { row: string; selectedColumn: string; } + +interface GenericLLMResponse { + answer: string; +}