3 Commits

Author SHA1 Message Date
Tiago Ribeiro
26ad153f7c Merged in develop (pull request #51)
Develop
2025-01-06 21:33:05 +00:00
Tiago Ribeiro
7d04b144c4 Merge branch 'develop' 2024-12-23 16:39:03 +00:00
carlos.mesquita
111108556b Merged in feature/training-content (pull request #30)
Pydantic was causing validation errors when passportID was an int

Approved-by: Tiago Ribeiro
2024-09-08 20:49:13 +00:00
29 changed files with 71 additions and 172 deletions

View File

@@ -1,5 +1,4 @@
import random import random
from typing import List
from dependency_injector.wiring import Provide, inject from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, Path, Query, UploadFile from fastapi import APIRouter, Depends, Path, Query, UploadFile
@@ -32,7 +31,7 @@ async def upload(
@inject @inject
async def generate_listening_dialog( async def generate_listening_dialog(
section: int = Path(..., ge=1, le=4), section: int = Path(..., ge=1, le=4),
difficulty: List[str] = Query(default=None), difficulty: str = Query(default=None),
topic: str = Query(default=None), topic: str = Query(default=None),
listening_controller: IListeningController = Depends(Provide[controller]) listening_controller: IListeningController = Depends(Provide[controller])
): ):

View File

@@ -59,7 +59,7 @@ async def get_speaking_task(
topic: Optional[str] = Query(None), topic: Optional[str] = Query(None),
first_topic: Optional[str] = Query(None), first_topic: Optional[str] = Query(None),
second_topic: Optional[str] = Query(None), second_topic: Optional[str] = Query(None),
difficulty: List[str] = Query(default=None), difficulty: Optional[str] = None,
speaking_controller: ISpeakingController = Depends(Provide[controller]) speaking_controller: ISpeakingController = Depends(Provide[controller])
): ):
if not second_topic: if not second_topic:
@@ -67,7 +67,8 @@ async def get_speaking_task(
else: else:
topic_or_first_topic = first_topic if first_topic else random.choice(EducationalContent.MTI_TOPICS) topic_or_first_topic = first_topic if first_topic else random.choice(EducationalContent.MTI_TOPICS)
difficulty = [random.choice(EducationalContent.DIFFICULTIES)] if not difficulty else difficulty if not difficulty:
difficulty = random.choice(random.choice(EducationalContent.DIFFICULTIES))
second_topic = second_topic if second_topic else random.choice(EducationalContent.MTI_TOPICS) 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) return await speaking_controller.get_speaking_part(task, topic_or_first_topic, second_topic, difficulty)

View File

@@ -20,10 +20,10 @@ writing_router = APIRouter()
async def generate_writing_academic( async def generate_writing_academic(
task: int = Path(..., ge=1, le=2), task: int = Path(..., ge=1, le=2),
file: UploadFile = File(...), file: UploadFile = File(...),
difficulty: List[str] = Query(default=None), difficulty: Optional[List[str]] = None,
writing_controller: IWritingController = Depends(Provide[controller]) 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) return await writing_controller.get_writing_task_academic_question(task, file, difficulty)
@@ -34,10 +34,10 @@ async def generate_writing_academic(
@inject @inject
async def generate_writing( async def generate_writing(
task: int = Path(..., ge=1, le=2), task: int = Path(..., ge=1, le=2),
difficulty: List[str] = Query(default=None), difficulty: Optional[str] = None,
topic: str = Query(default=None), topic: str = Query(default=None),
writing_controller: IWritingController = Depends(Provide[controller]) 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 topic = random.choice(EducationalContent.MTI_TOPICS) if not topic else topic
return await writing_controller.get_writing_task_general_question(task, topic, difficulty) return await writing_controller.get_writing_task_general_question(task, topic, difficulty)

View File

@@ -1,5 +1,4 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List
from fastapi import UploadFile from fastapi import UploadFile
@@ -11,7 +10,7 @@ class IListeningController(ABC):
pass pass
@abstractmethod @abstractmethod
async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: List[str]): async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str):
pass pass
@abstractmethod @abstractmethod

View File

@@ -1,11 +1,10 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List
class ISpeakingController(ABC): class ISpeakingController(ABC):
@abstractmethod @abstractmethod
async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: List[str]): async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str):
pass pass
@abstractmethod @abstractmethod

View File

@@ -1,5 +1,4 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List
from fastapi.datastructures import UploadFile from fastapi.datastructures import UploadFile
@@ -7,9 +6,9 @@ from fastapi.datastructures import UploadFile
class IWritingController(ABC): class IWritingController(ABC):
@abstractmethod @abstractmethod
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
pass pass
@abstractmethod @abstractmethod
async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: List[str]): async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
pass pass

View File

@@ -1,5 +1,4 @@
import io import io
from typing import List
from fastapi import UploadFile from fastapi import UploadFile
from fastapi.responses import StreamingResponse, Response from fastapi.responses import StreamingResponse, Response
@@ -21,7 +20,7 @@ class ListeningController(IListeningController):
else: else:
return res return res
async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: List[str]): async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str):
return await self._service.generate_listening_dialog(section_id, topic, difficulty) return await self._service.generate_listening_dialog(section_id, topic, difficulty)
async def get_listening_question(self, dto: ListeningExercisesDTO): async def get_listening_question(self, dto: ListeningExercisesDTO):

