Changes to endpoints so they allow to only get context and then the exercises as well as tidying up a bit

This commit is contained in:
Carlos-Mesquita
2024-11-04 23:31:48 +00:00
parent 2a032c5aba
commit 84ed2f2f6a
83 changed files with 4229 additions and 1843 deletions

7
.env
View File

@@ -1,10 +1,13 @@
OPENAI_API_KEY=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
OPENAI_API_KEY=sk-proj-Ol2dgrsNwaUcEgOSefsST3BlbkFJLD2HeO7PuoYsJzZzgqtF
JWT_SECRET_KEY=6e9c124ba92e8814719dcb0f21200c8aa4d0f119a994ac5e06eb90a366c83ab2
JWT_TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0In0.Emrs2D3BmMP4b3zMjw0fJTPeyMwWEBDbxx2vvaWguO0
HEY_GEN_TOKEN=MjY4MDE0MjdjZmNhNDFmYTlhZGRkNmI3MGFlMzYwZDItMTY5NTExNzY3MA==
#HEY_GEN_TOKEN=MjY4MDE0MjdjZmNhNDFmYTlhZGRkNmI3MGFlMzYwZDItMTY5NTExNzY3MA==
GPT_ZERO_API_KEY=0195b9bb24c5439899f71230809c74af
MONGODB_URI=mongodb+srv://user:JKpFBymv0WLv3STj@encoach.lz18a.mongodb.net/?retryWrites=true&w=majority&appName=EnCoach
GOOGLE_APPLICATION_CREDENTIALS=firebase-configs/encoach-staging.json
ELAI_TOKEN=KtzxETdcZesZtwl7JKiYQapRvp0b4zMG
AWS_ACCESS_KEY_ID=AKIAWMFUPM6VZJ5MFFXK
AWS_SECRET_ACCESS_KEY=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
# Staging
ENV=staging

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from .home import home_router
from .listening import listening_router
from .reading import reading_router
from .speaking import speaking_router
@@ -8,13 +7,22 @@ from .training import training_router
from .writing import writing_router
from .grade import grade_router
from .user import user_router
from .level import level_router
router = APIRouter()
router.include_router(home_router, prefix="/api", tags=["Home"])
router.include_router(listening_router, prefix="/api/listening", tags=["Listening"])
router.include_router(reading_router, prefix="/api/reading", tags=["Reading"])
router.include_router(speaking_router, prefix="/api/speaking", tags=["Speaking"])
router.include_router(writing_router, prefix="/api/writing", tags=["Writing"])
router.include_router(grade_router, prefix="/api/grade", tags=["Grade"])
router.include_router(training_router, prefix="/api/training", tags=["Training"])
router.include_router(user_router, prefix="/api/user", tags=["Users"])
router = APIRouter(prefix="/api", tags=["Home"])
@router.get('/healthcheck')
async def healthcheck():
return {"healthy": True}
exercises_router = APIRouter()
exercises_router.include_router(listening_router, prefix="/listening", tags=["Listening"])
exercises_router.include_router(reading_router, prefix="/reading", tags=["Reading"])
exercises_router.include_router(speaking_router, prefix="/speaking", tags=["Speaking"])
exercises_router.include_router(writing_router, prefix="/writing", tags=["Writing"])
exercises_router.include_router(level_router, prefix="/level", tags=["Level"])
router.include_router(grade_router, prefix="/grade", tags=["Grade"])
router.include_router(training_router, prefix="/training", tags=["Training"])
router.include_router(user_router, prefix="/user", tags=["Users"])
router.include_router(exercises_router)

View File

