From b32e38156cfd2e12be780d20e2911308e8dbc936 Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Mon, 13 Jan 2025 01:13:28 +0000 Subject: [PATCH] ENCOA-311 --- ielts_be/api/exam/listening.py | 3 ++- ielts_be/api/exam/speaking.py | 5 ++-- ielts_be/api/exam/writing.py | 8 +++---- ielts_be/controllers/abc/exam/listening.py | 3 ++- ielts_be/controllers/abc/exam/speaking.py | 3 ++- ielts_be/controllers/abc/exam/writing.py | 5 ++-- ielts_be/controllers/impl/exam/listening.py | 3 ++- ielts_be/controllers/impl/exam/speaking.py | 3 ++- ielts_be/controllers/impl/exam/writing.py | 6 +++-- ielts_be/dtos/level.py | 3 ++- ielts_be/dtos/listening.py | 3 ++- ielts_be/dtos/reading.py | 3 ++- ielts_be/services/abc/exam/listening.py | 2 +- ielts_be/services/abc/exam/speaking.py | 2 +- ielts_be/services/abc/exam/writing.py | 6 ++--- ielts_be/services/impl/exam/level/__init__.py | 23 +++++++++++++------ .../impl/exam/level/exercises/fill_blanks.py | 15 ++++++++---- .../exam/level/exercises/multiple_choice.py | 12 +++++----- .../impl/exam/level/exercises/passage_utas.py | 10 ++++---- .../impl/exam/level/full_exams/level_utas.py | 2 ++ .../services/impl/exam/listening/__init__.py | 13 ++++++++--- .../services/impl/exam/reading/__init__.py | 9 +++++++- .../services/impl/exam/speaking/__init__.py | 6 +++-- .../services/impl/exam/writing/__init__.py | 17 ++++++++------ .../services/impl/exam/writing/academic.py | 5 ++-- ielts_be/utils/__init__.py | 4 +++- ielts_be/utils/pick_difficulty.py | 14 +++++++++++ 27 files changed, 126 insertions(+), 62 deletions(-) create mode 100644 ielts_be/utils/pick_difficulty.py diff --git a/ielts_be/api/exam/listening.py b/ielts_be/api/exam/listening.py index b969cc3..906aa20 100644 --- a/ielts_be/api/exam/listening.py +++ b/ielts_be/api/exam/listening.py @@ -1,4 +1,5 @@ import random +from typing import List from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends, Path, Query, UploadFile @@ -31,7 +32,7 @@ async def upload( @inject async def generate_listening_dialog( section: int = Path(..., ge=1, le=4), - difficulty: str = Query(default=None), + difficulty: List[str] = Query(default=None), topic: str = Query(default=None), listening_controller: IListeningController = Depends(Provide[controller]) ): diff --git a/ielts_be/api/exam/speaking.py b/ielts_be/api/exam/speaking.py index 2b955cd..015497b 100644 --- a/ielts_be/api/exam/speaking.py +++ b/ielts_be/api/exam/speaking.py @@ -59,7 +59,7 @@ async def get_speaking_task( topic: Optional[str] = Query(None), first_topic: Optional[str] = Query(None), second_topic: Optional[str] = Query(None), - difficulty: Optional[str] = None, + difficulty: List[str] = Query(default=None), speaking_controller: ISpeakingController = Depends(Provide[controller]) ): if not second_topic: @@ -67,8 +67,7 @@ async def get_speaking_task( else: topic_or_first_topic = first_topic if first_topic else random.choice(EducationalContent.MTI_TOPICS) - if not difficulty: - difficulty = random.choice(random.choice(EducationalContent.DIFFICULTIES)) + difficulty = [random.choice(EducationalContent.DIFFICULTIES)] if not difficulty else difficulty second_topic = second_topic if second_topic else random.choice(EducationalContent.MTI_TOPICS) return await speaking_controller.get_speaking_part(task, topic_or_first_topic, second_topic, difficulty) diff --git a/ielts_be/api/exam/writing.py b/ielts_be/api/exam/writing.py index 7988acb..0f7af49 100644 --- a/ielts_be/api/exam/writing.py +++ b/ielts_be/api/exam/writing.py @@ -20,10 +20,10 @@ writing_router = APIRouter() async def generate_writing_academic( task: int = Path(..., ge=1, le=2), file: UploadFile = File(...), - difficulty: Optional[List[str]] = None, + difficulty: List[str] = Query(default=None), writing_controller: IWritingController = Depends(Provide[controller]) ): - difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty + difficulty = [random.choice(EducationalContent.DIFFICULTIES)] if not difficulty else difficulty return await writing_controller.get_writing_task_academic_question(task, file, difficulty) @@ -34,10 +34,10 @@ async def generate_writing_academic( @inject async def generate_writing( task: int = Path(..., ge=1, le=2), - difficulty: Optional[str] = None, + difficulty: List[str] = Query(default=None), topic: str = Query(default=None), writing_controller: IWritingController = Depends(Provide[controller]) ): - difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty + difficulty = [random.choice(EducationalContent.DIFFICULTIES)] if not difficulty else difficulty topic = random.choice(EducationalContent.MTI_TOPICS) if not topic else topic return await writing_controller.get_writing_task_general_question(task, topic, difficulty) diff --git a/ielts_be/controllers/abc/exam/listening.py b/ielts_be/controllers/abc/exam/listening.py index 8dbed28..823aff9 100644 --- a/ielts_be/controllers/abc/exam/listening.py +++ b/ielts_be/controllers/abc/exam/listening.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import List from fastapi import UploadFile @@ -10,7 +11,7 @@ class IListeningController(ABC): pass @abstractmethod - async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str): + async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: List[str]): pass @abstractmethod diff --git a/ielts_be/controllers/abc/exam/speaking.py b/ielts_be/controllers/abc/exam/speaking.py index 73c5a38..292c4bd 100644 --- a/ielts_be/controllers/abc/exam/speaking.py +++ b/ielts_be/controllers/abc/exam/speaking.py @@ -1,10 +1,11 @@ from abc import ABC, abstractmethod +from typing import List class ISpeakingController(ABC): @abstractmethod - async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str): + async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: List[str]): pass @abstractmethod diff --git a/ielts_be/controllers/abc/exam/writing.py b/ielts_be/controllers/abc/exam/writing.py index 4fc9e0e..65eb682 100644 --- a/ielts_be/controllers/abc/exam/writing.py +++ b/ielts_be/controllers/abc/exam/writing.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import List from fastapi.datastructures import UploadFile @@ -6,9 +7,9 @@ from fastapi.datastructures import UploadFile class IWritingController(ABC): @abstractmethod - async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str): + async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): pass @abstractmethod - async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str): + async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: List[str]): pass diff --git a/ielts_be/controllers/impl/exam/listening.py b/ielts_be/controllers/impl/exam/listening.py index fbc3e1e..5e336c6 100644 --- a/ielts_be/controllers/impl/exam/listening.py +++ b/ielts_be/controllers/impl/exam/listening.py @@ -1,4 +1,5 @@ import io +from typing import List from fastapi import UploadFile from fastapi.responses import StreamingResponse, Response @@ -20,7 +21,7 @@ class ListeningController(IListeningController): else: return res - async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str): + async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: List[str]): return await self._service.generate_listening_dialog(section_id, topic, difficulty) async def get_listening_question(self, dto: ListeningExercisesDTO): diff --git a/ielts_be/controllers/impl/exam/speaking.py b/ielts_be/controllers/impl/exam/speaking.py index 0ca895d..f79c23d 100644 --- a/ielts_be/controllers/impl/exam/speaking.py +++ b/ielts_be/controllers/impl/exam/speaking.py @@ -1,4 +1,5 @@ import logging +from typing import List from ielts_be.controllers import ISpeakingController from ielts_be.services import ISpeakingService, IVideoGeneratorService @@ -11,7 +12,7 @@ class SpeakingController(ISpeakingController): self._vid_gen = vid_gen self._logger = logging.getLogger(__name__) - async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str): + async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: List[str]): return await self._service.get_speaking_part(task, topic, second_topic, difficulty) async def get_avatars(self): diff --git a/ielts_be/controllers/impl/exam/writing.py b/ielts_be/controllers/impl/exam/writing.py index 5fdf19b..f74ea60 100644 --- a/ielts_be/controllers/impl/exam/writing.py +++ b/ielts_be/controllers/impl/exam/writing.py @@ -1,3 +1,5 @@ +from typing import List + from fastapi import UploadFile, HTTPException from ielts_be.controllers import IWritingController @@ -9,10 +11,10 @@ class WritingController(IWritingController): def __init__(self, writing_service: IWritingService): self._service = writing_service - async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str): + async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): return await self._service.get_writing_task_general_question(task, topic, difficulty) - async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str): + async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: List[str]): if attachment.content_type not in ['image/jpeg', 'image/png']: raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG and PNG allowed.") return await self._service.get_writing_task_academic_question(task, attachment, difficulty) diff --git a/ielts_be/dtos/level.py b/ielts_be/dtos/level.py index e95f843..8709c89 100644 --- a/ielts_be/dtos/level.py +++ b/ielts_be/dtos/level.py @@ -12,7 +12,8 @@ class LevelExercises(BaseModel): sa_qty: Optional[int] = None mc_qty: Optional[int] = None topic: Optional[str] = None + difficulty: Optional[str] = None class LevelExercisesDTO(BaseModel): exercises: List[LevelExercises] - difficulty: Optional[str] = None + difficulty: Optional[List[str]] = None diff --git a/ielts_be/dtos/listening.py b/ielts_be/dtos/listening.py index 09dbefa..f048eb9 100644 --- a/ielts_be/dtos/listening.py +++ b/ielts_be/dtos/listening.py @@ -17,11 +17,12 @@ class SaveListeningDTO(BaseModel): class ListeningExercises(BaseModel): type: ListeningExerciseType quantity: int + difficulty: Optional[str] = None class ListeningExercisesDTO(BaseModel): text: str exercises: List[ListeningExercises] - difficulty: Optional[str] + difficulty: Optional[List[str]] = None class InstructionsDTO(BaseModel): text: str diff --git a/ielts_be/dtos/reading.py b/ielts_be/dtos/reading.py index 192f8b5..9ad8594 100644 --- a/ielts_be/dtos/reading.py +++ b/ielts_be/dtos/reading.py @@ -10,8 +10,9 @@ class ReadingExercise(BaseModel): quantity: int num_random_words: Optional[int] = Field(1) max_words: Optional[int] = Field(3) + difficulty: Optional[str] = None class ReadingDTO(BaseModel): text: str = Field(...) exercises: List[ReadingExercise] = Field(...) - difficulty: Optional[str] = None + difficulty: Optional[List[str]] = None diff --git a/ielts_be/services/abc/exam/listening.py b/ielts_be/services/abc/exam/listening.py index 723bf45..6071a51 100644 --- a/ielts_be/services/abc/exam/listening.py +++ b/ielts_be/services/abc/exam/listening.py @@ -9,7 +9,7 @@ from fastapi import UploadFile class IListeningService(ABC): @abstractmethod - async def generate_listening_dialog( self, section_id: int, topic: str, difficulty: str): + async def generate_listening_dialog( self, section_id: int, topic: str, difficulty: List[str]): pass @abstractmethod diff --git a/ielts_be/services/abc/exam/speaking.py b/ielts_be/services/abc/exam/speaking.py index 696cec9..e098f3e 100644 --- a/ielts_be/services/abc/exam/speaking.py +++ b/ielts_be/services/abc/exam/speaking.py @@ -6,7 +6,7 @@ class ISpeakingService(ABC): @abstractmethod async def get_speaking_part( - self, part: int, topic: str, second_topic: str, difficulty: str + self, part: int, topic: str, second_topic: str, difficulty: List[str] ) -> Dict: pass diff --git a/ielts_be/services/abc/exam/writing.py b/ielts_be/services/abc/exam/writing.py index 94d223d..058360f 100644 --- a/ielts_be/services/abc/exam/writing.py +++ b/ielts_be/services/abc/exam/writing.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, List from fastapi import UploadFile @@ -7,11 +7,11 @@ from fastapi import UploadFile class IWritingService(ABC): @abstractmethod - async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str): + async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): pass @abstractmethod - async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str): + async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: List[str]): pass @abstractmethod diff --git a/ielts_be/services/impl/exam/level/__init__.py b/ielts_be/services/impl/exam/level/__init__.py index 6ff23cb..5b48655 100644 --- a/ielts_be/services/impl/exam/level/__init__.py +++ b/ielts_be/services/impl/exam/level/__init__.py @@ -13,6 +13,7 @@ from ielts_be.services import ( ILevelService, ILLMService, IReadingService, IWritingService, IListeningService, ISpeakingService ) +from ielts_be.utils import pick_difficulty from .exercises import MultipleChoice, BlankSpace, PassageUtas, FillBlanks from .full_exams import CustomLevelModule, LevelUtas from .upload import UploadLevelModule @@ -50,19 +51,22 @@ class LevelService(ILevelService): async def upload_level(self, upload: UploadFile, solutions: Optional[UploadFile] = None) -> Dict: return await self._upload_module.generate_level_from_file(upload, solutions) - async def _generate_exercise(self, req_exercise, start_id): + async def _generate_exercise(self, req_exercise, start_id, difficulties): + difficulty = pick_difficulty(req_exercise.difficulty, difficulties) if req_exercise.type == "mcBlank": - questions = await self._mc.gen_multiple_choice("blank_space", req_exercise.quantity, start_id) + questions = await self._mc.gen_multiple_choice("blank_space", req_exercise.quantity, difficulty, start_id) questions["variant"] = "mcBlank" questions["type"] = "multipleChoice" questions["prompt"] = "Choose the correct word or group of words that completes the sentences." + questions["difficulty"] = difficulty return questions elif req_exercise.type == "mcUnderline": - questions = await self._mc.gen_multiple_choice("underline", req_exercise.quantity, start_id) + questions = await self._mc.gen_multiple_choice("underline", req_exercise.quantity, difficulty, start_id) questions["variant"] = "mcUnderline" questions["type"] = "multipleChoice" questions["prompt"] = "Choose the underlined word or group of words that is not correct." + questions["difficulty"] = difficulty return questions elif req_exercise.type == "passageUtas": @@ -70,21 +74,24 @@ class LevelService(ILevelService): exercise = await self._passage_utas.gen_reading_passage_utas( start_id, req_exercise.quantity, + difficulty, topic, req_exercise.text_size ) exercise["prompt"] = "Read the text and answer the questions below." - + exercise["difficulty"] = difficulty return exercise elif req_exercise.type == "fillBlanksMC": exercise = await self._fill_blanks.gen_fill_blanks( start_id, req_exercise.quantity, + difficulty, req_exercise.text_size, req_exercise.topic ) exercise["prompt"] = "Read the text below and choose the correct word for each space." + exercise["difficulty"] = difficulty return exercise async def generate_exercises(self, dto: LevelExercisesDTO): @@ -95,7 +102,7 @@ class LevelService(ILevelService): current_id += req_exercise.quantity tasks = [ - self._generate_exercise(req_exercise, start_id) + self._generate_exercise(req_exercise, start_id, dto.difficulty) for req_exercise, start_id in zip(dto.exercises, start_ids) ] questions = await gather(*tasks) @@ -105,10 +112,12 @@ class LevelService(ILevelService): # Just here to support other modules that I don't know if they are supposed to still be used async def gen_multiple_choice(self, mc_variant: str, quantity: int, start_id: int = 1): - return await self._mc.gen_multiple_choice(mc_variant, quantity, start_id) + difficulty = random.choice(EducationalContent.DIFFICULTIES) + return await self._mc.gen_multiple_choice(mc_variant, quantity, difficulty, start_id) async def gen_reading_passage_utas(self, start_id, mc_quantity: int, topic=Optional[str]): # sa_quantity: int, - return await self._passage_utas.gen_reading_passage_utas(start_id, mc_quantity, topic) + difficulty = random.choice(EducationalContent.DIFFICULTIES) + return await self._passage_utas.gen_reading_passage_utas(start_id, mc_quantity, difficulty, topic) async def gen_blank_space_text_utas(self, quantity: int, start_id: int, size: int, topic: str): return await self._blank_space.gen_blank_space_text_utas(quantity, start_id, size, topic) diff --git a/ielts_be/services/impl/exam/level/exercises/fill_blanks.py b/ielts_be/services/impl/exam/level/exercises/fill_blanks.py index 976aa56..f6be541 100644 --- a/ielts_be/services/impl/exam/level/exercises/fill_blanks.py +++ b/ielts_be/services/impl/exam/level/exercises/fill_blanks.py @@ -11,12 +11,10 @@ class FillBlanks: async def gen_fill_blanks( - self, start_id: int, quantity: int, size: int = 300, topic=None + self, start_id: int, quantity: int, difficulty: str, size: int = 300, topic=None ): if not topic: topic = random.choice(EducationalContent.MTI_TOPICS) - print(quantity) - print(start_id) messages = [ { "role": "system", @@ -34,8 +32,15 @@ class FillBlanks: 'JSON object containing: the modified text, a solutions array with each word\'s correct ' 'letter (A-D), and a words array containing each id with four options where one is ' 'the original word (matching the solution) and three are plausible but incorrect ' - 'alternatives that maintain grammatical consistency. ' - 'You cannot use repeated words!' #TODO: Solve this after + f'alternatives that maintain grammatical consistency and {difficulty} CEFR level complexity. ' + 'You cannot use repeated words!' + # TODO: Solve this after -> forgot about this TODO just saw now in + # 1/11/25 what I meant by this is gpt still sometimes returns repeated + # words even with explicit instructions to not do so, this is a general problem + # for all exercises, for more robust validation use self._llm.pydantic_prediction + # or implement a method that calls a mapper, catches validation error messages + # from that mapper and asks gpt to retry if creating a pydantic model for each + # operation proves to be unsustainable ) } ] diff --git a/ielts_be/services/impl/exam/level/exercises/multiple_choice.py b/ielts_be/services/impl/exam/level/exercises/multiple_choice.py index ea4e129..63cf085 100644 --- a/ielts_be/services/impl/exam/level/exercises/multiple_choice.py +++ b/ielts_be/services/impl/exam/level/exercises/multiple_choice.py @@ -10,16 +10,16 @@ class MultipleChoice: self._mc_variants = mc_variants async def gen_multiple_choice( - self, mc_variant: str, quantity: int, start_id: int = 1 + self, mc_variant: str, quantity: int, difficulty: str, start_id: int = 1 ): mc_template = self._mc_variants[mc_variant] blank_mod = " blank space " if mc_variant == "blank_space" else " " gen_multiple_choice_for_text: str = ( - 'Generate {quantity} multiple choice{blank}questions of 4 options for an english level exam, some easy ' - 'questions, some intermediate questions and some advanced questions. Ensure that the questions cover ' - 'a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence structure, and ' - 'punctuation. Make sure every question only has 1 correct answer.' + 'Generate {quantity} multiple choice{blank}questions of 4 options for an english level exam of {difficulty} ' + 'CEFR level, some easy questions, some intermediate questions and some advanced questions. Ensure that ' + 'the questions cover a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence ' + 'structure, and punctuation. Make sure every question only has 1 correct answer.' ) messages = [ @@ -31,7 +31,7 @@ class MultipleChoice: }, { "role": "user", - "content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod) + "content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod, difficulty=difficulty) } ] diff --git a/ielts_be/services/impl/exam/level/exercises/passage_utas.py b/ielts_be/services/impl/exam/level/exercises/passage_utas.py index 683cee4..cde7a80 100644 --- a/ielts_be/services/impl/exam/level/exercises/passage_utas.py +++ b/ielts_be/services/impl/exam/level/exercises/passage_utas.py @@ -13,11 +13,11 @@ class PassageUtas: self._mc_variants = mc_variants async def gen_reading_passage_utas( - self, start_id, mc_quantity: int, topic: Optional[str], word_size: Optional[int] # sa_quantity: int, + self, start_id, mc_quantity: int, difficulty: str, topic: Optional[str] = None, word_size: Optional[int] = None# sa_quantity: int, ): passage = await self._reading_service.generate_reading_passage(1, topic, word_size) - mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id, mc_quantity) + mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id, mc_quantity, difficulty) mc_exercises["type"] = "multipleChoice" """ exercises: { @@ -61,7 +61,7 @@ class PassageUtas: return question["questions"] - async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int): + async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int, difficulty: str): json_template = self._mc_variants["text_mc_utas"] messages = [ @@ -71,7 +71,9 @@ class PassageUtas: }, { "role": "user", - "content": f'Generate {mc_quantity} multiple choice questions of 4 options for this text:\n{text}' + "content": ( + f'Generate {mc_quantity} multiple choice questions of 4 options, {difficulty} CEFR ' + f'level difficulty, for this text:\n{text}') }, { "role": "user", diff --git a/ielts_be/services/impl/exam/level/full_exams/level_utas.py b/ielts_be/services/impl/exam/level/full_exams/level_utas.py index 6234524..654bb4b 100644 --- a/ielts_be/services/impl/exam/level/full_exams/level_utas.py +++ b/ielts_be/services/impl/exam/level/full_exams/level_utas.py @@ -1,6 +1,8 @@ import json +import random import uuid +from ielts_be.configs.constants import EducationalContent from ielts_be.services import ILLMService diff --git a/ielts_be/services/impl/exam/listening/__init__.py b/ielts_be/services/impl/exam/listening/__init__.py index a9a3345..81465ad 100644 --- a/ielts_be/services/impl/exam/listening/__init__.py +++ b/ielts_be/services/impl/exam/listening/__init__.py @@ -1,7 +1,7 @@ import asyncio from logging import getLogger import random -from typing import Dict, Any, Union +from typing import Dict, Any, Union, List from starlette.datastructures import UploadFile @@ -20,6 +20,7 @@ from .write_blank_forms import WriteBlankForms from .write_blanks import WriteBlanks from .write_blank_notes import WriteBlankNotes from ..shared import TrueFalse, MultipleChoice +from ielts_be.utils import pick_difficulty class ListeningService(IListeningService): @@ -94,7 +95,8 @@ class ListeningService(IListeningService): return await self._import.import_from_file(exercises, solutions) - async def generate_listening_dialog(self, section: int, topic: str, difficulty: str): + async def generate_listening_dialog(self, section: int, topic: str, difficulty: List[str]): + # TODO: difficulties to difficulty return await self._sections[f'section_{section}']["generate_dialogue"](section, topic) async def transcribe_dialog(self, audio: UploadFile): @@ -142,7 +144,7 @@ class ListeningService(IListeningService): "dialog or monologue", dto.text, start_id, - dto.difficulty + pick_difficulty(req_exercise.difficulty, dto.difficulty) ) ) start_id += req_exercise.quantity @@ -157,6 +159,7 @@ class ListeningService(IListeningService): question = await self._multiple_choice.gen_multiple_choice( text, req_exercise.quantity, start_id, difficulty, n_options ) + question["difficulty"] = difficulty self._logger.info(f"Added multiple choice: {question}") return question @@ -165,6 +168,7 @@ class ListeningService(IListeningService): dialog_type, text, req_exercise.quantity, start_id, difficulty ) question["variant"] = "questions" + question["difficulty"] = difficulty self._logger.info(f"Added write blanks questions: {question}") return question @@ -173,6 +177,7 @@ class ListeningService(IListeningService): dialog_type, text, req_exercise.quantity, start_id, difficulty ) question["variant"] = "fill" + question["difficulty"] = difficulty self._logger.info(f"Added write blanks notes: {question}") return question @@ -181,12 +186,14 @@ class ListeningService(IListeningService): dialog_type, text, req_exercise.quantity, start_id, difficulty ) question["variant"] = "form" + question["difficulty"] = difficulty self._logger.info(f"Added write blanks form: {question}") return question elif req_exercise.type == "trueFalse": question = await self._true_false.gen_true_false_not_given_exercise( text, req_exercise.quantity, start_id, difficulty, "listening" ) + question["difficulty"] = difficulty self._logger.info(f"Added trueFalse: {question}") return question diff --git a/ielts_be/services/impl/exam/reading/__init__.py b/ielts_be/services/impl/exam/reading/__init__.py index aa39b0b..12174bd 100644 --- a/ielts_be/services/impl/exam/reading/__init__.py +++ b/ielts_be/services/impl/exam/reading/__init__.py @@ -7,6 +7,7 @@ from ielts_be.configs.constants import GPTModels, FieldsAndExercises, Temperatur from ielts_be.dtos.reading import ReadingDTO from ielts_be.helpers import ExercisesHelper from ielts_be.services import IReadingService, ILLMService +from ielts_be.utils import pick_difficulty from .fill_blanks import FillBlanks from .idea_match import IdeaMatch from .paragraph_match import ParagraphMatch @@ -84,6 +85,7 @@ class ReadingService(IReadingService): question = await self._fill_blanks.gen_summary_fill_blanks_exercise( text, req_exercise.quantity, start_id, difficulty, req_exercise.num_random_words ) + question["difficulty"] = difficulty self._logger.info(f"Added fill blanks: {question}") return question @@ -91,6 +93,7 @@ class ReadingService(IReadingService): question = await self._true_false.gen_true_false_not_given_exercise( text, req_exercise.quantity, start_id, difficulty, "reading" ) + question["difficulty"] = difficulty self._logger.info(f"Added trueFalse: {question}") return question @@ -100,6 +103,7 @@ class ReadingService(IReadingService): ) if ExercisesHelper.answer_word_limit_ok(question): + question["difficulty"] = difficulty self._logger.info(f"Added write blanks: {question}") return question else: @@ -110,6 +114,7 @@ class ReadingService(IReadingService): question = await self._paragraph_match.gen_paragraph_match_exercise( text, req_exercise.quantity, start_id ) + question["difficulty"] = difficulty self._logger.info(f"Added paragraph match: {question}") return question @@ -118,12 +123,14 @@ class ReadingService(IReadingService): text, req_exercise.quantity, start_id ) question["variant"] = "ideaMatch" + question["difficulty"] = difficulty self._logger.info(f"Added idea match: {question}") return question elif req_exercise.type == "multipleChoice": question = await self._multiple_choice.gen_multiple_choice( text, req_exercise.quantity, start_id, difficulty, 4 ) + question["difficulty"] = difficulty self._logger.info(f"Added multiple choice: {question}") return question @@ -137,7 +144,7 @@ class ReadingService(IReadingService): req_exercise, dto.text, start_id, - dto.difficulty + pick_difficulty(req_exercise.difficulty, dto.difficulty) ) ) start_id += req_exercise.quantity diff --git a/ielts_be/services/impl/exam/speaking/__init__.py b/ielts_be/services/impl/exam/speaking/__init__.py index 007316b..29ae3bc 100644 --- a/ielts_be/services/impl/exam/speaking/__init__.py +++ b/ielts_be/services/impl/exam/speaking/__init__.py @@ -1,5 +1,6 @@ import logging import re +import random from typing import Dict, List @@ -99,8 +100,9 @@ class SpeakingService(ISpeakingService): } async def get_speaking_part( - self, part: int, topic: str, second_topic: str, difficulty: str + self, part: int, topic: str, second_topic: str, difficulty: List[str] ) -> Dict: + diff = difficulty[0] if len(difficulty) == 1 else random.choice(difficulty) task_values = self._tasks[f'task_{part}']['get'] if part == 1: @@ -157,7 +159,7 @@ class SpeakingService(ISpeakingService): ] response["type"] = part - response["difficulty"] = difficulty + response["difficulty"] = diff if part in {2, 3}: response["topic"] = topic diff --git a/ielts_be/services/impl/exam/writing/__init__.py b/ielts_be/services/impl/exam/writing/__init__.py index 9c9691c..9e3ca7b 100644 --- a/ielts_be/services/impl/exam/writing/__init__.py +++ b/ielts_be/services/impl/exam/writing/__init__.py @@ -1,3 +1,4 @@ +import random from typing import List, Dict, Optional from fastapi import UploadFile @@ -16,7 +17,8 @@ class WritingService(IWritingService): self._llm = llm self._grade = GradeWriting(llm, file_storage, ai_detector) - async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str): + async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): + diff = difficulty[0] if len(difficulty) == 1 else random.choice(difficulty) messages = [ { "role": "system", @@ -24,7 +26,7 @@ class WritingService(IWritingService): 'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}' ) }, - *get_writing_args_general(task, topic, difficulty) + *get_writing_args_general(task, topic, diff) ] llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O @@ -40,11 +42,12 @@ class WritingService(IWritingService): return { "question": self._add_newline_before_hyphen(question) if task == 1 else question, - "difficulty": difficulty, - "topic": topic + "topic": topic, + "difficulty": diff } - async def get_writing_task_academic_question(self, task: int, file: UploadFile, difficulty: str): + async def get_writing_task_academic_question(self, task: int, file: UploadFile, difficulty: List[str]): + diff = difficulty[0] if len(difficulty) == 1 else random.choice(difficulty) messages = [ { "role": "system", @@ -52,7 +55,7 @@ class WritingService(IWritingService): 'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}' ) }, - *(await get_writing_args_academic(task, file)) + *(await get_writing_args_academic(task, file, diff)) ] response = await self._llm.prediction( @@ -66,7 +69,7 @@ class WritingService(IWritingService): return { "question": self._add_newline_before_hyphen(question) if task == 1 else question, - "difficulty": difficulty, + "difficulty": diff, } async def grade_writing_task(self, task: int, question: str, answer: str, attachment: Optional[str] = None): diff --git a/ielts_be/services/impl/exam/writing/academic.py b/ielts_be/services/impl/exam/writing/academic.py index 4e8081f..80b979f 100644 --- a/ielts_be/services/impl/exam/writing/academic.py +++ b/ielts_be/services/impl/exam/writing/academic.py @@ -4,7 +4,7 @@ from typing import List, Dict from fastapi.datastructures import UploadFile -async def get_writing_args_academic(task: int, attachment: UploadFile) -> List[Dict]: +async def get_writing_args_academic(task: int, attachment: UploadFile, difficulty: str) -> List[Dict]: writing_args = { "1": { "prompt": ( @@ -16,7 +16,8 @@ async def get_writing_args_academic(task: int, attachment: UploadFile) -> List[D 'The generated prompt must:\n' '1. Clearly describe the type of visual representation in the image\n' '2. Provide a concise context for the data shown\n' - '3. End with the standard IELTS Task 1 Academic instruction:\n' + f'3. Be adequate for {difficulty} CEFR level users\n' + '4. End with the standard IELTS Task 1 Academic instruction:\n' '"Summarise the information by selecting and reporting the main features, and make comparisons where relevant."' ) }, diff --git a/ielts_be/utils/__init__.py b/ielts_be/utils/__init__.py index d0a7773..e04a809 100644 --- a/ielts_be/utils/__init__.py +++ b/ielts_be/utils/__init__.py @@ -1,7 +1,9 @@ from .handle_exception import handle_exception from .logger import suppress_loggers +from .pick_difficulty import pick_difficulty __all__ = [ "handle_exception", - "suppress_loggers" + "suppress_loggers", + "pick_difficulty" ] diff --git a/ielts_be/utils/pick_difficulty.py b/ielts_be/utils/pick_difficulty.py new file mode 100644 index 0000000..0560dda --- /dev/null +++ b/ielts_be/utils/pick_difficulty.py @@ -0,0 +1,14 @@ +import random +from typing import Optional, List + +from ielts_be.configs.constants import EducationalContent + + +def pick_difficulty(difficulty: Optional[str], difficulties: Optional[List[str]]) -> str: + if difficulty: + return difficulty + + if difficulties: + return random.choice(difficulties) + + return random.choice(EducationalContent.DIFFICULTIES)