Skip to content

Commit b08a292

Browse files
committed
feat(McqGeneration): add MCQ generation
1 parent ce19dd6 commit b08a292

File tree

37 files changed

+1283
-572
lines changed

37 files changed

+1283
-572
lines changed

app/controllers/course/assessment/question/multiple_responses_controller.rb

Lines changed: 77 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ def new
1414

1515
def create
1616
if @multiple_response_question.save
17-
render json: { redirectUrl: course_assessment_path(current_course, @assessment) }
17+
render json: {
18+
redirectUrl: course_assessment_path(current_course, @assessment),
19+
redirectEditUrl: edit_course_assessment_question_multiple_response_path(
20+
current_course, @assessment, @multiple_response_question
21+
)
22+
}
1823
else
1924
render json: { errors: @multiple_response_question.errors }, status: :bad_request
2025
end
@@ -32,7 +37,12 @@ def update
3237
update_skill_ids_if_params_present(multiple_response_question_params[:question_assessment])
3338

3439
if update_multiple_response_question
35-
render json: { redirectUrl: course_assessment_path(current_course, @assessment) }
40+
render json: {
41+
redirectUrl: course_assessment_path(current_course, @assessment),
42+
redirectEditUrl: edit_course_assessment_question_multiple_response_path(
43+
current_course, @assessment, @multiple_response_question
44+
)
45+
}
3646
else
3747
render json: { errors: @multiple_response_question.errors }, status: :bad_request
3848
end
@@ -50,93 +60,25 @@ def destroy
5060
end
5161

5262
def generate
53-
# Parse the form data
54-
custom_prompt = params[:custom_prompt] || ''
55-
number_of_questions = (params[:number_of_questions] || 1).to_i
56-
57-
# Parse source_question_data from JSON string
58-
source_question_data = {}
59-
if params[:source_question_data].present?
60-
begin
61-
source_question_data = JSON.parse(params[:source_question_data])
62-
rescue JSON::ParserError => e
63-
Rails.logger.warn "Failed to parse source_question_data: #{e.message}"
64-
source_question_data = {}
65-
end
66-
end
67-
68-
# Validate parameters
69-
if custom_prompt.blank?
70-
render json: { success: false, message: 'Custom prompt is required' }, status: :bad_request
71-
return
72-
end
63+
generation_params = parse_generation_params
7364

74-
if number_of_questions < 1 || number_of_questions > 3
75-
render json: { success: false, message: 'Number of questions must be between 1 and 3' }, status: :bad_request
65+
unless validate_generation_params(generation_params)
66+
render json: { success: false, message: 'Invalid parameters' }, status: :bad_request
7667
return
7768
end
7869

79-
# Create generation service
80-
generation_service = Course::Assessment::Question::MrqGenerationService.new(
81-
@assessment,
82-
{
83-
custom_prompt: custom_prompt,
84-
number_of_questions: number_of_questions,
85-
source_question_data: source_question_data
86-
}
87-
)
88-
89-
# Generate questions
70+
generation_service = Course::Assessment::Question::MrqGenerationService.new(@assessment, generation_params)
9071
generated_questions = generation_service.generate_questions
91-
# Transform the response to match the expected frontend format
9272
questions = generated_questions['questions'] || []
9373

9474
if questions.empty?
95-
render json: { success: false, message: 'No questions were generated' }, status: :internal_server_error
75+
render json: { success: false, message: 'No questions were generated' }, status: :bad_request
9676
return
9777
end
98-
# Format response for frontend
99-
response_data = {
100-
success: true,
101-
data: {
102-
title: questions.first['title'],
103-
description: questions.first['description'],
104-
options: questions.first['options'].map.with_index do |option, index|
105-
{
106-
id: index + 1,
107-
option: option['option'],
108-
correct: option['correct'],
109-
weight: index + 1,
110-
explanation: option['explanation'] || '',
111-
ignoreRandomization: false,
112-
toBeDeleted: false
113-
}
114-
end,
115-
allQuestions: questions.map.with_index do |question, q_index|
116-
{
117-
title: question['title'],
118-
description: question['description'],
119-
options: question['options'].map.with_index do |option, index|
120-
{
121-
id: index + 1,
122-
option: option['option'],
123-
correct: option['correct'],
124-
weight: index + 1,
125-
explanation: option['explanation'] || '',
126-
ignoreRandomization: false,
127-
toBeDeleted: false
128-
}
129-
end
130-
}
131-
end,
132-
numberOfQuestions: questions.length
133-
}
134-
}
13578