@@ -1,6 +1,7 @@
from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, UploadFile, Request
from app.dtos.level import LevelExercisesDTO
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
from app.controllers.abc import ILevelController
@@ -8,6 +9,17 @@ controller = "level_controller"
level_router = APIRouter()
@level_router.post(
'/',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def generate_exercises(
dto: LevelExercisesDTO,
level_controller: ILevelController = Depends(Provide[controller])
):
return await level_controller.generate_exercises(dto)
@level_router.get(
'/',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
@@ -31,7 +43,7 @@ async def get_level_utas(
@level_router.post(
'/upload',
'/import/',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
@@ -43,7 +55,7 @@ async def upload(
@level_router.post(
'/custom',
'/custom/',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject

View File

@@ -1,31 +1,42 @@
import random
from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, Path
from fastapi import APIRouter, Depends, Path, Query
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
from app.controllers.abc import IListeningController
from app.configs.constants import EducationalContent
from app.dtos.listening import SaveListeningDTO
from app.configs.constants import EducationalContent, ListeningExerciseType
from app.dtos.listening import SaveListeningDTO, GenerateListeningExercises
controller = "listening_controller"
listening_router = APIRouter()
@listening_router.get(
'/section/{section}',
'/{section}',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def get_listening_question(
exercises: list[str],
async def generate_listening_dialog(
section: int = Path(..., ge=1, le=4),
topic: str | None = None,
difficulty: str = random.choice(EducationalContent.DIFFICULTIES),
difficulty: str = Query(default=None),
topic: str = Query(default=None),
listening_controller: IListeningController = Depends(Provide[controller])
):
return await listening_controller.get_listening_question(section, topic, exercises, difficulty)
difficulty = random.choice(EducationalContent.DIFFICULTIES) if not difficulty else difficulty
topic = random.choice(EducationalContent.TOPICS) if not topic else topic
return await listening_controller.generate_listening_dialog(section, difficulty, topic)
@listening_router.post(
'/{section}',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def generate_listening_exercise(
dto: GenerateListeningExercises,
section: int = Path(..., ge=1, le=4),
listening_controller: IListeningController = Depends(Provide[controller])
):
return await listening_controller.get_listening_question(section, dto)
@listening_router.post(

View File

@@ -1,28 +1,51 @@
import random
from typing import Optional
from dependency_injector.wiring import Provide, inject
from fastapi import APIRouter, Depends, Path, Query
from fastapi import APIRouter, Depends, Path, Query, UploadFile
from app.dtos.reading import ReadingDTO
from app.middlewares import Authorized, IsAuthenticatedViaBearerToken
from app.configs.constants import EducationalContent
from app.controllers.abc import IReadingController
controller = "reading_controller"
reading_router = APIRouter()
@reading_router.get(
'/passage/{passage}',
@reading_router.post(
'/import',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def get_reading_passage(
passage: int = Path(..., ge=1, le=3),
topic: str = Query(default=random.choice(EducationalContent.TOPICS)),
exercises: list[str] = Query(default=[]),
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
async def upload(
exercises: UploadFile,
solutions: UploadFile = None,
reading_controller: IReadingController = Depends(Provide[controller])
):
return await reading_controller.get_reading_passage(passage, topic, exercises, difficulty)
print(exercises.filename)
#print(solutions.filename)
return await reading_controller.import_exam(exercises, solutions)
@reading_router.get(
'/{passage}',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def generate_passage(
topic: Optional[str] = Query(None),
word_count: Optional[int] = Query(None),
passage: int = Path(..., ge=1, le=3),
reading_controller: IReadingController = Depends(Provide[controller])
):
return await reading_controller.generate_reading_passage(passage, topic, word_count)
@reading_router.post(
'/{passage}',
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def generate_reading(
dto: ReadingDTO,
passage: int = Path(..., ge=1, le=3),
reading_controller: IReadingController = Depends(Provide[controller])
):
return await reading_controller.generate_reading_exercises(passage, dto)

View File

@@ -1,4 +1,5 @@
import random
from typing import Optional
from dependency_injector.wiring import inject, Provide
from fastapi import APIRouter, Path, Query, Depends, BackgroundTasks

View File

@@ -16,10 +16,12 @@ writing_router = APIRouter()
dependencies=[Depends(Authorized([IsAuthenticatedViaBearerToken]))]
)
@inject
async def get_writing_task_general_question(
async def generate_writing(
task: int = Path(..., ge=1, le=2),
topic: str = Query(default=random.choice(EducationalContent.MTI_TOPICS)),
difficulty: str = Query(default=random.choice(EducationalContent.DIFFICULTIES)),
difficulty: 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
topic = random.choice(EducationalContent.MTI_TOPICS) if not topic else topic
return await writing_controller.get_writing_task_general_question(task, topic, difficulty)

View File

@@ -41,6 +41,30 @@ class ExamVariant(Enum):
PARTIAL = "partial"
class ReadingExerciseType(str, Enum):
fillBlanks = "fillBlanks"
writeBlanks = "writeBlanks"
trueFalse = "trueFalse"
paragraphMatch = "paragraphMatch"
ideaMatch = "ideaMatch"
class ListeningExerciseType(str, Enum):
multipleChoice = "multipleChoice"
multipleChoice3Options = "multipleChoice3Options"
writeBlanksQuestions = "writeBlanksQuestions"
writeBlanksFill = "writeBlanksFill"
writeBlanksForm = "writeBlanksForm"
class LevelExerciseType(str, Enum):
multipleChoice = "multipleChoice"
mcBlank = "mcBlank"
mcUnderline = "mcUnderline"
blankSpace = "blankSpaceText"
passageUtas = "passageUtas"
fillBlanksMC = "fillBlanksMC"
class CustomLevelExerciseTypes(Enum):
MULTIPLE_CHOICE_4 = "multiple_choice_4"
MULTIPLE_CHOICE_BLANK_SPACE = "multiple_choice_blank_space"
@@ -75,7 +99,7 @@ class QuestionType(Enum):
READING_PASSAGE_3 = "Reading Passage 3"
class AvatarEnum(Enum):
class HeygenAvatars(Enum):
MATTHEW_NOAH = "5912afa7c77c47d3883af3d874047aaf"
VERA_CERISE = "9e58d96a383e4568a7f1e49df549e0e4"
EDWARD_TONY = "d2cdd9c0379a4d06ae2afb6e5039bd0c"
@@ -84,6 +108,77 @@ class AvatarEnum(Enum):
JEROME_RYAN = "0ee6aa7cc1084063a630ae514fccaa31"
TYLER_CHRISTOPHER = "5772cff935844516ad7eeff21f839e43"
from enum import Enum
class ELAIAvatars(Enum):
# Works
GIA_BUSINESS = {
"avatar_code": "gia.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
"avatar_canvas": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/gia/business/gia_business.png",
"voice_id": "EXAVITQu4vr4xnSDxMaL",
"voice_provider": "elevenlabs"
}
# Works
VADIM_BUSINESS = {
"avatar_code": "vadim.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/vadim/business/vadim_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/vadim/business/vadim_business.png",
"voice_id": "flq6f7yk4E4fJM5XTYuZ",
"voice_provider": "elevenlabs"
}
ORHAN_BUSINESS = {
"avatar_code": "orhan.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/orhan/business/orhan.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/orhan/business/orhan.png",
"voice_id": "en-US-AndrewMultilingualNeural",
"voice_provider": "azure"
}
FLORA_BUSINESS = {
"avatar_code": "flora.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/flora/business/flora_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/flora/business/flora_business.png",
"voice_id": "en-US-JaneNeural",
"voice_provider": "azure"
}
SCARLETT_BUSINESS = {
"avatar_code": "scarlett.business",
"avatar_gender": "female",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/scarlett/business/scarlett_business.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/scarlett/business/scarlett_business.png",
"voice_id": "en-US-NancyNeural",
"voice_provider": "azure"
}
PARKER_CASUAL = {
"avatar_code": "parker.casual",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/parker/casual/parker_casual.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/parker/casual/parker_casual.png",
"voice_id": "en-US-TonyNeural",
"voice_provider": "azure"
}
ETHAN_BUSINESS = {
"avatar_code": "ethan.business",
"avatar_gender": "male",
"avatar_url": "https://elai-avatars.s3.us-east-2.amazonaws.com/common/ethan/business/ethan_business_low.png",
"avatar_canvas": "https://d3u63mhbhkevz8.cloudfront.net/common/ethan/business/ethan_business_low.png",
"voice_id": "en-US-JasonNeural",
"voice_provider": "azure"
}
class FilePaths:
AUDIO_FILES_PATH = 'download-audio/'

View File

@@ -44,8 +44,11 @@ class DependencyInjector:
self._container.llm = providers.Factory(OpenAI, client=self._container.openai_client)
self._container.stt = providers.Factory(OpenAIWhisper, model=self._container.whisper_model)
self._container.tts = providers.Factory(AWSPolly, client=self._container.polly_client)
with open('app/services/impl/third_parties/elai/elai_conf.json', 'r') as file:
elai_conf = json.load(file)
self._container.vid_gen = providers.Factory(
Heygen, client=self._container.http_client, heygen_token=os.getenv("HEY_GEN_TOKEN")
ELAI, client=self._container.http_client, token=os.getenv("ELAI_TOKEN"), conf=elai_conf
)
self._container.ai_detector = providers.Factory(
GPTZero, client=self._container.http_client, gpt_zero_key=os.getenv("GPT_ZERO_API_KEY")
@@ -68,6 +71,7 @@ class DependencyInjector:
self._container.listening_service = providers.Factory(
ListeningService,
llm=self._container.llm,
stt=self._container.stt,
tts=self._container.tts,
file_storage=self._container.firebase_instance,
document_store=self._container.document_store

View File

@@ -15,5 +15,5 @@ __all__ = [
"ILevelController",
"IGradeController",
"ITrainingController",
"IUserController"
"IUserController",
]

View File

@@ -6,6 +6,10 @@ from typing import Dict
class ILevelController(ABC):
@abstractmethod
async def generate_exercises(self, dto):
pass
@abstractmethod
async def get_level_exam(self):
pass

View File

@@ -5,7 +5,11 @@ from typing import List
class IListeningController(ABC):
@abstractmethod
async def get_listening_question(self, section_id: int, topic: str, exercises: List[str], difficulty: str):
async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str):
pass
@abstractmethod
async def get_listening_question(self, section: int, dto):
pass
@abstractmethod

View File

@@ -1,10 +1,20 @@
from abc import ABC, abstractmethod
from typing import List
from typing import Optional
from fastapi import UploadFile
class IReadingController(ABC):
@abstractmethod
async def get_reading_passage(self, passage: int, topic: str, exercises: List[str], difficulty: str):
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
pass
@abstractmethod
async def generate_reading_passage(self, passage: int, topic: Optional[str], word_count: Optional[int]):
pass
@abstractmethod
async def generate_reading_exercises(self, passage: int, dto):
pass

View File

@@ -1,6 +1,8 @@
from fastapi import UploadFile
from typing import Dict
from watchfiles import awatch
from app.controllers.abc import ILevelController
from app.services.abc import ILevelService
@@ -10,6 +12,9 @@ class LevelController(ILevelController):
def __init__(self, level_service: ILevelService):
self._service = level_service
async def generate_exercises(self, dto):
return await self._service.generate_exercises(dto)
async def get_level_exam(self):
return await self._service.get_level_exam()

View File

@@ -1,7 +1,7 @@
from typing import List
from app.controllers.abc import IListeningController
from app.dtos.listening import SaveListeningDTO
from app.dtos.listening import SaveListeningDTO, GenerateListeningExercises
from app.services.abc import IListeningService
@@ -10,10 +10,11 @@ class ListeningController(IListeningController):
def __init__(self, listening_service: IListeningService):
self._service = listening_service
async def get_listening_question(
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str
):
return await self._service.get_listening_question(section_id, topic, req_exercises, difficulty)
async def generate_listening_dialog(self, section_id: int, topic: str, difficulty: str):
return await self._service.generate_listening_dialog(section_id, topic, difficulty)
async def get_listening_question(self, section: int, dto: GenerateListeningExercises):
return await self._service.get_listening_question(section, dto)
async def save_listening(self, data: SaveListeningDTO):
return await self._service.save_listening(data.parts, data.minTimer, data.difficulty, data.id)

View File

@@ -1,11 +1,12 @@
import random
import logging
from typing import List
from typing import Optional
from fastapi import UploadFile
from grpc import services
from app.controllers.abc import IReadingController
from app.dtos.reading import ReadingDTO
from app.services.abc import IReadingService
from app.configs.constants import FieldsAndExercises
from app.helpers import ExercisesHelper
class ReadingController(IReadingController):
@@ -13,31 +14,12 @@ class ReadingController(IReadingController):
def __init__(self, reading_service: IReadingService):
self._service = reading_service
self._logger = logging.getLogger(__name__)
self._passages = {
"passage_1": {
"start_id": 1,
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_1_EXERCISES
},
"passage_2": {
"start_id": 14,
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_2_EXERCISES
},
"passage_3": {
"start_id": 27,
"total_exercises": FieldsAndExercises.TOTAL_READING_PASSAGE_3_EXERCISES
}
}
async def get_reading_passage(self, passage_id: int, topic: str, req_exercises: List[str], difficulty: str):
passage = self._passages[f'passage_{str(passage_id)}']
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
return await self._service.import_exam(exercises, solutions)
if len(req_exercises) == 0:
req_exercises = random.sample(FieldsAndExercises.READING_EXERCISE_TYPES, 2)
async def generate_reading_passage(self, passage: int, topic: Optional[str], word_count: Optional[int]):
return await self._service.generate_reading_passage(passage, topic, word_count)
number_of_exercises_q = ExercisesHelper.divide_number_into_parts(
passage["total_exercises"], len(req_exercises)
)
return await self._service.gen_reading_passage(
passage_id, topic, req_exercises, number_of_exercises_q, difficulty, passage["start_id"]
)
async def generate_reading_exercises(self, passage: int, dto: ReadingDTO):
return await self._service.generate_reading_exercises(dto)

View File

110
app/dtos/exams/reading.py Normal file
View File

@@ -0,0 +1,110 @@
from enum import Enum
from pydantic import BaseModel, Field
from typing import List, Union
from uuid import uuid4, UUID
class WriteBlanksSolution(BaseModel):
id: str
solution: List[str]
class WriteBlanksExercise(BaseModel):
id: UUID = Field(default_factory=uuid4)
type: str = "writeBlanks"
maxWords: int
solutions: List[WriteBlanksSolution]
text: str
@property
def prompt(self) -> str:
return f"Choose no more than {self.maxWords} words and/or a number from the passage for each answer."
class MatchSentencesOption(BaseModel):
id: str
sentence: str
class MatchSentencesSentence(MatchSentencesOption):
solution: str
class MatchSentencesVariant(str, Enum):
HEADING = "heading"
IDEAMATCH = "ideaMatch"
class MatchSentencesExercise(BaseModel):
options: List[MatchSentencesOption]
sentences: List[MatchSentencesSentence]
type: str = "matchSentences"
variant: MatchSentencesVariant
@property
def prompt(self) -> str:
return (
"Choose the correct heading for paragraphs from the list of headings below."
if self.variant == MatchSentencesVariant.HEADING else
"Choose the correct author for the ideas/opinions from the list of authors below."
)
class TrueFalseSolution(str, Enum):
TRUE = "true"
FALSE = "false"
NOT_GIVEN = "not_given"
class TrueFalseQuestions(BaseModel):
prompt: str
solution: TrueFalseSolution
id: str
class TrueFalseExercise(BaseModel):
id: UUID = Field(default_factory=uuid4)
questions: List[TrueFalseQuestions]
type: str = "trueFalse"
prompt: str = "Do the following statements agree with the information given in the Reading Passage?"
class FillBlanksSolution(BaseModel):
id: str
solution: str
class FillBlanksWord(BaseModel):
letter: str
word: str
class FillBlanksExercise(BaseModel):
id: UUID = Field(default_factory=uuid4)
solutions: List[FillBlanksSolution]
text: str
type: str = "fillBlanks"
words: List[FillBlanksWord]
allowRepetition: bool = False
@property
def prompt(self) -> str:
prompt = "Complete the summary below. Write the letter of the corresponding word(s) for it."
return (
f"{prompt}"
if len(self.solutions) == len(self.words) else
f"{prompt}\\nThere are more words than spaces so you will not use them all."
)
Exercise = Union[FillBlanksExercise, TrueFalseExercise, MatchSentencesExercise, WriteBlanksExercise]
class Context(BaseModel):
title: str
content: str
class Part(BaseModel):
exercises: List[Exercise]
text: Context
class Exam(BaseModel):
id: UUID = Field(default_factory=uuid4)
module: str = "reading"
minTimer: int
isDiagnostic: bool = False
parts: List[Part]

19
app/dtos/level.py Normal file
View File

@@ -0,0 +1,19 @@
from typing import List, Optional
from pydantic import BaseModel
from app.configs.constants import LevelExerciseType
class LevelExercises(BaseModel):
type: LevelExerciseType
quantity: int
text_size: Optional[int]
sa_qty: Optional[int]
mc_qty: Optional[int]
topic: Optional[str]
class LevelExercisesDTO(BaseModel):
text: str
exercises: List[LevelExercises]
difficulty: Optional[str]

View File

@@ -1,10 +1,10 @@
import random
import uuid
from typing import List, Dict
from typing import List, Dict, Optional
from pydantic import BaseModel
from app.configs.constants import MinTimers, EducationalContent
from app.configs.constants import MinTimers, EducationalContent, ListeningExerciseType
class SaveListeningDTO(BaseModel):
@@ -12,3 +12,13 @@ class SaveListeningDTO(BaseModel):
minTimer: int = MinTimers.LISTENING_MIN_TIMER_DEFAULT
difficulty: str = random.choice(EducationalContent.DIFFICULTIES)
id: str = str(uuid.uuid4())
class ListeningExercises(BaseModel):
type: ListeningExerciseType
quantity: int
class GenerateListeningExercises(BaseModel):
text: str
exercises: List[ListeningExercises]
difficulty: Optional[str]

17
app/dtos/reading.py Normal file
View File

@@ -0,0 +1,17 @@
import random
from typing import List, Optional
from pydantic import BaseModel, Field
from app.configs.constants import ReadingExerciseType, EducationalContent
class ReadingExercise(BaseModel):
type: ReadingExerciseType
quantity: int
num_random_words: Optional[int] = Field(1)
max_words: Optional[int] = Field(3)
class ReadingDTO(BaseModel):
text: str = Field(...)
exercises: List[ReadingExercise] = Field(...)
difficulty: str = Field(random.choice(EducationalContent.DIFFICULTIES))

View File

@@ -3,7 +3,7 @@ from typing import List, Dict
from pydantic import BaseModel
from app.configs.constants import MinTimers, AvatarEnum
from app.configs.constants import MinTimers, ELAIAvatars
class SaveSpeakingDTO(BaseModel):
@@ -21,14 +21,14 @@ class GradeSpeakingAnswersDTO(BaseModel):
class GenerateVideo1DTO(BaseModel):
avatar: str = (random.choice(list(AvatarEnum))).value
avatar: str = (random.choice(list(ELAIAvatars))).name
questions: List[str]
first_topic: str
second_topic: str
class GenerateVideo2DTO(BaseModel):
avatar: str = (random.choice(list(AvatarEnum))).value
avatar: str = (random.choice(list(ELAIAvatars))).name
prompts: List[str] = []
suffix: str = ""
question: str
@@ -36,7 +36,7 @@ class GenerateVideo2DTO(BaseModel):
class GenerateVideo3DTO(BaseModel):
avatar: str = (random.choice(list(AvatarEnum))).value
avatar: str = (random.choice(list(ELAIAvatars))).name
questions: List[str]
topic: str

View File

@@ -12,6 +12,7 @@ import aiofiles
import numpy as np
import pypandoc
from PIL import Image
from fastapi import UploadFile
class FileHelper:
@@ -104,11 +105,15 @@ class FileHelper:
print(f"An error occurred while trying to remove the file {file_path}: {str(e)}")
@staticmethod
def save_upload(file) -> Tuple[str, str]:
async def save_upload(file: UploadFile, name: str = "upload", path_id: str = None) -> Tuple[str, str]:
ext = file.filename.split('.')[-1]
path_id = str(uuid.uuid4())
path_id = str(uuid.uuid4()) if path_id is None else path_id
os.makedirs(f'./tmp/{path_id}', exist_ok=True)
tmp_filename = f'./tmp/{path_id}/uploaded.{ext}'
file.save(tmp_filename)
tmp_filename = f'./tmp/{path_id}/{name}.{ext}'
file_bytes: bytes = await file.read()
async with aiofiles.open(tmp_filename, 'wb') as file:
await file.write(file_bytes)
return ext, path_id

View File

@@ -1,5 +1,5 @@
from .exam import ExamMapper
from .level import LevelMapper
__all__ = [
"ExamMapper"
"LevelMapper"
]

View File

@@ -2,7 +2,7 @@ from typing import Dict, Any
from pydantic import ValidationError
from app.dtos.exam import (
from app.dtos.exams.level import (
MultipleChoiceExercise,
FillBlanksExercise,
Part, Exam
@@ -10,7 +10,7 @@ from app.dtos.exam import (
from app.dtos.sheet import Sheet, Option, MultipleChoiceQuestion, FillBlanksWord
class ExamMapper:
class LevelMapper:
@staticmethod
def map_to_exam_model(response: Dict[str, Any]) -> Exam:

39
app/mappers/reading.py Normal file
View File

@@ -0,0 +1,39 @@
from typing import Dict, Any
from app.dtos.exams.reading import (
Part, Exam, Context, FillBlanksExercise,
TrueFalseExercise, MatchSentencesExercise,
WriteBlanksExercise
)
class ReadingMapper:
@staticmethod
def map_to_exam_model(response: Dict[str, Any]) -> Exam:
parts = []
for part in response['parts']:
part_exercises = part['exercises']
context = Context(**part['text'])
model_map = {
'fillBlanks': FillBlanksExercise,
'trueFalse': TrueFalseExercise,
'matchSentences': MatchSentencesExercise,
'writeBlanks': WriteBlanksExercise
}
exercises = []
for exercise in part_exercises:
exercise_type = exercise['type']
exercises.append(model_map[exercise_type](**exercise))
part_kwargs = {
"exercises": exercises,
"text": context
}
part_model = Part(**part_kwargs)
parts.append(part_model)
return Exam(parts=parts, minTimer=response["minTimer"])

View File

@@ -4,6 +4,7 @@ from .writing import IWritingService
from .speaking import ISpeakingService
from .reading import IReadingService
from .grade import IGradeService
from .exercises import IExerciseService
__all__ = [
"ILevelService",
@@ -12,4 +13,5 @@ __all__ = [
"ISpeakingService",
"IReadingService",
"IGradeService",
"IExerciseService"
]

View File

@@ -0,0 +1,33 @@
from abc import ABC, abstractmethod
from typing import Dict, Any
class IExerciseService(ABC):
@abstractmethod
async def generate_multiple_choice(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass
@abstractmethod
async def generate_blank_space_text(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass
@abstractmethod
async def generate_reading_passage_utas(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass
@abstractmethod
async def generate_writing_task(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass
@abstractmethod
async def generate_speaking_task(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass
@abstractmethod
async def generate_reading_task(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass
@abstractmethod
async def generate_listening_task(self, args: Dict, exercise_id: int) -> Dict[str, Any]:
pass

View File

@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
import random
from typing import Dict
from typing import Dict, Optional
from fastapi import UploadFile
@@ -10,6 +10,10 @@ from app.configs.constants import EducationalContent
class ILevelService(ABC):
@abstractmethod
async def generate_exercises(self, dto):
pass
@abstractmethod
async def get_level_exam(
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
@@ -30,18 +34,18 @@ class ILevelService(ABC):
@abstractmethod
async def gen_multiple_choice(
self, mc_variant: str, quantity: int, start_id: int = 1, *, utas: bool = False, all_exams=None
self, mc_variant: str, quantity: int, start_id: int = 1 #, *, utas: bool = False, all_exams=None
):
pass
@abstractmethod
async def gen_blank_space_text_utas(
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
self, quantity: int, start_id: int, size: int, topic: str
):
pass
@abstractmethod
async def gen_reading_passage_utas(
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
self, start_id, mc_quantity: int, topic: Optional[str] #sa_quantity: int,
):
pass

View File

@@ -3,14 +3,21 @@ from abc import ABC, abstractmethod
from queue import Queue
from typing import Dict, List
from fastapi import UploadFile
class IListeningService(ABC):
@abstractmethod
async def get_listening_question(
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str,
number_of_exercises_q=queue.Queue(), start_id=-1
):
async def generate_listening_dialog( self, section_id: int, topic: str, difficulty: str):
pass
@abstractmethod
async def get_listening_question(self, section: int, dto):
pass
@abstractmethod
async def get_dialog_from_audio(self, upload: UploadFile):
pass
@abstractmethod

View File

@@ -1,20 +1,15 @@
from abc import ABC, abstractmethod
from queue import Queue
from typing import List
from fastapi import UploadFile
class IReadingService(ABC):
@abstractmethod
async def gen_reading_passage(
self,
passage_id: int,
topic: str,
req_exercises: List[str],
number_of_exercises_q: Queue,
difficulty: str,
start_id: int
):
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
pass
@abstractmethod
async def generate_reading_exercises(self, dto):
pass
@abstractmethod

View File

@@ -1,7 +1,5 @@
from abc import ABC, abstractmethod
from app.configs.constants import AvatarEnum
class IVideoGeneratorService(ABC):

View File

@@ -1,5 +1,191 @@
from .level import LevelService
from typing import Dict, Optional
from fastapi import UploadFile
__all__ = [
"LevelService"
]
from app.dtos.level import LevelExercisesDTO
from app.repositories.abc import IDocumentStore
from app.services.abc import (
ILevelService, ILLMService, IReadingService,
IWritingService, IListeningService, ISpeakingService
)
from .exercises import MultipleChoice, BlankSpace, PassageUtas, FillBlanks
from .full_exams import CustomLevelModule, LevelUtas
from .upload import UploadLevelModule
class LevelService(ILevelService):
def __init__(
self,
llm: ILLMService,
document_store: IDocumentStore,
mc_variants: Dict,
reading_service: IReadingService,
writing_service: IWritingService,
speaking_service: ISpeakingService,
listening_service: IListeningService
):
self._llm = llm
self._document_store = document_store
self._reading_service = reading_service
self._upload_module = UploadLevelModule(llm)
self._mc_variants = mc_variants
self._mc = MultipleChoice(llm, mc_variants)
self._blank_space = BlankSpace(llm, mc_variants)
self._passage_utas = PassageUtas(llm, reading_service, mc_variants)
self._fill_blanks = FillBlanks(llm)
self._level_utas = LevelUtas(llm, self, mc_variants)
self._custom = CustomLevelModule(
llm, self, reading_service, listening_service, writing_service, speaking_service
)
async def upload_level(self, upload: UploadFile) -> Dict:
return await self._upload_module.generate_level_from_file(upload)
async def generate_exercises(self, dto: LevelExercisesDTO):
exercises = []
start_id = 1
for req_exercise in dto.exercises:
if req_exercise.type == "multipleChoice":
questions = await self._mc.gen_multiple_choice("normal", req_exercise.quantity, start_id)
exercises.append(questions)
elif req_exercise.type == "mcBlank":
questions = await self._mc.gen_multiple_choice("blank_space", req_exercise.quantity, start_id)
questions["variant"] = "mc"
exercises.append(questions)
elif req_exercise.type == "mcUnderline":
questions = await self._mc.gen_multiple_choice("underline", req_exercise.quantity, start_id)
exercises.append(questions)
elif req_exercise.type == "blankSpaceText":
questions = await self._blank_space.gen_blank_space_text_utas(
req_exercise.quantity, start_id, req_exercise.text_size, req_exercise.topic
)
exercises.append(questions)
elif req_exercise.type == "passageUtas":
questions = await self._passage_utas.gen_reading_passage_utas(
start_id, req_exercise.mc_qty, req_exercise.text_size
)
exercises.append(questions)
elif req_exercise.type == "fillBlanksMC":
questions = await self._passage_utas.gen_reading_passage_utas(
start_id, req_exercise.mc_qty, req_exercise.text_size
)
exercises.append(questions)
start_id = start_id + req_exercise.quantity
return exercises
# 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)
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)
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)
async def get_level_exam(
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
) -> Dict:
pass
async def get_level_utas(self):
return await self._level_utas.get_level_utas()
async def get_custom_level(self, data: Dict):
return await self._custom.get_custom_level(data)
"""
async def _generate_single_multiple_choice(self, mc_variant: str = "normal"):
mc_template = self._mc_variants[mc_variant]["questions"][0]
blank_mod = " blank space " if mc_variant == "blank_space" else " "
messages = [
{
"role": "system",
"content": (
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
)
},
{
"role": "user",
"content": (
f'Generate 1 multiple choice {blank_mod} question of 4 options for an english level exam, '
f'it can be easy, intermediate or advanced.'
)
}
]
if mc_variant == "underline":
messages.append({
"role": "user",
"content": (
'The type of multiple choice in the prompt has wrong words or group of words and the options '
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
'the boss <u>is</u> nice."\n'
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
)
})
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["options"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return question
"""
"""
async def _replace_exercise_if_exists(
self, all_exams, current_exercise, current_exam, seen_keys, mc_variant: str, utas: bool = False
):
# Extracting relevant fields for comparison
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 await self._replace_exercise_if_exists(
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam, seen_keys,
mc_variant, utas
)
else:
seen_keys.add(key)
if not utas:
for exam in all_exams:
exam_dict = exam.to_dict()
if len(exam_dict.get("parts", [])) > 0:
exercise_dict = exam_dict.get("parts", [])[0]
if len(exercise_dict.get("exercises", [])) > 0:
if any(
exercise["prompt"] == current_exercise["prompt"] and
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
current_exercise["options"])
for exercise in exercise_dict.get("exercises", [])[0]["questions"]
):
return await self._replace_exercise_if_exists(
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
seen_keys, mc_variant, utas
)
else:
for exam in all_exams:
if any(
exercise["prompt"] == current_exercise["prompt"] and
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
current_exercise["options"])
for exercise in exam.get("questions", [])
):
return await self._replace_exercise_if_exists(
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
seen_keys, mc_variant, utas
)
return current_exercise, seen_keys
"""

View File

@@ -0,0 +1,11 @@
from .multiple_choice import MultipleChoice
from .blank_space import BlankSpace
from .passage_utas import PassageUtas
from .fillBlanks import FillBlanks
__all__ = [
"MultipleChoice",
"BlankSpace",
"PassageUtas",
"FillBlanks"
]

View File

@@ -0,0 +1,44 @@
import random
from app.configs.constants import EducationalContent, GPTModels, TemperatureSettings
from app.services.abc import ILLMService
class BlankSpace:
def __init__(self, llm: ILLMService, mc_variants: dict):
self._llm = llm
self._mc_variants = mc_variants
async def gen_blank_space_text_utas(
self, quantity: int, start_id: int, size: int, topic=None
):
if not topic:
topic = random.choice(EducationalContent.MTI_TOPICS)
json_template = self._mc_variants["blank_space_text"]
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
},
{
"role": "user",
"content": f'Generate a text of at least {size} words about the topic {topic}.'
},
{
"role": "user",
"content": (
f'From the generated text choose {quantity} words (cannot be sequential words) to replace '
'once with {{id}} where id starts on ' + str(start_id) + ' and is incremented for each word. '
'The ids must be ordered throughout the text and the words must be replaced only once. '
'Put the removed words and respective ids on the words array of the json in the correct order.'
)
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["question"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return question["question"]

View File

@@ -0,0 +1,73 @@
import random
from app.configs.constants import GPTModels, TemperatureSettings, EducationalContent
from app.services.abc import ILLMService
class FillBlanks:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_fill_blanks(
self, quantity: int, start_id: int, size: int, topic=None
):
if not topic:
topic = random.choice(EducationalContent.MTI_TOPICS)
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {self._fill_blanks_mc_template()}'
},
{
"role": "user",
"content": f'Generate a text of at least {size} words about the topic {topic}.'
},
{
"role": "user",
"content": (
f'From the generated text choose {quantity} words (cannot be sequential words) to replace '
'once with {{id}} where id starts on ' + str(start_id) + ' and is incremented for each word. '
'The ids must be ordered throughout the text and the words must be replaced only once. '
'For each removed word you will place it in the solutions array and assign a letter from A to D,'
' then you will place that removed word and the chosen letter on the words array along with '
' other 3 other words for the remaining letter. This is a fill blanks question for an english '
'exam, so don\'t choose words completely at random.'
)
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["question"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return {
**question,
"type": "fillBlanks",
"variant": "mc",
"prompt": "Click a blank to select the appropriate word for it.",
}
@staticmethod
def _fill_blanks_mc_template():
return {
"text": "",
"solutions": [
{
"id": "",
"solution": ""
}
],
"words": [
{
"id": "",
"options": {
"A": "",
"B": "",
"C": "",
"D": ""
}
}
]
}

View File

@@ -0,0 +1,84 @@
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class MultipleChoice:
def __init__(self, llm: ILLMService, mc_variants: dict):
self._llm = llm
self._mc_variants = mc_variants
async def gen_multiple_choice(
self, mc_variant: str, quantity: int, 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.'
)
messages = [
{
"role": "system",
"content": (
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
)
},
{
"role": "user",
"content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod)
}
]
if mc_variant == "underline":
messages.append({
"role": "user",
"content": (
'The type of multiple choice in the prompt has wrong words or group of words and the options '
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
'the boss <u>is</u> nice."\n'
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
)
})
questions = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return ExercisesHelper.fix_exercise_ids(questions, start_id)
"""
if len(question["questions"]) != quantity:
return await self.gen_multiple_choice(mc_variant, quantity, start_id, utas=utas, all_exams=all_exams)
else:
if not utas:
all_exams = await self._document_store.get_all("level")
seen_keys = set()
for i in range(len(question["questions"])):
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
)
return {
"id": str(uuid.uuid4()),
"prompt": "Select the appropriate option.",
"questions": ExercisesHelper.fix_exercise_ids(question, start_id)["questions"],
"type": "multipleChoice",
}
else:
if all_exams is not None:
seen_keys = set()
for i in range(len(question["questions"])):
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
)
response = ExercisesHelper.fix_exercise_ids(question, start_id)
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
return response
"""

View File

@@ -0,0 +1,93 @@
from typing import Optional
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService, IReadingService
class PassageUtas:
def __init__(self, llm: ILLMService, reading_service: IReadingService, mc_variants: dict):
self._llm = llm
self._reading_service = reading_service
self._mc_variants = mc_variants
async def gen_reading_passage_utas(
self, start_id, mc_quantity: int, topic: Optional[str] # sa_quantity: int,
):
passage = await self._reading_service.generate_reading_passage(1, topic)
mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id, mc_quantity)
#short_answer = await self._gen_short_answer_utas(passage["text"], start_id, sa_quantity)
# + sa_quantity, mc_quantity)
"""
exercises: {
"shortAnswer": short_answer,
"multipleChoice": mc_exercises,
},
"""
return {
"exercises": mc_exercises,
"text": {
"content": passage["text"],
"title": passage["title"]
}
}
async def _gen_short_answer_utas(self, text: str, start_id: int, sa_quantity: int):
json_format = {"questions": [{"id": 1, "question": "question", "possible_answers": ["answer_1", "answer_2"]}]}
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
},
{
"role": "user",
"content": (
f'Generate {sa_quantity} short answer questions, and the possible answers, must have '
f'maximum 3 words per answer, about this text:\n"{text}"'
)
},
{
"role": "user",
"content": f'The id starts at {start_id}.'
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return question["questions"]
async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int):
json_template = self._mc_variants["text_mc_utas"]
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
},
{
"role": "user",
"content": f'Generate {mc_quantity} multiple choice questions of 4 options for this text:\n{text}'
},
{
"role": "user",
"content": 'Make sure every question only has 1 correct answer.'
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
if len(question["questions"]) != mc_quantity:
return await self._gen_text_multiple_choice_utas(text, mc_quantity, start_id)
else:
response = ExercisesHelper.fix_exercise_ids(question, start_id)
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
return response

View File

@@ -0,0 +1,7 @@
from .custom import CustomLevelModule
from .level_utas import LevelUtas
__all__ = [
"CustomLevelModule",
"LevelUtas"
]

View File

@@ -1,335 +1,335 @@
import queue
import random
from typing import Dict
from app.configs.constants import CustomLevelExerciseTypes, EducationalContent
from app.services.abc import (
ILLMService, ILevelService, IReadingService,
IWritingService, IListeningService, ISpeakingService
)
class CustomLevelModule:
def __init__(
self,
llm: ILLMService,
level: ILevelService,
reading: IReadingService,
listening: IListeningService,
writing: IWritingService,
speaking: ISpeakingService
):
self._llm = llm
self._level = level
self._reading = reading
self._listening = listening
self._writing = writing
self._speaking = speaking
# TODO: I've changed this to retrieve the args from the body request and not request query args
async def get_custom_level(self, data: Dict):
nr_exercises = int(data.get('nr_exercises'))
exercise_id = 1
response = {
"exercises": {},
"module": "level"
}
for i in range(1, nr_exercises + 1, 1):
exercise_type = data.get(f'exercise_{i}_type')
exercise_difficulty = data.get(f'exercise_{i}_difficulty', random.choice(['easy', 'medium', 'hard']))
exercise_qty = int(data.get(f'exercise_{i}_qty', -1))
exercise_topic = data.get(f'exercise_{i}_topic', random.choice(EducationalContent.TOPICS))
exercise_topic_2 = data.get(f'exercise_{i}_topic_2', random.choice(EducationalContent.TOPICS))
exercise_text_size = int(data.get(f'exercise_{i}_text_size', 700))
exercise_sa_qty = int(data.get(f'exercise_{i}_sa_qty', -1))
exercise_mc_qty = int(data.get(f'exercise_{i}_mc_qty', -1))
exercise_mc3_qty = int(data.get(f'exercise_{i}_mc3_qty', -1))
exercise_fillblanks_qty = int(data.get(f'exercise_{i}_fillblanks_qty', -1))
exercise_writeblanks_qty = int(data.get(f'exercise_{i}_writeblanks_qty', -1))
exercise_writeblanksquestions_qty = int(data.get(f'exercise_{i}_writeblanksquestions_qty', -1))
exercise_writeblanksfill_qty = int(data.get(f'exercise_{i}_writeblanksfill_qty', -1))
exercise_writeblanksform_qty = int(data.get(f'exercise_{i}_writeblanksform_qty', -1))
exercise_truefalse_qty = int(data.get(f'exercise_{i}_truefalse_qty', -1))
exercise_paragraphmatch_qty = int(data.get(f'exercise_{i}_paragraphmatch_qty', -1))
exercise_ideamatch_qty = int(data.get(f'exercise_{i}_ideamatch_qty', -1))
if exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_4.value:
response["exercises"][f"exercise_{i}"] = {}
response["exercises"][f"exercise_{i}"]["questions"] = []
response["exercises"][f"exercise_{i}"]["type"] = "multipleChoice"
while exercise_qty > 0:
if exercise_qty - 15 > 0:
qty = 15
else:
qty = exercise_qty
mc_response = await self._level.gen_multiple_choice(
"normal", qty, exercise_id, utas=True,
all_exams=response["exercises"][f"exercise_{i}"]["questions"]
)
response["exercises"][f"exercise_{i}"]["questions"].extend(mc_response["questions"])
exercise_id = exercise_id + qty
exercise_qty = exercise_qty - qty
elif exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_BLANK_SPACE.value:
response["exercises"][f"exercise_{i}"] = {}
response["exercises"][f"exercise_{i}"]["questions"] = []
response["exercises"][f"exercise_{i}"]["type"] = "multipleChoice"
while exercise_qty > 0:
if exercise_qty - 15 > 0:
qty = 15
else:
qty = exercise_qty
mc_response = await self._level.gen_multiple_choice(
"blank_space", qty, exercise_id, utas=True,
all_exams=response["exercises"][f"exercise_{i}"]["questions"]
)
response["exercises"][f"exercise_{i}"]["questions"].extend(mc_response["questions"])
exercise_id = exercise_id + qty
exercise_qty = exercise_qty - qty
elif exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_UNDERLINED.value:
response["exercises"][f"exercise_{i}"] = {}
response["exercises"][f"exercise_{i}"]["questions"] = []
response["exercises"][f"exercise_{i}"]["type"] = "multipleChoice"
while exercise_qty > 0:
if exercise_qty - 15 > 0:
qty = 15
else:
qty = exercise_qty
mc_response = await self._level.gen_multiple_choice(
"underline", qty, exercise_id, utas=True,
all_exams=response["exercises"][f"exercise_{i}"]["questions"]
)
response["exercises"][f"exercise_{i}"]["questions"].extend(mc_response["questions"])
exercise_id = exercise_id + qty
exercise_qty = exercise_qty - qty
elif exercise_type == CustomLevelExerciseTypes.BLANK_SPACE_TEXT.value:
response["exercises"][f"exercise_{i}"] = await self._level.gen_blank_space_text_utas(
exercise_qty, exercise_id, exercise_text_size
)
response["exercises"][f"exercise_{i}"]["type"] = "blankSpaceText"
exercise_id = exercise_id + exercise_qty
elif exercise_type == CustomLevelExerciseTypes.READING_PASSAGE_UTAS.value:
response["exercises"][f"exercise_{i}"] = await self._level.gen_reading_passage_utas(
exercise_id, exercise_sa_qty, exercise_mc_qty, exercise_topic
)
response["exercises"][f"exercise_{i}"]["type"] = "readingExercises"
exercise_id = exercise_id + exercise_qty
elif exercise_type == CustomLevelExerciseTypes.WRITING_LETTER.value:
response["exercises"][f"exercise_{i}"] = await self._writing.get_writing_task_general_question(
1, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{i}"]["type"] = "writing"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.WRITING_2.value:
response["exercises"][f"exercise_{i}"] = await self._writing.get_writing_task_general_question(
2, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{i}"]["type"] = "writing"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.SPEAKING_1.value:
response["exercises"][f"exercise_{i}"] = await self._speaking.get_speaking_part(
1, exercise_topic, exercise_difficulty, exercise_topic_2
)
response["exercises"][f"exercise_{i}"]["type"] = "interactiveSpeaking"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.SPEAKING_2.value:
response["exercises"][f"exercise_{i}"] = await self._speaking.get_speaking_part(
2, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{i}"]["type"] = "speaking"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.SPEAKING_3.value:
response["exercises"][f"exercise_{i}"] = await self._speaking.get_speaking_part(
3, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._reading.gen_reading_passage(
1, exercise_topic, exercises, exercise_qty_q, exercise_difficulty, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._reading.gen_reading_passage(
2, exercise_topic, exercises, exercise_qty_q, exercise_difficulty, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._reading.gen_reading_passage(
3, exercise_topic, exercises, exercise_qty_q, exercise_id, exercise_difficulty
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
1, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
2, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
3, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
4, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{i}"]["type"] = "listening"
exercise_id = exercise_id + total_qty
return response
import queue
import random
from typing import Dict
from app.configs.constants import CustomLevelExerciseTypes, EducationalContent
from app.services.abc import (
ILLMService, ILevelService, IReadingService,
IWritingService, IListeningService, ISpeakingService
)
class CustomLevelModule:
def __init__(
self,
llm: ILLMService,
level: ILevelService,
reading: IReadingService,
listening: IListeningService,
writing: IWritingService,
speaking: ISpeakingService
):
self._llm = llm
self._level = level
self._reading = reading
self._listening = listening
self._writing = writing
self._speaking = speaking
# TODO: I've changed this to retrieve the args from the body request and not request query args
async def get_custom_level(self, data: Dict):
nr_exercises = int(data.get('nr_exercises'))
exercise_id = 1
response = {
"exercises": {},
"module": "level"
}
for i in range(1, nr_exercises + 1, 1):
exercise_type = data.get(f'exercise_{i}_type')
exercise_difficulty = data.get(f'exercise_{i}_difficulty', random.choice(['easy', 'medium', 'hard']))
exercise_qty = int(data.get(f'exercise_{i}_qty', -1))
exercise_topic = data.get(f'exercise_{i}_topic', random.choice(EducationalContent.TOPICS))
exercise_topic_2 = data.get(f'exercise_{i}_topic_2', random.choice(EducationalContent.TOPICS))
exercise_text_size = int(data.get(f'exercise_{i}_text_size', 700))
exercise_sa_qty = int(data.get(f'exercise_{i}_sa_qty', -1))
exercise_mc_qty = int(data.get(f'exercise_{i}_mc_qty', -1))
exercise_mc3_qty = int(data.get(f'exercise_{i}_mc3_qty', -1))
exercise_fillblanks_qty = int(data.get(f'exercise_{i}_fillblanks_qty', -1))
exercise_writeblanks_qty = int(data.get(f'exercise_{i}_writeblanks_qty', -1))
exercise_writeblanksquestions_qty = int(data.get(f'exercise_{i}_writeblanksquestions_qty', -1))
exercise_writeblanksfill_qty = int(data.get(f'exercise_{i}_writeblanksfill_qty', -1))
exercise_writeblanksform_qty = int(data.get(f'exercise_{i}_writeblanksform_qty', -1))
exercise_truefalse_qty = int(data.get(f'exercise_{i}_truefalse_qty', -1))
exercise_paragraphmatch_qty = int(data.get(f'exercise_{i}_paragraphmatch_qty', -1))
exercise_ideamatch_qty = int(data.get(f'exercise_{i}_ideamatch_qty', -1))
if exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_4.value:
response["exercises"][f"exercise_{i}"] = {}
response["exercises"][f"exercise_{i}"]["questions"] = []
response["exercises"][f"exercise_{i}"]["type"] = "multipleChoice"
while exercise_qty > 0:
if exercise_qty - 15 > 0:
qty = 15
else:
qty = exercise_qty
mc_response = await self._level.gen_multiple_choice(
"normal", qty, exercise_id, utas=True,
all_exams=response["exercises"][f"exercise_{i}"]["questions"]
)
response["exercises"][f"exercise_{i}"]["questions"].extend(mc_response["questions"])
exercise_id = exercise_id + qty
exercise_qty = exercise_qty - qty
elif exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_BLANK_SPACE.value:
response["exercises"][f"exercise_{i}"] = {}
response["exercises"][f"exercise_{i}"]["questions"] = []
response["exercises"][f"exercise_{i}"]["type"] = "multipleChoice"
while exercise_qty > 0:
if exercise_qty - 15 > 0:
qty = 15
else:
qty = exercise_qty
mc_response = await self._level.gen_multiple_choice(
"blank_space", qty, exercise_id, utas=True,
all_exams=response["exercises"][f"exercise_{i}"]["questions"]
)
response["exercises"][f"exercise_{i}"]["questions"].extend(mc_response["questions"])
exercise_id = exercise_id + qty
exercise_qty = exercise_qty - qty
elif exercise_type == CustomLevelExerciseTypes.MULTIPLE_CHOICE_UNDERLINED.value:
response["exercises"][f"exercise_{i}"] = {}
response["exercises"][f"exercise_{i}"]["questions"] = []
response["exercises"][f"exercise_{i}"]["type"] = "multipleChoice"
while exercise_qty > 0:
if exercise_qty - 15 > 0:
qty = 15
else:
qty = exercise_qty
mc_response = await self._level.gen_multiple_choice(
"underline", qty, exercise_id, utas=True,
all_exams=response["exercises"][f"exercise_{i}"]["questions"]
)
response["exercises"][f"exercise_{i}"]["questions"].extend(mc_response["questions"])
exercise_id = exercise_id + qty
exercise_qty = exercise_qty - qty
elif exercise_type == CustomLevelExerciseTypes.BLANK_SPACE_TEXT.value:
response["exercises"][f"exercise_{i}"] = await self._level.gen_blank_space_text_utas(
exercise_qty, exercise_id, exercise_text_size
)
response["exercises"][f"exercise_{i}"]["type"] = "blankSpaceText"
exercise_id = exercise_id + exercise_qty
elif exercise_type == CustomLevelExerciseTypes.READING_PASSAGE_UTAS.value:
response["exercises"][f"exercise_{i}"] = await self._level.gen_reading_passage_utas(
exercise_id, exercise_sa_qty, exercise_mc_qty, exercise_topic
)
response["exercises"][f"exercise_{i}"]["type"] = "readingExercises"
exercise_id = exercise_id + exercise_qty
elif exercise_type == CustomLevelExerciseTypes.WRITING_LETTER.value:
response["exercises"][f"exercise_{i}"] = await self._writing.get_writing_task_general_question(
1, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{i}"]["type"] = "writing"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.WRITING_2.value:
response["exercises"][f"exercise_{i}"] = await self._writing.get_writing_task_general_question(
2, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{i}"]["type"] = "writing"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.SPEAKING_1.value:
response["exercises"][f"exercise_{i}"] = await self._speaking.get_speaking_part(
1, exercise_topic, exercise_difficulty, exercise_topic_2
)
response["exercises"][f"exercise_{i}"]["type"] = "interactiveSpeaking"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.SPEAKING_2.value:
response["exercises"][f"exercise_{i}"] = await self._speaking.get_speaking_part(
2, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{i}"]["type"] = "speaking"
exercise_id = exercise_id + 1
elif exercise_type == CustomLevelExerciseTypes.SPEAKING_3.value:
response["exercises"][f"exercise_{i}"] = await self._speaking.get_speaking_part(
3, exercise_topic, exercise_difficulty
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._reading.gen_reading_passage(
1, exercise_topic, exercises, exercise_qty_q, exercise_difficulty, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._reading.gen_reading_passage(
2, exercise_topic, exercises, exercise_qty_q, exercise_difficulty, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._reading.gen_reading_passage(
3, exercise_topic, exercises, exercise_qty_q, exercise_id, exercise_difficulty
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
1, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
2, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
3, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{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"][f"exercise_{i}"] = await self._listening.get_listening_question(
4, exercise_topic, exercises, exercise_difficulty, exercise_qty_q, exercise_id
)
response["exercises"][f"exercise_{i}"]["type"] = "listening"
exercise_id = exercise_id + total_qty
return response

View File

@@ -0,0 +1,119 @@
import json
import uuid
from app.services.abc import ILLMService
class LevelUtas:
def __init__(self, llm: ILLMService, level_service, mc_variants: dict):
self._llm = llm
self._mc_variants = mc_variants
self._level_service = level_service
async def get_level_utas(self, diagnostic: bool = False, min_timer: int = 25):
# Formats
mc = {
"id": str(uuid.uuid4()),
"prompt": "Choose the correct word or group of words that completes the sentences.",
"questions": None,
"type": "multipleChoice",
"part": 1
}
umc = {
"id": str(uuid.uuid4()),
"prompt": "Choose the underlined word or group of words that is not correct.",
"questions": None,
"type": "multipleChoice",
"part": 2
}
bs_1 = {
"id": str(uuid.uuid4()),
"prompt": "Read the text and write the correct word for each space.",
"questions": None,
"type": "blankSpaceText",
"part": 3
}
bs_2 = {
"id": str(uuid.uuid4()),
"prompt": "Read the text and write the correct word for each space.",
"questions": None,
"type": "blankSpaceText",
"part": 4
}
reading = {
"id": str(uuid.uuid4()),
"prompt": "Read the text and answer the questions below.",
"questions": None,
"type": "readingExercises",
"part": 5
}
all_mc_questions = []
# PART 1
# await self._gen_multiple_choice("normal", number_of_exercises, utas=False)
mc_exercises1 = await self._level_service.gen_multiple_choice(
"blank_space", 15, 1, utas=True, all_exams=all_mc_questions
)
print(json.dumps(mc_exercises1, indent=4))
all_mc_questions.append(mc_exercises1)
# PART 2
mc_exercises2 = await self._level_service.gen_multiple_choice(
"blank_space", 15, 16, utas=True, all_exams=all_mc_questions
)
print(json.dumps(mc_exercises2, indent=4))
all_mc_questions.append(mc_exercises2)
# PART 3
mc_exercises3 = await self._level_service.gen_multiple_choice(
"blank_space", 15, 31, utas=True, all_exams=all_mc_questions
)
print(json.dumps(mc_exercises3, indent=4))
all_mc_questions.append(mc_exercises3)
mc_exercises = mc_exercises1['questions'] + mc_exercises2['questions'] + mc_exercises3['questions']
print(json.dumps(mc_exercises, indent=4))
mc["questions"] = mc_exercises
# Underlined mc
underlined_mc = await self._level_service.gen_multiple_choice(
"underline", 15, 46, utas=True, all_exams=all_mc_questions
)
print(json.dumps(underlined_mc, indent=4))
umc["questions"] = underlined_mc
# Blank Space text 1
blank_space_text_1 = await self._level_service.gen_blank_space_text_utas(12, 61, 250)
print(json.dumps(blank_space_text_1, indent=4))
bs_1["questions"] = blank_space_text_1
# Blank Space text 2
blank_space_text_2 = await self._level_service.gen_blank_space_text_utas(14, 73, 350)
print(json.dumps(blank_space_text_2, indent=4))
bs_2["questions"] = blank_space_text_2
# Reading text
reading_text = await self._level_service.gen_reading_passage_utas(87, 10, 4)
print(json.dumps(reading_text, indent=4))
reading["questions"] = reading_text
return {
"exercises": {
"blankSpaceMultipleChoice": mc,
"underlinedMultipleChoice": umc,
"blankSpaceText1": bs_1,
"blankSpaceText2": bs_2,
"readingExercises": reading,
},
"isDiagnostic": diagnostic,
"minTimer": min_timer,
"module": "level"
}

View File

@@ -1,417 +0,0 @@
import json
import random
import uuid
from typing import Dict
from fastapi import UploadFile
from app.configs.constants import GPTModels, TemperatureSettings, EducationalContent
from app.helpers import ExercisesHelper
from app.repositories.abc import IDocumentStore
from app.services.abc import ILevelService, ILLMService, IReadingService, IWritingService, ISpeakingService, \
IListeningService
from .custom import CustomLevelModule
from .upload import UploadLevelModule
class LevelService(ILevelService):
def __init__(
self,
llm: ILLMService,
document_store: IDocumentStore,
mc_variants: Dict,
reading_service: IReadingService,
writing_service: IWritingService,
speaking_service: ISpeakingService,
listening_service: IListeningService
):
self._llm = llm
self._document_store = document_store
self._reading_service = reading_service
self._custom_module = CustomLevelModule(
llm, self, reading_service, listening_service, writing_service, speaking_service
)
self._upload_module = UploadLevelModule(llm)
# TODO: normal and blank spaces only differ on "multiple choice blank space questions" in the prompt
# mc_variants are stored in ./mc_variants.json
self._mc_variants = mc_variants
async def upload_level(self, upload: UploadFile) -> Dict:
return await self._upload_module.generate_level_from_file(upload)
async def get_custom_level(self, data: Dict):
return await self._custom_module.get_custom_level(data)
async def get_level_exam(
self, number_of_exercises: int = 25, min_timer: int = 25, diagnostic: bool = False
) -> Dict:
exercises = await self.gen_multiple_choice("normal", number_of_exercises, utas=False)
return {
"exercises": [exercises],
"isDiagnostic": diagnostic,
"minTimer": min_timer,
"module": "level"
}
async def get_level_utas(self, diagnostic: bool = False, min_timer: int = 25):
# Formats
mc = {
"id": str(uuid.uuid4()),
"prompt": "Choose the correct word or group of words that completes the sentences.",
"questions": None,
"type": "multipleChoice",
"part": 1
}
umc = {
"id": str(uuid.uuid4()),
"prompt": "Choose the underlined word or group of words that is not correct.",
"questions": None,
"type": "multipleChoice",
"part": 2
}
bs_1 = {
"id": str(uuid.uuid4()),
"prompt": "Read the text and write the correct word for each space.",
"questions": None,
"type": "blankSpaceText",
"part": 3
}
bs_2 = {
"id": str(uuid.uuid4()),
"prompt": "Read the text and write the correct word for each space.",
"questions": None,
"type": "blankSpaceText",
"part": 4
}
reading = {
"id": str(uuid.uuid4()),
"prompt": "Read the text and answer the questions below.",
"questions": None,
"type": "readingExercises",
"part": 5
}
all_mc_questions = []
# PART 1
# await self._gen_multiple_choice("normal", number_of_exercises, utas=False)
mc_exercises1 = await self.gen_multiple_choice(
"blank_space", 15, 1, utas=True, all_exams=all_mc_questions
)
print(json.dumps(mc_exercises1, indent=4))
all_mc_questions.append(mc_exercises1)
# PART 2
mc_exercises2 = await self.gen_multiple_choice(
"blank_space", 15, 16, utas=True, all_exams=all_mc_questions
)
print(json.dumps(mc_exercises2, indent=4))
all_mc_questions.append(mc_exercises2)
# PART 3
mc_exercises3 = await self.gen_multiple_choice(
"blank_space", 15, 31, utas=True, all_exams=all_mc_questions
)
print(json.dumps(mc_exercises3, indent=4))
all_mc_questions.append(mc_exercises3)
mc_exercises = mc_exercises1['questions'] + mc_exercises2['questions'] + mc_exercises3['questions']
print(json.dumps(mc_exercises, indent=4))
mc["questions"] = mc_exercises
# Underlined mc
underlined_mc = await self.gen_multiple_choice(
"underline", 15, 46, utas=True, all_exams=all_mc_questions
)
print(json.dumps(underlined_mc, indent=4))
umc["questions"] = underlined_mc
# Blank Space text 1
blank_space_text_1 = await self.gen_blank_space_text_utas(12, 61, 250)
print(json.dumps(blank_space_text_1, indent=4))
bs_1["questions"] = blank_space_text_1
# Blank Space text 2
blank_space_text_2 = await self.gen_blank_space_text_utas(14, 73, 350)
print(json.dumps(blank_space_text_2, indent=4))
bs_2["questions"] = blank_space_text_2
# Reading text
reading_text = await self.gen_reading_passage_utas(87, 10, 4)
print(json.dumps(reading_text, indent=4))
reading["questions"] = reading_text
return {
"exercises": {
"blankSpaceMultipleChoice": mc,
"underlinedMultipleChoice": umc,
"blankSpaceText1": bs_1,
"blankSpaceText2": bs_2,
"readingExercises": reading,
},
"isDiagnostic": diagnostic,
"minTimer": min_timer,
"module": "level"
}
async def gen_multiple_choice(
self, mc_variant: str, quantity: int, start_id: int = 1, *, utas: bool = False, all_exams=None
):
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.'
)
messages = [
{
"role": "system",
"content": (
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
)
},
{
"role": "user",
"content": gen_multiple_choice_for_text.format(quantity=str(quantity), blank=blank_mod)
}
]
if mc_variant == "underline":
messages.append({
"role": "user",
"content": (
'The type of multiple choice in the prompt has wrong words or group of words and the options '
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
'the boss <u>is</u> nice."\n'
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
)
})
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
if len(question["questions"]) != quantity:
return await self.gen_multiple_choice(mc_variant, quantity, start_id, utas=utas, all_exams=all_exams)
else:
if not utas:
all_exams = await self._document_store.get_all("level")
seen_keys = set()
for i in range(len(question["questions"])):
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
)
return {
"id": str(uuid.uuid4()),
"prompt": "Select the appropriate option.",
"questions": ExercisesHelper.fix_exercise_ids(question, start_id)["questions"],
"type": "multipleChoice",
}
else:
if all_exams is not None:
seen_keys = set()
for i in range(len(question["questions"])):
question["questions"][i], seen_keys = await self._replace_exercise_if_exists(
all_exams, question["questions"][i], question, seen_keys, mc_variant, utas
)
response = ExercisesHelper.fix_exercise_ids(question, start_id)
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
return response
async def _generate_single_multiple_choice(self, mc_variant: str = "normal"):
mc_template = self._mc_variants[mc_variant]["questions"][0]
blank_mod = " blank space " if mc_variant == "blank_space" else " "
messages = [
{
"role": "system",
"content": (
f'You are a helpful assistant designed to output JSON on this format: {mc_template}'
)
},
{
"role": "user",
"content": (
f'Generate 1 multiple choice {blank_mod} question of 4 options for an english level exam, '
f'it can be easy, intermediate or advanced.'
)
}
]
if mc_variant == "underline":
messages.append({
"role": "user",
"content": (
'The type of multiple choice in the prompt has wrong words or group of words and the options '
'are to find the wrong word or group of words that are underlined in the prompt. \nExample:\n'
'Prompt: "I <u>complain</u> about my boss <u>all the time</u>, but my colleagues <u>thinks</u> '
'the boss <u>is</u> nice."\n'
'Options:\na: "complain"\nb: "all the time"\nc: "thinks"\nd: "is"'
)
})
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["options"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return question
async def _replace_exercise_if_exists(
self, all_exams, current_exercise, current_exam, seen_keys, mc_variant: str, utas: bool = False
):
# Extracting relevant fields for comparison
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 await self._replace_exercise_if_exists(
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam, seen_keys,
mc_variant, utas
)
else:
seen_keys.add(key)
if not utas:
for exam in all_exams:
exam_dict = exam.to_dict()
if len(exam_dict.get("parts", [])) > 0:
exercise_dict = exam_dict.get("parts", [])[0]
if len(exercise_dict.get("exercises", [])) > 0:
if any(
exercise["prompt"] == current_exercise["prompt"] and
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
current_exercise["options"])
for exercise in exercise_dict.get("exercises", [])[0]["questions"]
):
return await self._replace_exercise_if_exists(
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
seen_keys, mc_variant, utas
)
else:
for exam in all_exams:
if any(
exercise["prompt"] == current_exercise["prompt"] and
any(exercise["options"][0]["text"] == current_option["text"] for current_option in
current_exercise["options"])
for exercise in exam.get("questions", [])
):
return await self._replace_exercise_if_exists(
all_exams, await self._generate_single_multiple_choice(mc_variant), current_exam,
seen_keys, mc_variant, utas
)
return current_exercise, seen_keys
async def gen_blank_space_text_utas(
self, quantity: int, start_id: int, size: int, topic=random.choice(EducationalContent.MTI_TOPICS)
):
json_template = self._mc_variants["blank_space_text"]
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
},
{
"role": "user",
"content": f'Generate a text of at least {size} words about the topic {topic}.'
},
{
"role": "user",
"content": (
f'From the generated text choose {quantity} words (cannot be sequential words) to replace '
'once with {{id}} where id starts on ' + str(start_id) + ' and is incremented for each word. '
'The ids must be ordered throughout the text and the words must be replaced only once. '
'Put the removed words and respective ids on the words array of the json in the correct order.'
)
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["question"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return question["question"]
async def gen_reading_passage_utas(
self, start_id, sa_quantity: int, mc_quantity: int, topic=random.choice(EducationalContent.MTI_TOPICS)
):
passage = await self._reading_service.generate_reading_passage(1, topic)
short_answer = await self._gen_short_answer_utas(passage["text"], start_id, sa_quantity)
mc_exercises = await self._gen_text_multiple_choice_utas(passage["text"], start_id + sa_quantity, mc_quantity)
return {
"exercises": {
"shortAnswer": short_answer,
"multipleChoice": mc_exercises,
},
"text": {
"content": passage["text"],
"title": passage["title"]
}
}
async def _gen_short_answer_utas(self, text: str, start_id: int, sa_quantity: int):
json_format = {"questions": [{"id": 1, "question": "question", "possible_answers": ["answer_1", "answer_2"]}]}
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {json_format}'
},
{
"role": "user",
"content": (
f'Generate {sa_quantity} short answer questions, and the possible answers, must have '
f'maximum 3 words per answer, about this text:\n"{text}"'
)
},
{
"role": "user",
"content": f'The id starts at {start_id}.'
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return question["questions"]
async def _gen_text_multiple_choice_utas(self, text: str, start_id: int, mc_quantity: int):
json_template = self._mc_variants["text_mc_utas"]
messages = [
{
"role": "system",
"content": f'You are a helpful assistant designed to output JSON on this format: {json_template}'
},
{
"role": "user",
"content": f'Generate {mc_quantity} multiple choice questions of 4 options for this text:\n{text}'
},
{
"role": "user",
"content": 'Make sure every question only has 1 correct answer.'
}
]
question = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
if len(question["questions"]) != mc_quantity:
return await self._gen_text_multiple_choice_utas(text, mc_quantity, start_id)
else:
response = ExercisesHelper.fix_exercise_ids(question, start_id)
response["questions"] = ExercisesHelper.randomize_mc_options_order(response["questions"])
return response

View File

@@ -34,22 +34,22 @@
"options": [
{
"id": "A",
"text": "And"
"text": "This"
},
{
"id": "B",
"text": "Cat"
"text": "Those"
},
{
"id": "C",
"text": "Happy"
"text": "These"
},
{
"id": "D",
"text": "Jump"
"text": "That"
}
],
"prompt": "Which of the following is a conjunction?",
"prompt": "_____ man there is very kind.",
"solution": "A",
"variant": "text"
}
@@ -62,23 +62,23 @@
"options": [
{
"id": "A",
"text": "a"
"text": "was"
},
{
"id": "B",
"text": "b"
"text": "for work"
},
{
"id": "C",
"text": "c"
"text": "because"
},
{
"id": "D",
"text": "d"
"text": "could"
}
],
"prompt": "prompt",
"solution": "A",
"prompt": "I <u>was</u> late <u>for work</u> yesterday <u>because</u> I <u>could</u> start my car.",
"solution": "D",
"variant": "text"
}
]

View File

@@ -1,18 +1,17 @@
import aiofiles
import os
import uuid
from logging import getLogger
from typing import Dict, Any, Tuple, Coroutine
from typing import Dict, Any, Coroutine
import pdfplumber
from fastapi import UploadFile
from app.services.abc import ILLMService
from app.helpers import LoggerHelper, FileHelper
from app.mappers import ExamMapper
from app.mappers import LevelMapper
from app.dtos.exam import Exam
from app.dtos.exams.level import Exam
from app.dtos.sheet import Sheet
@@ -21,17 +20,15 @@ class UploadLevelModule:
self._logger = getLogger(__name__)
self._llm = openai
# TODO: create a doc in firestore with a status and get its id, run this in a thread and modify the doc in
# firestore, return the id right away, in generation view poll for the id
async def generate_level_from_file(self, file: UploadFile) -> Dict[str, Any] | None:
ext, path_id = await self._save_upload(file)
ext, path_id = await FileHelper.save_upload(file)
FileHelper.convert_file_to_pdf(
f'./tmp/{path_id}/uploaded.{ext}', f'./tmp/{path_id}/exercises.pdf'
f'./tmp/{path_id}/upload.{ext}', f'./tmp/{path_id}/exercises.pdf'
)
file_has_images = self._check_pdf_for_images(f'./tmp/{path_id}/exercises.pdf')
if not file_has_images:
FileHelper.convert_file_to_html(f'./tmp/{path_id}/uploaded.{ext}', f'./tmp/{path_id}/exercises.html')
FileHelper.convert_file_to_html(f'./tmp/{path_id}/upload.{ext}', f'./tmp/{path_id}/exercises.html')
completion: Coroutine[Any, Any, Exam] = (
self._png_completion(path_id) if file_has_images else self._html_completion(path_id)
@@ -41,7 +38,7 @@ class UploadLevelModule:
FileHelper.remove_directory(f'./tmp/{path_id}')
if response:
return self.fix_ids(response.dict(exclude_none=True))
return self.fix_ids(response.model_dump(exclude_none=True))
return None
@staticmethod
@@ -53,20 +50,6 @@ class UploadLevelModule:
return True
return False
@staticmethod
async def _save_upload(file: UploadFile) -> Tuple[str, str]:
ext = file.filename.split('.')[-1]
path_id = str(uuid.uuid4())
os.makedirs(f'./tmp/{path_id}', exist_ok=True)
tmp_filename = f'./tmp/{path_id}/uploaded.{ext}'
file_bytes: bytes = await file.read()
async with aiofiles.open(tmp_filename, 'wb') as file:
await file.write(file_bytes)
return ext, path_id
def _level_json_schema(self):
return {
"parts": [
@@ -91,7 +74,7 @@ class UploadLevelModule:
"content": html
}
],
ExamMapper.map_to_exam_model,
LevelMapper.map_to_exam_model,
str(self._level_json_schema())
)
@@ -237,7 +220,7 @@ class UploadLevelModule:
sheet = await self._png_batch(path_id, batch, json_schema)
sheet.batch = i + 1
components.append(sheet.dict())
components.append(sheet.model_dump())
batches = {"batches": components}
@@ -253,7 +236,7 @@ class UploadLevelModule:
]
}
],
ExamMapper.map_to_sheet,
LevelMapper.map_to_sheet,
str(json_schema)
)
@@ -326,67 +309,10 @@ class UploadLevelModule:
"content": str(batches)
}
],
ExamMapper.map_to_exam_model,
LevelMapper.map_to_exam_model,
str(self._level_json_schema())
)
def _gpt_instructions_batches(self):
return {
"role": "system",
"content": (
'You are helpfull assistant. Your task is to merge multiple batches of english question sheet '
'components and solve the questions. Each batch may contain overlapping content with the previous '
'batch, or close enough content which needs to be excluded. The components are as follows:'
'- Part, a standalone part or part of a section of the question sheet: '
'{"type": "part", "part": "<name or number of the part>"}\n'
'- Multiple Choice Question, there are three types of multiple choice questions that differ on '
'the prompt field of the template: blanks, underlines and normal. '
'In a blanks question, the prompt has underscores to represent the blank space, you must select the '
'appropriate option to solve it.'
'In a underlines question, the prompt has 4 underlines represented by the html tags <u></u>, you must '
'select the option that makes the prompt incorrect to solve it. If the options order doesn\'t reflect '
'the order in which the underlines appear in the prompt you will need to fix it.'
'In a normal question there isn\'t either blanks or underlines in the prompt, you should just '
'select the appropriate solution.'
f'The template for these questions is the same: {self._multiple_choice_png()}\n'
'- Reading Passages, there are two types of reading passages with different templates. The one with '
'type "blanksPassage" where the text field holds the passage and a blank is represented by '
'{{<some number>}} and the other one with type "passage" that has the context field with just '
'reading passages. For both of these components you will have to remove any additional data that might '
'be related to a question description and also remove some "(<question id>)" and "_" from blanksPassage'
' if there are any. These components are used in conjunction with other ones.'
'- Blanks Options, options for a blanks reading passage exercise, this type of component is a group of '
'options with the question id and the options from a to d. The template is: '
f'{self._passage_blank_space_png()}\n\n'
'Now that you know the possible components here\'s what I want you to do:\n'
'1. Remove duplicates. A batch will have duplicates of other batches and the components of '
'the next batch should always take precedence over the previous one batch, what I mean by this is that '
'if batch 1 has, for example, multiple choice question with id 10 and the next one also has id 10, '
'you pick the next one.\n'
'2. Solve the exercises. There are 4 types of exercises, the 3 multipleChoice variants + a fill blanks '
'exercise. For the multiple choice question follow the previous instruction to solve them and place '
f'them in this format: {self._multiple_choice_html()}. For the fill blanks exercises you need to match '
'the correct blanksPassage to the correct fillBlanks options and then pick the correct option. Here is '
f'the template for this exercise: {self._passage_blank_space_html()}.\n'
f'3. Restructure the JSON to match this template: {self._level_json_schema()}. '
f'You must group the exercises by the parts in the order they appear in the batches components. '
f'The context field of a part is the context of a passage component that has text relevant to normal '
f'multiple choice questions.\n'
'Do your utmost to fullfill the requisites, make sure you include all non-duplicate questions'
'in your response and correctly structure the JSON.'
)
}
@staticmethod
def fix_ids(response):
counter = 1

View File

@@ -1,492 +0,0 @@
import queue
import uuid
from logging import getLogger
from queue import Queue
import random
from typing import Dict, List
from app.repositories.abc import IFileStorage, IDocumentStore
from app.services.abc import IListeningService, ILLMService, ITextToSpeechService
from app.configs.question_templates import getListeningTemplate, getListeningPartTemplate
from app.configs.constants import (
NeuralVoices, GPTModels, TemperatureSettings, FilePaths, MinTimers, ExamVariant, EducationalContent,
FieldsAndExercises
)
from app.helpers import ExercisesHelper, FileHelper
class ListeningService(IListeningService):
CONVERSATION_TAIL = (
"Please include random names and genders for the characters in your dialogue. "
"Make sure that the generated conversation does not contain forbidden subjects in muslim countries."
)
MONOLOGUE_TAIL = (
"Make sure that the generated monologue does not contain forbidden subjects in muslim countries."
)
def __init__(
self, llm: ILLMService,
tts: ITextToSpeechService,
file_storage: IFileStorage,
document_store: IDocumentStore
):
self._llm = llm
self._tts = tts
self._file_storage = file_storage
self._document_store = document_store
self._logger = getLogger(__name__)
self._sections = {
"section_1": {
"topic": EducationalContent.TWO_PEOPLE_SCENARIOS,
"exercise_types": FieldsAndExercises.LISTENING_1_EXERCISE_TYPES,
"exercise_sample_size": 1,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_1_EXERCISES,
"start_id": 1,
"generate_dialogue": self._generate_listening_conversation,
"type": "conversation",
},
"section_2": {
"topic": EducationalContent.SOCIAL_MONOLOGUE_CONTEXTS,
"exercise_types": FieldsAndExercises.LISTENING_2_EXERCISE_TYPES,
"exercise_sample_size": 2,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_2_EXERCISES,
"start_id": 11,
"generate_dialogue": self._generate_listening_monologue,
"type": "monologue",
},
"section_3": {
"topic": EducationalContent.FOUR_PEOPLE_SCENARIOS,
"exercise_types": FieldsAndExercises.LISTENING_3_EXERCISE_TYPES,
"exercise_sample_size": 1,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_3_EXERCISES,
"start_id": 21,
"generate_dialogue": self._generate_listening_conversation,
"type": "conversation",
},
"section_4": {
"topic": EducationalContent.ACADEMIC_SUBJECTS,
"exercise_types": FieldsAndExercises.LISTENING_EXERCISE_TYPES,
"exercise_sample_size": 2,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_4_EXERCISES,
"start_id": 31,
"generate_dialogue": self._generate_listening_monologue,
"type": "monologue"
}
}
async def get_listening_question(
self, section_id: int, topic: str, req_exercises: List[str], difficulty: str,
number_of_exercises_q=queue.Queue(), start_id=-1
):
FileHelper.delete_files_older_than_one_day(FilePaths.AUDIO_FILES_PATH)
section = self._sections[f"section_{section_id}"]
if not topic:
topic = random.choice(section["topic"])
if len(req_exercises) == 0:
req_exercises = random.sample(section["exercise_types"], section["exercise_sample_size"])
if number_of_exercises_q.empty():
number_of_exercises_q = ExercisesHelper.divide_number_into_parts(
section["total_exercises"], len(req_exercises)
)
if start_id == -1:
start_id = section["start_id"]
dialog = await self.generate_listening_question(section_id, topic)
if section_id in {1, 3}:
dialog = self.parse_conversation(dialog)
self._logger.info(f'Generated {section["type"]}: {dialog}')
exercises = await self.generate_listening_exercises(
section_id, str(dialog), req_exercises, number_of_exercises_q, start_id, difficulty
)
return {
"exercises": exercises,
"text": dialog,
"difficulty": difficulty
}
async def generate_listening_question(self, section: int, topic: str):
return await self._sections[f'section_{section}']["generate_dialogue"](section, topic)
async def generate_listening_exercises(
self, section: int, dialog: str,
req_exercises: list[str], number_of_exercises_q: Queue,
start_id: int, difficulty: str
):
dialog_type = self._sections[f'section_{section}']["type"]
exercises = []
for req_exercise in req_exercises:
number_of_exercises = number_of_exercises_q.get()
if req_exercise == "multipleChoice" or req_exercise == "multipleChoice3Options":
n_options = 4 if "multipleChoice" else 3
question = await self._gen_multiple_choice_exercise_listening(
dialog_type, dialog, number_of_exercises, start_id, difficulty, n_options
)
exercises.append(question)
print("Added multiple choice: " + str(question))
elif req_exercise == "writeBlanksQuestions":
question = await self._gen_write_blanks_questions_exercise_listening(
dialog_type, dialog, number_of_exercises, start_id, difficulty
)
exercises.append(question)
print("Added write blanks questions: " + str(question))
elif req_exercise == "writeBlanksFill":
question = await self._gen_write_blanks_notes_exercise_listening(
dialog_type, dialog, number_of_exercises, start_id, difficulty
)
exercises.append(question)
print("Added write blanks notes: " + str(question))
elif req_exercise == "writeBlanksForm":
question = await self._gen_write_blanks_form_exercise_listening(
dialog_type, dialog, number_of_exercises, start_id, difficulty
)
exercises.append(question)
print("Added write blanks form: " + str(question))
start_id = start_id + number_of_exercises
return exercises
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str, listening_id: str):
template = getListeningTemplate()
template['difficulty'] = difficulty
for i, part in enumerate(parts, start=0):
part_template = getListeningPartTemplate()
file_name = str(uuid.uuid4()) + ".mp3"
sound_file_path = FilePaths.AUDIO_FILES_PATH + file_name
firebase_file_path = FilePaths.FIREBASE_LISTENING_AUDIO_FILES_PATH + file_name
if "conversation" in part["text"]:
await self._tts.text_to_speech(part["text"]["conversation"], sound_file_path)
else:
await self._tts.text_to_speech(part["text"], sound_file_path)
file_url = await self._file_storage.upload_file_firebase_get_url(firebase_file_path, sound_file_path)
part_template["audio"]["source"] = file_url
part_template["exercises"] = part["exercises"]
template['parts'].append(part_template)
if min_timer != MinTimers.LISTENING_MIN_TIMER_DEFAULT:
template["minTimer"] = min_timer
template["variant"] = ExamVariant.PARTIAL.value
else:
template["variant"] = ExamVariant.FULL.value
listening_id = await self._document_store.save_to_db_with_id("listening", template, listening_id)
if listening_id:
return {**template, "id": listening_id}
else:
raise Exception("Failed to save question: " + str(parts))
# ==================================================================================================================
# generate_listening_question helpers
# ==================================================================================================================
async def _generate_listening_conversation(self, section: int, topic: str) -> Dict:
head = (
'Compose an authentic conversation between two individuals in the everyday social context of "'
if section == 1 else
'Compose an authentic and elaborate conversation between up to four individuals in the everyday '
'social context of "'
)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"conversation": [{"name": "name", "gender": "gender", "text": "text"}]}')
},
{
"role": "user",
"content": (
f'{head}{topic}". {self.CONVERSATION_TAIL}'
)
}
]
if section == 1:
messages.extend([
{
"role": "user",
"content": 'Try to have misleading discourse (refer multiple dates, multiple colors and etc).'
},
{
"role": "user",
"content": 'Try to have spelling of names (cities, people, etc)'
}
])
response = await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
["conversation"],
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return self._get_conversation_voices(response, True)
async def _generate_listening_monologue(self, section: int, topic: str) -> Dict:
head = (
'Generate a comprehensive monologue set in the social context of'
if section == 2 else
'Generate a comprehensive and complex monologue on the academic subject of'
)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"monologue": "monologue"}')
},
{
"role": "user",
"content": (
f'{head}: "{topic}". {self.MONOLOGUE_TAIL}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
["monologue"],
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return response["monologue"]
def _get_conversation_voices(self, response: Dict, unique_voices_across_segments: bool):
chosen_voices = []
name_to_voice = {}
for segment in response['conversation']:
if 'voice' not in segment:
name = segment['name']
if name in name_to_voice:
voice = name_to_voice[name]
else:
voice = None
# section 1
if unique_voices_across_segments:
while voice is None:
chosen_voice = self._get_random_voice(segment['gender'])
if chosen_voice not in chosen_voices:
voice = chosen_voice
chosen_voices.append(voice)
# section 3
else:
voice = self._get_random_voice(segment['gender'])
name_to_voice[name] = voice
segment['voice'] = voice
return response
@staticmethod
def _get_random_voice(gender: str):
if gender.lower() == 'male':
available_voices = NeuralVoices.MALE_NEURAL_VOICES
else:
available_voices = NeuralVoices.FEMALE_NEURAL_VOICES
return random.choice(available_voices)['Id']
# ==================================================================================================================
# generate_listening_exercises helpers
# ==================================================================================================================
async def _gen_multiple_choice_exercise_listening(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str, n_options: int = 4
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"questions": [{"id": "9", "options": [{"id": "A", "text": "Economic benefits"}, {"id": "B", "text": '
'"Government regulations"}, {"id": "C", "text": "Concerns about climate change"}, {"id": "D", "text": '
'"Technological advancement"}], "prompt": "What is the main reason for the shift towards renewable '
'energy sources?", "solution": "C", "variant": "text"}]}')
},
{
"role": "user",
"content": (
f'Generate {quantity} {difficulty} difficulty multiple choice questions of {n_options} '
f'options for this {dialog_type}:\n"' + text + '"')
}
]
questions = await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
["questions"],
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return {
"id": str(uuid.uuid4()),
"prompt": "Select the appropriate option.",
"questions": ExercisesHelper.fix_exercise_ids(questions, start_id)["questions"],
"type": "multipleChoice",
}
async def _gen_write_blanks_questions_exercise_listening(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"questions": [{"question": question, "possible_answers": ["answer_1", "answer_2"]}]}')
},
{
"role": "user",
"content": (
f'Generate {quantity} {difficulty} difficulty short answer questions, and the '
f'possible answers (max 3 words per answer), about this {dialog_type}:\n"{text}"')
}
]
questions = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = questions["questions"][:quantity]
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": f"You will hear a {dialog_type}. Answer the questions below using no more than three words or a number accordingly.",
"solutions": ExercisesHelper.build_write_blanks_solutions(questions, start_id),
"text": ExercisesHelper.build_write_blanks_text(questions, start_id),
"type": "writeBlanks"
}
async def _gen_write_blanks_notes_exercise_listening(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"notes": ["note_1", "note_2"]}')
},
{
"role": "user",
"content": (
f'Generate {quantity} {difficulty} difficulty notes taken from this '
f'{dialog_type}:\n"{text}"'
)
}
]
questions = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["notes"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = questions["notes"][:quantity]
formatted_phrases = "\n".join([f"{i + 1}. {phrase}" for i, phrase in enumerate(questions)])
word_messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this '
'format: {"words": ["word_1", "word_2"] }'
)
},
{
"role": "user",
"content": ('Select 1 word from each phrase in this list:\n"' + formatted_phrases + '"')
}
]
words = await self._llm.prediction(
GPTModels.GPT_4_O, word_messages, ["words"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
words = words["words"][:quantity]
replaced_notes = ExercisesHelper.replace_first_occurrences_with_placeholders_notes(questions, words, start_id)
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": "Fill the blank space with the word missing from the audio.",
"solutions": ExercisesHelper.build_write_blanks_solutions_listening(words, start_id),
"text": "\\n".join(replaced_notes),
"type": "writeBlanks"
}
async def _gen_write_blanks_form_exercise_listening(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"form": ["key: value", "key2: value"]}')
},
{
"role": "user",
"content": (
f'Generate a form with {quantity} {difficulty} difficulty key-value pairs '
f'about this {dialog_type}:\n"{text}"'
)
}
]
if dialog_type == "conversation":
messages.append({
"role": "user",
"content": (
'It must be a form and not questions. '
'Example: {"form": ["Color of car": "blue", "Brand of car": "toyota"]}'
)
})
parsed_form = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["form"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
parsed_form = parsed_form["form"][:quantity]
replaced_form, words = ExercisesHelper.build_write_blanks_text_form(parsed_form, start_id)
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": f"You will hear a {dialog_type}. Fill the form with words/numbers missing.",
"solutions": ExercisesHelper.build_write_blanks_solutions_listening(words, start_id),
"text": replaced_form,
"type": "writeBlanks"
}
@staticmethod
def parse_conversation(conversation_data):
conversation_list = conversation_data.get('conversation', [])
readable_text = []
for message in conversation_list:
name = message.get('name', 'Unknown')
text = message.get('text', '')
readable_text.append(f"{name}: {text}")
return "\n".join(readable_text)

View File

@@ -0,0 +1,294 @@
import queue
import uuid
from logging import getLogger
from queue import Queue
import random
from typing import Dict, List
from starlette.datastructures import UploadFile
from app.dtos.listening import GenerateListeningExercises
from app.repositories.abc import IFileStorage, IDocumentStore
from app.services.abc import IListeningService, ILLMService, ITextToSpeechService, ISpeechToTextService
from app.configs.question_templates import getListeningTemplate, getListeningPartTemplate
from app.configs.constants import (
NeuralVoices, GPTModels, TemperatureSettings, FilePaths, MinTimers, ExamVariant, EducationalContent,
FieldsAndExercises
)
from app.helpers import ExercisesHelper, FileHelper
from .multiple_choice import MultipleChoice
from .write_blank_forms import WriteBlankForms
from .write_blanks import WriteBlanks
from .write_blank_notes import WriteBlankNotes
class ListeningService(IListeningService):
CONVERSATION_TAIL = (
"Please include random names and genders for the characters in your dialogue. "
"Make sure that the generated conversation does not contain forbidden subjects in muslim countries."
)
MONOLOGUE_TAIL = (
"Make sure that the generated monologue does not contain forbidden subjects in muslim countries."
)
def __init__(
self, llm: ILLMService,
tts: ITextToSpeechService,
stt: ISpeechToTextService,
file_storage: IFileStorage,
document_store: IDocumentStore
):
self._llm = llm
self._tts = tts
self._stt = stt
self._file_storage = file_storage
self._document_store = document_store
self._logger = getLogger(__name__)
self._multiple_choice = MultipleChoice(llm)
self._write_blanks = WriteBlanks(llm)
self._write_blanks_forms = WriteBlankForms(llm)
self._write_blanks_notes = WriteBlankNotes(llm)
self._sections = {
"section_1": {
"topic": EducationalContent.TWO_PEOPLE_SCENARIOS,
"exercise_types": FieldsAndExercises.LISTENING_1_EXERCISE_TYPES,
"exercise_sample_size": 1,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_1_EXERCISES,
"generate_dialogue": self._generate_listening_conversation,
"type": "conversation",
},
"section_2": {
"topic": EducationalContent.SOCIAL_MONOLOGUE_CONTEXTS,
"exercise_types": FieldsAndExercises.LISTENING_2_EXERCISE_TYPES,
"exercise_sample_size": 2,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_2_EXERCISES,
"generate_dialogue": self._generate_listening_monologue,
"type": "monologue",
},
"section_3": {
"topic": EducationalContent.FOUR_PEOPLE_SCENARIOS,
"exercise_types": FieldsAndExercises.LISTENING_3_EXERCISE_TYPES,
"exercise_sample_size": 1,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_3_EXERCISES,
"generate_dialogue": self._generate_listening_conversation,
"type": "conversation",
},
"section_4": {
"topic": EducationalContent.ACADEMIC_SUBJECTS,
"exercise_types": FieldsAndExercises.LISTENING_EXERCISE_TYPES,
"exercise_sample_size": 2,
"total_exercises": FieldsAndExercises.TOTAL_LISTENING_SECTION_4_EXERCISES,
"generate_dialogue": self._generate_listening_monologue,
"type": "monologue"
}
}
async def generate_listening_dialog(self, section: int, topic: str, difficulty: str):
return await self._sections[f'section_{section}']["generate_dialogue"](section, topic)
async def get_dialog_from_audio(self, upload: UploadFile):
ext, path_id = await FileHelper.save_upload(upload)
dialog = await self._stt.speech_to_text(f'./tmp/{path_id}/upload.{ext}')
FileHelper.remove_directory(f'./tmp/{path_id}')
async def get_listening_question(self, section: int, dto: GenerateListeningExercises):
dialog_type = self._sections[f'section_{section}']["type"]
exercises = []
start_id = 1
for req_exercise in dto.exercises:
if req_exercise.type == "multipleChoice" or req_exercise.type == "multipleChoice3Options":
n_options = 4 if "multipleChoice" else 3
question = await self._multiple_choice.gen_multiple_choice(
dialog_type, dto.text, req_exercise.quantity, start_id, dto.difficulty, n_options
)
exercises.append(question)
self._logger.info(f"Added multiple choice: {question}")
elif req_exercise.type == "writeBlanksQuestions":
question = await self._write_blanks.gen_write_blanks_questions(
dialog_type, dto.text, req_exercise.quantity, start_id, dto.difficulty
)
question["variant"] = "questions"
exercises.append(question)
self._logger.info(f"Added write blanks questions: {question}")
elif req_exercise.type == "writeBlanksFill":
question = await self._write_blanks_notes.gen_write_blanks_notes(
dialog_type, dto.text, req_exercise.quantity, start_id, dto.difficulty
)
question["variant"] = "fill"
exercises.append(question)
self._logger.info(f"Added write blanks notes: {question}")
elif req_exercise.type == "writeBlanksForm":
question = await self._write_blanks_forms.gen_write_blanks_form(
dialog_type, dto.text, req_exercise.quantity, start_id, dto.difficulty
)
question["variant"] = "form"
exercises.append(question)
self._logger.info(f"Added write blanks form: {question}")
start_id = start_id + req_exercise.quantity
return {"exercises": exercises}
async def save_listening(self, parts: list[dict], min_timer: int, difficulty: str, listening_id: str):
template = getListeningTemplate()
template['difficulty'] = difficulty
for i, part in enumerate(parts, start=0):
part_template = getListeningPartTemplate()
file_name = str(uuid.uuid4()) + ".mp3"
sound_file_path = FilePaths.AUDIO_FILES_PATH + file_name
firebase_file_path = FilePaths.FIREBASE_LISTENING_AUDIO_FILES_PATH + file_name
if "conversation" in part["text"]:
await self._tts.text_to_speech(part["text"]["conversation"], sound_file_path)
else:
await self._tts.text_to_speech(part["text"], sound_file_path)
file_url = await self._file_storage.upload_file_firebase_get_url(firebase_file_path, sound_file_path)
part_template["audio"]["source"] = file_url
part_template["exercises"] = part["exercises"]
template['parts'].append(part_template)
if min_timer != MinTimers.LISTENING_MIN_TIMER_DEFAULT:
template["minTimer"] = min_timer
template["variant"] = ExamVariant.PARTIAL.value
else:
template["variant"] = ExamVariant.FULL.value
listening_id = await self._document_store.save_to_db("listening", template, listening_id)
if listening_id:
return {**template, "id": listening_id}
else:
raise Exception("Failed to save question: " + str(parts))
# ==================================================================================================================
# generate_listening_question helpers
# ==================================================================================================================
async def _generate_listening_conversation(self, section: int, topic: str) -> Dict:
head = (
'Compose an authentic conversation between two individuals in the everyday social context of "'
if section == 1 else
'Compose an authentic and elaborate conversation between up to four individuals in the everyday '
'social context of "'
)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"conversation": [{"name": "name", "gender": "gender", "text": "text"}]}')
},
{
"role": "user",
"content": (
f'{head}{topic}". {self.CONVERSATION_TAIL}'
)
}
]
if section == 1:
messages.extend([
{
"role": "user",
"content": 'Try to have misleading discourse (refer multiple dates, multiple colors and etc).'
},
{
"role": "user",
"content": 'Try to have spelling of names (cities, people, etc)'
}
])
response = await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
["conversation"],
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
conversation = self._get_conversation_voices(response, True)
return {"dialog": conversation["conversation"]}
async def _generate_listening_monologue(self, section: int, topic: str) -> Dict:
head = (
'Generate a comprehensive monologue set in the social context of'
if section == 2 else
'Generate a comprehensive and complex monologue on the academic subject of'
)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"monologue": "monologue"}')
},
{
"role": "user",
"content": (
f'{head}: "{topic}". {self.MONOLOGUE_TAIL}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
["monologue"],
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return {"dialog": response["monologue"]}
def _get_conversation_voices(self, response: Dict, unique_voices_across_segments: bool):
chosen_voices = []
name_to_voice = {}
for segment in response['conversation']:
if 'voice' not in segment:
name = segment['name']
if name in name_to_voice:
voice = name_to_voice[name]
else:
voice = None
# section 1
if unique_voices_across_segments:
while voice is None:
chosen_voice = self._get_random_voice(segment['gender'])
if chosen_voice not in chosen_voices:
voice = chosen_voice
chosen_voices.append(voice)
# section 3
else:
voice = self._get_random_voice(segment['gender'])
name_to_voice[name] = voice
segment['voice'] = voice
return response
@staticmethod
def _get_random_voice(gender: str):
if gender.lower() == 'male':
available_voices = NeuralVoices.MALE_NEURAL_VOICES
else:
available_voices = NeuralVoices.FEMALE_NEURAL_VOICES
return random.choice(available_voices)['Id']
@staticmethod
def parse_conversation(conversation_data):
conversation_list = conversation_data.get('conversation', [])
readable_text = []
for message in conversation_list:
name = message.get('name', 'Unknown')
text = message.get('text', '')
readable_text.append(f"{name}: {text}")
return "\n".join(readable_text)

View File

@@ -0,0 +1,46 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class MultipleChoice:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_multiple_choice(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str, n_options: int = 4
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"questions": [{"id": "9", "options": [{"id": "A", "text": "Economic benefits"}, {"id": "B", "text": '
'"Government regulations"}, {"id": "C", "text": "Concerns about climate change"}, {"id": "D", "text": '
'"Technological advancement"}], "prompt": "What is the main reason for the shift towards renewable '
'energy sources?", "solution": "C", "variant": "text"}]}')
},
{
"role": "user",
"content": (
f'Generate {quantity} {difficulty} difficulty multiple choice questions of {n_options} '
f'options for this {dialog_type}:\n"' + text + '"')
}
]
questions = await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
["questions"],
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
return {
"id": str(uuid.uuid4()),
"prompt": "Select the appropriate option.",
"questions": ExercisesHelper.fix_exercise_ids(questions, start_id)["questions"],
"type": "multipleChoice",
}

View File

@@ -0,0 +1,55 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class WriteBlankForms:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_write_blanks_form(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"form": ["key: value", "key2: value"]}')
},
{
"role": "user",
"content": (
f'Generate a form with {quantity} {difficulty} difficulty key-value pairs '
f'about this {dialog_type}:\n"{text}"'
)
}
]
if dialog_type == "conversation":
messages.append({
"role": "user",
"content": (
'It must be a form and not questions. '
'Example: {"form": ["Color of car": "blue", "Brand of car": "toyota"]}'
)
})
parsed_form = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["form"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
parsed_form = parsed_form["form"][:quantity]
replaced_form, words = ExercisesHelper.build_write_blanks_text_form(parsed_form, start_id)
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": f"You will hear a {dialog_type}. Fill the form with words/numbers missing.",
"solutions": ExercisesHelper.build_write_blanks_solutions_listening(words, start_id),
"text": replaced_form,
"type": "writeBlanks"
}

View File

@@ -0,0 +1,68 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class WriteBlankNotes:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_write_blanks_notes(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"notes": ["note_1", "note_2"]}')
},
{
"role": "user",
"content": (
f'Generate {quantity} {difficulty} difficulty notes taken from this '
f'{dialog_type}:\n"{text}"'
)
}
]
questions = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["notes"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = questions["notes"][:quantity]
formatted_phrases = "\n".join([f"{i + 1}. {phrase}" for i, phrase in enumerate(questions)])
word_messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this '
'format: {"words": ["word_1", "word_2"] }'
)
},
{
"role": "user",
"content": ('Select 1 word from each phrase in this list:\n"' + formatted_phrases + '"')
}
]
words = await self._llm.prediction(
GPTModels.GPT_4_O, word_messages, ["words"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
words = words["words"][:quantity]
replaced_notes = ExercisesHelper.replace_first_occurrences_with_placeholders_notes(questions, words, start_id)
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": "Fill the blank space with the word missing from the audio.",
"solutions": ExercisesHelper.build_write_blanks_solutions_listening(words, start_id),
"text": "\\n".join(replaced_notes),
"type": "writeBlanks"
}

View File

@@ -0,0 +1,43 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class WriteBlanks:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_write_blanks_questions(
self, dialog_type: str, text: str, quantity: int, start_id: int, difficulty: str
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"questions": [{"question": question, "possible_answers": ["answer_1", "answer_2"]}]}')
},
{
"role": "user",
"content": (
f'Generate {quantity} {difficulty} difficulty short answer questions, and the '
f'possible answers (max 3 words per answer), about this {dialog_type}:\n"{text}"')
}
]
questions = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = questions["questions"][:quantity]
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": f"You will hear a {dialog_type}. Answer the questions below using no more than three words or a number accordingly.",
"solutions": ExercisesHelper.build_write_blanks_solutions(questions, start_id),
"text": ExercisesHelper.build_write_blanks_text(questions, start_id),
"type": "writeBlanks"
}

View File

@@ -1,349 +0,0 @@
import random
import uuid
from queue import Queue
from typing import List
from app.services.abc import IReadingService, ILLMService
from app.configs.constants import QuestionType, TemperatureSettings, FieldsAndExercises, GPTModels
from app.helpers import ExercisesHelper
class ReadingService(IReadingService):
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_reading_passage(
self,
part: int,
topic: str,
req_exercises: List[str],
number_of_exercises_q: Queue,
difficulty: str,
start_id: int
):
passage = await self.generate_reading_passage(part, topic)
exercises = await self._generate_reading_exercises(
passage["text"], req_exercises, number_of_exercises_q, start_id, difficulty
)
if ExercisesHelper.contains_empty_dict(exercises):
return await self.gen_reading_passage(
part, topic, req_exercises, number_of_exercises_q, difficulty, start_id
)
return {
"exercises": exercises,
"text": {
"content": passage["text"],
"title": passage["title"]
},
"difficulty": difficulty
}
async def generate_reading_passage(self, part: int, topic: str, word_count: int = 800):
part_system_message = {
"1": 'The generated text should be fairly easy to understand and have multiple paragraphs.',
"2": 'The generated text should be fairly hard to understand and have multiple paragraphs.',
"3": (
'The generated text should be very hard to understand and include different points, theories, '
'subtle differences of opinions from people, correctly sourced to the person who said it, '
'over the specified topic and have multiple paragraphs.'
)
}
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"title": "title of the text", "text": "generated text"}')
},
{
"role": "user",
"content": (
f'Generate an extensive text for IELTS Reading Passage {part}, of at least {word_count} words, '
f'on the topic of "{topic}". The passage should offer a substantial amount of '
'information, analysis, or narrative relevant to the chosen subject matter. This text '
'passage aims to serve as the primary reading section of an IELTS test, providing an '
'in-depth and comprehensive exploration of the topic. Make sure that the generated text '
'does not contain forbidden subjects in muslim countries.'
)
},
{
"role": "system",
"content": part_system_message[str(part)]
}
]
if part == 3:
messages.append({
"role": "user",
"content": "Use real text excerpts on you generated passage and cite the sources."
})
return await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
FieldsAndExercises.GEN_TEXT_FIELDS,
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
async def _generate_reading_exercises(
self, passage: str, req_exercises: list, number_of_exercises_q, start_id, difficulty
):
exercises = []
for req_exercise in req_exercises:
number_of_exercises = number_of_exercises_q.get()
if req_exercise == "fillBlanks":
question = await self._gen_summary_fill_blanks_exercise(
passage, number_of_exercises, start_id, difficulty
)
exercises.append(question)
print("Added fill blanks: " + str(question))
elif req_exercise == "trueFalse":
question = await self._gen_true_false_not_given_exercise(
passage, number_of_exercises, start_id, difficulty
)
exercises.append(question)
print("Added trueFalse: " + str(question))
elif req_exercise == "writeBlanks":
question = await self._gen_write_blanks_exercise(passage, number_of_exercises, start_id, difficulty)
if ExercisesHelper.answer_word_limit_ok(question):
exercises.append(question)
print("Added write blanks: " + str(question))
else:
exercises.append({})
print("Did not add write blanks because it did not respect word limit")
elif req_exercise == "paragraphMatch":
question = await self._gen_paragraph_match_exercise(passage, number_of_exercises, start_id)
exercises.append(question)
print("Added paragraph match: " + str(question))
elif req_exercise == "ideaMatch":
question = await self._gen_idea_match_exercise(passage, number_of_exercises, start_id)
exercises.append(question)
print("Added idea match: " + str(question))
start_id = start_id + number_of_exercises
return exercises
async def _gen_summary_fill_blanks_exercise(
self, text: str, quantity: int, start_id, difficulty, num_random_words: int = 1
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: { "summary": "summary" }'
)
},
{
"role": "user",
"content": f'Summarize this text: "{text}"'
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["summary"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"words": ["word_1", "word_2"] }'
)
},
{
"role": "user",
"content": (
f'Select {quantity} {difficulty} difficulty words, it must be words and not expressions, '
f'from this:\n{response["summary"]}'
)
}
]
words_response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["words"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
response["words"] = words_response["words"]
replaced_summary = ExercisesHelper.replace_first_occurrences_with_placeholders(
response["summary"], response["words"], start_id
)
options_words = ExercisesHelper.add_random_words_and_shuffle(response["words"], num_random_words)
solutions = ExercisesHelper.fillblanks_build_solutions_array(response["words"], start_id)
return {
"allowRepetition": True,
"id": str(uuid.uuid4()),
"prompt": (
"Complete the summary below. Write the letter of the corresponding word(s) for it.\\nThere are "
"more words than spaces so you will not use them all. You may use any of the words more than once."
),
"solutions": solutions,
"text": replaced_summary,
"type": "fillBlanks",
"words": options_words
}
async def _gen_true_false_not_given_exercise(self, text: str, quantity: int, start_id, difficulty):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"prompts":[{"prompt": "statement_1", "solution": "true/false/not_given"}, '
'{"prompt": "statement_2", "solution": "true/false/not_given"}]}')
},
{
"role": "user",
"content": (
f'Generate {str(quantity)} {difficulty} difficulty statements based on the provided text. '
'Ensure that your statements accurately represent information or inferences from the text, and '
'provide a variety of responses, including, at least one of each True, False, and Not Given, '
f'as appropriate.\n\nReference text:\n\n {text}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["prompts"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = response["prompts"]
if len(questions) > quantity:
questions = ExercisesHelper.remove_excess_questions(questions, len(questions) - quantity)
for i, question in enumerate(questions, start=start_id):
question["id"] = str(i)
return {
"id": str(uuid.uuid4()),
"prompt": "Do the following statements agree with the information given in the Reading Passage?",
"questions": questions,
"type": "trueFalse"
}
async def _gen_write_blanks_exercise(self, text: str, quantity: int, start_id, difficulty):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"questions": [{"question": question, "possible_answers": ["answer_1", "answer_2"]}]}'
)
},
{
"role": "user",
"content": (
f'Generate {str(quantity)} {difficulty} difficulty short answer questions, and the '
f'possible answers, must have maximum 3 words per answer, about this text:\n"{text}"'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = response["questions"][:quantity]
return {
"id": str(uuid.uuid4()),
"maxWords": 3,
"prompt": "Choose no more than three words and/or a number from the passage for each answer.",
"solutions": ExercisesHelper.build_write_blanks_solutions(questions, start_id),
"text": ExercisesHelper.build_write_blanks_text(questions, start_id),
"type": "writeBlanks"
}
async def _gen_paragraph_match_exercise(self, text: str, quantity: int, start_id):
paragraphs = ExercisesHelper.assign_letters_to_paragraphs(text)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"headings": [ {"heading": "first paragraph heading"}, {"heading": "second paragraph heading"}]}'
)
},
{
"role": "user",
"content": (
'For every paragraph of the list generate a minimum 5 word heading for it. '
f'The paragraphs are these: {str(paragraphs)}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["headings"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
headings = response["headings"]
options = []
for i, paragraph in enumerate(paragraphs, start=0):
paragraph["heading"] = headings[i]["heading"]
options.append({
"id": paragraph["letter"],
"sentence": paragraph["paragraph"]
})
random.shuffle(paragraphs)
sentences = []
for i, paragraph in enumerate(paragraphs, start=start_id):
sentences.append({
"id": i,
"sentence": paragraph["heading"],
"solution": paragraph["letter"]
})
return {
"id": str(uuid.uuid4()),
"allowRepetition": False,
"options": options,
"prompt": "Choose the correct heading for paragraphs from the list of headings below.",
"sentences": sentences[:quantity],
"type": "matchSentences"
}
async def _gen_idea_match_exercise(self, text: str, quantity: int, start_id):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"ideas": [ '
'{"idea": "some idea or opinion", "from": "person, institution whose idea or opinion this is"}, '
'{"idea": "some other idea or opinion", "from": "person, institution whose idea or opinion this is"}'
']}'
)
},
{
"role": "user",
"content": (
f'From the text extract {quantity} ideas, theories, opinions and who they are from. '
f'The text: {text}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["ideas"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
ideas = response["ideas"]
return {
"id": str(uuid.uuid4()),
"allowRepetition": False,
"options": ExercisesHelper.build_options(ideas),
"prompt": "Choose the correct author for the ideas/opinions from the list of authors below.",
"sentences": ExercisesHelper.build_sentences(ideas, start_id),
"type": "matchSentences"
}

View File

@@ -0,0 +1,131 @@
from logging import getLogger
from fastapi import UploadFile
from app.configs.constants import GPTModels, FieldsAndExercises, TemperatureSettings
from app.dtos.reading import ReadingDTO
from app.helpers import ExercisesHelper
from app.services.abc import IReadingService, ILLMService
from .fill_blanks import FillBlanks
from .idea_match import IdeaMatch
from .paragraph_match import ParagraphMatch
from .true_false import TrueFalse
from .import_reading import ImportReadingModule
from .write_blanks import WriteBlanks
class ReadingService(IReadingService):
def __init__(self, llm: ILLMService):
self._llm = llm
self._fill_blanks = FillBlanks(llm)
self._idea_match = IdeaMatch(llm)
self._paragraph_match = ParagraphMatch(llm)
self._true_false = TrueFalse(llm)
self._write_blanks = WriteBlanks(llm)
self._logger = getLogger(__name__)
self._import = ImportReadingModule(llm)
async def import_exam(self, exercises: UploadFile, solutions: UploadFile = None):
return await self._import.import_from_file(exercises, solutions)
async def generate_reading_passage(self, part: int, topic: str, word_count: int = 800):
part_system_message = {
"1": 'The generated text should be fairly easy to understand and have multiple paragraphs.',
"2": 'The generated text should be fairly hard to understand and have multiple paragraphs.',
"3": (
'The generated text should be very hard to understand and include different points, theories, '
'subtle differences of opinions from people, correctly sourced to the person who said it, '
'over the specified topic and have multiple paragraphs.'
)
}
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"title": "title of the text", "text": "generated text"}')
},
{
"role": "user",
"content": (
f'Generate an extensive text for IELTS Reading Passage {part}, of at least {word_count} words, '
f'on the topic of "{topic}". The passage should offer a substantial amount of '
'information, analysis, or narrative relevant to the chosen subject matter. This text '
'passage aims to serve as the primary reading section of an IELTS test, providing an '
'in-depth and comprehensive exploration of the topic. Make sure that the generated text '
'does not contain forbidden subjects in muslim countries.'
)
},
{
"role": "system",
"content": part_system_message[str(part)]
}
]
if part == 3:
messages.append({
"role": "user",
"content": "Use real text excerpts on your generated passage and cite the sources."
})
return await self._llm.prediction(
GPTModels.GPT_4_O,
messages,
FieldsAndExercises.GEN_TEXT_FIELDS,
TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
async def generate_reading_exercises(self, dto: ReadingDTO):
exercises = []
start_id = 1
for req_exercise in dto.exercises:
if req_exercise.type == "fillBlanks":
question = await self._fill_blanks.gen_summary_fill_blanks_exercise(
dto.text, req_exercise.quantity, start_id, dto.difficulty, req_exercise.num_random_words
)
exercises.append(question)
self._logger.info(f"Added fill blanks: {question}")
elif req_exercise.type == "trueFalse":
question = await self._true_false.gen_true_false_not_given_exercise(
dto.text, req_exercise.quantity, start_id, dto.difficulty
)
exercises.append(question)
self._logger.info(f"Added trueFalse: {question}")
elif req_exercise.type == "writeBlanks":
question = await self._write_blanks.gen_write_blanks_exercise(
dto.text, req_exercise.quantity, start_id, dto.difficulty, req_exercise.max_words
)
if ExercisesHelper.answer_word_limit_ok(question):
exercises.append(question)
self._logger.info(f"Added write blanks: {question}")
else:
exercises.append({})
self._logger.info("Did not add write blanks because it did not respect word limit")
elif req_exercise.type == "paragraphMatch":
question = await self._paragraph_match.gen_paragraph_match_exercise(
dto.text, req_exercise.quantity, start_id
)
exercises.append(question)
self._logger.info(f"Added paragraph match: {question}")
elif req_exercise.type == "ideaMatch":
question = await self._idea_match.gen_idea_match_exercise(
dto.text, req_exercise.quantity, start_id
)
question["variant"] = "ideaMatch"
exercises.append(question)
self._logger.info(f"Added idea match: {question}")
start_id = start_id + req_exercise.quantity
return {
"exercises": exercises
}

View File

@@ -0,0 +1,73 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class FillBlanks:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_summary_fill_blanks_exercise(
self, text: str, quantity: int, start_id, difficulty, num_random_words: int = 1
):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: { "summary": "summary" }'
)
},
{
"role": "user",
"content": f'Summarize this text: "{text}"'
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["summary"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"words": ["word_1", "word_2"] }'
)
},
{
"role": "user",
"content": (
f'Select {quantity} {difficulty} difficulty words, it must be words and not expressions, '
f'from this:\n{response["summary"]}'
)
}
]
words_response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["words"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
response["words"] = words_response["words"]
replaced_summary = ExercisesHelper.replace_first_occurrences_with_placeholders(
response["summary"], response["words"], start_id
)
options_words = ExercisesHelper.add_random_words_and_shuffle(response["words"], num_random_words)
solutions = ExercisesHelper.fillblanks_build_solutions_array(response["words"], start_id)
return {
"allowRepetition": True,
"id": str(uuid.uuid4()),
"prompt": (
"Complete the summary below. Write the letter of the corresponding word(s) for it.\\nThere are "
"more words than spaces so you will not use them all. You may use any of the words more than once."
),
"solutions": solutions,
"text": replaced_summary,
"type": "fillBlanks",
"words": options_words
}

View File

@@ -0,0 +1,46 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class IdeaMatch:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_idea_match_exercise(self, text: str, quantity: int, start_id: int):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"ideas": [ '
'{"idea": "some idea or opinion", "from": "person, institution whose idea or opinion this is"}, '
'{"idea": "some other idea or opinion", "from": "person, institution whose idea or opinion this is"}'
']}'
)
},
{
"role": "user",
"content": (
f'From the text extract {quantity} ideas, theories, opinions and who they are from. '
f'The text: {text}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["ideas"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
ideas = response["ideas"]
return {
"id": str(uuid.uuid4()),
"allowRepetition": False,
"options": ExercisesHelper.build_options(ideas),
"prompt": "Choose the correct author for the ideas/opinions from the list of authors below.",
"sentences": ExercisesHelper.build_sentences(ideas, start_id),
"type": "matchSentences"
}

View File

@@ -0,0 +1,190 @@
from logging import getLogger
from typing import Dict, Any
from uuid import uuid4
import aiofiles
from fastapi import UploadFile
from app.helpers import FileHelper
from app.mappers.reading import ReadingMapper
from app.services.abc import ILLMService
from app.dtos.exams.reading import Exam
class ImportReadingModule:
def __init__(self, openai: ILLMService):
self._logger = getLogger(__name__)
self._llm = openai
async def import_from_file(
self, exercises: UploadFile, solutions: UploadFile = None
) -> Dict[str, Any] | None:
path_id = str(uuid4())
ext, _ = await FileHelper.save_upload(exercises, "exercises", path_id)
FileHelper.convert_file_to_html(f'./tmp/{path_id}/exercises.{ext}', f'./tmp/{path_id}/exercises.html')
if solutions:
ext, _ = await FileHelper.save_upload(solutions, "solutions", path_id)
FileHelper.convert_file_to_html(f'./tmp/{path_id}/solutions.{ext}', f'./tmp/{path_id}/solutions.html')
response = await self._get_reading_parts(path_id, solutions is not None)
FileHelper.remove_directory(f'./tmp/{path_id}')
if response:
return response.model_dump(exclude_none=True)
return None
async def _get_reading_parts(self, path_id: str, solutions: bool = False) -> Exam:
async with aiofiles.open(f'./tmp/{path_id}/exercises.html', 'r', encoding='utf-8') as f:
exercises_html = await f.read()
messages = [
self._instructions(),
{
"role": "user",
"content": f"Exam question sheet:\n\n{exercises_html}"
}
]
if solutions:
async with aiofiles.open(f'./tmp/{path_id}/solutions.html', 'r', encoding='utf-8') as f:
solutions_html = await f.read()
messages.append({
"role": "user",
"content": f"Solutions:\n\n{solutions_html}"
})
return await self._llm.pydantic_prediction(
messages,
ReadingMapper.map_to_exam_model,
str(self._reading_json_schema())
)
def _reading_json_schema(self):
json = self._reading_exam_template()
json["parts"][0]["exercises"] = [
self._write_blanks(),
self._fill_blanks(),
self._match_sentences(),
self._true_false()
]
@staticmethod
def _reading_exam_template():
return {
"minTimer": "<number of minutes as int not string>",
"parts": [
{
"text": {
"title": "<title of the passage>",
"content": "<the text of the passage>",
},
"exercises": []
}
]
}
@staticmethod
def _write_blanks():
return {
"maxWords": "<number of max words return the int value not string>",
"solutions": [
{
"id": "<number of the question as string>",
"solution": [
"<at least one solution can have alternative solutions (that dont exceed maxWords)>"
]
},
],
"text": "<all the questions formatted in this way: <question>{{<id>}}\\n<question2>{{<id2>}}\\n >",
"type": "writeBlanks"
}
@staticmethod
def _match_sentences():
return {
"options": [
{
"id": "<uppercase letter that identifies a paragraph>",
"sentence": "<either a heading or an idea>"
}
],
"sentences": [
{
"id": "<the question id not the option id>",
"solution": "<id in options>",
"sentence": "<heading or an idea>",
}
],
"type": "matchSentences",
"variant": "<heading OR ideaMatch (try to figure it out via the exercises instructions)>"
}
@staticmethod
def _true_false():
return {
"questions": [
{
"prompt": "<question>",
"solution": "<can only be one of these [\"true\", \"false\", \"not_given\"]>",
"id": "<the question id>"
}
],
"type": "trueFalse"
}
@staticmethod
def _fill_blanks():
return {
"solutions": [
{
"id": "<blank id>",
"solution": "<word>"
}
],
"text": "<section of text with blanks denoted by {{<blank id>}}>",
"type": "fillBlanks",
"words": [
{
"letter": "<uppercase letter that ids the words (may not be included and if not start at A)>",
"word": "<word>"
}
]
}
def _instructions(self, solutions = False):
solutions_str = " and its solutions" if solutions else ""
tail = (
"The solutions were not supplied so you will have to solve them. Do your utmost to get all the information and"
"all the solutions right!"
if not solutions else
"Do your utmost to correctly identify the sections, its exercises and respective solutions"
)
return {
"role": "system",
"content": (
f"You will receive html pertaining to an english exam question sheet{solutions_str}. Your job is to "
f"structure the data into a single json with this template: {self._reading_exam_template()}\n"
"You will need find out how many parts the exam has a correctly place its exercises. You will "
"encounter 4 types of exercises:\n"
" - \"writeBlanks\": short answer questions that have a answer word limit, generally two or three\n"
" - \"matchSentences\": a sentence needs to be matched with a paragraph\n"
" - \"trueFalse\": questions that its answers can only be true false or not given\n"
" - \"fillBlanks\": a text that has blank spaces on a section of text and a word bank which "
"contains the solutions and sometimes random words to throw off the students\n"
"These 4 types of exercises will need to be placed in the correct json template inside each part, "
"the templates are as follows:\n "
f"writeBlanks: {self._write_blanks()}\n"
f"matchSentences: {self._match_sentences()}\n"
f"trueFalse: {self._true_false()}\n"
f"fillBlanks: {self._fill_blanks()}\n\n"
f"{tail}"
)
}

View File

@@ -0,0 +1,63 @@
import random
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class ParagraphMatch:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_paragraph_match_exercise(self, text: str, quantity: int, start_id: int):
paragraphs = ExercisesHelper.assign_letters_to_paragraphs(text)
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"headings": [ {"heading": "first paragraph heading"}, {"heading": "second paragraph heading"}]}'
)
},
{
"role": "user",
"content": (
'For every paragraph of the list generate a minimum 5 word heading for it. '
f'The paragraphs are these: {str(paragraphs)}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["headings"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
headings = response["headings"]
options = []
for i, paragraph in enumerate(paragraphs, start=0):
paragraph["heading"] = headings[i]["heading"]
options.append({
"id": paragraph["letter"],
"sentence": paragraph["paragraph"]
})
random.shuffle(paragraphs)
sentences = []
for i, paragraph in enumerate(paragraphs, start=start_id):
sentences.append({
"id": i,
"sentence": paragraph["heading"],
"solution": paragraph["letter"]
})
return {
"id": str(uuid.uuid4()),
"allowRepetition": False,
"options": options,
"prompt": "Choose the correct heading for paragraphs from the list of headings below.",
"sentences": sentences[:quantity],
"type": "matchSentences"
}

View File

@@ -0,0 +1,49 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class TrueFalse:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_true_false_not_given_exercise(self, text: str, quantity: int, start_id: int, difficulty: str):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"prompts":[{"prompt": "statement_1", "solution": "true/false/not_given"}, '
'{"prompt": "statement_2", "solution": "true/false/not_given"}]}')
},
{
"role": "user",
"content": (
f'Generate {str(quantity)} {difficulty} difficulty statements based on the provided text. '
'Ensure that your statements accurately represent information or inferences from the text, and '
'provide a variety of responses, including, at least one of each True, False, and Not Given, '
f'as appropriate.\n\nReference text:\n\n {text}'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["prompts"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = response["prompts"]
if len(questions) > quantity:
questions = ExercisesHelper.remove_excess_questions(questions, len(questions) - quantity)
for i, question in enumerate(questions, start=start_id):
question["id"] = str(i)
return {
"id": str(uuid.uuid4()),
"prompt": "Do the following statements agree with the information given in the Reading Passage?",
"questions": questions,
"type": "trueFalse"
}

View File

@@ -0,0 +1,44 @@
import uuid
from app.configs.constants import GPTModels, TemperatureSettings
from app.helpers import ExercisesHelper
from app.services.abc import ILLMService
class WriteBlanks:
def __init__(self, llm: ILLMService):
self._llm = llm
async def gen_write_blanks_exercise(self, text: str, quantity: int, start_id: int, difficulty: str, max_words: int = 3):
messages = [
{
"role": "system",
"content": (
'You are a helpful assistant designed to output JSON on this format: '
'{"questions": [{"question": question, "possible_answers": ["answer_1", "answer_2"]}]}'
)
},
{
"role": "user",
"content": (
f'Generate {str(quantity)} {difficulty} difficulty short answer questions, and the '
f'possible answers, must have maximum {max_words} words per answer, about this text:\n"{text}"'
)
}
]
response = await self._llm.prediction(
GPTModels.GPT_4_O, messages, ["questions"], TemperatureSettings.GEN_QUESTION_TEMPERATURE
)
questions = response["questions"][:quantity]
return {
"id": str(uuid.uuid4()),
"maxWords": max_words,
"prompt": f"Choose no more than {max_words} words and/or a number from the passage for each answer.",
"solutions": ExercisesHelper.build_write_blanks_solutions(questions, start_id),
"text": ExercisesHelper.build_write_blanks_text(questions, start_id),
"type": "writeBlanks"
}

View File

@@ -9,7 +9,7 @@ from app.repositories.abc import IFileStorage, IDocumentStore
from app.services.abc import ISpeakingService, ILLMService, IVideoGeneratorService, ISpeechToTextService
from app.configs.constants import (
FieldsAndExercises, GPTModels, TemperatureSettings,
AvatarEnum, FilePaths
ELAIAvatars, FilePaths
)
from app.helpers import TextHelper
@@ -425,7 +425,7 @@ class SpeakingService(ISpeakingService):
self._logger.info(f'Saved speaking to DB with id {req_id} : {str(template)}')
async def _create_video_per_part(self, exercises: List[Dict], template: Dict, part: int):
avatar = (random.choice(list(AvatarEnum))).value
avatar = (random.choice(list(ELAIAvatars))).name
template_index = part - 1
# Using list comprehension to find the element with the desired value in the 'type' field

View File

@@ -19,7 +19,7 @@ class WritingService(IWritingService):
'You are a helpful assistant designed to output JSON on this format: {"prompt": "prompt content"}'
)
},
*self._get_writing_messages(task, topic, difficulty)
*self._get_writing_args(task, topic, difficulty)
]
llm_model = GPTModels.GPT_3_5_TURBO if task == 1 else GPTModels.GPT_4_O
@@ -40,36 +40,43 @@ class WritingService(IWritingService):
}
@staticmethod
def _get_writing_messages(task: int, topic: str, difficulty: str) -> List[Dict]:
# TODO: Should the muslim disclaimer be added to task 2?
task_prompt = (
'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, '
f'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 '
f'of {difficulty} difficulty and does not contain forbidden subjects in muslim countries.'
) if task == 1 else (
f'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 '
f'analysis of contrasting perspectives on the topic of "{topic}".'
)
task_instructions = (
'The prompt should end with "In the letter you should" followed by 3 bullet points of what '
'the answer should include.'
) if task == 1 else (
'The question should lead to an answer with either "theories", "complicated information" or '
'be "very descriptive" on the topic.'
)
def _get_writing_args(task: int, topic: str, difficulty: str) -> List[Dict]:
writing_args = {
"1": {
"prompt": (
'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, '
f'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 '
f'of {difficulty} difficulty and does not contain forbidden subjects in muslim countries.'
),
"instructions": (
'The prompt should end with "In the letter you should" followed by 3 bullet points of what '
'the answer should include.'
)
},
"2": {
# TODO: Should the muslim disclaimer be here as well?
"prompt": (
f'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 '
f'analysis of contrasting perspectives on the topic of "{topic}".'
),
"instructions": (
'The question should lead to an answer with either "theories", "complicated information" or '
'be "very descriptive" on the topic.'
)
}
}
messages = [
{
"role": "user",
"content": task_prompt
"content": writing_args[str(task)]["prompt"]
},
{
"role": "user",
"content": task_instructions
"content": writing_args[str(task)]["instructions"]
}
]