View File

@@ -1,5 +1,4 @@
import logging import logging
from typing import List
from ielts_be.controllers import ISpeakingController from ielts_be.controllers import ISpeakingController
from ielts_be.services import ISpeakingService, IVideoGeneratorService from ielts_be.services import ISpeakingService, IVideoGeneratorService
@@ -12,7 +11,7 @@ class SpeakingController(ISpeakingController):
self._vid_gen = vid_gen self._vid_gen = vid_gen
self._logger = logging.getLogger(__name__) self._logger = logging.getLogger(__name__)
async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: List[str]): async def get_speaking_part(self, task: int, topic: str, second_topic: str, difficulty: str):
return await self._service.get_speaking_part(task, topic, second_topic, difficulty) return await self._service.get_speaking_part(task, topic, second_topic, difficulty)
async def get_avatars(self): async def get_avatars(self):

View File

@@ -1,5 +1,3 @@
from typing import List
from fastapi import UploadFile, HTTPException from fastapi import UploadFile, HTTPException
from ielts_be.controllers import IWritingController from ielts_be.controllers import IWritingController
@@ -11,10 +9,10 @@ class WritingController(IWritingController):
def __init__(self, writing_service: IWritingService): def __init__(self, writing_service: IWritingService):
self._service = writing_service self._service = writing_service
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
return await self._service.get_writing_task_general_question(task, topic, difficulty) 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: List[str]): async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
if attachment.content_type not in ['image/jpeg', 'image/png']: if attachment.content_type not in ['image/jpeg', 'image/png']:
raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG and PNG allowed.") 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) return await self._service.get_writing_task_academic_question(task, attachment, difficulty)

View File

@@ -12,8 +12,7 @@ class LevelExercises(BaseModel):
sa_qty: Optional[int] = None sa_qty: Optional[int] = None
mc_qty: Optional[int] = None mc_qty: Optional[int] = None
topic: Optional[str] = None topic: Optional[str] = None
difficulty: Optional[str] = None
class LevelExercisesDTO(BaseModel): class LevelExercisesDTO(BaseModel):
exercises: List[LevelExercises] exercises: List[LevelExercises]
difficulty: Optional[List[str]] = None difficulty: Optional[str] = None

View File

@@ -17,12 +17,11 @@ class SaveListeningDTO(BaseModel):
class ListeningExercises(BaseModel): class ListeningExercises(BaseModel):
type: ListeningExerciseType type: ListeningExerciseType
quantity: int quantity: int
difficulty: Optional[str] = None
class ListeningExercisesDTO(BaseModel): class ListeningExercisesDTO(BaseModel):
text: str text: str
exercises: List[ListeningExercises] exercises: List[ListeningExercises]
difficulty: Optional[List[str]] = None difficulty: Optional[str]
class InstructionsDTO(BaseModel): class InstructionsDTO(BaseModel):
text: str text: str

View File

@@ -10,9 +10,8 @@ class ReadingExercise(BaseModel):
quantity: int quantity: int
num_random_words: Optional[int] = Field(1) num_random_words: Optional[int] = Field(1)
max_words: Optional[int] = Field(3) max_words: Optional[int] = Field(3)
difficulty: Optional[str] = None
class ReadingDTO(BaseModel): class ReadingDTO(BaseModel):
text: str = Field(...) text: str = Field(...)
exercises: List[ReadingExercise] = Field(...) exercises: List[ReadingExercise] = Field(...)
difficulty: Optional[List[str]] = None difficulty: Optional[str] = None

View File