136-
render json: response_data, status: :ok
79+
render json: format_generation_response(questions), status: :ok
13780
rescue StandardError => e
138-
Rails.logger.error "MRQ Generation Error: #{e.message}"
139-
Rails.logger.error e.backtrace.join("\n")
81+
Rails.logger.error "MCQ/MRQ Generation Error: #{e.message}"
14082
render json: { success: false, message: 'An error occurred while generating questions' },
14183
status: :internal_server_error
14284
end
@@ -177,4 +119,62 @@ def multiple_response_question_params
177119
def load_question_assessment
178120
@question_assessment = load_question_assessment_for(@multiple_response_question)
179121
end
122+
123+
def parse_generation_params
124+
{
125+
custom_prompt: params[:custom_prompt] || '',
126+
number_of_questions: (params[:number_of_questions] || 1).to_i,
127+
question_type: params[:question_type],
128+
source_question_data: parse_source_question_data
129+
}
130+
end
131+
132+
def parse_source_question_data
133+
return {} unless params[:source_question_data].present?
134+
135+
JSON.parse(params[:source_question_data])
136+
rescue JSON::ParserError
137+
{}
138+
end
139+
140+
def validate_generation_params(params)
141+
params[:custom_prompt].present? &&
142+
params[:number_of_questions] >= 1 && params[:number_of_questions] <= 10 &&
143+
%w[mrq mcq].include?(params[:question_type])
144+
end
145+
146+
def format_generation_response(questions)
147+
{
148+
success: true,
149+
data: {
150+
title: questions.first['title'],
151+
description: questions.first['description'],
152+
options: format_options(questions.first['options']),
153+
allQuestions: questions.map { |question| format_question(question) },
154+
numberOfQuestions: questions.length
155+
}
156+
}
157+
end
158+
159+
def format_options(options)
160+
options.map.with_index do |option, index|
161+
{
162+
id: index + 1,
163+
option: option['option'],
164+
correct: option['correct'],
165+
weight: index + 1,
166+
explanation: option['explanation'] || '',
167+
ignoreRandomization: false,
168+
toBeDeleted: false
169+
}
170+
end
171+
end
172+
173+
def format_question(question)
174+
{
175+
title: question['title'],
176+
description: question['description'],
177+
options: format_options(question['options'])
178+
}
179+
end
180180
end