View File

@@ -3,11 +3,13 @@ from .heygen import Heygen
from .openai import OpenAI
from .whisper import OpenAIWhisper
from .gpt_zero import GPTZero
from .elai import ELAI
__all__ = [
"AWSPolly",
"Heygen",
"OpenAI",
"OpenAIWhisper",
"GPTZero"
"GPTZero",
"ELAI"
]

View File

@@ -0,0 +1,95 @@
import asyncio
import os
import logging
from asyncio import sleep
from copy import deepcopy
import aiofiles
from charset_normalizer.md import getLogger
from httpx import AsyncClient
from app.configs.constants import ELAIAvatars
from app.services.abc import IVideoGeneratorService
class ELAI(IVideoGeneratorService):
_ELAI_ENDPOINT = 'https://apis.elai.io/api/v1/videos'
def __init__(self, client: AsyncClient, token: str, conf: dict):
self._http_client = client
self._conf = deepcopy(conf)
self._logger = getLogger(__name__)
self._GET_HEADER = {
"accept": "application/json",
"Authorization": f"Bearer {token}"
}
self._POST_HEADER = {
"accept": "application/json",
"content-type": "application/json",
"Authorization": f"Bearer {token}"
}
async def create_video(self, text: str, avatar: str):
avatar_url = ELAIAvatars[avatar].value.get("avatar_url")
avatar_code = ELAIAvatars[avatar].value.get("avatar_code")
avatar_gender = ELAIAvatars[avatar].value.get("avatar_gender")
avatar_canvas = ELAIAvatars[avatar].value.get("avatar_canvas")
voice_id = ELAIAvatars[avatar].value.get("voice_id")
voice_provider = ELAIAvatars[avatar].value.get("voice_provider")
self._conf["slides"][0]["canvas"]["objects"][0]["src"] = avatar_url
self._conf["slides"]["avatar"] = {
"code": avatar_code,
"gender": avatar_gender,
"canvas": avatar_canvas
}
self._conf["slides"]["speech"] = text
self._conf["slides"]["voice"] = voice_id
self._conf["slides"]["voiceProvider"] = voice_provider
response = await self._http_client.post(self._ELAI_ENDPOINT, headers=self._POST_HEADER, json=self._conf)
self._logger.info(response.status_code)
self._logger.info(response.json())
video_id = response.json()["_id"]
if video_id:
await self._http_client.post(f'{self._ELAI_ENDPOINT}/render/{video_id}', headers=self._GET_HEADER)
while True:
response = await self._http_client.get(f'{self._ELAI_ENDPOINT}/{video_id}', headers=self._GET_HEADER)
response_data = response.json()
if response_data['status'] == 'ready':
self._logger.info(response_data)
download_url = response_data.get('url')
output_directory = 'download-video/'
output_filename = video_id + '.mp4'
response = await self._http_client.get(download_url)
if response.status_code == 200:
os.makedirs(output_directory, exist_ok=True)
output_path = os.path.join(output_directory, output_filename)
with open(output_path, 'wb') as f:
f.write(response.content)
self._logger.info(f"File '{output_filename}' downloaded successfully.")
return output_filename
else:
self._logger.error(f"Failed to download file. Status code: {response.status_code}")
return None
elif response_data['status'] == 'failed':
self._logger.error('Video creation failed.')
break
else:
self._logger.info('Video is still processing. Checking again in 10 seconds...')
await sleep(10)