@@ -2,12 +2,10 @@ from .file import FileHelper
from .text import TextHelper from .text import TextHelper
from .token_counter import count_tokens from .token_counter import count_tokens
from .exercises import ExercisesHelper from .exercises import ExercisesHelper
from .difficulty import DifficultyHelper
__all__ = [ __all__ = [
"FileHelper", "FileHelper",
"TextHelper", "TextHelper",
"count_tokens", "count_tokens",
"ExercisesHelper", "ExercisesHelper",
"DifficultyHelper"
] ]

View File

@@ -1,40 +0,0 @@
import math
import random
from typing import Optional, List, Iterator
from ielts_be.configs.constants import EducationalContent
class DifficultyHelper:
def __init__(self, difficulties: Optional[List[str]]):
self.difficulties = difficulties
self.distributed: Optional[Iterator[str]] = None
def distribute_for_count(self, count: int) -> None:
if not self.difficulties or count == 0:
return
result = []
remaining = count
difficulties_count = len(self.difficulties)
for i, diff in enumerate(self.difficulties):
if i == difficulties_count - 1:
slots = remaining
else:
slots = math.ceil(remaining / (difficulties_count - i))
result.extend([diff] * slots)
remaining -= slots
self.distributed = iter(result)
def pick_difficulty(self, difficulty: Optional[str]) -> str:
if difficulty:
return difficulty if difficulty != "Random" else random.choice(EducationalContent.DIFFICULTIES)
if self.distributed:
return next(self.distributed)
return random.choice(EducationalContent.DIFFICULTIES)

View File

@@ -9,7 +9,7 @@ from fastapi import UploadFile
class IListeningService(ABC): class IListeningService(ABC):
@abstractmethod @abstractmethod
async def generate_listening_dialog( self, section_id: int, topic: str, difficulty: List[str]): async def generate_listening_dialog( self, section_id: int, topic: str, difficulty: str):
pass pass
@abstractmethod @abstractmethod

View File

@@ -6,7 +6,7 @@ class ISpeakingService(ABC):
@abstractmethod @abstractmethod
async def get_speaking_part( async def get_speaking_part(
self, part: int, topic: str, second_topic: str, difficulty: List[str] self, part: int, topic: str, second_topic: str, difficulty: str
) -> Dict: ) -> Dict:
pass pass

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional, List from typing import Optional
from fastapi import UploadFile from fastapi import UploadFile
@@ -7,11 +7,11 @@ from fastapi import UploadFile
class IWritingService(ABC): class IWritingService(ABC):
@abstractmethod @abstractmethod
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
pass pass
@abstractmethod @abstractmethod
async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: List[str]): async def get_writing_task_academic_question(self, task: int, attachment: UploadFile, difficulty: str):
pass pass
@abstractmethod @abstractmethod

View File