app/services/course/assessment/question/mrq_generation_service.rb

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,97 @@
11
# frozen_string_literal: true
22
class Course::Assessment::Question::MrqGenerationService
33
@output_schema = JSON.parse(
4-
File.read('app/services/course/assessment/question/prompts/mrq_generation_output_format.json')
4+
File.read('app/services/course/assessment/question/prompts/mcq_mrq_generation_output_format.json')
55
)
66
@output_parser = Langchain::OutputParsers::StructuredOutputParser.from_json_schema(
77
@output_schema
88
)
9-
@system_prompt = Langchain::Prompt.load_from_path(
9+
@mrq_system_prompt = Langchain::Prompt.load_from_path(
1010
file_path: 'app/services/course/assessment/question/prompts/mrq_generation_system_prompt.json'
1111
)
12-
@user_prompt = Langchain::Prompt.load_from_path(
12+
@mrq_user_prompt = Langchain::Prompt.load_from_path(
1313
file_path: 'app/services/course/assessment/question/prompts/mrq_generation_user_prompt.json'
1414
)
15+
@mcq_system_prompt = Langchain::Prompt.load_from_path(
16+
file_path: 'app/services/course/assessment/question/prompts/mcq_generation_system_prompt.json'
17+
)
18+
@mcq_user_prompt = Langchain::Prompt.load_from_path(
19+
file_path: 'app/services/course/assessment/question/prompts/mcq_generation_user_prompt.json'
20+
)
1521
@llm = LANGCHAIN_OPENAI
1622

1723
class << self
18-
attr_reader :system_prompt, :user_prompt, :output_schema, :output_parser
24+
attr_reader :output_schema, :output_parser,
25+
:mrq_system_prompt, :mrq_user_prompt, :mcq_system_prompt, :mcq_user_prompt
1926
attr_accessor :llm
2027
end
2128

29+
# Initializes the MRQ generation service with assessment and parameters.
30+
# @param [Course::Assessment] assessment The assessment to generate questions for.
31+
# @param [Hash] params Parameters for question generation.
32+
# @option params [String] :custom_prompt Custom instructions for the LLM.
33+
# @option params [Integer] :number_of_questions Number of questions to generate.
34+
# @option params [Hash] :source_question_data Data from an existing question to base new questions on.
35+
# @option params [String] :question_type Type of question to generate ('mrq' or 'mcq').
2236
def initialize(assessment, params)
2337
@assessment = assessment
2438
@params = params
2539
@custom_prompt = params[:custom_prompt].to_s
26-
@number_of_questions = params[:number_of_questions].to_i || 1
40+
@number_of_questions = (params[:number_of_questions] || 1).to_i
2741
@source_question_data = params[:source_question_data]
42+
@question_type = params[:question_type] || 'mrq'
2843
end
2944

30-
# Calls the LLM service to generate MRQ questions.
45+
# Calls the LLM service to generate MRQ or MCQ questions.
3146
# @return [Hash] The LLM's generation response containing multiple questions.
3247
def generate_questions
33-
formatted_system_prompt = self.class.system_prompt.format
34-
formatted_user_prompt = self.class.user_prompt.format(
35-
custom_prompt: @custom_prompt,
36-
number_of_questions: @number_of_questions,
37-
source_question_title: @source_question_data&.dig(:title) || '',
38-
source_question_description: @source_question_data&.dig(:description) || '',
39-
source_question_options: format_source_options(@source_question_data&.dig(:options) || [])
40-
)
41-
42-
messages = [
43-
{ role: 'system', content: formatted_system_prompt },
44-
{ role: 'user', content: formatted_user_prompt }
45-
]
48+
messages = build_messages
4649

4750
response = self.class.llm.chat(
4851
messages: messages,
4952
response_format: {
5053
type: 'json_schema',
5154
json_schema: {
52-
name: 'mrq_generation_output',
55+
name: 'mcq_mrq_generation_output',
5356
strict: true,
5457
schema: self.class.output_schema
5558
}
5659
}
5760
).completion
58-
5961
parse_llm_response(response)
6062
end
6163

6264
private
6365

66+
# Builds the messages array from system and user prompt for the LLM chat
67+
# @return [Array<Hash>] Array of messages formatted for the LLM chat
68+
def build_messages
69+
system_prompt, user_prompt = select_prompts
70+
formatted_system_prompt = system_prompt.format
71+
formatted_user_prompt = user_prompt.format(
72+
custom_prompt: @custom_prompt,
73+
number_of_questions: @number_of_questions,
74+
source_question_title: @source_question_data&.dig(:title) || '',
75+
source_question_description: @source_question_data&.dig(:description) || '',
76+
source_question_options: format_source_options(@source_question_data&.dig(:options) || [])
77+
)
78+
79+
[
80+
{ role: 'system', content: formatted_system_prompt },
81+
{ role: 'user', content: formatted_user_prompt }
82+
]
83+
end
84+
85+
# Selects the appropriate prompts based on the question type
86+
# @return [Array] Array containing system and user prompts
87+
def select_prompts
88+
if @question_type == 'mcq'
89+
[self.class.mcq_system_prompt, self.class.mcq_user_prompt]
90+
else
91+
[self.class.mrq_system_prompt, self.class.mrq_user_prompt]
92+
end
93+
end
94+
6495
# Formats source question options for inclusion in the LLM prompt
6596
# @param [Array] options The source question options
6697
# @return [String] Formatted string representation of options
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"_type": "prompt",
3+
"input_variables": ["format_instructions"],
4+
"template": "You are an expert educational content creator specializing in multiple choice questions (MCQ).\n\nYour task is to generate high-quality multiple choice questions based on the provided instructions and context.\n\nKey requirements for MCQ generation:\n1. Create questions that have exactly ONE correct answer. Each question should have only one option that is correct.\n2. Ensure all options are plausible and well-written\n3. Try to create 4 options per question. If that is not feasible, ensure at least 2 options are provided.\n4. Questions should be clear, concise, and educational\n5. Options should be mutually exclusive and cover different aspects\n6. Avoid obvious incorrect answers\n7. Use an appropriate difficulty level for the target audience\n8. Make sure distractors (incorrect options) are plausible but clearly wrong\n\nWhen provided with a source question, you may use it as inspiration or reference, but create original questions.\n\n{format_instructions}"
5+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"_type": "prompt",
3+
"input_variables": [
4+
"custom_prompt",
5+
"number_of_questions",
6+
"source_question_title",
7+
"source_question_description",
8+
"source_question_options"
9+
],
10+
"template": "Please generate {number_of_questions} multiple choice question(s) based on the following instructions:\n\nCustom Instructions: {custom_prompt}\n\nSource Question Context (for reference only):\nTitle: {source_question_title}\nDescription: {source_question_description}\nOptions:\n{source_question_options}\n\nGenerate {number_of_questions} high-quality multiple choice question(s) that:\n- Have clear, educational content\n- Include at least 2 options per question\n- Have exactly ONE correct answer per question\n- Are appropriate for educational assessment\n- Follow the custom instructions provided\n\nEach question should be original and well-structured for educational use."
11+
}

app/services/course/assessment/question/prompts/mrq_generation_output_format.json renamed to app/services/course/assessment/question/prompts/mcq_mrq_generation_output_format.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"description": {
1515
"type": "string",
16-
"description": "The question description"
16+
"description": "The description of the question"
1717
},
1818
"options": {
1919
"type": "array",
@@ -30,13 +30,13 @@
3030
},
3131
"explanation": {
3232
"type": "string",
33-
"description": "Explanation for why this option is correct or incorrect"
33+
"description": "Highly detailed explanation for why this option is correct or incorrect"
3434
}
3535
},
3636
"required": ["option", "correct", "explanation"],
3737
"additionalProperties": false
3838
},
39-
"description": "Array of 4-6 options for the question"
39+
"description": "Array of at least 2 options for the question"
4040
}
4141
},
4242
"required": ["title", "description", "options"],
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"_type": "prompt",
33
"input_variables": ["format_instructions"],
4-
"template": "You are an expert educational content creator specializing in multiple response questions (MRQ).\n\nYour task is to generate high-quality multiple response questions based on the provided instructions and context.\n\nKey requirements for MRQ generation:\n1. Create questions that may have one or more correct answers. It’s acceptable for some questions to have only one correct answer, or for options like \"None of the above\" to be correct.\n2. Ensure all options are plausible and well-written\n3. Include 2-6 options per question\n4. Questions should be clear, concise, and educational\n5. Options should be mutually exclusive when possible\n6. Avoid obvious incorrect answers\n7. Use an appropriate difficulty level for the target audience\n\nWhen provided with a source question, you may use it as inspiration or reference, but create original questions.\n\n{format_instructions}"
4+
"template": "You are an expert educational content creator specializing in multiple response questions (MRQ).\n\nYour task is to generate high-quality multiple response questions based on the provided instructions and context.\n\nKey requirements for MRQ generation:\n1. Create questions that may have one or more correct answers. It is acceptable for some questions to have only one correct answer, or for options like \"None of the above\" to be correct.\n2. Ensure all options are plausible and well-written\n3. Try to create 4 options per question. If that is not feasible, ensure at least 2 options are provided.\n4. Questions should be clear, concise, and educational\n5. Options should be mutually exclusive when possible\n6. Avoid obvious incorrect answers\n7. Use an appropriate difficulty level for the target audience\n\nWhen provided with a source question, you may use it as inspiration or reference, but create original questions.\n\n{format_instructions}"
55
}