View File

@@ -0,0 +1,72 @@
{
"name": "API test",
"slides": [
{
"id": 1,
"canvas": {
"objects": [
{
"type": "avatar",
"left": 151.5,
"top": 36,
"fill": "#4868FF",
"scaleX": 0.3,
"scaleY": 0.3,
"width": 1080,
"height": 1080,
"avatarType": "transparent",
"animation": {
"type": null,
"exitType": null
}
},
{
"type": "image",
"version": "5.3.0",
"originX": "left",
"originY": "top",
"left": 30,
"top": 30,
"width": 800,
"height": 600,
"fill": "rgb(0,0,0)",
"stroke": null,
"strokeWidth": 0,
"strokeDashArray": null,
"strokeLineCap": "butt",
"strokeDashOffset": 0,
"strokeLineJoin": "miter",
"strokeUniform": false,
"strokeMiterLimit": 4,
"scaleX": 0.18821429,
"scaleY": 0.18821429,
"angle": 0,
"flipX": false,
"flipY": false,
"opacity": 1,
"shadow": null,
"visible": true,
"backgroundColor": "",
"fillRule": "nonzero",
"paintFirst": "fill",
"globalCompositeOperation": "source-over",
"skewX": 0,
"skewY": 0,
"cropX": 0,
"cropY": 0,
"id": 676845479989,
"src": "https://d3u63mhbhkevz8.cloudfront.net/production/uploads/66f5190349f943682dd776ff/en-coach-main-logo-800x600_sm1ype.jpg?Expires=1727654400&Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kM3U2M21oYmhrZXZ6OC5jbG91ZGZyb250Lm5ldC9wcm9kdWN0aW9uL3VwbG9hZHMvNjZmNTE5MDM0OWY5NDM2ODJkZDc3NmZmL2VuLWNvYWNoLW1haW4tbG9nby04MDB4NjAwX3NtMXlwZS5qcGciLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE3Mjc2NTQ0MDB9fX1dfQ__&Signature=kTVzlDeS7cua2HiAE5G%7E-yFqbhu0bHraFH5SauUln7yuNXoX7vtiKIBYiL%7Eps3LCLEZS77arSZ7H%7EG8CKzabHDjAR-Y6Uc%7ELD5KQaMmk0jbAxbC3Wdoq6cfd0qIwEuodQYlC0It2WBidP8KsgOy3uUQ%7EvcBoqlb255yMFw4pHuptOBB1kPs%7EFyzDV0fnRNsKaYRcy0Fn2EFUp13axm0CZQclazuLFM622AyCydKMy0vfxV%7Etny3sskwPaUe2OANGMFg07Q1pRuy6fUON0DsbhAh1tA2H6-nnem5KbFwiZK3IIwwYGBx3H41ovzC6Ejt80Fd0%7EPSHw7GzVBnUmtP-IA__&Key-Pair-Id=K1Y7U91AR6T7E5",
"crossOrigin": "anonymous",
"filters": [],
"_exists": true
}
],
"background": "#ffffff",
"version": "4.4.0"
},
"animation": "fade_in",
"language": "English",
"voiceType": "text"
}
]
}