@@ -8,7 +8,6 @@ import random
from ielts_be.configs.constants import EducationalContent from ielts_be.configs.constants import EducationalContent
from ielts_be.dtos.level import LevelExercisesDTO from ielts_be.dtos.level import LevelExercisesDTO
from ielts_be.helpers import DifficultyHelper
from ielts_be.repositories import IDocumentStore from ielts_be.repositories import IDocumentStore
from ielts_be.services import ( from ielts_be.services import (
ILevelService, ILLMService, IReadingService, ILevelService, ILLMService, IReadingService,
@@ -51,21 +50,19 @@ class LevelService(ILevelService):
async def upload_level(self, upload: UploadFile, solutions: Optional[UploadFile] = None) -> Dict: async def upload_level(self, upload: UploadFile, solutions: Optional[UploadFile] = None) -> Dict:
return await self._upload_module.generate_level_from_file(upload, solutions) return await self._upload_module.generate_level_from_file(upload, solutions)
async def _generate_exercise(self, req_exercise, start_id, difficulty): async def _generate_exercise(self, req_exercise, start_id):
if req_exercise.type == "mcBlank": if req_exercise.type == "mcBlank":
questions = await self._mc.gen_multiple_choice("blank_space", req_exercise.quantity, difficulty, start_id) questions = await self._mc.gen_multiple_choice("blank_space", req_exercise.quantity, start_id)
questions["variant"] = "mcBlank" questions["variant"] = "mcBlank"
questions["type"] = "multipleChoice" questions["type"] = "multipleChoice"
questions["prompt"] = "Choose the correct word or group of words that completes the sentences." questions["prompt"] = "Choose the correct word or group of words that completes the sentences."
questions["difficulty"] = difficulty
return questions return questions
elif req_exercise.type == "mcUnderline": elif req_exercise.type == "mcUnderline":
questions = await self._mc.gen_multiple_choice("underline", req_exercise.quantity, difficulty, start_id) questions = await self._mc.gen_multiple_choice("underline", req_exercise.quantity, start_id)
questions["variant"] = "mcUnderline" questions["variant"] = "mcUnderline"
questions["type"] = "multipleChoice" questions["type"] = "multipleChoice"
questions["prompt"] = "Choose the underlined word or group of words that is not correct." questions["prompt"] = "Choose the underlined word or group of words that is not correct."
questions["difficulty"] = difficulty
return questions return questions
elif req_exercise.type == "passageUtas": elif req_exercise.type == "passageUtas":
@@ -73,42 +70,34 @@ class LevelService(ILevelService):
exercise = await self._passage_utas.gen_reading_passage_utas( exercise = await self._passage_utas.gen_reading_passage_utas(
start_id, start_id,
req_exercise.quantity, req_exercise.quantity,
difficulty,
topic, topic,
req_exercise.text_size req_exercise.text_size
) )
exercise["prompt"] = "Read the text and answer the questions below." exercise["prompt"] = "Read the text and answer the questions below."
exercise["difficulty"] = difficulty
return exercise return exercise
elif req_exercise.type == "fillBlanksMC": elif req_exercise.type == "fillBlanksMC":
exercise = await self._fill_blanks.gen_fill_blanks( exercise = await self._fill_blanks.gen_fill_blanks(
start_id, start_id,
req_exercise.quantity, req_exercise.quantity,
difficulty,
req_exercise.text_size, req_exercise.text_size,
req_exercise.topic req_exercise.topic
) )
exercise["prompt"] = "Read the text below and choose the correct word for each space." exercise["prompt"] = "Read the text below and choose the correct word for each space."
exercise["difficulty"] = difficulty
return exercise return exercise
async def generate_exercises(self, dto: LevelExercisesDTO): async def generate_exercises(self, dto: LevelExercisesDTO):
start_ids = []
current_id = 1 current_id = 1
tasks = []
distributor = DifficultyHelper(dto.difficulty)
none_count = sum(1 for ex in dto.exercises if ex.difficulty is None)
distributor.distribute_for_count(none_count)
for req_exercise in dto.exercises: for req_exercise in dto.exercises:
difficulty = distributor.pick_difficulty(req_exercise.difficulty) start_ids.append(current_id)
tasks.append(
self._generate_exercise(req_exercise, current_id, difficulty)
)
current_id += req_exercise.quantity current_id += req_exercise.quantity
tasks = [
self._generate_exercise(req_exercise, start_id)
for req_exercise, start_id in zip(dto.exercises, start_ids)
]
questions = await gather(*tasks) questions = await gather(*tasks)
questions = [{'id': str(uuid4()), **exercise} for exercise in questions] questions = [{'id': str(uuid4()), **exercise} for exercise in questions]
@@ -116,12 +105,10 @@ class LevelService(ILevelService):
# Just here to support other modules that I don't know if they are supposed to still be used # 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): async def gen_multiple_choice(self, mc_variant: str, quantity: int, start_id: int = 1):
difficulty = random.choice(EducationalContent.DIFFICULTIES) return await self._mc.gen_multiple_choice(mc_variant, quantity, start_id)
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, async def gen_reading_passage_utas(self, start_id, mc_quantity: int, topic=Optional[str]): # sa_quantity: int,
difficulty = random.choice(EducationalContent.DIFFICULTIES) return await self._passage_utas.gen_reading_passage_utas(start_id, mc_quantity, topic)
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): 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) return await self._blank_space.gen_blank_space_text_utas(quantity, start_id, size, topic)

View File

@@ -11,10 +11,12 @@ class FillBlanks:
async def gen_fill_blanks( async def gen_fill_blanks(
self, start_id: int, quantity: int, difficulty: str, size: int = 300, topic=None self, start_id: int, quantity: int, size: int = 300, topic=None
): ):
if not topic: if not topic:
topic = random.choice(EducationalContent.MTI_TOPICS) topic = random.choice(EducationalContent.MTI_TOPICS)
print(quantity)
print(start_id)
messages = [ messages = [
{ {
"role": "system", "role": "system",
@@ -32,15 +34,8 @@ class FillBlanks:
'JSON object containing: the modified text, a solutions array with each word\'s correct ' '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 ' '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 ' 'the original word (matching the solution) and three are plausible but incorrect '
f'alternatives that maintain grammatical consistency and {difficulty} CEFR level complexity. ' 'alternatives that maintain grammatical consistency. '
'You cannot use repeated words!' 'You cannot use repeated words!' #TODO: Solve this after
# 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
) )
} }
] ]

