diff --git a/.dockerignore b/.dockerignore index 3e4bdd9..65460ac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ README.md *.pyd __pycache__ .pytest_cache +/scripts diff --git a/.gitignore b/.gitignore index e7f296a..9b77073 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ .env .DS_Store /firebase-configs/test_firebase.json +/scripts diff --git a/app.py b/app.py index daf946a..64fe488 100644 --- a/app.py +++ b/app.py @@ -70,25 +70,7 @@ def get_listening_section_1_question(): req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - if (len(req_exercises) == 0): - req_exercises = random.sample(LISTENING_1_EXERCISE_TYPES, 1) - - number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_1_EXERCISES, len(req_exercises)) - - processed_conversation = generate_listening_1_conversation(topic) - - app.logger.info("Generated conversation: " + str(processed_conversation)) - - start_id = 1 - exercises = generate_listening_conversation_exercises(parse_conversation(processed_conversation), - req_exercises, - number_of_exercises_q, - start_id, difficulty) - return { - "exercises": exercises, - "text": processed_conversation, - "difficulty": difficulty - } + return gen_listening_section_1(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -103,22 +85,7 @@ def get_listening_section_2_question(): req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - if (len(req_exercises) == 0): - req_exercises = random.sample(LISTENING_2_EXERCISE_TYPES, 2) - - number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_2_EXERCISES, len(req_exercises)) - - monologue = generate_listening_2_monologue(topic) - - app.logger.info("Generated monologue: " + str(monologue)) - start_id = 11 - exercises = generate_listening_monologue_exercises(str(monologue), req_exercises, number_of_exercises_q, - start_id, difficulty) - return { - "exercises": exercises, - "text": monologue, - "difficulty": difficulty - } + return gen_listening_section_2(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -133,24 +100,7 @@ def get_listening_section_3_question(): req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - if (len(req_exercises) == 0): - req_exercises = random.sample(LISTENING_3_EXERCISE_TYPES, 1) - - number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_3_EXERCISES, len(req_exercises)) - - processed_conversation = generate_listening_3_conversation(topic) - - app.logger.info("Generated conversation: " + str(processed_conversation)) - - start_id = 21 - exercises = generate_listening_conversation_exercises(parse_conversation(processed_conversation), req_exercises, - number_of_exercises_q, - start_id, difficulty) - return { - "exercises": exercises, - "text": processed_conversation, - "difficulty": difficulty - } + return gen_listening_section_3(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -165,22 +115,7 @@ def get_listening_section_4_question(): req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - if (len(req_exercises) == 0): - req_exercises = random.sample(LISTENING_EXERCISE_TYPES, 2) - - number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_4_EXERCISES, len(req_exercises)) - - monologue = generate_listening_4_monologue(topic) - - app.logger.info("Generated monologue: " + str(monologue)) - start_id = 31 - exercises = generate_listening_monologue_exercises(str(monologue), req_exercises, number_of_exercises_q, - start_id, difficulty) - return { - "exercises": exercises, - "text": monologue, - "difficulty": difficulty - } + return gen_listening_section_4(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -347,37 +282,7 @@ def get_writing_task_1_general_question(): difficulty = request.args.get("difficulty", default=random.choice(difficulties)) topic = request.args.get("topic", default=random.choice(mti_topics)) try: - messages = [ - { - "role": "system", - "content": ('You are a helpful assistant designed to output JSON on this format: ' - '{"prompt": "prompt content"}') - }, - { - "role": "user", - "content": ('Craft a prompt for an IELTS Writing Task 1 General Training exercise that instructs the ' - 'student to compose a letter. The prompt should present a specific scenario or situation, ' - 'based on the topic of "' + topic + '", requiring the student to provide information, ' - 'advice, or instructions within the letter. ' - 'Make sure that the generated prompt is ' - 'of ' + difficulty + 'difficulty and does not contain ' - 'forbidden subjects in muslim ' - 'countries.') - }, - { - "role": "user", - "content": 'The prompt should end with "In the letter you should" followed by 3 bullet points of what ' - 'the answer should include.' - } - ] - token_count = count_total_tokens(messages) - response = make_openai_call(GPT_3_5_TURBO, messages, token_count, "prompt", - GEN_QUESTION_TEMPERATURE) - return { - "question": add_newline_before_hyphen(response["prompt"].strip()), - "difficulty": difficulty, - "topic": topic - } + return gen_writing_task_1(topic, difficulty) except Exception as e: return str(e) @@ -512,32 +417,7 @@ def get_writing_task_2_general_question(): difficulty = request.args.get("difficulty", default=random.choice(difficulties)) topic = request.args.get("topic", default=random.choice(mti_topics)) try: - messages = [ - { - "role": "system", - "content": ('You are a helpful assistant designed to output JSON on this format: ' - '{"prompt": "prompt content"}') - }, - { - "role": "user", - "content": ( - 'Craft a comprehensive question of ' + difficulty + 'difficulty like the ones for IELTS Writing Task 2 General Training that directs the candidate ' - 'to delve into an in-depth analysis of contrasting perspectives on the topic of "' + topic + '". ' - 'The candidate should be asked to discuss the strengths and weaknesses of both viewpoints.') - }, - { - "role": "user", - "content": 'The question should lead to an answer with either "theories", "complicated information" or ' - 'be "very descriptive" on the topic.' - } - ] - token_count = count_total_tokens(messages) - response = make_openai_call(GPT_4_O, messages, token_count, "prompt", GEN_QUESTION_TEMPERATURE) - return { - "question": response["prompt"].strip(), - "difficulty": difficulty, - "topic": topic - } + return gen_writing_task_2(topic, difficulty) except Exception as e: return str(e) @@ -719,56 +599,8 @@ def get_speaking_task_1_question(): first_topic = request.args.get("first_topic", default=random.choice(mti_topics)) second_topic = request.args.get("second_topic", default=random.choice(mti_topics)) - json_format = { - "first_topic": "topic 1", - "second_topic": "topic 2", - "questions": [ - "Introductory question about the first topic, starting the topic with 'Let's talk about x' and then the " - "question.", - "Follow up question about the first topic", - "Follow up question about the first topic", - "Question about second topic", - "Follow up question about the second topic", - ] - } - try: - messages = [ - { - "role": "system", - "content": ( - 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)) - }, - { - "role": "user", - "content": ( - 'Craft 5 simple and single questions of easy difficulty for IELTS Speaking Part 1 ' - 'that encourages candidates to delve deeply into ' - 'personal experiences, preferences, or insights on the topic ' - 'of "' + first_topic + '" and the topic of "' + second_topic + '". ' - 'Make sure that the generated ' - 'question' - 'does not contain forbidden ' - 'subjects in' - 'muslim countries.') - }, - { - "role": "user", - "content": 'The questions should lead to the usage of 4 verb tenses (present perfect, present, ' - 'past and future).' - }, - { - "role": "user", - "content": 'They must be 1 single question each and not be double-barreled questions.' - - } - ] - token_count = count_total_tokens(messages) - response = make_openai_call(GPT_4_O, messages, token_count, ["first_topic"], - GEN_QUESTION_TEMPERATURE) - response["type"] = 1 - response["difficulty"] = difficulty - return response + return gen_speaking_part_1(first_topic, second_topic, difficulty) except Exception as e: return str(e) @@ -918,50 +750,8 @@ def get_speaking_task_2_question(): difficulty = request.args.get("difficulty", default=random.choice(difficulties)) topic = request.args.get("topic", default=random.choice(mti_topics)) - json_format = { - "topic": "topic", - "question": "question", - "prompts": [ - "prompt_1", - "prompt_2", - "prompt_3" - ], - "suffix": "And explain why..." - } - try: - messages = [ - { - "role": "system", - "content": 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format) - }, - { - "role": "user", - "content": ( - 'Create a question of medium difficulty for IELTS Speaking Part 2 ' - 'that encourages candidates to narrate a ' - 'personal experience or story related to the topic ' - 'of "' + topic + '". Include 3 prompts that ' - 'guide the candidate to describe ' - 'specific aspects of the experience, ' - 'such as details about the situation, ' - 'their actions, and the reasons it left a ' - 'lasting impression. Make sure that the ' - 'generated question does not contain ' - 'forbidden subjects in muslim countries.') - }, - { - "role": "user", - "content": 'The prompts must not be questions. Also include a suffix like the ones in the IELTS exams ' - 'that start with "And explain why".' - } - ] - token_count = count_total_tokens(messages) - response = make_openai_call(GPT_4_O, messages, token_count, GEN_FIELDS, GEN_QUESTION_TEMPERATURE) - response["type"] = 2 - response["difficulty"] = difficulty - response["topic"] = topic - return response + return gen_speaking_part_2(topic, difficulty) except Exception as e: return str(e) @@ -972,47 +762,8 @@ def get_speaking_task_3_question(): difficulty = request.args.get("difficulty", default=random.choice(difficulties)) topic = request.args.get("topic", default=random.choice(mti_topics)) - json_format = { - "topic": "topic", - "questions": [ - "Introductory question about the topic.", - "Follow up question about the topic", - "Follow up question about the topic", - "Follow up question about the topic", - "Follow up question about the topic" - ] - } try: - messages = [ - { - "role": "system", - "content": ( - 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)) - }, - { - "role": "user", - "content": ( - 'Formulate a set of 5 single questions of hard difficulty for IELTS Speaking Part 3 that encourage candidates to engage in a ' - 'meaningful discussion on the topic of "' + topic + '". Provide inquiries, ensuring ' - 'they explore various aspects, perspectives, and implications related to the topic.' - 'Make sure that the generated question does not contain forbidden subjects in muslim countries.') - - }, - { - "role": "user", - "content": 'They must be 1 single question each and not be double-barreled questions.' - - } - ] - token_count = count_total_tokens(messages) - response = make_openai_call(GPT_4_O, messages, token_count, GEN_FIELDS, GEN_QUESTION_TEMPERATURE) - # Remove the numbers from the questions only if the string starts with a number - response["questions"] = [re.sub(r"^\d+\.\s*", "", question) if re.match(r"^\d+\.", question) else question for - question in response["questions"]] - response["type"] = 3 - response["difficulty"] = difficulty - response["topic"] = topic - return response + return gen_speaking_part_3(topic, difficulty) except Exception as e: return str(e) @@ -1407,7 +1158,7 @@ def get_reading_passage_1_question(): topic = request.args.get('topic', default=random.choice(topics)) req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - return gen_reading_passage_1(topic, req_exercises, difficulty) + return gen_reading_passage_1(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -1420,7 +1171,7 @@ def get_reading_passage_2_question(): topic = request.args.get('topic', default=random.choice(topics)) req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - return gen_reading_passage_2(topic, req_exercises, difficulty) + return gen_reading_passage_2(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -1433,7 +1184,7 @@ def get_reading_passage_3_question(): topic = request.args.get('topic', default=random.choice(topics)) req_exercises = request.args.getlist('exercises') difficulty = request.args.get("difficulty", default=random.choice(difficulties)) - return gen_reading_passage_3(topic, req_exercises, difficulty) + return gen_reading_passage_3(topic, difficulty, req_exercises) except Exception as e: return str(e) @@ -1565,6 +1316,18 @@ class CustomLevelExerciseTypes(Enum): MULTIPLE_CHOICE_UNDERLINED = "multiple_choice_underlined" BLANK_SPACE_TEXT = "blank_space_text" READING_PASSAGE_UTAS = "reading_passage_utas" + WRITING_LETTER = "writing_letter" + WRITING_2 = "writing_2" + SPEAKING_1 = "speaking_1" + SPEAKING_2 = "speaking_2" + SPEAKING_3 = "speaking_3" + READING_1 = "reading_1" + READING_2 = "reading_2" + READING_3 = "reading_3" + LISTENING_1 = "listening_1" + LISTENING_2 = "listening_2" + LISTENING_3 = "listening_3" + LISTENING_4 = "listening_4" @app.route('/custom_level', methods=['GET']) @@ -1579,11 +1342,24 @@ def get_custom_level(): } for i in range(1, nr_exercises + 1, 1): exercise_type = request.args.get('exercise_' + str(i) + '_type') + exercise_difficulty = request.args.get('exercise_' + str(i) + '_difficulty', + random.choice(['easy', 'medium', 'hard'])) exercise_qty = int(request.args.get('exercise_' + str(i) + '_qty', -1)) exercise_topic = request.args.get('exercise_' + str(i) + '_topic', random.choice(topics)) + exercise_topic_2 = request.args.get('exercise_' + str(i) + '_topic_2', random.choice(topics)) exercise_text_size = int(request.args.get('exercise_' + str(i) + '_text_size', 700)) exercise_sa_qty = int(request.args.get('exercise_' + str(i) + '_sa_qty', -1)) exercise_mc_qty = int(request.args.get('exercise_' + str(i) + '_mc_qty', -1)) + exercise_mc3_qty = int(request.args.get('exercise_' + str(i) + '_mc3_qty', -1)) + exercise_fillblanks_qty = int(request.args.get('exercise_' + str(i) + '_fillblanks_qty', -1)) + exercise_writeblanks_qty = int(request.args.get('exercise_' + str(i) + '_writeblanks_qty', -1)) + exercise_writeblanksquestions_qty = int( + request.args.get('exercise_' + str(i) + '_writeblanksquestions_qty', -1)) + exercise_writeblanksfill_qty = int(request.args.get('exercise_' + str(i) + '_writeblanksfill_qty', -1)) + exercise_writeblanksform_qty = int(request.args.get('exercise_' + str(i) + '_writeblanksform_qty', -1)) + exercise_truefalse_qty = int(request.args.get('exercise_' + str(i) + '_truefalse_qty', -1)) + exercise_paragraphmatch_qty = int(request.args.get('exercise_' + str(i) + '_paragraphmatch_qty', -1)) + exercise_ideamatch_qty = int(request.args.get('exercise_' + str(i) + '_ideamatch_qty', -1)) if exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_4.value: response["exercises"]["exercise_" + str(i)] = {} @@ -1597,7 +1373,7 @@ def get_custom_level(): response["exercises"]["exercise_" + str(i)]["questions"].extend( generate_level_mc(exercise_id, qty, - response["exercises"]["exercise_" + str(i)]["questions"])["questions"]) + response["exercises"]["exercise_" + str(i)]["questions"])["questions"]) exercise_id = exercise_id + qty exercise_qty = exercise_qty - qty @@ -1613,7 +1389,8 @@ def get_custom_level(): response["exercises"]["exercise_" + str(i)]["questions"].extend( gen_multiple_choice_blank_space_utas(qty, exercise_id, - response["exercises"]["exercise_" + str(i)]["questions"])["questions"]) + response["exercises"]["exercise_" + str(i)]["questions"])[ + "questions"]) exercise_id = exercise_id + qty exercise_qty = exercise_qty - qty @@ -1629,7 +1406,8 @@ def get_custom_level(): response["exercises"]["exercise_" + str(i)]["questions"].extend( gen_multiple_choice_underlined_utas(qty, exercise_id, - response["exercises"]["exercise_" + str(i)]["questions"])["questions"]) + response["exercises"]["exercise_" + str(i)]["questions"])[ + "questions"]) exercise_id = exercise_id + qty exercise_qty = exercise_qty - qty @@ -1643,9 +1421,205 @@ def get_custom_level(): exercise_mc_qty, exercise_topic) response["exercises"]["exercise_" + str(i)]["type"] = "readingExercises" exercise_id = exercise_id + exercise_qty + elif exercise_type == CustomLevelExerciseTypes.WRITING_LETTER.value: + response["exercises"]["exercise_" + str(i)] = gen_writing_task_1(exercise_topic, exercise_difficulty) + response["exercises"]["exercise_" + str(i)]["type"] = "writing" + exercise_id = exercise_id + 1 + elif exercise_type == CustomLevelExerciseTypes.WRITING_2.value: + response["exercises"]["exercise_" + str(i)] = gen_writing_task_2(exercise_topic, exercise_difficulty) + response["exercises"]["exercise_" + str(i)]["type"] = "writing" + exercise_id = exercise_id + 1 + elif exercise_type == CustomLevelExerciseTypes.SPEAKING_1.value: + response["exercises"]["exercise_" + str(i)] = ( + gen_speaking_part_1(exercise_topic, exercise_topic_2, exercise_difficulty)) + response["exercises"]["exercise_" + str(i)]["type"] = "interactiveSpeaking" + exercise_id = exercise_id + 1 + elif exercise_type == CustomLevelExerciseTypes.SPEAKING_2.value: + response["exercises"]["exercise_" + str(i)] = gen_speaking_part_2(exercise_topic, exercise_difficulty) + response["exercises"]["exercise_" + str(i)]["type"] = "speaking" + exercise_id = exercise_id + 1 + elif exercise_type == CustomLevelExerciseTypes.SPEAKING_3.value: + response["exercises"]["exercise_" + str(i)] = gen_speaking_part_3(exercise_topic, exercise_difficulty) + response["exercises"]["exercise_" + str(i)]["type"] = "interactiveSpeaking" + exercise_id = exercise_id + 1 + elif exercise_type == CustomLevelExerciseTypes.READING_1.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_fillblanks_qty != -1: + exercises.append('fillBlanks') + exercise_qty_q.put(exercise_fillblanks_qty) + total_qty = total_qty + exercise_fillblanks_qty + if exercise_writeblanks_qty != -1: + exercises.append('writeBlanks') + exercise_qty_q.put(exercise_writeblanks_qty) + total_qty = total_qty + exercise_writeblanks_qty + if exercise_truefalse_qty != -1: + exercises.append('trueFalse') + exercise_qty_q.put(exercise_truefalse_qty) + total_qty = total_qty + exercise_truefalse_qty + if exercise_paragraphmatch_qty != -1: + exercises.append('paragraphMatch') + exercise_qty_q.put(exercise_paragraphmatch_qty) + total_qty = total_qty + exercise_paragraphmatch_qty + + response["exercises"]["exercise_" + str(i)] = gen_reading_passage_1(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "reading" + + exercise_id = exercise_id + total_qty + elif exercise_type == CustomLevelExerciseTypes.READING_2.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_fillblanks_qty != -1: + exercises.append('fillBlanks') + exercise_qty_q.put(exercise_fillblanks_qty) + total_qty = total_qty + exercise_fillblanks_qty + if exercise_writeblanks_qty != -1: + exercises.append('writeBlanks') + exercise_qty_q.put(exercise_writeblanks_qty) + total_qty = total_qty + exercise_writeblanks_qty + if exercise_truefalse_qty != -1: + exercises.append('trueFalse') + exercise_qty_q.put(exercise_truefalse_qty) + total_qty = total_qty + exercise_truefalse_qty + if exercise_paragraphmatch_qty != -1: + exercises.append('paragraphMatch') + exercise_qty_q.put(exercise_paragraphmatch_qty) + total_qty = total_qty + exercise_paragraphmatch_qty + + response["exercises"]["exercise_" + str(i)] = gen_reading_passage_2(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "reading" + + exercise_id = exercise_id + total_qty + elif exercise_type == CustomLevelExerciseTypes.READING_3.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_fillblanks_qty != -1: + exercises.append('fillBlanks') + exercise_qty_q.put(exercise_fillblanks_qty) + total_qty = total_qty + exercise_fillblanks_qty + if exercise_writeblanks_qty != -1: + exercises.append('writeBlanks') + exercise_qty_q.put(exercise_writeblanks_qty) + total_qty = total_qty + exercise_writeblanks_qty + if exercise_truefalse_qty != -1: + exercises.append('trueFalse') + exercise_qty_q.put(exercise_truefalse_qty) + total_qty = total_qty + exercise_truefalse_qty + if exercise_paragraphmatch_qty != -1: + exercises.append('paragraphMatch') + exercise_qty_q.put(exercise_paragraphmatch_qty) + total_qty = total_qty + exercise_paragraphmatch_qty + if exercise_ideamatch_qty != -1: + exercises.append('ideaMatch') + exercise_qty_q.put(exercise_ideamatch_qty) + total_qty = total_qty + exercise_ideamatch_qty + + response["exercises"]["exercise_" + str(i)] = gen_reading_passage_3(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "reading" + + exercise_id = exercise_id + total_qty + elif exercise_type == CustomLevelExerciseTypes.LISTENING_1.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_mc_qty != -1: + exercises.append('multipleChoice') + exercise_qty_q.put(exercise_mc_qty) + total_qty = total_qty + exercise_mc_qty + if exercise_writeblanksquestions_qty != -1: + exercises.append('writeBlanksQuestions') + exercise_qty_q.put(exercise_writeblanksquestions_qty) + total_qty = total_qty + exercise_writeblanksquestions_qty + if exercise_writeblanksfill_qty != -1: + exercises.append('writeBlanksFill') + exercise_qty_q.put(exercise_writeblanksfill_qty) + total_qty = total_qty + exercise_writeblanksfill_qty + if exercise_writeblanksform_qty != -1: + exercises.append('writeBlanksForm') + exercise_qty_q.put(exercise_writeblanksform_qty) + total_qty = total_qty + exercise_writeblanksform_qty + + response["exercises"]["exercise_" + str(i)] = gen_listening_section_1(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, + exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "listening" + + exercise_id = exercise_id + total_qty + elif exercise_type == CustomLevelExerciseTypes.LISTENING_2.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_mc_qty != -1: + exercises.append('multipleChoice') + exercise_qty_q.put(exercise_mc_qty) + total_qty = total_qty + exercise_mc_qty + if exercise_writeblanksquestions_qty != -1: + exercises.append('writeBlanksQuestions') + exercise_qty_q.put(exercise_writeblanksquestions_qty) + total_qty = total_qty + exercise_writeblanksquestions_qty + + response["exercises"]["exercise_" + str(i)] = gen_listening_section_2(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, + exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "listening" + + exercise_id = exercise_id + total_qty + elif exercise_type == CustomLevelExerciseTypes.LISTENING_3.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_mc3_qty != -1: + exercises.append('multipleChoice3Options') + exercise_qty_q.put(exercise_mc3_qty) + total_qty = total_qty + exercise_mc3_qty + if exercise_writeblanksquestions_qty != -1: + exercises.append('writeBlanksQuestions') + exercise_qty_q.put(exercise_writeblanksquestions_qty) + total_qty = total_qty + exercise_writeblanksquestions_qty + + response["exercises"]["exercise_" + str(i)] = gen_listening_section_3(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, + exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "listening" + + exercise_id = exercise_id + total_qty + elif exercise_type == CustomLevelExerciseTypes.LISTENING_4.value: + exercises = [] + exercise_qty_q = queue.Queue() + total_qty = 0 + if exercise_mc_qty != -1: + exercises.append('multipleChoice') + exercise_qty_q.put(exercise_mc_qty) + total_qty = total_qty + exercise_mc_qty + if exercise_writeblanksquestions_qty != -1: + exercises.append('writeBlanksQuestions') + exercise_qty_q.put(exercise_writeblanksquestions_qty) + total_qty = total_qty + exercise_writeblanksquestions_qty + if exercise_writeblanksfill_qty != -1: + exercises.append('writeBlanksFill') + exercise_qty_q.put(exercise_writeblanksfill_qty) + total_qty = total_qty + exercise_writeblanksfill_qty + if exercise_writeblanksform_qty != -1: + exercises.append('writeBlanksForm') + exercise_qty_q.put(exercise_writeblanksform_qty) + total_qty = total_qty + exercise_writeblanksform_qty + + response["exercises"]["exercise_" + str(i)] = gen_listening_section_4(exercise_topic, exercise_difficulty, + exercises, exercise_qty_q, + exercise_id) + response["exercises"]["exercise_" + str(i)]["type"] = "listening" + + exercise_id = exercise_id + total_qty return response + @app.route('/grade_short_answers', methods=['POST']) @jwt_required() def grade_short_answers(): @@ -1670,7 +1644,8 @@ def grade_short_answers(): }, { "role": "user", - "content": 'Grade these answers according to the text content and write a correct answer if they are wrong. Text, questions and answers:\n ' + str(data) + "content": 'Grade these answers according to the text content and write a correct answer if they are ' + 'wrong. Text, questions and answers:\n ' + str(data) } ] @@ -1680,6 +1655,7 @@ def grade_short_answers(): except Exception as e: return str(e) + @app.route('/fetch_tips', methods=['POST']) @jwt_required() def fetch_answer_tips(): diff --git a/helper/exercises.py b/helper/exercises.py index 53321c4..b3f22c5 100644 --- a/helper/exercises.py +++ b/helper/exercises.py @@ -15,19 +15,19 @@ from helper.speech_to_text_helper import has_x_words nltk.download('words') -def gen_reading_passage_1(topic, req_exercises, difficulty): +def gen_reading_passage_1(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=1): if (len(req_exercises) == 0): req_exercises = random.sample(READING_EXERCISE_TYPES, 2) - number_of_exercises_q = divide_number_into_parts(TOTAL_READING_PASSAGE_1_EXERCISES, len(req_exercises)) + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_READING_PASSAGE_1_EXERCISES, len(req_exercises)) passage = generate_reading_passage_1_text(topic) if passage == "": - return gen_reading_passage_1(topic, req_exercises, difficulty) - start_id = 1 + return gen_reading_passage_1(topic, difficulty, req_exercises, number_of_exercises_q, start_id) exercises = generate_reading_exercises(passage["text"], req_exercises, number_of_exercises_q, start_id, difficulty) if contains_empty_dict(exercises): - return gen_reading_passage_1(topic, req_exercises, difficulty) + return gen_reading_passage_1(topic, difficulty, req_exercises, number_of_exercises_q, start_id) return { "exercises": exercises, "text": { @@ -38,19 +38,19 @@ def gen_reading_passage_1(topic, req_exercises, difficulty): } -def gen_reading_passage_2(topic, req_exercises, difficulty): +def gen_reading_passage_2(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=14): if (len(req_exercises) == 0): req_exercises = random.sample(READING_EXERCISE_TYPES, 2) - number_of_exercises_q = divide_number_into_parts(TOTAL_READING_PASSAGE_2_EXERCISES, len(req_exercises)) + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_READING_PASSAGE_2_EXERCISES, len(req_exercises)) passage = generate_reading_passage_2_text(topic) if passage == "": - return gen_reading_passage_2(topic, req_exercises, difficulty) - start_id = 14 + return gen_reading_passage_2(topic, difficulty, req_exercises, number_of_exercises_q, start_id) exercises = generate_reading_exercises(passage["text"], req_exercises, number_of_exercises_q, start_id, difficulty) if contains_empty_dict(exercises): - return gen_reading_passage_2(topic, req_exercises, difficulty) + return gen_reading_passage_2(topic, difficulty, req_exercises, number_of_exercises_q, start_id) return { "exercises": exercises, "text": { @@ -61,19 +61,19 @@ def gen_reading_passage_2(topic, req_exercises, difficulty): } -def gen_reading_passage_3(topic, req_exercises, difficulty): +def gen_reading_passage_3(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=27): if (len(req_exercises) == 0): req_exercises = random.sample(READING_EXERCISE_TYPES, 2) - number_of_exercises_q = divide_number_into_parts(TOTAL_READING_PASSAGE_3_EXERCISES, len(req_exercises)) + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_READING_PASSAGE_3_EXERCISES, len(req_exercises)) passage = generate_reading_passage_3_text(topic) if passage == "": - return gen_reading_passage_3(topic, req_exercises, difficulty) - start_id = 27 + return gen_reading_passage_3(topic, difficulty, req_exercises, number_of_exercises_q, start_id) exercises = generate_reading_exercises(passage["text"], req_exercises, number_of_exercises_q, start_id, difficulty) if contains_empty_dict(exercises): - return gen_reading_passage_3(topic, req_exercises, difficulty) + return gen_reading_passage_3(topic, difficulty, req_exercises, number_of_exercises_q, start_id) return { "exercises": exercises, "text": { @@ -865,7 +865,8 @@ def gen_idea_match_exercise(text: str, quantity: int, start_id): { "role": "user", "content": ( - 'From the text extract ' + str(quantity) + ' ideas, theories, opinions and who they are from. The text: ' + str(text)) + 'From the text extract ' + str( + quantity) + ' ideas, theories, opinions and who they are from. The text: ' + str(text)) } ] @@ -882,6 +883,7 @@ def gen_idea_match_exercise(text: str, quantity: int, start_id): "type": "matchSentences" } + def build_options(ideas): options = [] letters = iter(string.ascii_uppercase) @@ -892,6 +894,7 @@ def build_options(ideas): }) return options + def build_sentences(ideas, start_id): sentences = [] letters = iter(string.ascii_uppercase) @@ -906,6 +909,7 @@ def build_sentences(ideas, start_id): sentence["id"] = i return sentences + def assign_letters_to_paragraphs(paragraphs): result = [] letters = iter(string.ascii_uppercase) @@ -1272,7 +1276,8 @@ def replace_exercise_if_exists(all_exams, current_exercise, current_exam, seen_k current_exercise["options"]) for exercise in exercise_dict.get("exercises", [])[0]["questions"] ): - return replace_exercise_if_exists(all_exams, generate_single_mc_level_question(), current_exam, seen_keys) + return replace_exercise_if_exists(all_exams, generate_single_mc_level_question(), current_exam, + seen_keys) return current_exercise, seen_keys @@ -1302,7 +1307,8 @@ def replace_blank_space_exercise_if_exists_utas(all_exams, current_exercise, cur key = (current_exercise['prompt'], tuple(sorted(option['text'] for option in current_exercise['options']))) # Check if the key is in the set if key in seen_keys: - return replace_exercise_if_exists_utas(all_exams, generate_single_mc_blank_space_level_question(), current_exam, seen_keys) + return replace_exercise_if_exists_utas(all_exams, generate_single_mc_blank_space_level_question(), current_exam, + seen_keys) else: seen_keys.add(key) @@ -1313,7 +1319,8 @@ def replace_blank_space_exercise_if_exists_utas(all_exams, current_exercise, cur current_exercise["options"]) for exercise in exam.get("questions", []) ): - return replace_exercise_if_exists_utas(all_exams, generate_single_mc_blank_space_level_question(), current_exam, + return replace_exercise_if_exists_utas(all_exams, generate_single_mc_blank_space_level_question(), + current_exam, seen_keys) return current_exercise, seen_keys @@ -1323,7 +1330,8 @@ def replace_underlined_exercise_if_exists_utas(all_exams, current_exercise, curr key = (current_exercise['prompt'], tuple(sorted(option['text'] for option in current_exercise['options']))) # Check if the key is in the set if key in seen_keys: - return replace_exercise_if_exists_utas(all_exams, generate_single_mc_underlined_level_question(), current_exam, seen_keys) + return replace_exercise_if_exists_utas(all_exams, generate_single_mc_underlined_level_question(), current_exam, + seen_keys) else: seen_keys.add(key) @@ -1334,7 +1342,8 @@ def replace_underlined_exercise_if_exists_utas(all_exams, current_exercise, curr current_exercise["options"]) for exercise in exam.get("questions", []) ): - return replace_exercise_if_exists_utas(all_exams, generate_single_mc_underlined_level_question(), current_exam, + return replace_exercise_if_exists_utas(all_exams, generate_single_mc_underlined_level_question(), + current_exam, seen_keys) return current_exercise, seen_keys @@ -1376,8 +1385,8 @@ def generate_single_mc_blank_space_level_question(): }, { "role": "user", - "content": ('Generate 1 multiple choice blank space question of 4 options for an english level exam, it can be easy, ' - 'intermediate or advanced.') + "content": ('Generate 1 multiple choice blank space question of 4 options for an english level exam, ' + 'it can be easy, intermediate or advanced.') } ] @@ -1401,8 +1410,8 @@ def generate_single_mc_underlined_level_question(): }, { "role": "user", - "content": ('Generate 1 multiple choice blank space question of 4 options for an english level exam, it can be easy, ' - 'intermediate or advanced.') + "content": ('Generate 1 multiple choice blank space question of 4 options for an english level exam, ' + 'it can be easy, intermediate or advanced.') }, { @@ -1469,9 +1478,9 @@ def gen_multiple_choice_blank_space_utas(quantity: int, start_id: int, all_exams if all_exams is not None: seen_keys = set() for i in range(len(question["questions"])): - question["questions"][i], seen_keys = replace_blank_space_exercise_if_exists_utas(all_exams, question["questions"][i], - question, - seen_keys) + question["questions"][i], seen_keys = ( + replace_blank_space_exercise_if_exists_utas(all_exams, question["questions"][i], question, + seen_keys)) response = fix_exercise_ids(question, start_id) response["questions"] = randomize_mc_options_order(response["questions"]) return response @@ -1546,11 +1555,9 @@ def gen_multiple_choice_underlined_utas(quantity: int, start_id: int, all_exams= if all_exams is not None: seen_keys = set() for i in range(len(question["questions"])): - question["questions"][i], seen_keys = replace_underlined_exercise_if_exists_utas(all_exams, - question["questions"][ - i], - question, - seen_keys) + question["questions"][i], seen_keys = ( + replace_underlined_exercise_if_exists_utas(all_exams, question["questions"][i], question, + seen_keys)) response = fix_exercise_ids(question, start_id) response["questions"] = randomize_mc_options_order(response["questions"]) return response @@ -1765,7 +1772,8 @@ def generate_level_mc(start_id: int, quantity: int, all_questions=None): if all_questions is not None: seen_keys = set() for i in range(len(question["questions"])): - question["questions"][i], seen_keys = replace_exercise_if_exists_utas(all_questions, question["questions"][i], + question["questions"][i], seen_keys = replace_exercise_if_exists_utas(all_questions, + question["questions"][i], question, seen_keys) response = fix_exercise_ids(question, start_id) @@ -1791,3 +1799,293 @@ def randomize_mc_options_order(questions): question['solution'] = option['id'] return questions + + +def gen_writing_task_1(topic, difficulty): + messages = [ + { + "role": "system", + "content": ('You are a helpful assistant designed to output JSON on this format: ' + '{"prompt": "prompt content"}') + }, + { + "role": "user", + "content": ('Craft a prompt for an IELTS Writing Task 1 General Training exercise that instructs the ' + 'student to compose a letter. The prompt should present a specific scenario or situation, ' + 'based on the topic of "' + topic + '", requiring the student to provide information, ' + 'advice, or instructions within the letter. ' + 'Make sure that the generated prompt is ' + 'of ' + difficulty + 'difficulty and does not contain ' + 'forbidden subjects in muslim ' + 'countries.') + }, + { + "role": "user", + "content": 'The prompt should end with "In the letter you should" followed by 3 bullet points of what ' + 'the answer should include.' + } + ] + token_count = count_total_tokens(messages) + response = make_openai_call(GPT_3_5_TURBO, messages, token_count, "prompt", + GEN_QUESTION_TEMPERATURE) + return { + "question": add_newline_before_hyphen(response["prompt"].strip()), + "difficulty": difficulty, + "topic": topic + } + + +def add_newline_before_hyphen(s): + return s.replace(" -", "\n-") + + +def gen_writing_task_2(topic, difficulty): + messages = [ + { + "role": "system", + "content": ('You are a helpful assistant designed to output JSON on this format: ' + '{"prompt": "prompt content"}') + }, + { + "role": "user", + "content": ( + 'Craft a comprehensive question of ' + difficulty + 'difficulty like the ones for IELTS Writing ' + 'Task 2 General Training that directs the ' + 'candidate' + 'to delve into an in-depth analysis of ' + 'contrasting perspectives on the topic ' + 'of "' + topic + '". The candidate should be ' + 'asked to discuss the ' + 'strengths and weaknesses of ' + 'both viewpoints.') + }, + { + "role": "user", + "content": 'The question should lead to an answer with either "theories", "complicated information" or ' + 'be "very descriptive" on the topic.' + } + ] + token_count = count_total_tokens(messages) + response = make_openai_call(GPT_4_O, messages, token_count, "prompt", GEN_QUESTION_TEMPERATURE) + return { + "question": response["prompt"].strip(), + "difficulty": difficulty, + "topic": topic + } + + +def gen_speaking_part_1(first_topic: str, second_topic: str, difficulty): + json_format = { + "first_topic": "topic 1", + "second_topic": "topic 2", + "questions": [ + "Introductory question about the first topic, starting the topic with 'Let's talk about x' and then the " + "question.", + "Follow up question about the first topic", + "Follow up question about the first topic", + "Question about second topic", + "Follow up question about the second topic", + ] + } + + messages = [ + { + "role": "system", + "content": ( + 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)) + }, + { + "role": "user", + "content": ( + 'Craft 5 simple and single questions of easy difficulty for IELTS Speaking Part 1 ' + 'that encourages candidates to delve deeply into ' + 'personal experiences, preferences, or insights on the topic ' + 'of "' + first_topic + '" and the topic of "' + second_topic + '". ' + 'Make sure that the generated ' + 'question' + 'does not contain forbidden ' + 'subjects in' + 'muslim countries.') + }, + { + "role": "user", + "content": 'The questions should lead to the usage of 4 verb tenses (present perfect, present, ' + 'past and future).' + }, + { + "role": "user", + "content": 'They must be 1 single question each and not be double-barreled questions.' + + } + ] + token_count = count_total_tokens(messages) + response = make_openai_call(GPT_4_O, messages, token_count, ["first_topic"], + GEN_QUESTION_TEMPERATURE) + response["type"] = 1 + response["difficulty"] = difficulty + return response + + +def gen_speaking_part_2(topic: str, difficulty): + json_format = { + "topic": "topic", + "question": "question", + "prompts": [ + "prompt_1", + "prompt_2", + "prompt_3" + ], + "suffix": "And explain why..." + } + + messages = [ + { + "role": "system", + "content": 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format) + }, + { + "role": "user", + "content": ( + 'Create a question of medium difficulty for IELTS Speaking Part 2 ' + 'that encourages candidates to narrate a ' + 'personal experience or story related to the topic ' + 'of "' + topic + '". Include 3 prompts that ' + 'guide the candidate to describe ' + 'specific aspects of the experience, ' + 'such as details about the situation, ' + 'their actions, and the reasons it left a ' + 'lasting impression. Make sure that the ' + 'generated question does not contain ' + 'forbidden subjects in muslim countries.') + }, + { + "role": "user", + "content": 'The prompts must not be questions. Also include a suffix like the ones in the IELTS exams ' + 'that start with "And explain why".' + } + ] + token_count = count_total_tokens(messages) + response = make_openai_call(GPT_4_O, messages, token_count, GEN_FIELDS, GEN_QUESTION_TEMPERATURE) + response["type"] = 2 + response["difficulty"] = difficulty + response["topic"] = topic + return response + + +def gen_speaking_part_3(topic: str, difficulty): + json_format = { + "topic": "topic", + "questions": [ + "Introductory question about the topic.", + "Follow up question about the topic", + "Follow up question about the topic", + "Follow up question about the topic", + "Follow up question about the topic" + ] + } + + messages = [ + { + "role": "system", + "content": ( + 'You are a helpful assistant designed to output JSON on this format: ' + str(json_format)) + }, + { + "role": "user", + "content": ( + 'Formulate a set of 5 single questions of hard difficulty for IELTS Speaking Part 3 that encourage candidates to engage in a ' + 'meaningful discussion on the topic of "' + topic + '". Provide inquiries, ensuring ' + 'they explore various aspects, perspectives, and implications related to the topic.' + 'Make sure that the generated question does not contain forbidden subjects in muslim countries.') + + }, + { + "role": "user", + "content": 'They must be 1 single question each and not be double-barreled questions.' + + } + ] + token_count = count_total_tokens(messages) + response = make_openai_call(GPT_4_O, messages, token_count, GEN_FIELDS, GEN_QUESTION_TEMPERATURE) + # Remove the numbers from the questions only if the string starts with a number + response["questions"] = [re.sub(r"^\d+\.\s*", "", question) if re.match(r"^\d+\.", question) else question for + question in response["questions"]] + response["type"] = 3 + response["difficulty"] = difficulty + response["topic"] = topic + return response + + +def gen_listening_section_1(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=1): + if (len(req_exercises) == 0): + req_exercises = random.sample(LISTENING_1_EXERCISE_TYPES, 1) + + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_1_EXERCISES, len(req_exercises)) + + processed_conversation = generate_listening_1_conversation(topic) + + exercises = generate_listening_conversation_exercises(parse_conversation(processed_conversation), + req_exercises, + number_of_exercises_q, + start_id, difficulty) + return { + "exercises": exercises, + "text": processed_conversation, + "difficulty": difficulty + } + + +def gen_listening_section_2(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=11): + if (len(req_exercises) == 0): + req_exercises = random.sample(LISTENING_2_EXERCISE_TYPES, 2) + + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_2_EXERCISES, len(req_exercises)) + + monologue = generate_listening_2_monologue(topic) + + exercises = generate_listening_monologue_exercises(str(monologue), req_exercises, number_of_exercises_q, + start_id, difficulty) + return { + "exercises": exercises, + "text": monologue, + "difficulty": difficulty + } + + +def gen_listening_section_3(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=21): + if (len(req_exercises) == 0): + req_exercises = random.sample(LISTENING_3_EXERCISE_TYPES, 1) + + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_3_EXERCISES, len(req_exercises)) + + processed_conversation = generate_listening_3_conversation(topic) + + exercises = generate_listening_conversation_exercises(parse_conversation(processed_conversation), req_exercises, + number_of_exercises_q, + start_id, difficulty) + return { + "exercises": exercises, + "text": processed_conversation, + "difficulty": difficulty + } + + +def gen_listening_section_4(topic, difficulty, req_exercises, number_of_exercises_q=queue.Queue(), start_id=31): + if (len(req_exercises) == 0): + req_exercises = random.sample(LISTENING_EXERCISE_TYPES, 2) + + if (number_of_exercises_q.empty()): + number_of_exercises_q = divide_number_into_parts(TOTAL_LISTENING_SECTION_4_EXERCISES, len(req_exercises)) + + monologue = generate_listening_4_monologue(topic) + + exercises = generate_listening_monologue_exercises(str(monologue), req_exercises, number_of_exercises_q, + start_id, difficulty) + return { + "exercises": exercises, + "text": monologue, + "difficulty": difficulty + } diff --git a/modules/training_content/service.py b/modules/training_content/service.py index 1381beb..f583571 100644 --- a/modules/training_content/service.py +++ b/modules/training_content/service.py @@ -1,3 +1,4 @@ +import json from datetime import datetime from logging import getLogger @@ -24,7 +25,8 @@ class TrainingContentService: self._logger = getLogger(__name__) self._llm = openai - def get_tips(self, stats): + def get_tips(self, training_content): + user, stats = training_content["userID"], training_content["stats"] exam_data, exam_map = self._sort_out_solutions(stats) training_content = self._get_exam_details_and_tips(exam_data) tips = self._query_kb(training_content.queries) @@ -39,7 +41,8 @@ class TrainingContentService: 'created_at': int(datetime.now().timestamp() * 1000), **exam_map, **usefull_tips.dict(), - **weak_areas + **weak_areas, + "user": user } doc_ref = self._db.collection('training').add(training_doc) return { @@ -70,7 +73,6 @@ class TrainingContentService: tips = {"tips": []} for query in queries: - print(f"{query.category} {query.text}") if query.category == "words": tips["tips"].extend( self._training_content_module.query_knowledge_base(query.text, "word_link") @@ -104,7 +106,16 @@ class TrainingContentService: ' with sentence structure and punctuation.", the "queries" field is where you will write queries ' 'for tips that will be displayed to the student, the category attribute is a collection of ' 'embeddings and the text will be the text used to query the knowledge base. The categories are ' - f'the following [{", ".join(self.TOOLS)}].' + f'the following [{", ".join(self.TOOLS)}]. The exam data will be a json where the key of the field ' + '"exams" is the exam id, an exam can be composed of multiple modules or single modules. The student' + ' will see your response so refrain from using phrasing like "The student" did x, y and z. If the ' + 'field "answer" in a question is an empty array "[]", then the student didn\'t answer any question ' + 'and you must address that in your response. Also questions aren\'t modules, the only modules are: ' + 'level, speaking, writing, reading and listening. The details array needs to be tailored to the ' + 'exam attempt, even if you receive the same exam you must treat as different exams by their id.' + 'Don\'t make references to an exam by it\'s id, the GUI will handle that so the student knows ' + 'which is the exam your comments and summary are referencing too. Even if the student hasn\'t ' + 'submitted no answers for an exam, you must still fill the details structure addressing that fact.' ) }, { @@ -150,42 +161,68 @@ class TrainingContentService: def _sort_out_solutions(self, stats): grouped_stats = {} for stat in stats: - exam_id = stat["exam"] + session_key = f'{str(stat["date"])}-{stat["user"]}' module = stat["module"] - if module not in grouped_stats: - grouped_stats[module] = {} - if exam_id not in grouped_stats[module]: - grouped_stats[module][exam_id] = [] - grouped_stats[module][exam_id].append(stat) + exam_id = stat["exam"] + + if session_key not in grouped_stats: + grouped_stats[session_key] = {} + if module not in grouped_stats[session_key]: + grouped_stats[session_key][module] = { + "stats": [], + "exam_id": exam_id + } + grouped_stats[session_key][module]["stats"].append(stat) exercises = {} exam_map = {} - for module, exams in grouped_stats.items(): - exercises[module] = {} - for exam_id, stat_group in exams.items(): - exam = self._get_doc_by_id(module, exam_id) - exercises[module][exam_id] = {"date": None, "exercises": [], "score": None} + for session_key, modules in grouped_stats.items(): + exercises[session_key] = {} + for module, module_stats in modules.items(): + exercises[session_key][module] = {} + + exam_id = module_stats["exam_id"] + if exam_id not in exercises[session_key][module]: + exercises[session_key][module][exam_id] = {"date": None, "exercises": []} + exam_total_questions = 0 exam_total_correct = 0 - for stat in stat_group: + + for stat in module_stats["stats"]: exam_total_questions += stat["score"]["total"] exam_total_correct += stat["score"]["correct"] - exercises[module][exam_id]["date"] = stat["date"] + exercises[session_key][module][exam_id]["date"] = stat["date"] - if exam_id not in exam_map: - exam_map[exam_id] = {"stat_ids": [], "score": 0} - exam_map[exam_id]["stat_ids"].append(stat["id"]) + if session_key not in exam_map: + exam_map[session_key] = {"stat_ids": [], "score": 0} + exam_map[session_key]["stat_ids"].append(stat["id"]) + exam = self._get_doc_by_id(module, exam_id) if module == "listening": - exercises[module][exam_id]["exercises"].extend(self._get_listening_solutions(stat, exam)) - if module == "reading": - exercises[module][exam_id]["exercises"].extend(self._get_reading_solutions(stat, exam)) - if module == "writing": - exercises[module][exam_id]["exercises"].extend(self._get_writing_prompts_and_answers(stat, exam)) + exercises[session_key][module][exam_id]["exercises"].extend( + self._get_listening_solutions(stat, exam)) + elif module == "reading": + exercises[session_key][module][exam_id]["exercises"].extend( + self._get_reading_solutions(stat, exam)) + elif module == "writing": + exercises[session_key][module][exam_id]["exercises"].extend( + self._get_writing_prompts_and_answers(stat, exam) + ) + elif module == "speaking": + exercises[session_key][module][exam_id]["exercises"].extend( + self._get_speaking_solutions(stat, exam) + ) + elif module == "level": + exercises[session_key][module][exam_id]["exercises"].extend( + self._get_level_solutions(stat, exam) + ) - exam_map[exam_id]["score"] = round((exam_total_correct / exam_total_questions) * 100) - exam_map[exam_id]["module"] = module - return exercises, exam_map + exam_map[session_key]["score"] = round((exam_total_correct / exam_total_questions) * 100) + exam_map[session_key]["module"] = module + with open('exam_result.json', 'w') as file: + json.dump({"exams": exercises}, file, indent=4) + + return {"exams": exercises}, exam_map def _get_writing_prompts_and_answers(self, stat, exam): result = [] @@ -211,6 +248,54 @@ class TrainingContentService: return result + @staticmethod + def _get_mc_question(exercise, stat): + shuffle_maps = stat.get("shuffleMaps", []) + answer = stat["solutions"] if len(shuffle_maps) == 0 else [] + if len(shuffle_maps) != 0: + for solution in stat["solutions"]: + shuffle_map = [ + item["map"] for item in shuffle_maps + if item["questionID"] == solution["question"] + ] + answer.append({ + "question": solution["question"], + "option": shuffle_map[solution["option"]] + }) + return { + "question": exercise["prompt"], + "exercise": exercise["questions"], + "answer": stat["solutions"] + } + + @staticmethod + def _swap_key_name(d, original_key, new_key): + d[new_key] = d.pop(original_key) + return d + + def _get_level_solutions(self, stat, exam): + result = [] + try: + for part in exam["parts"]: + for exercise in part["exercises"]: + if exercise["id"] == stat["exercise"]: + if stat["type"] == "fillBlanks": + result.append({ + "prompt": exercise["prompt"], + "template": exercise["text"], + "words": exercise["words"], + "solutions": exercise["solutions"], + "answer": [ + self._swap_key_name(item, 'solution', 'option') + for item in stat["solutions"] + ] + }) + elif stat["type"] == "multipleChoice": + result.append(self._get_mc_question(exercise, stat)) + except KeyError as e: + self._logger.warning(f"Malformed stat object: {str(e)}") + return result + def _get_listening_solutions(self, stat, exam): result = [] try: @@ -224,16 +309,54 @@ class TrainingContentService: "solution": exercise["solutions"], "answer": stat["solutions"] }) - if stat["type"] == "multipleChoice": + elif stat["type"] == "fillBlanks": result.append({ "question": exercise["prompt"], - "exercise": exercise["questions"], + "template": exercise["text"], + "words": exercise["words"], + "solutions": exercise["solutions"], "answer": stat["solutions"] }) + elif stat["type"] == "multipleChoice": + result.append(self._get_mc_question(exercise, stat)) + except KeyError as e: self._logger.warning(f"Malformed stat object: {str(e)}") return result + @staticmethod + def _find_shuffle_map(shuffle_maps, question_id): + return next((item["map"] for item in shuffle_maps if item["questionID"] == question_id), None) + + def _get_speaking_solutions(self, stat, exam): + result = {} + try: + result = { + "comments": { + key: value['comment'] for key, value in stat['solutions'][0]['evaluation']['task_response'].items()} + , + "exercises": {} + } + + for exercise in exam["exercises"]: + if exercise["id"] == stat["exercise"]: + if stat["type"] == "interactiveSpeaking": + for i in range(len(exercise["prompts"])): + result["exercises"][f"exercise_{i+1}"] = { + "question": exercise["prompts"][i]["text"] + } + for i in range(len(exercise["prompts"])): + answer = stat['solutions'][0]["evaluation"].get(f'transcript_{i+1}', '') + result["exercises"][f"exercise_{i+1}"]["answer"] = answer + elif stat["type"] == "speaking": + result["exercises"]["exercise_1"] = { + "question": exercise["text"], + "answer": stat['solutions'][0]["evaluation"].get(f'transcript', '') + } + except KeyError as e: + self._logger.warning(f"Malformed stat object: {str(e)}") + return [result] + def _get_reading_solutions(self, stat, exam): result = [] try: @@ -258,8 +381,13 @@ class TrainingContentService: "solutions": exercise["solutions"], "answer": stat["solutions"] }) - else: - # match_sentences + elif stat["type"] == "trueFalse": + result.append({ + "text": text, + "questions": exercise["questions"], + "answer": stat["solutions"] + }) + elif stat["type"] == "matchSentences": result.append({ "text": text, "question": exercise["prompt"], diff --git a/modules/upload_level/service.py b/modules/upload_level/service.py index bb4ed6b..85c46d1 100644 --- a/modules/upload_level/service.py +++ b/modules/upload_level/service.py @@ -36,7 +36,7 @@ class UploadLevelService: FileHelper.remove_directory(f'./tmp/{path_id}') if response: - return response.dict(exclude_none=True) + return self.fix_ids(response.dict(exclude_none=True)) return None @staticmethod @@ -378,3 +378,18 @@ class UploadLevelService: ) } + @staticmethod + def fix_ids(response): + counter = 1 + for part in response["parts"]: + for exercise in part["exercises"]: + if exercise["type"] == "multipleChoice": + for question in exercise["questions"]: + question["id"] = counter + counter += 1 + if exercise["type"] == "fillBlanks": + for i in range(len(exercise["words"])): + exercise["words"][i]["id"] = counter + exercise["solutions"][i]["id"] = counter + counter += 1 + return response \ No newline at end of file