View File

@@ -10,12 +10,11 @@ from app.services.abc import IVideoGeneratorService
class Heygen(IVideoGeneratorService):
# TODO: Not used, remove if not necessary
# CREATE_VIDEO_URL = 'https://api.heygen.com/v1/template.generate'
_GET_VIDEO_URL = 'https://api.heygen.com/v1/video_status.get'
def __init__(self, client: AsyncClient, heygen_token: str):
def __init__(self, client: AsyncClient, token: str):
pass
"""
self._get_header = {
'X-Api-Key': heygen_token
}
@@ -25,9 +24,12 @@ class Heygen(IVideoGeneratorService):
}
self._http_client = client
self._logger = logging.getLogger(__name__)
"""
async def create_video(self, text: str, avatar: str):
pass
# POST TO CREATE VIDEO
"""
create_video_url = 'https://api.heygen.com/v2/template/' + avatar + '/generate'
data = {
"test": False,
@@ -87,4 +89,5 @@ class Heygen(IVideoGeneratorService):
else:
self._logger.error(f"Failed to download file. Status code: {response.status_code}")
return None
"""

View File

@@ -120,7 +120,7 @@ class OpenAI(ILLMService):
params["temperature"] = temperature
attempt = 0
while attempt < max_retries:
while attempt < 3:
result = await self._client.chat.completions.create(**params)
result_content = result.choices[0].message.content
try:
@@ -142,6 +142,7 @@ class OpenAI(ILLMService):
"content": (
f"Previous response: {result_content}\n"
f"JSON format: {json_scheme}"
f"Validation errors: {e}"
)
}
]

