diff --git a/.env b/.env index e3ff363..7c1a7e0 100644 --- a/.env +++ b/.env @@ -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 diff --git a/app/api/__init__.py b/app/api/__init__.py index 622f981..2c98dd4 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -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) diff --git a/app/api/level.py b/app/api/level.py index e550eb5..66aad3c 100644 --- a/app/api/level.py +++ b/app/api/level.py @@ -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 diff --git a/app/api/listening.py b/app/api/listening.py index b29c34d..6636355 100644 --- a/app/api/listening.py +++ b/app/api/listening.py @@ -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( diff --git a/app/api/reading.py b/app/api/reading.py index ba5090e..9088484 100644 --- a/app/api/reading.py +++ b/app/api/reading.py @@ -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) diff --git a/app/api/speaking.py b/app/api/speaking.py index 41a11bd..f5646be 100644 --- a/app/api/speaking.py +++ b/app/api/speaking.py @@ -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 diff --git a/app/api/writing.py b/app/api/writing.py index 3bd3b8e..ed72941 100644 --- a/app/api/writing.py +++ b/app/api/writing.py @@ -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) diff --git a/app/configs/constants.py b/app/configs/constants.py index eac0de8..9ad8e93 100644 --- a/app/configs/constants.py +++ b/app/configs/constants.py @@ -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/' diff --git a/app/configs/dependency_injection.py b/app/configs/dependency_injection.py index 2114923..121f3bd 100644 --- a/app/configs/dependency_injection.py +++ b/app/configs/dependency_injection.py @@ -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 diff --git a/app/controllers/abc/__init__.py b/app/controllers/abc/__init__.py index 85fa452..abdee64 100644 --- a/app/controllers/abc/__init__.py +++ b/app/controllers/abc/__init__.py @@ -15,5 +15,5 @@ __all__ = [ "ILevelController", "IGradeController", "ITrainingController", - "IUserController" + "IUserController", ] diff --git a/app/controllers/abc/level.py b/app/controllers/abc/level.py index cba2151..a2ebb9c 100644 --- a/app/controllers/abc/level.py +++ b/app/controllers/abc/level.py @@ -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 diff --git a/app/controllers/abc/listening.py b/app/controllers/abc/listening.py index 19a8a09..5e07a50 100644 --- a/app/controllers/abc/listening.py +++ b/app/controllers/abc/listening.py @@ -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 diff --git a/app/controllers/abc/reading.py b/app/controllers/abc/reading.py index 03250ab..5ed60db 100644 --- a/app/controllers/abc/reading.py +++ b/app/controllers/abc/reading.py @@ -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 diff --git a/app/controllers/impl/level.py b/app/controllers/impl/level.py index 7133a1e..a3bc098 100644 --- a/app/controllers/impl/level.py +++ b/app/controllers/impl/level.py @@ -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() diff --git a/app/controllers/impl/listening.py b/app/controllers/impl/listening.py index e77e5f8..8102d71 100644 --- a/app/controllers/impl/listening.py +++ b/app/controllers/impl/listening.py @@ -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) diff --git a/app/controllers/impl/reading.py b/app/controllers/impl/reading.py index e4337c4..1836e7d 100644 --- a/app/controllers/impl/reading.py +++ b/app/controllers/impl/reading.py @@ -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) diff --git a/app/dtos/exams/__init__.py b/app/dtos/exams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/dtos/exam.py b/app/dtos/exams/level.py similarity index 100% rename from app/dtos/exam.py rename to app/dtos/exams/level.py diff --git a/app/dtos/exams/reading.py b/app/dtos/exams/reading.py new file mode 100644 index 0000000..2c7b499 --- /dev/null +++ b/app/dtos/exams/reading.py @@ -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] diff --git a/app/dtos/level.py b/app/dtos/level.py new file mode 100644 index 0000000..80b61dd --- /dev/null +++ b/app/dtos/level.py @@ -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] diff --git a/app/dtos/listening.py b/app/dtos/listening.py index 03270f8..81cc500 100644 --- a/app/dtos/listening.py +++ b/app/dtos/listening.py @@ -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] diff --git a/app/dtos/reading.py b/app/dtos/reading.py new file mode 100644 index 0000000..b5907e8 --- /dev/null +++ b/app/dtos/reading.py @@ -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)) diff --git a/app/dtos/speaking.py b/app/dtos/speaking.py index 243b4d4..97b9e79 100644 --- a/app/dtos/speaking.py +++ b/app/dtos/speaking.py @@ -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 diff --git a/app/helpers/file.py b/app/helpers/file.py index 6762ece..079e42e 100644 --- a/app/helpers/file.py +++ b/app/helpers/file.py @@ -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 diff --git a/app/mappers/__init__.py b/app/mappers/__init__.py index 2f71b3b..bc693aa 100644 --- a/app/mappers/__init__.py +++ b/app/mappers/__init__.py @@ -1,5 +1,5 @@ -from .exam import ExamMapper +from .level import LevelMapper __all__ = [ - "ExamMapper" + "LevelMapper" ] diff --git a/app/mappers/exam.py b/app/mappers/level.py similarity index 94% rename from app/mappers/exam.py rename to app/mappers/level.py index 8ebf13a..b009ddd 100644 --- a/app/mappers/exam.py +++ b/app/mappers/level.py @@ -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: diff --git a/app/mappers/reading.py b/app/mappers/reading.py new file mode 100644 index 0000000..85adbfc --- /dev/null +++ b/app/mappers/reading.py @@ -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"]) diff --git a/app/services/abc/exam/__init__.py b/app/services/abc/exam/__init__.py index 1f93263..c16bdac 100644 --- a/app/services/abc/exam/__init__.py +++ b/app/services/abc/exam/__init__.py @@ -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" ] diff --git a/app/services/abc/exam/exercises.py b/app/services/abc/exam/exercises.py new file mode 100644 index 0000000..eb91ab3 --- /dev/null +++ b/app/services/abc/exam/exercises.py @@ -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 diff --git a/app/services/abc/exam/level.py b/app/services/abc/exam/level.py index 758f090..a712f57 100644 --- a/app/services/abc/exam/level.py +++ b/app/services/abc/exam/level.py @@ -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 diff --git a/app/services/abc/exam/listening.py b/app/services/abc/exam/listening.py index 497f5c4..0f69a10 100644 --- a/app/services/abc/exam/listening.py +++ b/app/services/abc/exam/listening.py @@ -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 diff --git a/app/services/abc/exam/reading.py b/app/services/abc/exam/reading.py index 4af7588..b8bf805 100644 --- a/app/services/abc/exam/reading.py +++ b/app/services/abc/exam/reading.py @@ -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 diff --git a/app/services/abc/third_parties/vid_gen.py b/app/services/abc/third_parties/vid_gen.py index 31f6831..5ccaef9 100644 --- a/app/services/abc/third_parties/vid_gen.py +++ b/app/services/abc/third_parties/vid_gen.py @@ -1,7 +1,5 @@ from abc import ABC, abstractmethod -from app.configs.constants import AvatarEnum - class IVideoGeneratorService(ABC): diff --git a/app/services/impl/exam/level/__init__.py b/app/services/impl/exam/level/__init__.py index 7a4ada7..6bfd034 100644 --- a/app/services/impl/exam/level/__init__.py +++ b/app/services/impl/exam/level/__init__.py @@ -1,5 +1,191 @@ -from .level import LevelService +from typing import Dict, Optional +from fastapi import UploadFile -__all__ = [ - "LevelService" -] \ No newline at end of file +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 complain about my boss all the time, but my colleagues thinks ' + 'the boss is 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 +""" diff --git a/app/services/impl/exam/level/exercises/__init__.py b/app/services/impl/exam/level/exercises/__init__.py new file mode 100644 index 0000000..1d09802 --- /dev/null +++ b/app/services/impl/exam/level/exercises/__init__.py @@ -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" +] diff --git a/app/services/impl/exam/level/exercises/blank_space.py b/app/services/impl/exam/level/exercises/blank_space.py new file mode 100644 index 0000000..5758f1f --- /dev/null +++ b/app/services/impl/exam/level/exercises/blank_space.py @@ -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"] diff --git a/app/services/impl/exam/level/exercises/fillBlanks.py b/app/services/impl/exam/level/exercises/fillBlanks.py new file mode 100644 index 0000000..557b8d1 --- /dev/null +++ b/app/services/impl/exam/level/exercises/fillBlanks.py @@ -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": "" + } + } + ] + } \ No newline at end of file diff --git a/app/services/impl/exam/level/exercises/multiple_choice.py b/app/services/impl/exam/level/exercises/multiple_choice.py new file mode 100644 index 0000000..fe45819 --- /dev/null +++ b/app/services/impl/exam/level/exercises/multiple_choice.py @@ -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 complain about my boss all the time, but my colleagues thinks ' + 'the boss is 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 + """ + + diff --git a/app/services/impl/exam/level/exercises/passage_utas.py b/app/services/impl/exam/level/exercises/passage_utas.py new file mode 100644 index 0000000..1ba685b --- /dev/null +++ b/app/services/impl/exam/level/exercises/passage_utas.py @@ -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 \ No newline at end of file diff --git a/app/services/impl/exam/level/full_exams/__init__.py b/app/services/impl/exam/level/full_exams/__init__.py new file mode 100644 index 0000000..433a6e2 --- /dev/null +++ b/app/services/impl/exam/level/full_exams/__init__.py @@ -0,0 +1,7 @@ +from .custom import CustomLevelModule +from .level_utas import LevelUtas + +__all__ = [ + "CustomLevelModule", + "LevelUtas" +] diff --git a/app/services/impl/exam/level/custom.py b/app/services/impl/exam/level/full_exams/custom.py similarity index 98% rename from app/services/impl/exam/level/custom.py rename to app/services/impl/exam/level/full_exams/custom.py index dee8497..b9f6668 100644 --- a/app/services/impl/exam/level/custom.py +++ b/app/services/impl/exam/level/full_exams/custom.py @@ -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 \ No newline at end of file diff --git a/app/services/impl/exam/level/full_exams/level_utas.py b/app/services/impl/exam/level/full_exams/level_utas.py new file mode 100644 index 0000000..dc3c161 --- /dev/null +++ b/app/services/impl/exam/level/full_exams/level_utas.py @@ -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" + } \ No newline at end of file diff --git a/app/services/impl/exam/level/level.py b/app/services/impl/exam/level/level.py deleted file mode 100644 index fad8ce6..0000000 --- a/app/services/impl/exam/level/level.py +++ /dev/null @@ -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 complain about my boss all the time, but my colleagues thinks ' - 'the boss is 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 complain about my boss all the time, but my colleagues thinks ' - 'the boss is 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 diff --git a/app/services/impl/exam/level/mc_variants.json b/app/services/impl/exam/level/mc_variants.json index 5621bd7..699ae00 100644 --- a/app/services/impl/exam/level/mc_variants.json +++ b/app/services/impl/exam/level/mc_variants.json @@ -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 was late for work yesterday because I could start my car.", + "solution": "D", "variant": "text" } ] diff --git a/app/services/impl/exam/level/upload.py b/app/services/impl/exam/level/upload.py index fd720ce..7af6f4d 100644 --- a/app/services/impl/exam/level/upload.py +++ b/app/services/impl/exam/level/upload.py @@ -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": ""}\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 , 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 ' - '{{}} 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 "()" 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 diff --git a/app/services/impl/exam/listening.py b/app/services/impl/exam/listening.py deleted file mode 100644 index 9fdbfd2..0000000 --- a/app/services/impl/exam/listening.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/services/impl/exam/listening/__init__.py b/app/services/impl/exam/listening/__init__.py new file mode 100644 index 0000000..13a5aa2 --- /dev/null +++ b/app/services/impl/exam/listening/__init__.py @@ -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) \ No newline at end of file diff --git a/app/services/impl/exam/listening/multiple_choice.py b/app/services/impl/exam/listening/multiple_choice.py new file mode 100644 index 0000000..7e3211e --- /dev/null +++ b/app/services/impl/exam/listening/multiple_choice.py @@ -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", + } diff --git a/app/services/impl/exam/listening/write_blank_forms.py b/app/services/impl/exam/listening/write_blank_forms.py new file mode 100644 index 0000000..11b7339 --- /dev/null +++ b/app/services/impl/exam/listening/write_blank_forms.py @@ -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" + } diff --git a/app/services/impl/exam/listening/write_blank_notes.py b/app/services/impl/exam/listening/write_blank_notes.py new file mode 100644 index 0000000..c54448f --- /dev/null +++ b/app/services/impl/exam/listening/write_blank_notes.py @@ -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" + } \ No newline at end of file diff --git a/app/services/impl/exam/listening/write_blanks.py b/app/services/impl/exam/listening/write_blanks.py new file mode 100644 index 0000000..0049068 --- /dev/null +++ b/app/services/impl/exam/listening/write_blanks.py @@ -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" + } \ No newline at end of file diff --git a/app/services/impl/exam/reading.py b/app/services/impl/exam/reading.py deleted file mode 100644 index 62d38a9..0000000 --- a/app/services/impl/exam/reading.py +++ /dev/null @@ -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" - } diff --git a/app/services/impl/exam/reading/__init__.py b/app/services/impl/exam/reading/__init__.py new file mode 100644 index 0000000..e19e971 --- /dev/null +++ b/app/services/impl/exam/reading/__init__.py @@ -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 + } diff --git a/app/services/impl/exam/reading/fill_blanks.py b/app/services/impl/exam/reading/fill_blanks.py new file mode 100644 index 0000000..5888a77 --- /dev/null +++ b/app/services/impl/exam/reading/fill_blanks.py @@ -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 + } diff --git a/app/services/impl/exam/reading/idea_match.py b/app/services/impl/exam/reading/idea_match.py new file mode 100644 index 0000000..e2bcaa0 --- /dev/null +++ b/app/services/impl/exam/reading/idea_match.py @@ -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" + } diff --git a/app/services/impl/exam/reading/import_reading.py b/app/services/impl/exam/reading/import_reading.py new file mode 100644 index 0000000..b7aba4c --- /dev/null +++ b/app/services/impl/exam/reading/import_reading.py @@ -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": "", + "parts": [ + { + "text": { + "title": "", + "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}" + ) + } + + diff --git a/app/services/impl/exam/reading/paragraph_match.py b/app/services/impl/exam/reading/paragraph_match.py new file mode 100644 index 0000000..b28b8da --- /dev/null +++ b/app/services/impl/exam/reading/paragraph_match.py @@ -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" + } diff --git a/app/services/impl/exam/reading/true_false.py b/app/services/impl/exam/reading/true_false.py new file mode 100644 index 0000000..93182d5 --- /dev/null +++ b/app/services/impl/exam/reading/true_false.py @@ -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" + } diff --git a/app/services/impl/exam/reading/write_blanks.py b/app/services/impl/exam/reading/write_blanks.py new file mode 100644 index 0000000..9934e51 --- /dev/null +++ b/app/services/impl/exam/reading/write_blanks.py @@ -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" + } diff --git a/app/services/impl/exam/speaking.py b/app/services/impl/exam/speaking.py index b0364b0..f733537 100644 --- a/app/services/impl/exam/speaking.py +++ b/app/services/impl/exam/speaking.py @@ -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 diff --git a/app/services/impl/exam/writing.py b/app/services/impl/exam/writing.py index ca7e10a..9664d2a 100644 --- a/app/services/impl/exam/writing.py +++ b/app/services/impl/exam/writing.py @@ -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"] } ] diff --git a/app/services/impl/third_parties/__init__.py b/app/services/impl/third_parties/__init__.py index d8675cb..ec5f9c1 100644 --- a/app/services/impl/third_parties/__init__.py +++ b/app/services/impl/third_parties/__init__.py @@ -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" ] diff --git a/app/services/impl/third_parties/elai/__init__.py b/app/services/impl/third_parties/elai/__init__.py new file mode 100644 index 0000000..939b277 --- /dev/null +++ b/app/services/impl/third_parties/elai/__init__.py @@ -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) \ No newline at end of file diff --git a/app/services/impl/third_parties/elai/elai_conf.json b/app/services/impl/third_parties/elai/elai_conf.json new file mode 100644 index 0000000..bacfda0 --- /dev/null +++ b/app/services/impl/third_parties/elai/elai_conf.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/app/services/impl/third_parties/heygen.py b/app/services/impl/third_parties/heygen.py index 6427673..22d3124 100644 --- a/app/services/impl/third_parties/heygen.py +++ b/app/services/impl/third_parties/heygen.py @@ -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 + """ diff --git a/app/services/impl/third_parties/openai.py b/app/services/impl/third_parties/openai.py index 4b9d246..c43a874 100644 --- a/app/services/impl/third_parties/openai.py +++ b/app/services/impl/third_parties/openai.py @@ -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}" ) } ] diff --git a/audio-samples/.gitkeep b/audio-samples/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/audio-samples/placeholder.txt b/audio-samples/placeholder.txt deleted file mode 100644 index f89d219..0000000 --- a/audio-samples/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO \ No newline at end of file diff --git a/download-audio/.gitkeep b/download-audio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/download-audio/placeholder.txt b/download-audio/placeholder.txt deleted file mode 100644 index f89d219..0000000 --- a/download-audio/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO \ No newline at end of file diff --git a/download-video/.gitkeep b/download-video/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/download-video/placeholder.txt b/download-video/placeholder.txt deleted file mode 100644 index f89d219..0000000 --- a/download-video/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO \ No newline at end of file diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/exercises.docx b/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/exercises.docx new file mode 100644 index 0000000..cb25cae Binary files /dev/null and b/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/exercises.docx differ diff --git a/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/exercises.html b/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/exercises.html new file mode 100644 index 0000000..529bca2 --- /dev/null +++ b/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/exercises.html @@ -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>Marker’s 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 world’s 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'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 +don’t 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 Barcelona’s 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> diff --git a/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/solutions.docx b/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/solutions.docx new file mode 100644 index 0000000..fa6884f Binary files /dev/null and b/tmp/6994b8f4-409f-4502-aa73-1182f8a66ffe/solutions.docx differ diff --git a/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/exercises.docx b/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/exercises.docx new file mode 100644 index 0000000..cb25cae Binary files /dev/null and b/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/exercises.docx differ diff --git a/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/exercises.html b/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/exercises.html new file mode 100644 index 0000000..529bca2 --- /dev/null +++ b/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/exercises.html @@ -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>Marker’s 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 world’s 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'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 +don’t 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 Barcelona’s 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> diff --git a/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/solutions.doc b/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/solutions.doc new file mode 100644 index 0000000..fa6884f Binary files /dev/null and b/tmp/70b52f25-0e1e-4f22-9c4b-91d4d9667095/solutions.doc differ diff --git a/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/exercises.docx b/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/exercises.docx new file mode 100644 index 0000000..cb25cae Binary files /dev/null and b/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/exercises.docx differ diff --git a/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/exercises.html b/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/exercises.html new file mode 100644 index 0000000..529bca2 --- /dev/null +++ b/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/exercises.html @@ -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>Marker’s 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 world’s 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'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 +don’t 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 Barcelona’s 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> diff --git a/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/solutions.docx b/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/solutions.docx new file mode 100644 index 0000000..fa6884f Binary files /dev/null and b/tmp/f0053ff6-cec6-466c-9be5-d104b4ed7ab1/solutions.docx differ diff --git a/tmp/placeholder.txt b/tmp/placeholder.txt deleted file mode 100644 index f89d219..0000000 --- a/tmp/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -THIS FILE ONLY EXISTS TO KEEP THIS FOLDER IN THE REPO \ No newline at end of file