app/services/course/assessment/question/prompts/mrq_generation_user_prompt.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"source_question_description",
88
"source_question_options"
99
],
10-
"template": "Please generate {number_of_questions} multiple response question(s) based on the following instructions:\n\nCustom Instructions: {custom_prompt}\n\nSource Question Context (for reference only):\nTitle: {source_question_title}\nDescription: {source_question_description}\nOptions:\n{source_question_options}\n\nGenerate {number_of_questions} high-quality multiple response question(s) that:\n- Have clear, educational content\n- Include 4-6 options per question\n- Have multiple correct answers (at least 2)\n- Are appropriate for educational assessment\n- Follow the custom instructions provided\n\nEach question should be original and well-structured for educational use."
10+
"template": "Please generate {number_of_questions} multiple response question(s) based on the following instructions:\n\nCustom Instructions: {custom_prompt}\n\nSource Question Context (for reference only):\nTitle: {source_question_title}\nDescription: {source_question_description}\nOptions:\n{source_question_options}\n\nGenerate {number_of_questions} high-quality multiple response question(s) that:\n- Have clear, educational content\n- Include at least 2 options per question\n- Are appropriate for educational assessment\n- Follow the custom instructions provided\n\nEach question should be original and well-structured for educational use."
1111
}

app/views/course/assessment/assessments/show.json.jbuilder

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,12 @@ if can_observe
174174
end
175175

176176
json.generateQuestionUrls [
177-
# {
178-
# type: 'MultipleChoice',
179-
# url: generate_course_assessment_question_multiple_responses_path(current_course, assessment, {
180-
# multiple_choice: true
181-
# })
182-
# },
177+
{
178+
type: 'MultipleChoice',
179+
url: generate_course_assessment_question_multiple_responses_path(current_course, assessment, {
180+
multiple_choice: true
181+
})
182+
},
183183
{
184184
type: 'MultipleResponse',
185185
url: generate_course_assessment_question_multiple_responses_path(current_course, assessment)

0 commit comments

Comments
 (0)