0
audio-samples/.gitkeep Normal file
View File

View File

@@ -1 +0,0 @@
THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO

0
download-audio/.gitkeep Normal file
View File

View File

@@ -1 +0,0 @@
THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO

0
download-video/.gitkeep Normal file
View File

View File

@@ -1 +0,0 @@
THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO

0
tmp/.gitkeep Normal file
View File

View File

@@ -0,0 +1,473 @@
<p><img src="media/image1.png"
style="width:3.68056in;height:1.47222in" /></p>
<p><strong>University of Technology and Applied Sciences</strong></p>
<p><strong>General Foundation Programme</strong></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><p><strong>Midterm Exam</strong></p>
<p><strong>Level 2</strong></p></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p><strong>Fall Semester 2022-2023</strong></p>
<p><strong><u>Reading</u></strong></p>
<p><strong><u>Duration: 60 minutes</u></strong></p>
<table>
<colgroup>
<col style="width: 14%" />
<col style="width: 28%" />
<col style="width: 56%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Name</strong></td>
<td colspan="2"></td>
</tr>
<tr class="even">
<td><strong>Student Number</strong></td>
<td></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>Group</strong></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 52%" />
<col style="width: 47%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Section 1 (10 marks)</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>Section 2 (15 marks)</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>Total (25 marks)</strong></td>
<td></td>
</tr>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 51%" />
<col style="width: 48%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Markers Initials</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Four Unusual Schools</strong></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>Read about four of the worlds most unusual schools and
answer questions 1-10.</strong></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 6%" />
<col style="width: 35%" />
<col style="width: 29%" />
</colgroup>
<thead>
<tr class="header">
<th colspan="3">Green School is located in the jungles of Bali near the
Ayung River in Indonesia. John Hardy and his wife, Cynthia, founded the
school in 2008. A German carpenter, Jorg Stamm and a Swiss sculptor and
designer, Aldo Landwehr, built the school building. The building and the
classroom furniture are made out of bamboo. The eco-friendly school uses
clean energy from the sun, wind and water. The school provides green
education to the students. Students learn subjects such as river ecology
and rice cultivation.</th>
<th><p><strong>Green School</strong></p>
<p><img src="media/image2.jpeg"
style="width:2.1542in;height:1.58333in" /></p></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><p><strong>School of the Future</strong></p>
<p><img src="media/image3.jpeg" style="width:1.98941in;height:1.48958in"
alt="Microsoft&#39;s high school travelled a rocky road | Otago Daily Times Online News" /></p></td>
<td colspan="3">Created with the help of the software company,
Microsoft, School of the Future uses innovative teaching methods and the
latest technology. The school opened in West Philadelphia, U.S.A in
2006. It cost the school district $63 million to build this school.
Students carry laptops instead of books. They study Math and Science on
various Microsoft apps like One Note. Students have digital lockers in
the school that they open with an ID card. The school begins at 9:00am
and ends at 4:00pm like a normal work day instead of a typical school
day.</td>
</tr>
<tr class="even">
<td colspan="3">Established by Maurice De Hond in 2013, Steve Jobs
schools in the Netherlands allow children to learn at their own pace.
Each student starts with an Individual Development Plan made by the
child, his or her parents, and the school coach. In these schools, the
teacher is called a coach. All students receive iPads fully loaded
with apps to guide their learning. Students use these to study, play,
share work, prepare presentations and communicate with others.</td>
<td><p><strong>Steve Jobs schools</strong></p>
<p><img src="media/image4.png"
style="width:2.36806in;height:1.57432in" /></p></td>
</tr>
<tr class="odd">
<td colspan="2"><p><strong>Brooklyn Free School</strong></p>
<p><img src="media/image5.png"
style="width:2.20833in;height:1.62429in" /></p></td>
<td colspan="2">Founded in 2004 in New York City, Brooklyn Free School
is a unique school. Brooklyn Free School has no grades, no tests, no
compulsory classes or homework. Students are free to choose the subjects
they want to study. Students make the school rules. They decide if they
want to study, to play, to wander around or just sleep. On Wednesday
mornings, the entire school comes together to attend a weekly meeting to
discuss important issues and take decisions together.</td>
</tr>
</tbody>
</table>
<p><strong>READING SECTION 1</strong></p>
<p><em><strong>Questions 1 to 10</strong></em></p>
<p><em><strong>Read the scanning sheet about Four Unusual Schools and
answer the questions.</strong></em></p>
<p><strong>You may write your answers on the question paper, but you
MUST transfer your answers to the answer sheet before the 60 minutes are
over. You will NOT be given any extra time at the end to do
this.</strong></p>
<p><em><strong>Write no more than TWO WORDS AND/OR A NUMBER for each
answer.</strong></em></p>
<p><em><strong>Write the answer in the correct space on your answer
sheet.</strong></em></p>
<p><em><strong>Answers with incorrect spelling will be marked
wrong.</strong></em></p>
<p><strong>1.</strong> When are weekly meetings at Brooklyn Free
School?</p>
<p><strong>2.</strong> What is the nationality of the carpenter who
built the Green School?</p>
<p><strong>3.</strong> Who is known as a coach in Steve Jobs
schools?</p>
<p><strong>4.</strong> Who makes the school rules at Brooklyn Free
School?</p>
<p><strong>5.</strong> Which school was started by a married couple?</p>
<p><strong>6.</strong> How much did it cost the school district to build
the School of the Future?</p>
<p><strong>7.</strong> What have the Green School builders used to make
classroom furniture?</p>
<p><strong>8.</strong> What do School of the Future students use to open
their digital lockers?</p>
<p><strong>9.</strong> In which country can you find Steve Jobs
schools?</p>
<p><strong>10.</strong> What do students carry instead of books in the
School of the Future?</p>
<p><strong>READING SECTION 2</strong></p>
<p><em><strong>Read the passage and answer the
questions.</strong></em></p>
<p><strong>You may write your answers on the question paper, but you
MUST transfer your answers to the answer sheet before the 60 minutes are
over. You will NOT be given any extra time at the end to do
this.</strong></p>
<table>
<colgroup>
<col style="width: 4%" />
<col style="width: 95%" />
</colgroup>
<thead>
<tr class="header">
<th></th>
<th><p><strong>A</strong></p>
<p>Football is an extremely popular world sport. Young men look up to
famous footballers as role models. Several football stars began their
sporting careers as children playing on the streets. However, many of
them moved on to join football academies, or sports schools, to build
their talent and become professional players.</p>
<p><strong>B</strong></p>
<p>A football academy is a school set up to develop young footballers.
All major football clubs</p>
<p>such as FC Barcelona, Manchester United and Real Madrid have their
own academy.</p>
<p>They scout, or look for, young talent and then teach them to play
football to meet the club's standards at the academy.</p>
<p><strong>C</strong></p>
<p>Football academies provide football education to students who are 21
years old and below. A student must be at least 9 years old to join an
academy. However, some football clubs, such us Arsenal, have
pre-training programmes for even younger players. All the boys at an
academy continue their normal school education. It is important that
they are able to get good jobs in case they fail to become professional
footballers.</p>
<p><strong>D</strong></p>
<p>Players between the ages of 9 and 16 have to sign schoolboy forms.
They sign a new contract every two years. When the player turns 16, the
academy decides if they are going to offer the player a place on their
Youth Training Scheme. Each year, the best players receive a
scholarship. This gives them free football training and an academic
education.</p>
<p><strong>E</strong></p>
<p>In a football academy, players attend training sessions in the
afternoon on weekdays, and in the morning at weekends. On Sundays, they
dont train. They play matches against other academy teams. The football
academies also encourage their players to take up other sports such as
gymnastics or basketball.</p>
<p><strong>F</strong></p>
<p>FC Barcelona's football academy, La Masia, is one of the best
football academies in the world. Located in Barcelona, Spain, La Masia
has over 300 young players. Famous footballers and coaches such as
Lionel Messi, Pep Guardiola and Ces Fabregas are graduates of La Masia.
Many people think that Barcelonas success in the football world is due
to the excellent training programme provided at La Masia. Today, FC
Barcelona has academies in other parts of the world including Egypt,
Japan, America and Dubai.</p>
<p><em><strong>Questions 11 to 15</strong></em></p>
<p><em><strong>The Reading passage has 6 paragraphs,
A-F.</strong></em></p>
<p><em><strong>Read the following headings 1 to 6 and choose a suitable
title for each paragraph. Write the correct number on your answer sheet.
The first one has been done for you as an example. (WRITE <u>ONLY</u>
THE CORRECT NUMBER)</strong></em></p>
<p><strong>Headings</strong></p>
<p>1. From the streets to an academy</p>
<p>2. Agreements between young players and the academy</p>
<p>3. An academy that produced some famous names in football</p>
<p>4. Weekly routine of players in the academy</p>
<p>5. An academy for each big club</p>
<p>6. Learning about football but still going to school</p>
<p><strong>Paragraphs</strong></p>
<p><strong>Example: A = 1</strong></p>
<p><strong>11. B =</strong></p>
<p><strong>12. C =</strong></p>
<p><strong>13. D =</strong></p>
<p><strong>14. E =</strong></p>
<p><strong>15. F =</strong></p>
<p><em><strong>Questions 16 to 18</strong></em></p>
<p>Do the following statements agree with the information given in the
Reading Passage?</p>
<p>In the correct space on your answer sheet, write</p>
<p><em><strong>TRUE (T) if the statement agrees with the
information</strong></em></p>
<p><em><strong>FALSE (F) if the statement disagrees with the
information</strong></em></p>
<p><em><strong>NOT GIVEN (NG) if the information is not in the
passage</strong></em></p>
<p><strong>16</strong>. All famous footballers went to football
academies.</p>
<p><strong>17</strong>. Only a few important football clubs run their
own football academies.</p>
<p><strong>18</strong>. Most players join a football academy at 9 years
of age.</p>
<p><em><strong>Questions 19 to 21</strong></em></p>
<p><em><strong>Choose the correct letter, A, B or C.</strong></em></p>
<p><em><strong>Write <u>only the correct letter</u> on your answer
sheet.</strong></em></p>
<p><strong>19.</strong> Football academies take students</p>
<p><strong>A</strong>. under the age of 9.</p>
<p><strong>B.</strong> between the ages 9 and 21.</p>
<p><strong>C.</strong> only between the ages of 9 and 16.</p>
<p><strong>20</strong>. Football academies</p>
<p><strong>A</strong>. give scholarships to all players over 16.</p>
<p><strong>B.</strong> renew the contracts of players each year.</p>
<p><strong>C</strong>. may or may not accept a player on the Youth
Training Scheme.</p>
<p><strong>21</strong>. Football academy students</p>
<p><strong>A.</strong> cannot play other sports.</p>
<p><strong>B</strong>. play against other teams on weekdays.</p>
<p><strong>C</strong>. don't have training sessions on Sundays.</p>
<p><em><strong>Questions 22 to 25</strong></em></p>
<p><em><strong>Complete the summary below using words from the box. The
words are from the passage. Write your answers on your answer
sheet.</strong></em></p>
<p><em><strong>Answers with incorrect spelling will be marked
wrong.</strong></em></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>academies famous training clubs</strong></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p>The world's important football <strong>22.</strong> ___________, such
as FC Barcelona, and Real Madrid have their own football schools.
Located in Barcelona, Spain, La Masia is among the top</p>
<p><strong>23.</strong> __________ for football coaching. Lionel Messi,
Pep Guardiola and Ces Fabregas are</p>
<p><strong>24.</strong> ___________ players or coaches from La Masia. A
lot of people believe that La Masia's extremely good
<strong>25.</strong> ___________ programme is a reason for FC
Barcelona's success in football.</p>
<p><strong>University of Technology and Applied Sciences</strong></p>
<p><strong>Level 2, Midterm Exam, Fall Semester 2022-2023</strong></p>
<p><strong>Name: _________________________________________ Student ID:
________</strong></p>
<p><strong>College: ________________________________________ Group:
___________</strong></p>
<p><strong>Answer Sheet</strong></p>
<table>
<colgroup>
<col style="width: 10%" />
<col style="width: 75%" />
<col style="width: 14%" />
</colgroup>
<thead>
<tr class="header">
<th colspan="2"><strong>Reading</strong></th>
<th><strong>Marks</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>1.</strong></td>
<td></td>
<td rowspan="10"><strong>/10</strong></td>
</tr>
<tr class="even">
<td><strong>2.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>3.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>4.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>5.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>6.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>7.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>8.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>9.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>10.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>11.</strong></td>
<td></td>
<td rowspan="5"></td>
</tr>
<tr class="even">
<td><strong>12.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>13.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>14.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>15.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>16.</strong></td>
<td></td>
<td rowspan="10"><strong>/15</strong></td>
</tr>
<tr class="odd">
<td><strong>17.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>18.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>19.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>20.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>21.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>22.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>23.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>24.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>25.</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>For the Use of examiners only</strong></p>
<table>
<colgroup>
<col style="width: 49%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>Total Mark</strong></th>
<th><strong>Marker</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td></td>
<td></td>
</tr>
</tbody>
</table></th>
</tr>
</thead>
<tbody>
</tbody>
</table>