View File

@@ -10,16 +10,16 @@ class MultipleChoice:
self._mc_variants = mc_variants self._mc_variants = mc_variants
async def gen_multiple_choice( async def gen_multiple_choice(
self, mc_variant: str, quantity: int, difficulty: str, start_id: int = 1 self, mc_variant: str, quantity: int, start_id: int = 1
): ):
mc_template = self._mc_variants[mc_variant] mc_template = self._mc_variants[mc_variant]
blank_mod = " blank space " if mc_variant == "blank_space" else " " blank_mod = " blank space " if mc_variant == "blank_space" else " "
gen_multiple_choice_for_text: str = ( gen_multiple_choice_for_text: str = (
'Generate {quantity} multiple choice{blank}questions of 4 options for an english level exam of {difficulty} ' 'Generate {quantity} multiple choice{blank}questions of 4 options for an english level exam, some easy '
'CEFR level, some easy questions, some intermediate questions and some advanced questions. Ensure that ' 'questions, some intermediate questions and some advanced questions. Ensure that the questions cover '
'the questions cover a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence ' 'a range of topics such as verb tense, subject-verb agreement, pronoun usage, sentence structure, and '
'structure, and punctuation. Make sure every question only has 1 correct answer.' 'punctuation. Make sure every question only has 1 correct answer.'
) )
messages = [ messages = [
@@ -31,7 +31,7 @@ class MultipleChoice:
}, },
{ {
"role": "user", "role": "user",
"content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod, difficulty=difficulty) "content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod)
} }
] ]

View File

