diff --git a/packages/api/src/functions/gptHandlers.ts b/packages/api/src/functions/gptHandlers.ts index aef069b..26a9f84 100644 --- a/packages/api/src/functions/gptHandlers.ts +++ b/packages/api/src/functions/gptHandlers.ts @@ -40,7 +40,7 @@ Points: [Select from the following options based on how difficult you think the (points) => points.title, ).join(", ")}]`; -const generateChoicesPrompt = (numChoices: number) => { +export const generateChoicesPrompt = (numChoices: number) => { let choicesPrompt = ""; for (let i = 1; i <= numChoices; i++) { choicesPrompt += `Option ${i}: [Choice ${i}]\n`; diff --git a/packages/api/src/functions/randomQuestionsHandlers.ts b/packages/api/src/functions/randomQuestionsHandlers.ts new file mode 100644 index 0000000..7e62dcc --- /dev/null +++ b/packages/api/src/functions/randomQuestionsHandlers.ts @@ -0,0 +1,120 @@ +import { generateChoicesPrompt, timeAndPointsPrompt } from "./gptHandlers"; +import { + parseMultipleChoiceResponse, + parseMultiselectResponse, + parseIdentificationResponse, + parseTrueOrFalseResponse, + MultipleChoicePrompt, + MultiselectPrompt, + IdentificationPrompt, + TrueOrFalsePrompt, +} from "./gptHandlers"; + +type ParsedQuestion = + | MultipleChoicePrompt + | MultiselectPrompt + | IdentificationPrompt + | TrueOrFalsePrompt; + +export type QuestionType = + | "multipleChoice" + | "multiselect" + | "identification" + | "trueOrFalse"; + +const questionTypes: QuestionType[] = [ + "multipleChoice", + "multiselect", + "identification", + "trueOrFalse", +]; +const typeCounts: Record = { + multipleChoice: 0, + multiselect: 0, + identification: 0, + trueOrFalse: 0, +}; + +export const questionFormatGenerators: { + [key in QuestionType]: ( + numChoices?: number, + maxCharsForQuestion?: number, + maxCharsForChoice?: number, + ) => string; +} = { + multipleChoice: ( + numChoices = 4, + maxCharsForQuestion = 100, + ) => `separator\nQuestion: [Your question here, max ${maxCharsForQuestion} characters] + ${generateChoicesPrompt( + numChoices, + )}\nCorrect Answer: Option [Correct option number] ${timeAndPointsPrompt}`, + + multiselect: ( + numChoices = 4, + maxCharsForQuestion = 100, + ) => `separator\nQuestion: [Your question here, max ${maxCharsForQuestion} characters] + ${generateChoicesPrompt( + numChoices, + )}\nAll Correct Answers: Options [Correct option numbers separated by commas, e.g., 1,3] ${timeAndPointsPrompt}`, + + identification: (maxCharsForQuestion = 100, maxCharsForChoice = 68) => + `separator\nQuestion: [Your question here, max ${maxCharsForQuestion} characters]\nAnswer: [Your answer here, max ${maxCharsForChoice} characters] ${timeAndPointsPrompt}`, + + trueOrFalse: (maxCharsForQuestion = 100) => + `separator\nQuestion: [Your question here, max ${maxCharsForQuestion} characters]\nAnswer: [True/False] ${timeAndPointsPrompt}`, +}; + +export const generateCombinedQuestionPrompts = ( + numQuestions: number, + message: string, +): string => { + for (let i = 0; i < numQuestions; i++) { + const randomType = + questionTypes[Math.floor(Math.random() * questionTypes.length)]; + typeCounts[randomType as QuestionType]++; + } + + let combinedPrompts = `Based on: "${message}", create the following questions:\n`; + + questionTypes.forEach((type) => { + if (typeCounts[type] > 0) { + combinedPrompts += `\n${ + typeCounts[type] + } questions of type '${type}' using the format:\n${questionFormatGenerators[ + type + ]()}\n`; + } + }); + + return combinedPrompts.trim(); +}; + +export const processGeneratedQuestions = ( + generatedMessage: string, +): ParsedQuestion[] => { + const questionBlocks = generatedMessage + .split("separator") + .filter((block) => block.trim() !== ""); + const parsedQuestions: ParsedQuestion[] = []; + + questionBlocks.forEach((questionBlock) => { + if (questionBlock.includes("Correct Answer:")) { + parsedQuestions.push(parseMultipleChoiceResponse(questionBlock)); + } else if (questionBlock.includes("All Correct Answers:")) { + parsedQuestions.push(parseMultiselectResponse(questionBlock)); + } else if ( + questionBlock.includes("Answer:") && + !(questionBlock.includes("True") || questionBlock.includes("False")) + ) { + parsedQuestions.push(parseIdentificationResponse(questionBlock)); + } else if ( + questionBlock.includes("True") || + questionBlock.includes("False") + ) { + parsedQuestions.push(parseTrueOrFalseResponse(questionBlock)); + } + }); + + return parsedQuestions; +}; diff --git a/packages/api/src/router/gptApi.ts b/packages/api/src/router/gptApi.ts index a5abcad..b01c52e 100644 --- a/packages/api/src/router/gptApi.ts +++ b/packages/api/src/router/gptApi.ts @@ -17,9 +17,14 @@ import { } from "../functions/testCreationHandlers"; import { multipleQuestionsPromptInput, + multipleRandomQuestionsPromptInput, singleQuestionPromptInput, } from "../../../schema/src/gpt"; import { fetchGPT } from "../services/gptApiHandlers"; +import { + generateCombinedQuestionPrompts, + processGeneratedQuestions, +} from "../functions/randomQuestionsHandlers"; export const gptApiRouter = router({ generateQuestion: protectedProcedure @@ -113,4 +118,21 @@ export const gptApiRouter = router({ return answer; }), + + generateMultipleRandomQuestions: protectedProcedure + .input(multipleRandomQuestionsPromptInput) + .mutation(async ({ input }) => { + const { message, numOfQuestions } = input; + + const promptText = generateCombinedQuestionPrompts( + numOfQuestions, + message, + ); + + const generatedMessage = await fetchGPT(promptText); + + const processedQuestions = processGeneratedQuestions(generatedMessage); + + return processedQuestions; + }), }); diff --git a/packages/schema/src/gpt.ts b/packages/schema/src/gpt.ts index 841d213..0d34fb2 100644 --- a/packages/schema/src/gpt.ts +++ b/packages/schema/src/gpt.ts @@ -23,3 +23,8 @@ export const multipleQuestionsPromptInput = z.object({ maxCharsPerQuestion: z.number().optional(), maxCharsPerChoice: z.number().optional(), }); + +export const multipleRandomQuestionsPromptInput = z.object({ + message: z.string(), + numOfQuestions: z.number(), +});