View File

@@ -0,0 +1,473 @@
<p><img src="media/image1.png"
style="width:3.68056in;height:1.47222in" /></p>
<p><strong>University of Technology and Applied Sciences</strong></p>
<p><strong>General Foundation Programme</strong></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><p><strong>Midterm Exam</strong></p>
<p><strong>Level 2</strong></p></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p><strong>Fall Semester 2022-2023</strong></p>
<p><strong><u>Reading</u></strong></p>
<p><strong><u>Duration: 60 minutes</u></strong></p>
<table>
<colgroup>
<col style="width: 14%" />
<col style="width: 28%" />
<col style="width: 56%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Name</strong></td>
<td colspan="2"></td>
</tr>
<tr class="even">
<td><strong>Student Number</strong></td>
<td></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>Group</strong></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 52%" />
<col style="width: 47%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Section 1 (10 marks)</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>Section 2 (15 marks)</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>Total (25 marks)</strong></td>
<td></td>
</tr>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 51%" />
<col style="width: 48%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Markers Initials</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Four Unusual Schools</strong></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>Read about four of the worlds most unusual schools and
answer questions 1-10.</strong></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 6%" />
<col style="width: 35%" />
<col style="width: 29%" />
</colgroup>
<thead>
<tr class="header">
<th colspan="3">Green School is located in the jungles of Bali near the
Ayung River in Indonesia. John Hardy and his wife, Cynthia, founded the
school in 2008. A German carpenter, Jorg Stamm and a Swiss sculptor and
designer, Aldo Landwehr, built the school building. The building and the
classroom furniture are made out of bamboo. The eco-friendly school uses
clean energy from the sun, wind and water. The school provides green
education to the students. Students learn subjects such as river ecology
and rice cultivation.</th>
<th><p><strong>Green School</strong></p>
<p><img src="media/image2.jpeg"
style="width:2.1542in;height:1.58333in" /></p></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><p><strong>School of the Future</strong></p>
<p><img src="media/image3.jpeg" style="width:1.98941in;height:1.48958in"
alt="Microsoft&#39;s high school travelled a rocky road | Otago Daily Times Online News" /></p></td>
<td colspan="3">Created with the help of the software company,
Microsoft, School of the Future uses innovative teaching methods and the
latest technology. The school opened in West Philadelphia, U.S.A in
2006. It cost the school district $63 million to build this school.
Students carry laptops instead of books. They study Math and Science on
various Microsoft apps like One Note. Students have digital lockers in
the school that they open with an ID card. The school begins at 9:00am
and ends at 4:00pm like a normal work day instead of a typical school
day.</td>
</tr>
<tr class="even">
<td colspan="3">Established by Maurice De Hond in 2013, Steve Jobs
schools in the Netherlands allow children to learn at their own pace.
Each student starts with an Individual Development Plan made by the
child, his or her parents, and the school coach. In these schools, the
teacher is called a coach. All students receive iPads fully loaded
with apps to guide their learning. Students use these to study, play,
share work, prepare presentations and communicate with others.</td>
<td><p><strong>Steve Jobs schools</strong></p>
<p><img src="media/image4.png"
style="width:2.36806in;height:1.57432in" /></p></td>
</tr>
<tr class="odd">
<td colspan="2"><p><strong>Brooklyn Free School</strong></p>
<p><img src="media/image5.png"
style="width:2.20833in;height:1.62429in" /></p></td>
<td colspan="2">Founded in 2004 in New York City, Brooklyn Free School
is a unique school. Brooklyn Free School has no grades, no tests, no
compulsory classes or homework. Students are free to choose the subjects
they want to study. Students make the school rules. They decide if they
want to study, to play, to wander around or just sleep. On Wednesday
mornings, the entire school comes together to attend a weekly meeting to
discuss important issues and take decisions together.</td>
</tr>
</tbody>
</table>
<p><strong>READING SECTION 1</strong></p>
<p><em><strong>Questions 1 to 10</strong></em></p>
<p><em><strong>Read the scanning sheet about Four Unusual Schools and
answer the questions.</strong></em></p>
<p><strong>You may write your answers on the question paper, but you
MUST transfer your answers to the answer sheet before the 60 minutes are
over. You will NOT be given any extra time at the end to do
this.</strong></p>
<p><em><strong>Write no more than TWO WORDS AND/OR A NUMBER for each
answer.</strong></em></p>
<p><em><strong>Write the answer in the correct space on your answer
sheet.</strong></em></p>
<p><em><strong>Answers with incorrect spelling will be marked
wrong.</strong></em></p>
<p><strong>1.</strong> When are weekly meetings at Brooklyn Free
School?</p>
<p><strong>2.</strong> What is the nationality of the carpenter who
built the Green School?</p>
<p><strong>3.</strong> Who is known as a coach in Steve Jobs
schools?</p>
<p><strong>4.</strong> Who makes the school rules at Brooklyn Free
School?</p>
<p><strong>5.</strong> Which school was started by a married couple?</p>
<p><strong>6.</strong> How much did it cost the school district to build
the School of the Future?</p>
<p><strong>7.</strong> What have the Green School builders used to make
classroom furniture?</p>
<p><strong>8.</strong> What do School of the Future students use to open
their digital lockers?</p>
<p><strong>9.</strong> In which country can you find Steve Jobs
schools?</p>
<p><strong>10.</strong> What do students carry instead of books in the
School of the Future?</p>
<p><strong>READING SECTION 2</strong></p>
<p><em><strong>Read the passage and answer the
questions.</strong></em></p>
<p><strong>You may write your answers on the question paper, but you
MUST transfer your answers to the answer sheet before the 60 minutes are
over. You will NOT be given any extra time at the end to do
this.</strong></p>
<table>
<colgroup>
<col style="width: 4%" />
<col style="width: 95%" />
</colgroup>
<thead>
<tr class="header">
<th></th>
<th><p><strong>A</strong></p>
<p>Football is an extremely popular world sport. Young men look up to
famous footballers as role models. Several football stars began their
sporting careers as children playing on the streets. However, many of
them moved on to join football academies, or sports schools, to build
their talent and become professional players.</p>
<p><strong>B</strong></p>
<p>A football academy is a school set up to develop young footballers.
All major football clubs</p>
<p>such as FC Barcelona, Manchester United and Real Madrid have their
own academy.</p>
<p>They scout, or look for, young talent and then teach them to play
football to meet the club's standards at the academy.</p>
<p><strong>C</strong></p>
<p>Football academies provide football education to students who are 21
years old and below. A student must be at least 9 years old to join an
academy. However, some football clubs, such us Arsenal, have
pre-training programmes for even younger players. All the boys at an
academy continue their normal school education. It is important that
they are able to get good jobs in case they fail to become professional
footballers.</p>
<p><strong>D</strong></p>
<p>Players between the ages of 9 and 16 have to sign schoolboy forms.
They sign a new contract every two years. When the player turns 16, the
academy decides if they are going to offer the player a place on their
Youth Training Scheme. Each year, the best players receive a
scholarship. This gives them free football training and an academic
education.</p>
<p><strong>E</strong></p>
<p>In a football academy, players attend training sessions in the
afternoon on weekdays, and in the morning at weekends. On Sundays, they
dont train. They play matches against other academy teams. The football
academies also encourage their players to take up other sports such as
gymnastics or basketball.</p>
<p><strong>F</strong></p>
<p>FC Barcelona's football academy, La Masia, is one of the best
football academies in the world. Located in Barcelona, Spain, La Masia
has over 300 young players. Famous footballers and coaches such as
Lionel Messi, Pep Guardiola and Ces Fabregas are graduates of La Masia.
Many people think that Barcelonas success in the football world is due
to the excellent training programme provided at La Masia. Today, FC
Barcelona has academies in other parts of the world including Egypt,
Japan, America and Dubai.</p>
<p><em><strong>Questions 11 to 15</strong></em></p>
<p><em><strong>The Reading passage has 6 paragraphs,
A-F.</strong></em></p>
<p><em><strong>Read the following headings 1 to 6 and choose a suitable
title for each paragraph. Write the correct number on your answer sheet.
The first one has been done for you as an example. (WRITE <u>ONLY</u>
THE CORRECT NUMBER)</strong></em></p>
<p><strong>Headings</strong></p>
<p>1. From the streets to an academy</p>
<p>2. Agreements between young players and the academy</p>
<p>3. An academy that produced some famous names in football</p>
<p>4. Weekly routine of players in the academy</p>
<p>5. An academy for each big club</p>
<p>6. Learning about football but still going to school</p>
<p><strong>Paragraphs</strong></p>
<p><strong>Example: A = 1</strong></p>
<p><strong>11. B =</strong></p>
<p><strong>12. C =</strong></p>
<p><strong>13. D =</strong></p>
<p><strong>14. E =</strong></p>
<p><strong>15. F =</strong></p>
<p><em><strong>Questions 16 to 18</strong></em></p>
<p>Do the following statements agree with the information given in the
Reading Passage?</p>
<p>In the correct space on your answer sheet, write</p>
<p><em><strong>TRUE (T) if the statement agrees with the
information</strong></em></p>
<p><em><strong>FALSE (F) if the statement disagrees with the
information</strong></em></p>
<p><em><strong>NOT GIVEN (NG) if the information is not in the
passage</strong></em></p>
<p><strong>16</strong>. All famous footballers went to football
academies.</p>
<p><strong>17</strong>. Only a few important football clubs run their
own football academies.</p>
<p><strong>18</strong>. Most players join a football academy at 9 years
of age.</p>
<p><em><strong>Questions 19 to 21</strong></em></p>
<p><em><strong>Choose the correct letter, A, B or C.</strong></em></p>
<p><em><strong>Write <u>only the correct letter</u> on your answer
sheet.</strong></em></p>
<p><strong>19.</strong> Football academies take students</p>
<p><strong>A</strong>. under the age of 9.</p>
<p><strong>B.</strong> between the ages 9 and 21.</p>
<p><strong>C.</strong> only between the ages of 9 and 16.</p>
<p><strong>20</strong>. Football academies</p>
<p><strong>A</strong>. give scholarships to all players over 16.</p>
<p><strong>B.</strong> renew the contracts of players each year.</p>
<p><strong>C</strong>. may or may not accept a player on the Youth
Training Scheme.</p>
<p><strong>21</strong>. Football academy students</p>
<p><strong>A.</strong> cannot play other sports.</p>
<p><strong>B</strong>. play against other teams on weekdays.</p>
<p><strong>C</strong>. don't have training sessions on Sundays.</p>
<p><em><strong>Questions 22 to 25</strong></em></p>
<p><em><strong>Complete the summary below using words from the box. The
words are from the passage. Write your answers on your answer
sheet.</strong></em></p>
<p><em><strong>Answers with incorrect spelling will be marked
wrong.</strong></em></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>academies famous training clubs</strong></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p>The world's important football <strong>22.</strong> ___________, such
as FC Barcelona, and Real Madrid have their own football schools.
Located in Barcelona, Spain, La Masia is among the top</p>
<p><strong>23.</strong> __________ for football coaching. Lionel Messi,
Pep Guardiola and Ces Fabregas are</p>
<p><strong>24.</strong> ___________ players or coaches from La Masia. A
lot of people believe that La Masia's extremely good
<strong>25.</strong> ___________ programme is a reason for FC
Barcelona's success in football.</p>
<p><strong>University of Technology and Applied Sciences</strong></p>
<p><strong>Level 2, Midterm Exam, Fall Semester 2022-2023</strong></p>
<p><strong>Name: _________________________________________ Student ID:
________</strong></p>
<p><strong>College: ________________________________________ Group:
___________</strong></p>
<p><strong>Answer Sheet</strong></p>
<table>
<colgroup>
<col style="width: 10%" />
<col style="width: 75%" />
<col style="width: 14%" />
</colgroup>
<thead>
<tr class="header">
<th colspan="2"><strong>Reading</strong></th>
<th><strong>Marks</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>1.</strong></td>
<td></td>
<td rowspan="10"><strong>/10</strong></td>
</tr>
<tr class="even">
<td><strong>2.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>3.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>4.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>5.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>6.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>7.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>8.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>9.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>10.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>11.</strong></td>
<td></td>
<td rowspan="5"></td>
</tr>
<tr class="even">
<td><strong>12.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>13.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>14.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>15.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>16.</strong></td>
<td></td>
<td rowspan="10"><strong>/15</strong></td>
</tr>
<tr class="odd">
<td><strong>17.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>18.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>19.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>20.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>21.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>22.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>23.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>24.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>25.</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>For the Use of examiners only</strong></p>
<table>
<colgroup>
<col style="width: 49%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>Total Mark</strong></th>
<th><strong>Marker</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td></td>
<td></td>
</tr>
</tbody>
</table></th>
</tr>
</thead>
<tbody>
</tbody>
</table>