@@ -13,11 +13,11 @@ class PassageUtas:
self._mc_variants = mc_variants self._mc_variants = mc_variants
async def gen_reading_passage_utas( async def gen_reading_passage_utas(
self, start_id, mc_quantity: int, difficulty: str, topic: Optional[str] = None, word_size: Optional[int] = None# sa_quantity: int, self, start_id, mc_quantity: int, topic: Optional[str], word_size: Optional[int] # sa_quantity: int,
): ):
passage = await self._reading_service.generate_reading_passage(1, topic, word_size) 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, difficulty) mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id, mc_quantity)
mc_exercises["type"] = "multipleChoice" mc_exercises["type"] = "multipleChoice"
""" """
exercises: { exercises: {
@@ -61,7 +61,7 @@ class PassageUtas:
return question["questions"] return question["questions"]
async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int, difficulty: str): async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int):
json_template = self._mc_variants["text_mc_utas"] json_template = self._mc_variants["text_mc_utas"]
messages = [ messages = [
@@ -71,9 +71,7 @@ class PassageUtas:
}, },
{ {
"role": "user", "role": "user",
"content": ( "content": f'Generate {mc_quantity} multiple choice questions of 4 options for this text:\n{text}'
f'Generate {mc_quantity} multiple choice questions of 4 options, {difficulty} CEFR '
f'level difficulty, for this text:\n{text}')
}, },
{ {
"role": "user", "role": "user",

View File

@@ -1,8 +1,6 @@
import json import json
import random
import uuid import uuid
from ielts_be.configs.constants import EducationalContent
from ielts_be.services import ILLMService from ielts_be.services import ILLMService

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
from logging import getLogger from logging import getLogger
import random import random
from typing import Dict, Any, Union, List from typing import Dict, Any, Union
from starlette.datastructures import UploadFile from starlette.datastructures import UploadFile
@@ -13,7 +13,7 @@ from ielts_be.configs.constants import (
NeuralVoices, GPTModels, TemperatureSettings, EducationalContent, NeuralVoices, GPTModels, TemperatureSettings, EducationalContent,
FieldsAndExercises FieldsAndExercises
) )
from ielts_be.helpers import FileHelper, DifficultyHelper from ielts_be.helpers import FileHelper
from .audio_to_dialog import AudioToDialog from .audio_to_dialog import AudioToDialog
from .import_listening import ImportListeningModule from .import_listening import ImportListeningModule
from .write_blank_forms import WriteBlankForms from .write_blank_forms import WriteBlankForms
@@ -21,6 +21,7 @@ from .write_blanks import WriteBlanks
from .write_blank_notes import WriteBlankNotes from .write_blank_notes import WriteBlankNotes
from ..shared import TrueFalse, MultipleChoice from ..shared import TrueFalse, MultipleChoice
class ListeningService(IListeningService): class ListeningService(IListeningService):
CONVERSATION_TAIL = ( CONVERSATION_TAIL = (
@@ -93,8 +94,7 @@ class ListeningService(IListeningService):
return await self._import.import_from_file(exercises, solutions) return await self._import.import_from_file(exercises, solutions)
async def generate_listening_dialog(self, section: int, topic: str, difficulty: List[str]): async def generate_listening_dialog(self, section: int, topic: str, difficulty: str):
# TODO: difficulties to difficulty
return await self._sections[f'section_{section}']["generate_dialogue"](section, topic) return await self._sections[f'section_{section}']["generate_dialogue"](section, topic)
async def transcribe_dialog(self, audio: UploadFile): async def transcribe_dialog(self, audio: UploadFile):
@@ -135,11 +135,6 @@ class ListeningService(IListeningService):
start_id = 1 start_id = 1
exercise_tasks = [] exercise_tasks = []
diff_helper = DifficultyHelper(dto.difficulty)
none_count = sum(1 for ex in dto.exercises if ex.difficulty is None)
diff_helper.distribute_for_count(none_count)
for req_exercise in dto.exercises: for req_exercise in dto.exercises:
exercise_tasks.append( exercise_tasks.append(
self._generate_exercise( self._generate_exercise(
@@ -147,7 +142,7 @@ class ListeningService(IListeningService):
"dialog or monologue", "dialog or monologue",
dto.text, dto.text,
start_id, start_id,
diff_helper.pick_difficulty(req_exercise.difficulty) dto.difficulty
) )
) )
start_id += req_exercise.quantity start_id += req_exercise.quantity
@@ -162,7 +157,6 @@ class ListeningService(IListeningService):
question = await self._multiple_choice.gen_multiple_choice( question = await self._multiple_choice.gen_multiple_choice(
text, req_exercise.quantity, start_id, difficulty, n_options text, req_exercise.quantity, start_id, difficulty, n_options
) )
question["difficulty"] = difficulty
self._logger.info(f"Added multiple choice: {question}") self._logger.info(f"Added multiple choice: {question}")
return question return question
@@ -171,7 +165,6 @@ class ListeningService(IListeningService):
dialog_type, text, req_exercise.quantity, start_id, difficulty dialog_type, text, req_exercise.quantity, start_id, difficulty
) )
question["variant"] = "questions" question["variant"] = "questions"
question["difficulty"] = difficulty
self._logger.info(f"Added write blanks questions: {question}") self._logger.info(f"Added write blanks questions: {question}")
return question return question
@@ -180,7 +173,6 @@ class ListeningService(IListeningService):
dialog_type, text, req_exercise.quantity, start_id, difficulty dialog_type, text, req_exercise.quantity, start_id, difficulty
) )
question["variant"] = "fill" question["variant"] = "fill"
question["difficulty"] = difficulty
self._logger.info(f"Added write blanks notes: {question}") self._logger.info(f"Added write blanks notes: {question}")
return question return question
@@ -189,14 +181,12 @@ class ListeningService(IListeningService):
dialog_type, text, req_exercise.quantity, start_id, difficulty dialog_type, text, req_exercise.quantity, start_id, difficulty
) )
question["variant"] = "form" question["variant"] = "form"
question["difficulty"] = difficulty
self._logger.info(f"Added write blanks form: {question}") self._logger.info(f"Added write blanks form: {question}")
return question return question
elif req_exercise.type == "trueFalse": elif req_exercise.type == "trueFalse":
question = await self._true_false.gen_true_false_not_given_exercise( question = await self._true_false.gen_true_false_not_given_exercise(
text, req_exercise.quantity, start_id, difficulty, "listening" text, req_exercise.quantity, start_id, difficulty, "listening"
) )
question["difficulty"] = difficulty
self._logger.info(f"Added trueFalse: {question}") self._logger.info(f"Added trueFalse: {question}")
return question return question

View File

@@ -5,7 +5,7 @@ from fastapi import UploadFile
from ielts_be.configs.constants import GPTModels, FieldsAndExercises, TemperatureSettings from ielts_be.configs.constants import GPTModels, FieldsAndExercises, TemperatureSettings
from ielts_be.dtos.reading import ReadingDTO from ielts_be.dtos.reading import ReadingDTO
from ielts_be.helpers import ExercisesHelper, DifficultyHelper from ielts_be.helpers import ExercisesHelper
from ielts_be.services import IReadingService, ILLMService from ielts_be.services import IReadingService, ILLMService
from .fill_blanks import FillBlanks from .fill_blanks import FillBlanks
from .idea_match import IdeaMatch from .idea_match import IdeaMatch
@@ -84,7 +84,6 @@ class ReadingService(IReadingService):
question = await self._fill_blanks.gen_summary_fill_blanks_exercise( question = await self._fill_blanks.gen_summary_fill_blanks_exercise(
text, req_exercise.quantity, start_id, difficulty, req_exercise.num_random_words text, req_exercise.quantity, start_id, difficulty, req_exercise.num_random_words
) )
question["difficulty"] = difficulty
self._logger.info(f"Added fill blanks: {question}") self._logger.info(f"Added fill blanks: {question}")
return question return question
@@ -92,7 +91,6 @@ class ReadingService(IReadingService):
question = await self._true_false.gen_true_false_not_given_exercise( question = await self._true_false.gen_true_false_not_given_exercise(
text, req_exercise.quantity, start_id, difficulty, "reading" text, req_exercise.quantity, start_id, difficulty, "reading"
) )
question["difficulty"] = difficulty
self._logger.info(f"Added trueFalse: {question}") self._logger.info(f"Added trueFalse: {question}")
return question return question
@@ -102,7 +100,6 @@ class ReadingService(IReadingService):
) )
if ExercisesHelper.answer_word_limit_ok(question): if ExercisesHelper.answer_word_limit_ok(question):
question["difficulty"] = difficulty
self._logger.info(f"Added write blanks: {question}") self._logger.info(f"Added write blanks: {question}")
return question return question
else: else:
@@ -113,7 +110,6 @@ class ReadingService(IReadingService):
question = await self._paragraph_match.gen_paragraph_match_exercise( question = await self._paragraph_match.gen_paragraph_match_exercise(
text, req_exercise.quantity, start_id text, req_exercise.quantity, start_id
) )
question["difficulty"] = difficulty
self._logger.info(f"Added paragraph match: {question}") self._logger.info(f"Added paragraph match: {question}")
return question return question
@@ -122,14 +118,12 @@ class ReadingService(IReadingService):
text, req_exercise.quantity, start_id text, req_exercise.quantity, start_id
) )
question["variant"] = "ideaMatch" question["variant"] = "ideaMatch"
question["difficulty"] = difficulty
self._logger.info(f"Added idea match: {question}") self._logger.info(f"Added idea match: {question}")
return question return question
elif req_exercise.type == "multipleChoice": elif req_exercise.type == "multipleChoice":
question = await self._multiple_choice.gen_multiple_choice( question = await self._multiple_choice.gen_multiple_choice(
text, req_exercise.quantity, start_id, difficulty, 4 text, req_exercise.quantity, start_id, difficulty, 4
) )
question["difficulty"] = difficulty
self._logger.info(f"Added multiple choice: {question}") self._logger.info(f"Added multiple choice: {question}")
return question return question
@@ -137,18 +131,13 @@ class ReadingService(IReadingService):
exercise_tasks = [] exercise_tasks = []
start_id = 1 start_id = 1
diff_helper = DifficultyHelper(dto.difficulty)
none_count = sum(1 for ex in dto.exercises if ex.difficulty is None)
diff_helper.distribute_for_count(none_count)
for req_exercise in dto.exercises: for req_exercise in dto.exercises:
exercise_tasks.append( exercise_tasks.append(
self._generate_single_exercise( self._generate_single_exercise(
req_exercise, req_exercise,
dto.text, dto.text,
start_id, start_id,
diff_helper.pick_difficulty(req_exercise.difficulty) dto.difficulty
) )
) )
start_id += req_exercise.quantity start_id += req_exercise.quantity

View File

@@ -1,6 +1,5 @@
import logging import logging
import re import re
import random
from typing import Dict, List from typing import Dict, List
@@ -100,9 +99,8 @@ class SpeakingService(ISpeakingService):
} }
async def get_speaking_part( async def get_speaking_part(
self, part: int, topic: str, second_topic: str, difficulty: List[str] self, part: int, topic: str, second_topic: str, difficulty: str
) -> Dict: ) -> Dict:
diff = difficulty[0] if len(difficulty) == 1 else random.choice(difficulty)
task_values = self._tasks[f'task_{part}']['get'] task_values = self._tasks[f'task_{part}']['get']
if part == 1: if part == 1:
@@ -159,7 +157,7 @@ class SpeakingService(ISpeakingService):
] ]
response["type"] = part response["type"] = part
response["difficulty"] = diff response["difficulty"] = difficulty
if part in {2, 3}: if part in {2, 3}:
response["topic"] = topic response["topic"] = topic

View File

@@ -1,4 +1,3 @@
import random
from typing import List, Dict, Optional from typing import List, Dict, Optional
from fastapi import UploadFile from fastapi import UploadFile
@@ -17,8 +16,7 @@ class WritingService(IWritingService):
self._llm = llm self._llm = llm
self._grade = GradeWriting(llm, file_storage, ai_detector) self._grade = GradeWriting(llm, file_storage, ai_detector)
async def get_writing_task_general_question(self, task: int, topic: str, difficulty: List[str]): async def get_writing_task_general_question(self, task: int, topic: str, difficulty: str):
diff = difficulty[0] if len(difficulty) == 1 else random.choice(difficulty)
messages = [ messages = [
{ {
"role": "system", "role": "system",
@@ -26,7 +24,7 @@ class WritingService(IWritingService):
'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}' 'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}'
) )
}, },
*get_writing_args_general(task, topic, diff) *get_writing_args_general(task, topic, difficulty)
] ]
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
@@ -42,12 +40,11 @@ class WritingService(IWritingService):
return { return {
"question": self._add_newline_before_hyphen(question) if task == 1 else question, "question": self._add_newline_before_hyphen(question) if task == 1 else question,
"topic": topic, "difficulty": difficulty,
"difficulty": diff "topic": topic
} }
async def get_writing_task_academic_question(self, task: int, file: UploadFile, difficulty: List[str]): async def get_writing_task_academic_question(self, task: int, file: UploadFile, difficulty: str):
diff = difficulty[0] if len(difficulty) == 1 else random.choice(difficulty)
messages = [ messages = [
{ {
"role": "system", "role": "system",
@@ -55,7 +52,7 @@ class WritingService(IWritingService):
'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}' 'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}'
) )
}, },
*(await get_writing_args_academic(task, file, diff)) *(await get_writing_args_academic(task, file))
] ]
response = await self._llm.prediction( response = await self._llm.prediction(
@@ -69,7 +66,7 @@ class WritingService(IWritingService):
return { return {
"question": self._add_newline_before_hyphen(question) if task == 1 else question, "question": self._add_newline_before_hyphen(question) if task == 1 else question,
"difficulty": diff, "difficulty": difficulty,
} }
async def grade_writing_task(self, task: int, question: str, answer: str, attachment: Optional[str] = None): async def grade_writing_task(self, task: int, question: str, answer: str, attachment: Optional[str] = None):

View File

@@ -4,7 +4,7 @@ from typing import List, Dict
from fastapi.datastructures import UploadFile from fastapi.datastructures import UploadFile
async def get_writing_args_academic(task: int, attachment: UploadFile, difficulty: str) -> List[Dict]: async def get_writing_args_academic(task: int, attachment: UploadFile) -> List[Dict]:
writing_args = { writing_args = {
"1": { "1": {
"prompt": ( "prompt": (
@@ -16,8 +16,7 @@ async def get_writing_args_academic(task: int, attachment: UploadFile, difficult
'The generated prompt must:\n' 'The generated prompt must:\n'
'1. Clearly describe the type of visual representation in the image\n' '1. Clearly describe the type of visual representation in the image\n'
'2. Provide a concise context for the data shown\n' '2. Provide a concise context for the data shown\n'
f'3. Be adequate for {difficulty} CEFR level users\n' '3. End with the standard IELTS Task 1 Academic instruction:\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."' '"Summarise the information by selecting and reporting the main features, and make comparisons where relevant."'
) )
}, },

View File

@@ -16,8 +16,8 @@ class TrainingContentKnowledgeBase(IKnowledgeBase):
self._tips = None # self._read_json(path) self._tips = None # self._read_json(path)
self._category_metadata = None self._category_metadata = None
self._indices = None self._indices = None
self._logger = getLogger(__name__)
self.load_indices_and_metadata() self.load_indices_and_metadata()
self._logger = getLogger(__name__)
@staticmethod @staticmethod
def _read_json(path: str) -> Dict[str, any]: def _read_json(path: str) -> Dict[str, any]:

View File

@@ -3,5 +3,5 @@ from .logger import suppress_loggers
__all__ = [ __all__ = [
"handle_exception", "handle_exception",
"suppress_loggers", "suppress_loggers"
] ]