Binary file not shown.

View File

@@ -0,0 +1,473 @@
<p><img src="media/image1.png"
style="width:3.68056in;height:1.47222in" /></p>
<p><strong>University of Technology and Applied Sciences</strong></p>
<p><strong>General Foundation Programme</strong></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><p><strong>Midterm Exam</strong></p>
<p><strong>Level 2</strong></p></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p><strong>Fall Semester 2022-2023</strong></p>
<p><strong><u>Reading</u></strong></p>
<p><strong><u>Duration: 60 minutes</u></strong></p>
<table>
<colgroup>
<col style="width: 14%" />
<col style="width: 28%" />
<col style="width: 56%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Name</strong></td>
<td colspan="2"></td>
</tr>
<tr class="even">
<td><strong>Student Number</strong></td>
<td></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>Group</strong></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 52%" />
<col style="width: 47%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Section 1 (10 marks)</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>Section 2 (15 marks)</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>Total (25 marks)</strong></td>
<td></td>
</tr>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 51%" />
<col style="width: 48%" />
</colgroup>
<tbody>
<tr class="odd">
<td><strong>Markers Initials</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Four Unusual Schools</strong></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>Read about four of the worlds most unusual schools and
answer questions 1-10.</strong></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<table>
<colgroup>
<col style="width: 28%" />
<col style="width: 6%" />
<col style="width: 35%" />
<col style="width: 29%" />
</colgroup>
<thead>
<tr class="header">
<th colspan="3">Green School is located in the jungles of Bali near the
Ayung River in Indonesia. John Hardy and his wife, Cynthia, founded the
school in 2008. A German carpenter, Jorg Stamm and a Swiss sculptor and
designer, Aldo Landwehr, built the school building. The building and the
classroom furniture are made out of bamboo. The eco-friendly school uses
clean energy from the sun, wind and water. The school provides green
education to the students. Students learn subjects such as river ecology
and rice cultivation.</th>
<th><p><strong>Green School</strong></p>
<p><img src="media/image2.jpeg"
style="width:2.1542in;height:1.58333in" /></p></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><p><strong>School of the Future</strong></p>
<p><img src="media/image3.jpeg" style="width:1.98941in;height:1.48958in"
alt="Microsoft&#39;s high school travelled a rocky road | Otago Daily Times Online News" /></p></td>
<td colspan="3">Created with the help of the software company,
Microsoft, School of the Future uses innovative teaching methods and the
latest technology. The school opened in West Philadelphia, U.S.A in
2006. It cost the school district $63 million to build this school.
Students carry laptops instead of books. They study Math and Science on
various Microsoft apps like One Note. Students have digital lockers in
the school that they open with an ID card. The school begins at 9:00am
and ends at 4:00pm like a normal work day instead of a typical school
day.</td>
</tr>
<tr class="even">
<td colspan="3">Established by Maurice De Hond in 2013, Steve Jobs
schools in the Netherlands allow children to learn at their own pace.
Each student starts with an Individual Development Plan made by the
child, his or her parents, and the school coach. In these schools, the
teacher is called a coach. All students receive iPads fully loaded
with apps to guide their learning. Students use these to study, play,
share work, prepare presentations and communicate with others.</td>
<td><p><strong>Steve Jobs schools</strong></p>
<p><img src="media/image4.png"
style="width:2.36806in;height:1.57432in" /></p></td>
</tr>
<tr class="odd">
<td colspan="2"><p><strong>Brooklyn Free School</strong></p>
<p><img src="media/image5.png"
style="width:2.20833in;height:1.62429in" /></p></td>
<td colspan="2">Founded in 2004 in New York City, Brooklyn Free School
is a unique school. Brooklyn Free School has no grades, no tests, no
compulsory classes or homework. Students are free to choose the subjects
they want to study. Students make the school rules. They decide if they
want to study, to play, to wander around or just sleep. On Wednesday
mornings, the entire school comes together to attend a weekly meeting to
discuss important issues and take decisions together.</td>
</tr>
</tbody>
</table>
<p><strong>READING SECTION 1</strong></p>
<p><em><strong>Questions 1 to 10</strong></em></p>
<p><em><strong>Read the scanning sheet about Four Unusual Schools and
answer the questions.</strong></em></p>
<p><strong>You may write your answers on the question paper, but you
MUST transfer your answers to the answer sheet before the 60 minutes are
over. You will NOT be given any extra time at the end to do
this.</strong></p>
<p><em><strong>Write no more than TWO WORDS AND/OR A NUMBER for each
answer.</strong></em></p>
<p><em><strong>Write the answer in the correct space on your answer
sheet.</strong></em></p>
<p><em><strong>Answers with incorrect spelling will be marked
wrong.</strong></em></p>
<p><strong>1.</strong> When are weekly meetings at Brooklyn Free
School?</p>
<p><strong>2.</strong> What is the nationality of the carpenter who
built the Green School?</p>
<p><strong>3.</strong> Who is known as a coach in Steve Jobs
schools?</p>
<p><strong>4.</strong> Who makes the school rules at Brooklyn Free
School?</p>
<p><strong>5.</strong> Which school was started by a married couple?</p>
<p><strong>6.</strong> How much did it cost the school district to build
the School of the Future?</p>
<p><strong>7.</strong> What have the Green School builders used to make
classroom furniture?</p>
<p><strong>8.</strong> What do School of the Future students use to open
their digital lockers?</p>
<p><strong>9.</strong> In which country can you find Steve Jobs
schools?</p>
<p><strong>10.</strong> What do students carry instead of books in the
School of the Future?</p>
<p><strong>READING SECTION 2</strong></p>
<p><em><strong>Read the passage and answer the
questions.</strong></em></p>
<p><strong>You may write your answers on the question paper, but you
MUST transfer your answers to the answer sheet before the 60 minutes are
over. You will NOT be given any extra time at the end to do
this.</strong></p>
<table>
<colgroup>
<col style="width: 4%" />
<col style="width: 95%" />
</colgroup>
<thead>
<tr class="header">
<th></th>
<th><p><strong>A</strong></p>
<p>Football is an extremely popular world sport. Young men look up to
famous footballers as role models. Several football stars began their
sporting careers as children playing on the streets. However, many of
them moved on to join football academies, or sports schools, to build
their talent and become professional players.</p>
<p><strong>B</strong></p>
<p>A football academy is a school set up to develop young footballers.
All major football clubs</p>
<p>such as FC Barcelona, Manchester United and Real Madrid have their
own academy.</p>
<p>They scout, or look for, young talent and then teach them to play
football to meet the club's standards at the academy.</p>
<p><strong>C</strong></p>
<p>Football academies provide football education to students who are 21
years old and below. A student must be at least 9 years old to join an
academy. However, some football clubs, such us Arsenal, have
pre-training programmes for even younger players. All the boys at an
academy continue their normal school education. It is important that
they are able to get good jobs in case they fail to become professional
footballers.</p>
<p><strong>D</strong></p>
<p>Players between the ages of 9 and 16 have to sign schoolboy forms.
They sign a new contract every two years. When the player turns 16, the
academy decides if they are going to offer the player a place on their
Youth Training Scheme. Each year, the best players receive a
scholarship. This gives them free football training and an academic
education.</p>
<p><strong>E</strong></p>
<p>In a football academy, players attend training sessions in the
afternoon on weekdays, and in the morning at weekends. On Sundays, they
dont train. They play matches against other academy teams. The football
academies also encourage their players to take up other sports such as
gymnastics or basketball.</p>
<p><strong>F</strong></p>
<p>FC Barcelona's football academy, La Masia, is one of the best
football academies in the world. Located in Barcelona, Spain, La Masia
has over 300 young players. Famous footballers and coaches such as
Lionel Messi, Pep Guardiola and Ces Fabregas are graduates of La Masia.
Many people think that Barcelonas success in the football world is due
to the excellent training programme provided at La Masia. Today, FC
Barcelona has academies in other parts of the world including Egypt,
Japan, America and Dubai.</p>
<p><em><strong>Questions 11 to 15</strong></em></p>
<p><em><strong>The Reading passage has 6 paragraphs,
A-F.</strong></em></p>
<p><em><strong>Read the following headings 1 to 6 and choose a suitable
title for each paragraph. Write the correct number on your answer sheet.
The first one has been done for you as an example. (WRITE <u>ONLY</u>
THE CORRECT NUMBER)</strong></em></p>
<p><strong>Headings</strong></p>
<p>1. From the streets to an academy</p>
<p>2. Agreements between young players and the academy</p>
<p>3. An academy that produced some famous names in football</p>
<p>4. Weekly routine of players in the academy</p>
<p>5. An academy for each big club</p>
<p>6. Learning about football but still going to school</p>
<p><strong>Paragraphs</strong></p>
<p><strong>Example: A = 1</strong></p>
<p><strong>11. B =</strong></p>
<p><strong>12. C =</strong></p>
<p><strong>13. D =</strong></p>
<p><strong>14. E =</strong></p>
<p><strong>15. F =</strong></p>
<p><em><strong>Questions 16 to 18</strong></em></p>
<p>Do the following statements agree with the information given in the
Reading Passage?</p>
<p>In the correct space on your answer sheet, write</p>
<p><em><strong>TRUE (T) if the statement agrees with the
information</strong></em></p>
<p><em><strong>FALSE (F) if the statement disagrees with the
information</strong></em></p>
<p><em><strong>NOT GIVEN (NG) if the information is not in the
passage</strong></em></p>
<p><strong>16</strong>. All famous footballers went to football
academies.</p>
<p><strong>17</strong>. Only a few important football clubs run their
own football academies.</p>
<p><strong>18</strong>. Most players join a football academy at 9 years
of age.</p>
<p><em><strong>Questions 19 to 21</strong></em></p>
<p><em><strong>Choose the correct letter, A, B or C.</strong></em></p>
<p><em><strong>Write <u>only the correct letter</u> on your answer
sheet.</strong></em></p>
<p><strong>19.</strong> Football academies take students</p>
<p><strong>A</strong>. under the age of 9.</p>
<p><strong>B.</strong> between the ages 9 and 21.</p>
<p><strong>C.</strong> only between the ages of 9 and 16.</p>
<p><strong>20</strong>. Football academies</p>
<p><strong>A</strong>. give scholarships to all players over 16.</p>
<p><strong>B.</strong> renew the contracts of players each year.</p>
<p><strong>C</strong>. may or may not accept a player on the Youth
Training Scheme.</p>
<p><strong>21</strong>. Football academy students</p>
<p><strong>A.</strong> cannot play other sports.</p>
<p><strong>B</strong>. play against other teams on weekdays.</p>
<p><strong>C</strong>. don't have training sessions on Sundays.</p>
<p><em><strong>Questions 22 to 25</strong></em></p>
<p><em><strong>Complete the summary below using words from the box. The
words are from the passage. Write your answers on your answer
sheet.</strong></em></p>
<p><em><strong>Answers with incorrect spelling will be marked
wrong.</strong></em></p>
<table>
<colgroup>
<col style="width: 100%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>academies famous training clubs</strong></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<p>The world's important football <strong>22.</strong> ___________, such
as FC Barcelona, and Real Madrid have their own football schools.
Located in Barcelona, Spain, La Masia is among the top</p>
<p><strong>23.</strong> __________ for football coaching. Lionel Messi,
Pep Guardiola and Ces Fabregas are</p>
<p><strong>24.</strong> ___________ players or coaches from La Masia. A
lot of people believe that La Masia's extremely good
<strong>25.</strong> ___________ programme is a reason for FC
Barcelona's success in football.</p>
<p><strong>University of Technology and Applied Sciences</strong></p>
<p><strong>Level 2, Midterm Exam, Fall Semester 2022-2023</strong></p>
<p><strong>Name: _________________________________________ Student ID:
________</strong></p>
<p><strong>College: ________________________________________ Group:
___________</strong></p>
<p><strong>Answer Sheet</strong></p>
<table>
<colgroup>
<col style="width: 10%" />
<col style="width: 75%" />
<col style="width: 14%" />
</colgroup>
<thead>
<tr class="header">
<th colspan="2"><strong>Reading</strong></th>
<th><strong>Marks</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>1.</strong></td>
<td></td>
<td rowspan="10"><strong>/10</strong></td>
</tr>
<tr class="even">
<td><strong>2.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>3.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>4.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>5.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>6.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>7.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>8.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>9.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>10.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>11.</strong></td>
<td></td>
<td rowspan="5"></td>
</tr>
<tr class="even">
<td><strong>12.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>13.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>14.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>15.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>16.</strong></td>
<td></td>
<td rowspan="10"><strong>/15</strong></td>
</tr>
<tr class="odd">
<td><strong>17.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>18.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>19.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>20.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>21.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>22.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>23.</strong></td>
<td></td>
</tr>
<tr class="even">
<td><strong>24.</strong></td>
<td></td>
</tr>
<tr class="odd">
<td><strong>25.</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>For the Use of examiners only</strong></p>
<table>
<colgroup>
<col style="width: 49%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th><strong>Total Mark</strong></th>
<th><strong>Marker</strong></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td></td>
<td></td>
</tr>
</tbody>
</table></th>
</tr>
</thead>
<tbody>
</tbody>
</table>

View File

@@ -1 +0,0 @@
THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO